@ibgib/core-gib 0.1.12 → 0.1.14

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 (106) hide show
  1. package/dist/keystone/keystone-helpers.mjs +3 -3
  2. package/dist/keystone/keystone-helpers.mjs.map +1 -1
  3. package/dist/sync/sync-constants.d.mts +4 -1
  4. package/dist/sync/sync-constants.d.mts.map +1 -1
  5. package/dist/sync/sync-constants.mjs +3 -0
  6. package/dist/sync/sync-constants.mjs.map +1 -1
  7. package/dist/sync/sync-helpers.d.mts +18 -2
  8. package/dist/sync/sync-helpers.d.mts.map +1 -1
  9. package/dist/sync/sync-helpers.mjs +84 -3
  10. package/dist/sync/sync-helpers.mjs.map +1 -1
  11. package/dist/sync/sync-innerspace.respec.d.mts +0 -6
  12. package/dist/sync/sync-innerspace.respec.d.mts.map +1 -1
  13. package/dist/sync/sync-innerspace.respec.mjs +395 -241
  14. package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
  15. package/dist/sync/sync-peer/sync-peer-types.d.mts +31 -0
  16. package/dist/sync/sync-peer/sync-peer-types.d.mts.map +1 -0
  17. package/dist/sync/sync-peer/sync-peer-types.mjs +5 -0
  18. package/dist/sync/sync-peer/sync-peer-types.mjs.map +1 -0
  19. package/dist/sync/sync-peer/sync-peer-v1.d.mts +22 -0
  20. package/dist/sync/sync-peer/sync-peer-v1.d.mts.map +1 -0
  21. package/dist/sync/sync-peer/sync-peer-v1.mjs +13 -0
  22. package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -0
  23. package/dist/sync/sync-saga-context/sync-saga-context-constants.d.mts +8 -0
  24. package/dist/sync/sync-saga-context/sync-saga-context-constants.d.mts.map +1 -0
  25. package/dist/sync/sync-saga-context/sync-saga-context-constants.mjs +8 -0
  26. package/dist/sync/sync-saga-context/sync-saga-context-constants.mjs.map +1 -0
  27. package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts +54 -0
  28. package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts.map +1 -0
  29. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs +87 -0
  30. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs.map +1 -0
  31. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts +66 -0
  32. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts.map +1 -0
  33. package/dist/sync/sync-saga-context/sync-saga-context-types.mjs +12 -0
  34. package/dist/sync/sync-saga-context/sync-saga-context-types.mjs.map +1 -0
  35. package/dist/sync/sync-saga-coordinator.d.mts +136 -91
  36. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  37. package/dist/sync/sync-saga-coordinator.mjs +563 -287
  38. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  39. package/dist/sync/sync-saga-coordinator.respec.mjs +7 -7
  40. package/dist/sync/sync-saga-coordinator.respec.mjs.map +1 -1
  41. package/dist/sync/sync-saga-message/sync-saga-message-constants.d.mts +2 -0
  42. package/dist/sync/sync-saga-message/sync-saga-message-constants.d.mts.map +1 -0
  43. package/dist/sync/sync-saga-message/sync-saga-message-constants.mjs +2 -0
  44. package/dist/sync/sync-saga-message/sync-saga-message-constants.mjs.map +1 -0
  45. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts +15 -0
  46. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -0
  47. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +43 -0
  48. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -0
  49. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +39 -0
  50. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -0
  51. package/dist/sync/sync-saga-message/sync-saga-message-types.mjs +2 -0
  52. package/dist/sync/sync-saga-message/sync-saga-message-types.mjs.map +1 -0
  53. package/dist/sync/sync-types.d.mts +81 -3
  54. package/dist/sync/sync-types.d.mts.map +1 -1
  55. package/dist/sync/sync-types.mjs +27 -1
  56. package/dist/sync/sync-types.mjs.map +1 -1
  57. package/dist/timeline/timeline-api.d.mts +16 -3
  58. package/dist/timeline/timeline-api.d.mts.map +1 -1
  59. package/dist/timeline/timeline-api.mjs +7 -7
  60. package/dist/timeline/timeline-api.mjs.map +1 -1
  61. package/dist/witness/space/inner-space/inner-space-v1.d.mts.map +1 -1
  62. package/dist/witness/space/inner-space/inner-space-v1.mjs +3 -4
  63. package/dist/witness/space/inner-space/inner-space-v1.mjs.map +1 -1
  64. package/dist/witness/space/outer-space/outer-space-types.d.mts +2 -0
  65. package/dist/witness/space/outer-space/outer-space-types.d.mts.map +1 -1
  66. package/dist/witness/space/space-base-v1.d.mts +19 -1
  67. package/dist/witness/space/space-base-v1.d.mts.map +1 -1
  68. package/dist/witness/space/space-base-v1.mjs +66 -6
  69. package/dist/witness/space/space-base-v1.mjs.map +1 -1
  70. package/dist/witness/space/space-helper.d.mts +14 -0
  71. package/dist/witness/space/space-helper.d.mts.map +1 -1
  72. package/dist/witness/space/space-helper.mjs +44 -1
  73. package/dist/witness/space/space-helper.mjs.map +1 -1
  74. package/dist/witness/space/space-respec-helper.d.mts.map +1 -1
  75. package/dist/witness/space/space-respec-helper.mjs +1 -1
  76. package/dist/witness/space/space-respec-helper.mjs.map +1 -1
  77. package/dist/witness/space/space-types.d.mts +12 -1
  78. package/dist/witness/space/space-types.d.mts.map +1 -1
  79. package/dist/witness/space/space-types.mjs +4 -0
  80. package/dist/witness/space/space-types.mjs.map +1 -1
  81. package/package.json +2 -2
  82. package/src/keystone/keystone-helpers.mts +3 -3
  83. package/src/sync/README.md +275 -0
  84. package/src/sync/sync-constants.mts +5 -0
  85. package/src/sync/sync-helpers.mts +105 -6
  86. package/src/sync/sync-innerspace.respec.mts +458 -289
  87. package/src/sync/sync-peer/sync-peer-types.mts +43 -0
  88. package/src/sync/sync-peer/sync-peer-v1.mts +28 -0
  89. package/src/sync/sync-saga-context/sync-saga-context-constants.mts +8 -0
  90. package/src/sync/sync-saga-context/sync-saga-context-helpers.mts +147 -0
  91. package/src/sync/sync-saga-context/sync-saga-context-types.mts +80 -0
  92. package/src/sync/sync-saga-coordinator.mts +762 -329
  93. package/src/sync/sync-saga-coordinator.respec.mts +7 -7
  94. package/src/sync/sync-saga-message/sync-saga-message-constants.mts +1 -0
  95. package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +59 -0
  96. package/src/sync/sync-saga-message/sync-saga-message-types.mts +53 -0
  97. package/src/sync/sync-types.mts +103 -3
  98. package/src/timeline/timeline-api.mts +20 -4
  99. package/src/witness/space/inner-space/inner-space-v1.mts +3 -2
  100. package/src/witness/space/reconciliation-space/reconciliation-space-base.mts.OLD.md +884 -0
  101. package/src/witness/space/reconciliation-space/reconciliation-space-helper.mts.OLD.md +125 -0
  102. package/src/witness/space/space-base-v1.mts +62 -12
  103. package/src/witness/space/space-helper.mts +50 -1
  104. package/src/witness/space/space-respec-helper.mts +2 -1
  105. package/src/witness/space/space-types.mts +13 -1
  106. package/tmp.md +3 -9
@@ -2,70 +2,21 @@ import { extractErrorMsg, getUUID } from "@ibgib/helper-gib/dist/helpers/utils-h
2
2
  import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
3
3
  import { splitPerTjpAndOrDna, getTimelinesGroupedByTjp } from "../common/other/ibgib-helper.mjs";
4
4
  import { Factory_V1 } from "@ibgib/ts-gib/dist/V1/factory.mjs";
5
- import { isPrimitive } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
6
5
  import { GLOBAL_LOG_A_LOT } from "../core-constants.mjs";
7
- import { putInSpace, lockSpace, unlockSpace, getSpaceArgMetadata } from "../witness/space/space-helper.mjs";
8
- import { SyncStage, SYNC_ATOM } from "./sync-constants.mjs";
9
- import { getSyncIb } from "./sync-helpers.mjs";
6
+ import { putInSpace, getFromSpace } from "../witness/space/space-helper.mjs";
7
+ import { SyncStage, SYNC_ATOM, SYNC_MSG_REL8N_NAME } from "./sync-constants.mjs";
8
+ import { appendToTimeline, createTimeline } from "../timeline/timeline-api.mjs";
9
+ import { SyncMode, } from "./sync-types.mjs";
10
+ import { getSyncIb, isPastFrame } from "./sync-helpers.mjs";
10
11
  import { getDependencyGraph } from "../common/other/graph-helper.mjs";
11
- const logalot = GLOBAL_LOG_A_LOT;
12
+ import { getSyncSagaMessageIb } from "./sync-saga-message/sync-saga-message-helpers.mjs";
13
+ import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-constants.mjs";
14
+ import { Subject_V1 } from "../common/pubsub/subject/subject-v1.mjs";
15
+ import { SyncSagaContextCmd } from "./sync-saga-context/sync-saga-context-types.mjs";
16
+ import { createSyncSagaContext } from "./sync-saga-context/sync-saga-context-helpers.mjs";
17
+ const logalot = GLOBAL_LOG_A_LOT || true;
12
18
  /**
13
19
  * Orchestrates the synchronization process between two spaces (Source and Destination).
14
- *
15
- * ## Architecture: Dependency Graph Synchronization
16
- *
17
- * Instead of a naive file-by-file sync or a holistic "Space" sync, this coordinator operates
18
- * on a **Dependency Graph** derived from specific "Domain Roots" (e.g., a specific tag,
19
- * folder, or application root).
20
- *
21
- * ### Workflow Pipeline
22
- *
23
- * 1. **Graph Generation**:
24
- * * Generates a `FlatIbGibGraph` using `getDependencyGraph({ live: true })` starting
25
- * from the provided `domainIbGibs`.
26
- * * This ensures we capture the *latest* reachable state of all relevant timelines.
27
- *
28
- * 2. **Classification (`splitPerTjpAndOrDna`)**:
29
- * * **Stones**: Immutable, non-living ibGibs (no TJP/DNA). Trivial to sync (copy if missing).
30
- * * **Living**: Evolving timelines (TJP + DNA). Complex to sync (require ordering & merging).
31
- *
32
- * 3. **Timeline Ordering (`getTimelinesGroupedByTjp`)**:
33
- * * Living ibGibs are grouped into timelines.
34
- * * A "Timeline Dependency Graph" is built. Use Case: If a Comment Timeline refers to a
35
- * Post Timeline, the Post Timeline must be synced *before* the Comment Timeline to
36
- * ensure referential integrity at the destination.
37
- * * **Topological Sort** determines the execution order. Circular dependencies are
38
- * treated as siblings.
39
- *
40
- * 4. **Saga Execution ("Smart Coordinator, Dumb Space")**:
41
- * * The Coordinator (running on the Client/Source) drives the entire process via a
42
- * "Pull-Merge-Push" strategy to resolve conflicts.
43
- *
44
- * * **Phase 1: Knowledge Exchange (Init)**
45
- * * Generates a "Knowledge Vector" (Map<Tjp, LatestAddr>) of the Source's graph.
46
- * * Sends `SyncStage.Init` to Dest.
47
- * * Dest responds with its own Knowledge Vector for overlapping timelines.
48
- *
49
- * * **Phase 2: Gap Analysis & Conflict Resolution**
50
- * * Coordinator compares Source vs. Dest knowledge.
51
- * * **Fast-Forward**: Source is strictly ahead of Dest. Mark new frames for PUSH.
52
- * * **Fast-Backward**: Dest is strictly ahead of Source. Mark frames for PULL (to update Local).
53
- * * **Conflict/Divergence**: Both have new frames from a common ancestor.
54
- * * **LOCK**: `lockSpace({ scope: tjpGib })` on Dest to prevent race conditions.
55
- * * **PULL**: Download conflicting branch from Dest.
56
- * * **MERGE**: Execute merge logic locally (creating a new merge frame `A_merge`).
57
- * * **PUSH**: Mark `A_merge` (and dependencies) for PUSH.
58
- * * **UNLOCK**: Release Dest lock.
59
- *
60
- * * **Phase 3: Batch Streaming (Delta)**
61
- * * **Stones**: Batch `putInSpace` all missing "Stone" ibGibs first.
62
- * * **Timelines**: Batch `putInSpace` Living Timelines in topological order.
63
- * * *Note*: The `SyncFrame` (Init/Delta/Commit) tracks protocol state, but data transfer
64
- * happens via standard `putInSpace`.
65
- *
66
- * * **Phase 4: Commit**
67
- * * Update Local Index (register new latests).
68
- * * Send `SyncStage.Commit` to Dest to finalize session.
69
20
  */
70
21
  export class SyncSagaCoordinator {
71
22
  keystone;
@@ -74,187 +25,242 @@ export class SyncSagaCoordinator {
74
25
  this.keystone = keystone;
75
26
  }
76
27
  /**
77
- * Executes a synchronization saga.
78
- *
79
- * @param opts.source The local space (Client) driving the sync.
80
- * @param opts.dest The remote space (Server/Other Peer) to sync with.
81
- * @param opts.identity The Keystone Identity performing the sync.
82
- * @param opts.domainIbGibs The root ibGibs that define the scope of the sync (the "Dependency Graph").
83
- * @param opts.identitySecret Optional secret if needed (usually handled by `keystone` service).
28
+ * Executes a synchronization saga using the Symmetric Sync Protocol.
84
29
  */
85
- async sync({ source, dest, identity, domainIbGibs, identitySecret }) {
30
+ async sync({ peer, localSpace, metaspace, primaryIdentity, primaryIdentitySecret, domainIbGibs, }) {
86
31
  const lc = `${this.lc}[${this.sync.name}]`;
87
- try {
88
- if (logalot) {
89
- console.log(`${lc} starting...`);
90
- }
91
- // 1. Generate UUID for this saga
92
- // const uuid = await Factory_V1.primitive({ ib: "genuuid" }); // Hacky usage of primitive helper if available, or just UUID lib
93
- // if we don't have a uuid, we can generate one here.
94
- // but usually this is passed in.
95
- // atow we are just generating one.
96
- const uuid = await getUUID();
97
- // ---------------------------------------------------------
98
- // 1. Graph Generation & Classification
99
- // ---------------------------------------------------------
100
- // We want a live dependency graph to ensure we get the latest
101
- // states of timelines.
102
- const graph = await getDependencyGraph({
103
- ibGibs: domainIbGibs,
104
- live: true,
105
- space: source,
106
- });
107
- const validGraph = graph && Object.keys(graph).length > 0;
108
- if (logalot) {
109
- console.log(`${lc} graph generated. nodes: ${validGraph ? Object.keys(graph).length : 0}`);
110
- }
111
- // 2. Classification
112
- // We differentiate between "Stones" (Immutable, no timeline) and "Living" (Timelines).
113
- // Stones can be synced in bulk without ordering.
114
- // Living ibGibs must be grouped by timeline and ordered.
115
- const ibGibs = validGraph ? Object.values(graph) : [];
116
- const { mapWithTjp_YesDna, mapWithTjp_NoDna, mapWithoutTjps } = splitPerTjpAndOrDna({ ibGibs });
117
- // "Stones" come from mapWithoutTjps
118
- const stones = Object.values(mapWithoutTjps);
119
- // "Living" come from both TJP maps
120
- // We combine them to group by TJP
121
- const living = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
122
- if (logalot) {
123
- console.log(`${lc} classification results (count):`);
124
- console.log(`stones: ${stones.length}`);
125
- console.log(`living: ${living.length}`);
126
- }
127
- // 3. Timeline Ordering
128
- // Group living ibGibs by TJP to identify distinct timelines.
129
- // The helper returns them sorted by 'n' ascending.
130
- const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: living });
131
- // todo: perform topological sort on timelinesMap based on inter-timeline references.
132
- // For now, we sync independent timelines.
133
- const payloadAddrs = [];
134
- payloadAddrs.push(...stones.map(x => getIbGibAddr({ ibGib: x })));
135
- // 3. Timeline Ordering (Topological Sort)
136
- // Living ibGibs are grouped by timeline (TJP).
137
- // We need to order these timelines based on dependencies.
138
- const sortedTjps = this.sortTimelinesTopologically(timelinesMap);
139
- // Add ordered timelines to payload
140
- sortedTjps.forEach(tjp => {
141
- const timeline = timelinesMap[tjp];
142
- payloadAddrs.push(...timeline.map(x => getIbGibAddr({ ibGib: x })));
143
- });
144
- // 2. INIT & Phase 2: Knowledge Exchange
145
- const srcKnowledgeVector = {};
146
- Object.keys(timelinesMap).forEach(tjp => {
147
- const timeline = timelinesMap[tjp];
148
- const tip = timeline[timeline.length - 1];
149
- srcKnowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
150
- });
151
- const initData = {
152
- knowledgeVector: srcKnowledgeVector,
153
- identity: identity,
154
- mode: 'push'
155
- };
156
- // Get Source Knowledge (Time -> Latest)
157
- const srcTjps = Object.keys(timelinesMap);
158
- // Get Dest Knowledge
159
- const destKnowledge = await this.getLatestAddrsFromSpace({ space: dest, tjpAddrs: srcTjps });
160
- const conflicts = [];
161
- for (const tjp of srcTjps) {
162
- const srcTimeline = timelinesMap[tjp];
163
- const srcTip = srcTimeline[srcTimeline.length - 1];
164
- const srcTipAddr = getIbGibAddr({ ibGib: srcTip });
165
- const destTipAddr = destKnowledge[tjp];
166
- if (destTipAddr && destTipAddr !== srcTipAddr) {
167
- // Divergence found. Check if Source knows Dest's tip.
168
- if (graph[destTipAddr]) {
169
- // Fast-Forward: Dest tip IS in our graph, so we are ahead.
170
- // No action needed, proceed to push.
171
- }
172
- else {
173
- // Conflict: Dest has a tip we don't know about.
174
- conflicts.push(tjp);
175
- }
176
- }
32
+ if (logalot) {
33
+ console.log(`${lc} starting...`);
34
+ }
35
+ // 1. SETUP SAGA METADATA
36
+ const sagaId = await getUUID();
37
+ // Setup Observable & Promise
38
+ const updates$ = new Subject_V1();
39
+ let resolveDone;
40
+ let rejectDone;
41
+ const done = new Promise((resolve, reject) => {
42
+ resolveDone = resolve;
43
+ rejectDone = reject;
44
+ });
45
+ // WORKING CONTEXT (Transactional)
46
+ const tempSpace = await metaspace.createNewLocalSpace({
47
+ opts: {
48
+ allowCancel: false,
49
+ spaceName: undefined,
50
+ getFnPrompt: metaspace.getFnPrompt,
51
+ logalot
177
52
  }
178
- // Lock conflicts
179
- for (const tjp of conflicts) {
180
- if (logalot) {
181
- console.log(`${lc} Conflict detected for TJP ${tjp}. Acquiring lock...`);
182
- }
183
- // Lock VALIDITY: 60 seconds?
184
- await lockSpace({
185
- space: dest,
186
- scope: tjp,
187
- secondsValid: 60,
188
- instanceId: uuid,
53
+ });
54
+ if (!tempSpace) {
55
+ throw new Error(`Failed to create temp space (E: 8f4e2f3d6c1b4b1a8f4e2f3d6c1b4b1a)`);
56
+ }
57
+ const cleanup = async () => {
58
+ // In-memory temp space cleanup if needed (e.g. if we used a specialized disposable space)
59
+ // await tempSpace.destroy();
60
+ };
61
+ // Async execution wrapper
62
+ (async () => {
63
+ try {
64
+ // 2. BOOTSTRAP IDENTITY (Session Keystone)
65
+ const config = {
66
+ id: 'default',
67
+ type: 'hash-reveal-v1',
68
+ salt: sagaId,
69
+ behavior: { size: 10, replenish: 'top-up', selectSequentially: 1, selectRandomly: 1, targetBindingChars: 0 },
70
+ algo: 'SHA-256', rounds: 1
71
+ };
72
+ const sessionIdentity = await this.keystone.genesis({
73
+ masterSecret: sagaId,
74
+ configs: [config],
75
+ metaspace,
76
+ space: tempSpace
189
77
  });
190
- // todo: Implement Pull & Merge here
191
- // 1. Pull delta from Dest (destTip -> ... -> common ancestor)
192
- // 2. Merge locally
193
- // 3. Update graph/timelinesMap with merged result
194
- if (logalot) {
195
- console.warn(`${lc} Merge not fully implemented yet. Releasing lock.`);
196
- }
197
- await unlockSpace({
198
- space: dest,
199
- scope: tjp,
200
- instanceId: uuid,
78
+ // 3. CREATE INITIAL FRAME (Stage.init)
79
+ const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
80
+ sagaId,
81
+ sessionIdentity,
82
+ localSpace,
83
+ domainIbGibs,
84
+ tempSpace,
85
+ metaspace,
201
86
  });
202
- }
203
- const initFrame = await this.createSyncFrame({
204
- uuid,
205
- stage: SyncStage.init,
206
- payload: initData,
207
- identity
208
- });
209
- // Persist Init to Dest (Handshake)
210
- await putInSpace({ space: dest, ibGib: initFrame });
211
- // 3. DELTA
212
- // const payloadAddrs = [getIbGibAddr({ ibGib: identity })]; // Already computed above
213
- const deltaData = {
214
- payloadAddrs
215
- };
216
- const deltaFrame = await this.createSyncFrame({
217
- uuid,
218
- stage: SyncStage.delta,
219
- payload: deltaData,
220
- identity
221
- });
222
- await putInSpace({ space: dest, ibGib: deltaFrame });
223
- // 3.1 Stream the actual data (Batch Stream)
224
- // We must preserve the order in payloadAddrs to respect dependencies.
225
- // We batch them to improve throughput.
226
- const BATCH_SIZE = 20;
227
- for (let i = 0; i < payloadAddrs.length; i += BATCH_SIZE) {
228
- const chunkAddrs = payloadAddrs.slice(i, i + BATCH_SIZE);
229
- const chunkIbGibs = chunkAddrs
230
- .map(addr => {
231
- const ibGib = graph[addr];
232
- if (!ibGib) {
233
- throw new Error(`(UNEXPECTED) addr not found in graph: ${addr} (E: 8a402324057849318625aa85721665a5)`);
234
- }
235
- return ibGib;
236
- })
237
- .filter(ibGib => !isPrimitive({ ibGib }));
238
- if (chunkIbGibs.length > 0) {
87
+ // 4. EXECUTE SAGA LOOP (FSM)
88
+ const syncedIbGibs = await this.executeSagaLoop({
89
+ initialFrame: initFrame,
90
+ srcGraph,
91
+ peer,
92
+ sessionIdentity,
93
+ updates$,
94
+ tempSpace,
95
+ metaspace
96
+ });
97
+ // 5. MERGE RESULT (Optimistic Commit)
98
+ if (syncedIbGibs && syncedIbGibs.length > 0) {
239
99
  if (logalot) {
240
- console.log(`${lc} streaming batch ${Math.ceil(i / BATCH_SIZE) + 1} (${chunkIbGibs.length} ibGibs)...`);
100
+ console.log(`${lc} Merging ${syncedIbGibs.length} synced ibGibs to localSpace...`);
241
101
  }
242
- // We don't set isDna: true here because the batch might contain mixed types.
243
- // The Space implementation should handle routing based on internal metadata if needed.
244
- await putInSpace({ space: dest, ibGibs: chunkIbGibs });
102
+ await putInSpace({ space: localSpace, ibGibs: syncedIbGibs });
245
103
  }
104
+ resolveDone();
105
+ updates$.complete();
246
106
  }
247
- // 4. COMMIT
248
- const commitData = {
249
- success: true
250
- };
251
- const commitFrame = await this.createSyncFrame({
252
- uuid,
253
- stage: SyncStage.commit,
254
- payload: commitData,
255
- identity
107
+ catch (error) {
108
+ console.error(`${lc} ${extractErrorMsg(error)}`);
109
+ rejectDone(error);
110
+ updates$.error(error);
111
+ }
112
+ finally {
113
+ await cleanup();
114
+ }
115
+ })();
116
+ return {
117
+ sagaId,
118
+ updates$,
119
+ done
120
+ };
121
+ }
122
+ /**
123
+ * Drives the FSM loop of the Saga.
124
+ * Use "Ping Pong" style: Send Request -> Wait Response -> React (Handle) -> Repeat.
125
+ */
126
+ async executeSagaLoop({ initialFrame, srcGraph, peer, sessionIdentity, updates$, tempSpace, metaspace }) {
127
+ const lc = `${this.lc}[${this.executeSagaLoop.name}]`;
128
+ let currentFrame = initialFrame;
129
+ let nextPayload = [];
130
+ const allReceivedIbGibs = [];
131
+ while (currentFrame) {
132
+ // A. Create Context (Request)
133
+ const payloadAddrs = nextPayload.map(p => getIbGibAddr({ ibGib: p }));
134
+ const requestCtx = await createSyncSagaContext({
135
+ cmd: SyncSagaContextCmd.process,
136
+ sagaFrame: currentFrame,
137
+ msgs: [],
138
+ sessionKeystones: [sessionIdentity],
139
+ payload: payloadAddrs.length > 0 ? payloadAddrs : undefined,
256
140
  });
257
- await putInSpace({ space: dest, ibGib: commitFrame });
141
+ // B. Transmit
142
+ updates$.next(requestCtx);
143
+ const responseCtx = await peer.witness(requestCtx);
144
+ // C. Handle Response
145
+ if (!responseCtx) {
146
+ console.warn(`${lc} Peer returned no response context. Ending loop.`);
147
+ currentFrame = null;
148
+ break;
149
+ }
150
+ updates$.next(responseCtx);
151
+ // D. Extract Remote Frame & React
152
+ const remoteFrameAddr = responseCtx.rel8ns?.sagaFrame?.[0];
153
+ if (!remoteFrameAddr) {
154
+ console.warn(`${lc} Peer response has no sagaFrame. Ending loop.`);
155
+ currentFrame = null;
156
+ break;
157
+ }
158
+ const remoteFrame = await getFromSpace({ addr: remoteFrameAddr, space: tempSpace });
159
+ if (!remoteFrame) {
160
+ throw new Error(`Could not resolve remote frame: ${remoteFrameAddr}`);
161
+ }
162
+ // React (Reducer)
163
+ const result = await this.handleSagaFrame({
164
+ sagaIbGib: remoteFrame,
165
+ srcGraph,
166
+ space: tempSpace,
167
+ identity: sessionIdentity,
168
+ metaspace
169
+ });
170
+ currentFrame = result?.frame || null;
171
+ nextPayload = result?.payload || [];
172
+ if (result?.receivedPayload) {
173
+ allReceivedIbGibs.push(...result.receivedPayload);
174
+ }
175
+ }
176
+ return allReceivedIbGibs;
177
+ }
178
+ async analyzeTimelines({ domainIbGibs, space, }) {
179
+ const lc = `${this.lc}[${this.analyzeTimelines.name}]`;
180
+ const srcGraph = await getDependencyGraph({
181
+ ibGibs: domainIbGibs,
182
+ live: true,
183
+ space,
184
+ });
185
+ const srcGraphIsValid = srcGraph && Object.keys(srcGraph).length > 0;
186
+ if (logalot) {
187
+ console.log(`${lc} graph generated. nodes: ${srcGraphIsValid ? Object.keys(srcGraph).length : 0}`);
188
+ }
189
+ const srcIbGibs = srcGraphIsValid ? Object.values(srcGraph) : [];
190
+ const { mapWithTjp_YesDna: srcMapWithTjp_YesDna, mapWithTjp_NoDna: srcMapWithTjp_NoDna, mapWithoutTjps: src_MapWithoutTjps } = splitPerTjpAndOrDna({ ibGibs: srcIbGibs });
191
+ const srcStones = Object.values(src_MapWithoutTjps);
192
+ const srcLiving = [...Object.values(srcMapWithTjp_YesDna), ...Object.values(srcMapWithTjp_NoDna)];
193
+ const srcTimelinesMap = getTimelinesGroupedByTjp({ ibGibs: srcLiving });
194
+ const srcSortedTjps = this.sortTimelinesTopologically(srcTimelinesMap);
195
+ return { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph };
196
+ }
197
+ async resolveConflicts() {
198
+ throw new Error("resolveConflicts is no longer supported.");
199
+ }
200
+ async processBatchStream() {
201
+ // Deprecated by payload in SyncSagaContext
202
+ throw new Error("processBatchStream is no longer supported.");
203
+ }
204
+ /**
205
+ * Creates the Initial Saga Frame (Init Stage).
206
+ */
207
+ async createInitFrame({ sagaId, sessionIdentity, localSpace, domainIbGibs, tempSpace, metaspace }) {
208
+ const initData = {
209
+ sagaId,
210
+ stage: SyncStage.init,
211
+ knowledgeVector: {},
212
+ identity: sessionIdentity, // KeystoneIbGib is already public data
213
+ mode: SyncMode.sync,
214
+ };
215
+ // Populate Knowledge Vector
216
+ const { srcTimelinesMap, srcGraph } = await this.analyzeTimelines({ domainIbGibs, space: localSpace });
217
+ Object.keys(srcTimelinesMap).forEach(tjp => {
218
+ const timeline = srcTimelinesMap[tjp];
219
+ const tip = timeline.at(-1);
220
+ initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
221
+ });
222
+ const initStone = await this.createSyncMsgStone({
223
+ data: initData,
224
+ space: tempSpace,
225
+ metaspace
226
+ });
227
+ const sagaFrame = await this.evolveSyncSagaIbGib({
228
+ msgStones: [initStone],
229
+ identity: sessionIdentity,
230
+ space: tempSpace,
231
+ metaspace
232
+ });
233
+ return { sagaFrame, srcGraph };
234
+ }
235
+ /**
236
+ * Reacts to an incoming saga frame and dispatches to appropriate handler.
237
+ */
238
+ async handleSagaFrame({ sagaIbGib, srcGraph, space, identity, identitySecret, metaspace, }) {
239
+ const lc = `${this.lc}[${this.handleSagaFrame.name}]`;
240
+ try {
241
+ if (logalot) {
242
+ console.log(`${lc} starting... (I: 5deec8a1f7a6d263c88cd458ad990826)`);
243
+ }
244
+ if (!sagaIbGib.data) {
245
+ throw new Error(`(UNEXPECTED) sagaIbGib.data falsy? (E: 71b938adf1d87c2527bfd4f86dfd0826)`);
246
+ }
247
+ if (logalot) {
248
+ console.log(`${lc} handling frame stage: ${sagaIbGib.data.stage}`);
249
+ }
250
+ // Get Stage from Stone (or Frame for Init fallback)
251
+ const { stage, payload } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
252
+ switch (stage) {
253
+ case SyncStage.init:
254
+ return await this.handleInitFrame({ sagaIbGib, payload, space, metaspace, identity, identitySecret });
255
+ case SyncStage.ack:
256
+ return await this.handleAckFrame({ sagaIbGib, srcGraph, space, metaspace, identity });
257
+ case SyncStage.delta:
258
+ return await this.handleDeltaFrame({ sagaIbGib, srcGraph, space, identity, metaspace });
259
+ case SyncStage.commit:
260
+ return await this.handleCommitFrame({ sagaIbGib, space });
261
+ default:
262
+ throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
263
+ }
258
264
  }
259
265
  catch (error) {
260
266
  console.error(`${lc} ${extractErrorMsg(error)}`);
@@ -266,76 +272,346 @@ export class SyncSagaCoordinator {
266
272
  }
267
273
  }
268
274
  }
269
- async createSyncFrame({ uuid, stage, payload, identity }) {
270
- const data = {
271
- uuid,
272
- stage,
273
- payload
275
+ // #region Handlers
276
+ async handleInitFrame({ sagaIbGib, payload, space, metaspace, identity, identitySecret, }) {
277
+ const lc = `${this.lc}[${this.handleInitFrame.name}]`;
278
+ if (logalot) {
279
+ console.log(`${lc} starting...`);
280
+ }
281
+ // Extract Init Data
282
+ const initData = sagaIbGib.data.payload;
283
+ if (!initData || !initData.knowledgeVector) {
284
+ throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
285
+ }
286
+ const remoteKV = initData.knowledgeVector;
287
+ const remoteTjps = Object.keys(remoteKV);
288
+ // 1. Get Local Latest Addrs for all TJPs
289
+ let localKV = {};
290
+ if (remoteTjps.length > 0) {
291
+ // Batch get latest addrs for the TJPs
292
+ const arg = await space.argy({
293
+ argData: {
294
+ cmd: 'get',
295
+ cmdModifiers: ['latest', 'tjps'], // interprets input addrs as TJPs
296
+ ibGibAddrs: remoteTjps,
297
+ }
298
+ });
299
+ const res = await space.witness(arg);
300
+ if (res?.data?.latestAddrsMap) {
301
+ localKV = res.data.latestAddrsMap;
302
+ }
303
+ }
304
+ // 2. Gap Analysis
305
+ const deltaReqAddrs = [];
306
+ const pushOfferAddrs = [];
307
+ // const divergentTjps: string[] = []; // Handle later
308
+ for (const tjp of remoteTjps) {
309
+ const remoteAddr = remoteKV[tjp];
310
+ const localAddr = localKV[tjp];
311
+ if (!localAddr) {
312
+ deltaReqAddrs.push(remoteAddr);
313
+ continue;
314
+ }
315
+ if (localAddr === remoteAddr) {
316
+ continue;
317
+ }
318
+ // Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
319
+ const isRemoteInPast = await isPastFrame({
320
+ olderAddr: remoteAddr,
321
+ newerAddr: localAddr,
322
+ space,
323
+ });
324
+ if (isRemoteInPast) {
325
+ pushOfferAddrs.push(localAddr);
326
+ }
327
+ else {
328
+ deltaReqAddrs.push(remoteAddr);
329
+ }
330
+ }
331
+ // 3. Create Ack Frame
332
+ const sagaId = sagaIbGib.data.uuid;
333
+ // Create Payload Stone
334
+ const ackData = {
335
+ sagaId,
336
+ stage: SyncStage.ack,
337
+ deltaReqAddrs,
338
+ pushOfferAddrs,
339
+ };
340
+ const ackStone = await this.createSyncMsgStone({
341
+ data: ackData,
342
+ space,
343
+ metaspace,
344
+ });
345
+ // Evolve Saga
346
+ const ackFrame = await this.evolveSyncSagaIbGib({
347
+ prevSagaIbGib: sagaIbGib,
348
+ msgStones: [ackStone],
349
+ identity,
350
+ space,
351
+ metaspace,
352
+ });
353
+ return { frame: ackFrame };
354
+ }
355
+ async handleAckFrame({ sagaIbGib, srcGraph, space, metaspace, identity, }) {
356
+ const lc = `${this.lc}[${this.handleAckFrame.name}]`;
357
+ if (logalot) {
358
+ console.log(`${lc} starting...`);
359
+ }
360
+ const { payload } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
361
+ const ackData = payload;
362
+ if (!ackData || ackData.stage !== SyncStage.ack) {
363
+ throw new Error(`${lc} Invalid ack frame (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
364
+ }
365
+ const deltaReqAddrs = ackData.deltaReqAddrs || [];
366
+ const pushOfferAddrs = ackData.pushOfferAddrs || [];
367
+ // 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
368
+ const pullReqAddrs = [];
369
+ for (const addr of pushOfferAddrs) {
370
+ const existing = srcGraph[addr] || (await getFromSpace({ addr, space })).ibGibs?.[0];
371
+ if (!existing) {
372
+ pullReqAddrs.push(addr);
373
+ }
374
+ }
375
+ // 2. Process Delta Requests (Push Payload)
376
+ const payloadIbGibs = [];
377
+ for (const addr of deltaReqAddrs) {
378
+ let ibGib = srcGraph[addr];
379
+ if (!ibGib) {
380
+ const res = await getFromSpace({ addr, space });
381
+ if (res.ibGibs && res.ibGibs.length > 0) {
382
+ ibGib = res.ibGibs[0];
383
+ }
384
+ }
385
+ if (ibGib) {
386
+ payloadIbGibs.push(ibGib);
387
+ }
388
+ else {
389
+ console.warn(`${lc} Requested addr not found: ${addr}`);
390
+ }
391
+ }
392
+ // 3. Create Delta Frame
393
+ const sagaId = ackData.sagaId;
394
+ const deltaData = {
395
+ sagaId,
396
+ stage: SyncStage.delta,
397
+ payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
398
+ requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
274
399
  };
275
- const ib = await getSyncIb({ data });
276
- // Create the ibGib using Factory? Or manually?
277
- // Factory_V1.firstGen usually.
278
- const res = await Factory_V1.firstGen({
279
- parentIbGib: Factory_V1.primitive({ ib: SYNC_ATOM }),
400
+ const deltaStone = await this.createSyncMsgStone({
401
+ data: deltaData,
402
+ space,
403
+ metaspace,
404
+ });
405
+ const deltaFrame = await this.evolveSyncSagaIbGib({
406
+ prevSagaIbGib: sagaIbGib,
407
+ msgStones: [deltaStone],
408
+ identity,
409
+ space,
410
+ metaspace,
411
+ });
412
+ return { frame: deltaFrame, payload: payloadIbGibs };
413
+ }
414
+ async handleDeltaFrame({ sagaIbGib, srcGraph, space, metaspace, identity, }) {
415
+ const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
416
+ if (logalot) {
417
+ console.log(`${lc} starting...`);
418
+ }
419
+ const { payload } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
420
+ const deltaData = payload;
421
+ if (!deltaData || deltaData.stage !== SyncStage.delta) {
422
+ throw new Error(`${lc} Invalid delta frame (E: 7c28c8d8f08a4421b8344e6727271421)`);
423
+ }
424
+ const payloadAddrs = deltaData.payloadAddrs || [];
425
+ const requests = deltaData.requests || [];
426
+ // 1. Process Received Payload
427
+ const receivedPayload = [];
428
+ if (payloadAddrs.length > 0) {
429
+ const res = await space.witness({
430
+ attribute: 'ibGibs',
431
+ arg: {
432
+ ibGibAddrs: payloadAddrs,
433
+ cmd: 'get',
434
+ }
435
+ });
436
+ if (res.data?.ibGibs) {
437
+ receivedPayload.push(...res.data.ibGibs);
438
+ }
439
+ }
440
+ // 2. Fulfill Requests (Outgoing Payload)
441
+ const outgoingPayload = [];
442
+ for (const addr of requests) {
443
+ let ibGib = srcGraph[addr];
444
+ if (!ibGib) {
445
+ const res = await getFromSpace({ addr, space });
446
+ if (res.ibGibs && res.ibGibs.length > 0) {
447
+ ibGib = res.ibGibs[0];
448
+ }
449
+ }
450
+ if (ibGib) {
451
+ outgoingPayload.push(ibGib);
452
+ }
453
+ }
454
+ // 3. Determine Next Stage
455
+ if (requests.length > 0) {
456
+ // They requested more data -> Send Delta
457
+ const sagaId = deltaData.sagaId;
458
+ const responseDeltaData = {
459
+ sagaId,
460
+ stage: SyncStage.delta,
461
+ payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
462
+ };
463
+ const deltaStone = await this.createSyncMsgStone({
464
+ data: responseDeltaData,
465
+ space,
466
+ metaspace
467
+ });
468
+ const deltaFrame = await this.evolveSyncSagaIbGib({
469
+ prevSagaIbGib: sagaIbGib,
470
+ msgStones: [deltaStone],
471
+ identity,
472
+ space,
473
+ metaspace
474
+ });
475
+ return { frame: deltaFrame, payload: outgoingPayload, receivedPayload };
476
+ }
477
+ else {
478
+ // No requests -> Commit
479
+ const sagaId = deltaData.sagaId;
480
+ const commitData = {
481
+ sagaId,
482
+ stage: SyncStage.commit,
483
+ success: true,
484
+ };
485
+ const commitStone = await this.createSyncMsgStone({
486
+ data: commitData,
487
+ space,
488
+ metaspace
489
+ });
490
+ const commitFrame = await this.evolveSyncSagaIbGib({
491
+ prevSagaIbGib: sagaIbGib,
492
+ msgStones: [commitStone],
493
+ identity,
494
+ space,
495
+ metaspace
496
+ });
497
+ return { frame: commitFrame, receivedPayload };
498
+ }
499
+ }
500
+ async handleCommitFrame({ sagaIbGib, space, }) {
501
+ const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
502
+ if (logalot) {
503
+ console.log(`${lc} Commit received. Saga complete.`);
504
+ }
505
+ return null;
506
+ }
507
+ // #endregion Handlers
508
+ async createSyncMsgStone({ data, space, metaspace, }) {
509
+ const ib = await getSyncSagaMessageIb({ data });
510
+ const stone = await Factory_V1.stone({
280
511
  ib,
512
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
281
513
  data,
282
- rel8ns: {
283
- identity: [getIbGibAddr({ ibGib: identity })]
284
- },
285
- /**
286
- * dna is used for merging dynamic timelines, a la a CRDT-like
287
- * mechanism that looks somewhat like event sourcing but using
288
- * crypto primitives. in our particular case, the sync should be a
289
- * single, one-off timeline (not to be used for any future merging),
290
- * so we don't need dna.
291
- */
292
- dna: false,
293
- tjp: {
294
- timestamp: true,
295
- /**
296
- * we'll use the uuid passed in
297
- */
298
- uuid: false
299
- },
300
- nCounter: true,
514
+ uuid: true, // we want the stone to have its own uniqueness
301
515
  });
302
- return res.newIbGib;
516
+ await putInSpace({ space, ibGib: stone });
517
+ await metaspace.registerNewIbGib({ ibGib: stone });
518
+ return stone;
303
519
  }
304
- async getLatestAddrsFromSpace({ space, tjpAddrs, }) {
305
- const lc = `${this.lc}[${this.getLatestAddrsFromSpace.name}]`;
520
+ /**
521
+ * Evolves the saga timeline with a new frame.
522
+ */
523
+ async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, }) {
524
+ const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
306
525
  try {
307
- if (tjpAddrs.length === 0) {
308
- return {};
526
+ // Validation
527
+ if (!msgStones || msgStones.length === 0) {
528
+ throw new Error(`${lc} msgStones required (E: d13f8afcfb8816aeb26baae7df3ef726)`);
309
529
  }
310
- // Using the space's witness/argy pattern to invoke 'get' with 'latest' modifier
311
- const arg = await space.argy({
312
- ibMetadata: getSpaceArgMetadata({ space }),
313
- argData: {
314
- cmd: 'get',
315
- cmdModifiers: ['latest', 'addrs'],
316
- ibGibAddrs: tjpAddrs,
317
- }
318
- });
319
- const result = await space.witness(arg);
320
- const map = {};
321
- if (result?.data?.success && result.data.addrs) {
322
- // assume 1-to-1 mapping order as per convention if successful
323
- // verification is hard without specific return map
324
- if (result.data.addrs.length === tjpAddrs.length) {
325
- for (let i = 0; i < tjpAddrs.length; i++) {
326
- map[tjpAddrs[i]] = result.data.addrs[i];
327
- }
530
+ const firstStoneData = msgStones[0].data;
531
+ if (!firstStoneData?.sagaId || !firstStoneData?.stage) {
532
+ throw new Error(`${lc} Invalid stone data (missing sagaId or stage) (E: c004f85ce8f8958b11839f8d68c38826)`);
533
+ }
534
+ const sagaId = firstStoneData.sagaId;
535
+ const stage = firstStoneData.stage;
536
+ // Ensure all stones match sagaId and stage
537
+ for (const stone of msgStones) {
538
+ const d = stone.data;
539
+ if (d.sagaId !== sagaId) {
540
+ throw new Error(`${lc} Mismatched sagaId in stones. Expected ${sagaId}, got ${d.sagaId} (E: 29e02719e426ecbbf8ee66887a2be226)`);
328
541
  }
329
- else {
330
- console.warn(`${lc} result addrs length mismatch. requested: ${tjpAddrs.length}, got: ${result.data.addrs.length}. Mapping might be unsafe.`);
542
+ if (d.stage !== stage) {
543
+ throw new Error(`${lc} Mismatched stage in stones. Expected ${stage}, got ${d.stage} (E: d12c6571b0882f762921b60880c3f826)`);
331
544
  }
332
545
  }
333
- return map;
546
+ const identityAddr = getIbGibAddr({ ibGib: identity });
547
+ if (prevSagaIbGib) {
548
+ // Append to existing timeline
549
+ const resAppend = await appendToTimeline({
550
+ timeline: prevSagaIbGib,
551
+ rel8nInfos: [
552
+ {
553
+ rel8nName: SYNC_MSG_REL8N_NAME,
554
+ ibGibs: msgStones,
555
+ },
556
+ {
557
+ rel8nName: 'identity',
558
+ ibGibs: [identity],
559
+ }
560
+ ],
561
+ metaspace,
562
+ space,
563
+ noDna: true, // Explicitly no DNA for sync frames
564
+ });
565
+ const newFrame = resAppend;
566
+ return newFrame;
567
+ }
568
+ else {
569
+ // Create New Timeline (Root Frame)
570
+ const data = {
571
+ uuid: sagaId,
572
+ stage,
573
+ payload: undefined, // Data in stone
574
+ };
575
+ const ib = await getSyncIb({ data });
576
+ const stoneAddrs = msgStones.map(s => getIbGibAddr({ ibGib: s }));
577
+ const resNew = await createTimeline({
578
+ space,
579
+ metaspace,
580
+ ib,
581
+ data,
582
+ rel8ns: {
583
+ [SYNC_MSG_REL8N_NAME]: stoneAddrs,
584
+ identity: [identityAddr],
585
+ },
586
+ parentIb: SYNC_ATOM, // "sync"
587
+ noDna: true,
588
+ });
589
+ return resNew.newIbGib;
590
+ }
334
591
  }
335
592
  catch (error) {
336
593
  console.error(`${lc} ${extractErrorMsg(error)}`);
337
- return {}; // fail safe
594
+ throw error;
595
+ }
596
+ }
597
+ async getStageAndPayloadFromFrame({ ibGib, space }) {
598
+ // 1. Try Stone (Primary)
599
+ const stoneAddrs = ibGib.rel8ns?.[SYNC_MSG_REL8N_NAME];
600
+ if (stoneAddrs && stoneAddrs.length > 0) {
601
+ const stoneAddr = stoneAddrs[0];
602
+ const res = await getFromSpace({ addr: stoneAddr, space });
603
+ if (res.ibGibs && res.ibGibs.length > 0) {
604
+ const stone = res.ibGibs[0];
605
+ if (stone.data && stone.data.stage) {
606
+ return { stage: stone.data.stage, payload: stone.data };
607
+ }
608
+ }
609
+ }
610
+ // 2. Fallback to Frame Data (Legacy/Init compatibility)
611
+ if (ibGib.data && ibGib.data.stage) {
612
+ return { stage: ibGib.data.stage, payload: ibGib.data.payload };
338
613
  }
614
+ throw new Error(`Could not determine stage from frame ${getIbGibAddr({ ibGib })}`);
339
615
  }
340
616
  sortTimelinesTopologically(timelines) {
341
617
  const lc = `${this.lc}[${this.sortTimelinesTopologically.name}]`;