@ibgib/core-gib 0.1.27 → 0.1.29
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/meta-stone/meta-stone-helper.d.mts.map +1 -1
- package/dist/common/meta-stone/meta-stone-helper.mjs +19 -7
- package/dist/common/meta-stone/meta-stone-helper.mjs.map +1 -1
- package/dist/sync/sync-helpers.d.mts +11 -5
- package/dist/sync/sync-helpers.d.mts.map +1 -1
- package/dist/sync/sync-helpers.mjs +33 -9
- package/dist/sync/sync-helpers.mjs.map +1 -1
- package/dist/sync/sync-innerspace-constants.respec.mjs +36 -36
- package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs +5 -3
- package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs +2 -2
- package/dist/sync/sync-innerspace-multiple-timelines.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace-partial-update.respec.mjs +3 -3
- package/dist/sync/sync-innerspace-partial-update.respec.mjs.map +1 -1
- package/dist/sync/sync-innerspace.respec.mjs +49 -20
- package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.d.mts +0 -9
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.d.mts.map +1 -1
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs +13 -42
- package/dist/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-v1.mjs +2 -2
- package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -1
- package/dist/sync/sync-saga-coordinator.d.mts +23 -159
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +508 -172
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
- package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +12 -17
- package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -1
- package/dist/sync/sync-types.d.mts +18 -23
- package/dist/sync/sync-types.d.mts.map +1 -1
- package/dist/sync/sync-types.mjs +15 -21
- package/dist/sync/sync-types.mjs.map +1 -1
- package/package.json +1 -1
- package/src/common/meta-stone/meta-stone-helper.mts +17 -7
- package/src/sync/sync-helpers.mts +36 -13
- package/src/sync/sync-innerspace-constants.respec.mts +39 -39
- package/src/sync/sync-innerspace-deep-updates.respec.mts +6 -6
- package/src/sync/sync-innerspace-multiple-timelines.respec.mts +1 -1
- package/src/sync/sync-innerspace-partial-update.respec.mts +2 -2
- package/src/sync/sync-innerspace.respec.mts +20 -19
- package/src/sync/sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mts +11 -58
- package/src/sync/sync-peer/sync-peer-v1.mts +2 -2
- package/src/sync/sync-saga-coordinator.mts +472 -191
- package/src/sync/sync-saga-message/sync-saga-message-types.mts +13 -18
- package/src/sync/sync-types.mts +26 -33
- package/test_output.log +0 -0
- package/tmp.md +170 -62
|
@@ -29,10 +29,9 @@ import {
|
|
|
29
29
|
SyncData_V1, SyncIbGib_V1, SyncConflictStrategy, SyncMode, SyncOptions,
|
|
30
30
|
SyncRel8ns_V1, DomainIbGibAnalysisInfo, NextSagaFrameInfo,
|
|
31
31
|
SYNC_CONFLICT_STRATEGY_VALID_VALUES, HandleSagaResponseContextResult,
|
|
32
|
-
|
|
33
|
-
SYNC_EXECUTION_CONTEXT_VALID_VALUES,
|
|
32
|
+
SyncSagaFrameDependencyGraph,
|
|
34
33
|
} from "./sync-types.mjs";
|
|
35
|
-
import {
|
|
34
|
+
import { getSyncSagaFrameOrigin, getFullSyncSagaHistory, getSyncIb, getTempSpaceName, isPastFrame, putInSpace_dnasThenNonDnas, validateFullSyncSagaHistory } from "./sync-helpers.mjs";
|
|
36
35
|
import { getDeltaDependencyGraph, getDependencyGraph, toFlatGraph } from "../common/other/graph-helper.mjs";
|
|
37
36
|
import {
|
|
38
37
|
SyncSagaMessageData_V1, SyncSagaMessageInitData_V1,
|
|
@@ -57,9 +56,8 @@ import { IbGibSpaceResultData, IbGibSpaceResultIbGib, IbGibSpaceResultRel8ns } f
|
|
|
57
56
|
import { FlatIbGibGraph } from "../common/other/graph-types.mjs";
|
|
58
57
|
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const logalotControlDomain = true;
|
|
59
|
+
const logalot = GLOBAL_LOG_A_LOT;
|
|
60
|
+
const logalotControlDomain = false;
|
|
63
61
|
const lcControlDomain = '[ControlDomain]';
|
|
64
62
|
|
|
65
63
|
/**
|
|
@@ -76,10 +74,10 @@ const lcControlDomain = '[ControlDomain]';
|
|
|
76
74
|
* to a specific Saga session, not fixed node identities.
|
|
77
75
|
*/
|
|
78
76
|
export class SyncSagaCoordinator {
|
|
79
|
-
|
|
77
|
+
private lc: string = `[${SyncSagaCoordinator.name}]`;
|
|
80
78
|
|
|
81
79
|
constructor(
|
|
82
|
-
|
|
80
|
+
private keystone: KeystoneService_V1
|
|
83
81
|
) {
|
|
84
82
|
|
|
85
83
|
}
|
|
@@ -240,7 +238,11 @@ export class SyncSagaCoordinator {
|
|
|
240
238
|
|
|
241
239
|
if (!contextResult) {
|
|
242
240
|
if (logalot) { console.log(`${lc} Handler returned null (Saga End). (I: 43da8bb6c846b1fe7766332643be0e26)`); }
|
|
243
|
-
|
|
241
|
+
// does this ever hit now?
|
|
242
|
+
return null; /* <<<< returns early */
|
|
243
|
+
} else if (contextResult.nextFrameInfo?.sagaComplete) {
|
|
244
|
+
// this is the current proper workflow I believe as of 01/22/2026
|
|
245
|
+
return null; /* <<<< returns early */
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
// #region error conditions throw
|
|
@@ -248,8 +250,6 @@ export class SyncSagaCoordinator {
|
|
|
248
250
|
throw new Error(`Couldn't handle response saga context. errorMsg: ${contextResult.errorMsg} (E: 7b41a183cf3cb58a5859c803800cf826)`);
|
|
249
251
|
} else if (!contextResult.nextFrameInfo) {
|
|
250
252
|
throw new Error(`(UNEXPECTED) contextResult.nextFrameInfo falsy? (E: 5740542f5eb8ccb41dfec188d87c1e26)`);
|
|
251
|
-
} else if (contextResult.nextFrameInfo?.responseWasNull) {
|
|
252
|
-
throw new Error(`(UNEXPECTED) contextResult.nextFrameInfo.responseWasNull? logic flow should not have gotten here. (E: ae06748d8c0c5e70c92322c8fb0cb426)`);
|
|
253
253
|
}
|
|
254
254
|
// #endregion error conditions throw
|
|
255
255
|
|
|
@@ -278,7 +278,7 @@ export class SyncSagaCoordinator {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
private async getSessionIdentity({
|
|
282
282
|
sagaId,
|
|
283
283
|
metaspace,
|
|
284
284
|
tempSpace,
|
|
@@ -329,7 +329,7 @@ export class SyncSagaCoordinator {
|
|
|
329
329
|
* the NEXT request context.
|
|
330
330
|
* When the Peer responds with data (in the response context), it is resolved and put into `tempSpace`.
|
|
331
331
|
*/
|
|
332
|
-
|
|
332
|
+
private async executeSagaLoop({
|
|
333
333
|
initFrame,
|
|
334
334
|
initDomainGraph,
|
|
335
335
|
peer,
|
|
@@ -502,7 +502,11 @@ export class SyncSagaCoordinator {
|
|
|
502
502
|
});
|
|
503
503
|
|
|
504
504
|
if (!contextResult) {
|
|
505
|
-
|
|
505
|
+
// should this ever hit?
|
|
506
|
+
console.error(`${lc} NAG ERROR (DOES NOT THROW): does this ever hit now? (E: e04d02efc2a8e72a88b79f1f0f95ca26)`);
|
|
507
|
+
break;
|
|
508
|
+
} else if (contextResult.nextFrameInfo?.sagaComplete) {
|
|
509
|
+
if (logalot) { console.log(`${lc} Handler returned null (Saga End). (I: 123bf9e7dca8886de72553a8d4f29e26)`); }
|
|
506
510
|
break;
|
|
507
511
|
}
|
|
508
512
|
|
|
@@ -511,8 +515,6 @@ export class SyncSagaCoordinator {
|
|
|
511
515
|
throw new Error(`Couldn't handle response saga context. errorMsg: ${contextResult.errorMsg} (E: c948e81d513b2a0eb8b8afa878edc626)`);
|
|
512
516
|
} else if (!contextResult.nextFrameInfo) {
|
|
513
517
|
throw new Error(`(UNEXPECTED) contextResult.nextFrameInfo falsy? (E: c287a82e823e662a77923278e2418826)`);
|
|
514
|
-
} else if (contextResult.nextFrameInfo?.responseWasNull) {
|
|
515
|
-
throw new Error(`(UNEXPECTED) contextResult.nextFrameInfo.responseWasNull? logic flow should not have gotten here. (E: 104a32381db816b7183435e805b3d626)`);
|
|
516
518
|
}
|
|
517
519
|
// #endregion error conditions throw
|
|
518
520
|
|
|
@@ -610,7 +612,7 @@ export class SyncSagaCoordinator {
|
|
|
610
612
|
}
|
|
611
613
|
}
|
|
612
614
|
|
|
613
|
-
|
|
615
|
+
private async analyzeDomainIbGibs({
|
|
614
616
|
domainIbGibs,
|
|
615
617
|
space,
|
|
616
618
|
}: {
|
|
@@ -656,7 +658,7 @@ export class SyncSagaCoordinator {
|
|
|
656
658
|
* Generates the first frame containing the Knowledge Vector of the Local Space.
|
|
657
659
|
* This is sent to the Receiver to begin Gap Analysis.
|
|
658
660
|
*/
|
|
659
|
-
|
|
661
|
+
private async createInitFrame({
|
|
660
662
|
sagaId,
|
|
661
663
|
sessionIdentity,
|
|
662
664
|
domainIbGibs,
|
|
@@ -739,7 +741,7 @@ export class SyncSagaCoordinator {
|
|
|
739
741
|
*
|
|
740
742
|
* @returns when all {@link expectedAddrs} are done being transmitted.
|
|
741
743
|
*/
|
|
742
|
-
|
|
744
|
+
private async pollForDomainPayloads({
|
|
743
745
|
expectedAddrs,
|
|
744
746
|
pollIntervalMs,
|
|
745
747
|
domainPayloadsMap,
|
|
@@ -824,7 +826,7 @@ export class SyncSagaCoordinator {
|
|
|
824
826
|
*
|
|
825
827
|
* This is a one-off on the receiver.
|
|
826
828
|
*/
|
|
827
|
-
|
|
829
|
+
private async handleResponseSagaContext({
|
|
828
830
|
sagaContext,
|
|
829
831
|
initDomainGraph,
|
|
830
832
|
mySpace,
|
|
@@ -898,15 +900,18 @@ export class SyncSagaCoordinator {
|
|
|
898
900
|
sagaContext,
|
|
899
901
|
sagaIbGib,
|
|
900
902
|
srcGraph,
|
|
901
|
-
metaspace,
|
|
902
|
-
mySpace,
|
|
903
|
-
myTempSpace,
|
|
903
|
+
metaspace, mySpace, myTempSpace,
|
|
904
904
|
identity,
|
|
905
905
|
});
|
|
906
906
|
break;
|
|
907
907
|
|
|
908
908
|
case SyncStage.commit:
|
|
909
|
-
|
|
909
|
+
if (logalot) { console.log(`${lc}[${getSyncSagaFrameOrigin({ sagaFrame: sagaIbGib })}] mySpace.ib: ${mySpace.ib} (I: 5b270996d848907238d817fffa64a126)`); }
|
|
910
|
+
nextFrameInfo = await this.handleCommitFrame({
|
|
911
|
+
sagaIbGib,
|
|
912
|
+
metaspace, mySpace, myTempSpace,
|
|
913
|
+
identity,
|
|
914
|
+
});
|
|
910
915
|
break;
|
|
911
916
|
|
|
912
917
|
default:
|
|
@@ -940,7 +945,7 @@ export class SyncSagaCoordinator {
|
|
|
940
945
|
* 3. Identifies what Receiver needs (`deltaRequestAddrInfos`).
|
|
941
946
|
* 4. Returns an `Ack` frame containing these lists.
|
|
942
947
|
*/
|
|
943
|
-
|
|
948
|
+
private async handleInitFrame({
|
|
944
949
|
sagaIbGib,
|
|
945
950
|
messageData,
|
|
946
951
|
mySpace,
|
|
@@ -970,7 +975,7 @@ export class SyncSagaCoordinator {
|
|
|
970
975
|
try {
|
|
971
976
|
if (logalot) { console.log(`${lc} starting... (I: 9d88dcad0408c029e898a4bcf3b08426)`); }
|
|
972
977
|
|
|
973
|
-
console.log(`${lc} [TEST DEBUG] Receiver mySpace: ${mySpace.ib}`);
|
|
978
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] Receiver mySpace: ${mySpace.ib}`); }
|
|
974
979
|
|
|
975
980
|
// Extract Init Data
|
|
976
981
|
const initData = messageData as SyncSagaMessageInitData_V1; // Using renamed variable for clarity
|
|
@@ -1015,7 +1020,7 @@ export class SyncSagaCoordinator {
|
|
|
1015
1020
|
const remoteKV = initData.knowledgeVector;
|
|
1016
1021
|
if (logalot) { console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`); }
|
|
1017
1022
|
const remoteTjps = Object.keys(remoteKV);
|
|
1018
|
-
console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`);
|
|
1023
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`); }
|
|
1019
1024
|
if (logalot) { console.log(`${lc} remoteTjps: ${pretty(remoteTjps)} (I: 86ea4c53db0dc184c8b253386c402126)`); }
|
|
1020
1025
|
|
|
1021
1026
|
// 1. Get Local Latest Addrs for all TJPs
|
|
@@ -1029,7 +1034,7 @@ export class SyncSagaCoordinator {
|
|
|
1029
1034
|
if (!resGetLatestAddrs.data) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data falsy? (E: b180d813c088042b38e1e02e06a16926)`); }
|
|
1030
1035
|
if (!resGetLatestAddrs.data.latestAddrsMap) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data.latestAddrsMap falsy? (E: 16bc386dd51d0ff53a49620b1e641826)`); }
|
|
1031
1036
|
localLatestAddrsMap = resGetLatestAddrs.data.latestAddrsMap;
|
|
1032
|
-
console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localLatestAddrsMap)}`);
|
|
1037
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localLatestAddrsMap)}`); }
|
|
1033
1038
|
if (logalot) { console.log(`${lc} localKV: ${pretty(localLatestAddrsMap)} (I: 980975642cbccd8018cf0cd808d30826)`); }
|
|
1034
1039
|
}
|
|
1035
1040
|
|
|
@@ -1040,7 +1045,7 @@ export class SyncSagaCoordinator {
|
|
|
1040
1045
|
|
|
1041
1046
|
if (!localAddr) {
|
|
1042
1047
|
// We (Receiver) don't have this timeline at all. Request it.
|
|
1043
|
-
console.log(`${lc} [TEST DEBUG] Missing local timeline for TJP: ${tjp}. Requesting remoteAddr: ${remoteAddr}`);
|
|
1048
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] Missing local timeline for TJP: ${tjp}. Requesting remoteAddr: ${remoteAddr}`); }
|
|
1044
1049
|
deltaRequestAddrInfos.push({
|
|
1045
1050
|
addr: remoteAddr,
|
|
1046
1051
|
tjpAddr: tjp,
|
|
@@ -1054,10 +1059,10 @@ export class SyncSagaCoordinator {
|
|
|
1054
1059
|
|
|
1055
1060
|
if (localAddr === remoteAddr) {
|
|
1056
1061
|
// ...already synced
|
|
1057
|
-
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`);
|
|
1062
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`); }
|
|
1058
1063
|
continue;
|
|
1059
1064
|
}
|
|
1060
|
-
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`);
|
|
1065
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`); }
|
|
1061
1066
|
|
|
1062
1067
|
// we have this timeline but it's not synced...
|
|
1063
1068
|
|
|
@@ -1073,7 +1078,7 @@ export class SyncSagaCoordinator {
|
|
|
1073
1078
|
if (remoteIsInPast) {
|
|
1074
1079
|
// we're ahead, so push the delta of what the sender doesn't
|
|
1075
1080
|
// have (we have full knowledge)
|
|
1076
|
-
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote (sender) is in past - offering push`);
|
|
1081
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote (sender) is in past - offering push`); }
|
|
1077
1082
|
const deltaGraph = await getDeltaDependencyGraph({
|
|
1078
1083
|
ibGibAddr: localAddr,
|
|
1079
1084
|
live: false, // always live: false right?
|
|
@@ -1107,7 +1112,7 @@ export class SyncSagaCoordinator {
|
|
|
1107
1112
|
|
|
1108
1113
|
if (localIsInPast) {
|
|
1109
1114
|
// Fast-Forward: We update to remote's tip.
|
|
1110
|
-
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`);
|
|
1115
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`); }
|
|
1111
1116
|
deltaRequestAddrInfos.push({
|
|
1112
1117
|
addr: remoteAddr,
|
|
1113
1118
|
tjpAddr: tjp,
|
|
@@ -1115,7 +1120,7 @@ export class SyncSagaCoordinator {
|
|
|
1115
1120
|
});
|
|
1116
1121
|
} else {
|
|
1117
1122
|
// DIVERGENCE: Both have changes the other doesn't know about.
|
|
1118
|
-
console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`);
|
|
1123
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`); }
|
|
1119
1124
|
|
|
1120
1125
|
if (conflictStrategy === 'abort') {
|
|
1121
1126
|
// Abort Strategy: We will treat this as terminal.
|
|
@@ -1289,7 +1294,7 @@ export class SyncSagaCoordinator {
|
|
|
1289
1294
|
*
|
|
1290
1295
|
* Returns a `Delta` frame.
|
|
1291
1296
|
*/
|
|
1292
|
-
|
|
1297
|
+
private async handleAckFrame({
|
|
1293
1298
|
sagaContext,
|
|
1294
1299
|
sagaIbGib,
|
|
1295
1300
|
initDomainGraph,
|
|
@@ -1336,14 +1341,14 @@ export class SyncSagaCoordinator {
|
|
|
1336
1341
|
|
|
1337
1342
|
// 1. Check for Conflicts
|
|
1338
1343
|
const conflicts = ackData.conflicts || [];
|
|
1339
|
-
console.log(`${lc} [CONFLICT DEBUG] Received conflicts from Ack: ${conflicts.length}`);
|
|
1340
|
-
if (conflicts.length > 0) {
|
|
1344
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] Received conflicts from Ack: ${conflicts.length}`); }
|
|
1345
|
+
if (logalot && conflicts.length > 0) {
|
|
1341
1346
|
console.log(`${lc} [CONFLICT DEBUG] Conflicts detail: ${JSON.stringify(conflicts, null, 2)}`);
|
|
1342
1347
|
}
|
|
1343
1348
|
|
|
1344
1349
|
const terminalConflicts = conflicts.filter(c => c.terminal);
|
|
1345
1350
|
if (terminalConflicts.length > 0) {
|
|
1346
|
-
console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
|
|
1351
|
+
if (logalot) { console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`); }
|
|
1347
1352
|
// Terminal failure. Sender should probably Commit(Fail) or just Abort.
|
|
1348
1353
|
// For now, throw to trigger abort.
|
|
1349
1354
|
throw new Error(`${lc} Peer reported terminal conflicts. (E: 23a0096ee05a2ccfa89334e8f156b426)`);
|
|
@@ -1359,7 +1364,7 @@ export class SyncSagaCoordinator {
|
|
|
1359
1364
|
const outgoingDeltaAddrRequestInfos: SyncSagaRequestAddrInfo[] = []; // Additional requests for merging
|
|
1360
1365
|
|
|
1361
1366
|
if (conflicts.length > 0) {
|
|
1362
|
-
console.log(`${lc} [CONFLICT DEBUG] Processing ${conflicts.length} non-terminal conflicts`);
|
|
1367
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] Processing ${conflicts.length} non-terminal conflicts`); }
|
|
1363
1368
|
// We need to resolve these.
|
|
1364
1369
|
// Strategy:
|
|
1365
1370
|
// 1. Analyze Divergence (Sender vs Receiver)
|
|
@@ -1492,9 +1497,9 @@ export class SyncSagaCoordinator {
|
|
|
1492
1497
|
// }
|
|
1493
1498
|
}
|
|
1494
1499
|
|
|
1495
|
-
console.log(`${lc} [CONFLICT DEBUG] Finished processing ${conflicts.length} conflicts. outgoingDeltaAddrRequestInfos: ${outgoingDeltaAddrRequestInfos.length}`);
|
|
1500
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] Finished processing ${conflicts.length} conflicts. outgoingDeltaAddrRequestInfos: ${outgoingDeltaAddrRequestInfos.length}`); }
|
|
1496
1501
|
} else {
|
|
1497
|
-
console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`);
|
|
1502
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`); }
|
|
1498
1503
|
}
|
|
1499
1504
|
|
|
1500
1505
|
// 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
|
|
@@ -1565,70 +1570,6 @@ export class SyncSagaCoordinator {
|
|
|
1565
1570
|
}
|
|
1566
1571
|
}
|
|
1567
1572
|
|
|
1568
|
-
private async getPayloadsForRequestedInfos({
|
|
1569
|
-
deltaRequestAddrInfos,
|
|
1570
|
-
mySpace,
|
|
1571
|
-
}: {
|
|
1572
|
-
deltaRequestAddrInfos: SyncSagaRequestAddrInfo[];
|
|
1573
|
-
mySpace: IbGibSpaceAny;
|
|
1574
|
-
}): Promise<IbGib_V1[]> {
|
|
1575
|
-
const lc = `${this.lc}[${this.getPayloadsForRequestedInfos.name}]`;
|
|
1576
|
-
try {
|
|
1577
|
-
if (logalot) { console.log(`${lc} starting... (I: 4fe13d0d80050f20a8b74ba80cee5826)`); }
|
|
1578
|
-
/**
|
|
1579
|
-
* graph of ibgibs we will send to the receiver. addr-based, so will
|
|
1580
|
-
* already be unique (no need to call `unique` on the domains
|
|
1581
|
-
* ibgibs)
|
|
1582
|
-
*/
|
|
1583
|
-
const outgoingPayloadIbGibsDomainGraph: FlatIbGibGraph = {};
|
|
1584
|
-
for (const { addr, latestAddrAlreadyHave } of deltaRequestAddrInfos) {
|
|
1585
|
-
let deltaDepGraph: FlatIbGibGraph;
|
|
1586
|
-
if (latestAddrAlreadyHave) {
|
|
1587
|
-
// already has some, so only get the delta
|
|
1588
|
-
// remember: if we didn't have the other's latest addr, then
|
|
1589
|
-
// this would be in the conflicts not requested addrs
|
|
1590
|
-
deltaDepGraph = await getDeltaDependencyGraph({
|
|
1591
|
-
ibGibAddr: addr,
|
|
1592
|
-
latestCommonFrameAddr: latestAddrAlreadyHave,
|
|
1593
|
-
space: mySpace,
|
|
1594
|
-
live: true,
|
|
1595
|
-
});
|
|
1596
|
-
} else {
|
|
1597
|
-
// doesn't have anything, so get the entire dependency graph
|
|
1598
|
-
// INEFFICIENT: we've already gotten all of the domain
|
|
1599
|
-
// dependencies in initDomainGraph, but getDependencyGraph
|
|
1600
|
-
// only works against a space so we are going to rerun this.
|
|
1601
|
-
// an optimization would be to adapt/create a new
|
|
1602
|
-
// getDependencyGraph to work against an existing flat ibgib
|
|
1603
|
-
// map.
|
|
1604
|
-
deltaDepGraph = await getDependencyGraph({
|
|
1605
|
-
ibGibAddr: addr,
|
|
1606
|
-
space: mySpace,
|
|
1607
|
-
live: true,
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
const depGraphSize = Object.keys(deltaDepGraph).length;
|
|
1612
|
-
if (depGraphSize === 0) {
|
|
1613
|
-
throw new Error(`(UNEXPECTED) couldn't get requested addrs in mySpace (${mySpace.ib})? How did the receiver know to ask for these addrs if they weren't in our original graph? (E: 281eaebcdf77dd73e8245b2872100826)`);
|
|
1614
|
-
} else {
|
|
1615
|
-
// we have dependencies!
|
|
1616
|
-
Object.values(deltaDepGraph).forEach(x => {
|
|
1617
|
-
outgoingPayloadIbGibsDomainGraph[getIbGibAddr({ ibGib: x })] = x;
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
const result = Object.values(outgoingPayloadIbGibsDomainGraph);
|
|
1623
|
-
return result;
|
|
1624
|
-
} catch (error) {
|
|
1625
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
1626
|
-
throw error;
|
|
1627
|
-
} finally {
|
|
1628
|
-
if (logalot) { console.log(`${lc} complete.`); }
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
1573
|
/**
|
|
1633
1574
|
* Handles the `Delta` frame.
|
|
1634
1575
|
*
|
|
@@ -1639,7 +1580,7 @@ export class SyncSagaCoordinator {
|
|
|
1639
1580
|
* 2. **Fulfillment**: Checks `requests`. If Peer requested data, gathers it and prepares `outgoingPayload`.
|
|
1640
1581
|
* 3. **Completion**: If no more requests, transitions to `Commit`.
|
|
1641
1582
|
*/
|
|
1642
|
-
|
|
1583
|
+
private async handleDeltaFrame({
|
|
1643
1584
|
sagaContext,
|
|
1644
1585
|
sagaIbGib,
|
|
1645
1586
|
srcGraph,
|
|
@@ -1669,7 +1610,7 @@ export class SyncSagaCoordinator {
|
|
|
1669
1610
|
if (logalot) { console.log(`${lc} deltaData: ${pretty(deltaData)} (I: a76008681df458cfbcdc4848f825a826)`); }
|
|
1670
1611
|
// #endregion validate/sanity
|
|
1671
1612
|
|
|
1672
|
-
console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`);
|
|
1613
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`); }
|
|
1673
1614
|
|
|
1674
1615
|
const peerProposesCommit = deltaData.proposeCommit || false;
|
|
1675
1616
|
|
|
@@ -1679,12 +1620,12 @@ export class SyncSagaCoordinator {
|
|
|
1679
1620
|
const receivedPayloadIbGibs: IbGib_V1[] = sagaContext.payloadIbGibsDomain ?? [];
|
|
1680
1621
|
|
|
1681
1622
|
// 2. Fulfill Peer Requests (Outgoing Payload with Delta Dependencies)
|
|
1682
|
-
console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${(deltaData.deltaRequestAddrInfos || []).length} peer requests`);
|
|
1623
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${(deltaData.deltaRequestAddrInfos || []).length} peer requests`); }
|
|
1683
1624
|
const outgoingPayload = await this.getPayloadsForRequestedInfos({
|
|
1684
1625
|
deltaRequestAddrInfos: deltaData.deltaRequestAddrInfos || [],
|
|
1685
1626
|
mySpace,
|
|
1686
1627
|
});
|
|
1687
|
-
console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`);
|
|
1628
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`); }
|
|
1688
1629
|
|
|
1689
1630
|
// 3. Execute Merges (If applicable)
|
|
1690
1631
|
// Check if we have pending conflicts that we CAN resolve now that we have data.
|
|
@@ -1692,36 +1633,41 @@ export class SyncSagaCoordinator {
|
|
|
1692
1633
|
// Optimization: Do this only if we received payloads.
|
|
1693
1634
|
const mergeResultIbGibs: IbGib_V1[] = [];
|
|
1694
1635
|
|
|
1695
|
-
console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`);
|
|
1636
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`); }
|
|
1696
1637
|
|
|
1697
1638
|
if (receivedPayloadIbGibs.length > 0) {
|
|
1698
|
-
console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`);
|
|
1639
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`); }
|
|
1699
1640
|
// Find the Ack frame in history to get conflicts
|
|
1700
1641
|
// Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
|
|
1701
1642
|
// V1 timelines carry full history in `past`.
|
|
1702
1643
|
const pastAddrs = sagaIbGib.rel8ns?.past || [];
|
|
1703
|
-
console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`);
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1644
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`); }
|
|
1645
|
+
|
|
1646
|
+
const sagaHistory = await getFullSyncSagaHistory({
|
|
1647
|
+
sagaIbGib,
|
|
1648
|
+
space: mySpace,
|
|
1649
|
+
});
|
|
1650
|
+
// #region validate/sanity sagaHistory
|
|
1651
|
+
if (sagaHistory.length === 0) { throw new Error(`(UNEXPECTED) sagaHistory.length is 0? We're in handleDeltaFrame. So we should have at least init and ack. (E: 815735c9cf756b275719bf434f180826)`); }
|
|
1652
|
+
if (sagaHistory.length === 1) { throw new Error(`(UNEXPECTED) sagaHistory.length is 1? We're in handleDeltaFrame. So we should have at least init and ack. (E: 0e4ef8e3ed088e83b2cdcd18ecea9826)`); }
|
|
1653
|
+
// #endregion validate/sanity sagaHistory
|
|
1654
|
+
|
|
1655
|
+
const ackGraphs = sagaHistory.filter(x =>
|
|
1656
|
+
x.msgStones.at(0)!.data!.stage === SyncStage.ack
|
|
1657
|
+
);
|
|
1658
|
+
// #region validate/sanity ackGraphs
|
|
1659
|
+
if (ackGraphs.length > 1) {
|
|
1660
|
+
throw new Error(`(UNEXPECTED) more than one ack stage found in sagaHistory? we're expecting exactly one. (E: 7150983bb3a841caf8acd60826ba4f26)`);
|
|
1661
|
+
} else if (ackGraphs.length === 0) {
|
|
1662
|
+
throw new Error(`(UNEXPECTED) couldn't find ack stage in sagaHistory? (E: 7d2da859196b86de28e7c8183af1e826)`);
|
|
1663
|
+
}
|
|
1664
|
+
if (ackGraphs[0].msgStones.length !== 1) {
|
|
1665
|
+
throw new Error(`(UNEXPECTED) ackGraph has more than one msg stone? only one expected right now. (E: f07d388abff8d26f98cbf6e8d3932826)`);
|
|
1724
1666
|
}
|
|
1667
|
+
// #endregion validate/sanity ackGraphs
|
|
1668
|
+
|
|
1669
|
+
const ackData = ackGraphs[0].msgStones[0].data as SyncSagaMessageAckData_V1;
|
|
1670
|
+
if (!ackData) { throw new Error(`(UNEXPECTED) ackData falsy? (E: f9af88427f66a8db1ba868b810e8b826)`); }
|
|
1725
1671
|
|
|
1726
1672
|
if (ackData && ackData.conflicts) {
|
|
1727
1673
|
const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
|
|
@@ -1735,9 +1681,9 @@ export class SyncSagaCoordinator {
|
|
|
1735
1681
|
// We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
|
|
1736
1682
|
|
|
1737
1683
|
// Check if we have receiverTip in space
|
|
1738
|
-
console.log(`${lc} [CONFLICT DEBUG] Attempting merge for conflict. ReceiverTip: ${receiverTip}, SenderTip: ${senderTip}`);
|
|
1684
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] Attempting merge for conflict. ReceiverTip: ${receiverTip}, SenderTip: ${senderTip}`); }
|
|
1739
1685
|
const resRecTip = await getFromSpace({ addr: receiverTip, space: myTempSpace }); // Check myTempSpace for incoming data
|
|
1740
|
-
console.log(`${lc} [CONFLICT DEBUG] ReceiverTip found in myTempSpace: ${!!resRecTip.ibGibs?.[0]}`);
|
|
1686
|
+
if (logalot) { console.log(`${lc} [CONFLICT DEBUG] ReceiverTip found in myTempSpace: ${!!resRecTip.ibGibs?.[0]}`); }
|
|
1741
1687
|
if (resRecTip.success && resRecTip.ibGibs?.[0]) {
|
|
1742
1688
|
// We have the tip!
|
|
1743
1689
|
// Do we have the full history?
|
|
@@ -1753,13 +1699,13 @@ export class SyncSagaCoordinator {
|
|
|
1753
1699
|
metaspace,
|
|
1754
1700
|
});
|
|
1755
1701
|
if (mergeResult) {
|
|
1756
|
-
console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
|
|
1702
|
+
if (logalot) { console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
|
|
1757
1703
|
if (logalot) { console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
|
|
1758
1704
|
mergeResultIbGibs.push(mergeResult);
|
|
1759
1705
|
outgoingPayload.push(mergeResult); // Send result to peer
|
|
1760
1706
|
}
|
|
1761
1707
|
} catch (e) {
|
|
1762
|
-
console.error(`${lc} Merge failed: ${e}`);
|
|
1708
|
+
console.error(`${lc} (NOT THROWN) Merge failed: ${e} (E: 491b464fc6f4857f52ff3df213105826)`);
|
|
1763
1709
|
// If merge fails, we might Abort or just continue?
|
|
1764
1710
|
}
|
|
1765
1711
|
}
|
|
@@ -1785,20 +1731,8 @@ export class SyncSagaCoordinator {
|
|
|
1785
1731
|
stage: SyncStage.delta,
|
|
1786
1732
|
payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
|
|
1787
1733
|
requests: hasMyRequests ? myRequests : undefined,
|
|
1788
|
-
proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
|
|
1789
|
-
// Wait. If we send data, we are NOT committing yet.
|
|
1790
|
-
// We are sending data. The OTHER side must ingest it.
|
|
1791
|
-
// So proposeCommit = true?
|
|
1792
|
-
// "Here is the data. I'm done. If you are good, let's commit."
|
|
1793
|
-
// Yes.
|
|
1794
1734
|
};
|
|
1795
1735
|
|
|
1796
|
-
// BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
|
|
1797
|
-
// We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
|
|
1798
|
-
|
|
1799
|
-
// So yes, proposeCommit = true.
|
|
1800
|
-
responseDeltaData.proposeCommit = true;
|
|
1801
|
-
|
|
1802
1736
|
const deltaStone = await this.createSyncMsgStone({
|
|
1803
1737
|
data: responseDeltaData,
|
|
1804
1738
|
localSpace: mySpace,
|
|
@@ -1813,42 +1747,45 @@ export class SyncSagaCoordinator {
|
|
|
1813
1747
|
metaspace,
|
|
1814
1748
|
});
|
|
1815
1749
|
|
|
1816
|
-
// Build control payloads: frame + its dependencies (msg stone, identity)
|
|
1817
|
-
const payloadIbGibsControl: IbGib_V1[] = [deltaFrame, deltaStone];
|
|
1818
|
-
if (identity) { payloadIbGibsControl.push(identity); }
|
|
1819
|
-
|
|
1820
|
-
// return { frame: deltaFrame, payloadIbGibsControl, payloadIbGibsDomain: outgoingPayload };
|
|
1821
1750
|
return { frame: deltaFrame, payloadIbGibsDomain: outgoingPayload };
|
|
1822
|
-
// throw new Error(`not implemented (E: 2b38a8afb6d84efcee5ab51673387826)`);
|
|
1823
|
-
|
|
1824
1751
|
} else {
|
|
1825
1752
|
// We have nothing to send.
|
|
1826
1753
|
|
|
1827
1754
|
if (peerProposesCommit) {
|
|
1828
1755
|
// Peer is done. We are done. -> Commit.
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
};
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
data: commitData,
|
|
1837
|
-
localSpace: mySpace,
|
|
1838
|
-
metaspace
|
|
1756
|
+
// one last validate entire history?
|
|
1757
|
+
const history = await getFullSyncSagaHistory({
|
|
1758
|
+
sagaIbGib,
|
|
1759
|
+
space: mySpace,
|
|
1760
|
+
});
|
|
1761
|
+
const validationErrors = await validateFullSyncSagaHistory({
|
|
1762
|
+
history,
|
|
1839
1763
|
});
|
|
1764
|
+
if (validationErrors.length > 0) {
|
|
1765
|
+
const errorCommitFrame = await this.createCommitFrame({
|
|
1766
|
+
sagaIbGib,
|
|
1767
|
+
errors: validationErrors,
|
|
1768
|
+
metaspace, mySpace,
|
|
1769
|
+
identity,
|
|
1770
|
+
});
|
|
1771
|
+
return { frame: errorCommitFrame, }; /* <<<< returns early */
|
|
1772
|
+
}
|
|
1840
1773
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1774
|
+
await this.executeLocalCommit({
|
|
1775
|
+
sagaHistory: history,
|
|
1776
|
+
deltaFrame: sagaIbGib,
|
|
1777
|
+
localTempSpace: myTempSpace,
|
|
1845
1778
|
localSpace: mySpace,
|
|
1846
|
-
metaspace
|
|
1847
|
-
})
|
|
1779
|
+
metaspace,
|
|
1780
|
+
})
|
|
1848
1781
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1782
|
+
const commitFrame = await this.createCommitFrame({
|
|
1783
|
+
sagaIbGib,
|
|
1784
|
+
errors: undefined,
|
|
1785
|
+
metaspace,
|
|
1786
|
+
mySpace,
|
|
1787
|
+
identity,
|
|
1788
|
+
});
|
|
1852
1789
|
|
|
1853
1790
|
return { frame: commitFrame, };
|
|
1854
1791
|
|
|
@@ -1924,9 +1861,246 @@ export class SyncSagaCoordinator {
|
|
|
1924
1861
|
}
|
|
1925
1862
|
|
|
1926
1863
|
}
|
|
1864
|
+
/**
|
|
1865
|
+
* should throw if fails
|
|
1866
|
+
*/
|
|
1867
|
+
private async executeLocalCommit({
|
|
1868
|
+
deltaFrame,
|
|
1869
|
+
commitFrame,
|
|
1870
|
+
sagaHistory,
|
|
1871
|
+
metaspace, localSpace, localTempSpace,
|
|
1872
|
+
identity,
|
|
1873
|
+
}: {
|
|
1874
|
+
deltaFrame?: SyncIbGib_V1;
|
|
1875
|
+
commitFrame?: SyncIbGib_V1;
|
|
1876
|
+
sagaHistory: SyncSagaFrameDependencyGraph[];
|
|
1877
|
+
metaspace: MetaspaceService;
|
|
1878
|
+
localSpace: IbGibSpaceAny;
|
|
1879
|
+
localTempSpace: IbGibSpaceAny;
|
|
1880
|
+
identity?: KeystoneIbGib_V1;
|
|
1881
|
+
}): Promise<void> {
|
|
1882
|
+
const lc = `${this.lc}[${this.executeLocalCommit.name}]`;
|
|
1883
|
+
try {
|
|
1884
|
+
if (logalot) { console.log(`${lc} starting... (I: 6734980446b86a63c1af6e2e206de826)`); }
|
|
1885
|
+
if (!deltaFrame && !commitFrame) {
|
|
1886
|
+
throw new Error(`(UNEXPECTED) !deltaFrame && !commitFrame? we're expecting to execute the commit based off of EITHER a delta OR a commit frame. (E: 10cae319a2685a672866f5583514e326)`);
|
|
1887
|
+
} else if (deltaFrame && commitFrame) {
|
|
1888
|
+
throw new Error(`(UNEXPECTED) deltaFrame && commitFrame? we're expecting to execute the commit based off of EITHER a delta OR a commit frame. (E: b9037b1ed6d8684ac8a5d01328bad826)`);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* Sync has a two-stage commit. The first is from a delta frame with
|
|
1893
|
+
* `proposeCommit: true`. That produces a commit frame, which on the
|
|
1894
|
+
* other end triggers the second leg.
|
|
1895
|
+
*/
|
|
1896
|
+
const isFirstCommitLeg = !!deltaFrame;
|
|
1897
|
+
const currentFrame = isFirstCommitLeg ? deltaFrame : commitFrame;
|
|
1898
|
+
if (!currentFrame) { throw new Error(`(UNEXPECTED) !currentFrame? something wrong with my logic. (E: 4ec0ce6625cc1d677bc902c839ca1a26)`); }
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* we only want to register new ibgibs from the other end, so we
|
|
1902
|
+
* will compare the history payloads/push offers with the context
|
|
1903
|
+
* that provided those payloads.
|
|
1904
|
+
*
|
|
1905
|
+
* So we will filter out the history that we produced, which should
|
|
1906
|
+
* leave us only with externally created payload addrs.
|
|
1907
|
+
*
|
|
1908
|
+
* NOTE: I have this at the beginning of the function PURELY to get
|
|
1909
|
+
* a log value in to understand the context of this function's
|
|
1910
|
+
* output.
|
|
1911
|
+
*/
|
|
1912
|
+
const currentFrameOrigin = getSyncSagaFrameOrigin({ sagaFrame: currentFrame });
|
|
1927
1913
|
|
|
1914
|
+
if (logalot) { console.log(`${lc} currentFrameOrigin: ${currentFrameOrigin} (I: 3add8f8390c89743ae554bd3cc60b826)`); }
|
|
1928
1915
|
|
|
1929
|
-
|
|
1916
|
+
// #region validate/sanity
|
|
1917
|
+
if (!currentFrame.data) { throw new Error(`(UNEXPECTED) currentFrame.data falsy? (E: a8be68d48668d93d992d793834823826)`); }
|
|
1918
|
+
// #endregion validate/sanity
|
|
1919
|
+
|
|
1920
|
+
// * move all payload addrs from temp space to local space
|
|
1921
|
+
// * register each and every iteration of each timeline (including
|
|
1922
|
+
// stones, which are like their own sealed timeline of length 1)
|
|
1923
|
+
|
|
1924
|
+
const allPayloadAddrsDomainTransferred: IbGibAddr[] = [];
|
|
1925
|
+
const fnAddPayloadAddr = (addr: IbGibAddr) => {
|
|
1926
|
+
if (!allPayloadAddrsDomainTransferred.includes(addr)) {
|
|
1927
|
+
allPayloadAddrsDomainTransferred.push(addr);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
sagaHistory.filter(x => {
|
|
1932
|
+
const frameExecutionContext = getSyncSagaFrameOrigin({ sagaFrame: x.sagaIbGib });
|
|
1933
|
+
return currentFrameOrigin === frameExecutionContext;
|
|
1934
|
+
}).forEach(x => {
|
|
1935
|
+
if (!x.sagaIbGib.data) { throw new Error(`(UNEXPECTED) sagaIbGib.data falsy? (E: 34d7f8cdee14717ce828878d98f89826)`); }
|
|
1936
|
+
if (logalot) { console.log(`${lc}[${currentFrameOrigin}] history slice: ${pretty(x)} (I: e5a9436fb66b0df3183bd6d81be2ca26)`); }
|
|
1937
|
+
x.msgStones.forEach(msgStone => {
|
|
1938
|
+
if (!msgStone.data) { throw new Error(`(UNEXPECTED) msgStone.data falsy? (E: 21406101f91847514cc759c8a7382f26)`); }
|
|
1939
|
+
if (msgStone.data.stage === SyncStage.ack) {
|
|
1940
|
+
const ackData = msgStone.data as SyncSagaMessageAckData_V1;
|
|
1941
|
+
if (ackData.pushOfferInfos && ackData.pushOfferInfos.length > 0) {
|
|
1942
|
+
ackData.pushOfferInfos.forEach(info => {
|
|
1943
|
+
info.addrs.forEach(addr => fnAddPayloadAddr(addr));
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
} else if (msgStone.data.stage === SyncStage.delta) {
|
|
1947
|
+
const deltaData = msgStone.data as SyncSagaMessageDeltaData_V1;
|
|
1948
|
+
if (deltaData.payloadAddrsDomain && deltaData.payloadAddrsDomain.length > 0) {
|
|
1949
|
+
deltaData.payloadAddrsDomain.forEach(addr => fnAddPayloadAddr(addr));
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
})
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
if (logalot) { console.log(`${lc}[${currentFrameOrigin}] allPayloadAddrsDomainTransferred: ${allPayloadAddrsDomainTransferred.length > 0 ? allPayloadAddrsDomainTransferred.join(', ') : 'none'} (I: a1950eb3ca95bdc9ec18a4b8823e9826)`); }
|
|
1956
|
+
|
|
1957
|
+
// at this point, we have a list of ALL payload addrs retrieved.
|
|
1958
|
+
let allPayloadIbGibsDomainTransferred: IbGib_V1[] = [];
|
|
1959
|
+
if (allPayloadAddrsDomainTransferred.length > 0) {
|
|
1960
|
+
const resGetAllTransferred =
|
|
1961
|
+
await getFromSpace({ addrs: allPayloadAddrsDomainTransferred, space: localTempSpace });
|
|
1962
|
+
if (resGetAllTransferred.success && resGetAllTransferred.ibGibs && resGetAllTransferred.ibGibs.length === allPayloadAddrsDomainTransferred.length) {
|
|
1963
|
+
allPayloadIbGibsDomainTransferred = resGetAllTransferred.ibGibs.concat();
|
|
1964
|
+
} else {
|
|
1965
|
+
// errored out, gather info
|
|
1966
|
+
if (!resGetAllTransferred.rawResultIbGib) { throw new Error(`(UNEXPECTED) !resGetAllTransferred.rawResultIbGib falsy? (E: dc6cf8729668f4fbe8c024f887d97a26)`); }
|
|
1967
|
+
const { addrsNotFound, addrsErrored, errors } = resGetAllTransferred.rawResultIbGib.data as IbGibSpaceResultData;
|
|
1968
|
+
throw new Error(`(UNEXPECTED) we couldn't get all addrs transferred throughout the saga from the tempspace (${localTempSpace.ib})? addrsNotFound: ${addrsNotFound ?? []}. addrsErrored: ${addrsErrored ?? []}. errors: ${errors ?? []}(E: 222e341e634862b4d88ae7282584d826)`);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// now we have all ibgibs transferred, first put them all the local space
|
|
1973
|
+
if (allPayloadIbGibsDomainTransferred.length === 0) {
|
|
1974
|
+
// nothing was synced
|
|
1975
|
+
if (logalot) { console.log(`${lc}[${currentFrameOrigin}] no changes on this end (I: fa30d18e83a87ee9e8487fbb5d632b26)`); }
|
|
1976
|
+
} else {
|
|
1977
|
+
if (logalot) { console.log(`${lc}[${currentFrameOrigin}] ${allPayloadIbGibsDomainTransferred.length} changes detected (I: fa30d18e83a87ee9e8487fbb5d632b26)`); }
|
|
1978
|
+
|
|
1979
|
+
if (logalot) { console.log(`${lc} put all into localSpace (${localSpace.ib}) starting... (I: d5e0d9870e380b61e80c729660058826)`); }
|
|
1980
|
+
const { payload_Dnas, payload_NonDnas } =
|
|
1981
|
+
await putInSpace_dnasThenNonDnas({
|
|
1982
|
+
ibGibs: allPayloadIbGibsDomainTransferred,
|
|
1983
|
+
space: localSpace
|
|
1984
|
+
});
|
|
1985
|
+
if (logalot) { console.log(`${lc} put all into localSpace (${localSpace.ib}) complete. (I: d5e0d9870e380b61e80c729660058826)`); }
|
|
1986
|
+
|
|
1987
|
+
const { mapWithTjp_NoDna, mapWithTjp_YesDna, mapWithoutTjps } =
|
|
1988
|
+
splitPerTjpAndOrDna({ ibGibs: payload_NonDnas, filterPrimitives: true });
|
|
1989
|
+
|
|
1990
|
+
// first register all non-tjp stones
|
|
1991
|
+
if (logalot) { console.log(`${lc} register mapWithoutTjps starting... (I: 2b84d8f8dda85adde88696d8419bf726)`); }
|
|
1992
|
+
const nontjps = Object.values(mapWithoutTjps);
|
|
1993
|
+
for (const nontjp of nontjps) {
|
|
1994
|
+
if (logalot) { console.log(`${lc} registering ${getIbGibAddr({ ibGib: nontjp })} (I: 4647b4f42d27cffe0fbfce8846755826)`); }
|
|
1995
|
+
await metaspace.registerNewIbGib({
|
|
1996
|
+
ibGib: nontjp,
|
|
1997
|
+
space: localSpace,
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
if (logalot) { console.log(`${lc} mapWithoutTjps complete. (I: 2b84d8f8dda85adde88696d8419bf726)`); }
|
|
2001
|
+
|
|
2002
|
+
// next register each timeline in order...
|
|
2003
|
+
|
|
2004
|
+
// ...first the ones without dna...
|
|
2005
|
+
if (logalot) { console.log(`${lc} register mapWithTjp_NoDna starting... (I: 96e648e4db382499b8bfaa1b37c24826)`); }
|
|
2006
|
+
const timelinesByTjpAddr_NoDna =
|
|
2007
|
+
getTimelinesGroupedByTjp({ ibGibs: Object.values(mapWithTjp_NoDna) });
|
|
2008
|
+
const tjpAddrs_NoDna = Object.keys(timelinesByTjpAddr_NoDna);
|
|
2009
|
+
for (const tjpAddr_NoDna of tjpAddrs_NoDna) {
|
|
2010
|
+
const timelineIbGibs = timelinesByTjpAddr_NoDna[tjpAddr_NoDna];
|
|
2011
|
+
for (const ibGib of timelineIbGibs) {
|
|
2012
|
+
await metaspace.registerNewIbGib({
|
|
2013
|
+
ibGib,
|
|
2014
|
+
space: localSpace,
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (logalot) { console.log(`${lc} register mapWithTjp_NoDna complete. (I: 96e648e4db382499b8bfaa1b37c24826)`); }
|
|
2019
|
+
|
|
2020
|
+
// ...then the ones WITH dna.
|
|
2021
|
+
if (logalot) { console.log(`${lc} register mapWithTjp_YesDna starting... (I: d54a820e9442dee4681f6228a6795926)`); }
|
|
2022
|
+
const timelinesByTjpAddr_YesDna =
|
|
2023
|
+
getTimelinesGroupedByTjp({ ibGibs: Object.values(mapWithTjp_YesDna) });
|
|
2024
|
+
const tjpAddrs_YesDna = Object.keys(timelinesByTjpAddr_YesDna);
|
|
2025
|
+
for (const tjpAddr_YesDna of tjpAddrs_YesDna) {
|
|
2026
|
+
const timelineIbGibs = timelinesByTjpAddr_YesDna[tjpAddr_YesDna];
|
|
2027
|
+
for (const ibGib of timelineIbGibs) {
|
|
2028
|
+
await metaspace.registerNewIbGib({
|
|
2029
|
+
ibGib,
|
|
2030
|
+
space: localSpace,
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (logalot) { console.log(`${lc} register mapWithTjp_YesDna complete. (I: d54a820e9442dee4681f6228a6795926)`); }
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
2041
|
+
throw error;
|
|
2042
|
+
} finally {
|
|
2043
|
+
if (logalot) { console.log(`${lc} complete.`); }
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
private async createCommitFrame({
|
|
2048
|
+
sagaIbGib,
|
|
2049
|
+
errors,
|
|
2050
|
+
metaspace,
|
|
2051
|
+
mySpace,
|
|
2052
|
+
identity,
|
|
2053
|
+
}: {
|
|
2054
|
+
sagaIbGib: SyncIbGib_V1,
|
|
2055
|
+
/**
|
|
2056
|
+
* if truthy and non-empty, will create an errored commit frame (data
|
|
2057
|
+
* has `success: false`)
|
|
2058
|
+
*/
|
|
2059
|
+
errors?: string[],
|
|
2060
|
+
metaspace: MetaspaceService,
|
|
2061
|
+
mySpace: IbGibSpaceAny,
|
|
2062
|
+
identity: KeystoneIbGib_V1 | undefined,
|
|
2063
|
+
}): Promise<SyncIbGib_V1> {
|
|
2064
|
+
const lc = `[${this.createCommitFrame.name}]`;
|
|
2065
|
+
try {
|
|
2066
|
+
if (logalot) { console.log(`${lc} starting... (I: 48fc57c023f80122135a284855757526)`); }
|
|
2067
|
+
const commitData: SyncSagaMessageCommitData_V1 = errors && errors.length > 0 ?
|
|
2068
|
+
{
|
|
2069
|
+
// errored
|
|
2070
|
+
sagaId: sagaIbGib.data!.uuid,
|
|
2071
|
+
stage: SyncStage.commit,
|
|
2072
|
+
success: false,
|
|
2073
|
+
errors,
|
|
2074
|
+
} :
|
|
2075
|
+
{
|
|
2076
|
+
// not errored
|
|
2077
|
+
sagaId: sagaIbGib.data!.uuid,
|
|
2078
|
+
stage: SyncStage.commit,
|
|
2079
|
+
success: true,
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
const commitStone = await this.createSyncMsgStone({
|
|
2083
|
+
data: commitData,
|
|
2084
|
+
localSpace: mySpace,
|
|
2085
|
+
metaspace
|
|
2086
|
+
});
|
|
2087
|
+
const commitFrame = await this.evolveSyncSagaIbGib({
|
|
2088
|
+
prevSagaIbGib: sagaIbGib,
|
|
2089
|
+
msgStones: [commitStone],
|
|
2090
|
+
sessionIdentity: identity,
|
|
2091
|
+
localSpace: mySpace,
|
|
2092
|
+
metaspace,
|
|
2093
|
+
});
|
|
2094
|
+
return commitFrame;
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
2097
|
+
throw error;
|
|
2098
|
+
} finally {
|
|
2099
|
+
if (logalot) { console.log(`${lc} complete.`); }
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
private async handleCommitFrame({
|
|
1930
2104
|
sagaIbGib,
|
|
1931
2105
|
mySpace,
|
|
1932
2106
|
myTempSpace,
|
|
@@ -1943,23 +2117,66 @@ export class SyncSagaCoordinator {
|
|
|
1943
2117
|
try {
|
|
1944
2118
|
if (logalot) { console.log(`${lc} starting... (I: e179573bdd881202f8ba3168da1c3826)`); }
|
|
1945
2119
|
|
|
2120
|
+
if (!sagaIbGib.data) { throw new Error(`(UNEXPECTED) sagaIbGib.data falsy? (E: cbc31dbb7d28b5b8c8cddb510d7d2826)`); }
|
|
1946
2121
|
|
|
2122
|
+
if (sagaIbGib.data.errors && sagaIbGib.data.errors.length > 0) {
|
|
2123
|
+
// our saga errored out but we already committed the darn
|
|
2124
|
+
// changes previously
|
|
2125
|
+
throw new Error(`not implemented...saga errored out on other end but we've already committed our changes. we need to implement undoing our changes, i.e., rollback commit, which basically is deleting all the payloads that we already put in in our previous commit. (E: e8eb789e0c08f0b3cf93607cb73e9f26)`);
|
|
2126
|
+
} else {
|
|
2127
|
+
// Sender Logic (Finalizing):
|
|
2128
|
+
// If we are here, we received a Commit frame from the Peer.
|
|
2129
|
+
// This implies the Peer has successfully committed.
|
|
2130
|
+
// We should now:
|
|
2131
|
+
// 1. Validate (implicitly done by receiving valid frame)
|
|
2132
|
+
// 2. Perform our own cleanup (Temp -> Dest, if applicable)
|
|
2133
|
+
// 3. Return saga completion.
|
|
2134
|
+
|
|
2135
|
+
// one last validate entire history ?
|
|
2136
|
+
const history = await getFullSyncSagaHistory({
|
|
2137
|
+
sagaIbGib,
|
|
2138
|
+
space: mySpace,
|
|
2139
|
+
});
|
|
2140
|
+
const validationErrors = await validateFullSyncSagaHistory({
|
|
2141
|
+
history,
|
|
2142
|
+
});
|
|
2143
|
+
if (validationErrors.length > 0) {
|
|
2144
|
+
const errorCommitFrame = await this.createCommitFrame({
|
|
2145
|
+
sagaIbGib,
|
|
2146
|
+
metaspace,
|
|
2147
|
+
mySpace,
|
|
2148
|
+
identity,
|
|
2149
|
+
errors: validationErrors,
|
|
2150
|
+
});
|
|
2151
|
+
return { frame: errorCommitFrame, }; /* <<<< returns early */
|
|
2152
|
+
}
|
|
1947
2153
|
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2154
|
+
await this.executeLocalCommit({
|
|
2155
|
+
commitFrame: sagaIbGib,
|
|
2156
|
+
sagaHistory: history,
|
|
2157
|
+
localSpace: mySpace,
|
|
2158
|
+
localTempSpace: myTempSpace,
|
|
2159
|
+
metaspace,
|
|
2160
|
+
});
|
|
1955
2161
|
|
|
1956
|
-
|
|
1957
|
-
|
|
2162
|
+
// todo: implement explicit cleanup logic here and in peer
|
|
2163
|
+
console.error(`${lc} NAG ERROR (NOT THROWN): implement cleanup logic, including add a cleanup method to the peer (E: 3a9a24befb98a981a88fbdbf52920e26)`);
|
|
2164
|
+
if (logalot) { console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`); }
|
|
1958
2165
|
|
|
1959
|
-
|
|
2166
|
+
// the holy grail!
|
|
2167
|
+
return { sagaComplete: true };
|
|
2168
|
+
}
|
|
1960
2169
|
} catch (error) {
|
|
1961
|
-
|
|
1962
|
-
|
|
2170
|
+
const emsg = `${lc} ${extractErrorMsg(error)}`;
|
|
2171
|
+
console.error(emsg);
|
|
2172
|
+
const errorCommitFrame = await this.createCommitFrame({
|
|
2173
|
+
sagaIbGib,
|
|
2174
|
+
errors: [emsg],
|
|
2175
|
+
metaspace,
|
|
2176
|
+
mySpace,
|
|
2177
|
+
identity,
|
|
2178
|
+
});
|
|
2179
|
+
return { frame: errorCommitFrame }
|
|
1963
2180
|
} finally {
|
|
1964
2181
|
if (logalot) { console.log(`${lc} complete.`); }
|
|
1965
2182
|
}
|
|
@@ -1968,7 +2185,7 @@ export class SyncSagaCoordinator {
|
|
|
1968
2185
|
|
|
1969
2186
|
// #endregion Handlers
|
|
1970
2187
|
|
|
1971
|
-
|
|
2188
|
+
private async createSyncMsgStone<TStoneData extends SyncSagaMessageData_V1>({
|
|
1972
2189
|
data,
|
|
1973
2190
|
localSpace,
|
|
1974
2191
|
metaspace,
|
|
@@ -2000,11 +2217,75 @@ export class SyncSagaCoordinator {
|
|
|
2000
2217
|
}
|
|
2001
2218
|
}
|
|
2002
2219
|
|
|
2220
|
+
private async getPayloadsForRequestedInfos({
|
|
2221
|
+
deltaRequestAddrInfos,
|
|
2222
|
+
mySpace,
|
|
2223
|
+
}: {
|
|
2224
|
+
deltaRequestAddrInfos: SyncSagaRequestAddrInfo[];
|
|
2225
|
+
mySpace: IbGibSpaceAny;
|
|
2226
|
+
}): Promise<IbGib_V1[]> {
|
|
2227
|
+
const lc = `${this.lc}[${this.getPayloadsForRequestedInfos.name}]`;
|
|
2228
|
+
try {
|
|
2229
|
+
if (logalot) { console.log(`${lc} starting... (I: 4fe13d0d80050f20a8b74ba80cee5826)`); }
|
|
2230
|
+
/**
|
|
2231
|
+
* graph of ibgibs we will send to the receiver. addr-based, so will
|
|
2232
|
+
* already be unique (no need to call `unique` on the domains
|
|
2233
|
+
* ibgibs)
|
|
2234
|
+
*/
|
|
2235
|
+
const outgoingPayloadIbGibsDomainGraph: FlatIbGibGraph = {};
|
|
2236
|
+
for (const { addr, latestAddrAlreadyHave } of deltaRequestAddrInfos) {
|
|
2237
|
+
let deltaDepGraph: FlatIbGibGraph;
|
|
2238
|
+
if (latestAddrAlreadyHave) {
|
|
2239
|
+
// already has some, so only get the delta
|
|
2240
|
+
// remember: if we didn't have the other's latest addr, then
|
|
2241
|
+
// this would be in the conflicts not requested addrs
|
|
2242
|
+
deltaDepGraph = await getDeltaDependencyGraph({
|
|
2243
|
+
ibGibAddr: addr,
|
|
2244
|
+
latestCommonFrameAddr: latestAddrAlreadyHave,
|
|
2245
|
+
space: mySpace,
|
|
2246
|
+
live: true,
|
|
2247
|
+
});
|
|
2248
|
+
} else {
|
|
2249
|
+
// doesn't have anything, so get the entire dependency graph
|
|
2250
|
+
// INEFFICIENT: we've already gotten all of the domain
|
|
2251
|
+
// dependencies in initDomainGraph, but getDependencyGraph
|
|
2252
|
+
// only works against a space so we are going to rerun this.
|
|
2253
|
+
// an optimization would be to adapt/create a new
|
|
2254
|
+
// getDependencyGraph to work against an existing flat ibgib
|
|
2255
|
+
// map.
|
|
2256
|
+
deltaDepGraph = await getDependencyGraph({
|
|
2257
|
+
ibGibAddr: addr,
|
|
2258
|
+
space: mySpace,
|
|
2259
|
+
live: true,
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
const depGraphSize = Object.keys(deltaDepGraph).length;
|
|
2264
|
+
if (depGraphSize === 0) {
|
|
2265
|
+
throw new Error(`(UNEXPECTED) couldn't get requested addrs in mySpace (${mySpace.ib})? How did the receiver know to ask for these addrs if they weren't in our original graph? (E: 281eaebcdf77dd73e8245b2872100826)`);
|
|
2266
|
+
} else {
|
|
2267
|
+
// we have dependencies!
|
|
2268
|
+
Object.values(deltaDepGraph).forEach(x => {
|
|
2269
|
+
outgoingPayloadIbGibsDomainGraph[getIbGibAddr({ ibGib: x })] = x;
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
const result = Object.values(outgoingPayloadIbGibsDomainGraph);
|
|
2275
|
+
return result;
|
|
2276
|
+
} catch (error) {
|
|
2277
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
2278
|
+
throw error;
|
|
2279
|
+
} finally {
|
|
2280
|
+
if (logalot) { console.log(`${lc} complete.`); }
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2003
2284
|
|
|
2004
2285
|
/**
|
|
2005
2286
|
* Evolves the saga timeline with a new frame.
|
|
2006
2287
|
*/
|
|
2007
|
-
|
|
2288
|
+
private async evolveSyncSagaIbGib({
|
|
2008
2289
|
prevSagaIbGib,
|
|
2009
2290
|
conflictStrategy,
|
|
2010
2291
|
msgStones,
|
|
@@ -2128,7 +2409,7 @@ export class SyncSagaCoordinator {
|
|
|
2128
2409
|
}
|
|
2129
2410
|
}
|
|
2130
2411
|
|
|
2131
|
-
|
|
2412
|
+
private async getStageAndPayloadFromFrame({
|
|
2132
2413
|
sagaFrame,
|
|
2133
2414
|
space
|
|
2134
2415
|
}: {
|
|
@@ -2164,7 +2445,7 @@ export class SyncSagaCoordinator {
|
|
|
2164
2445
|
}
|
|
2165
2446
|
}
|
|
2166
2447
|
|
|
2167
|
-
|
|
2448
|
+
private sortTimelinesTopologically(timelines: { [tjp: string]: IbGib_V1[] }): string[] {
|
|
2168
2449
|
const lc = `${this.lc}[${this.sortTimelinesTopologically.name}]`;
|
|
2169
2450
|
const tjps = Object.keys(timelines);
|
|
2170
2451
|
if (tjps.length === 0) { return []; }
|