@syncular/server-hono 0.0.4-25 → 0.0.6-100
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 +6 -1
- package/dist/console/gateway.d.ts +3 -1
- package/dist/console/gateway.d.ts.map +1 -1
- package/dist/console/gateway.js +227 -42
- package/dist/console/gateway.js.map +1 -1
- package/dist/console/index.d.ts +2 -0
- package/dist/console/index.d.ts.map +1 -1
- package/dist/console/index.js +2 -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 +516 -81
- 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/console/ui.d.ts +38 -0
- package/dist/console/ui.d.ts.map +1 -0
- package/dist/console/ui.js +43 -0
- package/dist/console/ui.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 -6
- 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 +286 -54
- package/src/console/index.ts +2 -0
- package/src/console/routes.ts +663 -199
- package/src/console/schemas.ts +29 -0
- package/src/console/types.ts +185 -0
- package/src/console/ui.ts +100 -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/dist/routes.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - GET /snapshot-chunks/:chunkId (download encoded snapshot chunks)
|
|
7
7
|
* - GET /realtime (optional WebSocket "wake up" notifications)
|
|
8
8
|
*/
|
|
9
|
-
import type { ServerSyncDialect, ServerTableHandler, SnapshotChunkStorage, SyncCoreDb, SyncRealtimeBroadcaster } from '@syncular/server';
|
|
9
|
+
import type { ScopeCacheBackend, ServerSyncDialect, ServerTableHandler, SnapshotChunkStorage, SqlFamily, SyncCoreDb, SyncRealtimeBroadcaster, SyncServerAuth } from '@syncular/server';
|
|
10
10
|
import { type CompactOptions, type PruneOptions } from '@syncular/server';
|
|
11
11
|
import type { Context } from 'hono';
|
|
12
12
|
import { Hono } from 'hono';
|
|
@@ -14,9 +14,7 @@ import type { UpgradeWebSocket } from 'hono/ws';
|
|
|
14
14
|
import { type Kysely } from 'kysely';
|
|
15
15
|
import { type SyncRateLimitConfig } from './rate-limit';
|
|
16
16
|
import { WebSocketConnectionManager } from './ws';
|
|
17
|
-
export interface SyncAuthResult {
|
|
18
|
-
actorId: string;
|
|
19
|
-
partitionId?: string;
|
|
17
|
+
export interface SyncAuthResult extends SyncServerAuth {
|
|
20
18
|
}
|
|
21
19
|
/**
|
|
22
20
|
* WebSocket configuration for realtime sync.
|
|
@@ -67,6 +65,22 @@ export interface SyncRoutesConfigWithRateLimit {
|
|
|
67
65
|
* Default: 200
|
|
68
66
|
*/
|
|
69
67
|
maxOperationsPerPush?: number;
|
|
68
|
+
/**
|
|
69
|
+
* Request/response payload snapshots recorded for console inspection.
|
|
70
|
+
*/
|
|
71
|
+
requestPayloadSnapshots?: {
|
|
72
|
+
/**
|
|
73
|
+
* Enable payload snapshot storage in `sync_request_payloads`.
|
|
74
|
+
* Default: true when console event recording is enabled.
|
|
75
|
+
*/
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Max serialized payload size in bytes per request/response snapshot.
|
|
79
|
+
* Larger payloads are truncated with metadata.
|
|
80
|
+
* Default: 128 KiB.
|
|
81
|
+
*/
|
|
82
|
+
maxBytes?: number;
|
|
83
|
+
};
|
|
70
84
|
/**
|
|
71
85
|
* Rate limiting configuration.
|
|
72
86
|
* Set to false to disable all rate limiting.
|
|
@@ -106,11 +120,11 @@ export interface SyncRoutesConfigWithRateLimit {
|
|
|
106
120
|
instanceId?: string;
|
|
107
121
|
};
|
|
108
122
|
}
|
|
109
|
-
export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
|
|
123
|
+
export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb, Auth extends SyncAuthResult = SyncAuthResult, F extends SqlFamily = SqlFamily> {
|
|
110
124
|
db: Kysely<DB>;
|
|
111
|
-
dialect: ServerSyncDialect
|
|
112
|
-
handlers: ServerTableHandler<DB>[];
|
|
113
|
-
authenticate: (c: Context) => Promise<
|
|
125
|
+
dialect: ServerSyncDialect<F>;
|
|
126
|
+
handlers: ServerTableHandler<DB, Auth>[];
|
|
127
|
+
authenticate: (c: Context) => Promise<Auth | null>;
|
|
114
128
|
sync?: SyncRoutesConfigWithRateLimit;
|
|
115
129
|
wsConnectionManager?: WebSocketConnectionManager;
|
|
116
130
|
/**
|
|
@@ -119,6 +133,11 @@ export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
|
|
|
119
133
|
* (S3, R2, etc.) instead of inline in the database.
|
|
120
134
|
*/
|
|
121
135
|
chunkStorage?: SnapshotChunkStorage;
|
|
136
|
+
/**
|
|
137
|
+
* Optional scope cache backend for resolveScopes() results.
|
|
138
|
+
* Request-local memoization is always applied for every pull.
|
|
139
|
+
*/
|
|
140
|
+
scopeCache?: ScopeCacheBackend;
|
|
122
141
|
/**
|
|
123
142
|
* Optional live emitter for console websocket activity feed.
|
|
124
143
|
* When provided, sync lifecycle events are published to `/console/events/live`.
|
|
@@ -130,8 +149,13 @@ export interface CreateSyncRoutesOptions<DB extends SyncCoreDb = SyncCoreDb> {
|
|
|
130
149
|
data: Record<string, unknown>;
|
|
131
150
|
}): void;
|
|
132
151
|
};
|
|
152
|
+
/**
|
|
153
|
+
* Optional console schema readiness promise.
|
|
154
|
+
* When provided, request-event recording waits for this promise before writing.
|
|
155
|
+
*/
|
|
156
|
+
consoleSchemaReady?: Promise<void>;
|
|
133
157
|
}
|
|
134
|
-
export declare function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb>(options: CreateSyncRoutesOptions<DB>): Hono;
|
|
158
|
+
export declare function createSyncRoutes<DB extends SyncCoreDb = SyncCoreDb, Auth extends SyncAuthResult = SyncAuthResult, F extends SqlFamily = SqlFamily>(options: CreateSyncRoutesOptions<DB, Auth, F>): Hono;
|
|
135
159
|
export declare function getSyncWebSocketConnectionManager(routes: Hono): WebSocketConnectionManager | undefined;
|
|
136
160
|
export declare function getSyncRealtimeUnsubscribe(routes: Hono): (() => void) | undefined;
|
|
137
161
|
//# sourceMappingURL=routes.d.ts.map
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/routes.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* - GET /snapshot-chunks/:chunkId (download encoded snapshot chunks)
|
|
7
7
|
* - GET /realtime (optional WebSocket "wake up" notifications)
|
|
8
8
|
*/
|
|
9
|
-
import { captureSyncException, createSyncTimer, ErrorResponseSchema, logSyncEvent, SyncCombinedRequestSchema, SyncCombinedResponseSchema, SyncPushRequestSchema, } from '@syncular/core';
|
|
10
|
-
import { InvalidSubscriptionScopeError, pull, pushCommit, readSnapshotChunk, recordClientCursor,
|
|
9
|
+
import { captureSyncException, countSyncMetric, createSyncTimer, distributionSyncMetric, ErrorResponseSchema, logSyncEvent, SyncCombinedRequestSchema, SyncCombinedResponseSchema, SyncPushRequestSchema, } from '@syncular/core';
|
|
10
|
+
import { createServerHandlerCollection, InvalidSubscriptionScopeError, pull, pushCommit, readSnapshotChunk, recordClientCursor, } from '@syncular/server';
|
|
11
11
|
import { Hono } from 'hono';
|
|
12
12
|
import { describeRoute, resolver, validator as zValidator } from 'hono-openapi';
|
|
13
13
|
import { sql, } from 'kysely';
|
|
@@ -25,7 +25,7 @@ const realtimeUnsubscribeMap = new WeakMap();
|
|
|
25
25
|
const snapshotChunkParamsSchema = z.object({
|
|
26
26
|
chunkId: z.string().min(1),
|
|
27
27
|
});
|
|
28
|
-
const
|
|
28
|
+
const DEFAULT_REQUEST_PAYLOAD_SNAPSHOT_MAX_BYTES = 128 * 1024;
|
|
29
29
|
function createOpaqueId(prefix) {
|
|
30
30
|
const randomPart = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
31
31
|
? crypto.randomUUID()
|
|
@@ -64,6 +64,15 @@ function parseSentryTraceHeader(sentryTrace) {
|
|
|
64
64
|
return null;
|
|
65
65
|
return { traceId, spanId };
|
|
66
66
|
}
|
|
67
|
+
function readPositiveInteger(value, fallback) {
|
|
68
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
if (value <= 0) {
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
return Math.floor(value);
|
|
75
|
+
}
|
|
67
76
|
function readTraceContext(c) {
|
|
68
77
|
const traceparent = parseW3cTraceparent(c.req.header('traceparent'));
|
|
69
78
|
if (traceparent)
|
|
@@ -170,16 +179,16 @@ function countPullRows(response) {
|
|
|
170
179
|
return totalRows + commitRows + snapshotRows;
|
|
171
180
|
}, 0);
|
|
172
181
|
}
|
|
173
|
-
function encodePayloadSnapshot(value) {
|
|
182
|
+
function encodePayloadSnapshot(value, maxBytes) {
|
|
174
183
|
try {
|
|
175
184
|
const serialized = JSON.stringify(value);
|
|
176
|
-
if (serialized.length <=
|
|
185
|
+
if (serialized.length <= maxBytes) {
|
|
177
186
|
return serialized;
|
|
178
187
|
}
|
|
179
188
|
return JSON.stringify({
|
|
180
189
|
truncated: true,
|
|
181
190
|
originalSizeBytes: serialized.length,
|
|
182
|
-
preview: serialized.slice(0,
|
|
191
|
+
preview: serialized.slice(0, maxBytes),
|
|
183
192
|
});
|
|
184
193
|
}
|
|
185
194
|
catch {
|
|
@@ -208,10 +217,7 @@ export function createSyncRoutes(options) {
|
|
|
208
217
|
});
|
|
209
218
|
return c.text('Internal Server Error', 500);
|
|
210
219
|
});
|
|
211
|
-
const handlerRegistry =
|
|
212
|
-
for (const handler of options.handlers) {
|
|
213
|
-
handlerRegistry.register(handler);
|
|
214
|
-
}
|
|
220
|
+
const handlerRegistry = createServerHandlerCollection(options.handlers);
|
|
215
221
|
const config = options.sync ?? {};
|
|
216
222
|
const maxPullLimitCommits = config.maxPullLimitCommits ?? 100;
|
|
217
223
|
const maxSubscriptionsPerPull = config.maxSubscriptionsPerPull ?? 200;
|
|
@@ -221,6 +227,21 @@ export function createSyncRoutes(options) {
|
|
|
221
227
|
const consoleLiveEmitter = options.consoleLiveEmitter;
|
|
222
228
|
const shouldEmitConsoleLiveEvents = consoleLiveEmitter !== undefined;
|
|
223
229
|
const shouldRecordRequestEvents = shouldEmitConsoleLiveEvents;
|
|
230
|
+
const shouldCaptureRequestPayloadSnapshots = shouldRecordRequestEvents &&
|
|
231
|
+
config.requestPayloadSnapshots?.enabled !== false;
|
|
232
|
+
const requestPayloadSnapshotMaxBytes = readPositiveInteger(config.requestPayloadSnapshots?.maxBytes, DEFAULT_REQUEST_PAYLOAD_SNAPSHOT_MAX_BYTES);
|
|
233
|
+
const consoleSchemaReadyBase = shouldRecordRequestEvents
|
|
234
|
+
? (options.consoleSchemaReady ??
|
|
235
|
+
options.dialect.ensureConsoleSchema?.(options.db) ??
|
|
236
|
+
Promise.resolve())
|
|
237
|
+
: Promise.resolve();
|
|
238
|
+
const consoleSchemaReady = consoleSchemaReadyBase.catch((error) => {
|
|
239
|
+
logSyncEvent({
|
|
240
|
+
event: 'sync.console_schema_ready_failed',
|
|
241
|
+
error: error instanceof Error ? error.message : String(error),
|
|
242
|
+
});
|
|
243
|
+
throw error;
|
|
244
|
+
});
|
|
224
245
|
// -------------------------------------------------------------------------
|
|
225
246
|
// Optional WebSocket manager (scope-key based wake-ups)
|
|
226
247
|
// -------------------------------------------------------------------------
|
|
@@ -275,8 +296,8 @@ export function createSyncRoutes(options) {
|
|
|
275
296
|
payload_ref, partition_id, request_payload, response_payload, created_at
|
|
276
297
|
) VALUES (
|
|
277
298
|
${nextPayloadRef}, ${event.partitionId},
|
|
278
|
-
${encodePayloadSnapshot(event.payloadSnapshot.request)},
|
|
279
|
-
${encodePayloadSnapshot(event.payloadSnapshot.response)},
|
|
299
|
+
${encodePayloadSnapshot(event.payloadSnapshot.request, requestPayloadSnapshotMaxBytes)},
|
|
300
|
+
${encodePayloadSnapshot(event.payloadSnapshot.response, requestPayloadSnapshotMaxBytes)},
|
|
280
301
|
${nowIso}
|
|
281
302
|
)
|
|
282
303
|
ON CONFLICT (payload_ref) DO UPDATE SET
|
|
@@ -325,7 +346,9 @@ export function createSyncRoutes(options) {
|
|
|
325
346
|
if (!shouldRecordRequestEvents)
|
|
326
347
|
return;
|
|
327
348
|
const resolvedEvent = typeof event === 'function' ? event() : event;
|
|
328
|
-
void
|
|
349
|
+
void consoleSchemaReady
|
|
350
|
+
.then(() => recordRequestEvent(resolvedEvent))
|
|
351
|
+
.catch((error) => {
|
|
329
352
|
logAsyncFailureOnce('sync.request_event_record_failed', {
|
|
330
353
|
event: 'sync.request_event_record_failed',
|
|
331
354
|
userId: resolvedEvent.actorId,
|
|
@@ -463,8 +486,7 @@ export function createSyncRoutes(options) {
|
|
|
463
486
|
db: options.db,
|
|
464
487
|
dialect: options.dialect,
|
|
465
488
|
handlers: handlerRegistry,
|
|
466
|
-
|
|
467
|
-
partitionId,
|
|
489
|
+
auth,
|
|
468
490
|
request: {
|
|
469
491
|
clientId,
|
|
470
492
|
clientCommitId: pushBody.clientCommitId,
|
|
@@ -499,15 +521,17 @@ export function createSyncRoutes(options) {
|
|
|
499
521
|
commitSeq: pushed.response.commitSeq,
|
|
500
522
|
operationCount: pushOps.length,
|
|
501
523
|
tables: pushed.affectedTables,
|
|
502
|
-
payloadSnapshot:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
524
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
525
|
+
? {
|
|
526
|
+
request: {
|
|
527
|
+
clientId,
|
|
528
|
+
clientCommitId: pushBody.clientCommitId,
|
|
529
|
+
schemaVersion: pushBody.schemaVersion,
|
|
530
|
+
operations: pushBody.operations,
|
|
531
|
+
},
|
|
532
|
+
response: pushed.response,
|
|
533
|
+
}
|
|
534
|
+
: null,
|
|
511
535
|
}));
|
|
512
536
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
513
537
|
partitionId,
|
|
@@ -610,10 +634,10 @@ export function createSyncRoutes(options) {
|
|
|
610
634
|
db: options.db,
|
|
611
635
|
dialect: options.dialect,
|
|
612
636
|
handlers: handlerRegistry,
|
|
613
|
-
|
|
614
|
-
partitionId,
|
|
637
|
+
auth,
|
|
615
638
|
request,
|
|
616
639
|
chunkStorage: options.chunkStorage,
|
|
640
|
+
scopeCache: options.scopeCache,
|
|
617
641
|
});
|
|
618
642
|
}
|
|
619
643
|
catch (err) {
|
|
@@ -663,7 +687,7 @@ export function createSyncRoutes(options) {
|
|
|
663
687
|
const scopesSummary = shouldRecordRequestEvents
|
|
664
688
|
? summarizeScopeValues(pullResult.effectiveScopes)
|
|
665
689
|
: null;
|
|
666
|
-
const payloadSnapshot =
|
|
690
|
+
const payloadSnapshot = shouldCaptureRequestPayloadSnapshots
|
|
667
691
|
? {
|
|
668
692
|
request: {
|
|
669
693
|
clientId,
|
|
@@ -887,6 +911,31 @@ export function createSyncRoutes(options) {
|
|
|
887
911
|
logSyncEvent({ event: 'sync.realtime.connect', userId: auth.actorId });
|
|
888
912
|
let unregister = null;
|
|
889
913
|
let connRef = null;
|
|
914
|
+
const connectionCountBeforeUpgrade = wsConnectionManager.getConnectionCount(clientId);
|
|
915
|
+
let sessionStartedAtMs = null;
|
|
916
|
+
let sessionEnded = false;
|
|
917
|
+
const finishRealtimeSession = (reason) => {
|
|
918
|
+
if (sessionEnded)
|
|
919
|
+
return;
|
|
920
|
+
sessionEnded = true;
|
|
921
|
+
if (sessionStartedAtMs === null) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const durationMs = Math.max(0, Date.now() - sessionStartedAtMs);
|
|
925
|
+
countSyncMetric('sync.sessions.ended', 1, {
|
|
926
|
+
attributes: {
|
|
927
|
+
transportPath: realtimeTransportPath,
|
|
928
|
+
reason,
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
distributionSyncMetric('sync.sessions.duration_ms', durationMs, {
|
|
932
|
+
unit: 'millisecond',
|
|
933
|
+
attributes: {
|
|
934
|
+
transportPath: realtimeTransportPath,
|
|
935
|
+
reason,
|
|
936
|
+
},
|
|
937
|
+
});
|
|
938
|
+
};
|
|
890
939
|
const upgradeWebSocket = websocketConfig.upgradeWebSocket;
|
|
891
940
|
if (!upgradeWebSocket) {
|
|
892
941
|
return c.json({ error: 'WEBSOCKET_NOT_CONFIGURED' }, 500);
|
|
@@ -899,6 +948,20 @@ export function createSyncRoutes(options) {
|
|
|
899
948
|
transportPath: realtimeTransportPath,
|
|
900
949
|
});
|
|
901
950
|
connRef = conn;
|
|
951
|
+
sessionStartedAtMs = Date.now();
|
|
952
|
+
countSyncMetric('sync.sessions.started', 1, {
|
|
953
|
+
attributes: {
|
|
954
|
+
transportPath: realtimeTransportPath,
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
if (connectionCountBeforeUpgrade > 0) {
|
|
958
|
+
countSyncMetric('sync.transport.reconnects', 1, {
|
|
959
|
+
attributes: {
|
|
960
|
+
transportPath: realtimeTransportPath,
|
|
961
|
+
source: 'server',
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
}
|
|
902
965
|
unregister = wsConnectionManager.register(conn, initialScopeKeys);
|
|
903
966
|
conn.sendHeartbeat();
|
|
904
967
|
emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', () => ({
|
|
@@ -914,6 +977,7 @@ export function createSyncRoutes(options) {
|
|
|
914
977
|
unregister?.();
|
|
915
978
|
unregister = null;
|
|
916
979
|
connRef = null;
|
|
980
|
+
finishRealtimeSession('closed');
|
|
917
981
|
logSyncEvent({
|
|
918
982
|
event: 'sync.realtime.disconnect',
|
|
919
983
|
userId: auth.actorId,
|
|
@@ -929,6 +993,7 @@ export function createSyncRoutes(options) {
|
|
|
929
993
|
unregister?.();
|
|
930
994
|
unregister = null;
|
|
931
995
|
connRef = null;
|
|
996
|
+
finishRealtimeSession('error');
|
|
932
997
|
logSyncEvent({
|
|
933
998
|
event: 'sync.realtime.disconnect',
|
|
934
999
|
userId: auth.actorId,
|
|
@@ -949,7 +1014,7 @@ export function createSyncRoutes(options) {
|
|
|
949
1014
|
if (!msg || typeof msg !== 'object')
|
|
950
1015
|
return;
|
|
951
1016
|
if (msg.type === 'push') {
|
|
952
|
-
void handleWsPush(msg, connRef, auth
|
|
1017
|
+
void handleWsPush(msg, connRef, auth, clientId);
|
|
953
1018
|
return;
|
|
954
1019
|
}
|
|
955
1020
|
if (msg.type !== 'presence' || !msg.scopeKey)
|
|
@@ -1018,7 +1083,9 @@ export function createSyncRoutes(options) {
|
|
|
1018
1083
|
return;
|
|
1019
1084
|
wsConnectionManager.notifyScopeKeys(scopeKeys, commitSeq);
|
|
1020
1085
|
}
|
|
1021
|
-
async function handleWsPush(msg, conn,
|
|
1086
|
+
async function handleWsPush(msg, conn, auth, clientId) {
|
|
1087
|
+
const actorId = auth.actorId;
|
|
1088
|
+
const partitionId = auth.partitionId ?? 'default';
|
|
1022
1089
|
const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
|
|
1023
1090
|
if (!requestId)
|
|
1024
1091
|
return;
|
|
@@ -1053,14 +1120,16 @@ export function createSyncRoutes(options) {
|
|
|
1053
1120
|
durationMs: invalidDurationMs,
|
|
1054
1121
|
errorCode: 'INVALID_PUSH_PAYLOAD',
|
|
1055
1122
|
errorMessage: 'Invalid push payload',
|
|
1056
|
-
payloadSnapshot:
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1123
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1124
|
+
? {
|
|
1125
|
+
request: msg,
|
|
1126
|
+
response: {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
status: 'rejected',
|
|
1129
|
+
reason: 'invalid_push_payload',
|
|
1130
|
+
},
|
|
1131
|
+
}
|
|
1132
|
+
: null,
|
|
1064
1133
|
}));
|
|
1065
1134
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1066
1135
|
partitionId,
|
|
@@ -1110,19 +1179,21 @@ export function createSyncRoutes(options) {
|
|
|
1110
1179
|
errorCode: 'MAX_OPERATIONS_EXCEEDED',
|
|
1111
1180
|
errorMessage: `Maximum ${maxOperationsPerPush} operations per push`,
|
|
1112
1181
|
operationCount: pushOps.length,
|
|
1113
|
-
payloadSnapshot:
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1182
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1183
|
+
? {
|
|
1184
|
+
request: {
|
|
1185
|
+
clientId,
|
|
1186
|
+
clientCommitId: parsed.data.clientCommitId,
|
|
1187
|
+
schemaVersion: parsed.data.schemaVersion,
|
|
1188
|
+
operations: parsed.data.operations,
|
|
1189
|
+
},
|
|
1190
|
+
response: {
|
|
1191
|
+
ok: false,
|
|
1192
|
+
status: 'rejected',
|
|
1193
|
+
reason: 'max_operations_exceeded',
|
|
1194
|
+
},
|
|
1195
|
+
}
|
|
1196
|
+
: null,
|
|
1126
1197
|
}));
|
|
1127
1198
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1128
1199
|
partitionId,
|
|
@@ -1145,8 +1216,7 @@ export function createSyncRoutes(options) {
|
|
|
1145
1216
|
db: options.db,
|
|
1146
1217
|
dialect: options.dialect,
|
|
1147
1218
|
handlers: handlerRegistry,
|
|
1148
|
-
|
|
1149
|
-
partitionId,
|
|
1219
|
+
auth,
|
|
1150
1220
|
request: {
|
|
1151
1221
|
clientId,
|
|
1152
1222
|
clientCommitId: parsed.data.clientCommitId,
|
|
@@ -1181,15 +1251,17 @@ export function createSyncRoutes(options) {
|
|
|
1181
1251
|
commitSeq: pushed.response.commitSeq,
|
|
1182
1252
|
operationCount: pushOps.length,
|
|
1183
1253
|
tables: pushed.affectedTables,
|
|
1184
|
-
payloadSnapshot:
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1254
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1255
|
+
? {
|
|
1256
|
+
request: {
|
|
1257
|
+
clientId,
|
|
1258
|
+
clientCommitId: parsed.data.clientCommitId,
|
|
1259
|
+
schemaVersion: parsed.data.schemaVersion,
|
|
1260
|
+
operations: parsed.data.operations,
|
|
1261
|
+
},
|
|
1262
|
+
response: pushed.response,
|
|
1263
|
+
}
|
|
1264
|
+
: null,
|
|
1193
1265
|
}));
|
|
1194
1266
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1195
1267
|
partitionId,
|
|
@@ -1207,6 +1279,15 @@ export function createSyncRoutes(options) {
|
|
|
1207
1279
|
operationCount: pushOps.length,
|
|
1208
1280
|
tables: pushed.affectedTables,
|
|
1209
1281
|
}));
|
|
1282
|
+
const detectedConflicts = pushed.response.results.reduce((count, result) => count + (result.status === 'conflict' ? 1 : 0), 0);
|
|
1283
|
+
if (detectedConflicts > 0) {
|
|
1284
|
+
countSyncMetric('sync.conflicts.detected', detectedConflicts, {
|
|
1285
|
+
attributes: {
|
|
1286
|
+
syncPath: 'ws-push',
|
|
1287
|
+
transportPath: conn.transportPath,
|
|
1288
|
+
},
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1210
1291
|
// WS notifications to other clients
|
|
1211
1292
|
if (wsConnectionManager &&
|
|
1212
1293
|
pushed.response.ok === true &&
|
|
@@ -1283,15 +1364,17 @@ export function createSyncRoutes(options) {
|
|
|
1283
1364
|
durationMs: failedDurationMs,
|
|
1284
1365
|
errorCode: 'INTERNAL_SERVER_ERROR',
|
|
1285
1366
|
errorMessage: message,
|
|
1286
|
-
payloadSnapshot:
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1367
|
+
payloadSnapshot: shouldCaptureRequestPayloadSnapshots
|
|
1368
|
+
? {
|
|
1369
|
+
request: msg,
|
|
1370
|
+
response: {
|
|
1371
|
+
ok: false,
|
|
1372
|
+
status: 'rejected',
|
|
1373
|
+
reason: 'internal_server_error',
|
|
1374
|
+
message,
|
|
1375
|
+
},
|
|
1376
|
+
}
|
|
1377
|
+
: null,
|
|
1295
1378
|
}));
|
|
1296
1379
|
emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
|
|
1297
1380
|
partitionId,
|