Scopes and singletons
Factory-built singletons: one per app, one per component subtree, or a plain value or factory provider. Plus a way to keep inject() working in async callbacks.
@mmstack/dinpm
#rootInjectable
rootInjectable builds an app-wide singleton from a factory, without writing a class. It returns an inject function; the instance is created lazily on first use and shared for the rest of the application. The factory receives the Injector, and since it is a token factory it stays SSR-safe: each server request gets its own instance.
import { rootInjectable } from '@mmstack/di';
const injectClock = rootInjectable(() => ({ now: () => Date.now() }));
// anywhere in the app, always the same instance
const clock = injectClock();#createScope
createScope gives a family of factory-built singletons scoped to a component subtree. It returns a [register, provide] pair. Provide the scope at a component, register item factories once, and every consumer under that boundary shares one instance. Two sibling boundaries get their own sets, and instances are destroyed with the provider.
import { createScope } from '@mmstack/di';
const [register, provideFeatureScope] = createScope('FeatureScope');
// register an item factory (runs in the injection context)
const useFeatureItem = register(() => {
const dep = inject(SomeDependency);
return { id: crypto.randomUUID(), doWork: () => dep.work() };
});
@Component({
providers: [provideFeatureScope()], // one set of instances per boundary
})
class Feature {}
// in any child of that boundary:
readonly item = useFeatureItem(); // same instance for this boundary The provide function takes overrides, pairs of [injectFn, replacementFactory] that swap a registration at that boundary only. Dependents pick up the override transitively, which is handy in tests and stories.
#provideAs
provideAs builds a useValue or useFactory provider depending on what you hand it. A function becomes a factory, anything else a value.
import { provideAs } from '@mmstack/di';
providers: [
provideAs(CONFIG, { baseUrl: '/api' }), // useValue
provideAs(CLOCK, () => ({ now: () => Date.now() })), // useFactory
];#createRunInInjectionContext
Some callbacks run outside Angular's injection context, like an external library's event listener, so inject() throws there. createRunInInjectionContext captures the context up front and returns a runner that restores it later.
import { createRunInInjectionContext } from '@mmstack/di';
private runInContext = createRunInInjectionContext();
ngOnInit() {
externalLib.on('open', () => {
this.runInContext(() => {
const dialog = inject(DialogService); // works inside the callback
dialog.open();
});
});
}