@syncular/server-hono 0.0.6-93 → 0.0.6-96

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/src/routes.ts CHANGED
@@ -121,6 +121,22 @@ export interface SyncRoutesConfigWithRateLimit {
121
121
  * Default: 200
122
122
  */
123
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
+ };
124
140
  /**
125
141
  * Rate limiting configuration.
126
142
  * Set to false to disable all rate limiting.
@@ -197,6 +213,11 @@ export interface CreateSyncRoutesOptions<
197
213
  data: Record<string, unknown>;
198
214
  }): void;
199
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>;
200
221
  }
201
222
 
202
223
  // ============================================================================
@@ -207,7 +228,7 @@ const snapshotChunkParamsSchema = z.object({
207
228
  chunkId: z.string().min(1),
208
229
  });
209
230
 
210
- const MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES = 128 * 1024;
231
+ const DEFAULT_REQUEST_PAYLOAD_SNAPSHOT_MAX_BYTES = 128 * 1024;
211
232
 
212
233
  type TraceContext = {
213
234
  traceId: string | null;
@@ -254,6 +275,19 @@ function parseSentryTraceHeader(
254
275
  return { traceId, spanId };
255
276
  }
256
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
+
257
291
  function readTraceContext(c: Context): TraceContext {
258
292
  const traceparent = parseW3cTraceparent(c.req.header('traceparent'));
259
293
  if (traceparent) return traceparent;
@@ -401,16 +435,16 @@ function countPullRows(response: PullResult['response']): number {
401
435
  }, 0);
402
436
  }
403
437
 
404
- function encodePayloadSnapshot(value: unknown): string {
438
+ function encodePayloadSnapshot(value: unknown, maxBytes: number): string {
405
439
  try {
406
440
  const serialized = JSON.stringify(value);
407
- if (serialized.length <= MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES) {
441
+ if (serialized.length <= maxBytes) {
408
442
  return serialized;
409
443
  }
410
444
  return JSON.stringify({
411
445
  truncated: true,
412
446
  originalSizeBytes: serialized.length,
413
- preview: serialized.slice(0, MAX_REQUEST_PAYLOAD_SNAPSHOT_BYTES),
447
+ preview: serialized.slice(0, maxBytes),
414
448
  });
415
449
  } catch {
416
450
  return JSON.stringify({
@@ -465,6 +499,25 @@ export function createSyncRoutes<
465
499
  const consoleLiveEmitter = options.consoleLiveEmitter;
466
500
  const shouldEmitConsoleLiveEvents = consoleLiveEmitter !== undefined;
467
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
+ });
468
521
 
469
522
  // -------------------------------------------------------------------------
470
523
  // Optional WebSocket manager (scope-key based wake-ups)
@@ -575,8 +628,14 @@ export function createSyncRoutes<
575
628
  payload_ref, partition_id, request_payload, response_payload, created_at
576
629
  ) VALUES (
577
630
  ${nextPayloadRef}, ${event.partitionId},
578
- ${encodePayloadSnapshot(event.payloadSnapshot.request)},
579
- ${encodePayloadSnapshot(event.payloadSnapshot.response)},
631
+ ${encodePayloadSnapshot(
632
+ event.payloadSnapshot.request,
633
+ requestPayloadSnapshotMaxBytes
634
+ )},
635
+ ${encodePayloadSnapshot(
636
+ event.payloadSnapshot.response,
637
+ requestPayloadSnapshotMaxBytes
638
+ )},
580
639
  ${nowIso}
581
640
  )
582
641
  ON CONFLICT (payload_ref) DO UPDATE SET
@@ -630,15 +689,17 @@ export function createSyncRoutes<
630
689
 
631
690
  const resolvedEvent = typeof event === 'function' ? event() : event;
632
691
 
633
- void recordRequestEvent(resolvedEvent).catch((error) => {
634
- logAsyncFailureOnce('sync.request_event_record_failed', {
635
- event: 'sync.request_event_record_failed',
636
- userId: resolvedEvent.actorId,
637
- clientId: resolvedEvent.clientId,
638
- requestEventType: resolvedEvent.eventType,
639
- error: error instanceof Error ? error.message : String(error),
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
+ });
640
702
  });
641
- });
642
703
  };
643
704
 
644
705
  const authCache = new WeakMap<Context, Promise<Auth | null>>();
@@ -835,15 +896,17 @@ export function createSyncRoutes<
835
896
  commitSeq: pushed.response.commitSeq,
836
897
  operationCount: pushOps.length,
837
898
  tables: pushed.affectedTables,
838
- payloadSnapshot: {
839
- request: {
840
- clientId,
841
- clientCommitId: pushBody.clientCommitId,
842
- schemaVersion: pushBody.schemaVersion,
843
- operations: pushBody.operations,
844
- },
845
- response: pushed.response,
846
- },
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,
847
910
  }));
848
911
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
849
912
  partitionId,
@@ -1054,7 +1117,7 @@ export function createSyncRoutes<
1054
1117
  const scopesSummary = shouldRecordRequestEvents
1055
1118
  ? summarizeScopeValues(pullResult.effectiveScopes)
1056
1119
  : null;
1057
- const payloadSnapshot = shouldRecordRequestEvents
1120
+ const payloadSnapshot = shouldCaptureRequestPayloadSnapshots
1058
1121
  ? {
1059
1122
  request: {
1060
1123
  clientId,
@@ -1567,14 +1630,16 @@ export function createSyncRoutes<
1567
1630
  durationMs: invalidDurationMs,
1568
1631
  errorCode: 'INVALID_PUSH_PAYLOAD',
1569
1632
  errorMessage: 'Invalid push payload',
1570
- payloadSnapshot: {
1571
- request: msg,
1572
- response: {
1573
- ok: false,
1574
- status: 'rejected',
1575
- reason: 'invalid_push_payload',
1576
- },
1577
- },
1633
+ payloadSnapshot: shouldCaptureRequestPayloadSnapshots
1634
+ ? {
1635
+ request: msg,
1636
+ response: {
1637
+ ok: false,
1638
+ status: 'rejected',
1639
+ reason: 'invalid_push_payload',
1640
+ },
1641
+ }
1642
+ : null,
1578
1643
  }));
1579
1644
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1580
1645
  partitionId,
@@ -1625,19 +1690,21 @@ export function createSyncRoutes<
1625
1690
  errorCode: 'MAX_OPERATIONS_EXCEEDED',
1626
1691
  errorMessage: `Maximum ${maxOperationsPerPush} operations per push`,
1627
1692
  operationCount: pushOps.length,
1628
- payloadSnapshot: {
1629
- request: {
1630
- clientId,
1631
- clientCommitId: parsed.data.clientCommitId,
1632
- schemaVersion: parsed.data.schemaVersion,
1633
- operations: parsed.data.operations,
1634
- },
1635
- response: {
1636
- ok: false,
1637
- status: 'rejected',
1638
- reason: 'max_operations_exceeded',
1639
- },
1640
- },
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,
1641
1708
  }));
1642
1709
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1643
1710
  partitionId,
@@ -1699,15 +1766,17 @@ export function createSyncRoutes<
1699
1766
  commitSeq: pushed.response.commitSeq,
1700
1767
  operationCount: pushOps.length,
1701
1768
  tables: pushed.affectedTables,
1702
- payloadSnapshot: {
1703
- request: {
1704
- clientId,
1705
- clientCommitId: parsed.data.clientCommitId,
1706
- schemaVersion: parsed.data.schemaVersion,
1707
- operations: parsed.data.operations,
1708
- },
1709
- response: pushed.response,
1710
- },
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,
1711
1780
  }));
1712
1781
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1713
1782
  partitionId,
@@ -1829,15 +1898,17 @@ export function createSyncRoutes<
1829
1898
  durationMs: failedDurationMs,
1830
1899
  errorCode: 'INTERNAL_SERVER_ERROR',
1831
1900
  errorMessage: message,
1832
- payloadSnapshot: {
1833
- request: msg,
1834
- response: {
1835
- ok: false,
1836
- status: 'rejected',
1837
- reason: 'internal_server_error',
1838
- message,
1839
- },
1840
- },
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,
1841
1912
  }));
1842
1913
  emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
1843
1914
  partitionId,