@syncular/server-hono 0.0.6-185 → 0.0.6-188

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.
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
package/dist/routes.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * - GET /realtime (optional WebSocket "wake up" notifications)
8
8
  */
9
9
  import { captureSyncException, countSyncMetric, createSyncTimer, distributionSyncMetric, ErrorResponseSchema, logSyncEvent, ScopeValuesSchema, SyncCombinedRequestSchema, SyncCombinedResponseSchema, SyncPushRequestSchema, } from '@syncular/core';
10
- import { createServerHandlerCollection, InvalidSubscriptionScopeError, maybeCompactChanges, maybePruneSync, parseJsonValue, pull, pushCommit, readSnapshotChunk, recordClientCursor, resolveEffectiveScopesForSubscriptions, scopesToSnapshotChunkScopeKey, } from '@syncular/server';
10
+ import { createServerHandlerCollection, InvalidSubscriptionScopeError, maybeCompactChanges, maybePruneSync, parseJsonValue, pull, pushCommit, pushCommitBatch, readSnapshotChunk, recordClientCursor, resolveEffectiveScopesForSubscriptions, scopesToSnapshotChunkScopeKey, } 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';
@@ -488,6 +488,206 @@ export function createSyncRoutes(options) {
488
488
  });
489
489
  });
490
490
  };
491
+ function notifyRealtimeForAppliedPushes(ctx, pushedCommits) {
492
+ if (!wsConnectionManager && !realtimeBroadcaster) {
493
+ return;
494
+ }
495
+ let latestCommitSeq = 0;
496
+ let latestActorId = ctx.auth.actorId;
497
+ let latestCreatedAt;
498
+ const scopeKeys = new Set();
499
+ const emittedChanges = [];
500
+ for (const pushed of pushedCommits) {
501
+ if (pushed.response.ok !== true ||
502
+ pushed.response.status !== 'applied' ||
503
+ typeof pushed.response.commitSeq !== 'number') {
504
+ continue;
505
+ }
506
+ latestCommitSeq = Math.max(latestCommitSeq, pushed.response.commitSeq);
507
+ latestActorId = pushed.commitActorId ?? latestActorId;
508
+ latestCreatedAt = pushed.commitCreatedAt ?? latestCreatedAt;
509
+ for (const scopeKey of applyPartitionToScopeKeys(ctx.partitionId, pushed.scopeKeys)) {
510
+ scopeKeys.add(scopeKey);
511
+ }
512
+ emittedChanges.push(...pushed.emittedChanges);
513
+ }
514
+ if (latestCommitSeq <= 0 || scopeKeys.size === 0) {
515
+ return;
516
+ }
517
+ const combinedScopeKeys = Array.from(scopeKeys);
518
+ if (wsConnectionManager) {
519
+ wsConnectionManager.notifyScopeKeys(combinedScopeKeys, latestCommitSeq, {
520
+ excludeClientIds: [ctx.clientId],
521
+ changes: emittedChanges,
522
+ actorId: latestActorId,
523
+ createdAt: latestCreatedAt,
524
+ });
525
+ }
526
+ if (realtimeBroadcaster) {
527
+ realtimeBroadcaster
528
+ .publish({
529
+ type: 'commit',
530
+ commitSeq: latestCommitSeq,
531
+ partitionId: ctx.partitionId,
532
+ scopeKeys: combinedScopeKeys,
533
+ sourceInstanceId: instanceId,
534
+ })
535
+ .catch((error) => {
536
+ logAsyncFailureOnce('sync.realtime.broadcast_publish_failed', {
537
+ event: 'sync.realtime.broadcast_publish_failed',
538
+ userId: ctx.auth.actorId,
539
+ clientId: ctx.clientId,
540
+ error: error instanceof Error ? error.message : String(error),
541
+ });
542
+ });
543
+ }
544
+ }
545
+ function recordPushExecutionSideEffects(ctx, summary) {
546
+ recordRequestEventInBackground(() => ({
547
+ partitionId: ctx.partitionId,
548
+ requestId: ctx.requestId,
549
+ traceId: ctx.traceContext.traceId,
550
+ spanId: ctx.traceContext.spanId,
551
+ eventType: 'push',
552
+ syncPath: ctx.syncPath,
553
+ actorId: ctx.auth.actorId,
554
+ clientId: ctx.clientId,
555
+ transportPath: ctx.transportPath,
556
+ statusCode: 200,
557
+ outcome: summary.outcome,
558
+ responseStatus: normalizeResponseStatus(200, summary.outcome),
559
+ durationMs: summary.durationMs,
560
+ errorCode: firstPushErrorCode(summary.results),
561
+ commitSeq: summary.commitSeq,
562
+ operationCount: summary.operationCount,
563
+ tables: summary.tables,
564
+ payloadSnapshot: summary.payloadSnapshot,
565
+ }));
566
+ emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
567
+ partitionId: ctx.partitionId,
568
+ requestId: ctx.requestId,
569
+ traceId: ctx.traceContext.traceId,
570
+ spanId: ctx.traceContext.spanId,
571
+ actorId: ctx.auth.actorId,
572
+ clientId: ctx.clientId,
573
+ transportPath: ctx.transportPath,
574
+ syncPath: ctx.syncPath,
575
+ outcome: summary.outcome,
576
+ statusCode: 200,
577
+ durationMs: summary.durationMs,
578
+ commitSeq: summary.commitSeq,
579
+ operationCount: summary.operationCount,
580
+ tables: summary.tables,
581
+ }));
582
+ }
583
+ function maybeCountPushConflicts(ctx, results, enabled) {
584
+ if (enabled !== true) {
585
+ return;
586
+ }
587
+ const detectedConflicts = results.reduce((count, result) => count + (result.status === 'conflict' ? 1 : 0), 0);
588
+ if (detectedConflicts <= 0) {
589
+ return;
590
+ }
591
+ countSyncMetric('sync.conflicts.detected', detectedConflicts, {
592
+ attributes: {
593
+ syncPath: ctx.syncPath,
594
+ transportPath: ctx.transportPath,
595
+ },
596
+ });
597
+ }
598
+ function emitCommitLiveEvents(ctx, pushedCommits) {
599
+ for (const pushed of pushedCommits) {
600
+ if (pushed.response.ok !== true ||
601
+ pushed.response.status !== 'applied' ||
602
+ typeof pushed.response.commitSeq !== 'number') {
603
+ continue;
604
+ }
605
+ emitConsoleLiveEvent(consoleLiveEmitter, 'commit', () => ({
606
+ partitionId: ctx.partitionId,
607
+ commitSeq: pushed.response.commitSeq,
608
+ actorId: ctx.auth.actorId,
609
+ clientId: ctx.clientId,
610
+ affectedTables: pushed.affectedTables,
611
+ }));
612
+ }
613
+ }
614
+ async function executePushCommitBatchWithSideEffects(ctx, pushBodies, execOptions = {}) {
615
+ const timer = createSyncTimer();
616
+ const totalOperationCount = pushBodies.reduce((count, pushBody) => count + (pushBody.operations?.length ?? 0), 0);
617
+ const executedPushes = await pushCommitBatch({
618
+ db: options.db,
619
+ dialect: options.dialect,
620
+ handlers: handlerRegistry,
621
+ plugins: options.plugins,
622
+ auth: ctx.auth,
623
+ suppressTelemetry: true,
624
+ requests: pushBodies.map((pushBody) => ({
625
+ clientId: ctx.clientId,
626
+ clientCommitId: pushBody.clientCommitId,
627
+ operations: pushBody.operations,
628
+ schemaVersion: pushBody.schemaVersion,
629
+ })),
630
+ });
631
+ const affectedTables = new Set();
632
+ for (const pushed of executedPushes) {
633
+ for (const table of pushed.affectedTables) {
634
+ affectedTables.add(table);
635
+ }
636
+ }
637
+ const pushDurationMs = timer();
638
+ const latestCommitSeq = executedPushes.reduce((latest, pushed) => {
639
+ if (typeof pushed.response.commitSeq === 'number') {
640
+ return Math.max(latest, pushed.response.commitSeq);
641
+ }
642
+ return latest;
643
+ }, 0);
644
+ const aggregateStatus = executedPushes.every((pushed) => pushed.response.status === 'cached')
645
+ ? 'cached'
646
+ : executedPushes.every((pushed) => pushed.response.status === 'applied' ||
647
+ pushed.response.status === 'cached')
648
+ ? 'applied'
649
+ : 'rejected';
650
+ const aggregatedResults = executedPushes.flatMap((pushed) => pushed.response.results);
651
+ logSyncEvent({
652
+ event: 'sync.push',
653
+ userId: ctx.auth.actorId,
654
+ durationMs: pushDurationMs,
655
+ operationCount: totalOperationCount,
656
+ status: aggregateStatus,
657
+ commitSeq: latestCommitSeq > 0 ? latestCommitSeq : undefined,
658
+ });
659
+ recordPushExecutionSideEffects(ctx, {
660
+ durationMs: pushDurationMs,
661
+ outcome: aggregateStatus,
662
+ commitSeq: latestCommitSeq > 0 ? latestCommitSeq : null,
663
+ operationCount: totalOperationCount,
664
+ tables: Array.from(affectedTables),
665
+ results: aggregatedResults,
666
+ payloadSnapshot: shouldCaptureRequestPayloadSnapshots
667
+ ? {
668
+ request: {
669
+ clientId: ctx.clientId,
670
+ commits: pushBodies.map((pushBody) => ({
671
+ clientCommitId: pushBody.clientCommitId,
672
+ schemaVersion: pushBody.schemaVersion,
673
+ operations: pushBody.operations,
674
+ })),
675
+ },
676
+ response: {
677
+ ok: true,
678
+ commits: executedPushes.map((pushed, index) => ({
679
+ clientCommitId: pushBodies[index]?.clientCommitId ?? '',
680
+ ...pushed.response,
681
+ })),
682
+ },
683
+ }
684
+ : null,
685
+ });
686
+ maybeCountPushConflicts(ctx, aggregatedResults, execOptions.countConflictsMetric);
687
+ notifyRealtimeForAppliedPushes(ctx, executedPushes);
688
+ emitCommitLiveEvents(ctx, executedPushes);
689
+ return executedPushes;
690
+ }
491
691
  async function executePushCommitWithSideEffects(ctx, pushBody, execOptions = {}) {
492
692
  const timer = createSyncTimer();
493
693
  const pushOps = pushBody.operations ?? [];
@@ -513,24 +713,13 @@ export function createSyncRoutes(options) {
513
713
  status: pushed.response.status,
514
714
  commitSeq: pushed.response.commitSeq,
515
715
  });
516
- recordRequestEventInBackground(() => ({
517
- partitionId: ctx.partitionId,
518
- requestId: ctx.requestId,
519
- traceId: ctx.traceContext.traceId,
520
- spanId: ctx.traceContext.spanId,
521
- eventType: 'push',
522
- syncPath: ctx.syncPath,
523
- actorId: ctx.auth.actorId,
524
- clientId: ctx.clientId,
525
- transportPath: ctx.transportPath,
526
- statusCode: 200,
527
- outcome: pushed.response.status,
528
- responseStatus: normalizeResponseStatus(200, pushed.response.status),
716
+ recordPushExecutionSideEffects(ctx, {
529
717
  durationMs: pushDurationMs,
530
- errorCode: firstPushErrorCode(pushed.response.results),
531
- commitSeq: pushed.response.commitSeq,
718
+ outcome: pushed.response.status,
719
+ commitSeq: pushed.response.commitSeq ?? null,
532
720
  operationCount: pushOps.length,
533
721
  tables: pushed.affectedTables,
722
+ results: pushed.response.results,
534
723
  payloadSnapshot: shouldCaptureRequestPayloadSnapshots
535
724
  ? {
536
725
  request: {
@@ -542,77 +731,12 @@ export function createSyncRoutes(options) {
542
731
  response: pushed.response,
543
732
  }
544
733
  : null,
545
- }));
546
- emitConsoleLiveEvent(consoleLiveEmitter, 'push', () => ({
547
- partitionId: ctx.partitionId,
548
- requestId: ctx.requestId,
549
- traceId: ctx.traceContext.traceId,
550
- spanId: ctx.traceContext.spanId,
551
- actorId: ctx.auth.actorId,
552
- clientId: ctx.clientId,
553
- transportPath: ctx.transportPath,
554
- syncPath: ctx.syncPath,
555
- outcome: pushed.response.status,
556
- statusCode: 200,
557
- durationMs: pushDurationMs,
558
- commitSeq: pushed.response.commitSeq ?? null,
559
- operationCount: pushOps.length,
560
- tables: pushed.affectedTables,
561
- }));
562
- if (execOptions.countConflictsMetric === true) {
563
- const detectedConflicts = pushed.response.results.reduce((count, result) => count + (result.status === 'conflict' ? 1 : 0), 0);
564
- if (detectedConflicts > 0) {
565
- countSyncMetric('sync.conflicts.detected', detectedConflicts, {
566
- attributes: {
567
- syncPath: ctx.syncPath,
568
- transportPath: ctx.transportPath,
569
- },
570
- });
571
- }
572
- }
573
- if (wsConnectionManager &&
574
- pushed.response.ok === true &&
575
- pushed.response.status === 'applied' &&
576
- typeof pushed.response.commitSeq === 'number') {
577
- const scopeKeys = applyPartitionToScopeKeys(ctx.partitionId, pushed.scopeKeys);
578
- if (scopeKeys.length > 0) {
579
- wsConnectionManager.notifyScopeKeys(scopeKeys, pushed.response.commitSeq, {
580
- excludeClientIds: [ctx.clientId],
581
- changes: pushed.emittedChanges,
582
- actorId: pushed.commitActorId ?? ctx.auth.actorId,
583
- createdAt: pushed.commitCreatedAt ?? new Date().toISOString(),
584
- });
585
- if (realtimeBroadcaster) {
586
- realtimeBroadcaster
587
- .publish({
588
- type: 'commit',
589
- commitSeq: pushed.response.commitSeq,
590
- partitionId: ctx.partitionId,
591
- scopeKeys,
592
- sourceInstanceId: instanceId,
593
- })
594
- .catch((error) => {
595
- logAsyncFailureOnce('sync.realtime.broadcast_publish_failed', {
596
- event: 'sync.realtime.broadcast_publish_failed',
597
- userId: ctx.auth.actorId,
598
- clientId: ctx.clientId,
599
- error: error instanceof Error ? error.message : String(error),
600
- });
601
- });
602
- }
603
- }
604
- }
605
- if (pushed.response.ok === true &&
606
- pushed.response.status === 'applied' &&
607
- typeof pushed.response.commitSeq === 'number') {
608
- emitConsoleLiveEvent(consoleLiveEmitter, 'commit', () => ({
609
- partitionId: ctx.partitionId,
610
- commitSeq: pushed.response.commitSeq,
611
- actorId: ctx.auth.actorId,
612
- clientId: ctx.clientId,
613
- affectedTables: pushed.affectedTables,
614
- }));
734
+ });
735
+ maybeCountPushConflicts(ctx, pushed.response.results, execOptions.countConflictsMetric);
736
+ if (execOptions.deferRealtimeNotifications !== true) {
737
+ notifyRealtimeForAppliedPushes(ctx, [pushed]);
615
738
  }
739
+ emitCommitLiveEvents(ctx, [pushed]);
616
740
  return pushed;
617
741
  }
618
742
  const authCache = new WeakMap();
@@ -939,24 +1063,59 @@ export function createSyncRoutes(options) {
939
1063
  let pullResponse;
940
1064
  // --- Push phase ---
941
1065
  if (body.push) {
942
- const pushBody = body.push;
943
- const pushOps = pushBody.operations ?? [];
944
- if (pushOps.length > maxOperationsPerPush) {
945
- return c.json({
946
- error: 'TOO_MANY_OPERATIONS',
947
- message: `Maximum ${maxOperationsPerPush} operations per push`,
948
- }, 400);
1066
+ const pushBodies = body.push.commits ?? [];
1067
+ const pushedCommits = [];
1068
+ for (const pushBody of pushBodies) {
1069
+ const pushOps = pushBody.operations ?? [];
1070
+ if (pushOps.length > maxOperationsPerPush) {
1071
+ return c.json({
1072
+ error: 'TOO_MANY_OPERATIONS',
1073
+ message: `Maximum ${maxOperationsPerPush} operations per push`,
1074
+ }, 400);
1075
+ }
949
1076
  }
950
- const pushed = await executePushCommitWithSideEffects({
951
- auth,
952
- clientId,
953
- partitionId,
954
- requestId,
955
- traceContext,
956
- transportPath,
957
- syncPath: 'http-combined',
958
- }, pushBody);
959
- pushResponse = pushed.response;
1077
+ const executedPushes = pushBodies.length > 1
1078
+ ? await executePushCommitBatchWithSideEffects({
1079
+ auth,
1080
+ clientId,
1081
+ partitionId,
1082
+ requestId,
1083
+ traceContext,
1084
+ transportPath,
1085
+ syncPath: 'http-combined',
1086
+ }, pushBodies, {
1087
+ countConflictsMetric: true,
1088
+ })
1089
+ : [];
1090
+ for (let index = 0; index < pushBodies.length; index += 1) {
1091
+ const pushBody = pushBodies[index];
1092
+ if (!pushBody)
1093
+ continue;
1094
+ const pushed = pushBodies.length > 1
1095
+ ? executedPushes[index]
1096
+ : await executePushCommitWithSideEffects({
1097
+ auth,
1098
+ clientId,
1099
+ partitionId,
1100
+ requestId,
1101
+ traceContext,
1102
+ transportPath,
1103
+ syncPath: 'http-combined',
1104
+ }, pushBody, {
1105
+ countConflictsMetric: true,
1106
+ });
1107
+ if (!pushed) {
1108
+ throw new Error('Server returned incomplete batched push result');
1109
+ }
1110
+ pushedCommits.push({
1111
+ clientCommitId: pushBody.clientCommitId,
1112
+ ...pushed.response,
1113
+ });
1114
+ }
1115
+ pushResponse = {
1116
+ ok: true,
1117
+ commits: pushedCommits,
1118
+ };
960
1119
  }
961
1120
  // --- Pull phase ---
962
1121
  if (body.pull) {