di-craft 0.0.10 → 0.0.11

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
@@ -2,6 +2,31 @@
2
2
 
3
3
  A tiny, type-safe dependency injection container for TypeScript.
4
4
 
5
+ ## Contents
6
+
7
+ - [Quick start](#quick-start)
8
+ - [Philosophy](#philosophy)
9
+ - [Features](#features)
10
+ - [Install](#install)
11
+ - [Core concepts](#core-concepts)
12
+ - [Tokens](#tokens)
13
+ - [Providers](#providers)
14
+ - [Container](#container)
15
+ - [Scopes](#scopes)
16
+ - [Disposal](#disposal)
17
+ - [Child containers](#child-containers)
18
+ - [Cycle detection](#cycle-detection)
19
+ - [Example: per-request container](#example-per-request-container)
20
+ - [Error handling](#error-handling)
21
+ - [API reference](#api-reference)
22
+ - [License](#license)
23
+
24
+ ## Quick start
25
+
26
+ Declare typed tokens, describe how each one is built with a provider, then create
27
+ a container and resolve from it. Dependencies are wired explicitly through the
28
+ `deps` map and resolved for you.
29
+
5
30
  ```ts
6
31
  import {
7
32
  createContainer,
@@ -34,23 +59,17 @@ const users = container.get(USERS); // UserService, fully typed
34
59
 
35
60
  ## Philosophy
36
61
 
37
- `di-craft` is a small DI container with no magic.
38
-
39
- - No decorators
40
- - No `reflect-metadata`
41
- - No framework dependencies
42
-
43
- Just **tokens**, **providers**, a **container**, **scopes**, and **cycle detection**.
62
+ Dependency injection without the magic no decorators, no `reflect-metadata`, no
63
+ framework coupling. You work with just **tokens**, **providers**, a **container**,
64
+ **scopes**, and **cycle detection**.
44
65
 
45
66
  ## Features
46
67
 
47
68
  - Zero runtime dependencies
48
- - No decorators
49
- - No `reflect-metadata`
50
- - Framework agnostic
51
- - Type-safe tokens
52
- - Explicit factories
53
- - Singleton and transient scopes
69
+ - Type-safe tokens and factories
70
+ - Singleton, transient, and scoped lifetimes
71
+ - Hierarchical child containers
72
+ - Deterministic disposal with `onDispose` hooks
54
73
  - Circular dependency detection
55
74
  - Tree-shakable, tiny bundle size
56
75
  - Ships both ESM and CommonJS builds
@@ -104,15 +123,34 @@ The keys in `deps` become the keys of the object passed to `useFactory`, each re
104
123
 
105
124
  ### Container
106
125
 
126
+ The container holds your providers and resolves values on demand. Create one
127
+ from a list of providers (all optional), then add more, check, resolve, and
128
+ dispose:
129
+
130
+ - `register(provider, options?)` — add a provider at any time.
131
+ - `has(token)` — whether a provider for the token is registered.
132
+ - `get(token)` — resolve the value, building and caching it as its scope dictates.
133
+ - `dispose()` — run `onDispose` hooks and release resolved instances.
134
+
107
135
  ```ts
108
136
  const container = createContainer(providers); // providers are optional
109
137
 
110
138
  container.register(provideValue(PORT, 3000)); // register more at any time
111
139
  container.has(PORT); // true
112
140
  container.get(PORT); // 3000
141
+ await container.dispose(); // release resolved singletons
142
+ ```
143
+
144
+ Registering the same token twice throws `DuplicateProviderError`. To replace an
145
+ existing provider on purpose (handy for tests, mocks, and environment-specific
146
+ overrides), pass `{ allowOverride: true }`:
147
+
148
+ ```ts
149
+ container.register(provideValue(API, fakeApi), { allowOverride: true });
113
150
  ```
114
151
 
115
- Registering the same token twice throws `DuplicateProviderError`.
152
+ Overriding a token whose value was already resolved as a singleton drops the
153
+ cached instance, so the next `get` rebuilds it from the new provider.
116
154
 
117
155
  ### Scopes
118
156
 
@@ -120,10 +158,15 @@ Registering the same token twice throws `DuplicateProviderError`.
120
158
  | ---------------------- | ---------------------------------------------------------------- |
121
159
  | `singleton` (default) | The factory runs once; the same instance is returned every time. |
122
160
  | `transient` | The factory runs on every `get`, producing a fresh instance. |
161
+ | `scoped` | One instance per container. In a child container each child gets its own instance, while the provider can still be declared once on the parent. |
162
+
163
+ Use the `Scopes` helper for autocompletion, or pass the plain string — both work:
123
164
 
124
165
  ```ts
166
+ import { Scopes, provideFactory } from "di-craft";
167
+
125
168
  provideFactory(ID, {
126
- scope: "transient",
169
+ scope: Scopes.Transient, // or scope: "transient"
127
170
  useFactory: () => crypto.randomUUID(),
128
171
  });
129
172
 
@@ -132,6 +175,69 @@ container.get(ID) !== container.get(ID); // true
132
175
 
133
176
  A transient provider that depends on a singleton still reuses the shared singleton instance.
134
177
 
178
+ ### Disposal
179
+
180
+ Factory providers can declare an `onDispose` hook to release resources (database
181
+ pools, sockets, timers, subscriptions). Calling `container.dispose()` runs the
182
+ hooks for every resolved singleton and clears the cache:
183
+
184
+ ```ts
185
+ const DB = createToken<Pool>("db");
186
+
187
+ const container = createContainer([
188
+ provideFactory(DB, {
189
+ useFactory: () => createPool(url),
190
+ onDispose: (pool) => pool.end(), // may be sync or async
191
+ }),
192
+ ]);
193
+
194
+ container.get(DB);
195
+
196
+ await container.dispose(); // awaits async hooks, then clears instances
197
+ ```
198
+
199
+ Details:
200
+
201
+ - Hooks run in reverse creation order (dependents before their dependencies).
202
+ - `dispose()` returns a promise and awaits async hooks.
203
+ - It is idempotent — calling it again is a no-op.
204
+ - Only resolved singletons are disposed; transient and never-resolved instances are not tracked.
205
+
206
+ ### Child containers
207
+
208
+ `createChildContainer(parent, providers?)` creates a child that inherits
209
+ everything from its parent but can add or override providers locally. This is the
210
+ typical pattern for per-request isolation on a server: shared services live on
211
+ the root, request-specific values live on a short-lived child.
212
+
213
+ ```ts
214
+ const root = createContainer([
215
+ provideFactory(LOGGER, { useFactory: () => console }), // singleton, shared
216
+ provideFactory(HANDLER, {
217
+ scope: Scopes.Scoped, // one instance per child
218
+ deps: { request: REQUEST },
219
+ useFactory: ({ request }) => createHandler(request),
220
+ }),
221
+ ]);
222
+
223
+ function handle(request: Request) {
224
+ const child = createChildContainer(root, [provideValue(REQUEST, request)]);
225
+
226
+ child.get(LOGGER); // same logger as the root and every other child
227
+ child.get(HANDLER); // a fresh handler, unique to this child
228
+
229
+ return child.dispose(); // release only this child's instances
230
+ }
231
+ ```
232
+
233
+ How resolution works across the chain:
234
+
235
+ - A token is looked up in the child first, then walks up to the parent.
236
+ - `singleton` is cached on the container that **owns** the provider, so it is shared by the whole subtree.
237
+ - `scoped` is cached on the **requesting** child, so each child gets its own instance — even when the provider is declared once on the parent.
238
+ - A `scoped` provider resolves its dependencies from the requesting child, so it can depend on values registered only in that child (like `REQUEST`).
239
+ - `dispose()` only releases the container it is called on; it does not cascade to parents or children.
240
+
135
241
  ### Cycle detection
136
242
 
137
243
  If providers form a dependency cycle, resolution throws `CircularDependencyError` with the full path instead of overflowing the stack.
@@ -141,6 +247,87 @@ If providers form a dependency cycle, resolution throws `CircularDependencyError
141
247
  container.get(A); // throws: Circular dependency detected: A -> B -> A
142
248
  ```
143
249
 
250
+ ## Example: per-request container
251
+
252
+ A common server pattern: build one **root** container at startup with the
253
+ long-lived singletons (a database pool, clients, config), then fork a
254
+ short-lived **child** per request for request-specific state. `scoped` providers
255
+ give each request its own instance, and `dispose()` releases them once the
256
+ response is sent.
257
+
258
+ ```ts
259
+ import Fastify, { type FastifyRequest } from "fastify";
260
+ import {
261
+ type Container,
262
+ createContainer,
263
+ createChildContainer,
264
+ createToken,
265
+ provideValue,
266
+ provideFactory,
267
+ Scopes,
268
+ } from "di-craft";
269
+
270
+ const DB = createToken<Pool>("db");
271
+ const REQUEST = createToken<FastifyRequest>("request");
272
+ const REQUEST_ID = createToken<string>("request-id");
273
+ const USERS = createToken<UserService>("users");
274
+
275
+ const app = Fastify({ logger: true });
276
+
277
+ // Built once, shared by every request.
278
+ const root = createContainer([
279
+ provideFactory(DB, {
280
+ useFactory: () => createPool(process.env.DATABASE_URL),
281
+ onDispose: (pool) => pool.end(), // closed only on shutdown
282
+ }),
283
+ provideFactory(REQUEST_ID, {
284
+ scope: Scopes.Scoped, // one id per request
285
+ deps: { request: REQUEST }, // resolves the value registered on the child
286
+ useFactory: ({ request }) => request.id, // Fastify's built-in request id
287
+ }),
288
+ provideFactory(USERS, {
289
+ scope: Scopes.Scoped, // one service per request...
290
+ deps: { db: DB }, // ...but reuses the shared pool
291
+ useFactory: ({ db }) => new UserService(db),
292
+ }),
293
+ ]);
294
+
295
+ // Keep each request's child container without patching Fastify's types.
296
+ const containers = new WeakMap<FastifyRequest, Container>();
297
+
298
+ app.addHook("onRequest", async (request) => {
299
+ // A fresh child per request, seeded with the request object.
300
+ containers.set(request, createChildContainer(root, [provideValue(REQUEST, request)]));
301
+ });
302
+
303
+ app.addHook("onResponse", async (request) => {
304
+ // Release this request's scoped instances once the response is sent.
305
+ await containers.get(request)?.dispose();
306
+ });
307
+
308
+ app.get<{ Params: { id: string } }>("/users/:id", async (request) => {
309
+ const container = containers.get(request)!;
310
+ const users = container.get(USERS);
311
+ const id = container.get(REQUEST_ID); // unique to this request
312
+
313
+ return users.findById(request.params.id, { traceId: id });
314
+ });
315
+
316
+ // On shutdown, dispose the root to close the shared pool.
317
+ app.addHook("onClose", async () => {
318
+ await root.dispose();
319
+ });
320
+
321
+ await app.listen({ port: 3000 });
322
+ ```
323
+
324
+ What each scope buys you here:
325
+
326
+ - `DB` is a **singleton** — created once on the root and reused by every request, so you don't open a new pool per call.
327
+ - `REQUEST_ID` and `USERS` are **scoped** — a new instance per child, so each request gets isolated state even though the providers are declared once on the root.
328
+ - `REQUEST` is a per-request **value** registered on the child, and the scoped `REQUEST_ID` resolves it from that same child.
329
+ - The child's `dispose()` releases only that request's scoped instances; the shared pool stays open until `root.dispose()` runs on `onClose`.
330
+
144
331
  ## Error handling
145
332
 
146
333
  All errors extend the shared `DiError` base class, so you can catch any container error with a single check:
@@ -176,8 +363,10 @@ try {
176
363
  | `provideValue(token, value)` | Provider that returns an existing value. |
177
364
  | `provideFactory(token, options)` | Provider that builds a value via a factory. |
178
365
  | `createContainer(providers?)` | Create a container, optionally seeded with providers. |
366
+ | `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
367
+ | `Scopes` | Object of scope values (`Scopes.Singleton`, `Scopes.Transient`, `Scopes.Scoped`). |
179
368
 
180
- Exported types: `Container`, `Token`, `Provider`, `ValueProvider`, `FactoryProvider`, `Scope`.
369
+ Exported types: `Container`, `Token`, `Provider`, `ValueProvider`, `FactoryProvider`, `Scope`, `DisposeHook`, `RegisterOptions`.
181
370
 
182
371
  Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`, `CircularDependencyError`, `InvalidDependencyError`.
183
372
 
package/dist/index.cjs CHANGED
@@ -19,8 +19,8 @@ var DuplicateProviderError = class extends DiError {
19
19
  //#region src/registry/registry.ts
20
20
  var RegistryClass = class {
21
21
  providers = /* @__PURE__ */ new Map();
22
- register(provider) {
23
- if (this.providers.has(provider.provide.id)) throw new DuplicateProviderError(provider.provide.name);
22
+ register(provider, options) {
23
+ if (!options?.allowOverride && this.providers.has(provider.provide.id)) throw new DuplicateProviderError(provider.provide.name);
24
24
  this.providers.set(provider.provide.id, provider);
25
25
  }
26
26
  get(token) {
@@ -52,6 +52,13 @@ var CircularDependencyError = class extends DiError {
52
52
  }
53
53
  };
54
54
  //#endregion
55
+ //#region src/scope/scope.ts
56
+ const Scopes = {
57
+ Singleton: "singleton",
58
+ Transient: "transient",
59
+ Scoped: "scoped"
60
+ };
61
+ //#endregion
55
62
  //#region src/provider/provider.ts
56
63
  const provideValue = (token, useValue) => ({
57
64
  provide: token,
@@ -61,8 +68,9 @@ const provideFactory = (token, options) => {
61
68
  return {
62
69
  provide: token,
63
70
  useFactory: options.useFactory,
71
+ scope: options.scope ?? Scopes.Singleton,
64
72
  ...options.deps ? { deps: options.deps } : {},
65
- ...options.scope ? { scope: options.scope } : {}
73
+ ...options.onDispose ? { onDispose: options.onDispose } : {}
66
74
  };
67
75
  };
68
76
  const isValueProvider = (provider) => {
@@ -72,44 +80,101 @@ const isFactoryProvider = (provider) => {
72
80
  return "useFactory" in provider;
73
81
  };
74
82
  //#endregion
83
+ //#region src/store/store.ts
84
+ var StoreClass = class {
85
+ instances = /* @__PURE__ */ new Map();
86
+ get(token) {
87
+ return this.instances.get(token.id);
88
+ }
89
+ set(token, record) {
90
+ this.instances.set(token.id, record);
91
+ }
92
+ delete(token) {
93
+ this.instances.delete(token.id);
94
+ }
95
+ async dispose() {
96
+ const records = [...this.instances.values()].reverse();
97
+ this.instances.clear();
98
+ for (const record of records) if (record.onDispose) await record.onDispose(record.value);
99
+ }
100
+ };
101
+ const createStore = () => new StoreClass();
102
+ //#endregion
103
+ //#region src/resolver/context.ts
104
+ var ResolutionContext = class {
105
+ resolving = /* @__PURE__ */ new Set();
106
+ path = [];
107
+ enter(token) {
108
+ if (this.resolving.has(token.id)) {
109
+ const cycleStartIndex = this.path.findIndex((pathToken) => pathToken.id === token.id);
110
+ throw new CircularDependencyError([...this.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
111
+ }
112
+ this.resolving.add(token.id);
113
+ this.path.push(token);
114
+ }
115
+ exit(token) {
116
+ this.path.pop();
117
+ this.resolving.delete(token.id);
118
+ }
119
+ };
120
+ //#endregion
75
121
  //#region src/resolver/resolver.ts
76
122
  var ResolverClass = class {
77
123
  registry;
78
- instances = /* @__PURE__ */ new Map();
79
- constructor(registry) {
124
+ store = createStore();
125
+ parent;
126
+ constructor(registry, parent) {
80
127
  this.registry = registry;
128
+ this.parent = parent;
81
129
  }
82
130
  resolve(token) {
83
- return this.resolveToken(token, {
84
- resolving: /* @__PURE__ */ new Set(),
85
- path: []
86
- });
131
+ return this.resolveToken(token, new ResolutionContext());
132
+ }
133
+ invalidate(token) {
134
+ this.store.delete(token);
135
+ }
136
+ dispose() {
137
+ return this.store.dispose();
87
138
  }
88
139
  resolveToken(token, context) {
89
- const provider = this.registry.get(token);
140
+ const owner = this.findOwner(token);
141
+ if (!owner) throw new MissingProviderError(token.name);
142
+ const provider = owner.registry.get(token);
90
143
  if (!provider) throw new MissingProviderError(token.name);
91
144
  if (isValueProvider(provider)) return provider.useValue;
92
145
  if (isFactoryProvider(provider)) {
93
- const scope = provider.scope ?? "singleton";
94
- if (scope === "singleton" && this.instances.has(token.id)) return this.instances.get(token.id);
95
- if (context.resolving.has(token.id)) {
96
- const cycleStartIndex = context.path.findIndex((pathToken) => pathToken.id === token.id);
97
- throw new CircularDependencyError([...context.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
146
+ const host = this.selectHost(provider.scope, owner);
147
+ if (host) {
148
+ const cached = host.store.get(token);
149
+ if (cached) return cached.value;
98
150
  }
99
- context.resolving.add(token.id);
100
- context.path.push(token);
151
+ context.enter(token);
101
152
  try {
102
- const deps = this.resolveDeps(provider.deps, context);
153
+ const deps = (host ?? this).resolveDeps(provider.deps, context);
103
154
  const value = provider.useFactory(deps);
104
- if (scope === "singleton") this.instances.set(token.id, value);
155
+ if (host) host.store.set(token, {
156
+ value,
157
+ ...provider.onDispose ? { onDispose: provider.onDispose } : {}
158
+ });
105
159
  return value;
106
160
  } finally {
107
- context.path.pop();
108
- context.resolving.delete(token.id);
161
+ context.exit(token);
109
162
  }
110
163
  }
111
164
  throw new MissingProviderError(token.name);
112
165
  }
166
+ findOwner(token) {
167
+ let resolver = this;
168
+ while (resolver) {
169
+ if (resolver.registry.has(token)) return resolver;
170
+ resolver = resolver.parent;
171
+ }
172
+ }
173
+ selectHost(scope, owner) {
174
+ if (scope === Scopes.Transient) return;
175
+ if (scope === Scopes.Scoped) return this;
176
+ return owner;
177
+ }
113
178
  resolveDeps(deps, context) {
114
179
  if (!deps) return {};
115
180
  const resolvedDeps = {};
@@ -121,28 +186,35 @@ var ResolverClass = class {
121
186
  return resolvedDeps;
122
187
  }
123
188
  };
124
- const createResolver = (registry) => new ResolverClass(registry);
189
+ const createResolver = (registry, parent) => new ResolverClass(registry, parent);
125
190
  //#endregion
126
191
  //#region src/container/container.ts
127
192
  var ContainerClass = class {
128
193
  registry;
129
194
  resolver;
130
- constructor(providers = []) {
195
+ parent;
196
+ constructor(providers = [], parent) {
197
+ this.parent = parent;
131
198
  this.registry = createRegistry();
132
199
  for (const provider of providers) this.registry.register(provider);
133
- this.resolver = createResolver(this.registry);
200
+ this.resolver = createResolver(this.registry, parent?.resolver);
134
201
  }
135
- register(provider) {
136
- this.registry.register(provider);
202
+ register(provider, options) {
203
+ this.registry.register(provider, options);
204
+ if (options?.allowOverride) this.resolver.invalidate(provider.provide);
137
205
  }
138
206
  get(token) {
139
207
  return this.resolver.resolve(token);
140
208
  }
141
209
  has(token) {
142
- return this.registry.has(token);
210
+ return this.registry.has(token) || (this.parent?.has(token) ?? false);
211
+ }
212
+ dispose() {
213
+ return this.resolver.dispose();
143
214
  }
144
215
  };
145
216
  const createContainer = (providers = []) => new ContainerClass(providers);
217
+ const createChildContainer = (parent, providers = []) => new ContainerClass(providers, parent);
146
218
  //#endregion
147
219
  //#region src/token/token.ts
148
220
  var TokenClass = class {
@@ -160,6 +232,8 @@ exports.DiError = DiError;
160
232
  exports.DuplicateProviderError = DuplicateProviderError;
161
233
  exports.InvalidDependencyError = InvalidDependencyError;
162
234
  exports.MissingProviderError = MissingProviderError;
235
+ exports.Scopes = Scopes;
236
+ exports.createChildContainer = createChildContainer;
163
237
  exports.createContainer = createContainer;
164
238
  exports.createToken = createToken;
165
239
  exports.provideFactory = provideFactory;
package/dist/index.d.cts CHANGED
@@ -1,3 +1,11 @@
1
+ //#region src/scope/scope.d.ts
2
+ declare const Scopes: {
3
+ readonly Singleton: "singleton";
4
+ readonly Transient: "transient";
5
+ readonly Scoped: "scoped";
6
+ };
7
+ type Scope = (typeof Scopes)[keyof typeof Scopes];
8
+ //#endregion
1
9
  //#region src/token/types.d.ts
2
10
  type Token<T> = {
3
11
  readonly id: symbol;
@@ -13,6 +21,7 @@ type DepsMap = Record<string, Token<unknown>>;
13
21
  type TokenValue<TToken> = TToken extends Token<infer TValue> ? TValue : never;
14
22
  type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: TokenValue<TDeps[TKey]> };
15
23
  type Factory<T, TDeps extends DepsMap> = (deps: ResolveDeps<TDeps>) => T;
24
+ type DisposeHook<T> = (instance: T) => void | Promise<void>;
16
25
  type ValueProvider<T> = {
17
26
  readonly provide: Token<T>;
18
27
  readonly useValue: T;
@@ -22,10 +31,10 @@ type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
22
31
  readonly deps?: TDeps;
23
32
  readonly scope?: Scope;
24
33
  readonly useFactory: Factory<T, TDeps>;
34
+ readonly onDispose?: DisposeHook<T>;
25
35
  };
26
- type AnyFactoryProvider = FactoryProvider<unknown, any>;
36
+ type AnyFactoryProvider = FactoryProvider<any, any>;
27
37
  type Provider = ValueProvider<unknown> | AnyFactoryProvider;
28
- type Scope = "singleton" | "transient";
29
38
  //#endregion
30
39
  //#region src/provider/provider.d.ts
31
40
  declare const provideValue: <T>(token: Token<T>, useValue: T) => ValueProvider<T>;
@@ -33,18 +42,9 @@ declare const provideFactory: <T, TDeps extends DepsMap = Record<never, never>>(
33
42
  readonly deps?: TDeps;
34
43
  readonly scope?: Scope;
35
44
  readonly useFactory: Factory<T, TDeps>;
45
+ readonly onDispose?: DisposeHook<T>;
36
46
  }) => FactoryProvider<T, TDeps>;
37
47
  //#endregion
38
- //#region src/container/types.d.ts
39
- type Container = {
40
- register(provider: Provider): void;
41
- get<T>(token: Token<T>): T;
42
- has(token: Token<unknown>): boolean;
43
- };
44
- //#endregion
45
- //#region src/container/container.d.ts
46
- declare const createContainer: (providers?: readonly Provider[]) => Container;
47
- //#endregion
48
48
  //#region src/error/error.d.ts
49
49
  declare class DiError extends Error {
50
50
  constructor(message: string);
@@ -55,6 +55,23 @@ declare class DuplicateProviderError extends DiError {
55
55
  constructor(tokenName: string);
56
56
  }
57
57
  //#endregion
58
+ //#region src/registry/types.d.ts
59
+ type RegisterOptions = {
60
+ readonly allowOverride?: boolean;
61
+ };
62
+ //#endregion
63
+ //#region src/container/types.d.ts
64
+ type Container = {
65
+ register(provider: Provider, options?: RegisterOptions): void;
66
+ get<T>(token: Token<T>): T;
67
+ has(token: Token<unknown>): boolean;
68
+ dispose(): Promise<void>;
69
+ };
70
+ //#endregion
71
+ //#region src/container/container.d.ts
72
+ declare const createContainer: (providers?: readonly Provider[]) => Container;
73
+ declare const createChildContainer: (parent: Container, providers?: readonly Provider[]) => Container;
74
+ //#endregion
58
75
  //#region src/resolver/errors.d.ts
59
76
  declare class MissingProviderError extends DiError {
60
77
  constructor(tokenName: string);
@@ -66,4 +83,4 @@ declare class CircularDependencyError extends DiError {
66
83
  constructor(tokenNames: string[]);
67
84
  }
68
85
  //#endregion
69
- export { CircularDependencyError, type Container, DiError, DuplicateProviderError, type FactoryProvider, InvalidDependencyError, MissingProviderError, type Provider, type Scope, type Token, type ValueProvider, createContainer, createToken, provideFactory, provideValue };
86
+ export { CircularDependencyError, type Container, DiError, type DisposeHook, DuplicateProviderError, type FactoryProvider, InvalidDependencyError, MissingProviderError, type Provider, type RegisterOptions, type Scope, Scopes, type Token, type ValueProvider, createChildContainer, createContainer, createToken, provideFactory, provideValue };
package/dist/index.d.mts CHANGED
@@ -1,3 +1,11 @@
1
+ //#region src/scope/scope.d.ts
2
+ declare const Scopes: {
3
+ readonly Singleton: "singleton";
4
+ readonly Transient: "transient";
5
+ readonly Scoped: "scoped";
6
+ };
7
+ type Scope = (typeof Scopes)[keyof typeof Scopes];
8
+ //#endregion
1
9
  //#region src/token/types.d.ts
2
10
  type Token<T> = {
3
11
  readonly id: symbol;
@@ -13,6 +21,7 @@ type DepsMap = Record<string, Token<unknown>>;
13
21
  type TokenValue<TToken> = TToken extends Token<infer TValue> ? TValue : never;
14
22
  type ResolveDeps<TDeps extends DepsMap> = { readonly [TKey in keyof TDeps]: TokenValue<TDeps[TKey]> };
15
23
  type Factory<T, TDeps extends DepsMap> = (deps: ResolveDeps<TDeps>) => T;
24
+ type DisposeHook<T> = (instance: T) => void | Promise<void>;
16
25
  type ValueProvider<T> = {
17
26
  readonly provide: Token<T>;
18
27
  readonly useValue: T;
@@ -22,10 +31,10 @@ type FactoryProvider<T, TDeps extends DepsMap = Record<never, never>> = {
22
31
  readonly deps?: TDeps;
23
32
  readonly scope?: Scope;
24
33
  readonly useFactory: Factory<T, TDeps>;
34
+ readonly onDispose?: DisposeHook<T>;
25
35
  };
26
- type AnyFactoryProvider = FactoryProvider<unknown, any>;
36
+ type AnyFactoryProvider = FactoryProvider<any, any>;
27
37
  type Provider = ValueProvider<unknown> | AnyFactoryProvider;
28
- type Scope = "singleton" | "transient";
29
38
  //#endregion
30
39
  //#region src/provider/provider.d.ts
31
40
  declare const provideValue: <T>(token: Token<T>, useValue: T) => ValueProvider<T>;
@@ -33,18 +42,9 @@ declare const provideFactory: <T, TDeps extends DepsMap = Record<never, never>>(
33
42
  readonly deps?: TDeps;
34
43
  readonly scope?: Scope;
35
44
  readonly useFactory: Factory<T, TDeps>;
45
+ readonly onDispose?: DisposeHook<T>;
36
46
  }) => FactoryProvider<T, TDeps>;
37
47
  //#endregion
38
- //#region src/container/types.d.ts
39
- type Container = {
40
- register(provider: Provider): void;
41
- get<T>(token: Token<T>): T;
42
- has(token: Token<unknown>): boolean;
43
- };
44
- //#endregion
45
- //#region src/container/container.d.ts
46
- declare const createContainer: (providers?: readonly Provider[]) => Container;
47
- //#endregion
48
48
  //#region src/error/error.d.ts
49
49
  declare class DiError extends Error {
50
50
  constructor(message: string);
@@ -55,6 +55,23 @@ declare class DuplicateProviderError extends DiError {
55
55
  constructor(tokenName: string);
56
56
  }
57
57
  //#endregion
58
+ //#region src/registry/types.d.ts
59
+ type RegisterOptions = {
60
+ readonly allowOverride?: boolean;
61
+ };
62
+ //#endregion
63
+ //#region src/container/types.d.ts
64
+ type Container = {
65
+ register(provider: Provider, options?: RegisterOptions): void;
66
+ get<T>(token: Token<T>): T;
67
+ has(token: Token<unknown>): boolean;
68
+ dispose(): Promise<void>;
69
+ };
70
+ //#endregion
71
+ //#region src/container/container.d.ts
72
+ declare const createContainer: (providers?: readonly Provider[]) => Container;
73
+ declare const createChildContainer: (parent: Container, providers?: readonly Provider[]) => Container;
74
+ //#endregion
58
75
  //#region src/resolver/errors.d.ts
59
76
  declare class MissingProviderError extends DiError {
60
77
  constructor(tokenName: string);
@@ -66,4 +83,4 @@ declare class CircularDependencyError extends DiError {
66
83
  constructor(tokenNames: string[]);
67
84
  }
68
85
  //#endregion
69
- export { CircularDependencyError, type Container, DiError, DuplicateProviderError, type FactoryProvider, InvalidDependencyError, MissingProviderError, type Provider, type Scope, type Token, type ValueProvider, createContainer, createToken, provideFactory, provideValue };
86
+ export { CircularDependencyError, type Container, DiError, type DisposeHook, DuplicateProviderError, type FactoryProvider, InvalidDependencyError, MissingProviderError, type Provider, type RegisterOptions, type Scope, Scopes, type Token, type ValueProvider, createChildContainer, createContainer, createToken, provideFactory, provideValue };
package/dist/index.mjs CHANGED
@@ -18,8 +18,8 @@ var DuplicateProviderError = class extends DiError {
18
18
  //#region src/registry/registry.ts
19
19
  var RegistryClass = class {
20
20
  providers = /* @__PURE__ */ new Map();
21
- register(provider) {
22
- if (this.providers.has(provider.provide.id)) throw new DuplicateProviderError(provider.provide.name);
21
+ register(provider, options) {
22
+ if (!options?.allowOverride && this.providers.has(provider.provide.id)) throw new DuplicateProviderError(provider.provide.name);
23
23
  this.providers.set(provider.provide.id, provider);
24
24
  }
25
25
  get(token) {
@@ -51,6 +51,13 @@ var CircularDependencyError = class extends DiError {
51
51
  }
52
52
  };
53
53
  //#endregion
54
+ //#region src/scope/scope.ts
55
+ const Scopes = {
56
+ Singleton: "singleton",
57
+ Transient: "transient",
58
+ Scoped: "scoped"
59
+ };
60
+ //#endregion
54
61
  //#region src/provider/provider.ts
55
62
  const provideValue = (token, useValue) => ({
56
63
  provide: token,
@@ -60,8 +67,9 @@ const provideFactory = (token, options) => {
60
67
  return {
61
68
  provide: token,
62
69
  useFactory: options.useFactory,
70
+ scope: options.scope ?? Scopes.Singleton,
63
71
  ...options.deps ? { deps: options.deps } : {},
64
- ...options.scope ? { scope: options.scope } : {}
72
+ ...options.onDispose ? { onDispose: options.onDispose } : {}
65
73
  };
66
74
  };
67
75
  const isValueProvider = (provider) => {
@@ -71,44 +79,101 @@ const isFactoryProvider = (provider) => {
71
79
  return "useFactory" in provider;
72
80
  };
73
81
  //#endregion
82
+ //#region src/store/store.ts
83
+ var StoreClass = class {
84
+ instances = /* @__PURE__ */ new Map();
85
+ get(token) {
86
+ return this.instances.get(token.id);
87
+ }
88
+ set(token, record) {
89
+ this.instances.set(token.id, record);
90
+ }
91
+ delete(token) {
92
+ this.instances.delete(token.id);
93
+ }
94
+ async dispose() {
95
+ const records = [...this.instances.values()].reverse();
96
+ this.instances.clear();
97
+ for (const record of records) if (record.onDispose) await record.onDispose(record.value);
98
+ }
99
+ };
100
+ const createStore = () => new StoreClass();
101
+ //#endregion
102
+ //#region src/resolver/context.ts
103
+ var ResolutionContext = class {
104
+ resolving = /* @__PURE__ */ new Set();
105
+ path = [];
106
+ enter(token) {
107
+ if (this.resolving.has(token.id)) {
108
+ const cycleStartIndex = this.path.findIndex((pathToken) => pathToken.id === token.id);
109
+ throw new CircularDependencyError([...this.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
110
+ }
111
+ this.resolving.add(token.id);
112
+ this.path.push(token);
113
+ }
114
+ exit(token) {
115
+ this.path.pop();
116
+ this.resolving.delete(token.id);
117
+ }
118
+ };
119
+ //#endregion
74
120
  //#region src/resolver/resolver.ts
75
121
  var ResolverClass = class {
76
122
  registry;
77
- instances = /* @__PURE__ */ new Map();
78
- constructor(registry) {
123
+ store = createStore();
124
+ parent;
125
+ constructor(registry, parent) {
79
126
  this.registry = registry;
127
+ this.parent = parent;
80
128
  }
81
129
  resolve(token) {
82
- return this.resolveToken(token, {
83
- resolving: /* @__PURE__ */ new Set(),
84
- path: []
85
- });
130
+ return this.resolveToken(token, new ResolutionContext());
131
+ }
132
+ invalidate(token) {
133
+ this.store.delete(token);
134
+ }
135
+ dispose() {
136
+ return this.store.dispose();
86
137
  }
87
138
  resolveToken(token, context) {
88
- const provider = this.registry.get(token);
139
+ const owner = this.findOwner(token);
140
+ if (!owner) throw new MissingProviderError(token.name);
141
+ const provider = owner.registry.get(token);
89
142
  if (!provider) throw new MissingProviderError(token.name);
90
143
  if (isValueProvider(provider)) return provider.useValue;
91
144
  if (isFactoryProvider(provider)) {
92
- const scope = provider.scope ?? "singleton";
93
- if (scope === "singleton" && this.instances.has(token.id)) return this.instances.get(token.id);
94
- if (context.resolving.has(token.id)) {
95
- const cycleStartIndex = context.path.findIndex((pathToken) => pathToken.id === token.id);
96
- throw new CircularDependencyError([...context.path.slice(cycleStartIndex), token].map((cycleToken) => cycleToken.name));
145
+ const host = this.selectHost(provider.scope, owner);
146
+ if (host) {
147
+ const cached = host.store.get(token);
148
+ if (cached) return cached.value;
97
149
  }
98
- context.resolving.add(token.id);
99
- context.path.push(token);
150
+ context.enter(token);
100
151
  try {
101
- const deps = this.resolveDeps(provider.deps, context);
152
+ const deps = (host ?? this).resolveDeps(provider.deps, context);
102
153
  const value = provider.useFactory(deps);
103
- if (scope === "singleton") this.instances.set(token.id, value);
154
+ if (host) host.store.set(token, {
155
+ value,
156
+ ...provider.onDispose ? { onDispose: provider.onDispose } : {}
157
+ });
104
158
  return value;
105
159
  } finally {
106
- context.path.pop();
107
- context.resolving.delete(token.id);
160
+ context.exit(token);
108
161
  }
109
162
  }
110
163
  throw new MissingProviderError(token.name);
111
164
  }
165
+ findOwner(token) {
166
+ let resolver = this;
167
+ while (resolver) {
168
+ if (resolver.registry.has(token)) return resolver;
169
+ resolver = resolver.parent;
170
+ }
171
+ }
172
+ selectHost(scope, owner) {
173
+ if (scope === Scopes.Transient) return;
174
+ if (scope === Scopes.Scoped) return this;
175
+ return owner;
176
+ }
112
177
  resolveDeps(deps, context) {
113
178
  if (!deps) return {};
114
179
  const resolvedDeps = {};
@@ -120,28 +185,35 @@ var ResolverClass = class {
120
185
  return resolvedDeps;
121
186
  }
122
187
  };
123
- const createResolver = (registry) => new ResolverClass(registry);
188
+ const createResolver = (registry, parent) => new ResolverClass(registry, parent);
124
189
  //#endregion
125
190
  //#region src/container/container.ts
126
191
  var ContainerClass = class {
127
192
  registry;
128
193
  resolver;
129
- constructor(providers = []) {
194
+ parent;
195
+ constructor(providers = [], parent) {
196
+ this.parent = parent;
130
197
  this.registry = createRegistry();
131
198
  for (const provider of providers) this.registry.register(provider);
132
- this.resolver = createResolver(this.registry);
199
+ this.resolver = createResolver(this.registry, parent?.resolver);
133
200
  }
134
- register(provider) {
135
- this.registry.register(provider);
201
+ register(provider, options) {
202
+ this.registry.register(provider, options);
203
+ if (options?.allowOverride) this.resolver.invalidate(provider.provide);
136
204
  }
137
205
  get(token) {
138
206
  return this.resolver.resolve(token);
139
207
  }
140
208
  has(token) {
141
- return this.registry.has(token);
209
+ return this.registry.has(token) || (this.parent?.has(token) ?? false);
210
+ }
211
+ dispose() {
212
+ return this.resolver.dispose();
142
213
  }
143
214
  };
144
215
  const createContainer = (providers = []) => new ContainerClass(providers);
216
+ const createChildContainer = (parent, providers = []) => new ContainerClass(providers, parent);
145
217
  //#endregion
146
218
  //#region src/token/token.ts
147
219
  var TokenClass = class {
@@ -154,4 +226,4 @@ var TokenClass = class {
154
226
  };
155
227
  const createToken = (name) => new TokenClass(name);
156
228
  //#endregion
157
- export { CircularDependencyError, DiError, DuplicateProviderError, InvalidDependencyError, MissingProviderError, createContainer, createToken, provideFactory, provideValue };
229
+ export { CircularDependencyError, DiError, DuplicateProviderError, InvalidDependencyError, MissingProviderError, Scopes, createChildContainer, createContainer, createToken, provideFactory, provideValue };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "di-craft",
3
- "version": "0.0.10",
4
- "description": "A tiny TypeScript dependency injection container",
3
+ "version": "0.0.11",
4
+ "description": "A tiny, type-safe dependency injection container for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Egor Bezmen",