@avijs/di 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/container.d.ts +65 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +368 -0
- package/dist/container.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +39 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +91 -0
- package/dist/provider.js.map +1 -0
- package/dist/token.d.ts +22 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +16 -0
- package/dist/token.js.map +1 -0
- package/package.json +44 -3
- package/src/container.ts +469 -0
- package/src/index.ts +18 -0
- package/src/provider.ts +151 -0
- package/src/token.ts +44 -0
package/dist/provider.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { isToken, tokenName } from './token.js';
|
|
2
|
+
/** Validates a provider and converts it to one internal shape. `where` labels errors. */
|
|
3
|
+
export function normalizeProvider(provider, where) {
|
|
4
|
+
if (typeof provider === 'function') {
|
|
5
|
+
return classSpec(provider, provider, 'singleton', where);
|
|
6
|
+
}
|
|
7
|
+
if (typeof provider !== 'object' || provider === null) {
|
|
8
|
+
throw new TypeError(`Invalid provider at ${where}: ${String(provider)}${hint(provider)}`);
|
|
9
|
+
}
|
|
10
|
+
const p = provider;
|
|
11
|
+
if (!isToken(p['provide'])) {
|
|
12
|
+
throw new TypeError(`Invalid "provide" token at ${where}: ${String(p['provide'])}${hint(p['provide'])}`);
|
|
13
|
+
}
|
|
14
|
+
const token = p['provide'];
|
|
15
|
+
const label = `${where} (${tokenName(token)})`;
|
|
16
|
+
const kinds = ['useClass', 'useValue', 'useFactory'].filter((key) => key in p);
|
|
17
|
+
if (kinds.length !== 1) {
|
|
18
|
+
throw new TypeError(`Provider at ${label} needs exactly one of useClass, useValue or useFactory`);
|
|
19
|
+
}
|
|
20
|
+
if ('useValue' in p) {
|
|
21
|
+
const value = p['useValue'];
|
|
22
|
+
return {
|
|
23
|
+
token,
|
|
24
|
+
kind: 'value',
|
|
25
|
+
scope: 'singleton',
|
|
26
|
+
deps: [],
|
|
27
|
+
create: () => value,
|
|
28
|
+
dispose: undefined,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const scope = p['scope'] ?? 'singleton';
|
|
32
|
+
if (scope !== 'singleton' && scope !== 'transient' && scope !== 'request') {
|
|
33
|
+
throw new TypeError(`Invalid scope ${typeof scope === 'string' ? `"${scope}"` : typeof scope} at ${label}`);
|
|
34
|
+
}
|
|
35
|
+
if ('useClass' in p) {
|
|
36
|
+
if (typeof p['useClass'] !== 'function') {
|
|
37
|
+
throw new TypeError(`useClass at ${label} must be a class`);
|
|
38
|
+
}
|
|
39
|
+
return classSpec(token, p['useClass'], scope, label);
|
|
40
|
+
}
|
|
41
|
+
const factory = p['useFactory'];
|
|
42
|
+
if (typeof factory !== 'function')
|
|
43
|
+
throw new TypeError(`useFactory at ${label} must be a function`);
|
|
44
|
+
const deps = checkInject(p['inject'], `${label} inject`);
|
|
45
|
+
if (factory.length > deps.length) {
|
|
46
|
+
throw new TypeError(`Factory at ${label} takes ${String(factory.length)} arguments but inject lists ${String(deps.length)}`);
|
|
47
|
+
}
|
|
48
|
+
const dispose = p['dispose'];
|
|
49
|
+
if (dispose !== undefined && typeof dispose !== 'function') {
|
|
50
|
+
throw new TypeError(`dispose at ${label} must be a function`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
token,
|
|
54
|
+
kind: 'factory',
|
|
55
|
+
scope,
|
|
56
|
+
deps,
|
|
57
|
+
create: (values) => factory(...values),
|
|
58
|
+
dispose: dispose,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function classSpec(token, cls, scope, where) {
|
|
62
|
+
const deps = checkInject(cls.inject, `${where} ${cls.name}.inject`);
|
|
63
|
+
if (cls.length > deps.length) {
|
|
64
|
+
throw new TypeError(`${cls.name || 'Class'} takes ${String(cls.length)} constructor arguments but static inject lists ${String(deps.length)} (at ${where})`);
|
|
65
|
+
}
|
|
66
|
+
const Ctor = cls;
|
|
67
|
+
return {
|
|
68
|
+
token,
|
|
69
|
+
kind: 'class',
|
|
70
|
+
scope,
|
|
71
|
+
deps,
|
|
72
|
+
create: (values) => new Ctor(...values),
|
|
73
|
+
dispose: undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function checkInject(inject, where) {
|
|
77
|
+
if (inject === undefined)
|
|
78
|
+
return [];
|
|
79
|
+
if (!Array.isArray(inject))
|
|
80
|
+
throw new TypeError(`${where} must be an array of tokens`);
|
|
81
|
+
inject.forEach((token, i) => {
|
|
82
|
+
if (!isToken(token)) {
|
|
83
|
+
throw new TypeError(`Invalid token at ${where}[${String(i)}]: ${String(token)}${hint(token)}`);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return inject;
|
|
87
|
+
}
|
|
88
|
+
function hint(value) {
|
|
89
|
+
return value === undefined ? ' (is there a circular import?)' : '';
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Class<T = unknown> = abstract new (...args: never[]) => T;
|
|
2
|
+
/** A class Avi can construct. Dependencies are listed in `static inject`. */
|
|
3
|
+
export type Constructor<T = unknown> = (new (...args: never[]) => T) & {
|
|
4
|
+
readonly inject?: readonly Token[];
|
|
5
|
+
};
|
|
6
|
+
declare const tokenType: unique symbol;
|
|
7
|
+
/** A typed, non-class token. Created with `createToken<T>()`. */
|
|
8
|
+
export interface InjectionToken<T> {
|
|
9
|
+
readonly description: string;
|
|
10
|
+
readonly [tokenType]?: T;
|
|
11
|
+
}
|
|
12
|
+
export type Token<T = unknown> = Class<T> | InjectionToken<T>;
|
|
13
|
+
export type TokenType<K> = K extends Class<infer T> ? T : K extends InjectionToken<infer T> ? T : never;
|
|
14
|
+
/** Maps an `inject` tuple to the tuple of values it resolves to. */
|
|
15
|
+
export type ResolveTokens<K extends readonly Token[]> = {
|
|
16
|
+
-readonly [I in keyof K]: TokenType<K[I]>;
|
|
17
|
+
};
|
|
18
|
+
export declare function createToken<T>(description: string): InjectionToken<T>;
|
|
19
|
+
export declare function isToken(value: unknown): value is Token;
|
|
20
|
+
export declare function tokenName(token: Token): string;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,CAAC,CAAC,GAAG,OAAO,IAAI,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AAEtE,6EAA6E;AAC7E,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,GAAG;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,KAAK,EAAE,CAAC;CACpC,CAAC;AAEF,OAAO,CAAC,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;AAEvC,iEAAiE;AACjE,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;CAC1B;AAED,MAAM,MAAM,KAAK,CAAC,CAAC,GAAG,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;AAE9D,MAAM,MAAM,SAAS,CAAC,CAAC,IACrB,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,cAAc,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE/E,oEAAoE;AACpE,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,SAAS,KAAK,EAAE,IAAI;IACtD,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1C,CAAC;AAEF,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAKrE;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAOtD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE9C"}
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createToken(description) {
|
|
2
|
+
if (typeof description !== 'string' || description === '') {
|
|
3
|
+
throw new TypeError('createToken needs a non-empty description');
|
|
4
|
+
}
|
|
5
|
+
return Object.freeze({ description });
|
|
6
|
+
}
|
|
7
|
+
export function isToken(value) {
|
|
8
|
+
return (typeof value === 'function' ||
|
|
9
|
+
(typeof value === 'object' &&
|
|
10
|
+
value !== null &&
|
|
11
|
+
typeof value.description === 'string'));
|
|
12
|
+
}
|
|
13
|
+
export function tokenName(token) {
|
|
14
|
+
return typeof token === 'function' ? token.name || '(anonymous class)' : token.description;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAyBA,MAAM,UAAU,WAAW,CAAI,WAAmB;IAChD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,KAAK,EAAE,EAAE,CAAC;QAC1D,MAAM,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAc;IACpC,OAAO,CACL,OAAO,KAAK,KAAK,UAAU;QAC3B,CAAC,OAAO,KAAK,KAAK,QAAQ;YACxB,KAAK,KAAK,IAAI;YACd,OAAQ,KAAmC,CAAC,WAAW,KAAK,QAAQ,CAAC,CACxE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAY;IACpC,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,mBAAmB,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC;AAC7F,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avijs/di",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Explicit, reflection-free dependency injection for Avi.js.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"avi",
|
|
7
|
+
"avi.js",
|
|
8
|
+
"dependency-injection",
|
|
9
|
+
"ioc",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Christopher Robin Chase",
|
|
14
|
+
"homepage": "https://github.com/chrischase011/avi-js#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/chrischase011/avi-js/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/chrischase011/avi-js.git",
|
|
21
|
+
"directory": "packages/di"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.12"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"@avijs/source": "./src/index.ts",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
46
|
+
}
|
|
6
47
|
}
|
package/src/container.ts
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { normalizeProvider, type Provider, type ProviderSpec, type Scope } from './provider.js';
|
|
2
|
+
import { isToken, tokenName, type Token } from './token.js';
|
|
3
|
+
|
|
4
|
+
export interface ModuleDefinition {
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
readonly imports?: readonly ModuleDefinition[];
|
|
7
|
+
readonly providers?: readonly Provider[];
|
|
8
|
+
/** Own providers, tokens re-exported from imports, or imported modules to re-export. */
|
|
9
|
+
readonly exports?: readonly (Token | ModuleDefinition)[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ContainerOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Providers that replace the provider for the same token wherever it is declared. Meant for
|
|
15
|
+
* tests. An override that matches nothing is an error.
|
|
16
|
+
*/
|
|
17
|
+
readonly overrides?: readonly Provider[];
|
|
18
|
+
/**
|
|
19
|
+
* Tokens whose values each request supplies through `requestScope()`, such as the request
|
|
20
|
+
* itself. Visible in every module, as request-scoped providers.
|
|
21
|
+
*/
|
|
22
|
+
readonly requestTokens?: readonly Token[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Request-scoped instances for one request. */
|
|
26
|
+
export interface RequestScope {
|
|
27
|
+
/** Like `Container.resolve`, but request-scoped providers are created once for this scope. */
|
|
28
|
+
resolve(module: ModuleDefinition, token: Token): unknown;
|
|
29
|
+
/** Disposes this scope's instances in reverse creation order. */
|
|
30
|
+
dispose(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProviderDescription {
|
|
34
|
+
readonly token: string;
|
|
35
|
+
readonly kind: 'class' | 'value' | 'factory';
|
|
36
|
+
readonly scope: Scope;
|
|
37
|
+
readonly inject: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ModuleDescription {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly imports: readonly string[];
|
|
43
|
+
readonly providers: readonly ProviderDescription[];
|
|
44
|
+
readonly exports: readonly string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ProviderRecord extends ProviderSpec {
|
|
48
|
+
readonly module: ModuleRecord;
|
|
49
|
+
links: ProviderRecord[];
|
|
50
|
+
/** Request-scoped, or transient with a request-scoped dependency. Set by `#check`. */
|
|
51
|
+
requestBound: boolean;
|
|
52
|
+
/** Supplied per request, see `requestTokens`. */
|
|
53
|
+
supplied: boolean;
|
|
54
|
+
state: 'unchecked' | 'checking' | 'checked';
|
|
55
|
+
instance: unknown;
|
|
56
|
+
ready: Promise<void> | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ModuleRecord {
|
|
60
|
+
readonly definition: ModuleDefinition;
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly imports: ModuleRecord[];
|
|
63
|
+
readonly own: Map<Token, ProviderRecord>;
|
|
64
|
+
readonly exported: Map<Token, ProviderRecord>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Lookup = ProviderRecord | { ambiguous: string[] } | undefined;
|
|
68
|
+
|
|
69
|
+
interface ScopeState {
|
|
70
|
+
readonly values: ReadonlyMap<Token, unknown>;
|
|
71
|
+
readonly instances: Map<ProviderRecord, unknown>;
|
|
72
|
+
readonly created: { record: ProviderRecord; value: unknown }[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const REQUEST_MODULE: ModuleRecord = {
|
|
76
|
+
definition: { name: 'request' },
|
|
77
|
+
name: 'request',
|
|
78
|
+
imports: [],
|
|
79
|
+
own: new Map(),
|
|
80
|
+
exported: new Map(),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Builds and validates the module graph on construction. `init()` creates every singleton,
|
|
85
|
+
* `dispose()` tears them down in reverse creation order.
|
|
86
|
+
*/
|
|
87
|
+
export class Container {
|
|
88
|
+
readonly #modules = new Map<ModuleDefinition, ModuleRecord>();
|
|
89
|
+
readonly #roots: ModuleRecord[];
|
|
90
|
+
readonly #created: ProviderRecord[] = [];
|
|
91
|
+
readonly #overrides = new Map<Token, ProviderSpec>();
|
|
92
|
+
readonly #overridden = new Set<Token>();
|
|
93
|
+
readonly #supplied = new Map<Token, ProviderRecord>();
|
|
94
|
+
#state: 'new' | 'initializing' | 'ready' | 'disposed' = 'new';
|
|
95
|
+
|
|
96
|
+
constructor(roots: readonly ModuleDefinition[], options: ContainerOptions = {}) {
|
|
97
|
+
(options.overrides ?? []).forEach((provider, i) => {
|
|
98
|
+
const spec = normalizeProvider(provider, `overrides[${String(i)}]`);
|
|
99
|
+
if (this.#overrides.has(spec.token)) {
|
|
100
|
+
throw new Error(`${tokenName(spec.token)} is overridden twice`);
|
|
101
|
+
}
|
|
102
|
+
this.#overrides.set(spec.token, spec);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
for (const token of options.requestTokens ?? []) {
|
|
106
|
+
this.#supplied.set(token, {
|
|
107
|
+
token,
|
|
108
|
+
kind: 'value',
|
|
109
|
+
scope: 'request',
|
|
110
|
+
deps: [],
|
|
111
|
+
create: () => undefined,
|
|
112
|
+
dispose: undefined,
|
|
113
|
+
module: REQUEST_MODULE,
|
|
114
|
+
links: [],
|
|
115
|
+
requestBound: true,
|
|
116
|
+
supplied: true,
|
|
117
|
+
state: 'checked',
|
|
118
|
+
instance: undefined,
|
|
119
|
+
ready: undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.#roots = roots.map((root, i) => this.#collect(root, [], `root module ${String(i)}`));
|
|
124
|
+
for (const token of this.#overrides.keys()) {
|
|
125
|
+
if (!this.#overridden.has(token)) {
|
|
126
|
+
throw new Error(`The override for ${tokenName(token)} matches no provider in the app`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const module of this.#modules.values()) {
|
|
130
|
+
for (const record of module.own.values()) this.#check(record, [tokenName(record.token)]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Module definitions in dependency order: imports before importers. */
|
|
135
|
+
get modules(): ModuleDefinition[] {
|
|
136
|
+
return [...this.#modules.keys()];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A plain description of the validated graph, without creating any provider. */
|
|
140
|
+
describe(): ModuleDescription[] {
|
|
141
|
+
return [...this.#modules.values()].map((module) => ({
|
|
142
|
+
name: module.name,
|
|
143
|
+
imports: module.imports.map((imported) => imported.name),
|
|
144
|
+
providers: [...module.own.values()].map((record) => ({
|
|
145
|
+
token: tokenName(record.token),
|
|
146
|
+
kind: record.kind,
|
|
147
|
+
scope: record.scope,
|
|
148
|
+
inject: record.deps.map(tokenName),
|
|
149
|
+
})),
|
|
150
|
+
exports: [...module.exported.keys()].map(tokenName),
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Throws if `tokens` cannot all be resolved from `module`. `label` starts the error path. */
|
|
155
|
+
check(module: ModuleDefinition, tokens: readonly Token[], label: string): void {
|
|
156
|
+
const record = this.#module(module);
|
|
157
|
+
for (const token of tokens) {
|
|
158
|
+
const target = this.#lookupOrThrow(record, token, [label]);
|
|
159
|
+
this.#check(target, [label, tokenName(token)]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** True if resolving any of `tokens` from `module` needs a request scope. */
|
|
164
|
+
needsRequest(module: ModuleDefinition, tokens: readonly Token[]): boolean {
|
|
165
|
+
const record = this.#module(module);
|
|
166
|
+
return tokens.some((token) => this.#lookupOrThrow(record, token, []).requestBound);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Starts a request scope. `values` supplies the `requestTokens`. */
|
|
170
|
+
requestScope(values: ReadonlyMap<Token, unknown>): RequestScope {
|
|
171
|
+
this.#assertReady();
|
|
172
|
+
const scope: ScopeState = { values, instances: new Map(), created: [] };
|
|
173
|
+
return {
|
|
174
|
+
resolve: (module, token) =>
|
|
175
|
+
this.#instance(this.#lookupOrThrow(this.#module(module), token, []), scope),
|
|
176
|
+
dispose: () => disposeAll(scope.created.splice(0)),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async init(): Promise<void> {
|
|
181
|
+
if (this.#state !== 'new') throw new Error(`Container is already ${this.#state}`);
|
|
182
|
+
this.#state = 'initializing';
|
|
183
|
+
try {
|
|
184
|
+
for (const module of this.#modules.values()) {
|
|
185
|
+
for (const record of module.own.values()) {
|
|
186
|
+
if (record.scope === 'singleton') await this.#ensure(record);
|
|
187
|
+
else await this.#ensureDeps(record);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this.#state = 'ready';
|
|
191
|
+
} catch (error) {
|
|
192
|
+
await this.dispose().catch(() => undefined);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Resolves `token` as seen from `module`. Transients are created on every call. */
|
|
198
|
+
resolve(module: ModuleDefinition, token: Token): unknown {
|
|
199
|
+
this.#assertReady();
|
|
200
|
+
return this.#instance(this.#lookupOrThrow(this.#module(module), token, []));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Resolves `token` from what the root modules can see. Meant for tests and scripts. */
|
|
204
|
+
get(token: Token): unknown {
|
|
205
|
+
this.#assertReady();
|
|
206
|
+
const found = new Set<ProviderRecord>();
|
|
207
|
+
for (const root of this.#roots) {
|
|
208
|
+
const hit = lookup(root, token);
|
|
209
|
+
if (hit && 'ambiguous' in hit) {
|
|
210
|
+
throw new Error(`${tokenName(token)} is exported to ${root.name} by more than one import`);
|
|
211
|
+
}
|
|
212
|
+
if (hit) found.add(hit);
|
|
213
|
+
}
|
|
214
|
+
if (found.size === 0) throw new Error(`${tokenName(token)} is not visible to the root modules`);
|
|
215
|
+
if (found.size > 1)
|
|
216
|
+
throw new Error(`${tokenName(token)} is provided by more than one root module`);
|
|
217
|
+
return this.#instance([...found][0] as ProviderRecord);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async dispose(): Promise<void> {
|
|
221
|
+
this.#state = 'disposed';
|
|
222
|
+
await disposeAll(this.#created.splice(0).map((record) => ({ record, value: record.instance })));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#collect(definition: ModuleDefinition, stack: ModuleDefinition[], where: string): ModuleRecord {
|
|
226
|
+
if (typeof definition !== 'object' || (definition as unknown) === null) {
|
|
227
|
+
const value = definition as unknown;
|
|
228
|
+
const hint = value === undefined ? ' (is there a circular import?)' : '';
|
|
229
|
+
throw new TypeError(
|
|
230
|
+
`Invalid module at ${where}: ${value === null ? 'null' : typeof value}${hint}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const existing = this.#modules.get(definition);
|
|
234
|
+
if (existing) return existing;
|
|
235
|
+
|
|
236
|
+
const name = definition.name ?? '(anonymous module)';
|
|
237
|
+
if (stack.includes(definition)) {
|
|
238
|
+
const cycle = [...stack.slice(stack.indexOf(definition)), definition].map(moduleName);
|
|
239
|
+
throw new Error(`Module import cycle: ${cycle.join(' → ')}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const imports = (definition.imports ?? []).map((child, i) =>
|
|
243
|
+
this.#collect(child, [...stack, definition], `${name}.imports[${String(i)}]`),
|
|
244
|
+
);
|
|
245
|
+
const record: ModuleRecord = { definition, name, imports, own: new Map(), exported: new Map() };
|
|
246
|
+
|
|
247
|
+
(definition.providers ?? []).forEach((provider, i) => {
|
|
248
|
+
const declared = normalizeProvider(provider, `${name}.providers[${String(i)}]`);
|
|
249
|
+
if (record.own.has(declared.token)) {
|
|
250
|
+
throw new Error(`${tokenName(declared.token)} is provided twice in ${name}`);
|
|
251
|
+
}
|
|
252
|
+
const spec = this.#overrides.get(declared.token) ?? declared;
|
|
253
|
+
if (spec !== declared) this.#overridden.add(declared.token);
|
|
254
|
+
record.own.set(spec.token, {
|
|
255
|
+
...spec,
|
|
256
|
+
module: record,
|
|
257
|
+
links: [],
|
|
258
|
+
requestBound: false,
|
|
259
|
+
supplied: false,
|
|
260
|
+
state: 'unchecked',
|
|
261
|
+
instance: undefined,
|
|
262
|
+
ready: undefined,
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
for (const entry of definition.exports ?? []) exportEntry(record, entry);
|
|
267
|
+
this.#modules.set(definition, record);
|
|
268
|
+
return record;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#module(definition: ModuleDefinition): ModuleRecord {
|
|
272
|
+
const record = this.#modules.get(definition);
|
|
273
|
+
if (!record) throw new Error(`${moduleName(definition)} is not part of this application`);
|
|
274
|
+
return record;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#lookupOrThrow(module: ModuleRecord, token: Token, path: string[]): ProviderRecord {
|
|
278
|
+
const hit = lookup(module, token) ?? this.#supplied.get(token);
|
|
279
|
+
const chain = [...path, tokenName(token)].join(' → ');
|
|
280
|
+
if (!hit) {
|
|
281
|
+
const imports = module.imports.map((m) => m.name).join(', ') || 'none';
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Cannot resolve ${chain} in ${module.name}: ${tokenName(token)} is not provided by ${module.name} or exported by its imports (${imports})`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if ('ambiguous' in hit) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Cannot resolve ${chain} in ${module.name}: ${tokenName(token)} is exported by more than one import (${hit.ambiguous.join(', ')})`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return hit;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Links dependencies and detects missing providers and cycles. */
|
|
295
|
+
#check(record: ProviderRecord, path: string[]): void {
|
|
296
|
+
if (record.state === 'checked') return;
|
|
297
|
+
if (record.state === 'checking') {
|
|
298
|
+
const start = path.indexOf(tokenName(record.token));
|
|
299
|
+
throw new Error(`Dependency cycle: ${path.slice(start).join(' → ')}`);
|
|
300
|
+
}
|
|
301
|
+
record.state = 'checking';
|
|
302
|
+
record.links = record.deps.map((token) => {
|
|
303
|
+
const target = this.#lookupOrThrow(record.module, token, path);
|
|
304
|
+
this.#check(target, [...path, tokenName(token)]);
|
|
305
|
+
return target;
|
|
306
|
+
});
|
|
307
|
+
record.requestBound =
|
|
308
|
+
record.scope === 'request' ||
|
|
309
|
+
(record.scope === 'transient' && record.links.some((link) => link.requestBound));
|
|
310
|
+
const bound = record.scope === 'singleton' && record.links.find((link) => link.requestBound);
|
|
311
|
+
if (bound) {
|
|
312
|
+
const name = tokenName(record.token);
|
|
313
|
+
const chain = [...path, tokenName(bound.token)].join(' → ');
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Singleton ${name} in ${record.module.name} depends on request-scoped ${tokenName(bound.token)} (${chain}). A singleton outlives the request; make ${name} request-scoped or transient.`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
record.state = 'checked';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#ensure(record: ProviderRecord): Promise<void> {
|
|
322
|
+
record.ready ??= (async () => {
|
|
323
|
+
await this.#ensureDeps(record);
|
|
324
|
+
let value = this.#create(record);
|
|
325
|
+
if (record.kind === 'factory' && value instanceof Promise) {
|
|
326
|
+
try {
|
|
327
|
+
value = await (value as Promise<unknown>);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw creationError(record, error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
record.instance = value;
|
|
333
|
+
if (record.kind !== 'value') this.#created.push(record);
|
|
334
|
+
})();
|
|
335
|
+
return record.ready;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async #ensureDeps(record: ProviderRecord): Promise<void> {
|
|
339
|
+
for (const dep of record.links) {
|
|
340
|
+
if (dep.scope === 'singleton') await this.#ensure(dep);
|
|
341
|
+
else await this.#ensureDeps(dep);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#create(record: ProviderRecord, scope?: ScopeState): unknown {
|
|
346
|
+
const deps = record.links.map((dep) => this.#instance(dep, scope));
|
|
347
|
+
try {
|
|
348
|
+
return record.create(deps);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
throw creationError(record, error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#instance(record: ProviderRecord, scope?: ScopeState): unknown {
|
|
355
|
+
if (record.scope === 'singleton') return record.instance;
|
|
356
|
+
if (record.scope === 'request') {
|
|
357
|
+
if (!scope) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`${tokenName(record.token)} is request-scoped and can only be resolved while handling a request`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (record.supplied) {
|
|
363
|
+
if (!scope.values.has(record.token)) {
|
|
364
|
+
throw new Error(`This request scope has no value for ${tokenName(record.token)}`);
|
|
365
|
+
}
|
|
366
|
+
return scope.values.get(record.token);
|
|
367
|
+
}
|
|
368
|
+
if (scope.instances.has(record)) return scope.instances.get(record);
|
|
369
|
+
const value = this.#createSync(record, scope);
|
|
370
|
+
scope.instances.set(record, value);
|
|
371
|
+
scope.created.push({ record, value });
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
return this.#createSync(record, scope);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#createSync(record: ProviderRecord, scope: ScopeState | undefined): unknown {
|
|
378
|
+
const value = this.#create(record, scope);
|
|
379
|
+
if (record.kind === 'factory' && value instanceof Promise) {
|
|
380
|
+
(value as Promise<unknown>).catch(() => undefined);
|
|
381
|
+
const kind = record.scope === 'request' ? 'Request-scoped' : 'Transient';
|
|
382
|
+
throw new Error(
|
|
383
|
+
`${kind} provider ${tokenName(record.token)} in ${record.module.name} returned a Promise; only singletons may be async`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#assertReady(): void {
|
|
390
|
+
if (this.#state !== 'ready')
|
|
391
|
+
throw new Error('The container is not initialized; call init() first');
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function lookup(module: ModuleRecord, token: Token): Lookup {
|
|
396
|
+
const own = module.own.get(token);
|
|
397
|
+
if (own) return own;
|
|
398
|
+
const hits = new Map<ProviderRecord, string>();
|
|
399
|
+
for (const imported of module.imports) {
|
|
400
|
+
const hit = imported.exported.get(token);
|
|
401
|
+
if (hit && !hits.has(hit)) hits.set(hit, imported.name);
|
|
402
|
+
}
|
|
403
|
+
if (hits.size > 1) return { ambiguous: [...hits.values()] };
|
|
404
|
+
return hits.keys().next().value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function exportEntry(module: ModuleRecord, entry: unknown): void {
|
|
408
|
+
const imported = module.imports.find((m) => m.definition === entry);
|
|
409
|
+
if (imported) {
|
|
410
|
+
for (const [token, record] of imported.exported) module.exported.set(token, record);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (!isToken(entry)) {
|
|
414
|
+
throw new TypeError(
|
|
415
|
+
`Invalid export in ${module.name}: expected a token or an imported module${entry === undefined ? ' (is there a circular import?)' : ''}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const hit = lookup(module, entry);
|
|
419
|
+
if (!hit) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`${module.name} exports ${tokenName(entry)}, which it neither provides nor imports`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
if ('ambiguous' in hit) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`${module.name} exports ${tokenName(entry)}, which more than one import provides (${hit.ambiguous.join(', ')})`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
module.exported.set(entry, hit);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Disposes in reverse order, continuing past failures. */
|
|
433
|
+
async function disposeAll(
|
|
434
|
+
created: readonly { record: ProviderRecord; value: unknown }[],
|
|
435
|
+
): Promise<void> {
|
|
436
|
+
const errors: unknown[] = [];
|
|
437
|
+
for (const { record, value } of [...created].reverse()) {
|
|
438
|
+
try {
|
|
439
|
+
await disposeValue(record, value);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
errors.push(error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (errors.length > 0) {
|
|
445
|
+
throw new AggregateError(errors, `Failed to dispose ${String(errors.length)} provider(s)`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function disposeValue(record: ProviderRecord, instance: unknown): Promise<void> {
|
|
450
|
+
if (record.dispose) {
|
|
451
|
+
await record.dispose(instance);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const value = instance as Partial<AsyncDisposable & Disposable> | null | undefined;
|
|
455
|
+
const asyncDispose = value?.[Symbol.asyncDispose];
|
|
456
|
+
const syncDispose = value?.[Symbol.dispose];
|
|
457
|
+
if (typeof asyncDispose === 'function') await asyncDispose.call(value);
|
|
458
|
+
else if (typeof syncDispose === 'function') syncDispose.call(value);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function creationError(record: ProviderRecord, cause: unknown): Error {
|
|
462
|
+
return new Error(`Failed to create ${tokenName(record.token)} in ${record.module.name}`, {
|
|
463
|
+
cause,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function moduleName(definition: ModuleDefinition): string {
|
|
468
|
+
return definition.name ?? '(anonymous module)';
|
|
469
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { Container } from './container.js';
|
|
2
|
+
export type {
|
|
3
|
+
ContainerOptions,
|
|
4
|
+
ModuleDefinition,
|
|
5
|
+
ModuleDescription,
|
|
6
|
+
ProviderDescription,
|
|
7
|
+
RequestScope,
|
|
8
|
+
} from './container.js';
|
|
9
|
+
export type { ClassProvider, FactoryProvider, Provider, Scope, ValueProvider } from './provider.js';
|
|
10
|
+
export { createToken } from './token.js';
|
|
11
|
+
export type {
|
|
12
|
+
Class,
|
|
13
|
+
Constructor,
|
|
14
|
+
InjectionToken,
|
|
15
|
+
ResolveTokens,
|
|
16
|
+
Token,
|
|
17
|
+
TokenType,
|
|
18
|
+
} from './token.js';
|