@ibgib/core-gib 0.1.18 → 0.1.20

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.
Files changed (83) hide show
  1. package/dist/common/other/ibgib-helper.d.mts +13 -0
  2. package/dist/common/other/ibgib-helper.d.mts.map +1 -1
  3. package/dist/common/other/ibgib-helper.mjs +44 -0
  4. package/dist/common/other/ibgib-helper.mjs.map +1 -1
  5. package/dist/sync/merge-info/merge-info-constants.d.mts +2 -0
  6. package/dist/sync/merge-info/merge-info-constants.d.mts.map +1 -0
  7. package/dist/sync/merge-info/merge-info-constants.mjs +2 -0
  8. package/dist/sync/merge-info/merge-info-constants.mjs.map +1 -0
  9. package/dist/sync/merge-info/merge-info-helpers.d.mts +51 -0
  10. package/dist/sync/merge-info/merge-info-helpers.d.mts.map +1 -0
  11. package/dist/sync/merge-info/merge-info-helpers.mjs +92 -0
  12. package/dist/sync/merge-info/merge-info-helpers.mjs.map +1 -0
  13. package/dist/sync/merge-info/merge-info-helpers.respec.d.mts +2 -0
  14. package/dist/sync/merge-info/merge-info-helpers.respec.d.mts.map +1 -0
  15. package/dist/sync/merge-info/merge-info-helpers.respec.mjs +32 -0
  16. package/dist/sync/merge-info/merge-info-helpers.respec.mjs.map +1 -0
  17. package/dist/sync/merge-info/merge-info-types.d.mts +26 -0
  18. package/dist/sync/merge-info/merge-info-types.d.mts.map +1 -0
  19. package/dist/sync/merge-info/merge-info-types.mjs +2 -0
  20. package/dist/sync/merge-info/merge-info-types.mjs.map +1 -0
  21. package/dist/sync/strategies/conflict-optimistic.d.mts +37 -0
  22. package/dist/sync/strategies/conflict-optimistic.d.mts.map +1 -0
  23. package/dist/sync/strategies/conflict-optimistic.mjs +162 -0
  24. package/dist/sync/strategies/conflict-optimistic.mjs.map +1 -0
  25. package/dist/sync/sync-conflict.respec.d.mts +8 -0
  26. package/dist/sync/sync-conflict.respec.d.mts.map +1 -0
  27. package/dist/sync/sync-conflict.respec.mjs +158 -0
  28. package/dist/sync/sync-conflict.respec.mjs.map +1 -0
  29. package/dist/sync/sync-innerspace-constants.respec.d.mts +8 -0
  30. package/dist/sync/sync-innerspace-constants.respec.d.mts.map +1 -0
  31. package/dist/sync/sync-innerspace-constants.respec.mjs +116 -0
  32. package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -0
  33. package/dist/sync/sync-innerspace-deep-updates.respec.mjs +2 -3
  34. package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
  35. package/dist/sync/sync-innerspace-dest-ahead.respec.mjs +3 -3
  36. package/dist/sync/sync-innerspace-dest-ahead.respec.mjs.map +1 -1
  37. package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs +2 -2
  38. package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs.map +1 -1
  39. package/dist/sync/sync-innerspace-partial-update.respec.d.mts +7 -0
  40. package/dist/sync/sync-innerspace-partial-update.respec.d.mts.map +1 -0
  41. package/dist/sync/sync-innerspace-partial-update.respec.mjs +116 -0
  42. package/dist/sync/sync-innerspace-partial-update.respec.mjs.map +1 -0
  43. package/dist/sync/sync-innerspace.respec.mjs +5 -5
  44. package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
  45. package/dist/sync/sync-saga-coordinator.d.mts +23 -12
  46. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  47. package/dist/sync/sync-saga-coordinator.mjs +612 -95
  48. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  49. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts +11 -0
  50. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -1
  51. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +24 -0
  52. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -1
  53. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +30 -0
  54. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -1
  55. package/dist/sync/sync-types.d.mts +31 -4
  56. package/dist/sync/sync-types.d.mts.map +1 -1
  57. package/dist/sync/sync-types.mjs.map +1 -1
  58. package/ibgib-foundations.md +129 -0
  59. package/package.json +1 -1
  60. package/roadmap.md +59 -0
  61. package/src/common/other/ibgib-helper.mts +52 -0
  62. package/src/keystone/README.md +13 -155
  63. package/src/keystone/docs/architecture.md +55 -0
  64. package/src/sync/README.md +37 -42
  65. package/src/sync/docs/architecture.md +69 -0
  66. package/src/sync/docs/verification.md +43 -0
  67. package/src/sync/merge-info/merge-info-constants.mts +1 -0
  68. package/src/sync/merge-info/merge-info-helpers.mts +134 -0
  69. package/src/sync/merge-info/merge-info-helpers.respec.mts +41 -0
  70. package/src/sync/merge-info/merge-info-types.mts +28 -0
  71. package/src/sync/strategies/conflict-optimistic.mts +208 -0
  72. package/src/sync/sync-conflict.respec.mts +194 -0
  73. package/src/sync/sync-innerspace-constants.respec.mts +133 -0
  74. package/src/sync/sync-innerspace-deep-updates.respec.mts +1 -2
  75. package/src/sync/sync-innerspace-dest-ahead.respec.mts +2 -2
  76. package/src/sync/sync-innerspace-multiple-timelines.respec.mts +1 -1
  77. package/src/sync/sync-innerspace-partial-update.respec.mts +150 -0
  78. package/src/sync/sync-innerspace.respec.mts +4 -4
  79. package/src/sync/sync-saga-coordinator.mts +673 -103
  80. package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +41 -0
  81. package/src/sync/sync-saga-message/sync-saga-message-types.mts +28 -0
  82. package/src/sync/sync-types.mts +33 -4
  83. package/tmp.md +2 -374
@@ -23,6 +23,7 @@ import { appendToTimeline, createTimeline, Rel8nInfo, Rel8nRemovalInfo } from ".
23
23
  import {
24
24
  SyncData_V1, SyncIbGib_V1, SyncInitData, SyncConflictStrategy,
25
25
  SyncMode,
26
+ SyncOptions,
26
27
  } from "./sync-types.mjs";
27
28
  import { getSyncIb, isPastFrame } from "./sync-helpers.mjs";
28
29
  import { getSyncSagaDependencyGraph } from "./sync-helpers.mjs";
@@ -41,6 +42,9 @@ import { createSyncSagaContext } from "./sync-saga-context/sync-saga-context-hel
41
42
  import { newupSubject } from "../common/pubsub/subject/subject-helper.mjs";
42
43
  import { SubjectWitness } from "../common/pubsub/subject/subject-types.mjs";
43
44
 
45
+ import { mergeDivergentTimelines } from "./strategies/conflict-optimistic.mjs";
46
+ import { getSyncSagaMessageFromFrame } from "./sync-saga-message/sync-saga-message-helpers.mjs";
47
+
44
48
 
45
49
  const logalot = GLOBAL_LOG_A_LOT || true;
46
50
 
@@ -81,20 +85,19 @@ export class SyncSagaCoordinator {
81
85
  */
82
86
  async sync({
83
87
  peer,
84
- localSpace,
88
+ localSpace: _localSpace,
89
+ source: _source,
85
90
  metaspace,
86
91
  domainIbGibs,
87
- useSessionIdentity,
88
- }: {
89
- peer: SyncPeerWitness,
90
- localSpace: IbGibSpaceAny,
91
- metaspace: MetaspaceService,
92
- domainIbGibs: IbGib_V1[],
93
- useSessionIdentity: boolean,
94
- }): Promise<SyncSagaInfo> {
92
+ conflictStrategy = 'abort',
93
+ useSessionIdentity = true,
94
+ }: SyncOptions): Promise<SyncSagaInfo> {
95
95
  const lc = `${this.lc}[${this.sync.name}]`;
96
96
  if (logalot) { console.log(`${lc} starting...`); }
97
97
 
98
+ const localSpace = (_source || _localSpace)!;
99
+ if (!localSpace) { throw new Error(`${lc} source (or localSpace) required (E: 8a9b0c1d)`); }
100
+
98
101
  // 1. SETUP SAGA METADATA
99
102
  const sagaId = await getUUID();
100
103
 
@@ -150,6 +153,7 @@ export class SyncSagaCoordinator {
150
153
  domainIbGibs,
151
154
  tempSpace,
152
155
  metaspace,
156
+ conflictStrategy
153
157
  });
154
158
 
155
159
  // 4. EXECUTE SAGA LOOP (FSM)
@@ -337,6 +341,17 @@ export class SyncSagaCoordinator {
337
341
 
338
342
  // C. Handle Response
339
343
  if (!responseCtx) {
344
+ // Check if we just sent a Commit frame. If so, peer's silence is success/expected.
345
+ if (currentFrame) {
346
+ const msg = await getSyncSagaMessageFromFrame({ frameIbGib: currentFrame, space: localSpace });
347
+ if (logalot) { console.log(`${lc} Checking currentFrame stage: ${msg?.data?.stage} (Expected: ${SyncStage.commit})`); }
348
+ if (msg?.data?.stage === SyncStage.commit) {
349
+ if (logalot) { console.log(`${lc} Sender sent Commit. Peer returned no response. Saga Complete.`); }
350
+ currentFrame = null;
351
+ break;
352
+ }
353
+ }
354
+
340
355
  throw new Error(`responseCtx falsy. Peer returned no response context (E: c099d8073b48d85e881f917835158f26)`);
341
356
  // console.warn(`${lc} Peer returned no response context. Ending loop.`);
342
357
  // currentFrame = null;
@@ -381,7 +396,7 @@ export class SyncSagaCoordinator {
381
396
  const result = await this.handleSagaFrame({
382
397
  sagaIbGib: remoteFrame as SyncIbGib_V1,
383
398
  srcGraph,
384
- space: tempSpace,
399
+ space: localSpace, // Must be localSpace (Source) to find domain data
385
400
  identity: sessionIdentity,
386
401
  metaspace
387
402
  });
@@ -397,6 +412,49 @@ export class SyncSagaCoordinator {
397
412
  return allReceivedIbGibs;
398
413
  }
399
414
 
415
+ /**
416
+ * Helper to get Knowledge Vector for specific domain ibGibs or TJPs.
417
+ * Useful for testing and external validation.
418
+ */
419
+ public async getKnowledgeVector({
420
+ space,
421
+ metaspace,
422
+ domainIbGibs,
423
+ tjpAddrs,
424
+ }: {
425
+ space: IbGibSpaceAny,
426
+ metaspace: MetaspaceService,
427
+ domainIbGibs?: IbGib_V1[],
428
+ tjpAddrs?: string[],
429
+ }): Promise<{ [tjp: string]: string | null }> {
430
+ const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
431
+ if (logalot) { console.log(`${lc} starting...`); }
432
+
433
+ let tjps: string[] = [];
434
+ if (tjpAddrs) {
435
+ tjps = tjpAddrs;
436
+ } else if (domainIbGibs && domainIbGibs.length > 0) {
437
+ // Extract TJPs from domain Ibgibs
438
+ const { mapWithTjp_YesDna, mapWithTjp_NoDna } = splitPerTjpAndOrDna({ ibGibs: domainIbGibs });
439
+ const allWithTjp = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
440
+ const timelineMap = getTimelinesGroupedByTjp({ ibGibs: allWithTjp });
441
+ tjps = Object.keys(timelineMap);
442
+ } else {
443
+ // No info provided. Return empty? Or throw?
444
+ // User test context implied "everything", but implementation requires scope.
445
+ console.warn(`${lc} No domainIbGibs or tjpAddrs provided. Returning empty KV.`);
446
+ return {};
447
+ }
448
+
449
+ if (tjps.length === 0) { return {}; }
450
+
451
+ const res = await getLatestAddrs({ space, tjpAddrs: tjps });
452
+ if (!res.data || !res.data.latestAddrsMap) {
453
+ throw new Error(`${lc} Failed to get latest addrs. (E: 7a8b9c0d)`);
454
+ }
455
+ return res.data.latestAddrsMap;
456
+ }
457
+
400
458
  protected async analyzeTimelines({
401
459
  domainIbGibs,
402
460
  space,
@@ -450,14 +508,16 @@ export class SyncSagaCoordinator {
450
508
  localSpace,
451
509
  domainIbGibs,
452
510
  tempSpace,
453
- metaspace
511
+ metaspace,
512
+ conflictStrategy,
454
513
  }: {
455
514
  sagaId: string,
456
515
  sessionIdentity?: KeystoneIbGib_V1,
457
516
  localSpace: IbGibSpaceAny,
458
517
  domainIbGibs: IbGib_V1[],
459
518
  tempSpace: IbGibSpaceAny,
460
- metaspace: MetaspaceService
519
+ metaspace: MetaspaceService,
520
+ conflictStrategy: SyncConflictStrategy,
461
521
  }): Promise<{ sagaFrame: SyncIbGib_V1, srcGraph: { [addr: string]: IbGib_V1 } }> {
462
522
  const lc = `${this.lc}[${this.createInitFrame.name}]`;
463
523
  try {
@@ -496,7 +556,8 @@ export class SyncSagaCoordinator {
496
556
  msgStones: [initStone],
497
557
  identity: sessionIdentity,
498
558
  space: tempSpace,
499
- metaspace
559
+ metaspace,
560
+ conflictStrategy,
500
561
  });
501
562
  if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
502
563
 
@@ -552,19 +613,19 @@ export class SyncSagaCoordinator {
552
613
 
553
614
  switch (stage) {
554
615
  case SyncStage.init:
555
- return await this.handleInitFrame({ sagaIbGib, messageData, space, metaspace, identity, identitySecret });
616
+ return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, space, identity, identitySecret });
556
617
 
557
618
  case SyncStage.ack:
558
- return await this.handleAckFrame({ sagaIbGib, srcGraph, space, metaspace, identity });
619
+ return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, space, identity });
559
620
 
560
621
  case SyncStage.delta:
561
- return await this.handleDeltaFrame({ sagaIbGib, srcGraph, space, identity, metaspace });
622
+ return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, space, identity, });
562
623
 
563
624
  case SyncStage.commit:
564
- return await this.handleCommitFrame({ sagaIbGib, space });
625
+ return await this.handleCommitFrame({ sagaIbGib, metaspace, space, identity, });
565
626
 
566
627
  case SyncStage.conflict:
567
- return await this.handleConflictFrame({ sagaIbGib, space });
628
+ return await this.handleConflictFrame({ sagaIbGib, metaspace, space, });
568
629
 
569
630
  default:
570
631
  throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
@@ -610,14 +671,40 @@ export class SyncSagaCoordinator {
610
671
  if (logalot) { console.log(`${lc} starting...`); }
611
672
 
612
673
  // Extract Init Data
613
- const initData = messageData as SyncInitData; // Using renamed variable for clarity
674
+ const initData = messageData as SyncSagaMessageInitData_V1; // Using renamed variable for clarity
675
+ if (initData.stage !== SyncStage.init) {
676
+ throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
677
+ }
614
678
  if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
615
679
  if (!initData || !initData.knowledgeVector) {
616
680
  throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
617
681
  }
618
682
 
619
- // Stones Analysis
620
- // TODO: Handle stones separately if needed, though they are usually dependencies of timelines.
683
+ // Determine Strategy from Saga Data (since V1 stores it in root)
684
+ const conflictStrategy = sagaIbGib.data!.conflictStrategy || 'abort';
685
+
686
+ // 2. Gap Analysis
687
+ const conflicts: { tjpAddr: string, localAddr: string, remoteAddr: string, timelineAddrs: string[], reason: string, terminal: boolean }[] = [];
688
+
689
+ const deltaReqAddrs: string[] = [];
690
+ const pushOfferAddrs: string[] = [];
691
+
692
+ // Stones Analysis (Constants / Non-TJPs)
693
+ const stones = initData.stones || [];
694
+ if (stones.length > 0) {
695
+ if (logalot) { console.log(`${lc} processing stones: ${stones.length}`); }
696
+ // Check if we have these stones
697
+ const resStones = await getFromSpace({ space, addrs: stones });
698
+ const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
699
+ if (addrsNotFound && addrsNotFound.length > 0) {
700
+ if (logalot) { console.log(`${lc} stones missing (requesting): ${addrsNotFound.length}`); }
701
+ addrsNotFound.forEach(addr => {
702
+ if (!deltaReqAddrs.includes(addr)) {
703
+ deltaReqAddrs.push(addr);
704
+ }
705
+ });
706
+ }
707
+ }
621
708
 
622
709
  const remoteKV = initData.knowledgeVector;
623
710
  if (logalot) { console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`); }
@@ -639,11 +726,7 @@ export class SyncSagaCoordinator {
639
726
  }
640
727
 
641
728
  // 2. Gap Analysis
642
- const conflicts: { tjp: string, localAddr?: string, remoteAddr: string, reason: string }[] = [];
643
- const conflictStrategy: SyncConflictStrategy = initData.conflictStrategy || 'abort'; // Default to abort if not specified, or we should parameterize this
644
729
 
645
- const deltaReqAddrs: string[] = [];
646
- const pushOfferAddrs: string[] = [];
647
730
 
648
731
  for (const tjp of remoteTjps) {
649
732
  const remoteAddr = remoteKV[tjp];
@@ -686,21 +769,47 @@ export class SyncSagaCoordinator {
686
769
  deltaReqAddrs.push(remoteAddr);
687
770
  } else {
688
771
  // DIVERGENCE: Both have changes the other doesn't know about.
689
- conflicts.push({ tjp, localAddr, remoteAddr, reason: 'divergence' });
690
-
691
- // Conflict Strategy Handling
692
- // TODO: Implement "manual" strategy (requires user intervention/pause)
693
- // TODO: Implement "local_wins" strategy
694
- // TODO: Implement "server_wins" strategy
695
772
 
696
773
  if (conflictStrategy === 'abort') {
697
- // We will abort immediately after this loop check if any conflicts exist
774
+ // Abort Strategy: We will treat this as terminal.
775
+ // But for Unified Ack, we just mark it terminal in the list?
776
+ // Or do we actually throw/abort the saga?
777
+ // Current logic (below) aborts the saga if ANY conflict is terminal/abort.
778
+ conflicts.push({
779
+ tjpAddr: tjp,
780
+ localAddr: localAddr!,
781
+ remoteAddr,
782
+ timelineAddrs: [], // Not needed for abort
783
+ reason: 'divergence',
784
+ terminal: true
785
+ });
698
786
  } else if (conflictStrategy === 'optimistic') {
699
- // Optimistic: We Try to resolve by syncing timeline-by-timeline.
700
- // We request the remote data (deltaReq) and will attempt to merge later?
701
- // OR we treat it as a delta request for now, hoping the merge logic handles it?
702
- // For now, we request the remote tip.
703
- deltaReqAddrs.push(remoteAddr);
787
+ // Optimistic: We want to resolving this.
788
+ // We need to send our history to the Sender so they can Merge.
789
+
790
+ // Fetch Full History for Local Timeline
791
+ // Note: We might optimize this to only send "recent" history if we had a KV?
792
+ // But for now, get full past.
793
+ // Optimization: localKV might not have full history.
794
+ // We need to inspect the 'past' of the local tip.
795
+
796
+ // We need the ACTUAL object to get the past.
797
+ // We have localAddr.
798
+ const resLocalTip = await getFromSpace({ space, addr: localAddr! });
799
+ const localTip = resLocalTip.ibGibs?.[0];
800
+ if (!localTip) { throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`); }
801
+
802
+ const timelineAddrs = [localAddr!, ...(localTip.rel8ns?.past || [])];
803
+
804
+ conflicts.push({
805
+ tjpAddr: tjp,
806
+ localAddr: localAddr!,
807
+ remoteAddr,
808
+ timelineAddrs,
809
+ reason: 'divergence',
810
+ terminal: false
811
+ });
812
+
704
813
  } else {
705
814
  throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
706
815
  }
@@ -708,36 +817,98 @@ export class SyncSagaCoordinator {
708
817
  }
709
818
  }
710
819
 
711
- if (conflicts.length > 0 && conflictStrategy === 'abort') {
820
+ // Check if we should ABORT (if any conflict is terminal)
821
+ const hasTerminalConflicts = conflicts.some(c => c.terminal);
822
+
823
+ if (hasTerminalConflicts) {
712
824
  // Abort Strategy: Kill the saga.
713
- if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to conflicts: ${JSON.stringify(conflicts)}`); }
825
+ if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to terminal conflicts: ${JSON.stringify(conflicts)}`); }
714
826
 
715
- const sagaId = sagaIbGib.data!.uuid;
716
- const conflictData: SyncSagaMessageConflictData_V1 = {
717
- sagaId,
718
- stage: SyncStage.conflict,
719
- conflictStrategy,
720
- isTerminal: true,
721
- conflicts,
722
- };
827
+ // We reuse the ConflictData structure for terminal aborts?
828
+ // Or do we send an Ack with terminal conflicts?
829
+ // Original design had explicit Conflict Frame for Abort.
830
+ // Let's stick to that for purely terminal cases to be safe/explicit?
831
+ // Or Unified: Just send Ack with terminal=true conflicts. Sender sees them and aborts.
723
832
 
724
- const conflictStone = await this.createSyncMsgStone({
725
- data: conflictData,
726
- space,
727
- metaspace,
728
- });
833
+ // Decision: Unified Ack for everything is cleaner protocol.
834
+ // But wait, the original code below creates a Conflict Stone.
835
+ // Let's preserve the explicit 'Conflict' frame for total aborts if that's easier,
836
+ // OR fully switch to Ack.
837
+ // Protocol states: Init -> Ack. If Ack contains terminal errors, Sender can Commit(Fail).
729
838
 
730
- const conflictFrame = await this.evolveSyncSagaIbGib({
731
- prevSagaIbGib: sagaIbGib,
732
- msgStones: [conflictStone],
733
- identity,
734
- space,
735
- metaspace,
736
- });
839
+ // Let's use Ack with conflicts.
840
+ }
737
841
 
738
- return { frame: conflictFrame };
842
+ // 2. Add Push Offers (Missing in Local)
843
+ // Check if we have them. If not, ask for them.
844
+ for (const addr of pushOfferAddrs) {
845
+ const hasIt = await getFromSpace({ addr, space });
846
+ if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
847
+ // If we don't have it, we put it in `deltaReqAddrs` of the Ack.
848
+ deltaReqAddrs.push(addr);
849
+ }
739
850
  }
740
851
 
852
+ // 3. Build Knowledge Vector (Full History for known timelines)
853
+ // [NEW] Smart Diff
854
+ // We iterate over all relevant addresses (deltas we are requesting OR push offers we might have newer versions of).
855
+ // Since we are "reacting" to Init, we primarily want to tell the Sender what we DO have for the things they talked about.
856
+
857
+ const knowledgeVector: { [groupKey: string]: string[] } = {};
858
+ const relevantAddrs = new Set([...pushOfferAddrs, ...deltaReqAddrs]);
859
+
860
+ // [Smart Diff] Populate knowledge from timelines identified by Sender
861
+ for (const tjp of remoteTjps) {
862
+ const localAddr = localKV[tjp];
863
+ if (localAddr) {
864
+ const res = await getFromSpace({ addr: localAddr, space });
865
+ if (res.success && res.ibGibs?.[0]) {
866
+ const ibGib = res.ibGibs[0];
867
+ const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized
868
+ if (!knowledgeVector[realTjp]) {
869
+ const past = ibGib.rel8ns?.past || [];
870
+ knowledgeVector[realTjp] = [getIbGibAddr({ ibGib }), ...past];
871
+ }
872
+ }
873
+ }
874
+ }
875
+
876
+ // [Smart Diff] Also check individual requested addresses (Fall back for constants/unknown TJPs)
877
+ for (const addr of relevantAddrs) {
878
+ // Only process if not already covered by TJP logic above
879
+ // We can't really know if it's covered easily without resolving.
880
+ // But if we don't have it (requesting), we won't find it here anyway.
881
+ // If we DO have it (push offer), we might find it.
882
+ const res = await getFromSpace({ addr, space });
883
+ if (res.success && res.ibGibs?.[0]) {
884
+ const ibGib = res.ibGibs[0];
885
+ const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
886
+
887
+ if (!knowledgeVector[tjpAddr]) {
888
+ const past = ibGib.rel8ns?.past || [];
889
+ knowledgeVector[tjpAddr] = [getIbGibAddr({ ibGib }), ...past];
890
+ }
891
+ } else {
892
+ // We don't have `addr`.
893
+ }
894
+ }
895
+
896
+ // Also populate from `knowledgeVector` in Init if we want bidirectional?
897
+ // No, `Init` doesn't have `knowledgeVector` in `SyncSagaMessageInitData` yet (it has `SyncInitData` generic props).
898
+ // Let's assume standard flow:
899
+ // 1. Sender says "I have X"
900
+ // 2. Receiver says "I don't have X. But if I did have Y (ancestor), I'd tell you."
901
+ // Problem: Receiver doesn't know X is related to Y without X.
902
+
903
+ // SOLUTION:
904
+ // The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
905
+ // `SyncSagaMessageInitData_V1` extends `SyncInitData`.
906
+ // `SyncInitData` has `knowledgeVector`.
907
+ // If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
908
+
909
+ // Let's implement Sender populating `Init.knowledgeVector`.
910
+ // But `SyncSagaCoordinator.startSaga` creates Init.
911
+
741
912
  // 3. Create Ack Frame
742
913
  const sagaId = sagaIbGib.data!.uuid;
743
914
  // Create Payload Stone
@@ -746,6 +917,7 @@ export class SyncSagaCoordinator {
746
917
  stage: SyncStage.ack,
747
918
  deltaReqAddrs,
748
919
  pushOfferAddrs,
920
+ knowledgeVector,
749
921
  };
750
922
 
751
923
  const ackStone = await this.createSyncMsgStone({
@@ -806,6 +978,148 @@ export class SyncSagaCoordinator {
806
978
  if (ackData.stage !== SyncStage.ack) {
807
979
  throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
808
980
  }
981
+ if (logalot) { console.log(`${lc} ackData: ${pretty(ackData)} (I: 7f8e9d0a1b2c3d4e5f6g7h8i9j0k)`); }
982
+
983
+ // 1. Check for Conflicts
984
+ const conflicts = ackData.conflicts || [];
985
+ const terminalConflicts = conflicts.filter(c => c.terminal);
986
+ if (terminalConflicts.length > 0) {
987
+ console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
988
+ // Terminal failure. Sender should probably Commit(Fail) or just Abort.
989
+ // For now, throw to trigger abort.
990
+ throw new Error(`${lc} Peer reported terminal conflicts. (E: a1b2c3d4e5f6g7h8i9j0k)`);
991
+ }
992
+
993
+ const optimisticConflicts = conflicts.filter(c => !c.terminal);
994
+ const mergeDeltaReqs: string[] = []; // Additional requests for merging
995
+
996
+ if (optimisticConflicts.length > 0) {
997
+ if (logalot) { console.log(`${lc} Processing optimistic conflicts: ${optimisticConflicts.length}`); }
998
+ // We need to resolve these.
999
+ // Strategy:
1000
+ // 1. Analyze Divergence (Sender vs Receiver)
1001
+ // 2. Identify missing data needed for merge (Receiver's unique frames)
1002
+ // 3. Request that data (as Delta Reqs)
1003
+ // 4. (Later in Delta Phase) Perform Merge.
1004
+
1005
+ // BUT: The Delta Phase is usually generic "Send me these Addrs".
1006
+ // If we just add to `deltaReqAddrs` (which are requests for Sender to send to Receiver?),
1007
+ // wait. `ackData.deltaReqAddrs` are what RECEIVER wants from SENDER.
1008
+
1009
+ // We (Sender) are processing the Ack.
1010
+ // We need to request data FROM Receiver.
1011
+ // But the protocol 'Ack' step typically leads to 'Delta' (Sender sending data).
1012
+
1013
+ // If Sender needs data from Receiver, it usually happens in 'Pull' mode or a separate request?
1014
+ // Or can we send a 'Delta Request' frame?
1015
+ // Standard Saga: Init(Push) -> Ack(Pull Reqs) -> Delta(Push Data).
1016
+
1017
+ // If Sender needs data, we might need a "Reverse Delta" or "Pull" phase?
1018
+ // Or we just proceed to Delta (sending what Receiver wants),
1019
+ // AND we piggyback our own requests?
1020
+ // OR: We treat the Conflict Resolution as a sub-saga or side-effect?
1021
+
1022
+ // SIMPLIFICATION for V1:
1023
+ // If we need data to merge, we must get it.
1024
+ // We are the Coordinator (Active). We can fetch from Peer immediately?
1025
+ // `peer.pull(addr)`?
1026
+ // Yes! The Coordinator has the `peer`.
1027
+
1028
+ // Let's analyze and pull immediately.
1029
+
1030
+ for (const conflict of optimisticConflicts) {
1031
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
1032
+
1033
+ // Sender History
1034
+ // We need our own history for this timeline.
1035
+ // We know the 'senderTip' (remoteAddr in Ack).
1036
+ // Sender should verify it has this tip.
1037
+
1038
+ // Compute Diffs
1039
+ // We need to find `receiverOnly` addrs.
1040
+ // Receiver sent us `timelineAddrs` (Full History).
1041
+ const receiverHistorySet = new Set(timelineAddrs);
1042
+
1043
+ // We need our execution context's history for this senderTip.
1044
+ // We can fetch valid 'past' from space.
1045
+ const resSenderTip = await getFromSpace({ space, addr: senderTip });
1046
+ const senderTipIbGib = resSenderTip.ibGibs?.[0];
1047
+ if (!senderTipIbGib) { throw new Error(`${lc} Sender missing its own tip? ${senderTip} (E: 9c8d7e6f5g4h3i2j1k0l)`); }
1048
+
1049
+ // Basic Diff: Find what Receiver has that we don't.
1050
+ // Actually, we need to traverse OUR past to find commonality.
1051
+ const senderHistory = [senderTip, ...(senderTipIbGib.rel8ns?.past || [])];
1052
+
1053
+ const receiverOnlyAddrs = timelineAddrs.filter(addr => !senderHistory.includes(addr));
1054
+
1055
+ if (receiverOnlyAddrs.length > 0) {
1056
+ if (logalot) { console.log(`${lc} Pulling divergent history from Receiver: ${receiverOnlyAddrs.length} frames`); }
1057
+ // PULL these frames from Peer into Local Space
1058
+ // (Validation: We trust peer for now / verification happens on put)
1059
+ for (const addr of receiverOnlyAddrs) {
1060
+ // This 'pull' is a sync-peer method?
1061
+ // The Coordinator 'peer' passed in 'sync()' might be needed here?
1062
+ // Wait, `handleAckFrame` doesn't have reference to `peer`?
1063
+ // It only has `space`, `metaspace`.
1064
+ // The `peer` is held by the `executeSagaLoop`.
1065
+
1066
+ // PROBLEM: `handleAckFrame` is pure logic on the Space/Data?
1067
+ // No, it's a method on Coordinator.
1068
+ // But `executeSagaLoop` calls it.
1069
+ // We might need to return "Requirements" to the loop?
1070
+
1071
+ // Checking return type: `{ frame: SyncIbGib_V1, payloadIbGibs?: ... }`
1072
+ // It returns the NEXT frame (Delta).
1073
+
1074
+ // If we need to fetch data, we are blocked.
1075
+ // We can't easily "Pull" here without the Peer reference.
1076
+
1077
+ // OPTION A: Pass `peer` to `handleAckFrame`.
1078
+ // OPTION B: Return a strict list of "MissingDeps" and let Loop handle it.
1079
+
1080
+ // Let's assume we can resolve this by adding `peer` to signature or using `metaspace` if it's a peer-witness?
1081
+ // No, Peer is ephemeral connection.
1082
+
1083
+ // Let's add `peer` to `handleAckFrame` signature?
1084
+ // It breaks the pattern of just handling frame + space.
1085
+
1086
+ // ALTERNATIVE: Use the `Delta` frame to request data?
1087
+ // `SyncSagaMessageDeltaData` has `requests?: string[]`.
1088
+ // Sender sends Delta Frame.
1089
+ // Does Receiver handle Delta Requests?
1090
+ // `handleDeltaFrame` (Receiver) -> checks `requests`.
1091
+ // YES.
1092
+
1093
+ // So Sender puts `receiverOnlyAddrs` into `deltaFrame.requests`.
1094
+ // Receiver sees them, fetches them, and includes them in the Response (Commit?).
1095
+ // Wait, Init->Ack->Delta->Commit.
1096
+ // If Receiver sends data in Commit, that's "too late" for Sender to Merge in THIS saga round?
1097
+ // Unless Commit is not the end?
1098
+
1099
+ // Or we do a "Delta 2" loop?
1100
+
1101
+ // "Iterative Resolution Loop" from plan.
1102
+ // If we request data in Delta, Receiver sends it in Commit (or Delta-Response).
1103
+ // Sender gets Commit. Sees data. Merges.
1104
+ // Then Sender needs to Send the MERGE result.
1105
+ // Needs another Push/Delta.
1106
+
1107
+ // REFINED FLOW:
1108
+ // 1. Sender sends Delta Frame with `requests: [receiverOnlyAddrs]`.
1109
+ // 2. Receiver responds (Commit? or Ack 2?) with Payload (Divergent Frames).
1110
+ // 3. Sender handles response -> Merges.
1111
+ // 4. Sender sends Commit (containing Merge Frame).
1112
+
1113
+ // Issue: Current state machine is Init->Ack->Delta->Commit.
1114
+ // We need to keep Saga open.
1115
+ // If Sender sends Delta with requests, does it transition to Commit?
1116
+ }
1117
+ mergeDeltaReqs.push(...receiverOnlyAddrs);
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ // 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
809
1123
 
810
1124
  const deltaReqAddrs = ackData.deltaReqAddrs || [];
811
1125
  const pushOfferAddrs = ackData.pushOfferAddrs || [];
@@ -820,7 +1134,17 @@ export class SyncSagaCoordinator {
820
1134
  }
821
1135
 
822
1136
  // 2. Process Delta Requests (Push Payload)
1137
+ // [NEW] Smart Diff: Use knowledgeVector to skip dependencies
1138
+ const skipAddrs = new Set<string>();
1139
+ if (ackData.knowledgeVector) {
1140
+ Object.values(ackData.knowledgeVector).forEach(addrs => {
1141
+ addrs.forEach(a => skipAddrs.add(a));
1142
+ });
1143
+ }
1144
+
823
1145
  const payloadIbGibs: IbGib_V1[] = [];
1146
+ // Gather all tips to sync first
1147
+ const tipsToSync: IbGib_V1[] = [];
824
1148
  for (const addr of deltaReqAddrs) {
825
1149
  let ibGib = srcGraph[addr];
826
1150
  if (!ibGib) {
@@ -830,19 +1154,65 @@ export class SyncSagaCoordinator {
830
1154
  }
831
1155
  }
832
1156
  if (ibGib) {
833
- payloadIbGibs.push(ibGib);
1157
+ tipsToSync.push(ibGib);
834
1158
  } else {
835
1159
  throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
836
1160
  }
837
1161
  }
838
1162
 
1163
+ // Calculate Dependency Graph for ALL tips, effectively utilizing common history
1164
+ // Pass skipAddrs to `getDependencyGraph` or gather manually.
1165
+ // `getDependencyGraph` takes a single ibGib.
1166
+ // We can optimize by doing it for each tip and unioning the result?
1167
+ // Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
1168
+ // We will loop.
1169
+
1170
+ const allDepsSet = new Set<string>();
1171
+
1172
+ for (const tip of tipsToSync) {
1173
+ // Always include the tip itself
1174
+ const tipAddr = getIbGibAddr({ ibGib: tip });
1175
+ // Only process if not skipped (though deltaReq implies they barely just asked for it)
1176
+ // But detailed deps might be skipped.
1177
+
1178
+ // Get Graph with Skips
1179
+ // Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
1180
+ const deps = await getDependencyGraph({
1181
+ ibGib: tip,
1182
+ space,
1183
+ skipAddrs: Array.from(skipAddrs)
1184
+ });
1185
+
1186
+ // [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
1187
+ let tipIncluded = false;
1188
+
1189
+ if (deps) {
1190
+ Object.values(deps).forEach(d => {
1191
+ const dAddr = getIbGibAddr({ ibGib: d });
1192
+ if (!allDepsSet.has(dAddr)) {
1193
+ allDepsSet.add(dAddr);
1194
+ payloadIbGibs.push(d);
1195
+ }
1196
+ if (dAddr === tipAddr) { tipIncluded = true; }
1197
+ });
1198
+ }
1199
+
1200
+ if (!tipIncluded && !skipAddrs.has(tipAddr)) {
1201
+ if (logalot) { console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`); }
1202
+ if (!allDepsSet.has(tipAddr)) {
1203
+ allDepsSet.add(tipAddr);
1204
+ payloadIbGibs.push(tip);
1205
+ }
1206
+ }
1207
+ }
1208
+
839
1209
  // 3. Create Delta Frame
840
1210
  const sagaId = ackData.sagaId;
841
1211
  const deltaData: SyncSagaMessageDeltaData_V1 = {
842
- sagaId,
1212
+ sagaId: sagaIbGib.data!.uuid,
843
1213
  stage: SyncStage.delta,
844
1214
  payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
845
- requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
1215
+ requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
846
1216
  };
847
1217
 
848
1218
  const deltaStone = await this.createSyncMsgStone({
@@ -897,25 +1267,37 @@ export class SyncSagaCoordinator {
897
1267
  if (deltaData.stage !== SyncStage.delta) {
898
1268
  throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
899
1269
  }
1270
+ if (logalot) { console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`); }
900
1271
 
901
1272
  const payloadAddrs = deltaData.payloadAddrs || [];
902
- const requests = deltaData.requests || [];
1273
+ const peerRequests = deltaData.requests || [];
1274
+ const peerProposesCommit = deltaData.proposeCommit || false;
903
1275
 
904
- // 1. Process Received Payload
1276
+ // 1. Process Received Payload (Ingest)
905
1277
  const receivedPayloadIbGibs: IbGib_V1[] = [];
906
1278
  if (payloadAddrs.length > 0) {
1279
+ // We use `payloadAddrs` as the manifest.
1280
+ // The ACTUAL collection of ibGibs should be available via `getFromSpace`
1281
+ // assuming the "Transport" layer put them there implicitly?
1282
+ // OR, if we are local-only, we just get them.
1283
+ // The `handleDeltaFrame` contract assumes data is reachable in `space`.
1284
+
907
1285
  const res = await getFromSpace({
908
1286
  addrs: payloadAddrs,
909
1287
  space,
910
1288
  });
911
1289
  if (res.ibGibs) {
912
1290
  receivedPayloadIbGibs.push(...res.ibGibs);
1291
+ // Also put them? `getFromSpace` retrieves. If they are in space, they are persisted.
1292
+ // If this is a Temp Space, they are safe.
1293
+ } else {
1294
+ console.warn(`${lc} Failed to retrieve payloads listed in delta: ${payloadAddrs.join(', ')}`);
913
1295
  }
914
1296
  }
915
1297
 
916
- // 2. Fulfill Requests (Outgoing Payload)
1298
+ // 2. Fulfill Peer Requests (Outgoing Payload)
917
1299
  const outgoingPayload: IbGib_V1[] = [];
918
- for (const addr of requests) {
1300
+ for (const addr of peerRequests) {
919
1301
  let ibGib = srcGraph[addr];
920
1302
  if (!ibGib) {
921
1303
  const res = await getFromSpace({ addr, space });
@@ -925,19 +1307,116 @@ export class SyncSagaCoordinator {
925
1307
  }
926
1308
  if (ibGib) {
927
1309
  outgoingPayload.push(ibGib);
1310
+ } else {
1311
+ console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
928
1312
  }
929
1313
  }
930
1314
 
931
- // 3. Determine Next Stage
932
- if (requests.length > 0) {
933
- // They requested more data -> Send Delta
934
- const sagaId = deltaData.sagaId;
1315
+ // 3. Execute Merges (If applicable)
1316
+ // Check if we have pending conflicts that we CAN resolve now that we have data.
1317
+ // We look at the Saga History (Ack Frame) to find conflicts.
1318
+ // Optimization: Do this only if we received payloads.
1319
+ const mergeResultIbGibs: IbGib_V1[] = [];
1320
+
1321
+ if (receivedPayloadIbGibs.length > 0) {
1322
+ // Find the Ack frame in history to get conflicts
1323
+ // Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
1324
+ // V1 timelines carry full history in `past`.
1325
+ const pastAddrs = sagaIbGib.rel8ns?.past || [];
1326
+ let ackData: SyncSagaMessageAckData_V1 | undefined;
1327
+
1328
+ if (pastAddrs.length > 0) {
1329
+ // Batch fetch all past frames
1330
+ const resPast = await getFromSpace({ addrs: pastAddrs, space });
1331
+ if (resPast.success && resPast.ibGibs) {
1332
+ // Iterate backwards (most recent first) to find the latest Ack
1333
+ for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
1334
+ const pastFrame = resPast.ibGibs[i];
1335
+ const messageStone = await getSyncSagaMessageFromFrame({
1336
+ frameIbGib: pastFrame,
1337
+ space
1338
+ });
1339
+ if (messageStone?.data?.stage === SyncStage.ack) {
1340
+ ackData = messageStone.data as SyncSagaMessageAckData_V1;
1341
+ break;
1342
+ }
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ if (ackData && ackData.conflicts) {
1348
+ const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
1349
+ for (const conflict of optimisticConflicts) {
1350
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
1351
+ // We are Sender (usually) here if we are merging.
1352
+ // Check if we have the history needed (timelineAddrs).
1353
+ // Specifically, we needed the `receiverOnly` parts.
1354
+
1355
+ // We blindly attempt merge if we have both tips accessible?
1356
+ // We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
1357
+
1358
+ // Check if we have receiverTip in space
1359
+ const resRecTip = await getFromSpace({ addr: receiverTip, space });
1360
+ if (resRecTip.success && resRecTip.ibGibs?.[0]) {
1361
+ // We have the tip!
1362
+ // Do we have the full history?
1363
+ // `mergeDivergentTimelines` in `conflict-optimistic` will attempt to fetch history.
1364
+ // If we just ingested the missing pieces, `getFromSpace` inside `merge` should succeed.
1365
+
1366
+ // Perform Merge!
1367
+ try {
1368
+ const mergeResult = await mergeDivergentTimelines({
1369
+ tipA: (await getFromSpace({ addr: senderTip, space })).ibGibs![0], // Our tip
1370
+ tipB: resRecTip.ibGibs[0], // Their tip
1371
+ space,
1372
+ metaspace,
1373
+ });
1374
+ if (mergeResult) {
1375
+ if (logalot) { console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
1376
+ mergeResultIbGibs.push(mergeResult);
1377
+ outgoingPayload.push(mergeResult); // Send result to peer
1378
+ }
1379
+ } catch (e) {
1380
+ console.error(`${lc} Merge failed: ${e}`);
1381
+ // If merge fails, we might Abort or just continue?
1382
+ }
1383
+ }
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ // 4. Determine Next Action
1389
+ // We have `outgoingPayload` (Requests + Merge Results).
1390
+ // Does Peer have outstanding requests? No, we fulfilled `peerRequests`.
1391
+ // Do WE have outstanding requests?
1392
+ // We might if `mergeResult` requires further sync? Usually no, result is complete.
1393
+
1394
+ const myRequests: string[] = []; // If we had more needs (e.g. partial payload), we'd add here.
1395
+
1396
+ const hasOutgoing = outgoingPayload.length > 0;
1397
+ const hasMyRequests = myRequests.length > 0;
1398
+
1399
+ if (hasOutgoing || hasMyRequests) {
1400
+ // We have business to attend to -> Send Delta
935
1401
  const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
936
- sagaId,
1402
+ sagaId: deltaData.sagaId,
937
1403
  stage: SyncStage.delta,
938
1404
  payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
1405
+ requests: hasMyRequests ? myRequests : undefined,
1406
+ proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
1407
+ // Wait. If we send data, we are NOT committing yet.
1408
+ // We are sending data. The OTHER side must ingest it.
1409
+ // So proposeCommit = true?
1410
+ // "Here is the data. I'm done. If you are good, let's commit."
1411
+ // Yes.
939
1412
  };
940
1413
 
1414
+ // BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
1415
+ // We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
1416
+
1417
+ // So yes, proposeCommit = true.
1418
+ responseDeltaData.proposeCommit = true;
1419
+
941
1420
  const deltaStone = await this.createSyncMsgStone({
942
1421
  data: responseDeltaData,
943
1422
  space,
@@ -955,49 +1434,126 @@ export class SyncSagaCoordinator {
955
1434
  return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
956
1435
 
957
1436
  } else {
958
- // No requests -> Commit
959
- const sagaId = deltaData.sagaId;
960
- const commitData: SyncSagaMessageCommitData_V1 = {
961
- sagaId,
962
- stage: SyncStage.commit,
963
- success: true,
964
- };
1437
+ // We have nothing to send.
1438
+
1439
+ if (peerProposesCommit) {
1440
+ // Peer is done. We are done. -> Commit.
1441
+ const commitData: SyncSagaMessageCommitData_V1 = {
1442
+ sagaId: deltaData.sagaId,
1443
+ stage: SyncStage.commit,
1444
+ success: true,
1445
+ };
965
1446
 
966
- const commitStone = await this.createSyncMsgStone({
967
- data: commitData,
968
- space,
969
- metaspace
970
- });
1447
+ const commitStone = await this.createSyncMsgStone({
1448
+ data: commitData,
1449
+ space,
1450
+ metaspace
1451
+ });
971
1452
 
972
- const commitFrame = await this.evolveSyncSagaIbGib({
973
- prevSagaIbGib: sagaIbGib,
974
- msgStones: [commitStone],
975
- identity,
976
- space,
977
- metaspace
978
- });
1453
+ const commitFrame = await this.evolveSyncSagaIbGib({
1454
+ prevSagaIbGib: sagaIbGib,
1455
+ msgStones: [commitStone],
1456
+ identity,
1457
+ space,
1458
+ metaspace
1459
+ });
1460
+
1461
+ return { frame: commitFrame, receivedPayloadIbGibs };
1462
+
1463
+ } else {
1464
+ // peer did NOT propose commit (maybe they just sent data/requests and didn't ready flag).
1465
+ // But we are empty.
1466
+ // So WE propose commit.
1467
+ const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
1468
+ sagaId: deltaData.sagaId,
1469
+ stage: SyncStage.delta,
1470
+ proposeCommit: true,
1471
+ payloadAddrs: [], // Always include empty array if sending delta
1472
+ };
979
1473
 
980
- return { frame: commitFrame, receivedPayloadIbGibs };
1474
+ const deltaStone = await this.createSyncMsgStone({
1475
+ data: responseDeltaData,
1476
+ space,
1477
+ metaspace
1478
+ });
1479
+
1480
+ const deltaFrame = await this.evolveSyncSagaIbGib({
1481
+ prevSagaIbGib: sagaIbGib,
1482
+ msgStones: [deltaStone],
1483
+ identity,
1484
+ space,
1485
+ metaspace
1486
+ });
1487
+
1488
+ // Check if PEER proposed commit
1489
+ if (deltaData.proposeCommit) {
1490
+ if (logalot) { console.log(`${lc} Peer proposed commit. Accepting & Committing.`); }
1491
+ // Peer wants to commit and has no more requests.
1492
+ // We should Commit.
1493
+
1494
+ const commitData: SyncSagaMessageCommitData_V1 = {
1495
+ sagaId: deltaData.sagaId,
1496
+ stage: SyncStage.commit,
1497
+ success: true,
1498
+ };
1499
+
1500
+ const commitStone = await this.createSyncMsgStone({
1501
+ data: commitData,
1502
+ space,
1503
+ metaspace
1504
+ });
1505
+
1506
+ const commitFrame = await this.evolveSyncSagaIbGib({
1507
+ prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
1508
+ msgStones: [commitStone],
1509
+ identity,
1510
+ space,
1511
+ metaspace
1512
+ });
1513
+
1514
+ return { frame: commitFrame, receivedPayloadIbGibs };
1515
+ }
1516
+
1517
+ return { frame: deltaFrame, receivedPayloadIbGibs };
1518
+ }
981
1519
  }
982
1520
  }
983
1521
 
1522
+
984
1523
  protected async handleCommitFrame({
985
1524
  sagaIbGib,
986
1525
  space,
1526
+ metaspace,
987
1527
  }: {
988
1528
  sagaIbGib: SyncIbGib_V1,
989
1529
  space: IbGibSpaceAny,
1530
+ metaspace: MetaspaceService,
1531
+ identity?: KeystoneIbGib_V1,
990
1532
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
991
1533
  const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
992
- if (logalot) { console.log(`${lc} Commit received. Saga complete.`); }
1534
+ if (logalot) { console.log(`${lc} Commit received.`); }
1535
+
1536
+ // Sender Logic (Finalizing):
1537
+ // If we are here, we received a Commit frame from the Peer.
1538
+ // This implies the Peer has successfully committed.
1539
+ // We should now:
1540
+ // 1. Validate (implicitly done by receiving valid frame)
1541
+ // 2. Perform our own cleanup (Temp -> Dest, if applicable)
1542
+ // 3. Return null to signal saga completion.
1543
+
1544
+ // Note: Currently we don't have explicit cleanup logic implemented here yet (TODO).
1545
+
1546
+ if (logalot) { console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`); }
993
1547
  return null;
994
1548
  }
995
1549
 
996
1550
  protected async handleConflictFrame({
997
1551
  sagaIbGib,
1552
+ metaspace,
998
1553
  space,
999
1554
  }: {
1000
1555
  sagaIbGib: SyncIbGib_V1,
1556
+ metaspace: MetaspaceService,
1001
1557
  space: IbGibSpaceAny,
1002
1558
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
1003
1559
  const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
@@ -1025,16 +1581,27 @@ export class SyncSagaCoordinator {
1025
1581
  space: IbGibSpaceAny,
1026
1582
  metaspace: MetaspaceService,
1027
1583
  }): Promise<IbGib_V1<TStoneData>> {
1028
- const ib = await getSyncSagaMessageIb({ data });
1029
- const stone = await Factory_V1.stone({
1030
- ib,
1031
- parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
1032
- data,
1033
- uuid: true, // we want the stone to have its own uniqueness
1034
- });
1035
- await putInSpace({ space, ibGib: stone });
1036
- await metaspace.registerNewIbGib({ ibGib: stone });
1037
- return stone as IbGib_V1<TStoneData>;
1584
+ const lc = `${this.lc}[${this.createSyncMsgStone.name}]`;
1585
+ try {
1586
+ if (logalot) { console.log(`${lc} starting... (I: 5f7f98e8ff980364f7191fcee4531e26)`); }
1587
+
1588
+ const ib = await getSyncSagaMessageIb({ data });
1589
+ const stone = await Factory_V1.stone({
1590
+ ib,
1591
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
1592
+ data,
1593
+ uuid: true, // we want the stone to have its own uniqueness
1594
+ });
1595
+ if (logalot) { console.log(`${lc} Created stone: ${getIbGibAddr({ ibGib: stone })}`); }
1596
+ await putInSpace({ space, ibGib: stone });
1597
+ await metaspace.registerNewIbGib({ ibGib: stone });
1598
+ return stone as IbGib_V1<TStoneData>;
1599
+ } catch (error) {
1600
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1601
+ throw error;
1602
+ } finally {
1603
+ if (logalot) { console.log(`${lc} complete.`); }
1604
+ }
1038
1605
  }
1039
1606
 
1040
1607
 
@@ -1047,12 +1614,14 @@ export class SyncSagaCoordinator {
1047
1614
  identity,
1048
1615
  space,
1049
1616
  metaspace,
1617
+ conflictStrategy,
1050
1618
  }: {
1051
1619
  prevSagaIbGib?: SyncIbGib_V1,
1052
1620
  msgStones: IbGib_V1[],
1053
1621
  identity?: KeystoneIbGib_V1,
1054
1622
  space: IbGibSpaceAny,
1055
1623
  metaspace: MetaspaceService,
1624
+ conflictStrategy?: SyncConflictStrategy,
1056
1625
  }): Promise<SyncIbGib_V1> {
1057
1626
  const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
1058
1627
  try {
@@ -1129,6 +1698,7 @@ export class SyncSagaCoordinator {
1129
1698
  payload: undefined, // Data in stone
1130
1699
  n: 0,
1131
1700
  isTjp: true,
1701
+ conflictStrategy,
1132
1702
  };
1133
1703
  const ib = await getSyncIb({ data });
1134
1704