@syncular/server-hono 0.0.2-138 → 0.0.2-140
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/routes.d.ts +23 -2
- package/dist/console/routes.d.ts.map +1 -1
- package/dist/console/routes.js +1129 -219
- package/dist/console/routes.js.map +1 -1
- package/dist/console/schemas.d.ts +153 -0
- package/dist/console/schemas.d.ts.map +1 -1
- package/dist/console/schemas.js +80 -0
- package/dist/console/schemas.js.map +1 -1
- package/dist/create-server.d.ts +5 -0
- package/dist/create-server.d.ts.map +1 -1
- package/dist/create-server.js +17 -7
- package/dist/create-server.js.map +1 -1
- package/dist/routes.d.ts +11 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +514 -10
- package/dist/routes.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/console-routes.test.ts +1468 -0
- package/src/__tests__/create-server.test.ts +75 -0
- package/src/console/routes.ts +1471 -241
- package/src/console/schemas.ts +106 -0
- package/src/create-server.ts +28 -10
- package/src/routes.ts +616 -17
package/dist/routes.js
CHANGED
|
@@ -25,6 +25,179 @@ const realtimeUnsubscribeMap = new WeakMap();
|
|
|
25
25
|
const snapshotChunkParamsSchema = z.object({
|
|
26
26
|
chunkId: z.string().min(1),
|
|
27
27
|
});
|
|
28
|
+
const MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES = 128 * 1024;
|
|
29
|
+
function createOpaqueId(prefix) {
|
|
30
|
+
const randomPart = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
31
|
+
? crypto.randomUUID()
|
|
32
|
+
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
33
|
+
return `${prefix}-${randomPart}`;
|
|
34
|
+
}
|
|
35
|
+
function readRequestId(c) {
|
|
36
|
+
const headerRequestId = c.req.header('x-request-id')?.trim();
|
|
37
|
+
if (headerRequestId)
|
|
38
|
+
return headerRequestId;
|
|
39
|
+
return createOpaqueId('req');
|
|
40
|
+
}
|
|
41
|
+
function parseW3cTraceparent(traceparent) {
|
|
42
|
+
if (!traceparent)
|
|
43
|
+
return null;
|
|
44
|
+
const parsed = traceparent.trim();
|
|
45
|
+
const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i.exec(parsed);
|
|
46
|
+
if (!match)
|
|
47
|
+
return null;
|
|
48
|
+
const traceId = match[1]?.toLowerCase() ?? null;
|
|
49
|
+
const spanId = match[2]?.toLowerCase() ?? null;
|
|
50
|
+
if (!traceId || !spanId)
|
|
51
|
+
return null;
|
|
52
|
+
return { traceId, spanId };
|
|
53
|
+
}
|
|
54
|
+
function parseSentryTraceHeader(sentryTrace) {
|
|
55
|
+
if (!sentryTrace)
|
|
56
|
+
return null;
|
|
57
|
+
const parsed = sentryTrace.trim();
|
|
58
|
+
const match = /^([0-9a-f]{32})-([0-9a-f]{16})(?:-[01])?$/i.exec(parsed);
|
|
59
|
+
if (!match)
|
|
60
|
+
return null;
|
|
61
|
+
const traceId = match[1]?.toLowerCase() ?? null;
|
|
62
|
+
const spanId = match[2]?.toLowerCase() ?? null;
|
|
63
|
+
if (!traceId || !spanId)
|
|
64
|
+
return null;
|
|
65
|
+
return { traceId, spanId };
|
|
66
|
+
}
|
|
67
|
+
function readTraceContext(c) {
|
|
68
|
+
const traceparent = parseW3cTraceparent(c.req.header('traceparent'));
|
|
69
|
+
if (traceparent)
|
|
70
|
+
return traceparent;
|
|
71
|
+
const sentryTrace = parseSentryTraceHeader(c.req.header('sentry-trace'));
|
|
72
|
+
if (sentryTrace)
|
|
73
|
+
return sentryTrace;
|
|
74
|
+
return { traceId: null, spanId: null };
|
|
75
|
+
}
|
|
76
|
+
function readStringField(data, key) {
|
|
77
|
+
const value = data[key];
|
|
78
|
+
if (typeof value !== 'string')
|
|
79
|
+
return null;
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
82
|
+
}
|
|
83
|
+
function readTraceContextFromMessage(msg) {
|
|
84
|
+
const directTraceId = readStringField(msg, 'traceId') ?? readStringField(msg, 'trace_id');
|
|
85
|
+
const directSpanId = readStringField(msg, 'spanId') ?? readStringField(msg, 'span_id');
|
|
86
|
+
if (directTraceId || directSpanId) {
|
|
87
|
+
return { traceId: directTraceId, spanId: directSpanId };
|
|
88
|
+
}
|
|
89
|
+
const traceparent = readStringField(msg, 'traceparent') ?? readStringField(msg, 'traceParent');
|
|
90
|
+
const parsedTraceparent = parseW3cTraceparent(traceparent);
|
|
91
|
+
if (parsedTraceparent)
|
|
92
|
+
return parsedTraceparent;
|
|
93
|
+
const sentryTrace = readStringField(msg, 'sentry-trace') ??
|
|
94
|
+
readStringField(msg, 'sentryTrace') ??
|
|
95
|
+
readStringField(msg, 'sentry_trace');
|
|
96
|
+
const parsedSentryTrace = parseSentryTraceHeader(sentryTrace);
|
|
97
|
+
if (parsedSentryTrace)
|
|
98
|
+
return parsedSentryTrace;
|
|
99
|
+
return { traceId: null, spanId: null };
|
|
100
|
+
}
|
|
101
|
+
function normalizeResponseStatus(statusCode, outcome) {
|
|
102
|
+
if (statusCode >= 500)
|
|
103
|
+
return 'server_error';
|
|
104
|
+
if (statusCode >= 400)
|
|
105
|
+
return 'client_error';
|
|
106
|
+
if (statusCode >= 300)
|
|
107
|
+
return 'redirect';
|
|
108
|
+
if (statusCode >= 200) {
|
|
109
|
+
if (outcome === 'error' || outcome === 'rejected')
|
|
110
|
+
return 'failure';
|
|
111
|
+
return 'success';
|
|
112
|
+
}
|
|
113
|
+
return 'unknown';
|
|
114
|
+
}
|
|
115
|
+
function firstPushErrorCode(results) {
|
|
116
|
+
if (!Array.isArray(results))
|
|
117
|
+
return null;
|
|
118
|
+
for (const result of results) {
|
|
119
|
+
if (!result || typeof result !== 'object')
|
|
120
|
+
continue;
|
|
121
|
+
const status = Reflect.get(result, 'status');
|
|
122
|
+
if (status !== 'error')
|
|
123
|
+
continue;
|
|
124
|
+
const code = Reflect.get(result, 'code');
|
|
125
|
+
if (typeof code === 'string' && code.length > 0) {
|
|
126
|
+
return code;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function summarizeScopeValues(scopes) {
|
|
132
|
+
const summary = {};
|
|
133
|
+
for (const [key, value] of Object.entries(scopes)) {
|
|
134
|
+
if (typeof value === 'string') {
|
|
135
|
+
summary[key] = value;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
const normalized = value
|
|
140
|
+
.filter((entry) => typeof entry === 'string')
|
|
141
|
+
.slice(0, 20);
|
|
142
|
+
summary[key] = normalized;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return Object.keys(summary).length > 0 ? summary : null;
|
|
146
|
+
}
|
|
147
|
+
function summarizePullResponse(response) {
|
|
148
|
+
return {
|
|
149
|
+
subscriptions: response.subscriptions.map((subscription) => {
|
|
150
|
+
const changeCount = subscription.commits.reduce((totalChanges, commit) => totalChanges + commit.changes.length, 0);
|
|
151
|
+
const snapshotCount = subscription.snapshots?.length ?? 0;
|
|
152
|
+
const snapshotRowCount = subscription.snapshots?.reduce((totalRows, snapshot) => totalRows + snapshot.rows.length, 0) ?? 0;
|
|
153
|
+
return {
|
|
154
|
+
id: subscription.id,
|
|
155
|
+
status: subscription.status,
|
|
156
|
+
bootstrap: subscription.bootstrap,
|
|
157
|
+
nextCursor: subscription.nextCursor,
|
|
158
|
+
commitCount: subscription.commits.length,
|
|
159
|
+
changeCount,
|
|
160
|
+
snapshotCount,
|
|
161
|
+
snapshotRowCount,
|
|
162
|
+
};
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function countPullRows(response) {
|
|
167
|
+
return response.subscriptions.reduce((totalRows, subscription) => {
|
|
168
|
+
const commitRows = subscription.commits.reduce((totalChanges, commit) => totalChanges + commit.changes.length, 0);
|
|
169
|
+
const snapshotRows = subscription.snapshots?.reduce((totalSnapshotRows, snapshot) => totalSnapshotRows + snapshot.rows.length, 0) ?? 0;
|
|
170
|
+
return totalRows + commitRows + snapshotRows;
|
|
171
|
+
}, 0);
|
|
172
|
+
}
|
|
173
|
+
function encodePayloadSnapshot(value) {
|
|
174
|
+
try {
|
|
175
|
+
const serialized = JSON.stringify(value);
|
|
176
|
+
if (serialized.length <= MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES) {
|
|
177
|
+
return serialized;
|
|
178
|
+
}
|
|
179
|
+
return JSON.stringify({
|
|
180
|
+
truncated: true,
|
|
181
|
+
originalSizeBytes: serialized.length,
|
|
182
|
+
preview: serialized.slice(0, MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return JSON.stringify({
|
|
187
|
+
truncated: false,
|
|
188
|
+
serializationError: 'Could not serialize payload snapshot',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function emitConsoleLiveEvent(emitter, type, data) {
|
|
193
|
+
if (!emitter)
|
|
194
|
+
return;
|
|
195
|
+
emitter.emit({
|
|
196
|
+
type,
|
|
197
|
+
timestamp: new Date().toISOString(),
|
|
198
|
+
data,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
28
201
|
export function createSyncRoutes(options) {
|
|
29
202
|
const routes = new Hono();
|
|
30
203
|
routes.onError((error, c) => {
|
|
@@ -45,6 +218,7 @@ export function createSyncRoutes(options) {
|
|
|
45
218
|
const maxPullLimitSnapshotRows = config.maxPullLimitSnapshotRows ?? 5000;
|
|
46
219
|
const maxPullMaxSnapshotPages = config.maxPullMaxSnapshotPages ?? 10;
|
|
47
220
|
const maxOperationsPerPush = config.maxOperationsPerPush ?? 200;
|
|
221
|
+
const consoleLiveEmitter = options.consoleLiveEmitter;
|
|
48
222
|
// -------------------------------------------------------------------------
|
|
49
223
|
// Optional WebSocket manager (scope-key based wake-ups)
|
|
50
224
|
// -------------------------------------------------------------------------
|
|
@@ -89,18 +263,59 @@ export function createSyncRoutes(options) {
|
|
|
89
263
|
realtimeUnsubscribeMap.set(routes, unsubscribe);
|
|
90
264
|
}
|
|
91
265
|
const recordRequestEvent = async (event) => {
|
|
266
|
+
let payloadRef = event.payloadRef ?? null;
|
|
267
|
+
if (event.payloadSnapshot) {
|
|
268
|
+
const nextPayloadRef = payloadRef ?? createOpaqueId('payload');
|
|
269
|
+
const nowIso = new Date().toISOString();
|
|
270
|
+
try {
|
|
271
|
+
await sql `
|
|
272
|
+
INSERT INTO sync_request_payloads (
|
|
273
|
+
payload_ref, partition_id, request_payload, response_payload, created_at
|
|
274
|
+
) VALUES (
|
|
275
|
+
${nextPayloadRef}, ${event.partitionId},
|
|
276
|
+
${encodePayloadSnapshot(event.payloadSnapshot.request)},
|
|
277
|
+
${encodePayloadSnapshot(event.payloadSnapshot.response)},
|
|
278
|
+
${nowIso}
|
|
279
|
+
)
|
|
280
|
+
ON CONFLICT (payload_ref) DO UPDATE SET
|
|
281
|
+
partition_id = EXCLUDED.partition_id,
|
|
282
|
+
request_payload = EXCLUDED.request_payload,
|
|
283
|
+
response_payload = EXCLUDED.response_payload,
|
|
284
|
+
created_at = EXCLUDED.created_at
|
|
285
|
+
`.execute(options.db);
|
|
286
|
+
payloadRef = nextPayloadRef;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
payloadRef = null;
|
|
290
|
+
logAsyncFailureOnce('sync.request_payload_record_failed', {
|
|
291
|
+
event: 'sync.request_payload_record_failed',
|
|
292
|
+
userId: event.actorId,
|
|
293
|
+
clientId: event.clientId,
|
|
294
|
+
requestEventType: event.eventType,
|
|
295
|
+
error: error instanceof Error ? error.message : String(error),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
92
299
|
const tablesValue = options.dialect.arrayToDb(event.tables ?? []);
|
|
300
|
+
const scopesSummaryValue = event.scopesSummary
|
|
301
|
+
? JSON.stringify(event.scopesSummary)
|
|
302
|
+
: null;
|
|
93
303
|
await sql `
|
|
94
304
|
INSERT INTO sync_request_events (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
305
|
+
partition_id, request_id, trace_id, span_id,
|
|
306
|
+
event_type, sync_path, actor_id, client_id, transport_path,
|
|
307
|
+
status_code, outcome, response_status, error_code,
|
|
308
|
+
duration_ms, commit_seq, operation_count, row_count, subscription_count,
|
|
309
|
+
scopes_summary, tables, error_message, payload_ref
|
|
98
310
|
) VALUES (
|
|
99
|
-
${event.
|
|
100
|
-
${event.
|
|
101
|
-
${event.
|
|
102
|
-
${event.
|
|
103
|
-
${event.
|
|
311
|
+
${event.partitionId}, ${event.requestId}, ${event.traceId ?? null},
|
|
312
|
+
${event.spanId ?? null}, ${event.eventType}, ${event.syncPath},
|
|
313
|
+
${event.actorId}, ${event.clientId}, ${event.transportPath},
|
|
314
|
+
${event.statusCode}, ${event.outcome}, ${event.responseStatus},
|
|
315
|
+
${event.errorCode ?? null}, ${event.durationMs}, ${event.commitSeq ?? null},
|
|
316
|
+
${event.operationCount ?? null}, ${event.rowCount ?? null},
|
|
317
|
+
${event.subscriptionCount ?? null}, ${scopesSummaryValue}, ${tablesValue},
|
|
318
|
+
${event.errorMessage ?? null}, ${payloadRef}
|
|
104
319
|
)
|
|
105
320
|
`.execute(options.db);
|
|
106
321
|
};
|
|
@@ -224,6 +439,8 @@ export function createSyncRoutes(options) {
|
|
|
224
439
|
const partitionId = auth.partitionId ?? 'default';
|
|
225
440
|
const body = c.req.valid('json');
|
|
226
441
|
const clientId = body.clientId;
|
|
442
|
+
const requestId = readRequestId(c);
|
|
443
|
+
const traceContext = readTraceContext(c);
|
|
227
444
|
let pushResponse;
|
|
228
445
|
let pullResponse;
|
|
229
446
|
// --- Push phase ---
|
|
@@ -259,16 +476,48 @@ export function createSyncRoutes(options) {
|
|
|
259
476
|
commitSeq: pushed.response.commitSeq,
|
|
260
477
|
});
|
|
261
478
|
recordRequestEventInBackground({
|
|
479
|
+
partitionId,
|
|
480
|
+
requestId,
|
|
481
|
+
traceId: traceContext.traceId,
|
|
482
|
+
spanId: traceContext.spanId,
|
|
262
483
|
eventType: 'push',
|
|
484
|
+
syncPath: 'http-combined',
|
|
263
485
|
actorId: auth.actorId,
|
|
264
486
|
clientId,
|
|
265
487
|
transportPath: readTransportPath(c),
|
|
266
488
|
statusCode: 200,
|
|
267
489
|
outcome: pushed.response.status,
|
|
490
|
+
responseStatus: normalizeResponseStatus(200, pushed.response.status),
|
|
268
491
|
durationMs: pushDurationMs,
|
|
492
|
+
errorCode: firstPushErrorCode(pushed.response.results),
|
|
269
493
|
commitSeq: pushed.response.commitSeq,
|
|
270
494
|
operationCount: pushOps.length,
|
|
271
495
|
tables: pushed.affectedTables,
|
|
496
|
+
payloadSnapshot: {
|
|
497
|
+
request: {
|
|
498
|
+
clientId,
|
|
499
|
+
clientCommitId: body.push.clientCommitId,
|
|
500
|
+
schemaVersion: body.push.schemaVersion,
|
|
501
|
+
operations: body.push.operations,
|
|
502
|
+
},
|
|
503
|
+
response: pushed.response,
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
|
|
507
|
+
partitionId,
|
|
508
|
+
requestId,
|
|
509
|
+
traceId: traceContext.traceId,
|
|
510
|
+
spanId: traceContext.spanId,
|
|
511
|
+
actorId: auth.actorId,
|
|
512
|
+
clientId,
|
|
513
|
+
transportPath: readTransportPath(c),
|
|
514
|
+
syncPath: 'http-combined',
|
|
515
|
+
outcome: pushed.response.status,
|
|
516
|
+
statusCode: 200,
|
|
517
|
+
durationMs: pushDurationMs,
|
|
518
|
+
commitSeq: pushed.response.commitSeq ?? null,
|
|
519
|
+
operationCount: pushOps.length,
|
|
520
|
+
tables: pushed.affectedTables,
|
|
272
521
|
});
|
|
273
522
|
// WS notifications
|
|
274
523
|
if (wsConnectionManager &&
|
|
@@ -301,6 +550,17 @@ export function createSyncRoutes(options) {
|
|
|
301
550
|
}
|
|
302
551
|
}
|
|
303
552
|
}
|
|
553
|
+
if (pushed.response.ok === true &&
|
|
554
|
+
pushed.response.status === 'applied' &&
|
|
555
|
+
typeof pushed.response.commitSeq === 'number') {
|
|
556
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'commit', {
|
|
557
|
+
partitionId,
|
|
558
|
+
commitSeq: pushed.response.commitSeq,
|
|
559
|
+
actorId: auth.actorId,
|
|
560
|
+
clientId,
|
|
561
|
+
affectedTables: pushed.affectedTables,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
304
564
|
pushResponse = pushed.response;
|
|
305
565
|
}
|
|
306
566
|
// --- Pull phase ---
|
|
@@ -363,7 +623,17 @@ export function createSyncRoutes(options) {
|
|
|
363
623
|
actorId: auth.actorId,
|
|
364
624
|
cursor: pullResult.clientCursor,
|
|
365
625
|
effectiveScopes: pullResult.effectiveScopes,
|
|
366
|
-
})
|
|
626
|
+
})
|
|
627
|
+
.then(() => {
|
|
628
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
|
|
629
|
+
action: 'cursor_recorded',
|
|
630
|
+
partitionId,
|
|
631
|
+
actorId: auth.actorId,
|
|
632
|
+
clientId,
|
|
633
|
+
cursor: pullResult.clientCursor,
|
|
634
|
+
});
|
|
635
|
+
})
|
|
636
|
+
.catch((error) => {
|
|
367
637
|
logAsyncFailureOnce('sync.client_cursor_record_failed', {
|
|
368
638
|
event: 'sync.client_cursor_record_failed',
|
|
369
639
|
userId: auth.actorId,
|
|
@@ -381,13 +651,55 @@ export function createSyncRoutes(options) {
|
|
|
381
651
|
clientCursor: pullResult.clientCursor,
|
|
382
652
|
});
|
|
383
653
|
recordRequestEventInBackground({
|
|
654
|
+
partitionId,
|
|
655
|
+
requestId,
|
|
656
|
+
traceId: traceContext.traceId,
|
|
657
|
+
spanId: traceContext.spanId,
|
|
384
658
|
eventType: 'pull',
|
|
659
|
+
syncPath: 'http-combined',
|
|
385
660
|
actorId: auth.actorId,
|
|
386
661
|
clientId,
|
|
387
662
|
transportPath: readTransportPath(c),
|
|
388
663
|
statusCode: 200,
|
|
389
664
|
outcome: 'applied',
|
|
665
|
+
responseStatus: normalizeResponseStatus(200, 'applied'),
|
|
390
666
|
durationMs: pullDurationMs,
|
|
667
|
+
rowCount: countPullRows(pullResult.response),
|
|
668
|
+
subscriptionCount: request.subscriptions.length,
|
|
669
|
+
scopesSummary: summarizeScopeValues(pullResult.effectiveScopes),
|
|
670
|
+
payloadSnapshot: {
|
|
671
|
+
request: {
|
|
672
|
+
clientId,
|
|
673
|
+
limitCommits: request.limitCommits,
|
|
674
|
+
limitSnapshotRows: request.limitSnapshotRows,
|
|
675
|
+
maxSnapshotPages: request.maxSnapshotPages,
|
|
676
|
+
dedupeRows: request.dedupeRows,
|
|
677
|
+
subscriptions: request.subscriptions.map((subscription) => ({
|
|
678
|
+
id: subscription.id,
|
|
679
|
+
table: subscription.table,
|
|
680
|
+
scopes: subscription.scopes,
|
|
681
|
+
cursor: subscription.cursor,
|
|
682
|
+
bootstrapState: subscription.bootstrapState,
|
|
683
|
+
})),
|
|
684
|
+
},
|
|
685
|
+
response: summarizePullResponse(pullResult.response),
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'pull', {
|
|
689
|
+
partitionId,
|
|
690
|
+
requestId,
|
|
691
|
+
traceId: traceContext.traceId,
|
|
692
|
+
spanId: traceContext.spanId,
|
|
693
|
+
actorId: auth.actorId,
|
|
694
|
+
clientId,
|
|
695
|
+
transportPath: readTransportPath(c),
|
|
696
|
+
syncPath: 'http-combined',
|
|
697
|
+
outcome: 'applied',
|
|
698
|
+
statusCode: 200,
|
|
699
|
+
durationMs: pullDurationMs,
|
|
700
|
+
rowCount: countPullRows(pullResult.response),
|
|
701
|
+
subscriptionCount: request.subscriptions.length,
|
|
702
|
+
clientCursor: pullResult.clientCursor,
|
|
391
703
|
});
|
|
392
704
|
pullResponse = pullResult.response;
|
|
393
705
|
}
|
|
@@ -567,6 +879,14 @@ export function createSyncRoutes(options) {
|
|
|
567
879
|
connRef = conn;
|
|
568
880
|
unregister = wsConnectionManager.register(conn, initialScopeKeys);
|
|
569
881
|
conn.sendHeartbeat();
|
|
882
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
|
|
883
|
+
action: 'realtime_connected',
|
|
884
|
+
actorId: auth.actorId,
|
|
885
|
+
clientId,
|
|
886
|
+
partitionId,
|
|
887
|
+
transportPath: realtimeTransportPath,
|
|
888
|
+
scopeCount: initialScopeKeys.length,
|
|
889
|
+
});
|
|
570
890
|
},
|
|
571
891
|
onClose(_evt, _ws) {
|
|
572
892
|
unregister?.();
|
|
@@ -576,6 +896,12 @@ export function createSyncRoutes(options) {
|
|
|
576
896
|
event: 'sync.realtime.disconnect',
|
|
577
897
|
userId: auth.actorId,
|
|
578
898
|
});
|
|
899
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
|
|
900
|
+
action: 'realtime_disconnected',
|
|
901
|
+
actorId: auth.actorId,
|
|
902
|
+
clientId,
|
|
903
|
+
partitionId,
|
|
904
|
+
});
|
|
579
905
|
},
|
|
580
906
|
onError(_evt, _ws) {
|
|
581
907
|
unregister?.();
|
|
@@ -585,6 +911,12 @@ export function createSyncRoutes(options) {
|
|
|
585
911
|
event: 'sync.realtime.disconnect',
|
|
586
912
|
userId: auth.actorId,
|
|
587
913
|
});
|
|
914
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'client_update', {
|
|
915
|
+
action: 'realtime_error',
|
|
916
|
+
actorId: auth.actorId,
|
|
917
|
+
clientId,
|
|
918
|
+
partitionId,
|
|
919
|
+
});
|
|
588
920
|
},
|
|
589
921
|
onMessage(evt, _ws) {
|
|
590
922
|
if (!connRef)
|
|
@@ -668,10 +1000,13 @@ export function createSyncRoutes(options) {
|
|
|
668
1000
|
const requestId = typeof msg.requestId === 'string' ? msg.requestId : '';
|
|
669
1001
|
if (!requestId)
|
|
670
1002
|
return;
|
|
1003
|
+
const traceContext = readTraceContextFromMessage(msg);
|
|
1004
|
+
const timer = createSyncTimer();
|
|
671
1005
|
try {
|
|
672
1006
|
// Validate the push payload
|
|
673
1007
|
const parsed = SyncPushRequestSchema.omit({ clientId: true }).safeParse(msg);
|
|
674
1008
|
if (!parsed.success) {
|
|
1009
|
+
const invalidDurationMs = timer();
|
|
675
1010
|
conn.sendPushResponse({
|
|
676
1011
|
requestId,
|
|
677
1012
|
ok: false,
|
|
@@ -680,10 +1015,50 @@ export function createSyncRoutes(options) {
|
|
|
680
1015
|
{ opIndex: 0, status: 'error', error: 'Invalid push payload' },
|
|
681
1016
|
],
|
|
682
1017
|
});
|
|
1018
|
+
recordRequestEventInBackground({
|
|
1019
|
+
partitionId,
|
|
1020
|
+
requestId,
|
|
1021
|
+
traceId: traceContext.traceId,
|
|
1022
|
+
spanId: traceContext.spanId,
|
|
1023
|
+
eventType: 'push',
|
|
1024
|
+
syncPath: 'ws-push',
|
|
1025
|
+
actorId,
|
|
1026
|
+
clientId,
|
|
1027
|
+
transportPath: conn.transportPath,
|
|
1028
|
+
statusCode: 400,
|
|
1029
|
+
outcome: 'rejected',
|
|
1030
|
+
responseStatus: normalizeResponseStatus(400, 'rejected'),
|
|
1031
|
+
durationMs: invalidDurationMs,
|
|
1032
|
+
errorCode: 'INVALID_PUSH_PAYLOAD',
|
|
1033
|
+
errorMessage: 'Invalid push payload',
|
|
1034
|
+
payloadSnapshot: {
|
|
1035
|
+
request: msg,
|
|
1036
|
+
response: {
|
|
1037
|
+
ok: false,
|
|
1038
|
+
status: 'rejected',
|
|
1039
|
+
reason: 'invalid_push_payload',
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
|
|
1044
|
+
partitionId,
|
|
1045
|
+
requestId,
|
|
1046
|
+
traceId: traceContext.traceId,
|
|
1047
|
+
spanId: traceContext.spanId,
|
|
1048
|
+
actorId,
|
|
1049
|
+
clientId,
|
|
1050
|
+
transportPath: conn.transportPath,
|
|
1051
|
+
syncPath: 'ws-push',
|
|
1052
|
+
outcome: 'rejected',
|
|
1053
|
+
statusCode: 400,
|
|
1054
|
+
durationMs: invalidDurationMs,
|
|
1055
|
+
errorCode: 'INVALID_PUSH_PAYLOAD',
|
|
1056
|
+
});
|
|
683
1057
|
return;
|
|
684
1058
|
}
|
|
685
1059
|
const pushOps = parsed.data.operations ?? [];
|
|
686
1060
|
if (pushOps.length > maxOperationsPerPush) {
|
|
1061
|
+
const rejectedDurationMs = timer();
|
|
687
1062
|
conn.sendPushResponse({
|
|
688
1063
|
requestId,
|
|
689
1064
|
ok: false,
|
|
@@ -696,9 +1071,54 @@ export function createSyncRoutes(options) {
|
|
|
696
1071
|
},
|
|
697
1072
|
],
|
|
698
1073
|
});
|
|
1074
|
+
recordRequestEventInBackground({
|
|
1075
|
+
partitionId,
|
|
1076
|
+
requestId,
|
|
1077
|
+
traceId: traceContext.traceId,
|
|
1078
|
+
spanId: traceContext.spanId,
|
|
1079
|
+
eventType: 'push',
|
|
1080
|
+
syncPath: 'ws-push',
|
|
1081
|
+
actorId,
|
|
1082
|
+
clientId,
|
|
1083
|
+
transportPath: conn.transportPath,
|
|
1084
|
+
statusCode: 400,
|
|
1085
|
+
outcome: 'rejected',
|
|
1086
|
+
responseStatus: normalizeResponseStatus(400, 'rejected'),
|
|
1087
|
+
durationMs: rejectedDurationMs,
|
|
1088
|
+
errorCode: 'MAX_OPERATIONS_EXCEEDED',
|
|
1089
|
+
errorMessage: `Maximum ${maxOperationsPerPush} operations per push`,
|
|
1090
|
+
operationCount: pushOps.length,
|
|
1091
|
+
payloadSnapshot: {
|
|
1092
|
+
request: {
|
|
1093
|
+
clientId,
|
|
1094
|
+
clientCommitId: parsed.data.clientCommitId,
|
|
1095
|
+
schemaVersion: parsed.data.schemaVersion,
|
|
1096
|
+
operations: parsed.data.operations,
|
|
1097
|
+
},
|
|
1098
|
+
response: {
|
|
1099
|
+
ok: false,
|
|
1100
|
+
status: 'rejected',
|
|
1101
|
+
reason: 'max_operations_exceeded',
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
});
|
|
1105
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
|
|
1106
|
+
partitionId,
|
|
1107
|
+
requestId,
|
|
1108
|
+
traceId: traceContext.traceId,
|
|
1109
|
+
spanId: traceContext.spanId,
|
|
1110
|
+
actorId,
|
|
1111
|
+
clientId,
|
|
1112
|
+
transportPath: conn.transportPath,
|
|
1113
|
+
syncPath: 'ws-push',
|
|
1114
|
+
outcome: 'rejected',
|
|
1115
|
+
statusCode: 400,
|
|
1116
|
+
durationMs: rejectedDurationMs,
|
|
1117
|
+
operationCount: pushOps.length,
|
|
1118
|
+
errorCode: 'MAX_OPERATIONS_EXCEEDED',
|
|
1119
|
+
});
|
|
699
1120
|
return;
|
|
700
1121
|
}
|
|
701
|
-
const timer = createSyncTimer();
|
|
702
1122
|
const pushed = await pushCommit({
|
|
703
1123
|
db: options.db,
|
|
704
1124
|
dialect: options.dialect,
|
|
@@ -722,16 +1142,48 @@ export function createSyncRoutes(options) {
|
|
|
722
1142
|
commitSeq: pushed.response.commitSeq,
|
|
723
1143
|
});
|
|
724
1144
|
recordRequestEventInBackground({
|
|
1145
|
+
partitionId,
|
|
1146
|
+
requestId,
|
|
1147
|
+
traceId: traceContext.traceId,
|
|
1148
|
+
spanId: traceContext.spanId,
|
|
725
1149
|
eventType: 'push',
|
|
1150
|
+
syncPath: 'ws-push',
|
|
726
1151
|
actorId,
|
|
727
1152
|
clientId,
|
|
728
1153
|
transportPath: conn.transportPath,
|
|
729
1154
|
statusCode: 200,
|
|
730
1155
|
outcome: pushed.response.status,
|
|
1156
|
+
responseStatus: normalizeResponseStatus(200, pushed.response.status),
|
|
731
1157
|
durationMs: pushDurationMs,
|
|
1158
|
+
errorCode: firstPushErrorCode(pushed.response.results),
|
|
732
1159
|
commitSeq: pushed.response.commitSeq,
|
|
733
1160
|
operationCount: pushOps.length,
|
|
734
1161
|
tables: pushed.affectedTables,
|
|
1162
|
+
payloadSnapshot: {
|
|
1163
|
+
request: {
|
|
1164
|
+
clientId,
|
|
1165
|
+
clientCommitId: parsed.data.clientCommitId,
|
|
1166
|
+
schemaVersion: parsed.data.schemaVersion,
|
|
1167
|
+
operations: parsed.data.operations,
|
|
1168
|
+
},
|
|
1169
|
+
response: pushed.response,
|
|
1170
|
+
},
|
|
1171
|
+
});
|
|
1172
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
|
|
1173
|
+
partitionId,
|
|
1174
|
+
requestId,
|
|
1175
|
+
traceId: traceContext.traceId,
|
|
1176
|
+
spanId: traceContext.spanId,
|
|
1177
|
+
actorId,
|
|
1178
|
+
clientId,
|
|
1179
|
+
transportPath: conn.transportPath,
|
|
1180
|
+
syncPath: 'ws-push',
|
|
1181
|
+
outcome: pushed.response.status,
|
|
1182
|
+
statusCode: 200,
|
|
1183
|
+
durationMs: pushDurationMs,
|
|
1184
|
+
commitSeq: pushed.response.commitSeq ?? null,
|
|
1185
|
+
operationCount: pushOps.length,
|
|
1186
|
+
tables: pushed.affectedTables,
|
|
735
1187
|
});
|
|
736
1188
|
// WS notifications to other clients
|
|
737
1189
|
if (wsConnectionManager &&
|
|
@@ -764,6 +1216,17 @@ export function createSyncRoutes(options) {
|
|
|
764
1216
|
}
|
|
765
1217
|
}
|
|
766
1218
|
}
|
|
1219
|
+
if (pushed.response.ok === true &&
|
|
1220
|
+
pushed.response.status === 'applied' &&
|
|
1221
|
+
typeof pushed.response.commitSeq === 'number') {
|
|
1222
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'commit', {
|
|
1223
|
+
partitionId,
|
|
1224
|
+
commitSeq: pushed.response.commitSeq,
|
|
1225
|
+
actorId,
|
|
1226
|
+
clientId,
|
|
1227
|
+
affectedTables: pushed.affectedTables,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
767
1230
|
conn.sendPushResponse({
|
|
768
1231
|
requestId,
|
|
769
1232
|
ok: pushed.response.ok,
|
|
@@ -773,6 +1236,7 @@ export function createSyncRoutes(options) {
|
|
|
773
1236
|
});
|
|
774
1237
|
}
|
|
775
1238
|
catch (err) {
|
|
1239
|
+
const failedDurationMs = timer();
|
|
776
1240
|
captureSyncException(err, {
|
|
777
1241
|
event: 'sync.realtime.push_failed',
|
|
778
1242
|
requestId,
|
|
@@ -781,6 +1245,46 @@ export function createSyncRoutes(options) {
|
|
|
781
1245
|
partitionId,
|
|
782
1246
|
});
|
|
783
1247
|
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
1248
|
+
recordRequestEventInBackground({
|
|
1249
|
+
partitionId,
|
|
1250
|
+
requestId,
|
|
1251
|
+
traceId: traceContext.traceId,
|
|
1252
|
+
spanId: traceContext.spanId,
|
|
1253
|
+
eventType: 'push',
|
|
1254
|
+
syncPath: 'ws-push',
|
|
1255
|
+
actorId,
|
|
1256
|
+
clientId,
|
|
1257
|
+
transportPath: conn.transportPath,
|
|
1258
|
+
statusCode: 500,
|
|
1259
|
+
outcome: 'error',
|
|
1260
|
+
responseStatus: normalizeResponseStatus(500, 'error'),
|
|
1261
|
+
durationMs: failedDurationMs,
|
|
1262
|
+
errorCode: 'INTERNAL_SERVER_ERROR',
|
|
1263
|
+
errorMessage: message,
|
|
1264
|
+
payloadSnapshot: {
|
|
1265
|
+
request: msg,
|
|
1266
|
+
response: {
|
|
1267
|
+
ok: false,
|
|
1268
|
+
status: 'rejected',
|
|
1269
|
+
reason: 'internal_server_error',
|
|
1270
|
+
message,
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
});
|
|
1274
|
+
emitConsoleLiveEvent(consoleLiveEmitter, 'push', {
|
|
1275
|
+
partitionId,
|
|
1276
|
+
requestId,
|
|
1277
|
+
traceId: traceContext.traceId,
|
|
1278
|
+
spanId: traceContext.spanId,
|
|
1279
|
+
actorId,
|
|
1280
|
+
clientId,
|
|
1281
|
+
transportPath: conn.transportPath,
|
|
1282
|
+
syncPath: 'ws-push',
|
|
1283
|
+
outcome: 'error',
|
|
1284
|
+
statusCode: 500,
|
|
1285
|
+
durationMs: failedDurationMs,
|
|
1286
|
+
errorCode: 'INTERNAL_SERVER_ERROR',
|
|
1287
|
+
});
|
|
784
1288
|
conn.sendPushResponse({
|
|
785
1289
|
requestId,
|
|
786
1290
|
ok: false,
|