qhttpx 1.8.5 → 1.8.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +72 -52
- package/binding.gyp +18 -0
- package/dist/examples/api-server.js +29 -8
- package/dist/examples/basic.d.ts +1 -0
- package/dist/examples/basic.js +10 -0
- package/dist/examples/compression.d.ts +1 -0
- package/dist/examples/compression.js +17 -0
- package/dist/examples/cors.d.ts +1 -0
- package/dist/examples/cors.js +19 -0
- package/dist/examples/errors.d.ts +1 -0
- package/dist/examples/errors.js +25 -0
- package/dist/examples/file-upload.d.ts +1 -0
- package/dist/examples/file-upload.js +24 -0
- package/dist/examples/fusion.d.ts +1 -0
- package/dist/examples/fusion.js +21 -0
- package/dist/examples/rate-limiting.d.ts +1 -0
- package/dist/examples/rate-limiting.js +17 -0
- package/dist/examples/validation.d.ts +1 -0
- package/dist/examples/validation.js +23 -0
- package/dist/examples/websockets.d.ts +1 -0
- package/dist/examples/websockets.js +20 -0
- package/dist/package.json +11 -1
- package/dist/src/benchmarks/simple-json.js +6 -4
- package/dist/src/cli/index.js +33 -11
- package/dist/src/core/errors.d.ts +34 -0
- package/dist/src/core/errors.js +70 -0
- package/dist/src/core/native-adapter.d.ts +11 -0
- package/dist/src/core/native-adapter.js +211 -0
- package/dist/src/core/server.d.ts +52 -4
- package/dist/src/core/server.js +389 -261
- package/dist/src/core/types.d.ts +37 -0
- package/dist/src/index.d.ts +6 -1
- package/dist/src/index.js +19 -3
- package/dist/src/middleware/compression.d.ts +1 -5
- package/dist/src/middleware/cors.d.ts +1 -10
- package/dist/src/middleware/presets.d.ts +4 -1
- package/dist/src/middleware/presets.js +22 -3
- package/dist/src/middleware/rate-limit.d.ts +1 -19
- package/dist/src/middleware/rate-limit.js +6 -0
- package/dist/src/middleware/security.d.ts +1 -2
- package/dist/src/native/index.d.ts +29 -0
- package/dist/src/native/index.js +64 -0
- package/dist/src/router/radix-tree.d.ts +2 -0
- package/dist/src/router/radix-tree.js +54 -4
- package/dist/src/router/router.d.ts +1 -0
- package/dist/src/router/router.js +42 -2
- package/dist/tests/native-adapter.test.d.ts +1 -0
- package/dist/tests/native-adapter.test.js +71 -0
- package/dist/tests/resources.test.js +3 -0
- package/dist/tests/security.test.js +2 -2
- package/docs/AEGIS.md +34 -9
- package/docs/BENCHMARKS.md +8 -7
- package/docs/ERRORS.md +112 -0
- package/docs/FUSION.md +68 -0
- package/docs/MIDDLEWARE.md +65 -0
- package/docs/ROUTING.md +70 -0
- package/docs/STATIC.md +61 -0
- package/docs/WEBSOCKETS.md +76 -0
- package/package.json +11 -1
- package/src/native/README.md +31 -0
- package/src/native/addon.cc +8 -0
- package/src/native/index.ts +78 -0
- package/src/native/picohttpparser.c +608 -0
- package/src/native/picohttpparser.h +71 -0
- package/src/native/server.cc +262 -0
- package/src/native/server.h +30 -0
- package/.eslintrc.json +0 -22
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/npm-publish.yml +0 -37
- package/.github/workflows/release.yml +0 -21
- package/.prettierrc +0 -7
- package/assets/logo.svg +0 -25
- package/eslint.config.cjs +0 -26
- package/examples/api-server.ts +0 -62
- package/src/benchmarks/quantam-users.ts +0 -70
- package/src/benchmarks/simple-json.ts +0 -71
- package/src/benchmarks/ultra-mode.ts +0 -127
- package/src/cli/index.ts +0 -214
- package/src/client/index.ts +0 -93
- package/src/core/batch.ts +0 -110
- package/src/core/body-parser.ts +0 -151
- package/src/core/buffer-pool.ts +0 -96
- package/src/core/config.ts +0 -60
- package/src/core/fusion.ts +0 -210
- package/src/core/logger.ts +0 -70
- package/src/core/metrics.ts +0 -166
- package/src/core/resources.ts +0 -38
- package/src/core/scheduler.ts +0 -126
- package/src/core/scope.ts +0 -87
- package/src/core/serializer.ts +0 -41
- package/src/core/server.ts +0 -1234
- package/src/core/stream.ts +0 -111
- package/src/core/tasks.ts +0 -138
- package/src/core/types.ts +0 -192
- package/src/core/websocket.ts +0 -112
- package/src/core/worker-queue.ts +0 -90
- package/src/database/adapters/memory.ts +0 -99
- package/src/database/adapters/mongo.ts +0 -116
- package/src/database/adapters/postgres.ts +0 -86
- package/src/database/adapters/sqlite.ts +0 -44
- package/src/database/coalescer.ts +0 -153
- package/src/database/manager.ts +0 -97
- package/src/database/types.ts +0 -24
- package/src/index.ts +0 -58
- package/src/middleware/compression.ts +0 -147
- package/src/middleware/cors.ts +0 -98
- package/src/middleware/presets.ts +0 -50
- package/src/middleware/rate-limit.ts +0 -106
- package/src/middleware/security.ts +0 -109
- package/src/middleware/static.ts +0 -216
- package/src/openapi/generator.ts +0 -167
- package/src/router/radix-router.ts +0 -119
- package/src/router/radix-tree.ts +0 -106
- package/src/router/router.ts +0 -190
- package/src/testing/index.ts +0 -104
- package/src/utils/cookies.ts +0 -67
- package/src/utils/logger.ts +0 -59
- package/src/utils/signals.ts +0 -45
- package/src/utils/sse.ts +0 -41
- package/src/validation/index.ts +0 -3
- package/src/validation/simple.ts +0 -93
- package/src/validation/types.ts +0 -38
- package/src/validation/zod.ts +0 -14
- package/src/views/index.ts +0 -1
- package/src/views/types.ts +0 -4
- package/tests/adapters.test.ts +0 -120
- package/tests/batch.test.ts +0 -139
- package/tests/body-parser.test.ts +0 -83
- package/tests/compression-sse.test.ts +0 -98
- package/tests/cookies.test.ts +0 -74
- package/tests/cors.test.ts +0 -79
- package/tests/database.test.ts +0 -90
- package/tests/dx.test.ts +0 -130
- package/tests/ecosystem.test.ts +0 -156
- package/tests/features.test.ts +0 -51
- package/tests/fusion.test.ts +0 -121
- package/tests/http-basic.test.ts +0 -161
- package/tests/logger.test.ts +0 -48
- package/tests/middleware.test.ts +0 -137
- package/tests/observability.test.ts +0 -91
- package/tests/openapi.test.ts +0 -74
- package/tests/plugin.test.ts +0 -85
- package/tests/plugins.test.ts +0 -93
- package/tests/rate-limit.test.ts +0 -97
- package/tests/resources.test.ts +0 -64
- package/tests/scheduler.test.ts +0 -71
- package/tests/schema-routes.test.ts +0 -89
- package/tests/security.test.ts +0 -128
- package/tests/server-db.test.ts +0 -72
- package/tests/smoke.test.ts +0 -9
- package/tests/sqlite-fusion.test.ts +0 -106
- package/tests/static.test.ts +0 -111
- package/tests/stream.test.ts +0 -58
- package/tests/task-metrics.test.ts +0 -78
- package/tests/tasks.test.ts +0 -90
- package/tests/testing.test.ts +0 -53
- package/tests/validation.test.ts +0 -126
- package/tests/websocket.test.ts +0 -132
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -9
package/tests/dx.test.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
|
-
import { createHttpApp } from '../src/index';
|
|
4
|
-
import { app as singletonApp } from '../src/index';
|
|
5
|
-
|
|
6
|
-
describe('Developer Experience (DX) Features', () => {
|
|
7
|
-
it('supports destructured context in route handlers', async () => {
|
|
8
|
-
const app = createHttpApp();
|
|
9
|
-
|
|
10
|
-
app.get('/destructure', ({ json, path, query }) => {
|
|
11
|
-
json({ path, query, status: 'ok' });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const response = await fetch(`http://127.0.0.1:${port}/destructure?foo=bar`);
|
|
18
|
-
expect(response.status).toBe(200);
|
|
19
|
-
const body = await response.json();
|
|
20
|
-
expect(body).toEqual({
|
|
21
|
-
path: '/destructure',
|
|
22
|
-
query: { foo: 'bar' },
|
|
23
|
-
status: 'ok'
|
|
24
|
-
});
|
|
25
|
-
} finally {
|
|
26
|
-
await app.close();
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('supports destructured context in onError handler', async () => {
|
|
31
|
-
const app = createHttpApp();
|
|
32
|
-
|
|
33
|
-
app.get('/error', () => {
|
|
34
|
-
throw new Error('Boom');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
app.onError(({ error, json }) => {
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
json({ error: (error as any).message, handled: true }, 500);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const response = await fetch(`http://127.0.0.1:${port}/error`);
|
|
46
|
-
expect(response.status).toBe(500);
|
|
47
|
-
const body = await response.json();
|
|
48
|
-
expect(body).toEqual({ error: 'Boom', handled: true });
|
|
49
|
-
} finally {
|
|
50
|
-
await app.close();
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('supports notFound alias', async () => {
|
|
55
|
-
const app = createHttpApp();
|
|
56
|
-
|
|
57
|
-
app.notFound(({ json }) => {
|
|
58
|
-
json({ error: 'Not Found Custom' }, 404);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch(`http://127.0.0.1:${port}/missing`);
|
|
65
|
-
expect(response.status).toBe(404);
|
|
66
|
-
const body = await response.json();
|
|
67
|
-
expect(body).toEqual({ error: 'Not Found Custom' });
|
|
68
|
-
} finally {
|
|
69
|
-
await app.close();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('exports a singleton app instance', () => {
|
|
74
|
-
expect(singletonApp).toBeDefined();
|
|
75
|
-
expect(typeof singletonApp.get).toBe('function');
|
|
76
|
-
expect(typeof singletonApp.listen).toBe('function');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('supports destructured next in middleware', async () => {
|
|
80
|
-
const app = createHttpApp();
|
|
81
|
-
const calls: string[] = [];
|
|
82
|
-
|
|
83
|
-
// Middleware with destructuring: ({ next })
|
|
84
|
-
app.use(async ({ next, req }) => {
|
|
85
|
-
calls.push('start');
|
|
86
|
-
calls.push(req.method!); // Verify req is accessible
|
|
87
|
-
if (next) await next();
|
|
88
|
-
calls.push('end');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
app.get('/', ({ json }) => {
|
|
92
|
-
calls.push('handler');
|
|
93
|
-
json({ ok: true });
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
await fetch(`http://127.0.0.1:${port}/`);
|
|
100
|
-
expect(calls).toEqual(['start', 'GET', 'handler', 'end']);
|
|
101
|
-
} finally {
|
|
102
|
-
await app.close();
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('supports typed files in context', async () => {
|
|
107
|
-
// This is primarily a type check, but we can simulate a file structure
|
|
108
|
-
const app = createHttpApp();
|
|
109
|
-
|
|
110
|
-
app.post('/upload', ({ files, json }) => {
|
|
111
|
-
// Simulate type access
|
|
112
|
-
if (files && files['avatar']) {
|
|
113
|
-
const avatar = Array.isArray(files['avatar']) ? files['avatar'][0] : files['avatar'];
|
|
114
|
-
json({ filename: avatar.filename, size: avatar.size });
|
|
115
|
-
} else {
|
|
116
|
-
json({ error: 'no file' }, 400);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Mocking the context directly to test logic without full multipart request (BodyParser is tested elsewhere)
|
|
121
|
-
// But we can create a "fake" request if we used the internal methods,
|
|
122
|
-
// here we just want to ensure the code compiles and runs if files are present.
|
|
123
|
-
// Let's do a full test with createTestClient if possible, or just skip full multipart integration test
|
|
124
|
-
// since we updated the types.
|
|
125
|
-
// For now, let's just ensure the server runs.
|
|
126
|
-
|
|
127
|
-
await app.listen(0, '127.0.0.1');
|
|
128
|
-
await app.close();
|
|
129
|
-
});
|
|
130
|
-
});
|
package/tests/ecosystem.test.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { QHTTPX } from '../src/core/server';
|
|
3
|
-
import { QHTTPXMiddleware } from '../src/core/types';
|
|
4
|
-
|
|
5
|
-
// Mock External Libraries
|
|
6
|
-
const mockJose = {
|
|
7
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
8
|
-
jwtVerify: async (token: string, _secret: unknown) => {
|
|
9
|
-
if (token === 'valid_token') return { payload: { sub: 'user_123', role: 'admin' } };
|
|
10
|
-
throw new Error('Invalid Token');
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const mockRedis = {
|
|
15
|
-
get: async (key: string) => {
|
|
16
|
-
if (key === 'cache:page:home') return '<html>Cached Home</html>';
|
|
17
|
-
return null;
|
|
18
|
-
},
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
|
-
set: async (key: string, val: string) => 'OK'
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
describe('SaaS Ecosystem Compatibility', () => {
|
|
24
|
-
|
|
25
|
-
it('should allow integration of 3rd party JWT libraries (e.g. jose)', async () => {
|
|
26
|
-
const app = new QHTTPX();
|
|
27
|
-
const secret = 'super-secret';
|
|
28
|
-
// const token = await new SignJWT({ 'urn:example:claim': true })
|
|
29
|
-
// .setProtectedHeader({ alg: 'HS256' })
|
|
30
|
-
// .setIssuedAt()
|
|
31
|
-
// .setExpirationTime('2h')
|
|
32
|
-
// .sign(new TextEncoder().encode(secret));
|
|
33
|
-
|
|
34
|
-
// 1. Create a custom Authentication Middleware using "jose"
|
|
35
|
-
const authMiddleware: QHTTPXMiddleware = async (ctx, next) => {
|
|
36
|
-
const authHeader = ctx.req.headers['authorization'];
|
|
37
|
-
if (!authHeader) {
|
|
38
|
-
ctx.res.statusCode = 401;
|
|
39
|
-
ctx.res.end('Unauthorized');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const token = authHeader.split(' ')[1];
|
|
45
|
-
const { payload } = await mockJose.jwtVerify(token, secret);
|
|
46
|
-
|
|
47
|
-
// Store user in ctx.state (Standard Pattern)
|
|
48
|
-
ctx.state.user = payload;
|
|
49
|
-
await next();
|
|
50
|
-
} catch {
|
|
51
|
-
ctx.res.statusCode = 403;
|
|
52
|
-
ctx.res.end('Forbidden');
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
app.use(authMiddleware);
|
|
57
|
-
|
|
58
|
-
// Protected Route
|
|
59
|
-
app.get('/dashboard', async (ctx) => {
|
|
60
|
-
// Access the user injected by middleware
|
|
61
|
-
const user = ctx.state.user;
|
|
62
|
-
ctx.json({ message: `Welcome ${user.sub}`, role: user.role });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const { port } = await app.listen(0);
|
|
66
|
-
const url = `http://localhost:${port}`;
|
|
67
|
-
|
|
68
|
-
// Test Valid Token
|
|
69
|
-
const res1 = await fetch(`${url}/dashboard`, { headers: { Authorization: 'Bearer valid_token' } });
|
|
70
|
-
expect(res1.status).toBe(200);
|
|
71
|
-
const data1 = await res1.json();
|
|
72
|
-
expect(data1).toEqual({ message: 'Welcome user_123', role: 'admin' });
|
|
73
|
-
|
|
74
|
-
// Test Invalid Token
|
|
75
|
-
const res2 = await fetch(`${url}/dashboard`, { headers: { Authorization: 'Bearer bad_token' } });
|
|
76
|
-
expect(res2.status).toBe(403);
|
|
77
|
-
|
|
78
|
-
await app.close();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should allow integration of 3rd party Caching (e.g. Redis)', async () => {
|
|
82
|
-
const app = new QHTTPX();
|
|
83
|
-
|
|
84
|
-
// 2. Create a Caching Middleware using "redis"
|
|
85
|
-
const cacheMiddleware: QHTTPXMiddleware = async (ctx, next) => {
|
|
86
|
-
const key = `cache:page:${ctx.url.pathname.replace('/', '')}`;
|
|
87
|
-
const cached = await mockRedis.get(key);
|
|
88
|
-
|
|
89
|
-
if (cached) {
|
|
90
|
-
ctx.res.setHeader('X-Cache', 'HIT');
|
|
91
|
-
ctx.res.end(cached);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
ctx.res.setHeader('X-Cache', 'MISS');
|
|
96
|
-
await next();
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
app.use(cacheMiddleware);
|
|
100
|
-
|
|
101
|
-
app.get('/home', async (ctx) => {
|
|
102
|
-
ctx.html('<html>Fresh Home</html>');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const { port } = await app.listen(0);
|
|
106
|
-
const url = `http://localhost:${port}`;
|
|
107
|
-
|
|
108
|
-
// Test Cache Hit (Mocked)
|
|
109
|
-
const res1 = await fetch(`${url}/home`);
|
|
110
|
-
expect(res1.status).toBe(200);
|
|
111
|
-
expect(res1.headers.get('x-cache')).toBe('HIT');
|
|
112
|
-
const text1 = await res1.text();
|
|
113
|
-
expect(text1).toBe('<html>Cached Home</html>');
|
|
114
|
-
|
|
115
|
-
// Test Cache Miss (Mocked for other route)
|
|
116
|
-
// Note: In a real app we'd need to intercept res.end to write to redis,
|
|
117
|
-
// but for this capability check, reading is enough proof.
|
|
118
|
-
|
|
119
|
-
await app.close();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should allow integration of ORM/DB libraries', async () => {
|
|
123
|
-
const app = new QHTTPX();
|
|
124
|
-
|
|
125
|
-
// Mock TypeORM/Prisma usage
|
|
126
|
-
const db = {
|
|
127
|
-
users: {
|
|
128
|
-
findMany: async () => [{ id: 1, name: 'Alice' }],
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// Middleware to attach DB to context
|
|
133
|
-
app.use(async (ctx, next) => {
|
|
134
|
-
// We can attach to ctx.state or extend ctx type if we want
|
|
135
|
-
ctx.state.db = db;
|
|
136
|
-
await next();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
app.get('/users', async (ctx) => {
|
|
140
|
-
try {
|
|
141
|
-
const users = await ctx.state.db.users.findMany();
|
|
142
|
-
ctx.res.end(JSON.stringify(users));
|
|
143
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
144
|
-
} catch (err) {
|
|
145
|
-
ctx.res.statusCode = 500;
|
|
146
|
-
ctx.res.end('DB Error');
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const { port } = await app.listen(0);
|
|
151
|
-
const url = `http://localhost:${port}`;
|
|
152
|
-
const res = await fetch(`${url}/users`);
|
|
153
|
-
expect(res.status).toBe(200);
|
|
154
|
-
await app.close();
|
|
155
|
-
});
|
|
156
|
-
});
|
package/tests/features.test.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { QHTTPX } from '../src/index';
|
|
3
|
-
import { Logger } from '../src/core/logger';
|
|
4
|
-
import { hc } from '../src/client';
|
|
5
|
-
|
|
6
|
-
describe('Logger', () => {
|
|
7
|
-
it('should initialize with pino', () => {
|
|
8
|
-
const logger = new Logger({ level: 'info' });
|
|
9
|
-
expect(logger).toBeDefined();
|
|
10
|
-
// Check if methods exist (can't easily mock internal pino without spy)
|
|
11
|
-
expect(typeof logger.info).toBe('function');
|
|
12
|
-
expect(typeof logger.error).toBe('function');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should be integrated into QHTTPX', () => {
|
|
16
|
-
const app = new QHTTPX();
|
|
17
|
-
expect(app.logger).toBeDefined();
|
|
18
|
-
expect(app.logger instanceof Logger).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe('RPC Client', () => {
|
|
23
|
-
it('should construct correct URLs and methods', async () => {
|
|
24
|
-
// We mock fetch globally
|
|
25
|
-
const originalFetch = global.fetch;
|
|
26
|
-
global.fetch = async (url: RequestInfo | URL, options?: RequestInit) => {
|
|
27
|
-
return new Response(JSON.stringify({ url, method: options?.method }), {
|
|
28
|
-
status: 200,
|
|
29
|
-
headers: { 'Content-Type': 'application/json' },
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
-
const client = hc<any>('http://localhost:3000');
|
|
36
|
-
|
|
37
|
-
const res1 = await client.api.users[':id'].$get({ param: { id: '123' } });
|
|
38
|
-
const data1 = await res1.json();
|
|
39
|
-
expect(data1.url).toBe('http://localhost:3000/api/users/123');
|
|
40
|
-
expect(data1.method).toBe('GET');
|
|
41
|
-
|
|
42
|
-
const res2 = await client.auth.login.$post({ json: { username: 'test' } });
|
|
43
|
-
const data2 = await res2.json();
|
|
44
|
-
expect(data2.url).toBe('http://localhost:3000/auth/login');
|
|
45
|
-
expect(data2.method).toBe('POST');
|
|
46
|
-
|
|
47
|
-
} finally {
|
|
48
|
-
global.fetch = originalFetch;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
});
|
package/tests/fusion.test.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { QHTTPX } from '../src/core/server';
|
|
3
|
-
|
|
4
|
-
describe('Request Fusion Engine', () => {
|
|
5
|
-
let app: QHTTPX;
|
|
6
|
-
|
|
7
|
-
afterEach(async () => {
|
|
8
|
-
if (app) await app.close();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('should coalesce simultaneous requests to the same endpoint', async () => {
|
|
12
|
-
app = new QHTTPX({ enableRequestFusion: true });
|
|
13
|
-
|
|
14
|
-
let executionCount = 0;
|
|
15
|
-
|
|
16
|
-
// Simulate a slow handler
|
|
17
|
-
app.get('/slow', async (ctx) => {
|
|
18
|
-
executionCount++;
|
|
19
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
20
|
-
ctx.json({ count: executionCount });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const { port } = await app.listen(0);
|
|
24
|
-
const url = `http://localhost:${port}/slow`;
|
|
25
|
-
|
|
26
|
-
// Fire 3 requests simultaneously
|
|
27
|
-
const p1 = fetch(url).then(r => r.json());
|
|
28
|
-
const p2 = fetch(url).then(r => r.json());
|
|
29
|
-
const p3 = fetch(url).then(r => r.json());
|
|
30
|
-
|
|
31
|
-
const results = await Promise.all([p1, p2, p3]);
|
|
32
|
-
|
|
33
|
-
// All should get the same result
|
|
34
|
-
expect(results[0]).toEqual({ count: 1 });
|
|
35
|
-
expect(results[1]).toEqual({ count: 1 });
|
|
36
|
-
expect(results[2]).toEqual({ count: 1 });
|
|
37
|
-
|
|
38
|
-
// Handler should have run only once
|
|
39
|
-
expect(executionCount).toBe(1);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should NOT coalesce requests with different query params', async () => {
|
|
43
|
-
app = new QHTTPX({ enableRequestFusion: true });
|
|
44
|
-
|
|
45
|
-
let executionCount = 0;
|
|
46
|
-
|
|
47
|
-
app.get('/echo', async (ctx) => {
|
|
48
|
-
executionCount++;
|
|
49
|
-
await new Promise(resolve => setTimeout(resolve, 20));
|
|
50
|
-
ctx.json({ q: ctx.query.q, count: executionCount });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const { port } = await app.listen(0);
|
|
54
|
-
|
|
55
|
-
const p1 = fetch(`http://localhost:${port}/echo?q=a`).then(r => r.json());
|
|
56
|
-
const p2 = fetch(`http://localhost:${port}/echo?q=b`).then(r => r.json());
|
|
57
|
-
|
|
58
|
-
const results = await Promise.all([p1, p2]);
|
|
59
|
-
|
|
60
|
-
expect(results[0].q).toBe('a');
|
|
61
|
-
expect(results[1].q).toBe('b');
|
|
62
|
-
|
|
63
|
-
// Should run twice because keys are different
|
|
64
|
-
expect(executionCount).toBe(2);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should use cache window to coalesce burst traffic', async () => {
|
|
68
|
-
// 50ms window
|
|
69
|
-
app = new QHTTPX({
|
|
70
|
-
enableRequestFusion: { windowMs: 50 }
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
let executionCount = 0;
|
|
74
|
-
|
|
75
|
-
app.get('/burst', async (ctx) => {
|
|
76
|
-
executionCount++;
|
|
77
|
-
// Fast handler
|
|
78
|
-
ctx.json({ count: executionCount });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const { port } = await app.listen(0);
|
|
82
|
-
const url = `http://localhost:${port}/burst`;
|
|
83
|
-
|
|
84
|
-
// Request 1
|
|
85
|
-
const r1 = await fetch(url).then(r => r.json());
|
|
86
|
-
expect(r1.count).toBe(1);
|
|
87
|
-
|
|
88
|
-
// Request 2 (immediate, within window)
|
|
89
|
-
const r2 = await fetch(url).then(r => r.json());
|
|
90
|
-
expect(r2.count).toBe(1); // Should be cached
|
|
91
|
-
|
|
92
|
-
// Request 3 (wait 100ms, outside window)
|
|
93
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
94
|
-
const r3 = await fetch(url).then(r => r.json());
|
|
95
|
-
expect(r3.count).toBe(2); // New execution
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should respect vary headers (Authorization)', async () => {
|
|
99
|
-
app = new QHTTPX({ enableRequestFusion: true });
|
|
100
|
-
|
|
101
|
-
let executionCount = 0;
|
|
102
|
-
|
|
103
|
-
app.get('/auth', async (ctx) => {
|
|
104
|
-
executionCount++;
|
|
105
|
-
await new Promise(resolve => setTimeout(resolve, 20));
|
|
106
|
-
ctx.json({ user: ctx.req.headers['authorization'], count: executionCount });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const { port } = await app.listen(0);
|
|
110
|
-
const url = `http://localhost:${port}/auth`;
|
|
111
|
-
|
|
112
|
-
const p1 = fetch(url, { headers: { 'Authorization': 'UserA' } }).then(r => r.json());
|
|
113
|
-
const p2 = fetch(url, { headers: { 'Authorization': 'UserB' } }).then(r => r.json());
|
|
114
|
-
|
|
115
|
-
const results = await Promise.all([p1, p2]);
|
|
116
|
-
|
|
117
|
-
expect(results[0].user).toBe('UserA');
|
|
118
|
-
expect(results[1].user).toBe('UserB');
|
|
119
|
-
expect(executionCount).toBe(2);
|
|
120
|
-
});
|
|
121
|
-
});
|
package/tests/http-basic.test.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { QHTTPX } from '../src/index';
|
|
3
|
-
|
|
4
|
-
describe('QHTTPX minimal HTTP runtime', () => {
|
|
5
|
-
let app: QHTTPX | undefined;
|
|
6
|
-
|
|
7
|
-
afterEach(async () => {
|
|
8
|
-
if (app) {
|
|
9
|
-
await app.close();
|
|
10
|
-
app = undefined;
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('handles GET / and returns JSON', async () => {
|
|
15
|
-
app = new QHTTPX();
|
|
16
|
-
|
|
17
|
-
app.get('/', (ctx) => {
|
|
18
|
-
ctx.json({ message: 'Hello from QHTTPX' });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
22
|
-
|
|
23
|
-
const response = await fetch(`http://127.0.0.1:${port}/`);
|
|
24
|
-
expect(response.status).toBe(200);
|
|
25
|
-
const json = (await response.json()) as { message: string };
|
|
26
|
-
expect(json.message).toBe('Hello from QHTTPX');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('returns 404 for unknown route', async () => {
|
|
30
|
-
app = new QHTTPX();
|
|
31
|
-
|
|
32
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
33
|
-
|
|
34
|
-
const response = await fetch(`http://127.0.0.1:${port}/unknown`);
|
|
35
|
-
expect(response.status).toBe(404);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('supports params and query parsing', async () => {
|
|
39
|
-
app = new QHTTPX();
|
|
40
|
-
|
|
41
|
-
app.get('/users/:id', (ctx) => {
|
|
42
|
-
ctx.json({
|
|
43
|
-
id: ctx.params.id,
|
|
44
|
-
q: ctx.query.q,
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
49
|
-
|
|
50
|
-
const response = await fetch(`http://127.0.0.1:${port}/users/123?q=test`);
|
|
51
|
-
expect(response.status).toBe(200);
|
|
52
|
-
const json = (await response.json()) as { id: string; q: string };
|
|
53
|
-
expect(json.id).toBe('123');
|
|
54
|
-
expect(json.q).toBe('test');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('parses JSON body for POST', async () => {
|
|
58
|
-
app = new QHTTPX();
|
|
59
|
-
|
|
60
|
-
app.post('/echo', (ctx) => {
|
|
61
|
-
ctx.json({ body: ctx.body });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
65
|
-
|
|
66
|
-
const response = await fetch(`http://127.0.0.1:${port}/echo`, {
|
|
67
|
-
method: 'POST',
|
|
68
|
-
headers: {
|
|
69
|
-
'content-type': 'application/json',
|
|
70
|
-
},
|
|
71
|
-
body: JSON.stringify({ hello: 'world' }),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
expect(response.status).toBe(200);
|
|
75
|
-
const json = (await response.json()) as { body: { hello: string } };
|
|
76
|
-
expect(json.body.hello).toBe('world');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns 400 for invalid JSON body', async () => {
|
|
80
|
-
app = new QHTTPX();
|
|
81
|
-
|
|
82
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
83
|
-
|
|
84
|
-
app.post('/echo', (ctx) => {
|
|
85
|
-
ctx.json({ body: ctx.body });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const response = await fetch(`http://127.0.0.1:${port}/echo`, {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
headers: {
|
|
91
|
-
'content-type': 'application/json',
|
|
92
|
-
},
|
|
93
|
-
body: '{ invalid',
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(response.status).toBe(400);
|
|
97
|
-
const text = await response.text();
|
|
98
|
-
expect(text).toBe('Invalid JSON');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('enforces maxBodyBytes with 413 status', async () => {
|
|
102
|
-
app = new QHTTPX({ maxBodyBytes: 8 });
|
|
103
|
-
|
|
104
|
-
app.post('/echo', (ctx) => {
|
|
105
|
-
ctx.json({ body: ctx.body });
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
109
|
-
|
|
110
|
-
const response = await fetch(`http://127.0.0.1:${port}/echo`, {
|
|
111
|
-
method: 'POST',
|
|
112
|
-
headers: {
|
|
113
|
-
'content-type': 'application/json',
|
|
114
|
-
},
|
|
115
|
-
body: JSON.stringify({ hello: 'world' }),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
expect(response.status).toBe(413);
|
|
119
|
-
const text = await response.text();
|
|
120
|
-
expect(text).toBe('Payload Too Large');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('enforces maxConcurrency with 503 overload', async () => {
|
|
124
|
-
app = new QHTTPX({ maxConcurrency: 1 });
|
|
125
|
-
|
|
126
|
-
app.get('/slow', async (ctx) => {
|
|
127
|
-
await new Promise<void>((resolve) => {
|
|
128
|
-
setTimeout(resolve, 100);
|
|
129
|
-
});
|
|
130
|
-
ctx.json({ ok: true });
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
134
|
-
|
|
135
|
-
const url = `http://127.0.0.1:${port}/slow`;
|
|
136
|
-
|
|
137
|
-
const first = fetch(url);
|
|
138
|
-
const second = fetch(url);
|
|
139
|
-
|
|
140
|
-
const [firstRes, secondRes] = await Promise.all([first, second]);
|
|
141
|
-
|
|
142
|
-
const statuses = [firstRes.status, secondRes.status].sort();
|
|
143
|
-
expect(statuses).toEqual([200, 503]);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('applies request timeout with 504 status', async () => {
|
|
147
|
-
app = new QHTTPX({ requestTimeoutMs: 20 });
|
|
148
|
-
|
|
149
|
-
app.get('/timeout', async (ctx) => {
|
|
150
|
-
await new Promise<void>((resolve) => {
|
|
151
|
-
setTimeout(resolve, 100);
|
|
152
|
-
});
|
|
153
|
-
ctx.json({ ok: true });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
157
|
-
|
|
158
|
-
const response = await fetch(`http://127.0.0.1:${port}/timeout`);
|
|
159
|
-
expect(response.status).toBe(504);
|
|
160
|
-
});
|
|
161
|
-
});
|
package/tests/logger.test.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { QHTTPX, createLoggerMiddleware } from '../src';
|
|
3
|
-
|
|
4
|
-
describe('Logger middleware', () => {
|
|
5
|
-
let app: QHTTPX | undefined;
|
|
6
|
-
|
|
7
|
-
afterEach(async () => {
|
|
8
|
-
if (app) {
|
|
9
|
-
await app.close();
|
|
10
|
-
app = undefined;
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('logs basic request information to sink', async () => {
|
|
15
|
-
const sink = vi.fn();
|
|
16
|
-
|
|
17
|
-
app = new QHTTPX();
|
|
18
|
-
app.use(
|
|
19
|
-
createLoggerMiddleware({
|
|
20
|
-
sink,
|
|
21
|
-
}),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
app.get('/hello', (ctx) => {
|
|
25
|
-
ctx.send('ok');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const { port } = await app.listen(0, '127.0.0.1');
|
|
29
|
-
|
|
30
|
-
const response = await fetch(`http://127.0.0.1:${port}/hello`);
|
|
31
|
-
|
|
32
|
-
expect(response.status).toBe(200);
|
|
33
|
-
expect(await response.text()).toBe('ok');
|
|
34
|
-
|
|
35
|
-
expect(sink).toHaveBeenCalledTimes(1);
|
|
36
|
-
const entry = sink.mock.calls[0][0] as {
|
|
37
|
-
method: string;
|
|
38
|
-
path: string;
|
|
39
|
-
status: number;
|
|
40
|
-
durationMs: number;
|
|
41
|
-
};
|
|
42
|
-
expect(entry.method).toBe('GET');
|
|
43
|
-
expect(entry.path).toBe('/hello');
|
|
44
|
-
expect(entry.status).toBe(200);
|
|
45
|
-
expect(entry.durationMs).toBeGreaterThanOrEqual(0);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|