@ibgib/core-gib 0.1.19 → 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/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/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/graft-info/graft-info-types.d.mts.map +1 -0
- 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 +37 -0
- package/dist/sync/strategies/conflict-optimistic.d.mts.map +1 -0
- package/dist/sync/strategies/conflict-optimistic.mjs +112 -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 +277 -0
- package/dist/sync/sync-conflict.respec.mjs.map +1 -0
- 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-innerspace-constants.respec.mjs +2 -2
- package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs +0 -1
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace.respec.mjs +1 -1
- package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
- package/dist/sync/sync-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 +45 -27
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +811 -253
- 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 +25 -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 +24 -12
- 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 +147 -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/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 +149 -0
- package/src/sync/sync-conflict.respec.mts +330 -0
- package/src/sync/sync-constants.mts +1 -4
- package/src/sync/sync-innerspace-constants.respec.mts +1 -1
- package/src/sync/sync-innerspace-deep-updates.respec.mts +0 -1
- package/src/sync/sync-innerspace.respec.mts +1 -1
- package/src/sync/sync-peer/sync-peer-innerspace-v1.mts +85 -12
- package/src/sync/sync-saga-coordinator.mts +905 -268
- package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +43 -0
- package/src/sync/sync-saga-message/sync-saga-message-types.mts +23 -11
- package/src/sync/sync-types.mts +33 -4
- package/test_output.log +0 -0
- package/tmp.md +44 -426
|
@@ -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";
|
|
@@ -30,7 +31,7 @@ import { getDependencyGraph } from "../common/other/graph-helper.mjs";
|
|
|
30
31
|
import {
|
|
31
32
|
SyncSagaMessageData_V1, SyncSagaMessageInitData_V1,
|
|
32
33
|
SyncSagaMessageAckData_V1, SyncSagaMessageDeltaData_V1,
|
|
33
|
-
SyncSagaMessageCommitData_V1
|
|
34
|
+
SyncSagaMessageCommitData_V1
|
|
34
35
|
} from "./sync-saga-message/sync-saga-message-types.mjs";
|
|
35
36
|
import { getSyncSagaMessageIb } from "./sync-saga-message/sync-saga-message-helpers.mjs";
|
|
36
37
|
import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-constants.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
|
|
|
@@ -140,7 +143,7 @@ export class SyncSagaCoordinator {
|
|
|
140
143
|
const sessionIdentity = useSessionIdentity
|
|
141
144
|
? await this.getSessionIdentity({ sagaId, metaspace, tempSpace })
|
|
142
145
|
: undefined;
|
|
143
|
-
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)`); }
|
|
144
147
|
|
|
145
148
|
// 3. CREATE INITIAL FRAME (Stage.init)
|
|
146
149
|
const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
|
|
@@ -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)
|
|
@@ -331,12 +335,23 @@ export class SyncSagaCoordinator {
|
|
|
331
335
|
}
|
|
332
336
|
|
|
333
337
|
// B. Transmit
|
|
334
|
-
if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
|
|
338
|
+
// if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
|
|
335
339
|
updates$.next(requestCtx);
|
|
336
340
|
const responseCtx = await peer.witness(requestCtx);
|
|
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;
|
|
@@ -374,6 +389,7 @@ export class SyncSagaCoordinator {
|
|
|
374
389
|
await putInSpace({ space: tempSpace, ibGibs: remoteDeps });
|
|
375
390
|
}
|
|
376
391
|
|
|
392
|
+
|
|
377
393
|
// React (Reducer)
|
|
378
394
|
// This processes the FRAME we just got from the peer.
|
|
379
395
|
// i.e., We Sent Init -> Got Ack. This calls handleAckFrame.
|
|
@@ -381,7 +397,8 @@ export class SyncSagaCoordinator {
|
|
|
381
397
|
const result = await this.handleSagaFrame({
|
|
382
398
|
sagaIbGib: remoteFrame as SyncIbGib_V1,
|
|
383
399
|
srcGraph,
|
|
384
|
-
|
|
400
|
+
destSpace: localSpace, // Query existing data from localSpace (Source)
|
|
401
|
+
tempSpace: tempSpace, // Transaction space for saga frames
|
|
385
402
|
identity: sessionIdentity,
|
|
386
403
|
metaspace
|
|
387
404
|
});
|
|
@@ -397,6 +414,83 @@ export class SyncSagaCoordinator {
|
|
|
397
414
|
return allReceivedIbGibs;
|
|
398
415
|
}
|
|
399
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Helper to get Knowledge Vector for specific domain ibGibs or TJPs.
|
|
419
|
+
* Useful for testing and external validation.
|
|
420
|
+
*/
|
|
421
|
+
public async getKnowledgeVector({
|
|
422
|
+
space,
|
|
423
|
+
metaspace,
|
|
424
|
+
domainIbGibs,
|
|
425
|
+
tjpAddrs,
|
|
426
|
+
}: {
|
|
427
|
+
space: IbGibSpaceAny,
|
|
428
|
+
metaspace: MetaspaceService,
|
|
429
|
+
domainIbGibs?: IbGib_V1[],
|
|
430
|
+
tjpAddrs?: string[],
|
|
431
|
+
}): Promise<{ [tjp: string]: string | null }> {
|
|
432
|
+
const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
|
|
433
|
+
try {
|
|
434
|
+
if (logalot) { console.log(`${lc} starting... (I: e184f8a7818666febfbbd2d841ed3826)`); }
|
|
435
|
+
console.dir(space)
|
|
436
|
+
|
|
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 {}; }
|
|
475
|
+
|
|
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
|
+
}
|
|
482
|
+
|
|
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.`); }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
400
494
|
protected async analyzeTimelines({
|
|
401
495
|
domainIbGibs,
|
|
402
496
|
space,
|
|
@@ -450,14 +544,16 @@ export class SyncSagaCoordinator {
|
|
|
450
544
|
localSpace,
|
|
451
545
|
domainIbGibs,
|
|
452
546
|
tempSpace,
|
|
453
|
-
metaspace
|
|
547
|
+
metaspace,
|
|
548
|
+
conflictStrategy,
|
|
454
549
|
}: {
|
|
455
550
|
sagaId: string,
|
|
456
551
|
sessionIdentity?: KeystoneIbGib_V1,
|
|
457
552
|
localSpace: IbGibSpaceAny,
|
|
458
553
|
domainIbGibs: IbGib_V1[],
|
|
459
554
|
tempSpace: IbGibSpaceAny,
|
|
460
|
-
metaspace: MetaspaceService
|
|
555
|
+
metaspace: MetaspaceService,
|
|
556
|
+
conflictStrategy: SyncConflictStrategy,
|
|
461
557
|
}): Promise<{ sagaFrame: SyncIbGib_V1, srcGraph: { [addr: string]: IbGib_V1 } }> {
|
|
462
558
|
const lc = `${this.lc}[${this.createInitFrame.name}]`;
|
|
463
559
|
try {
|
|
@@ -485,20 +581,35 @@ export class SyncSagaCoordinator {
|
|
|
485
581
|
initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
|
|
486
582
|
});
|
|
487
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
|
+
|
|
488
589
|
const initStone = await this.createSyncMsgStone({
|
|
489
590
|
data: initData,
|
|
490
591
|
space: tempSpace,
|
|
491
592
|
metaspace
|
|
492
593
|
});
|
|
493
|
-
if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
|
|
594
|
+
// if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
|
|
494
595
|
|
|
495
596
|
const sagaFrame = await this.evolveSyncSagaIbGib({
|
|
496
597
|
msgStones: [initStone],
|
|
497
598
|
identity: sessionIdentity,
|
|
498
599
|
space: tempSpace,
|
|
600
|
+
metaspace,
|
|
601
|
+
conflictStrategy,
|
|
602
|
+
});
|
|
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,
|
|
499
609
|
metaspace
|
|
500
610
|
});
|
|
501
|
-
|
|
611
|
+
|
|
612
|
+
// if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
|
|
502
613
|
|
|
503
614
|
return { sagaFrame, srcGraph };
|
|
504
615
|
|
|
@@ -526,14 +637,16 @@ export class SyncSagaCoordinator {
|
|
|
526
637
|
async handleSagaFrame({
|
|
527
638
|
sagaIbGib,
|
|
528
639
|
srcGraph,
|
|
529
|
-
|
|
640
|
+
destSpace,
|
|
641
|
+
tempSpace,
|
|
530
642
|
identity,
|
|
531
643
|
identitySecret,
|
|
532
644
|
metaspace,
|
|
533
645
|
}: {
|
|
534
646
|
sagaIbGib: SyncIbGib_V1,
|
|
535
647
|
srcGraph: { [addr: string]: IbGib_V1 },
|
|
536
|
-
|
|
648
|
+
destSpace: IbGibSpaceAny,
|
|
649
|
+
tempSpace: IbGibSpaceAny,
|
|
537
650
|
identity?: KeystoneIbGib_V1,
|
|
538
651
|
identitySecret?: string,
|
|
539
652
|
metaspace: MetaspaceService,
|
|
@@ -546,25 +659,22 @@ export class SyncSagaCoordinator {
|
|
|
546
659
|
if (logalot) { console.log(`${lc} sagaIbGib: ${pretty(sagaIbGib)} (I: 1b99d87d262e9d18d8a607a80b1a0126)`); }
|
|
547
660
|
|
|
548
661
|
// Get Stage from Stone (or Frame for Init fallback)
|
|
549
|
-
const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
662
|
+
const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
550
663
|
|
|
551
664
|
if (logalot) { console.log(`${lc} handling frame stage: ${stage}`); }
|
|
552
665
|
|
|
553
666
|
switch (stage) {
|
|
554
667
|
case SyncStage.init:
|
|
555
|
-
return await this.handleInitFrame({ sagaIbGib, messageData,
|
|
668
|
+
return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, destSpace, tempSpace, identity, identitySecret });
|
|
556
669
|
|
|
557
670
|
case SyncStage.ack:
|
|
558
|
-
return await this.handleAckFrame({ sagaIbGib, srcGraph,
|
|
671
|
+
return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity });
|
|
559
672
|
|
|
560
673
|
case SyncStage.delta:
|
|
561
|
-
return await this.handleDeltaFrame({ sagaIbGib, srcGraph,
|
|
674
|
+
return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity, });
|
|
562
675
|
|
|
563
676
|
case SyncStage.commit:
|
|
564
|
-
return await this.handleCommitFrame({ sagaIbGib,
|
|
565
|
-
|
|
566
|
-
case SyncStage.conflict:
|
|
567
|
-
return await this.handleConflictFrame({ sagaIbGib, space });
|
|
677
|
+
return await this.handleCommitFrame({ sagaIbGib, metaspace, destSpace, tempSpace, identity, });
|
|
568
678
|
|
|
569
679
|
default:
|
|
570
680
|
throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
|
|
@@ -594,31 +704,39 @@ export class SyncSagaCoordinator {
|
|
|
594
704
|
protected async handleInitFrame({
|
|
595
705
|
sagaIbGib,
|
|
596
706
|
messageData,
|
|
597
|
-
|
|
707
|
+
destSpace,
|
|
708
|
+
tempSpace,
|
|
598
709
|
metaspace,
|
|
599
710
|
identity,
|
|
600
711
|
identitySecret,
|
|
601
712
|
}: {
|
|
602
713
|
sagaIbGib: SyncIbGib_V1,
|
|
603
714
|
messageData: any,
|
|
604
|
-
|
|
715
|
+
destSpace: IbGibSpaceAny,
|
|
716
|
+
tempSpace: IbGibSpaceAny,
|
|
605
717
|
metaspace: MetaspaceService,
|
|
606
718
|
identity?: KeystoneIbGib_V1,
|
|
607
719
|
identitySecret?: string,
|
|
608
720
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
609
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]'})`);
|
|
610
723
|
if (logalot) { console.log(`${lc} starting...`); }
|
|
611
724
|
|
|
612
725
|
// Extract Init Data
|
|
613
726
|
const initData = messageData as SyncSagaMessageInitData_V1; // Using renamed variable for clarity
|
|
614
|
-
if (
|
|
727
|
+
if (initData.stage !== SyncStage.init) {
|
|
728
|
+
throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
|
|
729
|
+
}
|
|
730
|
+
// if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
|
|
615
731
|
if (!initData || !initData.knowledgeVector) {
|
|
616
732
|
throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
|
|
617
733
|
}
|
|
618
734
|
|
|
735
|
+
// Determine Strategy from Saga Data (since V1 stores it in root)
|
|
736
|
+
const conflictStrategy = sagaIbGib.data!.conflictStrategy || 'abort';
|
|
737
|
+
|
|
619
738
|
// 2. Gap Analysis
|
|
620
|
-
const conflicts: {
|
|
621
|
-
const conflictStrategy: SyncConflictStrategy = initData.conflictStrategy || 'abort'; // Default to abort if not specified, or we should parameterize this
|
|
739
|
+
const conflicts: { tjpAddr: string, localAddr: string, remoteAddr: string, timelineAddrs: string[], reason: string, terminal: boolean }[] = [];
|
|
622
740
|
|
|
623
741
|
const deltaReqAddrs: string[] = [];
|
|
624
742
|
const pushOfferAddrs: string[] = [];
|
|
@@ -628,7 +746,7 @@ export class SyncSagaCoordinator {
|
|
|
628
746
|
if (stones.length > 0) {
|
|
629
747
|
if (logalot) { console.log(`${lc} processing stones: ${stones.length}`); }
|
|
630
748
|
// Check if we have these stones
|
|
631
|
-
const resStones = await getFromSpace({ space, addrs: stones });
|
|
749
|
+
const resStones = await getFromSpace({ space: destSpace, addrs: stones });
|
|
632
750
|
const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
|
|
633
751
|
if (addrsNotFound && addrsNotFound.length > 0) {
|
|
634
752
|
if (logalot) { console.log(`${lc} stones missing (requesting): ${addrsNotFound.length}`); }
|
|
@@ -643,6 +761,7 @@ export class SyncSagaCoordinator {
|
|
|
643
761
|
const remoteKV = initData.knowledgeVector;
|
|
644
762
|
if (logalot) { console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`); }
|
|
645
763
|
const remoteTjps = Object.keys(remoteKV);
|
|
764
|
+
console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`);
|
|
646
765
|
if (logalot) { console.log(`${lc} remoteTjps: ${pretty(remoteTjps)} (I: 86ea4c53db0dc184c8b253386c402126)`); }
|
|
647
766
|
|
|
648
767
|
// 1. Get Local Latest Addrs for all TJPs
|
|
@@ -650,12 +769,13 @@ export class SyncSagaCoordinator {
|
|
|
650
769
|
if (remoteTjps.length > 0) {
|
|
651
770
|
// Batch get latest addrs for the TJPs
|
|
652
771
|
const resGetLatestAddrs = await getLatestAddrs({
|
|
653
|
-
space,
|
|
772
|
+
space: destSpace,
|
|
654
773
|
tjpAddrs: remoteTjps,
|
|
655
774
|
});
|
|
656
775
|
if (!resGetLatestAddrs.data) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data falsy? (E: b180d813c088042b38e1e02e06a16926)`); }
|
|
657
776
|
if (!resGetLatestAddrs.data.latestAddrsMap) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data.latestAddrsMap falsy? (E: 16bc386dd51d0ff53a49620b1e641826)`); }
|
|
658
777
|
localKV = resGetLatestAddrs.data.latestAddrsMap;
|
|
778
|
+
console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localKV)}`);
|
|
659
779
|
if (logalot) { console.log(`${lc} localKV: ${pretty(localKV)} (I: 980975642cbccd8018cf0cd808d30826)`); }
|
|
660
780
|
}
|
|
661
781
|
|
|
@@ -668,24 +788,28 @@ export class SyncSagaCoordinator {
|
|
|
668
788
|
|
|
669
789
|
if (!localAddr) {
|
|
670
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}`);
|
|
671
792
|
deltaReqAddrs.push(remoteAddr);
|
|
672
793
|
continue;
|
|
673
794
|
}
|
|
674
795
|
|
|
675
796
|
if (localAddr === remoteAddr) {
|
|
676
797
|
// Synced
|
|
798
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`);
|
|
677
799
|
continue;
|
|
678
800
|
}
|
|
801
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`);
|
|
679
802
|
|
|
680
803
|
// Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
|
|
681
804
|
// (Sender has older version, Receiver has newer) -> Receiver Offers Push
|
|
682
805
|
const isRemoteInPast = await isPastFrame({
|
|
683
806
|
olderAddr: remoteAddr,
|
|
684
807
|
newerAddr: localAddr,
|
|
685
|
-
space,
|
|
808
|
+
space: destSpace,
|
|
686
809
|
});
|
|
687
810
|
|
|
688
811
|
if (isRemoteInPast) {
|
|
812
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote is in past - offering push`);
|
|
689
813
|
pushOfferAddrs.push(localAddr);
|
|
690
814
|
} else {
|
|
691
815
|
// Remote is not in our past.
|
|
@@ -695,29 +819,57 @@ export class SyncSagaCoordinator {
|
|
|
695
819
|
const isLocalInPast = await isPastFrame({
|
|
696
820
|
olderAddr: localAddr,
|
|
697
821
|
newerAddr: remoteAddr,
|
|
698
|
-
space,
|
|
822
|
+
space: destSpace,
|
|
699
823
|
});
|
|
700
824
|
|
|
701
825
|
if (isLocalInPast) {
|
|
702
826
|
// Fast-Forward: We update to remote's tip.
|
|
827
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`);
|
|
703
828
|
deltaReqAddrs.push(remoteAddr);
|
|
704
829
|
} else {
|
|
705
830
|
// DIVERGENCE: Both have changes the other doesn't know about.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
// Conflict Strategy Handling
|
|
709
|
-
// TODO: Implement "manual" strategy (requires user intervention/pause)
|
|
710
|
-
// TODO: Implement "local_wins" strategy
|
|
711
|
-
// TODO: Implement "server_wins" strategy
|
|
831
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`);
|
|
712
832
|
|
|
713
833
|
if (conflictStrategy === 'abort') {
|
|
714
|
-
// We will
|
|
834
|
+
// Abort Strategy: We will treat this as terminal.
|
|
835
|
+
// But for Unified Ack, we just mark it terminal in the list?
|
|
836
|
+
// Or do we actually throw/abort the saga?
|
|
837
|
+
// Current logic (below) aborts the saga if ANY conflict is terminal/abort.
|
|
838
|
+
conflicts.push({
|
|
839
|
+
tjpAddr: tjp,
|
|
840
|
+
localAddr: localAddr!,
|
|
841
|
+
remoteAddr,
|
|
842
|
+
timelineAddrs: [], // Not needed for abort
|
|
843
|
+
reason: 'divergence',
|
|
844
|
+
terminal: true
|
|
845
|
+
});
|
|
715
846
|
} else if (conflictStrategy === 'optimistic') {
|
|
716
|
-
// Optimistic: We
|
|
717
|
-
// We
|
|
718
|
-
|
|
719
|
-
//
|
|
720
|
-
|
|
847
|
+
// Optimistic: We want to resolving this.
|
|
848
|
+
// We need to send our history to the Sender so they can Merge.
|
|
849
|
+
|
|
850
|
+
// Fetch Full History for Local Timeline
|
|
851
|
+
// Note: We might optimize this to only send "recent" history if we had a KV?
|
|
852
|
+
// But for now, get full past.
|
|
853
|
+
// Optimization: localKV might not have full history.
|
|
854
|
+
// We need to inspect the 'past' of the local tip.
|
|
855
|
+
|
|
856
|
+
// We need the ACTUAL object to get the past.
|
|
857
|
+
// We have localAddr.
|
|
858
|
+
const resLocalTip = await getFromSpace({ space: destSpace, addr: localAddr! });
|
|
859
|
+
const localTip = resLocalTip.ibGibs?.[0];
|
|
860
|
+
if (!localTip) { throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`); }
|
|
861
|
+
|
|
862
|
+
const timelineAddrs = [localAddr!, ...(localTip.rel8ns?.past || [])];
|
|
863
|
+
|
|
864
|
+
conflicts.push({
|
|
865
|
+
tjpAddr: tjp,
|
|
866
|
+
localAddr: localAddr!,
|
|
867
|
+
remoteAddr,
|
|
868
|
+
timelineAddrs,
|
|
869
|
+
reason: 'divergence',
|
|
870
|
+
terminal: false
|
|
871
|
+
});
|
|
872
|
+
|
|
721
873
|
} else {
|
|
722
874
|
throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
|
|
723
875
|
}
|
|
@@ -725,40 +877,32 @@ export class SyncSagaCoordinator {
|
|
|
725
877
|
}
|
|
726
878
|
}
|
|
727
879
|
|
|
728
|
-
if
|
|
880
|
+
// Check if we should ABORT (if any conflict is terminal)
|
|
881
|
+
const hasTerminalConflicts = conflicts.some(c => c.terminal);
|
|
882
|
+
|
|
883
|
+
if (hasTerminalConflicts) {
|
|
729
884
|
// Abort Strategy: Kill the saga.
|
|
730
|
-
if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to conflicts: ${JSON.stringify(conflicts)}`); }
|
|
885
|
+
if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to terminal conflicts: ${JSON.stringify(conflicts)}`); }
|
|
731
886
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
isTerminal: true,
|
|
738
|
-
conflicts,
|
|
739
|
-
};
|
|
887
|
+
// We reuse the ConflictData structure for terminal aborts?
|
|
888
|
+
// Or do we send an Ack with terminal conflicts?
|
|
889
|
+
// Original design had explicit Conflict Frame for Abort.
|
|
890
|
+
// Let's stick to that for purely terminal cases to be safe/explicit?
|
|
891
|
+
// Or Unified: Just send Ack with terminal=true conflicts. Sender sees them and aborts.
|
|
740
892
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const conflictFrame = await this.evolveSyncSagaIbGib({
|
|
748
|
-
prevSagaIbGib: sagaIbGib,
|
|
749
|
-
msgStones: [conflictStone],
|
|
750
|
-
identity,
|
|
751
|
-
space,
|
|
752
|
-
metaspace,
|
|
753
|
-
});
|
|
893
|
+
// Decision: Unified Ack for everything is cleaner protocol.
|
|
894
|
+
// But wait, the original code below creates a Conflict Stone.
|
|
895
|
+
// Let's preserve the explicit 'Conflict' frame for total aborts if that's easier,
|
|
896
|
+
// OR fully switch to Ack.
|
|
897
|
+
// Protocol states: Init -> Ack. If Ack contains terminal errors, Sender can Commit(Fail).
|
|
754
898
|
|
|
755
|
-
|
|
899
|
+
// Let's use Ack with conflicts.
|
|
756
900
|
}
|
|
757
901
|
|
|
758
902
|
// 2. Add Push Offers (Missing in Local)
|
|
759
903
|
// Check if we have them. If not, ask for them.
|
|
760
904
|
for (const addr of pushOfferAddrs) {
|
|
761
|
-
const hasIt = await getFromSpace({ addr, space });
|
|
905
|
+
const hasIt = await getFromSpace({ addr, space: destSpace });
|
|
762
906
|
if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
|
|
763
907
|
// If we don't have it, we put it in `deltaReqAddrs` of the Ack.
|
|
764
908
|
deltaReqAddrs.push(addr);
|
|
@@ -777,7 +921,7 @@ export class SyncSagaCoordinator {
|
|
|
777
921
|
for (const tjp of remoteTjps) {
|
|
778
922
|
const localAddr = localKV[tjp];
|
|
779
923
|
if (localAddr) {
|
|
780
|
-
const res = await getFromSpace({ addr: localAddr, space });
|
|
924
|
+
const res = await getFromSpace({ addr: localAddr, space: destSpace });
|
|
781
925
|
if (res.success && res.ibGibs?.[0]) {
|
|
782
926
|
const ibGib = res.ibGibs[0];
|
|
783
927
|
const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized
|
|
@@ -795,7 +939,7 @@ export class SyncSagaCoordinator {
|
|
|
795
939
|
// We can't really know if it's covered easily without resolving.
|
|
796
940
|
// But if we don't have it (requesting), we won't find it here anyway.
|
|
797
941
|
// If we DO have it (push offer), we might find it.
|
|
798
|
-
const res = await getFromSpace({ addr, space });
|
|
942
|
+
const res = await getFromSpace({ addr, space: destSpace });
|
|
799
943
|
if (res.success && res.ibGibs?.[0]) {
|
|
800
944
|
const ibGib = res.ibGibs[0];
|
|
801
945
|
const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
|
|
@@ -817,27 +961,7 @@ export class SyncSagaCoordinator {
|
|
|
817
961
|
// Problem: Receiver doesn't know X is related to Y without X.
|
|
818
962
|
|
|
819
963
|
// SOLUTION:
|
|
820
|
-
// The *Sender* must include TJP
|
|
821
|
-
// OR: Receiver does a check.
|
|
822
|
-
// Wait, if Sender sends V2, it's just an address.
|
|
823
|
-
// If Receiver doesn't have it, it's opaque.
|
|
824
|
-
|
|
825
|
-
// REVISIT "Constant / No TJP" logic:
|
|
826
|
-
// IF we are testing "Sender Newer", meaning Sender has V2, Receiver has V1.
|
|
827
|
-
// Sender calls `sync([V2])`. Init Frame contains `stones: [V2_Address]`.
|
|
828
|
-
// Receiver checks V2_Address. Not found.
|
|
829
|
-
// Receiver requests V2.
|
|
830
|
-
// Receiver sends Ack(DeltaReq: [V2], Knowledge: {}).
|
|
831
|
-
// Sender receives Ack. Sender sends V2 *AND* its deps (V1, Root).
|
|
832
|
-
// Receiver has V1. Sender sends V1 anyway.
|
|
833
|
-
// This is "Naive Deep Sync".
|
|
834
|
-
|
|
835
|
-
// TO ACHIEVE "Smart Diff":
|
|
836
|
-
// Receiver needs to know "Oh, V2 is a timeline tip of TJP_A".
|
|
837
|
-
// If Sender doesn't send TJP info, Receiver is blind.
|
|
838
|
-
|
|
839
|
-
// Proposed Fix (Short Term):
|
|
840
|
-
// `SyncInitData` should include TJP mappings or we rely on `knowledgeVector` in `Init`?
|
|
964
|
+
// The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
|
|
841
965
|
// `SyncSagaMessageInitData_V1` extends `SyncInitData`.
|
|
842
966
|
// `SyncInitData` has `knowledgeVector`.
|
|
843
967
|
// If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
|
|
@@ -854,11 +978,12 @@ export class SyncSagaCoordinator {
|
|
|
854
978
|
deltaReqAddrs,
|
|
855
979
|
pushOfferAddrs,
|
|
856
980
|
knowledgeVector,
|
|
981
|
+
conflicts: conflicts.length > 0 ? conflicts : undefined, // Include conflicts if any detected
|
|
857
982
|
};
|
|
858
983
|
|
|
859
984
|
const ackStone = await this.createSyncMsgStone({
|
|
860
985
|
data: ackData,
|
|
861
|
-
space,
|
|
986
|
+
space: tempSpace,
|
|
862
987
|
metaspace,
|
|
863
988
|
});
|
|
864
989
|
if (logalot) { console.log(`${lc} ackStone created: ${pretty(ackStone)} (I: 313708132dd53ff946befb7833657826)`); }
|
|
@@ -868,11 +993,14 @@ export class SyncSagaCoordinator {
|
|
|
868
993
|
prevSagaIbGib: sagaIbGib,
|
|
869
994
|
msgStones: [ackStone],
|
|
870
995
|
identity,
|
|
871
|
-
space,
|
|
996
|
+
space: tempSpace,
|
|
872
997
|
metaspace,
|
|
873
998
|
});
|
|
874
999
|
|
|
875
|
-
|
|
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)`); }
|
|
876
1004
|
|
|
877
1005
|
return { frame: ackFrame };
|
|
878
1006
|
}
|
|
@@ -892,138 +1020,364 @@ export class SyncSagaCoordinator {
|
|
|
892
1020
|
protected async handleAckFrame({
|
|
893
1021
|
sagaIbGib,
|
|
894
1022
|
srcGraph,
|
|
895
|
-
|
|
1023
|
+
destSpace,
|
|
1024
|
+
tempSpace,
|
|
896
1025
|
metaspace,
|
|
897
1026
|
identity,
|
|
898
1027
|
}: {
|
|
899
1028
|
sagaIbGib: SyncIbGib_V1,
|
|
900
1029
|
srcGraph: { [addr: string]: IbGib_V1 },
|
|
901
|
-
|
|
1030
|
+
destSpace: IbGibSpaceAny,
|
|
1031
|
+
tempSpace: IbGibSpaceAny,
|
|
902
1032
|
metaspace: MetaspaceService,
|
|
903
1033
|
identity?: KeystoneIbGib_V1,
|
|
904
1034
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
905
1035
|
const lc = `${this.lc}[${this.handleAckFrame.name}]`;
|
|
906
|
-
|
|
1036
|
+
try {
|
|
1037
|
+
if (logalot) { console.log(`${lc} starting... (I: 605b6860e898267a5b50c6d85704be26)`); }
|
|
907
1038
|
|
|
908
|
-
|
|
909
|
-
|
|
1039
|
+
const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
1040
|
+
const ackData = messageData as SyncSagaMessageAckData_V1;
|
|
910
1041
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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)`); }
|
|
917
1049
|
|
|
918
|
-
|
|
919
|
-
|
|
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)}`);
|
|
1055
|
+
}
|
|
920
1056
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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)`);
|
|
927
1063
|
}
|
|
928
|
-
}
|
|
929
1064
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1065
|
+
const optimisticConflicts = conflicts.filter(c => !c.terminal);
|
|
1066
|
+
const mergeDeltaReqs: string[] = []; // Additional requests for merging
|
|
1067
|
+
|
|
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.
|
|
1076
|
+
|
|
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.
|
|
1080
|
+
|
|
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
|
+
}
|
|
938
1190
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
}
|
|
948
1250
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
tipsToSync.push(ibGib);
|
|
1251
|
+
|
|
1252
|
+
console.log(`${lc} [CONFLICT DEBUG] Finished processing ${optimisticConflicts.length} conflicts. mergeDeltaReqs: ${mergeDeltaReqs.length}`);
|
|
952
1253
|
} else {
|
|
953
|
-
|
|
1254
|
+
console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`);
|
|
954
1255
|
}
|
|
955
|
-
}
|
|
956
1256
|
|
|
957
|
-
|
|
958
|
-
// Pass skipAddrs to `getDependencyGraph` or gather manually.
|
|
959
|
-
// `getDependencyGraph` takes a single ibGib.
|
|
960
|
-
// We can optimize by doing it for each tip and unioning the result?
|
|
961
|
-
// Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
|
|
962
|
-
// We will loop.
|
|
963
|
-
|
|
964
|
-
const allDepsSet = new Set<string>();
|
|
965
|
-
|
|
966
|
-
for (const tip of tipsToSync) {
|
|
967
|
-
// Always include the tip itself
|
|
968
|
-
const tipAddr = getIbGibAddr({ ibGib: tip });
|
|
969
|
-
// Only process if not skipped (though deltaReq implies they barely just asked for it)
|
|
970
|
-
// But detailed deps might be skipped.
|
|
971
|
-
|
|
972
|
-
// Get Graph with Skips
|
|
973
|
-
// Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
|
|
974
|
-
const deps = await getDependencyGraph({
|
|
975
|
-
ibGib: tip,
|
|
976
|
-
space,
|
|
977
|
-
skipAddrs: Array.from(skipAddrs)
|
|
978
|
-
});
|
|
1257
|
+
// 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
|
|
979
1258
|
|
|
980
|
-
|
|
981
|
-
|
|
1259
|
+
const deltaReqAddrs = ackData.deltaReqAddrs || [];
|
|
1260
|
+
const pushOfferAddrs = ackData.pushOfferAddrs || [];
|
|
982
1261
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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));
|
|
991
1277
|
});
|
|
992
1278
|
}
|
|
993
1279
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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)`);
|
|
999
1295
|
}
|
|
1000
1296
|
}
|
|
1001
|
-
}
|
|
1002
1297
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
+
});
|
|
1011
1320
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
space,
|
|
1015
|
-
metaspace,
|
|
1016
|
-
});
|
|
1321
|
+
// [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
|
|
1322
|
+
let tipIncluded = false;
|
|
1017
1323
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
+
}
|
|
1025
1334
|
|
|
1026
|
-
|
|
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)}`); }
|
|
1373
|
+
|
|
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
|
+
}
|
|
1027
1381
|
}
|
|
1028
1382
|
|
|
1029
1383
|
/**
|
|
@@ -1039,20 +1393,22 @@ export class SyncSagaCoordinator {
|
|
|
1039
1393
|
protected async handleDeltaFrame({
|
|
1040
1394
|
sagaIbGib,
|
|
1041
1395
|
srcGraph,
|
|
1042
|
-
|
|
1396
|
+
destSpace,
|
|
1397
|
+
tempSpace,
|
|
1043
1398
|
metaspace,
|
|
1044
1399
|
identity,
|
|
1045
1400
|
}: {
|
|
1046
1401
|
sagaIbGib: SyncIbGib_V1,
|
|
1047
1402
|
srcGraph: { [addr: string]: IbGib_V1 },
|
|
1048
|
-
|
|
1403
|
+
destSpace: IbGibSpaceAny,
|
|
1404
|
+
tempSpace: IbGibSpaceAny,
|
|
1049
1405
|
metaspace: MetaspaceService,
|
|
1050
1406
|
identity?: KeystoneIbGib_V1,
|
|
1051
1407
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[], receivedPayloadIbGibs?: IbGib_V1[] } | null> {
|
|
1052
1408
|
const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
|
|
1053
1409
|
if (logalot) { console.log(`${lc} starting...`); }
|
|
1054
1410
|
|
|
1055
|
-
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
1411
|
+
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
1056
1412
|
const deltaData = messageData as SyncSagaMessageDeltaData_V1;
|
|
1057
1413
|
|
|
1058
1414
|
if (!deltaData) {
|
|
@@ -1061,50 +1417,204 @@ export class SyncSagaCoordinator {
|
|
|
1061
1417
|
if (deltaData.stage !== SyncStage.delta) {
|
|
1062
1418
|
throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
|
|
1063
1419
|
}
|
|
1420
|
+
if (logalot) { console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`); }
|
|
1421
|
+
|
|
1422
|
+
console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`);
|
|
1064
1423
|
|
|
1065
1424
|
const payloadAddrs = deltaData.payloadAddrs || [];
|
|
1066
|
-
const
|
|
1425
|
+
const peerRequests = deltaData.requests || [];
|
|
1426
|
+
const peerProposesCommit = deltaData.proposeCommit || false;
|
|
1067
1427
|
|
|
1068
|
-
// 1. Process Received Payload
|
|
1428
|
+
// 1. Process Received Payload (Ingest)
|
|
1069
1429
|
const receivedPayloadIbGibs: IbGib_V1[] = [];
|
|
1070
1430
|
if (payloadAddrs.length > 0) {
|
|
1431
|
+
// We use `payloadAddrs` as the manifest.
|
|
1432
|
+
// The ACTUAL collection of ibGibs should be available via `getFromSpace`
|
|
1433
|
+
// assuming the "Transport" layer put them there implicitly?
|
|
1434
|
+
// OR, if we are local-only, we just get them.
|
|
1435
|
+
// The `handleDeltaFrame` contract assumes data is reachable in `space`.
|
|
1436
|
+
|
|
1071
1437
|
const res = await getFromSpace({
|
|
1072
1438
|
addrs: payloadAddrs,
|
|
1073
|
-
space,
|
|
1439
|
+
space: tempSpace, // Incoming data is in tempSpace
|
|
1074
1440
|
});
|
|
1075
1441
|
if (res.ibGibs) {
|
|
1076
1442
|
receivedPayloadIbGibs.push(...res.ibGibs);
|
|
1443
|
+
// Also put them? `getFromSpace` retrieves. If they are in space, they are persisted.
|
|
1444
|
+
// If this is a Temp Space, they are safe.
|
|
1445
|
+
} else {
|
|
1446
|
+
console.warn(`${lc} Failed to retrieve payloads listed in delta: ${payloadAddrs.join(', ')}`);
|
|
1077
1447
|
}
|
|
1078
1448
|
}
|
|
1079
1449
|
|
|
1080
|
-
// 2. Fulfill Requests (Outgoing Payload)
|
|
1450
|
+
// 2. Fulfill Peer Requests (Outgoing Payload with Delta Dependencies)
|
|
1081
1451
|
const outgoingPayload: IbGib_V1[] = [];
|
|
1082
|
-
|
|
1452
|
+
const outgoingAddrsSet = new Set<string>(); // Track what we've added
|
|
1453
|
+
|
|
1454
|
+
console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${peerRequests.length} peer requests`);
|
|
1455
|
+
|
|
1456
|
+
for (const addr of peerRequests) {
|
|
1457
|
+
// Get the requested ibGib
|
|
1083
1458
|
let ibGib = srcGraph[addr];
|
|
1084
1459
|
if (!ibGib) {
|
|
1085
|
-
const res = await getFromSpace({ addr, space });
|
|
1460
|
+
const res = await getFromSpace({ addr, space: destSpace }); // Query from destSpace
|
|
1086
1461
|
if (res.ibGibs && res.ibGibs.length > 0) {
|
|
1087
1462
|
ibGib = res.ibGibs[0];
|
|
1088
1463
|
}
|
|
1089
1464
|
}
|
|
1465
|
+
|
|
1090
1466
|
if (ibGib) {
|
|
1091
|
-
|
|
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
|
+
}
|
|
1494
|
+
} else {
|
|
1495
|
+
console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
|
|
1092
1496
|
}
|
|
1093
1497
|
}
|
|
1094
1498
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1499
|
+
console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`);
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
// 3. Execute Merges (If applicable)
|
|
1503
|
+
// Check if we have pending conflicts that we CAN resolve now that we have data.
|
|
1504
|
+
// We look at the Saga History (Ack Frame) to find conflicts.
|
|
1505
|
+
// Optimization: Do this only if we received payloads.
|
|
1506
|
+
const mergeResultIbGibs: IbGib_V1[] = [];
|
|
1507
|
+
|
|
1508
|
+
console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`);
|
|
1509
|
+
|
|
1510
|
+
if (receivedPayloadIbGibs.length > 0) {
|
|
1511
|
+
console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`);
|
|
1512
|
+
// Find the Ack frame in history to get conflicts
|
|
1513
|
+
// Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
|
|
1514
|
+
// V1 timelines carry full history in `past`.
|
|
1515
|
+
const pastAddrs = sagaIbGib.rel8ns?.past || [];
|
|
1516
|
+
console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`);
|
|
1517
|
+
let ackData: SyncSagaMessageAckData_V1 | undefined;
|
|
1518
|
+
|
|
1519
|
+
if (pastAddrs.length > 0) {
|
|
1520
|
+
// Batch fetch all past frames
|
|
1521
|
+
const resPast = await getFromSpace({ addrs: pastAddrs, space: tempSpace });
|
|
1522
|
+
if (resPast.success && resPast.ibGibs) {
|
|
1523
|
+
// Iterate backwards (most recent first) to find the latest Ack
|
|
1524
|
+
for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
|
|
1525
|
+
const pastFrame = resPast.ibGibs[i];
|
|
1526
|
+
const messageStone = await getSyncSagaMessageFromFrame({
|
|
1527
|
+
frameIbGib: pastFrame,
|
|
1528
|
+
space: tempSpace
|
|
1529
|
+
});
|
|
1530
|
+
if (messageStone?.data?.stage === SyncStage.ack) {
|
|
1531
|
+
ackData = messageStone.data as SyncSagaMessageAckData_V1;
|
|
1532
|
+
console.log(`${lc} [TEST DEBUG] Found Ack Frame. Conflicts: ${ackData.conflicts?.length || 0}`);
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (ackData && ackData.conflicts) {
|
|
1540
|
+
const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
|
|
1541
|
+
for (const conflict of optimisticConflicts) {
|
|
1542
|
+
const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
|
|
1543
|
+
// We are Sender (usually) here if we are merging.
|
|
1544
|
+
// Check if we have the history needed (timelineAddrs).
|
|
1545
|
+
// Specifically, we needed the `receiverOnly` parts.
|
|
1546
|
+
|
|
1547
|
+
// We blindly attempt merge if we have both tips accessible?
|
|
1548
|
+
// We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
|
|
1549
|
+
|
|
1550
|
+
// Check if we have receiverTip in space
|
|
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]}`);
|
|
1554
|
+
if (resRecTip.success && resRecTip.ibGibs?.[0]) {
|
|
1555
|
+
// We have the tip!
|
|
1556
|
+
// Do we have the full history?
|
|
1557
|
+
// `mergeDivergentTimelines` in `conflict-optimistic` will attempt to fetch history.
|
|
1558
|
+
// If we just ingested the missing pieces, `getFromSpace` inside `merge` should succeed.
|
|
1559
|
+
|
|
1560
|
+
// Perform Merge!
|
|
1561
|
+
try {
|
|
1562
|
+
const mergeResult = await mergeDivergentTimelines({
|
|
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
|
|
1566
|
+
metaspace,
|
|
1567
|
+
});
|
|
1568
|
+
if (mergeResult) {
|
|
1569
|
+
console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
|
|
1570
|
+
if (logalot) { console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
|
|
1571
|
+
mergeResultIbGibs.push(mergeResult);
|
|
1572
|
+
outgoingPayload.push(mergeResult); // Send result to peer
|
|
1573
|
+
}
|
|
1574
|
+
} catch (e) {
|
|
1575
|
+
console.error(`${lc} Merge failed: ${e}`);
|
|
1576
|
+
// If merge fails, we might Abort or just continue?
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// 4. Determine Next Action
|
|
1584
|
+
// We have `outgoingPayload` (Requests + Merge Results).
|
|
1585
|
+
// Does Peer have outstanding requests? No, we fulfilled `peerRequests`.
|
|
1586
|
+
// Do WE have outstanding requests?
|
|
1587
|
+
// We might if `mergeResult` requires further sync? Usually no, result is complete.
|
|
1588
|
+
|
|
1589
|
+
const myRequests: string[] = []; // If we had more needs (e.g. partial payload), we'd add here.
|
|
1590
|
+
|
|
1591
|
+
const hasOutgoing = outgoingPayload.length > 0;
|
|
1592
|
+
const hasMyRequests = myRequests.length > 0;
|
|
1593
|
+
|
|
1594
|
+
if (hasOutgoing || hasMyRequests) {
|
|
1595
|
+
// We have business to attend to -> Send Delta
|
|
1099
1596
|
const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
|
|
1100
|
-
sagaId,
|
|
1597
|
+
sagaId: deltaData.sagaId,
|
|
1101
1598
|
stage: SyncStage.delta,
|
|
1102
1599
|
payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
|
|
1600
|
+
requests: hasMyRequests ? myRequests : undefined,
|
|
1601
|
+
proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
|
|
1602
|
+
// Wait. If we send data, we are NOT committing yet.
|
|
1603
|
+
// We are sending data. The OTHER side must ingest it.
|
|
1604
|
+
// So proposeCommit = true?
|
|
1605
|
+
// "Here is the data. I'm done. If you are good, let's commit."
|
|
1606
|
+
// Yes.
|
|
1103
1607
|
};
|
|
1104
1608
|
|
|
1609
|
+
// BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
|
|
1610
|
+
// We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
|
|
1611
|
+
|
|
1612
|
+
// So yes, proposeCommit = true.
|
|
1613
|
+
responseDeltaData.proposeCommit = true;
|
|
1614
|
+
|
|
1105
1615
|
const deltaStone = await this.createSyncMsgStone({
|
|
1106
1616
|
data: responseDeltaData,
|
|
1107
|
-
space,
|
|
1617
|
+
space: tempSpace,
|
|
1108
1618
|
metaspace
|
|
1109
1619
|
});
|
|
1110
1620
|
|
|
@@ -1112,69 +1622,138 @@ export class SyncSagaCoordinator {
|
|
|
1112
1622
|
prevSagaIbGib: sagaIbGib,
|
|
1113
1623
|
msgStones: [deltaStone],
|
|
1114
1624
|
identity,
|
|
1115
|
-
space,
|
|
1625
|
+
space: tempSpace,
|
|
1116
1626
|
metaspace
|
|
1117
1627
|
});
|
|
1118
1628
|
|
|
1629
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1630
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
1631
|
+
|
|
1119
1632
|
return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
|
|
1120
1633
|
|
|
1121
1634
|
} else {
|
|
1122
|
-
//
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1635
|
+
// We have nothing to send.
|
|
1636
|
+
|
|
1637
|
+
if (peerProposesCommit) {
|
|
1638
|
+
// Peer is done. We are done. -> Commit.
|
|
1639
|
+
const commitData: SyncSagaMessageCommitData_V1 = {
|
|
1640
|
+
sagaId: deltaData.sagaId,
|
|
1641
|
+
stage: SyncStage.commit,
|
|
1642
|
+
success: true,
|
|
1643
|
+
};
|
|
1129
1644
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1645
|
+
const commitStone = await this.createSyncMsgStone({
|
|
1646
|
+
data: commitData,
|
|
1647
|
+
space: tempSpace,
|
|
1648
|
+
metaspace
|
|
1649
|
+
});
|
|
1135
1650
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1651
|
+
const commitFrame = await this.evolveSyncSagaIbGib({
|
|
1652
|
+
prevSagaIbGib: sagaIbGib,
|
|
1653
|
+
msgStones: [commitStone],
|
|
1654
|
+
identity,
|
|
1655
|
+
space: tempSpace,
|
|
1656
|
+
metaspace
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1660
|
+
await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
|
|
1661
|
+
|
|
1662
|
+
return { frame: commitFrame, receivedPayloadIbGibs };
|
|
1663
|
+
|
|
1664
|
+
} else {
|
|
1665
|
+
// peer did NOT propose commit (maybe they just sent data/requests and didn't ready flag).
|
|
1666
|
+
// But we are empty.
|
|
1667
|
+
// So WE propose commit.
|
|
1668
|
+
const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
|
|
1669
|
+
sagaId: deltaData.sagaId,
|
|
1670
|
+
stage: SyncStage.delta,
|
|
1671
|
+
proposeCommit: true,
|
|
1672
|
+
payloadAddrs: [], // Always include empty array if sending delta
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
const deltaStone = await this.createSyncMsgStone({
|
|
1676
|
+
data: responseDeltaData,
|
|
1677
|
+
space: tempSpace,
|
|
1678
|
+
metaspace
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
const deltaFrame = await this.evolveSyncSagaIbGib({
|
|
1682
|
+
prevSagaIbGib: sagaIbGib,
|
|
1683
|
+
msgStones: [deltaStone],
|
|
1684
|
+
identity,
|
|
1685
|
+
space: tempSpace,
|
|
1686
|
+
metaspace
|
|
1687
|
+
});
|
|
1143
1688
|
|
|
1144
|
-
|
|
1689
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1690
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
1691
|
+
|
|
1692
|
+
// Check if PEER proposed commit
|
|
1693
|
+
if (deltaData.proposeCommit) {
|
|
1694
|
+
if (logalot) { console.log(`${lc} Peer proposed commit. Accepting & Committing.`); }
|
|
1695
|
+
// Peer wants to commit and has no more requests.
|
|
1696
|
+
// We should Commit.
|
|
1697
|
+
|
|
1698
|
+
const commitData: SyncSagaMessageCommitData_V1 = {
|
|
1699
|
+
sagaId: deltaData.sagaId,
|
|
1700
|
+
stage: SyncStage.commit,
|
|
1701
|
+
success: true,
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
const commitStone = await this.createSyncMsgStone({
|
|
1705
|
+
data: commitData,
|
|
1706
|
+
space: tempSpace,
|
|
1707
|
+
metaspace
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
const commitFrame = await this.evolveSyncSagaIbGib({
|
|
1711
|
+
prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
|
|
1712
|
+
msgStones: [commitStone],
|
|
1713
|
+
identity,
|
|
1714
|
+
space: tempSpace,
|
|
1715
|
+
metaspace
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1719
|
+
await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
|
|
1720
|
+
|
|
1721
|
+
return { frame: commitFrame, receivedPayloadIbGibs };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
return { frame: deltaFrame, receivedPayloadIbGibs };
|
|
1725
|
+
}
|
|
1145
1726
|
}
|
|
1146
1727
|
}
|
|
1147
1728
|
|
|
1729
|
+
|
|
1148
1730
|
protected async handleCommitFrame({
|
|
1149
1731
|
sagaIbGib,
|
|
1150
|
-
|
|
1732
|
+
destSpace,
|
|
1733
|
+
tempSpace,
|
|
1734
|
+
metaspace,
|
|
1735
|
+
identity,
|
|
1151
1736
|
}: {
|
|
1152
1737
|
sagaIbGib: SyncIbGib_V1,
|
|
1153
|
-
|
|
1738
|
+
destSpace: IbGibSpaceAny,
|
|
1739
|
+
tempSpace: IbGibSpaceAny,
|
|
1740
|
+
metaspace: MetaspaceService,
|
|
1741
|
+
identity?: KeystoneIbGib_V1,
|
|
1154
1742
|
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
1155
1743
|
const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
|
|
1156
|
-
if (logalot) { console.log(`${lc} Commit received
|
|
1157
|
-
return null;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
protected async handleConflictFrame({
|
|
1161
|
-
sagaIbGib,
|
|
1162
|
-
space,
|
|
1163
|
-
}: {
|
|
1164
|
-
sagaIbGib: SyncIbGib_V1,
|
|
1165
|
-
space: IbGibSpaceAny,
|
|
1166
|
-
}): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
|
|
1167
|
-
const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
|
|
1168
|
-
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
1169
|
-
const conflictData = messageData as SyncSagaMessageConflictData_V1;
|
|
1744
|
+
if (logalot) { console.log(`${lc} Commit received.`); }
|
|
1170
1745
|
|
|
1171
|
-
|
|
1746
|
+
// Sender Logic (Finalizing):
|
|
1747
|
+
// If we are here, we received a Commit frame from the Peer.
|
|
1748
|
+
// This implies the Peer has successfully committed.
|
|
1749
|
+
// We should now:
|
|
1750
|
+
// 1. Validate (implicitly done by receiving valid frame)
|
|
1751
|
+
// 2. Perform our own cleanup (Temp -> Dest, if applicable)
|
|
1752
|
+
// 3. Return null to signal saga completion.
|
|
1172
1753
|
|
|
1173
|
-
|
|
1174
|
-
throw new Error(`${lc} Saga aborted due to conflicts: ${JSON.stringify(conflictData.conflicts)} (E: b08d1f2a3c4e5d6f7a8b9c0d1e2f3a4b)`);
|
|
1175
|
-
}
|
|
1754
|
+
// Note: Currently we don't have explicit cleanup logic implemented here yet (TODO).
|
|
1176
1755
|
|
|
1177
|
-
|
|
1756
|
+
if (logalot) { console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`); }
|
|
1178
1757
|
return null;
|
|
1179
1758
|
}
|
|
1180
1759
|
|
|
@@ -1189,19 +1768,74 @@ export class SyncSagaCoordinator {
|
|
|
1189
1768
|
space: IbGibSpaceAny,
|
|
1190
1769
|
metaspace: MetaspaceService,
|
|
1191
1770
|
}): Promise<IbGib_V1<TStoneData>> {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
data
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1771
|
+
const lc = `${this.lc}[${this.createSyncMsgStone.name}]`;
|
|
1772
|
+
try {
|
|
1773
|
+
if (logalot) { console.log(`${lc} starting... (I: 5f7f98e8ff980364f7191fcee4531e26)`); }
|
|
1774
|
+
|
|
1775
|
+
const ib = await getSyncSagaMessageIb({ data });
|
|
1776
|
+
const stone = await Factory_V1.stone({
|
|
1777
|
+
ib,
|
|
1778
|
+
parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
|
|
1779
|
+
data,
|
|
1780
|
+
uuid: true, // we want the stone to have its own uniqueness
|
|
1781
|
+
});
|
|
1782
|
+
if (logalot) { console.log(`${lc} Created stone: ${getIbGibAddr({ ibGib: stone })}`); }
|
|
1783
|
+
await putInSpace({ space, ibGib: stone });
|
|
1784
|
+
await metaspace.registerNewIbGib({ ibGib: stone });
|
|
1785
|
+
return stone as IbGib_V1<TStoneData>;
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
1788
|
+
throw error;
|
|
1789
|
+
} finally {
|
|
1790
|
+
if (logalot) { console.log(`${lc} complete.`); }
|
|
1791
|
+
}
|
|
1202
1792
|
}
|
|
1203
1793
|
|
|
1204
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
|
+
|
|
1205
1839
|
/**
|
|
1206
1840
|
* Evolves the saga timeline with a new frame.
|
|
1207
1841
|
*/
|
|
@@ -1211,12 +1845,14 @@ export class SyncSagaCoordinator {
|
|
|
1211
1845
|
identity,
|
|
1212
1846
|
space,
|
|
1213
1847
|
metaspace,
|
|
1848
|
+
conflictStrategy,
|
|
1214
1849
|
}: {
|
|
1215
1850
|
prevSagaIbGib?: SyncIbGib_V1,
|
|
1216
1851
|
msgStones: IbGib_V1[],
|
|
1217
1852
|
identity?: KeystoneIbGib_V1,
|
|
1218
1853
|
space: IbGibSpaceAny,
|
|
1219
1854
|
metaspace: MetaspaceService,
|
|
1855
|
+
conflictStrategy?: SyncConflictStrategy,
|
|
1220
1856
|
}): Promise<SyncIbGib_V1> {
|
|
1221
1857
|
const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
|
|
1222
1858
|
try {
|
|
@@ -1293,6 +1929,7 @@ export class SyncSagaCoordinator {
|
|
|
1293
1929
|
payload: undefined, // Data in stone
|
|
1294
1930
|
n: 0,
|
|
1295
1931
|
isTjp: true,
|
|
1932
|
+
conflictStrategy,
|
|
1296
1933
|
};
|
|
1297
1934
|
const ib = await getSyncIb({ data });
|
|
1298
1935
|
|