@velajs/vela 1.8.8 → 1.10.0
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/CHANGELOG.md +20 -0
- package/dist/__tests__/workers-als/entry-als.d.ts +9 -0
- package/dist/application.d.ts +10 -0
- package/dist/application.js +25 -0
- package/dist/cache/cache.module.d.ts +5 -7
- package/dist/cache/cache.module.js +44 -66
- package/dist/cache/cache.types.d.ts +26 -0
- package/dist/cache/cache.types.js +1 -1
- package/dist/cache/index.d.ts +2 -1
- package/dist/cache/index.js +1 -0
- package/dist/cache/tiered-cache.store.d.ts +19 -0
- package/dist/cache/tiered-cache.store.js +40 -0
- package/dist/config/config.module.d.ts +6 -5
- package/dist/config/config.module.js +46 -49
- package/dist/container/container.d.ts +33 -0
- package/dist/container/container.js +156 -7
- package/dist/container/disposable.d.ts +5 -0
- package/dist/container/disposable.js +33 -0
- package/dist/container/types.d.ts +8 -0
- package/dist/factory/bootstrap.js +10 -0
- package/dist/fetch/fetch.module.d.ts +5 -6
- package/dist/fetch/fetch.module.js +9 -42
- package/dist/http/ambient.d.ts +17 -0
- package/dist/http/ambient.js +44 -0
- package/dist/http/execution-context.js +4 -1
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/route.manager.d.ts +19 -0
- package/dist/http/route.manager.js +84 -0
- package/dist/i18n/deep-merge.d.ts +2 -0
- package/dist/i18n/deep-merge.js +15 -0
- package/dist/i18n/i18n.middleware.d.ts +15 -0
- package/dist/i18n/i18n.middleware.js +94 -0
- package/dist/i18n/i18n.module.d.ts +13 -0
- package/dist/i18n/i18n.module.js +61 -0
- package/dist/i18n/i18n.options.d.ts +30 -0
- package/dist/i18n/i18n.options.js +16 -0
- package/dist/i18n/i18n.service.d.ts +20 -0
- package/dist/i18n/i18n.service.js +49 -0
- package/dist/i18n/i18n.tokens.d.ts +6 -0
- package/dist/i18n/i18n.tokens.js +3 -0
- package/dist/i18n/i18n.types.d.ts +6 -0
- package/dist/i18n/i18n.types.js +1 -0
- package/dist/i18n/index.d.ts +10 -0
- package/dist/i18n/index.js +8 -0
- package/dist/i18n/message-loader.service.d.ts +21 -0
- package/dist/i18n/message-loader.service.js +90 -0
- package/dist/i18n/message-registry.d.ts +16 -0
- package/dist/i18n/message-registry.js +49 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -2
- package/dist/module/configurable-module.builder.d.ts +44 -0
- package/dist/module/configurable-module.builder.js +177 -0
- package/dist/module/configurable-module.types.d.ts +93 -0
- package/dist/module/configurable-module.types.js +5 -0
- package/dist/module/index.d.ts +2 -0
- package/dist/module/index.js +1 -0
- package/dist/openapi/document.js +20 -2
- package/dist/openapi/types.d.ts +54 -0
- package/dist/pipeline/types.d.ts +21 -4
- package/dist/registry/metadata.registry.d.ts +16 -16
- package/dist/registry/metadata.registry.js +90 -32
- package/dist/seeder/index.d.ts +5 -0
- package/dist/seeder/index.js +4 -0
- package/dist/seeder/seeder.decorator.d.ts +7 -0
- package/dist/seeder/seeder.decorator.js +15 -0
- package/dist/seeder/seeder.module.d.ts +18 -0
- package/dist/seeder/seeder.module.js +40 -0
- package/dist/seeder/seeder.registry.d.ts +23 -0
- package/dist/seeder/seeder.registry.js +83 -0
- package/dist/seeder/seeder.tokens.d.ts +2 -0
- package/dist/seeder/seeder.tokens.js +1 -0
- package/dist/seeder/seeder.types.d.ts +20 -0
- package/dist/seeder/seeder.types.js +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/path-template.d.ts +9 -0
- package/dist/storage/path-template.js +20 -0
- package/dist/storage/signed-url.d.ts +12 -0
- package/dist/storage/signed-url.js +55 -0
- package/dist/storage/storage.types.d.ts +41 -0
- package/dist/storage/storage.types.js +5 -0
- package/dist/throttler/throttler.module.d.ts +5 -8
- package/dist/throttler/throttler.module.js +27 -55
- package/dist/websocket/index.d.ts +12 -0
- package/dist/websocket/index.js +16 -0
- package/dist/websocket/websocket.decorators.d.ts +33 -0
- package/dist/websocket/websocket.decorators.js +58 -0
- package/dist/websocket/websocket.module.d.ts +18 -0
- package/dist/websocket/websocket.module.js +52 -0
- package/dist/websocket/websocket.tokens.d.ts +22 -0
- package/dist/websocket/websocket.tokens.js +20 -0
- package/dist/websocket/websocket.types.d.ts +86 -0
- package/dist/websocket/websocket.types.js +4 -0
- package/dist/websocket/ws-argument-resolver.d.ts +13 -0
- package/dist/websocket/ws-argument-resolver.js +40 -0
- package/dist/websocket/ws-dispatcher.d.ts +33 -0
- package/dist/websocket/ws-dispatcher.js +253 -0
- package/dist/websocket/ws-exception.d.ts +16 -0
- package/dist/websocket/ws-exception.js +32 -0
- package/dist/websocket/ws-execution-context.d.ts +9 -0
- package/dist/websocket/ws-execution-context.js +24 -0
- package/dist/websocket/ws-server.d.ts +31 -0
- package/dist/websocket/ws-server.js +74 -0
- package/dist/websocket/ws-sync.d.ts +47 -0
- package/dist/websocket/ws-sync.js +74 -0
- package/dist/websocket-node/index.d.ts +4 -0
- package/dist/websocket-node/index.js +5 -0
- package/dist/websocket-node/node-ws-client.d.ts +24 -0
- package/dist/websocket-node/node-ws-client.js +49 -0
- package/dist/websocket-node/redis-sync.d.ts +33 -0
- package/dist/websocket-node/redis-sync.js +56 -0
- package/dist/websocket-node/register-gateways.d.ts +20 -0
- package/dist/websocket-node/register-gateways.js +61 -0
- package/package.json +31 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Scope } from "../constants.js";
|
|
2
|
+
import { disposeInstance, isDisposable } from "./disposable.js";
|
|
2
3
|
import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
|
|
3
4
|
import { ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
|
|
4
5
|
const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips type-only imports at ' + 'runtime and `design:paramtypes` emits `Object`/`undefined` for their ' + 'positions. Use a runtime `import { X }` for DI tokens.';
|
|
@@ -25,6 +26,13 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
25
26
|
scopes = new Map();
|
|
26
27
|
globals = new Set();
|
|
27
28
|
diagnostics;
|
|
29
|
+
// The root container that owns shared state; a request child points back here
|
|
30
|
+
// so container-constructed SINGLETONs are tracked (and disposed) at the root,
|
|
31
|
+
// never by the ephemeral child that happened to first resolve them.
|
|
32
|
+
root = this;
|
|
33
|
+
// Container-constructed instances in creation order, for LIFO disposal.
|
|
34
|
+
// useValue providers are never tracked (they return before construction).
|
|
35
|
+
disposables = [];
|
|
28
36
|
constructor(options = {}){
|
|
29
37
|
this.diagnostics = options.diagnostics ?? 'log';
|
|
30
38
|
}
|
|
@@ -285,7 +293,7 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
285
293
|
if (!exporters) return undefined;
|
|
286
294
|
for (const owner of exporters){
|
|
287
295
|
const reg = this.providers.get(owner)?.get(token);
|
|
288
|
-
if (reg) return reg.scope;
|
|
296
|
+
if (reg) return reg.effectiveScope ?? reg.scope;
|
|
289
297
|
}
|
|
290
298
|
return undefined;
|
|
291
299
|
}
|
|
@@ -299,6 +307,95 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
299
307
|
];
|
|
300
308
|
}
|
|
301
309
|
/**
|
|
310
|
+
* Every statically-provided (useValue) instance across ALL module buckets.
|
|
311
|
+
* Unlike resolving a token (which yields only the first bucket's registration),
|
|
312
|
+
* this surfaces per-instance values — e.g. one BindingRef per multi-instance
|
|
313
|
+
* binding module — so framework adapters can initialize all of them.
|
|
314
|
+
*/ getUseValues() {
|
|
315
|
+
const out = [];
|
|
316
|
+
for (const bucket of this.providers.values()){
|
|
317
|
+
for (const reg of bucket.values()){
|
|
318
|
+
if (reg.useValue !== undefined) out.push(reg.useValue);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Compute request-scope bubbling for every registration (call once at
|
|
325
|
+
* bootstrap, after all providers are registered). A provider whose declared
|
|
326
|
+
* scope is not REQUEST but which (transitively) depends on a request-scoped
|
|
327
|
+
* provider is marked `effectiveScope = REQUEST`, so it is rebuilt per request
|
|
328
|
+
* instead of capturing the first request's instance — matching NestJS.
|
|
329
|
+
*/ computeEffectiveScopes() {
|
|
330
|
+
// Reverse-propagation worklist: seed with every declared-REQUEST provider,
|
|
331
|
+
// then flood "request-ness" to consumers transitively. This is order- and
|
|
332
|
+
// cycle-independent (a node flips to REQUEST at most once), avoiding the
|
|
333
|
+
// finalize-during-in-progress-cycle hazard of a recursive DFS.
|
|
334
|
+
const regs = [];
|
|
335
|
+
for (const bucket of this.providers.values()){
|
|
336
|
+
for (const reg of bucket.values()){
|
|
337
|
+
reg.effectiveScope = reg.scope;
|
|
338
|
+
regs.push(reg);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Build reverse edges: dependency registration -> registrations that consume it.
|
|
342
|
+
const consumers = new Map();
|
|
343
|
+
for (const reg of regs){
|
|
344
|
+
for (const depToken of this.dependencyTokensOf(reg)){
|
|
345
|
+
const depReg = this.tryFindRegistration(depToken, reg.declaringModuleId);
|
|
346
|
+
if (!depReg) continue;
|
|
347
|
+
const list = consumers.get(depReg);
|
|
348
|
+
if (list) list.push(reg);
|
|
349
|
+
else consumers.set(depReg, [
|
|
350
|
+
reg
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const queue = regs.filter((r)=>r.effectiveScope === Scope.REQUEST);
|
|
355
|
+
while(queue.length > 0){
|
|
356
|
+
const dep = queue.pop();
|
|
357
|
+
for (const consumer of consumers.get(dep) ?? []){
|
|
358
|
+
if (consumer.effectiveScope !== Scope.REQUEST) {
|
|
359
|
+
consumer.effectiveScope = Scope.REQUEST;
|
|
360
|
+
queue.push(consumer);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** The dependency tokens a registration would resolve when constructed. */ dependencyTokensOf(reg) {
|
|
366
|
+
if (reg.useExisting) return [
|
|
367
|
+
reg.useExisting
|
|
368
|
+
];
|
|
369
|
+
if (reg.useFactory) {
|
|
370
|
+
return (reg.inject ?? []).map((t)=>t instanceof ForwardRef ? t.factory() : t);
|
|
371
|
+
}
|
|
372
|
+
const cls = reg.useClass ?? (typeof reg.provide === 'function' ? reg.provide : undefined);
|
|
373
|
+
if (!cls) return [];
|
|
374
|
+
const paramTypes = getConstructorDependencies(cls);
|
|
375
|
+
const injectMetadata = getInjectMetadata(cls);
|
|
376
|
+
const injectMap = new Map(injectMetadata.map((m)=>[
|
|
377
|
+
m.index,
|
|
378
|
+
m
|
|
379
|
+
]));
|
|
380
|
+
const arity = Math.max(paramTypes.length, injectMetadata.reduce((max, m)=>Math.max(max, m.index + 1), 0));
|
|
381
|
+
const tokens = [];
|
|
382
|
+
for(let i = 0; i < arity; i++){
|
|
383
|
+
const raw = injectMap.get(i)?.token;
|
|
384
|
+
const token = raw instanceof ForwardRef ? raw.factory() : raw ?? paramTypes[i];
|
|
385
|
+
if (token && !isErasedTypeToken(token)) tokens.push(token);
|
|
386
|
+
}
|
|
387
|
+
return tokens;
|
|
388
|
+
}
|
|
389
|
+
tryFindRegistration(token, requestingModuleId) {
|
|
390
|
+
try {
|
|
391
|
+
return this.findRegistration(token, requestingModuleId);
|
|
392
|
+
} catch {
|
|
393
|
+
// Ambiguous (MultipleProvidersFoundError) or unreachable — treat as
|
|
394
|
+
// unknown for scope computation; real resolution will surface any error.
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
302
399
|
* Create a child container for request-scoped resolution.
|
|
303
400
|
* The child shares the parent's providers but caches REQUEST-scoped
|
|
304
401
|
* instances separately per child (per request).
|
|
@@ -312,6 +409,9 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
312
409
|
child.exporterIndex = this.exporterIndex;
|
|
313
410
|
child.scopes = this.scopes;
|
|
314
411
|
child.globals = this.globals;
|
|
412
|
+
// SINGLETON disposal is owned by the root; the child keeps only its own
|
|
413
|
+
// REQUEST-scoped disposables (cleared when the request ends).
|
|
414
|
+
child.root = this.root;
|
|
315
415
|
return child;
|
|
316
416
|
}
|
|
317
417
|
createDetached() {
|
|
@@ -343,6 +443,44 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
343
443
|
this.requestInstances.clear();
|
|
344
444
|
this.scopes.clear();
|
|
345
445
|
this.globals.clear();
|
|
446
|
+
this.disposables = [];
|
|
447
|
+
}
|
|
448
|
+
/** Whether this container has any tracked disposables (cheap request-path check). */ hasDisposables() {
|
|
449
|
+
return this.disposables.length > 0;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Dispose container-constructed instances (Symbol.asyncDispose /
|
|
453
|
+
* Symbol.dispose / .dispose()) in reverse creation order (LIFO). Errors are
|
|
454
|
+
* logged and skipped so one bad teardown never blocks the rest.
|
|
455
|
+
*
|
|
456
|
+
* A REQUEST child disposes only its own request-scoped instances; the root
|
|
457
|
+
* disposes shared SINGLETONs and clears their cached instance so a dev HMR
|
|
458
|
+
* generation cannot leak a stale graph. Shared provider maps are left intact
|
|
459
|
+
* for a request child (they belong to the root).
|
|
460
|
+
*/ async dispose() {
|
|
461
|
+
const pending = this.disposables;
|
|
462
|
+
this.disposables = [];
|
|
463
|
+
for(let i = pending.length - 1; i >= 0; i--){
|
|
464
|
+
try {
|
|
465
|
+
await disposeInstance(pending[i]);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (this.diagnostics !== 'silent') {
|
|
468
|
+
console.error('Error disposing instance:', error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
this.requestInstances.clear();
|
|
473
|
+
if (this.root === this) {
|
|
474
|
+
// Drop cached singleton instances (except useValue, which the app owns)
|
|
475
|
+
// so a rebuilt graph starts clean.
|
|
476
|
+
for (const bucket of this.providers.values()){
|
|
477
|
+
for (const registration of bucket.values()){
|
|
478
|
+
if (registration.scope === Scope.SINGLETON && registration.useValue === undefined) {
|
|
479
|
+
registration.instance = undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
346
484
|
}
|
|
347
485
|
resolveRegistration(registration, requestingModuleId) {
|
|
348
486
|
if (registration.useValue !== undefined) {
|
|
@@ -352,12 +490,16 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
352
490
|
// Pass through the original requester to catch alias leaks
|
|
353
491
|
return this.resolve(registration.useExisting, requestingModuleId);
|
|
354
492
|
}
|
|
493
|
+
// Effective scope accounts for request-scope bubbling: a SINGLETON that
|
|
494
|
+
// (transitively) depends on a request-scoped provider is treated as REQUEST
|
|
495
|
+
// so it is rebuilt per request instead of capturing the first one.
|
|
496
|
+
const scope = registration.effectiveScope ?? registration.scope;
|
|
355
497
|
// Singleton: return cached from registration (shared across all containers)
|
|
356
|
-
if (
|
|
498
|
+
if (scope === Scope.SINGLETON && registration.instance !== undefined) {
|
|
357
499
|
return registration.instance;
|
|
358
500
|
}
|
|
359
501
|
// Request: return cached from this child's requestInstances
|
|
360
|
-
if (
|
|
502
|
+
if (scope === Scope.REQUEST) {
|
|
361
503
|
const cached = this.requestInstances.get(registration.provide);
|
|
362
504
|
if (cached !== undefined) {
|
|
363
505
|
return cached;
|
|
@@ -384,10 +526,14 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
384
526
|
} else {
|
|
385
527
|
throw new Error(`Invalid provider registration for: ${this.tokenToString(registration.provide)}`);
|
|
386
528
|
}
|
|
387
|
-
if (
|
|
529
|
+
if (scope === Scope.SINGLETON) {
|
|
388
530
|
registration.instance = instance;
|
|
389
|
-
|
|
531
|
+
// Track for disposal on the ROOT — a singleton outlives the request
|
|
532
|
+
// child that may have first constructed it.
|
|
533
|
+
if (isDisposable(instance)) this.root.disposables.push(instance);
|
|
534
|
+
} else if (scope === Scope.REQUEST) {
|
|
390
535
|
this.requestInstances.set(registration.provide, instance);
|
|
536
|
+
if (isDisposable(instance)) this.disposables.push(instance);
|
|
391
537
|
}
|
|
392
538
|
return instance;
|
|
393
539
|
} finally{
|
|
@@ -468,7 +614,8 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
468
614
|
throw new Error(`No provider found for token: ${this.tokenToString(token)}`);
|
|
469
615
|
}
|
|
470
616
|
if (registration.useFactory) {
|
|
471
|
-
|
|
617
|
+
const scope = registration.effectiveScope ?? registration.scope;
|
|
618
|
+
if (scope === Scope.SINGLETON && registration.instance !== undefined) {
|
|
472
619
|
return registration.instance;
|
|
473
620
|
}
|
|
474
621
|
// Factory inject deps resolve without a requester (same escape hatch as
|
|
@@ -478,8 +625,10 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
|
|
|
478
625
|
return this.resolveAsync(resolved);
|
|
479
626
|
}));
|
|
480
627
|
const instance = await registration.useFactory(...dependencies);
|
|
481
|
-
if (
|
|
628
|
+
if (scope === Scope.SINGLETON) {
|
|
482
629
|
registration.instance = instance;
|
|
630
|
+
// Track for disposal on the ROOT, matching the sync resolveRegistration path.
|
|
631
|
+
if (isDisposable(instance)) this.root.disposables.push(instance);
|
|
483
632
|
}
|
|
484
633
|
return instance;
|
|
485
634
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Edge-safe disposal contract (mirrors stratal's). Precedence:
|
|
2
|
+
// Symbol.asyncDispose > Symbol.dispose > .dispose()
|
|
3
|
+
// Symbol.asyncDispose/Symbol.dispose are resolved at runtime (they are not in
|
|
4
|
+
// the ES2022 lib the project targets), so nothing here depends on the
|
|
5
|
+
// esnext.disposable typings.
|
|
6
|
+
const ASYNC_DISPOSE = Symbol.asyncDispose;
|
|
7
|
+
const SYNC_DISPOSE = Symbol.dispose;
|
|
8
|
+
function asRecord(value) {
|
|
9
|
+
if (value === null) return undefined;
|
|
10
|
+
return typeof value === 'object' || typeof value === 'function' ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
export function isDisposable(value) {
|
|
13
|
+
const obj = asRecord(value);
|
|
14
|
+
if (!obj) return false;
|
|
15
|
+
if (ASYNC_DISPOSE && typeof obj[ASYNC_DISPOSE] === 'function') return true;
|
|
16
|
+
if (SYNC_DISPOSE && typeof obj[SYNC_DISPOSE] === 'function') return true;
|
|
17
|
+
return typeof obj.dispose === 'function';
|
|
18
|
+
}
|
|
19
|
+
export async function disposeInstance(value) {
|
|
20
|
+
const obj = asRecord(value);
|
|
21
|
+
if (!obj) return;
|
|
22
|
+
if (ASYNC_DISPOSE && typeof obj[ASYNC_DISPOSE] === 'function') {
|
|
23
|
+
await obj[ASYNC_DISPOSE].call(obj);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (SYNC_DISPOSE && typeof obj[SYNC_DISPOSE] === 'function') {
|
|
27
|
+
obj[SYNC_DISPOSE].call(obj);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (typeof obj.dispose === 'function') {
|
|
31
|
+
await obj.dispose.call(obj);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -57,7 +57,15 @@ export interface ProviderOptions<T = unknown> {
|
|
|
57
57
|
}
|
|
58
58
|
export interface ProviderRegistration<T = unknown> {
|
|
59
59
|
provide: Token<T>;
|
|
60
|
+
/** The declared scope (from `@Injectable`/`@Controller`/provider options). */
|
|
60
61
|
scope: Scope;
|
|
62
|
+
/**
|
|
63
|
+
* Scope after request-scope bubbling: `REQUEST` if this provider (transitively)
|
|
64
|
+
* depends on a request-scoped provider, else the declared scope. Computed once
|
|
65
|
+
* at bootstrap (`Container.computeEffectiveScopes`); falls back to `scope`
|
|
66
|
+
* until then. Governs caching + eager instantiation.
|
|
67
|
+
*/
|
|
68
|
+
effectiveScope?: Scope;
|
|
61
69
|
/**
|
|
62
70
|
* Module that owns this registration. Used by `resolveClass` to determine
|
|
63
71
|
* the POV from which the class's dependencies resolve. Sandbox/bootstrap
|
|
@@ -54,6 +54,13 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
|
|
|
54
54
|
});
|
|
55
55
|
container.markGlobalToken(REQUEST_CONTEXT);
|
|
56
56
|
const routeManager = new RouteManager(container, options);
|
|
57
|
+
// Resolvable so non-HTTP transports (the WebSocket dispatcher) can read the
|
|
58
|
+
// same global-tier components (APP_* + app.useGlobalX()).
|
|
59
|
+
container.register({
|
|
60
|
+
provide: RouteManager,
|
|
61
|
+
useValue: routeManager
|
|
62
|
+
});
|
|
63
|
+
container.markGlobalToken(RouteManager);
|
|
57
64
|
ComponentManager.init(container);
|
|
58
65
|
const loader = new ModuleLoader(container, routeManager);
|
|
59
66
|
loader.load(rootModule);
|
|
@@ -68,6 +75,9 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
|
|
|
68
75
|
};
|
|
69
76
|
routeManager.useGlobalMiddleware(mw);
|
|
70
77
|
}
|
|
78
|
+
// All providers are registered — compute request-scope bubbling so effective
|
|
79
|
+
// scopes are known before eager instantiation and request resolution.
|
|
80
|
+
container.computeEffectiveScopes();
|
|
71
81
|
return {
|
|
72
82
|
container,
|
|
73
83
|
routeManager,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { AsyncModuleOptions, DynamicModule } from '../module/types';
|
|
2
1
|
import type { HttpModuleOptions } from './fetch.types';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}): DynamicModule;
|
|
2
|
+
declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<HttpModuleOptions, "forRoot", "create", {
|
|
3
|
+
isGlobal?: boolean;
|
|
4
|
+
}>;
|
|
5
|
+
export declare class HttpModule extends ConfigurableModuleClass {
|
|
8
6
|
}
|
|
7
|
+
export {};
|
|
@@ -5,52 +5,18 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
7
|
import { Module } from "../module/decorators.js";
|
|
8
|
-
import {
|
|
8
|
+
import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
|
|
9
9
|
import { HttpService, HTTP_MODULE_OPTIONS } from "./fetch.service.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
providers: [
|
|
16
|
-
{
|
|
17
|
-
provide: HTTP_MODULE_OPTIONS,
|
|
18
|
-
useValue: options
|
|
19
|
-
},
|
|
20
|
-
HttpService
|
|
21
|
-
],
|
|
22
|
-
exports: [
|
|
23
|
-
HttpService,
|
|
24
|
-
HTTP_MODULE_OPTIONS
|
|
25
|
-
]
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
static forRootAsync(options) {
|
|
29
|
-
return {
|
|
30
|
-
module: HttpModule,
|
|
31
|
-
key: options.key ?? stableHash({
|
|
32
|
-
inject: options.inject,
|
|
33
|
-
useFactory: options.useFactory
|
|
34
|
-
}),
|
|
35
|
-
imports: options.imports ?? [],
|
|
36
|
-
providers: [
|
|
37
|
-
{
|
|
38
|
-
provide: HTTP_MODULE_OPTIONS,
|
|
39
|
-
useFactory: options.useFactory,
|
|
40
|
-
inject: options.inject ?? []
|
|
41
|
-
},
|
|
42
|
-
HttpService
|
|
43
|
-
],
|
|
44
|
-
exports: [
|
|
45
|
-
HttpService,
|
|
46
|
-
HTTP_MODULE_OPTIONS
|
|
47
|
-
]
|
|
48
|
-
};
|
|
49
|
-
}
|
|
10
|
+
const { ConfigurableModuleClass } = new ConfigurableModuleBuilder({
|
|
11
|
+
moduleName: 'Http',
|
|
12
|
+
optionsInjectionToken: HTTP_MODULE_OPTIONS
|
|
13
|
+
}).build();
|
|
14
|
+
export class HttpModule extends ConfigurableModuleClass {
|
|
50
15
|
}
|
|
51
16
|
HttpModule = _ts_decorate([
|
|
52
17
|
Module({
|
|
53
18
|
providers: [
|
|
19
|
+
// Default for bare `imports: [HttpModule]`; forRoot(options) overrides via merge.
|
|
54
20
|
{
|
|
55
21
|
provide: HTTP_MODULE_OPTIONS,
|
|
56
22
|
useValue: {}
|
|
@@ -58,7 +24,8 @@ HttpModule = _ts_decorate([
|
|
|
58
24
|
HttpService
|
|
59
25
|
],
|
|
60
26
|
exports: [
|
|
61
|
-
HttpService
|
|
27
|
+
HttpService,
|
|
28
|
+
HTTP_MODULE_OPTIONS
|
|
62
29
|
]
|
|
63
30
|
})
|
|
64
31
|
], HttpModule);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { Container } from '../container/container';
|
|
3
|
+
import { type RequestContext } from './request-context';
|
|
4
|
+
/**
|
|
5
|
+
* Returns Hono's `contextStorage()` middleware. Register it as the FIRST global
|
|
6
|
+
* middleware so `getCurrentContainer()` works everywhere downstream. When using
|
|
7
|
+
* `VelaFactory.create(module, { ambientContainer: true })` this is wired
|
|
8
|
+
* automatically — call this only for manual/raw Hono setups.
|
|
9
|
+
*/
|
|
10
|
+
export declare function enableAmbientContainer(): MiddlewareHandler;
|
|
11
|
+
/**
|
|
12
|
+
* The current request's DI container. Throws with actionable guidance if
|
|
13
|
+
* ambient access isn't enabled or this runs outside a request.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getCurrentContainer(): Container;
|
|
16
|
+
/** The current request's {@link RequestContext} (via the ambient container). */
|
|
17
|
+
export declare function getCurrentRequestContext(): RequestContext;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { contextStorage, getContext } from "hono/context-storage";
|
|
2
|
+
import { REQUEST_CONTEXT } from "./request-context.js";
|
|
3
|
+
// Opt-in ambient access to the per-request DI container / RequestContext,
|
|
4
|
+
// for deep code that lacks the Hono `Context` (services, standalone helpers).
|
|
5
|
+
//
|
|
6
|
+
// Portable across vela's target runtimes because the `node:async_hooks`
|
|
7
|
+
// dependency lives inside Hono's `contextStorage()` middleware — vela's own
|
|
8
|
+
// source never imports `node:*`. Uses only the workerd-safe ALS subset
|
|
9
|
+
// (`run()`/`getStore()`); no `enterWith`, no cross-thenable propagation.
|
|
10
|
+
//
|
|
11
|
+
// Default path is unchanged: request scope is still carried by the per-request
|
|
12
|
+
// child container (see RouteManager.getRequestContainer). This is a second
|
|
13
|
+
// READ path, active only when enabled.
|
|
14
|
+
/**
|
|
15
|
+
* Returns Hono's `contextStorage()` middleware. Register it as the FIRST global
|
|
16
|
+
* middleware so `getCurrentContainer()` works everywhere downstream. When using
|
|
17
|
+
* `VelaFactory.create(module, { ambientContainer: true })` this is wired
|
|
18
|
+
* automatically — call this only for manual/raw Hono setups.
|
|
19
|
+
*/ export function enableAmbientContainer() {
|
|
20
|
+
return contextStorage();
|
|
21
|
+
}
|
|
22
|
+
function tryGetContext() {
|
|
23
|
+
try {
|
|
24
|
+
return getContext();
|
|
25
|
+
} catch {
|
|
26
|
+
// Thrown when called outside the contextStorage() middleware (i.e. ambient
|
|
27
|
+
// access not enabled, or outside a request).
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The current request's DI container. Throws with actionable guidance if
|
|
33
|
+
* ambient access isn't enabled or this runs outside a request.
|
|
34
|
+
*/ export function getCurrentContainer() {
|
|
35
|
+
const context = tryGetContext();
|
|
36
|
+
const container = context?.get('container');
|
|
37
|
+
if (!container) {
|
|
38
|
+
throw new Error('getCurrentContainer() is unavailable: enable ambient access via ' + "VelaFactory.create(module, { ambientContainer: true }) (or app.use(enableAmbientContainer())) " + 'and only call it within a request.');
|
|
39
|
+
}
|
|
40
|
+
return container;
|
|
41
|
+
}
|
|
42
|
+
/** The current request's {@link RequestContext} (via the ambient container). */ export function getCurrentRequestContext() {
|
|
43
|
+
return getCurrentContainer().resolve(REQUEST_CONTEXT);
|
|
44
|
+
}
|
|
@@ -10,7 +10,10 @@ export function buildExecutionContext(c, controller, handlerName) {
|
|
|
10
10
|
switchToHttp: ()=>({
|
|
11
11
|
getRequest: ()=>c.req.raw,
|
|
12
12
|
getResponse: ()=>c
|
|
13
|
-
})
|
|
13
|
+
}),
|
|
14
|
+
switchToWs: ()=>{
|
|
15
|
+
throw new Error('switchToWs() called on an HTTP ExecutionContext. This handler runs over HTTP, not a WebSocket gateway.');
|
|
16
|
+
}
|
|
14
17
|
};
|
|
15
18
|
}
|
|
16
19
|
// Sentinel marker exposed via `ExecutionContext.getClass()` when the host is a
|
package/dist/http/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export { MiddlewareBuilder } from '../module/middleware';
|
|
|
4
4
|
export type { MiddlewareConsumer, MiddlewareConfigProxy, RouteInfo, NestModule, MiddlewareRouteDefinition } from '../module/middleware';
|
|
5
5
|
export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, isController, } from './decorators';
|
|
6
6
|
export { createLazyParamDecorator } from './lazy-param.decorator';
|
|
7
|
+
export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext, } from './ambient';
|
|
7
8
|
export type { RouteMetadata, ControllerMetadata, ControllerOptions, ParamMetadata, ControllerRegistration, } from './types';
|
package/dist/http/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { RouteManager } from "./route.manager.js";
|
|
|
2
2
|
export { MiddlewareBuilder } from "../module/middleware.js";
|
|
3
3
|
export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, isController } from "./decorators.js";
|
|
4
4
|
export { createLazyParamDecorator } from "./lazy-param.decorator.js";
|
|
5
|
+
export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext } from "./ambient.js";
|
|
@@ -9,6 +9,12 @@ export interface RouteManagerOptions {
|
|
|
9
9
|
getClientIp?: (c: Context) => string | null;
|
|
10
10
|
middleware?: MiddlewareHandler[];
|
|
11
11
|
globalPrefix?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Opt into ambient request-container access (`getCurrentContainer()` /
|
|
14
|
+
* `getCurrentRequestContext()`). Registers Hono's `contextStorage()` as the
|
|
15
|
+
* first middleware. Off by default — the explicit child container is unchanged.
|
|
16
|
+
*/
|
|
17
|
+
ambientContainer?: boolean;
|
|
12
18
|
}
|
|
13
19
|
export declare class RouteManager {
|
|
14
20
|
private container;
|
|
@@ -22,7 +28,20 @@ export declare class RouteManager {
|
|
|
22
28
|
private globalPrefix;
|
|
23
29
|
private consumerMiddlewareDefinitions;
|
|
24
30
|
private readonly handlerExecutor;
|
|
31
|
+
private readonly ambientContainer;
|
|
25
32
|
constructor(container: Container, options?: RouteManagerOptions);
|
|
33
|
+
/**
|
|
34
|
+
* The global-tier components (`APP_*` provider tokens + imperative
|
|
35
|
+
* `app.useGlobalX()` instances). Exposed so non-HTTP transports (the
|
|
36
|
+
* WebSocket dispatcher) can apply the same app-wide guards/pipes/interceptors/
|
|
37
|
+
* filters as HTTP routes, read live so late registrations propagate.
|
|
38
|
+
*/
|
|
39
|
+
getGlobalComponents(): {
|
|
40
|
+
guards: Array<GuardType | Token<CanActivate>>;
|
|
41
|
+
pipes: Array<PipeType | Token<PipeTransform>>;
|
|
42
|
+
interceptors: Array<InterceptorType | Token<NestInterceptor>>;
|
|
43
|
+
filters: Array<FilterType | Token<ExceptionFilter>>;
|
|
44
|
+
};
|
|
26
45
|
registerConsumerMiddleware(definitions: MiddlewareRouteDefinition[]): this;
|
|
27
46
|
setGlobalPrefix(prefix: string): this;
|
|
28
47
|
useGlobalMiddleware(...middleware: MiddlewareType[]): this;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import { contextStorage } from "hono/context-storage";
|
|
2
3
|
import { HttpMethod } from "../constants.js";
|
|
3
4
|
import { HttpException } from "../errors/http-exception.js";
|
|
4
5
|
import { getMetadata } from "../metadata.js";
|
|
@@ -14,6 +15,39 @@ import { ComponentManager } from "../pipeline/component.manager.js";
|
|
|
14
15
|
import { shouldFilterCatch } from "../pipeline/decorators.js";
|
|
15
16
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
16
17
|
const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
|
|
18
|
+
// Mirror a response body stream, invoking `onDone` exactly once when it is fully
|
|
19
|
+
// read, errors, or is cancelled. Lets request-scoped resources be disposed only
|
|
20
|
+
// after the body has drained — never mid-stream.
|
|
21
|
+
function disposeStreamWhenDone(body, onDone) {
|
|
22
|
+
const reader = body.getReader();
|
|
23
|
+
let finished = false;
|
|
24
|
+
const finish = ()=>{
|
|
25
|
+
if (!finished) {
|
|
26
|
+
finished = true;
|
|
27
|
+
onDone();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return new ReadableStream({
|
|
31
|
+
async pull (controller) {
|
|
32
|
+
try {
|
|
33
|
+
const { done, value } = await reader.read();
|
|
34
|
+
if (done) {
|
|
35
|
+
controller.close();
|
|
36
|
+
finish();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
controller.enqueue(value);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
controller.error(error);
|
|
42
|
+
finish();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
cancel (reason) {
|
|
46
|
+
finish();
|
|
47
|
+
return reader.cancel(reason);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
17
51
|
export class RouteManager {
|
|
18
52
|
container;
|
|
19
53
|
static METHOD_REGISTRAR = new Map([
|
|
@@ -59,8 +93,10 @@ export class RouteManager {
|
|
|
59
93
|
globalPrefix = '';
|
|
60
94
|
consumerMiddlewareDefinitions = [];
|
|
61
95
|
handlerExecutor;
|
|
96
|
+
ambientContainer;
|
|
62
97
|
constructor(container, options = {}){
|
|
63
98
|
this.container = container;
|
|
99
|
+
this.ambientContainer = options.ambientContainer ?? false;
|
|
64
100
|
const argumentResolver = new ArgumentResolver(options.getClientIp ?? defaultGetClientIp);
|
|
65
101
|
this.handlerExecutor = new HandlerExecutor(argumentResolver, ()=>({
|
|
66
102
|
guards: this.globalGuards,
|
|
@@ -69,6 +105,19 @@ export class RouteManager {
|
|
|
69
105
|
filters: this.globalFilters
|
|
70
106
|
}), (c)=>this.getRequestContainer(c));
|
|
71
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The global-tier components (`APP_*` provider tokens + imperative
|
|
110
|
+
* `app.useGlobalX()` instances). Exposed so non-HTTP transports (the
|
|
111
|
+
* WebSocket dispatcher) can apply the same app-wide guards/pipes/interceptors/
|
|
112
|
+
* filters as HTTP routes, read live so late registrations propagate.
|
|
113
|
+
*/ getGlobalComponents() {
|
|
114
|
+
return {
|
|
115
|
+
guards: this.globalGuards,
|
|
116
|
+
pipes: this.globalPipes,
|
|
117
|
+
interceptors: this.globalInterceptors,
|
|
118
|
+
filters: this.globalFilters
|
|
119
|
+
};
|
|
120
|
+
}
|
|
72
121
|
registerConsumerMiddleware(definitions) {
|
|
73
122
|
this.consumerMiddlewareDefinitions.push(...definitions);
|
|
74
123
|
return this;
|
|
@@ -221,6 +270,41 @@ export class RouteManager {
|
|
|
221
270
|
}
|
|
222
271
|
async build() {
|
|
223
272
|
const app = new Hono();
|
|
273
|
+
// Outermost: dispose the per-request child container once the request is
|
|
274
|
+
// fully done. Fast-paths out when the child has no request-scoped
|
|
275
|
+
// disposables (the common case → zero overhead / no behavior change).
|
|
276
|
+
// Streaming-safe: when a response body is present, disposal is deferred
|
|
277
|
+
// until the body is fully read (or errors/cancels), never mid-stream.
|
|
278
|
+
app.use('*', async (c, next)=>{
|
|
279
|
+
let threw = false;
|
|
280
|
+
try {
|
|
281
|
+
await next();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
threw = true;
|
|
284
|
+
throw error;
|
|
285
|
+
} finally{
|
|
286
|
+
const child = c.get('container');
|
|
287
|
+
if (child?.hasDisposables()) {
|
|
288
|
+
const body = threw ? null : c.res?.body ?? null;
|
|
289
|
+
if (body) {
|
|
290
|
+
// Defer disposal to when the runtime finishes reading the body.
|
|
291
|
+
c.res = new Response(disposeStreamWhenDone(body, ()=>void child.dispose()), {
|
|
292
|
+
status: c.res.status,
|
|
293
|
+
statusText: c.res.statusText,
|
|
294
|
+
headers: c.res.headers
|
|
295
|
+
});
|
|
296
|
+
} else {
|
|
297
|
+
// No body (or error path) — nothing streaming, dispose now.
|
|
298
|
+
await child.dispose();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
// Opt-in ambient container: contextStorage() must be an early middleware so
|
|
304
|
+
// getContext() is available to everything downstream.
|
|
305
|
+
if (this.ambientContainer) {
|
|
306
|
+
app.use('*', contextStorage());
|
|
307
|
+
}
|
|
224
308
|
// Sort global middleware by priority (lower runs first) with insertion
|
|
225
309
|
// index as tiebreaker so equal priorities preserve registration order.
|
|
226
310
|
const sortedGlobal = this.globalMiddleware.map((entry, index)=>({
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Deep-merge two message trees; source overrides target at the leaf level. */ export function deepMerge(target, source) {
|
|
2
|
+
const result = {
|
|
3
|
+
...target
|
|
4
|
+
};
|
|
5
|
+
for (const key of Object.keys(source)){
|
|
6
|
+
const targetValue = target[key];
|
|
7
|
+
const sourceValue = source[key];
|
|
8
|
+
if (typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue) && typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)) {
|
|
9
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
10
|
+
} else {
|
|
11
|
+
result[key] = sourceValue;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|