@ibgib/core-gib 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/keystone/keystone-helpers.mjs +3 -3
  2. package/dist/keystone/keystone-helpers.mjs.map +1 -1
  3. package/dist/sync/sync-constants.d.mts +4 -1
  4. package/dist/sync/sync-constants.d.mts.map +1 -1
  5. package/dist/sync/sync-constants.mjs +3 -0
  6. package/dist/sync/sync-constants.mjs.map +1 -1
  7. package/dist/sync/sync-helpers.d.mts +18 -2
  8. package/dist/sync/sync-helpers.d.mts.map +1 -1
  9. package/dist/sync/sync-helpers.mjs +84 -3
  10. package/dist/sync/sync-helpers.mjs.map +1 -1
  11. package/dist/sync/sync-innerspace.respec.d.mts +0 -6
  12. package/dist/sync/sync-innerspace.respec.d.mts.map +1 -1
  13. package/dist/sync/sync-innerspace.respec.mjs +395 -241
  14. package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
  15. package/dist/sync/sync-peer/sync-peer-types.d.mts +31 -0
  16. package/dist/sync/sync-peer/sync-peer-types.d.mts.map +1 -0
  17. package/dist/sync/sync-peer/sync-peer-types.mjs +5 -0
  18. package/dist/sync/sync-peer/sync-peer-types.mjs.map +1 -0
  19. package/dist/sync/sync-peer/sync-peer-v1.d.mts +22 -0
  20. package/dist/sync/sync-peer/sync-peer-v1.d.mts.map +1 -0
  21. package/dist/sync/sync-peer/sync-peer-v1.mjs +13 -0
  22. package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -0
  23. package/dist/sync/sync-saga-context/sync-saga-context-constants.d.mts +8 -0
  24. package/dist/sync/sync-saga-context/sync-saga-context-constants.d.mts.map +1 -0
  25. package/dist/sync/sync-saga-context/sync-saga-context-constants.mjs +8 -0
  26. package/dist/sync/sync-saga-context/sync-saga-context-constants.mjs.map +1 -0
  27. package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts +54 -0
  28. package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts.map +1 -0
  29. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs +87 -0
  30. package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs.map +1 -0
  31. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts +66 -0
  32. package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts.map +1 -0
  33. package/dist/sync/sync-saga-context/sync-saga-context-types.mjs +12 -0
  34. package/dist/sync/sync-saga-context/sync-saga-context-types.mjs.map +1 -0
  35. package/dist/sync/sync-saga-coordinator.d.mts +136 -91
  36. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  37. package/dist/sync/sync-saga-coordinator.mjs +563 -287
  38. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  39. package/dist/sync/sync-saga-coordinator.respec.mjs +7 -7
  40. package/dist/sync/sync-saga-coordinator.respec.mjs.map +1 -1
  41. package/dist/sync/sync-saga-message/sync-saga-message-constants.d.mts +2 -0
  42. package/dist/sync/sync-saga-message/sync-saga-message-constants.d.mts.map +1 -0
  43. package/dist/sync/sync-saga-message/sync-saga-message-constants.mjs +2 -0
  44. package/dist/sync/sync-saga-message/sync-saga-message-constants.mjs.map +1 -0
  45. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts +15 -0
  46. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -0
  47. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +43 -0
  48. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -0
  49. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +39 -0
  50. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -0
  51. package/dist/sync/sync-saga-message/sync-saga-message-types.mjs +2 -0
  52. package/dist/sync/sync-saga-message/sync-saga-message-types.mjs.map +1 -0
  53. package/dist/sync/sync-types.d.mts +81 -3
  54. package/dist/sync/sync-types.d.mts.map +1 -1
  55. package/dist/sync/sync-types.mjs +27 -1
  56. package/dist/sync/sync-types.mjs.map +1 -1
  57. package/dist/timeline/timeline-api.d.mts +16 -3
  58. package/dist/timeline/timeline-api.d.mts.map +1 -1
  59. package/dist/timeline/timeline-api.mjs +7 -7
  60. package/dist/timeline/timeline-api.mjs.map +1 -1
  61. package/dist/witness/space/inner-space/inner-space-v1.d.mts.map +1 -1
  62. package/dist/witness/space/inner-space/inner-space-v1.mjs +3 -4
  63. package/dist/witness/space/inner-space/inner-space-v1.mjs.map +1 -1
  64. package/dist/witness/space/outer-space/outer-space-types.d.mts +2 -0
  65. package/dist/witness/space/outer-space/outer-space-types.d.mts.map +1 -1
  66. package/dist/witness/space/space-base-v1.d.mts +19 -1
  67. package/dist/witness/space/space-base-v1.d.mts.map +1 -1
  68. package/dist/witness/space/space-base-v1.mjs +66 -6
  69. package/dist/witness/space/space-base-v1.mjs.map +1 -1
  70. package/dist/witness/space/space-helper.d.mts +14 -0
  71. package/dist/witness/space/space-helper.d.mts.map +1 -1
  72. package/dist/witness/space/space-helper.mjs +44 -1
  73. package/dist/witness/space/space-helper.mjs.map +1 -1
  74. package/dist/witness/space/space-respec-helper.d.mts.map +1 -1
  75. package/dist/witness/space/space-respec-helper.mjs +1 -1
  76. package/dist/witness/space/space-respec-helper.mjs.map +1 -1
  77. package/dist/witness/space/space-types.d.mts +12 -1
  78. package/dist/witness/space/space-types.d.mts.map +1 -1
  79. package/dist/witness/space/space-types.mjs +4 -0
  80. package/dist/witness/space/space-types.mjs.map +1 -1
  81. package/package.json +2 -2
  82. package/src/keystone/keystone-helpers.mts +3 -3
  83. package/src/sync/README.md +275 -0
  84. package/src/sync/sync-constants.mts +5 -0
  85. package/src/sync/sync-helpers.mts +105 -6
  86. package/src/sync/sync-innerspace.respec.mts +458 -289
  87. package/src/sync/sync-peer/sync-peer-types.mts +43 -0
  88. package/src/sync/sync-peer/sync-peer-v1.mts +28 -0
  89. package/src/sync/sync-saga-context/sync-saga-context-constants.mts +8 -0
  90. package/src/sync/sync-saga-context/sync-saga-context-helpers.mts +147 -0
  91. package/src/sync/sync-saga-context/sync-saga-context-types.mts +80 -0
  92. package/src/sync/sync-saga-coordinator.mts +762 -329
  93. package/src/sync/sync-saga-coordinator.respec.mts +7 -7
  94. package/src/sync/sync-saga-message/sync-saga-message-constants.mts +1 -0
  95. package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +59 -0
  96. package/src/sync/sync-saga-message/sync-saga-message-types.mts +53 -0
  97. package/src/sync/sync-types.mts +103 -3
  98. package/src/timeline/timeline-api.mts +20 -4
  99. package/src/witness/space/inner-space/inner-space-v1.mts +3 -2
  100. package/src/witness/space/reconciliation-space/reconciliation-space-base.mts.OLD.md +884 -0
  101. package/src/witness/space/reconciliation-space/reconciliation-space-helper.mts.OLD.md +125 -0
  102. package/src/witness/space/space-base-v1.mts +62 -12
  103. package/src/witness/space/space-helper.mts +50 -1
  104. package/src/witness/space/space-respec-helper.mts +2 -1
  105. package/src/witness/space/space-types.mts +13 -1
  106. package/tmp.md +3 -9
@@ -2,104 +2,51 @@ import {
2
2
  extractErrorMsg,
3
3
  getUUID, // so our uuid's are uniform across all ibgib code
4
4
  getTimestamp, // so our timestamp strings are uniform
5
- getTimestampInTicks // so our timestamp in ticks as a string are uniform
5
+ getTimestampInTicks, // so our timestamp in ticks as a string are uniform
6
+ pretty,
7
+ clone
6
8
  } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
7
9
  import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
8
10
  import { splitPerTjpAndOrDna, getTimelinesGroupedByTjp } from "../common/other/ibgib-helper.mjs";
9
11
  import { Factory_V1 } from "@ibgib/ts-gib/dist/V1/factory.mjs";
10
- 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";
11
13
  import { isPrimitive } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
12
14
 
13
15
  import { GLOBAL_LOG_A_LOT } from "../core-constants.mjs";
14
16
  import { IbGibSpaceAny } from "../witness/space/space-base-v1.mjs";
15
- import { putInSpace, lockSpace, unlockSpace, getSpaceArgMetadata, getLatestAddrs } from "../witness/space/space-helper.mjs";
17
+ import { putInSpace, lockSpace, unlockSpace, getSpaceArgMetadata, getLatestAddrs, getFromSpace } from "../witness/space/space-helper.mjs";
16
18
  import { KeystoneIbGib_V1 } from "../keystone/keystone-types.mjs";
17
19
  import { KeystoneService_V1 } from "../keystone/keystone-service-v1.mjs";
18
- 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";
19
23
  import {
20
24
  SyncData_V1, SyncIbGib_V1, SyncInitData, SyncDeltaData, SyncCommitData,
25
+ SyncConflictStrategy, SyncOptions,
26
+ SyncMode,
21
27
  } from "./sync-types.mjs";
22
- import { getSyncIb } from "./sync-helpers.mjs";
28
+ import { getSyncIb, isPastFrame } from "./sync-helpers.mjs";
23
29
  import { getDependencyGraph } from "../common/other/graph-helper.mjs";
24
-
25
- const logalot = GLOBAL_LOG_A_LOT;
26
-
27
- export interface SyncOptions {
28
- /**
29
- * The space containing the data we want to send.
30
- */
31
- source: IbGibSpaceAny;
32
- /**
33
- * The space receiving the data.
34
- */
35
- dest: IbGibSpaceAny;
36
- /**
37
- * The identity authorizing this sync.
38
- */
39
- identity: KeystoneIbGib_V1;
40
- /**
41
- * The secret for the identity (to sign the commit).
42
- */
43
- identitySecret: string;
44
- }
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";
44
+
45
+
46
+ const logalot = GLOBAL_LOG_A_LOT || true;
45
47
 
46
48
  /**
47
49
  * Orchestrates the synchronization process between two spaces (Source and Destination).
48
- *
49
- * ## Architecture: Dependency Graph Synchronization
50
- *
51
- * Instead of a naive file-by-file sync or a holistic "Space" sync, this coordinator operates
52
- * on a **Dependency Graph** derived from specific "Domain Roots" (e.g., a specific tag,
53
- * folder, or application root).
54
- *
55
- * ### Workflow Pipeline
56
- *
57
- * 1. **Graph Generation**:
58
- * * Generates a `FlatIbGibGraph` using `getDependencyGraph({ live: true })` starting
59
- * from the provided `domainIbGibs`.
60
- * * This ensures we capture the *latest* reachable state of all relevant timelines.
61
- *
62
- * 2. **Classification (`splitPerTjpAndOrDna`)**:
63
- * * **Stones**: Immutable, non-living ibGibs (no TJP/DNA). Trivial to sync (copy if missing).
64
- * * **Living**: Evolving timelines (TJP + DNA). Complex to sync (require ordering & merging).
65
- *
66
- * 3. **Timeline Ordering (`getTimelinesGroupedByTjp`)**:
67
- * * Living ibGibs are grouped into timelines.
68
- * * A "Timeline Dependency Graph" is built. Use Case: If a Comment Timeline refers to a
69
- * Post Timeline, the Post Timeline must be synced *before* the Comment Timeline to
70
- * ensure referential integrity at the destination.
71
- * * **Topological Sort** determines the execution order. Circular dependencies are
72
- * treated as siblings.
73
- *
74
- * 4. **Saga Execution ("Smart Coordinator, Dumb Space")**:
75
- * * The Coordinator (running on the Client/Source) drives the entire process via a
76
- * "Pull-Merge-Push" strategy to resolve conflicts.
77
- *
78
- * * **Phase 1: Knowledge Exchange (Init)**
79
- * * Generates a "Knowledge Vector" (Map<Tjp, LatestAddr>) of the Source's graph.
80
- * * Sends `SyncStage.Init` to Dest.
81
- * * Dest responds with its own Knowledge Vector for overlapping timelines.
82
- *
83
- * * **Phase 2: Gap Analysis & Conflict Resolution**
84
- * * Coordinator compares Source vs. Dest knowledge.
85
- * * **Fast-Forward**: Source is strictly ahead of Dest. Mark new frames for PUSH.
86
- * * **Fast-Backward**: Dest is strictly ahead of Source. Mark frames for PULL (to update Local).
87
- * * **Conflict/Divergence**: Both have new frames from a common ancestor.
88
- * * **LOCK**: `lockSpace({ scope: tjpGib })` on Dest to prevent race conditions.
89
- * * **PULL**: Download conflicting branch from Dest.
90
- * * **MERGE**: Execute merge logic locally (creating a new merge frame `A_merge`).
91
- * * **PUSH**: Mark `A_merge` (and dependencies) for PUSH.
92
- * * **UNLOCK**: Release Dest lock.
93
- *
94
- * * **Phase 3: Batch Streaming (Delta)**
95
- * * **Stones**: Batch `putInSpace` all missing "Stone" ibGibs first.
96
- * * **Timelines**: Batch `putInSpace` Living Timelines in topological order.
97
- * * *Note*: The `SyncFrame` (Init/Delta/Commit) tracks protocol state, but data transfer
98
- * happens via standard `putInSpace`.
99
- *
100
- * * **Phase 4: Commit**
101
- * * Update Local Index (register new latests).
102
- * * Send `SyncStage.Commit` to Dest to finalize session.
103
50
  */
104
51
  export class SyncSagaCoordinator {
105
52
  protected lc: string = `[${SyncSagaCoordinator.name}]`;
@@ -109,214 +56,341 @@ export class SyncSagaCoordinator {
109
56
  ) { }
110
57
 
111
58
  /**
112
- * Executes a synchronization saga.
113
- *
114
- * @param opts.source The local space (Client) driving the sync.
115
- * @param opts.dest The remote space (Server/Other Peer) to sync with.
116
- * @param opts.identity The Keystone Identity performing the sync.
117
- * @param opts.domainIbGibs The root ibGibs that define the scope of the sync (the "Dependency Graph").
118
- * @param opts.identitySecret Optional secret if needed (usually handled by `keystone` service).
59
+ * Executes a synchronization saga using the Symmetric Sync Protocol.
119
60
  */
120
61
  async sync({
121
- source,
122
- dest,
123
- identity,
62
+ peer,
63
+ localSpace,
64
+ metaspace,
65
+ primaryIdentity,
66
+ primaryIdentitySecret,
124
67
  domainIbGibs,
125
- identitySecret
126
68
  }: {
127
- source: IbGibSpaceAny,
128
- dest: IbGibSpaceAny,
129
- identity: KeystoneIbGib_V1,
69
+ peer: SyncPeerWitness,
70
+ localSpace: IbGibSpaceAny,
71
+ metaspace: MetaspaceService,
72
+ primaryIdentity: KeystoneIbGib_V1,
73
+ primaryIdentitySecret: string,
130
74
  domainIbGibs: IbGib_V1[],
131
- identitySecret?: string,
132
- }): Promise<void> {
75
+ }): Promise<SyncSagaInfo> {
133
76
  const lc = `${this.lc}[${this.sync.name}]`;
134
- try {
135
- if (logalot) { console.log(`${lc} starting...`); }
136
-
137
- // 1. Generate UUID for this saga
138
- // const uuid = await Factory_V1.primitive({ ib: "genuuid" }); // Hacky usage of primitive helper if available, or just UUID lib
139
-
140
- // if we don't have a uuid, we can generate one here.
141
- // but usually this is passed in.
142
- // atow we are just generating one.
143
- const uuid = await getUUID();
144
-
145
- // ---------------------------------------------------------
146
- // 1. Graph Generation & Classification
147
- // ---------------------------------------------------------
148
- // We want a live dependency graph to ensure we get the latest
149
- // states of timelines.
150
- const graph = await getDependencyGraph({
151
- ibGibs: domainIbGibs,
152
- live: true,
153
- space: source,
154
- });
155
- const validGraph = graph && Object.keys(graph).length > 0;
156
- if (logalot) { console.log(`${lc} graph generated. nodes: ${validGraph ? Object.keys(graph).length : 0}`); }
157
-
158
- // 2. Classification
159
- // We differentiate between "Stones" (Immutable, no timeline) and "Living" (Timelines).
160
- // Stones can be synced in bulk without ordering.
161
- // Living ibGibs must be grouped by timeline and ordered.
162
- const ibGibs = validGraph ? Object.values(graph) : [];
163
- const { mapWithTjp_YesDna, mapWithTjp_NoDna, mapWithoutTjps } = splitPerTjpAndOrDna({ ibGibs });
164
-
165
- // "Stones" come from mapWithoutTjps
166
- const stones = Object.values(mapWithoutTjps);
167
-
168
- // "Living" come from both TJP maps
169
- // We combine them to group by TJP
170
- const living = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
171
-
172
- if (logalot) {
173
- console.log(`${lc} classification results (count):`);
174
- console.log(`stones: ${stones.length}`);
175
- console.log(`living: ${living.length}`);
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
+ });
90
+
91
+ // WORKING CONTEXT (Transactional)
92
+ const tempSpace = await metaspace.createNewLocalSpace({
93
+ opts: {
94
+ allowCancel: false,
95
+ spaceName: undefined,
96
+ getFnPrompt: metaspace.getFnPrompt!,
97
+ logalot
176
98
  }
99
+ });
100
+ if (!tempSpace) { throw new Error(`Failed to create temp space (E: 8f4e2f3d6c1b4b1a8f4e2f3d6c1b4b1a)`); }
177
101
 
178
- // 3. Timeline Ordering
179
- // Group living ibGibs by TJP to identify distinct timelines.
180
- // The helper returns them sorted by 'n' ascending.
181
- const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: living });
182
-
183
- // todo: perform topological sort on timelinesMap based on inter-timeline references.
184
- // For now, we sync independent timelines.
185
-
186
- const payloadAddrs: string[] = [];
187
- payloadAddrs.push(...stones.map(x => getIbGibAddr({ ibGib: x })));
188
- // 3. Timeline Ordering (Topological Sort)
189
- // Living ibGibs are grouped by timeline (TJP).
190
- // We need to order these timelines based on dependencies.
191
- const sortedTjps = this.sortTimelinesTopologically(timelinesMap);
192
-
193
- // Add ordered timelines to payload
194
- sortedTjps.forEach(tjp => {
195
- const timeline = timelinesMap[tjp];
196
- payloadAddrs.push(...timeline.map(x => getIbGibAddr({ ibGib: x })));
197
- });
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
+ };
198
106
 
199
- // 2. INIT & Phase 2: Knowledge Exchange
200
- const srcKnowledgeVector: { [tjp: string]: string } = {};
201
- Object.keys(timelinesMap).forEach(tjp => {
202
- const timeline = timelinesMap[tjp];
203
- const tip = timeline[timeline.length - 1];
204
- srcKnowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
205
- });
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
+ });
206
124
 
207
- const initData: SyncInitData = {
208
- knowledgeVector: srcKnowledgeVector,
209
- identity: identity,
210
- mode: 'push'
211
- };
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
+ });
212
134
 
213
- // Get Source Knowledge (Time -> Latest)
214
- const srcTjps = Object.keys(timelinesMap);
215
- // Get Dest Knowledge
216
- const destKnowledge = await this.getLatestAddrsFromSpace({ space: dest, tjpAddrs: srcTjps });
217
-
218
- const conflicts: string[] = [];
219
- for (const tjp of srcTjps) {
220
- const srcTimeline = timelinesMap[tjp];
221
- const srcTip = srcTimeline[srcTimeline.length - 1];
222
- const srcTipAddr = getIbGibAddr({ ibGib: srcTip });
223
- const destTipAddr = destKnowledge[tjp];
224
-
225
- if (destTipAddr && destTipAddr !== srcTipAddr) {
226
- // Divergence found. Check if Source knows Dest's tip.
227
- if (graph[destTipAddr]) {
228
- // Fast-Forward: Dest tip IS in our graph, so we are ahead.
229
- // No action needed, proceed to push.
230
- } else {
231
- // Conflict: Dest has a tip we don't know about.
232
- conflicts.push(tjp);
233
- }
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 });
234
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();
235
161
  }
162
+ })();
236
163
 
237
- // Lock conflicts
238
- for (const tjp of conflicts) {
239
- if (logalot) { console.log(`${lc} Conflict detected for TJP ${tjp}. Acquiring lock...`); }
240
- // Lock VALIDITY: 60 seconds?
241
- await lockSpace({
242
- space: dest,
243
- scope: tjp,
244
- secondsValid: 60,
245
- instanceId: uuid,
246
- });
164
+ return {
165
+ sagaId,
166
+ updates$,
167
+ done
168
+ };
169
+ }
247
170
 
248
- // todo: Implement Pull & Merge here
249
- // 1. Pull delta from Dest (destTip -> ... -> common ancestor)
250
- // 2. Merge locally
251
- // 3. Update graph/timelinesMap with merged result
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,
208
+ });
252
209
 
253
- if (logalot) { console.warn(`${lc} Merge not fully implemented yet. Releasing lock.`); }
210
+ // B. Transmit
211
+ updates$.next(requestCtx);
212
+ const responseCtx = await peer.witness(requestCtx);
254
213
 
255
- await unlockSpace({
256
- space: dest,
257
- scope: tjp,
258
- instanceId: uuid,
259
- });
214
+ // C. Handle Response
215
+ if (!responseCtx) {
216
+ console.warn(`${lc} Peer returned no response context. Ending loop.`);
217
+ currentFrame = null;
218
+ break;
260
219
  }
261
220
 
262
- const initFrame = await this.createSyncFrame({
263
- uuid,
264
- stage: SyncStage.init,
265
- payload: initData,
266
- identity
267
- });
221
+ updates$.next(responseCtx);
268
222
 
269
- // Persist Init to Dest (Handshake)
270
- await putInSpace({ space: dest, ibGib: initFrame });
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
+ }
271
230
 
272
- // 3. DELTA
273
- // const payloadAddrs = [getIbGibAddr({ ibGib: identity })]; // Already computed above
231
+ const remoteFrame = await getFromSpace({ addr: remoteFrameAddr, space: tempSpace });
232
+ if (!remoteFrame) { throw new Error(`Could not resolve remote frame: ${remoteFrameAddr}`); }
274
233
 
275
- const deltaData: SyncDeltaData = {
276
- payloadAddrs
277
- };
278
- const deltaFrame = await this.createSyncFrame({
279
- uuid,
280
- stage: SyncStage.delta,
281
- payload: deltaData,
282
- identity
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
283
241
  });
284
- await putInSpace({ space: dest, ibGib: deltaFrame });
285
-
286
- // 3.1 Stream the actual data (Batch Stream)
287
- // We must preserve the order in payloadAddrs to respect dependencies.
288
- // We batch them to improve throughput.
289
- const BATCH_SIZE = 20;
290
- for (let i = 0; i < payloadAddrs.length; i += BATCH_SIZE) {
291
- const chunkAddrs = payloadAddrs.slice(i, i + BATCH_SIZE);
292
- const chunkIbGibs = chunkAddrs
293
- .map(addr => {
294
- const ibGib = graph[addr];
295
- if (!ibGib) { throw new Error(`(UNEXPECTED) addr not found in graph: ${addr} (E: 8a402324057849318625aa85721665a5)`); }
296
- return ibGib;
297
- })
298
- .filter(ibGib => !isPrimitive({ ibGib }));
299
-
300
- if (chunkIbGibs.length > 0) {
301
- if (logalot) { console.log(`${lc} streaming batch ${Math.ceil(i / BATCH_SIZE) + 1} (${chunkIbGibs.length} ibGibs)...`); }
302
- // We don't set isDna: true here because the batch might contain mixed types.
303
- // The Space implementation should handle routing based on internal metadata if needed.
304
- await putInSpace({ space: dest, ibGibs: chunkIbGibs });
305
- }
242
+
243
+ currentFrame = result?.frame || null;
244
+ nextPayload = result?.payload || [];
245
+ if (result?.receivedPayload) {
246
+ allReceivedIbGibs.push(...result.receivedPayload);
306
247
  }
248
+ }
307
249
 
308
- // 4. COMMIT
309
- const commitData: SyncCommitData = {
310
- success: true
311
- };
312
- const commitFrame = await this.createSyncFrame({
313
- uuid,
314
- stage: SyncStage.commit,
315
- payload: commitData,
316
- identity
317
- });
318
- await putInSpace({ space: dest, ibGib: commitFrame });
250
+ return allReceivedIbGibs;
251
+ }
252
+
253
+ protected async analyzeTimelines({
254
+ domainIbGibs,
255
+ space,
256
+ }: {
257
+ domainIbGibs: IbGib_V1[],
258
+ space: IbGibSpaceAny,
259
+ }): Promise<{
260
+ srcStones: IbGib_V1[],
261
+ srcTimelinesMap: { [tjp: string]: IbGib_V1[] },
262
+ srcSortedTjps: string[],
263
+ srcGraph: { [addr: string]: IbGib_V1 },
264
+ }> {
265
+ const lc = `${this.lc}[${this.analyzeTimelines.name}]`;
266
+ const srcGraph = await getDependencyGraph({
267
+ ibGibs: domainIbGibs,
268
+ live: true,
269
+ space,
270
+ });
271
+
272
+ const srcGraphIsValid = srcGraph && Object.keys(srcGraph).length > 0;
273
+ if (logalot) { console.log(`${lc} graph generated. nodes: ${srcGraphIsValid ? Object.keys(srcGraph).length : 0}`); }
274
+
275
+ const srcIbGibs = srcGraphIsValid ? Object.values(srcGraph) : [];
276
+ const {
277
+ mapWithTjp_YesDna: srcMapWithTjp_YesDna,
278
+ mapWithTjp_NoDna: srcMapWithTjp_NoDna,
279
+ mapWithoutTjps: src_MapWithoutTjps
280
+ } = splitPerTjpAndOrDna({ ibGibs: srcIbGibs });
281
+
282
+ const srcStones = Object.values(src_MapWithoutTjps);
283
+ const srcLiving = [...Object.values(srcMapWithTjp_YesDna), ...Object.values(srcMapWithTjp_NoDna)];
319
284
 
285
+ const srcTimelinesMap = getTimelinesGroupedByTjp({ ibGibs: srcLiving });
286
+ const srcSortedTjps = this.sortTimelinesTopologically(srcTimelinesMap);
287
+
288
+ return { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph };
289
+ }
290
+
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,
355
+ srcGraph,
356
+ space,
357
+ identity,
358
+ identitySecret,
359
+ metaspace,
360
+ }: {
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
+ }
320
394
  } catch (error) {
321
395
  console.error(`${lc} ${extractErrorMsg(error)}`);
322
396
  throw error;
@@ -325,93 +399,451 @@ export class SyncSagaCoordinator {
325
399
  }
326
400
  }
327
401
 
328
- protected async createSyncFrame({
329
- uuid,
330
- stage,
402
+ // #region Handlers
403
+
404
+ protected async handleInitFrame({
405
+ sagaIbGib,
331
406
  payload,
332
- identity
407
+ space,
408
+ metaspace,
409
+ identity,
410
+ identitySecret,
333
411
  }: {
334
- uuid: string,
335
- stage: any, // SyncStage
412
+ sagaIbGib: SyncIbGib_V1,
336
413
  payload: any,
337
- identity: KeystoneIbGib_V1
338
- }): Promise<SyncIbGib_V1> {
339
- const data: SyncData_V1 = {
340
- uuid,
341
- stage,
342
- payload
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,
343
487
  };
344
- const ib = await getSyncIb({ data });
345
- // Create the ibGib using Factory? Or manually?
346
- // Factory_V1.firstGen usually.
347
- const res = await Factory_V1.firstGen({
348
- parentIbGib: Factory_V1.primitive({ ib: SYNC_ATOM }),
349
- ib,
350
- data,
351
- rel8ns: {
352
- identity: [getIbGibAddr({ ibGib: identity })]
353
- },
354
- /**
355
- * dna is used for merging dynamic timelines, a la a CRDT-like
356
- * mechanism that looks somewhat like event sourcing but using
357
- * crypto primitives. in our particular case, the sync should be a
358
- * single, one-off timeline (not to be used for any future merging),
359
- * so we don't need dna.
360
- */
361
- dna: false,
362
- tjp: {
363
- timestamp: true,
364
- /**
365
- * we'll use the uuid passed in
366
- */
367
- uuid: false
368
- },
369
- nCounter: true,
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,
370
502
  });
371
- return res.newIbGib as SyncIbGib_V1;
503
+
504
+ return { frame: ackFrame };
372
505
  }
373
506
 
374
- protected async getLatestAddrsFromSpace({
507
+ protected async handleAckFrame({
508
+ sagaIbGib,
509
+ srcGraph,
375
510
  space,
376
- tjpAddrs,
511
+ metaspace,
512
+ identity,
377
513
  }: {
514
+ sagaIbGib: SyncIbGib_V1,
515
+ srcGraph: { [addr: string]: IbGib_V1 },
378
516
  space: IbGibSpaceAny,
379
- tjpAddrs: string[],
380
- }): Promise<{ [tjpAddr: string]: string }> {
381
- const lc = `${this.lc}[${this.getLatestAddrsFromSpace.name}]`;
382
- try {
383
- if (tjpAddrs.length === 0) { return {}; }
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...`); }
384
522
 
385
- // Using the space's witness/argy pattern to invoke 'get' with 'latest' modifier
386
- const arg = await space.argy({
387
- ibMetadata: getSpaceArgMetadata({ space }),
388
- argData: {
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);
539
+ }
540
+ }
541
+
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];
550
+ }
551
+ }
552
+ if (ibGib) {
553
+ payloadIbGibs.push(ibGib);
554
+ } else {
555
+ console.warn(`${lc} Requested addr not found: ${addr}`);
556
+ }
557
+ }
558
+
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,
389
618
  cmd: 'get',
390
- cmdModifiers: ['latest', 'addrs'],
391
- ibGibAddrs: tjpAddrs,
619
+ } as any
620
+ });
621
+ if (res.data?.ibGibs) {
622
+ receivedPayload.push(...res.data.ibGibs);
623
+ }
624
+ }
625
+
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];
392
634
  }
635
+ }
636
+ if (ibGib) {
637
+ outgoingPayload.push(ibGib);
638
+ }
639
+ }
640
+
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
+ };
650
+
651
+ const deltaStone = await this.createSyncMsgStone({
652
+ data: responseDeltaData,
653
+ space,
654
+ metaspace
393
655
  });
394
- const result = await space.witness(arg);
395
-
396
- const map: { [tjp: string]: string } = {};
397
- if (result?.data?.success && result.data.addrs) {
398
- // assume 1-to-1 mapping order as per convention if successful
399
- // verification is hard without specific return map
400
- if (result.data.addrs.length === tjpAddrs.length) {
401
- for (let i = 0; i < tjpAddrs.length; i++) {
402
- map[tjpAddrs[i]] = result.data.addrs[i];
403
- }
404
- } else {
405
- console.warn(`${lc} result addrs length mismatch. requested: ${tjpAddrs.length}, got: ${result.data.addrs.length}. Mapping might be unsafe.`);
656
+
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
+ };
675
+
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
688
+ });
689
+
690
+ return { frame: commitFrame, receivedPayload };
691
+ }
692
+ }
693
+
694
+ protected async handleCommitFrame({
695
+ sagaIbGib,
696
+ space,
697
+ }: {
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;
704
+ }
705
+
706
+ // #endregion Handlers
707
+
708
+ protected async createSyncMsgStone<TStoneData extends SyncSagaMessageData_V1>({
709
+ data,
710
+ space,
711
+ metaspace,
712
+ }: {
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({
719
+ ib,
720
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
721
+ data,
722
+ uuid: true, // we want the stone to have its own uniqueness
723
+ });
724
+ await putInSpace({ space, ibGib: stone });
725
+ await metaspace.registerNewIbGib({ ibGib: stone });
726
+ return stone as IbGib_V1<TStoneData>;
727
+ }
728
+
729
+
730
+ /**
731
+ * Evolves the saga timeline with a new frame.
732
+ */
733
+ protected async evolveSyncSagaIbGib({
734
+ prevSagaIbGib,
735
+ msgStones,
736
+ identity,
737
+ space,
738
+ metaspace,
739
+ }: {
740
+ prevSagaIbGib?: SyncIbGib_V1,
741
+ msgStones: IbGib_V1[],
742
+ identity: KeystoneIbGib_V1,
743
+ space: IbGibSpaceAny,
744
+ metaspace: MetaspaceService,
745
+ }): Promise<SyncIbGib_V1> {
746
+ const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
747
+ try {
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)`);
406
767
  }
407
768
  }
408
- return map;
769
+
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;
793
+ } else {
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;
818
+ }
819
+
409
820
  } catch (error) {
410
821
  console.error(`${lc} ${extractErrorMsg(error)}`);
411
- return {}; // fail safe
822
+ throw error;
412
823
  }
413
824
  }
414
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
+ }
415
847
  protected sortTimelinesTopologically(timelines: { [tjp: string]: IbGib_V1[] }): string[] {
416
848
  const lc = `${this.lc}[${this.sortTimelinesTopologically.name}]`;
417
849
  const tjps = Object.keys(timelines);
@@ -474,4 +906,5 @@ export class SyncSagaCoordinator {
474
906
 
475
907
  return sorted;
476
908
  }
909
+
477
910
  }