@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
@@ -9,7 +9,7 @@ import {
9
9
  import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
10
10
  import { splitPerTjpAndOrDna, getTimelinesGroupedByTjp } from "../common/other/ibgib-helper.mjs";
11
11
  import { Factory_V1 } from "@ibgib/ts-gib/dist/V1/factory.mjs";
12
- import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
12
+ import { IbGib_V1, IbGibRel8ns_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
13
13
  import { isPrimitive } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
14
14
 
15
15
  import { GLOBAL_LOG_A_LOT } from "../core-constants.mjs";
@@ -17,102 +17,36 @@ import { IbGibSpaceAny } from "../witness/space/space-base-v1.mjs";
17
17
  import { putInSpace, lockSpace, unlockSpace, getSpaceArgMetadata, getLatestAddrs, getFromSpace } from "../witness/space/space-helper.mjs";
18
18
  import { KeystoneIbGib_V1 } from "../keystone/keystone-types.mjs";
19
19
  import { KeystoneService_V1 } from "../keystone/keystone-service-v1.mjs";
20
- import { SyncStage, SYNC_ATOM } from "./sync-constants.mjs";
20
+ import { MetaspaceService } from "../witness/space/metaspace/metaspace-types.mjs";
21
+ import { SyncStage, SYNC_ATOM, SYNC_MSG_REL8N_NAME } from "./sync-constants.mjs";
22
+ import { appendToTimeline, createTimeline } from "../timeline/timeline-api.mjs";
21
23
  import {
22
24
  SyncData_V1, SyncIbGib_V1, SyncInitData, SyncDeltaData, SyncCommitData,
25
+ SyncConflictStrategy, SyncOptions,
26
+ SyncMode,
23
27
  } from "./sync-types.mjs";
24
- import { getSyncIb } from "./sync-helpers.mjs";
28
+ import { getSyncIb, isPastFrame } from "./sync-helpers.mjs";
25
29
  import { getDependencyGraph } from "../common/other/graph-helper.mjs";
30
+ import {
31
+ SyncSagaMessageData_V1,
32
+ SyncSagaMessageInitData_V1,
33
+ SyncSagaMessageAckData_V1,
34
+ SyncSagaMessageDeltaData_V1,
35
+ SyncSagaMessageCommitData_V1
36
+ } from "./sync-saga-message/sync-saga-message-types.mjs";
37
+ import { getSyncSagaMessageIb } from "./sync-saga-message/sync-saga-message-helpers.mjs";
38
+ import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-constants.mjs";
39
+ import { Subject_V1 } from "../common/pubsub/subject/subject-v1.mjs";
40
+ import { SyncSagaInfo } from "./sync-types.mjs";
41
+ import { SyncPeerWitness } from "./sync-peer/sync-peer-types.mjs";
42
+ import { SyncSagaContextIbGib_V1, SyncSagaContextCmd } from "./sync-saga-context/sync-saga-context-types.mjs";
43
+ import { createSyncSagaContext } from "./sync-saga-context/sync-saga-context-helpers.mjs";
26
44
 
27
- const logalot = GLOBAL_LOG_A_LOT || true;
28
-
29
- export interface SyncOptions {
30
- /**
31
- * The space containing the data we want to send.
32
- */
33
- source: IbGibSpaceAny;
34
- /**
35
- * The space receiving the data.
36
- */
37
- dest: IbGibSpaceAny;
38
- /**
39
- * The identity authorizing this sync.
40
- */
41
- identity: KeystoneIbGib_V1;
42
- /**
43
- * The secret for the identity (to sign the commit).
44
- */
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;
51
- }
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
45
 
46
+ const logalot = GLOBAL_LOG_A_LOT || true;
58
47
 
59
48
  /**
60
49
  * Orchestrates the synchronization process between two spaces (Source and Destination).
61
- *
62
- * ## Architecture: Dependency Graph Synchronization
63
- *
64
- * Instead of a naive file-by-file sync or a holistic "Space" sync, this coordinator operates
65
- * on a **Dependency Graph** derived from specific "Domain Roots" (e.g., a specific tag,
66
- * folder, or application root).
67
- *
68
- * ### Workflow Pipeline
69
- *
70
- * 1. **Graph Generation**:
71
- * * Generates a `FlatIbGibGraph` using `getDependencyGraph({ live: true })` starting
72
- * from the provided `domainIbGibs`.
73
- * * This ensures we capture the *latest* reachable state of all relevant timelines.
74
- *
75
- * 2. **Classification (`splitPerTjpAndOrDna`)**:
76
- * * **Stones**: Immutable, non-living ibGibs (no TJP/DNA). Trivial to sync (copy if missing).
77
- * * **Living**: Evolving timelines (TJP + DNA). Complex to sync (require ordering & merging).
78
- *
79
- * 3. **Timeline Ordering (`getTimelinesGroupedByTjp`)**:
80
- * * Living ibGibs are grouped into timelines.
81
- * * A "Timeline Dependency Graph" is built. Use Case: If a Comment Timeline refers to a
82
- * Post Timeline, the Post Timeline must be synced *before* the Comment Timeline to
83
- * ensure referential integrity at the destination.
84
- * * **Topological Sort** determines the execution order. Circular dependencies are
85
- * treated as siblings.
86
- *
87
- * 4. **Saga Execution ("Smart Coordinator, Dumb Space")**:
88
- * * The Coordinator (running on the Client/Source) drives the entire process via a
89
- * "Pull-Merge-Push" strategy to resolve conflicts.
90
- *
91
- * * **Phase 1: Knowledge Exchange (Init)**
92
- * * Generates a "Knowledge Vector" (Map<Tjp, LatestAddr>) of the Source's graph.
93
- * * Sends `SyncStage.Init` to Dest.
94
- * * Dest responds with its own Knowledge Vector for overlapping timelines.
95
- *
96
- * * **Phase 2: Gap Analysis & Conflict Resolution**
97
- * * Coordinator compares Source vs. Dest knowledge.
98
- * * **Fast-Forward**: Source is strictly ahead of Dest. Mark new frames for PUSH.
99
- * * **Fast-Backward**: Dest is strictly ahead of Source. Mark frames for PULL (to update Local).
100
- * * **Conflict/Divergence**: Both have new frames from a common ancestor.
101
- * * **LOCK**: `lockSpace({ scope: tjpGib })` on Dest to prevent race conditions.
102
- * * **PULL**: Download conflicting branch from Dest.
103
- * * **MERGE**: Execute merge logic locally (creating a new merge frame `A_merge`).
104
- * * **PUSH**: Mark `A_merge` (and dependencies) for PUSH.
105
- * * **UNLOCK**: Release Dest lock.
106
- *
107
- * * **Phase 3: Batch Streaming (Delta)**
108
- * * **Stones**: Batch `putInSpace` all missing "Stone" ibGibs first.
109
- * * **Timelines**: Batch `putInSpace` Living Timelines in topological order.
110
- * * *Note*: The `SyncFrame` (Init/Delta/Commit) tracks protocol state, but data transfer
111
- * happens via standard `putInSpace`.
112
- *
113
- * * **Phase 4: Commit**
114
- * * Update Local Index (register new latests).
115
- * * Send `SyncStage.Commit` to Dest to finalize session.
116
50
  */
117
51
  export class SyncSagaCoordinator {
118
52
  protected lc: string = `[${SyncSagaCoordinator.name}]`;
@@ -122,137 +56,200 @@ export class SyncSagaCoordinator {
122
56
  ) { }
123
57
 
124
58
  /**
125
- * Executes a synchronization saga.
126
- *
127
- * @param opts.source The local space (Client) driving the sync.
128
- * @param opts.dest The remote space (Server/Other Peer) to sync with.
129
- * @param opts.identity The Keystone Identity performing the sync.
130
- * @param opts.domainIbGibs The root ibGibs that define the scope of the sync (the "Dependency Graph").
131
- * @param opts.identitySecret Optional secret if needed (usually handled by `keystone` service).
59
+ * Executes a synchronization saga using the Symmetric Sync Protocol.
132
60
  */
133
61
  async sync({
134
- source,
135
- dest,
136
- identity,
62
+ peer,
63
+ localSpace,
64
+ metaspace,
65
+ primaryIdentity,
66
+ primaryIdentitySecret,
137
67
  domainIbGibs,
138
- identitySecret,
139
- conflictStrategy
140
68
  }: {
141
- source: IbGibSpaceAny,
142
- dest: IbGibSpaceAny,
143
- identity: KeystoneIbGib_V1,
69
+ peer: SyncPeerWitness,
70
+ localSpace: IbGibSpaceAny,
71
+ metaspace: MetaspaceService,
72
+ primaryIdentity: KeystoneIbGib_V1,
73
+ primaryIdentitySecret: string,
144
74
  domainIbGibs: IbGib_V1[],
145
- identitySecret?: string,
146
- conflictStrategy?: SyncConflictStrategy,
147
- }): Promise<void> {
75
+ }): Promise<SyncSagaInfo> {
148
76
  const lc = `${this.lc}[${this.sync.name}]`;
149
- try {
150
- if (logalot) { console.log(`${lc} starting...`); }
151
-
152
- // 1. Generate UUID for this saga
153
- // const uuid = await Factory_V1.primitive({ ib: "genuuid" }); // Hacky usage of primitive helper if available, or just UUID lib
154
-
155
- // if we don't have a uuid, we can generate one here.
156
- // but usually this is passed in.
157
- // atow we are just generating one.
158
- const uuid = await getUUID();
159
-
160
- // ---------------------------------------------------------
161
- // 1. Graph Generation & Classification
162
- // ---------------------------------------------------------
163
- const { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph } = await this.analyzeTimelines({
164
- domainIbGibs,
165
- space: source
166
- });
77
+ if (logalot) { console.log(`${lc} starting...`); }
78
+
79
+ // 1. SETUP SAGA METADATA
80
+ const sagaId = await getUUID();
81
+
82
+ // Setup Observable & Promise
83
+ const updates$ = new Subject_V1<SyncSagaContextIbGib_V1>();
84
+ let resolveDone!: () => void;
85
+ let rejectDone!: (err: any) => void;
86
+ const done = new Promise<void>((resolve, reject) => {
87
+ resolveDone = resolve;
88
+ rejectDone = reject;
89
+ });
167
90
 
168
- // ---------------------------------------------------------
169
- // 2. Knowledge Exchange (Init) & 3. Conflict Resolution
170
- // ---------------------------------------------------------
171
- // Phase 1: Init Data
172
- const srcKnowledgeVector: { [tjp: string]: string } = {};
173
- Object.keys(srcTimelinesMap).forEach(tjp => {
174
- const timeline = srcTimelinesMap[tjp];
175
- const tip = timeline.at(-1)!;
176
- srcKnowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
177
- });
91
+ // WORKING CONTEXT (Transactional)
92
+ const tempSpace = await metaspace.createNewLocalSpace({
93
+ opts: {
94
+ allowCancel: false,
95
+ spaceName: undefined,
96
+ getFnPrompt: metaspace.getFnPrompt!,
97
+ logalot
98
+ }
99
+ });
100
+ if (!tempSpace) { throw new Error(`Failed to create temp space (E: 8f4e2f3d6c1b4b1a8f4e2f3d6c1b4b1a)`); }
178
101
 
179
- // Need to determine mode correctly. Just defaulting to push for now
180
- // as per original impl instructions (or TODO)
181
- const initData: SyncInitData = {
182
- knowledgeVector: srcKnowledgeVector,
183
- identity: identity,
184
- mode: 'push'
185
- };
102
+ const cleanup = async () => {
103
+ // In-memory temp space cleanup if needed (e.g. if we used a specialized disposable space)
104
+ // await tempSpace.destroy();
105
+ };
186
106
 
187
- const initFrame = await this.createSyncFrame({
188
- uuid,
189
- stage: SyncStage.init,
190
- payload: initData,
191
- identity
192
- });
193
- await putInSpace({ space: dest, ibGib: initFrame });
194
-
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
- });
107
+ // Async execution wrapper
108
+ (async () => {
109
+ try {
110
+ // 2. BOOTSTRAP IDENTITY (Session Keystone)
111
+ const config: any = {
112
+ id: 'default',
113
+ type: 'hash-reveal-v1',
114
+ salt: sagaId,
115
+ behavior: { size: 10, replenish: 'top-up', selectSequentially: 1, selectRandomly: 1, targetBindingChars: 0 },
116
+ algo: 'SHA-256', rounds: 1
117
+ };
118
+ const sessionIdentity = await this.keystone.genesis({
119
+ masterSecret: sagaId,
120
+ configs: [config],
121
+ metaspace,
122
+ space: tempSpace
123
+ });
205
124
 
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
- });
125
+ // 3. CREATE INITIAL FRAME (Stage.init)
126
+ const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
127
+ sagaId,
128
+ sessionIdentity,
129
+ localSpace,
130
+ domainIbGibs,
131
+ tempSpace,
132
+ metaspace,
133
+ });
215
134
 
216
- const deltaData: SyncDeltaData = { payloadAddrs };
217
- const deltaFrame = await this.createSyncFrame({
218
- uuid,
219
- stage: SyncStage.delta,
220
- payload: deltaData,
221
- identity
222
- });
223
- await putInSpace({ space: dest, ibGib: deltaFrame });
135
+ // 4. EXECUTE SAGA LOOP (FSM)
136
+ const syncedIbGibs = await this.executeSagaLoop({
137
+ initialFrame: initFrame,
138
+ srcGraph,
139
+ peer,
140
+ sessionIdentity,
141
+ updates$,
142
+ tempSpace,
143
+ metaspace
144
+ });
145
+
146
+ // 5. MERGE RESULT (Optimistic Commit)
147
+ if (syncedIbGibs && syncedIbGibs.length > 0) {
148
+ if (logalot) { console.log(`${lc} Merging ${syncedIbGibs.length} synced ibGibs to localSpace...`); }
149
+ await putInSpace({ space: localSpace, ibGibs: syncedIbGibs });
150
+ }
151
+
152
+ resolveDone();
153
+ updates$.complete();
154
+
155
+ } catch (error) {
156
+ console.error(`${lc} ${extractErrorMsg(error)}`);
157
+ rejectDone(error);
158
+ updates$.error(error);
159
+ } finally {
160
+ await cleanup();
161
+ }
162
+ })();
224
163
 
225
- // Streaming Logic
226
- await this.processBatchStream({
227
- dest,
228
- payloadAddrs,
229
- srcGraph
164
+ return {
165
+ sagaId,
166
+ updates$,
167
+ done
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Drives the FSM loop of the Saga.
173
+ * Use "Ping Pong" style: Send Request -> Wait Response -> React (Handle) -> Repeat.
174
+ */
175
+ protected async executeSagaLoop({
176
+ initialFrame,
177
+ srcGraph,
178
+ peer,
179
+ sessionIdentity,
180
+ updates$,
181
+ tempSpace,
182
+ metaspace
183
+ }: {
184
+ initialFrame: SyncIbGib_V1,
185
+ srcGraph: { [addr: string]: IbGib_V1 },
186
+ peer: SyncPeerWitness,
187
+ sessionIdentity: KeystoneIbGib_V1,
188
+ updates$: Subject_V1<SyncSagaContextIbGib_V1>,
189
+ tempSpace: IbGibSpaceAny,
190
+ metaspace: MetaspaceService
191
+ }): Promise<IbGib_V1[]> {
192
+ const lc = `${this.lc}[${this.executeSagaLoop.name}]`;
193
+
194
+ let currentFrame: SyncIbGib_V1 | null = initialFrame;
195
+ let nextPayload: IbGib_V1[] = [];
196
+ const allReceivedIbGibs: IbGib_V1[] = [];
197
+
198
+ while (currentFrame) {
199
+ // A. Create Context (Request)
200
+ const payloadAddrs = nextPayload.map(p => getIbGibAddr({ ibGib: p }));
201
+
202
+ const requestCtx = await createSyncSagaContext({
203
+ cmd: SyncSagaContextCmd.process,
204
+ sagaFrame: currentFrame,
205
+ msgs: [],
206
+ sessionKeystones: [sessionIdentity],
207
+ payload: payloadAddrs.length > 0 ? payloadAddrs : undefined,
230
208
  });
231
209
 
232
- // ---------------------------------------------------------
233
- // 5. Commit
234
- // ---------------------------------------------------------
235
- const commitData: SyncCommitData = { success: true };
236
- const commitFrame = await this.createSyncFrame({
237
- uuid,
238
- stage: SyncStage.commit,
239
- payload: commitData,
240
- identity
210
+ // B. Transmit
211
+ updates$.next(requestCtx);
212
+ const responseCtx = await peer.witness(requestCtx);
213
+
214
+ // C. Handle Response
215
+ if (!responseCtx) {
216
+ console.warn(`${lc} Peer returned no response context. Ending loop.`);
217
+ currentFrame = null;
218
+ break;
219
+ }
220
+
221
+ updates$.next(responseCtx);
222
+
223
+ // D. Extract Remote Frame & React
224
+ const remoteFrameAddr = responseCtx.rel8ns?.sagaFrame?.[0];
225
+ if (!remoteFrameAddr) {
226
+ console.warn(`${lc} Peer response has no sagaFrame. Ending loop.`);
227
+ currentFrame = null;
228
+ break;
229
+ }
230
+
231
+ const remoteFrame = await getFromSpace({ addr: remoteFrameAddr, space: tempSpace });
232
+ if (!remoteFrame) { throw new Error(`Could not resolve remote frame: ${remoteFrameAddr}`); }
233
+
234
+ // React (Reducer)
235
+ const result = await this.handleSagaFrame({
236
+ sagaIbGib: remoteFrame as SyncIbGib_V1,
237
+ srcGraph,
238
+ space: tempSpace,
239
+ identity: sessionIdentity,
240
+ metaspace
241
241
  });
242
- await putInSpace({ space: dest, ibGib: commitFrame });
243
242
 
244
- } catch (error) {
245
- console.error(`${lc} ${extractErrorMsg(error)}`);
246
- throw error;
247
- } finally {
248
- if (logalot) { console.log(`${lc} complete.`); }
243
+ currentFrame = result?.frame || null;
244
+ nextPayload = result?.payload || [];
245
+ if (result?.receivedPayload) {
246
+ allReceivedIbGibs.push(...result.receivedPayload);
247
+ }
249
248
  }
249
+
250
+ return allReceivedIbGibs;
250
251
  }
251
252
 
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
253
  protected async analyzeTimelines({
257
254
  domainIbGibs,
258
255
  space,
@@ -285,339 +282,568 @@ export class SyncSagaCoordinator {
285
282
  const srcStones = Object.values(src_MapWithoutTjps);
286
283
  const srcLiving = [...Object.values(srcMapWithTjp_YesDna), ...Object.values(srcMapWithTjp_NoDna)];
287
284
 
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
285
  const srcTimelinesMap = getTimelinesGroupedByTjp({ ibGibs: srcLiving });
295
286
  const srcSortedTjps = this.sortTimelinesTopologically(srcTimelinesMap);
296
287
 
297
288
  return { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph };
298
289
  }
299
290
 
300
- protected async resolveConflicts({
301
- source,
302
- dest,
303
- srcTimelinesMap,
304
- srcKnowledgeVector,
291
+ protected async resolveConflicts(): Promise<void> {
292
+ throw new Error("resolveConflicts is no longer supported.");
293
+ }
294
+
295
+ protected async processBatchStream(): Promise<void> {
296
+ // Deprecated by payload in SyncSagaContext
297
+ throw new Error("processBatchStream is no longer supported.");
298
+ }
299
+
300
+ /**
301
+ * Creates the Initial Saga Frame (Init Stage).
302
+ */
303
+ protected async createInitFrame({
304
+ sagaId,
305
+ sessionIdentity,
306
+ localSpace,
307
+ domainIbGibs,
308
+ tempSpace,
309
+ metaspace
310
+ }: {
311
+ sagaId: string,
312
+ sessionIdentity: KeystoneIbGib_V1,
313
+ localSpace: IbGibSpaceAny,
314
+ domainIbGibs: IbGib_V1[],
315
+ tempSpace: IbGibSpaceAny,
316
+ metaspace: MetaspaceService
317
+ }): Promise<{ sagaFrame: SyncIbGib_V1, srcGraph: { [addr: string]: IbGib_V1 } }> {
318
+ const initData: SyncSagaMessageInitData_V1 = {
319
+ sagaId,
320
+ stage: SyncStage.init,
321
+ knowledgeVector: {},
322
+ identity: sessionIdentity, // KeystoneIbGib is already public data
323
+ mode: SyncMode.sync,
324
+ };
325
+
326
+ // Populate Knowledge Vector
327
+ const { srcTimelinesMap, srcGraph } = await this.analyzeTimelines({ domainIbGibs, space: localSpace });
328
+ Object.keys(srcTimelinesMap).forEach(tjp => {
329
+ const timeline = srcTimelinesMap[tjp];
330
+ const tip = timeline.at(-1)!;
331
+ initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
332
+ });
333
+
334
+ const initStone = await this.createSyncMsgStone({
335
+ data: initData,
336
+ space: tempSpace,
337
+ metaspace
338
+ });
339
+
340
+ const sagaFrame = await this.evolveSyncSagaIbGib({
341
+ msgStones: [initStone],
342
+ identity: sessionIdentity,
343
+ space: tempSpace,
344
+ metaspace
345
+ });
346
+
347
+ return { sagaFrame, srcGraph };
348
+ }
349
+
350
+ /**
351
+ * Reacts to an incoming saga frame and dispatches to appropriate handler.
352
+ */
353
+ async handleSagaFrame({
354
+ sagaIbGib,
305
355
  srcGraph,
306
- uuid,
307
- conflictStrategy,
356
+ space,
357
+ identity,
358
+ identitySecret,
359
+ metaspace,
308
360
  }: {
309
- source: IbGibSpaceAny,
310
- dest: IbGibSpaceAny,
311
- srcTimelinesMap: { [tjp: string]: IbGib_V1[] },
312
- srcKnowledgeVector: { [tjp: string]: string },
361
+ sagaIbGib: SyncIbGib_V1,
362
+ srcGraph: { [addr: string]: IbGib_V1 },
363
+ space: IbGibSpaceAny,
364
+ identity: KeystoneIbGib_V1,
365
+ identitySecret?: string,
366
+ metaspace: MetaspaceService,
367
+ }): Promise<{ frame: SyncIbGib_V1, payload?: IbGib_V1[], receivedPayload?: IbGib_V1[] } | null> {
368
+ const lc = `${this.lc}[${this.handleSagaFrame.name}]`;
369
+ try {
370
+ if (logalot) { console.log(`${lc} starting... (I: 5deec8a1f7a6d263c88cd458ad990826)`); }
371
+
372
+ if (!sagaIbGib.data) { throw new Error(`(UNEXPECTED) sagaIbGib.data falsy? (E: 71b938adf1d87c2527bfd4f86dfd0826)`); }
373
+ if (logalot) { console.log(`${lc} handling frame stage: ${sagaIbGib.data.stage}`); }
374
+
375
+ // Get Stage from Stone (or Frame for Init fallback)
376
+ const { stage, payload } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
377
+
378
+ switch (stage) {
379
+ case SyncStage.init:
380
+ return await this.handleInitFrame({ sagaIbGib, payload, space, metaspace, identity, identitySecret });
381
+
382
+ case SyncStage.ack:
383
+ return await this.handleAckFrame({ sagaIbGib, srcGraph, space, metaspace, identity });
384
+
385
+ case SyncStage.delta:
386
+ return await this.handleDeltaFrame({ sagaIbGib, srcGraph, space, identity, metaspace });
387
+
388
+ case SyncStage.commit:
389
+ return await this.handleCommitFrame({ sagaIbGib, space });
390
+
391
+ default:
392
+ throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
393
+ }
394
+ } catch (error) {
395
+ console.error(`${lc} ${extractErrorMsg(error)}`);
396
+ throw error;
397
+ } finally {
398
+ if (logalot) { console.log(`${lc} complete.`); }
399
+ }
400
+ }
401
+
402
+ // #region Handlers
403
+
404
+ protected async handleInitFrame({
405
+ sagaIbGib,
406
+ payload,
407
+ space,
408
+ metaspace,
409
+ identity,
410
+ identitySecret,
411
+ }: {
412
+ sagaIbGib: SyncIbGib_V1,
413
+ payload: any,
414
+ space: IbGibSpaceAny,
415
+ metaspace: MetaspaceService,
416
+ identity: KeystoneIbGib_V1,
417
+ identitySecret?: string,
418
+ }): Promise<{ frame: SyncIbGib_V1, payload?: IbGib_V1[] } | null> {
419
+ const lc = `${this.lc}[${this.handleInitFrame.name}]`;
420
+ if (logalot) { console.log(`${lc} starting...`); }
421
+
422
+ // Extract Init Data
423
+ const initData = sagaIbGib.data!.payload as SyncInitData;
424
+ if (!initData || !initData.knowledgeVector) {
425
+ throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
426
+ }
427
+ const remoteKV = initData.knowledgeVector;
428
+ const remoteTjps = Object.keys(remoteKV);
429
+
430
+ // 1. Get Local Latest Addrs for all TJPs
431
+ let localKV: { [tjp: string]: string | null } = {};
432
+ if (remoteTjps.length > 0) {
433
+ // Batch get latest addrs for the TJPs
434
+ const arg = await space.argy({
435
+ argData: {
436
+ cmd: 'get',
437
+ cmdModifiers: ['latest', 'tjps'], // interprets input addrs as TJPs
438
+ ibGibAddrs: remoteTjps,
439
+ } as any
440
+ });
441
+ const res = await space.witness(arg);
442
+ if (res?.data?.latestAddrsMap) {
443
+ localKV = res.data.latestAddrsMap;
444
+ }
445
+ }
446
+
447
+ // 2. Gap Analysis
448
+ const deltaReqAddrs: string[] = [];
449
+ const pushOfferAddrs: string[] = [];
450
+ // const divergentTjps: string[] = []; // Handle later
451
+
452
+ for (const tjp of remoteTjps) {
453
+ const remoteAddr = remoteKV[tjp];
454
+ const localAddr = localKV[tjp];
455
+
456
+ if (!localAddr) {
457
+ deltaReqAddrs.push(remoteAddr);
458
+ continue;
459
+ }
460
+
461
+ if (localAddr === remoteAddr) {
462
+ continue;
463
+ }
464
+
465
+ // Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
466
+ const isRemoteInPast = await isPastFrame({
467
+ olderAddr: remoteAddr,
468
+ newerAddr: localAddr,
469
+ space,
470
+ });
471
+
472
+ if (isRemoteInPast) {
473
+ pushOfferAddrs.push(localAddr);
474
+ } else {
475
+ deltaReqAddrs.push(remoteAddr);
476
+ }
477
+ }
478
+
479
+ // 3. Create Ack Frame
480
+ const sagaId = sagaIbGib.data!.uuid;
481
+ // Create Payload Stone
482
+ const ackData: SyncSagaMessageAckData_V1 = {
483
+ sagaId,
484
+ stage: SyncStage.ack,
485
+ deltaReqAddrs,
486
+ pushOfferAddrs,
487
+ };
488
+
489
+ const ackStone = await this.createSyncMsgStone({
490
+ data: ackData,
491
+ space,
492
+ metaspace,
493
+ });
494
+
495
+ // Evolve Saga
496
+ const ackFrame = await this.evolveSyncSagaIbGib({
497
+ prevSagaIbGib: sagaIbGib,
498
+ msgStones: [ackStone],
499
+ identity,
500
+ space,
501
+ metaspace,
502
+ });
503
+
504
+ return { frame: ackFrame };
505
+ }
506
+
507
+ protected async handleAckFrame({
508
+ sagaIbGib,
509
+ srcGraph,
510
+ space,
511
+ metaspace,
512
+ identity,
513
+ }: {
514
+ sagaIbGib: SyncIbGib_V1,
313
515
  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}`);
516
+ space: IbGibSpaceAny,
517
+ metaspace: MetaspaceService,
518
+ identity: KeystoneIbGib_V1,
519
+ }): Promise<{ frame: SyncIbGib_V1, payload?: IbGib_V1[] } | null> {
520
+ const lc = `${this.lc}[${this.handleAckFrame.name}]`;
521
+ if (logalot) { console.log(`${lc} starting...`); }
522
+
523
+ const { payload } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
524
+ const ackData = payload as SyncSagaMessageAckData_V1;
525
+
526
+ if (!ackData || ackData.stage !== SyncStage.ack) {
527
+ throw new Error(`${lc} Invalid ack frame (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
528
+ }
529
+
530
+ const deltaReqAddrs = ackData.deltaReqAddrs || [];
531
+ const pushOfferAddrs = ackData.pushOfferAddrs || [];
532
+
533
+ // 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
534
+ const pullReqAddrs: string[] = [];
535
+ for (const addr of pushOfferAddrs) {
536
+ const existing = srcGraph[addr] || (await getFromSpace({ addr, space })).ibGibs?.[0];
537
+ if (!existing) {
538
+ pullReqAddrs.push(addr);
337
539
  }
540
+ }
338
541
 
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).`); }
542
+ // 2. Process Delta Requests (Push Payload)
543
+ const payloadIbGibs: IbGib_V1[] = [];
544
+ for (const addr of deltaReqAddrs) {
545
+ let ibGib = srcGraph[addr];
546
+ if (!ibGib) {
547
+ const res = await getFromSpace({ addr, space });
548
+ if (res.ibGibs && res.ibGibs.length > 0) {
549
+ ibGib = res.ibGibs[0];
349
550
  }
551
+ }
552
+ if (ibGib) {
553
+ payloadIbGibs.push(ibGib);
350
554
  } else {
351
- if (logalot) { console.log(`${lc} No divergence detected (Tips match or Dest unknown).`); }
555
+ console.warn(`${lc} Requested addr not found: ${addr}`);
352
556
  }
353
557
  }
354
558
 
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,
559
+ // 3. Create Delta Frame
560
+ const sagaId = ackData.sagaId;
561
+ const deltaData: SyncSagaMessageDeltaData_V1 = {
562
+ sagaId,
563
+ stage: SyncStage.delta,
564
+ payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
565
+ requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
566
+ };
567
+
568
+ const deltaStone = await this.createSyncMsgStone({
569
+ data: deltaData,
570
+ space,
571
+ metaspace,
572
+ });
573
+
574
+ const deltaFrame = await this.evolveSyncSagaIbGib({
575
+ prevSagaIbGib: sagaIbGib,
576
+ msgStones: [deltaStone],
577
+ identity,
578
+ space,
579
+ metaspace,
580
+ });
581
+
582
+ return { frame: deltaFrame, payload: payloadIbGibs };
583
+ }
584
+
585
+ protected async handleDeltaFrame({
586
+ sagaIbGib,
587
+ srcGraph,
588
+ space,
589
+ metaspace,
590
+ identity,
591
+ }: {
592
+ sagaIbGib: SyncIbGib_V1,
593
+ srcGraph: { [addr: string]: IbGib_V1 },
594
+ space: IbGibSpaceAny,
595
+ metaspace: MetaspaceService,
596
+ identity: KeystoneIbGib_V1,
597
+ }): Promise<{ frame: SyncIbGib_V1, payload?: IbGib_V1[], receivedPayload?: IbGib_V1[] } | null> {
598
+ const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
599
+ if (logalot) { console.log(`${lc} starting...`); }
600
+
601
+ const { payload } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
602
+ const deltaData = payload as SyncSagaMessageDeltaData_V1;
603
+
604
+ if (!deltaData || deltaData.stage !== SyncStage.delta) {
605
+ throw new Error(`${lc} Invalid delta frame (E: 7c28c8d8f08a4421b8344e6727271421)`);
606
+ }
607
+
608
+ const payloadAddrs = deltaData.payloadAddrs || [];
609
+ const requests = deltaData.requests || [];
610
+
611
+ // 1. Process Received Payload
612
+ const receivedPayload: IbGib_V1[] = [];
613
+ if (payloadAddrs.length > 0) {
614
+ const res = await space.witness({
615
+ attribute: 'ibGibs',
616
+ arg: {
617
+ ibGibAddrs: payloadAddrs,
618
+ cmd: 'get',
619
+ } as any
364
620
  });
621
+ if (res.data?.ibGibs) {
622
+ receivedPayload.push(...res.data.ibGibs);
623
+ }
624
+ }
365
625
 
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;
626
+ // 2. Fulfill Requests (Outgoing Payload)
627
+ const outgoingPayload: IbGib_V1[] = [];
628
+ for (const addr of requests) {
629
+ let ibGib = srcGraph[addr];
630
+ if (!ibGib) {
631
+ const res = await getFromSpace({ addr, space });
632
+ if (res.ibGibs && res.ibGibs.length > 0) {
633
+ ibGib = res.ibGibs[0];
372
634
  }
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
- });
635
+ }
636
+ if (ibGib) {
637
+ outgoingPayload.push(ibGib);
638
+ }
639
+ }
399
640
 
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
- }
641
+ // 3. Determine Next Stage
642
+ if (requests.length > 0) {
643
+ // They requested more data -> Send Delta
644
+ const sagaId = deltaData.sagaId;
645
+ const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
646
+ sagaId,
647
+ stage: SyncStage.delta,
648
+ payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
649
+ };
412
650
 
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
- }
651
+ const deltaStone = await this.createSyncMsgStone({
652
+ data: responseDeltaData,
653
+ space,
654
+ metaspace
655
+ });
472
656
 
473
- } catch (err) {
474
- console.error(`${lc} Error dealing with conflict/pull for TJP ${tjp}: ${extractErrorMsg(err)}`);
475
- if (conflictStrategy === 'abort') throw err;
476
- }
657
+ const deltaFrame = await this.evolveSyncSagaIbGib({
658
+ prevSagaIbGib: sagaIbGib,
659
+ msgStones: [deltaStone],
660
+ identity,
661
+ space,
662
+ metaspace
663
+ });
664
+
665
+ return { frame: deltaFrame, payload: outgoingPayload, receivedPayload };
666
+
667
+ } else {
668
+ // No requests -> Commit
669
+ const sagaId = deltaData.sagaId;
670
+ const commitData: SyncSagaMessageCommitData_V1 = {
671
+ sagaId,
672
+ stage: SyncStage.commit,
673
+ success: true,
674
+ };
477
675
 
478
- await unlockSpace({
479
- space: dest,
480
- scope: tjp,
481
- instanceId: uuid,
676
+ const commitStone = await this.createSyncMsgStone({
677
+ data: commitData,
678
+ space,
679
+ metaspace
680
+ });
681
+
682
+ const commitFrame = await this.evolveSyncSagaIbGib({
683
+ prevSagaIbGib: sagaIbGib,
684
+ msgStones: [commitStone],
685
+ identity,
686
+ space,
687
+ metaspace
482
688
  });
689
+
690
+ return { frame: commitFrame, receivedPayload };
483
691
  }
484
692
  }
485
693
 
486
- protected async processBatchStream({
487
- dest,
488
- payloadAddrs,
489
- srcGraph
694
+ protected async handleCommitFrame({
695
+ sagaIbGib,
696
+ space,
490
697
  }: {
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
- }
698
+ sagaIbGib: SyncIbGib_V1,
699
+ space: IbGibSpaceAny,
700
+ }): Promise<{ frame: SyncIbGib_V1, payload?: IbGib_V1[] } | null> {
701
+ const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
702
+ if (logalot) { console.log(`${lc} Commit received. Saga complete.`); }
703
+ return null;
517
704
  }
518
705
 
519
- protected async createSyncFrame({
520
- uuid,
521
- stage,
522
- payload,
523
- identity
706
+ // #endregion Handlers
707
+
708
+ protected async createSyncMsgStone<TStoneData extends SyncSagaMessageData_V1>({
709
+ data,
710
+ space,
711
+ metaspace,
524
712
  }: {
525
- uuid: string,
526
- stage: any, // SyncStage
527
- payload: any,
528
- identity: KeystoneIbGib_V1
529
- }): Promise<SyncIbGib_V1> {
530
- const data: SyncData_V1 = {
531
- uuid,
532
- stage,
533
- payload
534
- };
535
- const ib = await getSyncIb({ data });
536
- // Create the ibGib using Factory? Or manually?
537
- // Factory_V1.firstGen usually.
538
- const res = await Factory_V1.firstGen({
539
- parentIbGib: Factory_V1.primitive({ ib: SYNC_ATOM }),
713
+ data: TStoneData,
714
+ space: IbGibSpaceAny,
715
+ metaspace: MetaspaceService,
716
+ }): Promise<IbGib_V1<TStoneData>> {
717
+ const ib = await getSyncSagaMessageIb({ data });
718
+ const stone = await Factory_V1.stone({
540
719
  ib,
720
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
541
721
  data,
542
- rel8ns: {
543
- identity: [getIbGibAddr({ ibGib: identity })]
544
- },
545
- /**
546
- * dna is used for merging dynamic timelines, a la a CRDT-like
547
- * mechanism that looks somewhat like event sourcing but using
548
- * crypto primitives. in our particular case, the sync should be a
549
- * single, one-off timeline (not to be used for any future merging),
550
- * so we don't need dna.
551
- */
552
- dna: false,
553
- tjp: {
554
- timestamp: true,
555
- /**
556
- * we'll use the uuid passed in
557
- */
558
- uuid: false
559
- },
560
- nCounter: true,
722
+ uuid: true, // we want the stone to have its own uniqueness
561
723
  });
562
- return res.newIbGib as SyncIbGib_V1;
724
+ await putInSpace({ space, ibGib: stone });
725
+ await metaspace.registerNewIbGib({ ibGib: stone });
726
+ return stone as IbGib_V1<TStoneData>;
563
727
  }
564
728
 
565
- protected async getLatestAddrsFromSpace({
729
+
730
+ /**
731
+ * Evolves the saga timeline with a new frame.
732
+ */
733
+ protected async evolveSyncSagaIbGib({
734
+ prevSagaIbGib,
735
+ msgStones,
736
+ identity,
566
737
  space,
567
- tjpAddrs,
738
+ metaspace,
568
739
  }: {
740
+ prevSagaIbGib?: SyncIbGib_V1,
741
+ msgStones: IbGib_V1[],
742
+ identity: KeystoneIbGib_V1,
569
743
  space: IbGibSpaceAny,
570
- tjpAddrs: string[],
571
- }): Promise<{ [tjpAddr: string]: string | null }> {
572
- const lc = `${this.lc}[${this.getLatestAddrsFromSpace.name}]`;
744
+ metaspace: MetaspaceService,
745
+ }): Promise<SyncIbGib_V1> {
746
+ const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
573
747
  try {
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 {};
748
+ // Validation
749
+ if (!msgStones || msgStones.length === 0) {
750
+ throw new Error(`${lc} msgStones required (E: d13f8afcfb8816aeb26baae7df3ef726)`);
751
+ }
752
+ const firstStoneData = msgStones[0].data as SyncSagaMessageData_V1;
753
+ if (!firstStoneData?.sagaId || !firstStoneData?.stage) {
754
+ throw new Error(`${lc} Invalid stone data (missing sagaId or stage) (E: c004f85ce8f8958b11839f8d68c38826)`);
755
+ }
756
+ const sagaId = firstStoneData.sagaId;
757
+ const stage = firstStoneData.stage;
758
+
759
+ // Ensure all stones match sagaId and stage
760
+ for (const stone of msgStones) {
761
+ const d = stone.data as SyncSagaMessageData_V1;
762
+ if (d.sagaId !== sagaId) {
763
+ throw new Error(`${lc} Mismatched sagaId in stones. Expected ${sagaId}, got ${d.sagaId} (E: 29e02719e426ecbbf8ee66887a2be226)`);
764
+ }
765
+ if (d.stage !== stage) {
766
+ throw new Error(`${lc} Mismatched stage in stones. Expected ${stage}, got ${d.stage} (E: d12c6571b0882f762921b60880c3f826)`);
767
+ }
577
768
  }
578
769
 
579
- let map: { [tjp: string]: string | null } = {};
580
- const resLatest = await getLatestAddrs({
581
- tjpAddrs,
582
- space,
583
- });
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 };
770
+ const identityAddr = getIbGibAddr({ ibGib: identity });
771
+
772
+ if (prevSagaIbGib) {
773
+ // Append to existing timeline
774
+ const resAppend = await appendToTimeline({
775
+ timeline: prevSagaIbGib,
776
+ rel8nInfos: [
777
+ {
778
+ rel8nName: SYNC_MSG_REL8N_NAME,
779
+ ibGibs: msgStones,
780
+ },
781
+ {
782
+ rel8nName: 'identity',
783
+ ibGibs: [identity],
784
+ }
785
+ ],
786
+ metaspace,
787
+ space,
788
+ noDna: true, // Explicitly no DNA for sync frames
789
+ });
790
+
791
+ const newFrame = resAppend as SyncIbGib_V1;
792
+ return newFrame;
587
793
  } else {
588
- tjpAddrs.forEach(x => { map[x] = null; });
794
+ // Create New Timeline (Root Frame)
795
+ const data: SyncData_V1 = {
796
+ uuid: sagaId,
797
+ stage,
798
+ payload: undefined, // Data in stone
799
+ };
800
+ const ib = await getSyncIb({ data });
801
+
802
+ const stoneAddrs = msgStones.map(s => getIbGibAddr({ ibGib: s }));
803
+
804
+ const resNew = await createTimeline({
805
+ space,
806
+ metaspace,
807
+ ib,
808
+ data,
809
+ rel8ns: {
810
+ [SYNC_MSG_REL8N_NAME]: stoneAddrs,
811
+ identity: [identityAddr],
812
+ },
813
+ parentIb: SYNC_ATOM, // "sync"
814
+ noDna: true,
815
+ });
816
+
817
+ return resNew.newIbGib as SyncIbGib_V1;
589
818
  }
590
819
 
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
- // }
614
- return map;
615
820
  } catch (error) {
616
821
  console.error(`${lc} ${extractErrorMsg(error)}`);
617
- return {}; // fail safe
822
+ throw error;
618
823
  }
619
824
  }
620
825
 
826
+ protected async getStageAndPayloadFromFrame({ ibGib, space }: { ibGib: IbGib_V1, space: IbGibSpaceAny }): Promise<{ stage: SyncStage, payload: any }> {
827
+ // 1. Try Stone (Primary)
828
+ const stoneAddrs = ibGib.rel8ns?.[SYNC_MSG_REL8N_NAME];
829
+ if (stoneAddrs && stoneAddrs.length > 0) {
830
+ const stoneAddr = stoneAddrs[0];
831
+ const res = await getFromSpace({ addr: stoneAddr, space });
832
+ if (res.ibGibs && res.ibGibs.length > 0) {
833
+ const stone = res.ibGibs[0];
834
+ if (stone.data && stone.data.stage) {
835
+ return { stage: stone.data.stage, payload: stone.data };
836
+ }
837
+ }
838
+ }
839
+
840
+ // 2. Fallback to Frame Data (Legacy/Init compatibility)
841
+ if (ibGib.data && ibGib.data.stage) {
842
+ return { stage: ibGib.data.stage, payload: ibGib.data.payload };
843
+ }
844
+
845
+ throw new Error(`Could not determine stage from frame ${getIbGibAddr({ ibGib })}`);
846
+ }
621
847
  protected sortTimelinesTopologically(timelines: { [tjp: string]: IbGib_V1[] }): string[] {
622
848
  const lc = `${this.lc}[${this.sortTimelinesTopologically.name}]`;
623
849
  const tjps = Object.keys(timelines);
@@ -681,47 +907,4 @@ export class SyncSagaCoordinator {
681
907
  return sorted;
682
908
  }
683
909
 
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
- }
727
910
  }