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,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BatchExecutor = void 0;
4
+ const coalescer_1 = require("../database/coalescer");
5
+ class BatchExecutor {
6
+ constructor(dbManager) {
7
+ this.ops = new Map();
8
+ this.coalescers = new Map();
9
+ this.dbManager = dbManager;
10
+ }
11
+ register(name, handler) {
12
+ this.ops.set(name, handler);
13
+ }
14
+ async handleBatch(ctx, batch) {
15
+ if (!Array.isArray(batch)) {
16
+ throw new Error('Batch must be an array');
17
+ }
18
+ // If we have a DB manager, we want to intercept queries for fusion
19
+ // We create a proxy for the DB manager attached to the context
20
+ let batchCtx = ctx;
21
+ if (this.dbManager && ctx.db) {
22
+ // Create a localized context that intercepts DB calls
23
+ // We need to ensure that when `ctx.db.get()` is called, it returns a wrapped adapter
24
+ // But `ctx.db` is the manager itself.
25
+ // We can't easily wrap the manager instance shared across requests.
26
+ // Instead, we can wrap the `ctx.db` property for this request scope.
27
+ const originalDb = ctx.db;
28
+ // Create a proxy for the database manager
29
+ const dbProxy = new Proxy(originalDb, {
30
+ get: (target, prop, receiver) => {
31
+ if (prop === 'get' || prop === 'connect') {
32
+ return (name) => {
33
+ const adapter = target.get(name);
34
+ // Check if we have a coalescer for this connection
35
+ const connName = name || 'default'; // Simplify for now
36
+ let coalescer = this.coalescers.get(connName);
37
+ // In a real per-request batch scope, we should create a new Coalescer
38
+ // OR reset the existing one?
39
+ // Actually, the Coalescer uses a short timeout (next tick).
40
+ // So it is safe to share across concurrent requests IF we want global batching.
41
+ // BUT "auto transaction scope" implies per-request isolation.
42
+ // For "Query Fusion", global batching is even better (cross-request fusion).
43
+ // However, for this task, let's stick to per-request or global safely.
44
+ // Since Coalescer flushes on next tick, sharing it is fine and efficient.
45
+ if (!coalescer) {
46
+ coalescer = new coalescer_1.QueryCoalescer(adapter);
47
+ this.coalescers.set(connName, coalescer);
48
+ }
49
+ // Return a proxy to the adapter that uses the coalescer
50
+ return {
51
+ ...adapter,
52
+ query: (query, params) => coalescer.query(query, params),
53
+ isConnected: () => adapter.isConnected(),
54
+ connect: () => adapter.connect(),
55
+ disconnect: () => adapter.disconnect()
56
+ };
57
+ };
58
+ }
59
+ return Reflect.get(target, prop, receiver);
60
+ }
61
+ });
62
+ // Create a new context object that inherits from original but overrides db
63
+ // We use Object.create to prototype inherit, but ctx is a flat object in QHTTPX.
64
+ // We must shallow copy.
65
+ batchCtx = { ...ctx, db: dbProxy };
66
+ }
67
+ // Helper for internal calls
68
+ // "ctx.call" implementation
69
+ // The user wants: `ctx.call("getUser", { id: 5 })`
70
+ // We attach this to the context
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ batchCtx.call = async (op, params) => {
73
+ const handler = this.ops.get(op);
74
+ if (!handler) {
75
+ throw new Error(`Unknown operation: ${op}`);
76
+ }
77
+ return handler(params, batchCtx);
78
+ };
79
+ // Execute all ops
80
+ // We can run them in parallel since we want to fuse queries
81
+ const results = await Promise.all(batch.map(async (item) => {
82
+ try {
83
+ const handler = this.ops.get(item.op);
84
+ if (!handler) {
85
+ return { error: `Unknown operation: ${item.op}`, id: item.id };
86
+ }
87
+ const result = await handler(item.params, batchCtx);
88
+ return { result, id: item.id };
89
+ }
90
+ catch (err) {
91
+ return { error: (err instanceof Error ? err.message : String(err)), id: item.id };
92
+ }
93
+ }));
94
+ return { results };
95
+ }
96
+ }
97
+ exports.BatchExecutor = BatchExecutor;
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BodyParser = void 0;
7
+ const querystring_1 = require("querystring");
8
+ const busboy_1 = __importDefault(require("busboy"));
9
+ class BodyParser {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ static async parse(req, options = {}) {
12
+ const method = (req.method || 'GET').toUpperCase();
13
+ if (method === 'GET' || method === 'HEAD') {
14
+ return { body: undefined };
15
+ }
16
+ const contentType = req.headers['content-type'] || '';
17
+ if (contentType.includes('multipart/form-data')) {
18
+ return this.parseMultipart(req, options);
19
+ }
20
+ const chunks = [];
21
+ let totalBytes = 0;
22
+ const maxBodyBytes = options.maxBodyBytes;
23
+ for await (const chunk of req) {
24
+ const bufferChunk = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
25
+ chunks.push(bufferChunk);
26
+ totalBytes += bufferChunk.length;
27
+ if (maxBodyBytes !== undefined && totalBytes > maxBodyBytes) {
28
+ throw new Error('QHTTPX_BODY_TOO_LARGE');
29
+ }
30
+ }
31
+ if (chunks.length === 0) {
32
+ return { body: undefined };
33
+ }
34
+ const buffer = Buffer.concat(chunks);
35
+ if (typeof contentType === 'string' &&
36
+ contentType.includes('application/json')) {
37
+ const text = buffer.toString('utf8');
38
+ try {
39
+ return { body: JSON.parse(text) };
40
+ }
41
+ catch {
42
+ throw new Error('QHTTPX_INVALID_JSON');
43
+ }
44
+ }
45
+ if (typeof contentType === 'string' &&
46
+ contentType.includes('application/x-www-form-urlencoded')) {
47
+ const text = buffer.toString('utf8');
48
+ return { body: (0, querystring_1.parse)(text) };
49
+ }
50
+ return { body: buffer };
51
+ }
52
+ static parseMultipart(req, options) {
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ return new Promise((resolve, reject) => {
55
+ let bb;
56
+ try {
57
+ bb = (0, busboy_1.default)({ headers: req.headers, limits: { fileSize: options.maxBodyBytes } });
58
+ }
59
+ catch (err) {
60
+ return reject(err);
61
+ }
62
+ const body = {};
63
+ const files = {};
64
+ bb.on('field', (name, val) => {
65
+ if (Object.prototype.hasOwnProperty.call(body, name)) {
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ if (Array.isArray(body[name])) {
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ body[name].push(val);
70
+ }
71
+ else {
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ body[name] = [body[name], val];
74
+ }
75
+ }
76
+ else {
77
+ body[name] = val;
78
+ }
79
+ });
80
+ bb.on('file', (name, file, info) => {
81
+ const { filename, encoding, mimeType } = info;
82
+ const chunks = [];
83
+ file.on('data', (chunk) => {
84
+ chunks.push(chunk);
85
+ });
86
+ file.on('end', () => {
87
+ const buffer = Buffer.concat(chunks);
88
+ const fileData = {
89
+ filename,
90
+ encoding,
91
+ mimeType,
92
+ data: buffer,
93
+ size: buffer.length,
94
+ };
95
+ if (Object.prototype.hasOwnProperty.call(files, name)) {
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ if (Array.isArray(files[name])) {
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ files[name].push(fileData);
100
+ }
101
+ else {
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ files[name] = [files[name], fileData];
104
+ }
105
+ }
106
+ else {
107
+ files[name] = fileData;
108
+ }
109
+ });
110
+ });
111
+ bb.on('close', () => {
112
+ resolve({ body, files });
113
+ });
114
+ bb.on('error', (err) => {
115
+ reject(err);
116
+ });
117
+ req.pipe(bb);
118
+ });
119
+ }
120
+ }
121
+ exports.BodyParser = BodyParser;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Buffer pool for response bodies (small, medium, large).
4
+ * Reuses allocated buffers to reduce GC pressure.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.BufferPool = void 0;
8
+ class BufferPool {
9
+ constructor(config = {}) {
10
+ this.smallBuffers = [];
11
+ this.mediumBuffers = [];
12
+ this.largeBuffers = [];
13
+ this.smallSize = config.smallSize ?? 4096; // 4KB
14
+ this.mediumSize = config.mediumSize ?? 65536; // 64KB
15
+ this.largeSize = config.largeSize ?? 262144; // 256KB
16
+ this.smallPoolSize = config.smallPoolSize ?? 32;
17
+ this.mediumPoolSize = config.mediumPoolSize ?? 8;
18
+ this.largePoolSize = config.largePoolSize ?? 2;
19
+ // Preallocate buffers
20
+ for (let i = 0; i < this.smallPoolSize; i += 1) {
21
+ this.smallBuffers.push(Buffer.allocUnsafe(this.smallSize));
22
+ }
23
+ for (let i = 0; i < this.mediumPoolSize; i += 1) {
24
+ this.mediumBuffers.push(Buffer.allocUnsafe(this.mediumSize));
25
+ }
26
+ for (let i = 0; i < this.largePoolSize; i += 1) {
27
+ this.largeBuffers.push(Buffer.allocUnsafe(this.largeSize));
28
+ }
29
+ }
30
+ /**
31
+ * Acquire a buffer suitable for the given size.
32
+ * Returns a buffer from the appropriate pool.
33
+ */
34
+ acquire(size) {
35
+ if (size <= this.smallSize) {
36
+ return this.smallBuffers.pop() || Buffer.allocUnsafe(this.smallSize);
37
+ }
38
+ if (size <= this.mediumSize) {
39
+ return this.mediumBuffers.pop() || Buffer.allocUnsafe(this.mediumSize);
40
+ }
41
+ return this.largeBuffers.pop() || Buffer.allocUnsafe(this.largeSize);
42
+ }
43
+ /**
44
+ * Release a buffer back to the appropriate pool.
45
+ */
46
+ release(buffer) {
47
+ if (buffer.length === this.smallSize && this.smallBuffers.length < this.smallPoolSize) {
48
+ this.smallBuffers.push(buffer);
49
+ }
50
+ else if (buffer.length === this.mediumSize &&
51
+ this.mediumBuffers.length < this.mediumPoolSize) {
52
+ this.mediumBuffers.push(buffer);
53
+ }
54
+ else if (buffer.length === this.largeSize &&
55
+ this.largeBuffers.length < this.largePoolSize) {
56
+ this.largeBuffers.push(buffer);
57
+ }
58
+ }
59
+ /**
60
+ * Get the number of available buffers in each pool.
61
+ */
62
+ getPoolStatus() {
63
+ return {
64
+ small: this.smallBuffers.length,
65
+ medium: this.mediumBuffers.length,
66
+ large: this.largeBuffers.length,
67
+ };
68
+ }
69
+ }
70
+ exports.BufferPool = BufferPool;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadConfig = loadConfig;
4
+ function loadConfig(options = {}) {
5
+ const env = options.env ?? process.env;
6
+ const defaults = options.defaults ?? {};
7
+ const prefix = options.prefix ?? 'QHTTPX_';
8
+ const getNumber = (key) => {
9
+ const value = env[prefix + key];
10
+ if (!value) {
11
+ return undefined;
12
+ }
13
+ const n = Number(value);
14
+ if (!Number.isFinite(n) || n <= 0) {
15
+ return undefined;
16
+ }
17
+ return n;
18
+ };
19
+ const getBoolean = (key) => {
20
+ const value = env[prefix + key];
21
+ if (!value) {
22
+ return undefined;
23
+ }
24
+ const lower = value.toLowerCase();
25
+ if (lower === 'true' || lower === '1') {
26
+ return true;
27
+ }
28
+ if (lower === 'false' || lower === '0') {
29
+ return false;
30
+ }
31
+ return undefined;
32
+ };
33
+ const maxConcurrency = getNumber('MAX_CONCURRENCY');
34
+ const requestTimeoutMs = getNumber('REQUEST_TIMEOUT_MS');
35
+ const maxMemoryBytes = getNumber('MAX_MEMORY_BYTES');
36
+ const maxBodyBytes = getNumber('MAX_BODY_BYTES');
37
+ const keepAliveTimeoutMs = getNumber('KEEP_ALIVE_TIMEOUT_MS');
38
+ const headersTimeoutMs = getNumber('HEADERS_TIMEOUT_MS');
39
+ const metricsEnabled = getBoolean('METRICS_ENABLED');
40
+ return {
41
+ ...defaults,
42
+ maxConcurrency: maxConcurrency ?? defaults.maxConcurrency,
43
+ requestTimeoutMs: requestTimeoutMs ?? defaults.requestTimeoutMs,
44
+ maxMemoryBytes: maxMemoryBytes ?? defaults.maxMemoryBytes,
45
+ maxBodyBytes: maxBodyBytes ?? defaults.maxBodyBytes,
46
+ keepAliveTimeoutMs: keepAliveTimeoutMs ?? defaults.keepAliveTimeoutMs,
47
+ headersTimeoutMs: headersTimeoutMs ?? defaults.headersTimeoutMs,
48
+ metricsEnabled: metricsEnabled ?? defaults.metricsEnabled,
49
+ };
50
+ }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RequestFusion = void 0;
4
+ const crypto_1 = require("crypto");
5
+ class RequestFusion {
6
+ constructor(options = {}) {
7
+ this.inflight = new Map();
8
+ this.cache = new Map();
9
+ this.options = typeof options === 'boolean' ? {} : options;
10
+ if (!this.options.vary) {
11
+ this.options.vary = ['authorization', 'cookie'];
12
+ }
13
+ }
14
+ async coalesce(ctx, next) {
15
+ const key = this.getKey(ctx);
16
+ // 1. Check Cache (Short-lived "Micro-TTL")
17
+ if (this.options.windowMs && this.options.windowMs > 0) {
18
+ const cached = this.cache.get(key);
19
+ if (cached) {
20
+ if (Date.now() < cached.expires) {
21
+ this.applyResult(ctx, cached.result);
22
+ return;
23
+ }
24
+ else {
25
+ this.cache.delete(key);
26
+ }
27
+ }
28
+ }
29
+ // 2. Check In-Flight
30
+ if (this.inflight.has(key)) {
31
+ try {
32
+ const result = await this.inflight.get(key);
33
+ this.applyResult(ctx, result);
34
+ return;
35
+ }
36
+ catch (err) {
37
+ // If leader failed, we should probably fail too or retry?
38
+ // For now, let's propagate the error.
39
+ throw err;
40
+ }
41
+ }
42
+ // 3. Be the Leader
43
+ let resolve;
44
+ let reject;
45
+ const promise = new Promise((res, rej) => {
46
+ resolve = res;
47
+ reject = rej;
48
+ });
49
+ this.inflight.set(key, promise);
50
+ // Hijack context methods to capture result
51
+ // Cast to any to allow overwriting readonly methods
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ const mutableCtx = ctx;
54
+ const originalJson = mutableCtx.json;
55
+ const originalSend = mutableCtx.send;
56
+ const originalHtml = mutableCtx.html;
57
+ const originalRedirect = mutableCtx.redirect;
58
+ let captured = false;
59
+ // Helper to cleanup and resolve
60
+ const finish = (result) => {
61
+ if (captured)
62
+ return;
63
+ captured = true;
64
+ resolve(result);
65
+ // Cache if needed
66
+ if (this.options.windowMs && this.options.windowMs > 0) {
67
+ this.cache.set(key, {
68
+ result,
69
+ expires: Date.now() + this.options.windowMs
70
+ });
71
+ // Cleanup cache later
72
+ setTimeout(() => this.cache.delete(key), this.options.windowMs);
73
+ }
74
+ // Remove from inflight immediately (unless we rely on cache for window)
75
+ // Actually, if we have windowMs, we rely on cache.
76
+ // If windowMs=0, we remove immediately.
77
+ this.inflight.delete(key);
78
+ };
79
+ mutableCtx.json = (payload, status = 200) => {
80
+ finish({ type: 'json', payload, status });
81
+ return originalJson.call(ctx, payload, status);
82
+ };
83
+ mutableCtx.send = (payload, status = 200) => {
84
+ finish({ type: 'send', payload, status });
85
+ return originalSend.call(ctx, payload, status);
86
+ };
87
+ mutableCtx.html = (payload, status = 200) => {
88
+ finish({ type: 'html', payload, status });
89
+ return originalHtml.call(ctx, payload, status);
90
+ };
91
+ mutableCtx.redirect = (url, status = 302) => {
92
+ finish({ type: 'redirect', payload: url, status });
93
+ return originalRedirect.call(ctx, url, status);
94
+ };
95
+ try {
96
+ await next(ctx);
97
+ // If next() completes but no response method was called,
98
+ // it implies the handler might be async and forgot to await,
99
+ // or it's a 404/middleware issue.
100
+ // If not captured yet, we can't resolve the followers properly.
101
+ // They will hang unless we reject or resolve with something.
102
+ // However, typical QHTTPX usage implies ctx.json/send is called.
103
+ }
104
+ catch (err) {
105
+ reject(err);
106
+ this.inflight.delete(key);
107
+ // Restore methods
108
+ mutableCtx.json = originalJson;
109
+ mutableCtx.send = originalSend;
110
+ mutableCtx.html = originalHtml;
111
+ mutableCtx.redirect = originalRedirect;
112
+ throw err;
113
+ }
114
+ }
115
+ applyResult(ctx, result) {
116
+ switch (result.type) {
117
+ case 'json':
118
+ ctx.json(result.payload, result.status);
119
+ break;
120
+ case 'send': {
121
+ if (typeof result.payload === 'string' || Buffer.isBuffer(result.payload)) {
122
+ ctx.send(result.payload, result.status);
123
+ }
124
+ else {
125
+ ctx.send(String(result.payload), result.status);
126
+ }
127
+ break;
128
+ }
129
+ case 'html': {
130
+ if (typeof result.payload === 'string') {
131
+ ctx.html(result.payload, result.status);
132
+ }
133
+ else {
134
+ ctx.html(String(result.payload), result.status);
135
+ }
136
+ break;
137
+ }
138
+ case 'redirect': {
139
+ if (typeof result.payload === 'string') {
140
+ ctx.redirect(result.payload, result.status);
141
+ }
142
+ else {
143
+ ctx.redirect(String(result.payload), result.status);
144
+ }
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ getKey(ctx) {
150
+ // Base: Method + Path
151
+ let data = `${ctx.req.method}|${ctx.url?.pathname}|`;
152
+ // Query
153
+ if (ctx.query) {
154
+ // Deterministic sort
155
+ const keys = Object.keys(ctx.query).sort();
156
+ for (const k of keys) {
157
+ data += `${k}=${ctx.query[k]}|`;
158
+ }
159
+ }
160
+ // Body (if parsed)
161
+ if (ctx.body) {
162
+ // We assume body is simple JSON.
163
+ // For objects, order matters for hashing.
164
+ // fast-json-stringify or JSON.stringify isn't always deterministic key order.
165
+ // But for performance, JSON.stringify is often "good enough" if keys aren't shuffled.
166
+ // For strict correctness, we should use a canonical stringify, but that's slow.
167
+ // Let's use JSON.stringify for now.
168
+ data += JSON.stringify(ctx.body);
169
+ }
170
+ // Vary Headers
171
+ if (this.options.vary) {
172
+ for (const header of this.options.vary) {
173
+ const val = ctx.req.headers[header.toLowerCase()];
174
+ if (val) {
175
+ data += `|${header}:${val}`;
176
+ }
177
+ }
178
+ }
179
+ // We hash it to keep the key size manageable
180
+ return (0, crypto_1.createHash)('sha1').update(data).digest('hex');
181
+ }
182
+ }
183
+ exports.RequestFusion = RequestFusion;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Logger = void 0;
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ const pino_1 = __importDefault(require("pino"));
9
+ class Logger {
10
+ constructor(options = {}) {
11
+ const isDev = process.env.NODE_ENV !== 'production';
12
+ const pretty = options.pretty ?? isDev;
13
+ this.pino = (0, pino_1.default)({
14
+ name: options.name || 'qhttpx',
15
+ level: options.level || 'info',
16
+ transport: pretty
17
+ ? {
18
+ target: 'pino-pretty',
19
+ options: {
20
+ colorize: true,
21
+ translateTime: 'HH:MM:ss Z',
22
+ ignore: 'pid,hostname',
23
+ },
24
+ }
25
+ : undefined,
26
+ });
27
+ }
28
+ info(arg1, ...args) {
29
+ this.pino.info(arg1, ...args);
30
+ }
31
+ error(arg1, ...args) {
32
+ this.pino.error(arg1, ...args);
33
+ }
34
+ warn(arg1, ...args) {
35
+ this.pino.warn(arg1, ...args);
36
+ }
37
+ debug(arg1, ...args) {
38
+ this.pino.debug(arg1, ...args);
39
+ }
40
+ fatal(arg1, ...args) {
41
+ this.pino.fatal(arg1, ...args);
42
+ }
43
+ child(bindings) {
44
+ const child = new Logger();
45
+ child.pino = this.pino.child(bindings);
46
+ return child;
47
+ }
48
+ }
49
+ exports.Logger = Logger;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Metrics = void 0;
4
+ class Metrics {
5
+ constructor(scheduler, options = {}, taskEngine) {
6
+ this.latencies = [];
7
+ this.totalRequests = 0;
8
+ this.inFlightRequests = 0;
9
+ this.totalErrors = 0;
10
+ this.totalTimeouts = 0;
11
+ this.scheduler = scheduler;
12
+ this.taskEngine = taskEngine;
13
+ this.maxLatencies = options.maxLatencies ?? 1000;
14
+ this.enabled = options.enabled ?? true;
15
+ }
16
+ onRequestStart() {
17
+ if (!this.enabled) {
18
+ return;
19
+ }
20
+ this.inFlightRequests += 1;
21
+ }
22
+ onRequestEnd(durationMs, statusCode) {
23
+ if (!this.enabled) {
24
+ return;
25
+ }
26
+ this.totalRequests += 1;
27
+ if (this.inFlightRequests > 0) {
28
+ this.inFlightRequests -= 1;
29
+ }
30
+ if (statusCode >= 500) {
31
+ this.totalErrors += 1;
32
+ }
33
+ this.recordLatency(durationMs);
34
+ }
35
+ onTimeout() {
36
+ if (!this.enabled) {
37
+ return;
38
+ }
39
+ this.totalTimeouts += 1;
40
+ }
41
+ snapshot() {
42
+ const uptime = process.uptime();
43
+ const requestsPerSecond = uptime > 0 ? this.totalRequests / uptime : this.totalRequests;
44
+ const latency = this.latencySnapshot();
45
+ const memoryUsage = process.memoryUsage();
46
+ let tasks;
47
+ if (this.taskEngine) {
48
+ tasks = this.taskEngine.getMetrics();
49
+ }
50
+ return {
51
+ totalRequests: this.totalRequests,
52
+ inFlightRequests: this.inFlightRequests,
53
+ totalErrors: this.totalErrors,
54
+ totalTimeouts: this.totalTimeouts,
55
+ requestsPerSecond,
56
+ latency,
57
+ scheduler: {
58
+ inFlight: this.scheduler.getCurrentInFlight(),
59
+ },
60
+ tasks,
61
+ memory: {
62
+ rssBytes: memoryUsage.rss,
63
+ heapUsedBytes: memoryUsage.heapUsed,
64
+ },
65
+ };
66
+ }
67
+ recordLatency(durationMs) {
68
+ if (!this.enabled) {
69
+ return;
70
+ }
71
+ if (!Number.isFinite(durationMs) || durationMs < 0) {
72
+ return;
73
+ }
74
+ this.latencies.push(durationMs);
75
+ if (this.latencies.length > this.maxLatencies) {
76
+ this.latencies.shift();
77
+ }
78
+ }
79
+ latencySnapshot() {
80
+ if (this.latencies.length === 0) {
81
+ return {
82
+ p50: null,
83
+ p95: null,
84
+ p99: null,
85
+ };
86
+ }
87
+ const sorted = [...this.latencies].sort((a, b) => a - b);
88
+ const p50 = this.percentile(sorted, 0.5);
89
+ const p95 = this.percentile(sorted, 0.95);
90
+ const p99 = this.percentile(sorted, 0.99);
91
+ return {
92
+ p50,
93
+ p95,
94
+ p99,
95
+ };
96
+ }
97
+ percentile(sorted, p) {
98
+ if (sorted.length === 0) {
99
+ return 0;
100
+ }
101
+ if (p <= 0) {
102
+ return sorted[0];
103
+ }
104
+ if (p >= 1) {
105
+ return sorted[sorted.length - 1];
106
+ }
107
+ const index = Math.floor(p * (sorted.length - 1));
108
+ return sorted[index];
109
+ }
110
+ }
111
+ exports.Metrics = Metrics;