@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.
- 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.d.mts +8 -0
- package/dist/sync/sync-innerspace-constants.respec.d.mts.map +1 -0
- package/dist/sync/sync-innerspace-constants.respec.mjs +116 -0
- package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -0
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs +2 -3
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-dest-ahead.respec.mjs +3 -3
- package/dist/sync/sync-innerspace-dest-ahead.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs +2 -2
- package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-partial-update.respec.d.mts +7 -0
- package/dist/sync/sync-innerspace-partial-update.respec.d.mts.map +1 -0
- package/dist/sync/sync-innerspace-partial-update.respec.mjs +116 -0
- package/dist/sync/sync-innerspace-partial-update.respec.mjs.map +1 -0
- package/dist/sync/sync-innerspace.respec.mjs +5 -5
- 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 +612 -95
- 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 +30 -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 +133 -0
- package/src/sync/sync-innerspace-deep-updates.respec.mts +1 -2
- package/src/sync/sync-innerspace-dest-ahead.respec.mts +2 -2
- package/src/sync/sync-innerspace-multiple-timelines.respec.mts +1 -1
- package/src/sync/sync-innerspace-partial-update.respec.mts +150 -0
- package/src/sync/sync-innerspace.respec.mts +4 -4
- package/src/sync/sync-saga-coordinator.mts +673 -103
- package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +41 -0
- package/src/sync/sync-saga-message/sync-saga-message-types.mts +28 -0
- package/src/sync/sync-types.mts +33 -4
- 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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
620
|
-
|
|
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
|
|
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
|
|
700
|
-
// We
|
|
701
|
-
|
|
702
|
-
//
|
|
703
|
-
|
|
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
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
msgStones: [conflictStone],
|
|
733
|
-
identity,
|
|
734
|
-
space,
|
|
735
|
-
metaspace,
|
|
736
|
-
});
|
|
839
|
+
// Let's use Ack with conflicts.
|
|
840
|
+
}
|
|
737
841
|
|
|
738
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
932
|
-
if
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
//
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1447
|
+
const commitStone = await this.createSyncMsgStone({
|
|
1448
|
+
data: commitData,
|
|
1449
|
+
space,
|
|
1450
|
+
metaspace
|
|
1451
|
+
});
|
|
971
1452
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
data
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
|