@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.
- package/dist/keystone/keystone-helpers.mjs +3 -3
- package/dist/keystone/keystone-helpers.mjs.map +1 -1
- package/dist/sync/sync-constants.d.mts +4 -1
- package/dist/sync/sync-constants.d.mts.map +1 -1
- package/dist/sync/sync-constants.mjs +3 -0
- package/dist/sync/sync-constants.mjs.map +1 -1
- package/dist/sync/sync-helpers.d.mts +18 -2
- package/dist/sync/sync-helpers.d.mts.map +1 -1
- package/dist/sync/sync-helpers.mjs +84 -3
- package/dist/sync/sync-helpers.mjs.map +1 -1
- package/dist/sync/sync-innerspace.respec.d.mts +0 -6
- package/dist/sync/sync-innerspace.respec.d.mts.map +1 -1
- package/dist/sync/sync-innerspace.respec.mjs +395 -404
- package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
- package/dist/sync/sync-peer/sync-peer-types.d.mts +31 -0
- package/dist/sync/sync-peer/sync-peer-types.d.mts.map +1 -0
- package/dist/sync/sync-peer/sync-peer-types.mjs +5 -0
- package/dist/sync/sync-peer/sync-peer-types.mjs.map +1 -0
- package/dist/sync/sync-peer/sync-peer-v1.d.mts +22 -0
- package/dist/sync/sync-peer/sync-peer-v1.d.mts.map +1 -0
- package/dist/sync/sync-peer/sync-peer-v1.mjs +13 -0
- package/dist/sync/sync-peer/sync-peer-v1.mjs.map +1 -0
- package/dist/sync/sync-saga-context/sync-saga-context-constants.d.mts +8 -0
- package/dist/sync/sync-saga-context/sync-saga-context-constants.d.mts.map +1 -0
- package/dist/sync/sync-saga-context/sync-saga-context-constants.mjs +8 -0
- package/dist/sync/sync-saga-context/sync-saga-context-constants.mjs.map +1 -0
- package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts +54 -0
- package/dist/sync/sync-saga-context/sync-saga-context-helpers.d.mts.map +1 -0
- package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs +87 -0
- package/dist/sync/sync-saga-context/sync-saga-context-helpers.mjs.map +1 -0
- package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts +66 -0
- package/dist/sync/sync-saga-context/sync-saga-context-types.d.mts.map +1 -0
- package/dist/sync/sync-saga-context/sync-saga-context-types.mjs +12 -0
- package/dist/sync/sync-saga-context/sync-saga-context-types.mjs.map +1 -0
- package/dist/sync/sync-saga-coordinator.d.mts +115 -125
- package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
- package/dist/sync/sync-saga-coordinator.mjs +539 -456
- package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
- package/dist/sync/sync-saga-coordinator.respec.mjs +7 -7
- package/dist/sync/sync-saga-coordinator.respec.mjs.map +1 -1
- package/dist/sync/sync-saga-message/sync-saga-message-constants.d.mts +2 -0
- package/dist/sync/sync-saga-message/sync-saga-message-constants.d.mts.map +1 -0
- package/dist/sync/sync-saga-message/sync-saga-message-constants.mjs +2 -0
- package/dist/sync/sync-saga-message/sync-saga-message-constants.mjs.map +1 -0
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts +15 -0
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -0
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +43 -0
- package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -0
- package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +39 -0
- package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -0
- package/dist/sync/sync-saga-message/sync-saga-message-types.mjs +2 -0
- package/dist/sync/sync-saga-message/sync-saga-message-types.mjs.map +1 -0
- package/dist/sync/sync-types.d.mts +81 -3
- package/dist/sync/sync-types.d.mts.map +1 -1
- package/dist/sync/sync-types.mjs +27 -1
- package/dist/sync/sync-types.mjs.map +1 -1
- package/dist/timeline/timeline-api.d.mts +16 -3
- package/dist/timeline/timeline-api.d.mts.map +1 -1
- package/dist/timeline/timeline-api.mjs +7 -7
- package/dist/timeline/timeline-api.mjs.map +1 -1
- package/dist/witness/space/outer-space/outer-space-types.d.mts +2 -0
- package/dist/witness/space/outer-space/outer-space-types.d.mts.map +1 -1
- package/dist/witness/space/space-base-v1.d.mts +19 -1
- package/dist/witness/space/space-base-v1.d.mts.map +1 -1
- package/dist/witness/space/space-base-v1.mjs +66 -6
- package/dist/witness/space/space-base-v1.mjs.map +1 -1
- package/dist/witness/space/space-helper.d.mts +14 -0
- package/dist/witness/space/space-helper.d.mts.map +1 -1
- package/dist/witness/space/space-helper.mjs +44 -1
- package/dist/witness/space/space-helper.mjs.map +1 -1
- package/dist/witness/space/space-respec-helper.d.mts.map +1 -1
- package/dist/witness/space/space-respec-helper.mjs +1 -1
- package/dist/witness/space/space-respec-helper.mjs.map +1 -1
- package/dist/witness/space/space-types.d.mts +12 -1
- package/dist/witness/space/space-types.d.mts.map +1 -1
- package/dist/witness/space/space-types.mjs +4 -0
- package/dist/witness/space/space-types.mjs.map +1 -1
- package/package.json +2 -2
- package/src/keystone/keystone-helpers.mts +3 -3
- package/src/sync/README.md +275 -0
- package/src/sync/sync-constants.mts +5 -0
- package/src/sync/sync-helpers.mts +105 -6
- package/src/sync/sync-innerspace.respec.mts +458 -457
- package/src/sync/sync-peer/sync-peer-types.mts +43 -0
- package/src/sync/sync-peer/sync-peer-v1.mts +28 -0
- package/src/sync/sync-saga-context/sync-saga-context-constants.mts +8 -0
- package/src/sync/sync-saga-context/sync-saga-context-helpers.mts +147 -0
- package/src/sync/sync-saga-context/sync-saga-context-types.mts +80 -0
- package/src/sync/sync-saga-coordinator.mts +709 -526
- package/src/sync/sync-saga-coordinator.respec.mts +7 -7
- package/src/sync/sync-saga-message/sync-saga-message-constants.mts +1 -0
- package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +59 -0
- package/src/sync/sync-saga-message/sync-saga-message-types.mts +53 -0
- package/src/sync/sync-types.mts +103 -3
- package/src/timeline/timeline-api.mts +20 -4
- package/src/witness/space/space-base-v1.mts +62 -12
- package/src/witness/space/space-helper.mts +50 -1
- package/src/witness/space/space-respec-helper.mts +2 -1
- package/src/witness/space/space-types.mts +13 -1
- 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 {
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
62
|
+
peer,
|
|
63
|
+
localSpace,
|
|
64
|
+
metaspace,
|
|
65
|
+
primaryIdentity,
|
|
66
|
+
primaryIdentitySecret,
|
|
137
67
|
domainIbGibs,
|
|
138
|
-
identitySecret,
|
|
139
|
-
conflictStrategy
|
|
140
68
|
}: {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
69
|
+
peer: SyncPeerWitness,
|
|
70
|
+
localSpace: IbGibSpaceAny,
|
|
71
|
+
metaspace: MetaspaceService,
|
|
72
|
+
primaryIdentity: KeystoneIbGib_V1,
|
|
73
|
+
primaryIdentitySecret: string,
|
|
144
74
|
domainIbGibs: IbGib_V1[],
|
|
145
|
-
|
|
146
|
-
conflictStrategy?: SyncConflictStrategy,
|
|
147
|
-
}): Promise<void> {
|
|
75
|
+
}): Promise<SyncSagaInfo> {
|
|
148
76
|
const lc = `${this.lc}[${this.sync.name}]`;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
356
|
+
space,
|
|
357
|
+
identity,
|
|
358
|
+
identitySecret,
|
|
359
|
+
metaspace,
|
|
308
360
|
}: {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
555
|
+
console.warn(`${lc} Requested addr not found: ${addr}`);
|
|
352
556
|
}
|
|
353
557
|
}
|
|
354
558
|
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
srcGraph
|
|
694
|
+
protected async handleCommitFrame({
|
|
695
|
+
sagaIbGib,
|
|
696
|
+
space,
|
|
490
697
|
}: {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
706
|
+
// #endregion Handlers
|
|
707
|
+
|
|
708
|
+
protected async createSyncMsgStone<TStoneData extends SyncSagaMessageData_V1>({
|
|
709
|
+
data,
|
|
710
|
+
space,
|
|
711
|
+
metaspace,
|
|
524
712
|
}: {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
724
|
+
await putInSpace({ space, ibGib: stone });
|
|
725
|
+
await metaspace.registerNewIbGib({ ibGib: stone });
|
|
726
|
+
return stone as IbGib_V1<TStoneData>;
|
|
563
727
|
}
|
|
564
728
|
|
|
565
|
-
|
|
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
|
-
|
|
738
|
+
metaspace,
|
|
568
739
|
}: {
|
|
740
|
+
prevSagaIbGib?: SyncIbGib_V1,
|
|
741
|
+
msgStones: IbGib_V1[],
|
|
742
|
+
identity: KeystoneIbGib_V1,
|
|
569
743
|
space: IbGibSpaceAny,
|
|
570
|
-
|
|
571
|
-
}): Promise<
|
|
572
|
-
const lc = `${this.lc}[${this.
|
|
744
|
+
metaspace: MetaspaceService,
|
|
745
|
+
}): Promise<SyncIbGib_V1> {
|
|
746
|
+
const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
|
|
573
747
|
try {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|