npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@jeengbe/cache

v3.3.0

Published

A strongly typed caching framework that works with any engine.

Downloads

661

Readme

A strongly typed caching framework that works with any engine. In-memory and Redis adapters included.

License Version Coverage Badge

It provides a general Cache class that interacts with cache adapters, which are responsible for communicating with the cache engine (e.g. Redis). The package comes with a cache adapter for an in-memory cache that saves its content to the disk, one that does absolutely nothing (no values saved and never returns a value) and a cache adapter for Redis.

To use several cache instances on the same cache engine, every cache accepts a prefix parameter that is prepended to all keys before they are stored. This allows for different namespaces and versioning stored in the same cache engine.

Values are serialized to string for storage in the cache. By default, JSON.stringify/JSON.parse are used, but custom serializers may be provided for serializing e.g. with Protocol Buffers.

Installation

The package is published as @jeengbe/cache. Versions follow Semantic Versioning.

Gotta go fast, no time to read

// service/index.ts
export interface MyService {
  doThing(param: number): Promise<string>;
}

// service/cached.ts
type MyServiceCacheTypes = {
  [K in `thing-${number}`]: string;
};

export class CachedMyService implements MyService {
  constructor(
    private readonly cache: Cache<MyServiceCacheTypes>,
    private readonly delegate: MyService,
  ) {}

  async doThing(param: number): Promise<string> {
    return await this.cache.cached(`thing-${param}`, () =>
      this.delegate.doThing(param),
    );
  }
}

// init.ts
const cacheAdapter = new RedisCacheAdapter(new Redis(redisConfig));
const cache = new Cache<MyServiceCacheTypes>(cacheAdapter);

Usage

Create a new cache object

The generic Cache class takes a type parameter that dictates which cache keys correspond to which values. First, decare the type as an object where the object properties are available cache keys and values the respective values in the cache. Use template literal types to account for patterns like `cached-${id}` in your keys. Intersect several mapped types to merge several objects with template literal types:

type XCacheTypes = {
  [K in `cached-a-${string}`]: number;
} & {
  [K in `cached-b-${number}`]: string;
} & {
  'general-c': string;
  'general-d': boolean;
};

A cache with the above types accepts the following:

  • Keys that start with "cached-a-" may cache a number.
  • Keys that start with "cached-b-" followed by a number may cache a string.
  • The key "general-c" may cache a string.
  • The key "general-d" may cache a boolean.

All read/write operations on corresponding instance are strongly typed. To create a new cache object, instantiate the class with a suiting cache adapter and optionally a prefix.

import { Cache, VoidCacheAdapter } from '@jeengbe/cache';

const xCache = new Cache<XCacheTypes>(new VoidCacheAdapter(), 'x:1');

Consider this context for all following code examples in this document:

type Result = { id: string; calculated: number };

declare const resultCache: Cache<{
  [K in `expensive-${string}`]: Result;
}>;

Get (get/mget)

Use get to get the value for a single cached key.

To get the values for several keys in one operation, use mget.

If you want to get the value, and, if none present, calculate and store a new one, consider using cached/mcached instead.

Set (set/mset)

Use set to set the cached value for a single key.

To set the cached values for several keys, use mset.

Unlike conventional cache packages, mset takes an array of values and a function to determine the cache key for each value.

declare const items: readonly Result[];

await resultCache.mset(items, (item) => `expensive-${item.id}`, '1d');

Delete (del/mdel)

Use del to delete the cached value for a single key.

To delete the cached values for several keys, use mdel.

Delete by pattern (pdel)

Use pdel to delete the cached values for all keys that match the given glob-style pattern.

Please note that the pattern is not fully typed and can be any string. PRs welcome. :)

Clear cache (clear)

Use clear to delete the cached values for all keys.

Query whether in cache (has/mhas)

Use has to check whether there exists a cached value for a single key.

To delete whether several keys have a cached value, use mhas. mhas only reports whether all of the provided keys have a value cached and returns no inforation about which/how many of the given keys have no value cached.

Get and if not present, calculate and set (cached/mcached)

Use cached to get the cached value for a cache, and if the value is not in the cache, run a function to produce a value and cache that.

Because this is a commonly used pattern, this package provides a convenience method for this.

Use mcached if you want to get several cached values and only compute those for which there is no value stored in the cache. It takes an array of data, from which each the cache key is generated. If at least one key has no cached value, the producer is called with an array of those data items for whose key no value was cached.

Note that this is no atomic operation and the key is in no way locked while the producer is awaited.

declare function expensiveFunction(id: string): Promise<Result>;
declare const id: string;

const result = await resultCache.cached(
  //  ^? Result
  `expensive-${id}`,
  () => expensiveFunction(id),
  '1d',
);
declare function expensiveBatchFunction(
  ids: readonly string[],
): Promise<Result[]>;
declare const ids: string[];

const results = await resultCache.mcached(
  //  ^? Result[]
  ids,
  (id) => `expensive-${id}`,
  (m) => expensiveBatchFunction(m),
  '1d',
);

Cache Adapters

Redis

import { RedisCacheAdapter } from '@jeengbe/cache';
import { Redis } from 'ioredis';

const cacheAdapter = new RedisCacheAdapter(new Redis(redisConfig));

Void

Stores no values, get operations always return undefined and has always returns false.

import { VoidCacheAdapter } from '@jeengbe/cache';

const cacheAdapter = new VoidCacheAdapter();

In memory

Keeps the values in memory.

The package @isaacs/ttlcache can be used for the cache implementation.

import TtlCache from '@isaacs/ttlcache';
import { MemoryCacheAdapter } from '@jeengbe/cache';

const cacheAdapter = new MemoryCacheAdapter(new TtlCache());

It is also possible to back up the memory after every write operation. To do that, construct the adapter with a cache backup saver. To save the memory to disk, pass it a DiskCacheBackupSaver as shown:

import TtlCache from '@isaacs/ttlcache';
import { DiskCacheBackupSaver, MemoryCacheAdapter } from '@jeengbe/cache';

const cacheAdapter = new MemoryCacheAdapter(
  new TtlCache(),
  new DiskCacheBackupSaver(diskCacheBackupLocation),
);

No-TTL in memory

Keeps the values in memory, but ignores TTL i.e. values to not expire.

Ideal for unit tests that involve a cache so that you don't have to mock the cache.

import { NoTtlMemoryCacheAdapter } from '@jeengbe/cache';

const cacheAdapter = new NoTtlMemoryCacheAdapter();

Notes

ms duration format

All methods that take a duration (set/cached, etc.) accept either a ttl in milliseconds, or any duration string that can be parsed by the ms package.

Array.map and mget, mdel, mhas

For the operations mget, mdel, mhas, you may run into compiler errors if you map over an input array normally (see example below). To fix this, add an as const assertion to ensure that mapped keys are properly typed.

declare const ids: string[];

// Missing 'as const':
await resultCache.mget(
  ids.map((id) => `expensive-${id}`),
  // ~~~~~ Type 'string' is not assignable to type '`expensive-${string}`'.
);

// Correctly typed:
await resultCache.mget(ids.map((id) => `expensive-${id}` as const));

The longer explanation is that for a variety of reasons, ids.map((id) => `expensive-${id}`) results in a string[] instead of exactly `expensive-${string}`[]. So when string[] is used as keys for a strongly typed signature like mget, the compiler (rightfully so) complains. By changing it to ids.map((id) => `expensive-${id}` as const), we make the type as exact as possible, which then gives the explicit string types we need.

The reason as const is not necessary for mset, mcached is that the compiler is able to infer the as const automatically here. Normally, (item) => `expensive-${item.id}` results in () => string, but because the compiler expects an explicit () => `expensive-${string}` it also tries to determine a stricter signature for the method, which satisfies the strongly typed signature.

In theory, this would also work with the above described map limitation, but the compiler does not check that deep, so the inferring from mget signature -> map return type -> map callback is not made, and .map results in string[].

declare const items: readonly Result[];

await resultCache.mset(items, (item) => `expensive-${item.id}`, '1d');

Alternatively, you can pass a type parameter to map, but that's less elegant if you ask me:

declare const ids: string[];

await resultCache.mget(
  ids.map<`expensive-${string}`>((id) => `expensive-${id}`),
);