@ibgib/core-gib 0.1.20 → 0.1.21
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/sync/graft-info/graft-info-constants.d.mts +5 -0
- package/dist/sync/graft-info/graft-info-constants.d.mts.map +1 -0
- package/dist/sync/graft-info/graft-info-constants.mjs +5 -0
- package/dist/sync/graft-info/graft-info-constants.mjs.map +1 -0
- package/dist/sync/graft-info/graft-info-helpers.d.mts +49 -0
- package/dist/sync/graft-info/graft-info-helpers.d.mts.map +1 -0
- package/dist/sync/graft-info/graft-info-helpers.mjs +236 -0
- package/dist/sync/graft-info/graft-info-helpers.mjs.map +1 -0
- package/dist/sync/graft-info/graft-info-helpers.respec.d.mts +2 -0
- package/dist/sync/graft-info/graft-info-helpers.respec.d.mts.map +1 -0
- package/dist/sync/graft-info/graft-info-helpers.respec.mjs +70 -0
- package/dist/sync/graft-info/graft-info-helpers.respec.mjs.map +1 -0
- package/dist/sync/graft-info/graft-info-types.d.mts +31 -0
- package/dist/sync/{merge-info/merge-info-types.d.mts.map → graft-info/graft-info-types.d.mts.map} +1 -1
- package/dist/sync/graft-info/graft-info-types.mjs +2 -0
- package/dist/sync/graft-info/graft-info-types.mjs.map +1 -0
- package/dist/sync/strategies/conflict-optimistic.d.mts +1 -1
- package/dist/sync/strategies/conflict-optimistic.d.mts.map +1 -1
- package/dist/sync/strategies/conflict-optimistic.mjs +10 -60
- package/dist/sync/strategies/conflict-optimistic.mjs.map +1 -1
- package/dist/sync/sync-conflict.respec.mjs +152 -33
- package/dist/sync/sync-conflict.respec.mjs.map +1 -1
- package/dist/sync/sync-constants.d.mts +1 -3
- package/dist/sync/sync-constants.d.mts.map +1 -1
- package/dist/sync/sync-constants.mjs +0 -2
- package/dist/sync/sync-constants.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-innerspace-v1.d.mts +5 -2
- package/dist/sync/sync-peer/sync-peer-innerspace-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-innerspace-v1.mjs +70 -7
- package/dist/sync/sync-peer/sync-peer-innerspace-v1.mjs.map +1 -1
- package/dist/sync/sync-saga-coordinator.d.mts +25 -18
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +508 -316
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
- 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 +1 -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 +1 -12
- package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -1
- package/ibgib-foundations.md +20 -2
- package/package.json +1 -1
- package/src/sync/graft-info/graft-info-constants.mts +4 -0
- package/src/sync/graft-info/graft-info-helpers.mts +308 -0
- package/src/sync/graft-info/graft-info-helpers.respec.mts +83 -0
- package/src/sync/graft-info/graft-info-types.mts +33 -0
- package/src/sync/strategies/conflict-optimistic.mts +11 -70
- package/src/sync/sync-conflict.respec.mts +171 -35
- package/src/sync/sync-constants.mts +1 -4
- package/src/sync/sync-peer/sync-peer-innerspace-v1.mts +85 -12
- package/src/sync/sync-saga-coordinator.mts +569 -338
- package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +2 -0
- package/src/sync/sync-saga-message/sync-saga-message-types.mts +0 -11
- package/test_output.log +0 -0
- package/tmp.md +43 -2
- package/dist/sync/merge-info/merge-info-constants.d.mts +0 -2
- package/dist/sync/merge-info/merge-info-constants.d.mts.map +0 -1
- package/dist/sync/merge-info/merge-info-constants.mjs +0 -2
- package/dist/sync/merge-info/merge-info-constants.mjs.map +0 -1
- package/dist/sync/merge-info/merge-info-helpers.d.mts +0 -51
- package/dist/sync/merge-info/merge-info-helpers.d.mts.map +0 -1
- package/dist/sync/merge-info/merge-info-helpers.mjs +0 -92
- package/dist/sync/merge-info/merge-info-helpers.mjs.map +0 -1
- package/dist/sync/merge-info/merge-info-helpers.respec.d.mts +0 -2
- package/dist/sync/merge-info/merge-info-helpers.respec.d.mts.map +0 -1
- package/dist/sync/merge-info/merge-info-helpers.respec.mjs +0 -32
- package/dist/sync/merge-info/merge-info-helpers.respec.mjs.map +0 -1
- package/dist/sync/merge-info/merge-info-types.d.mts +0 -26
- package/dist/sync/merge-info/merge-info-types.mjs +0 -2
- package/dist/sync/merge-info/merge-info-types.mjs.map +0 -1
- package/src/sync/merge-info/merge-info-constants.mts +0 -1
- package/src/sync/merge-info/merge-info-helpers.mts +0 -134
- package/src/sync/merge-info/merge-info-helpers.respec.mts +0 -41
- package/src/sync/merge-info/merge-info-types.mts +0 -28
|
@@ -31,7 +31,7 @@ import { getDependencyGraph } from "../common/other/graph-helper.mjs";
|
|
|
31
31
|
import {
|
|
32
32
|
SyncSagaMessageData_V1, SyncSagaMessageInitData_V1,
|
|
33
33
|
SyncSagaMessageAckData_V1, SyncSagaMessageDeltaData_V1,
|
|
34
|
-
SyncSagaMessageCommitData_V1
|
|
34
|
+
SyncSagaMessageCommitData_V1
|
|
35
35
|
} from "./sync-saga-message/sync-saga-message-types.mjs";
|
|
36
36
|
import { getSyncSagaMessageIb } from "./sync-saga-message/sync-saga-message-helpers.mjs";
|
|
37
37
|
import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-constants.mjs";
|
|
@@ -143,7 +143,7 @@ export class SyncSagaCoordinator {
|
|
|
143
143
|
const sessionIdentity = useSessionIdentity
|
|
144
144
|
? await this.getSessionIdentity({ sagaId, metaspace, tempSpace })
|
|
145
145
|
: undefined;
|
|
146
|
-
if (logalot) { console.log(`${lc} sessionIdentity: ${sessionIdentity ? pretty(sessionIdentity) : 'undefined'} (I: abc01872800b3a66b819a05898bba826)`); }
|
|
146
|
+
// if (logalot) { console.log(`${lc} sessionIdentity: ${sessionIdentity ? pretty(sessionIdentity) : 'undefined'} (I: abc01872800b3a66b819a05898bba826)`); }
|
|
147
147
|
|
|
148
148
|
// 3. CREATE INITIAL FRAME (Stage.init)
|
|
149
149
|
const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
|
|
@@ -335,7 +335,7 @@ export class SyncSagaCoordinator {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
// B. Transmit
|
|
338
|
-
if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
|
|
338
|
+
// if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
|
|
339
339
|
updates$.next(requestCtx);
|
|
340
340
|
const responseCtx = await peer.witness(requestCtx);
|
|
341
341
|
|
|
@@ -389,6 +389,7 @@ export class SyncSagaCoordinator {
|
|
|
389
389
|
await putInSpace({ space: tempSpace, ibGibs: remoteDeps });
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
|
|
392
393
|
// React (Reducer)
|
|
393
394
|
// This processes the FRAME we just got from the peer.
|
|
394
395
|
// i.e., We Sent Init -> Got Ack. This calls handleAckFrame.
|
|
@@ -396,7 +397,8 @@ export class SyncSagaCoordinator {
|
|
|
396
397
|
const result = await this.handleSagaFrame({
|
|
397
398
|
sagaIbGib: remoteFrame as SyncIbGib_V1,
|
|
398
399
|
srcGraph,
|
|
399
|
-
|
|
400
|
+
destSpace: localSpace, // Query existing data from localSpace (Source)
|
|
401
|
+
tempSpace: tempSpace, // Transaction space for saga frames
|
|
400
402
|
identity: sessionIdentity,
|
|
401
403
|
metaspace
|
|
402
404
|
});
|
|
@@ -428,31 +430,65 @@ export class SyncSagaCoordinator {
|
|
|
428
430
|
tjpAddrs?: string[],
|
|
429
431
|
}): Promise<{ [tjp: string]: string | null }> {
|
|
430
432
|
const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
|
|
431
|
-
|
|
433
|
+
try {
|
|
434
|
+
if (logalot) { console.log(`${lc} starting... (I: e184f8a7818666febfbbd2d841ed3826)`); }
|
|
435
|
+
console.dir(space)
|
|
432
436
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
437
|
+
if (!(domainIbGibs && domainIbGibs.length > 0) &&
|
|
438
|
+
!(tjpAddrs && tjpAddrs.length > 0)
|
|
439
|
+
) {
|
|
440
|
+
throw new Error(`(UNEXPECTED) domainIbGibs and tjpAddrs falsy/empty? we need one or the other (E: f674285111c8648398cd79d8c08ec826)`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if ((domainIbGibs && domainIbGibs.length > 0) &&
|
|
444
|
+
(tjpAddrs && tjpAddrs.length > 0)
|
|
445
|
+
) {
|
|
446
|
+
throw new Error(`(UNEXPECTED) both domainIbGibs and tjpAddrs truthy? only pass in one or the other. (E: f674285111c8648398cd79d8c08ec826)`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let tjps: string[] = [];
|
|
450
|
+
if (tjpAddrs) {
|
|
451
|
+
tjps = tjpAddrs;
|
|
452
|
+
} else if (domainIbGibs && domainIbGibs.length > 0) {
|
|
453
|
+
// Extract TJPs from domain Ibgibs
|
|
454
|
+
if (logalot) { console.log(`${lc} domainIbGibs (${domainIbGibs.length}) provided. (I: a378995a0658af1f086ac1f297486c26)`); }
|
|
455
|
+
|
|
456
|
+
const { mapWithTjp_YesDna, mapWithTjp_NoDna } =
|
|
457
|
+
splitPerTjpAndOrDna({ ibGibs: domainIbGibs });
|
|
458
|
+
if (logalot) { console.log(`${lc}[TEST DEBUG] mapWithTjp_YesDna: ${JSON.stringify(mapWithTjp_YesDna)} (I: 287e22897148298e185712c8d50cfb26)`); }
|
|
459
|
+
if (logalot) { console.log(`${lc}[TEST DEBUG] mapWithTjp_NoDna: ${JSON.stringify(mapWithTjp_NoDna)} (I: 1bdc62656294aed0f9df334647dc7326)`); }
|
|
460
|
+
|
|
461
|
+
const allWithTjp = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
|
|
462
|
+
const timelineMap = getTimelinesGroupedByTjp({ ibGibs: allWithTjp });
|
|
463
|
+
if (logalot) { console.log(`${lc}[TEST DEBUG] timelineMap: ${JSON.stringify(timelineMap)} (I: 2cc04898e5f85179fb1ac7f827abc426)`); }
|
|
464
|
+
|
|
465
|
+
tjps = Object.keys(timelineMap);
|
|
466
|
+
if (logalot) { console.log(`${lc}[TEST DEBUG] tjps: ${tjps} (I: 3dd548667cbd967c68e57c88dc570826)`); }
|
|
467
|
+
} else {
|
|
468
|
+
// No info provided. Return empty? Or throw?
|
|
469
|
+
// User test context implied "everything", but implementation requires scope.
|
|
470
|
+
console.warn(`${lc} No domainIbGibs or tjpAddrs provided. Returning empty KV.`);
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (tjps.length === 0) { return {}; }
|
|
448
475
|
|
|
449
|
-
|
|
476
|
+
if (logalot) { console.log(`${lc} getting latest addrs for tjps: ${tjps} (I: d4e7080b8ba8187c583b82fd91ac0626)`); }
|
|
477
|
+
|
|
478
|
+
const res = await getLatestAddrs({ space, tjpAddrs: tjps });
|
|
479
|
+
if (!res.data || !res.data.latestAddrsMap) {
|
|
480
|
+
throw new Error(`${lc} Failed to get latest addrs. (E: 7a8b9c0d)`);
|
|
481
|
+
}
|
|
450
482
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
483
|
+
if (logalot) { console.log(`${lc}[TEST DEBUG] res.data.latestAddrsMap: ${JSON.stringify(res.data.latestAddrsMap)} (I: a8e128bdf80898ac2e6d8021a5bff726)`); }
|
|
484
|
+
|
|
485
|
+
return res.data.latestAddrsMap;
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
488
|
+
throw error;
|
|
489
|
+
} finally {
|
|
490
|
+
if (logalot) { console.log(`${lc} complete.`); }
|
|
454
491
|
}
|
|
455
|
-
return res.data.latestAddrsMap;
|
|
456
492
|
}
|
|
457
493
|
|
|
458
494
|
protected async analyzeTimelines({
|
|
@@ -545,12 +581,17 @@ export class SyncSagaCoordinator {
|
|
|
545
581
|
initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
|
|
546
582
|
});
|
|
547
583
|
|
|
584
|
+
if (logalot) {
|
|
585
|
+
console.log(`${lc} SyncStage.init: ${SyncStage.init}, SyncStage.commit: ${SyncStage.commit}`);
|
|
586
|
+
console.log(`${lc} initData.stage: ${initData.stage}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
548
589
|
const initStone = await this.createSyncMsgStone({
|
|
549
590
|
data: initData,
|
|
550
591
|
space: tempSpace,
|
|
551
592
|
metaspace
|
|
552
593
|
});
|
|
553
|
-
if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
|
|
594
|
+
// if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
|
|
554
595
|
|
|
555
596
|
const sagaFrame = await this.evolveSyncSagaIbGib({
|
|
556
597
|
msgStones: [initStone],
|
|
@@ -559,7 +600,16 @@ export class SyncSagaCoordinator {
|
|
|
559
600
|
metaspace,
|
|
560
601
|
conflictStrategy,
|
|
561
602
|
});
|
|
562
|
-
|
|
603
|
+
|
|
604
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
605
|
+
await this.ensureSagaFrameInBothSpaces({
|
|
606
|
+
frame: sagaFrame,
|
|
607
|
+
destSpace: localSpace, // localSpace is the Sender's destSpace
|
|
608
|
+
tempSpace,
|
|
609
|
+
metaspace
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
|
|
563
613
|
|
|
564
614
|
return { sagaFrame, srcGraph };
|
|
565
615
|
|
|
@@ -587,14 +637,16 @@ export class SyncSagaCoordinator {
|
|
|
587
637
|
async handleSagaFrame({
|
|
588
638
|
sagaIbGib,
|
|
589
639
|
srcGraph,
|
|
590
|
-
|
|
640
|
+
destSpace,
|
|
641
|
+
tempSpace,
|
|
591
642
|
identity,
|
|
592
643
|
identitySecret,
|
|
593
644
|
metaspace,
|
|
594
645
|
}: {
|
|
595
646
|
sagaIbGib: SyncIbGib_V1,
|
|
596
647
|
srcGraph: { [addr: string]: IbGib_V1 },
|
|
597
|
-
|
|
648
|
+
destSpace: IbGibSpaceAny,
|
|
649
|
+
tempSpace: IbGibSpaceAny,
|
|
598
650
|
identity?: KeystoneIbGib_V1,
|
|
599
651
|
identitySecret?: string,
|
|
600
652
|
metaspace: MetaspaceService,
|
|
@@ -607,25 +659,22 @@ export class SyncSagaCoordinator {
|
|
|
607
659
|
if (logalot) { console.log(`${lc} sagaIbGib: ${pretty(sagaIbGib)} (I: 1b99d87d262e9d18d8a607a80b1a0126)`); }
|
|
608
660
|
|
|
609
661
|
// Get Stage from Stone (or Frame for Init fallback)
|
|
610
|
-
const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
662
|
+
const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
611
663
|
|
|
612
664
|
if (logalot) { console.log(`${lc} handling frame stage: ${stage}`); }
|
|
613
665
|
|
|
614
666
|
switch (stage) {
|
|
615
667
|
case SyncStage.init:
|
|
616
|
-
return await this.handleInitFrame({ sagaIbGib, messageData, metaspace,
|
|
668
|
+
return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, destSpace, tempSpace, identity, identitySecret });
|
|
617
669
|
|
|
618
670
|
case SyncStage.ack:
|
|
619
|
-
return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace,
|
|
671
|
+
return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity });
|
|
620
672
|
|
|
621
673
|
case SyncStage.delta:
|
|
622
|
-
return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace,
|
|
674
|
+
return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity, });
|
|
623
675
|
|
|
624
676
|
case SyncStage.commit:
|
|
625
|
-
return await this.handleCommitFrame({ sagaIbGib, metaspace,
|
|
626
|
-
|
|
627
|
-
case SyncStage.conflict:
|
|
628
|
-
return await this.handleConflictFrame({ sagaIbGib, metaspace, space, });
|
|
677
|
+
return await this.handleCommitFrame({ sagaIbGib, metaspace, destSpace, tempSpace, identity, });
|
|
629
678
|
|
|
630
679
|
default:
|
|
631
680
|
throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
|
|
@@ -655,19 +704,22 @@ export class SyncSagaCoordinator {
|
|
|
655
704
|
protected async handleInitFrame({
|
|
656
705
|
sagaIbGib,
|
|
657
706
|
messageData,
|
|
658
|
-
|
|
707
|
+
destSpace,
|
|
708
|
+
tempSpace,
|
|
659
709
|
metaspace,
|
|
660
710
|
identity,
|
|
661
711
|
identitySecret,
|
|
662
712
|
}: {
|
|
663
713
|
sagaIbGib: SyncIbGib_V1,
|
|
664
714
|
messageData: any,
|
|
665
|
-
|
|
715
|
+
destSpace: IbGibSpaceAny,
|
|
716
|
+
tempSpace: IbGibSpaceAny,
|
|
666
717
|
metaspace: MetaspaceService,
|
|
667
718
|
identity?: KeystoneIbGib_V1,
|
|
668
719
|
identitySecret?: string,
|
|
669
720
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
670
721
|
const lc = `${this.lc}[${this.handleInitFrame.name}]`;
|
|
722
|
+
console.log(`${lc} [TEST DEBUG] Received destSpace: ${destSpace.data?.name || destSpace.ib} (uuid: ${destSpace.data?.uuid || '[no uuid]'})`);
|
|
671
723
|
if (logalot) { console.log(`${lc} starting...`); }
|
|
672
724
|
|
|
673
725
|
// Extract Init Data
|
|
@@ -675,7 +727,7 @@ export class SyncSagaCoordinator {
|
|
|
675
727
|
if (initData.stage !== SyncStage.init) {
|
|
676
728
|
throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
|
|
677
729
|
}
|
|
678
|
-
if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
|
|
730
|
+
// if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
|
|
679
731
|
if (!initData || !initData.knowledgeVector) {
|
|
680
732
|
throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
|
|
681
733
|
}
|
|
@@ -694,7 +746,7 @@ export class SyncSagaCoordinator {
|
|
|
694
746
|
if (stones.length > 0) {
|
|
695
747
|
if (logalot) { console.log(`${lc} processing stones: ${stones.length}`); }
|
|
696
748
|
// Check if we have these stones
|
|
697
|
-
const resStones = await getFromSpace({ space, addrs: stones });
|
|
749
|
+
const resStones = await getFromSpace({ space: destSpace, addrs: stones });
|
|
698
750
|
const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
|
|
699
751
|
if (addrsNotFound && addrsNotFound.length > 0) {
|
|
700
752
|
if (logalot) { console.log(`${lc} stones missing (requesting): ${addrsNotFound.length}`); }
|
|
@@ -709,6 +761,7 @@ export class SyncSagaCoordinator {
|
|
|
709
761
|
const remoteKV = initData.knowledgeVector;
|
|
710
762
|
if (logalot) { console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`); }
|
|
711
763
|
const remoteTjps = Object.keys(remoteKV);
|
|
764
|
+
console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`);
|
|
712
765
|
if (logalot) { console.log(`${lc} remoteTjps: ${pretty(remoteTjps)} (I: 86ea4c53db0dc184c8b253386c402126)`); }
|
|
713
766
|
|
|
714
767
|
// 1. Get Local Latest Addrs for all TJPs
|
|
@@ -716,12 +769,13 @@ export class SyncSagaCoordinator {
|
|
|
716
769
|
if (remoteTjps.length > 0) {
|
|
717
770
|
// Batch get latest addrs for the TJPs
|
|
718
771
|
const resGetLatestAddrs = await getLatestAddrs({
|
|
719
|
-
space,
|
|
772
|
+
space: destSpace,
|
|
720
773
|
tjpAddrs: remoteTjps,
|
|
721
774
|
});
|
|
722
775
|
if (!resGetLatestAddrs.data) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data falsy? (E: b180d813c088042b38e1e02e06a16926)`); }
|
|
723
776
|
if (!resGetLatestAddrs.data.latestAddrsMap) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data.latestAddrsMap falsy? (E: 16bc386dd51d0ff53a49620b1e641826)`); }
|
|
724
777
|
localKV = resGetLatestAddrs.data.latestAddrsMap;
|
|
778
|
+
console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localKV)}`);
|
|
725
779
|
if (logalot) { console.log(`${lc} localKV: ${pretty(localKV)} (I: 980975642cbccd8018cf0cd808d30826)`); }
|
|
726
780
|
}
|
|
727
781
|
|
|
@@ -734,24 +788,28 @@ export class SyncSagaCoordinator {
|
|
|
734
788
|
|
|
735
789
|
if (!localAddr) {
|
|
736
790
|
// We (Receiver) don't have this timeline. Request it.
|
|
791
|
+
console.log(`${lc} [TEST DEBUG] Missing local timeline for TJP: ${tjp}. Requesting remoteAddr: ${remoteAddr}`);
|
|
737
792
|
deltaReqAddrs.push(remoteAddr);
|
|
738
793
|
continue;
|
|
739
794
|
}
|
|
740
795
|
|
|
741
796
|
if (localAddr === remoteAddr) {
|
|
742
797
|
// Synced
|
|
798
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`);
|
|
743
799
|
continue;
|
|
744
800
|
}
|
|
801
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`);
|
|
745
802
|
|
|
746
803
|
// Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
|
|
747
804
|
// (Sender has older version, Receiver has newer) -> Receiver Offers Push
|
|
748
805
|
const isRemoteInPast = await isPastFrame({
|
|
749
806
|
olderAddr: remoteAddr,
|
|
750
807
|
newerAddr: localAddr,
|
|
751
|
-
space,
|
|
808
|
+
space: destSpace,
|
|
752
809
|
});
|
|
753
810
|
|
|
754
811
|
if (isRemoteInPast) {
|
|
812
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote is in past - offering push`);
|
|
755
813
|
pushOfferAddrs.push(localAddr);
|
|
756
814
|
} else {
|
|
757
815
|
// Remote is not in our past.
|
|
@@ -761,14 +819,16 @@ export class SyncSagaCoordinator {
|
|
|
761
819
|
const isLocalInPast = await isPastFrame({
|
|
762
820
|
olderAddr: localAddr,
|
|
763
821
|
newerAddr: remoteAddr,
|
|
764
|
-
space,
|
|
822
|
+
space: destSpace,
|
|
765
823
|
});
|
|
766
824
|
|
|
767
825
|
if (isLocalInPast) {
|
|
768
826
|
// Fast-Forward: We update to remote's tip.
|
|
827
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`);
|
|
769
828
|
deltaReqAddrs.push(remoteAddr);
|
|
770
829
|
} else {
|
|
771
830
|
// DIVERGENCE: Both have changes the other doesn't know about.
|
|
831
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`);
|
|
772
832
|
|
|
773
833
|
if (conflictStrategy === 'abort') {
|
|
774
834
|
// Abort Strategy: We will treat this as terminal.
|
|
@@ -795,7 +855,7 @@ export class SyncSagaCoordinator {
|
|
|
795
855
|
|
|
796
856
|
// We need the ACTUAL object to get the past.
|
|
797
857
|
// We have localAddr.
|
|
798
|
-
const resLocalTip = await getFromSpace({ space, addr: localAddr! });
|
|
858
|
+
const resLocalTip = await getFromSpace({ space: destSpace, addr: localAddr! });
|
|
799
859
|
const localTip = resLocalTip.ibGibs?.[0];
|
|
800
860
|
if (!localTip) { throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`); }
|
|
801
861
|
|
|
@@ -842,7 +902,7 @@ export class SyncSagaCoordinator {
|
|
|
842
902
|
// 2. Add Push Offers (Missing in Local)
|
|
843
903
|
// Check if we have them. If not, ask for them.
|
|
844
904
|
for (const addr of pushOfferAddrs) {
|
|
845
|
-
const hasIt = await getFromSpace({ addr, space });
|
|
905
|
+
const hasIt = await getFromSpace({ addr, space: destSpace });
|
|
846
906
|
if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
|
|
847
907
|
// If we don't have it, we put it in `deltaReqAddrs` of the Ack.
|
|
848
908
|
deltaReqAddrs.push(addr);
|
|
@@ -861,7 +921,7 @@ export class SyncSagaCoordinator {
|
|
|
861
921
|
for (const tjp of remoteTjps) {
|
|
862
922
|
const localAddr = localKV[tjp];
|
|
863
923
|
if (localAddr) {
|
|
864
|
-
const res = await getFromSpace({ addr: localAddr, space });
|
|
924
|
+
const res = await getFromSpace({ addr: localAddr, space: destSpace });
|
|
865
925
|
if (res.success && res.ibGibs?.[0]) {
|
|
866
926
|
const ibGib = res.ibGibs[0];
|
|
867
927
|
const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized
|
|
@@ -879,7 +939,7 @@ export class SyncSagaCoordinator {
|
|
|
879
939
|
// We can't really know if it's covered easily without resolving.
|
|
880
940
|
// But if we don't have it (requesting), we won't find it here anyway.
|
|
881
941
|
// If we DO have it (push offer), we might find it.
|
|
882
|
-
const res = await getFromSpace({ addr, space });
|
|
942
|
+
const res = await getFromSpace({ addr, space: destSpace });
|
|
883
943
|
if (res.success && res.ibGibs?.[0]) {
|
|
884
944
|
const ibGib = res.ibGibs[0];
|
|
885
945
|
const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
|
|
@@ -918,11 +978,12 @@ export class SyncSagaCoordinator {
|
|
|
918
978
|
deltaReqAddrs,
|
|
919
979
|
pushOfferAddrs,
|
|
920
980
|
knowledgeVector,
|
|
981
|
+
conflicts: conflicts.length > 0 ? conflicts : undefined, // Include conflicts if any detected
|
|
921
982
|
};
|
|
922
983
|
|
|
923
984
|
const ackStone = await this.createSyncMsgStone({
|
|
924
985
|
data: ackData,
|
|
925
|
-
space,
|
|
986
|
+
space: tempSpace,
|
|
926
987
|
metaspace,
|
|
927
988
|
});
|
|
928
989
|
if (logalot) { console.log(`${lc} ackStone created: ${pretty(ackStone)} (I: 313708132dd53ff946befb7833657826)`); }
|
|
@@ -932,11 +993,14 @@ export class SyncSagaCoordinator {
|
|
|
932
993
|
prevSagaIbGib: sagaIbGib,
|
|
933
994
|
msgStones: [ackStone],
|
|
934
995
|
identity,
|
|
935
|
-
space,
|
|
996
|
+
space: tempSpace,
|
|
936
997
|
metaspace,
|
|
937
998
|
});
|
|
938
999
|
|
|
939
|
-
|
|
1000
|
+
// IMMEDIATELY persist to both spaces for audit trail (before any errors can occur)
|
|
1001
|
+
await this.ensureSagaFrameInBothSpaces({ frame: ackFrame, destSpace, tempSpace, metaspace });
|
|
1002
|
+
|
|
1003
|
+
// if (logalot) { console.log(`${lc} ackFrame created: ${pretty(ackFrame)} (I: be24480592eec478086bb3da49286826)`); }
|
|
940
1004
|
|
|
941
1005
|
return { frame: ackFrame };
|
|
942
1006
|
}
|
|
@@ -956,280 +1020,364 @@ export class SyncSagaCoordinator {
|
|
|
956
1020
|
protected async handleAckFrame({
|
|
957
1021
|
sagaIbGib,
|
|
958
1022
|
srcGraph,
|
|
959
|
-
|
|
1023
|
+
destSpace,
|
|
1024
|
+
tempSpace,
|
|
960
1025
|
metaspace,
|
|
961
1026
|
identity,
|
|
962
1027
|
}: {
|
|
963
1028
|
sagaIbGib: SyncIbGib_V1,
|
|
964
1029
|
srcGraph: { [addr: string]: IbGib_V1 },
|
|
965
|
-
|
|
1030
|
+
destSpace: IbGibSpaceAny,
|
|
1031
|
+
tempSpace: IbGibSpaceAny,
|
|
966
1032
|
metaspace: MetaspaceService,
|
|
967
1033
|
identity?: KeystoneIbGib_V1,
|
|
968
1034
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
969
1035
|
const lc = `${this.lc}[${this.handleAckFrame.name}]`;
|
|
970
|
-
|
|
1036
|
+
try {
|
|
1037
|
+
if (logalot) { console.log(`${lc} starting... (I: 605b6860e898267a5b50c6d85704be26)`); }
|
|
971
1038
|
|
|
972
|
-
|
|
973
|
-
|
|
1039
|
+
const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
1040
|
+
const ackData = messageData as SyncSagaMessageAckData_V1;
|
|
974
1041
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
}
|
|
1042
|
+
if (!ackData) {
|
|
1043
|
+
throw new Error(`${lc} ackData falsy (E: 3b8415edc876084c88a25b98e2d55826)`);
|
|
1044
|
+
}
|
|
1045
|
+
if (ackData.stage !== SyncStage.ack) {
|
|
1046
|
+
throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
|
|
1047
|
+
}
|
|
1048
|
+
if (logalot) { console.log(`${lc} ackData: ${pretty(ackData)} (I: 7f8e9d0a1b2c3d4e5f6g7h8i9j0k)`); }
|
|
992
1049
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
}
|
|
1050
|
+
// 1. Check for Conflicts
|
|
1051
|
+
const conflicts = ackData.conflicts || [];
|
|
1052
|
+
console.log(`${lc} [CONFLICT DEBUG] Received conflicts from Ack: ${conflicts.length}`);
|
|
1053
|
+
if (conflicts.length > 0) {
|
|
1054
|
+
console.log(`${lc} [CONFLICT DEBUG] Conflicts detail: ${JSON.stringify(conflicts, null, 2)}`);
|
|
1119
1055
|
}
|
|
1120
|
-
}
|
|
1121
1056
|
|
|
1122
|
-
|
|
1057
|
+
const terminalConflicts = conflicts.filter(c => c.terminal);
|
|
1058
|
+
if (terminalConflicts.length > 0) {
|
|
1059
|
+
console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
|
|
1060
|
+
// Terminal failure. Sender should probably Commit(Fail) or just Abort.
|
|
1061
|
+
// For now, throw to trigger abort.
|
|
1062
|
+
throw new Error(`${lc} Peer reported terminal conflicts. (E: a1b2c3d4e5f6g7h8i9j0k)`);
|
|
1063
|
+
}
|
|
1123
1064
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1065
|
+
const optimisticConflicts = conflicts.filter(c => !c.terminal);
|
|
1066
|
+
const mergeDeltaReqs: string[] = []; // Additional requests for merging
|
|
1126
1067
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1068
|
+
if (optimisticConflicts.length > 0) {
|
|
1069
|
+
console.log(`${lc} [CONFLICT DEBUG] Processing ${optimisticConflicts.length} optimistic conflicts`);
|
|
1070
|
+
// We need to resolve these.
|
|
1071
|
+
// Strategy:
|
|
1072
|
+
// 1. Analyze Divergence (Sender vs Receiver)
|
|
1073
|
+
// 2. Identify missing data needed for merge (Receiver's unique frames)
|
|
1074
|
+
// 3. Request that data (as Delta Reqs)
|
|
1075
|
+
// 4. (Later in Delta Phase) Perform Merge.
|
|
1135
1076
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
if (ackData.knowledgeVector) {
|
|
1140
|
-
Object.values(ackData.knowledgeVector).forEach(addrs => {
|
|
1141
|
-
addrs.forEach(a => skipAddrs.add(a));
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1077
|
+
// BUT: The Delta Phase is usually generic "Send me these Addrs".
|
|
1078
|
+
// If we just add to `deltaReqAddrs` (which are requests for Sender to send to Receiver?),
|
|
1079
|
+
// wait. `ackData.deltaReqAddrs` are what RECEIVER wants from SENDER.
|
|
1144
1080
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1081
|
+
// We (Sender) are processing the Ack.
|
|
1082
|
+
// We need to request data FROM Receiver.
|
|
1083
|
+
// But the protocol 'Ack' step typically leads to 'Delta' (Sender sending data).
|
|
1084
|
+
|
|
1085
|
+
// If Sender needs data from Receiver, it usually happens in 'Pull' mode or a separate request?
|
|
1086
|
+
// Or can we send a 'Delta Request' frame?
|
|
1087
|
+
// Standard Saga: Init(Push) -> Ack(Pull Reqs) -> Delta(Push Data).
|
|
1088
|
+
|
|
1089
|
+
// If Sender needs data, we might need a "Reverse Delta" or "Pull" phase?
|
|
1090
|
+
// Or we just proceed to Delta (sending what Receiver wants),
|
|
1091
|
+
// AND we piggyback our own requests?
|
|
1092
|
+
// OR: We treat the Conflict Resolution as a sub-saga or side-effect?
|
|
1093
|
+
|
|
1094
|
+
// SIMPLIFICATION for V1:
|
|
1095
|
+
// If we need data to merge, we must get it.
|
|
1096
|
+
// We are the Coordinator (Active). We can fetch from Peer immediately?
|
|
1097
|
+
// `peer.pull(addr)`?
|
|
1098
|
+
// Yes! The Coordinator has the `peer`.
|
|
1099
|
+
|
|
1100
|
+
// Let's analyze and pull immediately.
|
|
1101
|
+
|
|
1102
|
+
for (const conflict of optimisticConflicts) {
|
|
1103
|
+
const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
|
|
1104
|
+
|
|
1105
|
+
// Sender History
|
|
1106
|
+
// We need our own history for this timeline.
|
|
1107
|
+
// We know the 'senderTip' (remoteAddr in Ack).
|
|
1108
|
+
// Sender should verify it has this tip.
|
|
1109
|
+
|
|
1110
|
+
// Compute Diffs
|
|
1111
|
+
// We need to find `receiverOnly` addrs.
|
|
1112
|
+
// Receiver sent us `timelineAddrs` (Full History).
|
|
1113
|
+
const receiverHistorySet = new Set(timelineAddrs);
|
|
1114
|
+
|
|
1115
|
+
// We need our execution context's history for this senderTip.
|
|
1116
|
+
// We can fetch valid 'past' from space.
|
|
1117
|
+
const resSenderTip = await getFromSpace({ space: destSpace, addr: senderTip });
|
|
1118
|
+
const senderTipIbGib = resSenderTip.ibGibs?.[0];
|
|
1119
|
+
if (!senderTipIbGib) { throw new Error(`${lc} Sender missing its own tip? ${senderTip} (E: 9c8d7e6f5g4h3i2j1k0l)`); }
|
|
1120
|
+
|
|
1121
|
+
// Basic Diff: Find what Receiver has that we don't.
|
|
1122
|
+
// Actually, we need to traverse OUR past to find commonality.
|
|
1123
|
+
const senderHistory = [senderTip, ...(senderTipIbGib.rel8ns?.past || [])];
|
|
1124
|
+
|
|
1125
|
+
const receiverOnlyAddrs = timelineAddrs.filter(addr => !senderHistory.includes(addr));
|
|
1126
|
+
|
|
1127
|
+
if (receiverOnlyAddrs.length > 0) {
|
|
1128
|
+
console.log(`${lc} [CONFLICT DEBUG] Found ${receiverOnlyAddrs.length} receiver-only frames - need to pull for merge`);
|
|
1129
|
+
console.log(`${lc} [CONFLICT DEBUG] Receiver-only addrs:`, receiverOnlyAddrs);
|
|
1130
|
+
// PULL these frames from Peer into Local Space
|
|
1131
|
+
// (Validation: We trust peer for now / verification happens on put)
|
|
1132
|
+
for (const addr of receiverOnlyAddrs) {
|
|
1133
|
+
// This 'pull' is a sync-peer method?
|
|
1134
|
+
// The Coordinator 'peer' passed in 'sync()' might be needed here?
|
|
1135
|
+
// Wait, `handleAckFrame` doesn't have reference to `peer`?
|
|
1136
|
+
// It only has `space`, `metaspace`.
|
|
1137
|
+
// The `peer` is held by the `executeSagaLoop`.
|
|
1138
|
+
|
|
1139
|
+
// PROBLEM: `handleAckFrame` is pure logic on the Space/Data?
|
|
1140
|
+
// No, it's a method on Coordinator.
|
|
1141
|
+
// But `executeSagaLoop` calls it.
|
|
1142
|
+
// We might need to return "Requirements" to the loop?
|
|
1143
|
+
|
|
1144
|
+
// Checking return type: `{ frame: SyncIbGib_V1, payloadIbGibs?: ... }`
|
|
1145
|
+
// It returns the NEXT frame (Delta).
|
|
1146
|
+
|
|
1147
|
+
// If we need to fetch data, we are blocked.
|
|
1148
|
+
// We can't easily "Pull" here without the Peer reference.
|
|
1149
|
+
|
|
1150
|
+
// OPTION A: Pass `peer` to `handleAckFrame`.
|
|
1151
|
+
// OPTION B: Return a strict list of "MissingDeps" and let Loop handle it.
|
|
1152
|
+
|
|
1153
|
+
// Let's assume we can resolve this by adding `peer` to signature or using `metaspace` if it's a peer-witness?
|
|
1154
|
+
// No, Peer is ephemeral connection.
|
|
1155
|
+
|
|
1156
|
+
// Let's add `peer` to `handleAckFrame` signature?
|
|
1157
|
+
// It breaks the pattern of just handling frame + space.
|
|
1158
|
+
|
|
1159
|
+
// ALTERNATIVE: Use the `Delta` frame to request data?
|
|
1160
|
+
// `SyncSagaMessageDeltaData` has `requests?: string[]`.
|
|
1161
|
+
// Sender sends Delta Frame.
|
|
1162
|
+
// Does Receiver handle Delta Requests?
|
|
1163
|
+
// `handleDeltaFrame` (Receiver) -> checks `requests`.
|
|
1164
|
+
// YES.
|
|
1165
|
+
|
|
1166
|
+
// So Sender puts `receiverOnlyAddrs` into `deltaFrame.requests`.
|
|
1167
|
+
// Receiver sees them, fetches them, and includes them in the Response (Commit?).
|
|
1168
|
+
// Wait, Init->Ack->Delta->Commit.
|
|
1169
|
+
// If Receiver sends data in Commit, that's "too late" for Sender to Merge in THIS saga round?
|
|
1170
|
+
// Unless Commit is not the end?
|
|
1171
|
+
|
|
1172
|
+
// Or we do a "Delta 2" loop?
|
|
1173
|
+
|
|
1174
|
+
// "Iterative Resolution Loop" from plan.
|
|
1175
|
+
// If we request data in Delta, Receiver sends it in Commit (or Delta-Response).
|
|
1176
|
+
// Sender gets Commit. Sees data. Merges.
|
|
1177
|
+
// Then Sender needs to Send the MERGE result.
|
|
1178
|
+
// Needs another Push/Delta.
|
|
1179
|
+
|
|
1180
|
+
// REFINED FLOW:
|
|
1181
|
+
// 1. Sender sends Delta Frame with `requests: [receiverOnlyAddrs]`.
|
|
1182
|
+
// 2. Receiver responds (Commit? or Ack 2?) with Payload (Divergent Frames).
|
|
1183
|
+
// 3. Sender handles response -> Merges.
|
|
1184
|
+
// 4. Sender sends Commit (containing Merge Frame).
|
|
1185
|
+
|
|
1186
|
+
// Issue: Current state machine is Init->Ack->Delta->Commit.
|
|
1187
|
+
// We need to keep Saga open.
|
|
1188
|
+
// If Sender sends Delta with requests, does it transition to Commit?
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Compute DELTA dependencies for each receiver-only frame
|
|
1192
|
+
// Find LCA to determine what dependencies we already have
|
|
1193
|
+
const lcaAddr = timelineAddrs.find(addr => senderHistory.includes(addr));
|
|
1194
|
+
console.log(`${lc} [CONFLICT DEBUG] LCA: ${lcaAddr || 'NONE'}`);
|
|
1195
|
+
|
|
1196
|
+
const skipAddrsSet = new Set<string>();
|
|
1197
|
+
if (lcaAddr) {
|
|
1198
|
+
try {
|
|
1199
|
+
const lcaRes = await getFromSpace({ addr: lcaAddr, space: destSpace });
|
|
1200
|
+
const lcaIbGib = lcaRes.ibGibs?.[0];
|
|
1201
|
+
if (lcaIbGib) {
|
|
1202
|
+
const lcaDeps = await getDependencyGraph({ ibGib: lcaIbGib, space: destSpace });
|
|
1203
|
+
if (lcaDeps) Object.keys(lcaDeps).forEach(a => skipAddrsSet.add(a));
|
|
1204
|
+
console.log(`${lc} [CONFLICT DEBUG] LCA deps to skip: ${skipAddrsSet.size}`);
|
|
1205
|
+
}
|
|
1206
|
+
} catch (e) {
|
|
1207
|
+
console.warn(`${lc} Error getting LCA deps: ${extractErrorMsg(e)}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
// For each receiver-only frame, get its DELTA dependency graph (minus LCA deps)
|
|
1213
|
+
for (const addr of receiverOnlyAddrs) {
|
|
1214
|
+
// Add the frame itself first
|
|
1215
|
+
if (!mergeDeltaReqs.includes(addr)) {
|
|
1216
|
+
mergeDeltaReqs.push(addr);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Get the frame's delta dependencies (skip LCA's deps)
|
|
1220
|
+
try {
|
|
1221
|
+
const frameRes = await getFromSpace({ addr, space: destSpace });
|
|
1222
|
+
const frameIbGib = frameRes.ibGibs?.[0];
|
|
1223
|
+
|
|
1224
|
+
if (frameIbGib) {
|
|
1225
|
+
// Get dependency graph, skipping all LCA dependencies
|
|
1226
|
+
const frameDeltaDeps = await getDependencyGraph({
|
|
1227
|
+
ibGib: frameIbGib,
|
|
1228
|
+
space: destSpace,
|
|
1229
|
+
skipAddrs: Array.from(skipAddrsSet), // Skip entire LCA dep graph
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
if (frameDeltaDeps) {
|
|
1233
|
+
// Add all delta dependencies (Object.keys gives us the addresses)
|
|
1234
|
+
Object.keys(frameDeltaDeps).forEach(depAddr => {
|
|
1235
|
+
if (!mergeDeltaReqs.includes(depAddr) && !skipAddrsSet.has(depAddr)) {
|
|
1236
|
+
mergeDeltaReqs.push(depAddr);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
} catch (depError) {
|
|
1242
|
+
console.warn(`${lc} [CONFLICT DEBUG] Error getting delta deps for ${addr}: ${extractErrorMsg(depError)}`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
console.log(`${lc} [CONFLICT DEBUG] Total merge requests (frames + delta deps): ${mergeDeltaReqs.length}`);
|
|
1247
|
+
} else {
|
|
1248
|
+
console.log(`${lc} [CONFLICT DEBUG] No receiver-only frames found for this conflict`);
|
|
1249
|
+
}
|
|
1154
1250
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
tipsToSync.push(ibGib);
|
|
1251
|
+
|
|
1252
|
+
console.log(`${lc} [CONFLICT DEBUG] Finished processing ${optimisticConflicts.length} conflicts. mergeDeltaReqs: ${mergeDeltaReqs.length}`);
|
|
1158
1253
|
} else {
|
|
1159
|
-
|
|
1254
|
+
console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`);
|
|
1160
1255
|
}
|
|
1161
|
-
}
|
|
1162
1256
|
|
|
1163
|
-
|
|
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
|
-
});
|
|
1257
|
+
// 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
|
|
1185
1258
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1259
|
+
const deltaReqAddrs = ackData.deltaReqAddrs || [];
|
|
1260
|
+
const pushOfferAddrs = ackData.pushOfferAddrs || [];
|
|
1188
1261
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1262
|
+
// 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
|
|
1263
|
+
const pullReqAddrs: string[] = [];
|
|
1264
|
+
for (const addr of pushOfferAddrs) {
|
|
1265
|
+
const existing = srcGraph[addr] || (await getFromSpace({ addr, space: destSpace })).ibGibs?.[0];
|
|
1266
|
+
if (!existing) {
|
|
1267
|
+
pullReqAddrs.push(addr);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// 2. Process Delta Requests (Push Payload)
|
|
1272
|
+
// [NEW] Smart Diff: Use knowledgeVector to skip dependencies
|
|
1273
|
+
const skipAddrs = new Set<string>();
|
|
1274
|
+
if (ackData.knowledgeVector) {
|
|
1275
|
+
Object.values(ackData.knowledgeVector).forEach(addrs => {
|
|
1276
|
+
addrs.forEach(a => skipAddrs.add(a));
|
|
1197
1277
|
});
|
|
1198
1278
|
}
|
|
1199
1279
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1280
|
+
const payloadIbGibs: IbGib_V1[] = [];
|
|
1281
|
+
// Gather all tips to sync first
|
|
1282
|
+
const tipsToSync: IbGib_V1[] = [];
|
|
1283
|
+
for (const addr of deltaReqAddrs) {
|
|
1284
|
+
let ibGib = srcGraph[addr];
|
|
1285
|
+
if (!ibGib) {
|
|
1286
|
+
const res = await getFromSpace({ addr, space: destSpace });
|
|
1287
|
+
if (res.ibGibs && res.ibGibs.length > 0) {
|
|
1288
|
+
ibGib = res.ibGibs[0];
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (ibGib) {
|
|
1292
|
+
tipsToSync.push(ibGib);
|
|
1293
|
+
} else {
|
|
1294
|
+
throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
|
|
1205
1295
|
}
|
|
1206
1296
|
}
|
|
1207
|
-
}
|
|
1208
1297
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1298
|
+
// Calculate Dependency Graph for ALL tips, effectively utilizing common history
|
|
1299
|
+
// Pass skipAddrs to `getDependencyGraph` or gather manually.
|
|
1300
|
+
// `getDependencyGraph` takes a single ibGib.
|
|
1301
|
+
// We can optimize by doing it for each tip and unioning the result?
|
|
1302
|
+
// Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
|
|
1303
|
+
// We will loop.
|
|
1304
|
+
|
|
1305
|
+
const allDepsSet = new Set<string>();
|
|
1306
|
+
|
|
1307
|
+
for (const tip of tipsToSync) {
|
|
1308
|
+
// Always include the tip itself
|
|
1309
|
+
const tipAddr = getIbGibAddr({ ibGib: tip });
|
|
1310
|
+
// Only process if not skipped (though deltaReq implies they barely just asked for it)
|
|
1311
|
+
// But detailed deps might be skipped.
|
|
1312
|
+
|
|
1313
|
+
// Get Graph with Skips
|
|
1314
|
+
// Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
|
|
1315
|
+
const deps = await getDependencyGraph({
|
|
1316
|
+
ibGib: tip,
|
|
1317
|
+
space: destSpace,
|
|
1318
|
+
skipAddrs: Array.from(skipAddrs)
|
|
1319
|
+
});
|
|
1217
1320
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
space,
|
|
1221
|
-
metaspace,
|
|
1222
|
-
});
|
|
1321
|
+
// [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
|
|
1322
|
+
let tipIncluded = false;
|
|
1223
1323
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1324
|
+
if (deps) {
|
|
1325
|
+
Object.values(deps).forEach(d => {
|
|
1326
|
+
const dAddr = getIbGibAddr({ ibGib: d });
|
|
1327
|
+
if (!allDepsSet.has(dAddr)) {
|
|
1328
|
+
allDepsSet.add(dAddr);
|
|
1329
|
+
payloadIbGibs.push(d);
|
|
1330
|
+
}
|
|
1331
|
+
if (dAddr === tipAddr) { tipIncluded = true; }
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (!tipIncluded && !skipAddrs.has(tipAddr)) {
|
|
1336
|
+
if (logalot) { console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`); }
|
|
1337
|
+
if (!allDepsSet.has(tipAddr)) {
|
|
1338
|
+
allDepsSet.add(tipAddr);
|
|
1339
|
+
payloadIbGibs.push(tip);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// 3. Create Delta Frame
|
|
1345
|
+
const sagaId = ackData.sagaId;
|
|
1346
|
+
const deltaData: SyncSagaMessageDeltaData_V1 = {
|
|
1347
|
+
sagaId: sagaIbGib.data!.uuid,
|
|
1348
|
+
stage: SyncStage.delta,
|
|
1349
|
+
payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
|
|
1350
|
+
requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
if (logalot) { console.log(`${lc} Creating Delta Stone. Data stage: ${deltaData.stage}`); }
|
|
1354
|
+
|
|
1355
|
+
const deltaStone = await this.createSyncMsgStone({
|
|
1356
|
+
data: deltaData,
|
|
1357
|
+
space: tempSpace,
|
|
1358
|
+
metaspace,
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
const deltaFrame = await this.evolveSyncSagaIbGib({
|
|
1362
|
+
prevSagaIbGib: sagaIbGib,
|
|
1363
|
+
msgStones: [deltaStone],
|
|
1364
|
+
identity,
|
|
1365
|
+
space: tempSpace,
|
|
1366
|
+
metaspace,
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1370
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
1371
|
+
|
|
1372
|
+
if (logalot) { console.log(`${lc} Delta Frame created. Rel8ns: ${JSON.stringify(deltaFrame.rel8ns)}`); }
|
|
1231
1373
|
|
|
1232
|
-
|
|
1374
|
+
return { frame: deltaFrame, payloadIbGibs };
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
1377
|
+
throw error;
|
|
1378
|
+
} finally {
|
|
1379
|
+
if (logalot) { console.log(`${lc} complete.`); }
|
|
1380
|
+
}
|
|
1233
1381
|
}
|
|
1234
1382
|
|
|
1235
1383
|
/**
|
|
@@ -1245,20 +1393,22 @@ export class SyncSagaCoordinator {
|
|
|
1245
1393
|
protected async handleDeltaFrame({
|
|
1246
1394
|
sagaIbGib,
|
|
1247
1395
|
srcGraph,
|
|
1248
|
-
|
|
1396
|
+
destSpace,
|
|
1397
|
+
tempSpace,
|
|
1249
1398
|
metaspace,
|
|
1250
1399
|
identity,
|
|
1251
1400
|
}: {
|
|
1252
1401
|
sagaIbGib: SyncIbGib_V1,
|
|
1253
1402
|
srcGraph: { [addr: string]: IbGib_V1 },
|
|
1254
|
-
|
|
1403
|
+
destSpace: IbGibSpaceAny,
|
|
1404
|
+
tempSpace: IbGibSpaceAny,
|
|
1255
1405
|
metaspace: MetaspaceService,
|
|
1256
1406
|
identity?: KeystoneIbGib_V1,
|
|
1257
1407
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[], receivedPayloadIbGibs?: IbGib_V1[] } | null> {
|
|
1258
1408
|
const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
|
|
1259
1409
|
if (logalot) { console.log(`${lc} starting...`); }
|
|
1260
1410
|
|
|
1261
|
-
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
1411
|
+
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
1262
1412
|
const deltaData = messageData as SyncSagaMessageDeltaData_V1;
|
|
1263
1413
|
|
|
1264
1414
|
if (!deltaData) {
|
|
@@ -1269,6 +1419,8 @@ export class SyncSagaCoordinator {
|
|
|
1269
1419
|
}
|
|
1270
1420
|
if (logalot) { console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`); }
|
|
1271
1421
|
|
|
1422
|
+
console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`);
|
|
1423
|
+
|
|
1272
1424
|
const payloadAddrs = deltaData.payloadAddrs || [];
|
|
1273
1425
|
const peerRequests = deltaData.requests || [];
|
|
1274
1426
|
const peerProposesCommit = deltaData.proposeCommit || false;
|
|
@@ -1284,7 +1436,7 @@ export class SyncSagaCoordinator {
|
|
|
1284
1436
|
|
|
1285
1437
|
const res = await getFromSpace({
|
|
1286
1438
|
addrs: payloadAddrs,
|
|
1287
|
-
space,
|
|
1439
|
+
space: tempSpace, // Incoming data is in tempSpace
|
|
1288
1440
|
});
|
|
1289
1441
|
if (res.ibGibs) {
|
|
1290
1442
|
receivedPayloadIbGibs.push(...res.ibGibs);
|
|
@@ -1295,49 +1447,89 @@ export class SyncSagaCoordinator {
|
|
|
1295
1447
|
}
|
|
1296
1448
|
}
|
|
1297
1449
|
|
|
1298
|
-
// 2. Fulfill Peer Requests (Outgoing Payload)
|
|
1450
|
+
// 2. Fulfill Peer Requests (Outgoing Payload with Delta Dependencies)
|
|
1299
1451
|
const outgoingPayload: IbGib_V1[] = [];
|
|
1452
|
+
const outgoingAddrsSet = new Set<string>(); // Track what we've added
|
|
1453
|
+
|
|
1454
|
+
console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${peerRequests.length} peer requests`);
|
|
1455
|
+
|
|
1300
1456
|
for (const addr of peerRequests) {
|
|
1457
|
+
// Get the requested ibGib
|
|
1301
1458
|
let ibGib = srcGraph[addr];
|
|
1302
1459
|
if (!ibGib) {
|
|
1303
|
-
const res = await getFromSpace({ addr, space });
|
|
1460
|
+
const res = await getFromSpace({ addr, space: destSpace }); // Query from destSpace
|
|
1304
1461
|
if (res.ibGibs && res.ibGibs.length > 0) {
|
|
1305
1462
|
ibGib = res.ibGibs[0];
|
|
1306
1463
|
}
|
|
1307
1464
|
}
|
|
1465
|
+
|
|
1308
1466
|
if (ibGib) {
|
|
1309
|
-
|
|
1467
|
+
// Add the requested ibGib itself
|
|
1468
|
+
const ibGibAddr = getIbGibAddr({ ibGib });
|
|
1469
|
+
if (!outgoingAddrsSet.has(ibGibAddr)) {
|
|
1470
|
+
outgoingPayload.push(ibGib);
|
|
1471
|
+
outgoingAddrsSet.add(ibGibAddr);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Expand to include full dependency graph for this ibGib
|
|
1475
|
+
// (Receiver needs all deps to properly process/merge)
|
|
1476
|
+
try {
|
|
1477
|
+
const deps = await getDependencyGraph({
|
|
1478
|
+
ibGib,
|
|
1479
|
+
space: destSpace,
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
if (deps) {
|
|
1483
|
+
Object.values(deps).forEach(depIbGib => {
|
|
1484
|
+
const depAddr = getIbGibAddr({ ibGib: depIbGib });
|
|
1485
|
+
if (!outgoingAddrsSet.has(depAddr)) {
|
|
1486
|
+
outgoingPayload.push(depIbGib);
|
|
1487
|
+
outgoingAddrsSet.add(depAddr);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
} catch (depError) {
|
|
1492
|
+
console.warn(`${lc} [CONFLICT DEBUG] Error expanding deps for ${addr}: ${extractErrorMsg(depError)}`);
|
|
1493
|
+
}
|
|
1310
1494
|
} else {
|
|
1311
1495
|
console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
|
|
1312
1496
|
}
|
|
1313
1497
|
}
|
|
1314
1498
|
|
|
1499
|
+
console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`);
|
|
1500
|
+
|
|
1501
|
+
|
|
1315
1502
|
// 3. Execute Merges (If applicable)
|
|
1316
1503
|
// Check if we have pending conflicts that we CAN resolve now that we have data.
|
|
1317
1504
|
// We look at the Saga History (Ack Frame) to find conflicts.
|
|
1318
1505
|
// Optimization: Do this only if we received payloads.
|
|
1319
1506
|
const mergeResultIbGibs: IbGib_V1[] = [];
|
|
1320
1507
|
|
|
1508
|
+
console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`);
|
|
1509
|
+
|
|
1321
1510
|
if (receivedPayloadIbGibs.length > 0) {
|
|
1511
|
+
console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`);
|
|
1322
1512
|
// Find the Ack frame in history to get conflicts
|
|
1323
1513
|
// Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
|
|
1324
1514
|
// V1 timelines carry full history in `past`.
|
|
1325
1515
|
const pastAddrs = sagaIbGib.rel8ns?.past || [];
|
|
1516
|
+
console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`);
|
|
1326
1517
|
let ackData: SyncSagaMessageAckData_V1 | undefined;
|
|
1327
1518
|
|
|
1328
1519
|
if (pastAddrs.length > 0) {
|
|
1329
1520
|
// Batch fetch all past frames
|
|
1330
|
-
const resPast = await getFromSpace({ addrs: pastAddrs, space });
|
|
1521
|
+
const resPast = await getFromSpace({ addrs: pastAddrs, space: tempSpace });
|
|
1331
1522
|
if (resPast.success && resPast.ibGibs) {
|
|
1332
1523
|
// Iterate backwards (most recent first) to find the latest Ack
|
|
1333
1524
|
for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
|
|
1334
1525
|
const pastFrame = resPast.ibGibs[i];
|
|
1335
1526
|
const messageStone = await getSyncSagaMessageFromFrame({
|
|
1336
1527
|
frameIbGib: pastFrame,
|
|
1337
|
-
space
|
|
1528
|
+
space: tempSpace
|
|
1338
1529
|
});
|
|
1339
1530
|
if (messageStone?.data?.stage === SyncStage.ack) {
|
|
1340
1531
|
ackData = messageStone.data as SyncSagaMessageAckData_V1;
|
|
1532
|
+
console.log(`${lc} [TEST DEBUG] Found Ack Frame. Conflicts: ${ackData.conflicts?.length || 0}`);
|
|
1341
1533
|
break;
|
|
1342
1534
|
}
|
|
1343
1535
|
}
|
|
@@ -1356,7 +1548,9 @@ export class SyncSagaCoordinator {
|
|
|
1356
1548
|
// We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
|
|
1357
1549
|
|
|
1358
1550
|
// Check if we have receiverTip in space
|
|
1359
|
-
|
|
1551
|
+
console.log(`${lc} [CONFLICT DEBUG] Attempting merge for conflict. ReceiverTip: ${receiverTip}, SenderTip: ${senderTip}`);
|
|
1552
|
+
const resRecTip = await getFromSpace({ addr: receiverTip, space: tempSpace }); // Check tempSpace for incoming data
|
|
1553
|
+
console.log(`${lc} [CONFLICT DEBUG] ReceiverTip found in tempSpace: ${!!resRecTip.ibGibs?.[0]}`);
|
|
1360
1554
|
if (resRecTip.success && resRecTip.ibGibs?.[0]) {
|
|
1361
1555
|
// We have the tip!
|
|
1362
1556
|
// Do we have the full history?
|
|
@@ -1366,12 +1560,13 @@ export class SyncSagaCoordinator {
|
|
|
1366
1560
|
// Perform Merge!
|
|
1367
1561
|
try {
|
|
1368
1562
|
const mergeResult = await mergeDivergentTimelines({
|
|
1369
|
-
tipA: (await getFromSpace({ addr: senderTip, space })).ibGibs![0], // Our tip
|
|
1370
|
-
tipB: resRecTip.ibGibs[0], // Their tip
|
|
1371
|
-
space,
|
|
1563
|
+
tipA: (await getFromSpace({ addr: senderTip, space: destSpace })).ibGibs![0], // Our tip from destSpace
|
|
1564
|
+
tipB: resRecTip.ibGibs[0], // Their tip (from tempSpace)
|
|
1565
|
+
space: tempSpace, // Merge uses tempSpace
|
|
1372
1566
|
metaspace,
|
|
1373
1567
|
});
|
|
1374
1568
|
if (mergeResult) {
|
|
1569
|
+
console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
|
|
1375
1570
|
if (logalot) { console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
|
|
1376
1571
|
mergeResultIbGibs.push(mergeResult);
|
|
1377
1572
|
outgoingPayload.push(mergeResult); // Send result to peer
|
|
@@ -1419,7 +1614,7 @@ export class SyncSagaCoordinator {
|
|
|
1419
1614
|
|
|
1420
1615
|
const deltaStone = await this.createSyncMsgStone({
|
|
1421
1616
|
data: responseDeltaData,
|
|
1422
|
-
space,
|
|
1617
|
+
space: tempSpace,
|
|
1423
1618
|
metaspace
|
|
1424
1619
|
});
|
|
1425
1620
|
|
|
@@ -1427,10 +1622,13 @@ export class SyncSagaCoordinator {
|
|
|
1427
1622
|
prevSagaIbGib: sagaIbGib,
|
|
1428
1623
|
msgStones: [deltaStone],
|
|
1429
1624
|
identity,
|
|
1430
|
-
space,
|
|
1625
|
+
space: tempSpace,
|
|
1431
1626
|
metaspace
|
|
1432
1627
|
});
|
|
1433
1628
|
|
|
1629
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1630
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
1631
|
+
|
|
1434
1632
|
return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
|
|
1435
1633
|
|
|
1436
1634
|
} else {
|
|
@@ -1446,7 +1644,7 @@ export class SyncSagaCoordinator {
|
|
|
1446
1644
|
|
|
1447
1645
|
const commitStone = await this.createSyncMsgStone({
|
|
1448
1646
|
data: commitData,
|
|
1449
|
-
space,
|
|
1647
|
+
space: tempSpace,
|
|
1450
1648
|
metaspace
|
|
1451
1649
|
});
|
|
1452
1650
|
|
|
@@ -1454,10 +1652,13 @@ export class SyncSagaCoordinator {
|
|
|
1454
1652
|
prevSagaIbGib: sagaIbGib,
|
|
1455
1653
|
msgStones: [commitStone],
|
|
1456
1654
|
identity,
|
|
1457
|
-
space,
|
|
1655
|
+
space: tempSpace,
|
|
1458
1656
|
metaspace
|
|
1459
1657
|
});
|
|
1460
1658
|
|
|
1659
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1660
|
+
await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
|
|
1661
|
+
|
|
1461
1662
|
return { frame: commitFrame, receivedPayloadIbGibs };
|
|
1462
1663
|
|
|
1463
1664
|
} else {
|
|
@@ -1473,7 +1674,7 @@ export class SyncSagaCoordinator {
|
|
|
1473
1674
|
|
|
1474
1675
|
const deltaStone = await this.createSyncMsgStone({
|
|
1475
1676
|
data: responseDeltaData,
|
|
1476
|
-
space,
|
|
1677
|
+
space: tempSpace,
|
|
1477
1678
|
metaspace
|
|
1478
1679
|
});
|
|
1479
1680
|
|
|
@@ -1481,10 +1682,13 @@ export class SyncSagaCoordinator {
|
|
|
1481
1682
|
prevSagaIbGib: sagaIbGib,
|
|
1482
1683
|
msgStones: [deltaStone],
|
|
1483
1684
|
identity,
|
|
1484
|
-
space,
|
|
1685
|
+
space: tempSpace,
|
|
1485
1686
|
metaspace
|
|
1486
1687
|
});
|
|
1487
1688
|
|
|
1689
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1690
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
1691
|
+
|
|
1488
1692
|
// Check if PEER proposed commit
|
|
1489
1693
|
if (deltaData.proposeCommit) {
|
|
1490
1694
|
if (logalot) { console.log(`${lc} Peer proposed commit. Accepting & Committing.`); }
|
|
@@ -1499,7 +1703,7 @@ export class SyncSagaCoordinator {
|
|
|
1499
1703
|
|
|
1500
1704
|
const commitStone = await this.createSyncMsgStone({
|
|
1501
1705
|
data: commitData,
|
|
1502
|
-
space,
|
|
1706
|
+
space: tempSpace,
|
|
1503
1707
|
metaspace
|
|
1504
1708
|
});
|
|
1505
1709
|
|
|
@@ -1507,10 +1711,13 @@ export class SyncSagaCoordinator {
|
|
|
1507
1711
|
prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
|
|
1508
1712
|
msgStones: [commitStone],
|
|
1509
1713
|
identity,
|
|
1510
|
-
space,
|
|
1714
|
+
space: tempSpace,
|
|
1511
1715
|
metaspace
|
|
1512
1716
|
});
|
|
1513
1717
|
|
|
1718
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1719
|
+
await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
|
|
1720
|
+
|
|
1514
1721
|
return { frame: commitFrame, receivedPayloadIbGibs };
|
|
1515
1722
|
}
|
|
1516
1723
|
|
|
@@ -1522,11 +1729,14 @@ export class SyncSagaCoordinator {
|
|
|
1522
1729
|
|
|
1523
1730
|
protected async handleCommitFrame({
|
|
1524
1731
|
sagaIbGib,
|
|
1525
|
-
|
|
1732
|
+
destSpace,
|
|
1733
|
+
tempSpace,
|
|
1526
1734
|
metaspace,
|
|
1735
|
+
identity,
|
|
1527
1736
|
}: {
|
|
1528
1737
|
sagaIbGib: SyncIbGib_V1,
|
|
1529
|
-
|
|
1738
|
+
destSpace: IbGibSpaceAny,
|
|
1739
|
+
tempSpace: IbGibSpaceAny,
|
|
1530
1740
|
metaspace: MetaspaceService,
|
|
1531
1741
|
identity?: KeystoneIbGib_V1,
|
|
1532
1742
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
@@ -1547,29 +1757,6 @@ export class SyncSagaCoordinator {
|
|
|
1547
1757
|
return null;
|
|
1548
1758
|
}
|
|
1549
1759
|
|
|
1550
|
-
protected async handleConflictFrame({
|
|
1551
|
-
sagaIbGib,
|
|
1552
|
-
metaspace,
|
|
1553
|
-
space,
|
|
1554
|
-
}: {
|
|
1555
|
-
sagaIbGib: SyncIbGib_V1,
|
|
1556
|
-
metaspace: MetaspaceService,
|
|
1557
|
-
space: IbGibSpaceAny,
|
|
1558
|
-
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
1559
|
-
const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
|
|
1560
|
-
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
1561
|
-
const conflictData = messageData as SyncSagaMessageConflictData_V1;
|
|
1562
|
-
|
|
1563
|
-
if (logalot) { console.log(`${lc} Conflict received. Strategy: ${conflictData?.conflictStrategy}. Terminal: ${conflictData?.isTerminal}`); }
|
|
1564
|
-
|
|
1565
|
-
if (conflictData?.isTerminal) {
|
|
1566
|
-
throw new Error(`${lc} Saga aborted due to conflicts: ${JSON.stringify(conflictData.conflicts)} (E: b08d1f2a3c4e5d6f7a8b9c0d1e2f3a4b)`);
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
// Non-terminal logic (stub for future)
|
|
1570
|
-
return null;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
1760
|
// #endregion Handlers
|
|
1574
1761
|
|
|
1575
1762
|
protected async createSyncMsgStone<TStoneData extends SyncSagaMessageData_V1>({
|
|
@@ -1605,6 +1792,50 @@ export class SyncSagaCoordinator {
|
|
|
1605
1792
|
}
|
|
1606
1793
|
|
|
1607
1794
|
|
|
1795
|
+
/**
|
|
1796
|
+
* Ensures saga frame and its msg stone(s) are in BOTH spaces for audit trail.
|
|
1797
|
+
* Control ibgibs (saga frames, msg stones, identity) must be in both destSpace and tempSpace.
|
|
1798
|
+
*/
|
|
1799
|
+
protected async ensureSagaFrameInBothSpaces({
|
|
1800
|
+
frame,
|
|
1801
|
+
destSpace,
|
|
1802
|
+
tempSpace,
|
|
1803
|
+
metaspace,
|
|
1804
|
+
}: {
|
|
1805
|
+
frame: SyncIbGib_V1,
|
|
1806
|
+
destSpace: IbGibSpaceAny,
|
|
1807
|
+
tempSpace: IbGibSpaceAny,
|
|
1808
|
+
metaspace: MetaspaceService,
|
|
1809
|
+
}): Promise<void> {
|
|
1810
|
+
// Frame itself (already in tempSpace from creation, need in destSpace for audit)
|
|
1811
|
+
await putInSpace({ space: destSpace, ibGib: frame });
|
|
1812
|
+
await metaspace.registerNewIbGib({ ibGib: frame });
|
|
1813
|
+
|
|
1814
|
+
// Msg stone(s) (already in tempSpace, need in destSpace for audit)
|
|
1815
|
+
const msgStoneAddrs = frame.rel8ns?.[SYNC_MSG_REL8N_NAME];
|
|
1816
|
+
if (msgStoneAddrs && msgStoneAddrs.length > 0) {
|
|
1817
|
+
const resMsgStones = await getFromSpace({ space: tempSpace, addrs: msgStoneAddrs });
|
|
1818
|
+
if (resMsgStones.ibGibs) {
|
|
1819
|
+
for (const msgStone of resMsgStones.ibGibs) {
|
|
1820
|
+
await putInSpace({ space: destSpace, ibGib: msgStone });
|
|
1821
|
+
await metaspace.registerNewIbGib({ ibGib: msgStone });
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Identity (if present, already in tempSpace, need in destSpace)
|
|
1827
|
+
const identityAddrs = frame.rel8ns?.identity;
|
|
1828
|
+
if (identityAddrs && identityAddrs.length > 0) {
|
|
1829
|
+
const resIdentity = await getFromSpace({ space: tempSpace, addrs: identityAddrs });
|
|
1830
|
+
if (resIdentity.ibGibs) {
|
|
1831
|
+
for (const identity of resIdentity.ibGibs) {
|
|
1832
|
+
await putInSpace({ space: destSpace, ibGib: identity });
|
|
1833
|
+
await metaspace.registerNewIbGib({ ibGib: identity });
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1608
1839
|
/**
|
|
1609
1840
|
* Evolves the saga timeline with a new frame.
|
|
1610
1841
|
*/
|