di-craft 0.0.14 → 0.0.16

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.
Files changed (2) hide show
  1. package/README.md +111 -61
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,21 @@
1
- # di-craft
1
+ <h1 align="center">di-craft</h1>
2
+
3
+ <p align="center">
4
+ <img src="./assets/logo.png" alt="di-craft" width="200" />
5
+ </p>
6
+
7
+ <p align="center">
8
+ <b>A tiny, type-safe dependency injection container for TypeScript</b>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <img alt="NPM Version" src="https://img.shields.io/npm/v/di-craft?style=flat-square&color=%2364d4c1&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fdi-craft">
13
+ <img alt="NPM package gzipped size" src="https://img.shields.io/bundlejs/size/di-craft?style=flat-square&label=gzip&color=%2364d4c1">
14
+ </p>
15
+
16
+ > [!NOTE]
17
+ > This README was generated with a bit of AI help — don't believe everything you see 🙂
2
18
 
3
- A tiny, type-safe dependency injection container for TypeScript.
4
19
 
5
20
  ## Contents
6
21
 
@@ -93,7 +108,8 @@ CommonJS code can load it with a dynamic `import()`.
93
108
 
94
109
  ### Tokens
95
110
 
96
- A token is a unique, type-carrying key. Identity is based on an internal `symbol`, **not** on the name — two tokens with the same name are still different.
111
+ A token is a unique, type-carrying key. Identity is based on an internal `symbol`,
112
+ **not** on the name — two tokens with the same name are still different.
97
113
 
98
114
  ```ts
99
115
  const PORT = createToken<number>("port");
@@ -101,7 +117,8 @@ const PORT = createToken<number>("port");
101
117
  PORT.name; // "port" — used only for error messages
102
118
  ```
103
119
 
104
- The type argument flows everywhere: providers must produce a matching value, and `container.get(PORT)` returns `number`.
120
+ The type argument flows everywhere: providers must produce a matching value, and
121
+ `container.get(PORT)` returns `number`.
105
122
 
106
123
  ### Providers
107
124
 
@@ -117,13 +134,14 @@ provideValue(PORT, 3000);
117
134
 
118
135
  ```ts
119
136
  provideFactory(HTTP, {
120
- deps: { config: CONFIG }, // optional, keyed map of tokens
121
- scope: "singleton", // optional, defaults to "singleton"
137
+ deps: { config: CONFIG }, // optional, keyed map of tokens
138
+ scope: "singleton", // optional, defaults to "singleton"
122
139
  useFactory: ({ config }) => new HttpClient(config.apiUrl),
123
140
  });
124
141
  ```
125
142
 
126
- The keys in `deps` become the keys of the object passed to `useFactory`, each resolved to its token's type.
143
+ The keys in `deps` become the keys of the object passed to `useFactory`, each
144
+ resolved to its token's type.
127
145
 
128
146
  ### Optional dependencies
129
147
 
@@ -153,8 +171,7 @@ const logger = container.get(optional(LOGGER)); // Logger | undefined
153
171
  ```
154
172
 
155
173
  Optional only affects the token itself: if a provider _is_ registered, it is
156
- resolved normally and its own errors (cycles, missing nested deps) still
157
- surface.
174
+ resolved normally and its own errors (cycles, missing nested deps) still surface.
158
175
 
159
176
  ### Container
160
177
 
@@ -164,16 +181,18 @@ dispose:
164
181
 
165
182
  - `register(provider, options?)` — add a provider at any time.
166
183
  - `has(token)` — whether a provider for the token is registered.
167
- - `get(token)` — resolve the value, building and caching it as its scope dictates. Accepts `optional(token)` to get `undefined` instead of throwing when absent.
168
- - `dispose()` run `onDispose` hooks and release resolved instances.
184
+ - `get(token)` — resolve the value, building and caching it as its scope dictates.
185
+ Accepts `optional(token)` to get `undefined` instead of throwing when absent.
186
+ - `dispose()` — run `onDispose` hooks and release tracked instances owned by this
187
+ container.
169
188
 
170
189
  ```ts
171
190
  const container = createContainer(providers); // providers are optional
172
191
 
173
192
  container.register(provideValue(PORT, 3000)); // register more at any time
174
- container.has(PORT); // true
175
- container.get(PORT); // 3000
176
- await container.dispose(); // release resolved singletons
193
+ container.has(PORT); // true
194
+ container.get(PORT); // 3000
195
+ await container.dispose(); // clears tracked instances and awaits disposal hooks
177
196
  ```
178
197
 
179
198
  Registering the same token twice throws `DuplicateProviderError`. To replace an
@@ -192,11 +211,11 @@ so the resource is released, then register the replacement.
192
211
 
193
212
  ### Scopes
194
213
 
195
- | Scope | Behavior |
196
- | ---------------------- | ---------------------------------------------------------------- |
197
- | `singleton` (default) | The factory runs once; the same instance is returned every time. |
198
- | `transient` | The factory runs on every `get`, producing a fresh instance. |
199
- | `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. |
214
+ | Scope | Behavior |
215
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
216
+ | `singleton` (default) | The factory runs once; the same instance is returned every time. |
217
+ | `transient` | The factory runs on every `get`, producing a fresh instance. |
218
+ | `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. |
200
219
 
201
220
  Use the `Scopes` helper for autocompletion, or pass the plain string — both work:
202
221
 
@@ -222,7 +241,8 @@ example, reuses the shared singleton instance.
222
241
 
223
242
  Factory providers can declare an `onDispose` hook to release resources (database
224
243
  pools, sockets, timers, subscriptions). Calling `container.dispose()` runs the
225
- hooks for every resolved singleton and clears the cache:
244
+ hooks for every resolved cached instance owned by that container and releases the
245
+ container's tracked instances:
226
246
 
227
247
  ```ts
228
248
  const DB = createToken<Pool>("db");
@@ -236,16 +256,21 @@ const container = createContainer([
236
256
 
237
257
  container.get(DB);
238
258
 
239
- await container.dispose(); // awaits async hooks, then clears instances
259
+ await container.dispose(); // clears tracked instances and awaits disposal hooks
240
260
  ```
241
261
 
242
262
  Details:
243
263
 
244
264
  - Hooks run in reverse creation order (dependents before their dependencies).
245
265
  - `dispose()` returns a promise and awaits async hooks.
246
- - It is idempotent calling it again is a no-op.
247
- - Only resolved singletons (and scoped instances on their container) are disposed; never-resolved instances are not tracked.
248
- - `onDispose` is only meaningful for cached instances. Declaring it on a `transient` provider throws `InvalidProviderError`, since transient instances are never tracked and the hook could never run.
266
+ - Instances are removed from the cache before hooks run, making disposal
267
+ idempotent and re-entrancy safe.
268
+ - Only resolved cached instances owned by that container are disposed: singletons
269
+ owned by that container and scoped instances created for that container.
270
+ Transient and never-resolved instances are not tracked.
271
+ - `onDispose` is only meaningful for cached instances. Declaring it on a
272
+ `transient` provider throws `InvalidProviderError`, since transient instances
273
+ are never tracked and the hook could never run.
249
274
 
250
275
  ### Child containers
251
276
 
@@ -277,14 +302,19 @@ function handle(request: Request) {
277
302
  How resolution works across the chain:
278
303
 
279
304
  - A token is looked up in the child first, then walks up to the parent.
280
- - `singleton` is cached on the container that **owns** the provider, so it is shared by the whole subtree.
281
- - `scoped` is cached on the **requesting** child, so each child gets its own instance — even when the provider is declared once on the parent.
282
- - A `scoped` provider resolves its dependencies from the requesting child, so it can depend on values registered only in that child (like `REQUEST`).
283
- - `dispose()` only releases the container it is called on; it does not cascade to parents or children.
305
+ - `singleton` is cached on the container that **owns** the provider, so it is
306
+ shared by the whole subtree.
307
+ - `scoped` is cached on the **requesting** child, so each child gets its own
308
+ instance even when the provider is declared once on the parent.
309
+ - A `scoped` provider resolves its dependencies from the requesting child, so it
310
+ can depend on values registered only in that child (like `REQUEST`).
311
+ - `dispose()` only releases the container it is called on; it does not cascade to
312
+ parents or children.
284
313
 
285
314
  ### Cycle detection
286
315
 
287
- If providers form a dependency cycle, resolution throws `CircularDependencyError` with the full path instead of overflowing the stack.
316
+ If providers form a dependency cycle, resolution throws `CircularDependencyError`
317
+ with the full path instead of overflowing the stack.
288
318
 
289
319
  ```ts
290
320
  // A -> B -> A
@@ -327,17 +357,33 @@ provideFactory(USERS, {
327
357
  // USERS is now Token<Promise<UsersRepo>> — consumers await it too.
328
358
  ```
329
359
 
360
+ When using `Promise<T>` as the token value, disposal hooks receive the promise
361
+ itself. Await it inside `onDispose` if cleanup needs the resolved value:
362
+
363
+ ```ts
364
+ const POOL = createToken<Promise<Pool>>("pool");
365
+
366
+ provideFactory(POOL, {
367
+ useFactory: () => createPool(),
368
+ onDispose: async (poolPromise) => {
369
+ const pool = await poolPromise;
370
+ await pool.end();
371
+ },
372
+ });
373
+ ```
374
+
330
375
  ## Dependency injection vs service location
331
376
 
332
- di-craft is built for **dependency injection**: dependencies are declared up front
333
- and handed to your code. The opposite is **service location**, where code reaches
334
- into a container at runtime to pull what it needs, hiding its real dependencies.
377
+ di-craft is built for **dependency injection**: dependencies are declared up
378
+ front and handed to your code. The opposite is **service location**, where code
379
+ reaches into a container at runtime to pull what it needs, hiding its real
380
+ dependencies.
335
381
 
336
382
  Two habits keep usage canonical: call `container.get()` only at the **composition
337
- root** (entrypoint, framework hooks, route handlers), and never pass the container
338
- into your classes or functions. di-craft enforces the key half for you — **a
339
- factory only ever receives its declared `deps`, never the container** — so a
340
- provider physically cannot locate arbitrary services.
383
+ root** (entrypoint, framework hooks, route handlers), and never pass the
384
+ container into your classes or functions. di-craft enforces the key half for you
385
+ — **a factory only ever receives its declared `deps`, never the container** — so
386
+ a provider physically cannot locate arbitrary services.
341
387
 
342
388
  ```ts
343
389
  // Dependency injection — deps are explicit, the class never sees the container.
@@ -360,14 +406,15 @@ class UserService {
360
406
  }
361
407
  ```
362
408
 
363
- The second form compiles, but it hides dependencies and defeats DI. No runtime flag
364
- can forbid it — `get()` is the same call the composition root relies on — so keep
365
- resolution at the edges by convention, or enforce it with a lint rule that allows
366
- `.get()` only in your composition-root files.
409
+ The second form compiles, but it hides dependencies and defeats DI. No runtime
410
+ flag can forbid it — `get()` is the same call the composition root relies on — so
411
+ keep resolution at the edges by convention, or enforce it with a lint rule that
412
+ allows `.get()` only in your composition-root files.
367
413
 
368
414
  ## Error handling
369
415
 
370
- All errors extend the shared `DiError` base class, so you can catch any container error with a single check:
416
+ All errors extend the shared `DiError` base class, so you can catch any container
417
+ error with a single check:
371
418
 
372
419
  ```ts
373
420
  import { DiError, MissingProviderError } from "di-craft";
@@ -385,29 +432,32 @@ try {
385
432
  }
386
433
  ```
387
434
 
388
- | Error | Thrown when |
389
- | -------------------------- | -------------------------------------------------------- |
390
- | `MissingProviderError` | A token is resolved but no provider is registered. |
391
- | `DuplicateProviderError` | A token is registered more than once. |
392
- | `CircularDependencyError` | Providers form a dependency cycle. |
393
- | `InvalidDependencyError` | A declared dependency token is missing/undefined. |
394
- | `InvalidProviderError` | A provider is misconfigured (`onDispose` on a transient, or a dependency that outlives its consumer) or an override would drop a live disposable instance. |
435
+ | Error | Thrown when |
436
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
437
+ | `MissingProviderError` | A token is resolved but no provider is registered. |
438
+ | `DuplicateProviderError` | A token is registered more than once. |
439
+ | `CircularDependencyError` | Providers form a dependency cycle. |
440
+ | `InvalidDependencyError` | A declared dependency token is missing/undefined. |
441
+ | `InvalidProviderError` | A provider is misconfigured (`onDispose` on a transient, or a dependency with a shorter lifetime than its consumer) or an override would drop a live disposable instance. |
395
442
 
396
443
  ## API reference
397
444
 
398
- | Export | Description |
399
- | ----------------------- | ---------------------------------------------------------- |
400
- | `createToken<T>(name)` | Create a unique, typed token. |
401
- | `provideValue(token, value)` | Provider that returns an existing value. |
402
- | `provideFactory(token, options)` | Provider that builds a value via a factory. |
403
- | `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
404
- | `createContainer(providers?)` | Create a container, optionally seeded with providers. |
405
- | `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
406
- | `Scopes` | Object of scope values (`Scopes.Singleton`, `Scopes.Transient`, `Scopes.Scoped`). |
407
-
408
- Exported types: `Container`, `Token`, `Provider`, `ValueProvider`, `FactoryProvider`, `Dependency`, `OptionalDependency`, `Scope`, `DisposeHook`, `RegisterOptions`.
409
-
410
- Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`, `CircularDependencyError`, `InvalidDependencyError`, `InvalidProviderError`.
445
+ | Export | Description |
446
+ | ------------------------------------------ | --------------------------------------------------------------------------- |
447
+ | `createToken<T>(name)` | Create a unique, typed token. |
448
+ | `provideValue(token, value)` | Provider that returns an existing value. |
449
+ | `provideFactory(token, options)` | Provider that builds a value via a factory. |
450
+ | `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
451
+ | `createContainer(providers?)` | Create a container, optionally seeded with providers. |
452
+ | `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
453
+ | `Scopes` | Object of scope values (`Scopes.Singleton`, `Scopes.Transient`, `Scopes.Scoped`). |
454
+
455
+ Exported types: `Container`, `Token`, `Provider`, `ValueProvider`,
456
+ `FactoryProvider`, `Dependency`, `OptionalDependency`, `Scope`, `DisposeHook`,
457
+ `RegisterOptions`.
458
+
459
+ Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`,
460
+ `CircularDependencyError`, `InvalidDependencyError`, `InvalidProviderError`.
411
461
 
412
462
  ## License
413
463
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "di-craft",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "A tiny, type-safe dependency injection container for TypeScript",
5
5
  "license": "MIT",
6
6
  "author": {