@webpieces/http-routing 0.4.988 → 0.4.990

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.
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LazyControllerState = exports.LazyController = exports.ApplicationImplementation = 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
+ /** Import-only boundary: resolve in the existing app container and preserve its singleton graph. */
45
+ class ApplicationImplementation {
46
+ controller;
47
+ constructor(controller) {
48
+ this.controller = controller;
49
+ }
50
+ }
51
+ exports.ApplicationImplementation = ApplicationImplementation;
52
+ /**
53
+ * Eager identity/security metadata, deferred implementation. The loader imports one feature and
54
+ * returns declarative bindings; it must not open resources or perform request work. A failure is
55
+ * retained per router until restart. The required deadline bounds waiting, not JavaScript CPU.
56
+ */
57
+ class LazyController {
58
+ name;
59
+ filepath;
60
+ timeoutMillis;
61
+ load;
62
+ constructor(name, filepath, timeoutMillis, load) {
63
+ this.name = name;
64
+ this.filepath = filepath;
65
+ this.timeoutMillis = timeoutMillis;
66
+ this.load = load;
67
+ if (!name.trim() || !filepath.trim())
68
+ throw new Error('LazyController requires a stable name and filter filepath.');
69
+ if (!Number.isFinite(timeoutMillis) || timeoutMillis <= 0 || timeoutMillis > 2_147_483_647)
70
+ throw new Error('LazyController timeoutMillis must be positive, finite and at most 2147483647.');
71
+ }
72
+ }
73
+ exports.LazyController = LazyController;
74
+ /** Isolated scope: cannot replace application bindings; explicitly inherited external overrides win. */
75
+ class LazyFeatureBinder extends Binder_1.ContainerBinder {
76
+ scope;
77
+ parent;
78
+ constructor(options, scope, parent) {
79
+ super(options);
80
+ this.scope = scope;
81
+ this.parent = parent;
82
+ }
83
+ bind(token) {
84
+ if (this.scope.isCurrentBound(token) || this.parent.isCurrentBound(token))
85
+ throw new Error(`Lazy feature cannot shadow or duplicate binding ${String(token)}.`);
86
+ return super.bind(token);
87
+ }
88
+ createRpcClientAndBind(api, deployment, options) {
89
+ if (!this.inherit(options?.token ?? api))
90
+ super.createRpcClientAndBind(api, deployment, options);
91
+ }
92
+ createPubSubClientAndBind(api, deployment, options) {
93
+ if (!this.inherit(options?.token ?? api))
94
+ super.createPubSubClientAndBind(api, deployment, options);
95
+ }
96
+ bindExternal(api, implementation) {
97
+ if (!this.inherit(api))
98
+ super.bindExternal(api, implementation);
99
+ }
100
+ inherit(token) {
101
+ if (!this.parent.isCurrentBound(token))
102
+ return false;
103
+ if (!this.scope.isCurrentBound(token))
104
+ this.scope.bind(token).toDynamicValue(() => this.parent.get(token));
105
+ return true;
106
+ }
107
+ }
108
+ /** One retained initialization promise per descriptor per router; callers keep their own context. */
109
+ class LazyControllerState {
110
+ parent;
111
+ api;
112
+ definition;
113
+ timer;
114
+ methods;
115
+ initialized;
116
+ constructor(parent, api, definition, timer, methods) {
117
+ this.parent = parent;
118
+ this.api = api;
119
+ this.definition = definition;
120
+ this.timer = timer;
121
+ this.methods = methods;
122
+ }
123
+ matchesApi(api) {
124
+ return this.api === api;
125
+ }
126
+ resolve() {
127
+ this.initialized ??= core_context_1.RequestContext.runDetachedScope(() => this.initialize());
128
+ return this.initialized;
129
+ }
130
+ async initialize() {
131
+ // No parent-container autobinding: private classes and shared dependencies are explicit.
132
+ // Inversify parent isBound/get can autobind into the parent, even when this child disables it.
133
+ const scope = new inversify_1.Container();
134
+ let abandoned = false;
135
+ let rejectDeadline;
136
+ const expired = new Promise((_resolve, reject) => {
137
+ rejectDeadline = reject;
138
+ });
139
+ const deadline = await this.timer.schedule(this.definition.timeoutMillis, () => {
140
+ abandoned = true;
141
+ rejectDeadline(new Error(`Lazy feature ${this.definition.name} initialization exceeded ${this.definition.timeoutMillis}ms.`));
142
+ });
143
+ // 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
144
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
145
+ try {
146
+ return await Promise.race([expired, this.build(scope, () => abandoned)]);
147
+ }
148
+ finally {
149
+ deadline.cancel();
150
+ }
151
+ }
152
+ async build(scope, abandoned) {
153
+ const implementation = await this.definition.load();
154
+ if (abandoned())
155
+ throw new Error(`Lazy feature ${this.definition.name} initialization was abandoned.`);
156
+ const controller = implementation.controller;
157
+ if (controller.name !== this.definition.name ||
158
+ !this.api.prototype.isPrototypeOf(controller.prototype))
159
+ throw new Error(`Lazy controller ${this.definition.name} must preserve its declared name and extend ${this.api.name}.`);
160
+ const source = Reflect.getMetadata(decorators_1.ROUTING_METADATA_KEYS.SOURCE_FILEPATH, controller);
161
+ if (source !== undefined && source !== this.definition.filepath)
162
+ throw new Error(`Lazy controller ${controller.name} filepath must match its @SourceFile metadata.`);
163
+ for (const method of this.methods) {
164
+ if (typeof Reflect.get(controller.prototype, method) !== 'function' ||
165
+ Reflect.get(controller.prototype, method) ===
166
+ Reflect.get(this.api.prototype, method))
167
+ throw new Error(`Lazy controller ${this.definition.name} must implement ${this.api.name}.${method}.`);
168
+ }
169
+ if (implementation instanceof ApplicationImplementation)
170
+ return this.parent.get(controller);
171
+ for (const token of implementation.shared) {
172
+ if (!this.parent.isCurrentBound(token) &&
173
+ !(typeof token !== 'function' && this.parent.isBound(token)))
174
+ throw new Error(`Lazy feature requires an explicitly bound shared dependency ${String(token)}.`);
175
+ if (scope.isCurrentBound(token))
176
+ throw new Error(`Duplicate shared feature dependency ${String(token)}.`);
177
+ scope.bind(token).toDynamicValue(() => this.parent.get(token));
178
+ }
179
+ // Candidate bindings never mutate the shared container, including when a module fails halfway.
180
+ for (const module of implementation.bindings) {
181
+ await scope.load(new inversify_1.ContainerModule((options) => module.configure(new LazyFeatureBinder(options, scope, this.parent))));
182
+ if (abandoned())
183
+ throw new Error(`Lazy feature ${this.definition.name} initialization was abandoned.`);
184
+ }
185
+ for (const singleton of [...implementation.singletons, controller]) {
186
+ if (scope.isCurrentBound(singleton) || this.parent.isCurrentBound(singleton))
187
+ throw new Error(`Lazy feature cannot shadow or duplicate singleton ${singleton.name}.`);
188
+ scope.bind(singleton).toSelf().inSingletonScope();
189
+ }
190
+ return scope.get(controller);
191
+ }
192
+ }
193
+ exports.LazyControllerState = LazyControllerState;
194
+ //# 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/** Import-only boundary: resolve in the existing app container and preserve its singleton graph. */\nexport class ApplicationImplementation<T extends object> {\n constructor(public readonly controller: Newable<T>) {}\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> | ApplicationImplementation<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 if (implementation instanceof ApplicationImplementation) return this.parent.get(controller);\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,15 @@
1
+ import type { WebpiecesRouter } from './WebpiecesRouter';
2
+ import type { LazyController, LazyImplementation, ApplicationImplementation } 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
+ application(strings: ApplicationImplementation<StringApi>, numbers: ApplicationImplementation<NumberApi>): void;
14
+ }
15
+ export {};
@@ -0,0 +1,29 @@
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
+ application(strings, numbers) {
22
+ // @ts-expect-error Application-container mode still enforces the API's result type.
23
+ const invalid = numbers;
24
+ void invalid;
25
+ void strings;
26
+ }
27
+ }
28
+ exports.LazyControllerCompileAssertions = LazyControllerCompileAssertions;
29
+ //# sourceMappingURL=LazyControllerCompileAssertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LazyControllerCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/LazyControllerCompileAssertions.ts"],"names":[],"mappings":";;;AAOA,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;IACD,WAAW,CACP,OAA6C,EAC7C,OAA6C;QAE7C,oFAAoF;QACpF,MAAM,OAAO,GAAyC,OAAO,CAAC;QAC9D,KAAK,OAAO,CAAC;QACb,KAAK,OAAO,CAAC;IACjB,CAAC;CACJ;AA5BD,0EA4BC","sourcesContent":["import type { WebpiecesRouter } from './WebpiecesRouter';\nimport type {\n LazyController,\n LazyImplementation,\n ApplicationImplementation,\n} 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 application(\n strings: ApplicationImplementation<StringApi>,\n numbers: ApplicationImplementation<NumberApi>,\n ): void {\n // @ts-expect-error Application-container mode still enforces the API's result type.\n const invalid: ApplicationImplementation<StringApi> = numbers;\n void invalid;\n void strings;\n }\n}\n"]}
@@ -59,6 +59,7 @@ export declare class RouteBuilderImpl implements RouteBuilder {
59
59
  private filterRegistry;
60
60
  private container?;
61
61
  private startupTiming;
62
+ private readonly lazyControllers;
62
63
  /** Installed by the router factory; request dispatch does not resolve controllers again. */
63
64
  setStartupObserver(observer?: RuntimeStartupObserver): void;
64
65
  /**
@@ -113,6 +114,9 @@ export declare class RouteBuilderImpl implements RouteBuilder {
113
114
  * @returns RouteHandlerWithMeta containing the handler and route definition
114
115
  */
115
116
  private createRouteHandlerWithMeta;
117
+ private createLazyRouteHandler;
118
+ /** Explicit build/integration validation only; never called by startup or transports. */
119
+ validateLazyRoutesForBuild(): Promise<void>;
116
120
  /**
117
121
  * Register a filter with the filter chain.
118
122
  *
@@ -2,14 +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
13
  const RuntimeStartupTiming_1 = require("./RuntimeStartupTiming");
12
- const log = core_util_1.LogManager.getLogger('RouteBuilder');
14
+ const log = core_util_2.LogManager.getLogger('RouteBuilder');
13
15
  /**
14
16
  * FilterWithMeta - Pairs a resolved filter instance with its definition.
15
17
  * Stores both the DI-resolved filter and the metadata needed for matching.
@@ -81,6 +83,7 @@ let RouteBuilderImpl = class RouteBuilderImpl {
81
83
  filterRegistry = [];
82
84
  container;
83
85
  startupTiming = new RuntimeStartupTiming_1.RuntimeStartupTiming();
86
+ lazyControllers = new Map();
84
87
  /** Installed by the router factory; request dispatch does not resolve controllers again. */
85
88
  setStartupObserver(observer) {
86
89
  this.startupTiming = new RuntimeStartupTiming_1.RuntimeStartupTiming(observer);
@@ -164,13 +167,16 @@ let RouteBuilderImpl = class RouteBuilderImpl {
164
167
  throw new Error('Container not set. Call setContainer() before registering routes.');
165
168
  }
166
169
  const routeMeta = route.routeMeta;
170
+ const implementation = route.implementation;
171
+ if (implementation instanceof LazyController_1.LazyController)
172
+ return this.createLazyRouteHandler(route, implementation);
167
173
  // Resolve controller instance from DI container ONCE (not on every request!)
168
174
  const container = this.container;
169
- const controller = this.startupTiming.run(RuntimeStartupTiming_1.RuntimeStartupPhase.CONTROLLER, route.controllerClass.name, () => container.get(route.controllerClass));
175
+ const controller = this.startupTiming.run(RuntimeStartupTiming_1.RuntimeStartupPhase.CONTROLLER, implementation.name, () => container.get(implementation));
170
176
  // Get the controller method
171
177
  const method = controller[routeMeta.methodName];
172
178
  if (typeof method !== 'function') {
173
- const controllerName = route.controllerClass.name || 'Unknown';
179
+ const controllerName = implementation.name || 'Unknown';
174
180
  throw new Error(`Method ${routeMeta.methodName} not found on controller ${controllerName}`);
175
181
  }
176
182
  const handler = new RouteHandlerImpl(controller,
@@ -180,6 +186,24 @@ let RouteBuilderImpl = class RouteBuilderImpl {
180
186
  // webpieces-disable no-any-unknown -- heterogeneous handlers share one registration collection
181
187
  return new RouteHandlerWithMeta(handler, route);
182
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
+ }
183
207
  /**
184
208
  * Register a filter with the filter chain.
185
209
  *
@@ -318,4 +342,18 @@ exports.RouteBuilderImpl = RouteBuilderImpl;
318
342
  exports.RouteBuilderImpl = RouteBuilderImpl = tslib_1.__decorate([
319
343
  (0, core_context_1.provideFrameworkSingleton)()
320
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
+ }
321
359
  //# sourceMappingURL=RouteBuilderImpl.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RouteBuilderImpl.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/RouteBuilderImpl.ts"],"names":[],"mappings":"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","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';\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\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 // 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 route.controllerClass.name,\n () => container.get(route.controllerClass),\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 = (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 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"]}
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"]}
@@ -1,4 +1,6 @@
1
- import { RouteMetadata } from "@webpieces/core-util";
1
+ import type { ClassType } from './ClassType';
2
+ import type { LazyController } from './LazyController';
3
+ import { RouteMetadata } from '@webpieces/core-util';
2
4
  /**
3
5
  * Represents a route configuration that can be registered with the router.
4
6
  * Similar to Java WebPieces Routes interface.
@@ -25,10 +27,10 @@ export interface RouteBuilder {
25
27
  */
26
28
  export declare class RouteDefinition {
27
29
  routeMeta: RouteMetadata;
28
- controllerClass: any;
30
+ implementation: ClassType<object> | LazyController<object>;
29
31
  controllerFilepath?: string | undefined;
30
- apiClass?: unknown | undefined;
31
- constructor(routeMeta: RouteMetadata, controllerClass: any, controllerFilepath?: string | undefined, apiClass?: unknown | undefined);
32
+ apiClass?: ClassType<object> | undefined;
33
+ constructor(routeMeta: RouteMetadata, implementation: ClassType<object> | LazyController<object>, controllerFilepath?: string | undefined, apiClass?: ClassType<object> | undefined);
32
34
  }
33
35
  /**
34
36
  * Definition of a filter with priority.
package/src/WebAppMeta.js CHANGED
@@ -9,16 +9,14 @@ exports.FilterDefinition = exports.RouteDefinition = void 0;
9
9
  */
10
10
  class RouteDefinition {
11
11
  routeMeta;
12
- controllerClass;
12
+ implementation;
13
13
  controllerFilepath;
14
14
  apiClass;
15
- constructor(routeMeta,
16
- // webpieces-disable no-any-unknown -- arbitrary DI controller class used as a container token
17
- controllerClass, controllerFilepath,
15
+ constructor(routeMeta, implementation, controllerFilepath,
18
16
  // The @ApiPath prototype this route belongs to; surfaced on ApiFactory.apiClients().
19
17
  apiClass) {
20
18
  this.routeMeta = routeMeta;
21
- this.controllerClass = controllerClass;
19
+ this.implementation = implementation;
22
20
  this.controllerFilepath = controllerFilepath;
23
21
  this.apiClass = apiClass;
24
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"WebAppMeta.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebAppMeta.ts"],"names":[],"mappings":";;;AAsBA;;;;;GAKG;AACH,MAAa,eAAe;IAEb;IAEA;IACA;IAEA;IANX,YACW,SAAwB;IAC/B,8FAA8F;IACvF,eAAoB,EACpB,kBAA2B;IAClC,qFAAqF;IAC9E,QAAkB;QALlB,cAAS,GAAT,SAAS,CAAe;QAExB,oBAAe,GAAf,eAAe,CAAK;QACpB,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,aAAQ,GAAR,QAAQ,CAAU;IAC1B,CAAC;CACP;AATD,0CASC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,gBAAgB;IACzB,QAAQ,CAAS;IACjB,6FAA6F;IAC7F,WAAW,CAAM;IACjB,uFAAuF;IACvF,MAAM,CAAO,CAAC,+DAA+D;IAE7E;;;OAGG;IACH,eAAe,CAAS;IAExB,8FAA8F;IAC9F,YAAY,QAAgB,EAAE,WAAgB,EAAE,eAAuB;QACnE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,4BAA4B;IACzD,CAAC;CACJ;AApBD,4CAoBC;AAGD,0FAA0F;AAC1F,2FAA2F;AAC3F,uGAAuG","sourcesContent":["import {RouteMetadata} from \"@webpieces/core-util\";\n\n/**\n * Represents a route configuration that can be registered with the router.\n * Similar to Java WebPieces Routes interface.\n */\nexport interface Routes {\n /**\n * Configure routes using the provided RouteBuilder.\n */\n configure(routeBuilder: RouteBuilder): void;\n}\n\n/**\n * Builder for registering routes.\n * Will be implemented in http-server package.\n */\nexport interface RouteBuilder {\n addRoute(route: RouteDefinition): void;\n addFilter(filter: FilterDefinition): void;\n}\n\n/**\n * Definition of a single route.\n *\n * Generic type parameter TResult represents the return type of the route handler.\n * This provides type safety for the entire request/response cycle.\n */\nexport class RouteDefinition {\n constructor(\n public routeMeta: RouteMetadata,\n // webpieces-disable no-any-unknown -- arbitrary DI controller class used as a container token\n public controllerClass: any,\n public controllerFilepath?: string,\n // The @ApiPath prototype this route belongs to; surfaced on ApiFactory.apiClients().\n public apiClass?: unknown,\n ) {}\n}\n\n/**\n * Definition of a filter with priority.\n *\n * Use filepathPattern to scope filters to specific controllers:\n * - 'src/controllers/admin/**' + '/*.ts' - All admin controllers\n * - '**' + '/admin/**' - Any file in admin directories\n * - '**' + '/UserController.ts' - Specific controller file\n *\n * If filepathPattern is not specified, the filter matches all controllers.\n *\n * Every filter runs for BOTH real HTTP requests AND the in-process createApiClient — there is\n * no transport tier. Transport-boundary auth is a fixed framework filter (AuthFilter) that\n * reads the transport-neutral HttpRequest, so it runs identically in both.\n */\nexport class FilterDefinition {\n priority: number;\n // webpieces-disable no-any-unknown -- an arbitrary DI filter class used as a container token\n filterClass: any;\n // webpieces-disable no-any-unknown -- the resolved filter instance, of arbitrary shape\n filter?: any; // Filter instance (set by RouteBuilder when resolving from DI)\n\n /**\n * Glob pattern to match controller file paths.\n * If not specified, defaults to matching all controllers.\n */\n filepathPattern: string;\n\n // webpieces-disable no-any-unknown -- filterClass param is an arbitrary DI filter class token\n constructor(priority: number, filterClass: any, filepathPattern: string) {\n this.priority = priority;\n this.filterClass = filterClass;\n this.filepathPattern = filepathPattern;\n this.filter = undefined; // Set later by RouteBuilder\n }\n}\n\n\n// The old WebAppMeta interface + WEBAPP_META_TOKEN were removed with the WebpiecesServer/\n// WebpiecesFactory flip. Apps now configure routes/filters imperatively on WebpiecesRouter\n// (see WebpiecesRouter.addRoutes/addFilter) instead of implementing WebAppMeta.getDIModules/getRoutes.\n"]}
1
+ {"version":3,"file":"WebAppMeta.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebAppMeta.ts"],"names":[],"mappings":";;;AAwBA;;;;;GAKG;AACH,MAAa,eAAe;IAEb;IACA;IACA;IAEA;IALX,YACW,SAAwB,EACxB,cAA0D,EAC1D,kBAA2B;IAClC,qFAAqF;IAC9E,QAA4B;QAJ5B,cAAS,GAAT,SAAS,CAAe;QACxB,mBAAc,GAAd,cAAc,CAA4C;QAC1D,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,aAAQ,GAAR,QAAQ,CAAoB;IACpC,CAAC;CACP;AARD,0CAQC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAa,gBAAgB;IACzB,QAAQ,CAAS;IACjB,6FAA6F;IAC7F,WAAW,CAAM;IACjB,uFAAuF;IACvF,MAAM,CAAO,CAAC,+DAA+D;IAE7E;;;OAGG;IACH,eAAe,CAAS;IAExB,8FAA8F;IAC9F,YAAY,QAAgB,EAAE,WAAgB,EAAE,eAAuB;QACnE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,4BAA4B;IACzD,CAAC;CACJ;AApBD,4CAoBC;AAED,0FAA0F;AAC1F,2FAA2F;AAC3F,uGAAuG","sourcesContent":["import type { ClassType } from './ClassType';\nimport type { LazyController } from './LazyController';\nimport { RouteMetadata } from '@webpieces/core-util';\n\n/**\n * Represents a route configuration that can be registered with the router.\n * Similar to Java WebPieces Routes interface.\n */\nexport interface Routes {\n /**\n * Configure routes using the provided RouteBuilder.\n */\n configure(routeBuilder: RouteBuilder): void;\n}\n\n/**\n * Builder for registering routes.\n * Will be implemented in http-server package.\n */\nexport interface RouteBuilder {\n addRoute(route: RouteDefinition): void;\n addFilter(filter: FilterDefinition): void;\n}\n\n/**\n * Definition of a single route.\n *\n * Generic type parameter TResult represents the return type of the route handler.\n * This provides type safety for the entire request/response cycle.\n */\nexport class RouteDefinition {\n constructor(\n public routeMeta: RouteMetadata,\n public implementation: ClassType<object> | LazyController<object>,\n public controllerFilepath?: string,\n // The @ApiPath prototype this route belongs to; surfaced on ApiFactory.apiClients().\n public apiClass?: ClassType<object>,\n ) {}\n}\n\n/**\n * Definition of a filter with priority.\n *\n * Use filepathPattern to scope filters to specific controllers:\n * - 'src/controllers/admin/**' + '/*.ts' - All admin controllers\n * - '**' + '/admin/**' - Any file in admin directories\n * - '**' + '/UserController.ts' - Specific controller file\n *\n * If filepathPattern is not specified, the filter matches all controllers.\n *\n * Every filter runs for BOTH real HTTP requests AND the in-process createApiClient — there is\n * no transport tier. Transport-boundary auth is a fixed framework filter (AuthFilter) that\n * reads the transport-neutral HttpRequest, so it runs identically in both.\n */\nexport class FilterDefinition {\n priority: number;\n // webpieces-disable no-any-unknown -- an arbitrary DI filter class used as a container token\n filterClass: any;\n // webpieces-disable no-any-unknown -- the resolved filter instance, of arbitrary shape\n filter?: any; // Filter instance (set by RouteBuilder when resolving from DI)\n\n /**\n * Glob pattern to match controller file paths.\n * If not specified, defaults to matching all controllers.\n */\n filepathPattern: string;\n\n // webpieces-disable no-any-unknown -- filterClass param is an arbitrary DI filter class token\n constructor(priority: number, filterClass: any, filepathPattern: string) {\n this.priority = priority;\n this.filterClass = filterClass;\n this.filepathPattern = filepathPattern;\n this.filter = undefined; // Set later by RouteBuilder\n }\n}\n\n// The old WebAppMeta interface + WEBAPP_META_TOKEN were removed with the WebpiecesServer/\n// WebpiecesFactory flip. Apps now configure routes/filters imperatively on WebpiecesRouter\n// (see WebpiecesRouter.addRoutes/addFilter) instead of implementing WebAppMeta.getDIModules/getRoutes.\n"]}
@@ -1,8 +1,9 @@
1
+ import { LazyController } from './LazyController';
1
2
  import { AuthorizationService } from './AuthorizationHook';
2
3
  import { InvocationAuthentication } from './InvocationAuthentication';
3
4
  import { Container, ContainerModule } from 'inversify';
4
5
  import { RouteBuilderImpl } from './RouteBuilderImpl';
5
- import { ClassType } from './ApiRoutingFactory';
6
+ import type { ClassType } from './ClassType';
6
7
  import { FilterDefinition } from './WebAppMeta';
7
8
  import { WebpiecesConfig } from './WebpiecesConfig';
8
9
  import { ApiClientFactory } from './ApiClientFactory';
@@ -81,9 +82,10 @@ export declare class WebpiecesRouter implements ApiFactory {
81
82
  private loadDIModules;
82
83
  /**
83
84
  * Wire an API prototype (with @ApiPath/@Endpoint decorators) to its controller.
84
- * Route registration resolves the controller and its dependency graph eagerly, before requests.
85
+ * A class resolves eagerly. A LazyController retains metadata now and imports its implementation
86
+ * only after the ordinary filter chain permits an invocation.
85
87
  */
86
- addRoutes<TApi, TController extends TApi>(api: ClassType<TApi>, controller: ClassType<TController>): this;
88
+ addRoutes<TApi extends object, TController extends TApi>(api: ClassType<TApi>, controller: ClassType<TController> | LazyController<TController>): this;
87
89
  /**
88
90
  * Register a user filter (runs in-process AND over HTTP, below the auto-installed fixed
89
91
  * LogApiFilter + AuthFilter).
@@ -103,6 +105,11 @@ export declare class WebpiecesRouter implements ApiFactory {
103
105
  */
104
106
  apiClients(): ApiClient[];
105
107
  authorizationService(): AuthorizationService;
108
+ /**
109
+ * Warm and validate every optional implementation in a SEPARATE build/integration process with
110
+ * fake external providers. Never invoke this in the production entry point or from a request.
111
+ */
112
+ validateLazyRoutesForBuild(): Promise<void>;
106
113
  /** The application DI container (child of the framework container). */
107
114
  getContainer(): Container;
108
115
  }