@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.
Files changed (179) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +49 -0
  3. package/dist/app/bridge.d.ts +12 -0
  4. package/dist/app/bridge.d.ts.map +1 -0
  5. package/dist/app/bridge.js +16 -0
  6. package/dist/app/bridge.js.map +1 -0
  7. package/dist/app/index.d.ts +23 -0
  8. package/dist/app/index.d.ts.map +1 -0
  9. package/dist/app/index.js +47 -0
  10. package/dist/app/index.js.map +1 -0
  11. package/dist/bench/index.d.ts +68 -0
  12. package/dist/bench/index.d.ts.map +1 -0
  13. package/dist/bench/index.js +250 -0
  14. package/dist/bench/index.js.map +1 -0
  15. package/dist/compression/index.d.ts +13 -0
  16. package/dist/compression/index.d.ts.map +1 -0
  17. package/dist/compression/index.js +211 -0
  18. package/dist/compression/index.js.map +1 -0
  19. package/dist/context/index.d.ts +74 -0
  20. package/dist/context/index.d.ts.map +1 -0
  21. package/dist/context/index.js +118 -0
  22. package/dist/context/index.js.map +1 -0
  23. package/dist/contract/__fixtures__/http-client.generated.d.ts +70 -0
  24. package/dist/contract/__fixtures__/http-client.generated.d.ts.map +1 -0
  25. package/dist/contract/__fixtures__/http-client.generated.js +86 -0
  26. package/dist/contract/__fixtures__/http-client.generated.js.map +1 -0
  27. package/dist/contract/__fixtures__/http-convergence.d.ts +396 -0
  28. package/dist/contract/__fixtures__/http-convergence.d.ts.map +1 -0
  29. package/dist/contract/__fixtures__/http-convergence.js +425 -0
  30. package/dist/contract/__fixtures__/http-convergence.js.map +1 -0
  31. package/dist/contract/compiler/client.d.ts +12 -0
  32. package/dist/contract/compiler/client.d.ts.map +1 -0
  33. package/dist/contract/compiler/client.js +1293 -0
  34. package/dist/contract/compiler/client.js.map +1 -0
  35. package/dist/contract/compiler/index.d.ts +18 -0
  36. package/dist/contract/compiler/index.d.ts.map +1 -0
  37. package/dist/contract/compiler/index.js +1058 -0
  38. package/dist/contract/compiler/index.js.map +1 -0
  39. package/dist/contract/index.d.ts +237 -0
  40. package/dist/contract/index.d.ts.map +1 -0
  41. package/dist/contract/index.js +22 -0
  42. package/dist/contract/index.js.map +1 -0
  43. package/dist/csrf/index.d.ts +15 -0
  44. package/dist/csrf/index.d.ts.map +1 -0
  45. package/dist/csrf/index.js +218 -0
  46. package/dist/csrf/index.js.map +1 -0
  47. package/dist/data/index.d.ts +35 -0
  48. package/dist/data/index.d.ts.map +1 -0
  49. package/dist/data/index.js +58 -0
  50. package/dist/data/index.js.map +1 -0
  51. package/dist/devtools/index.d.ts +74 -0
  52. package/dist/devtools/index.d.ts.map +1 -0
  53. package/dist/devtools/index.js +552 -0
  54. package/dist/devtools/index.js.map +1 -0
  55. package/dist/dto-pipes/index.d.ts +45 -0
  56. package/dist/dto-pipes/index.d.ts.map +1 -0
  57. package/dist/dto-pipes/index.js +78 -0
  58. package/dist/dto-pipes/index.js.map +1 -0
  59. package/dist/gateways/index.d.ts +31 -0
  60. package/dist/gateways/index.d.ts.map +1 -0
  61. package/dist/gateways/index.js +138 -0
  62. package/dist/gateways/index.js.map +1 -0
  63. package/dist/health/index.d.ts +20 -0
  64. package/dist/health/index.d.ts.map +1 -0
  65. package/dist/health/index.js +148 -0
  66. package/dist/health/index.js.map +1 -0
  67. package/dist/http-token.d.ts +2 -0
  68. package/dist/http-token.d.ts.map +1 -0
  69. package/dist/http-token.js +21 -0
  70. package/dist/http-token.js.map +1 -0
  71. package/dist/index.d.ts +18 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +40 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/integrations/index.d.ts +11 -0
  76. package/dist/integrations/index.d.ts.map +1 -0
  77. package/dist/integrations/index.js +39 -0
  78. package/dist/integrations/index.js.map +1 -0
  79. package/dist/middleware/errors.d.ts +14 -0
  80. package/dist/middleware/errors.d.ts.map +1 -0
  81. package/dist/middleware/errors.js +18 -0
  82. package/dist/middleware/errors.js.map +1 -0
  83. package/dist/middleware/index.d.ts +46 -0
  84. package/dist/middleware/index.d.ts.map +1 -0
  85. package/dist/middleware/index.js +64 -0
  86. package/dist/middleware/index.js.map +1 -0
  87. package/dist/modules/__fixtures__/large-graph.d.ts +239 -0
  88. package/dist/modules/__fixtures__/large-graph.d.ts.map +1 -0
  89. package/dist/modules/__fixtures__/large-graph.js +483 -0
  90. package/dist/modules/__fixtures__/large-graph.js.map +1 -0
  91. package/dist/openapi/__fixtures__/entities.d.ts +8 -0
  92. package/dist/openapi/__fixtures__/entities.d.ts.map +1 -0
  93. package/dist/openapi/__fixtures__/entities.js +4 -0
  94. package/dist/openapi/__fixtures__/entities.js.map +1 -0
  95. package/dist/openapi/__fixtures__/openapi-tools.fixture.d.ts +38 -0
  96. package/dist/openapi/__fixtures__/openapi-tools.fixture.d.ts.map +1 -0
  97. package/dist/openapi/__fixtures__/openapi-tools.fixture.js +172 -0
  98. package/dist/openapi/__fixtures__/openapi-tools.fixture.js.map +1 -0
  99. package/dist/openapi/__fixtures__/route-schemas.d.ts +2 -0
  100. package/dist/openapi/__fixtures__/route-schemas.d.ts.map +1 -0
  101. package/dist/openapi/__fixtures__/route-schemas.js +17 -0
  102. package/dist/openapi/__fixtures__/route-schemas.js.map +1 -0
  103. package/dist/openapi/index.d.ts +61 -0
  104. package/dist/openapi/index.d.ts.map +1 -0
  105. package/dist/openapi/index.js +314 -0
  106. package/dist/openapi/index.js.map +1 -0
  107. package/dist/pipeline/guards.d.ts +9 -0
  108. package/dist/pipeline/guards.d.ts.map +1 -0
  109. package/dist/pipeline/guards.js +11 -0
  110. package/dist/pipeline/guards.js.map +1 -0
  111. package/dist/pipeline/index.d.ts +141 -0
  112. package/dist/pipeline/index.d.ts.map +1 -0
  113. package/dist/pipeline/index.js +1624 -0
  114. package/dist/pipeline/index.js.map +1 -0
  115. package/dist/routing/index.d.ts +40 -0
  116. package/dist/routing/index.d.ts.map +1 -0
  117. package/dist/routing/index.js +162 -0
  118. package/dist/routing/index.js.map +1 -0
  119. package/dist/static/index.d.ts +14 -0
  120. package/dist/static/index.d.ts.map +1 -0
  121. package/dist/static/index.js +348 -0
  122. package/dist/static/index.js.map +1 -0
  123. package/dist/testing/index.d.ts +20 -0
  124. package/dist/testing/index.d.ts.map +1 -0
  125. package/dist/testing/index.js +32 -0
  126. package/dist/testing/index.js.map +1 -0
  127. package/dist/traced-router.d.ts +5 -0
  128. package/dist/traced-router.d.ts.map +1 -0
  129. package/dist/traced-router.js +6 -0
  130. package/dist/traced-router.js.map +1 -0
  131. package/dist/upload/index.d.ts +28 -0
  132. package/dist/upload/index.d.ts.map +1 -0
  133. package/dist/upload/index.js +410 -0
  134. package/dist/upload/index.js.map +1 -0
  135. package/dist/versioning/index.d.ts +35 -0
  136. package/dist/versioning/index.d.ts.map +1 -0
  137. package/dist/versioning/index.js +66 -0
  138. package/dist/versioning/index.js.map +1 -0
  139. package/dist/versioning/runtime.d.ts +5 -0
  140. package/dist/versioning/runtime.d.ts.map +1 -0
  141. package/dist/versioning/runtime.js +34 -0
  142. package/dist/versioning/runtime.js.map +1 -0
  143. package/package.json +143 -0
  144. package/src/app/bridge.ts +39 -0
  145. package/src/app/index.ts +76 -0
  146. package/src/bench/index.ts +329 -0
  147. package/src/compression/index.ts +255 -0
  148. package/src/context/index.ts +181 -0
  149. package/src/contract/__fixtures__/http-client.generated.ts +73 -0
  150. package/src/contract/__fixtures__/http-client.generated.ts.map +1 -0
  151. package/src/contract/__fixtures__/http-convergence.ts +564 -0
  152. package/src/contract/compiler/client.ts +1518 -0
  153. package/src/contract/compiler/index.ts +1457 -0
  154. package/src/contract/index.ts +266 -0
  155. package/src/csrf/index.ts +264 -0
  156. package/src/data/index.ts +65 -0
  157. package/src/devtools/index.ts +727 -0
  158. package/src/dto-pipes/index.ts +93 -0
  159. package/src/gateways/index.ts +180 -0
  160. package/src/health/index.ts +198 -0
  161. package/src/http-token.ts +22 -0
  162. package/src/index.ts +167 -0
  163. package/src/integrations/index.ts +50 -0
  164. package/src/middleware/errors.ts +17 -0
  165. package/src/middleware/index.ts +108 -0
  166. package/src/modules/__fixtures__/large-graph.ts +540 -0
  167. package/src/openapi/__fixtures__/entities.ts +19 -0
  168. package/src/openapi/__fixtures__/openapi-tools.fixture.ts +212 -0
  169. package/src/openapi/__fixtures__/route-schemas.ts +27 -0
  170. package/src/openapi/index.ts +483 -0
  171. package/src/pipeline/guards.ts +23 -0
  172. package/src/pipeline/index.ts +2041 -0
  173. package/src/routing/index.ts +201 -0
  174. package/src/static/index.ts +443 -0
  175. package/src/testing/index.ts +58 -0
  176. package/src/traced-router.ts +8 -0
  177. package/src/upload/index.ts +489 -0
  178. package/src/versioning/index.ts +106 -0
  179. 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
+ }
@@ -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
+ }