Skip to content

Core concept

Caching

Choose run-scoped, factory-wide, or disabled module caching.

The cache option controls whether a provider result is reused after dependency resolution. It accepts 'run', 'module', or 'none'.

cache: 'run' (default)

Each top-level module call starts a run with a fresh cache. Within that call, repeated access to the same dependency key reuses one result. A later top-level call computes it again.

import { Module, type Live } from '@favy/di';

let runs = 0;
const A = Module()('A', () => ++runs);
type ALive = Live<typeof A>;

const B = Module<ALive>()('B', ({ A }) => A);
type BLive = Live<typeof B>;

const C = Module<ALive & BLive>()('C', ({ A, B }) => A + B);

console.log(C({ A, B })); // 2: A ran once, then its value was reused
console.log(C({ A, B })); // 4: a new run computed A as 2
console.log(runs); // 2

The direct calls to C are separate runs. The two paths to A inside each run share the same resolved value.

cache: 'module'

This mode uses one cache owned by the factory returned from makeModule. It survives separate root calls and is shared by every callable created from that factory. Entries are keyed by each callable’s declared name.

import { makeModule, type Live } from '@favy/di';

const CachedModule = makeModule({ cache: 'module' });

let runs = 0;
const A = CachedModule()('A', () => ++runs);
type ALive = Live<typeof A>;

const B = CachedModule<ALive>()('B', ({ A }) => A);
type BLive = Live<typeof B>;

const C = CachedModule<ALive & BLive>()('C', ({ A, B }) => A + B);

console.log(C({ A, B })); // 2
console.log(C({ A, B })); // 2: C itself is cached by this factory
console.log(runs); // 1

CachedModule.flushCache();

console.log(C({ A, B })); // 4: the factory-wide cache was cleared
console.log(runs); // 2

Because the store is name-keyed, names must be unique within a factory that uses cache: 'module'. Two different callables with the same name address the same cache entry. flushCache() clears every entry owned by that factory, not just one module.

Only module results enter this factory-wide store. Plain input values still belong to the current root call, so using the same input key with a different value in another module does not leak the earlier value.

cache: 'none'

No resolved dependency value is retained. Every access invokes its provider again, even during one root call.

import { makeModule, type Live } from '@favy/di';

const UncachedModule = makeModule({ cache: 'none' });

let runs = 0;
const A = UncachedModule()('A', () => ++runs);
type ALive = Live<typeof A>;

const B = UncachedModule<ALive>()('B', ({ A }) => A);
type BLive = Live<typeof B>;

const C = UncachedModule<ALive & BLive>()('C', ({ A, B }) => A + B);

console.log(C({ A, B })); // 3: direct A = 1, then B's A = 2
console.log(C({ A, B })); // 7: direct A = 3, then B's A = 4
console.log(runs); // 4

Choosing a mode

ModeScopeRecomputed on a new root call?Typical use
'run'One root callYesRequest- or operation-scoped dependencies
'module'Entire factory, keyed by module nameNo, until flushCache()Process-wide singleton-style dependencies
'none'No cacheYes, on every accessValues that must always be recomputed

Caching only affects providers that are resolved. The lazy option separately controls whether unused providers run at all.