@ibgib/core-gib 0.1.19 → 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.
- package/dist/common/other/ibgib-helper.d.mts +13 -0
- package/dist/common/other/ibgib-helper.d.mts.map +1 -1
- package/dist/common/other/ibgib-helper.mjs +44 -0
- package/dist/common/other/ibgib-helper.mjs.map +1 -1
- package/dist/sync/merge-info/merge-info-constants.d.mts +2 -0
- package/dist/sync/merge-info/merge-info-constants.d.mts.map +1 -0
- package/dist/sync/merge-info/merge-info-constants.mjs +2 -0
- package/dist/sync/merge-info/merge-info-constants.mjs.map +1 -0
- package/dist/sync/merge-info/merge-info-helpers.d.mts +51 -0
- package/dist/sync/merge-info/merge-info-helpers.d.mts.map +1 -0
- package/dist/sync/merge-info/merge-info-helpers.mjs +92 -0
- package/dist/sync/merge-info/merge-info-helpers.mjs.map +1 -0
- package/dist/sync/merge-info/merge-info-helpers.respec.d.mts +2 -0
- package/dist/sync/merge-info/merge-info-helpers.respec.d.mts.map +1 -0
- package/dist/sync/merge-info/merge-info-helpers.respec.mjs +32 -0
- package/dist/sync/merge-info/merge-info-helpers.respec.mjs.map +1 -0
- package/dist/sync/merge-info/merge-info-types.d.mts +26 -0
- package/dist/sync/merge-info/merge-info-types.d.mts.map +1 -0
- package/dist/sync/merge-info/merge-info-types.mjs +2 -0
- package/dist/sync/merge-info/merge-info-types.mjs.map +1 -0
- package/dist/sync/strategies/conflict-optimistic.d.mts +37 -0
- package/dist/sync/strategies/conflict-optimistic.d.mts.map +1 -0
- package/dist/sync/strategies/conflict-optimistic.mjs +162 -0
- package/dist/sync/strategies/conflict-optimistic.mjs.map +1 -0
- package/dist/sync/sync-conflict.respec.d.mts +8 -0
- package/dist/sync/sync-conflict.respec.d.mts.map +1 -0
- package/dist/sync/sync-conflict.respec.mjs +158 -0
- package/dist/sync/sync-conflict.respec.mjs.map +1 -0
- package/dist/sync/sync-innerspace-constants.respec.mjs +2 -2
- package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs +0 -1
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace.respec.mjs +1 -1
- package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
- package/dist/sync/sync-saga-coordinator.d.mts +23 -12
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +473 -107
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts +11 -0
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -1
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +24 -0
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -1
- package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +23 -0
- package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -1
- package/dist/sync/sync-types.d.mts +31 -4
- package/dist/sync/sync-types.d.mts.map +1 -1
- package/dist/sync/sync-types.mjs.map +1 -1
- package/ibgib-foundations.md +129 -0
- package/package.json +1 -1
- package/roadmap.md +59 -0
- package/src/common/other/ibgib-helper.mts +52 -0
- package/src/keystone/README.md +13 -155
- package/src/keystone/docs/architecture.md +55 -0
- package/src/sync/README.md +37 -42
- package/src/sync/docs/architecture.md +69 -0
- package/src/sync/docs/verification.md +43 -0
- package/src/sync/merge-info/merge-info-constants.mts +1 -0
- package/src/sync/merge-info/merge-info-helpers.mts +134 -0
- package/src/sync/merge-info/merge-info-helpers.respec.mts +41 -0
- package/src/sync/merge-info/merge-info-types.mts +28 -0
- package/src/sync/strategies/conflict-optimistic.mts +208 -0
- package/src/sync/sync-conflict.respec.mts +194 -0
- package/src/sync/sync-innerspace-constants.respec.mts +1 -1
- package/src/sync/sync-innerspace-deep-updates.respec.mts +0 -1
- package/src/sync/sync-innerspace.respec.mts +1 -1
- package/src/sync/sync-saga-coordinator.mts +524 -118
- package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +41 -0
- package/src/sync/sync-saga-message/sync-saga-message-types.mts +23 -0
- package/src/sync/sync-types.mts +33 -4
- package/tmp.md +2 -425
|
@@ -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;
|
|
@@ -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,
|
|
492
|
+
return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, space, identity, identitySecret });
|
|
436
493
|
case SyncStage.ack:
|
|
437
|
-
return await this.handleAckFrame({ sagaIbGib, srcGraph,
|
|
494
|
+
return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, space, identity });
|
|
438
495
|
case SyncStage.delta:
|
|
439
|
-
return await this.handleDeltaFrame({ sagaIbGib, srcGraph, space, identity,
|
|
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,15 +532,19 @@ 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
|
}
|
|
544
|
+
// Determine Strategy from Saga Data (since V1 stores it in root)
|
|
545
|
+
const conflictStrategy = sagaIbGib.data.conflictStrategy || 'abort';
|
|
484
546
|
// 2. Gap Analysis
|
|
485
547
|
const conflicts = [];
|
|
486
|
-
const conflictStrategy = initData.conflictStrategy || 'abort'; // Default to abort if not specified, or we should parameterize this
|
|
487
548
|
const deltaReqAddrs = [];
|
|
488
549
|
const pushOfferAddrs = [];
|
|
489
550
|
// Stones Analysis (Constants / Non-TJPs)
|
|
@@ -571,20 +632,44 @@ export class SyncSagaCoordinator {
|
|
|
571
632
|
}
|
|
572
633
|
else {
|
|
573
634
|
// DIVERGENCE: Both have changes the other doesn't know about.
|
|
574
|
-
conflicts.push({ tjp, localAddr, remoteAddr, reason: 'divergence' });
|
|
575
|
-
// Conflict Strategy Handling
|
|
576
|
-
// TODO: Implement "manual" strategy (requires user intervention/pause)
|
|
577
|
-
// TODO: Implement "local_wins" strategy
|
|
578
|
-
// TODO: Implement "server_wins" strategy
|
|
579
635
|
if (conflictStrategy === 'abort') {
|
|
580
|
-
// We will
|
|
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
|
+
});
|
|
581
648
|
}
|
|
582
649
|
else if (conflictStrategy === 'optimistic') {
|
|
583
|
-
// Optimistic: We
|
|
584
|
-
// We
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
|
|
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
|
+
});
|
|
588
673
|
}
|
|
589
674
|
else {
|
|
590
675
|
throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
|
|
@@ -592,32 +677,24 @@ export class SyncSagaCoordinator {
|
|
|
592
677
|
}
|
|
593
678
|
}
|
|
594
679
|
}
|
|
595
|
-
if
|
|
680
|
+
// Check if we should ABORT (if any conflict is terminal)
|
|
681
|
+
const hasTerminalConflicts = conflicts.some(c => c.terminal);
|
|
682
|
+
if (hasTerminalConflicts) {
|
|
596
683
|
// Abort Strategy: Kill the saga.
|
|
597
684
|
if (logalot) {
|
|
598
|
-
console.warn(`${lc} ABORTING Sync Saga due to conflicts: ${JSON.stringify(conflicts)}`);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
metaspace,
|
|
612
|
-
});
|
|
613
|
-
const conflictFrame = await this.evolveSyncSagaIbGib({
|
|
614
|
-
prevSagaIbGib: sagaIbGib,
|
|
615
|
-
msgStones: [conflictStone],
|
|
616
|
-
identity,
|
|
617
|
-
space,
|
|
618
|
-
metaspace,
|
|
619
|
-
});
|
|
620
|
-
return { frame: conflictFrame };
|
|
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.
|
|
621
698
|
}
|
|
622
699
|
// 2. Add Push Offers (Missing in Local)
|
|
623
700
|
// Check if we have them. If not, ask for them.
|
|
@@ -675,24 +752,7 @@ export class SyncSagaCoordinator {
|
|
|
675
752
|
// 2. Receiver says "I don't have X. But if I did have Y (ancestor), I'd tell you."
|
|
676
753
|
// Problem: Receiver doesn't know X is related to Y without X.
|
|
677
754
|
// SOLUTION:
|
|
678
|
-
// The *Sender* must include TJP
|
|
679
|
-
// OR: Receiver does a check.
|
|
680
|
-
// Wait, if Sender sends V2, it's just an address.
|
|
681
|
-
// If Receiver doesn't have it, it's opaque.
|
|
682
|
-
// REVISIT "Constant / No TJP" logic:
|
|
683
|
-
// IF we are testing "Sender Newer", meaning Sender has V2, Receiver has V1.
|
|
684
|
-
// Sender calls `sync([V2])`. Init Frame contains `stones: [V2_Address]`.
|
|
685
|
-
// Receiver checks V2_Address. Not found.
|
|
686
|
-
// Receiver requests V2.
|
|
687
|
-
// Receiver sends Ack(DeltaReq: [V2], Knowledge: {}).
|
|
688
|
-
// Sender receives Ack. Sender sends V2 *AND* its deps (V1, Root).
|
|
689
|
-
// Receiver has V1. Sender sends V1 anyway.
|
|
690
|
-
// This is "Naive Deep Sync".
|
|
691
|
-
// TO ACHIEVE "Smart Diff":
|
|
692
|
-
// Receiver needs to know "Oh, V2 is a timeline tip of TJP_A".
|
|
693
|
-
// If Sender doesn't send TJP info, Receiver is blind.
|
|
694
|
-
// Proposed Fix (Short Term):
|
|
695
|
-
// `SyncInitData` should include TJP mappings or we rely on `knowledgeVector` in `Init`?
|
|
755
|
+
// The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
|
|
696
756
|
// `SyncSagaMessageInitData_V1` extends `SyncInitData`.
|
|
697
757
|
// `SyncInitData` has `knowledgeVector`.
|
|
698
758
|
// If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
|
|
@@ -754,6 +814,127 @@ export class SyncSagaCoordinator {
|
|
|
754
814
|
if (ackData.stage !== SyncStage.ack) {
|
|
755
815
|
throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
|
|
756
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)
|
|
757
938
|
const deltaReqAddrs = ackData.deltaReqAddrs || [];
|
|
758
939
|
const pushOfferAddrs = ackData.pushOfferAddrs || [];
|
|
759
940
|
// 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
|
|
@@ -836,10 +1017,10 @@ export class SyncSagaCoordinator {
|
|
|
836
1017
|
// 3. Create Delta Frame
|
|
837
1018
|
const sagaId = ackData.sagaId;
|
|
838
1019
|
const deltaData = {
|
|
839
|
-
sagaId,
|
|
1020
|
+
sagaId: sagaIbGib.data.uuid,
|
|
840
1021
|
stage: SyncStage.delta,
|
|
841
1022
|
payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
|
|
842
|
-
requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
|
|
1023
|
+
requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
|
|
843
1024
|
};
|
|
844
1025
|
const deltaStone = await this.createSyncMsgStone({
|
|
845
1026
|
data: deltaData,
|
|
@@ -878,22 +1059,36 @@ export class SyncSagaCoordinator {
|
|
|
878
1059
|
if (deltaData.stage !== SyncStage.delta) {
|
|
879
1060
|
throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
|
|
880
1061
|
}
|
|
1062
|
+
if (logalot) {
|
|
1063
|
+
console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`);
|
|
1064
|
+
}
|
|
881
1065
|
const payloadAddrs = deltaData.payloadAddrs || [];
|
|
882
|
-
const
|
|
883
|
-
|
|
1066
|
+
const peerRequests = deltaData.requests || [];
|
|
1067
|
+
const peerProposesCommit = deltaData.proposeCommit || false;
|
|
1068
|
+
// 1. Process Received Payload (Ingest)
|
|
884
1069
|
const receivedPayloadIbGibs = [];
|
|
885
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`.
|
|
886
1076
|
const res = await getFromSpace({
|
|
887
1077
|
addrs: payloadAddrs,
|
|
888
1078
|
space,
|
|
889
1079
|
});
|
|
890
1080
|
if (res.ibGibs) {
|
|
891
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(', ')}`);
|
|
892
1087
|
}
|
|
893
1088
|
}
|
|
894
|
-
// 2. Fulfill Requests (Outgoing Payload)
|
|
1089
|
+
// 2. Fulfill Peer Requests (Outgoing Payload)
|
|
895
1090
|
const outgoingPayload = [];
|
|
896
|
-
for (const addr of
|
|
1091
|
+
for (const addr of peerRequests) {
|
|
897
1092
|
let ibGib = srcGraph[addr];
|
|
898
1093
|
if (!ibGib) {
|
|
899
1094
|
const res = await getFromSpace({ addr, space });
|
|
@@ -904,16 +1099,105 @@ export class SyncSagaCoordinator {
|
|
|
904
1099
|
if (ibGib) {
|
|
905
1100
|
outgoingPayload.push(ibGib);
|
|
906
1101
|
}
|
|
1102
|
+
else {
|
|
1103
|
+
console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
|
|
1104
|
+
}
|
|
907
1105
|
}
|
|
908
|
-
// 3.
|
|
909
|
-
if
|
|
910
|
-
|
|
911
|
-
|
|
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
|
+
}
|
|
1174
|
+
}
|
|
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
|
|
912
1185
|
const responseDeltaData = {
|
|
913
|
-
sagaId,
|
|
1186
|
+
sagaId: deltaData.sagaId,
|
|
914
1187
|
stage: SyncStage.delta,
|
|
915
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.
|
|
916
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;
|
|
917
1201
|
const deltaStone = await this.createSyncMsgStone({
|
|
918
1202
|
data: responseDeltaData,
|
|
919
1203
|
space,
|
|
@@ -929,36 +1213,99 @@ export class SyncSagaCoordinator {
|
|
|
929
1213
|
return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
|
|
930
1214
|
}
|
|
931
1215
|
else {
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
+
}
|
|
952
1288
|
}
|
|
953
1289
|
}
|
|
954
|
-
async handleCommitFrame({ sagaIbGib, space, }) {
|
|
1290
|
+
async handleCommitFrame({ sagaIbGib, space, metaspace, }) {
|
|
955
1291
|
const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
|
|
956
1292
|
if (logalot) {
|
|
957
|
-
console.log(`${lc} Commit received
|
|
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.`);
|
|
958
1305
|
}
|
|
959
1306
|
return null;
|
|
960
1307
|
}
|
|
961
|
-
async handleConflictFrame({ sagaIbGib, space, }) {
|
|
1308
|
+
async handleConflictFrame({ sagaIbGib, metaspace, space, }) {
|
|
962
1309
|
const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
|
|
963
1310
|
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
964
1311
|
const conflictData = messageData;
|
|
@@ -973,21 +1320,39 @@ export class SyncSagaCoordinator {
|
|
|
973
1320
|
}
|
|
974
1321
|
// #endregion Handlers
|
|
975
1322
|
async createSyncMsgStone({ data, space, metaspace, }) {
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
+
}
|
|
986
1351
|
}
|
|
987
1352
|
/**
|
|
988
1353
|
* Evolves the saga timeline with a new frame.
|
|
989
1354
|
*/
|
|
990
|
-
async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, }) {
|
|
1355
|
+
async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, conflictStrategy, }) {
|
|
991
1356
|
const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
|
|
992
1357
|
try {
|
|
993
1358
|
// Validation
|
|
@@ -1058,6 +1423,7 @@ export class SyncSagaCoordinator {
|
|
|
1058
1423
|
payload: undefined, // Data in stone
|
|
1059
1424
|
n: 0,
|
|
1060
1425
|
isTjp: true,
|
|
1426
|
+
conflictStrategy,
|
|
1061
1427
|
};
|
|
1062
1428
|
const ib = await getSyncIb({ data });
|
|
1063
1429
|
const stoneAddrs = msgStones.map(s => getIbGibAddr({ ibGib: s }));
|