@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
@@ -16,6 +16,8 @@ import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-consta
16
16
  import { SyncSagaContextCmd } from "./sync-saga-context/sync-saga-context-types.mjs";
17
17
  import { createSyncSagaContext } from "./sync-saga-context/sync-saga-context-helpers.mjs";
18
18
  import { newupSubject } from "../common/pubsub/subject/subject-helper.mjs";
19
+ import { mergeDivergentTimelines } from "./strategies/conflict-optimistic.mjs";
20
+ import { getSyncSagaMessageFromFrame } from "./sync-saga-message/sync-saga-message-helpers.mjs";
19
21
  const logalot = GLOBAL_LOG_A_LOT || true;
20
22
  /**
21
23
  * Orchestrates the synchronization process between two spaces (Source and Destination).
@@ -49,11 +51,15 @@ export class SyncSagaCoordinator {
49
51
  * @param opts.domainIbGibs - The root ibgibs defining the scope of the sync.
50
52
  * @param opts.useSessionIdentity - (Optional) Whether to create an ephemeral session identity. Default: true.
51
53
  */
52
- async sync({ peer, localSpace, metaspace, domainIbGibs, useSessionIdentity, }) {
54
+ async sync({ peer, localSpace: _localSpace, source: _source, metaspace, domainIbGibs, conflictStrategy = 'abort', useSessionIdentity = true, }) {
53
55
  const lc = `${this.lc}[${this.sync.name}]`;
54
56
  if (logalot) {
55
57
  console.log(`${lc} starting...`);
56
58
  }
59
+ const localSpace = (_source || _localSpace);
60
+ if (!localSpace) {
61
+ throw new Error(`${lc} source (or localSpace) required (E: 8a9b0c1d)`);
62
+ }
57
63
  // 1. SETUP SAGA METADATA
58
64
  const sagaId = await getUUID();
59
65
  // Setup Observable & Promise
@@ -108,6 +114,7 @@ export class SyncSagaCoordinator {
108
114
  domainIbGibs,
109
115
  tempSpace,
110
116
  metaspace,
117
+ conflictStrategy
111
118
  });
112
119
  // 4. EXECUTE SAGA LOOP (FSM)
113
120
  const syncedIbGibs = await this.executeSagaLoop({
@@ -266,6 +273,20 @@ export class SyncSagaCoordinator {
266
273
  const responseCtx = await peer.witness(requestCtx);
267
274
  // C. Handle Response
268
275
  if (!responseCtx) {
276
+ // Check if we just sent a Commit frame. If so, peer's silence is success/expected.
277
+ if (currentFrame) {
278
+ const msg = await getSyncSagaMessageFromFrame({ frameIbGib: currentFrame, space: localSpace });
279
+ if (logalot) {
280
+ console.log(`${lc} Checking currentFrame stage: ${msg?.data?.stage} (Expected: ${SyncStage.commit})`);
281
+ }
282
+ if (msg?.data?.stage === SyncStage.commit) {
283
+ if (logalot) {
284
+ console.log(`${lc} Sender sent Commit. Peer returned no response. Saga Complete.`);
285
+ }
286
+ currentFrame = null;
287
+ break;
288
+ }
289
+ }
269
290
  throw new Error(`responseCtx falsy. Peer returned no response context (E: c099d8073b48d85e881f917835158f26)`);
270
291
  // console.warn(`${lc} Peer returned no response context. Ending loop.`);
271
292
  // currentFrame = null;
@@ -305,7 +326,7 @@ export class SyncSagaCoordinator {
305
326
  const result = await this.handleSagaFrame({
306
327
  sagaIbGib: remoteFrame,
307
328
  srcGraph,
308
- space: tempSpace,
329
+ space: localSpace, // Must be localSpace (Source) to find domain data
309
330
  identity: sessionIdentity,
310
331
  metaspace
311
332
  });
@@ -318,6 +339,41 @@ export class SyncSagaCoordinator {
318
339
  }
319
340
  return allReceivedIbGibs;
320
341
  }
342
+ /**
343
+ * Helper to get Knowledge Vector for specific domain ibGibs or TJPs.
344
+ * Useful for testing and external validation.
345
+ */
346
+ async getKnowledgeVector({ space, metaspace, domainIbGibs, tjpAddrs, }) {
347
+ const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
348
+ if (logalot) {
349
+ console.log(`${lc} starting...`);
350
+ }
351
+ let tjps = [];
352
+ if (tjpAddrs) {
353
+ tjps = tjpAddrs;
354
+ }
355
+ else if (domainIbGibs && domainIbGibs.length > 0) {
356
+ // Extract TJPs from domain Ibgibs
357
+ const { mapWithTjp_YesDna, mapWithTjp_NoDna } = splitPerTjpAndOrDna({ ibGibs: domainIbGibs });
358
+ const allWithTjp = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
359
+ const timelineMap = getTimelinesGroupedByTjp({ ibGibs: allWithTjp });
360
+ tjps = Object.keys(timelineMap);
361
+ }
362
+ else {
363
+ // No info provided. Return empty? Or throw?
364
+ // User test context implied "everything", but implementation requires scope.
365
+ console.warn(`${lc} No domainIbGibs or tjpAddrs provided. Returning empty KV.`);
366
+ return {};
367
+ }
368
+ if (tjps.length === 0) {
369
+ return {};
370
+ }
371
+ const res = await getLatestAddrs({ space, tjpAddrs: tjps });
372
+ if (!res.data || !res.data.latestAddrsMap) {
373
+ throw new Error(`${lc} Failed to get latest addrs. (E: 7a8b9c0d)`);
374
+ }
375
+ return res.data.latestAddrsMap;
376
+ }
321
377
  async analyzeTimelines({ domainIbGibs, space, }) {
322
378
  const lc = `${this.lc}[${this.analyzeTimelines.name}]`;
323
379
  const srcGraph = await getDependencyGraph({
@@ -346,7 +402,7 @@ export class SyncSagaCoordinator {
346
402
  * Generates the first frame containing the Knowledge Vector of the Local Space.
347
403
  * This is sent to the Receiver to begin Gap Analysis.
348
404
  */
349
- async createInitFrame({ sagaId, sessionIdentity, localSpace, domainIbGibs, tempSpace, metaspace }) {
405
+ async createInitFrame({ sagaId, sessionIdentity, localSpace, domainIbGibs, tempSpace, metaspace, conflictStrategy, }) {
350
406
  const lc = `${this.lc}[${this.createInitFrame.name}]`;
351
407
  try {
352
408
  if (logalot) {
@@ -383,7 +439,8 @@ export class SyncSagaCoordinator {
383
439
  msgStones: [initStone],
384
440
  identity: sessionIdentity,
385
441
  space: tempSpace,
386
- metaspace
442
+ metaspace,
443
+ conflictStrategy,
387
444
  });
388
445
  if (logalot) {
389
446
  console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`);
@@ -432,15 +489,15 @@ export class SyncSagaCoordinator {
432
489
  }
433
490
  switch (stage) {
434
491
  case SyncStage.init:
435
- return await this.handleInitFrame({ sagaIbGib, messageData, space, metaspace, identity, identitySecret });
492
+ return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, space, identity, identitySecret });
436
493
  case SyncStage.ack:
437
- return await this.handleAckFrame({ sagaIbGib, srcGraph, space, metaspace, identity });
494
+ return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, space, identity });
438
495
  case SyncStage.delta:
439
- return await this.handleDeltaFrame({ sagaIbGib, srcGraph, space, identity, metaspace });
496
+ return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, space, identity, });
440
497
  case SyncStage.commit:
441
- return await this.handleCommitFrame({ sagaIbGib, space });
498
+ return await this.handleCommitFrame({ sagaIbGib, metaspace, space, identity, });
442
499
  case SyncStage.conflict:
443
- return await this.handleConflictFrame({ sagaIbGib, space });
500
+ return await this.handleConflictFrame({ sagaIbGib, metaspace, space, });
444
501
  default:
445
502
  throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
446
503
  }
@@ -475,14 +532,41 @@ export class SyncSagaCoordinator {
475
532
  }
476
533
  // Extract Init Data
477
534
  const initData = messageData; // Using renamed variable for clarity
535
+ if (initData.stage !== SyncStage.init) {
536
+ throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
537
+ }
478
538
  if (logalot) {
479
539
  console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`);
480
540
  }
481
541
  if (!initData || !initData.knowledgeVector) {
482
542
  throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
483
543
  }
484
- // Stones Analysis
485
- // TODO: Handle stones separately if needed, though they are usually dependencies of timelines.
544
+ // Determine Strategy from Saga Data (since V1 stores it in root)
545
+ const conflictStrategy = sagaIbGib.data.conflictStrategy || 'abort';
546
+ // 2. Gap Analysis
547
+ const conflicts = [];
548
+ const deltaReqAddrs = [];
549
+ const pushOfferAddrs = [];
550
+ // Stones Analysis (Constants / Non-TJPs)
551
+ const stones = initData.stones || [];
552
+ if (stones.length > 0) {
553
+ if (logalot) {
554
+ console.log(`${lc} processing stones: ${stones.length}`);
555
+ }
556
+ // Check if we have these stones
557
+ const resStones = await getFromSpace({ space, addrs: stones });
558
+ const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
559
+ if (addrsNotFound && addrsNotFound.length > 0) {
560
+ if (logalot) {
561
+ console.log(`${lc} stones missing (requesting): ${addrsNotFound.length}`);
562
+ }
563
+ addrsNotFound.forEach(addr => {
564
+ if (!deltaReqAddrs.includes(addr)) {
565
+ deltaReqAddrs.push(addr);
566
+ }
567
+ });
568
+ }
569
+ }
486
570
  const remoteKV = initData.knowledgeVector;
487
571
  if (logalot) {
488
572
  console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`);
@@ -511,10 +595,6 @@ export class SyncSagaCoordinator {
511
595
  }
512
596
  }
513
597
  // 2. Gap Analysis
514
- const conflicts = [];
515
- const conflictStrategy = initData.conflictStrategy || 'abort'; // Default to abort if not specified, or we should parameterize this
516
- const deltaReqAddrs = [];
517
- const pushOfferAddrs = [];
518
598
  for (const tjp of remoteTjps) {
519
599
  const remoteAddr = remoteKV[tjp];
520
600
  const localAddr = localKV[tjp];
@@ -552,20 +632,44 @@ export class SyncSagaCoordinator {
552
632
  }
553
633
  else {
554
634
  // DIVERGENCE: Both have changes the other doesn't know about.
555
- conflicts.push({ tjp, localAddr, remoteAddr, reason: 'divergence' });
556
- // Conflict Strategy Handling
557
- // TODO: Implement "manual" strategy (requires user intervention/pause)
558
- // TODO: Implement "local_wins" strategy
559
- // TODO: Implement "server_wins" strategy
560
635
  if (conflictStrategy === 'abort') {
561
- // We will abort immediately after this loop check if any conflicts exist
636
+ // Abort Strategy: We will treat this as terminal.
637
+ // But for Unified Ack, we just mark it terminal in the list?
638
+ // Or do we actually throw/abort the saga?
639
+ // Current logic (below) aborts the saga if ANY conflict is terminal/abort.
640
+ conflicts.push({
641
+ tjpAddr: tjp,
642
+ localAddr: localAddr,
643
+ remoteAddr,
644
+ timelineAddrs: [], // Not needed for abort
645
+ reason: 'divergence',
646
+ terminal: true
647
+ });
562
648
  }
563
649
  else if (conflictStrategy === 'optimistic') {
564
- // Optimistic: We Try to resolve by syncing timeline-by-timeline.
565
- // We request the remote data (deltaReq) and will attempt to merge later?
566
- // OR we treat it as a delta request for now, hoping the merge logic handles it?
567
- // For now, we request the remote tip.
568
- deltaReqAddrs.push(remoteAddr);
650
+ // Optimistic: We want to resolving this.
651
+ // We need to send our history to the Sender so they can Merge.
652
+ // Fetch Full History for Local Timeline
653
+ // Note: We might optimize this to only send "recent" history if we had a KV?
654
+ // But for now, get full past.
655
+ // Optimization: localKV might not have full history.
656
+ // We need to inspect the 'past' of the local tip.
657
+ // We need the ACTUAL object to get the past.
658
+ // We have localAddr.
659
+ const resLocalTip = await getFromSpace({ space, addr: localAddr });
660
+ const localTip = resLocalTip.ibGibs?.[0];
661
+ if (!localTip) {
662
+ throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`);
663
+ }
664
+ const timelineAddrs = [localAddr, ...(localTip.rel8ns?.past || [])];
665
+ conflicts.push({
666
+ tjpAddr: tjp,
667
+ localAddr: localAddr,
668
+ remoteAddr,
669
+ timelineAddrs,
670
+ reason: 'divergence',
671
+ terminal: false
672
+ });
569
673
  }
570
674
  else {
571
675
  throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
@@ -573,33 +677,87 @@ export class SyncSagaCoordinator {
573
677
  }
574
678
  }
575
679
  }
576
- if (conflicts.length > 0 && conflictStrategy === 'abort') {
680
+ // Check if we should ABORT (if any conflict is terminal)
681
+ const hasTerminalConflicts = conflicts.some(c => c.terminal);
682
+ if (hasTerminalConflicts) {
577
683
  // Abort Strategy: Kill the saga.
578
684
  if (logalot) {
579
- console.warn(`${lc} ABORTING Sync Saga due to conflicts: ${JSON.stringify(conflicts)}`);
685
+ console.warn(`${lc} ABORTING Sync Saga due to terminal conflicts: ${JSON.stringify(conflicts)}`);
686
+ }
687
+ // We reuse the ConflictData structure for terminal aborts?
688
+ // Or do we send an Ack with terminal conflicts?
689
+ // Original design had explicit Conflict Frame for Abort.
690
+ // Let's stick to that for purely terminal cases to be safe/explicit?
691
+ // Or Unified: Just send Ack with terminal=true conflicts. Sender sees them and aborts.
692
+ // Decision: Unified Ack for everything is cleaner protocol.
693
+ // But wait, the original code below creates a Conflict Stone.
694
+ // Let's preserve the explicit 'Conflict' frame for total aborts if that's easier,
695
+ // OR fully switch to Ack.
696
+ // Protocol states: Init -> Ack. If Ack contains terminal errors, Sender can Commit(Fail).
697
+ // Let's use Ack with conflicts.
698
+ }
699
+ // 2. Add Push Offers (Missing in Local)
700
+ // Check if we have them. If not, ask for them.
701
+ for (const addr of pushOfferAddrs) {
702
+ const hasIt = await getFromSpace({ addr, space });
703
+ if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
704
+ // If we don't have it, we put it in `deltaReqAddrs` of the Ack.
705
+ deltaReqAddrs.push(addr);
706
+ }
707
+ }
708
+ // 3. Build Knowledge Vector (Full History for known timelines)
709
+ // [NEW] Smart Diff
710
+ // We iterate over all relevant addresses (deltas we are requesting OR push offers we might have newer versions of).
711
+ // Since we are "reacting" to Init, we primarily want to tell the Sender what we DO have for the things they talked about.
712
+ const knowledgeVector = {};
713
+ const relevantAddrs = new Set([...pushOfferAddrs, ...deltaReqAddrs]);
714
+ // [Smart Diff] Populate knowledge from timelines identified by Sender
715
+ for (const tjp of remoteTjps) {
716
+ const localAddr = localKV[tjp];
717
+ if (localAddr) {
718
+ const res = await getFromSpace({ addr: localAddr, space });
719
+ if (res.success && res.ibGibs?.[0]) {
720
+ const ibGib = res.ibGibs[0];
721
+ const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized
722
+ if (!knowledgeVector[realTjp]) {
723
+ const past = ibGib.rel8ns?.past || [];
724
+ knowledgeVector[realTjp] = [getIbGibAddr({ ibGib }), ...past];
725
+ }
726
+ }
580
727
  }
581
- const sagaId = sagaIbGib.data.uuid;
582
- const conflictData = {
583
- sagaId,
584
- stage: SyncStage.conflict,
585
- conflictStrategy,
586
- isTerminal: true,
587
- conflicts,
588
- };
589
- const conflictStone = await this.createSyncMsgStone({
590
- data: conflictData,
591
- space,
592
- metaspace,
593
- });
594
- const conflictFrame = await this.evolveSyncSagaIbGib({
595
- prevSagaIbGib: sagaIbGib,
596
- msgStones: [conflictStone],
597
- identity,
598
- space,
599
- metaspace,
600
- });
601
- return { frame: conflictFrame };
602
728
  }
729
+ // [Smart Diff] Also check individual requested addresses (Fall back for constants/unknown TJPs)
730
+ for (const addr of relevantAddrs) {
731
+ // Only process if not already covered by TJP logic above
732
+ // We can't really know if it's covered easily without resolving.
733
+ // But if we don't have it (requesting), we won't find it here anyway.
734
+ // If we DO have it (push offer), we might find it.
735
+ const res = await getFromSpace({ addr, space });
736
+ if (res.success && res.ibGibs?.[0]) {
737
+ const ibGib = res.ibGibs[0];
738
+ const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
739
+ if (!knowledgeVector[tjpAddr]) {
740
+ const past = ibGib.rel8ns?.past || [];
741
+ knowledgeVector[tjpAddr] = [getIbGibAddr({ ibGib }), ...past];
742
+ }
743
+ }
744
+ else {
745
+ // We don't have `addr`.
746
+ }
747
+ }
748
+ // Also populate from `knowledgeVector` in Init if we want bidirectional?
749
+ // No, `Init` doesn't have `knowledgeVector` in `SyncSagaMessageInitData` yet (it has `SyncInitData` generic props).
750
+ // Let's assume standard flow:
751
+ // 1. Sender says "I have X"
752
+ // 2. Receiver says "I don't have X. But if I did have Y (ancestor), I'd tell you."
753
+ // Problem: Receiver doesn't know X is related to Y without X.
754
+ // SOLUTION:
755
+ // The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
756
+ // `SyncSagaMessageInitData_V1` extends `SyncInitData`.
757
+ // `SyncInitData` has `knowledgeVector`.
758
+ // If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
759
+ // Let's implement Sender populating `Init.knowledgeVector`.
760
+ // But `SyncSagaCoordinator.startSaga` creates Init.
603
761
  // 3. Create Ack Frame
604
762
  const sagaId = sagaIbGib.data.uuid;
605
763
  // Create Payload Stone
@@ -608,6 +766,7 @@ export class SyncSagaCoordinator {
608
766
  stage: SyncStage.ack,
609
767
  deltaReqAddrs,
610
768
  pushOfferAddrs,
769
+ knowledgeVector,
611
770
  };
612
771
  const ackStone = await this.createSyncMsgStone({
613
772
  data: ackData,
@@ -655,6 +814,127 @@ export class SyncSagaCoordinator {
655
814
  if (ackData.stage !== SyncStage.ack) {
656
815
  throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
657
816
  }
817
+ if (logalot) {
818
+ console.log(`${lc} ackData: ${pretty(ackData)} (I: 7f8e9d0a1b2c3d4e5f6g7h8i9j0k)`);
819
+ }
820
+ // 1. Check for Conflicts
821
+ const conflicts = ackData.conflicts || [];
822
+ const terminalConflicts = conflicts.filter(c => c.terminal);
823
+ if (terminalConflicts.length > 0) {
824
+ console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
825
+ // Terminal failure. Sender should probably Commit(Fail) or just Abort.
826
+ // For now, throw to trigger abort.
827
+ throw new Error(`${lc} Peer reported terminal conflicts. (E: a1b2c3d4e5f6g7h8i9j0k)`);
828
+ }
829
+ const optimisticConflicts = conflicts.filter(c => !c.terminal);
830
+ const mergeDeltaReqs = []; // Additional requests for merging
831
+ if (optimisticConflicts.length > 0) {
832
+ if (logalot) {
833
+ console.log(`${lc} Processing optimistic conflicts: ${optimisticConflicts.length}`);
834
+ }
835
+ // We need to resolve these.
836
+ // Strategy:
837
+ // 1. Analyze Divergence (Sender vs Receiver)
838
+ // 2. Identify missing data needed for merge (Receiver's unique frames)
839
+ // 3. Request that data (as Delta Reqs)
840
+ // 4. (Later in Delta Phase) Perform Merge.
841
+ // BUT: The Delta Phase is usually generic "Send me these Addrs".
842
+ // If we just add to `deltaReqAddrs` (which are requests for Sender to send to Receiver?),
843
+ // wait. `ackData.deltaReqAddrs` are what RECEIVER wants from SENDER.
844
+ // We (Sender) are processing the Ack.
845
+ // We need to request data FROM Receiver.
846
+ // But the protocol 'Ack' step typically leads to 'Delta' (Sender sending data).
847
+ // If Sender needs data from Receiver, it usually happens in 'Pull' mode or a separate request?
848
+ // Or can we send a 'Delta Request' frame?
849
+ // Standard Saga: Init(Push) -> Ack(Pull Reqs) -> Delta(Push Data).
850
+ // If Sender needs data, we might need a "Reverse Delta" or "Pull" phase?
851
+ // Or we just proceed to Delta (sending what Receiver wants),
852
+ // AND we piggyback our own requests?
853
+ // OR: We treat the Conflict Resolution as a sub-saga or side-effect?
854
+ // SIMPLIFICATION for V1:
855
+ // If we need data to merge, we must get it.
856
+ // We are the Coordinator (Active). We can fetch from Peer immediately?
857
+ // `peer.pull(addr)`?
858
+ // Yes! The Coordinator has the `peer`.
859
+ // Let's analyze and pull immediately.
860
+ for (const conflict of optimisticConflicts) {
861
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
862
+ // Sender History
863
+ // We need our own history for this timeline.
864
+ // We know the 'senderTip' (remoteAddr in Ack).
865
+ // Sender should verify it has this tip.
866
+ // Compute Diffs
867
+ // We need to find `receiverOnly` addrs.
868
+ // Receiver sent us `timelineAddrs` (Full History).
869
+ const receiverHistorySet = new Set(timelineAddrs);
870
+ // We need our execution context's history for this senderTip.
871
+ // We can fetch valid 'past' from space.
872
+ const resSenderTip = await getFromSpace({ space, addr: senderTip });
873
+ const senderTipIbGib = resSenderTip.ibGibs?.[0];
874
+ if (!senderTipIbGib) {
875
+ throw new Error(`${lc} Sender missing its own tip? ${senderTip} (E: 9c8d7e6f5g4h3i2j1k0l)`);
876
+ }
877
+ // Basic Diff: Find what Receiver has that we don't.
878
+ // Actually, we need to traverse OUR past to find commonality.
879
+ const senderHistory = [senderTip, ...(senderTipIbGib.rel8ns?.past || [])];
880
+ const receiverOnlyAddrs = timelineAddrs.filter(addr => !senderHistory.includes(addr));
881
+ if (receiverOnlyAddrs.length > 0) {
882
+ if (logalot) {
883
+ console.log(`${lc} Pulling divergent history from Receiver: ${receiverOnlyAddrs.length} frames`);
884
+ }
885
+ // PULL these frames from Peer into Local Space
886
+ // (Validation: We trust peer for now / verification happens on put)
887
+ for (const addr of receiverOnlyAddrs) {
888
+ // This 'pull' is a sync-peer method?
889
+ // The Coordinator 'peer' passed in 'sync()' might be needed here?
890
+ // Wait, `handleAckFrame` doesn't have reference to `peer`?
891
+ // It only has `space`, `metaspace`.
892
+ // The `peer` is held by the `executeSagaLoop`.
893
+ // PROBLEM: `handleAckFrame` is pure logic on the Space/Data?
894
+ // No, it's a method on Coordinator.
895
+ // But `executeSagaLoop` calls it.
896
+ // We might need to return "Requirements" to the loop?
897
+ // Checking return type: `{ frame: SyncIbGib_V1, payloadIbGibs?: ... }`
898
+ // It returns the NEXT frame (Delta).
899
+ // If we need to fetch data, we are blocked.
900
+ // We can't easily "Pull" here without the Peer reference.
901
+ // OPTION A: Pass `peer` to `handleAckFrame`.
902
+ // OPTION B: Return a strict list of "MissingDeps" and let Loop handle it.
903
+ // Let's assume we can resolve this by adding `peer` to signature or using `metaspace` if it's a peer-witness?
904
+ // No, Peer is ephemeral connection.
905
+ // Let's add `peer` to `handleAckFrame` signature?
906
+ // It breaks the pattern of just handling frame + space.
907
+ // ALTERNATIVE: Use the `Delta` frame to request data?
908
+ // `SyncSagaMessageDeltaData` has `requests?: string[]`.
909
+ // Sender sends Delta Frame.
910
+ // Does Receiver handle Delta Requests?
911
+ // `handleDeltaFrame` (Receiver) -> checks `requests`.
912
+ // YES.
913
+ // So Sender puts `receiverOnlyAddrs` into `deltaFrame.requests`.
914
+ // Receiver sees them, fetches them, and includes them in the Response (Commit?).
915
+ // Wait, Init->Ack->Delta->Commit.
916
+ // If Receiver sends data in Commit, that's "too late" for Sender to Merge in THIS saga round?
917
+ // Unless Commit is not the end?
918
+ // Or we do a "Delta 2" loop?
919
+ // "Iterative Resolution Loop" from plan.
920
+ // If we request data in Delta, Receiver sends it in Commit (or Delta-Response).
921
+ // Sender gets Commit. Sees data. Merges.
922
+ // Then Sender needs to Send the MERGE result.
923
+ // Needs another Push/Delta.
924
+ // REFINED FLOW:
925
+ // 1. Sender sends Delta Frame with `requests: [receiverOnlyAddrs]`.
926
+ // 2. Receiver responds (Commit? or Ack 2?) with Payload (Divergent Frames).
927
+ // 3. Sender handles response -> Merges.
928
+ // 4. Sender sends Commit (containing Merge Frame).
929
+ // Issue: Current state machine is Init->Ack->Delta->Commit.
930
+ // We need to keep Saga open.
931
+ // If Sender sends Delta with requests, does it transition to Commit?
932
+ }
933
+ mergeDeltaReqs.push(...receiverOnlyAddrs);
934
+ }
935
+ }
936
+ }
937
+ // 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
658
938
  const deltaReqAddrs = ackData.deltaReqAddrs || [];
659
939
  const pushOfferAddrs = ackData.pushOfferAddrs || [];
660
940
  // 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
@@ -666,7 +946,16 @@ export class SyncSagaCoordinator {
666
946
  }
667
947
  }
668
948
  // 2. Process Delta Requests (Push Payload)
949
+ // [NEW] Smart Diff: Use knowledgeVector to skip dependencies
950
+ const skipAddrs = new Set();
951
+ if (ackData.knowledgeVector) {
952
+ Object.values(ackData.knowledgeVector).forEach(addrs => {
953
+ addrs.forEach(a => skipAddrs.add(a));
954
+ });
955
+ }
669
956
  const payloadIbGibs = [];
957
+ // Gather all tips to sync first
958
+ const tipsToSync = [];
670
959
  for (const addr of deltaReqAddrs) {
671
960
  let ibGib = srcGraph[addr];
672
961
  if (!ibGib) {
@@ -676,19 +965,62 @@ export class SyncSagaCoordinator {
676
965
  }
677
966
  }
678
967
  if (ibGib) {
679
- payloadIbGibs.push(ibGib);
968
+ tipsToSync.push(ibGib);
680
969
  }
681
970
  else {
682
971
  throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
683
972
  }
684
973
  }
974
+ // Calculate Dependency Graph for ALL tips, effectively utilizing common history
975
+ // Pass skipAddrs to `getDependencyGraph` or gather manually.
976
+ // `getDependencyGraph` takes a single ibGib.
977
+ // We can optimize by doing it for each tip and unioning the result?
978
+ // Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
979
+ // We will loop.
980
+ const allDepsSet = new Set();
981
+ for (const tip of tipsToSync) {
982
+ // Always include the tip itself
983
+ const tipAddr = getIbGibAddr({ ibGib: tip });
984
+ // Only process if not skipped (though deltaReq implies they barely just asked for it)
985
+ // But detailed deps might be skipped.
986
+ // Get Graph with Skips
987
+ // Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
988
+ const deps = await getDependencyGraph({
989
+ ibGib: tip,
990
+ space,
991
+ skipAddrs: Array.from(skipAddrs)
992
+ });
993
+ // [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
994
+ let tipIncluded = false;
995
+ if (deps) {
996
+ Object.values(deps).forEach(d => {
997
+ const dAddr = getIbGibAddr({ ibGib: d });
998
+ if (!allDepsSet.has(dAddr)) {
999
+ allDepsSet.add(dAddr);
1000
+ payloadIbGibs.push(d);
1001
+ }
1002
+ if (dAddr === tipAddr) {
1003
+ tipIncluded = true;
1004
+ }
1005
+ });
1006
+ }
1007
+ if (!tipIncluded && !skipAddrs.has(tipAddr)) {
1008
+ if (logalot) {
1009
+ console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`);
1010
+ }
1011
+ if (!allDepsSet.has(tipAddr)) {
1012
+ allDepsSet.add(tipAddr);
1013
+ payloadIbGibs.push(tip);
1014
+ }
1015
+ }
1016
+ }
685
1017
  // 3. Create Delta Frame
686
1018
  const sagaId = ackData.sagaId;
687
1019
  const deltaData = {
688
- sagaId,
1020
+ sagaId: sagaIbGib.data.uuid,
689
1021
  stage: SyncStage.delta,
690
1022
  payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
691
- requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
1023
+ requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
692
1024
  };
693
1025
  const deltaStone = await this.createSyncMsgStone({
694
1026
  data: deltaData,
@@ -727,22 +1059,36 @@ export class SyncSagaCoordinator {
727
1059
  if (deltaData.stage !== SyncStage.delta) {
728
1060
  throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
729
1061
  }
1062
+ if (logalot) {
1063
+ console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`);
1064
+ }
730
1065
  const payloadAddrs = deltaData.payloadAddrs || [];
731
- const requests = deltaData.requests || [];
732
- // 1. Process Received Payload
1066
+ const peerRequests = deltaData.requests || [];
1067
+ const peerProposesCommit = deltaData.proposeCommit || false;
1068
+ // 1. Process Received Payload (Ingest)
733
1069
  const receivedPayloadIbGibs = [];
734
1070
  if (payloadAddrs.length > 0) {
1071
+ // We use `payloadAddrs` as the manifest.
1072
+ // The ACTUAL collection of ibGibs should be available via `getFromSpace`
1073
+ // assuming the "Transport" layer put them there implicitly?
1074
+ // OR, if we are local-only, we just get them.
1075
+ // The `handleDeltaFrame` contract assumes data is reachable in `space`.
735
1076
  const res = await getFromSpace({
736
1077
  addrs: payloadAddrs,
737
1078
  space,
738
1079
  });
739
1080
  if (res.ibGibs) {
740
1081
  receivedPayloadIbGibs.push(...res.ibGibs);
1082
+ // Also put them? `getFromSpace` retrieves. If they are in space, they are persisted.
1083
+ // If this is a Temp Space, they are safe.
1084
+ }
1085
+ else {
1086
+ console.warn(`${lc} Failed to retrieve payloads listed in delta: ${payloadAddrs.join(', ')}`);
741
1087
  }
742
1088
  }
743
- // 2. Fulfill Requests (Outgoing Payload)
1089
+ // 2. Fulfill Peer Requests (Outgoing Payload)
744
1090
  const outgoingPayload = [];
745
- for (const addr of requests) {
1091
+ for (const addr of peerRequests) {
746
1092
  let ibGib = srcGraph[addr];
747
1093
  if (!ibGib) {
748
1094
  const res = await getFromSpace({ addr, space });
@@ -753,16 +1099,105 @@ export class SyncSagaCoordinator {
753
1099
  if (ibGib) {
754
1100
  outgoingPayload.push(ibGib);
755
1101
  }
1102
+ else {
1103
+ console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
1104
+ }
1105
+ }
1106
+ // 3. Execute Merges (If applicable)
1107
+ // Check if we have pending conflicts that we CAN resolve now that we have data.
1108
+ // We look at the Saga History (Ack Frame) to find conflicts.
1109
+ // Optimization: Do this only if we received payloads.
1110
+ const mergeResultIbGibs = [];
1111
+ if (receivedPayloadIbGibs.length > 0) {
1112
+ // Find the Ack frame in history to get conflicts
1113
+ // Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
1114
+ // V1 timelines carry full history in `past`.
1115
+ const pastAddrs = sagaIbGib.rel8ns?.past || [];
1116
+ let ackData;
1117
+ if (pastAddrs.length > 0) {
1118
+ // Batch fetch all past frames
1119
+ const resPast = await getFromSpace({ addrs: pastAddrs, space });
1120
+ if (resPast.success && resPast.ibGibs) {
1121
+ // Iterate backwards (most recent first) to find the latest Ack
1122
+ for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
1123
+ const pastFrame = resPast.ibGibs[i];
1124
+ const messageStone = await getSyncSagaMessageFromFrame({
1125
+ frameIbGib: pastFrame,
1126
+ space
1127
+ });
1128
+ if (messageStone?.data?.stage === SyncStage.ack) {
1129
+ ackData = messageStone.data;
1130
+ break;
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ if (ackData && ackData.conflicts) {
1136
+ const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
1137
+ for (const conflict of optimisticConflicts) {
1138
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
1139
+ // We are Sender (usually) here if we are merging.
1140
+ // Check if we have the history needed (timelineAddrs).
1141
+ // Specifically, we needed the `receiverOnly` parts.
1142
+ // We blindly attempt merge if we have both tips accessible?
1143
+ // We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
1144
+ // Check if we have receiverTip in space
1145
+ const resRecTip = await getFromSpace({ addr: receiverTip, space });
1146
+ if (resRecTip.success && resRecTip.ibGibs?.[0]) {
1147
+ // We have the tip!
1148
+ // Do we have the full history?
1149
+ // `mergeDivergentTimelines` in `conflict-optimistic` will attempt to fetch history.
1150
+ // If we just ingested the missing pieces, `getFromSpace` inside `merge` should succeed.
1151
+ // Perform Merge!
1152
+ try {
1153
+ const mergeResult = await mergeDivergentTimelines({
1154
+ tipA: (await getFromSpace({ addr: senderTip, space })).ibGibs[0], // Our tip
1155
+ tipB: resRecTip.ibGibs[0], // Their tip
1156
+ space,
1157
+ metaspace,
1158
+ });
1159
+ if (mergeResult) {
1160
+ if (logalot) {
1161
+ console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
1162
+ }
1163
+ mergeResultIbGibs.push(mergeResult);
1164
+ outgoingPayload.push(mergeResult); // Send result to peer
1165
+ }
1166
+ }
1167
+ catch (e) {
1168
+ console.error(`${lc} Merge failed: ${e}`);
1169
+ // If merge fails, we might Abort or just continue?
1170
+ }
1171
+ }
1172
+ }
1173
+ }
756
1174
  }
757
- // 3. Determine Next Stage
758
- if (requests.length > 0) {
759
- // They requested more data -> Send Delta
760
- const sagaId = deltaData.sagaId;
1175
+ // 4. Determine Next Action
1176
+ // We have `outgoingPayload` (Requests + Merge Results).
1177
+ // Does Peer have outstanding requests? No, we fulfilled `peerRequests`.
1178
+ // Do WE have outstanding requests?
1179
+ // We might if `mergeResult` requires further sync? Usually no, result is complete.
1180
+ const myRequests = []; // If we had more needs (e.g. partial payload), we'd add here.
1181
+ const hasOutgoing = outgoingPayload.length > 0;
1182
+ const hasMyRequests = myRequests.length > 0;
1183
+ if (hasOutgoing || hasMyRequests) {
1184
+ // We have business to attend to -> Send Delta
761
1185
  const responseDeltaData = {
762
- sagaId,
1186
+ sagaId: deltaData.sagaId,
763
1187
  stage: SyncStage.delta,
764
1188
  payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
1189
+ requests: hasMyRequests ? myRequests : undefined,
1190
+ proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
1191
+ // Wait. If we send data, we are NOT committing yet.
1192
+ // We are sending data. The OTHER side must ingest it.
1193
+ // So proposeCommit = true?
1194
+ // "Here is the data. I'm done. If you are good, let's commit."
1195
+ // Yes.
765
1196
  };
1197
+ // BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
1198
+ // We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
1199
+ // So yes, proposeCommit = true.
1200
+ responseDeltaData.proposeCommit = true;
766
1201
  const deltaStone = await this.createSyncMsgStone({
767
1202
  data: responseDeltaData,
768
1203
  space,
@@ -778,36 +1213,99 @@ export class SyncSagaCoordinator {
778
1213
  return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
779
1214
  }
780
1215
  else {
781
- // No requests -> Commit
782
- const sagaId = deltaData.sagaId;
783
- const commitData = {
784
- sagaId,
785
- stage: SyncStage.commit,
786
- success: true,
787
- };
788
- const commitStone = await this.createSyncMsgStone({
789
- data: commitData,
790
- space,
791
- metaspace
792
- });
793
- const commitFrame = await this.evolveSyncSagaIbGib({
794
- prevSagaIbGib: sagaIbGib,
795
- msgStones: [commitStone],
796
- identity,
797
- space,
798
- metaspace
799
- });
800
- return { frame: commitFrame, receivedPayloadIbGibs };
1216
+ // We have nothing to send.
1217
+ if (peerProposesCommit) {
1218
+ // Peer is done. We are done. -> Commit.
1219
+ const commitData = {
1220
+ sagaId: deltaData.sagaId,
1221
+ stage: SyncStage.commit,
1222
+ success: true,
1223
+ };
1224
+ const commitStone = await this.createSyncMsgStone({
1225
+ data: commitData,
1226
+ space,
1227
+ metaspace
1228
+ });
1229
+ const commitFrame = await this.evolveSyncSagaIbGib({
1230
+ prevSagaIbGib: sagaIbGib,
1231
+ msgStones: [commitStone],
1232
+ identity,
1233
+ space,
1234
+ metaspace
1235
+ });
1236
+ return { frame: commitFrame, receivedPayloadIbGibs };
1237
+ }
1238
+ else {
1239
+ // peer did NOT propose commit (maybe they just sent data/requests and didn't ready flag).
1240
+ // But we are empty.
1241
+ // So WE propose commit.
1242
+ const responseDeltaData = {
1243
+ sagaId: deltaData.sagaId,
1244
+ stage: SyncStage.delta,
1245
+ proposeCommit: true,
1246
+ payloadAddrs: [], // Always include empty array if sending delta
1247
+ };
1248
+ const deltaStone = await this.createSyncMsgStone({
1249
+ data: responseDeltaData,
1250
+ space,
1251
+ metaspace
1252
+ });
1253
+ const deltaFrame = await this.evolveSyncSagaIbGib({
1254
+ prevSagaIbGib: sagaIbGib,
1255
+ msgStones: [deltaStone],
1256
+ identity,
1257
+ space,
1258
+ metaspace
1259
+ });
1260
+ // Check if PEER proposed commit
1261
+ if (deltaData.proposeCommit) {
1262
+ if (logalot) {
1263
+ console.log(`${lc} Peer proposed commit. Accepting & Committing.`);
1264
+ }
1265
+ // Peer wants to commit and has no more requests.
1266
+ // We should Commit.
1267
+ const commitData = {
1268
+ sagaId: deltaData.sagaId,
1269
+ stage: SyncStage.commit,
1270
+ success: true,
1271
+ };
1272
+ const commitStone = await this.createSyncMsgStone({
1273
+ data: commitData,
1274
+ space,
1275
+ metaspace
1276
+ });
1277
+ const commitFrame = await this.evolveSyncSagaIbGib({
1278
+ prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
1279
+ msgStones: [commitStone],
1280
+ identity,
1281
+ space,
1282
+ metaspace
1283
+ });
1284
+ return { frame: commitFrame, receivedPayloadIbGibs };
1285
+ }
1286
+ return { frame: deltaFrame, receivedPayloadIbGibs };
1287
+ }
801
1288
  }
802
1289
  }
803
- async handleCommitFrame({ sagaIbGib, space, }) {
1290
+ async handleCommitFrame({ sagaIbGib, space, metaspace, }) {
804
1291
  const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
805
1292
  if (logalot) {
806
- console.log(`${lc} Commit received. Saga complete.`);
1293
+ console.log(`${lc} Commit received.`);
1294
+ }
1295
+ // Sender Logic (Finalizing):
1296
+ // If we are here, we received a Commit frame from the Peer.
1297
+ // This implies the Peer has successfully committed.
1298
+ // We should now:
1299
+ // 1. Validate (implicitly done by receiving valid frame)
1300
+ // 2. Perform our own cleanup (Temp -> Dest, if applicable)
1301
+ // 3. Return null to signal saga completion.
1302
+ // Note: Currently we don't have explicit cleanup logic implemented here yet (TODO).
1303
+ if (logalot) {
1304
+ console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`);
807
1305
  }
808
1306
  return null;
809
1307
  }
810
- async handleConflictFrame({ sagaIbGib, space, }) {
1308
+ async handleConflictFrame({ sagaIbGib, metaspace, space, }) {
811
1309
  const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
812
1310
  const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
813
1311
  const conflictData = messageData;
@@ -822,21 +1320,39 @@ export class SyncSagaCoordinator {
822
1320
  }
823
1321
  // #endregion Handlers
824
1322
  async createSyncMsgStone({ data, space, metaspace, }) {
825
- const ib = await getSyncSagaMessageIb({ data });
826
- const stone = await Factory_V1.stone({
827
- ib,
828
- parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
829
- data,
830
- uuid: true, // we want the stone to have its own uniqueness
831
- });
832
- await putInSpace({ space, ibGib: stone });
833
- await metaspace.registerNewIbGib({ ibGib: stone });
834
- return stone;
1323
+ const lc = `${this.lc}[${this.createSyncMsgStone.name}]`;
1324
+ try {
1325
+ if (logalot) {
1326
+ console.log(`${lc} starting... (I: 5f7f98e8ff980364f7191fcee4531e26)`);
1327
+ }
1328
+ const ib = await getSyncSagaMessageIb({ data });
1329
+ const stone = await Factory_V1.stone({
1330
+ ib,
1331
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
1332
+ data,
1333
+ uuid: true, // we want the stone to have its own uniqueness
1334
+ });
1335
+ if (logalot) {
1336
+ console.log(`${lc} Created stone: ${getIbGibAddr({ ibGib: stone })}`);
1337
+ }
1338
+ await putInSpace({ space, ibGib: stone });
1339
+ await metaspace.registerNewIbGib({ ibGib: stone });
1340
+ return stone;
1341
+ }
1342
+ catch (error) {
1343
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1344
+ throw error;
1345
+ }
1346
+ finally {
1347
+ if (logalot) {
1348
+ console.log(`${lc} complete.`);
1349
+ }
1350
+ }
835
1351
  }
836
1352
  /**
837
1353
  * Evolves the saga timeline with a new frame.
838
1354
  */
839
- async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, }) {
1355
+ async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, conflictStrategy, }) {
840
1356
  const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
841
1357
  try {
842
1358
  // Validation
@@ -907,6 +1423,7 @@ export class SyncSagaCoordinator {
907
1423
  payload: undefined, // Data in stone
908
1424
  n: 0,
909
1425
  isTjp: true,
1426
+ conflictStrategy,
910
1427
  };
911
1428
  const ib = await getSyncIb({ data });
912
1429
  const stoneAddrs = msgStones.map(s => getIbGibAddr({ ibGib: s }));