@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.
- package/README.md +23 -0
- package/dist/api-key-auth.js +1 -1
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js +31 -8
- package/dist/blobs.js.map +1 -1
- package/dist/console/index.d.ts +1 -1
- package/dist/console/index.d.ts.map +1 -1
- package/dist/console/index.js +1 -1
- package/dist/console/index.js.map +1 -1
- package/dist/console/routes.d.ts +1 -2
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +65 -2
- package/dist/console/routes.js.map +1 -1
- package/dist/console/schemas.d.ts +138 -496
- package/dist/console/schemas.d.ts.map +1 -1
- package/dist/console/schemas.js +3 -9
- package/dist/console/schemas.js.map +1 -1
- package/dist/create-server.d.ts +3 -1
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +4 -3
- package/dist/create-server.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -9
- package/dist/index.js.map +1 -1
- package/dist/proxy/connection-manager.d.ts +1 -1
- package/dist/proxy/connection-manager.d.ts.map +1 -1
- package/dist/proxy/connection-manager.js +1 -1
- package/dist/proxy/connection-manager.js.map +1 -1
- package/dist/proxy/index.js +2 -2
- package/dist/proxy/routes.d.ts +2 -2
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +3 -3
- package/dist/proxy/routes.js.map +1 -1
- package/dist/routes.d.ts +2 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +447 -260
- package/dist/routes.js.map +1 -1
- package/dist/ws.d.ts +40 -3
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +51 -6
- package/dist/ws.js.map +1 -1
- package/package.json +32 -9
- package/src/__tests__/pull-chunk-storage.test.ts +415 -27
- package/src/__tests__/realtime-bridge.test.ts +3 -1
- package/src/__tests__/sync-rate-limit-routing.test.ts +181 -0
- package/src/blobs.ts +31 -8
- package/src/console/index.ts +1 -0
- package/src/console/routes.ts +78 -25
- package/src/console/schemas.ts +0 -31
- package/src/create-server.ts +6 -0
- package/src/index.ts +12 -3
- package/src/proxy/connection-manager.ts +2 -2
- package/src/proxy/routes.ts +3 -3
- package/src/routes.ts +570 -327
- 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
|
|
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
|
-
|
|
407
|
-
hash,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
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);
|
package/src/console/index.ts
CHANGED
package/src/console/routes.ts
CHANGED
|
@@ -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
|
// -------------------------------------------------------------------------
|
package/src/console/schemas.ts
CHANGED
|
@@ -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
|
// ============================================================================
|
package/src/create-server.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
288
|
+
handlers: this.config.handlers,
|
|
289
289
|
ctx: {
|
|
290
290
|
actorId: state.actorId,
|
|
291
291
|
clientId: state.clientId,
|
package/src/proxy/routes.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
97
|
+
handlers: config.handlers,
|
|
98
98
|
maxConnections: config.maxConnections,
|
|
99
99
|
idleTimeoutMs: config.idleTimeoutMs,
|
|
100
100
|
});
|