@syncular/server-hono 0.0.6-96 → 0.2.1

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,114 +0,0 @@
1
- import { afterEach, describe, expect, it } from 'bun:test';
2
- import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
- import path from 'node:path';
5
- import {
6
- CONSOLE_BASEPATH_META,
7
- CONSOLE_SERVER_URL_META,
8
- CONSOLE_TOKEN_META,
9
- } from '@syncular/console/runtime-config';
10
- import { Hono } from 'hono';
11
- import { mountConsoleUi } from '../console/ui';
12
-
13
- const tempDirs: string[] = [];
14
-
15
- async function createStaticFixture(): Promise<string> {
16
- const staticDir = await mkdtemp(path.join(tmpdir(), 'syncular-console-ui-'));
17
- tempDirs.push(staticDir);
18
-
19
- await mkdir(path.join(staticDir, 'assets'), { recursive: true });
20
- await writeFile(
21
- path.join(staticDir, 'index.html'),
22
- `<!doctype html>
23
- <html>
24
- <head>
25
- <meta name="${CONSOLE_BASEPATH_META}" content="" />
26
- <meta name="${CONSOLE_SERVER_URL_META}" content="" />
27
- <meta name="${CONSOLE_TOKEN_META}" content="" />
28
- <link rel="stylesheet" href="/assets/console.css" />
29
- </head>
30
- <body>
31
- <script type="module" src="/assets/main.js"></script>
32
- </body>
33
- </html>`
34
- );
35
- await writeFile(
36
- path.join(staticDir, 'assets', 'main.js'),
37
- 'console.log(1);\n'
38
- );
39
- await writeFile(path.join(staticDir, 'assets', 'console.css'), 'body{}\n');
40
-
41
- return staticDir;
42
- }
43
-
44
- afterEach(async () => {
45
- await Promise.all(
46
- tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))
47
- );
48
- });
49
-
50
- describe('mountConsoleUi', () => {
51
- it('serves console UI with default prefill derived from mount/api paths', async () => {
52
- const staticDir = await createStaticFixture();
53
- const app = new Hono();
54
-
55
- mountConsoleUi(app, {
56
- mountPath: 'console',
57
- apiBasePath: 'api',
58
- staticDir,
59
- });
60
-
61
- const response = await app.request('http://example.test/console');
62
- const html = await response.text();
63
-
64
- expect(response.status).toBe(200);
65
- expect(html).toContain(
66
- `<meta name="${CONSOLE_BASEPATH_META}" content="/console" />`
67
- );
68
- expect(html).toContain(
69
- `<meta name="${CONSOLE_SERVER_URL_META}" content="http://example.test/api" />`
70
- );
71
- expect(html).toContain(`<meta name="${CONSOLE_TOKEN_META}" content="" />`);
72
- expect(html).toContain('href="/console/assets/console.css"');
73
- expect(html).toContain('src="/console/assets/main.js"');
74
-
75
- const asset = await app.request(
76
- 'http://example.test/console/assets/main.js'
77
- );
78
- expect(asset.status).toBe(200);
79
- expect(await asset.text()).toContain('console.log(1);');
80
- });
81
-
82
- it('merges resolvePrefill and allows resolveToken to override token', async () => {
83
- const staticDir = await createStaticFixture();
84
- const app = new Hono();
85
-
86
- mountConsoleUi(app, {
87
- mountPath: '/ops-console',
88
- apiBasePath: '/api/internal',
89
- staticDir,
90
- resolvePrefill: async () => ({
91
- basePath: '/prefilled',
92
- serverUrl: 'https://prefill.example/api',
93
- token: 'prefill-token',
94
- }),
95
- resolveToken: async () => 'resolver-token',
96
- });
97
-
98
- const response = await app.request('http://localhost/ops-console');
99
- const html = await response.text();
100
-
101
- expect(response.status).toBe(200);
102
- expect(html).toContain(
103
- `<meta name="${CONSOLE_BASEPATH_META}" content="/prefilled" />`
104
- );
105
- expect(html).toContain(
106
- `<meta name="${CONSOLE_SERVER_URL_META}" content="https://prefill.example/api" />`
107
- );
108
- expect(html).toContain(
109
- `<meta name="${CONSOLE_TOKEN_META}" content="resolver-token" />`
110
- );
111
- expect(html).toContain('href="/prefilled/assets/console.css"');
112
- expect(html).toContain('src="/prefilled/assets/main.js"');
113
- });
114
- });
@@ -1,485 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
- import { createDatabase } from '@syncular/core';
3
- import { createPgliteDialect } from '@syncular/dialect-pglite';
4
- import {
5
- createServerHandler,
6
- ensureSyncSchema,
7
- type SyncCoreDb,
8
- } from '@syncular/server';
9
- import { createPostgresServerDialect } from '@syncular/server-dialect-postgres';
10
- import { Hono } from 'hono';
11
- import { defineWebSocketHelper } from 'hono/ws';
12
- import { type Kysely, sql } from 'kysely';
13
- import { createSyncServer } from '../create-server';
14
- import { getSyncWebSocketConnectionManager } from '../routes';
15
- import type { WebSocketConnection } from '../ws';
16
-
17
- interface TasksTable {
18
- id: string;
19
- user_id: string;
20
- title: string;
21
- server_version: number;
22
- }
23
-
24
- interface ServerDb extends SyncCoreDb {
25
- tasks: TasksTable;
26
- }
27
-
28
- interface ClientDb {
29
- tasks: TasksTable;
30
- }
31
-
32
- describe('createSyncServer console configuration', () => {
33
- let db: Kysely<ServerDb>;
34
- let previousConsoleToken: string | undefined;
35
-
36
- beforeEach(async () => {
37
- db = createDatabase<ServerDb>({
38
- dialect: createPgliteDialect(),
39
- family: 'postgres',
40
- });
41
- await ensureSyncSchema(db, createPostgresServerDialect());
42
- await db.schema
43
- .createTable('tasks')
44
- .ifNotExists()
45
- .addColumn('id', 'text', (col) => col.primaryKey())
46
- .addColumn('user_id', 'text', (col) => col.notNull())
47
- .addColumn('title', 'text', (col) => col.notNull())
48
- .addColumn('server_version', 'integer', (col) =>
49
- col.notNull().defaultTo(0)
50
- )
51
- .execute();
52
- previousConsoleToken = process.env.SYNC_CONSOLE_TOKEN;
53
- delete process.env.SYNC_CONSOLE_TOKEN;
54
- });
55
-
56
- afterEach(async () => {
57
- if (previousConsoleToken === undefined) {
58
- delete process.env.SYNC_CONSOLE_TOKEN;
59
- } else {
60
- process.env.SYNC_CONSOLE_TOKEN = previousConsoleToken;
61
- }
62
- await db.destroy();
63
- });
64
-
65
- function createTestHandler() {
66
- return createServerHandler<ServerDb, ClientDb, 'tasks'>({
67
- table: 'tasks',
68
- scopes: ['user:{user_id}'],
69
- resolveScopes: async (ctx) => ({ user_id: [ctx.actorId] }),
70
- });
71
- }
72
-
73
- function createOptions() {
74
- return {
75
- db,
76
- dialect: createPostgresServerDialect(),
77
- sync: {
78
- handlers: [createTestHandler()],
79
- authenticate: async () => ({ actorId: 'u1' }),
80
- },
81
- };
82
- }
83
-
84
- function createConn(args: {
85
- actorId: string;
86
- clientId: string;
87
- }): WebSocketConnection {
88
- return {
89
- actorId: args.actorId,
90
- clientId: args.clientId,
91
- transportPath: 'direct',
92
- get isOpen() {
93
- return true;
94
- },
95
- sendSync() {},
96
- sendHeartbeat() {},
97
- sendPresence() {},
98
- sendError() {},
99
- close() {},
100
- };
101
- }
102
-
103
- function createPushRequest(args?: {
104
- requestId?: string;
105
- title?: string;
106
- }): Request {
107
- return new Request('http://localhost/sync', {
108
- method: 'POST',
109
- headers: {
110
- 'content-type': 'application/json',
111
- ...(args?.requestId ? { 'x-request-id': args.requestId } : {}),
112
- },
113
- body: JSON.stringify({
114
- clientId: 'client-1',
115
- push: {
116
- clientCommitId: 'commit-1',
117
- schemaVersion: 1,
118
- operations: [
119
- {
120
- table: 'tasks',
121
- row_id: 'task-1',
122
- op: 'upsert',
123
- payload: {
124
- id: 'task-1',
125
- user_id: 'u1',
126
- title: args?.title ?? 'Task 1',
127
- server_version: 0,
128
- },
129
- },
130
- ],
131
- },
132
- }),
133
- });
134
- }
135
-
136
- function parseSnapshotValue(value: unknown): unknown {
137
- if (typeof value !== 'string') {
138
- return value;
139
- }
140
- try {
141
- return JSON.parse(value);
142
- } catch {
143
- return value;
144
- }
145
- }
146
-
147
- async function waitForRequestEventRow(requestId: string): Promise<{
148
- payload_ref: string | null;
149
- }> {
150
- for (let attempt = 0; attempt < 40; attempt++) {
151
- const result = await sql<{ payload_ref: string | null }>`
152
- SELECT payload_ref
153
- FROM sync_request_events
154
- WHERE request_id = ${requestId}
155
- ORDER BY event_id DESC
156
- LIMIT 1
157
- `.execute(db);
158
-
159
- const row = result.rows[0];
160
- if (row) {
161
- return row;
162
- }
163
-
164
- await new Promise((resolve) => setTimeout(resolve, 25));
165
- }
166
-
167
- throw new Error(`Timed out waiting for request event: ${requestId}`);
168
- }
169
-
170
- async function waitForRequestPayloadSnapshot(
171
- payloadRef: string
172
- ): Promise<unknown> {
173
- for (let attempt = 0; attempt < 40; attempt++) {
174
- const result = await sql<{ request_payload: unknown | null }>`
175
- SELECT request_payload
176
- FROM sync_request_payloads
177
- WHERE payload_ref = ${payloadRef}
178
- LIMIT 1
179
- `.execute(db);
180
-
181
- const row = result.rows[0];
182
- if (row && row.request_payload !== null) {
183
- return parseSnapshotValue(row.request_payload);
184
- }
185
-
186
- await new Promise((resolve) => setTimeout(resolve, 25));
187
- }
188
-
189
- throw new Error(`Timed out waiting for payload snapshot: ${payloadRef}`);
190
- }
191
-
192
- it('keeps console routes disabled when console config is omitted', () => {
193
- const server = createSyncServer(createOptions());
194
- expect(server.consoleRoutes).toBeUndefined();
195
- });
196
-
197
- it('throws when console is enabled without a token', () => {
198
- const options = createOptions();
199
- expect(() =>
200
- createSyncServer({
201
- ...options,
202
- console: {},
203
- })
204
- ).toThrow(
205
- 'Console is enabled but no token is configured. Set `console.token` or SYNC_CONSOLE_TOKEN.'
206
- );
207
- });
208
-
209
- it('accepts SYNC_CONSOLE_TOKEN when console token is omitted', () => {
210
- process.env.SYNC_CONSOLE_TOKEN = 'env-token';
211
- const options = createOptions();
212
- const server = createSyncServer({
213
- ...options,
214
- console: {},
215
- });
216
- expect(server.consoleRoutes).toBeDefined();
217
- });
218
-
219
- it('accepts an explicit console token', () => {
220
- process.env.SYNC_CONSOLE_TOKEN = 'env-token';
221
- const options = createOptions();
222
- const server = createSyncServer({
223
- ...options,
224
- console: { token: 'explicit-token' },
225
- });
226
- expect(server.consoleRoutes).toBeDefined();
227
- });
228
-
229
- it('returns not implemented when blobBucket is not configured', async () => {
230
- const options = createOptions();
231
- const server = createSyncServer({
232
- ...options,
233
- console: {
234
- token: 'console-token',
235
- },
236
- });
237
-
238
- const app = new Hono();
239
- app.route('/console', server.consoleRoutes!);
240
-
241
- const response = await app.request('http://localhost/console/storage', {
242
- headers: { Authorization: 'Bearer console-token' },
243
- });
244
-
245
- expect(response.status).toBe(501);
246
- expect(await response.json()).toEqual({
247
- error: 'BLOB_STORAGE_NOT_CONFIGURED',
248
- });
249
- });
250
-
251
- it('enables storage console routes when blobBucket is configured', async () => {
252
- const options = createOptions();
253
- const server = createSyncServer({
254
- ...options,
255
- console: {
256
- token: 'blob-token',
257
- blobBucket: {
258
- list: async () => ({
259
- objects: [
260
- {
261
- key: 'hello.txt',
262
- size: 12,
263
- uploaded: new Date('2025-01-01T00:00:00.000Z'),
264
- httpMetadata: { contentType: 'text/plain' },
265
- },
266
- ],
267
- truncated: false,
268
- cursor: undefined,
269
- }),
270
- get: async () => null,
271
- delete: async () => {},
272
- head: async () => null,
273
- },
274
- },
275
- });
276
-
277
- const app = new Hono();
278
- app.route('/console', server.consoleRoutes!);
279
-
280
- const unauthenticated = await app.request(
281
- 'http://localhost/console/storage'
282
- );
283
- expect(unauthenticated.status).toBe(401);
284
-
285
- const response = await app.request('http://localhost/console/storage', {
286
- headers: { Authorization: 'Bearer blob-token' },
287
- });
288
- expect(response.status).toBe(200);
289
- expect(await response.json()).toEqual({
290
- items: [
291
- {
292
- key: 'hello.txt',
293
- size: 12,
294
- uploaded: '2025-01-01T00:00:00.000Z',
295
- httpMetadata: { contentType: 'text/plain' },
296
- },
297
- ],
298
- truncated: false,
299
- cursor: null,
300
- });
301
- });
302
-
303
- it('forwards maxConnectionsPerClient from factory to realtime route', async () => {
304
- const options = createOptions();
305
- const upgradeWebSocket = defineWebSocketHelper(async () => {});
306
-
307
- const server = createSyncServer({
308
- ...options,
309
- upgradeWebSocket,
310
- routes: {
311
- websocket: {
312
- maxConnectionsPerClient: 1,
313
- },
314
- },
315
- });
316
-
317
- const manager = getSyncWebSocketConnectionManager(server.syncRoutes);
318
- if (!manager) {
319
- throw new Error('Expected websocket manager to be enabled.');
320
- }
321
- manager.register(createConn({ actorId: 'u1', clientId: 'client-1' }), []);
322
-
323
- const app = new Hono();
324
- app.route('/sync', server.syncRoutes);
325
-
326
- const response = await app.request(
327
- 'http://localhost/sync/realtime?clientId=client-1'
328
- );
329
- expect(response.status).toBe(429);
330
- expect(await response.json()).toEqual({
331
- error: 'WEBSOCKET_CONNECTION_LIMIT_CLIENT',
332
- });
333
- });
334
-
335
- it('allows disabling request payload snapshots for privacy-sensitive deployments', async () => {
336
- process.env.SYNC_CONSOLE_TOKEN = 'env-token';
337
- const options = createOptions();
338
- const server = createSyncServer({
339
- ...options,
340
- console: {},
341
- routes: {
342
- requestPayloadSnapshots: {
343
- enabled: false,
344
- },
345
- },
346
- });
347
-
348
- const app = new Hono();
349
- app.route('/sync', server.syncRoutes);
350
-
351
- const requestId = 'req-no-payload-snapshot';
352
- const response = await app.request(createPushRequest({ requestId }));
353
- expect(response.status).toBe(200);
354
-
355
- const eventRow = await waitForRequestEventRow(requestId);
356
- expect(eventRow.payload_ref).toBeNull();
357
-
358
- const payloadCountResult = await sql<{ total: number | string }>`
359
- SELECT COUNT(*)::int AS total
360
- FROM sync_request_payloads
361
- `.execute(db);
362
- const payloadCount = Number(payloadCountResult.rows[0]?.total ?? 0);
363
- expect(payloadCount).toBe(0);
364
- });
365
-
366
- it('supports aggressively reducing stored payload snapshot size', async () => {
367
- process.env.SYNC_CONSOLE_TOKEN = 'env-token';
368
- const options = createOptions();
369
- const server = createSyncServer({
370
- ...options,
371
- console: {},
372
- routes: {
373
- requestPayloadSnapshots: {
374
- maxBytes: 32,
375
- },
376
- },
377
- });
378
-
379
- const app = new Hono();
380
- app.route('/sync', server.syncRoutes);
381
-
382
- const requestId = 'req-small-payload-preview';
383
- const response = await app.request(
384
- createPushRequest({
385
- requestId,
386
- title: 'x'.repeat(1024),
387
- })
388
- );
389
- expect(response.status).toBe(200);
390
-
391
- const eventRow = await waitForRequestEventRow(requestId);
392
- expect(typeof eventRow.payload_ref).toBe('string');
393
- if (!eventRow.payload_ref) {
394
- throw new Error('Expected payload_ref to be present.');
395
- }
396
-
397
- const storedPayload = await waitForRequestPayloadSnapshot(
398
- eventRow.payload_ref
399
- );
400
- expect(typeof storedPayload).toBe('object');
401
- expect(Array.isArray(storedPayload)).toBe(false);
402
- if (!storedPayload || typeof storedPayload !== 'object') {
403
- throw new Error('Expected stored payload snapshot to be an object.');
404
- }
405
-
406
- const truncated = Reflect.get(storedPayload, 'truncated');
407
- const preview = Reflect.get(storedPayload, 'preview');
408
-
409
- expect(truncated).toBe(true);
410
- expect(typeof preview).toBe('string');
411
- if (typeof preview === 'string') {
412
- expect(preview.length).toBeLessThanOrEqual(32);
413
- }
414
- });
415
-
416
- it('forwards maxConnectionsTotal from factory to realtime route', async () => {
417
- const options = createOptions();
418
- const upgradeWebSocket = defineWebSocketHelper(async () => {});
419
-
420
- const server = createSyncServer({
421
- ...options,
422
- upgradeWebSocket,
423
- routes: {
424
- websocket: {
425
- maxConnectionsTotal: 1,
426
- },
427
- },
428
- });
429
-
430
- const manager = getSyncWebSocketConnectionManager(server.syncRoutes);
431
- if (!manager) {
432
- throw new Error('Expected websocket manager to be enabled.');
433
- }
434
- manager.register(createConn({ actorId: 'u1', clientId: 'client-1' }), []);
435
-
436
- const app = new Hono();
437
- app.route('/sync', server.syncRoutes);
438
-
439
- const response = await app.request(
440
- 'http://localhost/sync/realtime?clientId=client-2'
441
- );
442
- expect(response.status).toBe(429);
443
- expect(await response.json()).toEqual({
444
- error: 'WEBSOCKET_CONNECTION_LIMIT_TOTAL',
445
- });
446
- });
447
-
448
- it('emits live console events from sync lifecycle when console is enabled', async () => {
449
- process.env.SYNC_CONSOLE_TOKEN = 'env-token';
450
- const options = createOptions();
451
- const server = createSyncServer({
452
- ...options,
453
- console: {},
454
- });
455
-
456
- const liveEvents: Array<{
457
- type: string;
458
- data: Record<string, unknown>;
459
- }> = [];
460
- const listener = (event: {
461
- type: 'push' | 'pull' | 'commit' | 'client_update';
462
- timestamp: string;
463
- data: Record<string, unknown>;
464
- }) => {
465
- liveEvents.push({ type: event.type, data: event.data });
466
- };
467
-
468
- server.consoleEventEmitter?.addListener(listener);
469
- const app = new Hono();
470
- app.route('/sync', server.syncRoutes);
471
-
472
- const response = await app.request(createPushRequest());
473
- expect(response.status).toBe(200);
474
-
475
- const emittedTypes = liveEvents.map((event) => event.type);
476
- expect(emittedTypes).toContain('push');
477
- expect(emittedTypes).toContain('commit');
478
-
479
- const pushEvent = liveEvents.find((event) => event.type === 'push');
480
- expect(pushEvent?.data.actorId).toBe('u1');
481
- expect(pushEvent?.data.clientId).toBe('client-1');
482
-
483
- server.consoleEventEmitter?.removeListener(listener);
484
- });
485
- });