@git-stunts/git-warp 10.3.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 +6 -3
- package/bin/warp-graph.js +371 -141
- package/index.d.ts +31 -0
- package/index.js +4 -0
- package/package.json +8 -3
- package/src/domain/WarpGraph.js +263 -147
- 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 +19 -0
- 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/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 +17 -4
- 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 +92 -1
- package/src/visualization/renderers/ascii/history.js +28 -26
- 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 +15 -7
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +19 -5
- 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} */
|
|
@@ -187,6 +194,32 @@ export default class WarpGraph {
|
|
|
187
194
|
|
|
188
195
|
/** @type {Map<string, string>|null} */
|
|
189
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;
|
|
190
223
|
}
|
|
191
224
|
|
|
192
225
|
/**
|
|
@@ -227,6 +260,7 @@ export default class WarpGraph {
|
|
|
227
260
|
* @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
|
|
228
261
|
* @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
|
|
229
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)
|
|
230
264
|
* @returns {Promise<WarpGraph>} The opened graph instance
|
|
231
265
|
* @throws {Error} If graphName, writerId, checkpointPolicy, or onDeleteWithData is invalid
|
|
232
266
|
*
|
|
@@ -237,7 +271,7 @@ export default class WarpGraph {
|
|
|
237
271
|
* writerId: 'node-1'
|
|
238
272
|
* });
|
|
239
273
|
*/
|
|
240
|
-
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 }) {
|
|
241
275
|
// Validate inputs
|
|
242
276
|
validateGraphName(graphName);
|
|
243
277
|
validateWriterId(writerId);
|
|
@@ -269,7 +303,7 @@ export default class WarpGraph {
|
|
|
269
303
|
}
|
|
270
304
|
}
|
|
271
305
|
|
|
272
|
-
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 });
|
|
273
307
|
|
|
274
308
|
// Validate migration boundary
|
|
275
309
|
await graph._validateMigrationBoundary();
|
|
@@ -295,7 +329,7 @@ export default class WarpGraph {
|
|
|
295
329
|
|
|
296
330
|
/**
|
|
297
331
|
* Gets the persistence adapter.
|
|
298
|
-
* @returns {
|
|
332
|
+
* @returns {FullPersistence} The persistence adapter
|
|
299
333
|
*/
|
|
300
334
|
get persistence() {
|
|
301
335
|
return this._persistence;
|
|
@@ -337,9 +371,9 @@ export default class WarpGraph {
|
|
|
337
371
|
getCurrentState: () => this._cachedState,
|
|
338
372
|
expectedParentSha: parentSha,
|
|
339
373
|
onDeleteWithData: this._onDeleteWithData,
|
|
340
|
-
onCommitSuccess: (opts) => this._onPatchCommitted(this._writerId, opts),
|
|
374
|
+
onCommitSuccess: (/** @type {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(this._writerId, opts),
|
|
341
375
|
codec: this._codec,
|
|
342
|
-
logger: this._logger,
|
|
376
|
+
logger: this._logger || undefined,
|
|
343
377
|
});
|
|
344
378
|
}
|
|
345
379
|
|
|
@@ -348,7 +382,7 @@ export default class WarpGraph {
|
|
|
348
382
|
*
|
|
349
383
|
* @param {string} writerId - The writer ID to load patches for
|
|
350
384
|
* @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
|
|
351
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
385
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
|
|
352
386
|
*/
|
|
353
387
|
async getWriterPatches(writerId, stopAtSha = null) {
|
|
354
388
|
return await this._loadWriterPatches(writerId, stopAtSha);
|
|
@@ -399,7 +433,7 @@ export default class WarpGraph {
|
|
|
399
433
|
*
|
|
400
434
|
* @param {string} writerId - The writer ID to load patches for
|
|
401
435
|
* @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
|
|
402
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
436
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
|
|
403
437
|
* @private
|
|
404
438
|
*/
|
|
405
439
|
async _loadWriterPatches(writerId, stopAtSha = null) {
|
|
@@ -430,7 +464,7 @@ export default class WarpGraph {
|
|
|
430
464
|
|
|
431
465
|
// Read the patch blob
|
|
432
466
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
433
|
-
const patch = this._codec.decode(patchBuffer);
|
|
467
|
+
const patch = /** @type {import('./types/WarpTypesV2.js').PatchV2} */ (this._codec.decode(patchBuffer));
|
|
434
468
|
|
|
435
469
|
patches.push({ patch, sha: currentSha });
|
|
436
470
|
|
|
@@ -474,8 +508,8 @@ export default class WarpGraph {
|
|
|
474
508
|
incoming.get(to).push({ neighborId: from, label });
|
|
475
509
|
}
|
|
476
510
|
|
|
477
|
-
const sortNeighbors = (list) => {
|
|
478
|
-
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) => {
|
|
479
513
|
if (a.neighborId !== b.neighborId) {
|
|
480
514
|
return a.neighborId < b.neighborId ? -1 : 1;
|
|
481
515
|
}
|
|
@@ -529,7 +563,7 @@ export default class WarpGraph {
|
|
|
529
563
|
* provenance index, and frontier tracking.
|
|
530
564
|
*
|
|
531
565
|
* @param {string} writerId - The writer ID that committed the patch
|
|
532
|
-
* @param {{patch?:
|
|
566
|
+
* @param {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} [opts] - Commit details
|
|
533
567
|
* @private
|
|
534
568
|
*/
|
|
535
569
|
async _onPatchCommitted(writerId, { patch, sha } = {}) {
|
|
@@ -538,11 +572,11 @@ export default class WarpGraph {
|
|
|
538
572
|
// Eager re-materialize: apply the just-committed patch to cached state
|
|
539
573
|
// Only when the cache is clean — applying a patch to stale state would be incorrect
|
|
540
574
|
if (this._cachedState && !this._stateDirty && patch && sha) {
|
|
541
|
-
joinPatch(this._cachedState, patch, sha);
|
|
575
|
+
joinPatch(this._cachedState, /** @type {any} */ (patch), sha); // TODO(ts-cleanup): type patch array
|
|
542
576
|
await this._setMaterializedState(this._cachedState);
|
|
543
577
|
// Update provenance index with new patch
|
|
544
578
|
if (this._provenanceIndex) {
|
|
545
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
579
|
+
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
|
|
546
580
|
}
|
|
547
581
|
// Keep _lastFrontier in sync so hasFrontierChanged() won't misreport stale
|
|
548
582
|
if (this._lastFrontier) {
|
|
@@ -561,9 +595,9 @@ export default class WarpGraph {
|
|
|
561
595
|
async _materializeGraph() {
|
|
562
596
|
const state = await this.materialize();
|
|
563
597
|
if (!this._materializedGraph || this._materializedGraph.state !== state) {
|
|
564
|
-
await this._setMaterializedState(state);
|
|
598
|
+
await this._setMaterializedState(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (state));
|
|
565
599
|
}
|
|
566
|
-
return this._materializedGraph;
|
|
600
|
+
return /** @type {MaterializedGraph} */ (this._materializedGraph);
|
|
567
601
|
}
|
|
568
602
|
|
|
569
603
|
/**
|
|
@@ -602,14 +636,16 @@ export default class WarpGraph {
|
|
|
602
636
|
|
|
603
637
|
// When ceiling is active, delegate to ceiling-aware path (with its own cache)
|
|
604
638
|
if (ceiling !== null) {
|
|
605
|
-
return await this._materializeWithCeiling(ceiling, collectReceipts, t0);
|
|
639
|
+
return await this._materializeWithCeiling(ceiling, !!collectReceipts, t0);
|
|
606
640
|
}
|
|
607
641
|
|
|
608
642
|
try {
|
|
609
643
|
// Check for checkpoint
|
|
610
644
|
const checkpoint = await this._loadLatestCheckpoint();
|
|
611
645
|
|
|
646
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
|
|
612
647
|
let state;
|
|
648
|
+
/** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
613
649
|
let receipts;
|
|
614
650
|
let patchCount = 0;
|
|
615
651
|
|
|
@@ -617,20 +653,21 @@ export default class WarpGraph {
|
|
|
617
653
|
if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
|
|
618
654
|
const patches = await this._loadPatchesSince(checkpoint);
|
|
619
655
|
if (collectReceipts) {
|
|
620
|
-
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
|
|
621
657
|
state = result.state;
|
|
622
658
|
receipts = result.receipts;
|
|
623
659
|
} else {
|
|
624
|
-
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
|
|
625
661
|
}
|
|
626
662
|
patchCount = patches.length;
|
|
627
663
|
|
|
628
664
|
// Build provenance index: start from checkpoint index if present, then add new patches
|
|
629
|
-
|
|
630
|
-
|
|
665
|
+
const ckPI = /** @type {any} */ (checkpoint).provenanceIndex; // TODO(ts-cleanup): type checkpoint cast
|
|
666
|
+
this._provenanceIndex = ckPI
|
|
667
|
+
? ckPI.clone()
|
|
631
668
|
: new ProvenanceIndex();
|
|
632
669
|
for (const { patch, sha } of patches) {
|
|
633
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
670
|
+
/** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).addPatch(sha, patch.reads, patch.writes);
|
|
634
671
|
}
|
|
635
672
|
} else {
|
|
636
673
|
// 1. Discover all writers
|
|
@@ -661,11 +698,11 @@ export default class WarpGraph {
|
|
|
661
698
|
} else {
|
|
662
699
|
// 5. Reduce all patches to state
|
|
663
700
|
if (collectReceipts) {
|
|
664
|
-
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
|
|
665
702
|
state = result.state;
|
|
666
703
|
receipts = result.receipts;
|
|
667
704
|
} else {
|
|
668
|
-
state = reduceV5(allPatches);
|
|
705
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
|
|
669
706
|
}
|
|
670
707
|
patchCount = allPatches.length;
|
|
671
708
|
|
|
@@ -679,6 +716,7 @@ export default class WarpGraph {
|
|
|
679
716
|
}
|
|
680
717
|
|
|
681
718
|
await this._setMaterializedState(state);
|
|
719
|
+
this._provenanceDegraded = false;
|
|
682
720
|
this._cachedCeiling = null;
|
|
683
721
|
this._cachedFrontier = null;
|
|
684
722
|
this._lastFrontier = await this.getFrontier();
|
|
@@ -712,11 +750,11 @@ export default class WarpGraph {
|
|
|
712
750
|
this._logTiming('materialize', t0, { metrics: `${patchCount} patches` });
|
|
713
751
|
|
|
714
752
|
if (collectReceipts) {
|
|
715
|
-
return { state, receipts };
|
|
753
|
+
return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
|
|
716
754
|
}
|
|
717
755
|
return state;
|
|
718
756
|
} catch (err) {
|
|
719
|
-
this._logTiming('materialize', t0, { error: err });
|
|
757
|
+
this._logTiming('materialize', t0, { error: /** @type {Error} */ (err) });
|
|
720
758
|
throw err;
|
|
721
759
|
}
|
|
722
760
|
}
|
|
@@ -770,12 +808,13 @@ export default class WarpGraph {
|
|
|
770
808
|
|
|
771
809
|
// Cache hit: same ceiling, clean state, AND frontier unchanged.
|
|
772
810
|
// Bypass cache when collectReceipts is true — cached path has no receipts.
|
|
811
|
+
const cf = this._cachedFrontier;
|
|
773
812
|
if (
|
|
774
813
|
this._cachedState && !this._stateDirty &&
|
|
775
814
|
ceiling === this._cachedCeiling && !collectReceipts &&
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
[...frontier].every(([w, sha]) =>
|
|
815
|
+
cf !== null &&
|
|
816
|
+
cf.size === frontier.size &&
|
|
817
|
+
[...frontier].every(([w, sha]) => cf.get(w) === sha)
|
|
779
818
|
) {
|
|
780
819
|
return this._cachedState;
|
|
781
820
|
}
|
|
@@ -785,6 +824,7 @@ export default class WarpGraph {
|
|
|
785
824
|
if (writerIds.length === 0 || ceiling <= 0) {
|
|
786
825
|
const state = createEmptyStateV5();
|
|
787
826
|
this._provenanceIndex = new ProvenanceIndex();
|
|
827
|
+
this._provenanceDegraded = false;
|
|
788
828
|
await this._setMaterializedState(state);
|
|
789
829
|
this._cachedCeiling = ceiling;
|
|
790
830
|
this._cachedFrontier = frontier;
|
|
@@ -795,6 +835,32 @@ export default class WarpGraph {
|
|
|
795
835
|
return state;
|
|
796
836
|
}
|
|
797
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
|
+
|
|
798
864
|
const allPatches = [];
|
|
799
865
|
for (const writerId of writerIds) {
|
|
800
866
|
const writerPatches = await this._loadWriterPatches(writerId);
|
|
@@ -805,7 +871,9 @@ export default class WarpGraph {
|
|
|
805
871
|
}
|
|
806
872
|
}
|
|
807
873
|
|
|
874
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
|
|
808
875
|
let state;
|
|
876
|
+
/** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
809
877
|
let receipts;
|
|
810
878
|
|
|
811
879
|
if (allPatches.length === 0) {
|
|
@@ -814,27 +882,37 @@ export default class WarpGraph {
|
|
|
814
882
|
receipts = [];
|
|
815
883
|
}
|
|
816
884
|
} else if (collectReceipts) {
|
|
817
|
-
const result = reduceV5(allPatches, undefined, { receipts: true });
|
|
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
|
|
818
886
|
state = result.state;
|
|
819
887
|
receipts = result.receipts;
|
|
820
888
|
} else {
|
|
821
|
-
state = reduceV5(allPatches);
|
|
889
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
|
|
822
890
|
}
|
|
823
891
|
|
|
824
892
|
this._provenanceIndex = new ProvenanceIndex();
|
|
825
893
|
for (const { patch, sha } of allPatches) {
|
|
826
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
894
|
+
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
|
|
827
895
|
}
|
|
896
|
+
this._provenanceDegraded = false;
|
|
828
897
|
|
|
829
898
|
await this._setMaterializedState(state);
|
|
830
899
|
this._cachedCeiling = ceiling;
|
|
831
900
|
this._cachedFrontier = frontier;
|
|
832
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
|
+
|
|
833
911
|
// Skip auto-checkpoint and GC — this is an exploratory read
|
|
834
912
|
this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
|
|
835
913
|
|
|
836
914
|
if (collectReceipts) {
|
|
837
|
-
return { state, receipts };
|
|
915
|
+
return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
|
|
838
916
|
}
|
|
839
917
|
return state;
|
|
840
918
|
}
|
|
@@ -883,16 +961,16 @@ export default class WarpGraph {
|
|
|
883
961
|
}
|
|
884
962
|
|
|
885
963
|
// Capture pre-merge counts for receipt
|
|
886
|
-
const beforeNodes = this._cachedState.nodeAlive.
|
|
887
|
-
const beforeEdges = this._cachedState.edgeAlive.
|
|
964
|
+
const beforeNodes = orsetElements(this._cachedState.nodeAlive).length;
|
|
965
|
+
const beforeEdges = orsetElements(this._cachedState.edgeAlive).length;
|
|
888
966
|
const beforeFrontierSize = this._cachedState.observedFrontier.size;
|
|
889
967
|
|
|
890
968
|
// Perform the join
|
|
891
969
|
const mergedState = joinStates(this._cachedState, otherState);
|
|
892
970
|
|
|
893
971
|
// Calculate receipt
|
|
894
|
-
const afterNodes = mergedState.nodeAlive.
|
|
895
|
-
const afterEdges = mergedState.edgeAlive.
|
|
972
|
+
const afterNodes = orsetElements(mergedState.nodeAlive).length;
|
|
973
|
+
const afterEdges = orsetElements(mergedState.edgeAlive).length;
|
|
896
974
|
const afterFrontierSize = mergedState.observedFrontier.size;
|
|
897
975
|
|
|
898
976
|
// Count property changes (keys that existed in both but have different values)
|
|
@@ -954,7 +1032,7 @@ export default class WarpGraph {
|
|
|
954
1032
|
* @example
|
|
955
1033
|
* // Time-travel to a previous checkpoint
|
|
956
1034
|
* const oldState = await graph.materializeAt('abc123');
|
|
957
|
-
* console.log('Nodes at checkpoint:',
|
|
1035
|
+
* console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
|
|
958
1036
|
*/
|
|
959
1037
|
async materializeAt(checkpointSha) {
|
|
960
1038
|
// 1. Discover current writers to build target frontier
|
|
@@ -971,7 +1049,7 @@ export default class WarpGraph {
|
|
|
971
1049
|
}
|
|
972
1050
|
|
|
973
1051
|
// 3. Create a patch loader function for incremental materialization
|
|
974
|
-
const patchLoader = async (writerId, fromSha, toSha) => {
|
|
1052
|
+
const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
|
|
975
1053
|
// Load patches from fromSha (exclusive) to toSha (inclusive)
|
|
976
1054
|
// Walk from toSha back to fromSha
|
|
977
1055
|
const patches = [];
|
|
@@ -1004,7 +1082,7 @@ export default class WarpGraph {
|
|
|
1004
1082
|
|
|
1005
1083
|
// 4. Call materializeIncremental with the checkpoint and target frontier
|
|
1006
1084
|
const state = await materializeIncremental({
|
|
1007
|
-
persistence: this._persistence,
|
|
1085
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
1008
1086
|
graphName: this._graphName,
|
|
1009
1087
|
checkpointSha,
|
|
1010
1088
|
targetFrontier,
|
|
@@ -1048,23 +1126,24 @@ export default class WarpGraph {
|
|
|
1048
1126
|
// 3. Materialize current state (reuse cached if fresh, guard against recursion)
|
|
1049
1127
|
const prevCheckpointing = this._checkpointing;
|
|
1050
1128
|
this._checkpointing = true;
|
|
1129
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5} */
|
|
1051
1130
|
let state;
|
|
1052
1131
|
try {
|
|
1053
|
-
state = (this._cachedState && !this._stateDirty)
|
|
1132
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ ((this._cachedState && !this._stateDirty)
|
|
1054
1133
|
? this._cachedState
|
|
1055
|
-
: await this.materialize();
|
|
1134
|
+
: await this.materialize());
|
|
1056
1135
|
} finally {
|
|
1057
1136
|
this._checkpointing = prevCheckpointing;
|
|
1058
1137
|
}
|
|
1059
1138
|
|
|
1060
1139
|
// 4. Call CheckpointService.create() with provenance index if available
|
|
1061
1140
|
const checkpointSha = await createCheckpointCommit({
|
|
1062
|
-
persistence: this._persistence,
|
|
1141
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
1063
1142
|
graphName: this._graphName,
|
|
1064
1143
|
state,
|
|
1065
1144
|
frontier,
|
|
1066
1145
|
parents,
|
|
1067
|
-
provenanceIndex: this._provenanceIndex,
|
|
1146
|
+
provenanceIndex: this._provenanceIndex || undefined,
|
|
1068
1147
|
crypto: this._crypto,
|
|
1069
1148
|
codec: this._codec,
|
|
1070
1149
|
});
|
|
@@ -1078,7 +1157,7 @@ export default class WarpGraph {
|
|
|
1078
1157
|
// 6. Return checkpoint SHA
|
|
1079
1158
|
return checkpointSha;
|
|
1080
1159
|
} catch (err) {
|
|
1081
|
-
this._logTiming('createCheckpoint', t0, { error: err });
|
|
1160
|
+
this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
|
|
1082
1161
|
throw err;
|
|
1083
1162
|
}
|
|
1084
1163
|
}
|
|
@@ -1172,6 +1251,7 @@ export default class WarpGraph {
|
|
|
1172
1251
|
*/
|
|
1173
1252
|
async discoverTicks() {
|
|
1174
1253
|
const writerIds = await this.discoverWriters();
|
|
1254
|
+
/** @type {Set<number>} */
|
|
1175
1255
|
const globalTickSet = new Set();
|
|
1176
1256
|
const perWriter = new Map();
|
|
1177
1257
|
|
|
@@ -1179,6 +1259,7 @@ export default class WarpGraph {
|
|
|
1179
1259
|
const writerRef = buildWriterRef(this._graphName, writerId);
|
|
1180
1260
|
const tipSha = await this._persistence.readRef(writerRef);
|
|
1181
1261
|
const writerTicks = [];
|
|
1262
|
+
/** @type {Record<number, string>} */
|
|
1182
1263
|
const tickShas = {};
|
|
1183
1264
|
|
|
1184
1265
|
if (tipSha) {
|
|
@@ -1256,7 +1337,7 @@ export default class WarpGraph {
|
|
|
1256
1337
|
/**
|
|
1257
1338
|
* Loads the latest checkpoint for this graph.
|
|
1258
1339
|
*
|
|
1259
|
-
* @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
|
|
1260
1341
|
* @private
|
|
1261
1342
|
*/
|
|
1262
1343
|
async _loadLatestCheckpoint() {
|
|
@@ -1298,7 +1379,7 @@ export default class WarpGraph {
|
|
|
1298
1379
|
if (kind === 'patch') {
|
|
1299
1380
|
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
1300
1381
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
1301
|
-
const patch = this._codec.decode(patchBuffer);
|
|
1382
|
+
const patch = /** @type {{schema?: number}} */ (this._codec.decode(patchBuffer));
|
|
1302
1383
|
|
|
1303
1384
|
// If any patch has schema:1, we have v1 history
|
|
1304
1385
|
if (patch.schema === 1 || patch.schema === undefined) {
|
|
@@ -1313,8 +1394,8 @@ export default class WarpGraph {
|
|
|
1313
1394
|
/**
|
|
1314
1395
|
* Loads patches since a checkpoint for incremental materialization.
|
|
1315
1396
|
*
|
|
1316
|
-
* @param {{state:
|
|
1317
|
-
* @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
|
|
1318
1399
|
* @private
|
|
1319
1400
|
*/
|
|
1320
1401
|
async _loadPatchesSince(checkpoint) {
|
|
@@ -1396,7 +1477,7 @@ export default class WarpGraph {
|
|
|
1396
1477
|
*
|
|
1397
1478
|
* @param {string} writerId - The writer ID for this patch
|
|
1398
1479
|
* @param {string} incomingSha - The incoming patch commit SHA
|
|
1399
|
-
* @param {{state:
|
|
1480
|
+
* @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to validate against
|
|
1400
1481
|
* @returns {Promise<void>}
|
|
1401
1482
|
* @throws {Error} If patch is behind/same as checkpoint frontier (backfill rejected)
|
|
1402
1483
|
* @throws {Error} If patch does not extend checkpoint head (writer fork detected)
|
|
@@ -1444,18 +1525,19 @@ export default class WarpGraph {
|
|
|
1444
1525
|
_maybeRunGC(state) {
|
|
1445
1526
|
try {
|
|
1446
1527
|
const metrics = collectGCMetrics(state);
|
|
1528
|
+
/** @type {import('./services/GCPolicy.js').GCInputMetrics} */
|
|
1447
1529
|
const inputMetrics = {
|
|
1448
1530
|
...metrics,
|
|
1449
1531
|
patchesSinceCompaction: this._patchesSinceGC,
|
|
1450
1532
|
timeSinceCompaction: Date.now() - this._lastGCTime,
|
|
1451
1533
|
};
|
|
1452
|
-
const { shouldRun, reasons } = shouldRunGC(inputMetrics, this._gcPolicy);
|
|
1534
|
+
const { shouldRun, reasons } = shouldRunGC(inputMetrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
|
|
1453
1535
|
|
|
1454
1536
|
if (!shouldRun) {
|
|
1455
1537
|
return;
|
|
1456
1538
|
}
|
|
1457
1539
|
|
|
1458
|
-
if (this._gcPolicy.enabled) {
|
|
1540
|
+
if (/** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy).enabled) {
|
|
1459
1541
|
const appliedVV = computeAppliedVV(state);
|
|
1460
1542
|
const result = executeGC(state, appliedVV);
|
|
1461
1543
|
this._lastGCTime = Date.now();
|
|
@@ -1494,11 +1576,15 @@ export default class WarpGraph {
|
|
|
1494
1576
|
return { ran: false, result: null, reasons: [] };
|
|
1495
1577
|
}
|
|
1496
1578
|
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
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
|
+
};
|
|
1500
1586
|
|
|
1501
|
-
const { shouldRun, reasons } = shouldRunGC(metrics, this._gcPolicy);
|
|
1587
|
+
const { shouldRun, reasons } = shouldRunGC(metrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
|
|
1502
1588
|
|
|
1503
1589
|
if (!shouldRun) {
|
|
1504
1590
|
return { ran: false, result: null, reasons: [] };
|
|
@@ -1545,7 +1631,7 @@ export default class WarpGraph {
|
|
|
1545
1631
|
|
|
1546
1632
|
return result;
|
|
1547
1633
|
} catch (err) {
|
|
1548
|
-
this._logTiming('runGC', t0, { error: err });
|
|
1634
|
+
this._logTiming('runGC', t0, { error: /** @type {Error} */ (err) });
|
|
1549
1635
|
throw err;
|
|
1550
1636
|
}
|
|
1551
1637
|
}
|
|
@@ -1567,10 +1653,15 @@ export default class WarpGraph {
|
|
|
1567
1653
|
return null;
|
|
1568
1654
|
}
|
|
1569
1655
|
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
+
};
|
|
1574
1665
|
}
|
|
1575
1666
|
|
|
1576
1667
|
/**
|
|
@@ -1653,6 +1744,7 @@ export default class WarpGraph {
|
|
|
1653
1744
|
*/
|
|
1654
1745
|
async status() {
|
|
1655
1746
|
// Determine cachedState
|
|
1747
|
+
/** @type {'fresh' | 'stale' | 'none'} */
|
|
1656
1748
|
let cachedState;
|
|
1657
1749
|
if (this._cachedState === null) {
|
|
1658
1750
|
cachedState = 'none';
|
|
@@ -1702,7 +1794,7 @@ export default class WarpGraph {
|
|
|
1702
1794
|
* One handler's error does not prevent other handlers from being called.
|
|
1703
1795
|
*
|
|
1704
1796
|
* @param {Object} options - Subscription options
|
|
1705
|
-
* @param {(diff: import('./services/StateDiff.js').
|
|
1797
|
+
* @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with diff when graph changes
|
|
1706
1798
|
* @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
|
|
1707
1799
|
* @param {boolean} [options.replay=false] - If true, immediately fires onChange with initial state diff
|
|
1708
1800
|
* @returns {{unsubscribe: () => void}} Subscription handle
|
|
@@ -1745,7 +1837,7 @@ export default class WarpGraph {
|
|
|
1745
1837
|
} catch (err) {
|
|
1746
1838
|
if (onError) {
|
|
1747
1839
|
try {
|
|
1748
|
-
onError(err);
|
|
1840
|
+
onError(/** @type {Error} */ (err));
|
|
1749
1841
|
} catch {
|
|
1750
1842
|
// onError itself threw — swallow to prevent cascade
|
|
1751
1843
|
}
|
|
@@ -1782,7 +1874,7 @@ export default class WarpGraph {
|
|
|
1782
1874
|
*
|
|
1783
1875
|
* @param {string} pattern - Glob pattern (e.g., 'user:*', 'order:123', '*')
|
|
1784
1876
|
* @param {Object} options - Watch options
|
|
1785
|
-
* @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
|
|
1786
1878
|
* @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
|
|
1787
1879
|
* @param {number} [options.poll] - Poll interval in ms (min 1000); checks frontier and auto-materializes
|
|
1788
1880
|
* @returns {{unsubscribe: () => void}} Subscription handle
|
|
@@ -1823,31 +1915,32 @@ export default class WarpGraph {
|
|
|
1823
1915
|
|
|
1824
1916
|
// Pattern matching: same logic as QueryBuilder.match()
|
|
1825
1917
|
// Pre-compile pattern matcher once for performance
|
|
1918
|
+
/** @type {(nodeId: string) => boolean} */
|
|
1826
1919
|
let matchesPattern;
|
|
1827
1920
|
if (pattern === '*') {
|
|
1828
1921
|
matchesPattern = () => true;
|
|
1829
1922
|
} else if (pattern.includes('*')) {
|
|
1830
1923
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
1831
1924
|
const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
|
|
1832
|
-
matchesPattern = (nodeId) => regex.test(nodeId);
|
|
1925
|
+
matchesPattern = (/** @type {string} */ nodeId) => regex.test(nodeId);
|
|
1833
1926
|
} else {
|
|
1834
|
-
matchesPattern = (nodeId) => nodeId === pattern;
|
|
1927
|
+
matchesPattern = (/** @type {string} */ nodeId) => nodeId === pattern;
|
|
1835
1928
|
}
|
|
1836
1929
|
|
|
1837
1930
|
// Filtered onChange that only passes matching changes
|
|
1838
|
-
const filteredOnChange = (diff) => {
|
|
1931
|
+
const filteredOnChange = (/** @type {import('./services/StateDiff.js').StateDiffResult} */ diff) => {
|
|
1839
1932
|
const filteredDiff = {
|
|
1840
1933
|
nodes: {
|
|
1841
1934
|
added: diff.nodes.added.filter(matchesPattern),
|
|
1842
1935
|
removed: diff.nodes.removed.filter(matchesPattern),
|
|
1843
1936
|
},
|
|
1844
1937
|
edges: {
|
|
1845
|
-
added: diff.edges.added.filter(e => matchesPattern(e.from) || matchesPattern(e.to)),
|
|
1846
|
-
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)),
|
|
1847
1940
|
},
|
|
1848
1941
|
props: {
|
|
1849
|
-
set: diff.props.set.filter(p => matchesPattern(p.nodeId)),
|
|
1850
|
-
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)),
|
|
1851
1944
|
},
|
|
1852
1945
|
};
|
|
1853
1946
|
|
|
@@ -1869,6 +1962,7 @@ export default class WarpGraph {
|
|
|
1869
1962
|
const subscription = this.subscribe({ onChange: filteredOnChange, onError });
|
|
1870
1963
|
|
|
1871
1964
|
// Polling: periodically check frontier and auto-materialize if changed
|
|
1965
|
+
/** @type {ReturnType<typeof setInterval>|null} */
|
|
1872
1966
|
let pollIntervalId = null;
|
|
1873
1967
|
let pollInFlight = false;
|
|
1874
1968
|
if (poll) {
|
|
@@ -1950,7 +2044,7 @@ export default class WarpGraph {
|
|
|
1950
2044
|
* Creates a sync request to send to a remote peer.
|
|
1951
2045
|
* The request contains the local frontier for comparison.
|
|
1952
2046
|
*
|
|
1953
|
-
* @returns {Promise<
|
|
2047
|
+
* @returns {Promise<import('./services/SyncProtocol.js').SyncRequest>} The sync request
|
|
1954
2048
|
* @throws {Error} If listing refs fails
|
|
1955
2049
|
*
|
|
1956
2050
|
* @example
|
|
@@ -1965,8 +2059,8 @@ export default class WarpGraph {
|
|
|
1965
2059
|
/**
|
|
1966
2060
|
* Processes an incoming sync request and returns patches the requester needs.
|
|
1967
2061
|
*
|
|
1968
|
-
* @param {
|
|
1969
|
-
* @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
|
|
1970
2064
|
* @throws {Error} If listing refs or reading patches fails
|
|
1971
2065
|
*
|
|
1972
2066
|
* @example
|
|
@@ -1979,7 +2073,7 @@ export default class WarpGraph {
|
|
|
1979
2073
|
return await processSyncRequest(
|
|
1980
2074
|
request,
|
|
1981
2075
|
localFrontier,
|
|
1982
|
-
this._persistence,
|
|
2076
|
+
/** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
1983
2077
|
this._graphName,
|
|
1984
2078
|
{ codec: this._codec }
|
|
1985
2079
|
);
|
|
@@ -1991,8 +2085,8 @@ export default class WarpGraph {
|
|
|
1991
2085
|
*
|
|
1992
2086
|
* **Requires a cached state.**
|
|
1993
2087
|
*
|
|
1994
|
-
* @param {
|
|
1995
|
-
* @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
|
|
1996
2090
|
* @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
|
|
1997
2091
|
*
|
|
1998
2092
|
* @example
|
|
@@ -2007,8 +2101,8 @@ export default class WarpGraph {
|
|
|
2007
2101
|
});
|
|
2008
2102
|
}
|
|
2009
2103
|
|
|
2010
|
-
const currentFrontier = this._cachedState.observedFrontier;
|
|
2011
|
-
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));
|
|
2012
2106
|
|
|
2013
2107
|
// Update cached state
|
|
2014
2108
|
this._cachedState = result.state;
|
|
@@ -2079,7 +2173,7 @@ export default class WarpGraph {
|
|
|
2079
2173
|
let targetUrl = null;
|
|
2080
2174
|
if (!isDirectPeer) {
|
|
2081
2175
|
try {
|
|
2082
|
-
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));
|
|
2083
2177
|
} catch {
|
|
2084
2178
|
throw new SyncError('Invalid remote URL', {
|
|
2085
2179
|
code: 'E_SYNC_REMOTE_URL',
|
|
@@ -2104,13 +2198,13 @@ export default class WarpGraph {
|
|
|
2104
2198
|
}
|
|
2105
2199
|
|
|
2106
2200
|
let attempt = 0;
|
|
2107
|
-
const emit = (type, payload = {}) => {
|
|
2201
|
+
const emit = (/** @type {string} */ type, /** @type {Record<string, any>} */ payload = {}) => {
|
|
2108
2202
|
if (typeof onStatus === 'function') {
|
|
2109
|
-
onStatus({ type, attempt, ...payload });
|
|
2203
|
+
onStatus(/** @type {any} */ ({ type, attempt, ...payload })); // TODO(ts-cleanup): type sync protocol
|
|
2110
2204
|
}
|
|
2111
2205
|
};
|
|
2112
2206
|
|
|
2113
|
-
const shouldRetry = (err) => {
|
|
2207
|
+
const shouldRetry = (/** @type {any} */ err) => { // TODO(ts-cleanup): type error
|
|
2114
2208
|
if (isDirectPeer) { return false; }
|
|
2115
2209
|
if (err instanceof SyncError) {
|
|
2116
2210
|
return ['E_SYNC_REMOTE', 'E_SYNC_TIMEOUT', 'E_SYNC_NETWORK'].includes(err.code);
|
|
@@ -2140,7 +2234,7 @@ export default class WarpGraph {
|
|
|
2140
2234
|
const combinedSignal = signal
|
|
2141
2235
|
? AbortSignal.any([timeoutSignal, signal])
|
|
2142
2236
|
: timeoutSignal;
|
|
2143
|
-
return fetch(targetUrl.toString(), {
|
|
2237
|
+
return fetch(/** @type {URL} */ (targetUrl).toString(), {
|
|
2144
2238
|
method: 'POST',
|
|
2145
2239
|
headers: {
|
|
2146
2240
|
'content-type': 'application/json',
|
|
@@ -2151,7 +2245,7 @@ export default class WarpGraph {
|
|
|
2151
2245
|
});
|
|
2152
2246
|
});
|
|
2153
2247
|
} catch (err) {
|
|
2154
|
-
if (err?.name === 'AbortError') {
|
|
2248
|
+
if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
|
|
2155
2249
|
throw new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
2156
2250
|
}
|
|
2157
2251
|
if (err instanceof TimeoutError) {
|
|
@@ -2162,7 +2256,7 @@ export default class WarpGraph {
|
|
|
2162
2256
|
}
|
|
2163
2257
|
throw new SyncError('Network error', {
|
|
2164
2258
|
code: 'E_SYNC_NETWORK',
|
|
2165
|
-
context: { message: err?.message },
|
|
2259
|
+
context: { message: /** @type {any} */ (err)?.message }, // TODO(ts-cleanup): type error
|
|
2166
2260
|
});
|
|
2167
2261
|
}
|
|
2168
2262
|
|
|
@@ -2223,9 +2317,9 @@ export default class WarpGraph {
|
|
|
2223
2317
|
jitter: 'decorrelated',
|
|
2224
2318
|
signal,
|
|
2225
2319
|
shouldRetry,
|
|
2226
|
-
onRetry: (error, attemptNumber, delayMs) => {
|
|
2320
|
+
onRetry: (/** @type {Error} */ error, /** @type {number} */ attemptNumber, /** @type {number} */ delayMs) => {
|
|
2227
2321
|
if (typeof onStatus === 'function') {
|
|
2228
|
-
onStatus({ type: 'retrying', attempt: attemptNumber, delayMs, error });
|
|
2322
|
+
onStatus(/** @type {any} */ ({ type: 'retrying', attempt: attemptNumber, delayMs, error })); // TODO(ts-cleanup): type sync protocol
|
|
2229
2323
|
}
|
|
2230
2324
|
},
|
|
2231
2325
|
});
|
|
@@ -2234,12 +2328,12 @@ export default class WarpGraph {
|
|
|
2234
2328
|
|
|
2235
2329
|
if (materializeAfterSync) {
|
|
2236
2330
|
if (!this._cachedState) { await this.materialize(); }
|
|
2237
|
-
return { ...syncResult, state: this._cachedState };
|
|
2331
|
+
return { ...syncResult, state: /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState) };
|
|
2238
2332
|
}
|
|
2239
2333
|
return syncResult;
|
|
2240
2334
|
} catch (err) {
|
|
2241
|
-
this._logTiming('syncWith', t0, { error: err });
|
|
2242
|
-
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
|
|
2243
2337
|
const abortedError = new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
2244
2338
|
if (typeof onStatus === 'function') {
|
|
2245
2339
|
onStatus({ type: 'failed', attempt, error: abortedError });
|
|
@@ -2247,14 +2341,14 @@ export default class WarpGraph {
|
|
|
2247
2341
|
throw abortedError;
|
|
2248
2342
|
}
|
|
2249
2343
|
if (err instanceof RetryExhaustedError) {
|
|
2250
|
-
const cause = err.cause || err;
|
|
2344
|
+
const cause = /** @type {Error} */ (err.cause || err);
|
|
2251
2345
|
if (typeof onStatus === 'function') {
|
|
2252
2346
|
onStatus({ type: 'failed', attempt: err.attempts, error: cause });
|
|
2253
2347
|
}
|
|
2254
2348
|
throw cause;
|
|
2255
2349
|
}
|
|
2256
2350
|
if (typeof onStatus === 'function') {
|
|
2257
|
-
onStatus({ type: 'failed', attempt, error: err });
|
|
2351
|
+
onStatus({ type: 'failed', attempt, error: /** @type {Error} */ (err) });
|
|
2258
2352
|
}
|
|
2259
2353
|
throw err;
|
|
2260
2354
|
}
|
|
@@ -2273,7 +2367,7 @@ export default class WarpGraph {
|
|
|
2273
2367
|
* @throws {Error} If port is not a number
|
|
2274
2368
|
* @throws {Error} If httpPort adapter is not provided
|
|
2275
2369
|
*/
|
|
2276
|
-
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
|
|
2277
2371
|
if (typeof port !== 'number') {
|
|
2278
2372
|
throw new Error('serve() requires a numeric port');
|
|
2279
2373
|
}
|
|
@@ -2318,8 +2412,8 @@ export default class WarpGraph {
|
|
|
2318
2412
|
*/
|
|
2319
2413
|
async writer(writerId) {
|
|
2320
2414
|
// Build config adapters for resolveWriterId
|
|
2321
|
-
const configGet = async (key) => await this._persistence.configGet(key);
|
|
2322
|
-
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);
|
|
2323
2417
|
|
|
2324
2418
|
// Resolve the writer ID
|
|
2325
2419
|
const resolvedWriterId = await resolveWriterId({
|
|
@@ -2330,13 +2424,13 @@ export default class WarpGraph {
|
|
|
2330
2424
|
});
|
|
2331
2425
|
|
|
2332
2426
|
return new Writer({
|
|
2333
|
-
persistence: this._persistence,
|
|
2427
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
2334
2428
|
graphName: this._graphName,
|
|
2335
2429
|
writerId: resolvedWriterId,
|
|
2336
2430
|
versionVector: this._versionVector,
|
|
2337
|
-
getCurrentState: () => this._cachedState,
|
|
2431
|
+
getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
|
|
2338
2432
|
onDeleteWithData: this._onDeleteWithData,
|
|
2339
|
-
onCommitSuccess: (opts) => this._onPatchCommitted(resolvedWriterId, opts),
|
|
2433
|
+
onCommitSuccess: (/** @type {any} */ opts) => this._onPatchCommitted(resolvedWriterId, opts), // TODO(ts-cleanup): type sync protocol
|
|
2340
2434
|
codec: this._codec,
|
|
2341
2435
|
});
|
|
2342
2436
|
}
|
|
@@ -2384,13 +2478,13 @@ export default class WarpGraph {
|
|
|
2384
2478
|
}
|
|
2385
2479
|
|
|
2386
2480
|
return new Writer({
|
|
2387
|
-
persistence: this._persistence,
|
|
2481
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
2388
2482
|
graphName: this._graphName,
|
|
2389
2483
|
writerId: freshWriterId,
|
|
2390
2484
|
versionVector: this._versionVector,
|
|
2391
|
-
getCurrentState: () => this._cachedState,
|
|
2485
|
+
getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
|
|
2392
2486
|
onDeleteWithData: this._onDeleteWithData,
|
|
2393
|
-
onCommitSuccess: (commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts),
|
|
2487
|
+
onCommitSuccess: (/** @type {any} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts), // TODO(ts-cleanup): type sync protocol
|
|
2394
2488
|
codec: this._codec,
|
|
2395
2489
|
});
|
|
2396
2490
|
}
|
|
@@ -2545,7 +2639,8 @@ export default class WarpGraph {
|
|
|
2545
2639
|
*/
|
|
2546
2640
|
async hasNode(nodeId) {
|
|
2547
2641
|
await this._ensureFreshState();
|
|
2548
|
-
|
|
2642
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2643
|
+
return orsetContains(s.nodeAlive, nodeId);
|
|
2549
2644
|
}
|
|
2550
2645
|
|
|
2551
2646
|
/**
|
|
@@ -2570,15 +2665,16 @@ export default class WarpGraph {
|
|
|
2570
2665
|
*/
|
|
2571
2666
|
async getNodeProps(nodeId) {
|
|
2572
2667
|
await this._ensureFreshState();
|
|
2668
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2573
2669
|
|
|
2574
2670
|
// Check if node exists
|
|
2575
|
-
if (!orsetContains(
|
|
2671
|
+
if (!orsetContains(s.nodeAlive, nodeId)) {
|
|
2576
2672
|
return null;
|
|
2577
2673
|
}
|
|
2578
2674
|
|
|
2579
2675
|
// Collect all properties for this node
|
|
2580
2676
|
const props = new Map();
|
|
2581
|
-
for (const [propKey, register] of
|
|
2677
|
+
for (const [propKey, register] of s.prop) {
|
|
2582
2678
|
const decoded = decodePropKey(propKey);
|
|
2583
2679
|
if (decoded.nodeId === nodeId) {
|
|
2584
2680
|
props.set(decoded.propKey, register.value);
|
|
@@ -2612,26 +2708,28 @@ export default class WarpGraph {
|
|
|
2612
2708
|
*/
|
|
2613
2709
|
async getEdgeProps(from, to, label) {
|
|
2614
2710
|
await this._ensureFreshState();
|
|
2711
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2615
2712
|
|
|
2616
2713
|
// Check if edge exists
|
|
2617
2714
|
const edgeKey = encodeEdgeKey(from, to, label);
|
|
2618
|
-
if (!orsetContains(
|
|
2715
|
+
if (!orsetContains(s.edgeAlive, edgeKey)) {
|
|
2619
2716
|
return null;
|
|
2620
2717
|
}
|
|
2621
2718
|
|
|
2622
2719
|
// Check node liveness for both endpoints
|
|
2623
|
-
if (!orsetContains(
|
|
2624
|
-
!orsetContains(
|
|
2720
|
+
if (!orsetContains(s.nodeAlive, from) ||
|
|
2721
|
+
!orsetContains(s.nodeAlive, to)) {
|
|
2625
2722
|
return null;
|
|
2626
2723
|
}
|
|
2627
2724
|
|
|
2628
2725
|
// Determine the birth EventId for clean-slate filtering
|
|
2629
|
-
const birthEvent =
|
|
2726
|
+
const birthEvent = s.edgeBirthEvent?.get(edgeKey);
|
|
2630
2727
|
|
|
2631
2728
|
// Collect all properties for this edge, filtering out stale props
|
|
2632
2729
|
// (props set before the edge's most recent re-add)
|
|
2730
|
+
/** @type {Record<string, any>} */
|
|
2633
2731
|
const props = {};
|
|
2634
|
-
for (const [propKey, register] of
|
|
2732
|
+
for (const [propKey, register] of s.prop) {
|
|
2635
2733
|
if (!isEdgePropKey(propKey)) {
|
|
2636
2734
|
continue;
|
|
2637
2735
|
}
|
|
@@ -2673,11 +2771,13 @@ export default class WarpGraph {
|
|
|
2673
2771
|
*/
|
|
2674
2772
|
async neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
|
|
2675
2773
|
await this._ensureFreshState();
|
|
2774
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2676
2775
|
|
|
2776
|
+
/** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
|
|
2677
2777
|
const neighbors = [];
|
|
2678
2778
|
|
|
2679
2779
|
// Iterate over all visible edges
|
|
2680
|
-
for (const edgeKey of orsetElements(
|
|
2780
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
2681
2781
|
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
2682
2782
|
|
|
2683
2783
|
// Filter by label if specified
|
|
@@ -2688,15 +2788,15 @@ export default class WarpGraph {
|
|
|
2688
2788
|
// Check edge direction and collect neighbors
|
|
2689
2789
|
if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
|
|
2690
2790
|
// Ensure target node is visible
|
|
2691
|
-
if (orsetContains(
|
|
2692
|
-
neighbors.push({ nodeId: to, label, direction: 'outgoing' });
|
|
2791
|
+
if (orsetContains(s.nodeAlive, to)) {
|
|
2792
|
+
neighbors.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
|
|
2693
2793
|
}
|
|
2694
2794
|
}
|
|
2695
2795
|
|
|
2696
2796
|
if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
|
|
2697
2797
|
// Ensure source node is visible
|
|
2698
|
-
if (orsetContains(
|
|
2699
|
-
neighbors.push({ nodeId: from, label, direction: 'incoming' });
|
|
2798
|
+
if (orsetContains(s.nodeAlive, from)) {
|
|
2799
|
+
neighbors.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
|
|
2700
2800
|
}
|
|
2701
2801
|
}
|
|
2702
2802
|
}
|
|
@@ -2721,7 +2821,8 @@ export default class WarpGraph {
|
|
|
2721
2821
|
*/
|
|
2722
2822
|
async getNodes() {
|
|
2723
2823
|
await this._ensureFreshState();
|
|
2724
|
-
|
|
2824
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2825
|
+
return [...orsetElements(s.nodeAlive)];
|
|
2725
2826
|
}
|
|
2726
2827
|
|
|
2727
2828
|
/**
|
|
@@ -2744,12 +2845,13 @@ export default class WarpGraph {
|
|
|
2744
2845
|
*/
|
|
2745
2846
|
async getEdges() {
|
|
2746
2847
|
await this._ensureFreshState();
|
|
2848
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2747
2849
|
|
|
2748
2850
|
// Pre-collect edge props into a lookup: "from\0to\0label" → {propKey: value}
|
|
2749
2851
|
// Filters out stale props using full EventId ordering via compareEventIds
|
|
2750
2852
|
// against the edge's birth EventId (clean-slate semantics on re-add)
|
|
2751
2853
|
const edgePropsByKey = new Map();
|
|
2752
|
-
for (const [propKey, register] of
|
|
2854
|
+
for (const [propKey, register] of s.prop) {
|
|
2753
2855
|
if (!isEdgePropKey(propKey)) {
|
|
2754
2856
|
continue;
|
|
2755
2857
|
}
|
|
@@ -2757,7 +2859,7 @@ export default class WarpGraph {
|
|
|
2757
2859
|
const ek = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
|
|
2758
2860
|
|
|
2759
2861
|
// Clean-slate filter: skip props from before the edge's current incarnation
|
|
2760
|
-
const birthEvent =
|
|
2862
|
+
const birthEvent = s.edgeBirthEvent?.get(ek);
|
|
2761
2863
|
if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
|
|
2762
2864
|
continue;
|
|
2763
2865
|
}
|
|
@@ -2771,11 +2873,11 @@ export default class WarpGraph {
|
|
|
2771
2873
|
}
|
|
2772
2874
|
|
|
2773
2875
|
const edges = [];
|
|
2774
|
-
for (const edgeKey of orsetElements(
|
|
2876
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
2775
2877
|
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
2776
2878
|
// Only include edges where both endpoints are visible
|
|
2777
|
-
if (orsetContains(
|
|
2778
|
-
orsetContains(
|
|
2879
|
+
if (orsetContains(s.nodeAlive, from) &&
|
|
2880
|
+
orsetContains(s.nodeAlive, to)) {
|
|
2779
2881
|
const props = edgePropsByKey.get(edgeKey) || {};
|
|
2780
2882
|
edges.push({ from, to, label, props });
|
|
2781
2883
|
}
|
|
@@ -2794,7 +2896,8 @@ export default class WarpGraph {
|
|
|
2794
2896
|
*/
|
|
2795
2897
|
async getPropertyCount() {
|
|
2796
2898
|
await this._ensureFreshState();
|
|
2797
|
-
|
|
2899
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2900
|
+
return s.prop.size;
|
|
2798
2901
|
}
|
|
2799
2902
|
|
|
2800
2903
|
// ============================================================================
|
|
@@ -2913,9 +3016,9 @@ export default class WarpGraph {
|
|
|
2913
3016
|
try {
|
|
2914
3017
|
validateGraphName(resolvedForkName);
|
|
2915
3018
|
} catch (err) {
|
|
2916
|
-
throw new ForkError(`Invalid fork name: ${err.message}`, {
|
|
3019
|
+
throw new ForkError(`Invalid fork name: ${/** @type {Error} */ (err).message}`, {
|
|
2917
3020
|
code: 'E_FORK_NAME_INVALID',
|
|
2918
|
-
context: { forkName: resolvedForkName, originalError: err.message },
|
|
3021
|
+
context: { forkName: resolvedForkName, originalError: /** @type {Error} */ (err).message },
|
|
2919
3022
|
});
|
|
2920
3023
|
}
|
|
2921
3024
|
|
|
@@ -2934,9 +3037,9 @@ export default class WarpGraph {
|
|
|
2934
3037
|
try {
|
|
2935
3038
|
validateWriterId(resolvedForkWriterId);
|
|
2936
3039
|
} catch (err) {
|
|
2937
|
-
throw new ForkError(`Invalid fork writer ID: ${err.message}`, {
|
|
3040
|
+
throw new ForkError(`Invalid fork writer ID: ${/** @type {Error} */ (err).message}`, {
|
|
2938
3041
|
code: 'E_FORK_WRITER_ID_INVALID',
|
|
2939
|
-
context: { forkWriterId: resolvedForkWriterId, originalError: err.message },
|
|
3042
|
+
context: { forkWriterId: resolvedForkWriterId, originalError: /** @type {Error} */ (err).message },
|
|
2940
3043
|
});
|
|
2941
3044
|
}
|
|
2942
3045
|
|
|
@@ -2951,10 +3054,10 @@ export default class WarpGraph {
|
|
|
2951
3054
|
writerId: resolvedForkWriterId,
|
|
2952
3055
|
gcPolicy: this._gcPolicy,
|
|
2953
3056
|
adjacencyCacheSize: this._adjacencyCache?.maxSize ?? DEFAULT_ADJACENCY_CACHE_SIZE,
|
|
2954
|
-
checkpointPolicy: this._checkpointPolicy,
|
|
3057
|
+
checkpointPolicy: this._checkpointPolicy || undefined,
|
|
2955
3058
|
autoMaterialize: this._autoMaterialize,
|
|
2956
3059
|
onDeleteWithData: this._onDeleteWithData,
|
|
2957
|
-
logger: this._logger,
|
|
3060
|
+
logger: this._logger || undefined,
|
|
2958
3061
|
clock: this._clock,
|
|
2959
3062
|
crypto: this._crypto,
|
|
2960
3063
|
codec: this._codec,
|
|
@@ -2966,7 +3069,7 @@ export default class WarpGraph {
|
|
|
2966
3069
|
|
|
2967
3070
|
return forkGraph;
|
|
2968
3071
|
} catch (err) {
|
|
2969
|
-
this._logTiming('fork', t0, { error: err });
|
|
3072
|
+
this._logTiming('fork', t0, { error: /** @type {Error} */ (err) });
|
|
2970
3073
|
throw err;
|
|
2971
3074
|
}
|
|
2972
3075
|
}
|
|
@@ -3020,13 +3123,13 @@ export default class WarpGraph {
|
|
|
3020
3123
|
const t0 = this._clock.now();
|
|
3021
3124
|
|
|
3022
3125
|
try {
|
|
3023
|
-
const wormhole = await createWormholeImpl({
|
|
3126
|
+
const wormhole = await createWormholeImpl(/** @type {any} */ ({ // TODO(ts-cleanup): needs options type
|
|
3024
3127
|
persistence: this._persistence,
|
|
3025
3128
|
graphName: this._graphName,
|
|
3026
3129
|
fromSha,
|
|
3027
3130
|
toSha,
|
|
3028
3131
|
codec: this._codec,
|
|
3029
|
-
});
|
|
3132
|
+
}));
|
|
3030
3133
|
|
|
3031
3134
|
this._logTiming('createWormhole', t0, {
|
|
3032
3135
|
metrics: `${wormhole.patchCount} patches from=${fromSha.slice(0, 7)} to=${toSha.slice(0, 7)}`,
|
|
@@ -3034,7 +3137,7 @@ export default class WarpGraph {
|
|
|
3034
3137
|
|
|
3035
3138
|
return wormhole;
|
|
3036
3139
|
} catch (err) {
|
|
3037
|
-
this._logTiming('createWormhole', t0, { error: err });
|
|
3140
|
+
this._logTiming('createWormhole', t0, { error: /** @type {Error} */ (err) });
|
|
3038
3141
|
throw err;
|
|
3039
3142
|
}
|
|
3040
3143
|
}
|
|
@@ -3068,6 +3171,12 @@ export default class WarpGraph {
|
|
|
3068
3171
|
async patchesFor(entityId) {
|
|
3069
3172
|
await this._ensureFreshState();
|
|
3070
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
|
+
|
|
3071
3180
|
if (!this._provenanceIndex) {
|
|
3072
3181
|
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
3073
3182
|
code: 'E_NO_STATE',
|
|
@@ -3129,6 +3238,12 @@ export default class WarpGraph {
|
|
|
3129
3238
|
// Ensure fresh state before accessing provenance index
|
|
3130
3239
|
await this._ensureFreshState();
|
|
3131
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
|
+
|
|
3132
3247
|
if (!this._provenanceIndex) {
|
|
3133
3248
|
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
3134
3249
|
code: 'E_NO_STATE',
|
|
@@ -3163,7 +3278,7 @@ export default class WarpGraph {
|
|
|
3163
3278
|
this._logTiming('materializeSlice', t0, { metrics: `${sortedPatches.length} patches` });
|
|
3164
3279
|
|
|
3165
3280
|
if (collectReceipts) {
|
|
3166
|
-
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 }));
|
|
3167
3282
|
return {
|
|
3168
3283
|
state: result.state,
|
|
3169
3284
|
patchCount: sortedPatches.length,
|
|
@@ -3177,7 +3292,7 @@ export default class WarpGraph {
|
|
|
3177
3292
|
patchCount: sortedPatches.length,
|
|
3178
3293
|
};
|
|
3179
3294
|
} catch (err) {
|
|
3180
|
-
this._logTiming('materializeSlice', t0, { error: err });
|
|
3295
|
+
this._logTiming('materializeSlice', t0, { error: /** @type {Error} */ (err) });
|
|
3181
3296
|
throw err;
|
|
3182
3297
|
}
|
|
3183
3298
|
}
|
|
@@ -3214,7 +3329,7 @@ export default class WarpGraph {
|
|
|
3214
3329
|
visited.add(entityId);
|
|
3215
3330
|
|
|
3216
3331
|
// Get all patches that affected this entity
|
|
3217
|
-
const patchShas = this._provenanceIndex.patchesFor(entityId);
|
|
3332
|
+
const patchShas = /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).patchesFor(entityId);
|
|
3218
3333
|
|
|
3219
3334
|
for (const sha of patchShas) {
|
|
3220
3335
|
if (cone.has(sha)) {
|
|
@@ -3226,8 +3341,9 @@ export default class WarpGraph {
|
|
|
3226
3341
|
cone.set(sha, patch);
|
|
3227
3342
|
|
|
3228
3343
|
// Add read dependencies to the queue
|
|
3229
|
-
|
|
3230
|
-
|
|
3344
|
+
const patchReads = /** @type {any} */ (patch)?.reads; // TODO(ts-cleanup): type patch array
|
|
3345
|
+
if (patchReads) {
|
|
3346
|
+
for (const readEntity of patchReads) {
|
|
3231
3347
|
if (!visited.has(readEntity)) {
|
|
3232
3348
|
queue.push(readEntity);
|
|
3233
3349
|
}
|
|
@@ -3274,7 +3390,7 @@ export default class WarpGraph {
|
|
|
3274
3390
|
|
|
3275
3391
|
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
3276
3392
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
3277
|
-
return this._codec.decode(patchBuffer);
|
|
3393
|
+
return /** @type {Object} */ (this._codec.decode(patchBuffer));
|
|
3278
3394
|
}
|
|
3279
3395
|
|
|
3280
3396
|
/**
|
|
@@ -3302,8 +3418,8 @@ export default class WarpGraph {
|
|
|
3302
3418
|
* Sort order: Lamport timestamp (ascending), then writer ID, then SHA.
|
|
3303
3419
|
* This ensures deterministic ordering regardless of discovery order.
|
|
3304
3420
|
*
|
|
3305
|
-
* @param {Array<{patch:
|
|
3306
|
-
* @returns {Array<{patch:
|
|
3421
|
+
* @param {Array<{patch: any, sha: string}>} patches - Unsorted patch entries
|
|
3422
|
+
* @returns {Array<{patch: any, sha: string}>} Sorted patch entries
|
|
3307
3423
|
* @private
|
|
3308
3424
|
*/
|
|
3309
3425
|
_sortPatchesCausally(patches) {
|