@fougere/container 0.9.0-alpha.1 → 0.9.3-alpha.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/README.md CHANGED
@@ -13,6 +13,85 @@ that lives in another process.
13
13
  pnpm add @fougere/container
14
14
  ```
15
15
 
16
+ ## What the alternatives ask of a class
17
+
18
+ Measured at the tarball on 2026-09-11, not read off a README.
19
+
20
+ | | where a dependency is declared | what the class carries | deps | unpacked |
21
+ |---|---|---|---|---|
22
+ | [InversifyJS 8.2.3](https://www.npmjs.com/package/inversify) | a decorator | `@injectable()`, `@inject(TOKEN)` | 3 | 36 KB, plus its three |
23
+ | [tsyringe 4.10.0](https://www.npmjs.com/package/tsyringe) | a decorator, read through `Reflect` | `@injectable()`, and `import 'reflect-metadata'` at the entry point — it throws at load without one | `tslib` | 148 KB |
24
+ | [TypeDI 0.10.0](https://www.npmjs.com/package/typedi) | a decorator | `@Service()` | 0 | last published 2022-12-09 |
25
+ | [typed-inject 5.0.0](https://www.npmjs.com/package/typed-inject) | a static field | `static inject = tokens('logger')` | 0 | 102 KB |
26
+ | [Awilix 13.0.5](https://www.npmjs.com/package/awilix) | the parameter's NAME (`CLASSIC`), or the destructured property's (`PROXY`) | nothing — but the name becomes the contract | `fast-glob` | 326 KB |
27
+ | [didi 11.0.0](https://www.npmjs.com/package/didi) | the parameter's name, or `Car.$inject = ['engine']` | nothing, or the static | 0 | 49 KB |
28
+ | `@fougere/container` | the parameter's TYPE | nothing | 0 | 220 lines |
29
+
30
+ Every row but the last makes the class name the container. `@injectable()`, `static inject`
31
+ and `$inject` are the dependency written a second time, next to the signature that already
32
+ states it. A parameter name is the same thing spelled shorter, and it is the one a bundler
33
+ may rewrite — which is why didi ships `$inject` as the "minification safe manner", and why
34
+ `ProviderEntry.name` is written down here rather than read off `ctor.name` at boot.
35
+
36
+ Here the signature IS the declaration. `constructor(private users: UserRepository)` is read
37
+ by the scan (`depKeyOf`, `@fougere/compiler`), which hands the boot `deps:
38
+ ['UserRepository']`. So `register` receives the graph already resolved and this container
39
+ infers nothing — which is the whole reason it fits in 220 lines. What these libraries exist
40
+ to answer is answered before they would run.
41
+
42
+ ## What they have that this does not
43
+
44
+ - **typed-inject** resolves type-safely — `resolve<Token extends keyof TContext>` refuses at
45
+ compile time a token the injector cannot answer. Here `resolve<T>(name: string): T` is an
46
+ unchecked cast, and a wrong name is a boot error instead.
47
+ - **Awilix** has a third lifetime (`SCOPED`, one instance per scope), `aliasTo`, a disposer
48
+ per registration, and `loadModules`. This has two lifetimes, and disposes what it built.
49
+
50
+ And one thing none of them has: a resolver of last resort. `setFallback` has a single
51
+ caller — the boot, fabricating a façade for a frond declared in `remotes:`. Such a frond
52
+ registers nothing here, because it runs in another process.
53
+
54
+ ## This one, measured the same way
55
+
56
+ `0.8.4-alpha.0` — 21 KB unpacked, zero dependencies, 220 lines, one test file. Seven
57
+ methods, two lifetimes, a parent chain consulted upward on every miss.
58
+
59
+ The holes, probed rather than assumed:
60
+
61
+ - **A transient is never disposed**, deliberately — the container disposes what it KEEPS and
62
+ hands a transient over. Nothing says so where it is registered.
63
+ - **A second `register` of one name overwrites, silently.** `Bundle` refuses a duplicate
64
+ registration key, `serveRpc` a second reading, `portBindings` two implementations of a
65
+ port. This does not.
66
+ - `RegisterOptions` and `Disposable` are named by `Container` but not exported from the
67
+ entry, so a caller cannot spell the options object it passes.
68
+
69
+ ## Why not didi
70
+
71
+ It is the closest of the six, so it gets the measurement rather than a position.
72
+ `annotate(['engine', 'license'], Car)` puts the dependency list OUTSIDE the class — exactly
73
+ the shape the scan produces, and the class carries nothing, as here. `createChild` nests.
74
+ `get(name, false)` answers `null` instead of throwing. 49 KB, zero dependencies, published
75
+ 2025-12-11.
76
+
77
+ Two things stop it. **It has no disposal at all** — no `dispose`, `teardown`, `destroy` or
78
+ `close`, in its API, its code or its README — and `container.dispose()` is the middle of the
79
+ three levels `app.dispose()` runs in reverse of construction. And its unit is a
80
+ `ModuleDeclaration` carrying `__init__`, `__depends__` and `__exports__`: a second notion of
81
+ module beside the frond, onto which every frond would have to be projected.
82
+
83
+ Reading it paid anyway. Its `currentlyResolving` stack is the shape used here, and naming
84
+ the descent in EVERY refusal — not only in a cycle — is its idea: `'Zzz' is not registered
85
+ (resolving: A → B → Zzz)`. Two things were changed on the way in. Its `pop()` sits outside a
86
+ `finally` (didi 11.0.0, `dist/index.js`), so a constructor that throws leaves the name on the
87
+ stack and the next resolution of it reports a cycle that is not there — measured, and the
88
+ reason this one pops in a `finally`. And its stack belongs to one injector, where this one
89
+ shares it down the tree, which is what lets a path that continues in a parent be named whole.
90
+
91
+ The day this container needs less than it does — no disposal to run, no scope filled after
92
+ it is created — didi is the one to take.
93
+
94
+
16
95
  ---
17
96
 
18
97
  Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
@@ -12,10 +12,6 @@ export interface RegisterOptions {
12
12
  */
13
13
  deps?: string[];
14
14
  }
15
- /** Anything holding a resource can say so, and disposing the container says it back. */
16
- export interface Disposable {
17
- dispose(): void | Promise<void>;
18
- }
19
15
  /** DI container interface — the only thing application code sees. */
20
16
  export interface Container {
21
17
  /** The container builds it: `register('UserService', UserService, { deps: ['UserRepository'] })`. */
@@ -33,4 +29,4 @@ export interface Container {
33
29
  /** Children first, then what this scope built, in reverse — refusals travel as one `AggregateError`. */
34
30
  dispose(): Promise<void>;
35
31
  }
36
- //# sourceMappingURL=container.d.ts.map
32
+ //# sourceMappingURL=Container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Container.d.ts","sourceRoot":"","sources":["../../src/container/Container.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IAErC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB,qGAAqG;IACrG,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAEjF,+GAA+G;IAC/G,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAE/C,yEAAyE;IACzE,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAE5B,kEAAkE;IAClE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAE3B,yEAAyE;IACzE,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAEvD,yEAAyE;IACzE,WAAW,IAAI,SAAS,CAAC;IAEzB,wGAAwG;IACxG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Container.js","sourceRoot":"","sources":["../../src/container/Container.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ /** Anything holding a resource can say so, and disposing the container says it back. */
2
+ export interface Disposable {
3
+ dispose(): void | Promise<void>;
4
+ }
5
+ export declare class Disposables {
6
+ static is(value: unknown): value is Disposable;
7
+ }
8
+ //# sourceMappingURL=Disposable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Disposable.d.ts","sourceRoot":"","sources":["../../src/container/Disposable.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qBAAa,WAAW;IACtB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAM7C;CACF"}
@@ -0,0 +1,8 @@
1
+ export class Disposables {
2
+ static is(value) {
3
+ return (typeof value === 'object' &&
4
+ value !== null &&
5
+ typeof value.dispose === 'function');
6
+ }
7
+ }
8
+ //# sourceMappingURL=Disposable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Disposable.js","sourceRoot":"","sources":["../../src/container/Disposable.ts"],"names":[],"mappings":"AAKA,MAAM,OAAO,WAAW;IACtB,MAAM,CAAC,EAAE,CAAC,KAAc;QACtB,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;YACzB,KAAK,KAAK,IAAI;YACd,OAAQ,KAAoB,CAAC,OAAO,KAAK,UAAU,CACpD,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ import type { Container } from './Container.js';
2
+ export declare function createContainer(): Container;
3
+ //# sourceMappingURL=ScopeContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScopeContainer.d.ts","sourceRoot":"","sources":["../../src/container/ScopeContainer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAgC,MAAM,gBAAgB,CAAC;AAwK9E,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
@@ -0,0 +1,147 @@
1
+ import { Disposables } from './Disposable.js';
2
+ /** A scope reaches its parent and its children through members only a scope can read. */
3
+ class ScopeContainer {
4
+ parent;
5
+ registry = new Map();
6
+ // In construction order: a thing built later may hold one built earlier, so disposal
7
+ // walks this backwards.
8
+ built = [];
9
+ // Closed by this container, and before `built` — a child may hold what this scope built,
10
+ // never the other way round.
11
+ children = [];
12
+ fallback;
13
+ // The names being built right now, shared by the whole tree: a miss here is answered by the
14
+ // parent and the descent continues there, so only one stack can name the path whole —
15
+ // `child.resolve('A')` reaching a parent's `B` that asks for `A` back reports `A → B → A`.
16
+ resolving;
17
+ constructor(parent) {
18
+ this.parent = parent;
19
+ this.resolving = parent?.resolving ?? [];
20
+ }
21
+ register(name, ctor, options) {
22
+ const lifetime = options?.lifetime ?? 'transient';
23
+ const deps = options?.deps ?? [];
24
+ this.registry.set(name, {
25
+ factory: (c) => new ctor(...deps.map((d) => c.resolve(d))),
26
+ lifetime,
27
+ });
28
+ }
29
+ registerValue(name, value) {
30
+ // A value the container did not build is not the container's to dispose.
31
+ this.registry.set(name, {
32
+ factory: () => value,
33
+ lifetime: 'singleton',
34
+ instance: value,
35
+ });
36
+ }
37
+ resolve(name) {
38
+ const entry = this.registry.get(name);
39
+ // Not found locally — the parent holds it, and holds its instance too.
40
+ if (!entry && this.parent?.entryOf(name)) {
41
+ return this.parent.resolve(name);
42
+ }
43
+ // Nobody holds it. A frond declared in `remotes` registers nothing here, so its
44
+ // façade is fabricated by the fallback rather than found.
45
+ if (!entry) {
46
+ const made = this.fallbackOf()?.(name);
47
+ if (made !== undefined) {
48
+ this.registry.set(name, {
49
+ factory: () => made,
50
+ lifetime: 'singleton',
51
+ instance: made,
52
+ });
53
+ return made;
54
+ }
55
+ throw new Error(`[container] '${name}' is not registered${this.through(name)}`);
56
+ }
57
+ if (entry.instance !== undefined)
58
+ return entry.instance;
59
+ if (this.resolving.includes(name)) {
60
+ throw new Error(`[container] dependency cycle: ${[...this.resolving, name].join(' → ')}`);
61
+ }
62
+ // Popped in a finally, because a constructor that throws leaves the name on the stack
63
+ // otherwise and the next resolution of it reports a cycle that is not there.
64
+ this.resolving.push(name);
65
+ try {
66
+ const value = entry.factory(this);
67
+ // The container disposes what it KEEPS: a transient is handed over and forgotten,
68
+ // and its caller is the one who knows when it is done.
69
+ if (entry.lifetime === 'singleton') {
70
+ entry.instance = value;
71
+ this.remember(value);
72
+ }
73
+ return value;
74
+ }
75
+ finally {
76
+ this.resolving.pop();
77
+ }
78
+ }
79
+ has(name) {
80
+ return this.registry.has(name) || (this.parent?.has(name) ?? false);
81
+ }
82
+ createScope() {
83
+ const child = new ScopeContainer(this);
84
+ this.children.push(child);
85
+ return child;
86
+ }
87
+ async dispose() {
88
+ // Everything is told before anything throws, then the failures travel together.
89
+ // Dropped from the parent first: a scope that closes itself leaves a reference the
90
+ // parent would hold for the life of the process.
91
+ this.parent?.forget(this);
92
+ const failures = [];
93
+ // A copy: closing a child splices it out of `children`, so walking the array
94
+ // itself stepped over every second sibling.
95
+ for (const child of [...this.children].reverse()) {
96
+ try {
97
+ await child.dispose();
98
+ }
99
+ catch (error) {
100
+ failures.push(error);
101
+ }
102
+ }
103
+ this.children.length = 0;
104
+ for (const value of this.built.reverse()) {
105
+ try {
106
+ await value.dispose();
107
+ }
108
+ catch (error) {
109
+ failures.push(error);
110
+ }
111
+ }
112
+ this.built.length = 0;
113
+ this.registry.clear();
114
+ if (failures.length > 0) {
115
+ throw new AggregateError(failures, '[container] one or more disposals failed');
116
+ }
117
+ }
118
+ setFallback(resolve) {
119
+ this.fallback = resolve;
120
+ }
121
+ entryOf(name) {
122
+ return this.registry.get(name) ?? this.parent?.entryOf(name);
123
+ }
124
+ /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
125
+ fallbackOf() {
126
+ return this.fallback ?? this.parent?.fallbackOf();
127
+ }
128
+ forget(child) {
129
+ const at = this.children.indexOf(child);
130
+ if (at !== -1)
131
+ this.children.splice(at, 1);
132
+ }
133
+ through(name) {
134
+ return this.resolving.length > 0
135
+ ? ` (resolving: ${[...this.resolving, name].join(' → ')})`
136
+ : '';
137
+ }
138
+ remember(value) {
139
+ if (Disposables.is(value))
140
+ this.built.push(value);
141
+ return value;
142
+ }
143
+ }
144
+ export function createContainer() {
145
+ return new ScopeContainer();
146
+ }
147
+ //# sourceMappingURL=ScopeContainer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScopeContainer.js","sourceRoot":"","sources":["../../src/container/ScopeContainer.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { type Container, type Constructor, } from './container.js';
2
- export { createContainer } from './create.js';
1
+ export { type Container, type Constructor } from './container/Container.js';
2
+ export { createContainer } from './container/ScopeContainer.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,KAAK,WAAW,GACjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { createContainer } from './create.js';
1
+ export { createContainer } from './container/ScopeContainer.js';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/container",
3
- "version": "0.9.0-alpha.1",
3
+ "version": "0.9.3-alpha.0",
4
4
  "description": "Fougere's DI container: resolution by type, wired from the AST scan. Zero dependencies.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -1,4 +1,3 @@
1
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
1
  export type Constructor<T = unknown> = new (...args: any[]) => T;
3
2
 
4
3
  export interface RegisterOptions {
@@ -16,11 +15,6 @@ export interface RegisterOptions {
16
15
  deps?: string[];
17
16
  }
18
17
 
19
- /** Anything holding a resource can say so, and disposing the container says it back. */
20
- export interface Disposable {
21
- dispose(): void | Promise<void>;
22
- }
23
-
24
18
  /** DI container interface — the only thing application code sees. */
25
19
  export interface Container {
26
20
  /** The container builds it: `register('UserService', UserService, { deps: ['UserRepository'] })`. */
@@ -0,0 +1,14 @@
1
+ /** Anything holding a resource can say so, and disposing the container says it back. */
2
+ export interface Disposable {
3
+ dispose(): void | Promise<void>;
4
+ }
5
+
6
+ export class Disposables {
7
+ static is(value: unknown): value is Disposable {
8
+ return (
9
+ typeof value === 'object' &&
10
+ value !== null &&
11
+ typeof (value as Disposable).dispose === 'function'
12
+ );
13
+ }
14
+ }
@@ -0,0 +1,171 @@
1
+ import type { Container, Constructor, RegisterOptions } from './Container.js';
2
+ import { Disposables, type Disposable } from './Disposable.js';
3
+
4
+ interface Entry {
5
+ factory: (container: Container) => unknown;
6
+ lifetime: 'singleton' | 'transient';
7
+ instance?: unknown;
8
+ }
9
+
10
+ /** A scope reaches its parent and its children through members only a scope can read. */
11
+ class ScopeContainer implements Container {
12
+ private readonly registry = new Map<string, Entry>();
13
+ // In construction order: a thing built later may hold one built earlier, so disposal
14
+ // walks this backwards.
15
+ private readonly built: unknown[] = [];
16
+ // Closed by this container, and before `built` — a child may hold what this scope built,
17
+ // never the other way round.
18
+ private readonly children: ScopeContainer[] = [];
19
+ private fallback: ((name: string) => unknown) | undefined;
20
+ // The names being built right now, shared by the whole tree: a miss here is answered by the
21
+ // parent and the descent continues there, so only one stack can name the path whole —
22
+ // `child.resolve('A')` reaching a parent's `B` that asks for `A` back reports `A → B → A`.
23
+ private readonly resolving: string[];
24
+
25
+ constructor(private readonly parent?: ScopeContainer) {
26
+ this.resolving = parent?.resolving ?? [];
27
+ }
28
+
29
+ register<T>(name: string, ctor: Constructor<T>, options?: RegisterOptions): void {
30
+ const lifetime = options?.lifetime ?? 'transient';
31
+ const deps = options?.deps ?? [];
32
+ this.registry.set(name, {
33
+ factory: (c) => new ctor(...deps.map((d) => c.resolve(d))),
34
+ lifetime,
35
+ });
36
+ }
37
+
38
+ registerValue<T>(name: string, value: T): void {
39
+ // A value the container did not build is not the container's to dispose.
40
+ this.registry.set(name, {
41
+ factory: () => value,
42
+ lifetime: 'singleton',
43
+ instance: value,
44
+ });
45
+ }
46
+
47
+ resolve<T>(name: string): T {
48
+ const entry = this.registry.get(name);
49
+
50
+ // Not found locally — the parent holds it, and holds its instance too.
51
+ if (!entry && this.parent?.entryOf(name)) {
52
+ return this.parent.resolve<T>(name);
53
+ }
54
+
55
+ // Nobody holds it. A frond declared in `remotes` registers nothing here, so its
56
+ // façade is fabricated by the fallback rather than found.
57
+ if (!entry) {
58
+ const made = this.fallbackOf()?.(name);
59
+ if (made !== undefined) {
60
+ this.registry.set(name, {
61
+ factory: () => made,
62
+ lifetime: 'singleton',
63
+ instance: made,
64
+ });
65
+
66
+ return made as T;
67
+ }
68
+ throw new Error(`[container] '${name}' is not registered${this.through(name)}`);
69
+ }
70
+
71
+ if (entry.instance !== undefined) return entry.instance as T;
72
+
73
+ if (this.resolving.includes(name)) {
74
+ throw new Error(
75
+ `[container] dependency cycle: ${[...this.resolving, name].join(' → ')}`,
76
+ );
77
+ }
78
+
79
+ // Popped in a finally, because a constructor that throws leaves the name on the stack
80
+ // otherwise and the next resolution of it reports a cycle that is not there.
81
+ this.resolving.push(name);
82
+ try {
83
+ const value = entry.factory(this) as T;
84
+ // The container disposes what it KEEPS: a transient is handed over and forgotten,
85
+ // and its caller is the one who knows when it is done.
86
+ if (entry.lifetime === 'singleton') {
87
+ entry.instance = value;
88
+ this.remember(value);
89
+ }
90
+
91
+ return value;
92
+ } finally {
93
+ this.resolving.pop();
94
+ }
95
+ }
96
+
97
+ has(name: string): boolean {
98
+ return this.registry.has(name) || (this.parent?.has(name) ?? false);
99
+ }
100
+
101
+ createScope(): Container {
102
+ const child = new ScopeContainer(this);
103
+ this.children.push(child);
104
+
105
+ return child;
106
+ }
107
+
108
+ async dispose(): Promise<void> {
109
+ // Everything is told before anything throws, then the failures travel together.
110
+ // Dropped from the parent first: a scope that closes itself leaves a reference the
111
+ // parent would hold for the life of the process.
112
+ this.parent?.forget(this);
113
+ const failures: unknown[] = [];
114
+ // A copy: closing a child splices it out of `children`, so walking the array
115
+ // itself stepped over every second sibling.
116
+ for (const child of [...this.children].reverse()) {
117
+ try {
118
+ await child.dispose();
119
+ } catch (error) {
120
+ failures.push(error);
121
+ }
122
+ }
123
+ this.children.length = 0;
124
+ for (const value of this.built.reverse()) {
125
+ try {
126
+ await (value as Disposable).dispose();
127
+ } catch (error) {
128
+ failures.push(error);
129
+ }
130
+ }
131
+ this.built.length = 0;
132
+ this.registry.clear();
133
+ if (failures.length > 0) {
134
+ throw new AggregateError(failures, '[container] one or more disposals failed');
135
+ }
136
+ }
137
+
138
+ setFallback(resolve: (name: string) => unknown): void {
139
+ this.fallback = resolve;
140
+ }
141
+
142
+ private entryOf(name: string): Entry | undefined {
143
+ return this.registry.get(name) ?? this.parent?.entryOf(name);
144
+ }
145
+
146
+ /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
147
+ private fallbackOf(): ((name: string) => unknown) | undefined {
148
+ return this.fallback ?? this.parent?.fallbackOf();
149
+ }
150
+
151
+ private forget(child: ScopeContainer): void {
152
+ const at = this.children.indexOf(child);
153
+ if (at !== -1) this.children.splice(at, 1);
154
+ }
155
+
156
+ private through(name: string): string {
157
+ return this.resolving.length > 0
158
+ ? ` (resolving: ${[...this.resolving, name].join(' → ')})`
159
+ : '';
160
+ }
161
+
162
+ private remember<T>(value: T): T {
163
+ if (Disposables.is(value)) this.built.push(value);
164
+
165
+ return value;
166
+ }
167
+ }
168
+
169
+ export function createContainer(): Container {
170
+ return new ScopeContainer();
171
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,3 @@
1
- export {
2
- type Container,
3
- type Constructor,
4
- } from './container.js';
1
+ export { type Container, type Constructor } from './container/Container.js';
5
2
 
6
- export { createContainer } from './create.js';
3
+ export { createContainer } from './container/ScopeContainer.js';
@@ -1 +0,0 @@
1
- {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IAErC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB,qGAAqG;IACrG,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAEjF,+GAA+G;IAC/G,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAE/C,yEAAyE;IACzE,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAAC;IAE5B,kEAAkE;IAClE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAE3B,yEAAyE;IACzE,WAAW,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAEvD,yEAAyE;IACzE,WAAW,IAAI,SAAS,CAAC;IAEzB,wGAAwG;IACxG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
package/dist/container.js DELETED
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=container.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"container.js","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":""}
package/dist/create.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import type { Container } from './container.js';
2
- export declare function createContainer(): Container;
3
- //# sourceMappingURL=create.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AA4I1F,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
package/dist/create.js DELETED
@@ -1,117 +0,0 @@
1
- const isDisposable = (value) => typeof value === 'object' && value !== null &&
2
- typeof value.dispose === 'function';
3
- function createScope(parent) {
4
- const registry = new Map();
5
- // In construction order: a thing built later may hold one built earlier, so disposal
6
- // walks this backwards.
7
- const built = [];
8
- // Closed by this container, and before `built` — a child may hold what this scope built,
9
- // never the other way round.
10
- const children = [];
11
- let fallback;
12
- const remember = (value) => {
13
- if (isDisposable(value))
14
- built.push(value);
15
- return value;
16
- };
17
- const container = {
18
- register(name, ctor, options) {
19
- const lifetime = options?.lifetime ?? 'transient';
20
- const deps = options?.deps ?? [];
21
- registry.set(name, {
22
- factory: (c) => new ctor(...deps.map((d) => c.resolve(d))),
23
- lifetime,
24
- });
25
- },
26
- registerValue(name, value) {
27
- // A value the container did not build is not the container's to dispose.
28
- registry.set(name, { factory: () => value, lifetime: 'singleton', instance: value });
29
- },
30
- resolve(name) {
31
- let entry = registry.get(name);
32
- // Not found locally — the parent holds it, and holds its instance too.
33
- if (!entry && parent && parent._getEntry(name)) {
34
- return parent.resolve(name);
35
- }
36
- // Nobody holds it. A frond declared in `remotes` registers nothing here, so its
37
- // façade is fabricated by the fallback rather than found.
38
- if (!entry) {
39
- const made = container._getFallback()?.(name);
40
- if (made !== undefined) {
41
- registry.set(name, { factory: () => made, lifetime: 'singleton', instance: made });
42
- return made;
43
- }
44
- throw new Error(`[container] '${name}' is not registered`);
45
- }
46
- if (entry.instance !== undefined)
47
- return entry.instance;
48
- const value = entry.factory(container);
49
- // The container disposes what it KEEPS: a transient is handed over and forgotten,
50
- // and its caller is the one who knows when it is done.
51
- if (entry.lifetime === 'singleton') {
52
- entry.instance = value;
53
- remember(value);
54
- }
55
- return value;
56
- },
57
- has(name) {
58
- return registry.has(name) || (parent?.has(name) ?? false);
59
- },
60
- createScope() {
61
- const child = createScope(container);
62
- children.push(child);
63
- return child;
64
- },
65
- async dispose() {
66
- // Everything is told before anything throws, then the failures travel together.
67
- // Dropped from the parent first: a scope that closes itself leaves a reference the
68
- // parent would hold for the life of the process.
69
- parent?._forget(container);
70
- const failures = [];
71
- // A copy: closing a child splices it out of `children`, so walking the array
72
- // itself stepped over every second sibling.
73
- for (const child of [...children].reverse()) {
74
- try {
75
- await child.dispose();
76
- }
77
- catch (error) {
78
- failures.push(error);
79
- }
80
- }
81
- children.length = 0;
82
- for (const value of built.reverse()) {
83
- try {
84
- await value.dispose();
85
- }
86
- catch (error) {
87
- failures.push(error);
88
- }
89
- }
90
- built.length = 0;
91
- registry.clear();
92
- if (failures.length > 0) {
93
- throw new AggregateError(failures, '[container] one or more disposals failed');
94
- }
95
- },
96
- setFallback(resolve) {
97
- fallback = resolve;
98
- },
99
- _getEntry(name) {
100
- return registry.get(name) ?? parent?._getEntry(name);
101
- },
102
- _forget(child) {
103
- const at = children.indexOf(child);
104
- if (at !== -1)
105
- children.splice(at, 1);
106
- },
107
- /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
108
- _getFallback() {
109
- return fallback ?? parent?._getFallback();
110
- },
111
- };
112
- return container;
113
- }
114
- export function createContainer() {
115
- return createScope();
116
- }
117
- //# sourceMappingURL=create.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"create.js","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAcA,MAAM,YAAY,GAAG,CAAC,KAAc,EAAuB,EAAE,CAC3D,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;IAC3C,OAAQ,KAAoB,CAAC,OAAO,KAAK,UAAU,CAAC;AAEtD,SAAS,WAAW,CAAC,MAAuB;IAC1C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC1C,qFAAqF;IACrF,wBAAwB;IACxB,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,yFAAyF;IACzF,6BAA6B;IAC7B,MAAM,QAAQ,GAAqB,EAAE,CAAC;IACtC,IAAI,QAAiD,CAAC;IAEtD,MAAM,QAAQ,GAAG,CAAI,KAAQ,EAAK,EAAE;QAClC,IAAI,YAAY,CAAC,KAAK,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,SAAS,GAAmB;QAChC,QAAQ,CAAI,IAAY,EAAE,IAAoB,EAAE,OAAyB;YACvE,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,WAAW,CAAC;YAClD,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC;YACjC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE;gBACjB,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC1D,QAAQ;aACT,CAAC,CAAC;QACL,CAAC;QAED,aAAa,CAAI,IAAY,EAAE,KAAQ;YACrC,yEAAyE;YACzE,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,OAAO,CAAI,IAAY;YACrB,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAE/B,uEAAuE;YACvE,IAAI,CAAC,KAAK,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/C,OAAO,MAAM,CAAC,OAAO,CAAI,IAAI,CAAC,CAAC;YACjC,CAAC;YAED,gFAAgF;YAChF,0DAA0D;YAC1D,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,GAAG,SAAS,CAAC,YAAY,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;gBAC9C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;oBACvB,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;oBACnF,OAAO,IAAS,CAAC;gBACnB,CAAC;gBACD,MAAM,IAAI,KAAK,CAAC,gBAAgB,IAAI,qBAAqB,CAAC,CAAC;YAC7D,CAAC;YAED,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC,QAAa,CAAC;YAC7D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAM,CAAC;YAC5C,kFAAkF;YAClF,uDAAuD;YACvD,IAAI,KAAK,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACnC,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC;gBACvB,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,GAAG,CAAC,IAAY;YACd,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC;QAC5D,CAAC;QAED,WAAW;YACT,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;YACrC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,KAAK,CAAC,OAAO;YACX,gFAAgF;YAChF,mFAAmF;YACnF,iDAAiD;YACjD,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;YAC3B,MAAM,QAAQ,GAAc,EAAE,CAAC;YAC/B,6EAA6E;YAC7E,4CAA4C;YAC5C,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC5C,IAAI,CAAC;oBACH,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;gBACxB,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;YACD,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACpB,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;gBACpC,IAAI,CAAC;oBACH,MAAO,KAAoB,CAAC,OAAO,EAAE,CAAC;gBACxC,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;YACD,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjB,QAAQ,CAAC,KAAK,EAAE,CAAC;YACjB,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,cAAc,CAAC,QAAQ,EAAE,0CAA0C,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;QAED,WAAW,CAAC,OAAkC;YAC5C,QAAQ,GAAG,OAAO,CAAC;QACrB,CAAC;QAED,SAAS,CAAC,IAAY;YACpB,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,CAAC,KAAqB;YAC3B,MAAM,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACnC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACxC,CAAC;QAED,uFAAuF;QACvF,YAAY;YACV,OAAO,QAAQ,IAAI,MAAM,EAAE,YAAY,EAAE,CAAC;QAC5C,CAAC;KACF,CAAC;IAEF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC"}
package/src/create.ts DELETED
@@ -1,143 +0,0 @@
1
- import type { Container, RegisterOptions, Constructor, Disposable } from './container.js';
2
-
3
- interface Entry {
4
- factory: (container: Container) => unknown;
5
- lifetime: 'singleton' | 'transient';
6
- instance?: unknown;
7
- }
8
-
9
- interface ScopeContainer extends Container {
10
- _getEntry(name: string): Entry | undefined;
11
- _getFallback(): ((name: string) => unknown) | undefined;
12
- _forget(child: ScopeContainer): void;
13
- }
14
-
15
- const isDisposable = (value: unknown): value is Disposable =>
16
- typeof value === 'object' && value !== null &&
17
- typeof (value as Disposable).dispose === 'function';
18
-
19
- function createScope(parent?: ScopeContainer): ScopeContainer {
20
- const registry = new Map<string, Entry>();
21
- // In construction order: a thing built later may hold one built earlier, so disposal
22
- // walks this backwards.
23
- const built: unknown[] = [];
24
- // Closed by this container, and before `built` — a child may hold what this scope built,
25
- // never the other way round.
26
- const children: ScopeContainer[] = [];
27
- let fallback: ((name: string) => unknown) | undefined;
28
-
29
- const remember = <T>(value: T): T => {
30
- if (isDisposable(value)) built.push(value);
31
- return value;
32
- };
33
-
34
- const container: ScopeContainer = {
35
- register<T>(name: string, ctor: Constructor<T>, options?: RegisterOptions) {
36
- const lifetime = options?.lifetime ?? 'transient';
37
- const deps = options?.deps ?? [];
38
- registry.set(name, {
39
- factory: (c) => new ctor(...deps.map((d) => c.resolve(d))),
40
- lifetime,
41
- });
42
- },
43
-
44
- registerValue<T>(name: string, value: T) {
45
- // A value the container did not build is not the container's to dispose.
46
- registry.set(name, { factory: () => value, lifetime: 'singleton', instance: value });
47
- },
48
-
49
- resolve<T>(name: string): T {
50
- let entry = registry.get(name);
51
-
52
- // Not found locally — the parent holds it, and holds its instance too.
53
- if (!entry && parent && parent._getEntry(name)) {
54
- return parent.resolve<T>(name);
55
- }
56
-
57
- // Nobody holds it. A frond declared in `remotes` registers nothing here, so its
58
- // façade is fabricated by the fallback rather than found.
59
- if (!entry) {
60
- const made = container._getFallback()?.(name);
61
- if (made !== undefined) {
62
- registry.set(name, { factory: () => made, lifetime: 'singleton', instance: made });
63
- return made as T;
64
- }
65
- throw new Error(`[container] '${name}' is not registered`);
66
- }
67
-
68
- if (entry.instance !== undefined) return entry.instance as T;
69
- const value = entry.factory(container) as T;
70
- // The container disposes what it KEEPS: a transient is handed over and forgotten,
71
- // and its caller is the one who knows when it is done.
72
- if (entry.lifetime === 'singleton') {
73
- entry.instance = value;
74
- remember(value);
75
- }
76
- return value;
77
- },
78
-
79
- has(name: string): boolean {
80
- return registry.has(name) || (parent?.has(name) ?? false);
81
- },
82
-
83
- createScope(): Container {
84
- const child = createScope(container);
85
- children.push(child);
86
- return child;
87
- },
88
-
89
- async dispose(): Promise<void> {
90
- // Everything is told before anything throws, then the failures travel together.
91
- // Dropped from the parent first: a scope that closes itself leaves a reference the
92
- // parent would hold for the life of the process.
93
- parent?._forget(container);
94
- const failures: unknown[] = [];
95
- // A copy: closing a child splices it out of `children`, so walking the array
96
- // itself stepped over every second sibling.
97
- for (const child of [...children].reverse()) {
98
- try {
99
- await child.dispose();
100
- } catch (error) {
101
- failures.push(error);
102
- }
103
- }
104
- children.length = 0;
105
- for (const value of built.reverse()) {
106
- try {
107
- await (value as Disposable).dispose();
108
- } catch (error) {
109
- failures.push(error);
110
- }
111
- }
112
- built.length = 0;
113
- registry.clear();
114
- if (failures.length > 0) {
115
- throw new AggregateError(failures, '[container] one or more disposals failed');
116
- }
117
- },
118
-
119
- setFallback(resolve: (name: string) => unknown) {
120
- fallback = resolve;
121
- },
122
-
123
- _getEntry(name: string): Entry | undefined {
124
- return registry.get(name) ?? parent?._getEntry(name);
125
- },
126
-
127
- _forget(child: ScopeContainer) {
128
- const at = children.indexOf(child);
129
- if (at !== -1) children.splice(at, 1);
130
- },
131
-
132
- /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
133
- _getFallback() {
134
- return fallback ?? parent?._getFallback();
135
- },
136
- };
137
-
138
- return container;
139
- }
140
-
141
- export function createContainer(): Container {
142
- return createScope();
143
- }