@fougere/container 0.10.0-alpha.0 → 0.12.0-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.
@@ -0,0 +1,5 @@
1
+ export declare class ContainerError extends Error {
2
+ constructor(message: string);
3
+ static all(failures: unknown[], message: string): AggregateError;
4
+ }
5
+ //# sourceMappingURL=ContainerError.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContainerError.d.ts","sourceRoot":"","sources":["../src/ContainerError.ts"],"names":[],"mappings":"AAAA,qBAAa,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAO,EAAE,MAAM,EAI1B;IAED,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,CAE/D;CACF"}
@@ -0,0 +1,10 @@
1
+ export class ContainerError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = new.target.name;
5
+ }
6
+ static all(failures, message) {
7
+ return new AggregateError(failures, message);
8
+ }
9
+ }
10
+ //# sourceMappingURL=ContainerError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContainerError.js","sourceRoot":"","sources":["../src/ContainerError.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;IAED,MAAM,CAAC,GAAG,CAAC,QAAmB,EAAE,OAAe;QAC7C,OAAO,IAAI,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/C,CAAC;CACF"}
@@ -1,7 +1,4 @@
1
- export interface Disposable {
2
- dispose(): void | Promise<void>;
3
- }
4
1
  export declare class Disposables {
5
- static is(value: unknown): value is Disposable;
2
+ static is(value: unknown): value is AsyncDisposable;
6
3
  }
7
4
  //# sourceMappingURL=Disposable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Disposable.d.ts","sourceRoot":"","sources":["../src/Disposable.ts"],"names":[],"mappings":"AAAA,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"}
1
+ {"version":3,"file":"Disposable.d.ts","sourceRoot":"","sources":["../src/Disposable.ts"],"names":[],"mappings":"AAAA,qBAAa,WAAW;IACtB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAGlD;CACF"}
@@ -1,8 +1,7 @@
1
1
  export class Disposables {
2
2
  static is(value) {
3
- return (typeof value === 'object' &&
4
- value !== null &&
5
- typeof value.dispose === 'function');
3
+ return typeof value === 'object' && value !== null
4
+ && typeof value[Symbol.asyncDispose] === 'function';
6
5
  }
7
6
  }
8
7
  //# sourceMappingURL=Disposable.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Disposable.js","sourceRoot":"","sources":["../src/Disposable.ts"],"names":[],"mappings":"AAIA,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"}
1
+ {"version":3,"file":"Disposable.js","sourceRoot":"","sources":["../src/Disposable.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,WAAW;IACtB,MAAM,CAAC,EAAE,CAAC,KAAc;QACtB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;eAC7C,OAAQ,KAAyB,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,UAAU,CAAC;IAC7E,CAAC;CACF"}
@@ -4,11 +4,11 @@ import type { RegisterOptions } from './registration/RegisterOptions.js';
4
4
  /** A scope reaches its parent and its children through members only a scope can read. */
5
5
  export declare class ScopeContainer implements Container {
6
6
  private readonly parent?;
7
- private readonly registry;
8
7
  private readonly built;
9
8
  private readonly children;
10
- private fallback;
9
+ private readonly registry;
11
10
  private readonly resolving;
11
+ private fallback;
12
12
  constructor(parent?: ScopeContainer | undefined);
13
13
  register<T>(name: string, ctor: Constructor<T>, options?: RegisterOptions): void;
14
14
  registerValue<T>(name: string, value: T): void;
@@ -16,12 +16,14 @@ export declare class ScopeContainer implements Container {
16
16
  has(name: string): boolean;
17
17
  createScope(): Container;
18
18
  dispose(): Promise<void>;
19
+ private disposeChildren;
20
+ private disposeBuilt;
19
21
  setFallback(resolve: (name: string) => unknown): void;
20
- private entryOf;
21
- /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
22
- private fallbackOf;
22
+ private build;
23
+ private buildFallback;
23
24
  private forget;
24
- private through;
25
+ /** What this scope will close. A value that answers no `[Symbol.asyncDispose]` is not one of them. */
25
26
  private remember;
27
+ private errorMessage;
26
28
  }
27
29
  //# sourceMappingURL=ScopeContainer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ScopeContainer.d.ts","sourceRoot":"","sources":["../src/ScopeContainer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAQzE,yFAAyF;AACzF,qBAAa,cAAe,YAAW,SAAS;IAalC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAZpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IAGvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAA0C;IAI1D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IAErC,YAA6B,MAAM,CAAC,EAAE,cAAc,YAAA,EAEnD;IAED,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAO/E;IAED,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAO7C;IAED,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAgD1B;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzB;IAED,WAAW,IAAI,SAAS,CAKvB;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CA4B7B;IAED,WAAW,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAEpD;IAED,OAAO,CAAC,OAAO;IAIf,uFAAuF;IACvF,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,QAAQ;CAKjB"}
1
+ {"version":3,"file":"ScopeContainer.d.ts","sourceRoot":"","sources":["../src/ScopeContainer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAQzE,yFAAyF;AACzF,qBAAa,cAAe,YAAW,SAAS;IAQlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAPpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IAErC,OAAO,CAAC,QAAQ,CAA0C;IAE1D,YAA6B,MAAM,CAAC,EAAE,cAAc,YAAA,EAEnD;IAED,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAS/E;IAED,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAM7C;IAED,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAY1B;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzB;IAED,WAAW,IAAI,SAAS,CAMvB;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAoB7B;YAEa,eAAe;YAkBf,YAAY;IAa1B,WAAW,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAIpD;IAED,OAAO,CAAC,KAAK;IAwBb,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,MAAM;IAMd,sGAAsG;IACtG,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,YAAY;CAQrB"}
@@ -1,17 +1,13 @@
1
1
  import { Disposables } from './Disposable.js';
2
+ import { ContainerError } from './ContainerError.js';
2
3
  /** A scope reaches its parent and its children through members only a scope can read. */
3
4
  export class ScopeContainer {
4
5
  parent;
5
- registry = new Map();
6
6
  built = [];
7
- // Closed by this container, and before `built` — a child may hold what this scope built,
8
- // never the other way round.
9
7
  children = [];
10
- fallback;
11
- // The names being built right now, shared by the whole tree: a miss here is answered by the
12
- // parent and the descent continues there, so only one stack can name the path whole —
13
- // `child.resolve('A')` reaching a parent's `B` that asks for `A` back reports `A → B → A`.
8
+ registry = new Map();
14
9
  resolving;
10
+ fallback;
15
11
  constructor(parent) {
16
12
  this.parent = parent;
17
13
  this.resolving = parent?.resolving ?? [];
@@ -25,7 +21,6 @@ export class ScopeContainer {
25
21
  });
26
22
  }
27
23
  registerValue(name, value) {
28
- // A value the container did not build is not the container's to dispose.
29
24
  this.registry.set(name, {
30
25
  factory: () => value,
31
26
  lifetime: 'singleton',
@@ -34,45 +29,14 @@ export class ScopeContainer {
34
29
  }
35
30
  resolve(name) {
36
31
  const entry = this.registry.get(name);
37
- // Not found locally — the parent holds it, and holds its instance too.
38
- if (!entry && this.parent?.entryOf(name)) {
39
- return this.parent.resolve(name);
40
- }
41
- // Nobody holds it. A frond declared in `remotes` registers nothing here, so its
42
- // façade is fabricated by the fallback rather than found.
43
32
  if (!entry) {
44
- const made = this.fallbackOf()?.(name);
45
- if (made !== undefined) {
46
- this.registry.set(name, {
47
- factory: () => made,
48
- lifetime: 'singleton',
49
- instance: made,
50
- });
51
- return made;
52
- }
53
- throw new Error(`[container] '${name}' is not registered${this.through(name)}`);
33
+ if (this.parent)
34
+ return this.parent.resolve(name);
35
+ return this.buildFallback(name);
54
36
  }
55
37
  if (entry.instance !== undefined)
56
38
  return entry.instance;
57
- if (this.resolving.includes(name)) {
58
- throw new Error(`[container] dependency cycle: ${[...this.resolving, name].join(' → ')}`);
59
- }
60
- // Popped in a finally, because a constructor that throws leaves the name on the stack
61
- // otherwise and the next resolution of it reports a cycle that is not there.
62
- this.resolving.push(name);
63
- try {
64
- const value = entry.factory(this);
65
- // The container disposes what it KEEPS: a transient is handed over and forgotten,
66
- // and its caller is the one who knows when it is done.
67
- if (entry.lifetime === 'singleton') {
68
- entry.instance = value;
69
- this.remember(value);
70
- }
71
- return value;
72
- }
73
- finally {
74
- this.resolving.pop();
75
- }
39
+ return this.build(name, entry);
76
40
  }
77
41
  has(name) {
78
42
  return this.registry.has(name) || (this.parent?.has(name) ?? false);
@@ -83,60 +47,93 @@ export class ScopeContainer {
83
47
  return child;
84
48
  }
85
49
  async dispose() {
86
- // Everything is told before anything throws, then the failures travel together.
87
- // Dropped from the parent first: a scope that closes itself leaves a reference the
88
- // parent would hold for the life of the process.
89
50
  this.parent?.forget(this);
51
+ const failures = await this.disposeChildren();
52
+ failures.push(...(await this.disposeBuilt()));
53
+ this.registry.clear();
54
+ if (this.children.length > 0) {
55
+ failures.push(new ContainerError(`${this.children.length} scope(s) still held after this one closed — probably opened while it was closing.`));
56
+ }
57
+ if (failures.length > 0) {
58
+ throw ContainerError.all(failures, 'one or more disposals failed');
59
+ }
60
+ }
61
+ async disposeChildren() {
90
62
  const failures = [];
91
- // A copy: closing a child splices it out of `children`, so walking the array
92
- // itself stepped over every second sibling.
93
- for (const child of [...this.children].reverse()) {
63
+ while (this.children.length > 0) {
64
+ const child = this.children[this.children.length - 1];
94
65
  try {
95
66
  await child.dispose();
96
67
  }
97
68
  catch (error) {
98
69
  failures.push(error);
99
70
  }
71
+ finally {
72
+ this.forget(child);
73
+ }
100
74
  }
101
- this.children.length = 0;
102
- for (const value of this.built.reverse()) {
75
+ return failures;
76
+ }
77
+ async disposeBuilt() {
78
+ const failures = [];
79
+ while (this.built.length > 0) {
103
80
  try {
104
- await value.dispose();
81
+ await this.built.pop()?.[Symbol.asyncDispose]();
105
82
  }
106
83
  catch (error) {
107
84
  failures.push(error);
108
85
  }
109
86
  }
110
- this.built.length = 0;
111
- this.registry.clear();
112
- if (failures.length > 0) {
113
- throw new AggregateError(failures, '[container] one or more disposals failed');
114
- }
87
+ return failures;
115
88
  }
116
89
  setFallback(resolve) {
90
+ if (this.parent)
91
+ return this.parent.setFallback(resolve);
117
92
  this.fallback = resolve;
118
93
  }
119
- entryOf(name) {
120
- return this.registry.get(name) ?? this.parent?.entryOf(name);
94
+ build(name, entry) {
95
+ if (this.resolving.includes(name)) {
96
+ throw new ContainerError(`dependency cycle: ${[...this.resolving, name].join(' → ')}`);
97
+ }
98
+ this.resolving.push(name);
99
+ try {
100
+ const value = entry.factory(this);
101
+ if (entry.lifetime === 'singleton') {
102
+ entry.instance = value;
103
+ this.remember(value);
104
+ }
105
+ return value;
106
+ }
107
+ finally {
108
+ this.resolving.pop();
109
+ }
121
110
  }
122
- /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
123
- fallbackOf() {
124
- return this.fallback ?? this.parent?.fallbackOf();
111
+ buildFallback(name) {
112
+ const made = this.fallback?.(name);
113
+ if (made === undefined)
114
+ throw new ContainerError(this.errorMessage(name));
115
+ this.registry.set(name, {
116
+ factory: () => made,
117
+ lifetime: 'singleton',
118
+ instance: made,
119
+ });
120
+ return made;
125
121
  }
126
122
  forget(child) {
127
123
  const at = this.children.indexOf(child);
128
124
  if (at !== -1)
129
125
  this.children.splice(at, 1);
130
126
  }
131
- through(name) {
132
- return this.resolving.length > 0
133
- ? ` (resolving: ${[...this.resolving, name].join(' → ')})`
134
- : '';
135
- }
127
+ /** What this scope will close. A value that answers no `[Symbol.asyncDispose]` is not one of them. */
136
128
  remember(value) {
137
129
  if (Disposables.is(value))
138
130
  this.built.push(value);
139
- return value;
131
+ }
132
+ errorMessage(name) {
133
+ const path = this.resolving.length > 0
134
+ ? ` (resolving: ${[...this.resolving, name].join(' → ')})`
135
+ : '';
136
+ return `'${name}' is not registered${path}`;
140
137
  }
141
138
  }
142
139
  //# sourceMappingURL=ScopeContainer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ScopeContainer.js","sourceRoot":"","sources":["../src/ScopeContainer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ScopeContainer.js","sourceRoot":"","sources":["../src/ScopeContainer.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
+ import { ContainerError } from './ContainerError.js';
1
2
  import type { Container } from './Container.js';
2
3
  import type { Constructor } from './registration/Constructor.js';
3
- export type { Container, Constructor };
4
+ import type { Lifetime } from './registration/Lifetime.js';
5
+ import type { RegisterOptions } from './registration/RegisterOptions.js';
6
+ export type { Container, Constructor, RegisterOptions, Lifetime };
7
+ export { ContainerError };
4
8
  export declare function createContainer(): Container;
5
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAEvC,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAEzE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ScopeContainer } from './ScopeContainer.js';
2
+ import { ContainerError } from './ContainerError.js';
3
+ export { ContainerError };
2
4
  export function createContainer() {
3
5
  return new ScopeContainer();
4
6
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAMrD,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,cAAc,EAAE,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAOrD,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,MAAM,UAAU,eAAe;IAC7B,OAAO,IAAI,cAAc,EAAE,CAAC;AAC9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/container",
3
- "version": "0.10.0-alpha.0",
3
+ "version": "0.12.0-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",
@@ -0,0 +1,11 @@
1
+ export class ContainerError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+
5
+ this.name = new.target.name;
6
+ }
7
+
8
+ static all(failures: unknown[], message: string): AggregateError {
9
+ return new AggregateError(failures, message);
10
+ }
11
+ }
package/src/Disposable.ts CHANGED
@@ -1,13 +1,6 @@
1
- export interface Disposable {
2
- dispose(): void | Promise<void>;
3
- }
4
-
5
1
  export class Disposables {
6
- static is(value: unknown): value is Disposable {
7
- return (
8
- typeof value === 'object' &&
9
- value !== null &&
10
- typeof (value as Disposable).dispose === 'function'
11
- );
2
+ static is(value: unknown): value is AsyncDisposable {
3
+ return typeof value === 'object' && value !== null
4
+ && typeof (value as AsyncDisposable)[Symbol.asyncDispose] === 'function';
12
5
  }
13
6
  }
@@ -1,5 +1,6 @@
1
1
  import type { Container } from './Container.js';
2
- import { Disposables, type Disposable } from './Disposable.js';
2
+ import { Disposables } from './Disposable.js';
3
+ import { ContainerError } from './ContainerError.js';
3
4
  import type { Constructor } from './registration/Constructor.js';
4
5
  import type { Lifetime } from './registration/Lifetime.js';
5
6
  import type { RegisterOptions } from './registration/RegisterOptions.js';
@@ -12,17 +13,12 @@ interface Entry {
12
13
 
13
14
  /** A scope reaches its parent and its children through members only a scope can read. */
14
15
  export class ScopeContainer implements Container {
16
+ private readonly built: AsyncDisposable[] = [];
17
+ private readonly children: ScopeContainer[] = [];
15
18
  private readonly registry = new Map<string, Entry>();
19
+ private readonly resolving: string[];
16
20
 
17
- private readonly built: unknown[] = [];
18
- // Closed by this container, and before `built` — a child may hold what this scope built,
19
- // never the other way round.
20
- private readonly children: ScopeContainer[] = [];
21
21
  private fallback: ((name: string) => unknown) | undefined;
22
- // The names being built right now, shared by the whole tree: a miss here is answered by the
23
- // parent and the descent continues there, so only one stack can name the path whole —
24
- // `child.resolve('A')` reaching a parent's `B` that asks for `A` back reports `A → B → A`.
25
- private readonly resolving: string[];
26
22
 
27
23
  constructor(private readonly parent?: ScopeContainer) {
28
24
  this.resolving = parent?.resolving ?? [];
@@ -30,7 +26,9 @@ export class ScopeContainer implements Container {
30
26
 
31
27
  register<T>(name: string, ctor: Constructor<T>, options?: RegisterOptions): void {
32
28
  const lifetime = options?.lifetime ?? 'transient';
29
+
33
30
  const deps = options?.deps ?? [];
31
+
34
32
  this.registry.set(name, {
35
33
  factory: (c) => new ctor(...deps.map((d) => c.resolve(d))),
36
34
  lifetime,
@@ -38,7 +36,6 @@ export class ScopeContainer implements Container {
38
36
  }
39
37
 
40
38
  registerValue<T>(name: string, value: T): void {
41
- // A value the container did not build is not the container's to dispose.
42
39
  this.registry.set(name, {
43
40
  factory: () => value,
44
41
  lifetime: 'singleton',
@@ -49,51 +46,15 @@ export class ScopeContainer implements Container {
49
46
  resolve<T>(name: string): T {
50
47
  const entry = this.registry.get(name);
51
48
 
52
- // Not found locally — the parent holds it, and holds its instance too.
53
- if (!entry && this.parent?.entryOf(name)) {
54
- return this.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
49
  if (!entry) {
60
- const made = this.fallbackOf()?.(name);
61
- if (made !== undefined) {
62
- this.registry.set(name, {
63
- factory: () => made,
64
- lifetime: 'singleton',
65
- instance: made,
66
- });
67
-
68
- return made as T;
69
- }
70
- throw new Error(`[container] '${name}' is not registered${this.through(name)}`);
71
- }
50
+ if (this.parent) return this.parent.resolve<T>(name);
72
51
 
73
- if (entry.instance !== undefined) return entry.instance as T;
74
-
75
- if (this.resolving.includes(name)) {
76
- throw new Error(
77
- `[container] dependency cycle: ${[...this.resolving, name].join(' → ')}`,
78
- );
52
+ return this.buildFallback<T>(name);
79
53
  }
80
54
 
81
- // Popped in a finally, because a constructor that throws leaves the name on the stack
82
- // otherwise and the next resolution of it reports a cycle that is not there.
83
- this.resolving.push(name);
84
- try {
85
- const value = entry.factory(this) as T;
86
- // The container disposes what it KEEPS: a transient is handed over and forgotten,
87
- // and its caller is the one who knows when it is done.
88
- if (entry.lifetime === 'singleton') {
89
- entry.instance = value;
90
- this.remember(value);
91
- }
55
+ if (entry.instance !== undefined) return entry.instance as T;
92
56
 
93
- return value;
94
- } finally {
95
- this.resolving.pop();
96
- }
57
+ return this.build<T>(name, entry);
97
58
  }
98
59
 
99
60
  has(name: string): boolean {
@@ -102,68 +63,126 @@ export class ScopeContainer implements Container {
102
63
 
103
64
  createScope(): Container {
104
65
  const child = new ScopeContainer(this);
66
+
105
67
  this.children.push(child);
106
68
 
107
69
  return child;
108
70
  }
109
71
 
110
72
  async dispose(): Promise<void> {
111
- // Everything is told before anything throws, then the failures travel together.
112
- // Dropped from the parent first: a scope that closes itself leaves a reference the
113
- // parent would hold for the life of the process.
114
73
  this.parent?.forget(this);
74
+
75
+ const failures = await this.disposeChildren();
76
+
77
+ failures.push(...(await this.disposeBuilt()));
78
+
79
+ this.registry.clear();
80
+
81
+ if (this.children.length > 0) {
82
+ failures.push(
83
+ new ContainerError(
84
+ `${this.children.length} scope(s) still held after this one closed — probably opened while it was closing.`,
85
+ ),
86
+ );
87
+ }
88
+
89
+ if (failures.length > 0) {
90
+ throw ContainerError.all(failures, 'one or more disposals failed');
91
+ }
92
+ }
93
+
94
+ private async disposeChildren(): Promise<unknown[]> {
115
95
  const failures: unknown[] = [];
116
- // A copy: closing a child splices it out of `children`, so walking the array
117
- // itself stepped over every second sibling.
118
- for (const child of [...this.children].reverse()) {
96
+
97
+ while (this.children.length > 0) {
98
+ const child = this.children[this.children.length - 1]!;
99
+
119
100
  try {
120
101
  await child.dispose();
121
102
  } catch (error) {
122
103
  failures.push(error);
104
+ } finally {
105
+ this.forget(child);
123
106
  }
124
107
  }
125
- this.children.length = 0;
126
- for (const value of this.built.reverse()) {
108
+
109
+ return failures;
110
+ }
111
+
112
+ private async disposeBuilt(): Promise<unknown[]> {
113
+ const failures: unknown[] = [];
114
+ while (this.built.length > 0) {
127
115
  try {
128
- await (value as Disposable).dispose();
116
+ await this.built.pop()?.[Symbol.asyncDispose]();
129
117
  } catch (error) {
130
118
  failures.push(error);
131
119
  }
132
120
  }
133
- this.built.length = 0;
134
- this.registry.clear();
135
- if (failures.length > 0) {
136
- throw new AggregateError(failures, '[container] one or more disposals failed');
137
- }
121
+
122
+ return failures;
138
123
  }
139
124
 
140
125
  setFallback(resolve: (name: string) => unknown): void {
126
+ if (this.parent) return this.parent.setFallback(resolve);
127
+
141
128
  this.fallback = resolve;
142
129
  }
143
130
 
144
- private entryOf(name: string): Entry | undefined {
145
- return this.registry.get(name) ?? this.parent?.entryOf(name);
131
+ private build<T>(name: string, entry: Entry): T {
132
+ if (this.resolving.includes(name)) {
133
+ throw new ContainerError(
134
+ `dependency cycle: ${[...this.resolving, name].join(' → ')}`,
135
+ );
136
+ }
137
+
138
+ this.resolving.push(name);
139
+
140
+ try {
141
+ const value = entry.factory(this) as T;
142
+
143
+ if (entry.lifetime === 'singleton') {
144
+ entry.instance = value;
145
+
146
+ this.remember(value);
147
+ }
148
+
149
+ return value;
150
+ } finally {
151
+ this.resolving.pop();
152
+ }
146
153
  }
147
154
 
148
- /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
149
- private fallbackOf(): ((name: string) => unknown) | undefined {
150
- return this.fallback ?? this.parent?.fallbackOf();
155
+ private buildFallback<T>(name: string): T {
156
+ const made = this.fallback?.(name);
157
+
158
+ if (made === undefined) throw new ContainerError(this.errorMessage(name));
159
+
160
+ this.registry.set(name, {
161
+ factory: () => made,
162
+ lifetime: 'singleton',
163
+ instance: made,
164
+ });
165
+
166
+ return made as T;
151
167
  }
152
168
 
153
169
  private forget(child: ScopeContainer): void {
154
170
  const at = this.children.indexOf(child);
171
+
155
172
  if (at !== -1) this.children.splice(at, 1);
156
173
  }
157
174
 
158
- private through(name: string): string {
159
- return this.resolving.length > 0
160
- ? ` (resolving: ${[...this.resolving, name].join(' → ')})`
161
- : '';
175
+ /** What this scope will close. A value that answers no `[Symbol.asyncDispose]` is not one of them. */
176
+ private remember(value: unknown): void {
177
+ if (Disposables.is(value)) this.built.push(value);
162
178
  }
163
179
 
164
- private remember<T>(value: T): T {
165
- if (Disposables.is(value)) this.built.push(value);
180
+ private errorMessage(name: string): string {
181
+ const path =
182
+ this.resolving.length > 0
183
+ ? ` (resolving: ${[...this.resolving, name].join(' → ')})`
184
+ : '';
166
185
 
167
- return value;
186
+ return `'${name}' is not registered${path}`;
168
187
  }
169
188
  }
package/src/index.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import { ScopeContainer } from './ScopeContainer.js';
2
+ import { ContainerError } from './ContainerError.js';
2
3
  import type { Container } from './Container.js';
3
4
  import type { Constructor } from './registration/Constructor.js';
5
+ import type { Lifetime } from './registration/Lifetime.js';
6
+ import type { RegisterOptions } from './registration/RegisterOptions.js';
4
7
 
5
- export type { Container, Constructor };
8
+ export type { Container, Constructor, RegisterOptions, Lifetime };
9
+ export { ContainerError };
6
10
 
7
11
  export function createContainer(): Container {
8
12
  return new ScopeContainer();