@fougere/container 0.2.0-alpha.2 → 0.4.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.
package/README.md CHANGED
@@ -17,4 +17,4 @@ pnpm add @fougere/container
17
17
 
18
18
  Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
19
19
  monolith to distributed, the same user code.
20
- Reference documentation: [the site](https://chok.github.io/fougere/) (en/fr).
20
+ Reference documentation: [the site](https://fougere.dev/) (en/fr).
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AAsH1F,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAA4C,MAAM,gBAAgB,CAAC;AAiJ1F,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
package/dist/create.js CHANGED
@@ -5,6 +5,12 @@ function createScope(parent) {
5
5
  // Construction order, so disposal can run in reverse: a thing built later may
6
6
  // hold something built earlier.
7
7
  const built = [];
8
+ // The scopes opened from this one. A child is built BY this container, so it is this
9
+ // container's to close — and it is closed first, because it may hold what the parent
10
+ // built while the parent holds nothing of its. Without this a frond's scope, which is
11
+ // where every provider lives, was never disposed at all: it is registered as a VALUE
12
+ // under `frond:<name>`, and a value is not the container's to dispose.
13
+ const children = [];
8
14
  let fallback;
9
15
  const remember = (value) => {
10
16
  if (isDisposable(value))
@@ -56,12 +62,28 @@ function createScope(parent) {
56
62
  return registry.has(name) || (parent?.has(name) ?? false);
57
63
  },
58
64
  createScope() {
59
- return createScope(container);
65
+ const child = createScope(container);
66
+ children.push(child);
67
+ return child;
60
68
  },
61
69
  async dispose() {
62
70
  // Reverse order, and one failure must not silence the rest: everything gets
63
71
  // told, then the errors travel together.
72
+ // Its parent kept a reference so it could close this scope; the scope closing itself
73
+ // makes that reference garbage. Nothing created a scope at RUN time until frames did,
74
+ // so the list only ever grew at boot and stayed bounded — one per request, or one per
75
+ // transaction, and it grows for the life of the process.
76
+ parent?._forget(container);
64
77
  const failures = [];
78
+ for (const child of children.reverse()) {
79
+ try {
80
+ await child.dispose();
81
+ }
82
+ catch (error) {
83
+ failures.push(error);
84
+ }
85
+ }
86
+ children.length = 0;
65
87
  for (const value of built.reverse()) {
66
88
  try {
67
89
  await value.dispose();
@@ -82,6 +104,11 @@ function createScope(parent) {
82
104
  _getEntry(name) {
83
105
  return registry.get(name) ?? parent?._getEntry(name);
84
106
  },
107
+ _forget(child) {
108
+ const at = children.indexOf(child);
109
+ if (at !== -1)
110
+ children.splice(at, 1);
111
+ },
85
112
  /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
86
113
  _getFallback() {
87
114
  return fallback ?? parent?._getFallback();
@@ -1 +1 @@
1
- {"version":3,"file":"create.js","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"create.js","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/container",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.4.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",
@@ -26,7 +26,8 @@
26
26
  }
27
27
  },
28
28
  "files": [
29
- "dist"
29
+ "dist",
30
+ "src"
30
31
  ],
31
32
  "devDependencies": {
32
33
  "vitest": "^4.1.0"
@@ -0,0 +1,92 @@
1
+ /**
2
+ * A class constructor with any arguments.
3
+ */
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ export type Constructor<T = unknown> = new (...args: any[]) => T;
6
+
7
+ /**
8
+ * Registration options.
9
+ */
10
+ export interface RegisterOptions {
11
+ /**
12
+ * Lifetime of the resolved value. Default: 'transient'.
13
+ *
14
+ * Two words, because two are used: `Config` is a singleton, every handler,
15
+ * presenter, collector and provider is transient — a fresh instance per
16
+ * resolution, which is what keeps one call's state out of the next. A third,
17
+ * `'scoped'`, was declared and never passed by any caller: {@link Container.createScope}
18
+ * already answers "one instance per frond, per surface", and two mechanisms for
19
+ * one need is one too many.
20
+ */
21
+ lifetime?: 'singleton' | 'transient';
22
+ /** Dependency type names for type-based resolution (from AST scan). */
23
+ deps?: string[];
24
+ }
25
+
26
+ /**
27
+ * Anything holding a resource can say so, and disposing the container says it back.
28
+ *
29
+ * Not an interface a class implements — a shape a class happens to have. A handler
30
+ * that opens nothing declares nothing.
31
+ */
32
+ export interface Disposable {
33
+ dispose(): void | Promise<void>;
34
+ }
35
+
36
+ /**
37
+ * DI container interface — the only thing application code sees.
38
+ *
39
+ * It is deliberately small, and the reason is that the scan already knows the graph:
40
+ * every class and every dependency is read from source before boot, so there is
41
+ * almost nothing left to bind late. What remains is a Map with a parent chain, plus
42
+ * the two gestures the scan cannot cover — {@link createScope} for isolation, and
43
+ * {@link setFallback} for a frond that lives in another process.
44
+ *
45
+ * Resolution is by name, and the names are TYPE names produced by the AST scan.
46
+ * The container never sees a type; "DI by type" is realized upstream, in the
47
+ * scanner, which is why this file mentions neither.
48
+ */
49
+ export interface Container {
50
+ /** Register a class by name. Its `deps` are resolved from this container. */
51
+ register<T>(name: string, ctor: Constructor<T>, options?: RegisterOptions): void;
52
+
53
+ /** Register a pre-built value by name. */
54
+ registerValue<T>(name: string, value: T): void;
55
+
56
+ /** Resolve a dependency by name. */
57
+ resolve<T>(name: string): T;
58
+
59
+ /** Check if a name is registered (including parent scopes). */
60
+ has(name: string): boolean;
61
+
62
+ /**
63
+ * A resolver of last resort, consulted when no scope holds the name.
64
+ *
65
+ * It exists for one reason: a frond declared in `remotes` registers nothing locally,
66
+ * so its façade cannot be *found* — it has to be fabricated. Set on the root, inherited
67
+ * by every scope. Returning `undefined` means "I don't know either", and the miss
68
+ * throws as before.
69
+ *
70
+ * Optional: a container without one behaves exactly as it did.
71
+ */
72
+ setFallback?(resolve: (name: string) => unknown): void;
73
+
74
+ /** Create a child scope. Inherits parent registrations. */
75
+ createScope(): Container;
76
+
77
+ /**
78
+ * Dispose the container: every instance it KEPT — the singletons — that has a
79
+ * `dispose` method is told, most recent first, and awaited. A failure does not
80
+ * silence the rest; they travel out together in an `AggregateError`.
81
+ *
82
+ * This used to clear the registry and nothing else, while the doc above it promised
83
+ * disposal — so `await using app` (which routes here through `app[Symbol.asyncDispose]`)
84
+ * announced a cleanup that never happened. A container that holds instances is the
85
+ * only thing that knows they exist; if it stays silent, nobody else can speak.
86
+ *
87
+ * What it does NOT dispose: a transient, handed over and forgotten in the same
88
+ * breath, whose caller is the one who knows when it is done; and a value passed to
89
+ * {@link registerValue}, which the container did not build.
90
+ */
91
+ dispose(): Promise<void>;
92
+ }
package/src/create.ts ADDED
@@ -0,0 +1,148 @@
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
+ // Construction order, so disposal can run in reverse: a thing built later may
22
+ // hold something built earlier.
23
+ const built: unknown[] = [];
24
+ // The scopes opened from this one. A child is built BY this container, so it is this
25
+ // container's to close — and it is closed first, because it may hold what the parent
26
+ // built while the parent holds nothing of its. Without this a frond's scope, which is
27
+ // where every provider lives, was never disposed at all: it is registered as a VALUE
28
+ // under `frond:<name>`, and a value is not the container's to dispose.
29
+ const children: ScopeContainer[] = [];
30
+ let fallback: ((name: string) => unknown) | undefined;
31
+
32
+ const remember = <T>(value: T): T => {
33
+ if (isDisposable(value)) built.push(value);
34
+ return value;
35
+ };
36
+
37
+ const container: ScopeContainer = {
38
+ register<T>(name: string, ctor: Constructor<T>, options?: RegisterOptions) {
39
+ const lifetime = options?.lifetime ?? 'transient';
40
+ const deps = options?.deps ?? [];
41
+ registry.set(name, {
42
+ factory: (c) => new ctor(...deps.map((d) => c.resolve(d))),
43
+ lifetime,
44
+ });
45
+ },
46
+
47
+ registerValue<T>(name: string, value: T) {
48
+ // A value the container did not build is not the container's to dispose.
49
+ registry.set(name, { factory: () => value, lifetime: 'singleton', instance: value });
50
+ },
51
+
52
+ resolve<T>(name: string): T {
53
+ let entry = registry.get(name);
54
+
55
+ // Not found locally — the parent holds it, and holds its instance too.
56
+ if (!entry && parent && parent._getEntry(name)) {
57
+ return parent.resolve<T>(name);
58
+ }
59
+
60
+ // Nobody holds it. Before failing, ask whoever set a last resort — a frond declared
61
+ // in `remotes` registers nothing here, so its façade is fabricated rather than found.
62
+ if (!entry) {
63
+ const made = container._getFallback()?.(name);
64
+ if (made !== undefined) {
65
+ registry.set(name, { factory: () => made, lifetime: 'singleton', instance: made });
66
+ return made as T;
67
+ }
68
+ throw new Error(`[container] '${name}' is not registered`);
69
+ }
70
+
71
+ if (entry.instance !== undefined) return entry.instance as T;
72
+ const value = entry.factory(container) as T;
73
+ // The container disposes what it KEEPS. A transient is handed over and
74
+ // forgotten in the same breath — remembering it would be a leak that grows
75
+ // once per call, and its caller is the one who knows when it is done.
76
+ if (entry.lifetime === 'singleton') {
77
+ entry.instance = value;
78
+ remember(value);
79
+ }
80
+ return value;
81
+ },
82
+
83
+ has(name: string): boolean {
84
+ return registry.has(name) || (parent?.has(name) ?? false);
85
+ },
86
+
87
+ createScope(): Container {
88
+ const child = createScope(container);
89
+ children.push(child);
90
+ return child;
91
+ },
92
+
93
+ async dispose(): Promise<void> {
94
+ // Reverse order, and one failure must not silence the rest: everything gets
95
+ // told, then the errors travel together.
96
+ // Its parent kept a reference so it could close this scope; the scope closing itself
97
+ // makes that reference garbage. Nothing created a scope at RUN time until frames did,
98
+ // so the list only ever grew at boot and stayed bounded — one per request, or one per
99
+ // transaction, and it grows for the life of the process.
100
+ parent?._forget(container);
101
+ const failures: unknown[] = [];
102
+ for (const child of children.reverse()) {
103
+ try {
104
+ await child.dispose();
105
+ } catch (error) {
106
+ failures.push(error);
107
+ }
108
+ }
109
+ children.length = 0;
110
+ for (const value of built.reverse()) {
111
+ try {
112
+ await (value as Disposable).dispose();
113
+ } catch (error) {
114
+ failures.push(error);
115
+ }
116
+ }
117
+ built.length = 0;
118
+ registry.clear();
119
+ if (failures.length > 0) {
120
+ throw new AggregateError(failures, '[container] one or more disposals failed');
121
+ }
122
+ },
123
+
124
+ setFallback(resolve: (name: string) => unknown) {
125
+ fallback = resolve;
126
+ },
127
+
128
+ _getEntry(name: string): Entry | undefined {
129
+ return registry.get(name) ?? parent?._getEntry(name);
130
+ },
131
+
132
+ _forget(child: ScopeContainer) {
133
+ const at = children.indexOf(child);
134
+ if (at !== -1) children.splice(at, 1);
135
+ },
136
+
137
+ /** Set on the root, honoured from any scope — a scope inherits it by asking upward. */
138
+ _getFallback() {
139
+ return fallback ?? parent?._getFallback();
140
+ },
141
+ };
142
+
143
+ return container;
144
+ }
145
+
146
+ export function createContainer(): Container {
147
+ return createScope();
148
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export {
2
+ type Container,
3
+ type RegisterOptions,
4
+ type Constructor,
5
+ type Disposable,
6
+ } from './container.js';
7
+ export { createContainer } from './create.js';