@ibgib/core-gib 0.1.13 → 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 (100) 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 -404
  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 +115 -125
  36. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  37. package/dist/sync/sync-saga-coordinator.mjs +539 -456
  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/outer-space/outer-space-types.d.mts +2 -0
  62. package/dist/witness/space/outer-space/outer-space-types.d.mts.map +1 -1
  63. package/dist/witness/space/space-base-v1.d.mts +19 -1
  64. package/dist/witness/space/space-base-v1.d.mts.map +1 -1
  65. package/dist/witness/space/space-base-v1.mjs +66 -6
  66. package/dist/witness/space/space-base-v1.mjs.map +1 -1
  67. package/dist/witness/space/space-helper.d.mts +14 -0
  68. package/dist/witness/space/space-helper.d.mts.map +1 -1
  69. package/dist/witness/space/space-helper.mjs +44 -1
  70. package/dist/witness/space/space-helper.mjs.map +1 -1
  71. package/dist/witness/space/space-respec-helper.d.mts.map +1 -1
  72. package/dist/witness/space/space-respec-helper.mjs +1 -1
  73. package/dist/witness/space/space-respec-helper.mjs.map +1 -1
  74. package/dist/witness/space/space-types.d.mts +12 -1
  75. package/dist/witness/space/space-types.d.mts.map +1 -1
  76. package/dist/witness/space/space-types.mjs +4 -0
  77. package/dist/witness/space/space-types.mjs.map +1 -1
  78. package/package.json +2 -2
  79. package/src/keystone/keystone-helpers.mts +3 -3
  80. package/src/sync/README.md +275 -0
  81. package/src/sync/sync-constants.mts +5 -0
  82. package/src/sync/sync-helpers.mts +105 -6
  83. package/src/sync/sync-innerspace.respec.mts +458 -457
  84. package/src/sync/sync-peer/sync-peer-types.mts +43 -0
  85. package/src/sync/sync-peer/sync-peer-v1.mts +28 -0
  86. package/src/sync/sync-saga-context/sync-saga-context-constants.mts +8 -0
  87. package/src/sync/sync-saga-context/sync-saga-context-helpers.mts +147 -0
  88. package/src/sync/sync-saga-context/sync-saga-context-types.mts +80 -0
  89. package/src/sync/sync-saga-coordinator.mts +709 -526
  90. package/src/sync/sync-saga-coordinator.respec.mts +7 -7
  91. package/src/sync/sync-saga-message/sync-saga-message-constants.mts +1 -0
  92. package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +59 -0
  93. package/src/sync/sync-saga-message/sync-saga-message-types.mts +53 -0
  94. package/src/sync/sync-types.mts +103 -3
  95. package/src/timeline/timeline-api.mts +20 -4
  96. package/src/witness/space/space-base-v1.mts +62 -12
  97. package/src/witness/space/space-helper.mts +50 -1
  98. package/src/witness/space/space-respec-helper.mts +2 -1
  99. package/src/witness/space/space-types.mts +13 -1
  100. package/tmp.md +3 -9
@@ -1,72 +1,22 @@
1
- import { extractErrorMsg, getUUID, // so our timestamp in ticks as a string are uniform
2
- pretty, clone } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
1
+ import { extractErrorMsg, getUUID } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
3
2
  import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
4
3
  import { splitPerTjpAndOrDna, getTimelinesGroupedByTjp } from "../common/other/ibgib-helper.mjs";
5
4
  import { Factory_V1 } from "@ibgib/ts-gib/dist/V1/factory.mjs";
6
- import { isPrimitive } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
7
5
  import { GLOBAL_LOG_A_LOT } from "../core-constants.mjs";
8
- import { putInSpace, lockSpace, unlockSpace, getLatestAddrs, getFromSpace } from "../witness/space/space-helper.mjs";
9
- import { SyncStage, SYNC_ATOM } from "./sync-constants.mjs";
10
- 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";
11
11
  import { getDependencyGraph } from "../common/other/graph-helper.mjs";
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";
12
17
  const logalot = GLOBAL_LOG_A_LOT || true;
13
18
  /**
14
19
  * Orchestrates the synchronization process between two spaces (Source and Destination).
15
- *
16
- * ## Architecture: Dependency Graph Synchronization
17
- *
18
- * Instead of a naive file-by-file sync or a holistic "Space" sync, this coordinator operates
19
- * on a **Dependency Graph** derived from specific "Domain Roots" (e.g., a specific tag,
20
- * folder, or application root).
21
- *
22
- * ### Workflow Pipeline
23
- *
24
- * 1. **Graph Generation**:
25
- * * Generates a `FlatIbGibGraph` using `getDependencyGraph({ live: true })` starting
26
- * from the provided `domainIbGibs`.
27
- * * This ensures we capture the *latest* reachable state of all relevant timelines.
28
- *
29
- * 2. **Classification (`splitPerTjpAndOrDna`)**:
30
- * * **Stones**: Immutable, non-living ibGibs (no TJP/DNA). Trivial to sync (copy if missing).
31
- * * **Living**: Evolving timelines (TJP + DNA). Complex to sync (require ordering & merging).
32
- *
33
- * 3. **Timeline Ordering (`getTimelinesGroupedByTjp`)**:
34
- * * Living ibGibs are grouped into timelines.
35
- * * A "Timeline Dependency Graph" is built. Use Case: If a Comment Timeline refers to a
36
- * Post Timeline, the Post Timeline must be synced *before* the Comment Timeline to
37
- * ensure referential integrity at the destination.
38
- * * **Topological Sort** determines the execution order. Circular dependencies are
39
- * treated as siblings.
40
- *
41
- * 4. **Saga Execution ("Smart Coordinator, Dumb Space")**:
42
- * * The Coordinator (running on the Client/Source) drives the entire process via a
43
- * "Pull-Merge-Push" strategy to resolve conflicts.
44
- *
45
- * * **Phase 1: Knowledge Exchange (Init)**
46
- * * Generates a "Knowledge Vector" (Map<Tjp, LatestAddr>) of the Source's graph.
47
- * * Sends `SyncStage.Init` to Dest.
48
- * * Dest responds with its own Knowledge Vector for overlapping timelines.
49
- *
50
- * * **Phase 2: Gap Analysis & Conflict Resolution**
51
- * * Coordinator compares Source vs. Dest knowledge.
52
- * * **Fast-Forward**: Source is strictly ahead of Dest. Mark new frames for PUSH.
53
- * * **Fast-Backward**: Dest is strictly ahead of Source. Mark frames for PULL (to update Local).
54
- * * **Conflict/Divergence**: Both have new frames from a common ancestor.
55
- * * **LOCK**: `lockSpace({ scope: tjpGib })` on Dest to prevent race conditions.
56
- * * **PULL**: Download conflicting branch from Dest.
57
- * * **MERGE**: Execute merge logic locally (creating a new merge frame `A_merge`).
58
- * * **PUSH**: Mark `A_merge` (and dependencies) for PUSH.
59
- * * **UNLOCK**: Release Dest lock.
60
- *
61
- * * **Phase 3: Batch Streaming (Delta)**
62
- * * **Stones**: Batch `putInSpace` all missing "Stone" ibGibs first.
63
- * * **Timelines**: Batch `putInSpace` Living Timelines in topological order.
64
- * * *Note*: The `SyncFrame` (Init/Delta/Commit) tracks protocol state, but data transfer
65
- * happens via standard `putInSpace`.
66
- *
67
- * * **Phase 4: Commit**
68
- * * Update Local Index (register new latests).
69
- * * Send `SyncStage.Commit` to Dest to finalize session.
70
20
  */
71
21
  export class SyncSagaCoordinator {
72
22
  keystone;
@@ -75,116 +25,156 @@ export class SyncSagaCoordinator {
75
25
  this.keystone = keystone;
76
26
  }
77
27
  /**
78
- * Executes a synchronization saga.
79
- *
80
- * @param opts.source The local space (Client) driving the sync.
81
- * @param opts.dest The remote space (Server/Other Peer) to sync with.
82
- * @param opts.identity The Keystone Identity performing the sync.
83
- * @param opts.domainIbGibs The root ibGibs that define the scope of the sync (the "Dependency Graph").
84
- * @param opts.identitySecret Optional secret if needed (usually handled by `keystone` service).
28
+ * Executes a synchronization saga using the Symmetric Sync Protocol.
85
29
  */
86
- async sync({ source, dest, identity, domainIbGibs, identitySecret, conflictStrategy }) {
30
+ async sync({ peer, localSpace, metaspace, primaryIdentity, primaryIdentitySecret, domainIbGibs, }) {
87
31
  const lc = `${this.lc}[${this.sync.name}]`;
88
- try {
89
- if (logalot) {
90
- console.log(`${lc} starting...`);
91
- }
92
- // 1. Generate UUID for this saga
93
- // const uuid = await Factory_V1.primitive({ ib: "genuuid" }); // Hacky usage of primitive helper if available, or just UUID lib
94
- // if we don't have a uuid, we can generate one here.
95
- // but usually this is passed in.
96
- // atow we are just generating one.
97
- const uuid = await getUUID();
98
- // ---------------------------------------------------------
99
- // 1. Graph Generation & Classification
100
- // ---------------------------------------------------------
101
- const { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph } = await this.analyzeTimelines({
102
- domainIbGibs,
103
- space: source
104
- });
105
- // ---------------------------------------------------------
106
- // 2. Knowledge Exchange (Init) & 3. Conflict Resolution
107
- // ---------------------------------------------------------
108
- // Phase 1: Init Data
109
- const srcKnowledgeVector = {};
110
- Object.keys(srcTimelinesMap).forEach(tjp => {
111
- const timeline = srcTimelinesMap[tjp];
112
- const tip = timeline.at(-1);
113
- srcKnowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
114
- });
115
- // Need to determine mode correctly. Just defaulting to push for now
116
- // as per original impl instructions (or TODO)
117
- const initData = {
118
- knowledgeVector: srcKnowledgeVector,
119
- identity: identity,
120
- mode: 'push'
121
- };
122
- const initFrame = await this.createSyncFrame({
123
- uuid,
124
- stage: SyncStage.init,
125
- payload: initData,
126
- identity
127
- });
128
- await putInSpace({ space: dest, ibGib: initFrame });
129
- // Phase 2: Resolving Conflicts (Pull/Merge)
130
- await this.resolveConflicts({
131
- source,
132
- dest,
133
- srcTimelinesMap,
134
- srcKnowledgeVector,
135
- srcGraph,
136
- uuid,
137
- conflictStrategy: conflictStrategy || 'abort',
138
- });
139
- // ---------------------------------------------------------
140
- // 4. Batch Stream (Delta)
141
- // ---------------------------------------------------------
142
- const payloadAddrs = [];
143
- payloadAddrs.push(...srcStones.map(x => getIbGibAddr({ ibGib: x })));
144
- srcSortedTjps.forEach(tjp => {
145
- const timeline = srcTimelinesMap[tjp];
146
- payloadAddrs.push(...timeline.map(x => getIbGibAddr({ ibGib: x })));
147
- });
148
- const deltaData = { payloadAddrs };
149
- const deltaFrame = await this.createSyncFrame({
150
- uuid,
151
- stage: SyncStage.delta,
152
- payload: deltaData,
153
- identity
154
- });
155
- await putInSpace({ space: dest, ibGib: deltaFrame });
156
- // Streaming Logic
157
- await this.processBatchStream({
158
- dest,
159
- payloadAddrs,
160
- srcGraph
161
- });
162
- // ---------------------------------------------------------
163
- // 5. Commit
164
- // ---------------------------------------------------------
165
- const commitData = { success: true };
166
- const commitFrame = await this.createSyncFrame({
167
- uuid,
168
- stage: SyncStage.commit,
169
- payload: commitData,
170
- identity
171
- });
172
- await putInSpace({ space: dest, ibGib: commitFrame });
173
- }
174
- catch (error) {
175
- console.error(`${lc} ${extractErrorMsg(error)}`);
176
- throw error;
32
+ if (logalot) {
33
+ console.log(`${lc} starting...`);
177
34
  }
178
- finally {
179
- if (logalot) {
180
- console.log(`${lc} complete.`);
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
181
52
  }
53
+ });
54
+ if (!tempSpace) {
55
+ throw new Error(`Failed to create temp space (E: 8f4e2f3d6c1b4b1a8f4e2f3d6c1b4b1a)`);
182
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
77
+ });
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,
86
+ });
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) {
99
+ if (logalot) {
100
+ console.log(`${lc} Merging ${syncedIbGibs.length} synced ibGibs to localSpace...`);
101
+ }
102
+ await putInSpace({ space: localSpace, ibGibs: syncedIbGibs });
103
+ }
104
+ resolveDone();
105
+ updates$.complete();
106
+ }
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
+ };
183
121
  }
184
122
  /**
185
- * Analyses the timelines in the provided graph, classifying them into stones and living timelines,
186
- * and performing topological sort on the timelines.
123
+ * Drives the FSM loop of the Saga.
124
+ * Use "Ping Pong" style: Send Request -> Wait Response -> React (Handle) -> Repeat.
187
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,
140
+ });
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
+ }
188
178
  async analyzeTimelines({ domainIbGibs, space, }) {
189
179
  const lc = `${this.lc}[${this.analyzeTimelines.name}]`;
190
180
  const srcGraph = await getDependencyGraph({
@@ -200,302 +190,428 @@ export class SyncSagaCoordinator {
200
190
  const { mapWithTjp_YesDna: srcMapWithTjp_YesDna, mapWithTjp_NoDna: srcMapWithTjp_NoDna, mapWithoutTjps: src_MapWithoutTjps } = splitPerTjpAndOrDna({ ibGibs: srcIbGibs });
201
191
  const srcStones = Object.values(src_MapWithoutTjps);
202
192
  const srcLiving = [...Object.values(srcMapWithTjp_YesDna), ...Object.values(srcMapWithTjp_NoDna)];
203
- if (logalot) {
204
- console.log(`${lc} classification results (count):`);
205
- console.log(`srcStones.length: ${srcStones.length}`);
206
- console.log(`srcLiving.length: ${srcLiving.length}`);
207
- }
208
193
  const srcTimelinesMap = getTimelinesGroupedByTjp({ ibGibs: srcLiving });
209
194
  const srcSortedTjps = this.sortTimelinesTopologically(srcTimelinesMap);
210
195
  return { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph };
211
196
  }
212
- async resolveConflicts({ source, dest, srcTimelinesMap, srcKnowledgeVector, srcGraph, uuid, conflictStrategy, }) {
213
- const lc = `${this.lc}[${this.resolveConflicts.name}]`;
214
- const srcTjps = Object.keys(srcTimelinesMap);
215
- if (logalot) {
216
- console.log(`${lc} getting latest addrs from space as destKnowledge. srcTjps: ${srcTjps} (I: a954369f2b58843a6773b00dd18eb325)`);
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
+ }
217
264
  }
218
- // Get Dest Knowledge
219
- const destKnowledge = await this.getLatestAddrsFromSpace({ space: dest, tjpAddrs: srcTjps });
220
- if (logalot) {
221
- console.log(`${lc} destKnowledge:\n${pretty(destKnowledge)} (I: f76108b5f228f4e2e1e79ad387deb325)`);
265
+ catch (error) {
266
+ console.error(`${lc} ${extractErrorMsg(error)}`);
267
+ throw error;
222
268
  }
223
- const conflicts = [];
224
- for (const tjp of srcTjps) {
225
- const srcTimeline = srcTimelinesMap[tjp];
226
- const srcTip = srcTimeline[srcTimeline.length - 1];
227
- const srcTipAddr = getIbGibAddr({ ibGib: srcTip });
228
- const destTipAddr = destKnowledge[tjp];
269
+ finally {
229
270
  if (logalot) {
230
- console.log(`${lc} Checking TJP: ${tjp}`);
231
- console.log(`${lc} Source Tip: ${srcTipAddr}`);
232
- console.log(`${lc} Dest Tip: ${destTipAddr}`);
271
+ console.log(`${lc} complete.`);
233
272
  }
234
- if (destTipAddr && destTipAddr !== srcTipAddr) {
235
- if (logalot) {
236
- console.log(`${lc} Divergence found. Check if Source knows Dest's tip. (I: d084d8abb6a84cc4a8301b51dec98825)`);
237
- }
238
- if (srcGraph[destTipAddr]) {
239
- // Fast-Forward: Dest tip IS in our graph, so we are ahead.
240
- // No action needed, proceed to push.
241
- if (logalot) {
242
- console.log(`${lc} Fast-Forward (Source is ahead).`);
243
- }
244
- }
245
- else {
246
- // Conflict: Dest has a tip we don't know about.
247
- conflicts.push(tjp);
248
- if (logalot) {
249
- console.log(`${lc} CONFLICT detected (Desc is ahead/divergent).`);
250
- }
273
+ }
274
+ }
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,
251
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);
252
326
  }
253
327
  else {
254
- if (logalot) {
255
- console.log(`${lc} No divergence detected (Tips match or Dest unknown).`);
256
- }
328
+ deltaReqAddrs.push(remoteAddr);
257
329
  }
258
330
  }
259
- // Lock conflicts
260
- for (const tjp of conflicts) {
261
- if (logalot) {
262
- console.log(`${lc} Conflict detected for TJP ${tjp}. Acquiring lock...`);
263
- }
264
- // Lock VALIDITY: 60 seconds?
265
- await lockSpace({
266
- space: dest,
267
- scope: tjp,
268
- secondsValid: 60,
269
- instanceId: uuid,
270
- });
271
- // Pull & Merge Logic
272
- try {
273
- const destTipAddr = destKnowledge[tjp];
274
- if (!destTipAddr) {
275
- console.warn(`${lc} destTipAddr falsy in conflict loop? skipping. (W: 1a2b3c)`);
276
- continue;
277
- }
278
- const srcTipAddr = srcKnowledgeVector[tjp];
279
- if (logalot) {
280
- console.log(`${lc} Pulling TJP ${tjp} from Dest...`);
281
- }
282
- // 1. Get Delta from Dest
283
- // We want everything from Dest's tip back to what Source has.
284
- // We can use getDependencyGraph with 'skipAddrs' if we know what to skip.
285
- // Or just get the graph for the tip and filter?
286
- // Graph helper is robust. Let's try to get the graph stopping at Source's tip.
287
- // Optimization: We can't easily pass "stop at" to getDependencyGraph directly like "stopAt: [srcTipAddr]",
288
- // but we can pass `gotten` or `skipAddrs`.
289
- // If we pass `skipAddrs: [srcTipAddr]`, it should prune the graph traversal beyond that point?
290
- // Actually `skipAddrs` prevents retrieving that node AND its past. So yes!
291
- const skipAddrs = [];
292
- if (srcTipAddr) {
293
- skipAddrs.push(srcTipAddr);
294
- }
295
- const deltaGraph = await getDependencyGraph({
296
- space: dest,
297
- ibGibs: [],
298
- ibGibAddrs: [destTipAddr],
299
- live: false, // We usually just want the static graph back to the divergence point for now? Or live? Live is safer but slower.
300
- // If Dest is ahead, its tip IS the latest.
301
- skipAddrs,
302
- });
303
- if (logalot) {
304
- console.log(`${lc} Delta graph size: ${Object.keys(deltaGraph).length}`);
305
- }
306
- // 2. Put Delta into Source
307
- const deltaIbGibs = Object.values(deltaGraph);
308
- if (deltaIbGibs.length > 0) {
309
- if (logalot) {
310
- console.log(`${lc} Putting ${deltaIbGibs.length} ibGibs into Source (UUID: ${source.data?.uuid})...`);
311
- console.log(`${lc} Delta addrs: ${deltaIbGibs.map(x => getIbGibAddr({ ibGib: x })).join(', ')}`);
312
- }
313
- await putInSpace({ space: source, ibGibs: deltaIbGibs });
314
- if (logalot) {
315
- console.log(`${lc} Put complete.`);
316
- }
317
- }
318
- // 3. Re-evaluate / Merge
319
- // Now Source has destTipAddr.
320
- // If Source was strictly behind, srcTipAddr was an ancestor of destTipAddr.
321
- // Source is now effectively fast-forwarded to destTipAddr.
322
- // We need to check if we need to do a "Merge" (i.e. if Source HAD changes that are not in Dest).
323
- // If srcTipAddr was in deltaGraph (Wait, we skipped it), or rather if srcTipAddr is an ancestor of destTip.
324
- // If `skipAddrs` worked, we stopped AT srcTipAddr.
325
- // Ideally check if `destTipAddr` reaches `srcTipAddr`.
326
- // For now, in "Dest Ahead" scenario, we just assume we updated Source.
327
- // We update our local tracking to proceed with potential Pushing back (if we were merging).
328
- // If we simply pulled, do we need to push back?
329
- // If Dest was strictly ahead, Source catch up. Source == Dest. No Push needed.
330
- // Logic:
331
- // If Source now has destTipAddr as Latest, we are good.
332
- // TODO: Handle actual DIVERGENCE (Merge).
333
- // This implementation covers "Dest Ahead" (Fast-Backward).
334
- // But if we truly diverged (Source has changes not in Dest, and Dest has changes not in Source),
335
- // checks are needed.
336
- // If conflictStrategy is abort, we should probably check if `srcTipAddr` is an ancestor of `destTipAddr`.
337
- // If `srcTipAddr` is NOT an ancestor, then we have a divergence.
338
- // Getting ancestry is expensive without graph traversal.
339
- // But we just pulled `deltaGraph`. If `srcTipAddr` was `skipped` during traversal,
340
- // it implies it WAS reachable from `destTipAddr`.
341
- // Wait, `skipAddrs` just stops traversal. It doesn't confirm reachability if we assume it stops *at* the addr.
342
- // Actually `getDependencyGraph` with `skipAddrs` stops descending if it hits a skipped addr.
343
- // If it hits it, it means `srcTipAddr` IS an ancestor.
344
- // If it never hits it, `srcTipAddr` might NOT be an ancestor (divergence).
345
- // Or `srcTipAddr` is just disjoint?
346
- // Let's rely on the fact that if we simply "Pull", we are essentially overwriting Source's "Latest" pointer
347
- // if we don't do anything else.
348
- // If Source had 'A', and Dest had 'B', and we pull 'B'. Source now has 'A' and 'B'.
349
- // If we don't merge, Source is now just "containing" B.
350
- if (conflictStrategy === 'abort') {
351
- // Verify if it's a true divergence or just a Fast-Forward we haven't tracked yet.
352
- // If srcTipAddr is an ancestor of destTipAddr, it is a valid FF.
353
- if (srcTipAddr) {
354
- const isFF = await this.isAncestor({
355
- ancestorAddr: srcTipAddr,
356
- descendantAddr: destTipAddr,
357
- space: source
358
- });
359
- if (isFF) {
360
- if (logalot) {
361
- console.log(`${lc} Verified Fast-Forward/Pull (srcTip is ancestor). No abort.`);
362
- }
363
- }
364
- else {
365
- throw new Error(`Sync Aborted: Divergence detected on TJP ${tjp}. Source Tip: ${srcTipAddr}, Dest Tip: ${destTipAddr}. Strategy is 'abort'.`);
366
- }
367
- }
368
- else {
369
- // source has no tip (new timeline for source), so it's a valid pull.
370
- }
371
- }
372
- else if (conflictStrategy === 'optimistic') {
373
- 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)`);
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];
374
383
  }
375
384
  }
376
- catch (err) {
377
- console.error(`${lc} Error dealing with conflict/pull for TJP ${tjp}: ${extractErrorMsg(err)}`);
378
- if (conflictStrategy === 'abort')
379
- throw err;
385
+ if (ibGib) {
386
+ payloadIbGibs.push(ibGib);
387
+ }
388
+ else {
389
+ console.warn(`${lc} Requested addr not found: ${addr}`);
380
390
  }
381
- await unlockSpace({
382
- space: dest,
383
- scope: tjp,
384
- instanceId: uuid,
385
- });
386
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,
399
+ };
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 };
387
413
  }
388
- async processBatchStream({ dest, payloadAddrs, srcGraph }) {
389
- const lc = `${this.lc}[${this.processBatchStream.name}]`;
390
- // 3.1 Stream the actual data (Batch Stream)
391
- // We must preserve the order in payloadAddrs to respect dependencies.
392
- // We batch them to improve throughput.
393
- const BATCH_SIZE = 20;
394
- for (let i = 0; i < payloadAddrs.length; i += BATCH_SIZE) {
395
- const chunkAddrs = payloadAddrs.slice(i, i + BATCH_SIZE);
396
- const chunkIbGibs = chunkAddrs
397
- .map(addr => {
398
- const ibGib = srcGraph[addr];
399
- if (!ibGib) {
400
- throw new Error(`(UNEXPECTED) addr not found in graph: ${addr} (E: 8a402324057849318625aa85721665a5)`);
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',
401
434
  }
402
- return ibGib;
403
- })
404
- .filter(ibGib => !isPrimitive({ ibGib }));
405
- if (chunkIbGibs.length > 0) {
406
- if (logalot) {
407
- console.log(`${lc} streaming batch ${Math.ceil(i / BATCH_SIZE) + 1} (${chunkIbGibs.length} ibGibs)...`);
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];
408
448
  }
409
- // We don't set isDna: true here because the batch might contain mixed types.
410
- // The Space implementation should handle routing based on internal metadata if needed.
411
- await putInSpace({ space: dest, ibGibs: chunkIbGibs });
449
+ }
450
+ if (ibGib) {
451
+ outgoingPayload.push(ibGib);
412
452
  }
413
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
+ }
414
499
  }
415
- async createSyncFrame({ uuid, stage, payload, identity }) {
416
- const data = {
417
- uuid,
418
- stage,
419
- payload
420
- };
421
- const ib = await getSyncIb({ data });
422
- // Create the ibGib using Factory? Or manually?
423
- // Factory_V1.firstGen usually.
424
- const res = await Factory_V1.firstGen({
425
- parentIbGib: Factory_V1.primitive({ ib: SYNC_ATOM }),
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({
426
511
  ib,
512
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
427
513
  data,
428
- rel8ns: {
429
- identity: [getIbGibAddr({ ibGib: identity })]
430
- },
431
- /**
432
- * dna is used for merging dynamic timelines, a la a CRDT-like
433
- * mechanism that looks somewhat like event sourcing but using
434
- * crypto primitives. in our particular case, the sync should be a
435
- * single, one-off timeline (not to be used for any future merging),
436
- * so we don't need dna.
437
- */
438
- dna: false,
439
- tjp: {
440
- timestamp: true,
441
- /**
442
- * we'll use the uuid passed in
443
- */
444
- uuid: false
445
- },
446
- nCounter: true,
514
+ uuid: true, // we want the stone to have its own uniqueness
447
515
  });
448
- return res.newIbGib;
516
+ await putInSpace({ space, ibGib: stone });
517
+ await metaspace.registerNewIbGib({ ibGib: stone });
518
+ return stone;
449
519
  }
450
- async getLatestAddrsFromSpace({ space, tjpAddrs, }) {
451
- 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}]`;
452
525
  try {
453
- if (tjpAddrs.length === 0) {
454
- console.warn(`${lc} getting latest addrs for empty tjpAddrs? this may be reasonable but sounds odd. (W: 2f57c6d7c195034e982dfd38201db825)`);
455
- return {};
526
+ // Validation
527
+ if (!msgStones || msgStones.length === 0) {
528
+ throw new Error(`${lc} msgStones required (E: d13f8afcfb8816aeb26baae7df3ef726)`);
456
529
  }
457
- let map = {};
458
- const resLatest = await getLatestAddrs({
459
- tjpAddrs,
460
- space,
461
- });
462
- if (!resLatest.data) {
463
- throw new Error(`(UNEXPECTED) resLatest.data falsy? (E: 289dd61a79fa33a4f87baaa83e3c0825)`);
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)`);
464
533
  }
465
- if (resLatest.data.success) {
466
- map = clone(resLatest.data.latestAddrsMap);
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)`);
541
+ }
542
+ if (d.stage !== stage) {
543
+ throw new Error(`${lc} Mismatched stage in stones. Expected ${stage}, got ${d.stage} (E: d12c6571b0882f762921b60880c3f826)`);
544
+ }
545
+ }
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;
467
567
  }
468
568
  else {
469
- tjpAddrs.forEach(x => { map[x] = null; });
470
- }
471
- // Using the space's witness/argy pattern to invoke 'get' with 'latest' modifier
472
- // const arg = await space.argy({
473
- // ibMetadata: getSpaceArgMetadata({ space }),
474
- // argData: {
475
- // cmd: 'get',
476
- // cmdModifiers: ['latest', 'addrs'],
477
- // ibGibAddrs: tjpAddrs,
478
- // }
479
- // });
480
- // const result = await space.witness(arg);
481
- // const map: { [tjp: string]: string } = {};
482
- // if (result?.data?.success && result.data.addrs) {
483
- // // assume 1-to-1 mapping order as per convention if successful
484
- // // verification is hard without specific return map
485
- // if (result.data.addrs.length === tjpAddrs.length) {
486
- // for (let i = 0; i < tjpAddrs.length; i++) {
487
- // map[tjpAddrs[i]] = result.data.addrs[i];
488
- // }
489
- // } else {
490
- // console.warn(`${lc} result addrs length mismatch. requested: ${tjpAddrs.length}, got: ${result.data.addrs.length}. Mapping might be unsafe.`);
491
- // }
492
- // }
493
- return map;
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
+ }
494
591
  }
495
592
  catch (error) {
496
593
  console.error(`${lc} ${extractErrorMsg(error)}`);
497
- 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 };
498
613
  }
614
+ throw new Error(`Could not determine stage from frame ${getIbGibAddr({ ibGib })}`);
499
615
  }
500
616
  sortTimelinesTopologically(timelines) {
501
617
  const lc = `${this.lc}[${this.sortTimelinesTopologically.name}]`;
@@ -555,38 +671,5 @@ export class SyncSagaCoordinator {
555
671
  });
556
672
  return sorted;
557
673
  }
558
- /**
559
- * Checks if an address is an ancestor of another by traversing `past` relations.
560
- * Uses BFS. Returns true if `ancestorAddr` is found in the history of `descendantAddr`.
561
- */
562
- async isAncestor({ ancestorAddr, descendantAddr, space, }) {
563
- if (ancestorAddr === descendantAddr)
564
- return true;
565
- const queue = [descendantAddr];
566
- const visited = new Set();
567
- while (queue.length > 0) {
568
- const currentAddr = queue.shift();
569
- if (visited.has(currentAddr))
570
- continue;
571
- visited.add(currentAddr);
572
- if (currentAddr === ancestorAddr)
573
- return true;
574
- // Get node to find past
575
- const res = await getFromSpace({ space, addr: currentAddr });
576
- if (!res.success || !res.ibGibs || res.ibGibs.length === 0) {
577
- continue;
578
- }
579
- const node = res.ibGibs[0];
580
- if (node.rel8ns && node.rel8ns.past) {
581
- // Check pasts
582
- for (const pastAddr of node.rel8ns.past) {
583
- if (!visited.has(pastAddr)) {
584
- queue.push(pastAddr);
585
- }
586
- }
587
- }
588
- }
589
- return false;
590
- }
591
674
  }
592
675
  //# sourceMappingURL=sync-saga-coordinator.mjs.map