@syncular/server-hono 0.0.4-26 → 0.0.6-101
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/dist/console/gateway.d.ts +3 -1
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +218 -41
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/index.d.ts +1 -0
- package/dist/console/index.d.ts.map +1 -1
- package/dist/console/index.js +1 -0
- package/dist/console/index.js.map +1 -1
- package/dist/console/routes.d.ts +3 -97
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +507 -80
- package/dist/console/routes.js.map +1 -1
- package/dist/console/schemas.d.ts +29 -0
- package/dist/console/schemas.d.ts.map +1 -1
- package/dist/console/schemas.js +22 -0
- package/dist/console/schemas.js.map +1 -1
- package/dist/console/types.d.ts +175 -0
- package/dist/console/types.d.ts.map +1 -0
- package/dist/console/types.js +2 -0
- package/dist/console/types.js.map +1 -0
- package/dist/create-server.d.ts +17 -34
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +26 -26
- package/dist/create-server.js.map +1 -1
- package/dist/proxy/connection-manager.d.ts +3 -3
- package/dist/proxy/connection-manager.d.ts.map +1 -1
- package/dist/proxy/routes.d.ts +4 -4
- package/dist/proxy/routes.d.ts.map +1 -1
- package/dist/proxy/routes.js +1 -1
- package/dist/routes.d.ts +33 -9
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +153 -70
- package/dist/routes.js.map +1 -1
- package/package.json +21 -7
- package/src/__tests__/blob-routes.test.ts +424 -0
- package/src/__tests__/console-gateway-live-routes.test.ts +54 -3
- package/src/__tests__/console-routes.test.ts +161 -7
- package/src/__tests__/console-ui.test.ts +114 -0
- package/src/__tests__/create-server.test.ts +233 -10
- package/src/__tests__/pull-chunk-storage.test.ts +6 -2
- package/src/__tests__/realtime-bridge.test.ts +6 -2
- package/src/__tests__/sync-rate-limit-routing.test.ts +6 -2
- package/src/console/gateway.ts +277 -53
- package/src/console/index.ts +1 -0
- package/src/console/routes.ts +654 -198
- package/src/console/schemas.ts +29 -0
- package/src/console/types.ts +185 -0
- package/src/create-server.ts +56 -53
- package/src/proxy/connection-manager.ts +3 -3
- package/src/proxy/routes.ts +4 -4
- package/src/routes.ts +225 -96
package/src/routes.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
captureSyncException,
|
|
12
|
+
countSyncMetric,
|
|
12
13
|
createSyncTimer,
|
|
14
|
+
distributionSyncMetric,
|
|
13
15
|
ErrorResponseSchema,
|
|
14
16
|
logSyncEvent,
|
|
15
17
|
SyncCombinedRequestSchema,
|
|
@@ -17,15 +19,19 @@ import {
|
|
|
17
19
|
SyncPushRequestSchema,
|
|
18
20
|
} from '@syncular/core';
|
|
19
21
|
import type {
|
|
22
|
+
ScopeCacheBackend,
|
|
20
23
|
ServerSyncDialect,
|
|
21
24
|
ServerTableHandler,
|
|
22
25
|
SnapshotChunkStorage,
|
|
26
|
+
SqlFamily,
|
|
23
27
|
SyncCoreDb,
|
|
24
28
|
SyncRealtimeBroadcaster,
|
|
25
29
|
SyncRealtimeEvent,
|
|
30
|
+
SyncServerAuth,
|
|
26
31
|
} from '@syncular/server';
|
|
27
32
|
import {
|
|
28
33
|
type CompactOptions,
|
|
34
|
+
createServerHandlerCollection,
|
|
29
35
|
InvalidSubscriptionScopeError,
|
|
30
36
|
type PruneOptions,
|
|
31
37
|
type PullResult,
|
|
@@ -33,7 +39,6 @@ import {
|
|
|
33
39
|
pushCommit,
|
|
34
40
|
readSnapshotChunk,
|
|
35
41
|
recordClientCursor,
|
|
36
|
-
TableRegistry,
|
|
37
42
|
} from '@syncular/server';
|
|
38
43
|
import type { Context, MiddlewareHandler } from 'hono';
|
|
39
44
|
import { Hono } from 'hono';
|
|
@@ -64,10 +69,7 @@ import {
|
|
|
64
69
|
const wsConnectionManagerMap = new WeakMap<Hono, WebSocketConnectionManager>();
|
|
65
70
|
const realtimeUnsubscribeMap = new WeakMap<Hono, () => void>();
|
|
66
71
|
|
|
67
|
-
export interface SyncAuthResult {
|
|
68
|
-
actorId: string;
|
|
69
|
-
partitionId?: string;
|
|
70
|
-
}
|
|
72
|
+
export interface SyncAuthResult extends SyncServerAuth {}
|
|
71
73
|
|
|
72
74
|
/**
|
|
73
75
|
* WebSocket configuration for realtime sync.
|
|
@@ -119,6 +121,22 @@ export interface SyncRoutesConfigWithRateLimit {
|
|
|
119
121
|
* Default: 200
|
|
120
122
|
*/
|
|
121
123
|
maxOperationsPerPush?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Request/response payload snapshots recorded for console inspection.
|
|
126
|
+
*/
|
|
127
|
+
requestPayloadSnapshots?: {
|
|
128
|
+
/**
|
|
129
|
+
* Enable payload snapshot storage in `sync_request_payloads`.
|
|
130
|
+
* Default: true when console event recording is enabled.
|
|
131
|
+
*/
|
|
132
|
+
enabled?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Max serialized payload size in bytes per request/response snapshot.
|
|
135
|
+
* Larger payloads are truncated with metadata.
|
|
136
|
+
* Default: 128 KiB.
|
|
137
|
+
*/
|
|
138
|
+
maxBytes?: number;
|
|
139
|
+
};
|
|
122
140
|
/**
|
|
123
141
|
* Rate limiting configuration.
|
|
124
142
|
* Set to false to disable all rate limiting.
|
|
@@ -162,11 +180,15 @@ export interface SyncRoutesConfigWithRateLimit {
|
|
|
162
180
|
};
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
export interface CreateSyncRoutesOptions<
|
|
183
|
+
export interface CreateSyncRoutesOptions<
|
|
184
|
+
DB extends SyncCoreDb = SyncCoreDb,
|
|
185
|
+
Auth extends SyncAuthResult = SyncAuthResult,
|
|
186
|
+
F extends SqlFamily = SqlFamily,
|
|
187
|
+
> {
|
|
166
188
|
db: Kysely<DB>;
|
|
167
|
-
dialect: ServerSyncDialect
|
|
168
|
-
handlers: ServerTableHandler<DB>[];
|
|
169
|
-
authenticate: (c: Context) => Promise<
|
|
189
|
+
dialect: ServerSyncDialect<F>;
|
|
190
|
+
handlers: ServerTableHandler<DB, Auth>[];
|
|
191
|
+
authenticate: (c: Context) => Promise<Auth | null>;
|
|
170
192
|
sync?: SyncRoutesConfigWithRateLimit;
|
|
171
193
|
wsConnectionManager?: WebSocketConnectionManager;
|
|
172
194
|
/**
|
|
@@ -175,6 +197,11 @@ export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
|
|
|
175
197
|
* (S3, R2, etc.) instead of inline in the database.
|
|
176
198
|
*/
|
|
177
199
|
chunkStorage?: SnapshotChunkStorage;
|
|
200
|
+
/**
|
|
201
|
+
* Optional scope cache backend for resolveScopes() results.
|
|
202
|
+
* Request-local memoization is always applied for every pull.
|
|
203
|
+
*/
|
|
204
|
+
scopeCache?: ScopeCacheBackend;
|
|
178
205
|
/**
|
|
179
206
|
* Optional live emitter for console websocket activity feed.
|
|
180
207
|
* When provided, sync lifecycle events are published to `/console/events/live`.
|
|
@@ -186,6 +213,11 @@ export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
|
|
|
186
213
|
data: Record<string, unknown>;
|
|
187
214
|
}): void;
|
|
188
215
|
};
|
|
216
|
+
/**
|
|
217
|
+
* Optional console schema readiness promise.
|
|
218
|
+
* When provided, request-event recording waits for this promise before writing.
|
|
219
|
+
*/
|
|
220
|
+
consoleSchemaReady?: Promise<void>;
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
// ============================================================================
|
|
@@ -196,7 +228,7 @@ const snapshotChunkParamsSchema = z.object({
|
|
|
196
228
|
chunkId: z.string().min(1),
|
|
197
229
|
});
|
|
198
230
|
|
|
199
|
-
const
|
|
231
|
+
const DEFAULT_REQUEST_PAYLOAD_SNAPSHOT_MAX_BYTES = 128 * 1024;
|
|
200
232
|
|
|
201
233
|
type TraceContext = {
|
|
202
234
|
traceId: string | null;
|
|
@@ -243,6 +275,19 @@ function parseSentryTraceHeader(
|
|
|
243
275
|
return { traceId, spanId };
|
|
244
276
|
}
|
|
245
277
|
|
|
278
|
+
function readPositiveInteger(
|
|
279
|
+
value: number | undefined,
|
|
280
|
+
fallback: number
|
|
281
|
+
): number {
|
|
282
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
283
|
+
return fallback;
|
|
284
|
+
}
|
|
285
|
+
if (value <= 0) {
|
|
286
|
+
return fallback;
|
|
287
|
+
}
|
|
288
|
+
return Math.floor(value);
|
|
289
|
+
}
|
|
290
|
+
|
|
246
291
|
function readTraceContext(c: Context): TraceContext {
|
|
247
292
|
const traceparent = parseW3cTraceparent(c.req.header('traceparent'));
|
|
248
293
|
if (traceparent) return traceparent;
|
|
@@ -390,16 +435,16 @@ function countPullRows(response: PullResult['response']): number {
|
|
|
390
435
|
}, 0);
|
|
391
436
|
}
|
|
392
437
|
|
|
393
|
-
function encodePayloadSnapshot(value: unknown): string {
|
|
438
|
+
function encodePayloadSnapshot(value: unknown, maxBytes: number): string {
|
|
394
439
|
try {
|
|
395
440
|
const serialized = JSON.stringify(value);
|
|
396
|
-
if (serialized.length <=
|
|
441
|
+
if (serialized.length <= maxBytes) {
|
|
397
442
|
return serialized;
|
|
398
443
|
}
|
|
399
444
|
return JSON.stringify({
|
|
400
445
|
truncated: true,
|
|
401
446
|
originalSizeBytes: serialized.length,
|
|
402
|
-
preview: serialized.slice(0,
|
|
447
|
+
preview: serialized.slice(0, maxBytes),
|
|
403
448
|
});
|
|
404
449
|
} catch {
|
|
405
450
|
return JSON.stringify({
|
|
@@ -430,9 +475,11 @@ function emitConsoleLiveEvent(
|
|
|
430
475
|
});
|
|
431
476
|
}
|
|
432
477
|
|
|
433
|
-
export function createSyncRoutes<
|
|
434
|
-
|
|
435
|
-
|
|
478
|
+
export function createSyncRoutes<
|
|
479
|
+
DB extends SyncCoreDb = SyncCoreDb,
|
|
480
|
+
Auth extends SyncAuthResult = SyncAuthResult,
|
|
481
|
+
F extends SqlFamily = SqlFamily,
|
|
482
|
+
>(options: CreateSyncRoutesOptions<DB, Auth, F>): Hono {
|
|
436
483
|
const routes = new Hono();
|
|
437
484
|
routes.onError((error, c) => {
|
|
438
485
|
captureSyncException(error, {
|
|
@@ -442,10 +489,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
442
489
|
});
|
|
443
490
|
return c.text('Internal Server Error', 500);
|
|
444
491
|
});
|
|
445
|
-
const handlerRegistry =
|
|
446
|
-
for (const handler of options.handlers) {
|
|
447
|
-
handlerRegistry.register(handler);
|
|
448
|
-
}
|
|
492
|
+
const handlerRegistry = createServerHandlerCollection(options.handlers);
|
|
449
493
|
const config = options.sync ?? {};
|
|
450
494
|
const maxPullLimitCommits = config.maxPullLimitCommits ?? 100;
|
|
451
495
|
const maxSubscriptionsPerPull = config.maxSubscriptionsPerPull ?? 200;
|
|
@@ -455,6 +499,25 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
455
499
|
const consoleLiveEmitter = options.consoleLiveEmitter;
|
|
456
500
|
const shouldEmitConsoleLiveEvents = consoleLiveEmitter !== undefined;
|
|
457
501
|
const shouldRecordRequestEvents = shouldEmitConsoleLiveEvents;
|
|
502
|
+
const shouldCaptureRequestPayloadSnapshots =
|
|
503
|
+
shouldRecordRequestEvents &&
|
|
504
|
+
config.requestPayloadSnapshots?.enabled !== false;
|
|
505
|
+
const requestPayloadSnapshotMaxBytes = readPositiveInteger(
|
|
506
|
+
config.requestPayloadSnapshots?.maxBytes,
|
|
507
|
+
DEFAULT_REQUEST_PAYLOAD_SNAPSHOT_MAX_BYTES
|
|
508
|
+
);
|
|
509
|
+
const consoleSchemaReadyBase = shouldRecordRequestEvents
|
|
510
|
+
? (options.consoleSchemaReady ??
|
|
511
|
+
options.dialect.ensureConsoleSchema?.(options.db) ??
|
|
512
|
+
Promise.resolve())
|
|
513
|
+
: Promise.resolve();
|
|
514
|
+
const consoleSchemaReady = consoleSchemaReadyBase.catch((error) => {
|
|
515
|
+
logSyncEvent({
|
|
516
|
+
event: 'sync.console_schema_ready_failed',
|
|
517
|
+
error: error instanceof Error ? error.message : String(error),
|
|
518
|
+
});
|
|
519
|
+
throw error;
|
|
520
|
+
});
|
|
458
521
|
|
|
459
522
|
// -------------------------------------------------------------------------
|
|
460
523
|
// Optional WebSocket manager (scope-key based wake-ups)
|
|
@@ -565,8 +628,14 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
565
628
|
payload_ref, partition_id, request_payload, response_payload, created_at
|
|
566
629
|
) VALUES (
|
|
567
630
|
${nextPayloadRef}, ${event.partitionId},
|
|
568
|
-
${encodePayloadSnapshot(
|
|
569
|
-
|
|
631
|
+
${encodePayloadSnapshot(
|
|
632
|
+
event.payloadSnapshot.request,
|
|
633
|
+
requestPayloadSnapshotMaxBytes
|
|
634
|
+
)},
|
|
635
|
+
${encodePayloadSnapshot(
|
|
636
|
+
event.payloadSnapshot.response,
|
|
637
|
+
requestPayloadSnapshotMaxBytes
|
|
638
|
+
)},
|
|
570
639
|
${nowIso}
|
|
571
640
|
)
|
|
572
641
|
ON CONFLICT (payload_ref) DO UPDATE SET
|
|
@@ -620,19 +689,21 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
620
689
|
|
|
621
690
|
const resolvedEvent = typeof event === 'function' ? event() : event;
|
|
622
691
|
|
|
623
|
-
void
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
692
|
+
void consoleSchemaReady
|
|
693
|
+
.then(() => recordRequestEvent(resolvedEvent))
|
|
694
|
+
.catch((error) => {
|
|
695
|
+
logAsyncFailureOnce('sync.request_event_record_failed', {
|
|
696
|
+
event: 'sync.request_event_record_failed',
|
|
697
|
+
userId: resolvedEvent.actorId,
|
|
698
|
+
clientId: resolvedEvent.clientId,
|
|
699
|
+
requestEventType: resolvedEvent.eventType,
|
|
700
|
+
error: error instanceof Error ? error.message : String(error),
|
|
701
|
+
});
|
|
630
702
|
});
|
|
631
|
-
});
|
|
632
703
|
};
|
|
633
704
|
|
|
634
|
-
const authCache = new WeakMap<Context, Promise<
|
|
635
|
-
const getAuth = (c: Context): Promise<
|
|
705
|
+
const authCache = new WeakMap<Context, Promise<Auth | null>>();
|
|
706
|
+
const getAuth = (c: Context): Promise<Auth | null> => {
|
|
636
707
|
const cached = authCache.get(c);
|
|
637
708
|
if (cached) return cached;
|
|
638
709
|
const pending = options.authenticate(c);
|
|
@@ -787,8 +858,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
787
858
|
db: options.db,
|
|
788
859
|
dialect: options.dialect,
|
|
789
860
|
handlers: handlerRegistry,
|
|
790
|
-
|
|
791
|
-
partitionId,
|
|
861
|
+
auth,
|
|
792
862
|
request: {
|
|
793
863
|
clientId,
|
|
794
864
|
clientCommitId: pushBody.clientCommitId,
|
|
@@ -826,15 +896,17 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
826
896
|
commitSeq: pushed.response.commitSeq,
|
|
827
897
|
operationCount: pushOps.length,
|
|
828
898
|
tables: pushed.affectedTables,
|
|
829
|
-
payloadSnapshot:
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
899
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
900
|
+
? {
|
|
901
|
+
request: {
|
|
902
|
+
clientId,
|
|
903
|
+
clientCommitId: pushBody.clientCommitId,
|
|
904
|
+
schemaVersion: pushBody.schemaVersion,
|
|
905
|
+
operations: pushBody.operations,
|
|
906
|
+
},
|
|
907
|
+
response: pushed.response,
|
|
908
|
+
}
|
|
909
|
+
: null,
|
|
838
910
|
}));
|
|
839
911
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
840
912
|
partitionId,
|
|
@@ -979,10 +1051,10 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
979
1051
|
db: options.db,
|
|
980
1052
|
dialect: options.dialect,
|
|
981
1053
|
handlers: handlerRegistry,
|
|
982
|
-
|
|
983
|
-
partitionId,
|
|
1054
|
+
auth,
|
|
984
1055
|
request,
|
|
985
1056
|
chunkStorage: options.chunkStorage,
|
|
1057
|
+
scopeCache: options.scopeCache,
|
|
986
1058
|
});
|
|
987
1059
|
} catch (err) {
|
|
988
1060
|
if (err instanceof InvalidSubscriptionScopeError) {
|
|
@@ -1045,7 +1117,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1045
1117
|
const scopesSummary = shouldRecordRequestEvents
|
|
1046
1118
|
? summarizeScopeValues(pullResult.effectiveScopes)
|
|
1047
1119
|
: null;
|
|
1048
|
-
const payloadSnapshot =
|
|
1120
|
+
const payloadSnapshot = shouldCaptureRequestPayloadSnapshots
|
|
1049
1121
|
? {
|
|
1050
1122
|
request: {
|
|
1051
1123
|
clientId,
|
|
@@ -1318,6 +1390,32 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1318
1390
|
|
|
1319
1391
|
let unregister: (() => void) | null = null;
|
|
1320
1392
|
let connRef: ReturnType<typeof createWebSocketConnection> | null = null;
|
|
1393
|
+
const connectionCountBeforeUpgrade =
|
|
1394
|
+
wsConnectionManager.getConnectionCount(clientId);
|
|
1395
|
+
let sessionStartedAtMs: number | null = null;
|
|
1396
|
+
let sessionEnded = false;
|
|
1397
|
+
|
|
1398
|
+
const finishRealtimeSession = (reason: 'closed' | 'error') => {
|
|
1399
|
+
if (sessionEnded) return;
|
|
1400
|
+
sessionEnded = true;
|
|
1401
|
+
if (sessionStartedAtMs === null) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const durationMs = Math.max(0, Date.now() - sessionStartedAtMs);
|
|
1405
|
+
countSyncMetric('sync.sessions.ended', 1, {
|
|
1406
|
+
attributes: {
|
|
1407
|
+
transportPath: realtimeTransportPath,
|
|
1408
|
+
reason,
|
|
1409
|
+
},
|
|
1410
|
+
});
|
|
1411
|
+
distributionSyncMetric('sync.sessions.duration_ms', durationMs, {
|
|
1412
|
+
unit: 'millisecond',
|
|
1413
|
+
attributes: {
|
|
1414
|
+
transportPath: realtimeTransportPath,
|
|
1415
|
+
reason,
|
|
1416
|
+
},
|
|
1417
|
+
});
|
|
1418
|
+
};
|
|
1321
1419
|
|
|
1322
1420
|
const upgradeWebSocket = websocketConfig.upgradeWebSocket;
|
|
1323
1421
|
if (!upgradeWebSocket) {
|
|
@@ -1332,6 +1430,20 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1332
1430
|
transportPath: realtimeTransportPath,
|
|
1333
1431
|
});
|
|
1334
1432
|
connRef = conn;
|
|
1433
|
+
sessionStartedAtMs = Date.now();
|
|
1434
|
+
countSyncMetric('sync.sessions.started', 1, {
|
|
1435
|
+
attributes: {
|
|
1436
|
+
transportPath: realtimeTransportPath,
|
|
1437
|
+
},
|
|
1438
|
+
});
|
|
1439
|
+
if (connectionCountBeforeUpgrade > 0) {
|
|
1440
|
+
countSyncMetric('sync.transport.reconnects', 1, {
|
|
1441
|
+
attributes: {
|
|
1442
|
+
transportPath: realtimeTransportPath,
|
|
1443
|
+
source: 'server',
|
|
1444
|
+
},
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1335
1447
|
|
|
1336
1448
|
unregister = wsConnectionManager.register(conn, initialScopeKeys);
|
|
1337
1449
|
conn.sendHeartbeat();
|
|
@@ -1348,6 +1460,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1348
1460
|
unregister?.();
|
|
1349
1461
|
unregister = null;
|
|
1350
1462
|
connRef = null;
|
|
1463
|
+
finishRealtimeSession('closed');
|
|
1351
1464
|
logSyncEvent({
|
|
1352
1465
|
event: 'sync.realtime.disconnect',
|
|
1353
1466
|
userId: auth.actorId,
|
|
@@ -1363,6 +1476,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1363
1476
|
unregister?.();
|
|
1364
1477
|
unregister = null;
|
|
1365
1478
|
connRef = null;
|
|
1479
|
+
finishRealtimeSession('error');
|
|
1366
1480
|
logSyncEvent({
|
|
1367
1481
|
event: 'sync.realtime.disconnect',
|
|
1368
1482
|
userId: auth.actorId,
|
|
@@ -1383,13 +1497,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1383
1497
|
if (!msg || typeof msg !== 'object') return;
|
|
1384
1498
|
|
|
1385
1499
|
if (msg.type === 'push') {
|
|
1386
|
-
void handleWsPush(
|
|
1387
|
-
msg,
|
|
1388
|
-
connRef,
|
|
1389
|
-
auth.actorId,
|
|
1390
|
-
partitionId,
|
|
1391
|
-
clientId
|
|
1392
|
-
);
|
|
1500
|
+
void handleWsPush(msg, connRef, auth, clientId);
|
|
1393
1501
|
return;
|
|
1394
1502
|
}
|
|
1395
1503
|
|
|
@@ -1481,10 +1589,11 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1481
1589
|
async function handleWsPush(
|
|
1482
1590
|
msg: Record<string, unknown>,
|
|
1483
1591
|
conn: WebSocketConnection,
|
|
1484
|
-
|
|
1485
|
-
partitionId: string,
|
|
1592
|
+
auth: Auth,
|
|
1486
1593
|
clientId: string
|
|
1487
1594
|
): Promise<void> {
|
|
1595
|
+
const actorId = auth.actorId;
|
|
1596
|
+
const partitionId = auth.partitionId ?? 'default';
|
|
1488
1597
|
const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
|
|
1489
1598
|
if (!requestId) return;
|
|
1490
1599
|
const traceContext = readTraceContextFromMessage(msg);
|
|
@@ -1521,14 +1630,16 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1521
1630
|
durationMs: invalidDurationMs,
|
|
1522
1631
|
errorCode: 'INVALID_PUSH_PAYLOAD',
|
|
1523
1632
|
errorMessage: 'Invalid push payload',
|
|
1524
|
-
payloadSnapshot:
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1633
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1634
|
+
? {
|
|
1635
|
+
request: msg,
|
|
1636
|
+
response: {
|
|
1637
|
+
ok: false,
|
|
1638
|
+
status: 'rejected',
|
|
1639
|
+
reason: 'invalid_push_payload',
|
|
1640
|
+
},
|
|
1641
|
+
}
|
|
1642
|
+
: null,
|
|
1532
1643
|
}));
|
|
1533
1644
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1534
1645
|
partitionId,
|
|
@@ -1579,19 +1690,21 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1579
1690
|
errorCode: 'MAX_OPERATIONS_EXCEEDED',
|
|
1580
1691
|
errorMessage: `Maximum ${maxOperationsPerPush} operations per push`,
|
|
1581
1692
|
operationCount: pushOps.length,
|
|
1582
|
-
payloadSnapshot:
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1693
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1694
|
+
? {
|
|
1695
|
+
request: {
|
|
1696
|
+
clientId,
|
|
1697
|
+
clientCommitId: parsed.data.clientCommitId,
|
|
1698
|
+
schemaVersion: parsed.data.schemaVersion,
|
|
1699
|
+
operations: parsed.data.operations,
|
|
1700
|
+
},
|
|
1701
|
+
response: {
|
|
1702
|
+
ok: false,
|
|
1703
|
+
status: 'rejected',
|
|
1704
|
+
reason: 'max_operations_exceeded',
|
|
1705
|
+
},
|
|
1706
|
+
}
|
|
1707
|
+
: null,
|
|
1595
1708
|
}));
|
|
1596
1709
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1597
1710
|
partitionId,
|
|
@@ -1615,8 +1728,7 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1615
1728
|
db: options.db,
|
|
1616
1729
|
dialect: options.dialect,
|
|
1617
1730
|
handlers: handlerRegistry,
|
|
1618
|
-
|
|
1619
|
-
partitionId,
|
|
1731
|
+
auth,
|
|
1620
1732
|
request: {
|
|
1621
1733
|
clientId,
|
|
1622
1734
|
clientCommitId: parsed.data.clientCommitId,
|
|
@@ -1654,15 +1766,17 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1654
1766
|
commitSeq: pushed.response.commitSeq,
|
|
1655
1767
|
operationCount: pushOps.length,
|
|
1656
1768
|
tables: pushed.affectedTables,
|
|
1657
|
-
payloadSnapshot:
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1769
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1770
|
+
? {
|
|
1771
|
+
request: {
|
|
1772
|
+
clientId,
|
|
1773
|
+
clientCommitId: parsed.data.clientCommitId,
|
|
1774
|
+
schemaVersion: parsed.data.schemaVersion,
|
|
1775
|
+
operations: parsed.data.operations,
|
|
1776
|
+
},
|
|
1777
|
+
response: pushed.response,
|
|
1778
|
+
}
|
|
1779
|
+
: null,
|
|
1666
1780
|
}));
|
|
1667
1781
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1668
1782
|
partitionId,
|
|
@@ -1681,6 +1795,19 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1681
1795
|
tables: pushed.affectedTables,
|
|
1682
1796
|
}));
|
|
1683
1797
|
|
|
1798
|
+
const detectedConflicts = pushed.response.results.reduce(
|
|
1799
|
+
(count, result) => count + (result.status === 'conflict' ? 1 : 0),
|
|
1800
|
+
0
|
|
1801
|
+
);
|
|
1802
|
+
if (detectedConflicts > 0) {
|
|
1803
|
+
countSyncMetric('sync.conflicts.detected', detectedConflicts, {
|
|
1804
|
+
attributes: {
|
|
1805
|
+
syncPath: 'ws-push',
|
|
1806
|
+
transportPath: conn.transportPath,
|
|
1807
|
+
},
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1684
1811
|
// WS notifications to other clients
|
|
1685
1812
|
if (
|
|
1686
1813
|
wsConnectionManager &&
|
|
@@ -1771,15 +1898,17 @@ export function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(
|
|
|
1771
1898
|
durationMs: failedDurationMs,
|
|
1772
1899
|
errorCode: 'INTERNAL_SERVER_ERROR',
|
|
1773
1900
|
errorMessage: message,
|
|
1774
|
-
payloadSnapshot:
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1901
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1902
|
+
? {
|
|
1903
|
+
request: msg,
|
|
1904
|
+
response: {
|
|
1905
|
+
ok: false,
|
|
1906
|
+
status: 'rejected',
|
|
1907
|
+
reason: 'internal_server_error',
|
|
1908
|
+
message,
|
|
1909
|
+
},
|
|
1910
|
+
}
|
|
1911
|
+
: null,
|
|
1783
1912
|
}));
|
|
1784
1913
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1785
1914
|
partitionId,
|