@webpieces/http-routing 0.4.987 → 0.4.989

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +111 -0
  2. package/package.json +7 -7
  3. package/src/ApiClient.d.ts +1 -1
  4. package/src/ApiClient.js.map +1 -1
  5. package/src/ApiClientFactory.js.map +1 -1
  6. package/src/ApiRoutingFactory.d.ts +6 -9
  7. package/src/ApiRoutingFactory.js +14 -7
  8. package/src/ApiRoutingFactory.js.map +1 -1
  9. package/src/BindModule.d.ts +6 -0
  10. package/src/BindModule.js +3 -0
  11. package/src/BindModule.js.map +1 -0
  12. package/src/ClassType.d.ts +4 -0
  13. package/src/ClassType.js +3 -0
  14. package/src/ClassType.js.map +1 -0
  15. package/src/LazyController.d.ts +46 -0
  16. package/src/LazyController.js +184 -0
  17. package/src/LazyController.js.map +1 -0
  18. package/src/LazyControllerCompileAssertions.d.ts +14 -0
  19. package/src/LazyControllerCompileAssertions.js +23 -0
  20. package/src/LazyControllerCompileAssertions.js.map +1 -0
  21. package/src/RouteBuilderImpl.d.ts +8 -0
  22. package/src/RouteBuilderImpl.js +51 -5
  23. package/src/RouteBuilderImpl.js.map +1 -1
  24. package/src/RuntimeStartupTiming.d.ts +44 -0
  25. package/src/RuntimeStartupTiming.js +100 -0
  26. package/src/RuntimeStartupTiming.js.map +1 -0
  27. package/src/WebAppMeta.d.ts +6 -4
  28. package/src/WebAppMeta.js +3 -5
  29. package/src/WebAppMeta.js.map +1 -1
  30. package/src/WebpiecesRouter.d.ts +14 -3
  31. package/src/WebpiecesRouter.js +26 -10
  32. package/src/WebpiecesRouter.js.map +1 -1
  33. package/src/Wiring.d.ts +2 -6
  34. package/src/Wiring.js.map +1 -1
  35. package/src/WiringCompileAssertions.d.ts +2 -1
  36. package/src/WiringCompileAssertions.js.map +1 -1
  37. package/src/index.d.ts +7 -2
  38. package/src/index.js +9 -1
  39. package/src/index.js.map +1 -1
  40. package/src/setupRuntime.d.ts +6 -1
  41. package/src/setupRuntime.js +15 -8
  42. package/src/setupRuntime.js.map +1 -1
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LazyControllerState = exports.LazyController = exports.LazyImplementation = exports.LazyFeatureTimer = exports.LazyFeatureDeadline = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const decorators_1 = require("./decorators");
6
+ const inversify_1 = require("inversify");
7
+ const core_context_1 = require("@webpieces/core-context");
8
+ const Binder_1 = require("./Binder");
9
+ /** The cancellable native deadline edge; feature tests replace only this clock. */
10
+ class LazyFeatureDeadline {
11
+ cancel;
12
+ constructor(cancel) {
13
+ this.cancel = cancel;
14
+ }
15
+ }
16
+ exports.LazyFeatureDeadline = LazyFeatureDeadline;
17
+ /** Schedules a bounded first-use initialization; never a per-request authorization cache. */
18
+ class LazyFeatureTimer {
19
+ }
20
+ exports.LazyFeatureTimer = LazyFeatureTimer;
21
+ let NodeLazyFeatureTimer = class NodeLazyFeatureTimer extends LazyFeatureTimer {
22
+ async schedule(timeoutMillis, expire) {
23
+ const timer = setTimeout(expire, timeoutMillis);
24
+ return new LazyFeatureDeadline(() => clearTimeout(timer));
25
+ }
26
+ };
27
+ NodeLazyFeatureTimer = tslib_1.__decorate([
28
+ (0, core_context_1.provideFrameworkSingletonDefaultForApi)(LazyFeatureTimer)
29
+ ], NodeLazyFeatureTimer);
30
+ /** Imported only on first permitted use. Explicitly list private concrete singletons. */
31
+ class LazyImplementation {
32
+ controller;
33
+ bindings;
34
+ singletons;
35
+ shared;
36
+ constructor(controller, bindings, singletons, shared) {
37
+ this.controller = controller;
38
+ this.bindings = bindings;
39
+ this.singletons = singletons;
40
+ this.shared = shared;
41
+ }
42
+ }
43
+ exports.LazyImplementation = LazyImplementation;
44
+ /**
45
+ * Eager identity/security metadata, deferred implementation. The loader imports one feature and
46
+ * returns declarative bindings; it must not open resources or perform request work. A failure is
47
+ * retained per router until restart. The required deadline bounds waiting, not JavaScript CPU.
48
+ */
49
+ class LazyController {
50
+ name;
51
+ filepath;
52
+ timeoutMillis;
53
+ load;
54
+ constructor(name, filepath, timeoutMillis, load) {
55
+ this.name = name;
56
+ this.filepath = filepath;
57
+ this.timeoutMillis = timeoutMillis;
58
+ this.load = load;
59
+ if (!name.trim() || !filepath.trim())
60
+ throw new Error('LazyController requires a stable name and filter filepath.');
61
+ if (!Number.isFinite(timeoutMillis) || timeoutMillis <= 0 || timeoutMillis > 2_147_483_647)
62
+ throw new Error('LazyController timeoutMillis must be positive, finite and at most 2147483647.');
63
+ }
64
+ }
65
+ exports.LazyController = LazyController;
66
+ /** Isolated scope: cannot replace application bindings; explicitly inherited external overrides win. */
67
+ class LazyFeatureBinder extends Binder_1.ContainerBinder {
68
+ scope;
69
+ parent;
70
+ constructor(options, scope, parent) {
71
+ super(options);
72
+ this.scope = scope;
73
+ this.parent = parent;
74
+ }
75
+ bind(token) {
76
+ if (this.scope.isCurrentBound(token) || this.parent.isCurrentBound(token))
77
+ throw new Error(`Lazy feature cannot shadow or duplicate binding ${String(token)}.`);
78
+ return super.bind(token);
79
+ }
80
+ createRpcClientAndBind(api, deployment, options) {
81
+ if (!this.inherit(options?.token ?? api))
82
+ super.createRpcClientAndBind(api, deployment, options);
83
+ }
84
+ createPubSubClientAndBind(api, deployment, options) {
85
+ if (!this.inherit(options?.token ?? api))
86
+ super.createPubSubClientAndBind(api, deployment, options);
87
+ }
88
+ bindExternal(api, implementation) {
89
+ if (!this.inherit(api))
90
+ super.bindExternal(api, implementation);
91
+ }
92
+ inherit(token) {
93
+ if (!this.parent.isCurrentBound(token))
94
+ return false;
95
+ if (!this.scope.isCurrentBound(token))
96
+ this.scope.bind(token).toDynamicValue(() => this.parent.get(token));
97
+ return true;
98
+ }
99
+ }
100
+ /** One retained initialization promise per descriptor per router; callers keep their own context. */
101
+ class LazyControllerState {
102
+ parent;
103
+ api;
104
+ definition;
105
+ timer;
106
+ methods;
107
+ initialized;
108
+ constructor(parent, api, definition, timer, methods) {
109
+ this.parent = parent;
110
+ this.api = api;
111
+ this.definition = definition;
112
+ this.timer = timer;
113
+ this.methods = methods;
114
+ }
115
+ matchesApi(api) {
116
+ return this.api === api;
117
+ }
118
+ resolve() {
119
+ this.initialized ??= core_context_1.RequestContext.runDetachedScope(() => this.initialize());
120
+ return this.initialized;
121
+ }
122
+ async initialize() {
123
+ // No parent-container autobinding: private classes and shared dependencies are explicit.
124
+ // Inversify parent isBound/get can autobind into the parent, even when this child disables it.
125
+ const scope = new inversify_1.Container();
126
+ let abandoned = false;
127
+ let rejectDeadline;
128
+ const expired = new Promise((_resolve, reject) => {
129
+ rejectDeadline = reject;
130
+ });
131
+ const deadline = await this.timer.schedule(this.definition.timeoutMillis, () => {
132
+ abandoned = true;
133
+ rejectDeadline(new Error(`Lazy feature ${this.definition.name} initialization exceeded ${this.definition.timeoutMillis}ms.`));
134
+ });
135
+ // webpieces-disable no-unmanaged-exceptions -- terminal initialization boundary: release deadline on success, import/binding failure or timeout; rejection is retained and reported by normal dispatch
136
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
137
+ try {
138
+ return await Promise.race([expired, this.build(scope, () => abandoned)]);
139
+ }
140
+ finally {
141
+ deadline.cancel();
142
+ }
143
+ }
144
+ async build(scope, abandoned) {
145
+ const implementation = await this.definition.load();
146
+ if (abandoned())
147
+ throw new Error(`Lazy feature ${this.definition.name} initialization was abandoned.`);
148
+ const controller = implementation.controller;
149
+ if (controller.name !== this.definition.name ||
150
+ !this.api.prototype.isPrototypeOf(controller.prototype))
151
+ throw new Error(`Lazy controller ${this.definition.name} must preserve its declared name and extend ${this.api.name}.`);
152
+ const source = Reflect.getMetadata(decorators_1.ROUTING_METADATA_KEYS.SOURCE_FILEPATH, controller);
153
+ if (source !== undefined && source !== this.definition.filepath)
154
+ throw new Error(`Lazy controller ${controller.name} filepath must match its @SourceFile metadata.`);
155
+ for (const method of this.methods) {
156
+ if (typeof Reflect.get(controller.prototype, method) !== 'function' ||
157
+ Reflect.get(controller.prototype, method) ===
158
+ Reflect.get(this.api.prototype, method))
159
+ throw new Error(`Lazy controller ${this.definition.name} must implement ${this.api.name}.${method}.`);
160
+ }
161
+ for (const token of implementation.shared) {
162
+ if (!this.parent.isCurrentBound(token) &&
163
+ !(typeof token !== 'function' && this.parent.isBound(token)))
164
+ throw new Error(`Lazy feature requires an explicitly bound shared dependency ${String(token)}.`);
165
+ if (scope.isCurrentBound(token))
166
+ throw new Error(`Duplicate shared feature dependency ${String(token)}.`);
167
+ scope.bind(token).toDynamicValue(() => this.parent.get(token));
168
+ }
169
+ // Candidate bindings never mutate the shared container, including when a module fails halfway.
170
+ for (const module of implementation.bindings) {
171
+ await scope.load(new inversify_1.ContainerModule((options) => module.configure(new LazyFeatureBinder(options, scope, this.parent))));
172
+ if (abandoned())
173
+ throw new Error(`Lazy feature ${this.definition.name} initialization was abandoned.`);
174
+ }
175
+ for (const singleton of [...implementation.singletons, controller]) {
176
+ if (scope.isCurrentBound(singleton) || this.parent.isCurrentBound(singleton))
177
+ throw new Error(`Lazy feature cannot shadow or duplicate singleton ${singleton.name}.`);
178
+ scope.bind(singleton).toSelf().inSingletonScope();
179
+ }
180
+ return scope.get(controller);
181
+ }
182
+ }
183
+ exports.LazyControllerState = LazyControllerState;
184
+ //# sourceMappingURL=LazyController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LazyController.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/LazyController.ts"],"names":[],"mappings":"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","sourcesContent":["import { ROUTING_METADATA_KEYS } from './decorators';\nimport {\n Container,\n ContainerModule,\n type ContainerModuleLoadOptions,\n type Newable,\n type ServiceIdentifier,\n type BindToFluentSyntax,\n} from 'inversify';\nimport { RequestContext, provideFrameworkSingletonDefaultForApi } from '@webpieces/core-context';\nimport { type ApiPrototype } from '@webpieces/http-client-core';\nimport type { ClassType } from './ClassType';\nimport { ContainerBinder, type ClientBindOptions, type PubSubBindOptions } from './Binder';\nimport type { BindModule } from './BindModule';\n\n/** The cancellable native deadline edge; feature tests replace only this clock. */\nexport class LazyFeatureDeadline {\n constructor(public readonly cancel: () => void) {}\n}\n\n/** Schedules a bounded first-use initialization; never a per-request authorization cache. */\nexport abstract class LazyFeatureTimer {\n abstract schedule(timeoutMillis: number, expire: () => void): Promise<LazyFeatureDeadline>;\n}\n\n@provideFrameworkSingletonDefaultForApi(LazyFeatureTimer)\nclass NodeLazyFeatureTimer extends LazyFeatureTimer {\n override async schedule(\n timeoutMillis: number,\n expire: () => void,\n ): Promise<LazyFeatureDeadline> {\n const timer = setTimeout(expire, timeoutMillis);\n return new LazyFeatureDeadline(() => clearTimeout(timer));\n }\n}\n\n/** Imported only on first permitted use. Explicitly list private concrete singletons. */\nexport class LazyImplementation<T extends object> {\n constructor(\n public readonly controller: Newable<T>,\n public readonly bindings: readonly BindModule[],\n public readonly singletons: readonly Newable<object>[],\n public readonly shared: readonly ServiceIdentifier[],\n ) {}\n}\n\n/**\n * Eager identity/security metadata, deferred implementation. The loader imports one feature and\n * returns declarative bindings; it must not open resources or perform request work. A failure is\n * retained per router until restart. The required deadline bounds waiting, not JavaScript CPU.\n */\nexport class LazyController<T extends object> {\n constructor(\n public readonly name: string,\n public readonly filepath: string,\n public readonly timeoutMillis: number,\n public readonly load: () => Promise<LazyImplementation<T>>,\n ) {\n if (!name.trim() || !filepath.trim())\n throw new Error('LazyController requires a stable name and filter filepath.');\n if (!Number.isFinite(timeoutMillis) || timeoutMillis <= 0 || timeoutMillis > 2_147_483_647)\n throw new Error(\n 'LazyController timeoutMillis must be positive, finite and at most 2147483647.',\n );\n }\n}\n\n/** Isolated scope: cannot replace application bindings; explicitly inherited external overrides win. */\nclass LazyFeatureBinder extends ContainerBinder {\n constructor(\n options: ContainerModuleLoadOptions,\n private readonly scope: Container,\n private readonly parent: Container,\n ) {\n super(options);\n }\n\n override bind<T>(token: ServiceIdentifier<T>): BindToFluentSyntax<T> {\n if (this.scope.isCurrentBound(token) || this.parent.isCurrentBound(token))\n throw new Error(`Lazy feature cannot shadow or duplicate binding ${String(token)}.`);\n return super.bind(token);\n }\n\n override createRpcClientAndBind<T extends object>(\n api: ApiPrototype<T>,\n deployment: string,\n options?: ClientBindOptions<NoInfer<T>>,\n ): void {\n if (!this.inherit(options?.token ?? api))\n super.createRpcClientAndBind(api, deployment, options);\n }\n\n override createPubSubClientAndBind<T extends object>(\n api: ApiPrototype<T>,\n deployment: string,\n options?: PubSubBindOptions<NoInfer<T>>,\n ): void {\n if (!this.inherit(options?.token ?? api))\n super.createPubSubClientAndBind(api, deployment, options);\n }\n\n override bindExternal<T extends object>(\n api: ApiPrototype<T>,\n implementation: Newable<NoInfer<T>>,\n ): void {\n if (!this.inherit(api)) super.bindExternal(api, implementation);\n }\n private inherit<T>(token: ServiceIdentifier<T>): boolean {\n if (!this.parent.isCurrentBound(token)) return false;\n if (!this.scope.isCurrentBound(token))\n this.scope.bind(token).toDynamicValue(() => this.parent.get(token));\n return true;\n }\n}\n\n/** One retained initialization promise per descriptor per router; callers keep their own context. */\nexport class LazyControllerState<T extends object> {\n private initialized?: Promise<T>;\n\n constructor(\n private readonly parent: Container,\n private readonly api: ClassType<T>,\n private readonly definition: LazyController<T>,\n private readonly timer: LazyFeatureTimer,\n private readonly methods: readonly string[],\n ) {}\n\n matchesApi(api: ClassType<object>): boolean {\n return this.api === api;\n }\n\n resolve(): Promise<T> {\n this.initialized ??= RequestContext.runDetachedScope(() => this.initialize());\n return this.initialized;\n }\n\n private async initialize(): Promise<T> {\n // No parent-container autobinding: private classes and shared dependencies are explicit.\n // Inversify parent isBound/get can autobind into the parent, even when this child disables it.\n const scope = new Container();\n let abandoned = false;\n let rejectDeadline!: (error: Error) => void;\n const expired = new Promise<never>((_resolve, reject) => {\n rejectDeadline = reject;\n });\n const deadline = await this.timer.schedule(this.definition.timeoutMillis, () => {\n abandoned = true;\n rejectDeadline(\n new Error(\n `Lazy feature ${this.definition.name} initialization exceeded ${this.definition.timeoutMillis}ms.`,\n ),\n );\n });\n // webpieces-disable no-unmanaged-exceptions -- terminal initialization boundary: release deadline on success, import/binding failure or timeout; rejection is retained and reported by normal dispatch\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return await Promise.race([expired, this.build(scope, () => abandoned)]);\n } finally {\n deadline.cancel();\n }\n }\n\n private async build(scope: Container, abandoned: () => boolean): Promise<T> {\n const implementation = await this.definition.load();\n if (abandoned())\n throw new Error(`Lazy feature ${this.definition.name} initialization was abandoned.`);\n const controller = implementation.controller;\n if (\n controller.name !== this.definition.name ||\n !this.api.prototype.isPrototypeOf(controller.prototype)\n )\n throw new Error(\n `Lazy controller ${this.definition.name} must preserve its declared name and extend ${this.api.name}.`,\n );\n const source: string | undefined = Reflect.getMetadata(\n ROUTING_METADATA_KEYS.SOURCE_FILEPATH,\n controller,\n );\n if (source !== undefined && source !== this.definition.filepath)\n throw new Error(\n `Lazy controller ${controller.name} filepath must match its @SourceFile metadata.`,\n );\n for (const method of this.methods) {\n if (\n typeof Reflect.get(controller.prototype, method) !== 'function' ||\n Reflect.get(controller.prototype, method) ===\n Reflect.get(this.api.prototype, method)\n )\n throw new Error(\n `Lazy controller ${this.definition.name} must implement ${this.api.name}.${method}.`,\n );\n }\n for (const token of implementation.shared) {\n if (\n !this.parent.isCurrentBound(token) &&\n !(typeof token !== 'function' && this.parent.isBound(token))\n )\n throw new Error(\n `Lazy feature requires an explicitly bound shared dependency ${String(token)}.`,\n );\n if (scope.isCurrentBound(token))\n throw new Error(`Duplicate shared feature dependency ${String(token)}.`);\n scope.bind(token).toDynamicValue(() => this.parent.get(token));\n }\n // Candidate bindings never mutate the shared container, including when a module fails halfway.\n for (const module of implementation.bindings) {\n await scope.load(\n new ContainerModule((options: ContainerModuleLoadOptions) =>\n module.configure(new LazyFeatureBinder(options, scope, this.parent)),\n ),\n );\n if (abandoned())\n throw new Error(\n `Lazy feature ${this.definition.name} initialization was abandoned.`,\n );\n }\n for (const singleton of [...implementation.singletons, controller]) {\n if (scope.isCurrentBound(singleton) || this.parent.isCurrentBound(singleton))\n throw new Error(\n `Lazy feature cannot shadow or duplicate singleton ${singleton.name}.`,\n );\n scope.bind(singleton).toSelf().inSingletonScope();\n }\n return scope.get(controller);\n }\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { WebpiecesRouter } from './WebpiecesRouter';
2
+ import type { LazyController, LazyImplementation } from './LazyController';
3
+ declare abstract class StringApi {
4
+ abstract read(): Promise<string>;
5
+ }
6
+ declare abstract class NumberApi {
7
+ abstract read(): Promise<number>;
8
+ }
9
+ /** Compiled by tsc, never invoked: implementation types must agree with the registered API. */
10
+ export declare class LazyControllerCompileAssertions {
11
+ routes(router: WebpiecesRouter, strings: LazyController<StringApi>, numbers: LazyController<NumberApi>): void;
12
+ implementation(strings: LazyImplementation<StringApi>, numbers: LazyImplementation<NumberApi>): void;
13
+ }
14
+ export {};
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LazyControllerCompileAssertions = void 0;
4
+ class StringApi {
5
+ }
6
+ class NumberApi {
7
+ }
8
+ /** Compiled by tsc, never invoked: implementation types must agree with the registered API. */
9
+ class LazyControllerCompileAssertions {
10
+ routes(router, strings, numbers) {
11
+ router.addRoutes(StringApi, strings);
12
+ // @ts-expect-error The deferred controller must implement this contract's result type.
13
+ router.addRoutes(StringApi, numbers);
14
+ }
15
+ implementation(strings, numbers) {
16
+ // @ts-expect-error A number-result implementation cannot be a string-result implementation.
17
+ const invalid = numbers;
18
+ void invalid;
19
+ void strings;
20
+ }
21
+ }
22
+ exports.LazyControllerCompileAssertions = LazyControllerCompileAssertions;
23
+ //# sourceMappingURL=LazyControllerCompileAssertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LazyControllerCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/LazyControllerCompileAssertions.ts"],"names":[],"mappings":";;;AAGA,MAAe,SAAS;CAEvB;AACD,MAAe,SAAS;CAEvB;AAED,+FAA+F;AAC/F,MAAa,+BAA+B;IACxC,MAAM,CACF,MAAuB,EACvB,OAAkC,EAClC,OAAkC;QAElC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACrC,uFAAuF;QACvF,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACzC,CAAC;IACD,cAAc,CACV,OAAsC,EACtC,OAAsC;QAEtC,4FAA4F;QAC5F,MAAM,OAAO,GAAkC,OAAO,CAAC;QACvD,KAAK,OAAO,CAAC;QACb,KAAK,OAAO,CAAC;IACjB,CAAC;CACJ;AAnBD,0EAmBC","sourcesContent":["import type { WebpiecesRouter } from './WebpiecesRouter';\nimport type { LazyController, LazyImplementation } from './LazyController';\n\nabstract class StringApi {\n abstract read(): Promise<string>;\n}\nabstract class NumberApi {\n abstract read(): Promise<number>;\n}\n\n/** Compiled by tsc, never invoked: implementation types must agree with the registered API. */\nexport class LazyControllerCompileAssertions {\n routes(\n router: WebpiecesRouter,\n strings: LazyController<StringApi>,\n numbers: LazyController<NumberApi>,\n ): void {\n router.addRoutes(StringApi, strings);\n // @ts-expect-error The deferred controller must implement this contract's result type.\n router.addRoutes(StringApi, numbers);\n }\n implementation(\n strings: LazyImplementation<StringApi>,\n numbers: LazyImplementation<NumberApi>,\n ): void {\n // @ts-expect-error A number-result implementation cannot be a string-result implementation.\n const invalid: LazyImplementation<StringApi> = numbers;\n void invalid;\n void strings;\n }\n}\n"]}
@@ -6,6 +6,7 @@ import { RouteMetadata } from '@webpieces/core-util';
6
6
  import { Service } from '@webpieces/core-util';
7
7
  import { WpResponse } from './WpResponse';
8
8
  import { HttpFilter } from './FilterMatcher';
9
+ import { RuntimeStartupObserver } from './RuntimeStartupTiming';
9
10
  /**
10
11
  * FilterWithMeta - Pairs a resolved filter instance with its definition.
11
12
  * Stores both the DI-resolved filter and the metadata needed for matching.
@@ -57,6 +58,10 @@ export declare class RouteBuilderImpl implements RouteBuilder {
57
58
  private routes;
58
59
  private filterRegistry;
59
60
  private container?;
61
+ private startupTiming;
62
+ private readonly lazyControllers;
63
+ /** Installed by the router factory; request dispatch does not resolve controllers again. */
64
+ setStartupObserver(observer?: RuntimeStartupObserver): void;
60
65
  /**
61
66
  * Map for O(1) route lookup by method:path.
62
67
  * Used by both addRoute() and createRouteInvoker() for fast route access.
@@ -109,6 +114,9 @@ export declare class RouteBuilderImpl implements RouteBuilder {
109
114
  * @returns RouteHandlerWithMeta containing the handler and route definition
110
115
  */
111
116
  private createRouteHandlerWithMeta;
117
+ private createLazyRouteHandler;
118
+ /** Explicit build/integration validation only; never called by startup or transports. */
119
+ validateLazyRoutesForBuild(): Promise<void>;
112
120
  /**
113
121
  * Register a filter with the filter chain.
114
122
  *
@@ -2,13 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RouteBuilderImpl = exports.RouteHandlerWithMeta = exports.RouteHandlerImpl = exports.FilterWithMeta = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const LazyController_1 = require("./LazyController");
6
+ const core_util_1 = require("@webpieces/core-util");
5
7
  const core_context_1 = require("@webpieces/core-context");
6
8
  const WpResponse_1 = require("./WpResponse");
7
9
  const FilterMatcher_1 = require("./FilterMatcher");
8
- const core_util_1 = require("@webpieces/core-util");
10
+ const core_util_2 = require("@webpieces/core-util");
9
11
  const AuthorizationHook_1 = require("./AuthorizationHook");
10
12
  const AuthFilter_1 = require("./filters/AuthFilter");
11
- const log = core_util_1.LogManager.getLogger('RouteBuilder');
13
+ const RuntimeStartupTiming_1 = require("./RuntimeStartupTiming");
14
+ const log = core_util_2.LogManager.getLogger('RouteBuilder');
12
15
  /**
13
16
  * FilterWithMeta - Pairs a resolved filter instance with its definition.
14
17
  * Stores both the DI-resolved filter and the metadata needed for matching.
@@ -79,6 +82,12 @@ let RouteBuilderImpl = class RouteBuilderImpl {
79
82
  routes = [];
80
83
  filterRegistry = [];
81
84
  container;
85
+ startupTiming = new RuntimeStartupTiming_1.RuntimeStartupTiming();
86
+ lazyControllers = new Map();
87
+ /** Installed by the router factory; request dispatch does not resolve controllers again. */
88
+ setStartupObserver(observer) {
89
+ this.startupTiming = new RuntimeStartupTiming_1.RuntimeStartupTiming(observer);
90
+ }
82
91
  /**
83
92
  * Map for O(1) route lookup by method:path.
84
93
  * Used by both addRoute() and createRouteInvoker() for fast route access.
@@ -158,12 +167,16 @@ let RouteBuilderImpl = class RouteBuilderImpl {
158
167
  throw new Error('Container not set. Call setContainer() before registering routes.');
159
168
  }
160
169
  const routeMeta = route.routeMeta;
170
+ const implementation = route.implementation;
171
+ if (implementation instanceof LazyController_1.LazyController)
172
+ return this.createLazyRouteHandler(route, implementation);
161
173
  // Resolve controller instance from DI container ONCE (not on every request!)
162
- const controller = this.container.get(route.controllerClass);
174
+ const container = this.container;
175
+ const controller = this.startupTiming.run(RuntimeStartupTiming_1.RuntimeStartupPhase.CONTROLLER, implementation.name, () => container.get(implementation));
163
176
  // Get the controller method
164
177
  const method = controller[routeMeta.methodName];
165
178
  if (typeof method !== 'function') {
166
- const controllerName = route.controllerClass.name || 'Unknown';
179
+ const controllerName = implementation.name || 'Unknown';
167
180
  throw new Error(`Method ${routeMeta.methodName} not found on controller ${controllerName}`);
168
181
  }
169
182
  const handler = new RouteHandlerImpl(controller,
@@ -173,6 +186,24 @@ let RouteBuilderImpl = class RouteBuilderImpl {
173
186
  // webpieces-disable no-any-unknown -- heterogeneous handlers share one registration collection
174
187
  return new RouteHandlerWithMeta(handler, route);
175
188
  }
189
+ createLazyRouteHandler(route, implementation) {
190
+ const api = route.apiClass;
191
+ if (typeof api !== 'function' || !('prototype' in api))
192
+ throw new Error('Lazy routes require their API contract.');
193
+ let state = this.lazyControllers.get(implementation);
194
+ if (!state) {
195
+ state = new LazyController_1.LazyControllerState(this.container, api, implementation, this.container.get(LazyController_1.LazyFeatureTimer), Object.keys((0, core_util_1.getEndpoints)(api) ?? {}));
196
+ this.lazyControllers.set(implementation, state);
197
+ }
198
+ if (!state.matchesApi(api))
199
+ throw new Error('A LazyController descriptor belongs to exactly one API contract per router.');
200
+ return new RouteHandlerWithMeta(new LazyRouteHandler(state), route);
201
+ }
202
+ /** Explicit build/integration validation only; never called by startup or transports. */
203
+ async validateLazyRoutesForBuild() {
204
+ for (const state of this.lazyControllers.values())
205
+ await state.resolve();
206
+ }
176
207
  /**
177
208
  * Register a filter with the filter chain.
178
209
  *
@@ -186,7 +217,8 @@ let RouteBuilderImpl = class RouteBuilderImpl {
186
217
  throw new Error('Container not set. Call setContainer() before registering filters.');
187
218
  }
188
219
  // Resolve filter instance from DI container
189
- const filter = this.container.get(filterDef.filterClass);
220
+ const container = this.container;
221
+ const filter = this.startupTiming.run(RuntimeStartupTiming_1.RuntimeStartupPhase.FILTER, filterDef.filterClass.name, () => container.get(filterDef.filterClass));
190
222
  // Store filter with its definition
191
223
  const filterWithMeta = new FilterWithMeta(filter, filterDef);
192
224
  this.filterRegistry.push(filterWithMeta);
@@ -310,4 +342,18 @@ exports.RouteBuilderImpl = RouteBuilderImpl;
310
342
  exports.RouteBuilderImpl = RouteBuilderImpl = tslib_1.__decorate([
311
343
  (0, core_context_1.provideFrameworkSingleton)()
312
344
  ], RouteBuilderImpl);
345
+ /** This is the final invocation handler: authentication, authorization and app filters ran first. */
346
+ // webpieces-disable no-any-unknown -- heterogeneous registered API handlers erase their result type
347
+ class LazyRouteHandler {
348
+ state;
349
+ constructor(state) {
350
+ this.state = state;
351
+ }
352
+ // webpieces-disable no-any-unknown -- shared final dispatcher preserves arbitrary API results
353
+ async execute(meta) {
354
+ const controller = await this.state.resolve();
355
+ const method = Reflect.get(controller, meta.methodName);
356
+ return Reflect.apply(method, controller, [...meta.requestArgs]);
357
+ }
358
+ }
313
359
  //# sourceMappingURL=RouteBuilderImpl.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RouteBuilderImpl.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/RouteBuilderImpl.ts"],"names":[],"mappings":";;;;AAEA,0DAAoE;AAKpE,6CAA0C;AAC1C,mDAA4D;AAC5D,oDAAkD;AAClD,2DAA2D;AAC3D,qDAAkD;AAElD,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;AAMjD;;;GAGG;AACH,MAAa,cAAc;IAEZ;IACA;IAFX,YACW,MAAkB,EAClB,UAA4B;QAD5B,WAAM,GAAN,MAAM,CAAY;QAClB,eAAU,GAAV,UAAU,CAAkB;IACpC,CAAC;CACP;AALD,wCAKC;AAED;;;GAGG;AACH,MAAa,gBAAgB;IAEb;IAEA;IAHZ,YACY,UAAmC;IAC3C,0FAA0F;IAClF,MAA+D;QAF/D,eAAU,GAAV,UAAU,CAAyB;QAEnC,WAAM,GAAN,MAAM,CAAyD;IACxE,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,IAAgB;QAC1B,8CAA8C;QAC9C,sEAAsE;QACtE,MAAM,MAAM,GAAY,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QACxF,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAbD,4CAaC;AACD;;;;;;GAMG;AACH,MAAa,oBAAoB;IAElB;IACA;IAFX,YACW,uBAA8C,EAC9C,UAA2B;QAD3B,4BAAuB,GAAvB,uBAAuB,CAAuB;QAC9C,eAAU,GAAV,UAAU,CAAiB;IACnC,CAAC;CACP;AALD,oDAKC;AAED;;;;;;;;;;;;;;;GAeG;AAEI,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IACjB,MAAM,GAA2B,EAAE,CAAC;IACpC,cAAc,GAA0B,EAAE,CAAC;IAC3C,SAAS,CAAa;IAE9B;;;OAGG;IACK,QAAQ,GAAsC,IAAI,GAAG,EAAE,CAAC;IAEhE;;;OAGG;IACK,cAAc,CAAC,MAAc,EAAE,IAAY;QAC/C,OAAO,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC;IAC7C,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,SAAoB;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,KAAsB;QAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,aAAa,CAAC;QAC7C,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,SAAS,CAAC,IAAI,uDAAuD,CAAC,CAAC;QACnJ,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC9F,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,wCAAoB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,mBAAmB,EAAE,EAAE,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACnE,MAAM,aAAa,GAAG,IAAI,CAAC,0BAA0B,CAAC,KAAK,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAEhC,iDAAiD;QACjD,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAClF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC1C,CAAC;IAED;;;;;;OAMG;IACK,mBAAmB;QACvB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,MAAM,YAAY,uBAAU;gBAAE,OAAO,KAAK,CAAC,MAAM,CAAC;QAChE,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,0BAA0B,CAC9B,KAAsB;QAEtB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;QACzF,CAAC;QAED,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;QAElC,6EAA6E;QAC7E,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,CAA4B,CAAC;QAExF,4BAA4B;QAC5B,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,cAAc,GAAI,KAAK,CAAC,eAA8B,CAAC,IAAI,IAAI,SAAS,CAAC;YAC/E,MAAM,IAAI,KAAK,CACX,UAAU,SAAS,CAAC,UAAU,4BAA4B,cAAc,EAAE,CAC7E,CAAC;QACN,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAChC,UAAU;QACV,oGAAoG;QACpG,MAAiE,CACpE,CAAC;QAEF,uCAAuC;QACvC,+FAA+F;QAC/F,OAAO,IAAI,oBAAoB,CAAC,OAAgC,EAAE,KAAK,CAAC,CAAC;IAC7E,CAAC;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,SAA2B;QACjC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;QAC1F,CAAC;QAED,4CAA4C;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAa,SAAS,CAAC,WAAW,CAAC,CAAC;QAErE,mCAAmC;QACnC,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC7D,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC7C,CAAC;IAED;;;;OAIG;IACH,SAAS;QACL,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED;;;;OAIG;IACH,gBAAgB;QACZ,OAAO,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,GAAG,CAAC,CAAC,UAAU,CAAC,QAAQ,CAC1D,CAAC;IACN,CAAC;IAED;;OAEG;IACK,uBAAuB,CAAsB;IAErD;;OAEG;IACK,oBAAoB;QACxB,IAAI,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC;YAChC,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC9C,IAAI,CAAC,uBAAuB,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;gBACrD,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC;gBAC3B,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;gBACxB,OAAO,GAAG,CAAC;YACf,CAAC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,IAAI,CAAC,uBAAuB,CAAC;IACxC,CAAC;IAED;;;;;;;;;;OAUG;IACI,kBAAkB,CACrB,aAAmC;QAEnC,MAAM,KAAK,GAAG,aAAa,CAAC,UAAU,CAAC;QACvC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;QAElC,GAAG,CAAC,IAAI,CAAC,qBAAqB,SAAS,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAExE,kFAAkF;QAClF,uFAAuF;QACvF,gDAAgD;QAChD,MAAM,iBAAiB,GAAG,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAEtD,uCAAuC;QACvC,MAAM,eAAe,GAAG,6BAAa,CAAC,mBAAmB,CACrD,KAAK,CAAC,kBAAkB,EACxB,iBAAiB,CACpB,CAAC;QAEF,qDAAqD;QACrD,MAAM,iBAAiB,GAA6C;YAChE,MAAM,EAAE,KAAK,EAAE,IAAgB,EAAgC,EAAE;gBAC7D,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,uBAAuB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACzE,mFAAmF;gBACnF,kFAAkF;gBAClF,kFAAkF;gBAClF,IAAI,SAAS,CAAC,cAAc;oBAAE,OAAO,IAAI,uBAAU,CAAC,SAAS,CAAC,CAAC;gBAC/D,sFAAsF;gBACtF,OAAO,IAAI,uBAAU,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;YACxC,CAAC;SACJ,CAAC;QAEF,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACX,yHAAyH,CAC5H,CAAC;QACN,CAAC;QAED,mFAAmF;QACnF,yEAAyE;QACzE,0EAA0E;QAC1E,IAAI,OAAO,GAA6C,iBAAiB,CAAC;QAC1E,KAAK,IAAI,CAAC,GAAG,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACnD,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,OAAO,CAAC;IACnB,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,MAAc,EAAE,IAAY;QAC3C,oDAAoD;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC9C,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAE7C,IAAI,CAAC,aAAa,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,0FAA0F;QAC1F,OAAO,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IAClD,CAAC;IAED;;;;;;OAMG;IACH,YAAY,CAAC,MAAc,EAAE,IAAY;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC;IACxD,CAAC;CACJ,CAAA;AAhRY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,wCAAyB,GAAE;GACf,gBAAgB,CAgR5B","sourcesContent":["import { Container } from 'inversify';\nimport { RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { RouteHandler } from './RouteHandler';\nimport { MethodMeta } from './MethodMeta';\nimport { RouteMetadata, DocumentDesign } from '@webpieces/core-util';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { FilterMatcher, HttpFilter } from './FilterMatcher';\nimport { LogManager } from '@webpieces/core-util';\nimport { AuthorizationService } from './AuthorizationHook';\nimport { AuthFilter } from './filters/AuthFilter';\n\nconst log = LogManager.getLogger('RouteBuilder');\n\ninterface NamedClass {\n name?: string;\n}\n\n/**\n * FilterWithMeta - Pairs a resolved filter instance with its definition.\n * Stores both the DI-resolved filter and the metadata needed for matching.\n */\nexport class FilterWithMeta {\n constructor(\n public filter: HttpFilter,\n public definition: FilterDefinition,\n ) {}\n}\n\n/**\n * RouteHandlerImpl - Concrete implementation of RouteHandler.\n * Wraps a resolved controller and method to invoke on each request.\n */\nexport class RouteHandlerImpl<TResult> implements RouteHandler<TResult> {\n constructor(\n private controller: Record<string, unknown>,\n // webpieces-disable no-any-unknown -- route handlers invoke arbitrary API argument shapes\n private method: (this: unknown, ...args: unknown[]) => Promise<TResult>,\n ) {}\n\n async execute(meta: MethodMeta): Promise<TResult> {\n // Invoke the method with requestDto from meta\n // The controller is already resolved - no DI lookup on every request!\n const result: TResult = await this.method.apply(this.controller, [...meta.requestArgs]);\n return result;\n }\n}\n/**\n * RouteHandlerWithMeta - Pairs a route handler with its definition.\n * Stores both the handler (which wraps the DI-resolved controller) and the route metadata.\n *\n * We use unknown for the generic type since we store different TResult types in the same Map.\n * Type safety is maintained through the generic on RouteDefinition at registration time.\n */\nexport class RouteHandlerWithMeta {\n constructor(\n public invokeControllerHandler: RouteHandler<unknown>,\n public definition: RouteDefinition,\n ) {}\n}\n\n/**\n * RouteBuilderImpl - Concrete implementation of RouteBuilder interface.\n *\n * Similar to Java WebPieces RouteBuilder, this class is responsible for:\n * 1. Registering routes with their handlers\n * 2. Registering filters with priority\n *\n * This class is explicit (not anonymous) to:\n * - Improve traceability and debugging\n * - Make the code easier to understand\n * - Enable better IDE navigation (Cmd+Click on addRoute works!)\n *\n * DI Pattern: This class is registered in webpiecesContainer via @provideFrameworkSingleton()\n * but needs appContainer to resolve filters/controllers. The container is set via\n * setContainer() after appContainer is created (late binding pattern).\n */\n@provideFrameworkSingleton()\nexport class RouteBuilderImpl implements RouteBuilder {\n private routes: RouteHandlerWithMeta[] = [];\n private filterRegistry: Array<FilterWithMeta> = [];\n private container?: Container;\n\n /**\n * Map for O(1) route lookup by method:path.\n * Used by both addRoute() and createRouteInvoker() for fast route access.\n */\n private routeMap: Map<string, RouteHandlerWithMeta> = new Map();\n\n /**\n * Create route key for consistent lookup.\n * Key format: \"${METHOD}:${path}\" (e.g., \"POST:/search/item\")\n */\n private createRouteKey(method: string, path: string): string {\n return `${method.toUpperCase()}:${path}`;\n }\n\n /**\n * Set the DI container used for resolving filters and controllers.\n * Called by WebpiecesCoreServer after appContainer is created.\n *\n * @param container - The application DI container (appContainer)\n */\n setContainer(container: Container): void {\n this.container = container;\n }\n\n /**\n * Register a route with the router.\n *\n * Uses createRouteHandlerWithMeta() to create the handler, then stores it\n * in both the routes array and the routeMap for O(1) lookup.\n *\n * Two STARTUP checks run first, so a wiring mistake refuses to boot instead of 401ing / 403ing\n * the first real request: the authorization policy must be enforceable, and every auth mode the\n * route declares must be verifiable by what is bound (`AuthFilter.assertRouteVerifiable`, #1362).\n *\n * @param route - Route definition with controller class and method name\n */\n addRoute(route: RouteDefinition): void {\n const policy = route.routeMeta.authorization;\n if (!route.routeMeta.authMeta || !policy) throw new Error(`Endpoint ${route.routeMeta.path} requires @WpAuth/@WpAuthPublic and @WpAuthorization.`);\n if (!this.container) throw new Error('Set the route container before registering endpoints.');\n this.container.get(AuthorizationService).validate(policy);\n this.installedAuthFilter()?.assertRouteVerifiable(route.routeMeta);\n const routeWithMeta = this.createRouteHandlerWithMeta(route);\n this.routes.push(routeWithMeta);\n\n // Also add to map for O(1) lookup by method:path\n const key = this.createRouteKey(route.routeMeta.httpMethod, route.routeMeta.path);\n this.routeMap.set(key, routeWithMeta);\n }\n\n /**\n * The AuthFilter that will actually enforce routes — the instance `WebpiecesRouter` installed as a\n * fixed filter BEFORE any route is added, so it already holds every app binding. Asking IT (not a\n * fresh `container.get`) means the startup check judges exactly the bindings the request path\n * will use. A hand-assembled builder that installed no AuthFilter enforces no auth mode, so there\n * is nothing to verify.\n */\n private installedAuthFilter(): AuthFilter | undefined {\n for (const entry of this.filterRegistry) {\n if (entry.filter instanceof AuthFilter) return entry.filter;\n }\n return undefined;\n }\n\n /**\n * Create RouteHandlerWithMeta from a RouteDefinition.\n *\n * Resolves controller from DI container ONCE and creates a handler that\n * invokes the controller method with the request DTO.\n *\n * This method is used by:\n * - addRoute() for production route registration\n * - createRouteInvoker() for test clients (via createApiClient)\n *\n * @param route - Route definition with controller class and method name\n * @returns RouteHandlerWithMeta containing the handler and route definition\n */\n private createRouteHandlerWithMeta<TResult = unknown>(\n route: RouteDefinition,\n ): RouteHandlerWithMeta {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering routes.');\n }\n\n const routeMeta = route.routeMeta;\n\n // Resolve controller instance from DI container ONCE (not on every request!)\n const controller = this.container.get(route.controllerClass) as Record<string, unknown>;\n\n // Get the controller method\n const method = controller[routeMeta.methodName];\n if (typeof method !== 'function') {\n const controllerName = (route.controllerClass as NamedClass).name || 'Unknown';\n throw new Error(\n `Method ${routeMeta.methodName} not found on controller ${controllerName}`,\n );\n }\n\n const handler = new RouteHandlerImpl<TResult>(\n controller,\n // webpieces-disable no-any-unknown -- resolved controller method has the registered API's arguments\n method as (this: unknown, ...args: unknown[]) => Promise<TResult>,\n );\n\n // Return handler with route definition\n // webpieces-disable no-any-unknown -- heterogeneous handlers share one registration collection\n return new RouteHandlerWithMeta(handler as RouteHandler<unknown>, route);\n }\n\n /**\n * Register a filter with the filter chain.\n *\n * Resolves the filter from DI container and pairs it with the filter definition.\n * The definition includes pattern information used for route-specific filtering.\n *\n * @param filterDef - Filter definition with priority, filter class, and optional filepath pattern\n */\n addFilter(filterDef: FilterDefinition): void {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering filters.');\n }\n\n // Resolve filter instance from DI container\n const filter = this.container.get<HttpFilter>(filterDef.filterClass);\n\n // Store filter with its definition\n const filterWithMeta = new FilterWithMeta(filter, filterDef);\n this.filterRegistry.push(filterWithMeta);\n }\n\n /**\n * Get all registered routes.\n *\n * @returns Map of routes with handlers and definitions, keyed by \"METHOD:path\"\n */\n getRoutes(): RouteHandlerWithMeta[] {\n return this.routes;\n }\n\n /**\n * Get all filters sorted by priority (highest priority first).\n *\n * @returns Array of FilterWithMeta sorted by priority\n */\n getSortedFilters(): Array<FilterWithMeta> {\n return [...this.filterRegistry].sort(\n (a, b) => b.definition.priority - a.definition.priority,\n );\n }\n\n /**\n * Cached filter definitions for lazy route setup.\n */\n private cachedFilterDefinitions?: FilterDefinition[];\n\n /**\n * Get filter definitions, computing once and caching.\n */\n private getFilterDefinitions(): FilterDefinition[] {\n if (!this.cachedFilterDefinitions) {\n const sortedFilters = this.getSortedFilters();\n this.cachedFilterDefinitions = sortedFilters.map((fwm) => {\n const def = fwm.definition;\n def.filter = fwm.filter;\n return def;\n });\n }\n return this.cachedFilterDefinitions;\n }\n\n /**\n * Setup a single route by creating its filter chain.\n * This is called lazily by createHandler() and getRouteService().\n *\n * Creates a Service that wraps the filter chain and controller invocation.\n * The service is DTO-only and has no Express dependency.\n *\n * @param key - Route key in format \"METHOD:path\"\n * @param routeWithMeta - Route handler with metadata\n * @returns The service for this route\n */\n public createRouteHandler(\n routeWithMeta: RouteHandlerWithMeta,\n ): Service<MethodMeta, WpResponse<unknown>> {\n const route = routeWithMeta.definition;\n const routeMeta = route.routeMeta;\n\n log.info(`Setting up route: ${routeMeta.httpMethod} ${routeMeta.path}`);\n\n // ONE chain for both HTTP and in-process — no transport tier. The fixed framework\n // filters (LogApiFilter, AuthFilter) are auto-installed and read the transport-neutral\n // HttpRequest, so they run identically in both.\n const filterDefinitions = this.getFilterDefinitions();\n\n // Find matching filters for this route\n const matchingFilters = FilterMatcher.findMatchingFilters(\n route.controllerFilepath,\n filterDefinitions,\n );\n\n // Create service that wraps the controller execution\n const controllerService: Service<MethodMeta, WpResponse<unknown>> = {\n invoke: async (meta: MethodMeta): Promise<WpResponse<unknown>> => {\n const result = await routeWithMeta.invokeControllerHandler.execute(meta);\n // A `responseBody: NONE` endpoint (Promise<void>) has NO response: every transport\n // sends 0 bytes (HTTP 204) and every client resolves undefined, so the in-process\n // client must too. LogApiCall logs an undefined response as `response=undefined`.\n if (routeMeta.noResponseBody) return new WpResponse(undefined);\n // Any other route that yields undefined/null still answers a JSON body: coerce to {}.\n return new WpResponse(result ?? {});\n },\n };\n\n if (matchingFilters.length === 0) {\n throw new Error(\n 'No filters found for route — the framework auto-installs LogApiFilter + AuthFilter, so this indicates a wiring problem.',\n );\n }\n\n // Chain filters: highest priority (first in array) should run first (be outermost)\n // Build from innermost (lowest priority) to outermost (highest priority)\n // Start with controller, then wrap with filters in reverse priority order\n let service: Service<MethodMeta, WpResponse<unknown>> = controllerService;\n for (let i = matchingFilters.length - 1; i >= 0; i--) {\n service = matchingFilters[i].chainService(service);\n }\n\n return service;\n }\n\n /**\n * Create an invoker function for a route (for testing via createApiClient).\n * Uses routeMap for O(1) lookup, sets up the filter chain ONCE,\n * and returns a Service that can be called multiple times without\n * recreating the filter chain.\n *\n * This method is called by WebpiecesServer.createApiClient() during proxy setup.\n * The returned Service is stored as the proxy method and invoked on each call.\n *\n * @param method - HTTP method (GET, POST, etc.)\n * @param path - URL path\n * @returns A Service that invokes the route\n */\n createRouteInvoker(method: string, path: string): Service<MethodMeta, WpResponse<unknown>> {\n // Use routeMap for O(1) lookup (not linear search!)\n const key = this.createRouteKey(method, path);\n const routeWithMeta = this.routeMap.get(key);\n\n if (!routeWithMeta) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Setup filter chain ONCE (not on every invocation!). Same chain as HTTP — auth included.\n return this.createRouteHandler(routeWithMeta);\n }\n\n /**\n * Look up the RouteMetadata (incl. authMeta) for a registered route by method+path.\n * Used to build a MethodMeta for an in-process dispatch (e.g. a delivered cloud\n * task) so the filter chain sees the same routeMeta production HTTP would.\n *\n * @returns the route's RouteMetadata, or undefined if no route is registered.\n */\n getRouteMeta(method: string, path: string): RouteMetadata | undefined {\n const key = this.createRouteKey(method, path);\n return this.routeMap.get(key)?.definition.routeMeta;\n }\n}\n"]}
1
+ {"version":3,"file":"RouteBuilderImpl.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/RouteBuilderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import { LazyController, LazyControllerState, LazyFeatureTimer } from './LazyController';\nimport { getEndpoints } from '@webpieces/core-util';\nimport { Container } from 'inversify';\nimport { RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { RouteHandler } from './RouteHandler';\nimport { MethodMeta } from './MethodMeta';\nimport { RouteMetadata, DocumentDesign } from '@webpieces/core-util';\nimport { Service } from '@webpieces/core-util';\nimport { WpResponse } from './WpResponse';\nimport { FilterMatcher, HttpFilter } from './FilterMatcher';\nimport { LogManager } from '@webpieces/core-util';\nimport { AuthorizationService } from './AuthorizationHook';\nimport { AuthFilter } from './filters/AuthFilter';\nimport {\n RuntimeStartupObserver,\n RuntimeStartupPhase,\n RuntimeStartupTiming,\n} from './RuntimeStartupTiming';\n\nconst log = LogManager.getLogger('RouteBuilder');\n\ninterface NamedClass {\n name?: string;\n}\n\n/**\n * FilterWithMeta - Pairs a resolved filter instance with its definition.\n * Stores both the DI-resolved filter and the metadata needed for matching.\n */\nexport class FilterWithMeta {\n constructor(\n public filter: HttpFilter,\n public definition: FilterDefinition,\n ) {}\n}\n\n/**\n * RouteHandlerImpl - Concrete implementation of RouteHandler.\n * Wraps a resolved controller and method to invoke on each request.\n */\nexport class RouteHandlerImpl<TResult> implements RouteHandler<TResult> {\n constructor(\n private controller: Record<string, unknown>,\n // webpieces-disable no-any-unknown -- route handlers invoke arbitrary API argument shapes\n private method: (this: unknown, ...args: unknown[]) => Promise<TResult>,\n ) {}\n\n async execute(meta: MethodMeta): Promise<TResult> {\n // Invoke the method with requestDto from meta\n // The controller is already resolved - no DI lookup on every request!\n const result: TResult = await this.method.apply(this.controller, [...meta.requestArgs]);\n return result;\n }\n}\n/**\n * RouteHandlerWithMeta - Pairs a route handler with its definition.\n * Stores both the handler (which wraps the DI-resolved controller) and the route metadata.\n *\n * We use unknown for the generic type since we store different TResult types in the same Map.\n * Type safety is maintained through the generic on RouteDefinition at registration time.\n */\nexport class RouteHandlerWithMeta {\n constructor(\n public invokeControllerHandler: RouteHandler<unknown>,\n public definition: RouteDefinition,\n ) {}\n}\n\n/**\n * RouteBuilderImpl - Concrete implementation of RouteBuilder interface.\n *\n * Similar to Java WebPieces RouteBuilder, this class is responsible for:\n * 1. Registering routes with their handlers\n * 2. Registering filters with priority\n *\n * This class is explicit (not anonymous) to:\n * - Improve traceability and debugging\n * - Make the code easier to understand\n * - Enable better IDE navigation (Cmd+Click on addRoute works!)\n *\n * DI Pattern: This class is registered in webpiecesContainer via @provideFrameworkSingleton()\n * but needs appContainer to resolve filters/controllers. The container is set via\n * setContainer() after appContainer is created (late binding pattern).\n */\n@provideFrameworkSingleton()\nexport class RouteBuilderImpl implements RouteBuilder {\n private routes: RouteHandlerWithMeta[] = [];\n private filterRegistry: Array<FilterWithMeta> = [];\n private container?: Container;\n private startupTiming = new RuntimeStartupTiming();\n private readonly lazyControllers = new Map<\n LazyController<object>,\n LazyControllerState<object>\n >();\n\n /** Installed by the router factory; request dispatch does not resolve controllers again. */\n setStartupObserver(observer?: RuntimeStartupObserver): void {\n this.startupTiming = new RuntimeStartupTiming(observer);\n }\n\n /**\n * Map for O(1) route lookup by method:path.\n * Used by both addRoute() and createRouteInvoker() for fast route access.\n */\n private routeMap: Map<string, RouteHandlerWithMeta> = new Map();\n\n /**\n * Create route key for consistent lookup.\n * Key format: \"${METHOD}:${path}\" (e.g., \"POST:/search/item\")\n */\n private createRouteKey(method: string, path: string): string {\n return `${method.toUpperCase()}:${path}`;\n }\n\n /**\n * Set the DI container used for resolving filters and controllers.\n * Called by WebpiecesCoreServer after appContainer is created.\n *\n * @param container - The application DI container (appContainer)\n */\n setContainer(container: Container): void {\n this.container = container;\n }\n\n /**\n * Register a route with the router.\n *\n * Uses createRouteHandlerWithMeta() to create the handler, then stores it\n * in both the routes array and the routeMap for O(1) lookup.\n *\n * Two STARTUP checks run first, so a wiring mistake refuses to boot instead of 401ing / 403ing\n * the first real request: the authorization policy must be enforceable, and every auth mode the\n * route declares must be verifiable by what is bound (`AuthFilter.assertRouteVerifiable`, #1362).\n *\n * @param route - Route definition with controller class and method name\n */\n addRoute(route: RouteDefinition): void {\n const policy = route.routeMeta.authorization;\n if (!route.routeMeta.authMeta || !policy)\n throw new Error(\n `Endpoint ${route.routeMeta.path} requires @WpAuth/@WpAuthPublic and @WpAuthorization.`,\n );\n if (!this.container)\n throw new Error('Set the route container before registering endpoints.');\n this.container.get(AuthorizationService).validate(policy);\n this.installedAuthFilter()?.assertRouteVerifiable(route.routeMeta);\n const routeWithMeta = this.createRouteHandlerWithMeta(route);\n this.routes.push(routeWithMeta);\n\n // Also add to map for O(1) lookup by method:path\n const key = this.createRouteKey(route.routeMeta.httpMethod, route.routeMeta.path);\n this.routeMap.set(key, routeWithMeta);\n }\n\n /**\n * The AuthFilter that will actually enforce routes — the instance `WebpiecesRouter` installed as a\n * fixed filter BEFORE any route is added, so it already holds every app binding. Asking IT (not a\n * fresh `container.get`) means the startup check judges exactly the bindings the request path\n * will use. A hand-assembled builder that installed no AuthFilter enforces no auth mode, so there\n * is nothing to verify.\n */\n private installedAuthFilter(): AuthFilter | undefined {\n for (const entry of this.filterRegistry) {\n if (entry.filter instanceof AuthFilter) return entry.filter;\n }\n return undefined;\n }\n\n /**\n * Create RouteHandlerWithMeta from a RouteDefinition.\n *\n * Resolves controller from DI container ONCE and creates a handler that\n * invokes the controller method with the request DTO.\n *\n * This method is used by:\n * - addRoute() for production route registration\n * - createRouteInvoker() for test clients (via createApiClient)\n *\n * @param route - Route definition with controller class and method name\n * @returns RouteHandlerWithMeta containing the handler and route definition\n */\n private createRouteHandlerWithMeta<TResult = unknown>(\n route: RouteDefinition,\n ): RouteHandlerWithMeta {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering routes.');\n }\n\n const routeMeta = route.routeMeta;\n\n const implementation = route.implementation;\n if (implementation instanceof LazyController)\n return this.createLazyRouteHandler(route, implementation);\n\n // Resolve controller instance from DI container ONCE (not on every request!)\n const container = this.container;\n const controller = this.startupTiming.run(\n RuntimeStartupPhase.CONTROLLER,\n implementation.name,\n () => container.get(implementation),\n ) as Record<\n string,\n // webpieces-disable no-any-unknown -- heterogeneous route handlers erase API-specific result and argument shapes\n unknown\n >;\n\n // Get the controller method\n const method = controller[routeMeta.methodName];\n if (typeof method !== 'function') {\n const controllerName = (implementation as NamedClass).name || 'Unknown';\n throw new Error(\n `Method ${routeMeta.methodName} not found on controller ${controllerName}`,\n );\n }\n\n const handler = new RouteHandlerImpl<TResult>(\n controller,\n // webpieces-disable no-any-unknown -- resolved controller method has the registered API's arguments\n method as (this: unknown, ...args: unknown[]) => Promise<TResult>,\n );\n\n // Return handler with route definition\n // webpieces-disable no-any-unknown -- heterogeneous handlers share one registration collection\n return new RouteHandlerWithMeta(handler as RouteHandler<unknown>, route);\n }\n\n private createLazyRouteHandler(\n route: RouteDefinition,\n implementation: LazyController<object>,\n ): RouteHandlerWithMeta {\n const api = route.apiClass;\n if (typeof api !== 'function' || !('prototype' in api))\n throw new Error('Lazy routes require their API contract.');\n let state = this.lazyControllers.get(implementation);\n if (!state) {\n state = new LazyControllerState(\n this.container!,\n api,\n implementation,\n this.container!.get(LazyFeatureTimer),\n Object.keys(getEndpoints(api) ?? {}),\n );\n this.lazyControllers.set(implementation, state);\n }\n if (!state.matchesApi(api))\n throw new Error(\n 'A LazyController descriptor belongs to exactly one API contract per router.',\n );\n return new RouteHandlerWithMeta(new LazyRouteHandler(state), route);\n }\n\n /** Explicit build/integration validation only; never called by startup or transports. */\n async validateLazyRoutesForBuild(): Promise<void> {\n for (const state of this.lazyControllers.values()) await state.resolve();\n }\n\n /**\n * Register a filter with the filter chain.\n *\n * Resolves the filter from DI container and pairs it with the filter definition.\n * The definition includes pattern information used for route-specific filtering.\n *\n * @param filterDef - Filter definition with priority, filter class, and optional filepath pattern\n */\n addFilter(filterDef: FilterDefinition): void {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering filters.');\n }\n\n // Resolve filter instance from DI container\n const container = this.container;\n const filter = this.startupTiming.run(\n RuntimeStartupPhase.FILTER,\n filterDef.filterClass.name,\n () => container.get<HttpFilter>(filterDef.filterClass),\n );\n\n // Store filter with its definition\n const filterWithMeta = new FilterWithMeta(filter, filterDef);\n this.filterRegistry.push(filterWithMeta);\n }\n\n /**\n * Get all registered routes.\n *\n * @returns Map of routes with handlers and definitions, keyed by \"METHOD:path\"\n */\n getRoutes(): RouteHandlerWithMeta[] {\n return this.routes;\n }\n\n /**\n * Get all filters sorted by priority (highest priority first).\n *\n * @returns Array of FilterWithMeta sorted by priority\n */\n getSortedFilters(): Array<FilterWithMeta> {\n return [...this.filterRegistry].sort(\n (a, b) => b.definition.priority - a.definition.priority,\n );\n }\n\n /**\n * Cached filter definitions for lazy route setup.\n */\n private cachedFilterDefinitions?: FilterDefinition[];\n\n /**\n * Get filter definitions, computing once and caching.\n */\n private getFilterDefinitions(): FilterDefinition[] {\n if (!this.cachedFilterDefinitions) {\n const sortedFilters = this.getSortedFilters();\n this.cachedFilterDefinitions = sortedFilters.map((fwm) => {\n const def = fwm.definition;\n def.filter = fwm.filter;\n return def;\n });\n }\n return this.cachedFilterDefinitions;\n }\n\n /**\n * Setup a single route by creating its filter chain.\n * This is called lazily by createHandler() and getRouteService().\n *\n * Creates a Service that wraps the filter chain and controller invocation.\n * The service is DTO-only and has no Express dependency.\n *\n * @param key - Route key in format \"METHOD:path\"\n * @param routeWithMeta - Route handler with metadata\n * @returns The service for this route\n */\n public createRouteHandler(\n routeWithMeta: RouteHandlerWithMeta,\n ): Service<MethodMeta, WpResponse<unknown>> {\n const route = routeWithMeta.definition;\n const routeMeta = route.routeMeta;\n\n log.info(`Setting up route: ${routeMeta.httpMethod} ${routeMeta.path}`);\n\n // ONE chain for both HTTP and in-process — no transport tier. The fixed framework\n // filters (LogApiFilter, AuthFilter) are auto-installed and read the transport-neutral\n // HttpRequest, so they run identically in both.\n const filterDefinitions = this.getFilterDefinitions();\n\n // Find matching filters for this route\n const matchingFilters = FilterMatcher.findMatchingFilters(\n route.controllerFilepath,\n filterDefinitions,\n );\n\n // Create service that wraps the controller execution\n const controllerService: Service<MethodMeta, WpResponse<unknown>> = {\n invoke: async (meta: MethodMeta): Promise<WpResponse<unknown>> => {\n const result = await routeWithMeta.invokeControllerHandler.execute(meta);\n // A `responseBody: NONE` endpoint (Promise<void>) has NO response: every transport\n // sends 0 bytes (HTTP 204) and every client resolves undefined, so the in-process\n // client must too. LogApiCall logs an undefined response as `response=undefined`.\n if (routeMeta.noResponseBody) return new WpResponse(undefined);\n // Any other route that yields undefined/null still answers a JSON body: coerce to {}.\n return new WpResponse(result ?? {});\n },\n };\n\n if (matchingFilters.length === 0) {\n throw new Error(\n 'No filters found for route — the framework auto-installs LogApiFilter + AuthFilter, so this indicates a wiring problem.',\n );\n }\n\n // Chain filters: highest priority (first in array) should run first (be outermost)\n // Build from innermost (lowest priority) to outermost (highest priority)\n // Start with controller, then wrap with filters in reverse priority order\n let service: Service<MethodMeta, WpResponse<unknown>> = controllerService;\n for (let i = matchingFilters.length - 1; i >= 0; i--) {\n service = matchingFilters[i].chainService(service);\n }\n\n return service;\n }\n\n /**\n * Create an invoker function for a route (for testing via createApiClient).\n * Uses routeMap for O(1) lookup, sets up the filter chain ONCE,\n * and returns a Service that can be called multiple times without\n * recreating the filter chain.\n *\n * This method is called by WebpiecesServer.createApiClient() during proxy setup.\n * The returned Service is stored as the proxy method and invoked on each call.\n *\n * @param method - HTTP method (GET, POST, etc.)\n * @param path - URL path\n * @returns A Service that invokes the route\n */\n createRouteInvoker(method: string, path: string): Service<MethodMeta, WpResponse<unknown>> {\n // Use routeMap for O(1) lookup (not linear search!)\n const key = this.createRouteKey(method, path);\n const routeWithMeta = this.routeMap.get(key);\n\n if (!routeWithMeta) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Setup filter chain ONCE (not on every invocation!). Same chain as HTTP — auth included.\n return this.createRouteHandler(routeWithMeta);\n }\n\n /**\n * Look up the RouteMetadata (incl. authMeta) for a registered route by method+path.\n * Used to build a MethodMeta for an in-process dispatch (e.g. a delivered cloud\n * task) so the filter chain sees the same routeMeta production HTTP would.\n *\n * @returns the route's RouteMetadata, or undefined if no route is registered.\n */\n getRouteMeta(method: string, path: string): RouteMetadata | undefined {\n const key = this.createRouteKey(method, path);\n return this.routeMap.get(key)?.definition.routeMeta;\n }\n}\n\n/** This is the final invocation handler: authentication, authorization and app filters ran first. */\n// webpieces-disable no-any-unknown -- heterogeneous registered API handlers erase their result type\nclass LazyRouteHandler implements RouteHandler<unknown> {\n constructor(private readonly state: LazyControllerState<object>) {}\n\n // webpieces-disable no-any-unknown -- shared final dispatcher preserves arbitrary API results\n async execute(meta: MethodMeta): Promise<unknown> {\n const controller = await this.state.resolve();\n const method = Reflect.get(controller, meta.methodName);\n return Reflect.apply(method, controller, [...meta.requestArgs]);\n }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /** Startup operations; durations are inclusive, so nested operations must not be added together. */
2
+ export declare enum RuntimeStartupPhase {
3
+ HEADERS = "headers",
4
+ LOGGING = "logging",
5
+ WIRING = "wiring",
6
+ CONTAINER = "container",
7
+ FRAMEWORK_DI = "framework-di",
8
+ APPLICATION_DI = "application-di",
9
+ BIND_MODULE = "binding-module",
10
+ FIXED_FILTERS = "fixed-filters",
11
+ FILTER = "filter-resolution",
12
+ ROUTE_MODULE = "route-module",
13
+ ROUTES = "route-registration",
14
+ CONTROLLER = "controller-resolution"
15
+ }
16
+ /** Code identity only: never configuration, credentials, request data or exception payloads. */
17
+ export declare class RuntimeStartupOperation {
18
+ readonly phase: RuntimeStartupPhase;
19
+ readonly name: string;
20
+ constructor(phase: RuntimeStartupPhase, name: string);
21
+ }
22
+ /** The host owns the monotonic clock/output and closes each started span exactly once. */
23
+ export interface RuntimeStartupSpan {
24
+ success(): void;
25
+ failure(): void;
26
+ }
27
+ /**
28
+ * Optional startup-only diagnostics. Start/finish use the host's clock, so timing requires no SDK.
29
+ * A controller-resolution event measures container.get, including dependencies and singleton reuse;
30
+ * it does not assert that a new controller was constructed. Normal API calls emit no events.
31
+ */
32
+ export interface RuntimeStartupObserver {
33
+ start(operation: RuntimeStartupOperation): RuntimeStartupSpan;
34
+ }
35
+ /** Internal operation boundary. With no observer it creates no timing records and reads no clock. */
36
+ export declare class RuntimeStartupTiming {
37
+ private readonly observer?;
38
+ private disabled;
39
+ constructor(observer?: RuntimeStartupObserver | undefined);
40
+ run<T>(phase: RuntimeStartupPhase, name: string, operation: () => T): T;
41
+ runAsync<T>(phase: RuntimeStartupPhase, name: string, operation: () => T | Promise<T>): Promise<T>;
42
+ private begin;
43
+ private finish;
44
+ }