qhttpx 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/.github/workflows/npm-publish.yml +37 -0
  4. package/.github/workflows/release.yml +21 -0
  5. package/.prettierrc +7 -0
  6. package/CHANGELOG.md +145 -0
  7. package/LICENSE +21 -0
  8. package/README.md +343 -0
  9. package/dist/package.json +61 -0
  10. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  11. package/dist/src/benchmarks/quantam-users.js +56 -0
  12. package/dist/src/benchmarks/simple-json.js +58 -0
  13. package/dist/src/benchmarks/ultra-mode.js +122 -0
  14. package/dist/src/cli/index.js +200 -0
  15. package/dist/src/client/index.js +72 -0
  16. package/dist/src/core/batch.js +97 -0
  17. package/dist/src/core/body-parser.js +121 -0
  18. package/dist/src/core/buffer-pool.js +70 -0
  19. package/dist/src/core/config.js +50 -0
  20. package/dist/src/core/fusion.js +183 -0
  21. package/dist/src/core/logger.js +49 -0
  22. package/dist/src/core/metrics.js +111 -0
  23. package/dist/src/core/resources.js +25 -0
  24. package/dist/src/core/scheduler.js +85 -0
  25. package/dist/src/core/scope.js +68 -0
  26. package/dist/src/core/serializer.js +44 -0
  27. package/dist/src/core/server.js +905 -0
  28. package/dist/src/core/stream.js +71 -0
  29. package/dist/src/core/tasks.js +87 -0
  30. package/dist/src/core/types.js +19 -0
  31. package/dist/src/core/websocket.js +86 -0
  32. package/dist/src/core/worker-queue.js +73 -0
  33. package/dist/src/database/adapters/memory.js +90 -0
  34. package/dist/src/database/adapters/mongo.js +141 -0
  35. package/dist/src/database/adapters/postgres.js +111 -0
  36. package/dist/src/database/adapters/sqlite.js +42 -0
  37. package/dist/src/database/coalescer.js +134 -0
  38. package/dist/src/database/manager.js +87 -0
  39. package/dist/src/database/types.js +2 -0
  40. package/dist/src/index.js +61 -0
  41. package/dist/src/middleware/compression.js +133 -0
  42. package/dist/src/middleware/cors.js +66 -0
  43. package/dist/src/middleware/presets.js +33 -0
  44. package/dist/src/middleware/rate-limit.js +77 -0
  45. package/dist/src/middleware/security.js +69 -0
  46. package/dist/src/middleware/static.js +191 -0
  47. package/dist/src/openapi/generator.js +149 -0
  48. package/dist/src/router/radix-router.js +89 -0
  49. package/dist/src/router/radix-tree.js +81 -0
  50. package/dist/src/router/router.js +146 -0
  51. package/dist/src/testing/index.js +84 -0
  52. package/dist/src/utils/cookies.js +59 -0
  53. package/dist/src/utils/logger.js +45 -0
  54. package/dist/src/utils/signals.js +31 -0
  55. package/dist/src/utils/sse.js +32 -0
  56. package/dist/src/validation/index.js +19 -0
  57. package/dist/src/validation/simple.js +102 -0
  58. package/dist/src/validation/types.js +12 -0
  59. package/dist/src/validation/zod.js +18 -0
  60. package/dist/src/views/index.js +17 -0
  61. package/dist/src/views/types.js +2 -0
  62. package/dist/tests/adapters.test.js +106 -0
  63. package/dist/tests/batch.test.js +117 -0
  64. package/dist/tests/body-parser.test.js +52 -0
  65. package/dist/tests/compression-sse.test.js +87 -0
  66. package/dist/tests/cookies.test.js +63 -0
  67. package/dist/tests/cors.test.js +55 -0
  68. package/dist/tests/database.test.js +80 -0
  69. package/dist/tests/dx.test.js +64 -0
  70. package/dist/tests/ecosystem.test.js +133 -0
  71. package/dist/tests/features.test.js +47 -0
  72. package/dist/tests/fusion.test.js +92 -0
  73. package/dist/tests/http-basic.test.js +124 -0
  74. package/dist/tests/logger.test.js +33 -0
  75. package/dist/tests/middleware.test.js +109 -0
  76. package/dist/tests/observability.test.js +59 -0
  77. package/dist/tests/openapi.test.js +64 -0
  78. package/dist/tests/plugin.test.js +65 -0
  79. package/dist/tests/plugins.test.js +71 -0
  80. package/dist/tests/rate-limit.test.js +77 -0
  81. package/dist/tests/resources.test.js +44 -0
  82. package/dist/tests/scheduler.test.js +46 -0
  83. package/dist/tests/schema-routes.test.js +77 -0
  84. package/dist/tests/security.test.js +83 -0
  85. package/dist/tests/server-db.test.js +72 -0
  86. package/dist/tests/smoke.test.js +10 -0
  87. package/dist/tests/sqlite-fusion.test.js +92 -0
  88. package/dist/tests/static.test.js +102 -0
  89. package/dist/tests/stream.test.js +44 -0
  90. package/dist/tests/task-metrics.test.js +53 -0
  91. package/dist/tests/tasks.test.js +62 -0
  92. package/dist/tests/testing.test.js +47 -0
  93. package/dist/tests/validation.test.js +107 -0
  94. package/dist/tests/websocket.test.js +146 -0
  95. package/dist/vitest.config.js +9 -0
  96. package/docs/AEGIS.md +76 -0
  97. package/docs/BENCHMARKS.md +36 -0
  98. package/docs/CAPABILITIES.md +70 -0
  99. package/docs/CLI.md +43 -0
  100. package/docs/DATABASE.md +142 -0
  101. package/docs/ECOSYSTEM.md +146 -0
  102. package/docs/NEXT_STEPS.md +99 -0
  103. package/docs/OPENAPI.md +99 -0
  104. package/docs/PLUGINS.md +59 -0
  105. package/docs/REAL_WORLD_EXAMPLES.md +109 -0
  106. package/docs/ROADMAP.md +366 -0
  107. package/docs/VALIDATION.md +136 -0
  108. package/eslint.config.cjs +26 -0
  109. package/examples/api-server.ts +254 -0
  110. package/package.json +61 -0
  111. package/src/benchmarks/compare-frameworks.ts +149 -0
  112. package/src/benchmarks/quantam-users.ts +70 -0
  113. package/src/benchmarks/simple-json.ts +71 -0
  114. package/src/benchmarks/ultra-mode.ts +159 -0
  115. package/src/cli/index.ts +214 -0
  116. package/src/client/index.ts +93 -0
  117. package/src/core/batch.ts +110 -0
  118. package/src/core/body-parser.ts +151 -0
  119. package/src/core/buffer-pool.ts +96 -0
  120. package/src/core/config.ts +60 -0
  121. package/src/core/fusion.ts +210 -0
  122. package/src/core/logger.ts +70 -0
  123. package/src/core/metrics.ts +166 -0
  124. package/src/core/resources.ts +38 -0
  125. package/src/core/scheduler.ts +126 -0
  126. package/src/core/scope.ts +87 -0
  127. package/src/core/serializer.ts +41 -0
  128. package/src/core/server.ts +1113 -0
  129. package/src/core/stream.ts +111 -0
  130. package/src/core/tasks.ts +138 -0
  131. package/src/core/types.ts +178 -0
  132. package/src/core/websocket.ts +112 -0
  133. package/src/core/worker-queue.ts +90 -0
  134. package/src/database/adapters/memory.ts +99 -0
  135. package/src/database/adapters/mongo.ts +116 -0
  136. package/src/database/adapters/postgres.ts +86 -0
  137. package/src/database/adapters/sqlite.ts +44 -0
  138. package/src/database/coalescer.ts +153 -0
  139. package/src/database/manager.ts +97 -0
  140. package/src/database/types.ts +24 -0
  141. package/src/index.ts +42 -0
  142. package/src/middleware/compression.ts +147 -0
  143. package/src/middleware/cors.ts +98 -0
  144. package/src/middleware/presets.ts +50 -0
  145. package/src/middleware/rate-limit.ts +106 -0
  146. package/src/middleware/security.ts +109 -0
  147. package/src/middleware/static.ts +216 -0
  148. package/src/openapi/generator.ts +167 -0
  149. package/src/router/radix-router.ts +119 -0
  150. package/src/router/radix-tree.ts +106 -0
  151. package/src/router/router.ts +190 -0
  152. package/src/testing/index.ts +104 -0
  153. package/src/utils/cookies.ts +67 -0
  154. package/src/utils/logger.ts +59 -0
  155. package/src/utils/signals.ts +45 -0
  156. package/src/utils/sse.ts +41 -0
  157. package/src/validation/index.ts +3 -0
  158. package/src/validation/simple.ts +93 -0
  159. package/src/validation/types.ts +38 -0
  160. package/src/validation/zod.ts +14 -0
  161. package/src/views/index.ts +1 -0
  162. package/src/views/types.ts +4 -0
  163. package/tests/adapters.test.ts +120 -0
  164. package/tests/batch.test.ts +139 -0
  165. package/tests/body-parser.test.ts +83 -0
  166. package/tests/compression-sse.test.ts +98 -0
  167. package/tests/cookies.test.ts +74 -0
  168. package/tests/cors.test.ts +79 -0
  169. package/tests/database.test.ts +90 -0
  170. package/tests/dx.test.ts +78 -0
  171. package/tests/ecosystem.test.ts +156 -0
  172. package/tests/features.test.ts +51 -0
  173. package/tests/fusion.test.ts +121 -0
  174. package/tests/http-basic.test.ts +161 -0
  175. package/tests/logger.test.ts +48 -0
  176. package/tests/middleware.test.ts +137 -0
  177. package/tests/observability.test.ts +91 -0
  178. package/tests/openapi.test.ts +74 -0
  179. package/tests/plugin.test.ts +85 -0
  180. package/tests/plugins.test.ts +93 -0
  181. package/tests/rate-limit.test.ts +97 -0
  182. package/tests/resources.test.ts +64 -0
  183. package/tests/scheduler.test.ts +71 -0
  184. package/tests/schema-routes.test.ts +89 -0
  185. package/tests/security.test.ts +128 -0
  186. package/tests/server-db.test.ts +72 -0
  187. package/tests/smoke.test.ts +9 -0
  188. package/tests/sqlite-fusion.test.ts +106 -0
  189. package/tests/static.test.ts +111 -0
  190. package/tests/stream.test.ts +58 -0
  191. package/tests/task-metrics.test.ts +78 -0
  192. package/tests/tasks.test.ts +90 -0
  193. package/tests/testing.test.ts +53 -0
  194. package/tests/validation.test.ts +126 -0
  195. package/tests/websocket.test.ts +132 -0
  196. package/tsconfig.json +16 -0
  197. package/vitest.config.ts +9 -0
@@ -0,0 +1,166 @@
1
+ import type { TaskEngine, TaskMetrics } from './tasks';
2
+ import { Scheduler } from './scheduler';
3
+
4
+ export type LatencySnapshot = {
5
+ p50: number | null;
6
+ p95: number | null;
7
+ p99: number | null;
8
+ };
9
+
10
+ export type MetricsSnapshot = {
11
+ totalRequests: number;
12
+ inFlightRequests: number;
13
+ totalErrors: number;
14
+ totalTimeouts: number;
15
+ requestsPerSecond: number;
16
+ latency: LatencySnapshot;
17
+ scheduler: {
18
+ inFlight: number;
19
+ };
20
+ tasks?: TaskMetrics;
21
+ memory: {
22
+ rssBytes: number;
23
+ heapUsedBytes: number;
24
+ };
25
+ };
26
+
27
+ export class Metrics {
28
+ private readonly scheduler: Scheduler;
29
+
30
+ private readonly taskEngine?: TaskEngine;
31
+
32
+ private readonly latencies: number[] = [];
33
+
34
+ private readonly maxLatencies: number;
35
+
36
+ private readonly enabled: boolean;
37
+
38
+ private totalRequests = 0;
39
+
40
+ private inFlightRequests = 0;
41
+
42
+ private totalErrors = 0;
43
+
44
+ private totalTimeouts = 0;
45
+
46
+ constructor(
47
+ scheduler: Scheduler,
48
+ options: { maxLatencies?: number; enabled?: boolean } = {},
49
+ taskEngine?: TaskEngine,
50
+ ) {
51
+ this.scheduler = scheduler;
52
+ this.taskEngine = taskEngine;
53
+ this.maxLatencies = options.maxLatencies ?? 1000;
54
+ this.enabled = options.enabled ?? true;
55
+ }
56
+
57
+ onRequestStart(): void {
58
+ if (!this.enabled) {
59
+ return;
60
+ }
61
+ this.inFlightRequests += 1;
62
+ }
63
+
64
+ onRequestEnd(durationMs: number, statusCode: number): void {
65
+ if (!this.enabled) {
66
+ return;
67
+ }
68
+ this.totalRequests += 1;
69
+ if (this.inFlightRequests > 0) {
70
+ this.inFlightRequests -= 1;
71
+ }
72
+ if (statusCode >= 500) {
73
+ this.totalErrors += 1;
74
+ }
75
+ this.recordLatency(durationMs);
76
+ }
77
+
78
+ onTimeout(): void {
79
+ if (!this.enabled) {
80
+ return;
81
+ }
82
+ this.totalTimeouts += 1;
83
+ }
84
+
85
+ snapshot(): MetricsSnapshot {
86
+ const uptime = process.uptime();
87
+ const requestsPerSecond =
88
+ uptime > 0 ? this.totalRequests / uptime : this.totalRequests;
89
+
90
+ const latency = this.latencySnapshot();
91
+
92
+ const memoryUsage = process.memoryUsage();
93
+
94
+ let tasks: TaskMetrics | undefined;
95
+ if (this.taskEngine) {
96
+ tasks = this.taskEngine.getMetrics();
97
+ }
98
+
99
+ return {
100
+ totalRequests: this.totalRequests,
101
+ inFlightRequests: this.inFlightRequests,
102
+ totalErrors: this.totalErrors,
103
+ totalTimeouts: this.totalTimeouts,
104
+ requestsPerSecond,
105
+ latency,
106
+ scheduler: {
107
+ inFlight: this.scheduler.getCurrentInFlight(),
108
+ },
109
+ tasks,
110
+ memory: {
111
+ rssBytes: memoryUsage.rss,
112
+ heapUsedBytes: memoryUsage.heapUsed,
113
+ },
114
+ };
115
+ }
116
+
117
+ private recordLatency(durationMs: number): void {
118
+ if (!this.enabled) {
119
+ return;
120
+ }
121
+ if (!Number.isFinite(durationMs) || durationMs < 0) {
122
+ return;
123
+ }
124
+ this.latencies.push(durationMs);
125
+ if (this.latencies.length > this.maxLatencies) {
126
+ this.latencies.shift();
127
+ }
128
+ }
129
+
130
+ private latencySnapshot(): LatencySnapshot {
131
+ if (this.latencies.length === 0) {
132
+ return {
133
+ p50: null,
134
+ p95: null,
135
+ p99: null,
136
+ };
137
+ }
138
+
139
+ const sorted = [...this.latencies].sort((a, b) => a - b);
140
+
141
+ const p50 = this.percentile(sorted, 0.5);
142
+ const p95 = this.percentile(sorted, 0.95);
143
+ const p99 = this.percentile(sorted, 0.99);
144
+
145
+ return {
146
+ p50,
147
+ p95,
148
+ p99,
149
+ };
150
+ }
151
+
152
+ private percentile(sorted: number[], p: number): number {
153
+ if (sorted.length === 0) {
154
+ return 0;
155
+ }
156
+ if (p <= 0) {
157
+ return sorted[0];
158
+ }
159
+ if (p >= 1) {
160
+ return sorted[sorted.length - 1];
161
+ }
162
+ const index = Math.floor(p * (sorted.length - 1));
163
+ return sorted[index];
164
+ }
165
+ }
166
+
@@ -0,0 +1,38 @@
1
+ import os from 'os';
2
+
3
+ export type WorkerSetting = 'auto' | number;
4
+
5
+ export function calculateWorkerCount(setting: WorkerSetting): number {
6
+ if (typeof setting === 'number') {
7
+ if (!Number.isFinite(setting) || setting <= 0) {
8
+ return 1;
9
+ }
10
+ return Math.floor(setting);
11
+ }
12
+
13
+ const cpuCount = os.cpus().length || 1;
14
+ return Math.max(1, cpuCount);
15
+ }
16
+
17
+ export type ResourceThresholds = {
18
+ maxRssBytes?: number;
19
+ };
20
+
21
+ export type ResourceSample = {
22
+ rssBytes: number;
23
+ };
24
+
25
+ export function isResourceOverloaded(
26
+ sample: ResourceSample,
27
+ thresholds: ResourceThresholds,
28
+ ): boolean {
29
+ if (
30
+ thresholds.maxRssBytes !== undefined &&
31
+ sample.rssBytes > thresholds.maxRssBytes
32
+ ) {
33
+ return true;
34
+ }
35
+
36
+ return false;
37
+ }
38
+
@@ -0,0 +1,126 @@
1
+ import { WorkerQueue } from './worker-queue';
2
+ import { RoutePriority } from './types';
3
+
4
+ export type SchedulerOptions = {
5
+ maxConcurrency?: number;
6
+ workers?: number;
7
+ };
8
+
9
+ export type RunOptions = {
10
+ priority?: RoutePriority;
11
+ onOverloaded?: () => void;
12
+ timeoutMs?: number;
13
+ onTimeout?: () => void;
14
+ };
15
+
16
+ export type SchedulerStats = {
17
+ inFlight: number;
18
+ maxConcurrency: number;
19
+ workers: number;
20
+ perWorkerStats: {
21
+ workerId: number;
22
+ queued: number;
23
+ }[];
24
+ };
25
+
26
+ export class Scheduler {
27
+ private inFlight = 0;
28
+
29
+ private readonly maxConcurrency: number;
30
+
31
+ private readonly workerCount: number;
32
+
33
+ private readonly perWorkerQueues: WorkerQueue<() => void>[];
34
+
35
+ private nextWorkerIndex = 0;
36
+
37
+ constructor(options: SchedulerOptions = {}) {
38
+ const max = options.maxConcurrency ?? Infinity;
39
+ this.maxConcurrency = max > 0 ? max : Infinity;
40
+
41
+ // Initialize per-worker queues
42
+ this.workerCount = options.workers ?? 1;
43
+ this.perWorkerQueues = [];
44
+ for (let i = 0; i < this.workerCount; i += 1) {
45
+ this.perWorkerQueues.push(new WorkerQueue(1024));
46
+ }
47
+ }
48
+
49
+ getCurrentInFlight(): number {
50
+ return this.inFlight;
51
+ }
52
+
53
+ /**
54
+ * Get scheduler statistics (queued tasks per worker, etc.)
55
+ */
56
+ getStats(): SchedulerStats {
57
+ return {
58
+ inFlight: this.inFlight,
59
+ maxConcurrency: this.maxConcurrency,
60
+ workers: this.workerCount,
61
+ perWorkerStats: this.perWorkerQueues.map((q, i) => ({
62
+ workerId: i,
63
+ queued: q.getSize(),
64
+ })),
65
+ };
66
+ }
67
+
68
+ async run(
69
+ task: () => void | Promise<void>,
70
+ options: RunOptions,
71
+ ): Promise<void> {
72
+ const priority = options.priority ?? RoutePriority.STANDARD;
73
+
74
+ let threshold = this.maxConcurrency;
75
+ if (priority === RoutePriority.BEST_EFFORT) {
76
+ // Shed best-effort requests if we are above 80% capacity
77
+ threshold = Math.max(1, Math.floor(this.maxConcurrency * 0.8));
78
+ } else if (priority === RoutePriority.STANDARD) {
79
+ // Shed standard requests if we are above 95% capacity
80
+ threshold = Math.max(1, Math.floor(this.maxConcurrency * 0.95));
81
+ }
82
+ // CRITICAL allows up to 100%
83
+
84
+ if (this.inFlight >= threshold) {
85
+ if (options.onOverloaded) {
86
+ options.onOverloaded();
87
+ }
88
+ return;
89
+ }
90
+
91
+ this.inFlight += 1;
92
+
93
+ let timeoutId: NodeJS.Timeout | undefined;
94
+
95
+ try {
96
+ if (!options.timeoutMs || options.timeoutMs <= 0) {
97
+ const result = task();
98
+ if (result && typeof (result as Promise<void>).then === 'function') {
99
+ await result;
100
+ }
101
+ return;
102
+ }
103
+
104
+ const taskPromise = Promise.resolve(task()).then(() => 'ok' as const);
105
+
106
+ const timeoutPromise = new Promise<'timeout'>((resolve) => {
107
+ timeoutId = setTimeout(() => {
108
+ resolve('timeout');
109
+ }, options.timeoutMs);
110
+ });
111
+
112
+ const result = await Promise.race([taskPromise, timeoutPromise]);
113
+
114
+ if (result === 'timeout') {
115
+ if (options.onTimeout) {
116
+ options.onTimeout();
117
+ }
118
+ }
119
+ } finally {
120
+ if (timeoutId) {
121
+ clearTimeout(timeoutId);
122
+ }
123
+ this.inFlight -= 1;
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,87 @@
1
+ import { QHTTPX } from './server';
2
+ import {
3
+ QHTTPXHandler,
4
+ QHTTPXRouteOptions,
5
+ QHTTPXMiddleware,
6
+ QHTTPXPlugin,
7
+ QHTTPXPluginOptions,
8
+ } from './types';
9
+
10
+ /**
11
+ * A Scope represents a prefixed or isolated context for plugins.
12
+ * It proxies methods to the main QHTTPX instance but handles prefixing.
13
+ */
14
+ export class QHTTPXScope {
15
+ constructor(
16
+ private readonly app: QHTTPX,
17
+ private readonly prefix: string = ''
18
+ ) {}
19
+
20
+ /**
21
+ * Registers a sub-plugin within this scope.
22
+ * Prefixes are concatenated (e.g. /v1 + /users = /v1/users).
23
+ */
24
+ public async register<Options extends QHTTPXPluginOptions>(
25
+ plugin: QHTTPXPlugin<Options>,
26
+ options?: Options
27
+ ): Promise<void> {
28
+ const newPrefix = this.joinPaths(this.prefix, options?.prefix || '');
29
+ const scope = new QHTTPXScope(this.app, newPrefix);
30
+ await plugin(scope, options as Options);
31
+ }
32
+
33
+ public use(middleware: QHTTPXMiddleware): void {
34
+ // Middleware in scopes is currently global (TODO: Encapsulated middleware)
35
+ this.app.use(middleware);
36
+ }
37
+
38
+ public get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
39
+ this.app._registerRoute('GET', this.joinPaths(this.prefix, path), handler);
40
+ }
41
+
42
+ public post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
43
+ this.app._registerRoute('POST', this.joinPaths(this.prefix, path), handler);
44
+ }
45
+
46
+ public put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
47
+ this.app._registerRoute('PUT', this.joinPaths(this.prefix, path), handler);
48
+ }
49
+
50
+ public delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
51
+ this.app._registerRoute('DELETE', this.joinPaths(this.prefix, path), handler);
52
+ }
53
+
54
+ public patch(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
55
+ this.app._registerRoute('PATCH', this.joinPaths(this.prefix, path), handler);
56
+ }
57
+
58
+ public options(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
59
+ this.app._registerRoute('OPTIONS', this.joinPaths(this.prefix, path), handler);
60
+ }
61
+
62
+ public head(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
63
+ this.app._registerRoute('HEAD', this.joinPaths(this.prefix, path), handler);
64
+ }
65
+
66
+ // Helper to access the main app if needed
67
+ public getApp(): QHTTPX {
68
+ return this.app;
69
+ }
70
+
71
+ private joinPaths(head: string, tail: string): string {
72
+ if (!head) return tail;
73
+ if (!tail) return head;
74
+
75
+ // Ensure clean slash joining
76
+ const headSlash = head.endsWith('/');
77
+ const tailSlash = tail.startsWith('/');
78
+
79
+ if (headSlash && tailSlash) {
80
+ return head + tail.slice(1);
81
+ }
82
+ if (!headSlash && !tailSlash) {
83
+ return head + '/' + tail;
84
+ }
85
+ return head + tail;
86
+ }
87
+ }
@@ -0,0 +1,41 @@
1
+ import stringify from 'fast-json-stringify';
2
+
3
+ // Cache of compiled stringifiers per schema
4
+ const stringifierCache = new Map<string, (value: unknown) => string>();
5
+
6
+ // Default fast JSON stringifier for generic objects
7
+ const defaultStringifier = stringify({
8
+ type: 'object',
9
+ additionalProperties: true,
10
+ } as Parameters<typeof stringify>[0]);
11
+
12
+ /**
13
+ * Fast JSON serializer using fast-json-stringify
14
+ * For best performance, use schema-based stringifiers per route
15
+ */
16
+ export function fastJsonStringify(value: unknown, schema?: unknown): string {
17
+ if (schema) {
18
+ const schemaKey = JSON.stringify(schema);
19
+ let stringifier = stringifierCache.get(schemaKey);
20
+ if (!stringifier) {
21
+ stringifier = stringify(schema as Parameters<typeof stringify>[0]);
22
+ stringifierCache.set(schemaKey, stringifier);
23
+ }
24
+ return stringifier(value);
25
+ }
26
+ return defaultStringifier(value as object);
27
+ }
28
+
29
+ /**
30
+ * Get a pre-compiled stringifier for a specific schema
31
+ * Use this in route handlers for maximum performance
32
+ */
33
+ export function getStringifier(schema: unknown) {
34
+ const schemaKey = JSON.stringify(schema);
35
+ let stringifier = stringifierCache.get(schemaKey);
36
+ if (!stringifier) {
37
+ stringifier = stringify(schema as Parameters<typeof stringify>[0]);
38
+ stringifierCache.set(schemaKey, stringifier);
39
+ }
40
+ return stringifier;
41
+ }