@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
|
@@ -16,6 +16,8 @@ import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-consta
|
|
|
16
16
|
import { SyncSagaContextCmd } from "./sync-saga-context/sync-saga-context-types.mjs";
|
|
17
17
|
import { createSyncSagaContext } from "./sync-saga-context/sync-saga-context-helpers.mjs";
|
|
18
18
|
import { newupSubject } from "../common/pubsub/subject/subject-helper.mjs";
|
|
19
|
+
import { mergeDivergentTimelines } from "./strategies/conflict-optimistic.mjs";
|
|
20
|
+
import { getSyncSagaMessageFromFrame } from "./sync-saga-message/sync-saga-message-helpers.mjs";
|
|
19
21
|
const logalot = GLOBAL_LOG_A_LOT || true;
|
|
20
22
|
/**
|
|
21
23
|
* Orchestrates the synchronization process between two spaces (Source and Destination).
|
|
@@ -49,11 +51,15 @@ export class SyncSagaCoordinator {
|
|
|
49
51
|
* @param opts.domainIbGibs - The root ibgibs defining the scope of the sync.
|
|
50
52
|
* @param opts.useSessionIdentity - (Optional) Whether to create an ephemeral session identity. Default: true.
|
|
51
53
|
*/
|
|
52
|
-
async sync({ peer, localSpace, metaspace, domainIbGibs, useSessionIdentity, }) {
|
|
54
|
+
async sync({ peer, localSpace: _localSpace, source: _source, metaspace, domainIbGibs, conflictStrategy = 'abort', useSessionIdentity = true, }) {
|
|
53
55
|
const lc = `${this.lc}[${this.sync.name}]`;
|
|
54
56
|
if (logalot) {
|
|
55
57
|
console.log(`${lc} starting...`);
|
|
56
58
|
}
|
|
59
|
+
const localSpace = (_source || _localSpace);
|
|
60
|
+
if (!localSpace) {
|
|
61
|
+
throw new Error(`${lc} source (or localSpace) required (E: 8a9b0c1d)`);
|
|
62
|
+
}
|
|
57
63
|
// 1. SETUP SAGA METADATA
|
|
58
64
|
const sagaId = await getUUID();
|
|
59
65
|
// Setup Observable & Promise
|
|
@@ -97,9 +103,7 @@ export class SyncSagaCoordinator {
|
|
|
97
103
|
const sessionIdentity = useSessionIdentity
|
|
98
104
|
? await this.getSessionIdentity({ sagaId, metaspace, tempSpace })
|
|
99
105
|
: undefined;
|
|
100
|
-
if (logalot) {
|
|
101
|
-
console.log(`${lc} sessionIdentity: ${sessionIdentity ? pretty(sessionIdentity) : 'undefined'} (I: abc01872800b3a66b819a05898bba826)`);
|
|
102
|
-
}
|
|
106
|
+
// if (logalot) { console.log(`${lc} sessionIdentity: ${sessionIdentity ? pretty(sessionIdentity) : 'undefined'} (I: abc01872800b3a66b819a05898bba826)`); }
|
|
103
107
|
// 3. CREATE INITIAL FRAME (Stage.init)
|
|
104
108
|
const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
|
|
105
109
|
sagaId,
|
|
@@ -108,6 +112,7 @@ export class SyncSagaCoordinator {
|
|
|
108
112
|
domainIbGibs,
|
|
109
113
|
tempSpace,
|
|
110
114
|
metaspace,
|
|
115
|
+
conflictStrategy
|
|
111
116
|
});
|
|
112
117
|
// 4. EXECUTE SAGA LOOP (FSM)
|
|
113
118
|
const syncedIbGibs = await this.executeSagaLoop({
|
|
@@ -259,13 +264,25 @@ export class SyncSagaCoordinator {
|
|
|
259
264
|
});
|
|
260
265
|
}
|
|
261
266
|
// B. Transmit
|
|
262
|
-
if (logalot) {
|
|
263
|
-
console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`);
|
|
264
|
-
}
|
|
267
|
+
// if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
|
|
265
268
|
updates$.next(requestCtx);
|
|
266
269
|
const responseCtx = await peer.witness(requestCtx);
|
|
267
270
|
// C. Handle Response
|
|
268
271
|
if (!responseCtx) {
|
|
272
|
+
// Check if we just sent a Commit frame. If so, peer's silence is success/expected.
|
|
273
|
+
if (currentFrame) {
|
|
274
|
+
const msg = await getSyncSagaMessageFromFrame({ frameIbGib: currentFrame, space: localSpace });
|
|
275
|
+
if (logalot) {
|
|
276
|
+
console.log(`${lc} Checking currentFrame stage: ${msg?.data?.stage} (Expected: ${SyncStage.commit})`);
|
|
277
|
+
}
|
|
278
|
+
if (msg?.data?.stage === SyncStage.commit) {
|
|
279
|
+
if (logalot) {
|
|
280
|
+
console.log(`${lc} Sender sent Commit. Peer returned no response. Saga Complete.`);
|
|
281
|
+
}
|
|
282
|
+
currentFrame = null;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
269
286
|
throw new Error(`responseCtx falsy. Peer returned no response context (E: c099d8073b48d85e881f917835158f26)`);
|
|
270
287
|
// console.warn(`${lc} Peer returned no response context. Ending loop.`);
|
|
271
288
|
// currentFrame = null;
|
|
@@ -305,7 +322,8 @@ export class SyncSagaCoordinator {
|
|
|
305
322
|
const result = await this.handleSagaFrame({
|
|
306
323
|
sagaIbGib: remoteFrame,
|
|
307
324
|
srcGraph,
|
|
308
|
-
|
|
325
|
+
destSpace: localSpace, // Query existing data from localSpace (Source)
|
|
326
|
+
tempSpace: tempSpace, // Transaction space for saga frames
|
|
309
327
|
identity: sessionIdentity,
|
|
310
328
|
metaspace
|
|
311
329
|
});
|
|
@@ -318,6 +336,82 @@ export class SyncSagaCoordinator {
|
|
|
318
336
|
}
|
|
319
337
|
return allReceivedIbGibs;
|
|
320
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Helper to get Knowledge Vector for specific domain ibGibs or TJPs.
|
|
341
|
+
* Useful for testing and external validation.
|
|
342
|
+
*/
|
|
343
|
+
async getKnowledgeVector({ space, metaspace, domainIbGibs, tjpAddrs, }) {
|
|
344
|
+
const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
|
|
345
|
+
try {
|
|
346
|
+
if (logalot) {
|
|
347
|
+
console.log(`${lc} starting... (I: e184f8a7818666febfbbd2d841ed3826)`);
|
|
348
|
+
}
|
|
349
|
+
console.dir(space);
|
|
350
|
+
if (!(domainIbGibs && domainIbGibs.length > 0) &&
|
|
351
|
+
!(tjpAddrs && tjpAddrs.length > 0)) {
|
|
352
|
+
throw new Error(`(UNEXPECTED) domainIbGibs and tjpAddrs falsy/empty? we need one or the other (E: f674285111c8648398cd79d8c08ec826)`);
|
|
353
|
+
}
|
|
354
|
+
if ((domainIbGibs && domainIbGibs.length > 0) &&
|
|
355
|
+
(tjpAddrs && tjpAddrs.length > 0)) {
|
|
356
|
+
throw new Error(`(UNEXPECTED) both domainIbGibs and tjpAddrs truthy? only pass in one or the other. (E: f674285111c8648398cd79d8c08ec826)`);
|
|
357
|
+
}
|
|
358
|
+
let tjps = [];
|
|
359
|
+
if (tjpAddrs) {
|
|
360
|
+
tjps = tjpAddrs;
|
|
361
|
+
}
|
|
362
|
+
else if (domainIbGibs && domainIbGibs.length > 0) {
|
|
363
|
+
// Extract TJPs from domain Ibgibs
|
|
364
|
+
if (logalot) {
|
|
365
|
+
console.log(`${lc} domainIbGibs (${domainIbGibs.length}) provided. (I: a378995a0658af1f086ac1f297486c26)`);
|
|
366
|
+
}
|
|
367
|
+
const { mapWithTjp_YesDna, mapWithTjp_NoDna } = splitPerTjpAndOrDna({ ibGibs: domainIbGibs });
|
|
368
|
+
if (logalot) {
|
|
369
|
+
console.log(`${lc}[TEST DEBUG] mapWithTjp_YesDna: ${JSON.stringify(mapWithTjp_YesDna)} (I: 287e22897148298e185712c8d50cfb26)`);
|
|
370
|
+
}
|
|
371
|
+
if (logalot) {
|
|
372
|
+
console.log(`${lc}[TEST DEBUG] mapWithTjp_NoDna: ${JSON.stringify(mapWithTjp_NoDna)} (I: 1bdc62656294aed0f9df334647dc7326)`);
|
|
373
|
+
}
|
|
374
|
+
const allWithTjp = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
|
|
375
|
+
const timelineMap = getTimelinesGroupedByTjp({ ibGibs: allWithTjp });
|
|
376
|
+
if (logalot) {
|
|
377
|
+
console.log(`${lc}[TEST DEBUG] timelineMap: ${JSON.stringify(timelineMap)} (I: 2cc04898e5f85179fb1ac7f827abc426)`);
|
|
378
|
+
}
|
|
379
|
+
tjps = Object.keys(timelineMap);
|
|
380
|
+
if (logalot) {
|
|
381
|
+
console.log(`${lc}[TEST DEBUG] tjps: ${tjps} (I: 3dd548667cbd967c68e57c88dc570826)`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// No info provided. Return empty? Or throw?
|
|
386
|
+
// User test context implied "everything", but implementation requires scope.
|
|
387
|
+
console.warn(`${lc} No domainIbGibs or tjpAddrs provided. Returning empty KV.`);
|
|
388
|
+
return {};
|
|
389
|
+
}
|
|
390
|
+
if (tjps.length === 0) {
|
|
391
|
+
return {};
|
|
392
|
+
}
|
|
393
|
+
if (logalot) {
|
|
394
|
+
console.log(`${lc} getting latest addrs for tjps: ${tjps} (I: d4e7080b8ba8187c583b82fd91ac0626)`);
|
|
395
|
+
}
|
|
396
|
+
const res = await getLatestAddrs({ space, tjpAddrs: tjps });
|
|
397
|
+
if (!res.data || !res.data.latestAddrsMap) {
|
|
398
|
+
throw new Error(`${lc} Failed to get latest addrs. (E: 7a8b9c0d)`);
|
|
399
|
+
}
|
|
400
|
+
if (logalot) {
|
|
401
|
+
console.log(`${lc}[TEST DEBUG] res.data.latestAddrsMap: ${JSON.stringify(res.data.latestAddrsMap)} (I: a8e128bdf80898ac2e6d8021a5bff726)`);
|
|
402
|
+
}
|
|
403
|
+
return res.data.latestAddrsMap;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
if (logalot) {
|
|
411
|
+
console.log(`${lc} complete.`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
321
415
|
async analyzeTimelines({ domainIbGibs, space, }) {
|
|
322
416
|
const lc = `${this.lc}[${this.analyzeTimelines.name}]`;
|
|
323
417
|
const srcGraph = await getDependencyGraph({
|
|
@@ -346,7 +440,7 @@ export class SyncSagaCoordinator {
|
|
|
346
440
|
* Generates the first frame containing the Knowledge Vector of the Local Space.
|
|
347
441
|
* This is sent to the Receiver to begin Gap Analysis.
|
|
348
442
|
*/
|
|
349
|
-
async createInitFrame({ sagaId, sessionIdentity, localSpace, domainIbGibs, tempSpace, metaspace }) {
|
|
443
|
+
async createInitFrame({ sagaId, sessionIdentity, localSpace, domainIbGibs, tempSpace, metaspace, conflictStrategy, }) {
|
|
350
444
|
const lc = `${this.lc}[${this.createInitFrame.name}]`;
|
|
351
445
|
try {
|
|
352
446
|
if (logalot) {
|
|
@@ -371,23 +465,31 @@ export class SyncSagaCoordinator {
|
|
|
371
465
|
const tip = timeline.at(-1);
|
|
372
466
|
initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
|
|
373
467
|
});
|
|
468
|
+
if (logalot) {
|
|
469
|
+
console.log(`${lc} SyncStage.init: ${SyncStage.init}, SyncStage.commit: ${SyncStage.commit}`);
|
|
470
|
+
console.log(`${lc} initData.stage: ${initData.stage}`);
|
|
471
|
+
}
|
|
374
472
|
const initStone = await this.createSyncMsgStone({
|
|
375
473
|
data: initData,
|
|
376
474
|
space: tempSpace,
|
|
377
475
|
metaspace
|
|
378
476
|
});
|
|
379
|
-
if (logalot) {
|
|
380
|
-
console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`);
|
|
381
|
-
}
|
|
477
|
+
// if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
|
|
382
478
|
const sagaFrame = await this.evolveSyncSagaIbGib({
|
|
383
479
|
msgStones: [initStone],
|
|
384
480
|
identity: sessionIdentity,
|
|
385
481
|
space: tempSpace,
|
|
482
|
+
metaspace,
|
|
483
|
+
conflictStrategy,
|
|
484
|
+
});
|
|
485
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
486
|
+
await this.ensureSagaFrameInBothSpaces({
|
|
487
|
+
frame: sagaFrame,
|
|
488
|
+
destSpace: localSpace, // localSpace is the Sender's destSpace
|
|
489
|
+
tempSpace,
|
|
386
490
|
metaspace
|
|
387
491
|
});
|
|
388
|
-
if (logalot) {
|
|
389
|
-
console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`);
|
|
390
|
-
}
|
|
492
|
+
// if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
|
|
391
493
|
return { sagaFrame, srcGraph };
|
|
392
494
|
}
|
|
393
495
|
catch (error) {
|
|
@@ -413,7 +515,7 @@ export class SyncSagaCoordinator {
|
|
|
413
515
|
* * If running on **Sender**: Handles `Ack` (via `handleAckFrame`).
|
|
414
516
|
* * If running on **Either**: Handles `Delta` (via `handleDeltaFrame`) or `Commit`.
|
|
415
517
|
*/
|
|
416
|
-
async handleSagaFrame({ sagaIbGib, srcGraph,
|
|
518
|
+
async handleSagaFrame({ sagaIbGib, srcGraph, destSpace, tempSpace, identity, identitySecret, metaspace, }) {
|
|
417
519
|
const lc = `${this.lc}[${this.handleSagaFrame.name}]`;
|
|
418
520
|
try {
|
|
419
521
|
if (logalot) {
|
|
@@ -426,21 +528,19 @@ export class SyncSagaCoordinator {
|
|
|
426
528
|
console.log(`${lc} sagaIbGib: ${pretty(sagaIbGib)} (I: 1b99d87d262e9d18d8a607a80b1a0126)`);
|
|
427
529
|
}
|
|
428
530
|
// Get Stage from Stone (or Frame for Init fallback)
|
|
429
|
-
const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
531
|
+
const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
430
532
|
if (logalot) {
|
|
431
533
|
console.log(`${lc} handling frame stage: ${stage}`);
|
|
432
534
|
}
|
|
433
535
|
switch (stage) {
|
|
434
536
|
case SyncStage.init:
|
|
435
|
-
return await this.handleInitFrame({ sagaIbGib, messageData,
|
|
537
|
+
return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, destSpace, tempSpace, identity, identitySecret });
|
|
436
538
|
case SyncStage.ack:
|
|
437
|
-
return await this.handleAckFrame({ sagaIbGib, srcGraph,
|
|
539
|
+
return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity });
|
|
438
540
|
case SyncStage.delta:
|
|
439
|
-
return await this.handleDeltaFrame({ sagaIbGib, srcGraph,
|
|
541
|
+
return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity, });
|
|
440
542
|
case SyncStage.commit:
|
|
441
|
-
return await this.handleCommitFrame({ sagaIbGib,
|
|
442
|
-
case SyncStage.conflict:
|
|
443
|
-
return await this.handleConflictFrame({ sagaIbGib, space });
|
|
543
|
+
return await this.handleCommitFrame({ sagaIbGib, metaspace, destSpace, tempSpace, identity, });
|
|
444
544
|
default:
|
|
445
545
|
throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
|
|
446
546
|
}
|
|
@@ -468,22 +568,25 @@ export class SyncSagaCoordinator {
|
|
|
468
568
|
* 3. Identifies what Receiver needs (`deltaReqAddrs`).
|
|
469
569
|
* 4. Returns an `Ack` frame containing these lists.
|
|
470
570
|
*/
|
|
471
|
-
async handleInitFrame({ sagaIbGib, messageData,
|
|
571
|
+
async handleInitFrame({ sagaIbGib, messageData, destSpace, tempSpace, metaspace, identity, identitySecret, }) {
|
|
472
572
|
const lc = `${this.lc}[${this.handleInitFrame.name}]`;
|
|
573
|
+
console.log(`${lc} [TEST DEBUG] Received destSpace: ${destSpace.data?.name || destSpace.ib} (uuid: ${destSpace.data?.uuid || '[no uuid]'})`);
|
|
473
574
|
if (logalot) {
|
|
474
575
|
console.log(`${lc} starting...`);
|
|
475
576
|
}
|
|
476
577
|
// Extract Init Data
|
|
477
578
|
const initData = messageData; // Using renamed variable for clarity
|
|
478
|
-
if (
|
|
479
|
-
|
|
579
|
+
if (initData.stage !== SyncStage.init) {
|
|
580
|
+
throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
|
|
480
581
|
}
|
|
582
|
+
// if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
|
|
481
583
|
if (!initData || !initData.knowledgeVector) {
|
|
482
584
|
throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
|
|
483
585
|
}
|
|
586
|
+
// Determine Strategy from Saga Data (since V1 stores it in root)
|
|
587
|
+
const conflictStrategy = sagaIbGib.data.conflictStrategy || 'abort';
|
|
484
588
|
// 2. Gap Analysis
|
|
485
589
|
const conflicts = [];
|
|
486
|
-
const conflictStrategy = initData.conflictStrategy || 'abort'; // Default to abort if not specified, or we should parameterize this
|
|
487
590
|
const deltaReqAddrs = [];
|
|
488
591
|
const pushOfferAddrs = [];
|
|
489
592
|
// Stones Analysis (Constants / Non-TJPs)
|
|
@@ -493,7 +596,7 @@ export class SyncSagaCoordinator {
|
|
|
493
596
|
console.log(`${lc} processing stones: ${stones.length}`);
|
|
494
597
|
}
|
|
495
598
|
// Check if we have these stones
|
|
496
|
-
const resStones = await getFromSpace({ space, addrs: stones });
|
|
599
|
+
const resStones = await getFromSpace({ space: destSpace, addrs: stones });
|
|
497
600
|
const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
|
|
498
601
|
if (addrsNotFound && addrsNotFound.length > 0) {
|
|
499
602
|
if (logalot) {
|
|
@@ -511,6 +614,7 @@ export class SyncSagaCoordinator {
|
|
|
511
614
|
console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`);
|
|
512
615
|
}
|
|
513
616
|
const remoteTjps = Object.keys(remoteKV);
|
|
617
|
+
console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`);
|
|
514
618
|
if (logalot) {
|
|
515
619
|
console.log(`${lc} remoteTjps: ${pretty(remoteTjps)} (I: 86ea4c53db0dc184c8b253386c402126)`);
|
|
516
620
|
}
|
|
@@ -519,7 +623,7 @@ export class SyncSagaCoordinator {
|
|
|
519
623
|
if (remoteTjps.length > 0) {
|
|
520
624
|
// Batch get latest addrs for the TJPs
|
|
521
625
|
const resGetLatestAddrs = await getLatestAddrs({
|
|
522
|
-
space,
|
|
626
|
+
space: destSpace,
|
|
523
627
|
tjpAddrs: remoteTjps,
|
|
524
628
|
});
|
|
525
629
|
if (!resGetLatestAddrs.data) {
|
|
@@ -529,6 +633,7 @@ export class SyncSagaCoordinator {
|
|
|
529
633
|
throw new Error(`(UNEXPECTED) resGetLatestAddrs.data.latestAddrsMap falsy? (E: 16bc386dd51d0ff53a49620b1e641826)`);
|
|
530
634
|
}
|
|
531
635
|
localKV = resGetLatestAddrs.data.latestAddrsMap;
|
|
636
|
+
console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localKV)}`);
|
|
532
637
|
if (logalot) {
|
|
533
638
|
console.log(`${lc} localKV: ${pretty(localKV)} (I: 980975642cbccd8018cf0cd808d30826)`);
|
|
534
639
|
}
|
|
@@ -539,21 +644,25 @@ export class SyncSagaCoordinator {
|
|
|
539
644
|
const localAddr = localKV[tjp];
|
|
540
645
|
if (!localAddr) {
|
|
541
646
|
// We (Receiver) don't have this timeline. Request it.
|
|
647
|
+
console.log(`${lc} [TEST DEBUG] Missing local timeline for TJP: ${tjp}. Requesting remoteAddr: ${remoteAddr}`);
|
|
542
648
|
deltaReqAddrs.push(remoteAddr);
|
|
543
649
|
continue;
|
|
544
650
|
}
|
|
545
651
|
if (localAddr === remoteAddr) {
|
|
546
652
|
// Synced
|
|
653
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`);
|
|
547
654
|
continue;
|
|
548
655
|
}
|
|
656
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`);
|
|
549
657
|
// Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
|
|
550
658
|
// (Sender has older version, Receiver has newer) -> Receiver Offers Push
|
|
551
659
|
const isRemoteInPast = await isPastFrame({
|
|
552
660
|
olderAddr: remoteAddr,
|
|
553
661
|
newerAddr: localAddr,
|
|
554
|
-
space,
|
|
662
|
+
space: destSpace,
|
|
555
663
|
});
|
|
556
664
|
if (isRemoteInPast) {
|
|
665
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote is in past - offering push`);
|
|
557
666
|
pushOfferAddrs.push(localAddr);
|
|
558
667
|
}
|
|
559
668
|
else {
|
|
@@ -563,28 +672,54 @@ export class SyncSagaCoordinator {
|
|
|
563
672
|
const isLocalInPast = await isPastFrame({
|
|
564
673
|
olderAddr: localAddr,
|
|
565
674
|
newerAddr: remoteAddr,
|
|
566
|
-
space,
|
|
675
|
+
space: destSpace,
|
|
567
676
|
});
|
|
568
677
|
if (isLocalInPast) {
|
|
569
678
|
// Fast-Forward: We update to remote's tip.
|
|
679
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`);
|
|
570
680
|
deltaReqAddrs.push(remoteAddr);
|
|
571
681
|
}
|
|
572
682
|
else {
|
|
573
683
|
// DIVERGENCE: Both have changes the other doesn't know about.
|
|
574
|
-
|
|
575
|
-
// Conflict Strategy Handling
|
|
576
|
-
// TODO: Implement "manual" strategy (requires user intervention/pause)
|
|
577
|
-
// TODO: Implement "local_wins" strategy
|
|
578
|
-
// TODO: Implement "server_wins" strategy
|
|
684
|
+
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`);
|
|
579
685
|
if (conflictStrategy === 'abort') {
|
|
580
|
-
// We will
|
|
686
|
+
// Abort Strategy: We will treat this as terminal.
|
|
687
|
+
// But for Unified Ack, we just mark it terminal in the list?
|
|
688
|
+
// Or do we actually throw/abort the saga?
|
|
689
|
+
// Current logic (below) aborts the saga if ANY conflict is terminal/abort.
|
|
690
|
+
conflicts.push({
|
|
691
|
+
tjpAddr: tjp,
|
|
692
|
+
localAddr: localAddr,
|
|
693
|
+
remoteAddr,
|
|
694
|
+
timelineAddrs: [], // Not needed for abort
|
|
695
|
+
reason: 'divergence',
|
|
696
|
+
terminal: true
|
|
697
|
+
});
|
|
581
698
|
}
|
|
582
699
|
else if (conflictStrategy === 'optimistic') {
|
|
583
|
-
// Optimistic: We
|
|
584
|
-
// We
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
|
|
700
|
+
// Optimistic: We want to resolving this.
|
|
701
|
+
// We need to send our history to the Sender so they can Merge.
|
|
702
|
+
// Fetch Full History for Local Timeline
|
|
703
|
+
// Note: We might optimize this to only send "recent" history if we had a KV?
|
|
704
|
+
// But for now, get full past.
|
|
705
|
+
// Optimization: localKV might not have full history.
|
|
706
|
+
// We need to inspect the 'past' of the local tip.
|
|
707
|
+
// We need the ACTUAL object to get the past.
|
|
708
|
+
// We have localAddr.
|
|
709
|
+
const resLocalTip = await getFromSpace({ space: destSpace, addr: localAddr });
|
|
710
|
+
const localTip = resLocalTip.ibGibs?.[0];
|
|
711
|
+
if (!localTip) {
|
|
712
|
+
throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`);
|
|
713
|
+
}
|
|
714
|
+
const timelineAddrs = [localAddr, ...(localTip.rel8ns?.past || [])];
|
|
715
|
+
conflicts.push({
|
|
716
|
+
tjpAddr: tjp,
|
|
717
|
+
localAddr: localAddr,
|
|
718
|
+
remoteAddr,
|
|
719
|
+
timelineAddrs,
|
|
720
|
+
reason: 'divergence',
|
|
721
|
+
terminal: false
|
|
722
|
+
});
|
|
588
723
|
}
|
|
589
724
|
else {
|
|
590
725
|
throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
|
|
@@ -592,37 +727,29 @@ export class SyncSagaCoordinator {
|
|
|
592
727
|
}
|
|
593
728
|
}
|
|
594
729
|
}
|
|
595
|
-
if
|
|
730
|
+
// Check if we should ABORT (if any conflict is terminal)
|
|
731
|
+
const hasTerminalConflicts = conflicts.some(c => c.terminal);
|
|
732
|
+
if (hasTerminalConflicts) {
|
|
596
733
|
// Abort Strategy: Kill the saga.
|
|
597
734
|
if (logalot) {
|
|
598
|
-
console.warn(`${lc} ABORTING Sync Saga due to conflicts: ${JSON.stringify(conflicts)}`);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
metaspace,
|
|
612
|
-
});
|
|
613
|
-
const conflictFrame = await this.evolveSyncSagaIbGib({
|
|
614
|
-
prevSagaIbGib: sagaIbGib,
|
|
615
|
-
msgStones: [conflictStone],
|
|
616
|
-
identity,
|
|
617
|
-
space,
|
|
618
|
-
metaspace,
|
|
619
|
-
});
|
|
620
|
-
return { frame: conflictFrame };
|
|
735
|
+
console.warn(`${lc} ABORTING Sync Saga due to terminal conflicts: ${JSON.stringify(conflicts)}`);
|
|
736
|
+
}
|
|
737
|
+
// We reuse the ConflictData structure for terminal aborts?
|
|
738
|
+
// Or do we send an Ack with terminal conflicts?
|
|
739
|
+
// Original design had explicit Conflict Frame for Abort.
|
|
740
|
+
// Let's stick to that for purely terminal cases to be safe/explicit?
|
|
741
|
+
// Or Unified: Just send Ack with terminal=true conflicts. Sender sees them and aborts.
|
|
742
|
+
// Decision: Unified Ack for everything is cleaner protocol.
|
|
743
|
+
// But wait, the original code below creates a Conflict Stone.
|
|
744
|
+
// Let's preserve the explicit 'Conflict' frame for total aborts if that's easier,
|
|
745
|
+
// OR fully switch to Ack.
|
|
746
|
+
// Protocol states: Init -> Ack. If Ack contains terminal errors, Sender can Commit(Fail).
|
|
747
|
+
// Let's use Ack with conflicts.
|
|
621
748
|
}
|
|
622
749
|
// 2. Add Push Offers (Missing in Local)
|
|
623
750
|
// Check if we have them. If not, ask for them.
|
|
624
751
|
for (const addr of pushOfferAddrs) {
|
|
625
|
-
const hasIt = await getFromSpace({ addr, space });
|
|
752
|
+
const hasIt = await getFromSpace({ addr, space: destSpace });
|
|
626
753
|
if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
|
|
627
754
|
// If we don't have it, we put it in `deltaReqAddrs` of the Ack.
|
|
628
755
|
deltaReqAddrs.push(addr);
|
|
@@ -638,7 +765,7 @@ export class SyncSagaCoordinator {
|
|
|
638
765
|
for (const tjp of remoteTjps) {
|
|
639
766
|
const localAddr = localKV[tjp];
|
|
640
767
|
if (localAddr) {
|
|
641
|
-
const res = await getFromSpace({ addr: localAddr, space });
|
|
768
|
+
const res = await getFromSpace({ addr: localAddr, space: destSpace });
|
|
642
769
|
if (res.success && res.ibGibs?.[0]) {
|
|
643
770
|
const ibGib = res.ibGibs[0];
|
|
644
771
|
const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized
|
|
@@ -655,7 +782,7 @@ export class SyncSagaCoordinator {
|
|
|
655
782
|
// We can't really know if it's covered easily without resolving.
|
|
656
783
|
// But if we don't have it (requesting), we won't find it here anyway.
|
|
657
784
|
// If we DO have it (push offer), we might find it.
|
|
658
|
-
const res = await getFromSpace({ addr, space });
|
|
785
|
+
const res = await getFromSpace({ addr, space: destSpace });
|
|
659
786
|
if (res.success && res.ibGibs?.[0]) {
|
|
660
787
|
const ibGib = res.ibGibs[0];
|
|
661
788
|
const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
|
|
@@ -675,24 +802,7 @@ export class SyncSagaCoordinator {
|
|
|
675
802
|
// 2. Receiver says "I don't have X. But if I did have Y (ancestor), I'd tell you."
|
|
676
803
|
// Problem: Receiver doesn't know X is related to Y without X.
|
|
677
804
|
// SOLUTION:
|
|
678
|
-
// The *Sender* must include TJP
|
|
679
|
-
// OR: Receiver does a check.
|
|
680
|
-
// Wait, if Sender sends V2, it's just an address.
|
|
681
|
-
// If Receiver doesn't have it, it's opaque.
|
|
682
|
-
// REVISIT "Constant / No TJP" logic:
|
|
683
|
-
// IF we are testing "Sender Newer", meaning Sender has V2, Receiver has V1.
|
|
684
|
-
// Sender calls `sync([V2])`. Init Frame contains `stones: [V2_Address]`.
|
|
685
|
-
// Receiver checks V2_Address. Not found.
|
|
686
|
-
// Receiver requests V2.
|
|
687
|
-
// Receiver sends Ack(DeltaReq: [V2], Knowledge: {}).
|
|
688
|
-
// Sender receives Ack. Sender sends V2 *AND* its deps (V1, Root).
|
|
689
|
-
// Receiver has V1. Sender sends V1 anyway.
|
|
690
|
-
// This is "Naive Deep Sync".
|
|
691
|
-
// TO ACHIEVE "Smart Diff":
|
|
692
|
-
// Receiver needs to know "Oh, V2 is a timeline tip of TJP_A".
|
|
693
|
-
// If Sender doesn't send TJP info, Receiver is blind.
|
|
694
|
-
// Proposed Fix (Short Term):
|
|
695
|
-
// `SyncInitData` should include TJP mappings or we rely on `knowledgeVector` in `Init`?
|
|
805
|
+
// The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
|
|
696
806
|
// `SyncSagaMessageInitData_V1` extends `SyncInitData`.
|
|
697
807
|
// `SyncInitData` has `knowledgeVector`.
|
|
698
808
|
// If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
|
|
@@ -707,10 +817,11 @@ export class SyncSagaCoordinator {
|
|
|
707
817
|
deltaReqAddrs,
|
|
708
818
|
pushOfferAddrs,
|
|
709
819
|
knowledgeVector,
|
|
820
|
+
conflicts: conflicts.length > 0 ? conflicts : undefined, // Include conflicts if any detected
|
|
710
821
|
};
|
|
711
822
|
const ackStone = await this.createSyncMsgStone({
|
|
712
823
|
data: ackData,
|
|
713
|
-
space,
|
|
824
|
+
space: tempSpace,
|
|
714
825
|
metaspace,
|
|
715
826
|
});
|
|
716
827
|
if (logalot) {
|
|
@@ -721,12 +832,12 @@ export class SyncSagaCoordinator {
|
|
|
721
832
|
prevSagaIbGib: sagaIbGib,
|
|
722
833
|
msgStones: [ackStone],
|
|
723
834
|
identity,
|
|
724
|
-
space,
|
|
835
|
+
space: tempSpace,
|
|
725
836
|
metaspace,
|
|
726
837
|
});
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
838
|
+
// IMMEDIATELY persist to both spaces for audit trail (before any errors can occur)
|
|
839
|
+
await this.ensureSagaFrameInBothSpaces({ frame: ackFrame, destSpace, tempSpace, metaspace });
|
|
840
|
+
// if (logalot) { console.log(`${lc} ackFrame created: ${pretty(ackFrame)} (I: be24480592eec478086bb3da49286826)`); }
|
|
730
841
|
return { frame: ackFrame };
|
|
731
842
|
}
|
|
732
843
|
/**
|
|
@@ -741,119 +852,318 @@ export class SyncSagaCoordinator {
|
|
|
741
852
|
*
|
|
742
853
|
* Returns a `Delta` frame.
|
|
743
854
|
*/
|
|
744
|
-
async handleAckFrame({ sagaIbGib, srcGraph,
|
|
855
|
+
async handleAckFrame({ sagaIbGib, srcGraph, destSpace, tempSpace, metaspace, identity, }) {
|
|
745
856
|
const lc = `${this.lc}[${this.handleAckFrame.name}]`;
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
750
|
-
const ackData = messageData;
|
|
751
|
-
if (!ackData) {
|
|
752
|
-
throw new Error(`${lc} ackData falsy (E: 3b8415edc876084c88a25b98e2d55826)`);
|
|
753
|
-
}
|
|
754
|
-
if (ackData.stage !== SyncStage.ack) {
|
|
755
|
-
throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
|
|
756
|
-
}
|
|
757
|
-
const deltaReqAddrs = ackData.deltaReqAddrs || [];
|
|
758
|
-
const pushOfferAddrs = ackData.pushOfferAddrs || [];
|
|
759
|
-
// 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
|
|
760
|
-
const pullReqAddrs = [];
|
|
761
|
-
for (const addr of pushOfferAddrs) {
|
|
762
|
-
const existing = srcGraph[addr] || (await getFromSpace({ addr, space })).ibGibs?.[0];
|
|
763
|
-
if (!existing) {
|
|
764
|
-
pullReqAddrs.push(addr);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
// 2. Process Delta Requests (Push Payload)
|
|
768
|
-
// [NEW] Smart Diff: Use knowledgeVector to skip dependencies
|
|
769
|
-
const skipAddrs = new Set();
|
|
770
|
-
if (ackData.knowledgeVector) {
|
|
771
|
-
Object.values(ackData.knowledgeVector).forEach(addrs => {
|
|
772
|
-
addrs.forEach(a => skipAddrs.add(a));
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
const payloadIbGibs = [];
|
|
776
|
-
// Gather all tips to sync first
|
|
777
|
-
const tipsToSync = [];
|
|
778
|
-
for (const addr of deltaReqAddrs) {
|
|
779
|
-
let ibGib = srcGraph[addr];
|
|
780
|
-
if (!ibGib) {
|
|
781
|
-
const res = await getFromSpace({ addr, space });
|
|
782
|
-
if (res.ibGibs && res.ibGibs.length > 0) {
|
|
783
|
-
ibGib = res.ibGibs[0];
|
|
784
|
-
}
|
|
857
|
+
try {
|
|
858
|
+
if (logalot) {
|
|
859
|
+
console.log(`${lc} starting... (I: 605b6860e898267a5b50c6d85704be26)`);
|
|
785
860
|
}
|
|
786
|
-
|
|
787
|
-
|
|
861
|
+
const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
862
|
+
const ackData = messageData;
|
|
863
|
+
if (!ackData) {
|
|
864
|
+
throw new Error(`${lc} ackData falsy (E: 3b8415edc876084c88a25b98e2d55826)`);
|
|
788
865
|
}
|
|
789
|
-
|
|
790
|
-
throw new Error(`${lc}
|
|
866
|
+
if (ackData.stage !== SyncStage.ack) {
|
|
867
|
+
throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
|
|
791
868
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
869
|
+
if (logalot) {
|
|
870
|
+
console.log(`${lc} ackData: ${pretty(ackData)} (I: 7f8e9d0a1b2c3d4e5f6g7h8i9j0k)`);
|
|
871
|
+
}
|
|
872
|
+
// 1. Check for Conflicts
|
|
873
|
+
const conflicts = ackData.conflicts || [];
|
|
874
|
+
console.log(`${lc} [CONFLICT DEBUG] Received conflicts from Ack: ${conflicts.length}`);
|
|
875
|
+
if (conflicts.length > 0) {
|
|
876
|
+
console.log(`${lc} [CONFLICT DEBUG] Conflicts detail: ${JSON.stringify(conflicts, null, 2)}`);
|
|
877
|
+
}
|
|
878
|
+
const terminalConflicts = conflicts.filter(c => c.terminal);
|
|
879
|
+
if (terminalConflicts.length > 0) {
|
|
880
|
+
console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
|
|
881
|
+
// Terminal failure. Sender should probably Commit(Fail) or just Abort.
|
|
882
|
+
// For now, throw to trigger abort.
|
|
883
|
+
throw new Error(`${lc} Peer reported terminal conflicts. (E: a1b2c3d4e5f6g7h8i9j0k)`);
|
|
884
|
+
}
|
|
885
|
+
const optimisticConflicts = conflicts.filter(c => !c.terminal);
|
|
886
|
+
const mergeDeltaReqs = []; // Additional requests for merging
|
|
887
|
+
if (optimisticConflicts.length > 0) {
|
|
888
|
+
console.log(`${lc} [CONFLICT DEBUG] Processing ${optimisticConflicts.length} optimistic conflicts`);
|
|
889
|
+
// We need to resolve these.
|
|
890
|
+
// Strategy:
|
|
891
|
+
// 1. Analyze Divergence (Sender vs Receiver)
|
|
892
|
+
// 2. Identify missing data needed for merge (Receiver's unique frames)
|
|
893
|
+
// 3. Request that data (as Delta Reqs)
|
|
894
|
+
// 4. (Later in Delta Phase) Perform Merge.
|
|
895
|
+
// BUT: The Delta Phase is usually generic "Send me these Addrs".
|
|
896
|
+
// If we just add to `deltaReqAddrs` (which are requests for Sender to send to Receiver?),
|
|
897
|
+
// wait. `ackData.deltaReqAddrs` are what RECEIVER wants from SENDER.
|
|
898
|
+
// We (Sender) are processing the Ack.
|
|
899
|
+
// We need to request data FROM Receiver.
|
|
900
|
+
// But the protocol 'Ack' step typically leads to 'Delta' (Sender sending data).
|
|
901
|
+
// If Sender needs data from Receiver, it usually happens in 'Pull' mode or a separate request?
|
|
902
|
+
// Or can we send a 'Delta Request' frame?
|
|
903
|
+
// Standard Saga: Init(Push) -> Ack(Pull Reqs) -> Delta(Push Data).
|
|
904
|
+
// If Sender needs data, we might need a "Reverse Delta" or "Pull" phase?
|
|
905
|
+
// Or we just proceed to Delta (sending what Receiver wants),
|
|
906
|
+
// AND we piggyback our own requests?
|
|
907
|
+
// OR: We treat the Conflict Resolution as a sub-saga or side-effect?
|
|
908
|
+
// SIMPLIFICATION for V1:
|
|
909
|
+
// If we need data to merge, we must get it.
|
|
910
|
+
// We are the Coordinator (Active). We can fetch from Peer immediately?
|
|
911
|
+
// `peer.pull(addr)`?
|
|
912
|
+
// Yes! The Coordinator has the `peer`.
|
|
913
|
+
// Let's analyze and pull immediately.
|
|
914
|
+
for (const conflict of optimisticConflicts) {
|
|
915
|
+
const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
|
|
916
|
+
// Sender History
|
|
917
|
+
// We need our own history for this timeline.
|
|
918
|
+
// We know the 'senderTip' (remoteAddr in Ack).
|
|
919
|
+
// Sender should verify it has this tip.
|
|
920
|
+
// Compute Diffs
|
|
921
|
+
// We need to find `receiverOnly` addrs.
|
|
922
|
+
// Receiver sent us `timelineAddrs` (Full History).
|
|
923
|
+
const receiverHistorySet = new Set(timelineAddrs);
|
|
924
|
+
// We need our execution context's history for this senderTip.
|
|
925
|
+
// We can fetch valid 'past' from space.
|
|
926
|
+
const resSenderTip = await getFromSpace({ space: destSpace, addr: senderTip });
|
|
927
|
+
const senderTipIbGib = resSenderTip.ibGibs?.[0];
|
|
928
|
+
if (!senderTipIbGib) {
|
|
929
|
+
throw new Error(`${lc} Sender missing its own tip? ${senderTip} (E: 9c8d7e6f5g4h3i2j1k0l)`);
|
|
820
930
|
}
|
|
821
|
-
|
|
822
|
-
|
|
931
|
+
// Basic Diff: Find what Receiver has that we don't.
|
|
932
|
+
// Actually, we need to traverse OUR past to find commonality.
|
|
933
|
+
const senderHistory = [senderTip, ...(senderTipIbGib.rel8ns?.past || [])];
|
|
934
|
+
const receiverOnlyAddrs = timelineAddrs.filter(addr => !senderHistory.includes(addr));
|
|
935
|
+
if (receiverOnlyAddrs.length > 0) {
|
|
936
|
+
console.log(`${lc} [CONFLICT DEBUG] Found ${receiverOnlyAddrs.length} receiver-only frames - need to pull for merge`);
|
|
937
|
+
console.log(`${lc} [CONFLICT DEBUG] Receiver-only addrs:`, receiverOnlyAddrs);
|
|
938
|
+
// PULL these frames from Peer into Local Space
|
|
939
|
+
// (Validation: We trust peer for now / verification happens on put)
|
|
940
|
+
for (const addr of receiverOnlyAddrs) {
|
|
941
|
+
// This 'pull' is a sync-peer method?
|
|
942
|
+
// The Coordinator 'peer' passed in 'sync()' might be needed here?
|
|
943
|
+
// Wait, `handleAckFrame` doesn't have reference to `peer`?
|
|
944
|
+
// It only has `space`, `metaspace`.
|
|
945
|
+
// The `peer` is held by the `executeSagaLoop`.
|
|
946
|
+
// PROBLEM: `handleAckFrame` is pure logic on the Space/Data?
|
|
947
|
+
// No, it's a method on Coordinator.
|
|
948
|
+
// But `executeSagaLoop` calls it.
|
|
949
|
+
// We might need to return "Requirements" to the loop?
|
|
950
|
+
// Checking return type: `{ frame: SyncIbGib_V1, payloadIbGibs?: ... }`
|
|
951
|
+
// It returns the NEXT frame (Delta).
|
|
952
|
+
// If we need to fetch data, we are blocked.
|
|
953
|
+
// We can't easily "Pull" here without the Peer reference.
|
|
954
|
+
// OPTION A: Pass `peer` to `handleAckFrame`.
|
|
955
|
+
// OPTION B: Return a strict list of "MissingDeps" and let Loop handle it.
|
|
956
|
+
// Let's assume we can resolve this by adding `peer` to signature or using `metaspace` if it's a peer-witness?
|
|
957
|
+
// No, Peer is ephemeral connection.
|
|
958
|
+
// Let's add `peer` to `handleAckFrame` signature?
|
|
959
|
+
// It breaks the pattern of just handling frame + space.
|
|
960
|
+
// ALTERNATIVE: Use the `Delta` frame to request data?
|
|
961
|
+
// `SyncSagaMessageDeltaData` has `requests?: string[]`.
|
|
962
|
+
// Sender sends Delta Frame.
|
|
963
|
+
// Does Receiver handle Delta Requests?
|
|
964
|
+
// `handleDeltaFrame` (Receiver) -> checks `requests`.
|
|
965
|
+
// YES.
|
|
966
|
+
// So Sender puts `receiverOnlyAddrs` into `deltaFrame.requests`.
|
|
967
|
+
// Receiver sees them, fetches them, and includes them in the Response (Commit?).
|
|
968
|
+
// Wait, Init->Ack->Delta->Commit.
|
|
969
|
+
// If Receiver sends data in Commit, that's "too late" for Sender to Merge in THIS saga round?
|
|
970
|
+
// Unless Commit is not the end?
|
|
971
|
+
// Or we do a "Delta 2" loop?
|
|
972
|
+
// "Iterative Resolution Loop" from plan.
|
|
973
|
+
// If we request data in Delta, Receiver sends it in Commit (or Delta-Response).
|
|
974
|
+
// Sender gets Commit. Sees data. Merges.
|
|
975
|
+
// Then Sender needs to Send the MERGE result.
|
|
976
|
+
// Needs another Push/Delta.
|
|
977
|
+
// REFINED FLOW:
|
|
978
|
+
// 1. Sender sends Delta Frame with `requests: [receiverOnlyAddrs]`.
|
|
979
|
+
// 2. Receiver responds (Commit? or Ack 2?) with Payload (Divergent Frames).
|
|
980
|
+
// 3. Sender handles response -> Merges.
|
|
981
|
+
// 4. Sender sends Commit (containing Merge Frame).
|
|
982
|
+
// Issue: Current state machine is Init->Ack->Delta->Commit.
|
|
983
|
+
// We need to keep Saga open.
|
|
984
|
+
// If Sender sends Delta with requests, does it transition to Commit?
|
|
985
|
+
}
|
|
986
|
+
// Compute DELTA dependencies for each receiver-only frame
|
|
987
|
+
// Find LCA to determine what dependencies we already have
|
|
988
|
+
const lcaAddr = timelineAddrs.find(addr => senderHistory.includes(addr));
|
|
989
|
+
console.log(`${lc} [CONFLICT DEBUG] LCA: ${lcaAddr || 'NONE'}`);
|
|
990
|
+
const skipAddrsSet = new Set();
|
|
991
|
+
if (lcaAddr) {
|
|
992
|
+
try {
|
|
993
|
+
const lcaRes = await getFromSpace({ addr: lcaAddr, space: destSpace });
|
|
994
|
+
const lcaIbGib = lcaRes.ibGibs?.[0];
|
|
995
|
+
if (lcaIbGib) {
|
|
996
|
+
const lcaDeps = await getDependencyGraph({ ibGib: lcaIbGib, space: destSpace });
|
|
997
|
+
if (lcaDeps)
|
|
998
|
+
Object.keys(lcaDeps).forEach(a => skipAddrsSet.add(a));
|
|
999
|
+
console.log(`${lc} [CONFLICT DEBUG] LCA deps to skip: ${skipAddrsSet.size}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
catch (e) {
|
|
1003
|
+
console.warn(`${lc} Error getting LCA deps: ${extractErrorMsg(e)}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// For each receiver-only frame, get its DELTA dependency graph (minus LCA deps)
|
|
1007
|
+
for (const addr of receiverOnlyAddrs) {
|
|
1008
|
+
// Add the frame itself first
|
|
1009
|
+
if (!mergeDeltaReqs.includes(addr)) {
|
|
1010
|
+
mergeDeltaReqs.push(addr);
|
|
1011
|
+
}
|
|
1012
|
+
// Get the frame's delta dependencies (skip LCA's deps)
|
|
1013
|
+
try {
|
|
1014
|
+
const frameRes = await getFromSpace({ addr, space: destSpace });
|
|
1015
|
+
const frameIbGib = frameRes.ibGibs?.[0];
|
|
1016
|
+
if (frameIbGib) {
|
|
1017
|
+
// Get dependency graph, skipping all LCA dependencies
|
|
1018
|
+
const frameDeltaDeps = await getDependencyGraph({
|
|
1019
|
+
ibGib: frameIbGib,
|
|
1020
|
+
space: destSpace,
|
|
1021
|
+
skipAddrs: Array.from(skipAddrsSet), // Skip entire LCA dep graph
|
|
1022
|
+
});
|
|
1023
|
+
if (frameDeltaDeps) {
|
|
1024
|
+
// Add all delta dependencies (Object.keys gives us the addresses)
|
|
1025
|
+
Object.keys(frameDeltaDeps).forEach(depAddr => {
|
|
1026
|
+
if (!mergeDeltaReqs.includes(depAddr) && !skipAddrsSet.has(depAddr)) {
|
|
1027
|
+
mergeDeltaReqs.push(depAddr);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
catch (depError) {
|
|
1034
|
+
console.warn(`${lc} [CONFLICT DEBUG] Error getting delta deps for ${addr}: ${extractErrorMsg(depError)}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
console.log(`${lc} [CONFLICT DEBUG] Total merge requests (frames + delta deps): ${mergeDeltaReqs.length}`);
|
|
823
1038
|
}
|
|
1039
|
+
else {
|
|
1040
|
+
console.log(`${lc} [CONFLICT DEBUG] No receiver-only frames found for this conflict`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
console.log(`${lc} [CONFLICT DEBUG] Finished processing ${optimisticConflicts.length} conflicts. mergeDeltaReqs: ${mergeDeltaReqs.length}`);
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`);
|
|
1047
|
+
}
|
|
1048
|
+
// 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
|
|
1049
|
+
const deltaReqAddrs = ackData.deltaReqAddrs || [];
|
|
1050
|
+
const pushOfferAddrs = ackData.pushOfferAddrs || [];
|
|
1051
|
+
// 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
|
|
1052
|
+
const pullReqAddrs = [];
|
|
1053
|
+
for (const addr of pushOfferAddrs) {
|
|
1054
|
+
const existing = srcGraph[addr] || (await getFromSpace({ addr, space: destSpace })).ibGibs?.[0];
|
|
1055
|
+
if (!existing) {
|
|
1056
|
+
pullReqAddrs.push(addr);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
// 2. Process Delta Requests (Push Payload)
|
|
1060
|
+
// [NEW] Smart Diff: Use knowledgeVector to skip dependencies
|
|
1061
|
+
const skipAddrs = new Set();
|
|
1062
|
+
if (ackData.knowledgeVector) {
|
|
1063
|
+
Object.values(ackData.knowledgeVector).forEach(addrs => {
|
|
1064
|
+
addrs.forEach(a => skipAddrs.add(a));
|
|
824
1065
|
});
|
|
825
1066
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1067
|
+
const payloadIbGibs = [];
|
|
1068
|
+
// Gather all tips to sync first
|
|
1069
|
+
const tipsToSync = [];
|
|
1070
|
+
for (const addr of deltaReqAddrs) {
|
|
1071
|
+
let ibGib = srcGraph[addr];
|
|
1072
|
+
if (!ibGib) {
|
|
1073
|
+
const res = await getFromSpace({ addr, space: destSpace });
|
|
1074
|
+
if (res.ibGibs && res.ibGibs.length > 0) {
|
|
1075
|
+
ibGib = res.ibGibs[0];
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (ibGib) {
|
|
1079
|
+
tipsToSync.push(ibGib);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// Calculate Dependency Graph for ALL tips, effectively utilizing common history
|
|
1086
|
+
// Pass skipAddrs to `getDependencyGraph` or gather manually.
|
|
1087
|
+
// `getDependencyGraph` takes a single ibGib.
|
|
1088
|
+
// We can optimize by doing it for each tip and unioning the result?
|
|
1089
|
+
// Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
|
|
1090
|
+
// We will loop.
|
|
1091
|
+
const allDepsSet = new Set();
|
|
1092
|
+
for (const tip of tipsToSync) {
|
|
1093
|
+
// Always include the tip itself
|
|
1094
|
+
const tipAddr = getIbGibAddr({ ibGib: tip });
|
|
1095
|
+
// Only process if not skipped (though deltaReq implies they barely just asked for it)
|
|
1096
|
+
// But detailed deps might be skipped.
|
|
1097
|
+
// Get Graph with Skips
|
|
1098
|
+
// Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
|
|
1099
|
+
const deps = await getDependencyGraph({
|
|
1100
|
+
ibGib: tip,
|
|
1101
|
+
space: destSpace,
|
|
1102
|
+
skipAddrs: Array.from(skipAddrs)
|
|
1103
|
+
});
|
|
1104
|
+
// [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
|
|
1105
|
+
let tipIncluded = false;
|
|
1106
|
+
if (deps) {
|
|
1107
|
+
Object.values(deps).forEach(d => {
|
|
1108
|
+
const dAddr = getIbGibAddr({ ibGib: d });
|
|
1109
|
+
if (!allDepsSet.has(dAddr)) {
|
|
1110
|
+
allDepsSet.add(dAddr);
|
|
1111
|
+
payloadIbGibs.push(d);
|
|
1112
|
+
}
|
|
1113
|
+
if (dAddr === tipAddr) {
|
|
1114
|
+
tipIncluded = true;
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
829
1117
|
}
|
|
830
|
-
if (!
|
|
831
|
-
|
|
832
|
-
|
|
1118
|
+
if (!tipIncluded && !skipAddrs.has(tipAddr)) {
|
|
1119
|
+
if (logalot) {
|
|
1120
|
+
console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`);
|
|
1121
|
+
}
|
|
1122
|
+
if (!allDepsSet.has(tipAddr)) {
|
|
1123
|
+
allDepsSet.add(tipAddr);
|
|
1124
|
+
payloadIbGibs.push(tip);
|
|
1125
|
+
}
|
|
833
1126
|
}
|
|
834
1127
|
}
|
|
1128
|
+
// 3. Create Delta Frame
|
|
1129
|
+
const sagaId = ackData.sagaId;
|
|
1130
|
+
const deltaData = {
|
|
1131
|
+
sagaId: sagaIbGib.data.uuid,
|
|
1132
|
+
stage: SyncStage.delta,
|
|
1133
|
+
payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
|
|
1134
|
+
requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
|
|
1135
|
+
};
|
|
1136
|
+
if (logalot) {
|
|
1137
|
+
console.log(`${lc} Creating Delta Stone. Data stage: ${deltaData.stage}`);
|
|
1138
|
+
}
|
|
1139
|
+
const deltaStone = await this.createSyncMsgStone({
|
|
1140
|
+
data: deltaData,
|
|
1141
|
+
space: tempSpace,
|
|
1142
|
+
metaspace,
|
|
1143
|
+
});
|
|
1144
|
+
const deltaFrame = await this.evolveSyncSagaIbGib({
|
|
1145
|
+
prevSagaIbGib: sagaIbGib,
|
|
1146
|
+
msgStones: [deltaStone],
|
|
1147
|
+
identity,
|
|
1148
|
+
space: tempSpace,
|
|
1149
|
+
metaspace,
|
|
1150
|
+
});
|
|
1151
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1152
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
1153
|
+
if (logalot) {
|
|
1154
|
+
console.log(`${lc} Delta Frame created. Rel8ns: ${JSON.stringify(deltaFrame.rel8ns)}`);
|
|
1155
|
+
}
|
|
1156
|
+
return { frame: deltaFrame, payloadIbGibs };
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
finally {
|
|
1163
|
+
if (logalot) {
|
|
1164
|
+
console.log(`${lc} complete.`);
|
|
1165
|
+
}
|
|
835
1166
|
}
|
|
836
|
-
// 3. Create Delta Frame
|
|
837
|
-
const sagaId = ackData.sagaId;
|
|
838
|
-
const deltaData = {
|
|
839
|
-
sagaId,
|
|
840
|
-
stage: SyncStage.delta,
|
|
841
|
-
payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
|
|
842
|
-
requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
|
|
843
|
-
};
|
|
844
|
-
const deltaStone = await this.createSyncMsgStone({
|
|
845
|
-
data: deltaData,
|
|
846
|
-
space,
|
|
847
|
-
metaspace,
|
|
848
|
-
});
|
|
849
|
-
const deltaFrame = await this.evolveSyncSagaIbGib({
|
|
850
|
-
prevSagaIbGib: sagaIbGib,
|
|
851
|
-
msgStones: [deltaStone],
|
|
852
|
-
identity,
|
|
853
|
-
space,
|
|
854
|
-
metaspace,
|
|
855
|
-
});
|
|
856
|
-
return { frame: deltaFrame, payloadIbGibs };
|
|
857
1167
|
}
|
|
858
1168
|
/**
|
|
859
1169
|
* Handles the `Delta` frame.
|
|
@@ -865,12 +1175,12 @@ export class SyncSagaCoordinator {
|
|
|
865
1175
|
* 2. **Fulfillment**: Checks `requests`. If Peer requested data, gathers it and prepares `outgoingPayload`.
|
|
866
1176
|
* 3. **Completion**: If no more requests, transitions to `Commit`.
|
|
867
1177
|
*/
|
|
868
|
-
async handleDeltaFrame({ sagaIbGib, srcGraph,
|
|
1178
|
+
async handleDeltaFrame({ sagaIbGib, srcGraph, destSpace, tempSpace, metaspace, identity, }) {
|
|
869
1179
|
const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
|
|
870
1180
|
if (logalot) {
|
|
871
1181
|
console.log(`${lc} starting...`);
|
|
872
1182
|
}
|
|
873
|
-
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
|
|
1183
|
+
const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
|
|
874
1184
|
const deltaData = messageData;
|
|
875
1185
|
if (!deltaData) {
|
|
876
1186
|
throw new Error(`${lc} deltaData falsy (E: 7c28c8d8f08a4421b8344e6727271421)`);
|
|
@@ -878,116 +1188,363 @@ export class SyncSagaCoordinator {
|
|
|
878
1188
|
if (deltaData.stage !== SyncStage.delta) {
|
|
879
1189
|
throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
|
|
880
1190
|
}
|
|
1191
|
+
if (logalot) {
|
|
1192
|
+
console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`);
|
|
1193
|
+
}
|
|
1194
|
+
console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`);
|
|
881
1195
|
const payloadAddrs = deltaData.payloadAddrs || [];
|
|
882
|
-
const
|
|
883
|
-
|
|
1196
|
+
const peerRequests = deltaData.requests || [];
|
|
1197
|
+
const peerProposesCommit = deltaData.proposeCommit || false;
|
|
1198
|
+
// 1. Process Received Payload (Ingest)
|
|
884
1199
|
const receivedPayloadIbGibs = [];
|
|
885
1200
|
if (payloadAddrs.length > 0) {
|
|
1201
|
+
// We use `payloadAddrs` as the manifest.
|
|
1202
|
+
// The ACTUAL collection of ibGibs should be available via `getFromSpace`
|
|
1203
|
+
// assuming the "Transport" layer put them there implicitly?
|
|
1204
|
+
// OR, if we are local-only, we just get them.
|
|
1205
|
+
// The `handleDeltaFrame` contract assumes data is reachable in `space`.
|
|
886
1206
|
const res = await getFromSpace({
|
|
887
1207
|
addrs: payloadAddrs,
|
|
888
|
-
space,
|
|
1208
|
+
space: tempSpace, // Incoming data is in tempSpace
|
|
889
1209
|
});
|
|
890
1210
|
if (res.ibGibs) {
|
|
891
1211
|
receivedPayloadIbGibs.push(...res.ibGibs);
|
|
1212
|
+
// Also put them? `getFromSpace` retrieves. If they are in space, they are persisted.
|
|
1213
|
+
// If this is a Temp Space, they are safe.
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
console.warn(`${lc} Failed to retrieve payloads listed in delta: ${payloadAddrs.join(', ')}`);
|
|
892
1217
|
}
|
|
893
1218
|
}
|
|
894
|
-
// 2. Fulfill Requests (Outgoing Payload)
|
|
1219
|
+
// 2. Fulfill Peer Requests (Outgoing Payload with Delta Dependencies)
|
|
895
1220
|
const outgoingPayload = [];
|
|
896
|
-
|
|
1221
|
+
const outgoingAddrsSet = new Set(); // Track what we've added
|
|
1222
|
+
console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${peerRequests.length} peer requests`);
|
|
1223
|
+
for (const addr of peerRequests) {
|
|
1224
|
+
// Get the requested ibGib
|
|
897
1225
|
let ibGib = srcGraph[addr];
|
|
898
1226
|
if (!ibGib) {
|
|
899
|
-
const res = await getFromSpace({ addr, space });
|
|
1227
|
+
const res = await getFromSpace({ addr, space: destSpace }); // Query from destSpace
|
|
900
1228
|
if (res.ibGibs && res.ibGibs.length > 0) {
|
|
901
1229
|
ibGib = res.ibGibs[0];
|
|
902
1230
|
}
|
|
903
1231
|
}
|
|
904
1232
|
if (ibGib) {
|
|
905
|
-
|
|
1233
|
+
// Add the requested ibGib itself
|
|
1234
|
+
const ibGibAddr = getIbGibAddr({ ibGib });
|
|
1235
|
+
if (!outgoingAddrsSet.has(ibGibAddr)) {
|
|
1236
|
+
outgoingPayload.push(ibGib);
|
|
1237
|
+
outgoingAddrsSet.add(ibGibAddr);
|
|
1238
|
+
}
|
|
1239
|
+
// Expand to include full dependency graph for this ibGib
|
|
1240
|
+
// (Receiver needs all deps to properly process/merge)
|
|
1241
|
+
try {
|
|
1242
|
+
const deps = await getDependencyGraph({
|
|
1243
|
+
ibGib,
|
|
1244
|
+
space: destSpace,
|
|
1245
|
+
});
|
|
1246
|
+
if (deps) {
|
|
1247
|
+
Object.values(deps).forEach(depIbGib => {
|
|
1248
|
+
const depAddr = getIbGibAddr({ ibGib: depIbGib });
|
|
1249
|
+
if (!outgoingAddrsSet.has(depAddr)) {
|
|
1250
|
+
outgoingPayload.push(depIbGib);
|
|
1251
|
+
outgoingAddrsSet.add(depAddr);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
catch (depError) {
|
|
1257
|
+
console.warn(`${lc} [CONFLICT DEBUG] Error expanding deps for ${addr}: ${extractErrorMsg(depError)}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
|
|
906
1262
|
}
|
|
907
1263
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1264
|
+
console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`);
|
|
1265
|
+
// 3. Execute Merges (If applicable)
|
|
1266
|
+
// Check if we have pending conflicts that we CAN resolve now that we have data.
|
|
1267
|
+
// We look at the Saga History (Ack Frame) to find conflicts.
|
|
1268
|
+
// Optimization: Do this only if we received payloads.
|
|
1269
|
+
const mergeResultIbGibs = [];
|
|
1270
|
+
console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`);
|
|
1271
|
+
if (receivedPayloadIbGibs.length > 0) {
|
|
1272
|
+
console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`);
|
|
1273
|
+
// Find the Ack frame in history to get conflicts
|
|
1274
|
+
// Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
|
|
1275
|
+
// V1 timelines carry full history in `past`.
|
|
1276
|
+
const pastAddrs = sagaIbGib.rel8ns?.past || [];
|
|
1277
|
+
console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`);
|
|
1278
|
+
let ackData;
|
|
1279
|
+
if (pastAddrs.length > 0) {
|
|
1280
|
+
// Batch fetch all past frames
|
|
1281
|
+
const resPast = await getFromSpace({ addrs: pastAddrs, space: tempSpace });
|
|
1282
|
+
if (resPast.success && resPast.ibGibs) {
|
|
1283
|
+
// Iterate backwards (most recent first) to find the latest Ack
|
|
1284
|
+
for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
|
|
1285
|
+
const pastFrame = resPast.ibGibs[i];
|
|
1286
|
+
const messageStone = await getSyncSagaMessageFromFrame({
|
|
1287
|
+
frameIbGib: pastFrame,
|
|
1288
|
+
space: tempSpace
|
|
1289
|
+
});
|
|
1290
|
+
if (messageStone?.data?.stage === SyncStage.ack) {
|
|
1291
|
+
ackData = messageStone.data;
|
|
1292
|
+
console.log(`${lc} [TEST DEBUG] Found Ack Frame. Conflicts: ${ackData.conflicts?.length || 0}`);
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (ackData && ackData.conflicts) {
|
|
1299
|
+
const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
|
|
1300
|
+
for (const conflict of optimisticConflicts) {
|
|
1301
|
+
const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
|
|
1302
|
+
// We are Sender (usually) here if we are merging.
|
|
1303
|
+
// Check if we have the history needed (timelineAddrs).
|
|
1304
|
+
// Specifically, we needed the `receiverOnly` parts.
|
|
1305
|
+
// We blindly attempt merge if we have both tips accessible?
|
|
1306
|
+
// We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
|
|
1307
|
+
// Check if we have receiverTip in space
|
|
1308
|
+
console.log(`${lc} [CONFLICT DEBUG] Attempting merge for conflict. ReceiverTip: ${receiverTip}, SenderTip: ${senderTip}`);
|
|
1309
|
+
const resRecTip = await getFromSpace({ addr: receiverTip, space: tempSpace }); // Check tempSpace for incoming data
|
|
1310
|
+
console.log(`${lc} [CONFLICT DEBUG] ReceiverTip found in tempSpace: ${!!resRecTip.ibGibs?.[0]}`);
|
|
1311
|
+
if (resRecTip.success && resRecTip.ibGibs?.[0]) {
|
|
1312
|
+
// We have the tip!
|
|
1313
|
+
// Do we have the full history?
|
|
1314
|
+
// `mergeDivergentTimelines` in `conflict-optimistic` will attempt to fetch history.
|
|
1315
|
+
// If we just ingested the missing pieces, `getFromSpace` inside `merge` should succeed.
|
|
1316
|
+
// Perform Merge!
|
|
1317
|
+
try {
|
|
1318
|
+
const mergeResult = await mergeDivergentTimelines({
|
|
1319
|
+
tipA: (await getFromSpace({ addr: senderTip, space: destSpace })).ibGibs[0], // Our tip from destSpace
|
|
1320
|
+
tipB: resRecTip.ibGibs[0], // Their tip (from tempSpace)
|
|
1321
|
+
space: tempSpace, // Merge uses tempSpace
|
|
1322
|
+
metaspace,
|
|
1323
|
+
});
|
|
1324
|
+
if (mergeResult) {
|
|
1325
|
+
console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
|
|
1326
|
+
if (logalot) {
|
|
1327
|
+
console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
|
|
1328
|
+
}
|
|
1329
|
+
mergeResultIbGibs.push(mergeResult);
|
|
1330
|
+
outgoingPayload.push(mergeResult); // Send result to peer
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
catch (e) {
|
|
1334
|
+
console.error(`${lc} Merge failed: ${e}`);
|
|
1335
|
+
// If merge fails, we might Abort or just continue?
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// 4. Determine Next Action
|
|
1342
|
+
// We have `outgoingPayload` (Requests + Merge Results).
|
|
1343
|
+
// Does Peer have outstanding requests? No, we fulfilled `peerRequests`.
|
|
1344
|
+
// Do WE have outstanding requests?
|
|
1345
|
+
// We might if `mergeResult` requires further sync? Usually no, result is complete.
|
|
1346
|
+
const myRequests = []; // If we had more needs (e.g. partial payload), we'd add here.
|
|
1347
|
+
const hasOutgoing = outgoingPayload.length > 0;
|
|
1348
|
+
const hasMyRequests = myRequests.length > 0;
|
|
1349
|
+
if (hasOutgoing || hasMyRequests) {
|
|
1350
|
+
// We have business to attend to -> Send Delta
|
|
912
1351
|
const responseDeltaData = {
|
|
913
|
-
sagaId,
|
|
1352
|
+
sagaId: deltaData.sagaId,
|
|
914
1353
|
stage: SyncStage.delta,
|
|
915
1354
|
payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
|
|
1355
|
+
requests: hasMyRequests ? myRequests : undefined,
|
|
1356
|
+
proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
|
|
1357
|
+
// Wait. If we send data, we are NOT committing yet.
|
|
1358
|
+
// We are sending data. The OTHER side must ingest it.
|
|
1359
|
+
// So proposeCommit = true?
|
|
1360
|
+
// "Here is the data. I'm done. If you are good, let's commit."
|
|
1361
|
+
// Yes.
|
|
916
1362
|
};
|
|
1363
|
+
// BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
|
|
1364
|
+
// We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
|
|
1365
|
+
// So yes, proposeCommit = true.
|
|
1366
|
+
responseDeltaData.proposeCommit = true;
|
|
917
1367
|
const deltaStone = await this.createSyncMsgStone({
|
|
918
1368
|
data: responseDeltaData,
|
|
919
|
-
space,
|
|
1369
|
+
space: tempSpace,
|
|
920
1370
|
metaspace
|
|
921
1371
|
});
|
|
922
1372
|
const deltaFrame = await this.evolveSyncSagaIbGib({
|
|
923
1373
|
prevSagaIbGib: sagaIbGib,
|
|
924
1374
|
msgStones: [deltaStone],
|
|
925
1375
|
identity,
|
|
926
|
-
space,
|
|
1376
|
+
space: tempSpace,
|
|
927
1377
|
metaspace
|
|
928
1378
|
});
|
|
1379
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1380
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
929
1381
|
return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
|
|
930
1382
|
}
|
|
931
1383
|
else {
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1384
|
+
// We have nothing to send.
|
|
1385
|
+
if (peerProposesCommit) {
|
|
1386
|
+
// Peer is done. We are done. -> Commit.
|
|
1387
|
+
const commitData = {
|
|
1388
|
+
sagaId: deltaData.sagaId,
|
|
1389
|
+
stage: SyncStage.commit,
|
|
1390
|
+
success: true,
|
|
1391
|
+
};
|
|
1392
|
+
const commitStone = await this.createSyncMsgStone({
|
|
1393
|
+
data: commitData,
|
|
1394
|
+
space: tempSpace,
|
|
1395
|
+
metaspace
|
|
1396
|
+
});
|
|
1397
|
+
const commitFrame = await this.evolveSyncSagaIbGib({
|
|
1398
|
+
prevSagaIbGib: sagaIbGib,
|
|
1399
|
+
msgStones: [commitStone],
|
|
1400
|
+
identity,
|
|
1401
|
+
space: tempSpace,
|
|
1402
|
+
metaspace
|
|
1403
|
+
});
|
|
1404
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1405
|
+
await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
|
|
1406
|
+
return { frame: commitFrame, receivedPayloadIbGibs };
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
// peer did NOT propose commit (maybe they just sent data/requests and didn't ready flag).
|
|
1410
|
+
// But we are empty.
|
|
1411
|
+
// So WE propose commit.
|
|
1412
|
+
const responseDeltaData = {
|
|
1413
|
+
sagaId: deltaData.sagaId,
|
|
1414
|
+
stage: SyncStage.delta,
|
|
1415
|
+
proposeCommit: true,
|
|
1416
|
+
payloadAddrs: [], // Always include empty array if sending delta
|
|
1417
|
+
};
|
|
1418
|
+
const deltaStone = await this.createSyncMsgStone({
|
|
1419
|
+
data: responseDeltaData,
|
|
1420
|
+
space: tempSpace,
|
|
1421
|
+
metaspace
|
|
1422
|
+
});
|
|
1423
|
+
const deltaFrame = await this.evolveSyncSagaIbGib({
|
|
1424
|
+
prevSagaIbGib: sagaIbGib,
|
|
1425
|
+
msgStones: [deltaStone],
|
|
1426
|
+
identity,
|
|
1427
|
+
space: tempSpace,
|
|
1428
|
+
metaspace
|
|
1429
|
+
});
|
|
1430
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1431
|
+
await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
|
|
1432
|
+
// Check if PEER proposed commit
|
|
1433
|
+
if (deltaData.proposeCommit) {
|
|
1434
|
+
if (logalot) {
|
|
1435
|
+
console.log(`${lc} Peer proposed commit. Accepting & Committing.`);
|
|
1436
|
+
}
|
|
1437
|
+
// Peer wants to commit and has no more requests.
|
|
1438
|
+
// We should Commit.
|
|
1439
|
+
const commitData = {
|
|
1440
|
+
sagaId: deltaData.sagaId,
|
|
1441
|
+
stage: SyncStage.commit,
|
|
1442
|
+
success: true,
|
|
1443
|
+
};
|
|
1444
|
+
const commitStone = await this.createSyncMsgStone({
|
|
1445
|
+
data: commitData,
|
|
1446
|
+
space: tempSpace,
|
|
1447
|
+
metaspace
|
|
1448
|
+
});
|
|
1449
|
+
const commitFrame = await this.evolveSyncSagaIbGib({
|
|
1450
|
+
prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
|
|
1451
|
+
msgStones: [commitStone],
|
|
1452
|
+
identity,
|
|
1453
|
+
space: tempSpace,
|
|
1454
|
+
metaspace
|
|
1455
|
+
});
|
|
1456
|
+
// IMMEDIATELY persist to both spaces for audit trail
|
|
1457
|
+
await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
|
|
1458
|
+
return { frame: commitFrame, receivedPayloadIbGibs };
|
|
1459
|
+
}
|
|
1460
|
+
return { frame: deltaFrame, receivedPayloadIbGibs };
|
|
1461
|
+
}
|
|
952
1462
|
}
|
|
953
1463
|
}
|
|
954
|
-
async handleCommitFrame({ sagaIbGib,
|
|
1464
|
+
async handleCommitFrame({ sagaIbGib, destSpace, tempSpace, metaspace, identity, }) {
|
|
955
1465
|
const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
|
|
956
1466
|
if (logalot) {
|
|
957
|
-
console.log(`${lc} Commit received
|
|
1467
|
+
console.log(`${lc} Commit received.`);
|
|
958
1468
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1469
|
+
// Sender Logic (Finalizing):
|
|
1470
|
+
// If we are here, we received a Commit frame from the Peer.
|
|
1471
|
+
// This implies the Peer has successfully committed.
|
|
1472
|
+
// We should now:
|
|
1473
|
+
// 1. Validate (implicitly done by receiving valid frame)
|
|
1474
|
+
// 2. Perform our own cleanup (Temp -> Dest, if applicable)
|
|
1475
|
+
// 3. Return null to signal saga completion.
|
|
1476
|
+
// Note: Currently we don't have explicit cleanup logic implemented here yet (TODO).
|
|
965
1477
|
if (logalot) {
|
|
966
|
-
console.log(`${lc}
|
|
967
|
-
}
|
|
968
|
-
if (conflictData?.isTerminal) {
|
|
969
|
-
throw new Error(`${lc} Saga aborted due to conflicts: ${JSON.stringify(conflictData.conflicts)} (E: b08d1f2a3c4e5d6f7a8b9c0d1e2f3a4b)`);
|
|
1478
|
+
console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`);
|
|
970
1479
|
}
|
|
971
|
-
// Non-terminal logic (stub for future)
|
|
972
1480
|
return null;
|
|
973
1481
|
}
|
|
974
1482
|
// #endregion Handlers
|
|
975
1483
|
async createSyncMsgStone({ data, space, metaspace, }) {
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1484
|
+
const lc = `${this.lc}[${this.createSyncMsgStone.name}]`;
|
|
1485
|
+
try {
|
|
1486
|
+
if (logalot) {
|
|
1487
|
+
console.log(`${lc} starting... (I: 5f7f98e8ff980364f7191fcee4531e26)`);
|
|
1488
|
+
}
|
|
1489
|
+
const ib = await getSyncSagaMessageIb({ data });
|
|
1490
|
+
const stone = await Factory_V1.stone({
|
|
1491
|
+
ib,
|
|
1492
|
+
parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
|
|
1493
|
+
data,
|
|
1494
|
+
uuid: true, // we want the stone to have its own uniqueness
|
|
1495
|
+
});
|
|
1496
|
+
if (logalot) {
|
|
1497
|
+
console.log(`${lc} Created stone: ${getIbGibAddr({ ibGib: stone })}`);
|
|
1498
|
+
}
|
|
1499
|
+
await putInSpace({ space, ibGib: stone });
|
|
1500
|
+
await metaspace.registerNewIbGib({ ibGib: stone });
|
|
1501
|
+
return stone;
|
|
1502
|
+
}
|
|
1503
|
+
catch (error) {
|
|
1504
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
1505
|
+
throw error;
|
|
1506
|
+
}
|
|
1507
|
+
finally {
|
|
1508
|
+
if (logalot) {
|
|
1509
|
+
console.log(`${lc} complete.`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Ensures saga frame and its msg stone(s) are in BOTH spaces for audit trail.
|
|
1515
|
+
* Control ibgibs (saga frames, msg stones, identity) must be in both destSpace and tempSpace.
|
|
1516
|
+
*/
|
|
1517
|
+
async ensureSagaFrameInBothSpaces({ frame, destSpace, tempSpace, metaspace, }) {
|
|
1518
|
+
// Frame itself (already in tempSpace from creation, need in destSpace for audit)
|
|
1519
|
+
await putInSpace({ space: destSpace, ibGib: frame });
|
|
1520
|
+
await metaspace.registerNewIbGib({ ibGib: frame });
|
|
1521
|
+
// Msg stone(s) (already in tempSpace, need in destSpace for audit)
|
|
1522
|
+
const msgStoneAddrs = frame.rel8ns?.[SYNC_MSG_REL8N_NAME];
|
|
1523
|
+
if (msgStoneAddrs && msgStoneAddrs.length > 0) {
|
|
1524
|
+
const resMsgStones = await getFromSpace({ space: tempSpace, addrs: msgStoneAddrs });
|
|
1525
|
+
if (resMsgStones.ibGibs) {
|
|
1526
|
+
for (const msgStone of resMsgStones.ibGibs) {
|
|
1527
|
+
await putInSpace({ space: destSpace, ibGib: msgStone });
|
|
1528
|
+
await metaspace.registerNewIbGib({ ibGib: msgStone });
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
// Identity (if present, already in tempSpace, need in destSpace)
|
|
1533
|
+
const identityAddrs = frame.rel8ns?.identity;
|
|
1534
|
+
if (identityAddrs && identityAddrs.length > 0) {
|
|
1535
|
+
const resIdentity = await getFromSpace({ space: tempSpace, addrs: identityAddrs });
|
|
1536
|
+
if (resIdentity.ibGibs) {
|
|
1537
|
+
for (const identity of resIdentity.ibGibs) {
|
|
1538
|
+
await putInSpace({ space: destSpace, ibGib: identity });
|
|
1539
|
+
await metaspace.registerNewIbGib({ ibGib: identity });
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
986
1543
|
}
|
|
987
1544
|
/**
|
|
988
1545
|
* Evolves the saga timeline with a new frame.
|
|
989
1546
|
*/
|
|
990
|
-
async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, }) {
|
|
1547
|
+
async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, conflictStrategy, }) {
|
|
991
1548
|
const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
|
|
992
1549
|
try {
|
|
993
1550
|
// Validation
|
|
@@ -1058,6 +1615,7 @@ export class SyncSagaCoordinator {
|
|
|
1058
1615
|
payload: undefined, // Data in stone
|
|
1059
1616
|
n: 0,
|
|
1060
1617
|
isTjp: true,
|
|
1618
|
+
conflictStrategy,
|
|
1061
1619
|
};
|
|
1062
1620
|
const ib = await getSyncIb({ data });
|
|
1063
1621
|
const stoneAddrs = msgStones.map(s => getIbGibAddr({ ibGib: s }));
|