@syncular/server-hono 0.0.6-96 → 0.2.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 (104) hide show
  1. package/README.md +5 -23
  2. package/dist/admin-page.d.ts +14 -0
  3. package/dist/admin-page.js +217 -0
  4. package/dist/admin.d.ts +39 -0
  5. package/dist/admin.js +142 -0
  6. package/dist/index.d.ts +16 -14
  7. package/dist/index.js +134 -23
  8. package/package.json +19 -53
  9. package/src/admin-page.ts +217 -0
  10. package/src/admin.ts +193 -0
  11. package/src/index.ts +175 -31
  12. package/dist/api-key-auth.d.ts +0 -49
  13. package/dist/api-key-auth.d.ts.map +0 -1
  14. package/dist/api-key-auth.js +0 -110
  15. package/dist/api-key-auth.js.map +0 -1
  16. package/dist/blobs.d.ts +0 -69
  17. package/dist/blobs.d.ts.map +0 -1
  18. package/dist/blobs.js +0 -383
  19. package/dist/blobs.js.map +0 -1
  20. package/dist/console/gateway.d.ts +0 -33
  21. package/dist/console/gateway.d.ts.map +0 -1
  22. package/dist/console/gateway.js +0 -2534
  23. package/dist/console/gateway.js.map +0 -1
  24. package/dist/console/index.d.ts +0 -11
  25. package/dist/console/index.d.ts.map +0 -1
  26. package/dist/console/index.js +0 -11
  27. package/dist/console/index.js.map +0 -1
  28. package/dist/console/routes.d.ts +0 -33
  29. package/dist/console/routes.d.ts.map +0 -1
  30. package/dist/console/routes.js +0 -3029
  31. package/dist/console/routes.js.map +0 -1
  32. package/dist/console/schemas.d.ts +0 -490
  33. package/dist/console/schemas.d.ts.map +0 -1
  34. package/dist/console/schemas.js +0 -303
  35. package/dist/console/schemas.js.map +0 -1
  36. package/dist/console/types.d.ts +0 -175
  37. package/dist/console/types.d.ts.map +0 -1
  38. package/dist/console/types.js +0 -2
  39. package/dist/console/types.js.map +0 -1
  40. package/dist/console/ui.d.ts +0 -38
  41. package/dist/console/ui.d.ts.map +0 -1
  42. package/dist/console/ui.js +0 -43
  43. package/dist/console/ui.js.map +0 -1
  44. package/dist/create-server.d.ts +0 -68
  45. package/dist/create-server.d.ts.map +0 -1
  46. package/dist/create-server.js +0 -110
  47. package/dist/create-server.js.map +0 -1
  48. package/dist/index.d.ts.map +0 -1
  49. package/dist/index.js.map +0 -1
  50. package/dist/openapi.d.ts +0 -45
  51. package/dist/openapi.d.ts.map +0 -1
  52. package/dist/openapi.js +0 -59
  53. package/dist/openapi.js.map +0 -1
  54. package/dist/proxy/connection-manager.d.ts +0 -78
  55. package/dist/proxy/connection-manager.d.ts.map +0 -1
  56. package/dist/proxy/connection-manager.js +0 -251
  57. package/dist/proxy/connection-manager.js.map +0 -1
  58. package/dist/proxy/index.d.ts +0 -8
  59. package/dist/proxy/index.d.ts.map +0 -1
  60. package/dist/proxy/index.js +0 -8
  61. package/dist/proxy/index.js.map +0 -1
  62. package/dist/proxy/routes.d.ts +0 -74
  63. package/dist/proxy/routes.d.ts.map +0 -1
  64. package/dist/proxy/routes.js +0 -147
  65. package/dist/proxy/routes.js.map +0 -1
  66. package/dist/rate-limit.d.ts +0 -101
  67. package/dist/rate-limit.d.ts.map +0 -1
  68. package/dist/rate-limit.js +0 -186
  69. package/dist/rate-limit.js.map +0 -1
  70. package/dist/routes.d.ts +0 -161
  71. package/dist/routes.d.ts.map +0 -1
  72. package/dist/routes.js +0 -1493
  73. package/dist/routes.js.map +0 -1
  74. package/dist/ws.d.ts +0 -235
  75. package/dist/ws.d.ts.map +0 -1
  76. package/dist/ws.js +0 -614
  77. package/dist/ws.js.map +0 -1
  78. package/src/__tests__/blob-routes.test.ts +0 -424
  79. package/src/__tests__/console-gateway-live-routes.test.ts +0 -297
  80. package/src/__tests__/console-gateway-routes.test.ts +0 -1364
  81. package/src/__tests__/console-routes.test.ts +0 -1626
  82. package/src/__tests__/console-ui.test.ts +0 -114
  83. package/src/__tests__/create-server.test.ts +0 -485
  84. package/src/__tests__/pull-chunk-storage.test.ts +0 -576
  85. package/src/__tests__/rate-limit.test.ts +0 -78
  86. package/src/__tests__/realtime-bridge.test.ts +0 -135
  87. package/src/__tests__/sync-rate-limit-routing.test.ts +0 -185
  88. package/src/__tests__/ws-connection-manager.test.ts +0 -176
  89. package/src/api-key-auth.ts +0 -179
  90. package/src/blobs.ts +0 -534
  91. package/src/console/gateway.ts +0 -3603
  92. package/src/console/index.ts +0 -11
  93. package/src/console/routes.ts +0 -3936
  94. package/src/console/schemas.ts +0 -434
  95. package/src/console/types.ts +0 -185
  96. package/src/console/ui.ts +0 -100
  97. package/src/create-server.ts +0 -207
  98. package/src/openapi.ts +0 -74
  99. package/src/proxy/connection-manager.ts +0 -340
  100. package/src/proxy/index.ts +0 -8
  101. package/src/proxy/routes.ts +0 -223
  102. package/src/rate-limit.ts +0 -321
  103. package/src/routes.ts +0 -2059
  104. package/src/ws.ts +0 -802
@@ -1,576 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
- import { gunzipSync } from 'node:zlib';
3
- import {
4
- configureSyncTelemetry,
5
- createDatabase,
6
- decodeSnapshotRows,
7
- getSyncTelemetry,
8
- SyncCombinedResponseSchema,
9
- type SyncPullResponse,
10
- type SyncSnapshotChunkRef,
11
- type SyncSpan,
12
- type SyncTelemetry,
13
- } from '@syncular/core';
14
- import {
15
- createServerHandler,
16
- ensureSyncSchema,
17
- type SnapshotChunkStorage,
18
- type SyncCoreDb,
19
- } from '@syncular/server';
20
- import { Hono } from 'hono';
21
- import type { Kysely } from 'kysely';
22
- import { createBunSqliteDialect } from '../../../dialect-bun-sqlite/src';
23
- import { createSqliteServerDialect } from '../../../server-dialect-sqlite/src';
24
- import { createSyncRoutes } from '../routes';
25
-
26
- interface TasksTable {
27
- id: string;
28
- user_id: string;
29
- title: string;
30
- server_version: number;
31
- }
32
-
33
- interface ServerDb extends SyncCoreDb {
34
- tasks: TasksTable;
35
- }
36
-
37
- interface ClientDb {
38
- tasks: TasksTable;
39
- }
40
-
41
- function createExceptionCaptureTelemetry(calls: {
42
- exceptions: Array<{
43
- error: unknown;
44
- context: Record<string, unknown> | undefined;
45
- }>;
46
- }): SyncTelemetry {
47
- return {
48
- log() {},
49
- tracer: {
50
- startSpan(_options, callback) {
51
- const span: SyncSpan = {
52
- setAttribute() {},
53
- setAttributes() {},
54
- setStatus() {},
55
- };
56
- return callback(span);
57
- },
58
- },
59
- metrics: {
60
- count() {},
61
- gauge() {},
62
- distribution() {},
63
- },
64
- captureException(error, context) {
65
- calls.exceptions.push({ error, context });
66
- },
67
- };
68
- }
69
-
70
- function mustGetFirstChunkId(payload: SyncPullResponse): string {
71
- const chunkId = payload.subscriptions[0]?.snapshots?.[0]?.chunks?.[0]?.id;
72
- if (!chunkId) {
73
- throw new Error('Expected pull bootstrap response to include a chunk id.');
74
- }
75
- return chunkId;
76
- }
77
-
78
- async function streamToBytes(
79
- stream: ReadableStream<Uint8Array>
80
- ): Promise<Uint8Array> {
81
- const reader = stream.getReader();
82
- try {
83
- const chunks: Uint8Array[] = [];
84
- let total = 0;
85
- while (true) {
86
- const { done, value } = await reader.read();
87
- if (done) break;
88
- if (!value) continue;
89
- chunks.push(value);
90
- total += value.length;
91
- }
92
-
93
- const out = new Uint8Array(total);
94
- let offset = 0;
95
- for (const chunk of chunks) {
96
- out.set(chunk, offset);
97
- offset += chunk.length;
98
- }
99
- return out;
100
- } finally {
101
- reader.releaseLock();
102
- }
103
- }
104
-
105
- describe('createSyncRoutes chunkStorage wiring', () => {
106
- let db: Kysely<ServerDb>;
107
- const dialect = createSqliteServerDialect();
108
-
109
- beforeEach(async () => {
110
- db = createDatabase<ServerDb>({
111
- dialect: createBunSqliteDialect({ path: ':memory:' }),
112
- family: 'sqlite',
113
- });
114
- await ensureSyncSchema(db, dialect);
115
-
116
- await db.schema
117
- .createTable('tasks')
118
- .addColumn('id', 'text', (col) => col.primaryKey())
119
- .addColumn('user_id', 'text', (col) => col.notNull())
120
- .addColumn('title', 'text', (col) => col.notNull())
121
- .addColumn('server_version', 'integer', (col) =>
122
- col.notNull().defaultTo(0)
123
- )
124
- .execute();
125
- });
126
-
127
- afterEach(async () => {
128
- await db.destroy();
129
- });
130
-
131
- it('uses external chunk storage in /pull and serves chunks from it', async () => {
132
- await db
133
- .insertInto('tasks')
134
- .values({
135
- id: 't1',
136
- user_id: 'u1',
137
- title: 'Task 1',
138
- server_version: 1,
139
- })
140
- .execute();
141
-
142
- const tasksHandler = createServerHandler<ServerDb, ClientDb, 'tasks'>({
143
- table: 'tasks',
144
- scopes: ['user:{user_id}'],
145
- resolveScopes: async (ctx) => ({ user_id: [ctx.actorId] }),
146
- });
147
-
148
- const externalChunkBodies = new Map<string, Uint8Array>();
149
- let storeChunkCalls = 0;
150
- const chunkStorage: SnapshotChunkStorage = {
151
- name: 'test-external',
152
- async storeChunk(metadata) {
153
- storeChunkCalls += 1;
154
- const ref: SyncSnapshotChunkRef = {
155
- id: `chunk-${storeChunkCalls}`,
156
- sha256: metadata.sha256,
157
- byteLength: metadata.body.length,
158
- encoding: metadata.encoding,
159
- compression: metadata.compression,
160
- };
161
- externalChunkBodies.set(ref.id, new Uint8Array(metadata.body));
162
- return ref;
163
- },
164
- async readChunk(chunkId: string) {
165
- const body = externalChunkBodies.get(chunkId);
166
- return body ? new Uint8Array(body) : null;
167
- },
168
- async findChunk() {
169
- return null;
170
- },
171
- async cleanupExpired() {
172
- return 0;
173
- },
174
- };
175
-
176
- const routes = createSyncRoutes({
177
- db,
178
- dialect,
179
- handlers: [tasksHandler],
180
- authenticate: async (c) => {
181
- const actorId = c.req.header('x-user-id');
182
- return actorId ? { actorId } : null;
183
- },
184
- chunkStorage,
185
- });
186
-
187
- const app = new Hono();
188
- app.route('/sync', routes);
189
-
190
- const pullResponse = await app.request(
191
- new Request('http://localhost/sync', {
192
- method: 'POST',
193
- headers: {
194
- 'content-type': 'application/json',
195
- 'x-user-id': 'u1',
196
- },
197
- body: JSON.stringify({
198
- clientId: 'client-1',
199
- pull: {
200
- limitCommits: 10,
201
- limitSnapshotRows: 100,
202
- maxSnapshotPages: 1,
203
- subscriptions: [
204
- {
205
- id: 'sub-1',
206
- table: 'tasks',
207
- scopes: { user_id: 'u1' },
208
- cursor: -1,
209
- },
210
- ],
211
- },
212
- }),
213
- })
214
- );
215
-
216
- expect(pullResponse.status).toBe(200);
217
- const combined = SyncCombinedResponseSchema.parse(
218
- await pullResponse.json()
219
- );
220
- const parsed = combined.pull!;
221
- const chunkId = mustGetFirstChunkId(parsed);
222
- expect(storeChunkCalls).toBe(1);
223
- expect(externalChunkBodies.has(chunkId)).toBe(true);
224
-
225
- const storedExternal = externalChunkBodies.get(chunkId);
226
- if (!storedExternal) {
227
- throw new Error('Expected external chunk body to be stored.');
228
- }
229
-
230
- const rows = decodeSnapshotRows(gunzipSync(storedExternal));
231
-
232
- const snapshotChunkCountRow = await db
233
- .selectFrom('sync_snapshot_chunks')
234
- .select(({ fn }) => fn.countAll().as('count'))
235
- .executeTakeFirstOrThrow();
236
-
237
- expect(Number(snapshotChunkCountRow.count)).toBe(0);
238
- expect(rows).toEqual([
239
- { id: 't1', user_id: 'u1', title: 'Task 1', server_version: 1 },
240
- ]);
241
- }, 10_000);
242
-
243
- it('uses storeChunkStream when the adapter provides it', async () => {
244
- await db
245
- .insertInto('tasks')
246
- .values([
247
- { id: 't1', user_id: 'u1', title: 'Task 1', server_version: 1 },
248
- { id: 't2', user_id: 'u1', title: 'Task 2', server_version: 2 },
249
- ])
250
- .execute();
251
-
252
- const tasksHandler = createServerHandler<ServerDb, ClientDb, 'tasks'>({
253
- table: 'tasks',
254
- scopes: ['user:{user_id}'],
255
- resolveScopes: async (ctx) => ({ user_id: [ctx.actorId] }),
256
- });
257
-
258
- const externalChunkBodies = new Map<string, Uint8Array>();
259
- let storeChunkCalls = 0;
260
- let storeChunkStreamCalls = 0;
261
- const chunkStorage: SnapshotChunkStorage = {
262
- name: 'test-external-stream',
263
- async storeChunk(metadata) {
264
- storeChunkCalls += 1;
265
- const ref: SyncSnapshotChunkRef = {
266
- id: `chunk-${storeChunkCalls}`,
267
- sha256: metadata.sha256,
268
- byteLength: metadata.body.length,
269
- encoding: metadata.encoding,
270
- compression: metadata.compression,
271
- };
272
- externalChunkBodies.set(ref.id, new Uint8Array(metadata.body));
273
- return ref;
274
- },
275
- async storeChunkStream(metadata) {
276
- storeChunkStreamCalls += 1;
277
- const body = await streamToBytes(metadata.bodyStream);
278
- const ref: SyncSnapshotChunkRef = {
279
- id: `chunk-stream-${storeChunkStreamCalls}`,
280
- sha256: metadata.sha256,
281
- byteLength: body.length,
282
- encoding: metadata.encoding,
283
- compression: metadata.compression,
284
- };
285
- externalChunkBodies.set(ref.id, body);
286
- return ref;
287
- },
288
- async readChunk(chunkId: string) {
289
- const body = externalChunkBodies.get(chunkId);
290
- return body ? new Uint8Array(body) : null;
291
- },
292
- async findChunk() {
293
- return null;
294
- },
295
- async cleanupExpired() {
296
- return 0;
297
- },
298
- };
299
-
300
- const routes = createSyncRoutes({
301
- db,
302
- dialect,
303
- handlers: [tasksHandler],
304
- authenticate: async (c) => {
305
- const actorId = c.req.header('x-user-id');
306
- return actorId ? { actorId } : null;
307
- },
308
- chunkStorage,
309
- });
310
-
311
- const app = new Hono();
312
- app.route('/sync', routes);
313
-
314
- const pullResponse = await app.request(
315
- new Request('http://localhost/sync', {
316
- method: 'POST',
317
- headers: {
318
- 'content-type': 'application/json',
319
- 'x-user-id': 'u1',
320
- },
321
- body: JSON.stringify({
322
- clientId: 'client-1',
323
- pull: {
324
- limitCommits: 10,
325
- limitSnapshotRows: 1,
326
- maxSnapshotPages: 2,
327
- subscriptions: [
328
- {
329
- id: 'sub-1',
330
- table: 'tasks',
331
- scopes: { user_id: 'u1' },
332
- cursor: -1,
333
- },
334
- ],
335
- },
336
- }),
337
- })
338
- );
339
-
340
- expect(pullResponse.status).toBe(200);
341
- const combined = SyncCombinedResponseSchema.parse(
342
- await pullResponse.json()
343
- );
344
- const parsed = combined.pull!;
345
- const chunkId = mustGetFirstChunkId(parsed);
346
-
347
- expect(storeChunkStreamCalls).toBe(1);
348
- expect(storeChunkCalls).toBe(0);
349
- expect(externalChunkBodies.has(chunkId)).toBe(true);
350
-
351
- const storedExternal = externalChunkBodies.get(chunkId);
352
- if (!storedExternal) {
353
- throw new Error('Expected external chunk body to be stored.');
354
- }
355
-
356
- const rows = decodeSnapshotRows(gunzipSync(storedExternal));
357
- expect(rows).toEqual([
358
- { id: 't1', user_id: 'u1', title: 'Task 1', server_version: 1 },
359
- { id: 't2', user_id: 'u1', title: 'Task 2', server_version: 2 },
360
- ]);
361
- }, 10_000);
362
-
363
- it('bundles multiple snapshot pages into one stored chunk', async () => {
364
- await db
365
- .insertInto('tasks')
366
- .values([
367
- { id: 't1', user_id: 'u1', title: 'Task 1', server_version: 1 },
368
- { id: 't2', user_id: 'u1', title: 'Task 2', server_version: 2 },
369
- { id: 't3', user_id: 'u1', title: 'Task 3', server_version: 3 },
370
- ])
371
- .execute();
372
-
373
- const tasksHandler = createServerHandler<ServerDb, ClientDb, 'tasks'>({
374
- table: 'tasks',
375
- scopes: ['user:{user_id}'],
376
- resolveScopes: async (ctx) => ({ user_id: [ctx.actorId] }),
377
- });
378
-
379
- const externalChunkBodies = new Map<string, Uint8Array>();
380
- let storeChunkCalls = 0;
381
- const chunkStorage: SnapshotChunkStorage = {
382
- name: 'test-external',
383
- async storeChunk(metadata) {
384
- storeChunkCalls += 1;
385
- const ref: SyncSnapshotChunkRef = {
386
- id: `chunk-${storeChunkCalls}`,
387
- sha256: metadata.sha256,
388
- byteLength: metadata.body.length,
389
- encoding: metadata.encoding,
390
- compression: metadata.compression,
391
- };
392
- externalChunkBodies.set(ref.id, new Uint8Array(metadata.body));
393
- return ref;
394
- },
395
- async readChunk(chunkId: string) {
396
- const body = externalChunkBodies.get(chunkId);
397
- return body ? new Uint8Array(body) : null;
398
- },
399
- async findChunk() {
400
- return null;
401
- },
402
- async cleanupExpired() {
403
- return 0;
404
- },
405
- };
406
-
407
- const routes = createSyncRoutes({
408
- db,
409
- dialect,
410
- handlers: [tasksHandler],
411
- authenticate: async (c) => {
412
- const actorId = c.req.header('x-user-id');
413
- return actorId ? { actorId } : null;
414
- },
415
- chunkStorage,
416
- });
417
-
418
- const app = new Hono();
419
- app.route('/sync', routes);
420
-
421
- const pullResponse = await app.request(
422
- new Request('http://localhost/sync', {
423
- method: 'POST',
424
- headers: {
425
- 'content-type': 'application/json',
426
- 'x-user-id': 'u1',
427
- },
428
- body: JSON.stringify({
429
- clientId: 'client-1',
430
- pull: {
431
- limitCommits: 10,
432
- limitSnapshotRows: 1,
433
- maxSnapshotPages: 3,
434
- subscriptions: [
435
- {
436
- id: 'sub-1',
437
- table: 'tasks',
438
- scopes: { user_id: 'u1' },
439
- cursor: -1,
440
- },
441
- ],
442
- },
443
- }),
444
- })
445
- );
446
-
447
- expect(pullResponse.status).toBe(200);
448
- const combined = SyncCombinedResponseSchema.parse(
449
- await pullResponse.json()
450
- );
451
- const parsed = combined.pull!;
452
- const chunkId = mustGetFirstChunkId(parsed);
453
-
454
- expect(parsed.subscriptions[0]?.snapshots?.length).toBe(1);
455
- expect(storeChunkCalls).toBe(1);
456
-
457
- const storedExternal = externalChunkBodies.get(chunkId);
458
- if (!storedExternal) {
459
- throw new Error('Expected external chunk body to be stored.');
460
- }
461
-
462
- const rows = decodeSnapshotRows(gunzipSync(storedExternal));
463
-
464
- expect(rows).toEqual([
465
- { id: 't1', user_id: 'u1', title: 'Task 1', server_version: 1 },
466
- { id: 't2', user_id: 'u1', title: 'Task 2', server_version: 2 },
467
- { id: 't3', user_id: 'u1', title: 'Task 3', server_version: 3 },
468
- ]);
469
- }, 10_000);
470
-
471
- it('captures unhandled pull exceptions via telemetry', async () => {
472
- await db
473
- .insertInto('tasks')
474
- .values({
475
- id: 't1',
476
- user_id: 'u1',
477
- title: 'Task 1',
478
- server_version: 1,
479
- })
480
- .execute();
481
-
482
- const tasksHandler = createServerHandler<ServerDb, ClientDb, 'tasks'>({
483
- table: 'tasks',
484
- scopes: ['user:{user_id}'],
485
- resolveScopes: async (ctx) => ({ user_id: [ctx.actorId] }),
486
- });
487
-
488
- const chunkStorageError = new Error('chunk storage failed');
489
- const chunkStorage: SnapshotChunkStorage = {
490
- name: 'failing-external',
491
- async storeChunk() {
492
- throw chunkStorageError;
493
- },
494
- async storeChunkStream() {
495
- throw chunkStorageError;
496
- },
497
- async readChunk() {
498
- return null;
499
- },
500
- async findChunk() {
501
- return null;
502
- },
503
- async cleanupExpired() {
504
- return 0;
505
- },
506
- };
507
-
508
- const captured = {
509
- exceptions: [] as Array<{
510
- error: unknown;
511
- context: Record<string, unknown> | undefined;
512
- }>,
513
- };
514
- const previousTelemetry = getSyncTelemetry();
515
- configureSyncTelemetry(createExceptionCaptureTelemetry(captured));
516
-
517
- try {
518
- const routes = createSyncRoutes({
519
- db,
520
- dialect,
521
- handlers: [tasksHandler],
522
- authenticate: async (c) => {
523
- const actorId = c.req.header('x-user-id');
524
- return actorId ? { actorId } : null;
525
- },
526
- chunkStorage,
527
- });
528
-
529
- const app = new Hono();
530
- app.route('/sync', routes);
531
-
532
- const response = await app.request(
533
- new Request('http://localhost/sync', {
534
- method: 'POST',
535
- headers: {
536
- 'content-type': 'application/json',
537
- 'x-user-id': 'u1',
538
- },
539
- body: JSON.stringify({
540
- clientId: 'client-1',
541
- pull: {
542
- limitCommits: 10,
543
- limitSnapshotRows: 100,
544
- maxSnapshotPages: 1,
545
- subscriptions: [
546
- {
547
- id: 'sub-1',
548
- table: 'tasks',
549
- scopes: { user_id: 'u1' },
550
- cursor: -1,
551
- },
552
- ],
553
- },
554
- }),
555
- })
556
- );
557
- expect(response.status).toBe(500);
558
-
559
- const capturedUnhandledException = captured.exceptions.find(
560
- (entry) => entry.context?.event === 'sync.route.unhandled'
561
- );
562
- expect(capturedUnhandledException).toBeDefined();
563
- if (!capturedUnhandledException) {
564
- throw new Error('Expected unhandled exception telemetry entry');
565
- }
566
- expect(capturedUnhandledException.context).toEqual({
567
- event: 'sync.route.unhandled',
568
- method: 'POST',
569
- path: '/sync',
570
- });
571
- expect(capturedUnhandledException.error).toBe(chunkStorageError);
572
- } finally {
573
- configureSyncTelemetry(previousTelemetry);
574
- }
575
- });
576
- });
@@ -1,78 +0,0 @@
1
- import { afterEach, describe, expect, it } from 'bun:test';
2
- import { Hono } from 'hono';
3
- import { createRateLimiter, resetRateLimitStore } from '../rate-limit';
4
-
5
- afterEach(() => {
6
- resetRateLimitStore();
7
- });
8
-
9
- describe('rate limiter store isolation', () => {
10
- it('does not share counters across independently configured routes', async () => {
11
- const app = new Hono();
12
-
13
- app.use(
14
- '/pull',
15
- createRateLimiter({
16
- maxRequests: 1,
17
- windowMs: 60_000,
18
- keyGenerator: () => 'actor-1',
19
- })
20
- );
21
- app.use(
22
- '/push',
23
- createRateLimiter({
24
- maxRequests: 1,
25
- windowMs: 60_000,
26
- keyGenerator: () => 'actor-1',
27
- })
28
- );
29
-
30
- app.get('/pull', (c) => c.text('ok'));
31
- app.get('/push', (c) => c.text('ok'));
32
-
33
- const pullFirst = await app.request('http://localhost/pull');
34
- const pushFirst = await app.request('http://localhost/push');
35
- const pushSecond = await app.request('http://localhost/push');
36
-
37
- expect(pullFirst.status).toBe(200);
38
- expect(pushFirst.status).toBe(200);
39
- expect(pushSecond.status).toBe(429);
40
- });
41
-
42
- it('keeps window durations isolated per limiter', async () => {
43
- const app = new Hono();
44
-
45
- app.use(
46
- '/short',
47
- createRateLimiter({
48
- maxRequests: 1,
49
- windowMs: 10,
50
- keyGenerator: () => 'actor-1',
51
- })
52
- );
53
- app.use(
54
- '/long',
55
- createRateLimiter({
56
- maxRequests: 1,
57
- windowMs: 1_000,
58
- keyGenerator: () => 'actor-1',
59
- })
60
- );
61
-
62
- app.get('/short', (c) => c.text('ok'));
63
- app.get('/long', (c) => c.text('ok'));
64
-
65
- // Initialize the short-window limiter first.
66
- expect((await app.request('http://localhost/short')).status).toBe(200);
67
-
68
- // Exhaust long-window limiter.
69
- expect((await app.request('http://localhost/long')).status).toBe(200);
70
- expect((await app.request('http://localhost/long')).status).toBe(429);
71
-
72
- // Wait longer than short window but shorter than long window.
73
- await Bun.sleep(30);
74
-
75
- // Long limiter must still be limited.
76
- expect((await app.request('http://localhost/long')).status).toBe(429);
77
- });
78
- });