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

typescript-cacheable

v3.0.3

Published

An in-memory caching (memoization) decorator for Typescript

Downloads

5,319

Readme

Typescript Cacheable

An in-memory caching (memoization) decorator for TypeScript. It will cache the results of expensive methods or property accessors. The underlying function is wrapped to apply caching concerns.

Quick Start

Apply the decorator to cache long-running (or high compute cost) methods or getters.

In the example below, the first invocation will take 100ms. Subsequent invocations will take 1-2ms. The result will be cached globally, until the end of time, as long as the owning object lives.

@Cacheable()
public async findHappiest(): Promise<Dwarf> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(new Dwarf('Huck', 'Finn'));
        }, 100);
    });
}

Methods with Parameters

When the type of each parameter can be serialized to JSON

If the parameters can be serialized to JSON, simply apply the decorator:

@Cacheable()
public async countByLastName(name: string): Promise<number> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(12);
        }, 100);
    });
}

Subsequent invocations for the same set of arguments will return the cached value. Values will be cached globally, until the end of time - consider the memory implications! For example, there should be a finite set of possible argument values.

Note that argument values of undefined are supported, even though undefined is not a valid JSON value. (undefined within objects is still not supported.)

When the cache key can't be inferred

If the argument cannot be serialized to JSON (perhaps to due circular references) and the cache key can't be inferred, parameters can implement the CacheableKey interface:

export class WeatherConditions implements CacheableKey {
    cacheKey(): string {
        return `${this.temperature}:${this.humidity}`;
    }
}

The cacheKey should be unique, given state of the instance. Now WeatherConditions can serve as a cache key, alongside other arguments if required:

@Cacheable()
public async findAdaptedFor(weather: WeatherConditions): Promise<Dwarf> {
   //
}

Scopes

Global

The default scope is global. The previous examples are the equivalent of:

@Cacheable({scope: 'GLOBAL'})
public async findHappiest(): Promise<Dwarf> {
    // etc
}

Local

TypeScript cacheable integrates with AsyncLocalStorage to provide caching scoped to the call-chain, such as the current http request in a web app.

Local Storage Example

Firstly, local storage must be activated to establish the call-chain.

To be able to use local storage in an express app, bind it to http requests, as follows:

export const app = express()
const als = new AsyncLocalStorage<Context>()

export const getStore = (): unknown => {
    return als.getStore()
}

app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
    const context = new Context()
    als.run(context, () => {
        return next()
    })
})
:

It is really important that the instance of AsyncLocalStorage you are binding in your call chain is the same one you use for the getStore function.

The first invocation to the method below, within the current http request, will compute a new value, after which the cached value will be returned. Note that for local storage caching you must provide a function that will return the AsyncLocalStorage store.

import { getStore } from './SimpleApp'
:
@Cacheable({ scope: 'LOCAL_STORAGE', getStore: getStore })
public async findCompanion(): Promise<Dwarf> {
    return new Promise((resolve) => {
        setTimeout(() => {
            const dwarf = new Dwarf(faker.name.firstName(), faker.name.lastName());
            resolve(dwarf);
        }, 100);
    });
}

Follow the same pattern as above for your own web stack. Open an issue if you need help.

Time to Live

By default, cached values live indefinitely within the applicable scope. Expiration can be enabled using the time to live option. Example:

@Cacheable({ ttl: 1000 })
public async findGrumpiest(): Promise<Dwarf> {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(new Dwarf('Huck', 'Finn'));
        }, 100);
    });
}

Time to live is specified in milliseconds.

Null/Undefined Behavior

The cacheUndefined option specifies how undefined/null return values are treated.

The default (true) means that if the cached method returns a null or undefined value, subsequent calls with the same parameters will return null/undefined from the cache, without invoking the underlying method again.

When false a null return value from the cache will result in the cached method body being evaluated again. Use this to cache temporal values, such as fx rates where once they exist for a given date are immutable, but may as yet be undefined.

@Cacheable({ cacheUndefined: false })
public async findGrumpiest(): Promise<Dwarf> {
}

Convenience (Direct Cache Access) Methods

Sometimes you need to be able to directly manipulate the cache outside the functionality provided by the @Cacheable() decorator. For example, when running tests, sometimes the caching can actually get in the way.

The convenience methods are:

| Action | Purpose | GLOBAL | LOCAL_STORAGE | Arguments | | ------- | -------------------------------------------- | --------------- | --------------------- | ---------------------------------------------- | | Init | Initialise the entire cache | globalInit | localStorageInit | getStore function (LOCAL_STORAGE only) | | Clear | Clear all cache entries for an object method | globalClear | localStorageClear | target object, method name | | Delete | Delete a single cache entry | globalDelete | localStorageDelete | target object, method name, method args | | Get | Get a cached value | globalGet | localStorageGet | target object, method name, method args | | Set | Set a cached value | globalSet | localStorageSet | target object, method name, method args, value | | Methods | Return cached methods for an object | globalMethods | localStorageMethods | target object | | Keys | Return cached keys for an object method | globalKeys | localStorageKeys | target object, method |

Please note if you call localStorageClear(someObject, someMethod) before initialising the cache (either explicitly with localStorageInit(getStore) or implicitly by calling a locally cached function), an exception with be thrown.

Example

Let's say we have the following method to retrieve an invoice from the database and optionally lock it:

    public async findById(id: string, forUpdate: boolean = false): Promise<Invoice> {
        const rows = await this.persistenceManager.query(
            `select * from invoice where id = $1${forUpdate ? ' for no key update' : ''}`,
            [id]
        );
        if (rows.length === 0) {
            throw AppError.with(ErrorCode.INVALID_INVOICE);
        }
        const invoice = this.fromDB(rows[0]);
        if (forUpdate) {
            invoice.captureBeforeUpdate();
        }
        return invoice;
    }

Obviously our code is never going to call this method more than once for a request, but just in case we make it cacheable with the local storage cache:

    @Cacheable({ scope: 'LOCAL_STORAGE', getStore: getStore })
    public async findById(id: string, forUpdate: boolean = false): Promise<Invoice> {
        const rows = await this.persistenceManager.query(
            `select * from invoice where id = $1${forUpdate ? ' for no key update' : ''}`,
            [id]
        );
        if (rows.length === 0) {
            throw AppError.with(ErrorCode.INVALID_INVOICE);
        }
        const invoice = this.fromDB(rows[0]);
        if (forUpdate) {
            invoice.captureBeforeUpdate();
        }
        return invoice;
    }

Then we think it would be a good idea if any call with forUpdate set to true would populate the cache for the same id value, but forUpdate set to false.

Note:

Because our method defines an argument with a default value (forUpdate) we need to set cache entries for both when the argument is populated explicitly and when it is populated by default:

    @Cacheable({ scope: 'LOCAL_STORAGE', getStore: getStore })
    public async findById(id: string, forUpdate: boolean = false): Promise<Invoice> {
        const rows = await this.persistenceManager.query(
            `select * from invoice where id = $1${forUpdate ? ' for no key update' : ''}`,
            [id]
        );
        if (rows.length === 0) {
            throw AppError.with(ErrorCode.INVALID_INVOICE);
        }
        const invoice = this.fromDB(rows[0]);
        if (forUpdate) {
            invoice.captureBeforeUpdate();
            localStorageSet(this, 'findById', [id], invoice);
            localStorageSet(this, 'findById', [id, false], invoice);
        }
        return invoice;
    }

We could do something similar in the update method. The update method itself would not use the @Cacheable() decorator but after the update completes it would directly populate/update the cache for the findById method to avoid any subsequent database round trip.

Be Involved

TypeScript Cacheable is maintained by TreviPay. The organization is an innovator in the fintech space and provides the leading B2B payment experience. We're hiring, by the way!

Contributions are very welcome.

LICENSE

TypeScript cacheable is licensed under Mozilla Public License 2.0