@c9up/ream 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Application.d.ts +27 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +29 -0
- package/dist/Application.js.map +1 -1
- package/dist/ConfigLoader.d.ts +4 -0
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +0 -8
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +13 -1
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/HttpKernel.d.ts +8 -0
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +35 -12
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +8 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +39 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +11 -4
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +55 -43
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +52 -0
- package/dist/bodyparser/qsParse.d.ts.map +1 -0
- package/dist/bodyparser/qsParse.js +161 -0
- package/dist/bodyparser/qsParse.js.map +1 -0
- package/dist/bootstrap/hyper-server.d.ts.map +1 -1
- package/dist/bootstrap/hyper-server.js +2 -8
- package/dist/bootstrap/hyper-server.js.map +1 -1
- package/dist/container/Container.d.ts +41 -0
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +193 -26
- package/dist/container/Container.js.map +1 -1
- package/dist/container/ContainerResolver.d.ts +46 -0
- package/dist/container/ContainerResolver.d.ts.map +1 -0
- package/dist/container/ContainerResolver.js +60 -0
- package/dist/container/ContainerResolver.js.map +1 -0
- package/dist/container/ContextualBindingsBuilder.d.ts +20 -0
- package/dist/container/ContextualBindingsBuilder.d.ts.map +1 -0
- package/dist/container/ContextualBindingsBuilder.js +31 -0
- package/dist/container/ContextualBindingsBuilder.js.map +1 -0
- package/dist/container/types.d.ts +9 -6
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -5
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/decorators/Service.js +10 -1
- package/dist/decorators/Service.js.map +1 -1
- package/dist/events/Emitter.d.ts +66 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +165 -28
- package/dist/events/Emitter.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +5 -5
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/duration.d.ts +32 -0
- package/dist/helpers/duration.d.ts.map +1 -0
- package/dist/helpers/duration.js +74 -0
- package/dist/helpers/duration.js.map +1 -0
- package/dist/helpers/napi-loader.d.ts +10 -1
- package/dist/helpers/napi-loader.d.ts.map +1 -1
- package/dist/helpers/napi-loader.js +18 -3
- package/dist/helpers/napi-loader.js.map +1 -1
- package/dist/helpers/types.d.ts +1 -1
- package/dist/helpers/types.d.ts.map +1 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +17 -5
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +24 -5
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +12 -2
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +48 -2
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +95 -14
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +143 -8
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +241 -21
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +74 -3
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +177 -4
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/router/Router.d.ts +36 -7
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +66 -22
- package/dist/router/Router.js.map +1 -1
- package/dist/security/CookieSigner.d.ts +35 -4
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +99 -10
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/server/Server.d.ts +2 -3
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +30 -13
- package/dist/server/Server.js.map +1 -1
- package/dist/session/ReadOnlyValuesStore.d.ts +24 -0
- package/dist/session/ReadOnlyValuesStore.d.ts.map +1 -0
- package/dist/session/ReadOnlyValuesStore.js +45 -0
- package/dist/session/ReadOnlyValuesStore.js.map +1 -0
- package/dist/session/Session.d.ts +181 -9
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +284 -33
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +87 -33
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +66 -0
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -0
- package/dist/session/drivers/DatabaseDriver.js +132 -0
- package/dist/session/drivers/DatabaseDriver.js.map +1 -0
- package/dist/session/drivers/FileDriver.d.ts +22 -0
- package/dist/session/drivers/FileDriver.d.ts.map +1 -0
- package/dist/session/drivers/FileDriver.js +66 -0
- package/dist/session/drivers/FileDriver.js.map +1 -0
- package/dist/session/drivers/MemoryDriver.d.ts +10 -2
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +70 -0
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +10 -2
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +58 -0
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/session/templateTags.d.ts +42 -0
- package/dist/session/templateTags.d.ts.map +1 -0
- package/dist/session/templateTags.js +101 -0
- package/dist/session/templateTags.js.map +1 -0
- package/dist/testing/RequestBuilder.d.ts +38 -1
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +40 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/testing/TestClient.d.ts +3 -1
- package/dist/testing/TestClient.d.ts.map +1 -1
- package/dist/testing/TestClient.js +3 -1
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/types/helpers.d.ts +1 -1
- package/dist/types/helpers.d.ts.map +1 -1
- package/dist/utils/Macroable.d.ts +14 -5
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +20 -18
- package/dist/utils/Macroable.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +39 -0
- package/src/ConfigLoader.ts +6 -0
- package/src/ErrorBoundary.ts +13 -1
- package/src/HttpKernel.ts +45 -19
- package/src/Ignitor.ts +39 -0
- package/src/bodyparser/BodyParserMiddleware.ts +66 -44
- package/src/bodyparser/qsParse.ts +179 -0
- package/src/bootstrap/hyper-server.ts +2 -9
- package/src/container/Container.ts +265 -25
- package/src/container/ContainerResolver.ts +84 -0
- package/src/container/ContextualBindingsBuilder.ts +41 -0
- package/src/container/types.ts +11 -7
- package/src/decorators/Service.ts +16 -3
- package/src/events/Emitter.ts +200 -25
- package/src/graphql/GraphQLEngine.ts +7 -8
- package/src/helpers/duration.ts +71 -0
- package/src/helpers/napi-loader.ts +19 -3
- package/src/helpers/types.ts +4 -1
- package/src/http/Exception.ts +17 -5
- package/src/http/HttpContext.ts +28 -5
- package/src/http/RedirectBuilder.ts +126 -11
- package/src/http/Request.ts +303 -13
- package/src/http/Response.ts +213 -6
- package/src/index.ts +17 -3
- package/src/router/Router.ts +98 -33
- package/src/security/CookieSigner.ts +102 -10
- package/src/server/Server.ts +36 -16
- package/src/session/ReadOnlyValuesStore.ts +52 -0
- package/src/session/Session.ts +364 -37
- package/src/session/SessionMiddleware.ts +93 -30
- package/src/session/drivers/DatabaseDriver.ts +184 -0
- package/src/session/drivers/FileDriver.ts +82 -0
- package/src/session/drivers/MemoryDriver.ts +75 -2
- package/src/session/drivers/RedisDriver.ts +77 -2
- package/src/session/templateTags.ts +124 -0
- package/src/testing/RequestBuilder.ts +76 -0
- package/src/testing/TestClient.ts +10 -2
- package/src/types/helpers.ts +4 -1
- package/src/utils/Macroable.ts +23 -8
- package/dist/utils/inflect.d.ts +0 -17
- package/dist/utils/inflect.d.ts.map +0 -1
- package/dist/utils/inflect.js +0 -32
- package/dist/utils/inflect.js.map +0 -1
- 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 =
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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:
|
|
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.
|
|
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
|
-
//
|
|
492
|
+
// 5. Check explicit bindings — the factory may be async.
|
|
316
493
|
const binding = this.#bindings.get(key)
|
|
317
494
|
if (binding) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.#
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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 =
|
|
401
|
-
|
|
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 =
|
|
410
|
-
|
|
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
|
-
|
|
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 =
|
|
450
|
-
|
|
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:
|
|
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
|
+
}
|
package/src/container/types.ts
CHANGED
|
@@ -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
|
-
//
|
|
18
|
-
|
|
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
|
-
*
|
|
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))`).
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
43
|
-
|
|
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
|
}
|