@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.
- package/README.md +1 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +6 -1
- package/dist/Application.js.map +1 -1
- package/dist/GracefulShutdown.js +6 -5
- package/dist/GracefulShutdown.js.map +1 -1
- package/dist/Provider.d.ts +20 -2
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +33 -7
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +61 -12
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +3 -2
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/commands/MigrateBase.d.ts.map +1 -1
- package/dist/commands/MigrateBase.js +4 -1
- package/dist/commands/MigrateBase.js.map +1 -1
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +7 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/console/types.d.ts +14 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/console/types.js.map +1 -1
- package/dist/container/Container.d.ts +19 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +89 -31
- package/dist/container/Container.js.map +1 -1
- package/dist/container/ContainerResolver.js.map +1 -1
- package/dist/container/types.d.ts +11 -1
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -1
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +4 -2
- package/dist/env/Env.js.map +1 -1
- package/dist/env/interpolate.d.ts.map +1 -1
- package/dist/env/interpolate.js +5 -3
- package/dist/env/interpolate.js.map +1 -1
- package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
- package/dist/errors/FuzzyMatcher.js +30 -13
- package/dist/errors/FuzzyMatcher.js.map +1 -1
- package/dist/errors/PipelineStageError.d.ts.map +1 -1
- package/dist/errors/PipelineStageError.js +4 -2
- package/dist/errors/PipelineStageError.js.map +1 -1
- package/dist/events/Emitter.d.ts +21 -4
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +6 -2
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/testing/FakeBus.d.ts +1 -1
- package/dist/events/testing/FakeBus.d.ts.map +1 -1
- package/dist/events/testing/FakeBus.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +2 -2
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +4 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/bytes.d.ts.map +1 -1
- package/dist/helpers/bytes.js +13 -2
- package/dist/helpers/bytes.js.map +1 -1
- package/dist/helpers/string.js +3 -3
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/Exception.d.ts +8 -4
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +14 -8
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +10 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +4 -9
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.d.ts.map +1 -1
- package/dist/http/SseStream.js +10 -1
- package/dist/http/SseStream.js.map +1 -1
- package/dist/http/etag.d.ts +44 -0
- package/dist/http/etag.d.ts.map +1 -0
- package/dist/http/etag.js +65 -0
- package/dist/http/etag.js.map +1 -0
- package/dist/http/mime.d.ts +33 -0
- package/dist/http/mime.d.ts.map +1 -0
- package/dist/http/mime.js +1347 -0
- package/dist/http/mime.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/native/generated.d.ts +12 -8
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.js +6 -4
- package/dist/openapi/OpenApiGenerator.js.map +1 -1
- package/dist/router/Router.d.ts +10 -1
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +22 -10
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +16 -3
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +34 -3
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Schedule.d.ts +6 -1
- package/dist/scheduler/Schedule.d.ts.map +1 -1
- package/dist/scheduler/Schedule.js +5 -0
- package/dist/scheduler/Schedule.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +12 -5
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.js +3 -2
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +23 -3
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +10 -2
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +78 -59
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +10 -11
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.js +6 -6
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +6 -2
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +4 -0
- package/dist/utils/Macroable.js.map +1 -1
- package/dist/utils/objectPath.d.ts.map +1 -1
- package/dist/utils/objectPath.js +14 -6
- package/dist/utils/objectPath.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +5 -7
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/build-napi-types.mjs +4 -3
- package/scripts/generate-mime.mjs +119 -0
- package/scripts/generate-napi-types.mjs +28 -12
- package/src/Application.ts +6 -1
- package/src/GracefulShutdown.ts +5 -5
- package/src/Provider.ts +21 -2
- package/src/bodyparser/MultipartFile.ts +94 -16
- package/src/bodyparser/parseSize.ts +3 -2
- package/src/commands/MigrateBase.ts +4 -1
- package/src/console/BaseCommand.ts +1 -1
- package/src/console/Kernel.ts +8 -0
- package/src/console/types.ts +15 -0
- package/src/container/Container.ts +120 -57
- package/src/container/ContainerResolver.ts +1 -1
- package/src/container/types.ts +11 -1
- package/src/decorators/Service.ts +1 -1
- package/src/env/Env.ts +4 -2
- package/src/env/interpolate.ts +5 -3
- package/src/errors/FuzzyMatcher.ts +34 -13
- package/src/errors/PipelineStageError.ts +4 -2
- package/src/events/Emitter.ts +34 -8
- package/src/events/testing/FakeBus.ts +12 -5
- package/src/graphql/GraphQLEngine.ts +11 -7
- package/src/helpers/bytes.ts +13 -2
- package/src/helpers/string.ts +3 -3
- package/src/http/Exception.ts +14 -8
- package/src/http/Request.ts +8 -5
- package/src/http/Response.ts +4 -8
- package/src/http/SseStream.ts +16 -7
- package/src/http/etag.ts +68 -0
- package/src/http/mime.ts +1347 -0
- package/src/index.ts +3 -3
- package/src/native/generated.ts +12 -8
- package/src/openapi/OpenApiGenerator.ts +6 -4
- package/src/router/Router.ts +31 -11
- package/src/rpc/RpcRouter.ts +38 -8
- package/src/scheduler/Schedule.ts +8 -1
- package/src/security/CookieSigner.ts +15 -5
- package/src/security/SignedUrl.ts +3 -2
- package/src/session/drivers/DatabaseDriver.ts +36 -8
- package/src/session/drivers/RedisDriver.ts +13 -5
- package/src/storage/StaticMiddleware.ts +81 -66
- package/src/testing/ApiResponse.ts +9 -10
- package/src/testing/TestClient.ts +6 -6
- package/src/utils/Macroable.ts +6 -2
- 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:
|
|
36
|
-
set: Set<
|
|
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<
|
|
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<
|
|
73
|
-
#singletons: 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<
|
|
83
|
-
#overrides: 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<
|
|
90
|
+
#singletonBackup: Map<ServiceToken, unknown> = new Map()
|
|
91
91
|
/** Alias key → the token it forwards to (AdonisJS `container.alias`). */
|
|
92
|
-
#aliases: Map<
|
|
92
|
+
#aliases: Map<ServiceToken, ServiceToken> = new Map()
|
|
93
93
|
/** parent class → (binding key → the factory that class gets instead). */
|
|
94
|
-
#contextualBindings: Map<
|
|
95
|
-
new
|
|
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<
|
|
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:
|
|
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:
|
|
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<
|
|
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>(
|
|
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
|
|
413
|
-
keyFor(token: ServiceToken):
|
|
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
|
|
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>(
|
|
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
|
|
566
|
-
const
|
|
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 '${
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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}'
|
|
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<
|
|
23
|
+
readonly #values = new Map<ServiceToken, unknown>()
|
|
24
24
|
|
|
25
25
|
constructor(container: Container) {
|
|
26
26
|
this.#container = container
|
package/src/container/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
}
|
package/src/env/interpolate.ts
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
85
|
-
if (
|
|
104
|
+
const [best, ...rest] = findClosestMatches(input, candidates)
|
|
105
|
+
if (best === undefined) return ''
|
|
86
106
|
|
|
87
|
-
if (
|
|
88
|
-
return `Did you mean '${
|
|
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
|
-
|
|
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 =
|
|
49
|
+
const nextStage = PIPELINE_STAGES[clampedIndex + 1] ?? '(end)'
|
|
48
50
|
|
|
49
51
|
const errorCode = originalError instanceof ReamError ? originalError.code : 'E_PIPELINE_ERROR'
|
|
50
52
|
|
package/src/events/Emitter.ts
CHANGED
|
@@ -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):
|
|
26
|
+
handle(event: T): unknown
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
type EventConstructor<T = unknown> = new (...args: never[]) => T
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
331
|
-
:
|
|
332
|
-
|
|
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):
|
|
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
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
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) =>
|
|
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
|