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.
Multi-Providers
In most cases, one token maps to one provider. However, some features require a collection of contributions from across the application, such as a list of all available diagnostics, a set of validation rules, or a collection of plugin handlers.
Multi-Providers allow multiple modules to contribute to a single token, which is then resolved as an array of all visible contributions.
The MultiToken
To create a multi-provider, use the MultiToken class. This tells NexusDI that this token is designed to collect multiple values and not resolve to a single one.
import { MultiToken, Nexus, provide } from '@nexusdi/core';
interface Diagnostic {
readonly system: string;
check(): boolean;
}
const DIAGNOSTICS = new MultiToken<Diagnostic>('Diagnostics');
class ReactorDiagnostic implements Diagnostic {
readonly system = 'reactor';
check() {
return true;
}
}
await using ship = await Nexus.create([
provide(DIAGNOSTICS, { useClass: ReactorDiagnostic }),
provide(DIAGNOSTICS, { useValue: { system: 'hull', check: () => true } }),
]);
const systems = ship.get(DIAGNOSTICS).map((each) => each.system); // -> ['reactor', 'hull']
console.log(systems);Any number of modules can call provide(DIAGNOSTICS, ...) to add their own contribution to the list. A contribution is either a class, as ReactorDiagnostic is above, or a value, as the hull check is. A contribution that needs deps uses a factory. ship.get(DIAGNOSTICS) returns the contributions as an array, in the order the providers are listed. Across modules, a module’s own contributions come first, then those of its imports in import order.
Try it: Save the region as diagnostics.ts and run npx tsx diagnostics.ts. It prints [ 'reactor', 'hull' ]. Add a third provide(DIAGNOSTICS, { useValue: { system: 'shields', check: () => false } }) after the hull one and run it again: it prints [ 'reactor', 'hull', 'shields' ]. Delete that line and the array loses shields again. Then add const DRONE_COUNT = new Token<number>('DroneCount'); (import Token), provide(DRONE_COUNT, { useValue: 3 }) and provide(DIAGNOSTICS, { useFactory: (n: number) => ({ system: 'drone' + n, check: () => true }), deps: [DRONE_COUNT] }) to the list: the array gains drone3.
Consuming Contributions with all()
Because a MultiToken represents a collection, you cannot use it as a standard dependency. Instead, you must use the all() modifier in your static deps list. all() is the only modifier a multi-token takes: lazy() and optional() each make Nexus.create reject with NEXUS_BLUEPRINT_INVALID, wrapping NEXUS_INVALID_PROVIDER, and so does a bare MultiToken.
import { MultiToken, Nexus, Token, all, defineModule } from '@nexusdi/core';
import { optional, provide } from '@nexusdi/core';
interface Diagnostic {
readonly system: string;
check(): boolean;
}
interface ISubspaceLink {
send(report: string): string;
}
interface IDiagnosticsPanel {
report(): string;
}
const DIAGNOSTICS = new MultiToken<Diagnostic>('Diagnostics');
const SUBSPACE_LINK = new Token<ISubspaceLink>('SubspaceLink');
const DIAGNOSTICS_PANEL = new Token<IDiagnosticsPanel>('DiagnosticsPanel');
class ReactorDiagnostic implements Diagnostic {
readonly system = 'reactor';
check() {
return true;
}
}
class StatusBoard implements IDiagnosticsPanel {
static deps = [all(DIAGNOSTICS), optional(SUBSPACE_LINK)] as const;
constructor(
private readonly checks: Diagnostic[],
private readonly link: ISubspaceLink | undefined,
) {}
report() {
const line = this.checks
.map((each) => `${each.system} ${each.check() ? 'green' : 'red'}`)
.join(', ');
return this.link === undefined ? line : this.link.send(line);
}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [provide(DIAGNOSTICS, { useClass: ReactorDiagnostic })],
exports: [DIAGNOSTICS],
});
const Tactical = defineModule({
name: 'Tactical',
providers: [
provide(DIAGNOSTICS, { useValue: { system: 'hull', check: () => true } }),
],
exports: [DIAGNOSTICS],
});
const Meridian = defineModule({
name: 'Meridian',
imports: [Engineering, Tactical],
providers: [provide(DIAGNOSTICS_PANEL, { useClass: StatusBoard })],
});
await using ship = await Nexus.create(Meridian);
const report = ship.get(DIAGNOSTICS_PANEL).report(); // -> 'reactor green, hull green'
console.log(report);all(TOKEN) tells NexusDI to resolve every visible contribution to that token and pass them to the constructor as an array. Here Engineering contributes the reactor diagnostic and Tactical the hull check, each module exports DIAGNOSTICS, and StatusBoard in Meridian reads both. The same panel takes optional(SUBSPACE_LINK), which resolves to undefined while no module provides a link.
Try it: Save the region as panel.ts and run it. It prints reactor green, hull green. Add a Comms module that provides and exports SUBSPACE_LINK with a useValue whose send(report) returns 'sent: ' + report, import it into Meridian, and run it again: the panel prints sent: reactor green, hull green. Take Comms out of Meridian’s imports and the panel prints reactor green, hull green again.
Visibility and Encapsulation
Multi-providers follow the same visibility rules as standard providers:
- Local Contributions: A module always sees its own contributions.
- Imported Contributions: A module sees contributions from any imported module that explicitly exports the
MultiToken. - Private Contributions: If a module contributes to a
MultiTokenbut does not export it, that contribution remains private to the module.
This allows you to have “internal” diagnostics that only the module itself can see, while sharing “public” diagnostics with the rest of the application. all() elsewhere leaves a private contribution out with no error, because a private contribution is a valid design: the module can read its own entries.
import { MultiToken, Nexus, Token, all, defineModule } from '@nexusdi/core';
import { provide } from '@nexusdi/core';
interface Diagnostic {
readonly system: string;
check(): boolean;
}
interface IDiagnosticsPanel {
systems(): string[];
}
const DIAGNOSTICS = new MultiToken<Diagnostic>('Diagnostics');
const DIAGNOSTICS_PANEL = new Token<IDiagnosticsPanel>('DiagnosticsPanel');
class ReactorDiagnostic implements Diagnostic {
readonly system = 'reactor';
check() {
return true;
}
}
class StatusBoard implements IDiagnosticsPanel {
static deps = [all(DIAGNOSTICS)] as const;
constructor(private readonly checks: Diagnostic[]) {}
systems() {
return this.checks.map((each) => each.system);
}
}
const Engineering = defineModule({
name: 'Engineering',
providers: [provide(DIAGNOSTICS, { useClass: ReactorDiagnostic })],
exports: [DIAGNOSTICS],
});
// Tactical contributes to DIAGNOSTICS and does not export it.
const Tactical = defineModule({
name: 'Tactical',
providers: [
provide(DIAGNOSTICS, { useValue: { system: 'hull', check: () => true } }),
],
});
await using ship = await Nexus.create(
defineModule({
name: 'Meridian',
imports: [Engineering, Tactical],
providers: [provide(DIAGNOSTICS_PANEL, { useClass: StatusBoard })],
}),
);
const systems = ship.get(DIAGNOSTICS_PANEL).systems(); // -> ['reactor']
console.log(systems);Tactical contributes the hull check and has no exports, so the panel sees only the reactor.
Try it: Save the region as private.ts and run it. It prints [ 'reactor' ]. Add exports: [DIAGNOSTICS] to Tactical and run it again: it prints [ 'reactor', 'hull' ]. Remove exports from Tactical and it prints [ 'reactor' ] again, with no error either way.