@ibgib/core-gib 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sync/sync-innerspace.respec.mjs +328 -165
- package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
- package/dist/sync/sync-saga-coordinator.d.mts +57 -2
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +346 -153
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
- package/dist/witness/space/inner-space/inner-space-v1.d.mts.map +1 -1
- package/dist/witness/space/inner-space/inner-space-v1.mjs +3 -4
- package/dist/witness/space/inner-space/inner-space-v1.mjs.map +1 -1
- package/package.json +1 -1
- package/src/sync/sync-innerspace.respec.mts +359 -191
- package/src/sync/sync-saga-coordinator.mts +411 -161
- package/src/witness/space/inner-space/inner-space-v1.mts +3 -2
- package/src/witness/space/reconciliation-space/reconciliation-space-base.mts.OLD.md +884 -0
- package/src/witness/space/reconciliation-space/reconciliation-space-helper.mts.OLD.md +125 -0
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
extractErrorMsg,
|
|
3
3
|
getUUID, // so our uuid's are uniform across all ibgib code
|
|
4
4
|
getTimestamp, // so our timestamp strings are uniform
|
|
5
|
-
getTimestampInTicks // so our timestamp in ticks as a string are uniform
|
|
5
|
+
getTimestampInTicks, // so our timestamp in ticks as a string are uniform
|
|
6
|
+
pretty,
|
|
7
|
+
clone
|
|
6
8
|
} from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
7
9
|
import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
|
|
8
10
|
import { splitPerTjpAndOrDna, getTimelinesGroupedByTjp } from "../common/other/ibgib-helper.mjs";
|
|
@@ -12,7 +14,7 @@ import { isPrimitive } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.m
|
|
|
12
14
|
|
|
13
15
|
import { GLOBAL_LOG_A_LOT } from "../core-constants.mjs";
|
|
14
16
|
import { IbGibSpaceAny } from "../witness/space/space-base-v1.mjs";
|
|
15
|
-
import { putInSpace, lockSpace, unlockSpace, getSpaceArgMetadata, getLatestAddrs } from "../witness/space/space-helper.mjs";
|
|
17
|
+
import { putInSpace, lockSpace, unlockSpace, getSpaceArgMetadata, getLatestAddrs, getFromSpace } from "../witness/space/space-helper.mjs";
|
|
16
18
|
import { KeystoneIbGib_V1 } from "../keystone/keystone-types.mjs";
|
|
17
19
|
import { KeystoneService_V1 } from "../keystone/keystone-service-v1.mjs";
|
|
18
20
|
import { SyncStage, SYNC_ATOM } from "./sync-constants.mjs";
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
import { getSyncIb } from "./sync-helpers.mjs";
|
|
23
25
|
import { getDependencyGraph } from "../common/other/graph-helper.mjs";
|
|
24
26
|
|
|
25
|
-
const logalot = GLOBAL_LOG_A_LOT;
|
|
27
|
+
const logalot = GLOBAL_LOG_A_LOT || true;
|
|
26
28
|
|
|
27
29
|
export interface SyncOptions {
|
|
28
30
|
/**
|
|
@@ -41,8 +43,19 @@ export interface SyncOptions {
|
|
|
41
43
|
* The secret for the identity (to sign the commit).
|
|
42
44
|
*/
|
|
43
45
|
identitySecret: string;
|
|
46
|
+
/**
|
|
47
|
+
* How to handle conflicts when both Source and Dest have diverged on the same timeline.
|
|
48
|
+
* @default 'abort'
|
|
49
|
+
*/
|
|
50
|
+
conflictStrategy?: SyncConflictStrategy;
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
export type SyncConflictStrategy =
|
|
54
|
+
| 'abort' // Throw error on divergence
|
|
55
|
+
| 'optimistic' // Attempt to merge (e.g. valid if CRDT/DAG based)
|
|
56
|
+
| 'manual'; // (Future) Store conflict and defer
|
|
57
|
+
|
|
58
|
+
|
|
46
59
|
/**
|
|
47
60
|
* Orchestrates the synchronization process between two spaces (Source and Destination).
|
|
48
61
|
*
|
|
@@ -122,13 +135,15 @@ export class SyncSagaCoordinator {
|
|
|
122
135
|
dest,
|
|
123
136
|
identity,
|
|
124
137
|
domainIbGibs,
|
|
125
|
-
identitySecret
|
|
138
|
+
identitySecret,
|
|
139
|
+
conflictStrategy
|
|
126
140
|
}: {
|
|
127
141
|
source: IbGibSpaceAny,
|
|
128
142
|
dest: IbGibSpaceAny,
|
|
129
143
|
identity: KeystoneIbGib_V1,
|
|
130
144
|
domainIbGibs: IbGib_V1[],
|
|
131
145
|
identitySecret?: string,
|
|
146
|
+
conflictStrategy?: SyncConflictStrategy,
|
|
132
147
|
}): Promise<void> {
|
|
133
148
|
const lc = `${this.lc}[${this.sync.name}]`;
|
|
134
149
|
try {
|
|
@@ -145,136 +160,60 @@ export class SyncSagaCoordinator {
|
|
|
145
160
|
// ---------------------------------------------------------
|
|
146
161
|
// 1. Graph Generation & Classification
|
|
147
162
|
// ---------------------------------------------------------
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ibGibs: domainIbGibs,
|
|
152
|
-
live: true,
|
|
153
|
-
space: source,
|
|
163
|
+
const { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph } = await this.analyzeTimelines({
|
|
164
|
+
domainIbGibs,
|
|
165
|
+
space: source
|
|
154
166
|
});
|
|
155
|
-
const validGraph = graph && Object.keys(graph).length > 0;
|
|
156
|
-
if (logalot) { console.log(`${lc} graph generated. nodes: ${validGraph ? Object.keys(graph).length : 0}`); }
|
|
157
|
-
|
|
158
|
-
// 2. Classification
|
|
159
|
-
// We differentiate between "Stones" (Immutable, no timeline) and "Living" (Timelines).
|
|
160
|
-
// Stones can be synced in bulk without ordering.
|
|
161
|
-
// Living ibGibs must be grouped by timeline and ordered.
|
|
162
|
-
const ibGibs = validGraph ? Object.values(graph) : [];
|
|
163
|
-
const { mapWithTjp_YesDna, mapWithTjp_NoDna, mapWithoutTjps } = splitPerTjpAndOrDna({ ibGibs });
|
|
164
|
-
|
|
165
|
-
// "Stones" come from mapWithoutTjps
|
|
166
|
-
const stones = Object.values(mapWithoutTjps);
|
|
167
|
-
|
|
168
|
-
// "Living" come from both TJP maps
|
|
169
|
-
// We combine them to group by TJP
|
|
170
|
-
const living = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
|
|
171
|
-
|
|
172
|
-
if (logalot) {
|
|
173
|
-
console.log(`${lc} classification results (count):`);
|
|
174
|
-
console.log(`stones: ${stones.length}`);
|
|
175
|
-
console.log(`living: ${living.length}`);
|
|
176
|
-
}
|
|
177
167
|
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// todo: perform topological sort on timelinesMap based on inter-timeline references.
|
|
184
|
-
// For now, we sync independent timelines.
|
|
185
|
-
|
|
186
|
-
const payloadAddrs: string[] = [];
|
|
187
|
-
payloadAddrs.push(...stones.map(x => getIbGibAddr({ ibGib: x })));
|
|
188
|
-
// 3. Timeline Ordering (Topological Sort)
|
|
189
|
-
// Living ibGibs are grouped by timeline (TJP).
|
|
190
|
-
// We need to order these timelines based on dependencies.
|
|
191
|
-
const sortedTjps = this.sortTimelinesTopologically(timelinesMap);
|
|
192
|
-
|
|
193
|
-
// Add ordered timelines to payload
|
|
194
|
-
sortedTjps.forEach(tjp => {
|
|
195
|
-
const timeline = timelinesMap[tjp];
|
|
196
|
-
payloadAddrs.push(...timeline.map(x => getIbGibAddr({ ibGib: x })));
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// 2. INIT & Phase 2: Knowledge Exchange
|
|
168
|
+
// ---------------------------------------------------------
|
|
169
|
+
// 2. Knowledge Exchange (Init) & 3. Conflict Resolution
|
|
170
|
+
// ---------------------------------------------------------
|
|
171
|
+
// Phase 1: Init Data
|
|
200
172
|
const srcKnowledgeVector: { [tjp: string]: string } = {};
|
|
201
|
-
Object.keys(
|
|
202
|
-
const timeline =
|
|
203
|
-
const tip = timeline
|
|
173
|
+
Object.keys(srcTimelinesMap).forEach(tjp => {
|
|
174
|
+
const timeline = srcTimelinesMap[tjp];
|
|
175
|
+
const tip = timeline.at(-1)!;
|
|
204
176
|
srcKnowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
|
|
205
177
|
});
|
|
206
178
|
|
|
179
|
+
// Need to determine mode correctly. Just defaulting to push for now
|
|
180
|
+
// as per original impl instructions (or TODO)
|
|
207
181
|
const initData: SyncInitData = {
|
|
208
182
|
knowledgeVector: srcKnowledgeVector,
|
|
209
183
|
identity: identity,
|
|
210
184
|
mode: 'push'
|
|
211
185
|
};
|
|
212
186
|
|
|
213
|
-
// Get Source Knowledge (Time -> Latest)
|
|
214
|
-
const srcTjps = Object.keys(timelinesMap);
|
|
215
|
-
// Get Dest Knowledge
|
|
216
|
-
const destKnowledge = await this.getLatestAddrsFromSpace({ space: dest, tjpAddrs: srcTjps });
|
|
217
|
-
|
|
218
|
-
const conflicts: string[] = [];
|
|
219
|
-
for (const tjp of srcTjps) {
|
|
220
|
-
const srcTimeline = timelinesMap[tjp];
|
|
221
|
-
const srcTip = srcTimeline[srcTimeline.length - 1];
|
|
222
|
-
const srcTipAddr = getIbGibAddr({ ibGib: srcTip });
|
|
223
|
-
const destTipAddr = destKnowledge[tjp];
|
|
224
|
-
|
|
225
|
-
if (destTipAddr && destTipAddr !== srcTipAddr) {
|
|
226
|
-
// Divergence found. Check if Source knows Dest's tip.
|
|
227
|
-
if (graph[destTipAddr]) {
|
|
228
|
-
// Fast-Forward: Dest tip IS in our graph, so we are ahead.
|
|
229
|
-
// No action needed, proceed to push.
|
|
230
|
-
} else {
|
|
231
|
-
// Conflict: Dest has a tip we don't know about.
|
|
232
|
-
conflicts.push(tjp);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Lock conflicts
|
|
238
|
-
for (const tjp of conflicts) {
|
|
239
|
-
if (logalot) { console.log(`${lc} Conflict detected for TJP ${tjp}. Acquiring lock...`); }
|
|
240
|
-
// Lock VALIDITY: 60 seconds?
|
|
241
|
-
await lockSpace({
|
|
242
|
-
space: dest,
|
|
243
|
-
scope: tjp,
|
|
244
|
-
secondsValid: 60,
|
|
245
|
-
instanceId: uuid,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// todo: Implement Pull & Merge here
|
|
249
|
-
// 1. Pull delta from Dest (destTip -> ... -> common ancestor)
|
|
250
|
-
// 2. Merge locally
|
|
251
|
-
// 3. Update graph/timelinesMap with merged result
|
|
252
|
-
|
|
253
|
-
if (logalot) { console.warn(`${lc} Merge not fully implemented yet. Releasing lock.`); }
|
|
254
|
-
|
|
255
|
-
await unlockSpace({
|
|
256
|
-
space: dest,
|
|
257
|
-
scope: tjp,
|
|
258
|
-
instanceId: uuid,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
187
|
const initFrame = await this.createSyncFrame({
|
|
263
188
|
uuid,
|
|
264
189
|
stage: SyncStage.init,
|
|
265
190
|
payload: initData,
|
|
266
191
|
identity
|
|
267
192
|
});
|
|
268
|
-
|
|
269
|
-
// Persist Init to Dest (Handshake)
|
|
270
193
|
await putInSpace({ space: dest, ibGib: initFrame });
|
|
271
194
|
|
|
272
|
-
//
|
|
273
|
-
|
|
195
|
+
// Phase 2: Resolving Conflicts (Pull/Merge)
|
|
196
|
+
await this.resolveConflicts({
|
|
197
|
+
source,
|
|
198
|
+
dest,
|
|
199
|
+
srcTimelinesMap,
|
|
200
|
+
srcKnowledgeVector,
|
|
201
|
+
srcGraph,
|
|
202
|
+
uuid,
|
|
203
|
+
conflictStrategy: conflictStrategy || 'abort',
|
|
204
|
+
});
|
|
274
205
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
206
|
+
// ---------------------------------------------------------
|
|
207
|
+
// 4. Batch Stream (Delta)
|
|
208
|
+
// ---------------------------------------------------------
|
|
209
|
+
const payloadAddrs: string[] = [];
|
|
210
|
+
payloadAddrs.push(...srcStones.map(x => getIbGibAddr({ ibGib: x })));
|
|
211
|
+
srcSortedTjps.forEach(tjp => {
|
|
212
|
+
const timeline = srcTimelinesMap[tjp];
|
|
213
|
+
payloadAddrs.push(...timeline.map(x => getIbGibAddr({ ibGib: x })));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const deltaData: SyncDeltaData = { payloadAddrs };
|
|
278
217
|
const deltaFrame = await this.createSyncFrame({
|
|
279
218
|
uuid,
|
|
280
219
|
stage: SyncStage.delta,
|
|
@@ -283,32 +222,17 @@ export class SyncSagaCoordinator {
|
|
|
283
222
|
});
|
|
284
223
|
await putInSpace({ space: dest, ibGib: deltaFrame });
|
|
285
224
|
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const chunkIbGibs = chunkAddrs
|
|
293
|
-
.map(addr => {
|
|
294
|
-
const ibGib = graph[addr];
|
|
295
|
-
if (!ibGib) { throw new Error(`(UNEXPECTED) addr not found in graph: ${addr} (E: 8a402324057849318625aa85721665a5)`); }
|
|
296
|
-
return ibGib;
|
|
297
|
-
})
|
|
298
|
-
.filter(ibGib => !isPrimitive({ ibGib }));
|
|
299
|
-
|
|
300
|
-
if (chunkIbGibs.length > 0) {
|
|
301
|
-
if (logalot) { console.log(`${lc} streaming batch ${Math.ceil(i / BATCH_SIZE) + 1} (${chunkIbGibs.length} ibGibs)...`); }
|
|
302
|
-
// We don't set isDna: true here because the batch might contain mixed types.
|
|
303
|
-
// The Space implementation should handle routing based on internal metadata if needed.
|
|
304
|
-
await putInSpace({ space: dest, ibGibs: chunkIbGibs });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
225
|
+
// Streaming Logic
|
|
226
|
+
await this.processBatchStream({
|
|
227
|
+
dest,
|
|
228
|
+
payloadAddrs,
|
|
229
|
+
srcGraph
|
|
230
|
+
});
|
|
307
231
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
};
|
|
232
|
+
// ---------------------------------------------------------
|
|
233
|
+
// 5. Commit
|
|
234
|
+
// ---------------------------------------------------------
|
|
235
|
+
const commitData: SyncCommitData = { success: true };
|
|
312
236
|
const commitFrame = await this.createSyncFrame({
|
|
313
237
|
uuid,
|
|
314
238
|
stage: SyncStage.commit,
|
|
@@ -325,6 +249,273 @@ export class SyncSagaCoordinator {
|
|
|
325
249
|
}
|
|
326
250
|
}
|
|
327
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Analyses the timelines in the provided graph, classifying them into stones and living timelines,
|
|
254
|
+
* and performing topological sort on the timelines.
|
|
255
|
+
*/
|
|
256
|
+
protected async analyzeTimelines({
|
|
257
|
+
domainIbGibs,
|
|
258
|
+
space,
|
|
259
|
+
}: {
|
|
260
|
+
domainIbGibs: IbGib_V1[],
|
|
261
|
+
space: IbGibSpaceAny,
|
|
262
|
+
}): Promise<{
|
|
263
|
+
srcStones: IbGib_V1[],
|
|
264
|
+
srcTimelinesMap: { [tjp: string]: IbGib_V1[] },
|
|
265
|
+
srcSortedTjps: string[],
|
|
266
|
+
srcGraph: { [addr: string]: IbGib_V1 },
|
|
267
|
+
}> {
|
|
268
|
+
const lc = `${this.lc}[${this.analyzeTimelines.name}]`;
|
|
269
|
+
const srcGraph = await getDependencyGraph({
|
|
270
|
+
ibGibs: domainIbGibs,
|
|
271
|
+
live: true,
|
|
272
|
+
space,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const srcGraphIsValid = srcGraph && Object.keys(srcGraph).length > 0;
|
|
276
|
+
if (logalot) { console.log(`${lc} graph generated. nodes: ${srcGraphIsValid ? Object.keys(srcGraph).length : 0}`); }
|
|
277
|
+
|
|
278
|
+
const srcIbGibs = srcGraphIsValid ? Object.values(srcGraph) : [];
|
|
279
|
+
const {
|
|
280
|
+
mapWithTjp_YesDna: srcMapWithTjp_YesDna,
|
|
281
|
+
mapWithTjp_NoDna: srcMapWithTjp_NoDna,
|
|
282
|
+
mapWithoutTjps: src_MapWithoutTjps
|
|
283
|
+
} = splitPerTjpAndOrDna({ ibGibs: srcIbGibs });
|
|
284
|
+
|
|
285
|
+
const srcStones = Object.values(src_MapWithoutTjps);
|
|
286
|
+
const srcLiving = [...Object.values(srcMapWithTjp_YesDna), ...Object.values(srcMapWithTjp_NoDna)];
|
|
287
|
+
|
|
288
|
+
if (logalot) {
|
|
289
|
+
console.log(`${lc} classification results (count):`);
|
|
290
|
+
console.log(`srcStones.length: ${srcStones.length}`);
|
|
291
|
+
console.log(`srcLiving.length: ${srcLiving.length}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const srcTimelinesMap = getTimelinesGroupedByTjp({ ibGibs: srcLiving });
|
|
295
|
+
const srcSortedTjps = this.sortTimelinesTopologically(srcTimelinesMap);
|
|
296
|
+
|
|
297
|
+
return { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
protected async resolveConflicts({
|
|
301
|
+
source,
|
|
302
|
+
dest,
|
|
303
|
+
srcTimelinesMap,
|
|
304
|
+
srcKnowledgeVector,
|
|
305
|
+
srcGraph,
|
|
306
|
+
uuid,
|
|
307
|
+
conflictStrategy,
|
|
308
|
+
}: {
|
|
309
|
+
source: IbGibSpaceAny,
|
|
310
|
+
dest: IbGibSpaceAny,
|
|
311
|
+
srcTimelinesMap: { [tjp: string]: IbGib_V1[] },
|
|
312
|
+
srcKnowledgeVector: { [tjp: string]: string },
|
|
313
|
+
srcGraph: { [addr: string]: IbGib_V1 },
|
|
314
|
+
uuid: string,
|
|
315
|
+
conflictStrategy: SyncConflictStrategy,
|
|
316
|
+
}): Promise<void> {
|
|
317
|
+
const lc = `${this.lc}[${this.resolveConflicts.name}]`;
|
|
318
|
+
|
|
319
|
+
const srcTjps = Object.keys(srcTimelinesMap);
|
|
320
|
+
if (logalot) { console.log(`${lc} getting latest addrs from space as destKnowledge. srcTjps: ${srcTjps} (I: a954369f2b58843a6773b00dd18eb325)`); }
|
|
321
|
+
|
|
322
|
+
// Get Dest Knowledge
|
|
323
|
+
const destKnowledge = await this.getLatestAddrsFromSpace({ space: dest, tjpAddrs: srcTjps });
|
|
324
|
+
if (logalot) { console.log(`${lc} destKnowledge:\n${pretty(destKnowledge)} (I: f76108b5f228f4e2e1e79ad387deb325)`); }
|
|
325
|
+
|
|
326
|
+
const conflicts: string[] = [];
|
|
327
|
+
for (const tjp of srcTjps) {
|
|
328
|
+
const srcTimeline = srcTimelinesMap[tjp];
|
|
329
|
+
const srcTip = srcTimeline[srcTimeline.length - 1];
|
|
330
|
+
const srcTipAddr = getIbGibAddr({ ibGib: srcTip });
|
|
331
|
+
const destTipAddr = destKnowledge[tjp];
|
|
332
|
+
|
|
333
|
+
if (logalot) {
|
|
334
|
+
console.log(`${lc} Checking TJP: ${tjp}`);
|
|
335
|
+
console.log(`${lc} Source Tip: ${srcTipAddr}`);
|
|
336
|
+
console.log(`${lc} Dest Tip: ${destTipAddr}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (destTipAddr && destTipAddr !== srcTipAddr) {
|
|
340
|
+
if (logalot) { console.log(`${lc} Divergence found. Check if Source knows Dest's tip. (I: d084d8abb6a84cc4a8301b51dec98825)`); }
|
|
341
|
+
if (srcGraph[destTipAddr]) {
|
|
342
|
+
// Fast-Forward: Dest tip IS in our graph, so we are ahead.
|
|
343
|
+
// No action needed, proceed to push.
|
|
344
|
+
if (logalot) { console.log(`${lc} Fast-Forward (Source is ahead).`); }
|
|
345
|
+
} else {
|
|
346
|
+
// Conflict: Dest has a tip we don't know about.
|
|
347
|
+
conflicts.push(tjp);
|
|
348
|
+
if (logalot) { console.log(`${lc} CONFLICT detected (Desc is ahead/divergent).`); }
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
if (logalot) { console.log(`${lc} No divergence detected (Tips match or Dest unknown).`); }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Lock conflicts
|
|
356
|
+
for (const tjp of conflicts) {
|
|
357
|
+
if (logalot) { console.log(`${lc} Conflict detected for TJP ${tjp}. Acquiring lock...`); }
|
|
358
|
+
// Lock VALIDITY: 60 seconds?
|
|
359
|
+
await lockSpace({
|
|
360
|
+
space: dest,
|
|
361
|
+
scope: tjp,
|
|
362
|
+
secondsValid: 60,
|
|
363
|
+
instanceId: uuid,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Pull & Merge Logic
|
|
367
|
+
try {
|
|
368
|
+
const destTipAddr = destKnowledge[tjp];
|
|
369
|
+
if (!destTipAddr) {
|
|
370
|
+
console.warn(`${lc} destTipAddr falsy in conflict loop? skipping. (W: 1a2b3c)`);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const srcTipAddr = srcKnowledgeVector[tjp];
|
|
374
|
+
|
|
375
|
+
if (logalot) { console.log(`${lc} Pulling TJP ${tjp} from Dest...`); }
|
|
376
|
+
|
|
377
|
+
// 1. Get Delta from Dest
|
|
378
|
+
// We want everything from Dest's tip back to what Source has.
|
|
379
|
+
// We can use getDependencyGraph with 'skipAddrs' if we know what to skip.
|
|
380
|
+
// Or just get the graph for the tip and filter?
|
|
381
|
+
// Graph helper is robust. Let's try to get the graph stopping at Source's tip.
|
|
382
|
+
|
|
383
|
+
// Optimization: We can't easily pass "stop at" to getDependencyGraph directly like "stopAt: [srcTipAddr]",
|
|
384
|
+
// but we can pass `gotten` or `skipAddrs`.
|
|
385
|
+
// If we pass `skipAddrs: [srcTipAddr]`, it should prune the graph traversal beyond that point?
|
|
386
|
+
// Actually `skipAddrs` prevents retrieving that node AND its past. So yes!
|
|
387
|
+
|
|
388
|
+
const skipAddrs: string[] = [];
|
|
389
|
+
if (srcTipAddr) { skipAddrs.push(srcTipAddr); }
|
|
390
|
+
|
|
391
|
+
const deltaGraph = await getDependencyGraph({
|
|
392
|
+
space: dest,
|
|
393
|
+
ibGibs: [],
|
|
394
|
+
ibGibAddrs: [destTipAddr],
|
|
395
|
+
live: false, // We usually just want the static graph back to the divergence point for now? Or live? Live is safer but slower.
|
|
396
|
+
// If Dest is ahead, its tip IS the latest.
|
|
397
|
+
skipAddrs,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (logalot) { console.log(`${lc} Delta graph size: ${Object.keys(deltaGraph).length}`); }
|
|
401
|
+
|
|
402
|
+
// 2. Put Delta into Source
|
|
403
|
+
const deltaIbGibs = Object.values(deltaGraph);
|
|
404
|
+
if (deltaIbGibs.length > 0) {
|
|
405
|
+
if (logalot) {
|
|
406
|
+
console.log(`${lc} Putting ${deltaIbGibs.length} ibGibs into Source (UUID: ${(source as any).data?.uuid})...`);
|
|
407
|
+
console.log(`${lc} Delta addrs: ${deltaIbGibs.map(x => getIbGibAddr({ ibGib: x })).join(', ')}`);
|
|
408
|
+
}
|
|
409
|
+
await putInSpace({ space: source, ibGibs: deltaIbGibs });
|
|
410
|
+
if (logalot) { console.log(`${lc} Put complete.`); }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 3. Re-evaluate / Merge
|
|
414
|
+
// Now Source has destTipAddr.
|
|
415
|
+
// If Source was strictly behind, srcTipAddr was an ancestor of destTipAddr.
|
|
416
|
+
// Source is now effectively fast-forwarded to destTipAddr.
|
|
417
|
+
|
|
418
|
+
// We need to check if we need to do a "Merge" (i.e. if Source HAD changes that are not in Dest).
|
|
419
|
+
// If srcTipAddr was in deltaGraph (Wait, we skipped it), or rather if srcTipAddr is an ancestor of destTip.
|
|
420
|
+
// If `skipAddrs` worked, we stopped AT srcTipAddr.
|
|
421
|
+
|
|
422
|
+
// Ideally check if `destTipAddr` reaches `srcTipAddr`.
|
|
423
|
+
// For now, in "Dest Ahead" scenario, we just assume we updated Source.
|
|
424
|
+
// We update our local tracking to proceed with potential Pushing back (if we were merging).
|
|
425
|
+
|
|
426
|
+
// If we simply pulled, do we need to push back?
|
|
427
|
+
// If Dest was strictly ahead, Source catch up. Source == Dest. No Push needed.
|
|
428
|
+
// Logic:
|
|
429
|
+
// If Source now has destTipAddr as Latest, we are good.
|
|
430
|
+
|
|
431
|
+
// TODO: Handle actual DIVERGENCE (Merge).
|
|
432
|
+
// This implementation covers "Dest Ahead" (Fast-Backward).
|
|
433
|
+
// But if we truly diverged (Source has changes not in Dest, and Dest has changes not in Source),
|
|
434
|
+
// checks are needed.
|
|
435
|
+
|
|
436
|
+
// If conflictStrategy is abort, we should probably check if `srcTipAddr` is an ancestor of `destTipAddr`.
|
|
437
|
+
// If `srcTipAddr` is NOT an ancestor, then we have a divergence.
|
|
438
|
+
// Getting ancestry is expensive without graph traversal.
|
|
439
|
+
// But we just pulled `deltaGraph`. If `srcTipAddr` was `skipped` during traversal,
|
|
440
|
+
// it implies it WAS reachable from `destTipAddr`.
|
|
441
|
+
// Wait, `skipAddrs` just stops traversal. It doesn't confirm reachability if we assume it stops *at* the addr.
|
|
442
|
+
// Actually `getDependencyGraph` with `skipAddrs` stops descending if it hits a skipped addr.
|
|
443
|
+
// If it hits it, it means `srcTipAddr` IS an ancestor.
|
|
444
|
+
// If it never hits it, `srcTipAddr` might NOT be an ancestor (divergence).
|
|
445
|
+
// Or `srcTipAddr` is just disjoint?
|
|
446
|
+
|
|
447
|
+
// Let's rely on the fact that if we simply "Pull", we are essentially overwriting Source's "Latest" pointer
|
|
448
|
+
// if we don't do anything else.
|
|
449
|
+
// If Source had 'A', and Dest had 'B', and we pull 'B'. Source now has 'A' and 'B'.
|
|
450
|
+
// If we don't merge, Source is now just "containing" B.
|
|
451
|
+
|
|
452
|
+
if (conflictStrategy === 'abort') {
|
|
453
|
+
// Verify if it's a true divergence or just a Fast-Forward we haven't tracked yet.
|
|
454
|
+
// If srcTipAddr is an ancestor of destTipAddr, it is a valid FF.
|
|
455
|
+
if (srcTipAddr) {
|
|
456
|
+
const isFF = await this.isAncestor({
|
|
457
|
+
ancestorAddr: srcTipAddr,
|
|
458
|
+
descendantAddr: destTipAddr,
|
|
459
|
+
space: source
|
|
460
|
+
});
|
|
461
|
+
if (isFF) {
|
|
462
|
+
if (logalot) { console.log(`${lc} Verified Fast-Forward/Pull (srcTip is ancestor). No abort.`); }
|
|
463
|
+
} else {
|
|
464
|
+
throw new Error(`Sync Aborted: Divergence detected on TJP ${tjp}. Source Tip: ${srcTipAddr}, Dest Tip: ${destTipAddr}. Strategy is 'abort'.`);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// source has no tip (new timeline for source), so it's a valid pull.
|
|
468
|
+
}
|
|
469
|
+
} else if (conflictStrategy === 'optimistic') {
|
|
470
|
+
console.warn(`${lc} Optimistic merge not yet fully implemented. Proceeding with Pulled data but Source Tip is still the effective 'latest' for this session unless merged. (W: todo_merge)`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
} catch (err) {
|
|
474
|
+
console.error(`${lc} Error dealing with conflict/pull for TJP ${tjp}: ${extractErrorMsg(err)}`);
|
|
475
|
+
if (conflictStrategy === 'abort') throw err;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
await unlockSpace({
|
|
479
|
+
space: dest,
|
|
480
|
+
scope: tjp,
|
|
481
|
+
instanceId: uuid,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
protected async processBatchStream({
|
|
487
|
+
dest,
|
|
488
|
+
payloadAddrs,
|
|
489
|
+
srcGraph
|
|
490
|
+
}: {
|
|
491
|
+
dest: IbGibSpaceAny,
|
|
492
|
+
payloadAddrs: string[],
|
|
493
|
+
srcGraph: { [addr: string]: IbGib_V1 }
|
|
494
|
+
}): Promise<void> {
|
|
495
|
+
const lc = `${this.lc}[${this.processBatchStream.name}]`;
|
|
496
|
+
// 3.1 Stream the actual data (Batch Stream)
|
|
497
|
+
// We must preserve the order in payloadAddrs to respect dependencies.
|
|
498
|
+
// We batch them to improve throughput.
|
|
499
|
+
const BATCH_SIZE = 20;
|
|
500
|
+
for (let i = 0; i < payloadAddrs.length; i += BATCH_SIZE) {
|
|
501
|
+
const chunkAddrs = payloadAddrs.slice(i, i + BATCH_SIZE);
|
|
502
|
+
const chunkIbGibs = chunkAddrs
|
|
503
|
+
.map(addr => {
|
|
504
|
+
const ibGib = srcGraph[addr];
|
|
505
|
+
if (!ibGib) { throw new Error(`(UNEXPECTED) addr not found in graph: ${addr} (E: 8a402324057849318625aa85721665a5)`); }
|
|
506
|
+
return ibGib;
|
|
507
|
+
})
|
|
508
|
+
.filter(ibGib => !isPrimitive({ ibGib }));
|
|
509
|
+
|
|
510
|
+
if (chunkIbGibs.length > 0) {
|
|
511
|
+
if (logalot) { console.log(`${lc} streaming batch ${Math.ceil(i / BATCH_SIZE) + 1} (${chunkIbGibs.length} ibGibs)...`); }
|
|
512
|
+
// We don't set isDna: true here because the batch might contain mixed types.
|
|
513
|
+
// The Space implementation should handle routing based on internal metadata if needed.
|
|
514
|
+
await putInSpace({ space: dest, ibGibs: chunkIbGibs });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
328
519
|
protected async createSyncFrame({
|
|
329
520
|
uuid,
|
|
330
521
|
stage,
|
|
@@ -377,34 +568,49 @@ export class SyncSagaCoordinator {
|
|
|
377
568
|
}: {
|
|
378
569
|
space: IbGibSpaceAny,
|
|
379
570
|
tjpAddrs: string[],
|
|
380
|
-
}): Promise<{ [tjpAddr: string]: string }> {
|
|
571
|
+
}): Promise<{ [tjpAddr: string]: string | null }> {
|
|
381
572
|
const lc = `${this.lc}[${this.getLatestAddrsFromSpace.name}]`;
|
|
382
573
|
try {
|
|
383
|
-
if (tjpAddrs.length === 0) {
|
|
574
|
+
if (tjpAddrs.length === 0) {
|
|
575
|
+
console.warn(`${lc} getting latest addrs for empty tjpAddrs? this may be reasonable but sounds odd. (W: 2f57c6d7c195034e982dfd38201db825)`);
|
|
576
|
+
return {};
|
|
577
|
+
}
|
|
384
578
|
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
cmd: 'get',
|
|
390
|
-
cmdModifiers: ['latest', 'addrs'],
|
|
391
|
-
ibGibAddrs: tjpAddrs,
|
|
392
|
-
}
|
|
579
|
+
let map: { [tjp: string]: string | null } = {};
|
|
580
|
+
const resLatest = await getLatestAddrs({
|
|
581
|
+
tjpAddrs,
|
|
582
|
+
space,
|
|
393
583
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
// verification is hard without specific return map
|
|
400
|
-
if (result.data.addrs.length === tjpAddrs.length) {
|
|
401
|
-
for (let i = 0; i < tjpAddrs.length; i++) {
|
|
402
|
-
map[tjpAddrs[i]] = result.data.addrs[i];
|
|
403
|
-
}
|
|
404
|
-
} else {
|
|
405
|
-
console.warn(`${lc} result addrs length mismatch. requested: ${tjpAddrs.length}, got: ${result.data.addrs.length}. Mapping might be unsafe.`);
|
|
406
|
-
}
|
|
584
|
+
if (!resLatest.data) { throw new Error(`(UNEXPECTED) resLatest.data falsy? (E: 289dd61a79fa33a4f87baaa83e3c0825)`); }
|
|
585
|
+
if (resLatest.data.success) {
|
|
586
|
+
map = clone(resLatest.data.latestAddrsMap) as { [key: string]: string | null };
|
|
587
|
+
} else {
|
|
588
|
+
tjpAddrs.forEach(x => { map[x] = null; });
|
|
407
589
|
}
|
|
590
|
+
|
|
591
|
+
// Using the space's witness/argy pattern to invoke 'get' with 'latest' modifier
|
|
592
|
+
// const arg = await space.argy({
|
|
593
|
+
// ibMetadata: getSpaceArgMetadata({ space }),
|
|
594
|
+
// argData: {
|
|
595
|
+
// cmd: 'get',
|
|
596
|
+
// cmdModifiers: ['latest', 'addrs'],
|
|
597
|
+
// ibGibAddrs: tjpAddrs,
|
|
598
|
+
// }
|
|
599
|
+
// });
|
|
600
|
+
// const result = await space.witness(arg);
|
|
601
|
+
|
|
602
|
+
// const map: { [tjp: string]: string } = {};
|
|
603
|
+
// if (result?.data?.success && result.data.addrs) {
|
|
604
|
+
// // assume 1-to-1 mapping order as per convention if successful
|
|
605
|
+
// // verification is hard without specific return map
|
|
606
|
+
// if (result.data.addrs.length === tjpAddrs.length) {
|
|
607
|
+
// for (let i = 0; i < tjpAddrs.length; i++) {
|
|
608
|
+
// map[tjpAddrs[i]] = result.data.addrs[i];
|
|
609
|
+
// }
|
|
610
|
+
// } else {
|
|
611
|
+
// console.warn(`${lc} result addrs length mismatch. requested: ${tjpAddrs.length}, got: ${result.data.addrs.length}. Mapping might be unsafe.`);
|
|
612
|
+
// }
|
|
613
|
+
// }
|
|
408
614
|
return map;
|
|
409
615
|
} catch (error) {
|
|
410
616
|
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
@@ -474,4 +680,48 @@ export class SyncSagaCoordinator {
|
|
|
474
680
|
|
|
475
681
|
return sorted;
|
|
476
682
|
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Checks if an address is an ancestor of another by traversing `past` relations.
|
|
686
|
+
* Uses BFS. Returns true if `ancestorAddr` is found in the history of `descendantAddr`.
|
|
687
|
+
*/
|
|
688
|
+
protected async isAncestor({
|
|
689
|
+
ancestorAddr,
|
|
690
|
+
descendantAddr,
|
|
691
|
+
space,
|
|
692
|
+
}: {
|
|
693
|
+
ancestorAddr: string,
|
|
694
|
+
descendantAddr: string,
|
|
695
|
+
space: IbGibSpaceAny,
|
|
696
|
+
}): Promise<boolean> {
|
|
697
|
+
if (ancestorAddr === descendantAddr) return true;
|
|
698
|
+
|
|
699
|
+
const queue = [descendantAddr];
|
|
700
|
+
const visited = new Set<string>();
|
|
701
|
+
|
|
702
|
+
while (queue.length > 0) {
|
|
703
|
+
const currentAddr = queue.shift()!;
|
|
704
|
+
if (visited.has(currentAddr)) continue;
|
|
705
|
+
visited.add(currentAddr);
|
|
706
|
+
|
|
707
|
+
if (currentAddr === ancestorAddr) return true;
|
|
708
|
+
|
|
709
|
+
// Get node to find past
|
|
710
|
+
const res = await getFromSpace({ space, addr: currentAddr });
|
|
711
|
+
if (!res.success || !res.ibGibs || res.ibGibs.length === 0) {
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const node = res.ibGibs[0];
|
|
715
|
+
if (node.rel8ns && node.rel8ns.past) {
|
|
716
|
+
// Check pasts
|
|
717
|
+
for (const pastAddr of node.rel8ns.past) {
|
|
718
|
+
if (!visited.has(pastAddr)) {
|
|
719
|
+
queue.push(pastAddr);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
477
727
|
}
|