This page documents the 0.4 release candidates of @nexusdi/core. The newest on npm is 0.4.0-rc.1. The documentation for 0.3.2, the current release, is at nexus.js.org. Try the release candidate.
Lifetimes
When a container resolves a token, it needs to know if it should return a shared instance or create a brand new one. This behavior is governed by the Lifetime.
Lifetimes allow you to balance memory usage against the need for fresh state, ensuring that your components get the exact behavior they expect.
Singleton vs. Transient
NexusDI provides two primary lifetimes for general use:
Singleton (The Default)
A singleton is created exactly once for the entire lifetime of the container. Every component that depends on a singleton receives the same instance.
Transient
A transient dependency is created every single time it is requested. If two different components depend on the same transient token, they will each receive their own unique instance.
import { Nexus, Token, provide } from '@nexusdi/core';
interface IShipComputer {
readonly serial: number;
}
interface ISurveyDrone {
readonly serial: number;
readonly computer: IShipComputer;
}
const COMPUTER = new Token<IShipComputer>('ShipComputer');
const DRONE = new Token<ISurveyDrone>('SurveyDrone');
let computers = 0;
let drones = 0;
class QuantumComputer implements IShipComputer {
readonly serial = ++computers;
}
class ScoutDrone implements ISurveyDrone {
static deps = [COMPUTER] as const;
readonly serial = ++drones;
constructor(readonly computer: IShipComputer) {}
}
await using ship = await Nexus.create([
provide(COMPUTER, { useClass: QuantumComputer }),
provide(DRONE, { useClass: ScoutDrone, lifetime: 'transient' }),
]);
const first = ship.get(DRONE);
const second = ship.get(DRONE);
const serials = [first.serial, second.serial]; // -> [1, 2]
console.log(serials);
const sharedComputer = first.computer === second.computer; // -> true
console.log(sharedComputer);QuantumComputer declares no lifetime, so it is a singleton. The two get(DRONE) calls launch two ScoutDrones, and both receive the one QuantumComputer.
By switching a provider to lifetime: 'transient', you can transform a shared service into a private, per-consumer utility. A provider sets it with the lifetime option of provide(), next to useClass or useFactory.
The Provider Decides
The provider that is resolved sets the lifetime, and a consumer cannot change it. When COMPUTER itself is transient, each drone gets a computer of its own:
import { Nexus, Token, provide } from '@nexusdi/core';
interface IShipComputer {
readonly serial: number;
}
interface ISurveyDrone {
readonly computer: IShipComputer;
}
const COMPUTER = new Token<IShipComputer>('ShipComputer');
const DRONE = new Token<ISurveyDrone>('SurveyDrone');
let computers = 0;
class QuantumComputer implements IShipComputer {
readonly serial = ++computers;
}
class ScoutDrone implements ISurveyDrone {
static deps = [COMPUTER] as const;
constructor(readonly computer: IShipComputer) {}
}
await using ship = await Nexus.create([
provide(COMPUTER, { useClass: QuantumComputer, lifetime: 'transient' }),
provide(DRONE, { useClass: ScoutDrone, lifetime: 'transient' }),
]);
const first = ship.get(DRONE);
const second = ship.get(DRONE);
const sharedComputer = first.computer === second.computer; // -> false
console.log(sharedComputer);Try it: Save the first program as lifetimes.ts and run npx tsx lifetimes.ts. It prints [ 1, 2 ] and then true. Add lifetime: 'transient' to the COMPUTER provider and run it again. npx tsc reports nothing, serials still prints [ 1, 2 ], and sharedComputer prints false, because each drone now holds its own computer. Remove the option again and sharedComputer prints true.
A third lifetime, scoped, builds one instance per scope, such as one per request. Scopes and the REQUEST Token teaches it.
Lazy Initialization with eager: false
By default, all singletons are created during the Nexus.create startup phase, so a singleton whose constructor throws stops the app at startup. NexusDI checks the wiring of every provider, eager: false included, before it builds anything. eager only decides when an instance is built.
However, some dependencies are expensive to create and might not be used in every execution path. By setting eager: false, you move the instantiation from startup to the first time the dependency is actually requested via get().
import { Nexus, Token, provide } from '@nexusdi/core';
interface INavCharts {
plot(target: string): string;
}
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
const log: string[] = [];
class StarCharts implements INavCharts {
constructor() {
log.push('charts loaded');
}
plot(target: string) {
return `course to ${target}`;
}
}
await using ship = await Nexus.create([
provide(NAV_CHARTS, { useClass: StarCharts, eager: false }),
]);
const beforeGet = [...log]; // -> []
console.log(beforeGet);
const course = ship.get(NAV_CHARTS).plot('Kepler-442b'); // -> 'course to Kepler-442b'
console.log(course);
const afterGet = [...log]; // -> ['charts loaded']
console.log(afterGet);An eager provider that depends on an eager: false provider still builds it at create. The option suits a client whose constructor opens a connection that a short-lived command may never use. Nexus.create refuses eager: false on a transient, a useValue and a useExisting provider with NEXUS_INVALID_PROVIDER inside NEXUS_BLUEPRINT_INVALID.
Try it: Run the eager: false program. It prints [], then course to Kepler-442b, then [ 'charts loaded' ]. Delete eager: false and run it again. beforeGet now prints [ 'charts loaded' ], because Nexus.create built the charts before the first get(). Put eager: false back and beforeGet prints [] again.
The Async Constraint
Because get() is designed to be synchronous for performance and predictability, lazy providers cannot be asynchronous.
If you have a provider that requires an async factory, it must be eager. This ensures the promise is resolved during startup, so that the subsequent get() call can return the value instantly. TypeScript rejects eager: false on an async factory with TS2769 on the provide( call, and the message of the last overload names the rule:
Fails the type check
const charts = provide(NAV_CHARTS, {
useFactory: async () => ({ plot: (target: string) => `course to ${target}` }),
eager: false,
});
// TS2769: Type 'false' is not assignable to type 'true | "NEXUS_LAZY_ASYNC: an async
// provider is built during create or createScope, so it cannot be eager: false. ..." | undefined'In JavaScript, a factory that returns a promise at that first get() makes get() throw NEXUS_LAZY_ASYNC. A transient factory must also return synchronously: a JavaScript one that returns a promise makes get() throw NEXUS_ASYNC_TRANSIENT, and TypeScript names that code when the factory is async.
Try it: In the eager: false program, replace the useClass option with useFactory: async () => new StarCharts() and run npx tsc. TypeScript reports TS2769 on the provide( call, and the last overload’s message holds the NEXUS_LAZY_ASYNC text above. Remove async and npx tsc reports nothing. Dropping eager: false also clears the error.
Summary of Lifetimes
| Lifetime | When is it created? | How many instances? |
|---|---|---|
| Singleton | At startup (Eager) | One shared instance |
| Lazy Singleton | On first get() | One shared instance |
| Transient | On every get() | A new instance every time |
| Scoped | Factories on scope creation, classes on first get() in the scope | One per scope (e.g., per request) |