@git-stunts/git-warp 10.1.2 → 10.4.2
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 +31 -4
- package/bin/warp-graph.js +1242 -59
- package/index.d.ts +31 -0
- package/index.js +4 -0
- package/package.json +13 -3
- package/src/domain/WarpGraph.js +487 -140
- package/src/domain/crdt/LWW.js +1 -1
- package/src/domain/crdt/ORSet.js +10 -6
- package/src/domain/crdt/VersionVector.js +5 -1
- package/src/domain/errors/EmptyMessageError.js +2 -4
- package/src/domain/errors/ForkError.js +4 -0
- package/src/domain/errors/IndexError.js +4 -0
- package/src/domain/errors/OperationAbortedError.js +4 -0
- package/src/domain/errors/QueryError.js +4 -0
- package/src/domain/errors/SchemaUnsupportedError.js +4 -0
- package/src/domain/errors/ShardCorruptionError.js +2 -6
- package/src/domain/errors/ShardLoadError.js +2 -6
- package/src/domain/errors/ShardValidationError.js +2 -7
- package/src/domain/errors/StorageError.js +2 -6
- package/src/domain/errors/SyncError.js +4 -0
- package/src/domain/errors/TraversalError.js +4 -0
- package/src/domain/errors/WarpError.js +2 -4
- package/src/domain/errors/WormholeError.js +4 -0
- package/src/domain/services/AnchorMessageCodec.js +1 -4
- package/src/domain/services/BitmapIndexBuilder.js +10 -6
- package/src/domain/services/BitmapIndexReader.js +27 -21
- package/src/domain/services/BoundaryTransitionRecord.js +22 -15
- package/src/domain/services/CheckpointMessageCodec.js +1 -7
- package/src/domain/services/CheckpointSerializerV5.js +20 -19
- package/src/domain/services/CheckpointService.js +18 -18
- package/src/domain/services/CommitDagTraversalService.js +13 -1
- package/src/domain/services/DagPathFinding.js +40 -18
- package/src/domain/services/DagTopology.js +7 -6
- package/src/domain/services/DagTraversal.js +5 -3
- package/src/domain/services/Frontier.js +7 -6
- package/src/domain/services/HealthCheckService.js +15 -14
- package/src/domain/services/HookInstaller.js +64 -13
- package/src/domain/services/HttpSyncServer.js +15 -14
- package/src/domain/services/IndexRebuildService.js +12 -12
- package/src/domain/services/IndexStalenessChecker.js +13 -6
- package/src/domain/services/JoinReducer.js +28 -27
- package/src/domain/services/LogicalTraversal.js +7 -6
- package/src/domain/services/MessageCodecInternal.js +2 -0
- package/src/domain/services/ObserverView.js +6 -6
- package/src/domain/services/PatchBuilderV2.js +9 -9
- package/src/domain/services/PatchMessageCodec.js +1 -7
- package/src/domain/services/ProvenanceIndex.js +6 -8
- package/src/domain/services/ProvenancePayload.js +1 -2
- package/src/domain/services/QueryBuilder.js +29 -23
- package/src/domain/services/StateDiff.js +7 -7
- package/src/domain/services/StateSerializerV5.js +8 -6
- package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
- package/src/domain/services/SyncProtocol.js +23 -26
- package/src/domain/services/TemporalQuery.js +4 -3
- package/src/domain/services/TranslationCost.js +4 -4
- package/src/domain/services/WormholeService.js +19 -15
- package/src/domain/types/TickReceipt.js +10 -6
- package/src/domain/types/WarpTypesV2.js +2 -3
- package/src/domain/utils/CachedValue.js +1 -1
- package/src/domain/utils/LRUCache.js +3 -3
- package/src/domain/utils/MinHeap.js +2 -2
- package/src/domain/utils/RefLayout.js +106 -15
- package/src/domain/utils/WriterId.js +2 -2
- package/src/domain/utils/defaultCodec.js +9 -2
- package/src/domain/utils/defaultCrypto.js +36 -0
- package/src/domain/utils/parseCursorBlob.js +51 -0
- package/src/domain/utils/roaring.js +5 -5
- package/src/domain/utils/seekCacheKey.js +32 -0
- package/src/domain/warp/PatchSession.js +3 -3
- package/src/domain/warp/Writer.js +2 -2
- package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
- package/src/infrastructure/adapters/ClockAdapter.js +2 -2
- package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
- package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
- package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
- package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
- package/src/infrastructure/codecs/CborCodec.js +16 -8
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/CodecPort.js +2 -2
- package/src/ports/CommitPort.js +8 -21
- package/src/ports/ConfigPort.js +3 -3
- package/src/ports/CryptoPort.js +7 -7
- package/src/ports/GraphPersistencePort.js +12 -14
- package/src/ports/HttpServerPort.js +1 -5
- package/src/ports/IndexStoragePort.js +1 -0
- package/src/ports/LoggerPort.js +9 -9
- package/src/ports/RefPort.js +5 -5
- package/src/ports/SeekCachePort.js +73 -0
- package/src/ports/TreePort.js +3 -3
- package/src/visualization/layouts/converters.js +14 -7
- package/src/visualization/layouts/elkAdapter.js +24 -11
- package/src/visualization/layouts/elkLayout.js +23 -7
- package/src/visualization/layouts/index.js +3 -3
- package/src/visualization/renderers/ascii/check.js +30 -17
- package/src/visualization/renderers/ascii/graph.js +122 -16
- package/src/visualization/renderers/ascii/history.js +29 -90
- package/src/visualization/renderers/ascii/index.js +1 -1
- package/src/visualization/renderers/ascii/info.js +9 -7
- package/src/visualization/renderers/ascii/materialize.js +20 -16
- package/src/visualization/renderers/ascii/opSummary.js +81 -0
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +344 -0
- package/src/visualization/renderers/ascii/table.js +1 -1
- package/src/visualization/renderers/svg/index.js +5 -1
package/src/domain/WarpGraph.js
CHANGED
|
@@ -17,13 +17,14 @@ import { ProvenancePayload } from './services/ProvenancePayload.js';
|
|
|
17
17
|
import { diffStates, isEmptyDiff } from './services/StateDiff.js';
|
|
18
18
|
import { orsetContains, orsetElements } from './crdt/ORSet.js';
|
|
19
19
|
import defaultCodec from './utils/defaultCodec.js';
|
|
20
|
+
import defaultCrypto from './utils/defaultCrypto.js';
|
|
20
21
|
import { decodePatchMessage, detectMessageKind, encodeAnchorMessage } from './services/WarpMessageCodec.js';
|
|
21
22
|
import { loadCheckpoint, materializeIncremental, create as createCheckpointCommit } from './services/CheckpointService.js';
|
|
22
23
|
import { createFrontier, updateFrontier } from './services/Frontier.js';
|
|
23
24
|
import { createVersionVector, vvClone, vvIncrement } from './crdt/VersionVector.js';
|
|
24
25
|
import { DEFAULT_GC_POLICY, shouldRunGC, executeGC } from './services/GCPolicy.js';
|
|
25
26
|
import { collectGCMetrics } from './services/GCMetrics.js';
|
|
26
|
-
import { computeAppliedVV } from './services/CheckpointSerializerV5.js';
|
|
27
|
+
import { computeAppliedVV, serializeFullStateV5, deserializeFullStateV5 } from './services/CheckpointSerializerV5.js';
|
|
27
28
|
import { computeStateHashV5 } from './services/StateSerializerV5.js';
|
|
28
29
|
import {
|
|
29
30
|
createSyncRequest,
|
|
@@ -48,8 +49,13 @@ import OperationAbortedError from './errors/OperationAbortedError.js';
|
|
|
48
49
|
import { compareEventIds } from './utils/EventId.js';
|
|
49
50
|
import { TemporalQuery } from './services/TemporalQuery.js';
|
|
50
51
|
import HttpSyncServer from './services/HttpSyncServer.js';
|
|
52
|
+
import { buildSeekCacheKey } from './utils/seekCacheKey.js';
|
|
51
53
|
import defaultClock from './utils/defaultClock.js';
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {import('../ports/GraphPersistencePort.js').default & import('../ports/RefPort.js').default & import('../ports/CommitPort.js').default & import('../ports/BlobPort.js').default & import('../ports/TreePort.js').default & import('../ports/ConfigPort.js').default} FullPersistence
|
|
57
|
+
*/
|
|
58
|
+
|
|
53
59
|
const DEFAULT_SYNC_SERVER_MAX_BYTES = 4 * 1024 * 1024;
|
|
54
60
|
const DEFAULT_SYNC_WITH_RETRIES = 3;
|
|
55
61
|
const DEFAULT_SYNC_WITH_BASE_DELAY_MS = 250;
|
|
@@ -99,10 +105,11 @@ export default class WarpGraph {
|
|
|
99
105
|
* @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
|
|
100
106
|
* @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
|
|
101
107
|
* @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
|
|
108
|
+
* @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
|
|
102
109
|
*/
|
|
103
|
-
constructor({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize = DEFAULT_ADJACENCY_CACHE_SIZE, checkpointPolicy, autoMaterialize = false, onDeleteWithData = 'warn', logger, clock, crypto, codec }) {
|
|
104
|
-
/** @type {
|
|
105
|
-
this._persistence = persistence;
|
|
110
|
+
constructor({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize = DEFAULT_ADJACENCY_CACHE_SIZE, checkpointPolicy, autoMaterialize = false, onDeleteWithData = 'warn', logger, clock, crypto, codec, seekCache }) {
|
|
111
|
+
/** @type {FullPersistence} */
|
|
112
|
+
this._persistence = /** @type {FullPersistence} */ (persistence);
|
|
106
113
|
|
|
107
114
|
/** @type {string} */
|
|
108
115
|
this._graphName = graphName;
|
|
@@ -146,7 +153,7 @@ export default class WarpGraph {
|
|
|
146
153
|
/** @type {MaterializedGraph|null} */
|
|
147
154
|
this._materializedGraph = null;
|
|
148
155
|
|
|
149
|
-
/** @type {import('./utils/LRUCache.js').default
|
|
156
|
+
/** @type {import('./utils/LRUCache.js').default<string, {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}>|null} */
|
|
150
157
|
this._adjacencyCache = adjacencyCacheSize > 0 ? new LRUCache(adjacencyCacheSize) : null;
|
|
151
158
|
|
|
152
159
|
/** @type {Map<string, string>|null} */
|
|
@@ -158,8 +165,8 @@ export default class WarpGraph {
|
|
|
158
165
|
/** @type {import('../ports/ClockPort.js').default} */
|
|
159
166
|
this._clock = clock || defaultClock;
|
|
160
167
|
|
|
161
|
-
/** @type {import('../ports/CryptoPort.js').default
|
|
162
|
-
this._crypto = crypto;
|
|
168
|
+
/** @type {import('../ports/CryptoPort.js').default} */
|
|
169
|
+
this._crypto = crypto || defaultCrypto;
|
|
163
170
|
|
|
164
171
|
/** @type {import('../ports/CodecPort.js').default} */
|
|
165
172
|
this._codec = codec || defaultCodec;
|
|
@@ -167,7 +174,7 @@ export default class WarpGraph {
|
|
|
167
174
|
/** @type {'reject'|'cascade'|'warn'} */
|
|
168
175
|
this._onDeleteWithData = onDeleteWithData;
|
|
169
176
|
|
|
170
|
-
/** @type {Array<{onChange: Function, onError?: Function}>} */
|
|
177
|
+
/** @type {Array<{onChange: Function, onError?: Function, pendingReplay?: boolean}>} */
|
|
171
178
|
this._subscribers = [];
|
|
172
179
|
|
|
173
180
|
/** @type {import('./services/JoinReducer.js').WarpStateV5|null} */
|
|
@@ -178,6 +185,41 @@ export default class WarpGraph {
|
|
|
178
185
|
|
|
179
186
|
/** @type {import('./services/TemporalQuery.js').TemporalQuery|null} */
|
|
180
187
|
this._temporalQuery = null;
|
|
188
|
+
|
|
189
|
+
/** @type {number|null} */
|
|
190
|
+
this._seekCeiling = null;
|
|
191
|
+
|
|
192
|
+
/** @type {number|null} */
|
|
193
|
+
this._cachedCeiling = null;
|
|
194
|
+
|
|
195
|
+
/** @type {Map<string, string>|null} */
|
|
196
|
+
this._cachedFrontier = null;
|
|
197
|
+
|
|
198
|
+
/** @type {import('../ports/SeekCachePort.js').default|null} */
|
|
199
|
+
this._seekCache = seekCache || null;
|
|
200
|
+
|
|
201
|
+
/** @type {boolean} */
|
|
202
|
+
this._provenanceDegraded = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Returns the attached seek cache, or null if none is set.
|
|
207
|
+
* @returns {import('../ports/SeekCachePort.js').default|null}
|
|
208
|
+
*/
|
|
209
|
+
get seekCache() {
|
|
210
|
+
return this._seekCache;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Attaches a persistent seek cache after construction.
|
|
215
|
+
*
|
|
216
|
+
* Useful when the cache adapter cannot be created until after the
|
|
217
|
+
* graph is opened (e.g. the CLI wires it based on flags).
|
|
218
|
+
*
|
|
219
|
+
* @param {import('../ports/SeekCachePort.js').default} cache - SeekCachePort implementation
|
|
220
|
+
*/
|
|
221
|
+
setSeekCache(cache) {
|
|
222
|
+
this._seekCache = cache;
|
|
181
223
|
}
|
|
182
224
|
|
|
183
225
|
/**
|
|
@@ -218,6 +260,7 @@ export default class WarpGraph {
|
|
|
218
260
|
* @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
|
|
219
261
|
* @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
|
|
220
262
|
* @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
|
|
263
|
+
* @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
|
|
221
264
|
* @returns {Promise<WarpGraph>} The opened graph instance
|
|
222
265
|
* @throws {Error} If graphName, writerId, checkpointPolicy, or onDeleteWithData is invalid
|
|
223
266
|
*
|
|
@@ -228,7 +271,7 @@ export default class WarpGraph {
|
|
|
228
271
|
* writerId: 'node-1'
|
|
229
272
|
* });
|
|
230
273
|
*/
|
|
231
|
-
static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec }) {
|
|
274
|
+
static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache }) {
|
|
232
275
|
// Validate inputs
|
|
233
276
|
validateGraphName(graphName);
|
|
234
277
|
validateWriterId(writerId);
|
|
@@ -260,7 +303,7 @@ export default class WarpGraph {
|
|
|
260
303
|
}
|
|
261
304
|
}
|
|
262
305
|
|
|
263
|
-
const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec });
|
|
306
|
+
const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache });
|
|
264
307
|
|
|
265
308
|
// Validate migration boundary
|
|
266
309
|
await graph._validateMigrationBoundary();
|
|
@@ -286,7 +329,7 @@ export default class WarpGraph {
|
|
|
286
329
|
|
|
287
330
|
/**
|
|
288
331
|
* Gets the persistence adapter.
|
|
289
|
-
* @returns {
|
|
332
|
+
* @returns {FullPersistence} The persistence adapter
|
|
290
333
|
*/
|
|
291
334
|
get persistence() {
|
|
292
335
|
return this._persistence;
|
|
@@ -328,9 +371,9 @@ export default class WarpGraph {
|
|
|
328
371
|
getCurrentState: () => this._cachedState,
|
|
329
372
|
expectedParentSha: parentSha,
|
|
330
373
|
onDeleteWithData: this._onDeleteWithData,
|
|
331
|
-
onCommitSuccess: (opts) => this._onPatchCommitted(this._writerId, opts),
|
|
374
|
+
onCommitSuccess: (/** @type {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(this._writerId, opts),
|
|
332
375
|
codec: this._codec,
|
|
333
|
-
logger: this._logger,
|
|
376
|
+
logger: this._logger || undefined,
|
|
334
377
|
});
|
|
335
378
|
}
|
|
336
379
|
|
|
@@ -339,7 +382,7 @@ export default class WarpGraph {
|
|
|
339
382
|
*
|
|
340
383
|
* @param {string} writerId - The writer ID to load patches for
|
|
341
384
|
* @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
|
|
342
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
385
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
|
|
343
386
|
*/
|
|
344
387
|
async getWriterPatches(writerId, stopAtSha = null) {
|
|
345
388
|
return await this._loadWriterPatches(writerId, stopAtSha);
|
|
@@ -390,7 +433,7 @@ export default class WarpGraph {
|
|
|
390
433
|
*
|
|
391
434
|
* @param {string} writerId - The writer ID to load patches for
|
|
392
435
|
* @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
|
|
393
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
436
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
|
|
394
437
|
* @private
|
|
395
438
|
*/
|
|
396
439
|
async _loadWriterPatches(writerId, stopAtSha = null) {
|
|
@@ -421,7 +464,7 @@ export default class WarpGraph {
|
|
|
421
464
|
|
|
422
465
|
// Read the patch blob
|
|
423
466
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
424
|
-
const patch = this._codec.decode(patchBuffer);
|
|
467
|
+
const patch = /** @type {import('./types/WarpTypesV2.js').PatchV2} */ (this._codec.decode(patchBuffer));
|
|
425
468
|
|
|
426
469
|
patches.push({ patch, sha: currentSha });
|
|
427
470
|
|
|
@@ -465,8 +508,8 @@ export default class WarpGraph {
|
|
|
465
508
|
incoming.get(to).push({ neighborId: from, label });
|
|
466
509
|
}
|
|
467
510
|
|
|
468
|
-
const sortNeighbors = (list) => {
|
|
469
|
-
list.sort((a, b) => {
|
|
511
|
+
const sortNeighbors = (/** @type {Array<{neighborId: string, label: string}>} */ list) => {
|
|
512
|
+
list.sort((/** @type {{neighborId: string, label: string}} */ a, /** @type {{neighborId: string, label: string}} */ b) => {
|
|
470
513
|
if (a.neighborId !== b.neighborId) {
|
|
471
514
|
return a.neighborId < b.neighborId ? -1 : 1;
|
|
472
515
|
}
|
|
@@ -520,7 +563,7 @@ export default class WarpGraph {
|
|
|
520
563
|
* provenance index, and frontier tracking.
|
|
521
564
|
*
|
|
522
565
|
* @param {string} writerId - The writer ID that committed the patch
|
|
523
|
-
* @param {{patch?:
|
|
566
|
+
* @param {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} [opts] - Commit details
|
|
524
567
|
* @private
|
|
525
568
|
*/
|
|
526
569
|
async _onPatchCommitted(writerId, { patch, sha } = {}) {
|
|
@@ -529,11 +572,11 @@ export default class WarpGraph {
|
|
|
529
572
|
// Eager re-materialize: apply the just-committed patch to cached state
|
|
530
573
|
// Only when the cache is clean — applying a patch to stale state would be incorrect
|
|
531
574
|
if (this._cachedState && !this._stateDirty && patch && sha) {
|
|
532
|
-
joinPatch(this._cachedState, patch, sha);
|
|
575
|
+
joinPatch(this._cachedState, /** @type {any} */ (patch), sha); // TODO(ts-cleanup): type patch array
|
|
533
576
|
await this._setMaterializedState(this._cachedState);
|
|
534
577
|
// Update provenance index with new patch
|
|
535
578
|
if (this._provenanceIndex) {
|
|
536
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
579
|
+
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
|
|
537
580
|
}
|
|
538
581
|
// Keep _lastFrontier in sync so hasFrontierChanged() won't misreport stale
|
|
539
582
|
if (this._lastFrontier) {
|
|
@@ -552,9 +595,9 @@ export default class WarpGraph {
|
|
|
552
595
|
async _materializeGraph() {
|
|
553
596
|
const state = await this.materialize();
|
|
554
597
|
if (!this._materializedGraph || this._materializedGraph.state !== state) {
|
|
555
|
-
await this._setMaterializedState(state);
|
|
598
|
+
await this._setMaterializedState(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (state));
|
|
556
599
|
}
|
|
557
|
-
return this._materializedGraph;
|
|
600
|
+
return /** @type {MaterializedGraph} */ (this._materializedGraph);
|
|
558
601
|
}
|
|
559
602
|
|
|
560
603
|
/**
|
|
@@ -570,11 +613,16 @@ export default class WarpGraph {
|
|
|
570
613
|
* When false or omitted (default), returns just the state for backward
|
|
571
614
|
* compatibility with zero receipt overhead.
|
|
572
615
|
*
|
|
616
|
+
* When a Lamport ceiling is active (via `options.ceiling` or the
|
|
617
|
+
* instance-level `_seekCeiling`), delegates to a ceiling-aware path
|
|
618
|
+
* that replays only patches with `lamport <= ceiling`, bypassing
|
|
619
|
+
* checkpoints, auto-checkpoint, and GC.
|
|
620
|
+
*
|
|
573
621
|
* Side effects: Updates internal cached state, version vector, last frontier,
|
|
574
622
|
* and patches-since-checkpoint counter. May trigger auto-checkpoint and GC
|
|
575
623
|
* based on configured policies. Notifies subscribers if state changed.
|
|
576
624
|
*
|
|
577
|
-
* @param {{receipts?: boolean}} [options] - Optional configuration
|
|
625
|
+
* @param {{receipts?: boolean, ceiling?: number|null}} [options] - Optional configuration
|
|
578
626
|
* @returns {Promise<import('./services/JoinReducer.js').WarpStateV5|{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}>} The materialized graph state, or { state, receipts } when receipts enabled
|
|
579
627
|
* @throws {Error} If checkpoint loading fails or patch decoding fails
|
|
580
628
|
* @throws {Error} If writer ref access or patch blob reading fails
|
|
@@ -583,12 +631,21 @@ export default class WarpGraph {
|
|
|
583
631
|
const t0 = this._clock.now();
|
|
584
632
|
// ZERO-COST: only resolve receipts flag when options provided
|
|
585
633
|
const collectReceipts = options && options.receipts;
|
|
634
|
+
// Resolve ceiling: explicit option > instance-level seek ceiling > null (latest)
|
|
635
|
+
const ceiling = this._resolveCeiling(options);
|
|
636
|
+
|
|
637
|
+
// When ceiling is active, delegate to ceiling-aware path (with its own cache)
|
|
638
|
+
if (ceiling !== null) {
|
|
639
|
+
return await this._materializeWithCeiling(ceiling, !!collectReceipts, t0);
|
|
640
|
+
}
|
|
586
641
|
|
|
587
642
|
try {
|
|
588
643
|
// Check for checkpoint
|
|
589
644
|
const checkpoint = await this._loadLatestCheckpoint();
|
|
590
645
|
|
|
646
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
|
|
591
647
|
let state;
|
|
648
|
+
/** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
592
649
|
let receipts;
|
|
593
650
|
let patchCount = 0;
|
|
594
651
|
|
|
@@ -596,20 +653,21 @@ export default class WarpGraph {
|
|
|
596
653
|
if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
|
|
597
654
|
const patches = await this._loadPatchesSince(checkpoint);
|
|
598
655
|
if (collectReceipts) {
|
|
599
|
-
const result = reduceV5(patches, checkpoint.state, { receipts: true });
|
|
656
|
+
const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {any} */ (patches), /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (checkpoint.state), { receipts: true })); // TODO(ts-cleanup): type patch array
|
|
600
657
|
state = result.state;
|
|
601
658
|
receipts = result.receipts;
|
|
602
659
|
} else {
|
|
603
|
-
state = reduceV5(patches, checkpoint.state);
|
|
660
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (patches), /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (checkpoint.state))); // TODO(ts-cleanup): type patch array
|
|
604
661
|
}
|
|
605
662
|
patchCount = patches.length;
|
|
606
663
|
|
|
607
664
|
// Build provenance index: start from checkpoint index if present, then add new patches
|
|
608
|
-
|
|
609
|
-
|
|
665
|
+
const ckPI = /** @type {any} */ (checkpoint).provenanceIndex; // TODO(ts-cleanup): type checkpoint cast
|
|
666
|
+
this._provenanceIndex = ckPI
|
|
667
|
+
? ckPI.clone()
|
|
610
668
|
: new ProvenanceIndex();
|
|
611
669
|
for (const { patch, sha } of patches) {
|
|
612
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
670
|
+
/** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).addPatch(sha, patch.reads, patch.writes);
|
|
613
671
|
}
|
|
614
672
|
} else {
|
|
615
673
|
// 1. Discover all writers
|
|
@@ -640,11 +698,11 @@ export default class WarpGraph {
|
|
|
640
698
|
} else {
|
|
641
699
|
// 5. Reduce all patches to state
|
|
642
700
|
if (collectReceipts) {
|
|
643
|
-
const result = reduceV5(allPatches, undefined, { receipts: true });
|
|
701
|
+
const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {any} */ (allPatches), undefined, { receipts: true })); // TODO(ts-cleanup): type patch array
|
|
644
702
|
state = result.state;
|
|
645
703
|
receipts = result.receipts;
|
|
646
704
|
} else {
|
|
647
|
-
state = reduceV5(allPatches);
|
|
705
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
|
|
648
706
|
}
|
|
649
707
|
patchCount = allPatches.length;
|
|
650
708
|
|
|
@@ -658,6 +716,9 @@ export default class WarpGraph {
|
|
|
658
716
|
}
|
|
659
717
|
|
|
660
718
|
await this._setMaterializedState(state);
|
|
719
|
+
this._provenanceDegraded = false;
|
|
720
|
+
this._cachedCeiling = null;
|
|
721
|
+
this._cachedFrontier = null;
|
|
661
722
|
this._lastFrontier = await this.getFrontier();
|
|
662
723
|
this._patchesSinceCheckpoint = patchCount;
|
|
663
724
|
|
|
@@ -689,15 +750,173 @@ export default class WarpGraph {
|
|
|
689
750
|
this._logTiming('materialize', t0, { metrics: `${patchCount} patches` });
|
|
690
751
|
|
|
691
752
|
if (collectReceipts) {
|
|
692
|
-
return { state, receipts };
|
|
753
|
+
return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
|
|
693
754
|
}
|
|
694
755
|
return state;
|
|
695
756
|
} catch (err) {
|
|
696
|
-
this._logTiming('materialize', t0, { error: err });
|
|
757
|
+
this._logTiming('materialize', t0, { error: /** @type {Error} */ (err) });
|
|
697
758
|
throw err;
|
|
698
759
|
}
|
|
699
760
|
}
|
|
700
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Resolves the effective ceiling from options and instance state.
|
|
764
|
+
*
|
|
765
|
+
* Precedence: explicit `ceiling` in options overrides the instance-level
|
|
766
|
+
* `_seekCeiling`. Uses the `'ceiling' in options` check, so passing
|
|
767
|
+
* `{ ceiling: null }` explicitly clears the seek ceiling for that call
|
|
768
|
+
* (returns `null`), while omitting the key falls through to `_seekCeiling`.
|
|
769
|
+
*
|
|
770
|
+
* @param {{ceiling?: number|null}} [options] - Options object; when the
|
|
771
|
+
* `ceiling` key is present (even if `null`), its value takes precedence
|
|
772
|
+
* @returns {number|null} Lamport ceiling to apply, or `null` for latest
|
|
773
|
+
* @private
|
|
774
|
+
*/
|
|
775
|
+
_resolveCeiling(options) {
|
|
776
|
+
if (options && options.ceiling !== undefined) {
|
|
777
|
+
return options.ceiling;
|
|
778
|
+
}
|
|
779
|
+
return this._seekCeiling;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Materializes the graph with a Lamport ceiling (time-travel).
|
|
784
|
+
*
|
|
785
|
+
* Bypasses checkpoints entirely — replays all patches from all writers,
|
|
786
|
+
* filtering to only those with `lamport <= ceiling`. Skips auto-checkpoint
|
|
787
|
+
* and GC since this is an exploratory read.
|
|
788
|
+
*
|
|
789
|
+
* Uses a dedicated cache keyed on `ceiling` + frontier snapshot. Cache
|
|
790
|
+
* is bypassed when the writer frontier has advanced (new writers or
|
|
791
|
+
* updated tips) or when `collectReceipts` is `true` because the cached
|
|
792
|
+
* path does not retain receipt data.
|
|
793
|
+
*
|
|
794
|
+
* @param {number} ceiling - Maximum Lamport tick to include (patches with
|
|
795
|
+
* `lamport <= ceiling` are replayed; `ceiling <= 0` yields empty state)
|
|
796
|
+
* @param {boolean} collectReceipts - When `true`, return receipts alongside
|
|
797
|
+
* state and skip the ceiling cache
|
|
798
|
+
* @param {number} t0 - Start timestamp for performance logging
|
|
799
|
+
* @returns {Promise<import('./services/JoinReducer.js').WarpStateV5 |
|
|
800
|
+
* {state: import('./services/JoinReducer.js').WarpStateV5,
|
|
801
|
+
* receipts: import('./types/TickReceipt.js').TickReceipt[]}>}
|
|
802
|
+
* Plain state when `collectReceipts` is falsy; `{ state, receipts }`
|
|
803
|
+
* when truthy
|
|
804
|
+
* @private
|
|
805
|
+
*/
|
|
806
|
+
async _materializeWithCeiling(ceiling, collectReceipts, t0) {
|
|
807
|
+
const frontier = await this.getFrontier();
|
|
808
|
+
|
|
809
|
+
// Cache hit: same ceiling, clean state, AND frontier unchanged.
|
|
810
|
+
// Bypass cache when collectReceipts is true — cached path has no receipts.
|
|
811
|
+
const cf = this._cachedFrontier;
|
|
812
|
+
if (
|
|
813
|
+
this._cachedState && !this._stateDirty &&
|
|
814
|
+
ceiling === this._cachedCeiling && !collectReceipts &&
|
|
815
|
+
cf !== null &&
|
|
816
|
+
cf.size === frontier.size &&
|
|
817
|
+
[...frontier].every(([w, sha]) => cf.get(w) === sha)
|
|
818
|
+
) {
|
|
819
|
+
return this._cachedState;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const writerIds = [...frontier.keys()];
|
|
823
|
+
|
|
824
|
+
if (writerIds.length === 0 || ceiling <= 0) {
|
|
825
|
+
const state = createEmptyStateV5();
|
|
826
|
+
this._provenanceIndex = new ProvenanceIndex();
|
|
827
|
+
this._provenanceDegraded = false;
|
|
828
|
+
await this._setMaterializedState(state);
|
|
829
|
+
this._cachedCeiling = ceiling;
|
|
830
|
+
this._cachedFrontier = frontier;
|
|
831
|
+
this._logTiming('materialize', t0, { metrics: '0 patches (ceiling)' });
|
|
832
|
+
if (collectReceipts) {
|
|
833
|
+
return { state, receipts: [] };
|
|
834
|
+
}
|
|
835
|
+
return state;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Persistent cache check — skip when collectReceipts is requested
|
|
839
|
+
let cacheKey;
|
|
840
|
+
if (this._seekCache && !collectReceipts) {
|
|
841
|
+
cacheKey = buildSeekCacheKey(ceiling, frontier);
|
|
842
|
+
try {
|
|
843
|
+
const cached = await this._seekCache.get(cacheKey);
|
|
844
|
+
if (cached) {
|
|
845
|
+
try {
|
|
846
|
+
const state = deserializeFullStateV5(cached, { codec: this._codec });
|
|
847
|
+
this._provenanceIndex = new ProvenanceIndex();
|
|
848
|
+
this._provenanceDegraded = true;
|
|
849
|
+
await this._setMaterializedState(state);
|
|
850
|
+
this._cachedCeiling = ceiling;
|
|
851
|
+
this._cachedFrontier = frontier;
|
|
852
|
+
this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
|
|
853
|
+
return state;
|
|
854
|
+
} catch {
|
|
855
|
+
// Corrupted payload — self-heal by removing the bad entry
|
|
856
|
+
try { await this._seekCache.delete(cacheKey); } catch { /* best-effort */ }
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
} catch {
|
|
860
|
+
// Cache read failed — fall through to full materialization
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const allPatches = [];
|
|
865
|
+
for (const writerId of writerIds) {
|
|
866
|
+
const writerPatches = await this._loadWriterPatches(writerId);
|
|
867
|
+
for (const entry of writerPatches) {
|
|
868
|
+
if (entry.patch.lamport <= ceiling) {
|
|
869
|
+
allPatches.push(entry);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
|
|
875
|
+
let state;
|
|
876
|
+
/** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
877
|
+
let receipts;
|
|
878
|
+
|
|
879
|
+
if (allPatches.length === 0) {
|
|
880
|
+
state = createEmptyStateV5();
|
|
881
|
+
if (collectReceipts) {
|
|
882
|
+
receipts = [];
|
|
883
|
+
}
|
|
884
|
+
} else if (collectReceipts) {
|
|
885
|
+
const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(/** @type {any} */ (allPatches), undefined, { receipts: true })); // TODO(ts-cleanup): type patch array
|
|
886
|
+
state = result.state;
|
|
887
|
+
receipts = result.receipts;
|
|
888
|
+
} else {
|
|
889
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
this._provenanceIndex = new ProvenanceIndex();
|
|
893
|
+
for (const { patch, sha } of allPatches) {
|
|
894
|
+
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
|
|
895
|
+
}
|
|
896
|
+
this._provenanceDegraded = false;
|
|
897
|
+
|
|
898
|
+
await this._setMaterializedState(state);
|
|
899
|
+
this._cachedCeiling = ceiling;
|
|
900
|
+
this._cachedFrontier = frontier;
|
|
901
|
+
|
|
902
|
+
// Store to persistent cache (fire-and-forget — failure is non-fatal)
|
|
903
|
+
if (this._seekCache && !collectReceipts && allPatches.length > 0) {
|
|
904
|
+
if (!cacheKey) {
|
|
905
|
+
cacheKey = buildSeekCacheKey(ceiling, frontier);
|
|
906
|
+
}
|
|
907
|
+
const buf = serializeFullStateV5(state, { codec: this._codec });
|
|
908
|
+
this._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Skip auto-checkpoint and GC — this is an exploratory read
|
|
912
|
+
this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
|
|
913
|
+
|
|
914
|
+
if (collectReceipts) {
|
|
915
|
+
return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
|
|
916
|
+
}
|
|
917
|
+
return state;
|
|
918
|
+
}
|
|
919
|
+
|
|
701
920
|
/**
|
|
702
921
|
* Joins (merges) another state into the current cached state.
|
|
703
922
|
*
|
|
@@ -742,16 +961,16 @@ export default class WarpGraph {
|
|
|
742
961
|
}
|
|
743
962
|
|
|
744
963
|
// Capture pre-merge counts for receipt
|
|
745
|
-
const beforeNodes = this._cachedState.nodeAlive.
|
|
746
|
-
const beforeEdges = this._cachedState.edgeAlive.
|
|
964
|
+
const beforeNodes = orsetElements(this._cachedState.nodeAlive).length;
|
|
965
|
+
const beforeEdges = orsetElements(this._cachedState.edgeAlive).length;
|
|
747
966
|
const beforeFrontierSize = this._cachedState.observedFrontier.size;
|
|
748
967
|
|
|
749
968
|
// Perform the join
|
|
750
969
|
const mergedState = joinStates(this._cachedState, otherState);
|
|
751
970
|
|
|
752
971
|
// Calculate receipt
|
|
753
|
-
const afterNodes = mergedState.nodeAlive.
|
|
754
|
-
const afterEdges = mergedState.edgeAlive.
|
|
972
|
+
const afterNodes = orsetElements(mergedState.nodeAlive).length;
|
|
973
|
+
const afterEdges = orsetElements(mergedState.edgeAlive).length;
|
|
755
974
|
const afterFrontierSize = mergedState.observedFrontier.size;
|
|
756
975
|
|
|
757
976
|
// Count property changes (keys that existed in both but have different values)
|
|
@@ -813,7 +1032,7 @@ export default class WarpGraph {
|
|
|
813
1032
|
* @example
|
|
814
1033
|
* // Time-travel to a previous checkpoint
|
|
815
1034
|
* const oldState = await graph.materializeAt('abc123');
|
|
816
|
-
* console.log('Nodes at checkpoint:',
|
|
1035
|
+
* console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
|
|
817
1036
|
*/
|
|
818
1037
|
async materializeAt(checkpointSha) {
|
|
819
1038
|
// 1. Discover current writers to build target frontier
|
|
@@ -830,7 +1049,7 @@ export default class WarpGraph {
|
|
|
830
1049
|
}
|
|
831
1050
|
|
|
832
1051
|
// 3. Create a patch loader function for incremental materialization
|
|
833
|
-
const patchLoader = async (writerId, fromSha, toSha) => {
|
|
1052
|
+
const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
|
|
834
1053
|
// Load patches from fromSha (exclusive) to toSha (inclusive)
|
|
835
1054
|
// Walk from toSha back to fromSha
|
|
836
1055
|
const patches = [];
|
|
@@ -863,7 +1082,7 @@ export default class WarpGraph {
|
|
|
863
1082
|
|
|
864
1083
|
// 4. Call materializeIncremental with the checkpoint and target frontier
|
|
865
1084
|
const state = await materializeIncremental({
|
|
866
|
-
persistence: this._persistence,
|
|
1085
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
867
1086
|
graphName: this._graphName,
|
|
868
1087
|
checkpointSha,
|
|
869
1088
|
targetFrontier,
|
|
@@ -907,23 +1126,24 @@ export default class WarpGraph {
|
|
|
907
1126
|
// 3. Materialize current state (reuse cached if fresh, guard against recursion)
|
|
908
1127
|
const prevCheckpointing = this._checkpointing;
|
|
909
1128
|
this._checkpointing = true;
|
|
1129
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5} */
|
|
910
1130
|
let state;
|
|
911
1131
|
try {
|
|
912
|
-
state = (this._cachedState && !this._stateDirty)
|
|
1132
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ ((this._cachedState && !this._stateDirty)
|
|
913
1133
|
? this._cachedState
|
|
914
|
-
: await this.materialize();
|
|
1134
|
+
: await this.materialize());
|
|
915
1135
|
} finally {
|
|
916
1136
|
this._checkpointing = prevCheckpointing;
|
|
917
1137
|
}
|
|
918
1138
|
|
|
919
1139
|
// 4. Call CheckpointService.create() with provenance index if available
|
|
920
1140
|
const checkpointSha = await createCheckpointCommit({
|
|
921
|
-
persistence: this._persistence,
|
|
1141
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
922
1142
|
graphName: this._graphName,
|
|
923
1143
|
state,
|
|
924
1144
|
frontier,
|
|
925
1145
|
parents,
|
|
926
|
-
provenanceIndex: this._provenanceIndex,
|
|
1146
|
+
provenanceIndex: this._provenanceIndex || undefined,
|
|
927
1147
|
crypto: this._crypto,
|
|
928
1148
|
codec: this._codec,
|
|
929
1149
|
});
|
|
@@ -937,7 +1157,7 @@ export default class WarpGraph {
|
|
|
937
1157
|
// 6. Return checkpoint SHA
|
|
938
1158
|
return checkpointSha;
|
|
939
1159
|
} catch (err) {
|
|
940
|
-
this._logTiming('createCheckpoint', t0, { error: err });
|
|
1160
|
+
this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
|
|
941
1161
|
throw err;
|
|
942
1162
|
}
|
|
943
1163
|
}
|
|
@@ -1010,6 +1230,81 @@ export default class WarpGraph {
|
|
|
1010
1230
|
return writerIds.sort();
|
|
1011
1231
|
}
|
|
1012
1232
|
|
|
1233
|
+
/**
|
|
1234
|
+
* Discovers all distinct Lamport ticks across all writers.
|
|
1235
|
+
*
|
|
1236
|
+
* Walks each writer's patch chain from tip to root, reading commit
|
|
1237
|
+
* messages (no CBOR blob deserialization) to extract Lamport timestamps.
|
|
1238
|
+
* Stops when a non-patch commit (e.g. checkpoint) is encountered.
|
|
1239
|
+
* Logs a warning for any non-monotonic lamport sequence within a single
|
|
1240
|
+
* writer's chain.
|
|
1241
|
+
*
|
|
1242
|
+
* @returns {Promise<{
|
|
1243
|
+
* ticks: number[],
|
|
1244
|
+
* maxTick: number,
|
|
1245
|
+
* perWriter: Map<string, {ticks: number[], tipSha: string|null}>
|
|
1246
|
+
* }>} `ticks` is the sorted (ascending) deduplicated union of all
|
|
1247
|
+
* Lamport values; `maxTick` is the largest value (0 if none);
|
|
1248
|
+
* `perWriter` maps each writer ID to its ticks in ascending order
|
|
1249
|
+
* and its current tip SHA (or `null` if the writer ref is missing)
|
|
1250
|
+
* @throws {Error} If reading refs or commit metadata fails
|
|
1251
|
+
*/
|
|
1252
|
+
async discoverTicks() {
|
|
1253
|
+
const writerIds = await this.discoverWriters();
|
|
1254
|
+
/** @type {Set<number>} */
|
|
1255
|
+
const globalTickSet = new Set();
|
|
1256
|
+
const perWriter = new Map();
|
|
1257
|
+
|
|
1258
|
+
for (const writerId of writerIds) {
|
|
1259
|
+
const writerRef = buildWriterRef(this._graphName, writerId);
|
|
1260
|
+
const tipSha = await this._persistence.readRef(writerRef);
|
|
1261
|
+
const writerTicks = [];
|
|
1262
|
+
/** @type {Record<number, string>} */
|
|
1263
|
+
const tickShas = {};
|
|
1264
|
+
|
|
1265
|
+
if (tipSha) {
|
|
1266
|
+
let currentSha = tipSha;
|
|
1267
|
+
let lastLamport = Infinity;
|
|
1268
|
+
|
|
1269
|
+
while (currentSha) {
|
|
1270
|
+
const nodeInfo = await this._persistence.getNodeInfo(currentSha);
|
|
1271
|
+
const kind = detectMessageKind(nodeInfo.message);
|
|
1272
|
+
if (kind !== 'patch') {
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
1277
|
+
globalTickSet.add(patchMeta.lamport);
|
|
1278
|
+
writerTicks.push(patchMeta.lamport);
|
|
1279
|
+
tickShas[patchMeta.lamport] = currentSha;
|
|
1280
|
+
|
|
1281
|
+
// Check monotonic invariant (walking newest→oldest, lamport should decrease)
|
|
1282
|
+
if (patchMeta.lamport > lastLamport && this._logger) {
|
|
1283
|
+
this._logger.warn(`[warp] non-monotonic lamport for writer ${writerId}: ${patchMeta.lamport} > ${lastLamport}`);
|
|
1284
|
+
}
|
|
1285
|
+
lastLamport = patchMeta.lamport;
|
|
1286
|
+
|
|
1287
|
+
if (nodeInfo.parents && nodeInfo.parents.length > 0) {
|
|
1288
|
+
currentSha = nodeInfo.parents[0];
|
|
1289
|
+
} else {
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
perWriter.set(writerId, {
|
|
1296
|
+
ticks: writerTicks.reverse(),
|
|
1297
|
+
tipSha: tipSha || null,
|
|
1298
|
+
tickShas,
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const ticks = [...globalTickSet].sort((a, b) => a - b);
|
|
1303
|
+
const maxTick = ticks.length > 0 ? ticks[ticks.length - 1] : 0;
|
|
1304
|
+
|
|
1305
|
+
return { ticks, maxTick, perWriter };
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1013
1308
|
// ============================================================================
|
|
1014
1309
|
// Schema Migration Support
|
|
1015
1310
|
// ============================================================================
|
|
@@ -1042,7 +1337,7 @@ export default class WarpGraph {
|
|
|
1042
1337
|
/**
|
|
1043
1338
|
* Loads the latest checkpoint for this graph.
|
|
1044
1339
|
*
|
|
1045
|
-
* @returns {Promise<{state:
|
|
1340
|
+
* @returns {Promise<{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number, provenanceIndex?: import('./services/ProvenanceIndex.js').ProvenanceIndex}|null>} The checkpoint or null
|
|
1046
1341
|
* @private
|
|
1047
1342
|
*/
|
|
1048
1343
|
async _loadLatestCheckpoint() {
|
|
@@ -1084,7 +1379,7 @@ export default class WarpGraph {
|
|
|
1084
1379
|
if (kind === 'patch') {
|
|
1085
1380
|
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
1086
1381
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
1087
|
-
const patch = this._codec.decode(patchBuffer);
|
|
1382
|
+
const patch = /** @type {{schema?: number}} */ (this._codec.decode(patchBuffer));
|
|
1088
1383
|
|
|
1089
1384
|
// If any patch has schema:1, we have v1 history
|
|
1090
1385
|
if (patch.schema === 1 || patch.schema === undefined) {
|
|
@@ -1099,8 +1394,8 @@ export default class WarpGraph {
|
|
|
1099
1394
|
/**
|
|
1100
1395
|
* Loads patches since a checkpoint for incremental materialization.
|
|
1101
1396
|
*
|
|
1102
|
-
* @param {{state:
|
|
1103
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
1397
|
+
* @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to start from
|
|
1398
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Patches since checkpoint
|
|
1104
1399
|
* @private
|
|
1105
1400
|
*/
|
|
1106
1401
|
async _loadPatchesSince(checkpoint) {
|
|
@@ -1182,7 +1477,7 @@ export default class WarpGraph {
|
|
|
1182
1477
|
*
|
|
1183
1478
|
* @param {string} writerId - The writer ID for this patch
|
|
1184
1479
|
* @param {string} incomingSha - The incoming patch commit SHA
|
|
1185
|
-
* @param {{state:
|
|
1480
|
+
* @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to validate against
|
|
1186
1481
|
* @returns {Promise<void>}
|
|
1187
1482
|
* @throws {Error} If patch is behind/same as checkpoint frontier (backfill rejected)
|
|
1188
1483
|
* @throws {Error} If patch does not extend checkpoint head (writer fork detected)
|
|
@@ -1230,18 +1525,19 @@ export default class WarpGraph {
|
|
|
1230
1525
|
_maybeRunGC(state) {
|
|
1231
1526
|
try {
|
|
1232
1527
|
const metrics = collectGCMetrics(state);
|
|
1528
|
+
/** @type {import('./services/GCPolicy.js').GCInputMetrics} */
|
|
1233
1529
|
const inputMetrics = {
|
|
1234
1530
|
...metrics,
|
|
1235
1531
|
patchesSinceCompaction: this._patchesSinceGC,
|
|
1236
1532
|
timeSinceCompaction: Date.now() - this._lastGCTime,
|
|
1237
1533
|
};
|
|
1238
|
-
const { shouldRun, reasons } = shouldRunGC(inputMetrics, this._gcPolicy);
|
|
1534
|
+
const { shouldRun, reasons } = shouldRunGC(inputMetrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
|
|
1239
1535
|
|
|
1240
1536
|
if (!shouldRun) {
|
|
1241
1537
|
return;
|
|
1242
1538
|
}
|
|
1243
1539
|
|
|
1244
|
-
if (this._gcPolicy.enabled) {
|
|
1540
|
+
if (/** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy).enabled) {
|
|
1245
1541
|
const appliedVV = computeAppliedVV(state);
|
|
1246
1542
|
const result = executeGC(state, appliedVV);
|
|
1247
1543
|
this._lastGCTime = Date.now();
|
|
@@ -1280,11 +1576,15 @@ export default class WarpGraph {
|
|
|
1280
1576
|
return { ran: false, result: null, reasons: [] };
|
|
1281
1577
|
}
|
|
1282
1578
|
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
metrics
|
|
1579
|
+
const rawMetrics = collectGCMetrics(this._cachedState);
|
|
1580
|
+
/** @type {import('./services/GCPolicy.js').GCInputMetrics} */
|
|
1581
|
+
const metrics = {
|
|
1582
|
+
...rawMetrics,
|
|
1583
|
+
patchesSinceCompaction: this._patchesSinceGC,
|
|
1584
|
+
timeSinceCompaction: this._lastGCTime > 0 ? Date.now() - this._lastGCTime : 0,
|
|
1585
|
+
};
|
|
1286
1586
|
|
|
1287
|
-
const { shouldRun, reasons } = shouldRunGC(metrics, this._gcPolicy);
|
|
1587
|
+
const { shouldRun, reasons } = shouldRunGC(metrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
|
|
1288
1588
|
|
|
1289
1589
|
if (!shouldRun) {
|
|
1290
1590
|
return { ran: false, result: null, reasons: [] };
|
|
@@ -1331,7 +1631,7 @@ export default class WarpGraph {
|
|
|
1331
1631
|
|
|
1332
1632
|
return result;
|
|
1333
1633
|
} catch (err) {
|
|
1334
|
-
this._logTiming('runGC', t0, { error: err });
|
|
1634
|
+
this._logTiming('runGC', t0, { error: /** @type {Error} */ (err) });
|
|
1335
1635
|
throw err;
|
|
1336
1636
|
}
|
|
1337
1637
|
}
|
|
@@ -1353,10 +1653,15 @@ export default class WarpGraph {
|
|
|
1353
1653
|
return null;
|
|
1354
1654
|
}
|
|
1355
1655
|
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1656
|
+
const rawMetrics = collectGCMetrics(this._cachedState);
|
|
1657
|
+
return {
|
|
1658
|
+
...rawMetrics,
|
|
1659
|
+
nodeCount: rawMetrics.nodeLiveDots,
|
|
1660
|
+
edgeCount: rawMetrics.edgeLiveDots,
|
|
1661
|
+
tombstoneCount: rawMetrics.totalTombstones,
|
|
1662
|
+
patchesSinceCompaction: this._patchesSinceGC,
|
|
1663
|
+
lastCompactionTime: this._lastGCTime,
|
|
1664
|
+
};
|
|
1360
1665
|
}
|
|
1361
1666
|
|
|
1362
1667
|
/**
|
|
@@ -1439,6 +1744,7 @@ export default class WarpGraph {
|
|
|
1439
1744
|
*/
|
|
1440
1745
|
async status() {
|
|
1441
1746
|
// Determine cachedState
|
|
1747
|
+
/** @type {'fresh' | 'stale' | 'none'} */
|
|
1442
1748
|
let cachedState;
|
|
1443
1749
|
if (this._cachedState === null) {
|
|
1444
1750
|
cachedState = 'none';
|
|
@@ -1488,7 +1794,7 @@ export default class WarpGraph {
|
|
|
1488
1794
|
* One handler's error does not prevent other handlers from being called.
|
|
1489
1795
|
*
|
|
1490
1796
|
* @param {Object} options - Subscription options
|
|
1491
|
-
* @param {(diff: import('./services/StateDiff.js').
|
|
1797
|
+
* @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with diff when graph changes
|
|
1492
1798
|
* @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
|
|
1493
1799
|
* @param {boolean} [options.replay=false] - If true, immediately fires onChange with initial state diff
|
|
1494
1800
|
* @returns {{unsubscribe: () => void}} Subscription handle
|
|
@@ -1531,7 +1837,7 @@ export default class WarpGraph {
|
|
|
1531
1837
|
} catch (err) {
|
|
1532
1838
|
if (onError) {
|
|
1533
1839
|
try {
|
|
1534
|
-
onError(err);
|
|
1840
|
+
onError(/** @type {Error} */ (err));
|
|
1535
1841
|
} catch {
|
|
1536
1842
|
// onError itself threw — swallow to prevent cascade
|
|
1537
1843
|
}
|
|
@@ -1568,7 +1874,7 @@ export default class WarpGraph {
|
|
|
1568
1874
|
*
|
|
1569
1875
|
* @param {string} pattern - Glob pattern (e.g., 'user:*', 'order:123', '*')
|
|
1570
1876
|
* @param {Object} options - Watch options
|
|
1571
|
-
* @param {(diff: import('./services/StateDiff.js').
|
|
1877
|
+
* @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with filtered diff when matching changes occur
|
|
1572
1878
|
* @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
|
|
1573
1879
|
* @param {number} [options.poll] - Poll interval in ms (min 1000); checks frontier and auto-materializes
|
|
1574
1880
|
* @returns {{unsubscribe: () => void}} Subscription handle
|
|
@@ -1609,31 +1915,32 @@ export default class WarpGraph {
|
|
|
1609
1915
|
|
|
1610
1916
|
// Pattern matching: same logic as QueryBuilder.match()
|
|
1611
1917
|
// Pre-compile pattern matcher once for performance
|
|
1918
|
+
/** @type {(nodeId: string) => boolean} */
|
|
1612
1919
|
let matchesPattern;
|
|
1613
1920
|
if (pattern === '*') {
|
|
1614
1921
|
matchesPattern = () => true;
|
|
1615
1922
|
} else if (pattern.includes('*')) {
|
|
1616
1923
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
1617
1924
|
const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
|
|
1618
|
-
matchesPattern = (nodeId) => regex.test(nodeId);
|
|
1925
|
+
matchesPattern = (/** @type {string} */ nodeId) => regex.test(nodeId);
|
|
1619
1926
|
} else {
|
|
1620
|
-
matchesPattern = (nodeId) => nodeId === pattern;
|
|
1927
|
+
matchesPattern = (/** @type {string} */ nodeId) => nodeId === pattern;
|
|
1621
1928
|
}
|
|
1622
1929
|
|
|
1623
1930
|
// Filtered onChange that only passes matching changes
|
|
1624
|
-
const filteredOnChange = (diff) => {
|
|
1931
|
+
const filteredOnChange = (/** @type {import('./services/StateDiff.js').StateDiffResult} */ diff) => {
|
|
1625
1932
|
const filteredDiff = {
|
|
1626
1933
|
nodes: {
|
|
1627
1934
|
added: diff.nodes.added.filter(matchesPattern),
|
|
1628
1935
|
removed: diff.nodes.removed.filter(matchesPattern),
|
|
1629
1936
|
},
|
|
1630
1937
|
edges: {
|
|
1631
|
-
added: diff.edges.added.filter(e => matchesPattern(e.from) || matchesPattern(e.to)),
|
|
1632
|
-
removed: diff.edges.removed.filter(e => matchesPattern(e.from) || matchesPattern(e.to)),
|
|
1938
|
+
added: diff.edges.added.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
|
|
1939
|
+
removed: diff.edges.removed.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
|
|
1633
1940
|
},
|
|
1634
1941
|
props: {
|
|
1635
|
-
set: diff.props.set.filter(p => matchesPattern(p.nodeId)),
|
|
1636
|
-
removed: diff.props.removed.filter(p => matchesPattern(p.nodeId)),
|
|
1942
|
+
set: diff.props.set.filter((/** @type {import('./services/StateDiff.js').PropSet} */ p) => matchesPattern(p.nodeId)),
|
|
1943
|
+
removed: diff.props.removed.filter((/** @type {import('./services/StateDiff.js').PropRemoved} */ p) => matchesPattern(p.nodeId)),
|
|
1637
1944
|
},
|
|
1638
1945
|
};
|
|
1639
1946
|
|
|
@@ -1655,6 +1962,7 @@ export default class WarpGraph {
|
|
|
1655
1962
|
const subscription = this.subscribe({ onChange: filteredOnChange, onError });
|
|
1656
1963
|
|
|
1657
1964
|
// Polling: periodically check frontier and auto-materialize if changed
|
|
1965
|
+
/** @type {ReturnType<typeof setInterval>|null} */
|
|
1658
1966
|
let pollIntervalId = null;
|
|
1659
1967
|
let pollInFlight = false;
|
|
1660
1968
|
if (poll) {
|
|
@@ -1736,7 +2044,7 @@ export default class WarpGraph {
|
|
|
1736
2044
|
* Creates a sync request to send to a remote peer.
|
|
1737
2045
|
* The request contains the local frontier for comparison.
|
|
1738
2046
|
*
|
|
1739
|
-
* @returns {Promise<
|
|
2047
|
+
* @returns {Promise<import('./services/SyncProtocol.js').SyncRequest>} The sync request
|
|
1740
2048
|
* @throws {Error} If listing refs fails
|
|
1741
2049
|
*
|
|
1742
2050
|
* @example
|
|
@@ -1751,8 +2059,8 @@ export default class WarpGraph {
|
|
|
1751
2059
|
/**
|
|
1752
2060
|
* Processes an incoming sync request and returns patches the requester needs.
|
|
1753
2061
|
*
|
|
1754
|
-
* @param {
|
|
1755
|
-
* @returns {Promise<
|
|
2062
|
+
* @param {import('./services/SyncProtocol.js').SyncRequest} request - The incoming sync request
|
|
2063
|
+
* @returns {Promise<import('./services/SyncProtocol.js').SyncResponse>} The sync response
|
|
1756
2064
|
* @throws {Error} If listing refs or reading patches fails
|
|
1757
2065
|
*
|
|
1758
2066
|
* @example
|
|
@@ -1765,7 +2073,7 @@ export default class WarpGraph {
|
|
|
1765
2073
|
return await processSyncRequest(
|
|
1766
2074
|
request,
|
|
1767
2075
|
localFrontier,
|
|
1768
|
-
this._persistence,
|
|
2076
|
+
/** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
1769
2077
|
this._graphName,
|
|
1770
2078
|
{ codec: this._codec }
|
|
1771
2079
|
);
|
|
@@ -1777,8 +2085,8 @@ export default class WarpGraph {
|
|
|
1777
2085
|
*
|
|
1778
2086
|
* **Requires a cached state.**
|
|
1779
2087
|
*
|
|
1780
|
-
* @param {
|
|
1781
|
-
* @returns {{state:
|
|
2088
|
+
* @param {import('./services/SyncProtocol.js').SyncResponse} response - The sync response
|
|
2089
|
+
* @returns {{state: import('./services/JoinReducer.js').WarpStateV5, applied: number}} Result with updated state
|
|
1782
2090
|
* @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
|
|
1783
2091
|
*
|
|
1784
2092
|
* @example
|
|
@@ -1793,8 +2101,8 @@ export default class WarpGraph {
|
|
|
1793
2101
|
});
|
|
1794
2102
|
}
|
|
1795
2103
|
|
|
1796
|
-
const currentFrontier = this._cachedState.observedFrontier;
|
|
1797
|
-
const result = applySyncResponse(response, this._cachedState, currentFrontier);
|
|
2104
|
+
const currentFrontier = /** @type {any} */ (this._cachedState.observedFrontier); // TODO(ts-cleanup): narrow port type
|
|
2105
|
+
const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, applied: number}} */ (applySyncResponse(response, this._cachedState, currentFrontier));
|
|
1798
2106
|
|
|
1799
2107
|
// Update cached state
|
|
1800
2108
|
this._cachedState = result.state;
|
|
@@ -1865,7 +2173,7 @@ export default class WarpGraph {
|
|
|
1865
2173
|
let targetUrl = null;
|
|
1866
2174
|
if (!isDirectPeer) {
|
|
1867
2175
|
try {
|
|
1868
|
-
targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(remote);
|
|
2176
|
+
targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(/** @type {string} */ (remote));
|
|
1869
2177
|
} catch {
|
|
1870
2178
|
throw new SyncError('Invalid remote URL', {
|
|
1871
2179
|
code: 'E_SYNC_REMOTE_URL',
|
|
@@ -1890,13 +2198,13 @@ export default class WarpGraph {
|
|
|
1890
2198
|
}
|
|
1891
2199
|
|
|
1892
2200
|
let attempt = 0;
|
|
1893
|
-
const emit = (type, payload = {}) => {
|
|
2201
|
+
const emit = (/** @type {string} */ type, /** @type {Record<string, any>} */ payload = {}) => {
|
|
1894
2202
|
if (typeof onStatus === 'function') {
|
|
1895
|
-
onStatus({ type, attempt, ...payload });
|
|
2203
|
+
onStatus(/** @type {any} */ ({ type, attempt, ...payload })); // TODO(ts-cleanup): type sync protocol
|
|
1896
2204
|
}
|
|
1897
2205
|
};
|
|
1898
2206
|
|
|
1899
|
-
const shouldRetry = (err) => {
|
|
2207
|
+
const shouldRetry = (/** @type {any} */ err) => { // TODO(ts-cleanup): type error
|
|
1900
2208
|
if (isDirectPeer) { return false; }
|
|
1901
2209
|
if (err instanceof SyncError) {
|
|
1902
2210
|
return ['E_SYNC_REMOTE', 'E_SYNC_TIMEOUT', 'E_SYNC_NETWORK'].includes(err.code);
|
|
@@ -1926,7 +2234,7 @@ export default class WarpGraph {
|
|
|
1926
2234
|
const combinedSignal = signal
|
|
1927
2235
|
? AbortSignal.any([timeoutSignal, signal])
|
|
1928
2236
|
: timeoutSignal;
|
|
1929
|
-
return fetch(targetUrl.toString(), {
|
|
2237
|
+
return fetch(/** @type {URL} */ (targetUrl).toString(), {
|
|
1930
2238
|
method: 'POST',
|
|
1931
2239
|
headers: {
|
|
1932
2240
|
'content-type': 'application/json',
|
|
@@ -1937,7 +2245,7 @@ export default class WarpGraph {
|
|
|
1937
2245
|
});
|
|
1938
2246
|
});
|
|
1939
2247
|
} catch (err) {
|
|
1940
|
-
if (err?.name === 'AbortError') {
|
|
2248
|
+
if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
|
|
1941
2249
|
throw new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
1942
2250
|
}
|
|
1943
2251
|
if (err instanceof TimeoutError) {
|
|
@@ -1948,7 +2256,7 @@ export default class WarpGraph {
|
|
|
1948
2256
|
}
|
|
1949
2257
|
throw new SyncError('Network error', {
|
|
1950
2258
|
code: 'E_SYNC_NETWORK',
|
|
1951
|
-
context: { message: err?.message },
|
|
2259
|
+
context: { message: /** @type {any} */ (err)?.message }, // TODO(ts-cleanup): type error
|
|
1952
2260
|
});
|
|
1953
2261
|
}
|
|
1954
2262
|
|
|
@@ -2009,9 +2317,9 @@ export default class WarpGraph {
|
|
|
2009
2317
|
jitter: 'decorrelated',
|
|
2010
2318
|
signal,
|
|
2011
2319
|
shouldRetry,
|
|
2012
|
-
onRetry: (error, attemptNumber, delayMs) => {
|
|
2320
|
+
onRetry: (/** @type {Error} */ error, /** @type {number} */ attemptNumber, /** @type {number} */ delayMs) => {
|
|
2013
2321
|
if (typeof onStatus === 'function') {
|
|
2014
|
-
onStatus({ type: 'retrying', attempt: attemptNumber, delayMs, error });
|
|
2322
|
+
onStatus(/** @type {any} */ ({ type: 'retrying', attempt: attemptNumber, delayMs, error })); // TODO(ts-cleanup): type sync protocol
|
|
2015
2323
|
}
|
|
2016
2324
|
},
|
|
2017
2325
|
});
|
|
@@ -2020,12 +2328,12 @@ export default class WarpGraph {
|
|
|
2020
2328
|
|
|
2021
2329
|
if (materializeAfterSync) {
|
|
2022
2330
|
if (!this._cachedState) { await this.materialize(); }
|
|
2023
|
-
return { ...syncResult, state: this._cachedState };
|
|
2331
|
+
return { ...syncResult, state: /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState) };
|
|
2024
2332
|
}
|
|
2025
2333
|
return syncResult;
|
|
2026
2334
|
} catch (err) {
|
|
2027
|
-
this._logTiming('syncWith', t0, { error: err });
|
|
2028
|
-
if (err?.name === 'AbortError') {
|
|
2335
|
+
this._logTiming('syncWith', t0, { error: /** @type {Error} */ (err) });
|
|
2336
|
+
if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
|
|
2029
2337
|
const abortedError = new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
2030
2338
|
if (typeof onStatus === 'function') {
|
|
2031
2339
|
onStatus({ type: 'failed', attempt, error: abortedError });
|
|
@@ -2033,14 +2341,14 @@ export default class WarpGraph {
|
|
|
2033
2341
|
throw abortedError;
|
|
2034
2342
|
}
|
|
2035
2343
|
if (err instanceof RetryExhaustedError) {
|
|
2036
|
-
const cause = err.cause || err;
|
|
2344
|
+
const cause = /** @type {Error} */ (err.cause || err);
|
|
2037
2345
|
if (typeof onStatus === 'function') {
|
|
2038
2346
|
onStatus({ type: 'failed', attempt: err.attempts, error: cause });
|
|
2039
2347
|
}
|
|
2040
2348
|
throw cause;
|
|
2041
2349
|
}
|
|
2042
2350
|
if (typeof onStatus === 'function') {
|
|
2043
|
-
onStatus({ type: 'failed', attempt, error: err });
|
|
2351
|
+
onStatus({ type: 'failed', attempt, error: /** @type {Error} */ (err) });
|
|
2044
2352
|
}
|
|
2045
2353
|
throw err;
|
|
2046
2354
|
}
|
|
@@ -2059,7 +2367,7 @@ export default class WarpGraph {
|
|
|
2059
2367
|
* @throws {Error} If port is not a number
|
|
2060
2368
|
* @throws {Error} If httpPort adapter is not provided
|
|
2061
2369
|
*/
|
|
2062
|
-
async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort } = {}) {
|
|
2370
|
+
async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort } = /** @type {any} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
2063
2371
|
if (typeof port !== 'number') {
|
|
2064
2372
|
throw new Error('serve() requires a numeric port');
|
|
2065
2373
|
}
|
|
@@ -2104,8 +2412,8 @@ export default class WarpGraph {
|
|
|
2104
2412
|
*/
|
|
2105
2413
|
async writer(writerId) {
|
|
2106
2414
|
// Build config adapters for resolveWriterId
|
|
2107
|
-
const configGet = async (key) => await this._persistence.configGet(key);
|
|
2108
|
-
const configSet = async (key, value) => await this._persistence.configSet(key, value);
|
|
2415
|
+
const configGet = async (/** @type {string} */ key) => await this._persistence.configGet(key);
|
|
2416
|
+
const configSet = async (/** @type {string} */ key, /** @type {string} */ value) => await this._persistence.configSet(key, value);
|
|
2109
2417
|
|
|
2110
2418
|
// Resolve the writer ID
|
|
2111
2419
|
const resolvedWriterId = await resolveWriterId({
|
|
@@ -2116,13 +2424,13 @@ export default class WarpGraph {
|
|
|
2116
2424
|
});
|
|
2117
2425
|
|
|
2118
2426
|
return new Writer({
|
|
2119
|
-
persistence: this._persistence,
|
|
2427
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
2120
2428
|
graphName: this._graphName,
|
|
2121
2429
|
writerId: resolvedWriterId,
|
|
2122
2430
|
versionVector: this._versionVector,
|
|
2123
|
-
getCurrentState: () => this._cachedState,
|
|
2431
|
+
getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
|
|
2124
2432
|
onDeleteWithData: this._onDeleteWithData,
|
|
2125
|
-
onCommitSuccess: (opts) => this._onPatchCommitted(resolvedWriterId, opts),
|
|
2433
|
+
onCommitSuccess: (/** @type {any} */ opts) => this._onPatchCommitted(resolvedWriterId, opts), // TODO(ts-cleanup): type sync protocol
|
|
2126
2434
|
codec: this._codec,
|
|
2127
2435
|
});
|
|
2128
2436
|
}
|
|
@@ -2170,13 +2478,13 @@ export default class WarpGraph {
|
|
|
2170
2478
|
}
|
|
2171
2479
|
|
|
2172
2480
|
return new Writer({
|
|
2173
|
-
persistence: this._persistence,
|
|
2481
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
2174
2482
|
graphName: this._graphName,
|
|
2175
2483
|
writerId: freshWriterId,
|
|
2176
2484
|
versionVector: this._versionVector,
|
|
2177
|
-
getCurrentState: () => this._cachedState,
|
|
2485
|
+
getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
|
|
2178
2486
|
onDeleteWithData: this._onDeleteWithData,
|
|
2179
|
-
onCommitSuccess: (commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts),
|
|
2487
|
+
onCommitSuccess: (/** @type {any} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts), // TODO(ts-cleanup): type sync protocol
|
|
2180
2488
|
codec: this._codec,
|
|
2181
2489
|
});
|
|
2182
2490
|
}
|
|
@@ -2331,7 +2639,8 @@ export default class WarpGraph {
|
|
|
2331
2639
|
*/
|
|
2332
2640
|
async hasNode(nodeId) {
|
|
2333
2641
|
await this._ensureFreshState();
|
|
2334
|
-
|
|
2642
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2643
|
+
return orsetContains(s.nodeAlive, nodeId);
|
|
2335
2644
|
}
|
|
2336
2645
|
|
|
2337
2646
|
/**
|
|
@@ -2356,15 +2665,16 @@ export default class WarpGraph {
|
|
|
2356
2665
|
*/
|
|
2357
2666
|
async getNodeProps(nodeId) {
|
|
2358
2667
|
await this._ensureFreshState();
|
|
2668
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2359
2669
|
|
|
2360
2670
|
// Check if node exists
|
|
2361
|
-
if (!orsetContains(
|
|
2671
|
+
if (!orsetContains(s.nodeAlive, nodeId)) {
|
|
2362
2672
|
return null;
|
|
2363
2673
|
}
|
|
2364
2674
|
|
|
2365
2675
|
// Collect all properties for this node
|
|
2366
2676
|
const props = new Map();
|
|
2367
|
-
for (const [propKey, register] of
|
|
2677
|
+
for (const [propKey, register] of s.prop) {
|
|
2368
2678
|
const decoded = decodePropKey(propKey);
|
|
2369
2679
|
if (decoded.nodeId === nodeId) {
|
|
2370
2680
|
props.set(decoded.propKey, register.value);
|
|
@@ -2398,26 +2708,28 @@ export default class WarpGraph {
|
|
|
2398
2708
|
*/
|
|
2399
2709
|
async getEdgeProps(from, to, label) {
|
|
2400
2710
|
await this._ensureFreshState();
|
|
2711
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2401
2712
|
|
|
2402
2713
|
// Check if edge exists
|
|
2403
2714
|
const edgeKey = encodeEdgeKey(from, to, label);
|
|
2404
|
-
if (!orsetContains(
|
|
2715
|
+
if (!orsetContains(s.edgeAlive, edgeKey)) {
|
|
2405
2716
|
return null;
|
|
2406
2717
|
}
|
|
2407
2718
|
|
|
2408
2719
|
// Check node liveness for both endpoints
|
|
2409
|
-
if (!orsetContains(
|
|
2410
|
-
!orsetContains(
|
|
2720
|
+
if (!orsetContains(s.nodeAlive, from) ||
|
|
2721
|
+
!orsetContains(s.nodeAlive, to)) {
|
|
2411
2722
|
return null;
|
|
2412
2723
|
}
|
|
2413
2724
|
|
|
2414
2725
|
// Determine the birth EventId for clean-slate filtering
|
|
2415
|
-
const birthEvent =
|
|
2726
|
+
const birthEvent = s.edgeBirthEvent?.get(edgeKey);
|
|
2416
2727
|
|
|
2417
2728
|
// Collect all properties for this edge, filtering out stale props
|
|
2418
2729
|
// (props set before the edge's most recent re-add)
|
|
2730
|
+
/** @type {Record<string, any>} */
|
|
2419
2731
|
const props = {};
|
|
2420
|
-
for (const [propKey, register] of
|
|
2732
|
+
for (const [propKey, register] of s.prop) {
|
|
2421
2733
|
if (!isEdgePropKey(propKey)) {
|
|
2422
2734
|
continue;
|
|
2423
2735
|
}
|
|
@@ -2459,11 +2771,13 @@ export default class WarpGraph {
|
|
|
2459
2771
|
*/
|
|
2460
2772
|
async neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
|
|
2461
2773
|
await this._ensureFreshState();
|
|
2774
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2462
2775
|
|
|
2776
|
+
/** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
|
|
2463
2777
|
const neighbors = [];
|
|
2464
2778
|
|
|
2465
2779
|
// Iterate over all visible edges
|
|
2466
|
-
for (const edgeKey of orsetElements(
|
|
2780
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
2467
2781
|
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
2468
2782
|
|
|
2469
2783
|
// Filter by label if specified
|
|
@@ -2474,15 +2788,15 @@ export default class WarpGraph {
|
|
|
2474
2788
|
// Check edge direction and collect neighbors
|
|
2475
2789
|
if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
|
|
2476
2790
|
// Ensure target node is visible
|
|
2477
|
-
if (orsetContains(
|
|
2478
|
-
neighbors.push({ nodeId: to, label, direction: 'outgoing' });
|
|
2791
|
+
if (orsetContains(s.nodeAlive, to)) {
|
|
2792
|
+
neighbors.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
|
|
2479
2793
|
}
|
|
2480
2794
|
}
|
|
2481
2795
|
|
|
2482
2796
|
if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
|
|
2483
2797
|
// Ensure source node is visible
|
|
2484
|
-
if (orsetContains(
|
|
2485
|
-
neighbors.push({ nodeId: from, label, direction: 'incoming' });
|
|
2798
|
+
if (orsetContains(s.nodeAlive, from)) {
|
|
2799
|
+
neighbors.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
|
|
2486
2800
|
}
|
|
2487
2801
|
}
|
|
2488
2802
|
}
|
|
@@ -2507,7 +2821,8 @@ export default class WarpGraph {
|
|
|
2507
2821
|
*/
|
|
2508
2822
|
async getNodes() {
|
|
2509
2823
|
await this._ensureFreshState();
|
|
2510
|
-
|
|
2824
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2825
|
+
return [...orsetElements(s.nodeAlive)];
|
|
2511
2826
|
}
|
|
2512
2827
|
|
|
2513
2828
|
/**
|
|
@@ -2530,12 +2845,13 @@ export default class WarpGraph {
|
|
|
2530
2845
|
*/
|
|
2531
2846
|
async getEdges() {
|
|
2532
2847
|
await this._ensureFreshState();
|
|
2848
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2533
2849
|
|
|
2534
2850
|
// Pre-collect edge props into a lookup: "from\0to\0label" → {propKey: value}
|
|
2535
2851
|
// Filters out stale props using full EventId ordering via compareEventIds
|
|
2536
2852
|
// against the edge's birth EventId (clean-slate semantics on re-add)
|
|
2537
2853
|
const edgePropsByKey = new Map();
|
|
2538
|
-
for (const [propKey, register] of
|
|
2854
|
+
for (const [propKey, register] of s.prop) {
|
|
2539
2855
|
if (!isEdgePropKey(propKey)) {
|
|
2540
2856
|
continue;
|
|
2541
2857
|
}
|
|
@@ -2543,7 +2859,7 @@ export default class WarpGraph {
|
|
|
2543
2859
|
const ek = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
|
|
2544
2860
|
|
|
2545
2861
|
// Clean-slate filter: skip props from before the edge's current incarnation
|
|
2546
|
-
const birthEvent =
|
|
2862
|
+
const birthEvent = s.edgeBirthEvent?.get(ek);
|
|
2547
2863
|
if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
|
|
2548
2864
|
continue;
|
|
2549
2865
|
}
|
|
@@ -2557,11 +2873,11 @@ export default class WarpGraph {
|
|
|
2557
2873
|
}
|
|
2558
2874
|
|
|
2559
2875
|
const edges = [];
|
|
2560
|
-
for (const edgeKey of orsetElements(
|
|
2876
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
2561
2877
|
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
2562
2878
|
// Only include edges where both endpoints are visible
|
|
2563
|
-
if (orsetContains(
|
|
2564
|
-
orsetContains(
|
|
2879
|
+
if (orsetContains(s.nodeAlive, from) &&
|
|
2880
|
+
orsetContains(s.nodeAlive, to)) {
|
|
2565
2881
|
const props = edgePropsByKey.get(edgeKey) || {};
|
|
2566
2882
|
edges.push({ from, to, label, props });
|
|
2567
2883
|
}
|
|
@@ -2580,7 +2896,8 @@ export default class WarpGraph {
|
|
|
2580
2896
|
*/
|
|
2581
2897
|
async getPropertyCount() {
|
|
2582
2898
|
await this._ensureFreshState();
|
|
2583
|
-
|
|
2899
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2900
|
+
return s.prop.size;
|
|
2584
2901
|
}
|
|
2585
2902
|
|
|
2586
2903
|
// ============================================================================
|
|
@@ -2699,9 +3016,9 @@ export default class WarpGraph {
|
|
|
2699
3016
|
try {
|
|
2700
3017
|
validateGraphName(resolvedForkName);
|
|
2701
3018
|
} catch (err) {
|
|
2702
|
-
throw new ForkError(`Invalid fork name: ${err.message}`, {
|
|
3019
|
+
throw new ForkError(`Invalid fork name: ${/** @type {Error} */ (err).message}`, {
|
|
2703
3020
|
code: 'E_FORK_NAME_INVALID',
|
|
2704
|
-
context: { forkName: resolvedForkName, originalError: err.message },
|
|
3021
|
+
context: { forkName: resolvedForkName, originalError: /** @type {Error} */ (err).message },
|
|
2705
3022
|
});
|
|
2706
3023
|
}
|
|
2707
3024
|
|
|
@@ -2720,9 +3037,9 @@ export default class WarpGraph {
|
|
|
2720
3037
|
try {
|
|
2721
3038
|
validateWriterId(resolvedForkWriterId);
|
|
2722
3039
|
} catch (err) {
|
|
2723
|
-
throw new ForkError(`Invalid fork writer ID: ${err.message}`, {
|
|
3040
|
+
throw new ForkError(`Invalid fork writer ID: ${/** @type {Error} */ (err).message}`, {
|
|
2724
3041
|
code: 'E_FORK_WRITER_ID_INVALID',
|
|
2725
|
-
context: { forkWriterId: resolvedForkWriterId, originalError: err.message },
|
|
3042
|
+
context: { forkWriterId: resolvedForkWriterId, originalError: /** @type {Error} */ (err).message },
|
|
2726
3043
|
});
|
|
2727
3044
|
}
|
|
2728
3045
|
|
|
@@ -2737,10 +3054,10 @@ export default class WarpGraph {
|
|
|
2737
3054
|
writerId: resolvedForkWriterId,
|
|
2738
3055
|
gcPolicy: this._gcPolicy,
|
|
2739
3056
|
adjacencyCacheSize: this._adjacencyCache?.maxSize ?? DEFAULT_ADJACENCY_CACHE_SIZE,
|
|
2740
|
-
checkpointPolicy: this._checkpointPolicy,
|
|
3057
|
+
checkpointPolicy: this._checkpointPolicy || undefined,
|
|
2741
3058
|
autoMaterialize: this._autoMaterialize,
|
|
2742
3059
|
onDeleteWithData: this._onDeleteWithData,
|
|
2743
|
-
logger: this._logger,
|
|
3060
|
+
logger: this._logger || undefined,
|
|
2744
3061
|
clock: this._clock,
|
|
2745
3062
|
crypto: this._crypto,
|
|
2746
3063
|
codec: this._codec,
|
|
@@ -2752,7 +3069,7 @@ export default class WarpGraph {
|
|
|
2752
3069
|
|
|
2753
3070
|
return forkGraph;
|
|
2754
3071
|
} catch (err) {
|
|
2755
|
-
this._logTiming('fork', t0, { error: err });
|
|
3072
|
+
this._logTiming('fork', t0, { error: /** @type {Error} */ (err) });
|
|
2756
3073
|
throw err;
|
|
2757
3074
|
}
|
|
2758
3075
|
}
|
|
@@ -2806,13 +3123,13 @@ export default class WarpGraph {
|
|
|
2806
3123
|
const t0 = this._clock.now();
|
|
2807
3124
|
|
|
2808
3125
|
try {
|
|
2809
|
-
const wormhole = await createWormholeImpl({
|
|
3126
|
+
const wormhole = await createWormholeImpl(/** @type {any} */ ({ // TODO(ts-cleanup): needs options type
|
|
2810
3127
|
persistence: this._persistence,
|
|
2811
3128
|
graphName: this._graphName,
|
|
2812
3129
|
fromSha,
|
|
2813
3130
|
toSha,
|
|
2814
3131
|
codec: this._codec,
|
|
2815
|
-
});
|
|
3132
|
+
}));
|
|
2816
3133
|
|
|
2817
3134
|
this._logTiming('createWormhole', t0, {
|
|
2818
3135
|
metrics: `${wormhole.patchCount} patches from=${fromSha.slice(0, 7)} to=${toSha.slice(0, 7)}`,
|
|
@@ -2820,7 +3137,7 @@ export default class WarpGraph {
|
|
|
2820
3137
|
|
|
2821
3138
|
return wormhole;
|
|
2822
3139
|
} catch (err) {
|
|
2823
|
-
this._logTiming('createWormhole', t0, { error: err });
|
|
3140
|
+
this._logTiming('createWormhole', t0, { error: /** @type {Error} */ (err) });
|
|
2824
3141
|
throw err;
|
|
2825
3142
|
}
|
|
2826
3143
|
}
|
|
@@ -2854,6 +3171,12 @@ export default class WarpGraph {
|
|
|
2854
3171
|
async patchesFor(entityId) {
|
|
2855
3172
|
await this._ensureFreshState();
|
|
2856
3173
|
|
|
3174
|
+
if (this._provenanceDegraded) {
|
|
3175
|
+
throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
|
|
3176
|
+
code: 'E_PROVENANCE_DEGRADED',
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
|
|
2857
3180
|
if (!this._provenanceIndex) {
|
|
2858
3181
|
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
2859
3182
|
code: 'E_NO_STATE',
|
|
@@ -2915,6 +3238,12 @@ export default class WarpGraph {
|
|
|
2915
3238
|
// Ensure fresh state before accessing provenance index
|
|
2916
3239
|
await this._ensureFreshState();
|
|
2917
3240
|
|
|
3241
|
+
if (this._provenanceDegraded) {
|
|
3242
|
+
throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
|
|
3243
|
+
code: 'E_PROVENANCE_DEGRADED',
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
|
|
2918
3247
|
if (!this._provenanceIndex) {
|
|
2919
3248
|
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
2920
3249
|
code: 'E_NO_STATE',
|
|
@@ -2949,7 +3278,7 @@ export default class WarpGraph {
|
|
|
2949
3278
|
this._logTiming('materializeSlice', t0, { metrics: `${sortedPatches.length} patches` });
|
|
2950
3279
|
|
|
2951
3280
|
if (collectReceipts) {
|
|
2952
|
-
const result = reduceV5(sortedPatches, undefined, { receipts: true });
|
|
3281
|
+
const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(sortedPatches, undefined, { receipts: true }));
|
|
2953
3282
|
return {
|
|
2954
3283
|
state: result.state,
|
|
2955
3284
|
patchCount: sortedPatches.length,
|
|
@@ -2963,7 +3292,7 @@ export default class WarpGraph {
|
|
|
2963
3292
|
patchCount: sortedPatches.length,
|
|
2964
3293
|
};
|
|
2965
3294
|
} catch (err) {
|
|
2966
|
-
this._logTiming('materializeSlice', t0, { error: err });
|
|
3295
|
+
this._logTiming('materializeSlice', t0, { error: /** @type {Error} */ (err) });
|
|
2967
3296
|
throw err;
|
|
2968
3297
|
}
|
|
2969
3298
|
}
|
|
@@ -3000,7 +3329,7 @@ export default class WarpGraph {
|
|
|
3000
3329
|
visited.add(entityId);
|
|
3001
3330
|
|
|
3002
3331
|
// Get all patches that affected this entity
|
|
3003
|
-
const patchShas = this._provenanceIndex.patchesFor(entityId);
|
|
3332
|
+
const patchShas = /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).patchesFor(entityId);
|
|
3004
3333
|
|
|
3005
3334
|
for (const sha of patchShas) {
|
|
3006
3335
|
if (cone.has(sha)) {
|
|
@@ -3012,8 +3341,9 @@ export default class WarpGraph {
|
|
|
3012
3341
|
cone.set(sha, patch);
|
|
3013
3342
|
|
|
3014
3343
|
// Add read dependencies to the queue
|
|
3015
|
-
|
|
3016
|
-
|
|
3344
|
+
const patchReads = /** @type {any} */ (patch)?.reads; // TODO(ts-cleanup): type patch array
|
|
3345
|
+
if (patchReads) {
|
|
3346
|
+
for (const readEntity of patchReads) {
|
|
3017
3347
|
if (!visited.has(readEntity)) {
|
|
3018
3348
|
queue.push(readEntity);
|
|
3019
3349
|
}
|
|
@@ -3025,6 +3355,23 @@ export default class WarpGraph {
|
|
|
3025
3355
|
return cone;
|
|
3026
3356
|
}
|
|
3027
3357
|
|
|
3358
|
+
/**
|
|
3359
|
+
* Loads a single patch by its SHA.
|
|
3360
|
+
*
|
|
3361
|
+
* @param {string} sha - The patch commit SHA
|
|
3362
|
+
* @returns {Promise<Object>} The decoded patch object
|
|
3363
|
+
* @throws {Error} If the commit is not a patch or loading fails
|
|
3364
|
+
*
|
|
3365
|
+
* @public
|
|
3366
|
+
* @remarks
|
|
3367
|
+
* Thin wrapper around the internal `_loadPatchBySha` helper. Exposed for
|
|
3368
|
+
* CLI/debug tooling (e.g. seek tick receipts) that needs to inspect patch
|
|
3369
|
+
* operations without re-materializing intermediate states.
|
|
3370
|
+
*/
|
|
3371
|
+
async loadPatchBySha(sha) {
|
|
3372
|
+
return await this._loadPatchBySha(sha);
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3028
3375
|
/**
|
|
3029
3376
|
* Loads a single patch by its SHA.
|
|
3030
3377
|
*
|
|
@@ -3043,7 +3390,7 @@ export default class WarpGraph {
|
|
|
3043
3390
|
|
|
3044
3391
|
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
3045
3392
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
3046
|
-
return this._codec.decode(patchBuffer);
|
|
3393
|
+
return /** @type {Object} */ (this._codec.decode(patchBuffer));
|
|
3047
3394
|
}
|
|
3048
3395
|
|
|
3049
3396
|
/**
|
|
@@ -3071,8 +3418,8 @@ export default class WarpGraph {
|
|
|
3071
3418
|
* Sort order: Lamport timestamp (ascending), then writer ID, then SHA.
|
|
3072
3419
|
* This ensures deterministic ordering regardless of discovery order.
|
|
3073
3420
|
*
|
|
3074
|
-
* @param {Array<{patch:
|
|
3075
|
-
* @returns {Array<{patch:
|
|
3421
|
+
* @param {Array<{patch: any, sha: string}>} patches - Unsorted patch entries
|
|
3422
|
+
* @returns {Array<{patch: any, sha: string}>} Sorted patch entries
|
|
3076
3423
|
* @private
|
|
3077
3424
|
*/
|
|
3078
3425
|
_sortPatchesCausally(patches) {
|