@stitchem/core 0.0.3 → 0.0.7

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.
@@ -170,12 +170,12 @@ export declare class Context implements AsyncDisposable {
170
170
  private resolveSingletons;
171
171
  /** Instantiates component classes from registered component keys. */
172
172
  private resolveComponents;
173
- /** Calls onReady() on all singleton providers, module classes, and components. */
174
- private fireOnReady;
175
173
  /** Disposes injectables in reverse module order, before component disposal. */
176
174
  private disposeInjectables;
177
175
  /** Disposes components in reverse module order, before provider disposal. */
178
176
  private disposeComponents;
177
+ /** Calls onDispose() on module class instances in reverse module order. */
178
+ private disposeModuleInstances;
179
179
  private ensureInitialized;
180
180
  }
181
181
  //# sourceMappingURL=context.d.ts.map
@@ -57,7 +57,7 @@ import { ModuleGraph } from '../module/module.graph.js';
57
57
  import { ModuleRef } from '../module/module.ref.js';
58
58
  import { Injector } from '../injector/injector.js';
59
59
  import { CoreError } from '../errors/core.error.js';
60
- import { hasOnReady } from '../core/core.lifecycle.js';
60
+ import { hasOnDispose } from '../core/core.lifecycle.js';
61
61
  import { ConsoleLogger, NoopLogger } from '../logger/console.logger.js';
62
62
  import { LOGGER } from '../logger/logger.token.js';
63
63
  import { Scope } from './scope.js';
@@ -163,9 +163,6 @@ export class Context {
163
163
  return cached;
164
164
  const scope = Scope.current() ?? Scope.STATIC;
165
165
  const instance = await this.injector.instantiateClassGlobal(ctor, scope);
166
- if (hasOnReady(instance)) {
167
- await instance.onReady();
168
- }
169
166
  this.rootModule.setInjectable(ctor, instance);
170
167
  return instance;
171
168
  }
@@ -263,6 +260,7 @@ export class Context {
263
260
  }
264
261
  await this.container.dispose(scope);
265
262
  if (!scope) {
263
+ await this.disposeModuleInstances();
266
264
  this.initialized = false;
267
265
  }
268
266
  }
@@ -274,7 +272,6 @@ export class Context {
274
272
  this.container.buildGlobalExports();
275
273
  await this.resolveSingletons();
276
274
  await this.resolveComponents();
277
- await this.fireOnReady();
278
275
  this.initialized = true;
279
276
  }
280
277
  /** Registers a ModuleRef factory provider for each module. */
@@ -346,30 +343,6 @@ export class Context {
346
343
  }
347
344
  }
348
345
  }
349
- /** Calls onReady() on all singleton providers, module classes, and components. */
350
- async fireOnReady() {
351
- for (const id of this.moduleGraph.sortedIds) {
352
- const module = this.container.getModule(id);
353
- for (const [, wrapper] of module.providers) {
354
- if (!wrapper.isSingleton)
355
- continue;
356
- const instance = wrapper.getInstance(Scope.STATIC);
357
- if (instance !== undefined && hasOnReady(instance)) {
358
- await instance.onReady();
359
- }
360
- }
361
- if (module.instance !== undefined && hasOnReady(module.instance)) {
362
- await module.instance.onReady();
363
- }
364
- for (const key of this.componentKeys) {
365
- for (const entry of module.getComponents(key)) {
366
- if (hasOnReady(entry.instance)) {
367
- await entry.instance.onReady();
368
- }
369
- }
370
- }
371
- }
372
- }
373
346
  /** Disposes injectables in reverse module order, before component disposal. */
374
347
  async disposeInjectables() {
375
348
  const moduleIds = [...this.moduleGraph.sortedIds].reverse();
@@ -386,6 +359,16 @@ export class Context {
386
359
  await module.disposeComponents();
387
360
  }
388
361
  }
362
+ /** Calls onDispose() on module class instances in reverse module order. */
363
+ async disposeModuleInstances() {
364
+ const moduleIds = [...this.moduleGraph.sortedIds].reverse();
365
+ for (const id of moduleIds) {
366
+ const module = this.container.getModule(id);
367
+ if (module.instance !== undefined && hasOnDispose(module.instance)) {
368
+ await module.instance.onDispose();
369
+ }
370
+ }
371
+ }
389
372
  ensureInitialized() {
390
373
  if (!this.initialized) {
391
374
  throw new Error('Context is not initialized. Call Context.create() first.');
@@ -10,13 +10,6 @@ export interface OnInit {
10
10
  export interface OnDispose {
11
11
  onDispose(): void | Promise<void>;
12
12
  }
13
- /**
14
- * Interface for providers that need to run logic after all modules are initialized.
15
- * Fires after every singleton provider has been created and onInit() has completed.
16
- */
17
- export interface OnReady {
18
- onReady(): void | Promise<void>;
19
- }
20
13
  /**
21
14
  * Type guard for OnInit.
22
15
  *
@@ -31,11 +24,4 @@ export declare function hasOnInit(instance: unknown): instance is OnInit;
31
24
  * @returns True if the instance implements the OnDispose interface.
32
25
  */
33
26
  export declare function hasOnDispose(instance: unknown): instance is OnDispose;
34
- /**
35
- * Type guard for OnReady.
36
- *
37
- * @param instance - The value to check for OnReady conformance.
38
- * @returns True if the instance implements the OnReady interface.
39
- */
40
- export declare function hasOnReady(instance: unknown): instance is OnReady;
41
27
  //# sourceMappingURL=core.lifecycle.d.ts.map
@@ -22,16 +22,4 @@ export function hasOnDispose(instance) {
22
22
  'onDispose' in instance &&
23
23
  typeof instance.onDispose === 'function');
24
24
  }
25
- /**
26
- * Type guard for OnReady.
27
- *
28
- * @param instance - The value to check for OnReady conformance.
29
- * @returns True if the instance implements the OnReady interface.
30
- */
31
- export function hasOnReady(instance) {
32
- return (typeof instance === 'object' &&
33
- instance !== null &&
34
- 'onReady' in instance &&
35
- typeof instance.onReady === 'function');
36
- }
37
25
  //# sourceMappingURL=core.lifecycle.js.map
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export type { Token } from './token/token.types.js';
6
6
  export { Lifetime } from './core/core.lifetime.js';
7
7
  export { lazy } from './token/lazy.token.js';
8
8
  export { isConstructor } from './core/core.utils.js';
9
- export type { OnInit, OnDispose, OnReady } from './core/core.lifecycle.js';
9
+ export type { OnInit, OnDispose } from './core/core.lifecycle.js';
10
10
  export type { Provider, ClassProvider, ValueProvider, FactoryProvider, ExistingProvider, } from './provider/provider.interface.js';
11
11
  export type { ModuleMetadata, DynamicModule, ModuleImport, } from './module/module.types.js';
12
12
  export { Context } from './context/context.js';
@@ -10,8 +10,15 @@ import { Container } from '../container/container.js';
10
10
  */
11
11
  export declare class Injector {
12
12
  private readonly container;
13
- /** Tracks tokens currently being resolved (circular dependency detection). */
14
- private readonly resolutionStack;
13
+ /**
14
+ * Per-scope resolution stack for circular dependency detection.
15
+ * Each scope (e.g. per HTTP request) gets its own stack, preventing
16
+ * false circular dependency errors under concurrency.
17
+ * WeakMap ensures stacks are GC'd when their scope is disposed.
18
+ */
19
+ private readonly resolutionStacks;
20
+ /** Returns or creates the resolution stack for the given scope. */
21
+ private getResolutionStack;
15
22
  constructor(container: Container);
16
23
  /**
17
24
  * Resolves an instance from a wrapper (async).
@@ -12,8 +12,22 @@ import { isValueProvider, isFactoryProvider, isExistingProvider, isClassProvider
12
12
  */
13
13
  export class Injector {
14
14
  container;
15
- /** Tracks tokens currently being resolved (circular dependency detection). */
16
- resolutionStack = new Set();
15
+ /**
16
+ * Per-scope resolution stack for circular dependency detection.
17
+ * Each scope (e.g. per HTTP request) gets its own stack, preventing
18
+ * false circular dependency errors under concurrency.
19
+ * WeakMap ensures stacks are GC'd when their scope is disposed.
20
+ */
21
+ resolutionStacks = new WeakMap();
22
+ /** Returns or creates the resolution stack for the given scope. */
23
+ getResolutionStack(scope) {
24
+ let stack = this.resolutionStacks.get(scope);
25
+ if (!stack) {
26
+ stack = new Set();
27
+ this.resolutionStacks.set(scope, stack);
28
+ }
29
+ return stack;
30
+ }
17
31
  constructor(container) {
18
32
  this.container = container;
19
33
  }
@@ -28,7 +42,7 @@ export class Injector {
28
42
  const cached = this.guardResolution(wrapper, scope);
29
43
  if (cached !== null)
30
44
  return cached;
31
- this.resolutionStack.add(wrapper.token);
45
+ this.getResolutionStack(scope).add(wrapper.token);
32
46
  try {
33
47
  const resolver = this.moduleResolver(module);
34
48
  const instance = await this.createInstance(wrapper, resolver, scope);
@@ -39,7 +53,7 @@ export class Injector {
39
53
  return instance;
40
54
  }
41
55
  finally {
42
- this.resolutionStack.delete(wrapper.token);
56
+ this.getResolutionStack(scope).delete(wrapper.token);
43
57
  }
44
58
  }
45
59
  /**
@@ -53,7 +67,7 @@ export class Injector {
53
67
  const cached = this.guardResolution(wrapper, scope);
54
68
  if (cached !== null)
55
69
  return cached;
56
- this.resolutionStack.add(wrapper.token);
70
+ this.getResolutionStack(scope).add(wrapper.token);
57
71
  try {
58
72
  const resolver = this.moduleResolver(module);
59
73
  const instance = this.createInstanceSync(wrapper, resolver, scope);
@@ -67,7 +81,7 @@ export class Injector {
67
81
  return instance;
68
82
  }
69
83
  finally {
70
- this.resolutionStack.delete(wrapper.token);
84
+ this.getResolutionStack(scope).delete(wrapper.token);
71
85
  }
72
86
  }
73
87
  /**
@@ -77,13 +91,13 @@ export class Injector {
77
91
  * @throws CoreError (CIRCULAR_DEPENDENCY) if a circular dependency is detected
78
92
  */
79
93
  async createUncached(wrapper, module, scope = Scope.STATIC) {
80
- if (this.resolutionStack.has(wrapper.token)) {
94
+ if (this.getResolutionStack(scope).has(wrapper.token)) {
81
95
  throw CoreError.circularDependency([
82
- ...this.resolutionStack,
96
+ ...this.getResolutionStack(scope),
83
97
  wrapper.token,
84
98
  ]);
85
99
  }
86
- this.resolutionStack.add(wrapper.token);
100
+ this.getResolutionStack(scope).add(wrapper.token);
87
101
  try {
88
102
  const resolver = this.moduleResolver(module);
89
103
  const instance = await this.createInstance(wrapper, resolver, scope);
@@ -93,7 +107,7 @@ export class Injector {
93
107
  return instance;
94
108
  }
95
109
  finally {
96
- this.resolutionStack.delete(wrapper.token);
110
+ this.getResolutionStack(scope).delete(wrapper.token);
97
111
  }
98
112
  }
99
113
  /**
@@ -142,9 +156,9 @@ export class Injector {
142
156
  if (wrapper.isResolved(scope)) {
143
157
  return wrapper.getInstance(scope);
144
158
  }
145
- if (this.resolutionStack.has(wrapper.token)) {
159
+ if (this.getResolutionStack(scope).has(wrapper.token)) {
146
160
  throw CoreError.circularDependency([
147
- ...this.resolutionStack,
161
+ ...this.getResolutionStack(scope),
148
162
  wrapper.token,
149
163
  ]);
150
164
  }
@@ -1,5 +1,4 @@
1
1
  import { Scope } from '../context/scope.js';
2
- import { hasOnReady } from '../core/core.lifecycle.js';
3
2
  import { CoreError } from '../errors/core.error.js';
4
3
  /**
5
4
  * Reference to a module, injectable into providers.
@@ -47,14 +46,16 @@ export class ModuleRef {
47
46
  * Resolves a dependency from this module's context.
48
47
  * Respects module visibility (can only resolve visible tokens).
49
48
  */
50
- async resolve(token, scope = Scope.STATIC) {
49
+ async resolve(token, scope) {
50
+ const resolvedScope = scope ?? Scope.current() ?? Scope.STATIC;
51
51
  const { wrapper, module } = this.container.getProviderByToken(token, this.module);
52
- return this.injector.loadInstance(wrapper, module, scope);
52
+ return this.injector.loadInstance(wrapper, module, resolvedScope);
53
53
  }
54
54
  /** Synchronously resolves a dependency from this module's context. */
55
- resolveSync(token, scope = Scope.STATIC) {
55
+ resolveSync(token, scope) {
56
+ const resolvedScope = scope ?? Scope.current() ?? Scope.STATIC;
56
57
  const { wrapper, module } = this.container.getProviderByToken(token, this.module);
57
- return this.injector.loadInstanceSync(wrapper, module, scope);
58
+ return this.injector.loadInstanceSync(wrapper, module, resolvedScope);
58
59
  }
59
60
  /**
60
61
  * Creates a new instance of a registered provider, bypassing cache.
@@ -62,10 +63,11 @@ export class ModuleRef {
62
63
  *
63
64
  * @throws CoreError (PROVIDER_NOT_FOUND) if the class is not registered
64
65
  */
65
- async create(ctor, scope = Scope.STATIC) {
66
+ async create(ctor, scope) {
67
+ const resolvedScope = scope ?? Scope.current() ?? Scope.STATIC;
66
68
  const wrapper = this.module.getProvider(ctor);
67
69
  if (wrapper) {
68
- return this.injector.createUncached(wrapper, this.module, scope);
70
+ return this.injector.createUncached(wrapper, this.module, resolvedScope);
69
71
  }
70
72
  throw CoreError.providerNotFound(ctor, this.module.id);
71
73
  }
@@ -73,8 +75,9 @@ export class ModuleRef {
73
75
  * Instantiates any class by resolving its dependencies.
74
76
  * The class does not need to be registered as a provider.
75
77
  */
76
- async constructClass(ctor, scope = Scope.STATIC) {
77
- return this.injector.instantiateClass(ctor, this.module, scope);
78
+ async constructClass(ctor, scope) {
79
+ const resolvedScope = scope ?? Scope.current() ?? Scope.STATIC;
80
+ return this.injector.instantiateClass(ctor, this.module, resolvedScope);
78
81
  }
79
82
  /**
80
83
  * Resolves an injectable class instance with caching and lifecycle hooks.
@@ -97,9 +100,6 @@ export class ModuleRef {
97
100
  const instance = await (options?.strict === false
98
101
  ? this.injector.instantiateClassGlobal(ctor, scope)
99
102
  : this.injector.instantiateClass(ctor, this.module, scope));
100
- if (hasOnReady(instance)) {
101
- await instance.onReady();
102
- }
103
103
  this.module.setInjectable(ctor, instance);
104
104
  return instance;
105
105
  }
@@ -5,7 +5,7 @@ import { TestModuleBuilder } from './test.module-builder.js';
5
5
  *
6
6
  * @example
7
7
  * ```ts
8
- * const module = await Test.createModule({
8
+ * await using module = await Test.createModule({
9
9
  * imports: [UserModule],
10
10
  * providers: [UserService],
11
11
  * })
@@ -13,7 +13,6 @@ import { TestModuleBuilder } from './test.module-builder.js';
13
13
  * .compile();
14
14
  *
15
15
  * const userService = await module.resolve(UserService);
16
- * await module.close();
17
16
  * ```
18
17
  */
19
18
  export declare class Test {
package/dist/test/test.js CHANGED
@@ -4,7 +4,7 @@ import { TestModuleBuilder } from './test.module-builder.js';
4
4
  *
5
5
  * @example
6
6
  * ```ts
7
- * const module = await Test.createModule({
7
+ * await using module = await Test.createModule({
8
8
  * imports: [UserModule],
9
9
  * providers: [UserService],
10
10
  * })
@@ -12,7 +12,6 @@ import { TestModuleBuilder } from './test.module-builder.js';
12
12
  * .compile();
13
13
  *
14
14
  * const userService = await module.resolve(UserService);
15
- * await module.close();
16
15
  * ```
17
16
  */
18
17
  export class Test {
@@ -38,7 +38,6 @@ import { ModuleGraph } from '../module/module.graph.js';
38
38
  import { ModuleRef } from '../module/module.ref.js';
39
39
  import { Injector } from '../injector/injector.js';
40
40
  import { Scope } from '../context/scope.js';
41
- import { hasOnReady } from '../core/core.lifecycle.js';
42
41
  import { getProviderToken } from '../provider/provider.guards.js';
43
42
  import { TestModule } from './test.module.js';
44
43
  // ---------------------------------------------------------------------------
@@ -178,9 +177,6 @@ export class TestModuleBuilder {
178
177
  const result = override.factory(...deps);
179
178
  instance = result instanceof Promise ? await result : result;
180
179
  }
181
- if (hasOnReady(instance)) {
182
- await instance.onReady();
183
- }
184
180
  for (const id of moduleGraph.sortedIds) {
185
181
  const mod = container.getModule(id);
186
182
  mod.setInjectable(override.ctor, instance);
@@ -215,28 +211,6 @@ export class TestModuleBuilder {
215
211
  }
216
212
  }
217
213
  }
218
- // Fire onReady lifecycle.
219
- for (const id of moduleGraph.sortedIds) {
220
- const mod = container.getModule(id);
221
- for (const [, wrapper] of mod.providers) {
222
- if (!wrapper.isSingleton)
223
- continue;
224
- const instance = wrapper.getInstance(Scope.STATIC);
225
- if (instance !== undefined && hasOnReady(instance)) {
226
- await instance.onReady();
227
- }
228
- }
229
- if (mod.instance !== undefined && hasOnReady(mod.instance)) {
230
- await mod.instance.onReady();
231
- }
232
- for (const key of componentKeys) {
233
- for (const entry of mod.getComponents(key)) {
234
- if (hasOnReady(entry.instance)) {
235
- await entry.instance.onReady();
236
- }
237
- }
238
- }
239
- }
240
214
  return new TestModule(container, injector, rootModule, moduleGraph.sortedIds);
241
215
  }
242
216
  /** @internal */
@@ -61,11 +61,8 @@ export declare class TestModule implements AsyncDisposable {
61
61
  /** Creates a new disposable scope for scoped resolution. */
62
62
  createScope(): Scope;
63
63
  [Symbol.asyncDispose](): Promise<void>;
64
- /**
65
- * Disposes the test module.
66
- * Call in afterEach/afterAll to clean up.
67
- */
68
- close(scope?: Scope): Promise<void>;
64
+ /** Disposes a single scope's instances. */
65
+ private disposeScope;
69
66
  private ensureNotDisposed;
70
67
  }
71
68
  //# sourceMappingURL=test.module.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { Scope } from '../context/scope.js';
2
2
  import { ModuleRef } from '../module/module.ref.js';
3
- import { hasOnReady } from '../core/core.lifecycle.js';
3
+ import { hasOnDispose } from '../core/core.lifecycle.js';
4
4
  /**
5
5
  * A compiled test module.
6
6
  * Provides resolution and lifecycle management for test scenarios.
@@ -83,9 +83,6 @@ export class TestModule {
83
83
  return cached;
84
84
  const scope = Scope.current() ?? Scope.STATIC;
85
85
  const instance = await this.injector.instantiateClassGlobal(ctor, scope);
86
- if (hasOnReady(instance)) {
87
- await instance.onReady();
88
- }
89
86
  this.rootModule.setInjectable(ctor, instance);
90
87
  return instance;
91
88
  }
@@ -111,36 +108,35 @@ export class TestModule {
111
108
  /** Creates a new disposable scope for scoped resolution. */
112
109
  createScope() {
113
110
  this.ensureNotDisposed();
114
- return Scope.create((scope) => this.close(scope));
111
+ return Scope.create((scope) => this.disposeScope(scope));
115
112
  }
116
113
  async [Symbol.asyncDispose]() {
117
- await this.close();
118
- }
119
- /**
120
- * Disposes the test module.
121
- * Call in afterEach/afterAll to clean up.
122
- */
123
- async close(scope) {
124
- if (this.disposed && !scope)
114
+ if (this.disposed)
125
115
  return;
126
- if (!scope) {
127
- // Dispose injectables → components → providers.
128
- const moduleIds = [...this.sortedIds].reverse();
129
- for (const id of moduleIds) {
130
- const module = this.container.getModule(id);
131
- if (module)
132
- await module.disposeInjectables();
133
- }
134
- for (const id of moduleIds) {
135
- const module = this.container.getModule(id);
136
- if (module)
137
- await module.disposeComponents();
116
+ // Dispose injectables → components → providers → module instances.
117
+ const moduleIds = [...this.sortedIds].reverse();
118
+ for (const id of moduleIds) {
119
+ const module = this.container.getModule(id);
120
+ if (module)
121
+ await module.disposeInjectables();
122
+ }
123
+ for (const id of moduleIds) {
124
+ const module = this.container.getModule(id);
125
+ if (module)
126
+ await module.disposeComponents();
127
+ }
128
+ await this.container.dispose();
129
+ for (const id of moduleIds) {
130
+ const module = this.container.getModule(id);
131
+ if (module?.instance !== undefined && hasOnDispose(module.instance)) {
132
+ await module.instance.onDispose();
138
133
  }
139
134
  }
135
+ this.disposed = true;
136
+ }
137
+ /** Disposes a single scope's instances. */
138
+ async disposeScope(scope) {
140
139
  await this.container.dispose(scope);
141
- if (!scope) {
142
- this.disposed = true;
143
- }
144
140
  }
145
141
  ensureNotDisposed() {
146
142
  if (this.disposed) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stitchem/core",
3
- "version": "0.0.3",
4
- "description": "Extensible modular dependency injection for TypeScript",
3
+ "version": "0.0.7",
4
+ "description": "",
5
5
  "author": {
6
6
  "name": "hexac",
7
7
  "url": "https://existin.space",