@c9up/ream 0.1.32 → 0.1.33

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 (188) hide show
  1. package/dist/Application.d.ts +27 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +29 -0
  4. package/dist/Application.js.map +1 -1
  5. package/dist/ErrorBoundary.d.ts.map +1 -1
  6. package/dist/ErrorBoundary.js +13 -1
  7. package/dist/ErrorBoundary.js.map +1 -1
  8. package/dist/HttpKernel.d.ts +8 -0
  9. package/dist/HttpKernel.d.ts.map +1 -1
  10. package/dist/HttpKernel.js +35 -12
  11. package/dist/HttpKernel.js.map +1 -1
  12. package/dist/Ignitor.d.ts +8 -0
  13. package/dist/Ignitor.d.ts.map +1 -1
  14. package/dist/Ignitor.js +39 -0
  15. package/dist/Ignitor.js.map +1 -1
  16. package/dist/bodyparser/BodyParserMiddleware.d.ts +11 -4
  17. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  18. package/dist/bodyparser/BodyParserMiddleware.js +55 -43
  19. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  20. package/dist/bodyparser/qsParse.d.ts +52 -0
  21. package/dist/bodyparser/qsParse.d.ts.map +1 -0
  22. package/dist/bodyparser/qsParse.js +161 -0
  23. package/dist/bodyparser/qsParse.js.map +1 -0
  24. package/dist/bootstrap/hyper-server.d.ts.map +1 -1
  25. package/dist/bootstrap/hyper-server.js +2 -8
  26. package/dist/bootstrap/hyper-server.js.map +1 -1
  27. package/dist/container/Container.d.ts +41 -0
  28. package/dist/container/Container.d.ts.map +1 -1
  29. package/dist/container/Container.js +193 -26
  30. package/dist/container/Container.js.map +1 -1
  31. package/dist/container/ContainerResolver.d.ts +46 -0
  32. package/dist/container/ContainerResolver.d.ts.map +1 -0
  33. package/dist/container/ContainerResolver.js +60 -0
  34. package/dist/container/ContainerResolver.js.map +1 -0
  35. package/dist/container/ContextualBindingsBuilder.d.ts +20 -0
  36. package/dist/container/ContextualBindingsBuilder.d.ts.map +1 -0
  37. package/dist/container/ContextualBindingsBuilder.js +31 -0
  38. package/dist/container/ContextualBindingsBuilder.js.map +1 -0
  39. package/dist/container/types.d.ts +9 -6
  40. package/dist/container/types.d.ts.map +1 -1
  41. package/dist/decorators/Service.d.ts +1 -5
  42. package/dist/decorators/Service.d.ts.map +1 -1
  43. package/dist/decorators/Service.js +10 -1
  44. package/dist/decorators/Service.js.map +1 -1
  45. package/dist/events/Emitter.d.ts +66 -6
  46. package/dist/events/Emitter.d.ts.map +1 -1
  47. package/dist/events/Emitter.js +165 -28
  48. package/dist/events/Emitter.js.map +1 -1
  49. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  50. package/dist/graphql/GraphQLEngine.js +5 -5
  51. package/dist/graphql/GraphQLEngine.js.map +1 -1
  52. package/dist/helpers/duration.d.ts +32 -0
  53. package/dist/helpers/duration.d.ts.map +1 -0
  54. package/dist/helpers/duration.js +74 -0
  55. package/dist/helpers/duration.js.map +1 -0
  56. package/dist/helpers/napi-loader.d.ts +10 -1
  57. package/dist/helpers/napi-loader.d.ts.map +1 -1
  58. package/dist/helpers/napi-loader.js +18 -3
  59. package/dist/helpers/napi-loader.js.map +1 -1
  60. package/dist/helpers/types.d.ts +1 -1
  61. package/dist/helpers/types.d.ts.map +1 -1
  62. package/dist/http/Exception.d.ts.map +1 -1
  63. package/dist/http/Exception.js +17 -5
  64. package/dist/http/Exception.js.map +1 -1
  65. package/dist/http/HttpContext.d.ts +16 -5
  66. package/dist/http/HttpContext.d.ts.map +1 -1
  67. package/dist/http/HttpContext.js +4 -2
  68. package/dist/http/HttpContext.js.map +1 -1
  69. package/dist/http/RedirectBuilder.d.ts +48 -2
  70. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  71. package/dist/http/RedirectBuilder.js +95 -14
  72. package/dist/http/RedirectBuilder.js.map +1 -1
  73. package/dist/http/Request.d.ts +128 -8
  74. package/dist/http/Request.d.ts.map +1 -1
  75. package/dist/http/Request.js +259 -8
  76. package/dist/http/Request.js.map +1 -1
  77. package/dist/http/Response.d.ts +74 -3
  78. package/dist/http/Response.d.ts.map +1 -1
  79. package/dist/http/Response.js +177 -4
  80. package/dist/http/Response.js.map +1 -1
  81. package/dist/index.d.ts +9 -3
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +5 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/router/Router.d.ts +36 -7
  86. package/dist/router/Router.d.ts.map +1 -1
  87. package/dist/router/Router.js +66 -22
  88. package/dist/router/Router.js.map +1 -1
  89. package/dist/security/CookieSigner.d.ts +35 -4
  90. package/dist/security/CookieSigner.d.ts.map +1 -1
  91. package/dist/security/CookieSigner.js +99 -10
  92. package/dist/security/CookieSigner.js.map +1 -1
  93. package/dist/server/Server.d.ts +2 -3
  94. package/dist/server/Server.d.ts.map +1 -1
  95. package/dist/server/Server.js +30 -13
  96. package/dist/server/Server.js.map +1 -1
  97. package/dist/session/ReadOnlyValuesStore.d.ts +24 -0
  98. package/dist/session/ReadOnlyValuesStore.d.ts.map +1 -0
  99. package/dist/session/ReadOnlyValuesStore.js +45 -0
  100. package/dist/session/ReadOnlyValuesStore.js.map +1 -0
  101. package/dist/session/Session.d.ts +181 -9
  102. package/dist/session/Session.d.ts.map +1 -1
  103. package/dist/session/Session.js +284 -33
  104. package/dist/session/Session.js.map +1 -1
  105. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  106. package/dist/session/SessionMiddleware.js +87 -33
  107. package/dist/session/SessionMiddleware.js.map +1 -1
  108. package/dist/session/drivers/DatabaseDriver.d.ts +66 -0
  109. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -0
  110. package/dist/session/drivers/DatabaseDriver.js +132 -0
  111. package/dist/session/drivers/DatabaseDriver.js.map +1 -0
  112. package/dist/session/drivers/FileDriver.d.ts +22 -0
  113. package/dist/session/drivers/FileDriver.d.ts.map +1 -0
  114. package/dist/session/drivers/FileDriver.js +66 -0
  115. package/dist/session/drivers/FileDriver.js.map +1 -0
  116. package/dist/session/drivers/MemoryDriver.d.ts +10 -2
  117. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  118. package/dist/session/drivers/MemoryDriver.js +70 -0
  119. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  120. package/dist/session/drivers/RedisDriver.d.ts +10 -2
  121. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  122. package/dist/session/drivers/RedisDriver.js +58 -0
  123. package/dist/session/drivers/RedisDriver.js.map +1 -1
  124. package/dist/session/templateTags.d.ts +42 -0
  125. package/dist/session/templateTags.d.ts.map +1 -0
  126. package/dist/session/templateTags.js +101 -0
  127. package/dist/session/templateTags.js.map +1 -0
  128. package/dist/testing/RequestBuilder.d.ts +38 -1
  129. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  130. package/dist/testing/RequestBuilder.js +40 -1
  131. package/dist/testing/RequestBuilder.js.map +1 -1
  132. package/dist/testing/TestClient.d.ts +3 -1
  133. package/dist/testing/TestClient.d.ts.map +1 -1
  134. package/dist/testing/TestClient.js +3 -1
  135. package/dist/testing/TestClient.js.map +1 -1
  136. package/dist/types/helpers.d.ts +1 -1
  137. package/dist/types/helpers.d.ts.map +1 -1
  138. package/dist/utils/Macroable.d.ts +14 -5
  139. package/dist/utils/Macroable.d.ts.map +1 -1
  140. package/dist/utils/Macroable.js +20 -18
  141. package/dist/utils/Macroable.js.map +1 -1
  142. package/events.win32-x64-msvc.node +0 -0
  143. package/index.win32-x64-msvc.node +0 -0
  144. package/package.json +1 -1
  145. package/scheduler.win32-x64-msvc.node +0 -0
  146. package/src/Application.ts +39 -0
  147. package/src/ErrorBoundary.ts +13 -1
  148. package/src/HttpKernel.ts +45 -19
  149. package/src/Ignitor.ts +39 -0
  150. package/src/bodyparser/BodyParserMiddleware.ts +66 -44
  151. package/src/bodyparser/qsParse.ts +179 -0
  152. package/src/bootstrap/hyper-server.ts +2 -9
  153. package/src/container/Container.ts +265 -25
  154. package/src/container/ContainerResolver.ts +84 -0
  155. package/src/container/ContextualBindingsBuilder.ts +41 -0
  156. package/src/container/types.ts +11 -7
  157. package/src/decorators/Service.ts +16 -3
  158. package/src/events/Emitter.ts +200 -25
  159. package/src/graphql/GraphQLEngine.ts +7 -8
  160. package/src/helpers/duration.ts +71 -0
  161. package/src/helpers/napi-loader.ts +19 -3
  162. package/src/helpers/types.ts +4 -1
  163. package/src/http/Exception.ts +17 -5
  164. package/src/http/HttpContext.ts +20 -5
  165. package/src/http/RedirectBuilder.ts +126 -11
  166. package/src/http/Request.ts +283 -13
  167. package/src/http/Response.ts +213 -6
  168. package/src/index.ts +17 -3
  169. package/src/router/Router.ts +98 -33
  170. package/src/security/CookieSigner.ts +102 -10
  171. package/src/server/Server.ts +36 -16
  172. package/src/session/ReadOnlyValuesStore.ts +52 -0
  173. package/src/session/Session.ts +364 -37
  174. package/src/session/SessionMiddleware.ts +93 -30
  175. package/src/session/drivers/DatabaseDriver.ts +184 -0
  176. package/src/session/drivers/FileDriver.ts +82 -0
  177. package/src/session/drivers/MemoryDriver.ts +75 -2
  178. package/src/session/drivers/RedisDriver.ts +77 -2
  179. package/src/session/templateTags.ts +124 -0
  180. package/src/testing/RequestBuilder.ts +76 -0
  181. package/src/testing/TestClient.ts +10 -2
  182. package/src/types/helpers.ts +4 -1
  183. package/src/utils/Macroable.ts +23 -8
  184. package/dist/utils/inflect.d.ts +0 -17
  185. package/dist/utils/inflect.d.ts.map +0 -1
  186. package/dist/utils/inflect.js +0 -32
  187. package/dist/utils/inflect.js.map +0 -1
  188. package/src/utils/inflect.ts +0 -29
@@ -22,16 +22,9 @@ import { createRequire } from 'node:module'
22
22
  import { dirname, join } from 'node:path'
23
23
  import { arch, platform } from 'node:process'
24
24
  import { fileURLToPath } from 'node:url'
25
+ import { NAPI_PLATFORM_MAP } from '../helpers/napi-loader.js'
25
26
  import type { HyperServerLike } from '../Ignitor.js'
26
27
 
27
- const PLATFORM_SUFFIX: Record<string, string> = {
28
- 'linux-x64': 'linux-x64-gnu',
29
- 'linux-arm64': 'linux-arm64-gnu',
30
- 'darwin-x64': 'darwin-x64',
31
- 'darwin-arm64': 'darwin-arm64',
32
- 'win32-x64': 'win32-x64-msvc',
33
- }
34
-
35
28
  interface HyperServerCtor {
36
29
  new (port: number, host?: string): HyperServerLike
37
30
  }
@@ -57,7 +50,7 @@ function isHyperServerNapi(value: unknown): value is HyperServerNapiModule {
57
50
  export function createHyperServerFactory():
58
51
  | ((port: number, host?: string) => HyperServerLike)
59
52
  | undefined {
60
- const suffix = PLATFORM_SUFFIX[`${platform}-${arch}`]
53
+ const suffix = NAPI_PLATFORM_MAP[`${platform}-${arch}`]
61
54
  if (!suffix) return undefined
62
55
 
63
56
  const require2 = createRequire(import.meta.url)
@@ -19,6 +19,8 @@ import {
19
19
  } from '../decorators/Service.js'
20
20
  import { didYouMean } from '../errors/FuzzyMatcher.js'
21
21
  import { ReamError } from '../errors/ReamError.js'
22
+ import { ContainerResolver } from './ContainerResolver.js'
23
+ import { ContextualBindingsBuilder } from './ContextualBindingsBuilder.js'
22
24
  import type { Binding, ServiceFactory, ServiceToken } from './types.js'
23
25
 
24
26
  /**
@@ -32,11 +34,52 @@ const RESERVED_TOKEN_NAMES = new Set(['__proto__', 'constructor', 'prototype'])
32
34
  interface ResolutionChain {
33
35
  stack: string[]
34
36
  set: Set<string>
37
+ /**
38
+ * Values bound on the {@link ContainerResolver} that opened this chain.
39
+ *
40
+ * Carried on the chain, not passed down each private method, so a value
41
+ * bound for one request is visible to everything that resolution triggers —
42
+ * a controller's constructor dependency sees the same `HttpContext` the
43
+ * middleware bound — and to nothing outside it.
44
+ */
45
+ values?: ReadonlyMap<string, unknown>
46
+ /**
47
+ * How many scoped values this chain has handed out so far.
48
+ *
49
+ * A singleton built while a request's values are in scope may have captured
50
+ * one — cache it app-wide and every later request gets the first request's
51
+ * `HttpContext`. Comparing this counter before and after a build says whether
52
+ * it actually read one, so a singleton that touched no request state still
53
+ * caches exactly as before.
54
+ */
55
+ scopedReads?: number
56
+ /**
57
+ * The class whose dependencies are being resolved right now.
58
+ *
59
+ * A contextual binding answers "when THIS class asks for that, give it
60
+ * this", so the lookup needs the direct dependent. Carried on the chain for
61
+ * the same reason the scoped values are: `#autoConstruct` knows it, and
62
+ * every method between there and the lookup would otherwise have to pass it
63
+ * down. Saved and restored around each construction, so a dependency's own
64
+ * dependencies see IT as their parent, not its parent.
65
+ */
66
+ parent?: new (
67
+ ...args: never[]
68
+ ) => unknown
35
69
  }
36
70
 
37
71
  export class Container {
38
72
  #bindings: Map<string, Binding> = new Map()
39
73
  #singletons: Map<string, unknown> = new Map()
74
+ /**
75
+ * Singletons currently being built, by key.
76
+ *
77
+ * The factory is async, so two resolves at cold start both get past the
78
+ * cache check and both run it — producing two instances where the whole
79
+ * point is one, and silently discarding the first (a second connection
80
+ * pool, a second scheduler). Concurrent callers await the same promise.
81
+ */
82
+ #pendingSingletons: Map<string, Promise<unknown>> = new Map()
40
83
  #overrides: Map<string, ServiceFactory> = new Map()
41
84
  /**
42
85
  * Snapshot of singletons taken when `swap`/`override` shadows a previously
@@ -47,6 +90,9 @@ export class Container {
47
90
  #singletonBackup: Map<string, unknown> = new Map()
48
91
  /** Alias key → the token it forwards to (AdonisJS `container.alias`). */
49
92
  #aliases: Map<string, ServiceToken> = new Map()
93
+ /** parent class → (binding key → the factory that class gets instead). */
94
+ #contextualBindings: Map<new (...args: never[]) => unknown, Map<string, ServiceFactory>> =
95
+ new Map()
50
96
  /** token key → post-resolution callbacks (AdonisJS `container.resolving`). */
51
97
  #resolvingHooks: Map<string, Array<(value: unknown) => void | Promise<void>>> = new Map()
52
98
  /**
@@ -81,6 +127,33 @@ export class Container {
81
127
  this.#bindings.set(key, { token, factory: () => value, scope: 'singleton', dependencies: [] })
82
128
  }
83
129
 
130
+ /**
131
+ * Give ONE class a different implementation of a dependency (AdonisJS
132
+ * `container.contextualBinding`).
133
+ *
134
+ * `contextualBinding(UsersController, Hash, () => new Argon2())` — everyone
135
+ * else asking for `Hash` still gets the container's binding. See
136
+ * {@link when} for the fluent spelling.
137
+ */
138
+ contextualBinding(
139
+ parent: new (...args: never[]) => unknown,
140
+ binding: ServiceToken,
141
+ factory: ServiceFactory,
142
+ ): void {
143
+ const forParent = this.#contextualBindings.get(parent) ?? new Map()
144
+ forParent.set(this.#tokenToKey(binding), factory)
145
+ this.#contextualBindings.set(parent, forParent)
146
+ }
147
+
148
+ /**
149
+ * Fluent contextual binding (AdonisJS `container.when`):
150
+ *
151
+ * container.when(UsersController).asksFor(Hash).provide(() => new Argon2())
152
+ */
153
+ when(parent: new (...args: never[]) => unknown): ContextualBindingsBuilder {
154
+ return new ContextualBindingsBuilder(this, parent)
155
+ }
156
+
84
157
  /**
85
158
  * Register an alias — resolving `alias` forwards to `target` (AdonisJS
86
159
  * `container.alias('db', Database)`). The target may be a class, string, or
@@ -176,11 +249,30 @@ export class Container {
176
249
  * res])`); only the `undefined` slots are container-resolved, top-level only.
177
250
  */
178
251
  async resolve<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
179
- // Dereference an alias to its target before anything else.
180
- const aliasTarget = this.#aliases.get(this.#tokenToKey(token))
181
- if (aliasTarget !== undefined) return this.resolve<T>(aliasTarget, runtimeValues)
182
-
183
- const key = this.#tokenToKey(token)
252
+ // Dereference aliases to the final target before anything else. Walked in
253
+ // a LOOP with its own seen-set: recursing into `resolve()` re-entered
254
+ // before the cycle guard below was armed, so `alias('a','b')` +
255
+ // `alias('b','a')` overflowed the stack instead of naming the cycle.
256
+ let resolvedToken = token
257
+ let key = this.#tokenToKey(resolvedToken)
258
+ const seenAliases = new Set<string>([key])
259
+ for (
260
+ let target = this.#aliases.get(key);
261
+ target !== undefined;
262
+ target = this.#aliases.get(key)
263
+ ) {
264
+ resolvedToken = target
265
+ key = this.#tokenToKey(target)
266
+ if (seenAliases.has(key)) {
267
+ const cycle = [...seenAliases, key].join(' → ')
268
+ throw new ReamError('CIRCULAR_DEPENDENCY', `Circular alias detected: ${cycle}`, {
269
+ hint: 'An alias chain must end at a real binding — remove one of the aliases.',
270
+ context: { chain: cycle },
271
+ })
272
+ }
273
+ seenAliases.add(key)
274
+ }
275
+ token = resolvedToken
184
276
 
185
277
  // Top of a chain: open one, so everything this resolution triggers shares
186
278
  // it and nothing outside it does.
@@ -219,10 +311,23 @@ export class Container {
219
311
  method: K,
220
312
  runtimeValues?: unknown[],
221
313
  ): Promise<unknown> {
222
- type Ctor = new (...args: unknown[]) => unknown
314
+ type Ctor = new (...args: never[]) => unknown
223
315
  // `Object.getPrototypeOf` is typed `any` in lib.dom, so the assignment
224
316
  // narrows it back to a typed constructor without a cast expression.
225
317
  const target: Ctor = Object.getPrototypeOf(instance).constructor
318
+ // A method's dependencies are dependencies of the class declaring it, so a
319
+ // contextual binding applies to `call()` exactly as to a constructor. There
320
+ // is usually no chain open here — `call()` is an entry point — so one is
321
+ // opened to carry the parent, rather than each `resolve()` below opening
322
+ // its own with nobody in it.
323
+ const chain = this.#chain.getStore()
324
+ if (chain === undefined) {
325
+ return this.#chain.run({ stack: [], set: new Set(), parent: target }, () =>
326
+ this.call(instance, method, runtimeValues),
327
+ )
328
+ }
329
+ const outerParent = chain.parent
330
+ chain.parent = target
226
331
  const paramTypes: unknown[] =
227
332
  Reflect.getMetadata('design:paramtypes', target.prototype, method) ?? []
228
333
  // `@Inject('token')` on a method parameter — a named binding the reflected
@@ -252,6 +357,8 @@ export class Container {
252
357
  args.push(isInjectableClass(type) ? await this.resolve(type) : undefined)
253
358
  }
254
359
 
360
+ chain.parent = outerParent
361
+
255
362
  const member: unknown = instance[method]
256
363
  if (!isCallable(member)) {
257
364
  throw new Error(`Container.call: '${method}' is not a callable method`)
@@ -259,6 +366,55 @@ export class Container {
259
366
  return member.apply(instance, args)
260
367
  }
261
368
 
369
+ /**
370
+ * A resolver whose {@link ContainerResolver.bindValue} values are isolated
371
+ * from this container (AdonisJS `container.createResolver()`).
372
+ *
373
+ * Anything the resolver builds — and everything that build depends on — sees
374
+ * those values; nothing outside it does. This is how a request binds its own
375
+ * `HttpContext` without every other request seeing it.
376
+ */
377
+ createResolver(): ContainerResolver {
378
+ return new ContainerResolver(this)
379
+ }
380
+
381
+ /**
382
+ * @internal Run `fn` with `values` layered onto the resolution chain.
383
+ *
384
+ * Reuses an open chain rather than starting a second one, so cycle detection
385
+ * still spans the whole resolution when a resolver is used inside a factory.
386
+ */
387
+ runWithScopedValues<T>(values: ReadonlyMap<string, unknown>, fn: () => Promise<T>): Promise<T> {
388
+ const chain = this.#chain.getStore()
389
+ return this.#chain.run(
390
+ chain === undefined ? { stack: [], set: new Set(), values } : { ...chain, values },
391
+ fn,
392
+ )
393
+ }
394
+
395
+ /** @internal Normalise a token the way the container keys it. */
396
+ keyFor(token: ServiceToken): string {
397
+ return this.#tokenToKey(token)
398
+ }
399
+
400
+ /**
401
+ * Resolve `token` as if `parent` had asked for it, so `parent`'s contextual
402
+ * bindings apply (AdonisJS `resolveFor`). `parent: null` means nobody asked —
403
+ * identical to {@link make}.
404
+ */
405
+ async resolveFor<T>(
406
+ parent: (new (...args: never[]) => unknown) | null,
407
+ token: ServiceToken,
408
+ runtimeValues?: unknown[],
409
+ ): Promise<T> {
410
+ if (parent === null) return this.make<T>(token, runtimeValues)
411
+ const chain = this.#chain.getStore()
412
+ return this.#chain.run(
413
+ chain === undefined ? { stack: [], set: new Set(), parent } : { ...chain, parent },
414
+ () => this.make<T>(token, runtimeValues),
415
+ )
416
+ }
417
+
262
418
  // ─── Introspection ────────────────────────────────────────
263
419
 
264
420
  /** Check if a token is registered or resolvable. */
@@ -307,31 +463,74 @@ export class Container {
307
463
  return swapped as T
308
464
  }
309
465
 
310
- // 2. Check cached singletons
466
+ // 2. A contextual binding for the class currently being built — "when
467
+ // UsersController asks for Hash, give it Argon2". Before the resolver's
468
+ // values and the container's, after swaps: @adonisjs/fold's order.
469
+ const store = this.#chain.getStore()
470
+ const contextual =
471
+ store?.parent === undefined ? undefined : this.#contextualBindings.get(store.parent)?.get(key)
472
+ if (contextual !== undefined) {
473
+ const instance = await contextual(this)
474
+ await this.#runResolvingHooks(key, instance)
475
+ return instance as T
476
+ }
477
+
478
+ // 3. A value bound on the resolver that opened this chain. Before the
479
+ // container's own values, after swaps — the order @adonisjs/fold
480
+ // resolves in (`resolveFor`: swaps, resolver values, container values).
481
+ // No `resolving` hooks: the value was not constructed here.
482
+ if (store?.values?.has(key)) {
483
+ store.scopedReads = (store.scopedReads ?? 0) + 1
484
+ return store.values.get(key) as T
485
+ }
486
+
487
+ // 4. Check cached singletons
311
488
  if (this.#singletons.has(key)) {
312
489
  return this.#singletons.get(key) as T
313
490
  }
314
491
 
315
- // 3. Check explicit bindings — the factory may be async.
492
+ // 5. Check explicit bindings — the factory may be async.
316
493
  const binding = this.#bindings.get(key)
317
494
  if (binding) {
318
- const instance = binding.factory ? await binding.factory(this) : undefined
319
- if (binding.scope === 'singleton') {
320
- this.#singletons.set(key, instance)
495
+ if (binding.scope !== 'singleton') {
496
+ const instance = binding.factory ? await binding.factory(this) : undefined
497
+ await this.#runResolvingHooks(key, instance)
498
+ // Boundary cast from `unknown` — the caller brands the resolved type via T.
499
+ return instance as T
500
+ }
501
+
502
+ // A build already under way: join it rather than start a second one.
503
+ const inFlight = this.#pendingSingletons.get(key)
504
+ if (inFlight) return (await inFlight) as T
505
+
506
+ const readsBefore = store?.scopedReads ?? 0
507
+ const building = (async (): Promise<unknown> => {
508
+ const instance = binding.factory ? await binding.factory(this) : undefined
509
+ // See `ResolutionChain.scopedReads`: a build that consumed a
510
+ // request-scoped value is that request's, not the application's.
511
+ this.#cacheIfAppWide('singleton', key, instance, store, readsBefore)
512
+ // Inside the promise, so the hooks run exactly once per instance no
513
+ // matter how many callers were waiting.
514
+ await this.#runResolvingHooks(key, instance)
515
+ return instance
516
+ })()
517
+ this.#pendingSingletons.set(key, building)
518
+ try {
519
+ return (await building) as T
520
+ } finally {
521
+ // Cleared either way: a failed build must not poison later attempts.
522
+ this.#pendingSingletons.delete(key)
321
523
  }
322
- await this.#runResolvingHooks(key, instance)
323
- // Boundary cast from `unknown` — the caller brands the resolved type via T.
324
- return instance as T
325
524
  }
326
525
 
327
- // 4. Auto-construct if it's a class
526
+ // 6. Auto-construct if it's a class
328
527
  if (typeof token === 'function') {
329
528
  const instance = (await this.#autoConstruct(token, runtimeValues)) as T
330
529
  await this.#runResolvingHooks(key, instance)
331
530
  return instance
332
531
  }
333
532
 
334
- // 5. Not found
533
+ // 7. Not found
335
534
  const allKeys = [...this.#bindings.keys(), ...this.#overrides.keys()]
336
535
  const suggestion = didYouMean(key, allKeys)
337
536
  throw new ReamError(
@@ -352,7 +551,24 @@ export class Container {
352
551
  * 3. No params → plain `new Class()`
353
552
  */
354
553
  async #autoConstruct(
355
- target: new (...args: unknown[]) => unknown,
554
+ target: new (...args: never[]) => unknown,
555
+ runtimeValues?: unknown[],
556
+ ): Promise<unknown> {
557
+ // Everything resolved inside is a dependency OF `target`, which is what a
558
+ // contextual binding keys on. Restored on EVERY exit — the construction has
559
+ // three returns and can throw — so `target`'s own parent is back in place.
560
+ const store = this.#chain.getStore()
561
+ const outerParent = store?.parent
562
+ if (store !== undefined) store.parent = target
563
+ try {
564
+ return await this.#constructWithDeps(target, runtimeValues)
565
+ } finally {
566
+ if (store !== undefined) store.parent = outerParent
567
+ }
568
+ }
569
+
570
+ async #constructWithDeps(
571
+ target: new (...args: never[]) => unknown,
356
572
  runtimeValues?: unknown[],
357
573
  ): Promise<unknown> {
358
574
  const metadata = getServiceMetadata(target)
@@ -363,6 +579,10 @@ export class Container {
363
579
  return this.#singletons.get(key)
364
580
  }
365
581
 
582
+ // Same rule as the explicit singleton bindings: see `scopedReads`.
583
+ const store = this.#chain.getStore()
584
+ const readsBefore = store?.scopedReads ?? 0
585
+
366
586
  // A runtime value at this index (from `make(Class, [req, res])`) wins over
367
587
  // container resolution for that constructor slot.
368
588
  const runtimeAt = (index: number): unknown => runtimeValues?.[index]
@@ -397,8 +617,8 @@ export class Container {
397
617
  const depToken = injectTokens.get(index)
398
618
  deps.push(depToken ? await this.resolve(depToken) : undefined)
399
619
  }
400
- const instance = new target(...deps)
401
- if (scope === 'singleton') this.#singletons.set(key, instance)
620
+ const instance = Reflect.construct(target, deps)
621
+ this.#cacheIfAppWide(scope, key, instance, store, readsBefore)
402
622
  return instance
403
623
  }
404
624
  // (b) No @Inject/metadata, but the caller supplied runtime values — build
@@ -406,8 +626,8 @@ export class Container {
406
626
  // constructor). Slots beyond the runtime values stay undefined.
407
627
  if (target.length > 0 && (runtimeValues?.length ?? 0) > 0) {
408
628
  const deps = Array.from({ length: target.length }, (_value, index) => runtimeAt(index))
409
- const instance = new target(...deps)
410
- if (scope === 'singleton') this.#singletons.set(key, instance)
629
+ const instance = Reflect.construct(target, deps)
630
+ this.#cacheIfAppWide(scope, key, instance, store, readsBefore)
411
631
  return instance
412
632
  }
413
633
  // (c) The constructor declares parameters but we have no way to resolve
@@ -424,7 +644,7 @@ export class Container {
424
644
  }
425
645
  // (d) Genuine zero-argument class.
426
646
  const instance = new target()
427
- if (scope === 'singleton') this.#singletons.set(key, instance)
647
+ this.#cacheIfAppWide(scope, key, instance, store, readsBefore)
428
648
  return instance
429
649
  }
430
650
 
@@ -446,11 +666,31 @@ export class Container {
446
666
  deps.push(depToken ? await this.resolve(depToken) : undefined)
447
667
  }
448
668
 
449
- const instance = new target(...deps)
450
- if (scope === 'singleton') this.#singletons.set(key, instance)
669
+ const instance = Reflect.construct(target, deps)
670
+ this.#cacheIfAppWide(scope, key, instance, store, readsBefore)
451
671
  return instance
452
672
  }
453
673
 
674
+ /**
675
+ * Cache a singleton — unless it read a request-scoped value while building.
676
+ *
677
+ * See `ResolutionChain.scopedReads`: an instance that captured one belongs to
678
+ * that request, and caching it hands the first request's state to every
679
+ * later one. AdonisJS does not guard this; it is the kind of silent
680
+ * cross-request leak worth refusing.
681
+ */
682
+ #cacheIfAppWide(
683
+ scope: string,
684
+ key: string,
685
+ instance: unknown,
686
+ store: ResolutionChain | undefined,
687
+ readsBefore: number,
688
+ ): void {
689
+ if (scope !== 'singleton') return
690
+ if ((store?.scopedReads ?? 0) !== readsBefore) return
691
+ this.#singletons.set(key, instance)
692
+ }
693
+
454
694
  #tokenToKey(token: ServiceToken): string {
455
695
  if (typeof token === 'string') {
456
696
  this.#assertNotReserved(token, 'string')
@@ -507,7 +747,7 @@ const NON_INJECTABLE_CONSTRUCTORS = new Set<unknown>([
507
747
  ])
508
748
 
509
749
  /** True for a real, injectable class token (excludes native primitive constructors). */
510
- function isInjectableClass(value: unknown): value is new (...args: unknown[]) => unknown {
750
+ function isInjectableClass(value: unknown): value is new (...args: never[]) => unknown {
511
751
  return typeof value === 'function' && !NON_INJECTABLE_CONSTRUCTORS.has(value)
512
752
  }
513
753
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Per-request view of the {@link Container} — AdonisJS Fold's
3
+ * `ContainerResolver`.
4
+ *
5
+ * A resolver resolves exactly what the container resolves, plus the values
6
+ * bound on it with {@link ContainerResolver.bindValue}. Those values are
7
+ * ISOLATED: they are visible to everything this resolver builds, including the
8
+ * constructor dependencies of what it builds, and to nothing else. That is what
9
+ * makes it safe for the HTTP kernel to bind one request's `HttpContext` — on
10
+ * the container itself, the binding would leak into every other request.
11
+ *
12
+ * const resolver = container.createResolver()
13
+ * resolver.bindValue(HttpContext, ctx)
14
+ * await resolver.make(UsersController) // gets this request's ctx
15
+ */
16
+
17
+ import type { Container } from './Container.js'
18
+ import type { ServiceToken } from './types.js'
19
+
20
+ export class ContainerResolver {
21
+ readonly #container: Container
22
+ /** Values bound here, keyed the way the container keys its tokens. */
23
+ readonly #values = new Map<string, unknown>()
24
+
25
+ constructor(container: Container) {
26
+ this.#container = container
27
+ }
28
+
29
+ /**
30
+ * Bind a resolved value for this resolver only (AdonisJS
31
+ * `resolver.bindValue`). Rebinding the same token replaces it.
32
+ */
33
+ bindValue<T>(token: ServiceToken, value: T): void {
34
+ this.#values.set(this.#container.keyFor(token), value)
35
+ }
36
+
37
+ /**
38
+ * Resolve a token, seeing this resolver's values first (AdonisJS
39
+ * `resolver.make`).
40
+ */
41
+ make<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T> {
42
+ return this.#container.runWithScopedValues(this.#values, () =>
43
+ this.#container.make<T>(token, runtimeValues),
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Call a method with its dependencies injected, seeing this resolver's
49
+ * values (AdonisJS `resolver.call`).
50
+ */
51
+ call<T, K extends string & keyof T>(
52
+ instance: T,
53
+ method: K,
54
+ runtimeValues?: unknown[],
55
+ ): Promise<unknown> {
56
+ return this.#container.runWithScopedValues(this.#values, () =>
57
+ this.#container.call(instance, method, runtimeValues),
58
+ )
59
+ }
60
+
61
+ /**
62
+ * Resolve as if `parent` had asked, so `parent`'s contextual bindings apply
63
+ * (AdonisJS `resolver.resolveFor`). `null` means nobody asked.
64
+ */
65
+ resolveFor<T>(
66
+ parent: (new (...args: never[]) => unknown) | null,
67
+ token: ServiceToken,
68
+ runtimeValues?: unknown[],
69
+ ): Promise<T> {
70
+ return this.#container.runWithScopedValues(this.#values, () =>
71
+ this.#container.resolveFor<T>(parent, token, runtimeValues),
72
+ )
73
+ }
74
+
75
+ /** Whether the token resolves — here or on the container (AdonisJS `hasBinding`). */
76
+ hasBinding(token: ServiceToken): boolean {
77
+ return this.#values.has(this.#container.keyFor(token)) || this.#container.has(token)
78
+ }
79
+
80
+ /** Whether every token resolves (AdonisJS `hasAllBindings`). */
81
+ hasAllBindings(tokens: ServiceToken[]): boolean {
82
+ return tokens.every((token) => this.hasBinding(token))
83
+ }
84
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The fluent form of a contextual binding — AdonisJS `container.when()`.
3
+ *
4
+ * container.when(UsersController).asksFor(Hash).provide(() => new Argon2())
5
+ *
6
+ * Reads as the sentence it is: when THIS class asks for THAT, give it this.
7
+ * `container.contextualBinding(parent, binding, factory)` is the same thing in
8
+ * one call.
9
+ */
10
+
11
+ import type { Container } from './Container.js'
12
+ import type { ServiceFactory, ServiceToken } from './types.js'
13
+
14
+ export class ContextualBindingsBuilder {
15
+ readonly #container: Container
16
+ readonly #parent: new (
17
+ ...args: never[]
18
+ ) => unknown
19
+ #binding: ServiceToken | undefined
20
+
21
+ constructor(container: Container, parent: new (...args: never[]) => unknown) {
22
+ this.#container = container
23
+ this.#parent = parent
24
+ }
25
+
26
+ /** The dependency this class asks for. */
27
+ asksFor(binding: ServiceToken): this {
28
+ this.#binding = binding
29
+ return this
30
+ }
31
+
32
+ /** What it gets instead. */
33
+ provide(factory: ServiceFactory): void {
34
+ if (this.#binding === undefined) {
35
+ throw new Error(
36
+ 'Container.when(): call asksFor(binding) before provide() — a contextual binding needs to know which dependency it replaces.',
37
+ )
38
+ }
39
+ this.#container.contextualBinding(this.#parent, this.#binding, factory)
40
+ }
41
+ }
@@ -14,24 +14,28 @@ export interface ServiceMetadata {
14
14
  }
15
15
 
16
16
  /** Token used to resolve a service — class constructor, string name, or registered symbol. */
17
- // biome-ignore lint/suspicious/noExplicitAny: IoC container tokens must accept any constructor signature
18
- export type ServiceToken = (new (...args: any[]) => unknown) | string | symbol
17
+ // `never[]` accepts every constructor shape — parameters are contravariant, so
18
+ // a rest of `never` is assignable from any concrete list.
19
+ export type ServiceToken = (new (...args: never[]) => unknown) | string | symbol
19
20
 
20
21
  /**
21
- * Resolver handed to a binding factory so it can resolve its own dependencies
22
+ * What a binding factory is handed so it can resolve its own dependencies
22
23
  * (AdonisJS parity: `container.singleton(token, async (resolver) =>
23
- * await resolver.make(Dep))`). The Container itself satisfies this contract.
24
+ * await resolver.make(Dep))`).
25
+ *
26
+ * Narrow on purpose: both the `Container` and a per-request `ContainerResolver`
27
+ * satisfy it, so a factory works the same whichever one is resolving.
24
28
  */
25
- export interface ContainerResolver {
29
+ export interface FactoryResolver {
26
30
  make<T>(token: ServiceToken, runtimeValues?: unknown[]): Promise<T>
27
31
  }
28
32
 
29
33
  /**
30
34
  * Factory function for creating service instances. Receives a {@link
31
- * ContainerResolver} (AdonisJS parity) — the async container means a factory
35
+ * FactoryResolver} (AdonisJS parity) — the async container means a factory
32
36
  * that needs another binding must `await resolver.make(Dep)`.
33
37
  */
34
- export type ServiceFactory = (resolver: ContainerResolver) => unknown
38
+ export type ServiceFactory = (resolver: FactoryResolver) => unknown
35
39
 
36
40
  /** Binding entry in the container. */
37
41
  export interface Binding {
@@ -11,7 +11,10 @@ const SERVICE_METADATA_KEY = Symbol('ream:service')
11
11
  const INJECT_METADATA_KEY = Symbol('ream:inject')
12
12
 
13
13
  /** Constructor type — accepts unknown args, returns unknown instance. */
14
- type AnyConstructor = new (...args: unknown[]) => unknown
14
+ // `never[]` accepts every constructor shape: parameters are contravariant, so a
15
+ // rest of `never` is assignable from any concrete list. Same spelling as the
16
+ // router's, so a class flows between the two without an assertion.
17
+ type AnyConstructor = new (...args: never[]) => unknown
15
18
 
16
19
  /** Registry of all decorated services. */
17
20
  const serviceRegistry: Map<AnyConstructor, ServiceMetadata> = new Map()
@@ -33,14 +36,24 @@ export function getServiceMetadata(target: AnyConstructor): ServiceMetadata | un
33
36
  * @Service() decorator.
34
37
  * Registers the class in the IoC container for auto-resolution.
35
38
  */
39
+ /** Anything with a construct signature — a decorated class always is. */
40
+ function isConstructor(value: unknown): value is AnyConstructor {
41
+ return typeof value === 'function' && value.prototype !== undefined
42
+ }
43
+
36
44
  export function Service(options: { scope?: ServiceScope; as?: string } = {}): ClassDecorator {
37
45
  return (target) => {
38
46
  const metadata: ServiceMetadata = {
39
47
  scope: options.scope ?? 'singleton',
40
48
  as: options.as,
41
49
  }
42
- // biome-ignore lint/suspicious/noExplicitAny: ClassDecorator target is Function; registry requires AnyConstructor which is a subtype
43
- serviceRegistry.set(target as any as AnyConstructor, metadata)
50
+ // A ClassDecorator's target is typed `Function`, which carries no
51
+ // construct signature; the guard is what states that a decorated class IS
52
+ // constructible.
53
+ if (!isConstructor(target)) {
54
+ throw new TypeError('@Service() can only decorate a class')
55
+ }
56
+ serviceRegistry.set(target, metadata)
44
57
  Reflect.defineMetadata(SERVICE_METADATA_KEY, metadata, target)
45
58
  }
46
59
  }