@c9up/ream 0.2.13 → 0.2.15

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 (192) hide show
  1. package/README.md +1 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +6 -1
  4. package/dist/Application.js.map +1 -1
  5. package/dist/GracefulShutdown.js +6 -5
  6. package/dist/GracefulShutdown.js.map +1 -1
  7. package/dist/Provider.d.ts +20 -2
  8. package/dist/Provider.d.ts.map +1 -1
  9. package/dist/Provider.js.map +1 -1
  10. package/dist/bodyparser/MultipartFile.d.ts +33 -7
  11. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  12. package/dist/bodyparser/MultipartFile.js +61 -12
  13. package/dist/bodyparser/MultipartFile.js.map +1 -1
  14. package/dist/bodyparser/parseSize.d.ts.map +1 -1
  15. package/dist/bodyparser/parseSize.js +3 -2
  16. package/dist/bodyparser/parseSize.js.map +1 -1
  17. package/dist/commands/MigrateBase.d.ts.map +1 -1
  18. package/dist/commands/MigrateBase.js +4 -1
  19. package/dist/commands/MigrateBase.js.map +1 -1
  20. package/dist/console/Kernel.d.ts.map +1 -1
  21. package/dist/console/Kernel.js +7 -0
  22. package/dist/console/Kernel.js.map +1 -1
  23. package/dist/console/types.d.ts +14 -0
  24. package/dist/console/types.d.ts.map +1 -1
  25. package/dist/console/types.js.map +1 -1
  26. package/dist/container/Container.d.ts +19 -3
  27. package/dist/container/Container.d.ts.map +1 -1
  28. package/dist/container/Container.js +89 -31
  29. package/dist/container/Container.js.map +1 -1
  30. package/dist/container/ContainerResolver.js.map +1 -1
  31. package/dist/container/types.d.ts +11 -1
  32. package/dist/container/types.d.ts.map +1 -1
  33. package/dist/decorators/Service.d.ts +1 -1
  34. package/dist/decorators/Service.d.ts.map +1 -1
  35. package/dist/env/Env.d.ts.map +1 -1
  36. package/dist/env/Env.js +4 -2
  37. package/dist/env/Env.js.map +1 -1
  38. package/dist/env/interpolate.d.ts.map +1 -1
  39. package/dist/env/interpolate.js +5 -3
  40. package/dist/env/interpolate.js.map +1 -1
  41. package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
  42. package/dist/errors/FuzzyMatcher.js +30 -13
  43. package/dist/errors/FuzzyMatcher.js.map +1 -1
  44. package/dist/errors/PipelineStageError.d.ts.map +1 -1
  45. package/dist/errors/PipelineStageError.js +4 -2
  46. package/dist/errors/PipelineStageError.js.map +1 -1
  47. package/dist/events/Emitter.d.ts +21 -4
  48. package/dist/events/Emitter.d.ts.map +1 -1
  49. package/dist/events/Emitter.js +6 -2
  50. package/dist/events/Emitter.js.map +1 -1
  51. package/dist/events/testing/FakeBus.d.ts +1 -1
  52. package/dist/events/testing/FakeBus.d.ts.map +1 -1
  53. package/dist/events/testing/FakeBus.js.map +1 -1
  54. package/dist/graphql/GraphQLEngine.d.ts +2 -2
  55. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  56. package/dist/graphql/GraphQLEngine.js +4 -2
  57. package/dist/graphql/GraphQLEngine.js.map +1 -1
  58. package/dist/helpers/bytes.d.ts.map +1 -1
  59. package/dist/helpers/bytes.js +13 -2
  60. package/dist/helpers/bytes.js.map +1 -1
  61. package/dist/helpers/string.js +3 -3
  62. package/dist/helpers/string.js.map +1 -1
  63. package/dist/http/Exception.d.ts +8 -4
  64. package/dist/http/Exception.d.ts.map +1 -1
  65. package/dist/http/Exception.js +14 -8
  66. package/dist/http/Exception.js.map +1 -1
  67. package/dist/http/Request.d.ts.map +1 -1
  68. package/dist/http/Request.js +10 -7
  69. package/dist/http/Request.js.map +1 -1
  70. package/dist/http/Response.d.ts.map +1 -1
  71. package/dist/http/Response.js +4 -9
  72. package/dist/http/Response.js.map +1 -1
  73. package/dist/http/SseStream.d.ts.map +1 -1
  74. package/dist/http/SseStream.js +10 -1
  75. package/dist/http/SseStream.js.map +1 -1
  76. package/dist/http/etag.d.ts +44 -0
  77. package/dist/http/etag.d.ts.map +1 -0
  78. package/dist/http/etag.js +65 -0
  79. package/dist/http/etag.js.map +1 -0
  80. package/dist/http/mime.d.ts +33 -0
  81. package/dist/http/mime.d.ts.map +1 -0
  82. package/dist/http/mime.js +1347 -0
  83. package/dist/http/mime.js.map +1 -0
  84. package/dist/index.d.ts +3 -3
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +1 -1
  87. package/dist/native/generated.d.ts +12 -8
  88. package/dist/native/generated.d.ts.map +1 -1
  89. package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
  90. package/dist/openapi/OpenApiGenerator.js +6 -4
  91. package/dist/openapi/OpenApiGenerator.js.map +1 -1
  92. package/dist/router/Router.d.ts +10 -1
  93. package/dist/router/Router.d.ts.map +1 -1
  94. package/dist/router/Router.js +22 -10
  95. package/dist/router/Router.js.map +1 -1
  96. package/dist/rpc/RpcRouter.d.ts +16 -3
  97. package/dist/rpc/RpcRouter.d.ts.map +1 -1
  98. package/dist/rpc/RpcRouter.js +34 -3
  99. package/dist/rpc/RpcRouter.js.map +1 -1
  100. package/dist/scheduler/Schedule.d.ts +6 -1
  101. package/dist/scheduler/Schedule.d.ts.map +1 -1
  102. package/dist/scheduler/Schedule.js +5 -0
  103. package/dist/scheduler/Schedule.js.map +1 -1
  104. package/dist/security/CookieSigner.d.ts.map +1 -1
  105. package/dist/security/CookieSigner.js +12 -5
  106. package/dist/security/CookieSigner.js.map +1 -1
  107. package/dist/security/SignedUrl.js +3 -2
  108. package/dist/security/SignedUrl.js.map +1 -1
  109. package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
  110. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
  111. package/dist/session/drivers/DatabaseDriver.js +23 -3
  112. package/dist/session/drivers/DatabaseDriver.js.map +1 -1
  113. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  114. package/dist/session/drivers/RedisDriver.js +10 -2
  115. package/dist/session/drivers/RedisDriver.js.map +1 -1
  116. package/dist/storage/StaticMiddleware.d.ts.map +1 -1
  117. package/dist/storage/StaticMiddleware.js +78 -59
  118. package/dist/storage/StaticMiddleware.js.map +1 -1
  119. package/dist/testing/ApiResponse.d.ts.map +1 -1
  120. package/dist/testing/ApiResponse.js +10 -11
  121. package/dist/testing/ApiResponse.js.map +1 -1
  122. package/dist/testing/TestClient.js +6 -6
  123. package/dist/testing/TestClient.js.map +1 -1
  124. package/dist/utils/Macroable.d.ts +6 -2
  125. package/dist/utils/Macroable.d.ts.map +1 -1
  126. package/dist/utils/Macroable.js +4 -0
  127. package/dist/utils/Macroable.js.map +1 -1
  128. package/dist/utils/objectPath.d.ts.map +1 -1
  129. package/dist/utils/objectPath.js +14 -6
  130. package/dist/utils/objectPath.js.map +1 -1
  131. package/events.darwin-arm64.node +0 -0
  132. package/events.darwin-x64.node +0 -0
  133. package/events.linux-arm64-gnu.node +0 -0
  134. package/events.linux-x64-gnu.node +0 -0
  135. package/events.win32-x64-msvc.node +0 -0
  136. package/index.darwin-arm64.node +0 -0
  137. package/index.darwin-x64.node +0 -0
  138. package/index.linux-arm64-gnu.node +0 -0
  139. package/index.linux-x64-gnu.node +0 -0
  140. package/index.win32-x64-msvc.node +0 -0
  141. package/package.json +5 -7
  142. package/scheduler.darwin-arm64.node +0 -0
  143. package/scheduler.darwin-x64.node +0 -0
  144. package/scheduler.linux-arm64-gnu.node +0 -0
  145. package/scheduler.linux-x64-gnu.node +0 -0
  146. package/scheduler.win32-x64-msvc.node +0 -0
  147. package/scripts/build-napi-types.mjs +4 -3
  148. package/scripts/generate-mime.mjs +119 -0
  149. package/scripts/generate-napi-types.mjs +28 -12
  150. package/src/Application.ts +6 -1
  151. package/src/GracefulShutdown.ts +5 -5
  152. package/src/Provider.ts +21 -2
  153. package/src/bodyparser/MultipartFile.ts +94 -16
  154. package/src/bodyparser/parseSize.ts +3 -2
  155. package/src/commands/MigrateBase.ts +4 -1
  156. package/src/console/BaseCommand.ts +1 -1
  157. package/src/console/Kernel.ts +8 -0
  158. package/src/console/types.ts +15 -0
  159. package/src/container/Container.ts +120 -57
  160. package/src/container/ContainerResolver.ts +1 -1
  161. package/src/container/types.ts +11 -1
  162. package/src/decorators/Service.ts +1 -1
  163. package/src/env/Env.ts +4 -2
  164. package/src/env/interpolate.ts +5 -3
  165. package/src/errors/FuzzyMatcher.ts +34 -13
  166. package/src/errors/PipelineStageError.ts +4 -2
  167. package/src/events/Emitter.ts +34 -8
  168. package/src/events/testing/FakeBus.ts +12 -5
  169. package/src/graphql/GraphQLEngine.ts +11 -7
  170. package/src/helpers/bytes.ts +13 -2
  171. package/src/helpers/string.ts +3 -3
  172. package/src/http/Exception.ts +14 -8
  173. package/src/http/Request.ts +8 -5
  174. package/src/http/Response.ts +4 -8
  175. package/src/http/SseStream.ts +16 -7
  176. package/src/http/etag.ts +68 -0
  177. package/src/http/mime.ts +1347 -0
  178. package/src/index.ts +3 -3
  179. package/src/native/generated.ts +12 -8
  180. package/src/openapi/OpenApiGenerator.ts +6 -4
  181. package/src/router/Router.ts +31 -11
  182. package/src/rpc/RpcRouter.ts +38 -8
  183. package/src/scheduler/Schedule.ts +8 -1
  184. package/src/security/CookieSigner.ts +15 -5
  185. package/src/security/SignedUrl.ts +3 -2
  186. package/src/session/drivers/DatabaseDriver.ts +36 -8
  187. package/src/session/drivers/RedisDriver.ts +13 -5
  188. package/src/storage/StaticMiddleware.ts +81 -66
  189. package/src/testing/ApiResponse.ts +9 -10
  190. package/src/testing/TestClient.ts +6 -6
  191. package/src/utils/Macroable.ts +6 -2
  192. package/src/utils/objectPath.ts +12 -6
@@ -32,8 +32,8 @@ const RESERVED_TOKEN_NAMES = new Set(['__proto__', 'constructor', 'prototype'])
32
32
 
33
33
  /** One resolution chain: the tokens on it, and a set for O(1) membership. */
34
34
  interface ResolutionChain {
35
- stack: string[]
36
- set: Set<string>
35
+ stack: ServiceToken[]
36
+ set: Set<ServiceToken>
37
37
  /**
38
38
  * Values bound on the {@link ContainerResolver} that opened this chain.
39
39
  *
@@ -42,7 +42,7 @@ interface ResolutionChain {
42
42
  * a controller's constructor dependency sees the same `HttpContext` the
43
43
  * middleware bound — and to nothing outside it.
44
44
  */
45
- values?: ReadonlyMap<string, unknown>
45
+ values?: ReadonlyMap<ServiceToken, unknown>
46
46
  /**
47
47
  * How many scoped values this chain has handed out so far.
48
48
  *
@@ -63,14 +63,14 @@ interface ResolutionChain {
63
63
  * down. Saved and restored around each construction, so a dependency's own
64
64
  * dependencies see IT as their parent, not its parent.
65
65
  */
66
- parent?: new (
66
+ parent?: abstract new (
67
67
  ...args: never[]
68
68
  ) => unknown
69
69
  }
70
70
 
71
71
  export class Container {
72
- #bindings: Map<string, Binding> = new Map()
73
- #singletons: Map<string, unknown> = new Map()
72
+ #bindings: Map<ServiceToken, Binding> = new Map()
73
+ #singletons: Map<ServiceToken, unknown> = new Map()
74
74
  /**
75
75
  * Singletons currently being built, by key.
76
76
  *
@@ -79,22 +79,26 @@ export class Container {
79
79
  * point is one, and silently discarding the first (a second connection
80
80
  * pool, a second scheduler). Concurrent callers await the same promise.
81
81
  */
82
- #pendingSingletons: Map<string, Promise<unknown>> = new Map()
83
- #overrides: Map<string, ServiceFactory> = new Map()
82
+ #pendingSingletons: Map<ServiceToken, Promise<unknown>> = new Map()
83
+ #overrides: Map<ServiceToken, ServiceFactory> = new Map()
84
84
  /**
85
85
  * Snapshot of singletons taken when `swap`/`override` shadows a previously
86
86
  * resolved instance. `restore()` re-seats them without re-invoking the
87
87
  * original factory, so callers that captured the live reference keep
88
88
  * pointing at the same object after the test ends.
89
89
  */
90
- #singletonBackup: Map<string, unknown> = new Map()
90
+ #singletonBackup: Map<ServiceToken, unknown> = new Map()
91
91
  /** Alias key → the token it forwards to (AdonisJS `container.alias`). */
92
- #aliases: Map<string, ServiceToken> = new Map()
92
+ #aliases: Map<ServiceToken, ServiceToken> = new Map()
93
93
  /** parent class → (binding key → the factory that class gets instead). */
94
- #contextualBindings: Map<new (...args: never[]) => unknown, Map<string, ServiceFactory>> =
95
- new Map()
94
+ #contextualBindings: Map<
95
+ abstract new (
96
+ ...args: never[]
97
+ ) => unknown,
98
+ Map<ServiceToken, ServiceFactory>
99
+ > = new Map()
96
100
  /** token key → post-resolution callbacks (AdonisJS `container.resolving`). */
97
- #resolvingHooks: Map<string, Array<(value: unknown) => void | Promise<void>>> = new Map()
101
+ #resolvingHooks: Map<ServiceToken, Array<(value: unknown) => void | Promise<void>>> = new Map()
98
102
  /**
99
103
  * The resolution chain currently being walked, for cycle detection.
100
104
  *
@@ -176,7 +180,7 @@ export class Container {
176
180
  else this.#resolvingHooks.set(key, [callback])
177
181
  }
178
182
 
179
- async #runResolvingHooks(key: string, value: unknown): Promise<void> {
183
+ async #runResolvingHooks(key: ServiceToken, value: unknown): Promise<void> {
180
184
  const hooks = this.#resolvingHooks.get(key)
181
185
  if (hooks) for (const hook of hooks) await hook(value)
182
186
  }
@@ -235,7 +239,7 @@ export class Container {
235
239
  }
236
240
  }
237
241
 
238
- #restoreOne(key: string): void {
242
+ #restoreOne(key: ServiceToken): void {
239
243
  this.#overrides.delete(key)
240
244
  this.#singletons.delete(key)
241
245
  if (this.#singletonBackup.has(key)) {
@@ -272,7 +276,7 @@ export class Container {
272
276
  // `alias('b','a')` overflowed the stack instead of naming the cycle.
273
277
  let resolvedToken = token
274
278
  let key = this.#tokenToKey(resolvedToken)
275
- const seenAliases = new Set<string>([key])
279
+ const seenAliases = new Set<ServiceToken>([key])
276
280
  for (
277
281
  let target = this.#aliases.get(key);
278
282
  target !== undefined;
@@ -281,7 +285,7 @@ export class Container {
281
285
  resolvedToken = target
282
286
  key = this.#tokenToKey(target)
283
287
  if (seenAliases.has(key)) {
284
- const cycle = [...seenAliases, key].join(' → ')
288
+ const cycle = [...seenAliases, key].map(describeToken).join(' → ')
285
289
  throw new ReamError('E_CIRCULAR_DEPENDENCY', `Circular alias detected: ${cycle}`, {
286
290
  hint: 'An alias chain must end at a real binding — remove one of the aliases.',
287
291
  context: { chain: cycle },
@@ -301,7 +305,7 @@ export class Container {
301
305
  }
302
306
 
303
307
  if (chain.set.has(key)) {
304
- const cycle = [...chain.stack, key].join(' → ')
308
+ const cycle = [...chain.stack, key].map(describeToken).join(' → ')
305
309
  throw new ReamError('E_CIRCULAR_DEPENDENCY', `Circular dependency detected: ${cycle}`, {
306
310
  hint: 'Break the cycle by resolving one dependency lazily inside a method (`await container.make(Dep)`) instead of injecting it in the constructor.',
307
311
  context: { chain: cycle },
@@ -401,7 +405,10 @@ export class Container {
401
405
  * Reuses an open chain rather than starting a second one, so cycle detection
402
406
  * still spans the whole resolution when a resolver is used inside a factory.
403
407
  */
404
- runWithScopedValues<T>(values: ReadonlyMap<string, unknown>, fn: () => Promise<T>): Promise<T> {
408
+ runWithScopedValues<T>(
409
+ values: ReadonlyMap<ServiceToken, unknown>,
410
+ fn: () => Promise<T>,
411
+ ): Promise<T> {
405
412
  const chain = this.#chain.getStore()
406
413
  return this.#chain.run(
407
414
  chain === undefined ? { stack: [], set: new Set(), values } : { ...chain, values },
@@ -409,8 +416,8 @@ export class Container {
409
416
  )
410
417
  }
411
418
 
412
- /** @internal Normalise a token the way the container keys it. */
413
- keyFor(token: ServiceToken): string {
419
+ /** @internal Validate a token and hand back the value the container keys on. */
420
+ keyFor(token: ServiceToken): ServiceToken {
414
421
  return this.#tokenToKey(token)
415
422
  }
416
423
 
@@ -434,6 +441,41 @@ export class Container {
434
441
 
435
442
  // ─── Introspection ────────────────────────────────────────
436
443
 
444
+ /**
445
+ * Every token the container holds, sorted, with the kind beside it.
446
+ *
447
+ * `ream inspect` advertised a section for these and rendered none, because
448
+ * nothing could enumerate them — `has()` answers about one token you already
449
+ * know the name of, which is no help when the question is what is in there.
450
+ *
451
+ * These are bindings, not every `@inject()`-decorated class: a decorated
452
+ * class is discovered when it is resolved, and nothing registers it before
453
+ * that, so no complete list of those exists to return.
454
+ */
455
+ get bindings(): ReadonlyArray<{ token: string; scope: string; target?: string }> {
456
+ const seen = new Map<ServiceToken, { scope: string; target?: string }>()
457
+ for (const [key, binding] of this.#bindings) seen.set(key, { scope: binding.scope })
458
+ for (const key of this.#singletons.keys()) {
459
+ if (!seen.has(key)) seen.set(key, { scope: 'value' })
460
+ }
461
+ // Aliases LAST and overwriting, because that is the order `resolve()` uses:
462
+ // it follows an alias before it ever looks at a binding. Reporting the
463
+ // binding for a token that also has an alias showed a definition nobody
464
+ // gets — `service` listed as transient while resolving it hands back
465
+ // `target`. Where both exist the alias is what runs, and the binding it
466
+ // shadows is named beside it rather than dropped.
467
+ for (const [key, target] of this.#aliases) {
468
+ const shadowed = seen.get(key)
469
+ seen.set(key, {
470
+ scope: shadowed === undefined ? 'alias' : `alias (shadows ${shadowed.scope})`,
471
+ target: describeToken(target),
472
+ })
473
+ }
474
+ return [...seen.entries()]
475
+ .map(([token, entry]) => ({ token: describeToken(token), ...entry }))
476
+ .sort((a, b) => a.token.localeCompare(b.token))
477
+ }
478
+
437
479
  /** Check if a token is registered or resolvable. */
438
480
  has(token: ServiceToken): boolean {
439
481
  const key = this.#tokenToKey(token)
@@ -472,7 +514,7 @@ export class Container {
472
514
  /** Auto-register all @Service() decorated classes from the global registry. */
473
515
  autoRegister(): void {
474
516
  for (const [target, metadata] of getServiceRegistry()) {
475
- const key = metadata.as ?? target.name
517
+ const key = metadata.as ?? target
476
518
  if (!this.#bindings.has(key)) {
477
519
  const targetClass = target
478
520
  this.#bindings.set(key, {
@@ -487,7 +529,11 @@ export class Container {
487
529
 
488
530
  // ─── Internal resolution ──────────────────────────────────
489
531
 
490
- async #resolveInner<T>(key: string, token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
532
+ async #resolveInner<T>(
533
+ key: ServiceToken,
534
+ token: ServiceToken,
535
+ runtimeValues?: unknown[],
536
+ ): Promise<T> {
491
537
  // 1. Check swaps (test overrides) — a swap factory may be async.
492
538
  if (this.#overrides.has(key)) {
493
539
  const swapped = await this.#overrides.get(key)?.(this)
@@ -562,11 +608,12 @@ export class Container {
562
608
  }
563
609
 
564
610
  // 7. Not found
565
- const allKeys = [...this.#bindings.keys(), ...this.#overrides.keys()]
566
- const suggestion = didYouMean(key, allKeys)
611
+ const name = describeToken(key)
612
+ const allKeys = [...this.#bindings.keys(), ...this.#overrides.keys()].map(describeToken)
613
+ const suggestion = didYouMean(name, allKeys)
567
614
  throw new ReamError(
568
615
  'E_CONTAINER_NOT_FOUND',
569
- `No binding found for '${key}'.${suggestion ? ` ${suggestion}` : ''}`,
616
+ `No binding found for '${name}'.${suggestion ? ` ${suggestion}` : ''}`,
570
617
  {
571
618
  hint: 'Register it with container.singleton() or decorate with @inject().',
572
619
  },
@@ -582,7 +629,7 @@ export class Container {
582
629
  * 3. No params → plain `new Class()`
583
630
  */
584
631
  async #autoConstruct(
585
- target: new (...args: never[]) => unknown,
632
+ target: abstract new (...args: never[]) => unknown,
586
633
  runtimeValues?: unknown[],
587
634
  ): Promise<unknown> {
588
635
  // Everything resolved inside is a dependency OF `target`, which is what a
@@ -599,12 +646,14 @@ export class Container {
599
646
  }
600
647
 
601
648
  async #constructWithDeps(
602
- target: new (...args: never[]) => unknown,
649
+ target: abstract new (...args: never[]) => unknown,
603
650
  runtimeValues?: unknown[],
604
651
  ): Promise<unknown> {
605
652
  const metadata = getServiceMetadata(target)
606
653
  const scope = metadata?.scope ?? 'transient'
607
- const key = metadata?.as ?? target.name
654
+ // The class itself, not `target.name`: two classes called `Service` share a
655
+ // name and must not share a singleton cache entry.
656
+ const key = metadata?.as ?? target
608
657
 
609
658
  if (scope === 'singleton' && this.#singletons.has(key)) {
610
659
  return this.#singletons.get(key)
@@ -674,7 +723,12 @@ export class Container {
674
723
  )
675
724
  }
676
725
  // (d) Genuine zero-argument class.
677
- const instance = new target()
726
+ //
727
+ // `Reflect.construct`, not `new target()`: a token may be an abstract
728
+ // class (that is what a contextual binding binds against), and `new` on
729
+ // one is a compile error even though the runtime constructs it happily —
730
+ // `abstract` exists only at compile time.
731
+ const instance = Reflect.construct(target, [])
678
732
  this.#cacheIfAppWide(scope, key, instance, store, readsBefore)
679
733
  return instance
680
734
  }
@@ -712,7 +766,7 @@ export class Container {
712
766
  */
713
767
  #cacheIfAppWide(
714
768
  scope: string,
715
- key: string,
769
+ key: ServiceToken,
716
770
  instance: unknown,
717
771
  store: ResolutionChain | undefined,
718
772
  readsBefore: number,
@@ -722,39 +776,36 @@ export class Container {
722
776
  this.#singletons.set(key, instance)
723
777
  }
724
778
 
725
- #tokenToKey(token: ServiceToken): string {
726
- if (typeof token === 'string') {
727
- this.#assertNotReserved(token, 'string')
728
- return token
729
- }
730
- if (typeof token === 'symbol') {
731
- // Reject unique `Symbol(x)` — two `Symbol("foo")` calls create distinct
732
- // symbols, so an override registered with one would be invisible to a
733
- // resolver holding the other. Force callers onto `Symbol.for(x)` which
734
- // is the global registry and round-trips across modules.
735
- if (Symbol.keyFor(token) === undefined) {
736
- throw new ReamError(
737
- 'E_CONTAINER_INVALID_TOKEN',
738
- `Unique Symbol() tokens are not supported — use Symbol.for("${token.description ?? 'name'}") so different modules see the same symbol.`,
739
- )
740
- }
741
- const desc = token.description
742
- if (desc === undefined) {
743
- throw new ReamError('E_CONTAINER_INVALID_TOKEN', 'Symbol tokens must have a description.')
744
- }
745
- this.#assertNotReserved(desc, 'symbol description')
746
- return `Symbol(${desc})`
747
- }
748
- return token.name
779
+ /**
780
+ * Validate a token and hand back the value every map keys on: the token.
781
+ *
782
+ * Fold's stores are `Map<string | symbol | AbstractConstructor, …>`, so a
783
+ * token IS its own key and identity is preserved. Deriving a string key
784
+ * instead cost exactly what identity buys: `Symbol.for('cache')` became
785
+ * `"Symbol(cache)"` and collided with the plain string `"Symbol(cache)"`,
786
+ * and a unique `Symbol()` had to be refused because its description is all a
787
+ * string key can carry. Both go away when the token is the key.
788
+ */
789
+ #tokenToKey(token: ServiceToken): ServiceToken {
790
+ if (typeof token === 'string') this.#assertNotReserved(token)
791
+ return token
749
792
  }
750
793
 
751
- #assertNotReserved(name: string, kind: 'string' | 'symbol description'): void {
794
+ /**
795
+ * NAMED DEVIATION from Fold, kept deliberately.
796
+ *
797
+ * These three read as prototype keys, and a token named after one is far
798
+ * likelier to be a mistake than a deliberate binding. Nothing indexes tokens
799
+ * on a plain object any more — every store above is a `Map` — so this is a
800
+ * tripwire for a caller error, not a pollution guard.
801
+ */
802
+ #assertNotReserved(name: string): void {
752
803
  if (RESERVED_TOKEN_NAMES.has(name)) {
753
804
  throw new ReamError(
754
805
  'E_CONTAINER_RESERVED_TOKEN',
755
806
  `Reserved container token name '${name}'.`,
756
807
  {
757
- hint: `'${name}' would collide with an Object.prototype key. Pick a different ${kind}.`,
808
+ hint: `'${name}' reads as an Object.prototype key. Pick a different token name.`,
758
809
  },
759
810
  )
760
811
  }
@@ -781,6 +832,18 @@ const NON_INJECTABLE_CONSTRUCTORS = new Set<unknown>([
781
832
  Error,
782
833
  ])
783
834
 
835
+ /**
836
+ * A token as it reads in an error message or in `ream inspect`.
837
+ *
838
+ * Display only — the container keys on the token itself, so two distinct
839
+ * classes that share a name are two bindings that happen to print alike.
840
+ */
841
+ function describeToken(token: ServiceToken): string {
842
+ if (typeof token === 'string') return token
843
+ if (typeof token === 'symbol') return token.toString()
844
+ return token.name === '' ? 'AnonymousClass' : token.name
845
+ }
846
+
784
847
  /** True for a real, injectable class token (excludes native primitive constructors). */
785
848
  function isInjectableClass(value: unknown): value is new (...args: never[]) => unknown {
786
849
  return typeof value === 'function' && !NON_INJECTABLE_CONSTRUCTORS.has(value)
@@ -20,7 +20,7 @@ import type { ServiceToken } from './types.js'
20
20
  export class ContainerResolver {
21
21
  readonly #container: Container
22
22
  /** Values bound here, keyed the way the container keys its tokens. */
23
- readonly #values = new Map<string, unknown>()
23
+ readonly #values = new Map<ServiceToken, unknown>()
24
24
 
25
25
  constructor(container: Container) {
26
26
  this.#container = container
@@ -16,7 +16,17 @@ export interface ServiceMetadata {
16
16
  /** Token used to resolve a service — class constructor, string name, or registered symbol. */
17
17
  // `never[]` accepts every constructor shape — parameters are contravariant, so
18
18
  // a rest of `never` is assignable from any concrete list.
19
- export type ServiceToken = (new (...args: never[]) => unknown) | string | symbol
19
+ /**
20
+ * What the container accepts as a token.
21
+ *
22
+ * `abstract new`, not `new`: binding an implementation against an abstract base
23
+ * is the whole point of a contextual binding — `@Inject(Hash)` where `Hash` is
24
+ * the abstract class and `Bcrypt` is what the container provides. An abstract
25
+ * class has no construct signature, so spelt `new (...)` the type rejected
26
+ * exactly the pattern the feature exists for. `abstract new` admits concrete
27
+ * classes too.
28
+ */
29
+ export type ServiceToken = (abstract new (...args: never[]) => unknown) | string | symbol
20
30
 
21
31
  /**
22
32
  * What a binding factory is handed so it can resolve its own dependencies
@@ -14,7 +14,7 @@ const INJECT_METADATA_KEY = Symbol('ream:inject')
14
14
  // `never[]` accepts every constructor shape: parameters are contravariant, so a
15
15
  // rest of `never` is assignable from any concrete list. Same spelling as the
16
16
  // router's, so a class flows between the two without an assertion.
17
- type AnyConstructor = new (...args: never[]) => unknown
17
+ type AnyConstructor = abstract new (...args: never[]) => unknown
18
18
 
19
19
  /** Registry of all decorated services. */
20
20
  const serviceRegistry: Map<AnyConstructor, ServiceMetadata> = new Map()
package/src/env/Env.ts CHANGED
@@ -108,9 +108,11 @@ export class Env<Values extends Record<string, unknown>> {
108
108
  validate(values: Record<string, string | undefined>): EnvRecord<S> {
109
109
  const failures: string[] = []
110
110
  const collected: Record<string, unknown> = {}
111
- for (const key of Object.keys(schema)) {
111
+ // `entries` rather than `keys` plus a lookup: the validator comes back
112
+ // already known to be there instead of read again as maybe-missing.
113
+ for (const [key, node] of Object.entries(schema)) {
112
114
  try {
113
- collected[key] = schema[key].validate(key, values[key])
115
+ collected[key] = node.validate(key, values[key])
114
116
  } catch (err) {
115
117
  failures.push(err instanceof Error ? err.message : String(err))
116
118
  }
@@ -46,9 +46,11 @@ export function interpolate(value: string, lookup: (name: string) => string | un
46
46
  })
47
47
 
48
48
  const prefixed = /^([A-Za-z_][A-Za-z0-9_]*):([\s\S]*)$/.exec(substituted)
49
- if (prefixed) {
50
- const resolver = identifiers.get(prefixed[1])
51
- if (resolver) return resolver(prefixed[2])
49
+ // Both groups are required by the pattern, so a match carries both.
50
+ const [, identifier, rest] = prefixed ?? []
51
+ if (identifier !== undefined && rest !== undefined) {
52
+ const resolver = identifiers.get(identifier)
53
+ if (resolver) return resolver(rest)
52
54
  }
53
55
  return substituted
54
56
  }
@@ -14,23 +14,43 @@ export function levenshtein(a: string, b: string): number {
14
14
  if (m === 0) return n
15
15
  if (n === 0) return m
16
16
 
17
- const dp: number[][] = Array.from({ length: m + 1 }, () => Array<number>(n + 1).fill(0))
18
-
19
- for (let i = 0; i <= m; i++) dp[i][0] = i
20
- for (let j = 0; j <= n; j++) dp[0][j] = j
17
+ // Two rows rather than the whole matrix: the recurrence only ever reads the
18
+ // row above and the cell to the left, so the rest was allocated and never
19
+ // looked at again.
20
+ let previous: number[] = Array.from({ length: n + 1 }, (_, j) => j)
21
21
 
22
22
  for (let i = 1; i <= m; i++) {
23
+ const current: number[] = [i]
23
24
  for (let j = 1; j <= n; j++) {
24
25
  const cost = a[i - 1] === b[j - 1] ? 0 : 1
25
- dp[i][j] = Math.min(
26
- dp[i - 1][j] + 1, // deletion
27
- dp[i][j - 1] + 1, // insertion
28
- dp[i - 1][j - 1] + cost, // substitution
26
+ current.push(
27
+ Math.min(
28
+ cell(previous, j) + 1, // deletion
29
+ cell(current, j - 1) + 1, // insertion
30
+ cell(previous, j - 1) + cost, // substitution
31
+ ),
29
32
  )
30
33
  }
34
+ previous = current
31
35
  }
32
36
 
33
- return dp[m][n]
37
+ return cell(previous, n)
38
+ }
39
+
40
+ /**
41
+ * One cell of a row that has already been filled.
42
+ *
43
+ * Each row is built left to right and every column is written before it is
44
+ * read, so a miss cannot happen — and this is where that invariant is stated
45
+ * rather than asserted past it. Reading the row directly would have every
46
+ * access typed "a number, or nothing", which is not what the algorithm knows.
47
+ */
48
+ function cell(row: number[], index: number): number {
49
+ const value = row[index]
50
+ if (value === undefined) {
51
+ throw new RangeError(`levenshtein: column ${index} was read before it was written`)
52
+ }
53
+ return value
34
54
  }
35
55
 
36
56
  /**
@@ -81,12 +101,13 @@ export function findClosestMatches(
81
101
  * @returns Formatted suggestion string, or empty string if no matches
82
102
  */
83
103
  export function didYouMean(input: string, candidates: string[]): string {
84
- const matches = findClosestMatches(input, candidates)
85
- if (matches.length === 0) return ''
104
+ const [best, ...rest] = findClosestMatches(input, candidates)
105
+ if (best === undefined) return ''
86
106
 
87
- if (matches.length === 1) {
88
- return `Did you mean '${matches[0].candidate}'?`
107
+ if (rest.length === 0) {
108
+ return `Did you mean '${best.candidate}'?`
89
109
  }
110
+ const matches = [best, ...rest]
90
111
 
91
112
  const suggestions = matches.map((m) => `'${m.candidate}'`).join(', ')
92
113
  return `Did you mean one of: ${suggestions}?`
@@ -42,9 +42,11 @@ export function createPipelineError(
42
42
  ): ReamError {
43
43
  const total = PIPELINE_STAGES.length
44
44
  const clampedIndex = Math.max(0, Math.min(stageIndex, total - 1))
45
- const stageName = PIPELINE_STAGES[clampedIndex]
45
+ // The index is clamped into the list just above, so the lookup lands — the
46
+ // fallback is what says so without asserting past the read.
47
+ const stageName = PIPELINE_STAGES[clampedIndex] ?? '(unknown)'
46
48
  const position = `${clampedIndex + 1}/${total}`
47
- const nextStage = clampedIndex + 1 < total ? PIPELINE_STAGES[clampedIndex + 1] : '(end)'
49
+ const nextStage = PIPELINE_STAGES[clampedIndex + 1] ?? '(end)'
48
50
 
49
51
  const errorCode = originalError instanceof ReamError ? originalError.code : 'E_PIPELINE_ERROR'
50
52
 
@@ -23,14 +23,25 @@ const correlationStorage = new AsyncLocalStorage<string | undefined>()
23
23
  * Listener class interface — must have a handle() method.
24
24
  */
25
25
  export interface ListenerClass<T = unknown> {
26
- handle(event: T): Promise<void> | void
26
+ handle(event: T): unknown
27
27
  }
28
28
 
29
29
  type EventConstructor<T = unknown> = new (...args: never[]) => T
30
30
 
31
- type ListenerFn<T = unknown> = (event: T) => Promise<void> | void
31
+ /**
32
+ * A listener. Whatever it returns is awaited and dropped.
33
+ *
34
+ * `unknown`, not `Promise<void> | void`: a bare `void` return type accepts a
35
+ * function returning anything, but a UNION containing it does not — so
36
+ * `emitter.on('x', (d) => received.push(d))` was a type error for returning
37
+ * what `push` returns, which nothing reads.
38
+ */
39
+ type ListenerFn<T = unknown> = (event: T) => unknown
32
40
 
33
- type ListenerConstructor<T = unknown> = new (...args: never[]) => ListenerClass<T>
41
+ // `abstract new`: a listener bound against an abstract base is as valid a
42
+ // registration as one bound against a concrete class, and the container builds
43
+ // either.
44
+ type ListenerConstructor<T = unknown> = abstract new (...args: never[]) => ListenerClass<T>
34
45
 
35
46
  type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
36
47
 
@@ -41,7 +52,16 @@ type Listener<T = unknown> = ListenerFn<T> | ListenerConstructor<T>
41
52
  * satisfies it.
42
53
  */
43
54
  export interface EmitterResolver {
44
- make<T>(target: new (...args: never[]) => T): Promise<T>
55
+ /**
56
+ * Build a listener.
57
+ *
58
+ * Not generic: a generic method must work for every `T`, so nothing but a
59
+ * real container could implement it — the fake in the tests could not, and
60
+ * this file's own call site reached for `as ListenerConstructor<T>` to get
61
+ * past it. What comes back is checked below, which is what a container's
62
+ * answer deserves anyway.
63
+ */
64
+ make(target: abstract new (...args: never[]) => object): Promise<object>
45
65
  }
46
66
 
47
67
  /**
@@ -327,9 +347,15 @@ export class Emitter {
327
347
  if (isListenerClass(listener)) {
328
348
  // Listener class — resolve via container for @inject() support
329
349
  const instance = this.#resolver
330
- ? await this.#resolver.make(listener as ListenerConstructor<T>)
331
- : new (listener as ListenerConstructor<T>)()
332
- await instance.handle(event)
350
+ ? await this.#resolver.make(listener)
351
+ : Reflect.construct(listener, [])
352
+ const handle = Reflect.get(instance, 'handle')
353
+ if (typeof handle !== 'function') {
354
+ throw new Error(
355
+ `Listener ${String(Reflect.get(listener, 'name'))} resolved to something with no handle()`,
356
+ )
357
+ }
358
+ await handle.call(instance, event)
333
359
  } else {
334
360
  await (listener as ListenerFn<T>)(event)
335
361
  }
@@ -651,7 +677,7 @@ function classToEventName(cls: EventConstructor): string {
651
677
  }
652
678
 
653
679
  /** Check if a listener entry is a class (has prototype.handle) vs a function. */
654
- function isListenerClass(listener: Listener): boolean {
680
+ function isListenerClass(listener: Listener): listener is ListenerConstructor {
655
681
  return typeof listener === 'function' && typeof listener.prototype?.handle === 'function'
656
682
  }
657
683
 
@@ -41,10 +41,17 @@ export type FakeBusPredicateArg = FakeBusPredicate | ((event: CapturedEmit) => b
41
41
 
42
42
  interface Subscription {
43
43
  pattern: string
44
- /** Subscribers may return `void` (sync) or `PromiseLike<void>` (async).
45
- * Async rejections are logged via `console.error` to keep them from
46
- * escaping to `unhandledRejection`. */
47
- callback: (eventJson: string) => void | PromiseLike<void>
44
+ /**
45
+ * Subscribers may be sync or async; whatever they return is awaited and
46
+ * dropped, and an async rejection is logged via `console.error` rather than
47
+ * escaping to `unhandledRejection`.
48
+ *
49
+ * `unknown`, not `void | PromiseLike<void>`: a bare `void` return type
50
+ * accepts a function returning anything, but a UNION containing it does not
51
+ * — so widening the native bus's `=> void` to that union made it NARROWER in
52
+ * practice, and `(json) => received.push(json)` stopped compiling.
53
+ */
54
+ callback: (eventJson: string) => unknown
48
55
  }
49
56
 
50
57
  /** Request handler may return `void` (sync, fire-and-forget after `reply`)
@@ -106,7 +113,7 @@ export class FakeBus {
106
113
  return Promise.resolve('ok')
107
114
  }
108
115
 
109
- subscribe(pattern: string, callback: (eventJson: string) => void | PromiseLike<void>): number {
116
+ subscribe(pattern: string, callback: (eventJson: string) => unknown): number {
110
117
  const id = this.#nextSubId++
111
118
  this.#subscribers.set(id, { pattern, callback })
112
119
  return id