@ibgib/core-gib 0.1.11 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/sync/sync-innerspace.respec.mjs +366 -81
  3. package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
  4. package/dist/sync/sync-saga-coordinator.d.mts +57 -2
  5. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  6. package/dist/sync/sync-saga-coordinator.mjs +346 -153
  7. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  8. package/dist/timeline/timeline-api.respec.mjs +34 -7
  9. package/dist/timeline/timeline-api.respec.mjs.map +1 -1
  10. package/dist/witness/space/inner-space/inner-space-v1.d.mts +1 -1
  11. package/dist/witness/space/inner-space/inner-space-v1.d.mts.map +1 -1
  12. package/dist/witness/space/inner-space/inner-space-v1.mjs +7 -7
  13. package/dist/witness/space/inner-space/inner-space-v1.mjs.map +1 -1
  14. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace-helper.d.mts +18 -0
  15. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace-helper.d.mts.map +1 -1
  16. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace-helper.mjs +39 -10
  17. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace-helper.mjs.map +1 -1
  18. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.d.mts.map +1 -1
  19. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.mjs +2 -1
  20. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.mjs.map +1 -1
  21. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.respec.mjs +6 -4
  22. package/dist/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.respec.mjs.map +1 -1
  23. package/dist/witness/space/space-helper.d.mts.map +1 -1
  24. package/dist/witness/space/space-helper.mjs +1 -0
  25. package/dist/witness/space/space-helper.mjs.map +1 -1
  26. package/package.json +1 -1
  27. package/src/sync/sync-innerspace.respec.mts +401 -88
  28. package/src/sync/sync-saga-coordinator.mts +411 -161
  29. package/src/timeline/timeline-api.respec.mts +34 -16
  30. package/src/witness/space/inner-space/inner-space-v1.mts +8 -5
  31. package/src/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace-helper.mts +39 -10
  32. package/src/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.mts +2 -1
  33. package/src/witness/space/metaspace/metaspace-innerspace/metaspace-innerspace.respec.mts +5 -3
  34. package/src/witness/space/reconciliation-space/reconciliation-space-base.mts.OLD.md +884 -0
  35. package/src/witness/space/reconciliation-space/reconciliation-space-helper.mts.OLD.md +125 -0
  36. package/src/witness/space/space-helper.mts +1 -1
@@ -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
- // We want a live dependency graph to ensure we get the latest
149
- // states of timelines.
150
- const graph = await getDependencyGraph({
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
- // 3. Timeline Ordering
179
- // Group living ibGibs by TJP to identify distinct timelines.
180
- // The helper returns them sorted by 'n' ascending.
181
- const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: living });
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(timelinesMap).forEach(tjp => {
202
- const timeline = timelinesMap[tjp];
203
- const tip = timeline[timeline.length - 1];
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
- // 3. DELTA
273
- // const payloadAddrs = [getIbGibAddr({ ibGib: identity })]; // Already computed above
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
- const deltaData: SyncDeltaData = {
276
- payloadAddrs
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
- // 3.1 Stream the actual data (Batch Stream)
287
- // We must preserve the order in payloadAddrs to respect dependencies.
288
- // We batch them to improve throughput.
289
- const BATCH_SIZE = 20;
290
- for (let i = 0; i < payloadAddrs.length; i += BATCH_SIZE) {
291
- const chunkAddrs = payloadAddrs.slice(i, i + BATCH_SIZE);
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
- // 4. COMMIT
309
- const commitData: SyncCommitData = {
310
- success: true
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) { return {}; }
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
- // Using the space's witness/argy pattern to invoke 'get' with 'latest' modifier
386
- const arg = await space.argy({
387
- ibMetadata: getSpaceArgMetadata({ space }),
388
- argData: {
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
- const result = await space.witness(arg);
395
-
396
- const map: { [tjp: string]: string } = {};
397
- if (result?.data?.success && result.data.addrs) {
398
- // assume 1-to-1 mapping order as per convention if successful
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
  }