@syncular/server-hono 0.0.1 → 0.0.2-127

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 (56) hide show
  1. package/README.md +23 -0
  2. package/dist/api-key-auth.js +1 -1
  3. package/dist/blobs.d.ts.map +1 -1
  4. package/dist/blobs.js +31 -8
  5. package/dist/blobs.js.map +1 -1
  6. package/dist/console/index.d.ts +1 -1
  7. package/dist/console/index.d.ts.map +1 -1
  8. package/dist/console/index.js +1 -1
  9. package/dist/console/index.js.map +1 -1
  10. package/dist/console/routes.d.ts +1 -2
  11. package/dist/console/routes.d.ts.map +1 -1
  12. package/dist/console/routes.js +65 -2
  13. package/dist/console/routes.js.map +1 -1
  14. package/dist/console/schemas.d.ts +138 -496
  15. package/dist/console/schemas.d.ts.map +1 -1
  16. package/dist/console/schemas.js +3 -9
  17. package/dist/console/schemas.js.map +1 -1
  18. package/dist/create-server.d.ts +3 -1
  19. package/dist/create-server.d.ts.map +1 -1
  20. package/dist/create-server.js +4 -3
  21. package/dist/create-server.js.map +1 -1
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -9
  25. package/dist/index.js.map +1 -1
  26. package/dist/proxy/connection-manager.d.ts +1 -1
  27. package/dist/proxy/connection-manager.d.ts.map +1 -1
  28. package/dist/proxy/connection-manager.js +1 -1
  29. package/dist/proxy/connection-manager.js.map +1 -1
  30. package/dist/proxy/index.js +2 -2
  31. package/dist/proxy/routes.d.ts +2 -2
  32. package/dist/proxy/routes.d.ts.map +1 -1
  33. package/dist/proxy/routes.js +3 -3
  34. package/dist/proxy/routes.js.map +1 -1
  35. package/dist/routes.d.ts +2 -2
  36. package/dist/routes.d.ts.map +1 -1
  37. package/dist/routes.js +447 -260
  38. package/dist/routes.js.map +1 -1
  39. package/dist/ws.d.ts +40 -3
  40. package/dist/ws.d.ts.map +1 -1
  41. package/dist/ws.js +51 -6
  42. package/dist/ws.js.map +1 -1
  43. package/package.json +32 -9
  44. package/src/__tests__/pull-chunk-storage.test.ts +415 -27
  45. package/src/__tests__/realtime-bridge.test.ts +3 -1
  46. package/src/__tests__/sync-rate-limit-routing.test.ts +181 -0
  47. package/src/blobs.ts +31 -8
  48. package/src/console/index.ts +1 -0
  49. package/src/console/routes.ts +78 -25
  50. package/src/console/schemas.ts +0 -31
  51. package/src/create-server.ts +6 -0
  52. package/src/index.ts +12 -3
  53. package/src/proxy/connection-manager.ts +2 -2
  54. package/src/proxy/routes.ts +3 -3
  55. package/src/routes.ts +570 -327
  56. package/src/ws.ts +76 -13
package/src/blobs.ts CHANGED
@@ -397,18 +397,22 @@ export function createBlobRoutes<DB extends SyncBlobsDb>(
397
397
  );
398
398
  }
399
399
 
400
- // Store in database
400
+ // Store via the blob adapter (R2, database, etc.)
401
401
  const mimeType =
402
402
  c.req.header('Content-Type') ??
403
403
  metadata?.mimeType ??
404
404
  'application/octet-stream';
405
405
 
406
- await storeBlobInDatabase(db, {
407
- hash,
408
- size: bodyBytes.length,
409
- mimeType,
410
- body: bodyBytes,
411
- });
406
+ if (blobManager.adapter.put) {
407
+ await blobManager.adapter.put(hash, bodyBytes, { mimeType });
408
+ } else {
409
+ await storeBlobInDatabase(db, {
410
+ hash,
411
+ size: bodyBytes.length,
412
+ mimeType,
413
+ body: bodyBytes,
414
+ });
415
+ }
412
416
 
413
417
  return c.text('OK', 200);
414
418
  }
@@ -459,7 +463,26 @@ export function createBlobRoutes<DB extends SyncBlobsDb>(
459
463
  return c.json({ error: 'INVALID_TOKEN' }, 401);
460
464
  }
461
465
 
462
- // Read from database
466
+ // Read via the blob adapter (R2, database, etc.)
467
+ if (blobManager.adapter.get) {
468
+ const data = await blobManager.adapter.get(hash);
469
+ if (!data) {
470
+ return c.json({ error: 'NOT_FOUND' }, 404);
471
+ }
472
+ const meta = blobManager.adapter.getMetadata
473
+ ? await blobManager.adapter.getMetadata(hash)
474
+ : null;
475
+ return new Response(data as BodyInit, {
476
+ status: 200,
477
+ headers: {
478
+ 'Content-Type': meta?.mimeType ?? 'application/octet-stream',
479
+ 'Content-Length': String(data.length),
480
+ 'Cache-Control': 'private, max-age=31536000, immutable',
481
+ },
482
+ });
483
+ }
484
+
485
+ // Fallback: read from database directly
463
486
  const blob = await readBlobFromDatabase(db, hash);
464
487
  if (!blob) {
465
488
  return c.json({ error: 'NOT_FOUND' }, 404);
@@ -8,6 +8,7 @@
8
8
  export type {
9
9
  ConsoleAuthResult,
10
10
  ConsoleEventEmitter,
11
+ CreateConsoleRoutesOptions,
11
12
  } from './routes';
12
13
  export {
13
14
  createConsoleEventEmitter,
@@ -24,6 +24,7 @@ import type {
24
24
  import {
25
25
  compactChanges,
26
26
  computePruneWatermarkCommitSeq,
27
+ notifyExternalDataChange,
27
28
  pruneSync,
28
29
  readSyncStats,
29
30
  } from '@syncular/server';
@@ -83,31 +84,6 @@ import {
83
84
  TimeseriesStatsResponseSchema,
84
85
  } from './schemas';
85
86
 
86
- // Re-export types for backwards compatibility
87
- export type {
88
- ApiKeyType,
89
- ConsoleApiKey,
90
- ConsoleChange,
91
- ConsoleClearEventsResult,
92
- ConsoleClient,
93
- ConsoleCommitDetail,
94
- ConsoleCommitListItem,
95
- ConsoleCompactResult,
96
- ConsoleEvictResult,
97
- ConsolePaginatedResponse,
98
- ConsolePruneEventsResult,
99
- ConsolePrunePreview,
100
- ConsolePruneResult,
101
- ConsoleRequestEvent,
102
- ConsoleHandler,
103
- LatencyPercentiles,
104
- LatencyStatsResponse,
105
- LiveEvent,
106
- SyncStats,
107
- TimeseriesBucket,
108
- TimeseriesStatsResponse,
109
- };
110
-
111
87
  export interface ConsoleAuthResult {
112
88
  /** Identifier for the console user (for audit logging). */
113
89
  consoleUserId?: string;
@@ -1169,6 +1145,83 @@ export function createConsoleRoutes<DB extends SyncCoreDb>(
1169
1145
  }
1170
1146
  );
1171
1147
 
1148
+ // -------------------------------------------------------------------------
1149
+ // POST /notify-data-change
1150
+ // -------------------------------------------------------------------------
1151
+
1152
+ const NotifyDataChangeRequestSchema = z.object({
1153
+ tables: z.array(z.string().min(1)).min(1),
1154
+ partitionId: z.string().optional(),
1155
+ });
1156
+
1157
+ const NotifyDataChangeResponseSchema = z.object({
1158
+ commitSeq: z.number(),
1159
+ tables: z.array(z.string()),
1160
+ deletedChunks: z.number(),
1161
+ });
1162
+
1163
+ routes.post(
1164
+ '/notify-data-change',
1165
+ describeRoute({
1166
+ tags: ['console'],
1167
+ summary: 'Notify external data change',
1168
+ description:
1169
+ 'Creates a synthetic commit to force re-bootstrap for affected tables. ' +
1170
+ 'Use after pipeline imports or direct DB writes to notify connected clients.',
1171
+ responses: {
1172
+ 200: {
1173
+ description: 'Notification result',
1174
+ content: {
1175
+ 'application/json': {
1176
+ schema: resolver(NotifyDataChangeResponseSchema),
1177
+ },
1178
+ },
1179
+ },
1180
+ 400: {
1181
+ description: 'Invalid request',
1182
+ content: {
1183
+ 'application/json': { schema: resolver(ErrorResponseSchema) },
1184
+ },
1185
+ },
1186
+ 401: {
1187
+ description: 'Unauthenticated',
1188
+ content: {
1189
+ 'application/json': { schema: resolver(ErrorResponseSchema) },
1190
+ },
1191
+ },
1192
+ },
1193
+ }),
1194
+ zValidator('json', NotifyDataChangeRequestSchema),
1195
+ async (c) => {
1196
+ const auth = await requireAuth(c);
1197
+ if (!auth) return c.json({ error: 'UNAUTHENTICATED' }, 401);
1198
+
1199
+ const body = c.req.valid('json');
1200
+
1201
+ const result = await notifyExternalDataChange({
1202
+ db: options.db,
1203
+ dialect: options.dialect,
1204
+ tables: body.tables,
1205
+ partitionId: body.partitionId,
1206
+ });
1207
+
1208
+ logSyncEvent({
1209
+ event: 'console.notify_data_change',
1210
+ consoleUserId: auth.consoleUserId,
1211
+ tables: body.tables,
1212
+ commitSeq: result.commitSeq,
1213
+ deletedChunks: result.deletedChunks,
1214
+ });
1215
+
1216
+ // Wake all WS clients so they pull immediately
1217
+ if (options.wsConnectionManager) {
1218
+ options.wsConnectionManager.notifyAllClients(result.commitSeq);
1219
+ }
1220
+
1221
+ return c.json(result, 200);
1222
+ }
1223
+ );
1224
+
1172
1225
  // -------------------------------------------------------------------------
1173
1226
  // DELETE /clients/:id
1174
1227
  // -------------------------------------------------------------------------
@@ -146,17 +146,6 @@ export const ConsoleRequestEventSchema = z.object({
146
146
 
147
147
  export type ConsoleRequestEvent = z.infer<typeof ConsoleRequestEventSchema>;
148
148
 
149
- const ConsoleRequestEventFiltersSchema = z.object({
150
- eventType: z.enum(['push', 'pull']).optional(),
151
- actorId: z.string().optional(),
152
- clientId: z.string().optional(),
153
- outcome: z.string().optional(),
154
- });
155
-
156
- export type ConsoleRequestEventFilters = z.infer<
157
- typeof ConsoleRequestEventFiltersSchema
158
- >;
159
-
160
149
  export const ConsoleClearEventsResultSchema = z.object({
161
150
  deletedCount: z.number().int(),
162
151
  });
@@ -203,10 +192,6 @@ export const ConsoleApiKeyCreateRequestSchema = z.object({
203
192
  expiresInDays: z.number().int().positive().optional(),
204
193
  });
205
194
 
206
- export type ConsoleApiKeyCreateRequest = z.infer<
207
- typeof ConsoleApiKeyCreateRequestSchema
208
- >;
209
-
210
195
  export const ConsoleApiKeyCreateResponseSchema = z.object({
211
196
  key: ConsoleApiKeySchema,
212
197
  secretKey: z.string(),
@@ -220,10 +205,6 @@ export const ConsoleApiKeyRevokeResponseSchema = z.object({
220
205
  revoked: z.boolean(),
221
206
  });
222
207
 
223
- export type ConsoleApiKeyRevokeResponse = z.infer<
224
- typeof ConsoleApiKeyRevokeResponseSchema
225
- >;
226
-
227
208
  // ============================================================================
228
209
  // Pagination Schemas (Console-specific)
229
210
  // ============================================================================
@@ -233,10 +214,6 @@ export const ConsolePaginationQuerySchema = z.object({
233
214
  offset: z.coerce.number().int().min(0).default(0),
234
215
  });
235
216
 
236
- export type ConsolePaginationQuery = z.infer<
237
- typeof ConsolePaginationQuerySchema
238
- >;
239
-
240
217
  export const ConsolePaginatedResponseSchema = <T extends z.ZodTypeAny>(
241
218
  itemSchema: T
242
219
  ) =>
@@ -259,18 +236,12 @@ export type ConsolePaginatedResponse<T> = {
259
236
  // ============================================================================
260
237
 
261
238
  const TimeseriesIntervalSchema = z.enum(['minute', 'hour', 'day']);
262
- export type TimeseriesInterval = z.infer<typeof TimeseriesIntervalSchema>;
263
-
264
239
  const TimeseriesRangeSchema = z.enum(['1h', '6h', '24h', '7d', '30d']);
265
- export type TimeseriesRange = z.infer<typeof TimeseriesRangeSchema>;
266
-
267
240
  export const TimeseriesQuerySchema = z.object({
268
241
  interval: TimeseriesIntervalSchema.default('hour'),
269
242
  range: TimeseriesRangeSchema.default('24h'),
270
243
  });
271
244
 
272
- export type TimeseriesQuery = z.infer<typeof TimeseriesQuerySchema>;
273
-
274
245
  export const TimeseriesBucketSchema = z.object({
275
246
  timestamp: z.string(),
276
247
  pushCount: z.number().int(),
@@ -315,8 +286,6 @@ export const LatencyQuerySchema = z.object({
315
286
  range: TimeseriesRangeSchema.default('24h'),
316
287
  });
317
288
 
318
- export type LatencyQuery = z.infer<typeof LatencyQuerySchema>;
319
-
320
289
  // ============================================================================
321
290
  // Live Events Schemas (for WebSocket)
322
291
  // ============================================================================
@@ -9,6 +9,7 @@
9
9
  import type {
10
10
  ServerSyncDialect,
11
11
  ServerTableHandler,
12
+ SnapshotChunkStorage,
12
13
  SyncCoreDb,
13
14
  } from '@syncular/server';
14
15
  import type { Context } from 'hono';
@@ -42,6 +43,9 @@ export interface SyncServerOptions<DB extends SyncCoreDb = SyncCoreDb> {
42
43
  /** Authentication function - returns actorId or null for unauthenticated */
43
44
  authenticate: (c: Context) => Promise<SyncAuthResult | null>;
44
45
 
46
+ /** Snapshot chunk storage (external body storage, e.g. R2/S3) */
47
+ chunkStorage?: SnapshotChunkStorage;
48
+
45
49
  /** Sync route configuration */
46
50
  sync?: SyncRoutesConfigWithRateLimit;
47
51
 
@@ -108,6 +112,7 @@ export function createSyncServer<DB extends SyncCoreDb = SyncCoreDb>(
108
112
  dialect,
109
113
  handlers,
110
114
  authenticate,
115
+ chunkStorage,
111
116
  sync,
112
117
  upgradeWebSocket,
113
118
  console: consoleConfig,
@@ -123,6 +128,7 @@ export function createSyncServer<DB extends SyncCoreDb = SyncCoreDb>(
123
128
  dialect,
124
129
  handlers,
125
130
  authenticate,
131
+ chunkStorage,
126
132
  sync: {
127
133
  ...sync,
128
134
  websocket: upgradeWebSocket
package/src/index.ts CHANGED
@@ -9,13 +9,17 @@
9
9
  export * from './api-key-auth';
10
10
 
11
11
  // Blob routes
12
- export { createBlobRoutes } from './blobs';
12
+ export { type CreateBlobRoutesOptions, createBlobRoutes } from './blobs';
13
13
 
14
14
  // Console
15
15
  export * from './console';
16
16
 
17
17
  // Simplified server factory
18
- export { createSyncServer } from './create-server';
18
+ export {
19
+ createSyncServer,
20
+ type SyncServerOptions,
21
+ type SyncServerResult,
22
+ } from './create-server';
19
23
 
20
24
  // OpenAPI utilities
21
25
  export * from './openapi';
@@ -27,7 +31,12 @@ export * from './proxy';
27
31
  export * from './rate-limit';
28
32
 
29
33
  // Route types and factory
30
- export { createSyncRoutes } from './routes';
34
+ export {
35
+ type CreateSyncRoutesOptions,
36
+ createSyncRoutes,
37
+ getSyncRealtimeUnsubscribe,
38
+ getSyncWebSocketConnectionManager,
39
+ } from './routes';
31
40
 
32
41
  // WebSocket helpers for realtime sync
33
42
  export * from './ws';
@@ -27,7 +27,7 @@ export interface ProxyConnectionManagerConfig<
27
27
  /** Server sync dialect */
28
28
  dialect: ServerSyncDialect;
29
29
  /** Proxy table registry for oplog generation */
30
- shapes: ProxyTableRegistry;
30
+ handlers: ProxyTableRegistry;
31
31
  /** Maximum concurrent connections (default: 100) */
32
32
  maxConnections?: number;
33
33
  /** Idle connection timeout in ms (default: 30000) */
@@ -285,7 +285,7 @@ export class ProxyConnectionManager<DB extends SyncCoreDb = SyncCoreDb> {
285
285
  const result: ExecuteProxyQueryResult = await executeProxyQuery({
286
286
  db,
287
287
  dialect: this.config.dialect,
288
- shapes: this.config.shapes,
288
+ handlers: this.config.handlers,
289
289
  ctx: {
290
290
  actorId: state.actorId,
291
291
  clientId: state.clientId,
@@ -49,7 +49,7 @@ interface CreateProxyRoutesConfig<DB extends SyncCoreDb = SyncCoreDb> {
49
49
  /** Server sync dialect */
50
50
  dialect: ServerSyncDialect;
51
51
  /** Proxy table registry for oplog generation */
52
- shapes: ProxyTableRegistry;
52
+ handlers: ProxyTableRegistry;
53
53
  /** Authenticate the request and return actor info */
54
54
  authenticate: (c: Context) => Promise<ProxyAuthResult | null>;
55
55
  /** WebSocket upgrade function from Hono */
@@ -75,7 +75,7 @@ interface CreateProxyRoutesConfig<DB extends SyncCoreDb = SyncCoreDb> {
75
75
  *
76
76
  * app.route('/proxy', createProxyRoutes({
77
77
  * db,
78
- * shapes: proxyTableRegistry,
78
+ * handlers: proxyTableRegistry,
79
79
  * authenticate: async (c) => {
80
80
  * // Verify admin auth
81
81
  * return { actorId: 'admin:123' };
@@ -94,7 +94,7 @@ export function createProxyRoutes<DB extends SyncCoreDb>(
94
94
  const manager = new ProxyConnectionManager({
95
95
  db: config.db,
96
96
  dialect: config.dialect,
97
- shapes: config.shapes,
97
+ handlers: config.handlers,
98
98
  maxConnections: config.maxConnections,
99
99
  idleTimeoutMs: config.idleTimeoutMs,
100
100
  });