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.
Tokens and interfaces
A NexusDI token names a dependency the container can provide. A class is its own token, as the classes on the Getting started page were, and a Token<T> names a dependency whose type is an interface, which has no runtime value of its own.
Name an interface with a NexusDI Token<T>
A Token<T> gives an interface a runtime name the container can look up. new Token<INavCharts>('NavCharts') carries the type INavCharts, so get(NAV_CHARTS) returns an INavCharts, and provide(NAV_CHARTS, ...) accepts only something that fits it. The description, 'NavCharts', is the name NexusDI prints in error messages and in graph(). Token requires it.
provide(NAV_CHARTS, { useValue }) binds the token to a value the container hands out as it is. Nexus.create takes a list of provide() entries as well as a list of classes (the short form from the Getting started page):
import { Nexus, Token, provide } from '@nexusdi/core';
interface INavCharts {
plot(target: string): string;
}
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
const starCharts: INavCharts = {
plot: (target) => `course to ${target}`,
};
await using ship = await Nexus.create([
provide(NAV_CHARTS, { useValue: starCharts }),
]);
const course = ship.get(NAV_CHARTS).plot('Kepler-442b'); // -> 'course to Kepler-442b'
console.log(course);Try it: save the program as tokens.ts and run npx tsx tokens.ts. The terminal prints course to Kepler-442b. Then change starCharts so that plot returns a number. npx tsc rejects starCharts, because it no longer fits INavCharts. Change plot back to return a string and npx tsc reports nothing.
NexusDI compares tokens by identity
NexusDI compares two tokens with ===, so a token is the object new Token() returned, and its description takes no part in the lookup. Two tokens with the same description are two different tokens. A program shares a token by importing the one object, usually from a module file that declares it beside its interface.
import { Nexus, Token, isNexusError, provide } from '@nexusdi/core';
interface INavCharts {
plot(target: string): string;
}
const NAV_CHARTS = new Token<INavCharts>('NavCharts');
const BACKUP_CHARTS = new Token<INavCharts>('NavCharts');
await using ship = await Nexus.create([
provide(NAV_CHARTS, {
useValue: { plot: (target) => `course to ${target}` },
}),
]);
const same = NAV_CHARTS === BACKUP_CHARTS; // -> false
console.log(same);
const provided = ship.has(BACKUP_CHARTS); // -> false
console.log(provided);
let code: string | null = null;
try {
ship.get(BACKUP_CHARTS);
} catch (error) {
if (!isNexusError(error)) throw error;
code = error.code;
}
const missing = code; // -> 'NEXUS_MISSING_PROVIDER'
console.log(missing);has() answers false for BACKUP_CHARTS, and get(BACKUP_CHARTS) throws NEXUS_MISSING_PROVIDER, even though both tokens print as NavCharts.
Try it: in tokens.ts, declare a second new Token<INavCharts>('NavCharts') and ask the container for it with get(). Run the file: the run stops with an uncaught NEXUS_MISSING_PROVIDER error. Read the code the error carries. Then ask for NAV_CHARTS, the token the provide() call used, and the course prints again.
Bind interfaces with NexusDI tokens to swap an implementation
A consumer that depends on an interface token never names the class behind it, so the binding can change without touching the consumer. QuantumComputer declares static deps = [REACTOR] as const and takes an IReactorCore. A real FusionReactor and a test value fit the same token, and QuantumComputer builds over either:
import { Nexus, Token, provide } from '@nexusdi/core';
interface IReactorCore {
readonly output: number;
}
interface IShipComputer {
status(): string;
}
const REACTOR = new Token<IReactorCore>('ReactorCore');
const COMPUTER = new Token<IShipComputer>('ShipComputer');
class QuantumComputer implements IShipComputer {
static deps = [REACTOR] as const;
constructor(private readonly reactor: IReactorCore) {}
status() {
return `Reactor output ${this.reactor.output} GW.`;
}
}
class FusionReactor implements IReactorCore {
readonly output = 1.21;
}
const fakeReactor: IReactorCore = { output: 0 };
async function statusWith(reactor: IReactorCore) {
await using ship = await Nexus.create([
provide(REACTOR, { useValue: reactor }),
provide(COMPUTER, { useClass: QuantumComputer }),
]);
return ship.get(COMPUTER).status();
}
const real = await statusWith(new FusionReactor()); // -> 'Reactor output 1.21 GW.'
console.log(real);
const fake = await statusWith(fakeReactor); // -> 'Reactor output 0 GW.'
console.log(fake);The rest of these docs binds every service this way: an interface with a token named for its role, and a class bound to that token. The Modules page swaps a whole module on the same principle, and @nexusdi/testing replaces one binding in a real graph.
Try it: add a SimulatedReactor class with readonly output = 0.75 and call statusWith(new SimulatedReactor()). QuantumComputer prints Reactor output 0.75 GW., and its code stays as it is.