@git-stunts/git-warp 10.8.0 → 11.3.3
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/README.md +53 -32
- package/SECURITY.md +64 -0
- package/bin/cli/commands/check.js +168 -0
- package/bin/cli/commands/doctor/checks.js +422 -0
- package/bin/cli/commands/doctor/codes.js +46 -0
- package/bin/cli/commands/doctor/index.js +239 -0
- package/bin/cli/commands/doctor/types.js +89 -0
- package/bin/cli/commands/history.js +80 -0
- package/bin/cli/commands/info.js +139 -0
- package/bin/cli/commands/install-hooks.js +128 -0
- package/bin/cli/commands/materialize.js +99 -0
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +88 -0
- package/bin/cli/commands/query.js +235 -0
- package/bin/cli/commands/registry.js +32 -0
- package/bin/cli/commands/seek.js +598 -0
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +154 -0
- package/bin/cli/commands/verify-audit.js +114 -0
- package/bin/cli/commands/view.js +46 -0
- package/bin/cli/infrastructure.js +350 -0
- package/bin/cli/schemas.js +177 -0
- package/bin/cli/shared.js +244 -0
- package/bin/cli/types.js +96 -0
- package/bin/presenters/index.js +41 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +286 -28
- package/bin/warp-graph.js +5 -2346
- package/index.d.ts +111 -21
- package/index.js +2 -0
- package/package.json +10 -8
- package/src/domain/WarpGraph.js +109 -3252
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +3 -3
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +29 -0
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AuditMessageCodec.js +137 -0
- package/src/domain/services/AuditReceiptService.js +471 -0
- package/src/domain/services/AuditVerifierService.js +707 -0
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +2 -2
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +120 -55
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +4 -1
- package/src/domain/services/MessageSchemaDetector.js +2 -2
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +71 -4
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WarpMessageCodec.js +4 -1
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +42 -0
- package/src/domain/trust/TrustCrypto.js +111 -0
- package/src/domain/trust/TrustEvaluator.js +195 -0
- package/src/domain/trust/TrustRecordService.js +281 -0
- package/src/domain/trust/TrustStateBuilder.js +222 -0
- package/src/domain/trust/canonical.js +68 -0
- package/src/domain/trust/reasonCodes.js +64 -0
- package/src/domain/trust/schemas.js +160 -0
- package/src/domain/trust/verdict.js +42 -0
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/types/git-cas.d.ts +20 -0
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/RefLayout.js +59 -0
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +26 -17
- package/src/domain/warp/Writer.js +18 -3
- package/src/domain/warp/_internal.js +26 -0
- package/src/domain/warp/_wire.js +58 -0
- package/src/domain/warp/_wiredMethods.d.ts +254 -0
- package/src/domain/warp/checkpoint.methods.js +401 -0
- package/src/domain/warp/fork.methods.js +323 -0
- package/src/domain/warp/materialize.methods.js +238 -0
- package/src/domain/warp/materializeAdvanced.methods.js +350 -0
- package/src/domain/warp/patch.methods.js +554 -0
- package/src/domain/warp/provenance.methods.js +286 -0
- package/src/domain/warp/query.methods.js +280 -0
- package/src/domain/warp/subscribe.methods.js +272 -0
- package/src/domain/warp/sync.methods.js +554 -0
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/ports/CommitPort.js +10 -0
- package/src/ports/RefPort.js +17 -0
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
- package/src/hooks/post-merge.sh +0 -60
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced materialization methods for WarpGraph — ceiling-aware replay,
|
|
3
|
+
* checkpoint-based materializeAt, adjacency building, and state caching.
|
|
4
|
+
*
|
|
5
|
+
* Every function uses `this` bound to a WarpGraph instance at runtime
|
|
6
|
+
* via wireWarpMethods().
|
|
7
|
+
*
|
|
8
|
+
* @module domain/warp/materializeAdvanced.methods
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { reduceV5, createEmptyStateV5 } from '../services/JoinReducer.js';
|
|
12
|
+
import { orsetContains, orsetElements } from '../crdt/ORSet.js';
|
|
13
|
+
import { decodeEdgeKey } from '../services/KeyCodec.js';
|
|
14
|
+
import { vvClone } from '../crdt/VersionVector.js';
|
|
15
|
+
import { computeStateHashV5 } from '../services/StateSerializerV5.js';
|
|
16
|
+
import { ProvenanceIndex } from '../services/ProvenanceIndex.js';
|
|
17
|
+
import { serializeFullStateV5, deserializeFullStateV5 } from '../services/CheckpointSerializerV5.js';
|
|
18
|
+
import { buildSeekCacheKey } from '../utils/seekCacheKey.js';
|
|
19
|
+
import { materializeIncremental } from '../services/CheckpointService.js';
|
|
20
|
+
import { createFrontier, updateFrontier } from '../services/Frontier.js';
|
|
21
|
+
|
|
22
|
+
/** @typedef {import('../types/WarpPersistence.js').CorePersistence} CorePersistence */
|
|
23
|
+
/** @typedef {import('../services/JoinReducer.js').WarpStateV5} WarpStateV5 */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{ outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>> }} AdjacencyMap
|
|
27
|
+
* @typedef {{ state: WarpStateV5, stateHash: string, adjacency: AdjacencyMap }} MaterializedResult
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { buildWriterRef } from '../utils/RefLayout.js';
|
|
31
|
+
import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the effective ceiling from options and instance state.
|
|
35
|
+
*
|
|
36
|
+
* Precedence: explicit `ceiling` in options overrides the instance-level
|
|
37
|
+
* `_seekCeiling`. Uses the `'ceiling' in options` check, so passing
|
|
38
|
+
* `{ ceiling: null }` explicitly clears the seek ceiling for that call
|
|
39
|
+
* (returns `null`), while omitting the key falls through to `_seekCeiling`.
|
|
40
|
+
*
|
|
41
|
+
* @this {import('../WarpGraph.js').default}
|
|
42
|
+
* @param {{ceiling?: number|null}} [options] - Options object; when the
|
|
43
|
+
* `ceiling` key is present (even if `null`), its value takes precedence
|
|
44
|
+
* @returns {number|null} Lamport ceiling to apply, or `null` for latest
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
export function _resolveCeiling(options) {
|
|
48
|
+
if (options && 'ceiling' in options) {
|
|
49
|
+
return options.ceiling ?? null;
|
|
50
|
+
}
|
|
51
|
+
return this._seekCeiling;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Builds a deterministic adjacency map for the logical graph.
|
|
56
|
+
*
|
|
57
|
+
* @this {import('../WarpGraph.js').default}
|
|
58
|
+
* @param {import('../services/JoinReducer.js').WarpStateV5} state
|
|
59
|
+
* @returns {{outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
export function _buildAdjacency(state) {
|
|
63
|
+
const outgoing = new Map();
|
|
64
|
+
const incoming = new Map();
|
|
65
|
+
|
|
66
|
+
for (const edgeKey of orsetElements(state.edgeAlive)) {
|
|
67
|
+
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
68
|
+
|
|
69
|
+
if (!orsetContains(state.nodeAlive, from) || !orsetContains(state.nodeAlive, to)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!outgoing.has(from)) {
|
|
74
|
+
outgoing.set(from, []);
|
|
75
|
+
}
|
|
76
|
+
if (!incoming.has(to)) {
|
|
77
|
+
incoming.set(to, []);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
outgoing.get(from).push({ neighborId: to, label });
|
|
81
|
+
incoming.get(to).push({ neighborId: from, label });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sortNeighbors = (/** @type {Array<{neighborId: string, label: string}>} */ list) => {
|
|
85
|
+
list.sort((/** @type {{neighborId: string, label: string}} */ a, /** @type {{neighborId: string, label: string}} */ b) => {
|
|
86
|
+
if (a.neighborId !== b.neighborId) {
|
|
87
|
+
return a.neighborId < b.neighborId ? -1 : 1;
|
|
88
|
+
}
|
|
89
|
+
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (const list of outgoing.values()) {
|
|
94
|
+
sortNeighbors(list);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const list of incoming.values()) {
|
|
98
|
+
sortNeighbors(list);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { outgoing, incoming };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sets the cached state and materialized graph details.
|
|
106
|
+
*
|
|
107
|
+
* @this {import('../WarpGraph.js').default}
|
|
108
|
+
* @param {import('../services/JoinReducer.js').WarpStateV5} state
|
|
109
|
+
* @returns {Promise<MaterializedResult>}
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
export async function _setMaterializedState(state) {
|
|
113
|
+
this._cachedState = state;
|
|
114
|
+
this._stateDirty = false;
|
|
115
|
+
this._versionVector = vvClone(state.observedFrontier);
|
|
116
|
+
|
|
117
|
+
const stateHash = await computeStateHashV5(state, { crypto: this._crypto, codec: this._codec });
|
|
118
|
+
let adjacency;
|
|
119
|
+
|
|
120
|
+
if (this._adjacencyCache) {
|
|
121
|
+
adjacency = this._adjacencyCache.get(stateHash);
|
|
122
|
+
if (!adjacency) {
|
|
123
|
+
adjacency = this._buildAdjacency(state);
|
|
124
|
+
this._adjacencyCache.set(stateHash, adjacency);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
adjacency = this._buildAdjacency(state);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this._materializedGraph = { state, stateHash, adjacency };
|
|
131
|
+
return this._materializedGraph;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Materializes the graph with a Lamport ceiling (time-travel).
|
|
136
|
+
*
|
|
137
|
+
* Bypasses checkpoints entirely — replays all patches from all writers,
|
|
138
|
+
* filtering to only those with `lamport <= ceiling`. Skips auto-checkpoint
|
|
139
|
+
* and GC since this is an exploratory read.
|
|
140
|
+
*
|
|
141
|
+
* Uses a dedicated cache keyed on `ceiling` + frontier snapshot. Cache
|
|
142
|
+
* is bypassed when the writer frontier has advanced (new writers or
|
|
143
|
+
* updated tips) or when `collectReceipts` is `true` because the cached
|
|
144
|
+
* path does not retain receipt data.
|
|
145
|
+
*
|
|
146
|
+
* @this {import('../WarpGraph.js').default}
|
|
147
|
+
* @param {number} ceiling - Maximum Lamport tick to include (patches with
|
|
148
|
+
* `lamport <= ceiling` are replayed; `ceiling <= 0` yields empty state)
|
|
149
|
+
* @param {boolean} collectReceipts - When `true`, return receipts alongside
|
|
150
|
+
* state and skip the ceiling cache
|
|
151
|
+
* @param {number} t0 - Start timestamp for performance logging
|
|
152
|
+
* @returns {Promise<import('../services/JoinReducer.js').WarpStateV5 |
|
|
153
|
+
* {state: import('../services/JoinReducer.js').WarpStateV5,
|
|
154
|
+
* receipts: import('../types/TickReceipt.js').TickReceipt[]}>}
|
|
155
|
+
* Plain state when `collectReceipts` is falsy; `{ state, receipts }`
|
|
156
|
+
* when truthy
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
export async function _materializeWithCeiling(ceiling, collectReceipts, t0) {
|
|
160
|
+
const frontier = await this.getFrontier();
|
|
161
|
+
|
|
162
|
+
// Cache hit: same ceiling, clean state, AND frontier unchanged.
|
|
163
|
+
// Bypass cache when collectReceipts is true — cached path has no receipts.
|
|
164
|
+
const cf = this._cachedFrontier;
|
|
165
|
+
if (
|
|
166
|
+
this._cachedState && !this._stateDirty &&
|
|
167
|
+
ceiling === this._cachedCeiling && !collectReceipts &&
|
|
168
|
+
cf !== null &&
|
|
169
|
+
cf.size === frontier.size &&
|
|
170
|
+
[...frontier].every(([w, sha]) => cf.get(w) === sha)
|
|
171
|
+
) {
|
|
172
|
+
return this._cachedState;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const writerIds = [...frontier.keys()];
|
|
176
|
+
|
|
177
|
+
if (writerIds.length === 0 || ceiling <= 0) {
|
|
178
|
+
const state = createEmptyStateV5();
|
|
179
|
+
this._provenanceIndex = new ProvenanceIndex();
|
|
180
|
+
this._provenanceDegraded = false;
|
|
181
|
+
await this._setMaterializedState(state);
|
|
182
|
+
this._cachedCeiling = ceiling;
|
|
183
|
+
this._cachedFrontier = frontier;
|
|
184
|
+
this._logTiming('materialize', t0, { metrics: '0 patches (ceiling)' });
|
|
185
|
+
if (collectReceipts) {
|
|
186
|
+
return { state, receipts: [] };
|
|
187
|
+
}
|
|
188
|
+
return state;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Persistent cache check — skip when collectReceipts is requested
|
|
192
|
+
let cacheKey;
|
|
193
|
+
if (this._seekCache && !collectReceipts) {
|
|
194
|
+
cacheKey = buildSeekCacheKey(ceiling, frontier);
|
|
195
|
+
try {
|
|
196
|
+
const cached = await this._seekCache.get(cacheKey);
|
|
197
|
+
if (cached) {
|
|
198
|
+
try {
|
|
199
|
+
const state = deserializeFullStateV5(cached, { codec: this._codec });
|
|
200
|
+
this._provenanceIndex = new ProvenanceIndex();
|
|
201
|
+
this._provenanceDegraded = true;
|
|
202
|
+
await this._setMaterializedState(state);
|
|
203
|
+
this._cachedCeiling = ceiling;
|
|
204
|
+
this._cachedFrontier = frontier;
|
|
205
|
+
this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
|
|
206
|
+
return state;
|
|
207
|
+
} catch {
|
|
208
|
+
// Corrupted payload — self-heal by removing the bad entry
|
|
209
|
+
try { await this._seekCache.delete(cacheKey); } catch { /* best-effort */ }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// Cache read failed — fall through to full materialization
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const allPatches = [];
|
|
218
|
+
for (const writerId of writerIds) {
|
|
219
|
+
const writerPatches = await this._loadWriterPatches(writerId);
|
|
220
|
+
for (const entry of writerPatches) {
|
|
221
|
+
if (entry.patch.lamport <= ceiling) {
|
|
222
|
+
allPatches.push(entry);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** @type {import('../services/JoinReducer.js').WarpStateV5|undefined} */
|
|
228
|
+
let state;
|
|
229
|
+
/** @type {import('../types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
230
|
+
let receipts;
|
|
231
|
+
|
|
232
|
+
if (allPatches.length === 0) {
|
|
233
|
+
state = createEmptyStateV5();
|
|
234
|
+
if (collectReceipts) {
|
|
235
|
+
receipts = [];
|
|
236
|
+
}
|
|
237
|
+
} else if (collectReceipts) {
|
|
238
|
+
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipts: import('../types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (allPatches), undefined, { receipts: true }));
|
|
239
|
+
state = result.state;
|
|
240
|
+
receipts = result.receipts;
|
|
241
|
+
} else {
|
|
242
|
+
state = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {Parameters<typeof reduceV5>[0]} */ (allPatches)));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this._provenanceIndex = new ProvenanceIndex();
|
|
246
|
+
for (const { patch, sha } of allPatches) {
|
|
247
|
+
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
|
|
248
|
+
}
|
|
249
|
+
this._provenanceDegraded = false;
|
|
250
|
+
|
|
251
|
+
await this._setMaterializedState(state);
|
|
252
|
+
this._cachedCeiling = ceiling;
|
|
253
|
+
this._cachedFrontier = frontier;
|
|
254
|
+
|
|
255
|
+
// Store to persistent cache (fire-and-forget — failure is non-fatal)
|
|
256
|
+
if (this._seekCache && !collectReceipts && allPatches.length > 0) {
|
|
257
|
+
if (!cacheKey) {
|
|
258
|
+
cacheKey = buildSeekCacheKey(ceiling, frontier);
|
|
259
|
+
}
|
|
260
|
+
const buf = serializeFullStateV5(state, { codec: this._codec });
|
|
261
|
+
this._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Skip auto-checkpoint and GC — this is an exploratory read
|
|
265
|
+
this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
|
|
266
|
+
|
|
267
|
+
if (collectReceipts) {
|
|
268
|
+
return { state, receipts: /** @type {import('../types/TickReceipt.js').TickReceipt[]} */ (receipts) };
|
|
269
|
+
}
|
|
270
|
+
return state;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Materializes the graph state at a specific checkpoint.
|
|
275
|
+
*
|
|
276
|
+
* Loads the checkpoint state and frontier, discovers current writers,
|
|
277
|
+
* builds the target frontier from current writer tips, and applies
|
|
278
|
+
* incremental patches since the checkpoint.
|
|
279
|
+
*
|
|
280
|
+
* @this {import('../WarpGraph.js').default}
|
|
281
|
+
* @param {string} checkpointSha - The checkpoint commit SHA
|
|
282
|
+
* @returns {Promise<import('../services/JoinReducer.js').WarpStateV5>} The materialized graph state at the checkpoint
|
|
283
|
+
* @throws {Error} If checkpoint SHA is invalid or not found
|
|
284
|
+
* @throws {Error} If checkpoint loading or patch decoding fails
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* // Time-travel to a previous checkpoint
|
|
288
|
+
* const oldState = await graph.materializeAt('abc123');
|
|
289
|
+
* console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
|
|
290
|
+
*/
|
|
291
|
+
export async function materializeAt(checkpointSha) {
|
|
292
|
+
// 1. Discover current writers to build target frontier
|
|
293
|
+
const writerIds = await this.discoverWriters();
|
|
294
|
+
|
|
295
|
+
// 2. Build target frontier (current tips for all writers)
|
|
296
|
+
const targetFrontier = createFrontier();
|
|
297
|
+
for (const writerId of writerIds) {
|
|
298
|
+
const writerRef = buildWriterRef(this._graphName, writerId);
|
|
299
|
+
const tipSha = await this._persistence.readRef(writerRef);
|
|
300
|
+
if (tipSha) {
|
|
301
|
+
updateFrontier(targetFrontier, writerId, tipSha);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 3. Create a patch loader function for incremental materialization
|
|
306
|
+
const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
|
|
307
|
+
// Load patches from fromSha (exclusive) to toSha (inclusive)
|
|
308
|
+
// Walk from toSha back to fromSha
|
|
309
|
+
const patches = [];
|
|
310
|
+
let currentSha = toSha;
|
|
311
|
+
|
|
312
|
+
while (currentSha && currentSha !== fromSha) {
|
|
313
|
+
const nodeInfo = await this._persistence.getNodeInfo(currentSha);
|
|
314
|
+
const {message} = nodeInfo;
|
|
315
|
+
|
|
316
|
+
const kind = detectMessageKind(message);
|
|
317
|
+
if (kind !== 'patch') {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const patchMeta = decodePatchMessage(message);
|
|
322
|
+
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
323
|
+
const patch = this._codec.decode(patchBuffer);
|
|
324
|
+
|
|
325
|
+
patches.push({ patch, sha: currentSha });
|
|
326
|
+
|
|
327
|
+
if (nodeInfo.parents && nodeInfo.parents.length > 0) {
|
|
328
|
+
currentSha = nodeInfo.parents[0];
|
|
329
|
+
} else {
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return patches.reverse();
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// 4. Call materializeIncremental with the checkpoint and target frontier
|
|
338
|
+
/** @type {CorePersistence} */
|
|
339
|
+
const persistence = this._persistence;
|
|
340
|
+
const state = await materializeIncremental({
|
|
341
|
+
persistence,
|
|
342
|
+
graphName: this._graphName,
|
|
343
|
+
checkpointSha,
|
|
344
|
+
targetFrontier,
|
|
345
|
+
patchLoader,
|
|
346
|
+
codec: this._codec,
|
|
347
|
+
});
|
|
348
|
+
await this._setMaterializedState(state);
|
|
349
|
+
return state;
|
|
350
|
+
}
|