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,111 @@
1
+ import { Readable } from 'stream';
2
+ import { QHTTPXContext } from './types';
3
+
4
+ export type SseOptions = {
5
+ retryMs?: number;
6
+ };
7
+
8
+ export type SseStream = {
9
+ send: (data: unknown, event?: string) => void;
10
+ close: () => void;
11
+ };
12
+
13
+ export function createSseStream(
14
+ ctx: QHTTPXContext,
15
+ options: SseOptions = {},
16
+ ): SseStream {
17
+ const res = ctx.res;
18
+
19
+ if (!res.headersSent) {
20
+ res.statusCode = 200;
21
+ res.setHeader('content-type', 'text/event-stream; charset=utf-8');
22
+ res.setHeader('cache-control', 'no-cache');
23
+ res.setHeader('connection', 'keep-alive');
24
+ }
25
+
26
+ const anyRes = res as unknown as {
27
+ flushHeaders?: () => void;
28
+ flush?: () => void;
29
+ };
30
+ if (anyRes.flushHeaders) {
31
+ anyRes.flushHeaders();
32
+ }
33
+
34
+ if (typeof options.retryMs === 'number') {
35
+ res.write(`retry: ${options.retryMs}\n\n`);
36
+ }
37
+
38
+ const send = (data: unknown, event?: string) => {
39
+ if (res.writableEnded) {
40
+ return;
41
+ }
42
+
43
+ const payload =
44
+ typeof data === 'string' ? data : JSON.stringify(data);
45
+
46
+ let chunk = '';
47
+ if (event) {
48
+ chunk += `event: ${event}\n`;
49
+ }
50
+ chunk += `data: ${payload}\n\n`;
51
+
52
+ res.write(chunk);
53
+
54
+ if (anyRes.flush) {
55
+ anyRes.flush();
56
+ }
57
+ };
58
+
59
+ const close = () => {
60
+ if (!res.writableEnded) {
61
+ res.end();
62
+ }
63
+ };
64
+
65
+ return { send, close };
66
+ }
67
+
68
+ export type StreamOptions = {
69
+ contentType?: string;
70
+ status?: number;
71
+ };
72
+
73
+ export function sendStream(
74
+ ctx: QHTTPXContext,
75
+ stream: Readable,
76
+ options: StreamOptions = {},
77
+ ): Promise<void> {
78
+ const res = ctx.res;
79
+
80
+ if (!res.headersSent) {
81
+ if (options.status !== undefined) {
82
+ res.statusCode = options.status;
83
+ }
84
+ if (options.contentType) {
85
+ res.setHeader('content-type', options.contentType);
86
+ }
87
+ }
88
+
89
+ return new Promise((resolve, reject) => {
90
+ stream.on('error', (err) => {
91
+ if (!res.headersSent) {
92
+ res.statusCode = 500;
93
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
94
+ }
95
+ if (!res.writableEnded) {
96
+ res.end('Internal Server Error');
97
+ }
98
+ reject(err);
99
+ });
100
+
101
+ stream.on('end', () => {
102
+ if (!res.writableEnded) {
103
+ res.end();
104
+ }
105
+ resolve();
106
+ });
107
+
108
+ stream.pipe(res, { end: false });
109
+ });
110
+ }
111
+
@@ -0,0 +1,138 @@
1
+ import { Scheduler } from './scheduler';
2
+
3
+ export type QHTTPXTaskHandler = (payload: unknown) => void | Promise<void>;
4
+
5
+ export type QHTTPXTaskOptions = {
6
+ maxRetries?: number;
7
+ backoffMs?: number;
8
+ };
9
+
10
+ export type TaskMetrics = {
11
+ registeredTasks: number;
12
+ totalEnqueued: number;
13
+ totalCompleted: number;
14
+ totalFailed: number;
15
+ totalOverloaded: number;
16
+ totalRetried: number;
17
+ };
18
+
19
+ type TaskDefinition = {
20
+ name: string;
21
+ handler: QHTTPXTaskHandler;
22
+ options: QHTTPXTaskOptions;
23
+ };
24
+
25
+ export class TaskEngine {
26
+ private readonly scheduler: Scheduler;
27
+
28
+ private readonly tasks = new Map<string, TaskDefinition>();
29
+
30
+ private registeredTasksCount = 0;
31
+
32
+ private totalEnqueued = 0;
33
+
34
+ private totalCompleted = 0;
35
+
36
+ private totalFailed = 0;
37
+
38
+ private totalOverloaded = 0;
39
+
40
+ private totalRetried = 0;
41
+
42
+ constructor(scheduler: Scheduler) {
43
+ this.scheduler = scheduler;
44
+ }
45
+
46
+ register(
47
+ name: string,
48
+ handler: QHTTPXTaskHandler,
49
+ options: QHTTPXTaskOptions = {},
50
+ ): void {
51
+ if (!this.tasks.has(name)) {
52
+ this.registeredTasksCount += 1;
53
+ }
54
+ this.tasks.set(name, {
55
+ name,
56
+ handler,
57
+ options,
58
+ });
59
+ }
60
+
61
+ async enqueue(name: string, payload: unknown): Promise<void> {
62
+ const def = this.tasks.get(name);
63
+ if (!def) {
64
+ throw new Error(`Task "${name}" is not registered`);
65
+ }
66
+
67
+ this.totalEnqueued += 1;
68
+ await this.executeWithRetry(def, payload);
69
+ }
70
+
71
+ getMetrics(): TaskMetrics {
72
+ return {
73
+ registeredTasks: this.registeredTasksCount,
74
+ totalEnqueued: this.totalEnqueued,
75
+ totalCompleted: this.totalCompleted,
76
+ totalFailed: this.totalFailed,
77
+ totalOverloaded: this.totalOverloaded,
78
+ totalRetried: this.totalRetried,
79
+ };
80
+ }
81
+
82
+ private async executeWithRetry(
83
+ def: TaskDefinition,
84
+ payload: unknown,
85
+ ): Promise<void> {
86
+ const maxRetries = def.options.maxRetries ?? 0;
87
+ const backoffMs = def.options.backoffMs ?? 0;
88
+
89
+ let attempt = 0;
90
+
91
+ for (;;) {
92
+ let overloaded = false;
93
+ let error: unknown;
94
+
95
+ await this.scheduler.run(
96
+ async () => {
97
+ try {
98
+ await def.handler(payload);
99
+ } catch (err) {
100
+ error = err;
101
+ }
102
+ },
103
+ {
104
+ onOverloaded: () => {
105
+ overloaded = true;
106
+ },
107
+ },
108
+ );
109
+
110
+ if (!overloaded && !error) {
111
+ this.totalCompleted += 1;
112
+ return;
113
+ }
114
+
115
+ if (attempt >= maxRetries) {
116
+ if (error) {
117
+ this.totalFailed += 1;
118
+ throw error;
119
+ }
120
+ if (overloaded) {
121
+ this.totalOverloaded += 1;
122
+ throw new Error(`Task "${def.name}" overloaded`);
123
+ }
124
+ return;
125
+ }
126
+
127
+ attempt += 1;
128
+ this.totalRetried += 1;
129
+
130
+ if (backoffMs > 0) {
131
+ await new Promise<void>((resolve) => {
132
+ setTimeout(resolve, backoffMs);
133
+ });
134
+ }
135
+ }
136
+ }
137
+ }
138
+
@@ -0,0 +1,178 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ import { URL } from 'url';
3
+ import type { BufferPool, BufferPoolConfig } from './buffer-pool';
4
+ import type { DatabaseManager } from '../database/manager';
5
+ import type { RouteSchema, Validator } from '../validation/types';
6
+ import type { ViewEngine } from '../views/types';
7
+ import type { RequestFusionOptions } from './fusion';
8
+
9
+ export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
10
+
11
+ export enum RoutePriority {
12
+ CRITICAL = 'critical',
13
+ STANDARD = 'standard',
14
+ BEST_EFFORT = 'best-effort',
15
+ }
16
+
17
+ export type RouteOptions = {
18
+ priority?: RoutePriority;
19
+ };
20
+
21
+ export class HttpError extends Error {
22
+ status: number;
23
+
24
+ code?: string;
25
+
26
+ details?: unknown;
27
+
28
+ constructor(
29
+ status: number,
30
+ message?: string,
31
+ options: { code?: string; details?: unknown } = {},
32
+ ) {
33
+ super(message ?? 'HTTP Error');
34
+ this.status = status;
35
+ this.code = options.code;
36
+ this.details = options.details;
37
+ Object.setPrototypeOf(this, new.target.prototype);
38
+ }
39
+ }
40
+
41
+ export type CookieOptions = {
42
+ domain?: string;
43
+ path?: string;
44
+ expires?: Date;
45
+ maxAge?: number;
46
+ httpOnly?: boolean;
47
+ secure?: boolean;
48
+ sameSite?: 'lax' | 'strict' | 'none';
49
+ };
50
+
51
+ export type QHTTPXContext = {
52
+ readonly req: IncomingMessage;
53
+ readonly res: ServerResponse;
54
+ readonly url: URL;
55
+ readonly params: Record<string, string>;
56
+ readonly query: Record<string, string | string[]>;
57
+ body: unknown;
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ files?: Record<string, any>;
60
+ readonly cookies: Record<string, string>;
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ state: Record<string, any>;
63
+ readonly bufferPool: BufferPool;
64
+ requestId: string | undefined;
65
+ requestStart: number | undefined;
66
+ // Serializer for the current route
67
+ serializer?: (value: unknown) => string;
68
+ readonly json: (body: unknown, status?: number) => void;
69
+ readonly send: (body: string | Buffer, status?: number) => void;
70
+ readonly html: (html: string, status?: number) => void;
71
+ readonly redirect: (url: string, status?: number) => void;
72
+ readonly setCookie: (name: string, value: string, options?: CookieOptions) => void;
73
+ readonly db?: DatabaseManager;
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ readonly call?: (op: string, params: any) => Promise<any>;
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ readonly render: (view: string, locals?: Record<string, any>) => Promise<void>;
78
+ readonly validate: <T = unknown>(schema: unknown, data?: unknown) => Promise<T>;
79
+ disableAutoEnd?: boolean;
80
+ };
81
+
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ export type QHTTPXOpHandler = (params: any, ctx: QHTTPXContext) => any | Promise<any>;
84
+
85
+ export type QHTTPXHandler = (ctx: QHTTPXContext) => void | Promise<void>;
86
+
87
+ export type QHTTPXRouteOptions = {
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ schema?: RouteSchema | Record<string, any>;
90
+ handler: QHTTPXHandler;
91
+ priority?: RoutePriority;
92
+ };
93
+
94
+ export type QHTTPXMiddleware = (
95
+ ctx: QHTTPXContext,
96
+ next: () => Promise<void>,
97
+ ) => void | Promise<void>;
98
+
99
+ export type QHTTPXErrorHandler = (
100
+ err: unknown,
101
+ ctx: QHTTPXContext,
102
+ ) => void | Promise<void>;
103
+
104
+ export type QHTTPXNotFoundHandler = (
105
+ ctx: QHTTPXContext,
106
+ ) => void | Promise<void>;
107
+
108
+ export type QHTTPXMethodNotAllowedHandler = (
109
+ ctx: QHTTPXContext,
110
+ allowedMethods: HTTPMethod[],
111
+ ) => void | Promise<void>;
112
+
113
+ export type QHTTPXTraceEvent =
114
+ | {
115
+ type: 'request_start';
116
+ method: string;
117
+ path: string;
118
+ requestId?: string;
119
+ }
120
+ | {
121
+ type: 'request_end';
122
+ method: string;
123
+ path: string;
124
+ statusCode: number;
125
+ durationMs: number;
126
+ requestId?: string;
127
+ };
128
+
129
+ export type QHTTPXTracer = (
130
+ event: QHTTPXTraceEvent,
131
+ ) => void | Promise<void>;
132
+
133
+ export type QHTTPXTaskHandler = (payload: unknown) => void | Promise<void>;
134
+
135
+ export type QHTTPXTaskOptions = {
136
+ maxRetries?: number;
137
+ backoffMs?: number;
138
+ };
139
+
140
+ export type PerformanceMode = 'balanced' | 'ultra';
141
+
142
+ export type QHTTPXOptions = {
143
+ name?: string;
144
+ workers?: 'auto' | number;
145
+ maxConcurrency?: number;
146
+ requestTimeoutMs?: number;
147
+ maxMemoryBytes?: number;
148
+ maxBodyBytes?: number;
149
+ keepAliveTimeoutMs?: number;
150
+ headersTimeoutMs?: number;
151
+ metricsEnabled?: boolean;
152
+ jsonSerializer?: (value: unknown) => string | Buffer;
153
+ bufferPoolConfig?: BufferPoolConfig;
154
+ errorHandler?: QHTTPXErrorHandler;
155
+ notFoundHandler?: QHTTPXNotFoundHandler;
156
+ methodNotAllowedHandler?: QHTTPXMethodNotAllowedHandler;
157
+ tracer?: QHTTPXTracer;
158
+ performanceMode?: PerformanceMode;
159
+ database?: DatabaseManager;
160
+ enableBatching?: boolean | { endpoint: string };
161
+ validator?: Validator;
162
+ enableRequestFusion?: boolean | RequestFusionOptions;
163
+ viewEngine?: ViewEngine;
164
+ viewsPath?: string;
165
+ };
166
+
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ export type QHTTPXPlugin<Options = any> = (
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ app: any, // We use 'any' here to avoid circular dependency with QHTTPX class, or we could use an interface
171
+ options: Options
172
+ ) => void | Promise<void>;
173
+
174
+ export type QHTTPXPluginOptions = {
175
+ prefix?: string;
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ [key: string]: any;
178
+ };
@@ -0,0 +1,112 @@
1
+ import { IncomingMessage } from 'http';
2
+ import { Duplex } from 'stream';
3
+ import { URL } from 'url';
4
+ import { WebSocket, WebSocketServer } from 'ws';
5
+
6
+ export interface QHTTPXWebSocket extends WebSocket {
7
+ join(room: string): void;
8
+ leave(room: string): void;
9
+ id: string;
10
+ }
11
+
12
+ export type WSHandler = (ws: QHTTPXWebSocket, req: IncomingMessage) => void;
13
+
14
+ export class WebSocketManager {
15
+ private readonly wss: WebSocketServer;
16
+ private readonly handlers: { path: string; handler: WSHandler }[] = [];
17
+ private readonly rooms: Map<string, Set<QHTTPXWebSocket>> = new Map();
18
+
19
+ constructor(private readonly requestIdGenerator: () => string) {
20
+ this.wss = new WebSocketServer({ noServer: true });
21
+ }
22
+
23
+ register(path: string, handler: WSHandler): void {
24
+ this.handlers.push({ path, handler });
25
+ }
26
+
27
+ async handleUpgrade(
28
+ req: IncomingMessage,
29
+ socket: Duplex,
30
+ head: Buffer,
31
+ ): Promise<void> {
32
+ const rawUrl = req.url || '/';
33
+ const host = req.headers.host || 'localhost';
34
+ const urlObj = new URL(rawUrl, `http://${host}`);
35
+ const path = urlObj.pathname;
36
+
37
+ const handlerEntry = this.handlers.find((entry) => entry.path === path);
38
+
39
+ if (!handlerEntry) {
40
+ socket.destroy();
41
+ return;
42
+ }
43
+
44
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
45
+ const qws = ws as QHTTPXWebSocket;
46
+ qws.id = this.requestIdGenerator();
47
+
48
+ qws.join = (room: string) => this.join(room, qws);
49
+ qws.leave = (room: string) => this.leave(room, qws);
50
+
51
+ qws.on('close', () => {
52
+ this.leaveAll(qws);
53
+ });
54
+
55
+ handlerEntry.handler(qws, req);
56
+ });
57
+ }
58
+
59
+ private join(room: string, ws: QHTTPXWebSocket) {
60
+ if (!this.rooms.has(room)) {
61
+ this.rooms.set(room, new Set());
62
+ }
63
+ this.rooms.get(room)!.add(ws);
64
+ }
65
+
66
+ private leave(room: string, ws: QHTTPXWebSocket) {
67
+ const set = this.rooms.get(room);
68
+ if (set) {
69
+ set.delete(ws);
70
+ if (set.size === 0) {
71
+ this.rooms.delete(room);
72
+ }
73
+ }
74
+ }
75
+
76
+ private leaveAll(ws: QHTTPXWebSocket) {
77
+ for (const [room, set] of this.rooms) {
78
+ if (set.has(ws)) {
79
+ set.delete(ws);
80
+ if (set.size === 0) {
81
+ this.rooms.delete(room);
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ public to(room: string) {
88
+ return {
89
+ emit: (data: unknown) => {
90
+ const set = this.rooms.get(room);
91
+ if (set) {
92
+ const payload =
93
+ typeof data === 'string' ? data : JSON.stringify(data);
94
+ for (const client of set) {
95
+ if (client.readyState === WebSocket.OPEN) {
96
+ client.send(payload);
97
+ }
98
+ }
99
+ }
100
+ },
101
+ };
102
+ }
103
+
104
+ public broadcast(data: unknown) {
105
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
106
+ for (const client of this.wss.clients) {
107
+ if (client.readyState === WebSocket.OPEN) {
108
+ client.send(payload);
109
+ }
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Lock-free (or lock-minimal) work queue for per-worker task distribution.
3
+ * Uses a simple ring buffer for high throughput.
4
+ */
5
+
6
+ export type WorkItem<T> = {
7
+ id: number;
8
+ task: T;
9
+ priority: number;
10
+ };
11
+
12
+ export class WorkerQueue<T> {
13
+ private readonly capacity: number;
14
+ private readonly buffer: (WorkItem<T> | undefined)[];
15
+
16
+ private writeIndex = 0;
17
+ private readIndex = 0;
18
+ private size = 0;
19
+
20
+ constructor(capacity: number = 1024) {
21
+ if (capacity <= 0 || !Number.isInteger(capacity)) {
22
+ throw new Error('Capacity must be a positive integer');
23
+ }
24
+ // Ensure capacity is a power of 2 for efficient modulo with bitmask
25
+ this.capacity = Math.pow(2, Math.ceil(Math.log2(capacity)));
26
+ this.buffer = new Array(this.capacity);
27
+ }
28
+
29
+ /**
30
+ * Enqueue a work item. Returns true if successful, false if queue is full.
31
+ */
32
+ enqueue(item: WorkItem<T>): boolean {
33
+ if (this.size >= this.capacity) {
34
+ return false;
35
+ }
36
+
37
+ this.buffer[this.writeIndex] = item;
38
+ this.writeIndex = (this.writeIndex + 1) & (this.capacity - 1);
39
+ this.size += 1;
40
+
41
+ return true;
42
+ }
43
+
44
+ /**
45
+ * Dequeue a work item. Returns undefined if queue is empty.
46
+ */
47
+ dequeue(): WorkItem<T> | undefined {
48
+ if (this.size <= 0) {
49
+ return undefined;
50
+ }
51
+
52
+ const item = this.buffer[this.readIndex];
53
+ this.buffer[this.readIndex] = undefined;
54
+ this.readIndex = (this.readIndex + 1) & (this.capacity - 1);
55
+ this.size -= 1;
56
+
57
+ return item;
58
+ }
59
+
60
+ /**
61
+ * Peek at the next item without removing it.
62
+ */
63
+ peek(): WorkItem<T> | undefined {
64
+ if (this.size <= 0) {
65
+ return undefined;
66
+ }
67
+ return this.buffer[this.readIndex];
68
+ }
69
+
70
+ /**
71
+ * Check if the queue is empty.
72
+ */
73
+ isEmpty(): boolean {
74
+ return this.size === 0;
75
+ }
76
+
77
+ /**
78
+ * Get the current size of the queue.
79
+ */
80
+ getSize(): number {
81
+ return this.size;
82
+ }
83
+
84
+ /**
85
+ * Get the capacity of the queue.
86
+ */
87
+ getCapacity(): number {
88
+ return this.capacity;
89
+ }
90
+ }