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();
    });
  });
}