@fougere/container 0.11.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;IAgBlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAfpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;IAErD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IAIvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwB;IAEjD,OAAO,CAAC,QAAQ,CAA0C;IAK1D,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,CAS/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.11.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,21 +13,13 @@ 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 {
15
- private readonly registry = new Map<string, Entry>();
16
-
17
- private readonly built: unknown[] = [];
18
-
19
- // Closed by this container, and before `built` — a child may hold what this scope built,
20
- // never the other way round.
16
+ private readonly built: AsyncDisposable[] = [];
21
17
  private readonly children: ScopeContainer[] = [];
18
+ private readonly registry = new Map<string, Entry>();
19
+ private readonly resolving: string[];
22
20
 
23
21
  private fallback: ((name: string) => unknown) | undefined;
24
22
 
25
- // The names being built right now, shared by the whole tree: a miss here is answered by the
26
- // parent and the descent continues there, so only one stack can name the path whole —
27
- // `child.resolve('A')` reaching a parent's `B` that asks for `A` back reports `A → B → A`.
28
- private readonly resolving: string[];
29
-
30
23
  constructor(private readonly parent?: ScopeContainer) {
31
24
  this.resolving = parent?.resolving ?? [];
32
25
  }
@@ -43,7 +36,6 @@ export class ScopeContainer implements Container {
43
36
  }
44
37
 
45
38
  registerValue<T>(name: string, value: T): void {
46
- // A value the container did not build is not the container's to dispose.
47
39
  this.registry.set(name, {
48
40
  factory: () => value,
49
41
  lifetime: 'singleton',
@@ -54,51 +46,15 @@ export class ScopeContainer implements Container {
54
46
  resolve<T>(name: string): T {
55
47
  const entry = this.registry.get(name);
56
48
 
57
- // Not found locally — the parent holds it, and holds its instance too.
58
- if (!entry && this.parent?.entryOf(name)) {
59
- return this.parent.resolve<T>(name);
60
- }
61
-
62
- // Nobody holds it. A frond declared in `remotes` registers nothing here, so its
63
- // façade is fabricated by the fallback rather than found.
64
49
  if (!entry) {
65
- const made = this.fallbackOf()?.(name);
66
- if (made !== undefined) {
67
- this.registry.set(name, {
68
- factory: () => made,
69
- lifetime: 'singleton',
70
- instance: made,
71
- });
72
-
73
- return made as T;
74
- }
75
- throw new Error(`[container] '${name}' is not registered${this.through(name)}`);
76
- }
77
-
78
- if (entry.instance !== undefined) return entry.instance as T;
50
+ if (this.parent) return this.parent.resolve<T>(name);
79
51
 
80
- if (this.resolving.includes(name)) {
81
- throw new Error(
82
- `[container] dependency cycle: ${[...this.resolving, name].join(' → ')}`,
83
- );
52
+ return this.buildFallback<T>(name);
84
53
  }
85
54
 
86
- // Popped in a finally, because a constructor that throws leaves the name on the stack
87
- // otherwise and the next resolution of it reports a cycle that is not there.
88
- this.resolving.push(name);
89
- try {
90
- const value = entry.factory(this) as T;
91
- // The container disposes what it KEEPS: a transient is handed over and forgotten,
92
- // and its caller is the one who knows when it is done.
93
- if (entry.lifetime === 'singleton') {
94
- entry.instance = value;
95
- this.remember(value);
96
- }
55
+ if (entry.instance !== undefined) return entry.instance as T;
97
56
 
98
- return value;
99
- } finally {
100
- this.resolving.pop();
101
- }
57
+ return this.build<T>(name, entry);
102
58
  }
103
59
 
104
60
  has(name: string): boolean {
@@ -107,68 +63,126 @@ export class ScopeContainer implements Container {
107
63
 
108
64
  createScope(): Container {
109
65
  const child = new ScopeContainer(this);
66
+
110
67
  this.children.push(child);
111
68
 
112
69
  return child;
113
70
  }
114
71
 
115
72
  async dispose(): Promise<void> {
116
- // Everything is told before anything throws, then the failures travel together.
117
- // Dropped from the parent first: a scope that closes itself leaves a reference the
118
- // parent would hold for the life of the process.
119
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[]> {
120
95
  const failures: unknown[] = [];
121
- // A copy: closing a child splices it out of `children`, so walking the array
122
- // itself stepped over every second sibling.
123
- for (const child of [...this.children].reverse()) {
96
+
97
+ while (this.children.length > 0) {
98
+ const child = this.children[this.children.length - 1]!;
99
+
124
100
  try {
125
101
  await child.dispose();
126
102
  } catch (error) {
127
103
  failures.push(error);
104
+ } finally {
105
+ this.forget(child);
128
106
  }
129
107
  }
130
- this.children.length = 0;
131
- 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) {
132
115
  try {
133
- await (value as Disposable).dispose();
116
+ await this.built.pop()?.[Symbol.asyncDispose]();
134
117
  } catch (error) {
135
118
  failures.push(error);
136
119
  }
137
120
  }
138
- this.built.length = 0;
139
- this.registry.clear();
140
- if (failures.length > 0) {
141
- throw new AggregateError(failures, '[container] one or more disposals failed');
142
- }
121
+
122
+ return failures;
143
123
  }
144
124
 
145
125
  setFallback(resolve: (name: string) => unknown): void {
126
+ if (this.parent) return this.parent.setFallback(resolve);
127
+
146
128
  this.fallback = resolve;
147
129
  }
148
130
 
149
- private entryOf(name: string): Entry | undefined {
150
- 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
+ }
151
153
  }
152
154
 
153
- /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
154
- private fallbackOf(): ((name: string) => unknown) | undefined {
155
- 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;
156
167
  }
157
168
 
158
169
  private forget(child: ScopeContainer): void {
159
170
  const at = this.children.indexOf(child);
171
+
160
172
  if (at !== -1) this.children.splice(at, 1);
161
173
  }
162
174
 
163
- private through(name: string): string {
164
- return this.resolving.length > 0
165
- ? ` (resolving: ${[...this.resolving, name].join(' → ')})`
166
- : '';
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);
167
178
  }
168
179
 
169
- private remember<T>(value: T): T {
170
- 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
+ : '';
171
185
 
172
- return value;
186
+ return `'${name}' is not registered${path}`;
173
187
  }
174
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();