@zmdb/web 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +49 -0
- package/dist/app/bridge.d.ts +12 -0
- package/dist/app/bridge.d.ts.map +1 -0
- package/dist/app/bridge.js +16 -0
- package/dist/app/bridge.js.map +1 -0
- package/dist/app/index.d.ts +23 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +47 -0
- package/dist/app/index.js.map +1 -0
- package/dist/bench/index.d.ts +68 -0
- package/dist/bench/index.d.ts.map +1 -0
- package/dist/bench/index.js +250 -0
- package/dist/bench/index.js.map +1 -0
- package/dist/compression/index.d.ts +13 -0
- package/dist/compression/index.d.ts.map +1 -0
- package/dist/compression/index.js +211 -0
- package/dist/compression/index.js.map +1 -0
- package/dist/context/index.d.ts +74 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +118 -0
- package/dist/context/index.js.map +1 -0
- package/dist/contract/__fixtures__/http-client.generated.d.ts +70 -0
- package/dist/contract/__fixtures__/http-client.generated.d.ts.map +1 -0
- package/dist/contract/__fixtures__/http-client.generated.js +86 -0
- package/dist/contract/__fixtures__/http-client.generated.js.map +1 -0
- package/dist/contract/__fixtures__/http-convergence.d.ts +396 -0
- package/dist/contract/__fixtures__/http-convergence.d.ts.map +1 -0
- package/dist/contract/__fixtures__/http-convergence.js +425 -0
- package/dist/contract/__fixtures__/http-convergence.js.map +1 -0
- package/dist/contract/compiler/client.d.ts +12 -0
- package/dist/contract/compiler/client.d.ts.map +1 -0
- package/dist/contract/compiler/client.js +1293 -0
- package/dist/contract/compiler/client.js.map +1 -0
- package/dist/contract/compiler/index.d.ts +18 -0
- package/dist/contract/compiler/index.d.ts.map +1 -0
- package/dist/contract/compiler/index.js +1058 -0
- package/dist/contract/compiler/index.js.map +1 -0
- package/dist/contract/index.d.ts +237 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +22 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/csrf/index.d.ts +15 -0
- package/dist/csrf/index.d.ts.map +1 -0
- package/dist/csrf/index.js +218 -0
- package/dist/csrf/index.js.map +1 -0
- package/dist/data/index.d.ts +35 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +58 -0
- package/dist/data/index.js.map +1 -0
- package/dist/devtools/index.d.ts +74 -0
- package/dist/devtools/index.d.ts.map +1 -0
- package/dist/devtools/index.js +552 -0
- package/dist/devtools/index.js.map +1 -0
- package/dist/dto-pipes/index.d.ts +45 -0
- package/dist/dto-pipes/index.d.ts.map +1 -0
- package/dist/dto-pipes/index.js +78 -0
- package/dist/dto-pipes/index.js.map +1 -0
- package/dist/gateways/index.d.ts +31 -0
- package/dist/gateways/index.d.ts.map +1 -0
- package/dist/gateways/index.js +138 -0
- package/dist/gateways/index.js.map +1 -0
- package/dist/health/index.d.ts +20 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +148 -0
- package/dist/health/index.js.map +1 -0
- package/dist/http-token.d.ts +2 -0
- package/dist/http-token.d.ts.map +1 -0
- package/dist/http-token.js +21 -0
- package/dist/http-token.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/index.d.ts +11 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +39 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/middleware/errors.d.ts +14 -0
- package/dist/middleware/errors.d.ts.map +1 -0
- package/dist/middleware/errors.js +18 -0
- package/dist/middleware/errors.js.map +1 -0
- package/dist/middleware/index.d.ts +46 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +64 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/modules/__fixtures__/large-graph.d.ts +239 -0
- package/dist/modules/__fixtures__/large-graph.d.ts.map +1 -0
- package/dist/modules/__fixtures__/large-graph.js +483 -0
- package/dist/modules/__fixtures__/large-graph.js.map +1 -0
- package/dist/openapi/__fixtures__/entities.d.ts +8 -0
- package/dist/openapi/__fixtures__/entities.d.ts.map +1 -0
- package/dist/openapi/__fixtures__/entities.js +4 -0
- package/dist/openapi/__fixtures__/entities.js.map +1 -0
- package/dist/openapi/__fixtures__/openapi-tools.fixture.d.ts +38 -0
- package/dist/openapi/__fixtures__/openapi-tools.fixture.d.ts.map +1 -0
- package/dist/openapi/__fixtures__/openapi-tools.fixture.js +172 -0
- package/dist/openapi/__fixtures__/openapi-tools.fixture.js.map +1 -0
- package/dist/openapi/__fixtures__/route-schemas.d.ts +2 -0
- package/dist/openapi/__fixtures__/route-schemas.d.ts.map +1 -0
- package/dist/openapi/__fixtures__/route-schemas.js +17 -0
- package/dist/openapi/__fixtures__/route-schemas.js.map +1 -0
- package/dist/openapi/index.d.ts +61 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +314 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/pipeline/guards.d.ts +9 -0
- package/dist/pipeline/guards.d.ts.map +1 -0
- package/dist/pipeline/guards.js +11 -0
- package/dist/pipeline/guards.js.map +1 -0
- package/dist/pipeline/index.d.ts +141 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +1624 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/routing/index.d.ts +40 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +162 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/static/index.d.ts +14 -0
- package/dist/static/index.d.ts.map +1 -0
- package/dist/static/index.js +348 -0
- package/dist/static/index.js.map +1 -0
- package/dist/testing/index.d.ts +20 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/traced-router.d.ts +5 -0
- package/dist/traced-router.d.ts.map +1 -0
- package/dist/traced-router.js +6 -0
- package/dist/traced-router.js.map +1 -0
- package/dist/upload/index.d.ts +28 -0
- package/dist/upload/index.d.ts.map +1 -0
- package/dist/upload/index.js +410 -0
- package/dist/upload/index.js.map +1 -0
- package/dist/versioning/index.d.ts +35 -0
- package/dist/versioning/index.d.ts.map +1 -0
- package/dist/versioning/index.js +66 -0
- package/dist/versioning/index.js.map +1 -0
- package/dist/versioning/runtime.d.ts +5 -0
- package/dist/versioning/runtime.d.ts.map +1 -0
- package/dist/versioning/runtime.js +34 -0
- package/dist/versioning/runtime.js.map +1 -0
- package/package.json +143 -0
- package/src/app/bridge.ts +39 -0
- package/src/app/index.ts +76 -0
- package/src/bench/index.ts +329 -0
- package/src/compression/index.ts +255 -0
- package/src/context/index.ts +181 -0
- package/src/contract/__fixtures__/http-client.generated.ts +73 -0
- package/src/contract/__fixtures__/http-client.generated.ts.map +1 -0
- package/src/contract/__fixtures__/http-convergence.ts +564 -0
- package/src/contract/compiler/client.ts +1518 -0
- package/src/contract/compiler/index.ts +1457 -0
- package/src/contract/index.ts +266 -0
- package/src/csrf/index.ts +264 -0
- package/src/data/index.ts +65 -0
- package/src/devtools/index.ts +727 -0
- package/src/dto-pipes/index.ts +93 -0
- package/src/gateways/index.ts +180 -0
- package/src/health/index.ts +198 -0
- package/src/http-token.ts +22 -0
- package/src/index.ts +167 -0
- package/src/integrations/index.ts +50 -0
- package/src/middleware/errors.ts +17 -0
- package/src/middleware/index.ts +108 -0
- package/src/modules/__fixtures__/large-graph.ts +540 -0
- package/src/openapi/__fixtures__/entities.ts +19 -0
- package/src/openapi/__fixtures__/openapi-tools.fixture.ts +212 -0
- package/src/openapi/__fixtures__/route-schemas.ts +27 -0
- package/src/openapi/index.ts +483 -0
- package/src/pipeline/guards.ts +23 -0
- package/src/pipeline/index.ts +2041 -0
- package/src/routing/index.ts +201 -0
- package/src/static/index.ts +443 -0
- package/src/testing/index.ts +58 -0
- package/src/traced-router.ts +8 -0
- package/src/upload/index.ts +489 -0
- package/src/versioning/index.ts +106 -0
- package/src/versioning/runtime.ts +38 -0
package/package.json
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zmdb/web",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "HTTP framework for the zmdb application kernel: Stage-3 controllers, typed request context, middleware, OpenAPI, gateways, testing, and runtime adapters.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"decorators",
|
|
7
|
+
"http",
|
|
8
|
+
"openapi",
|
|
9
|
+
"stage-3",
|
|
10
|
+
"typescript",
|
|
11
|
+
"web-framework",
|
|
12
|
+
"zmdb"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/ambasta/zmdb#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/ambasta/zmdb/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "GPL-3.0-or-later",
|
|
19
|
+
"author": "zmdb contributors",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ambasta/zmdb.git",
|
|
23
|
+
"directory": "packages/web"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./routing": {
|
|
33
|
+
"types": "./dist/routing/index.d.ts",
|
|
34
|
+
"import": "./dist/routing/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./context": {
|
|
37
|
+
"types": "./dist/context/index.d.ts",
|
|
38
|
+
"import": "./dist/context/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./contract": {
|
|
41
|
+
"types": "./dist/contract/index.d.ts",
|
|
42
|
+
"import": "./dist/contract/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./contract/compiler": {
|
|
45
|
+
"types": "./dist/contract/compiler/index.d.ts",
|
|
46
|
+
"import": "./dist/contract/compiler/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./pipeline": {
|
|
49
|
+
"types": "./dist/pipeline/index.d.ts",
|
|
50
|
+
"import": "./dist/pipeline/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./static": {
|
|
53
|
+
"types": "./dist/static/index.d.ts",
|
|
54
|
+
"import": "./dist/static/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./compression": {
|
|
57
|
+
"types": "./dist/compression/index.d.ts",
|
|
58
|
+
"import": "./dist/compression/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./data": {
|
|
61
|
+
"types": "./dist/data/index.d.ts",
|
|
62
|
+
"import": "./dist/data/index.js"
|
|
63
|
+
},
|
|
64
|
+
"./middleware": {
|
|
65
|
+
"types": "./dist/middleware/index.d.ts",
|
|
66
|
+
"import": "./dist/middleware/index.js"
|
|
67
|
+
},
|
|
68
|
+
"./app": {
|
|
69
|
+
"types": "./dist/app/index.d.ts",
|
|
70
|
+
"import": "./dist/app/index.js"
|
|
71
|
+
},
|
|
72
|
+
"./dto-pipes": {
|
|
73
|
+
"types": "./dist/dto-pipes/index.d.ts",
|
|
74
|
+
"import": "./dist/dto-pipes/index.js"
|
|
75
|
+
},
|
|
76
|
+
"./upload": {
|
|
77
|
+
"types": "./dist/upload/index.d.ts",
|
|
78
|
+
"import": "./dist/upload/index.js"
|
|
79
|
+
},
|
|
80
|
+
"./csrf": {
|
|
81
|
+
"types": "./dist/csrf/index.d.ts",
|
|
82
|
+
"import": "./dist/csrf/index.js"
|
|
83
|
+
},
|
|
84
|
+
"./openapi": {
|
|
85
|
+
"types": "./dist/openapi/index.d.ts",
|
|
86
|
+
"import": "./dist/openapi/index.js"
|
|
87
|
+
},
|
|
88
|
+
"./versioning": {
|
|
89
|
+
"types": "./dist/versioning/index.d.ts",
|
|
90
|
+
"import": "./dist/versioning/index.js"
|
|
91
|
+
},
|
|
92
|
+
"./health": {
|
|
93
|
+
"types": "./dist/health/index.d.ts",
|
|
94
|
+
"import": "./dist/health/index.js"
|
|
95
|
+
},
|
|
96
|
+
"./gateways": {
|
|
97
|
+
"types": "./dist/gateways/index.d.ts",
|
|
98
|
+
"import": "./dist/gateways/index.js"
|
|
99
|
+
},
|
|
100
|
+
"./testing": {
|
|
101
|
+
"types": "./dist/testing/index.d.ts",
|
|
102
|
+
"import": "./dist/testing/index.js"
|
|
103
|
+
},
|
|
104
|
+
"./devtools": {
|
|
105
|
+
"types": "./dist/devtools/index.d.ts",
|
|
106
|
+
"import": "./dist/devtools/index.js"
|
|
107
|
+
},
|
|
108
|
+
"./integrations": {
|
|
109
|
+
"types": "./dist/integrations/index.d.ts",
|
|
110
|
+
"import": "./dist/integrations/index.js"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"publishConfig": {
|
|
114
|
+
"access": "public",
|
|
115
|
+
"tag": "beta"
|
|
116
|
+
},
|
|
117
|
+
"scripts": {
|
|
118
|
+
"build": "node ../../scripts/build-package.mjs",
|
|
119
|
+
"test": "vitest run"
|
|
120
|
+
},
|
|
121
|
+
"dependencies": {
|
|
122
|
+
"@zmdb/app": "1.0.0-beta.1",
|
|
123
|
+
"@zmdb/schema": "1.0.0-beta.1",
|
|
124
|
+
"@zmdb/validator": "1.0.0-beta.1"
|
|
125
|
+
},
|
|
126
|
+
"peerDependencies": {
|
|
127
|
+
"@zmdb/compiler": "1.0.0-beta.1",
|
|
128
|
+
"typescript": ">=7.0.2 <8.0.0"
|
|
129
|
+
},
|
|
130
|
+
"peerDependenciesMeta": {
|
|
131
|
+
"@zmdb/compiler": {
|
|
132
|
+
"optional": true
|
|
133
|
+
},
|
|
134
|
+
"typescript": {
|
|
135
|
+
"optional": true
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"engines": {
|
|
139
|
+
"node": ">=26"
|
|
140
|
+
},
|
|
141
|
+
"main": "./dist/index.js",
|
|
142
|
+
"types": "./dist/index.d.ts"
|
|
143
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Application, ApplicationOptions, CompiledModule, Constructor } from '@zmdb/app';
|
|
2
|
+
|
|
3
|
+
const APPLICATION_BRIDGE = Symbol.for('@zmdb/app.application-bridge');
|
|
4
|
+
const COMPILED_APPLICATION = Symbol.for('@zmdb/app.compiled-application');
|
|
5
|
+
|
|
6
|
+
export type CompiledController =
|
|
7
|
+
| { readonly kind: 'eager'; readonly controller: object }
|
|
8
|
+
| {
|
|
9
|
+
readonly kind: 'deferred';
|
|
10
|
+
readonly controller: Constructor<object>;
|
|
11
|
+
readonly instance: () => Promise<object>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface ApplicationBridge {
|
|
15
|
+
readonly controllers: readonly CompiledController[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface BridgedApplication extends Application {
|
|
19
|
+
readonly [APPLICATION_BRIDGE]?: ApplicationBridge;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CompiledApplicationOptions {
|
|
23
|
+
readonly [COMPILED_APPLICATION]?: CompiledModule;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function applicationControllersOf(application: Application): readonly CompiledController[] {
|
|
27
|
+
const carrier: BridgedApplication = application;
|
|
28
|
+
const bridge = carrier[APPLICATION_BRIDGE];
|
|
29
|
+
if (bridge === undefined) {
|
|
30
|
+
throw new Error('@zmdb/web: application omitted its controller graph');
|
|
31
|
+
}
|
|
32
|
+
return bridge.controllers;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function withCompiledApplication(options: ApplicationOptions, compiled: CompiledModule): ApplicationOptions {
|
|
36
|
+
const carrier: ApplicationOptions & CompiledApplicationOptions = { ...options };
|
|
37
|
+
Object.defineProperty(carrier, COMPILED_APPLICATION, { value: compiled });
|
|
38
|
+
return carrier;
|
|
39
|
+
}
|
package/src/app/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @zmdb/web — HTTP application composition over the protocol-neutral kernel.
|
|
2
|
+
// One @zmdb/app graph owns construction, lazy loading and lifecycle; this file
|
|
3
|
+
// adds one startup-built router; protocol integrations remain explicit app extensions.
|
|
4
|
+
|
|
5
|
+
import { createApplication, type Application, type ApplicationOptions, type ModuleClass } from '@zmdb/app';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createRouter,
|
|
9
|
+
toFetchHandler,
|
|
10
|
+
type GuardRegistry,
|
|
11
|
+
type Router,
|
|
12
|
+
type RouterOptions,
|
|
13
|
+
type WebRequest,
|
|
14
|
+
type WebResponse,
|
|
15
|
+
} from '../pipeline/index.js';
|
|
16
|
+
import type { VersionStrategy } from '../versioning/index.js';
|
|
17
|
+
import { applicationControllersOf, type CompiledController } from './bridge.js';
|
|
18
|
+
|
|
19
|
+
export type { OnApplicationBootstrap, OnModuleInit, OnShutdown } from '@zmdb/app/lifecycle';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Protocol integrations attach through `ApplicationOptions.extensions`.
|
|
23
|
+
*/
|
|
24
|
+
export interface WebApplicationOptions extends ApplicationOptions {
|
|
25
|
+
readonly guardRegistry?: GuardRegistry;
|
|
26
|
+
readonly versioning?: VersionStrategy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A protocol-neutral application with one HTTP router attached. */
|
|
30
|
+
export interface WebApplication extends Application {
|
|
31
|
+
handle(req: WebRequest): Promise<WebResponse>;
|
|
32
|
+
fetch(request: Request): Promise<Response>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compose one router over one application graph. The container, lazy handles,
|
|
37
|
+
* init function and async-dispose function are the app-owned members by
|
|
38
|
+
* identity; no second lifecycle or construction ledger exists here.
|
|
39
|
+
*/
|
|
40
|
+
export function createApp(rootModule: ModuleClass, options: WebApplicationOptions = {}): WebApplication {
|
|
41
|
+
const applicationOptions: ApplicationOptions = {
|
|
42
|
+
...(options.graceMs === undefined ? {} : { graceMs: options.graceMs }),
|
|
43
|
+
...(options.observability === undefined ? {} : { observability: options.observability }),
|
|
44
|
+
...(options.extensions === undefined ? {} : { extensions: options.extensions }),
|
|
45
|
+
};
|
|
46
|
+
const application = createApplication(rootModule, applicationOptions);
|
|
47
|
+
const controllerBindings: readonly CompiledController[] = applicationControllersOf(application);
|
|
48
|
+
|
|
49
|
+
const router: Router = createRouter(routerOptions(options));
|
|
50
|
+
for (const binding of controllerBindings) {
|
|
51
|
+
if (binding.kind === 'eager') {
|
|
52
|
+
router.register(binding.controller);
|
|
53
|
+
} else {
|
|
54
|
+
router.registerDeferred(binding.controller, binding.instance);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const fetchHandler = toFetchHandler(router);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
container: application.container,
|
|
61
|
+
lazy: application.lazy,
|
|
62
|
+
handle: req => router.handle(req),
|
|
63
|
+
fetch: request => fetchHandler(request),
|
|
64
|
+
init: application.init,
|
|
65
|
+
[Symbol.asyncDispose]: application[Symbol.asyncDispose],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function routerOptions(options: WebApplicationOptions): RouterOptions {
|
|
70
|
+
const observability = options.observability ?? {};
|
|
71
|
+
return {
|
|
72
|
+
...observability,
|
|
73
|
+
...(options.guardRegistry === undefined ? {} : { guardRegistry: options.guardRegistry }),
|
|
74
|
+
...(options.versioning === undefined ? {} : { versioning: options.versioning }),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// @zmdb/web — router benchmark & perf verification (epic #317, spec ./SPEC.md).
|
|
2
|
+
// An honest microbench + an init-time-resolution probe proving route resolution
|
|
3
|
+
// does not re-read metadata per request. No `as` on the consumer surface.
|
|
4
|
+
|
|
5
|
+
import '@zmdb/app';
|
|
6
|
+
import type { ModuleClass } from '@zmdb/app/modules';
|
|
7
|
+
import { tracedDriver, type ExecutingDriver, type Observability } from '@zmdb/app/observability';
|
|
8
|
+
|
|
9
|
+
import { createApp } from '../app/index.js';
|
|
10
|
+
import { createRouter, type ResponseBody } from '../pipeline/index.js';
|
|
11
|
+
import { Controller, Get } from '../routing/index.js';
|
|
12
|
+
import { Version, type VersionStrategy } from '../versioning/index.js';
|
|
13
|
+
import { pathForVersion } from '../versioning/runtime.js';
|
|
14
|
+
|
|
15
|
+
/** A probe that counts reads of a class's Symbol.metadata. */
|
|
16
|
+
export interface MetadataReadCounter {
|
|
17
|
+
count(): number;
|
|
18
|
+
restore(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Instrument a class so reads of its `Symbol.metadata` are counted. Replaces the
|
|
23
|
+
* own property with a counting getter over the original value; `restore` puts
|
|
24
|
+
* the original descriptor back. Test-only helper for the regression guard.
|
|
25
|
+
*/
|
|
26
|
+
export function countMetadataReads(target: object): MetadataReadCounter {
|
|
27
|
+
const original = Object.getOwnPropertyDescriptor(target, Symbol.metadata);
|
|
28
|
+
const stored = original?.value;
|
|
29
|
+
let reads = 0;
|
|
30
|
+
Object.defineProperty(target, Symbol.metadata, {
|
|
31
|
+
configurable: true,
|
|
32
|
+
enumerable: original?.enumerable ?? false,
|
|
33
|
+
get(): unknown {
|
|
34
|
+
reads += 1;
|
|
35
|
+
return stored;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
count: () => reads,
|
|
40
|
+
restore: () => {
|
|
41
|
+
if (original === undefined) {
|
|
42
|
+
Reflect.deleteProperty(target, Symbol.metadata);
|
|
43
|
+
} else {
|
|
44
|
+
Object.defineProperty(target, Symbol.metadata, original);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface BenchmarkBaseOptions {
|
|
51
|
+
readonly routes: number;
|
|
52
|
+
readonly iters: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Options for the router microbench, including one exact versioned route table. */
|
|
56
|
+
export type BenchmarkOptions =
|
|
57
|
+
| (BenchmarkBaseOptions & { readonly versioning?: undefined; readonly version?: undefined })
|
|
58
|
+
| (BenchmarkBaseOptions & { readonly versioning: VersionStrategy; readonly version: string });
|
|
59
|
+
|
|
60
|
+
/** Honest microbench result — raw timings, no scoring. */
|
|
61
|
+
export interface BenchmarkResult {
|
|
62
|
+
readonly iters: number;
|
|
63
|
+
readonly totalMs: number;
|
|
64
|
+
readonly opsPerSec: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The three tracing configurations measured by the operability benchmark. */
|
|
68
|
+
export type ObservabilityBenchmarkMode = 'off' | 'noop' | 'recording-exporter';
|
|
69
|
+
|
|
70
|
+
/** The independently timed observability hot paths. */
|
|
71
|
+
export type ObservabilityBenchmarkWorkload = 'request' | 'query';
|
|
72
|
+
|
|
73
|
+
/** Options for one independently orderable observability sample. */
|
|
74
|
+
export interface ObservabilityBenchmarkOptions {
|
|
75
|
+
readonly mode: ObservabilityBenchmarkMode;
|
|
76
|
+
readonly workload: ObservabilityBenchmarkWorkload;
|
|
77
|
+
readonly iters: number;
|
|
78
|
+
readonly observability?: Observability;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One raw workload timing. The checksum makes the loop's result observable. */
|
|
82
|
+
export interface ObservabilityBenchmarkResult extends BenchmarkResult {
|
|
83
|
+
readonly mode: ObservabilityBenchmarkMode;
|
|
84
|
+
readonly workload: ObservabilityBenchmarkWorkload;
|
|
85
|
+
readonly checksum: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Measure repeated eager application creation with raw timings. */
|
|
89
|
+
export function benchmarkAppStartup(rootModule: ModuleClass, iters: number): BenchmarkResult {
|
|
90
|
+
const start = performance.now();
|
|
91
|
+
for (let index = 0; index < iters; index += 1) {
|
|
92
|
+
createApp(rootModule);
|
|
93
|
+
}
|
|
94
|
+
const totalMs = performance.now() - start;
|
|
95
|
+
const opsPerSec = totalMs > 0 ? (iters / totalMs) * 1000 : iters;
|
|
96
|
+
return { iters, totalMs, opsPerSec };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build a router with `routes` GET routes and time `iters` `handle` calls
|
|
101
|
+
* against a matching path. Returns raw timings — no averaging into a score.
|
|
102
|
+
*/
|
|
103
|
+
export async function benchmarkRouter(options: BenchmarkOptions): Promise<BenchmarkResult> {
|
|
104
|
+
const controller = makeController(options.routes, options.version);
|
|
105
|
+
const router = createRouter(options.versioning === undefined ? {} : { versioning: options.versioning });
|
|
106
|
+
router.register(controller);
|
|
107
|
+
const routePath = `/bench/r${Math.max(0, options.routes - 1)}`;
|
|
108
|
+
let path = routePath;
|
|
109
|
+
let headers: Readonly<Record<string, string>> = {};
|
|
110
|
+
if (options.versioning !== undefined) {
|
|
111
|
+
if (options.versioning.kind === 'path') {
|
|
112
|
+
path = pathForVersion(options.versioning.prefix, options.version, routePath);
|
|
113
|
+
} else if (options.versioning.kind === 'header') {
|
|
114
|
+
headers = { [options.versioning.name.toLowerCase()]: options.version };
|
|
115
|
+
} else {
|
|
116
|
+
headers = { accept: `application/json; ${options.versioning.key}=${options.version}` };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const start = performance.now();
|
|
121
|
+
for (let i = 0; i < options.iters; i += 1) {
|
|
122
|
+
await router.handle({ method: 'GET', path, headers });
|
|
123
|
+
}
|
|
124
|
+
const totalMs = performance.now() - start;
|
|
125
|
+
const opsPerSec = totalMs > 0 ? (options.iters / totalMs) * 1000 : options.iters;
|
|
126
|
+
return { iters: options.iters, totalMs, opsPerSec };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Measure one request or driver hot path under an injected tracing configuration.
|
|
131
|
+
*
|
|
132
|
+
* Tracer construction belongs to the top-level benchmark runner: this shipped
|
|
133
|
+
* helper depends only on the framework's narrow observability port, never on an
|
|
134
|
+
* SDK. The runner calls the same function for elapsed-time warmup and for every
|
|
135
|
+
* raw sample, flushing an exporter only after this timer has stopped.
|
|
136
|
+
*/
|
|
137
|
+
export async function benchmarkObservability(
|
|
138
|
+
options: ObservabilityBenchmarkOptions,
|
|
139
|
+
): Promise<ObservabilityBenchmarkResult> {
|
|
140
|
+
return options.workload === 'request' ? benchmarkRequest(options) : benchmarkQuery(options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function benchmarkRequest(options: ObservabilityBenchmarkOptions): Promise<ObservabilityBenchmarkResult> {
|
|
144
|
+
const observability = options.observability ?? {};
|
|
145
|
+
const router = createRouter(observability);
|
|
146
|
+
router.register(makeController(1));
|
|
147
|
+
const request = Object.freeze({ method: 'GET', path: '/bench/r0', headers: Object.freeze({}) });
|
|
148
|
+
|
|
149
|
+
let checksum = 0;
|
|
150
|
+
const start = performance.now();
|
|
151
|
+
for (let i = 0; i < options.iters; i += 1) {
|
|
152
|
+
const response = await router.handle(request);
|
|
153
|
+
checksum = (checksum + response.status + responseBodySize(response.body)) >>> 0;
|
|
154
|
+
}
|
|
155
|
+
return observabilityResult(options, performance.now() - start, checksum);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function benchmarkQuery(options: ObservabilityBenchmarkOptions): Promise<ObservabilityBenchmarkResult> {
|
|
159
|
+
const observability = options.observability ?? {};
|
|
160
|
+
const driver = tracedDriver(
|
|
161
|
+
{
|
|
162
|
+
execute: query => {
|
|
163
|
+
const id = query.parameters[0];
|
|
164
|
+
return Promise.resolve(typeof id === 'number' && id % 2 === 0 ? TWO_ROWS : ONE_ROW);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
observability,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
let checksum = 0;
|
|
171
|
+
const start = performance.now();
|
|
172
|
+
for (let i = 0; i < options.iters; i += 1) {
|
|
173
|
+
const rows = await driver.execute(benchQuery(i));
|
|
174
|
+
checksum = (checksum + rows.length) >>> 0;
|
|
175
|
+
}
|
|
176
|
+
return observabilityResult(options, performance.now() - start, checksum);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function observabilityResult(
|
|
180
|
+
options: ObservabilityBenchmarkOptions,
|
|
181
|
+
totalMs: number,
|
|
182
|
+
checksum: number,
|
|
183
|
+
): ObservabilityBenchmarkResult {
|
|
184
|
+
return {
|
|
185
|
+
mode: options.mode,
|
|
186
|
+
workload: options.workload,
|
|
187
|
+
...result(options.iters, totalMs),
|
|
188
|
+
checksum,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
type BenchmarkQuery = Parameters<ExecutingDriver['execute']>[0];
|
|
193
|
+
|
|
194
|
+
const QUERY_TELEMETRY: NonNullable<BenchmarkQuery['telemetry']> = Object.freeze({
|
|
195
|
+
system: 'postgresql',
|
|
196
|
+
operation: 'SELECT',
|
|
197
|
+
collection: 'bench',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const BENCH_QUERIES: readonly BenchmarkQuery[] = Object.freeze(
|
|
201
|
+
Array.from({ length: 8 }, (_, index): BenchmarkQuery =>
|
|
202
|
+
Object.freeze({
|
|
203
|
+
text: 'SELECT "id" FROM "bench" WHERE "id" = $1',
|
|
204
|
+
parameters: Object.freeze([index + 1]),
|
|
205
|
+
telemetry: QUERY_TELEMETRY,
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const ONE_ROW: readonly Record<string, unknown>[] = Object.freeze([Object.freeze({ id: 1 })]);
|
|
211
|
+
const TWO_ROWS: readonly Record<string, unknown>[] = Object.freeze([
|
|
212
|
+
Object.freeze({ id: 1 }),
|
|
213
|
+
Object.freeze({ id: 2 }),
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
function benchQuery(index: number): BenchmarkQuery {
|
|
217
|
+
const query = BENCH_QUERIES[index % BENCH_QUERIES.length];
|
|
218
|
+
if (query === undefined) {
|
|
219
|
+
throw new Error('observability benchmark query pool is empty');
|
|
220
|
+
}
|
|
221
|
+
return query;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function responseBodySize(body: ResponseBody): number {
|
|
225
|
+
if (typeof body.value === 'string') {
|
|
226
|
+
return body.value.length;
|
|
227
|
+
}
|
|
228
|
+
if (body.value instanceof Uint8Array) {
|
|
229
|
+
return body.value.byteLength;
|
|
230
|
+
}
|
|
231
|
+
return 'length' in body ? (body.length ?? 0) : 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Build a controller instance with `count` GET routes /bench/r0../bench/r{n-1}.
|
|
235
|
+
// Defined via a factory so each benchmark gets a fresh class + metadata.
|
|
236
|
+
function makeController(count: number, version?: string): object {
|
|
237
|
+
class BenchController {
|
|
238
|
+
// Every route resolves to this one method, aliased under `count` names below. A benchmark
|
|
239
|
+
// that allocated a fresh closure per route would be measuring that instead of the router.
|
|
240
|
+
ok(): { ok: true } {
|
|
241
|
+
return { ok: true };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// `@Controller('/bench')` in decorator position instead, except that decorators are real
|
|
245
|
+
// syntax rather than types, so a file containing one cannot be loaded by Node's type
|
|
246
|
+
// stripping — and this package ships its `src`. A single decorator here was enough to make
|
|
247
|
+
// `import '@zmdb/web'` a SyntaxError, because the root index re-exports this module. Applied
|
|
248
|
+
// programmatically it costs a synthesised context; the routing decorators only ever touch
|
|
249
|
+
// `context.metadata`, which is what makes the substitution exact.
|
|
250
|
+
Controller('/bench')(BenchController, classContext(BenchController));
|
|
251
|
+
if (version !== undefined) {
|
|
252
|
+
Version(version)(BenchController, classContext(BenchController));
|
|
253
|
+
}
|
|
254
|
+
// Attach `count` routes by decorating dynamically-added methods. Since Stage-3
|
|
255
|
+
// method decorators can't be applied dynamically, we register handlers on the
|
|
256
|
+
// prototype and record routes through a single re-decoration pass: simplest is
|
|
257
|
+
// to define a fixed handler and add route metadata via the Get decorator on
|
|
258
|
+
// named prototype methods.
|
|
259
|
+
const proto = BenchController.prototype;
|
|
260
|
+
for (let i = 0; i < count; i += 1) {
|
|
261
|
+
const name = `r${i}`;
|
|
262
|
+
Object.defineProperty(proto, name, {
|
|
263
|
+
configurable: true,
|
|
264
|
+
writable: true,
|
|
265
|
+
value: proto.ok,
|
|
266
|
+
});
|
|
267
|
+
const decorate = Get(`/r${i}`);
|
|
268
|
+
const context = methodContext(name, BenchController);
|
|
269
|
+
decorate(readProtoMethod(proto, name), context);
|
|
270
|
+
}
|
|
271
|
+
return new BenchController();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function result(iters: number, totalMs: number): BenchmarkResult {
|
|
275
|
+
return {
|
|
276
|
+
iters,
|
|
277
|
+
totalMs,
|
|
278
|
+
opsPerSec: totalMs > 0 ? (iters / totalMs) * 1000 : iters,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A minimal ClassDecoratorContext, for the same reason. Controller reads `metadata` and nothing
|
|
283
|
+
// else; `name` and `addInitializer` are here to satisfy the type.
|
|
284
|
+
function classContext<T extends abstract new (...args: never[]) => unknown>(cls: T): ClassDecoratorContext<T> {
|
|
285
|
+
return {
|
|
286
|
+
kind: 'class',
|
|
287
|
+
name: cls.name,
|
|
288
|
+
metadata: ensureMetadata(cls),
|
|
289
|
+
addInitializer: (): void => undefined,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// A minimal ClassMethodDecoratorContext for programmatic decoration. The Get
|
|
294
|
+
// decorator only reads `name` + `metadata`; the rest satisfies the type.
|
|
295
|
+
function methodContext(name: string, cls: abstract new (...args: never[]) => unknown): ClassMethodDecoratorContext {
|
|
296
|
+
const metadata = ensureMetadata(cls);
|
|
297
|
+
const noop = (): void => undefined;
|
|
298
|
+
return {
|
|
299
|
+
kind: 'method',
|
|
300
|
+
name,
|
|
301
|
+
static: false,
|
|
302
|
+
private: false,
|
|
303
|
+
access: { has: () => true, get: obj => Reflect.get(Object(obj), name) },
|
|
304
|
+
metadata,
|
|
305
|
+
addInitializer: noop,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function ensureMetadata(cls: abstract new (...args: never[]) => unknown): DecoratorMetadata {
|
|
310
|
+
const existing = cls[Symbol.metadata];
|
|
311
|
+
if (existing !== undefined && existing !== null) {
|
|
312
|
+
return existing;
|
|
313
|
+
}
|
|
314
|
+
const created: DecoratorMetadata = Object.create(null);
|
|
315
|
+
Object.defineProperty(cls, Symbol.metadata, {
|
|
316
|
+
value: created,
|
|
317
|
+
configurable: true,
|
|
318
|
+
enumerable: false,
|
|
319
|
+
writable: false,
|
|
320
|
+
});
|
|
321
|
+
return created;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// boundary: reading a just-defined prototype method by name to hand to the Get
|
|
325
|
+
// decorator; it is a function by construction (§2.1).
|
|
326
|
+
function readProtoMethod(proto: object, name: string): (...args: never[]) => unknown {
|
|
327
|
+
const value = Reflect.get(proto, name);
|
|
328
|
+
return value as (...args: never[]) => unknown;
|
|
329
|
+
}
|