@git-stunts/git-warp 10.3.2 → 10.7.0
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/SECURITY.md +89 -1
- package/bin/warp-graph.js +574 -208
- package/index.d.ts +55 -0
- package/index.js +4 -0
- package/package.json +8 -4
- package/src/domain/WarpGraph.js +334 -161
- 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 +88 -19
- 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/SyncAuthService.js +396 -0
- 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 +25 -83
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
- 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/adapters/adapterValidation.js +90 -0
- 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 +187 -23
- 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,14 @@ 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 { signSyncRequest, canonicalizePath } from './services/SyncAuthService.js';
|
|
53
|
+
import { buildSeekCacheKey } from './utils/seekCacheKey.js';
|
|
51
54
|
import defaultClock from './utils/defaultClock.js';
|
|
52
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @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
|
|
58
|
+
*/
|
|
59
|
+
|
|
53
60
|
const DEFAULT_SYNC_SERVER_MAX_BYTES = 4 * 1024 * 1024;
|
|
54
61
|
const DEFAULT_SYNC_WITH_RETRIES = 3;
|
|
55
62
|
const DEFAULT_SYNC_WITH_BASE_DELAY_MS = 250;
|
|
@@ -71,6 +78,35 @@ function normalizeSyncPath(path) {
|
|
|
71
78
|
return path.startsWith('/') ? path : `/${path}`;
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Builds auth headers for an outgoing sync request if auth is configured.
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} params
|
|
85
|
+
* @param {{ secret: string, keyId?: string }|undefined} params.auth
|
|
86
|
+
* @param {string} params.bodyStr - Serialized request body
|
|
87
|
+
* @param {URL} params.targetUrl
|
|
88
|
+
* @param {import('../ports/CryptoPort.js').default} params.crypto
|
|
89
|
+
* @returns {Promise<Record<string, string>>}
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
async function buildSyncAuthHeaders({ auth, bodyStr, targetUrl, crypto }) {
|
|
93
|
+
if (!auth || !auth.secret) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
const bodyBuf = new TextEncoder().encode(bodyStr);
|
|
97
|
+
return await signSyncRequest(
|
|
98
|
+
{
|
|
99
|
+
method: 'POST',
|
|
100
|
+
path: canonicalizePath(targetUrl.pathname + (targetUrl.search || '')),
|
|
101
|
+
contentType: 'application/json',
|
|
102
|
+
body: bodyBuf,
|
|
103
|
+
secret: auth.secret,
|
|
104
|
+
keyId: auth.keyId || 'default',
|
|
105
|
+
},
|
|
106
|
+
{ crypto },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
74
110
|
const DEFAULT_ADJACENCY_CACHE_SIZE = 3;
|
|
75
111
|
|
|
76
112
|
/**
|
|
@@ -99,10 +135,11 @@ export default class WarpGraph {
|
|
|
99
135
|
* @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
|
|
100
136
|
* @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
|
|
101
137
|
* @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
|
|
138
|
+
* @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
|
|
102
139
|
*/
|
|
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;
|
|
140
|
+
constructor({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize = DEFAULT_ADJACENCY_CACHE_SIZE, checkpointPolicy, autoMaterialize = false, onDeleteWithData = 'warn', logger, clock, crypto, codec, seekCache }) {
|
|
141
|
+
/** @type {FullPersistence} */
|
|
142
|
+
this._persistence = /** @type {FullPersistence} */ (persistence);
|
|
106
143
|
|
|
107
144
|
/** @type {string} */
|
|
108
145
|
this._graphName = graphName;
|
|
@@ -146,7 +183,7 @@ export default class WarpGraph {
|
|
|
146
183
|
/** @type {MaterializedGraph|null} */
|
|
147
184
|
this._materializedGraph = null;
|
|
148
185
|
|
|
149
|
-
/** @type {import('./utils/LRUCache.js').default
|
|
186
|
+
/** @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
187
|
this._adjacencyCache = adjacencyCacheSize > 0 ? new LRUCache(adjacencyCacheSize) : null;
|
|
151
188
|
|
|
152
189
|
/** @type {Map<string, string>|null} */
|
|
@@ -158,8 +195,8 @@ export default class WarpGraph {
|
|
|
158
195
|
/** @type {import('../ports/ClockPort.js').default} */
|
|
159
196
|
this._clock = clock || defaultClock;
|
|
160
197
|
|
|
161
|
-
/** @type {import('../ports/CryptoPort.js').default
|
|
162
|
-
this._crypto = crypto;
|
|
198
|
+
/** @type {import('../ports/CryptoPort.js').default} */
|
|
199
|
+
this._crypto = crypto || defaultCrypto;
|
|
163
200
|
|
|
164
201
|
/** @type {import('../ports/CodecPort.js').default} */
|
|
165
202
|
this._codec = codec || defaultCodec;
|
|
@@ -167,7 +204,7 @@ export default class WarpGraph {
|
|
|
167
204
|
/** @type {'reject'|'cascade'|'warn'} */
|
|
168
205
|
this._onDeleteWithData = onDeleteWithData;
|
|
169
206
|
|
|
170
|
-
/** @type {Array<{onChange: Function, onError?: Function}>} */
|
|
207
|
+
/** @type {Array<{onChange: Function, onError?: Function, pendingReplay?: boolean}>} */
|
|
171
208
|
this._subscribers = [];
|
|
172
209
|
|
|
173
210
|
/** @type {import('./services/JoinReducer.js').WarpStateV5|null} */
|
|
@@ -187,6 +224,32 @@ export default class WarpGraph {
|
|
|
187
224
|
|
|
188
225
|
/** @type {Map<string, string>|null} */
|
|
189
226
|
this._cachedFrontier = null;
|
|
227
|
+
|
|
228
|
+
/** @type {import('../ports/SeekCachePort.js').default|null} */
|
|
229
|
+
this._seekCache = seekCache || null;
|
|
230
|
+
|
|
231
|
+
/** @type {boolean} */
|
|
232
|
+
this._provenanceDegraded = false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns the attached seek cache, or null if none is set.
|
|
237
|
+
* @returns {import('../ports/SeekCachePort.js').default|null}
|
|
238
|
+
*/
|
|
239
|
+
get seekCache() {
|
|
240
|
+
return this._seekCache;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Attaches a persistent seek cache after construction.
|
|
245
|
+
*
|
|
246
|
+
* Useful when the cache adapter cannot be created until after the
|
|
247
|
+
* graph is opened (e.g. the CLI wires it based on flags).
|
|
248
|
+
*
|
|
249
|
+
* @param {import('../ports/SeekCachePort.js').default} cache - SeekCachePort implementation
|
|
250
|
+
*/
|
|
251
|
+
setSeekCache(cache) {
|
|
252
|
+
this._seekCache = cache;
|
|
190
253
|
}
|
|
191
254
|
|
|
192
255
|
/**
|
|
@@ -227,6 +290,7 @@ export default class WarpGraph {
|
|
|
227
290
|
* @param {import('../ports/ClockPort.js').default} [options.clock] - Clock for timing instrumentation (defaults to performance-based clock)
|
|
228
291
|
* @param {import('../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for hashing
|
|
229
292
|
* @param {import('../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
|
|
293
|
+
* @param {import('../ports/SeekCachePort.js').default} [options.seekCache] - Persistent cache for seek materialization (optional)
|
|
230
294
|
* @returns {Promise<WarpGraph>} The opened graph instance
|
|
231
295
|
* @throws {Error} If graphName, writerId, checkpointPolicy, or onDeleteWithData is invalid
|
|
232
296
|
*
|
|
@@ -237,7 +301,7 @@ export default class WarpGraph {
|
|
|
237
301
|
* writerId: 'node-1'
|
|
238
302
|
* });
|
|
239
303
|
*/
|
|
240
|
-
static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec }) {
|
|
304
|
+
static async open({ persistence, graphName, writerId, gcPolicy = {}, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache }) {
|
|
241
305
|
// Validate inputs
|
|
242
306
|
validateGraphName(graphName);
|
|
243
307
|
validateWriterId(writerId);
|
|
@@ -269,7 +333,7 @@ export default class WarpGraph {
|
|
|
269
333
|
}
|
|
270
334
|
}
|
|
271
335
|
|
|
272
|
-
const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec });
|
|
336
|
+
const graph = new WarpGraph({ persistence, graphName, writerId, gcPolicy, adjacencyCacheSize, checkpointPolicy, autoMaterialize, onDeleteWithData, logger, clock, crypto, codec, seekCache });
|
|
273
337
|
|
|
274
338
|
// Validate migration boundary
|
|
275
339
|
await graph._validateMigrationBoundary();
|
|
@@ -295,7 +359,7 @@ export default class WarpGraph {
|
|
|
295
359
|
|
|
296
360
|
/**
|
|
297
361
|
* Gets the persistence adapter.
|
|
298
|
-
* @returns {
|
|
362
|
+
* @returns {FullPersistence} The persistence adapter
|
|
299
363
|
*/
|
|
300
364
|
get persistence() {
|
|
301
365
|
return this._persistence;
|
|
@@ -337,9 +401,9 @@ export default class WarpGraph {
|
|
|
337
401
|
getCurrentState: () => this._cachedState,
|
|
338
402
|
expectedParentSha: parentSha,
|
|
339
403
|
onDeleteWithData: this._onDeleteWithData,
|
|
340
|
-
onCommitSuccess: (opts) => this._onPatchCommitted(this._writerId, opts),
|
|
404
|
+
onCommitSuccess: (/** @type {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(this._writerId, opts),
|
|
341
405
|
codec: this._codec,
|
|
342
|
-
logger: this._logger,
|
|
406
|
+
logger: this._logger || undefined,
|
|
343
407
|
});
|
|
344
408
|
}
|
|
345
409
|
|
|
@@ -348,7 +412,7 @@ export default class WarpGraph {
|
|
|
348
412
|
*
|
|
349
413
|
* @param {string} writerId - The writer ID to load patches for
|
|
350
414
|
* @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
|
|
351
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
415
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
|
|
352
416
|
*/
|
|
353
417
|
async getWriterPatches(writerId, stopAtSha = null) {
|
|
354
418
|
return await this._loadWriterPatches(writerId, stopAtSha);
|
|
@@ -399,7 +463,7 @@ export default class WarpGraph {
|
|
|
399
463
|
*
|
|
400
464
|
* @param {string} writerId - The writer ID to load patches for
|
|
401
465
|
* @param {string|null} [stopAtSha=null] - Stop walking when reaching this SHA (exclusive)
|
|
402
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
466
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Array of patches
|
|
403
467
|
* @private
|
|
404
468
|
*/
|
|
405
469
|
async _loadWriterPatches(writerId, stopAtSha = null) {
|
|
@@ -430,7 +494,7 @@ export default class WarpGraph {
|
|
|
430
494
|
|
|
431
495
|
// Read the patch blob
|
|
432
496
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
433
|
-
const patch = this._codec.decode(patchBuffer);
|
|
497
|
+
const patch = /** @type {import('./types/WarpTypesV2.js').PatchV2} */ (this._codec.decode(patchBuffer));
|
|
434
498
|
|
|
435
499
|
patches.push({ patch, sha: currentSha });
|
|
436
500
|
|
|
@@ -474,8 +538,8 @@ export default class WarpGraph {
|
|
|
474
538
|
incoming.get(to).push({ neighborId: from, label });
|
|
475
539
|
}
|
|
476
540
|
|
|
477
|
-
const sortNeighbors = (list) => {
|
|
478
|
-
list.sort((a, b) => {
|
|
541
|
+
const sortNeighbors = (/** @type {Array<{neighborId: string, label: string}>} */ list) => {
|
|
542
|
+
list.sort((/** @type {{neighborId: string, label: string}} */ a, /** @type {{neighborId: string, label: string}} */ b) => {
|
|
479
543
|
if (a.neighborId !== b.neighborId) {
|
|
480
544
|
return a.neighborId < b.neighborId ? -1 : 1;
|
|
481
545
|
}
|
|
@@ -529,7 +593,7 @@ export default class WarpGraph {
|
|
|
529
593
|
* provenance index, and frontier tracking.
|
|
530
594
|
*
|
|
531
595
|
* @param {string} writerId - The writer ID that committed the patch
|
|
532
|
-
* @param {{patch?:
|
|
596
|
+
* @param {{patch?: import('./types/WarpTypesV2.js').PatchV2, sha?: string}} [opts] - Commit details
|
|
533
597
|
* @private
|
|
534
598
|
*/
|
|
535
599
|
async _onPatchCommitted(writerId, { patch, sha } = {}) {
|
|
@@ -538,11 +602,11 @@ export default class WarpGraph {
|
|
|
538
602
|
// Eager re-materialize: apply the just-committed patch to cached state
|
|
539
603
|
// Only when the cache is clean — applying a patch to stale state would be incorrect
|
|
540
604
|
if (this._cachedState && !this._stateDirty && patch && sha) {
|
|
541
|
-
joinPatch(this._cachedState, patch, sha);
|
|
605
|
+
joinPatch(this._cachedState, /** @type {any} */ (patch), sha); // TODO(ts-cleanup): type patch array
|
|
542
606
|
await this._setMaterializedState(this._cachedState);
|
|
543
607
|
// Update provenance index with new patch
|
|
544
608
|
if (this._provenanceIndex) {
|
|
545
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
609
|
+
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
|
|
546
610
|
}
|
|
547
611
|
// Keep _lastFrontier in sync so hasFrontierChanged() won't misreport stale
|
|
548
612
|
if (this._lastFrontier) {
|
|
@@ -561,9 +625,9 @@ export default class WarpGraph {
|
|
|
561
625
|
async _materializeGraph() {
|
|
562
626
|
const state = await this.materialize();
|
|
563
627
|
if (!this._materializedGraph || this._materializedGraph.state !== state) {
|
|
564
|
-
await this._setMaterializedState(state);
|
|
628
|
+
await this._setMaterializedState(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (state));
|
|
565
629
|
}
|
|
566
|
-
return this._materializedGraph;
|
|
630
|
+
return /** @type {MaterializedGraph} */ (this._materializedGraph);
|
|
567
631
|
}
|
|
568
632
|
|
|
569
633
|
/**
|
|
@@ -602,14 +666,16 @@ export default class WarpGraph {
|
|
|
602
666
|
|
|
603
667
|
// When ceiling is active, delegate to ceiling-aware path (with its own cache)
|
|
604
668
|
if (ceiling !== null) {
|
|
605
|
-
return await this._materializeWithCeiling(ceiling, collectReceipts, t0);
|
|
669
|
+
return await this._materializeWithCeiling(ceiling, !!collectReceipts, t0);
|
|
606
670
|
}
|
|
607
671
|
|
|
608
672
|
try {
|
|
609
673
|
// Check for checkpoint
|
|
610
674
|
const checkpoint = await this._loadLatestCheckpoint();
|
|
611
675
|
|
|
676
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
|
|
612
677
|
let state;
|
|
678
|
+
/** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
613
679
|
let receipts;
|
|
614
680
|
let patchCount = 0;
|
|
615
681
|
|
|
@@ -617,20 +683,21 @@ export default class WarpGraph {
|
|
|
617
683
|
if (checkpoint?.schema === 2 || checkpoint?.schema === 3) {
|
|
618
684
|
const patches = await this._loadPatchesSince(checkpoint);
|
|
619
685
|
if (collectReceipts) {
|
|
620
|
-
const result = reduceV5(patches, checkpoint.state, { receipts: true });
|
|
686
|
+
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
687
|
state = result.state;
|
|
622
688
|
receipts = result.receipts;
|
|
623
689
|
} else {
|
|
624
|
-
state = reduceV5(patches, checkpoint.state);
|
|
690
|
+
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
691
|
}
|
|
626
692
|
patchCount = patches.length;
|
|
627
693
|
|
|
628
694
|
// Build provenance index: start from checkpoint index if present, then add new patches
|
|
629
|
-
|
|
630
|
-
|
|
695
|
+
const ckPI = /** @type {any} */ (checkpoint).provenanceIndex; // TODO(ts-cleanup): type checkpoint cast
|
|
696
|
+
this._provenanceIndex = ckPI
|
|
697
|
+
? ckPI.clone()
|
|
631
698
|
: new ProvenanceIndex();
|
|
632
699
|
for (const { patch, sha } of patches) {
|
|
633
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
700
|
+
/** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).addPatch(sha, patch.reads, patch.writes);
|
|
634
701
|
}
|
|
635
702
|
} else {
|
|
636
703
|
// 1. Discover all writers
|
|
@@ -661,11 +728,11 @@ export default class WarpGraph {
|
|
|
661
728
|
} else {
|
|
662
729
|
// 5. Reduce all patches to state
|
|
663
730
|
if (collectReceipts) {
|
|
664
|
-
const result = reduceV5(allPatches, undefined, { receipts: true });
|
|
731
|
+
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
732
|
state = result.state;
|
|
666
733
|
receipts = result.receipts;
|
|
667
734
|
} else {
|
|
668
|
-
state = reduceV5(allPatches);
|
|
735
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
|
|
669
736
|
}
|
|
670
737
|
patchCount = allPatches.length;
|
|
671
738
|
|
|
@@ -679,6 +746,7 @@ export default class WarpGraph {
|
|
|
679
746
|
}
|
|
680
747
|
|
|
681
748
|
await this._setMaterializedState(state);
|
|
749
|
+
this._provenanceDegraded = false;
|
|
682
750
|
this._cachedCeiling = null;
|
|
683
751
|
this._cachedFrontier = null;
|
|
684
752
|
this._lastFrontier = await this.getFrontier();
|
|
@@ -712,11 +780,11 @@ export default class WarpGraph {
|
|
|
712
780
|
this._logTiming('materialize', t0, { metrics: `${patchCount} patches` });
|
|
713
781
|
|
|
714
782
|
if (collectReceipts) {
|
|
715
|
-
return { state, receipts };
|
|
783
|
+
return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
|
|
716
784
|
}
|
|
717
785
|
return state;
|
|
718
786
|
} catch (err) {
|
|
719
|
-
this._logTiming('materialize', t0, { error: err });
|
|
787
|
+
this._logTiming('materialize', t0, { error: /** @type {Error} */ (err) });
|
|
720
788
|
throw err;
|
|
721
789
|
}
|
|
722
790
|
}
|
|
@@ -770,12 +838,13 @@ export default class WarpGraph {
|
|
|
770
838
|
|
|
771
839
|
// Cache hit: same ceiling, clean state, AND frontier unchanged.
|
|
772
840
|
// Bypass cache when collectReceipts is true — cached path has no receipts.
|
|
841
|
+
const cf = this._cachedFrontier;
|
|
773
842
|
if (
|
|
774
843
|
this._cachedState && !this._stateDirty &&
|
|
775
844
|
ceiling === this._cachedCeiling && !collectReceipts &&
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
[...frontier].every(([w, sha]) =>
|
|
845
|
+
cf !== null &&
|
|
846
|
+
cf.size === frontier.size &&
|
|
847
|
+
[...frontier].every(([w, sha]) => cf.get(w) === sha)
|
|
779
848
|
) {
|
|
780
849
|
return this._cachedState;
|
|
781
850
|
}
|
|
@@ -785,6 +854,7 @@ export default class WarpGraph {
|
|
|
785
854
|
if (writerIds.length === 0 || ceiling <= 0) {
|
|
786
855
|
const state = createEmptyStateV5();
|
|
787
856
|
this._provenanceIndex = new ProvenanceIndex();
|
|
857
|
+
this._provenanceDegraded = false;
|
|
788
858
|
await this._setMaterializedState(state);
|
|
789
859
|
this._cachedCeiling = ceiling;
|
|
790
860
|
this._cachedFrontier = frontier;
|
|
@@ -795,6 +865,32 @@ export default class WarpGraph {
|
|
|
795
865
|
return state;
|
|
796
866
|
}
|
|
797
867
|
|
|
868
|
+
// Persistent cache check — skip when collectReceipts is requested
|
|
869
|
+
let cacheKey;
|
|
870
|
+
if (this._seekCache && !collectReceipts) {
|
|
871
|
+
cacheKey = buildSeekCacheKey(ceiling, frontier);
|
|
872
|
+
try {
|
|
873
|
+
const cached = await this._seekCache.get(cacheKey);
|
|
874
|
+
if (cached) {
|
|
875
|
+
try {
|
|
876
|
+
const state = deserializeFullStateV5(cached, { codec: this._codec });
|
|
877
|
+
this._provenanceIndex = new ProvenanceIndex();
|
|
878
|
+
this._provenanceDegraded = true;
|
|
879
|
+
await this._setMaterializedState(state);
|
|
880
|
+
this._cachedCeiling = ceiling;
|
|
881
|
+
this._cachedFrontier = frontier;
|
|
882
|
+
this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
|
|
883
|
+
return state;
|
|
884
|
+
} catch {
|
|
885
|
+
// Corrupted payload — self-heal by removing the bad entry
|
|
886
|
+
try { await this._seekCache.delete(cacheKey); } catch { /* best-effort */ }
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
// Cache read failed — fall through to full materialization
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
798
894
|
const allPatches = [];
|
|
799
895
|
for (const writerId of writerIds) {
|
|
800
896
|
const writerPatches = await this._loadWriterPatches(writerId);
|
|
@@ -805,7 +901,9 @@ export default class WarpGraph {
|
|
|
805
901
|
}
|
|
806
902
|
}
|
|
807
903
|
|
|
904
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5|undefined} */
|
|
808
905
|
let state;
|
|
906
|
+
/** @type {import('./types/TickReceipt.js').TickReceipt[]|undefined} */
|
|
809
907
|
let receipts;
|
|
810
908
|
|
|
811
909
|
if (allPatches.length === 0) {
|
|
@@ -814,27 +912,37 @@ export default class WarpGraph {
|
|
|
814
912
|
receipts = [];
|
|
815
913
|
}
|
|
816
914
|
} else if (collectReceipts) {
|
|
817
|
-
const result = reduceV5(allPatches, undefined, { receipts: true });
|
|
915
|
+
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
916
|
state = result.state;
|
|
819
917
|
receipts = result.receipts;
|
|
820
918
|
} else {
|
|
821
|
-
state = reduceV5(allPatches);
|
|
919
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {any} */ (allPatches))); // TODO(ts-cleanup): type patch array
|
|
822
920
|
}
|
|
823
921
|
|
|
824
922
|
this._provenanceIndex = new ProvenanceIndex();
|
|
825
923
|
for (const { patch, sha } of allPatches) {
|
|
826
|
-
this._provenanceIndex.addPatch(sha, patch.reads, patch.writes);
|
|
924
|
+
this._provenanceIndex.addPatch(sha, /** @type {string[]|undefined} */ (patch.reads), /** @type {string[]|undefined} */ (patch.writes));
|
|
827
925
|
}
|
|
926
|
+
this._provenanceDegraded = false;
|
|
828
927
|
|
|
829
928
|
await this._setMaterializedState(state);
|
|
830
929
|
this._cachedCeiling = ceiling;
|
|
831
930
|
this._cachedFrontier = frontier;
|
|
832
931
|
|
|
932
|
+
// Store to persistent cache (fire-and-forget — failure is non-fatal)
|
|
933
|
+
if (this._seekCache && !collectReceipts && allPatches.length > 0) {
|
|
934
|
+
if (!cacheKey) {
|
|
935
|
+
cacheKey = buildSeekCacheKey(ceiling, frontier);
|
|
936
|
+
}
|
|
937
|
+
const buf = serializeFullStateV5(state, { codec: this._codec });
|
|
938
|
+
this._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
|
|
939
|
+
}
|
|
940
|
+
|
|
833
941
|
// Skip auto-checkpoint and GC — this is an exploratory read
|
|
834
942
|
this._logTiming('materialize', t0, { metrics: `${allPatches.length} patches (ceiling=${ceiling})` });
|
|
835
943
|
|
|
836
944
|
if (collectReceipts) {
|
|
837
|
-
return { state, receipts };
|
|
945
|
+
return { state, receipts: /** @type {import('./types/TickReceipt.js').TickReceipt[]} */ (receipts) };
|
|
838
946
|
}
|
|
839
947
|
return state;
|
|
840
948
|
}
|
|
@@ -883,16 +991,16 @@ export default class WarpGraph {
|
|
|
883
991
|
}
|
|
884
992
|
|
|
885
993
|
// Capture pre-merge counts for receipt
|
|
886
|
-
const beforeNodes = this._cachedState.nodeAlive.
|
|
887
|
-
const beforeEdges = this._cachedState.edgeAlive.
|
|
994
|
+
const beforeNodes = orsetElements(this._cachedState.nodeAlive).length;
|
|
995
|
+
const beforeEdges = orsetElements(this._cachedState.edgeAlive).length;
|
|
888
996
|
const beforeFrontierSize = this._cachedState.observedFrontier.size;
|
|
889
997
|
|
|
890
998
|
// Perform the join
|
|
891
999
|
const mergedState = joinStates(this._cachedState, otherState);
|
|
892
1000
|
|
|
893
1001
|
// Calculate receipt
|
|
894
|
-
const afterNodes = mergedState.nodeAlive.
|
|
895
|
-
const afterEdges = mergedState.edgeAlive.
|
|
1002
|
+
const afterNodes = orsetElements(mergedState.nodeAlive).length;
|
|
1003
|
+
const afterEdges = orsetElements(mergedState.edgeAlive).length;
|
|
896
1004
|
const afterFrontierSize = mergedState.observedFrontier.size;
|
|
897
1005
|
|
|
898
1006
|
// Count property changes (keys that existed in both but have different values)
|
|
@@ -954,7 +1062,7 @@ export default class WarpGraph {
|
|
|
954
1062
|
* @example
|
|
955
1063
|
* // Time-travel to a previous checkpoint
|
|
956
1064
|
* const oldState = await graph.materializeAt('abc123');
|
|
957
|
-
* console.log('Nodes at checkpoint:',
|
|
1065
|
+
* console.log('Nodes at checkpoint:', orsetElements(oldState.nodeAlive));
|
|
958
1066
|
*/
|
|
959
1067
|
async materializeAt(checkpointSha) {
|
|
960
1068
|
// 1. Discover current writers to build target frontier
|
|
@@ -971,7 +1079,7 @@ export default class WarpGraph {
|
|
|
971
1079
|
}
|
|
972
1080
|
|
|
973
1081
|
// 3. Create a patch loader function for incremental materialization
|
|
974
|
-
const patchLoader = async (writerId, fromSha, toSha) => {
|
|
1082
|
+
const patchLoader = async (/** @type {string} */ writerId, /** @type {string|null} */ fromSha, /** @type {string} */ toSha) => {
|
|
975
1083
|
// Load patches from fromSha (exclusive) to toSha (inclusive)
|
|
976
1084
|
// Walk from toSha back to fromSha
|
|
977
1085
|
const patches = [];
|
|
@@ -1004,7 +1112,7 @@ export default class WarpGraph {
|
|
|
1004
1112
|
|
|
1005
1113
|
// 4. Call materializeIncremental with the checkpoint and target frontier
|
|
1006
1114
|
const state = await materializeIncremental({
|
|
1007
|
-
persistence: this._persistence,
|
|
1115
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
1008
1116
|
graphName: this._graphName,
|
|
1009
1117
|
checkpointSha,
|
|
1010
1118
|
targetFrontier,
|
|
@@ -1048,23 +1156,24 @@ export default class WarpGraph {
|
|
|
1048
1156
|
// 3. Materialize current state (reuse cached if fresh, guard against recursion)
|
|
1049
1157
|
const prevCheckpointing = this._checkpointing;
|
|
1050
1158
|
this._checkpointing = true;
|
|
1159
|
+
/** @type {import('./services/JoinReducer.js').WarpStateV5} */
|
|
1051
1160
|
let state;
|
|
1052
1161
|
try {
|
|
1053
|
-
state = (this._cachedState && !this._stateDirty)
|
|
1162
|
+
state = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ ((this._cachedState && !this._stateDirty)
|
|
1054
1163
|
? this._cachedState
|
|
1055
|
-
: await this.materialize();
|
|
1164
|
+
: await this.materialize());
|
|
1056
1165
|
} finally {
|
|
1057
1166
|
this._checkpointing = prevCheckpointing;
|
|
1058
1167
|
}
|
|
1059
1168
|
|
|
1060
1169
|
// 4. Call CheckpointService.create() with provenance index if available
|
|
1061
1170
|
const checkpointSha = await createCheckpointCommit({
|
|
1062
|
-
persistence: this._persistence,
|
|
1171
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
1063
1172
|
graphName: this._graphName,
|
|
1064
1173
|
state,
|
|
1065
1174
|
frontier,
|
|
1066
1175
|
parents,
|
|
1067
|
-
provenanceIndex: this._provenanceIndex,
|
|
1176
|
+
provenanceIndex: this._provenanceIndex || undefined,
|
|
1068
1177
|
crypto: this._crypto,
|
|
1069
1178
|
codec: this._codec,
|
|
1070
1179
|
});
|
|
@@ -1078,7 +1187,7 @@ export default class WarpGraph {
|
|
|
1078
1187
|
// 6. Return checkpoint SHA
|
|
1079
1188
|
return checkpointSha;
|
|
1080
1189
|
} catch (err) {
|
|
1081
|
-
this._logTiming('createCheckpoint', t0, { error: err });
|
|
1190
|
+
this._logTiming('createCheckpoint', t0, { error: /** @type {Error} */ (err) });
|
|
1082
1191
|
throw err;
|
|
1083
1192
|
}
|
|
1084
1193
|
}
|
|
@@ -1172,6 +1281,7 @@ export default class WarpGraph {
|
|
|
1172
1281
|
*/
|
|
1173
1282
|
async discoverTicks() {
|
|
1174
1283
|
const writerIds = await this.discoverWriters();
|
|
1284
|
+
/** @type {Set<number>} */
|
|
1175
1285
|
const globalTickSet = new Set();
|
|
1176
1286
|
const perWriter = new Map();
|
|
1177
1287
|
|
|
@@ -1179,6 +1289,7 @@ export default class WarpGraph {
|
|
|
1179
1289
|
const writerRef = buildWriterRef(this._graphName, writerId);
|
|
1180
1290
|
const tipSha = await this._persistence.readRef(writerRef);
|
|
1181
1291
|
const writerTicks = [];
|
|
1292
|
+
/** @type {Record<number, string>} */
|
|
1182
1293
|
const tickShas = {};
|
|
1183
1294
|
|
|
1184
1295
|
if (tipSha) {
|
|
@@ -1256,7 +1367,7 @@ export default class WarpGraph {
|
|
|
1256
1367
|
/**
|
|
1257
1368
|
* Loads the latest checkpoint for this graph.
|
|
1258
1369
|
*
|
|
1259
|
-
* @returns {Promise<{state:
|
|
1370
|
+
* @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
1371
|
* @private
|
|
1261
1372
|
*/
|
|
1262
1373
|
async _loadLatestCheckpoint() {
|
|
@@ -1298,7 +1409,7 @@ export default class WarpGraph {
|
|
|
1298
1409
|
if (kind === 'patch') {
|
|
1299
1410
|
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
1300
1411
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
1301
|
-
const patch = this._codec.decode(patchBuffer);
|
|
1412
|
+
const patch = /** @type {{schema?: number}} */ (this._codec.decode(patchBuffer));
|
|
1302
1413
|
|
|
1303
1414
|
// If any patch has schema:1, we have v1 history
|
|
1304
1415
|
if (patch.schema === 1 || patch.schema === undefined) {
|
|
@@ -1313,8 +1424,8 @@ export default class WarpGraph {
|
|
|
1313
1424
|
/**
|
|
1314
1425
|
* Loads patches since a checkpoint for incremental materialization.
|
|
1315
1426
|
*
|
|
1316
|
-
* @param {{state:
|
|
1317
|
-
* @returns {Promise<Array<{patch: import('./types/
|
|
1427
|
+
* @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to start from
|
|
1428
|
+
* @returns {Promise<Array<{patch: import('./types/WarpTypesV2.js').PatchV2, sha: string}>>} Patches since checkpoint
|
|
1318
1429
|
* @private
|
|
1319
1430
|
*/
|
|
1320
1431
|
async _loadPatchesSince(checkpoint) {
|
|
@@ -1396,7 +1507,7 @@ export default class WarpGraph {
|
|
|
1396
1507
|
*
|
|
1397
1508
|
* @param {string} writerId - The writer ID for this patch
|
|
1398
1509
|
* @param {string} incomingSha - The incoming patch commit SHA
|
|
1399
|
-
* @param {{state:
|
|
1510
|
+
* @param {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, stateHash: string, schema: number}} checkpoint - The checkpoint to validate against
|
|
1400
1511
|
* @returns {Promise<void>}
|
|
1401
1512
|
* @throws {Error} If patch is behind/same as checkpoint frontier (backfill rejected)
|
|
1402
1513
|
* @throws {Error} If patch does not extend checkpoint head (writer fork detected)
|
|
@@ -1444,18 +1555,19 @@ export default class WarpGraph {
|
|
|
1444
1555
|
_maybeRunGC(state) {
|
|
1445
1556
|
try {
|
|
1446
1557
|
const metrics = collectGCMetrics(state);
|
|
1558
|
+
/** @type {import('./services/GCPolicy.js').GCInputMetrics} */
|
|
1447
1559
|
const inputMetrics = {
|
|
1448
1560
|
...metrics,
|
|
1449
1561
|
patchesSinceCompaction: this._patchesSinceGC,
|
|
1450
1562
|
timeSinceCompaction: Date.now() - this._lastGCTime,
|
|
1451
1563
|
};
|
|
1452
|
-
const { shouldRun, reasons } = shouldRunGC(inputMetrics, this._gcPolicy);
|
|
1564
|
+
const { shouldRun, reasons } = shouldRunGC(inputMetrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
|
|
1453
1565
|
|
|
1454
1566
|
if (!shouldRun) {
|
|
1455
1567
|
return;
|
|
1456
1568
|
}
|
|
1457
1569
|
|
|
1458
|
-
if (this._gcPolicy.enabled) {
|
|
1570
|
+
if (/** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy).enabled) {
|
|
1459
1571
|
const appliedVV = computeAppliedVV(state);
|
|
1460
1572
|
const result = executeGC(state, appliedVV);
|
|
1461
1573
|
this._lastGCTime = Date.now();
|
|
@@ -1494,11 +1606,15 @@ export default class WarpGraph {
|
|
|
1494
1606
|
return { ran: false, result: null, reasons: [] };
|
|
1495
1607
|
}
|
|
1496
1608
|
|
|
1497
|
-
const
|
|
1498
|
-
|
|
1499
|
-
metrics
|
|
1609
|
+
const rawMetrics = collectGCMetrics(this._cachedState);
|
|
1610
|
+
/** @type {import('./services/GCPolicy.js').GCInputMetrics} */
|
|
1611
|
+
const metrics = {
|
|
1612
|
+
...rawMetrics,
|
|
1613
|
+
patchesSinceCompaction: this._patchesSinceGC,
|
|
1614
|
+
timeSinceCompaction: this._lastGCTime > 0 ? Date.now() - this._lastGCTime : 0,
|
|
1615
|
+
};
|
|
1500
1616
|
|
|
1501
|
-
const { shouldRun, reasons } = shouldRunGC(metrics, this._gcPolicy);
|
|
1617
|
+
const { shouldRun, reasons } = shouldRunGC(metrics, /** @type {import('./services/GCPolicy.js').GCPolicy} */ (this._gcPolicy));
|
|
1502
1618
|
|
|
1503
1619
|
if (!shouldRun) {
|
|
1504
1620
|
return { ran: false, result: null, reasons: [] };
|
|
@@ -1545,7 +1661,7 @@ export default class WarpGraph {
|
|
|
1545
1661
|
|
|
1546
1662
|
return result;
|
|
1547
1663
|
} catch (err) {
|
|
1548
|
-
this._logTiming('runGC', t0, { error: err });
|
|
1664
|
+
this._logTiming('runGC', t0, { error: /** @type {Error} */ (err) });
|
|
1549
1665
|
throw err;
|
|
1550
1666
|
}
|
|
1551
1667
|
}
|
|
@@ -1567,10 +1683,15 @@ export default class WarpGraph {
|
|
|
1567
1683
|
return null;
|
|
1568
1684
|
}
|
|
1569
1685
|
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1686
|
+
const rawMetrics = collectGCMetrics(this._cachedState);
|
|
1687
|
+
return {
|
|
1688
|
+
...rawMetrics,
|
|
1689
|
+
nodeCount: rawMetrics.nodeLiveDots,
|
|
1690
|
+
edgeCount: rawMetrics.edgeLiveDots,
|
|
1691
|
+
tombstoneCount: rawMetrics.totalTombstones,
|
|
1692
|
+
patchesSinceCompaction: this._patchesSinceGC,
|
|
1693
|
+
lastCompactionTime: this._lastGCTime,
|
|
1694
|
+
};
|
|
1574
1695
|
}
|
|
1575
1696
|
|
|
1576
1697
|
/**
|
|
@@ -1653,6 +1774,7 @@ export default class WarpGraph {
|
|
|
1653
1774
|
*/
|
|
1654
1775
|
async status() {
|
|
1655
1776
|
// Determine cachedState
|
|
1777
|
+
/** @type {'fresh' | 'stale' | 'none'} */
|
|
1656
1778
|
let cachedState;
|
|
1657
1779
|
if (this._cachedState === null) {
|
|
1658
1780
|
cachedState = 'none';
|
|
@@ -1702,7 +1824,7 @@ export default class WarpGraph {
|
|
|
1702
1824
|
* One handler's error does not prevent other handlers from being called.
|
|
1703
1825
|
*
|
|
1704
1826
|
* @param {Object} options - Subscription options
|
|
1705
|
-
* @param {(diff: import('./services/StateDiff.js').
|
|
1827
|
+
* @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with diff when graph changes
|
|
1706
1828
|
* @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
|
|
1707
1829
|
* @param {boolean} [options.replay=false] - If true, immediately fires onChange with initial state diff
|
|
1708
1830
|
* @returns {{unsubscribe: () => void}} Subscription handle
|
|
@@ -1745,7 +1867,7 @@ export default class WarpGraph {
|
|
|
1745
1867
|
} catch (err) {
|
|
1746
1868
|
if (onError) {
|
|
1747
1869
|
try {
|
|
1748
|
-
onError(err);
|
|
1870
|
+
onError(/** @type {Error} */ (err));
|
|
1749
1871
|
} catch {
|
|
1750
1872
|
// onError itself threw — swallow to prevent cascade
|
|
1751
1873
|
}
|
|
@@ -1782,7 +1904,7 @@ export default class WarpGraph {
|
|
|
1782
1904
|
*
|
|
1783
1905
|
* @param {string} pattern - Glob pattern (e.g., 'user:*', 'order:123', '*')
|
|
1784
1906
|
* @param {Object} options - Watch options
|
|
1785
|
-
* @param {(diff: import('./services/StateDiff.js').
|
|
1907
|
+
* @param {(diff: import('./services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with filtered diff when matching changes occur
|
|
1786
1908
|
* @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
|
|
1787
1909
|
* @param {number} [options.poll] - Poll interval in ms (min 1000); checks frontier and auto-materializes
|
|
1788
1910
|
* @returns {{unsubscribe: () => void}} Subscription handle
|
|
@@ -1823,31 +1945,32 @@ export default class WarpGraph {
|
|
|
1823
1945
|
|
|
1824
1946
|
// Pattern matching: same logic as QueryBuilder.match()
|
|
1825
1947
|
// Pre-compile pattern matcher once for performance
|
|
1948
|
+
/** @type {(nodeId: string) => boolean} */
|
|
1826
1949
|
let matchesPattern;
|
|
1827
1950
|
if (pattern === '*') {
|
|
1828
1951
|
matchesPattern = () => true;
|
|
1829
1952
|
} else if (pattern.includes('*')) {
|
|
1830
1953
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
1831
1954
|
const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
|
|
1832
|
-
matchesPattern = (nodeId) => regex.test(nodeId);
|
|
1955
|
+
matchesPattern = (/** @type {string} */ nodeId) => regex.test(nodeId);
|
|
1833
1956
|
} else {
|
|
1834
|
-
matchesPattern = (nodeId) => nodeId === pattern;
|
|
1957
|
+
matchesPattern = (/** @type {string} */ nodeId) => nodeId === pattern;
|
|
1835
1958
|
}
|
|
1836
1959
|
|
|
1837
1960
|
// Filtered onChange that only passes matching changes
|
|
1838
|
-
const filteredOnChange = (diff) => {
|
|
1961
|
+
const filteredOnChange = (/** @type {import('./services/StateDiff.js').StateDiffResult} */ diff) => {
|
|
1839
1962
|
const filteredDiff = {
|
|
1840
1963
|
nodes: {
|
|
1841
1964
|
added: diff.nodes.added.filter(matchesPattern),
|
|
1842
1965
|
removed: diff.nodes.removed.filter(matchesPattern),
|
|
1843
1966
|
},
|
|
1844
1967
|
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)),
|
|
1968
|
+
added: diff.edges.added.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
|
|
1969
|
+
removed: diff.edges.removed.filter((/** @type {import('./services/StateDiff.js').EdgeChange} */ e) => matchesPattern(e.from) || matchesPattern(e.to)),
|
|
1847
1970
|
},
|
|
1848
1971
|
props: {
|
|
1849
|
-
set: diff.props.set.filter(p => matchesPattern(p.nodeId)),
|
|
1850
|
-
removed: diff.props.removed.filter(p => matchesPattern(p.nodeId)),
|
|
1972
|
+
set: diff.props.set.filter((/** @type {import('./services/StateDiff.js').PropSet} */ p) => matchesPattern(p.nodeId)),
|
|
1973
|
+
removed: diff.props.removed.filter((/** @type {import('./services/StateDiff.js').PropRemoved} */ p) => matchesPattern(p.nodeId)),
|
|
1851
1974
|
},
|
|
1852
1975
|
};
|
|
1853
1976
|
|
|
@@ -1869,6 +1992,7 @@ export default class WarpGraph {
|
|
|
1869
1992
|
const subscription = this.subscribe({ onChange: filteredOnChange, onError });
|
|
1870
1993
|
|
|
1871
1994
|
// Polling: periodically check frontier and auto-materialize if changed
|
|
1995
|
+
/** @type {ReturnType<typeof setInterval>|null} */
|
|
1872
1996
|
let pollIntervalId = null;
|
|
1873
1997
|
let pollInFlight = false;
|
|
1874
1998
|
if (poll) {
|
|
@@ -1950,7 +2074,7 @@ export default class WarpGraph {
|
|
|
1950
2074
|
* Creates a sync request to send to a remote peer.
|
|
1951
2075
|
* The request contains the local frontier for comparison.
|
|
1952
2076
|
*
|
|
1953
|
-
* @returns {Promise<
|
|
2077
|
+
* @returns {Promise<import('./services/SyncProtocol.js').SyncRequest>} The sync request
|
|
1954
2078
|
* @throws {Error} If listing refs fails
|
|
1955
2079
|
*
|
|
1956
2080
|
* @example
|
|
@@ -1965,8 +2089,8 @@ export default class WarpGraph {
|
|
|
1965
2089
|
/**
|
|
1966
2090
|
* Processes an incoming sync request and returns patches the requester needs.
|
|
1967
2091
|
*
|
|
1968
|
-
* @param {
|
|
1969
|
-
* @returns {Promise<
|
|
2092
|
+
* @param {import('./services/SyncProtocol.js').SyncRequest} request - The incoming sync request
|
|
2093
|
+
* @returns {Promise<import('./services/SyncProtocol.js').SyncResponse>} The sync response
|
|
1970
2094
|
* @throws {Error} If listing refs or reading patches fails
|
|
1971
2095
|
*
|
|
1972
2096
|
* @example
|
|
@@ -1979,7 +2103,7 @@ export default class WarpGraph {
|
|
|
1979
2103
|
return await processSyncRequest(
|
|
1980
2104
|
request,
|
|
1981
2105
|
localFrontier,
|
|
1982
|
-
this._persistence,
|
|
2106
|
+
/** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
1983
2107
|
this._graphName,
|
|
1984
2108
|
{ codec: this._codec }
|
|
1985
2109
|
);
|
|
@@ -1991,8 +2115,8 @@ export default class WarpGraph {
|
|
|
1991
2115
|
*
|
|
1992
2116
|
* **Requires a cached state.**
|
|
1993
2117
|
*
|
|
1994
|
-
* @param {
|
|
1995
|
-
* @returns {{state:
|
|
2118
|
+
* @param {import('./services/SyncProtocol.js').SyncResponse} response - The sync response
|
|
2119
|
+
* @returns {{state: import('./services/JoinReducer.js').WarpStateV5, applied: number}} Result with updated state
|
|
1996
2120
|
* @throws {QueryError} If no cached state exists (code: `E_NO_STATE`)
|
|
1997
2121
|
*
|
|
1998
2122
|
* @example
|
|
@@ -2007,8 +2131,8 @@ export default class WarpGraph {
|
|
|
2007
2131
|
});
|
|
2008
2132
|
}
|
|
2009
2133
|
|
|
2010
|
-
const currentFrontier = this._cachedState.observedFrontier;
|
|
2011
|
-
const result = applySyncResponse(response, this._cachedState, currentFrontier);
|
|
2134
|
+
const currentFrontier = /** @type {any} */ (this._cachedState.observedFrontier); // TODO(ts-cleanup): narrow port type
|
|
2135
|
+
const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, applied: number}} */ (applySyncResponse(response, this._cachedState, currentFrontier));
|
|
2012
2136
|
|
|
2013
2137
|
// Update cached state
|
|
2014
2138
|
this._cachedState = result.state;
|
|
@@ -2047,18 +2171,15 @@ export default class WarpGraph {
|
|
|
2047
2171
|
* @param {string|WarpGraph} remote - URL or peer graph instance
|
|
2048
2172
|
* @param {Object} [options]
|
|
2049
2173
|
* @param {string} [options.path='/sync'] - Sync path (HTTP mode)
|
|
2050
|
-
* @param {number} [options.retries=3] - Retry count
|
|
2174
|
+
* @param {number} [options.retries=3] - Retry count
|
|
2051
2175
|
* @param {number} [options.baseDelayMs=250] - Base backoff delay
|
|
2052
2176
|
* @param {number} [options.maxDelayMs=2000] - Max backoff delay
|
|
2053
|
-
* @param {number} [options.timeoutMs=10000] - Request timeout
|
|
2054
|
-
* @param {AbortSignal} [options.signal] -
|
|
2177
|
+
* @param {number} [options.timeoutMs=10000] - Request timeout
|
|
2178
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
2055
2179
|
* @param {(event: {type: string, attempt: number, durationMs?: number, status?: number, error?: Error}) => void} [options.onStatus]
|
|
2056
|
-
* @param {boolean} [options.materialize=false] -
|
|
2180
|
+
* @param {boolean} [options.materialize=false] - Auto-materialize after sync
|
|
2181
|
+
* @param {{ secret: string, keyId?: string }} [options.auth] - Client auth credentials
|
|
2057
2182
|
* @returns {Promise<{applied: number, attempts: number, state?: import('./services/JoinReducer.js').WarpStateV5}>}
|
|
2058
|
-
* @throws {SyncError} If remote URL is invalid (code: `E_SYNC_REMOTE_URL`)
|
|
2059
|
-
* @throws {SyncError} If remote returns error or invalid response (code: `E_SYNC_REMOTE`, `E_SYNC_PROTOCOL`)
|
|
2060
|
-
* @throws {SyncError} If request times out (code: `E_SYNC_TIMEOUT`)
|
|
2061
|
-
* @throws {OperationAbortedError} If abort signal fires
|
|
2062
2183
|
*/
|
|
2063
2184
|
async syncWith(remote, options = {}) {
|
|
2064
2185
|
const t0 = this._clock.now();
|
|
@@ -2071,6 +2192,7 @@ export default class WarpGraph {
|
|
|
2071
2192
|
signal,
|
|
2072
2193
|
onStatus,
|
|
2073
2194
|
materialize: materializeAfterSync = false,
|
|
2195
|
+
auth,
|
|
2074
2196
|
} = options;
|
|
2075
2197
|
|
|
2076
2198
|
const hasPathOverride = Object.prototype.hasOwnProperty.call(options, 'path');
|
|
@@ -2079,7 +2201,7 @@ export default class WarpGraph {
|
|
|
2079
2201
|
let targetUrl = null;
|
|
2080
2202
|
if (!isDirectPeer) {
|
|
2081
2203
|
try {
|
|
2082
|
-
targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(remote);
|
|
2204
|
+
targetUrl = remote instanceof URL ? new URL(remote.toString()) : new URL(/** @type {string} */ (remote));
|
|
2083
2205
|
} catch {
|
|
2084
2206
|
throw new SyncError('Invalid remote URL', {
|
|
2085
2207
|
code: 'E_SYNC_REMOTE_URL',
|
|
@@ -2102,31 +2224,26 @@ export default class WarpGraph {
|
|
|
2102
2224
|
}
|
|
2103
2225
|
targetUrl.hash = '';
|
|
2104
2226
|
}
|
|
2105
|
-
|
|
2106
2227
|
let attempt = 0;
|
|
2107
|
-
const emit = (type, payload = {}) => {
|
|
2228
|
+
const emit = (/** @type {string} */ type, /** @type {Record<string, any>} */ payload = {}) => {
|
|
2108
2229
|
if (typeof onStatus === 'function') {
|
|
2109
|
-
onStatus({ type, attempt, ...payload });
|
|
2230
|
+
onStatus(/** @type {any} */ ({ type, attempt, ...payload })); // TODO(ts-cleanup): type sync protocol
|
|
2110
2231
|
}
|
|
2111
2232
|
};
|
|
2112
|
-
|
|
2113
|
-
const shouldRetry = (err) => {
|
|
2233
|
+
const shouldRetry = (/** @type {any} */ err) => { // TODO(ts-cleanup): type error
|
|
2114
2234
|
if (isDirectPeer) { return false; }
|
|
2115
2235
|
if (err instanceof SyncError) {
|
|
2116
2236
|
return ['E_SYNC_REMOTE', 'E_SYNC_TIMEOUT', 'E_SYNC_NETWORK'].includes(err.code);
|
|
2117
2237
|
}
|
|
2118
2238
|
return err instanceof TimeoutError;
|
|
2119
2239
|
};
|
|
2120
|
-
|
|
2121
2240
|
const executeAttempt = async () => {
|
|
2122
2241
|
checkAborted(signal, 'syncWith');
|
|
2123
2242
|
attempt += 1;
|
|
2124
2243
|
const attemptStart = Date.now();
|
|
2125
2244
|
emit('connecting');
|
|
2126
|
-
|
|
2127
2245
|
const request = await this.createSyncRequest();
|
|
2128
2246
|
emit('requestBuilt');
|
|
2129
|
-
|
|
2130
2247
|
let response;
|
|
2131
2248
|
if (isDirectPeer) {
|
|
2132
2249
|
emit('requestSent');
|
|
@@ -2134,24 +2251,29 @@ export default class WarpGraph {
|
|
|
2134
2251
|
emit('responseReceived');
|
|
2135
2252
|
} else {
|
|
2136
2253
|
emit('requestSent');
|
|
2254
|
+
const bodyStr = JSON.stringify(request);
|
|
2255
|
+
const authHeaders = await buildSyncAuthHeaders({
|
|
2256
|
+
auth, bodyStr, targetUrl: /** @type {URL} */ (targetUrl), crypto: this._crypto,
|
|
2257
|
+
});
|
|
2137
2258
|
let res;
|
|
2138
2259
|
try {
|
|
2139
2260
|
res = await timeout(timeoutMs, (timeoutSignal) => {
|
|
2140
2261
|
const combinedSignal = signal
|
|
2141
2262
|
? AbortSignal.any([timeoutSignal, signal])
|
|
2142
2263
|
: timeoutSignal;
|
|
2143
|
-
return fetch(targetUrl.toString(), {
|
|
2264
|
+
return fetch(/** @type {URL} */ (targetUrl).toString(), {
|
|
2144
2265
|
method: 'POST',
|
|
2145
2266
|
headers: {
|
|
2146
2267
|
'content-type': 'application/json',
|
|
2147
2268
|
'accept': 'application/json',
|
|
2269
|
+
...authHeaders,
|
|
2148
2270
|
},
|
|
2149
|
-
body:
|
|
2271
|
+
body: bodyStr,
|
|
2150
2272
|
signal: combinedSignal,
|
|
2151
2273
|
});
|
|
2152
2274
|
});
|
|
2153
2275
|
} catch (err) {
|
|
2154
|
-
if (err?.name === 'AbortError') {
|
|
2276
|
+
if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
|
|
2155
2277
|
throw new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
2156
2278
|
}
|
|
2157
2279
|
if (err instanceof TimeoutError) {
|
|
@@ -2162,7 +2284,7 @@ export default class WarpGraph {
|
|
|
2162
2284
|
}
|
|
2163
2285
|
throw new SyncError('Network error', {
|
|
2164
2286
|
code: 'E_SYNC_NETWORK',
|
|
2165
|
-
context: { message: err?.message },
|
|
2287
|
+
context: { message: /** @type {any} */ (err)?.message }, // TODO(ts-cleanup): type error
|
|
2166
2288
|
});
|
|
2167
2289
|
}
|
|
2168
2290
|
|
|
@@ -2223,9 +2345,9 @@ export default class WarpGraph {
|
|
|
2223
2345
|
jitter: 'decorrelated',
|
|
2224
2346
|
signal,
|
|
2225
2347
|
shouldRetry,
|
|
2226
|
-
onRetry: (error, attemptNumber, delayMs) => {
|
|
2348
|
+
onRetry: (/** @type {Error} */ error, /** @type {number} */ attemptNumber, /** @type {number} */ delayMs) => {
|
|
2227
2349
|
if (typeof onStatus === 'function') {
|
|
2228
|
-
onStatus({ type: 'retrying', attempt: attemptNumber, delayMs, error });
|
|
2350
|
+
onStatus(/** @type {any} */ ({ type: 'retrying', attempt: attemptNumber, delayMs, error })); // TODO(ts-cleanup): type sync protocol
|
|
2229
2351
|
}
|
|
2230
2352
|
},
|
|
2231
2353
|
});
|
|
@@ -2234,12 +2356,12 @@ export default class WarpGraph {
|
|
|
2234
2356
|
|
|
2235
2357
|
if (materializeAfterSync) {
|
|
2236
2358
|
if (!this._cachedState) { await this.materialize(); }
|
|
2237
|
-
return { ...syncResult, state: this._cachedState };
|
|
2359
|
+
return { ...syncResult, state: /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState) };
|
|
2238
2360
|
}
|
|
2239
2361
|
return syncResult;
|
|
2240
2362
|
} catch (err) {
|
|
2241
|
-
this._logTiming('syncWith', t0, { error: err });
|
|
2242
|
-
if (err?.name === 'AbortError') {
|
|
2363
|
+
this._logTiming('syncWith', t0, { error: /** @type {Error} */ (err) });
|
|
2364
|
+
if (/** @type {any} */ (err)?.name === 'AbortError') { // TODO(ts-cleanup): type error
|
|
2243
2365
|
const abortedError = new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
2244
2366
|
if (typeof onStatus === 'function') {
|
|
2245
2367
|
onStatus({ type: 'failed', attempt, error: abortedError });
|
|
@@ -2247,14 +2369,14 @@ export default class WarpGraph {
|
|
|
2247
2369
|
throw abortedError;
|
|
2248
2370
|
}
|
|
2249
2371
|
if (err instanceof RetryExhaustedError) {
|
|
2250
|
-
const cause = err.cause || err;
|
|
2372
|
+
const cause = /** @type {Error} */ (err.cause || err);
|
|
2251
2373
|
if (typeof onStatus === 'function') {
|
|
2252
2374
|
onStatus({ type: 'failed', attempt: err.attempts, error: cause });
|
|
2253
2375
|
}
|
|
2254
2376
|
throw cause;
|
|
2255
2377
|
}
|
|
2256
2378
|
if (typeof onStatus === 'function') {
|
|
2257
|
-
onStatus({ type: 'failed', attempt, error: err });
|
|
2379
|
+
onStatus({ type: 'failed', attempt, error: /** @type {Error} */ (err) });
|
|
2258
2380
|
}
|
|
2259
2381
|
throw err;
|
|
2260
2382
|
}
|
|
@@ -2269,11 +2391,12 @@ export default class WarpGraph {
|
|
|
2269
2391
|
* @param {string} [options.path='/sync'] - Path to handle sync requests
|
|
2270
2392
|
* @param {number} [options.maxRequestBytes=4194304] - Max request size in bytes
|
|
2271
2393
|
* @param {import('../ports/HttpServerPort.js').default} options.httpPort - HTTP server adapter (required)
|
|
2394
|
+
* @param {{ keys: Record<string, string>, mode?: 'enforce'|'log-only' }} [options.auth] - Auth configuration
|
|
2272
2395
|
* @returns {Promise<{close: () => Promise<void>, url: string}>} Server handle
|
|
2273
2396
|
* @throws {Error} If port is not a number
|
|
2274
2397
|
* @throws {Error} If httpPort adapter is not provided
|
|
2275
2398
|
*/
|
|
2276
|
-
async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort } = {}) {
|
|
2399
|
+
async serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort, auth } = /** @type {any} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
2277
2400
|
if (typeof port !== 'number') {
|
|
2278
2401
|
throw new Error('serve() requires a numeric port');
|
|
2279
2402
|
}
|
|
@@ -2281,12 +2404,17 @@ export default class WarpGraph {
|
|
|
2281
2404
|
throw new Error('serve() requires an httpPort adapter');
|
|
2282
2405
|
}
|
|
2283
2406
|
|
|
2407
|
+
const authConfig = auth
|
|
2408
|
+
? { ...auth, crypto: this._crypto, logger: this._logger || undefined }
|
|
2409
|
+
: undefined;
|
|
2410
|
+
|
|
2284
2411
|
const httpServer = new HttpSyncServer({
|
|
2285
2412
|
httpPort,
|
|
2286
2413
|
graph: this,
|
|
2287
2414
|
path,
|
|
2288
2415
|
host,
|
|
2289
2416
|
maxRequestBytes,
|
|
2417
|
+
auth: authConfig,
|
|
2290
2418
|
});
|
|
2291
2419
|
|
|
2292
2420
|
return await httpServer.listen(port);
|
|
@@ -2318,8 +2446,8 @@ export default class WarpGraph {
|
|
|
2318
2446
|
*/
|
|
2319
2447
|
async writer(writerId) {
|
|
2320
2448
|
// 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);
|
|
2449
|
+
const configGet = async (/** @type {string} */ key) => await this._persistence.configGet(key);
|
|
2450
|
+
const configSet = async (/** @type {string} */ key, /** @type {string} */ value) => await this._persistence.configSet(key, value);
|
|
2323
2451
|
|
|
2324
2452
|
// Resolve the writer ID
|
|
2325
2453
|
const resolvedWriterId = await resolveWriterId({
|
|
@@ -2330,13 +2458,13 @@ export default class WarpGraph {
|
|
|
2330
2458
|
});
|
|
2331
2459
|
|
|
2332
2460
|
return new Writer({
|
|
2333
|
-
persistence: this._persistence,
|
|
2461
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
2334
2462
|
graphName: this._graphName,
|
|
2335
2463
|
writerId: resolvedWriterId,
|
|
2336
2464
|
versionVector: this._versionVector,
|
|
2337
|
-
getCurrentState: () => this._cachedState,
|
|
2465
|
+
getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
|
|
2338
2466
|
onDeleteWithData: this._onDeleteWithData,
|
|
2339
|
-
onCommitSuccess: (opts) => this._onPatchCommitted(resolvedWriterId, opts),
|
|
2467
|
+
onCommitSuccess: (/** @type {any} */ opts) => this._onPatchCommitted(resolvedWriterId, opts), // TODO(ts-cleanup): type sync protocol
|
|
2340
2468
|
codec: this._codec,
|
|
2341
2469
|
});
|
|
2342
2470
|
}
|
|
@@ -2384,13 +2512,13 @@ export default class WarpGraph {
|
|
|
2384
2512
|
}
|
|
2385
2513
|
|
|
2386
2514
|
return new Writer({
|
|
2387
|
-
persistence: this._persistence,
|
|
2515
|
+
persistence: /** @type {any} */ (this._persistence), // TODO(ts-cleanup): narrow port type
|
|
2388
2516
|
graphName: this._graphName,
|
|
2389
2517
|
writerId: freshWriterId,
|
|
2390
2518
|
versionVector: this._versionVector,
|
|
2391
|
-
getCurrentState: () => this._cachedState,
|
|
2519
|
+
getCurrentState: () => /** @type {any} */ (this._cachedState), // TODO(ts-cleanup): narrow port type
|
|
2392
2520
|
onDeleteWithData: this._onDeleteWithData,
|
|
2393
|
-
onCommitSuccess: (commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts),
|
|
2521
|
+
onCommitSuccess: (/** @type {any} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts), // TODO(ts-cleanup): type sync protocol
|
|
2394
2522
|
codec: this._codec,
|
|
2395
2523
|
});
|
|
2396
2524
|
}
|
|
@@ -2545,7 +2673,8 @@ export default class WarpGraph {
|
|
|
2545
2673
|
*/
|
|
2546
2674
|
async hasNode(nodeId) {
|
|
2547
2675
|
await this._ensureFreshState();
|
|
2548
|
-
|
|
2676
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2677
|
+
return orsetContains(s.nodeAlive, nodeId);
|
|
2549
2678
|
}
|
|
2550
2679
|
|
|
2551
2680
|
/**
|
|
@@ -2570,15 +2699,16 @@ export default class WarpGraph {
|
|
|
2570
2699
|
*/
|
|
2571
2700
|
async getNodeProps(nodeId) {
|
|
2572
2701
|
await this._ensureFreshState();
|
|
2702
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2573
2703
|
|
|
2574
2704
|
// Check if node exists
|
|
2575
|
-
if (!orsetContains(
|
|
2705
|
+
if (!orsetContains(s.nodeAlive, nodeId)) {
|
|
2576
2706
|
return null;
|
|
2577
2707
|
}
|
|
2578
2708
|
|
|
2579
2709
|
// Collect all properties for this node
|
|
2580
2710
|
const props = new Map();
|
|
2581
|
-
for (const [propKey, register] of
|
|
2711
|
+
for (const [propKey, register] of s.prop) {
|
|
2582
2712
|
const decoded = decodePropKey(propKey);
|
|
2583
2713
|
if (decoded.nodeId === nodeId) {
|
|
2584
2714
|
props.set(decoded.propKey, register.value);
|
|
@@ -2612,26 +2742,28 @@ export default class WarpGraph {
|
|
|
2612
2742
|
*/
|
|
2613
2743
|
async getEdgeProps(from, to, label) {
|
|
2614
2744
|
await this._ensureFreshState();
|
|
2745
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2615
2746
|
|
|
2616
2747
|
// Check if edge exists
|
|
2617
2748
|
const edgeKey = encodeEdgeKey(from, to, label);
|
|
2618
|
-
if (!orsetContains(
|
|
2749
|
+
if (!orsetContains(s.edgeAlive, edgeKey)) {
|
|
2619
2750
|
return null;
|
|
2620
2751
|
}
|
|
2621
2752
|
|
|
2622
2753
|
// Check node liveness for both endpoints
|
|
2623
|
-
if (!orsetContains(
|
|
2624
|
-
!orsetContains(
|
|
2754
|
+
if (!orsetContains(s.nodeAlive, from) ||
|
|
2755
|
+
!orsetContains(s.nodeAlive, to)) {
|
|
2625
2756
|
return null;
|
|
2626
2757
|
}
|
|
2627
2758
|
|
|
2628
2759
|
// Determine the birth EventId for clean-slate filtering
|
|
2629
|
-
const birthEvent =
|
|
2760
|
+
const birthEvent = s.edgeBirthEvent?.get(edgeKey);
|
|
2630
2761
|
|
|
2631
2762
|
// Collect all properties for this edge, filtering out stale props
|
|
2632
2763
|
// (props set before the edge's most recent re-add)
|
|
2764
|
+
/** @type {Record<string, any>} */
|
|
2633
2765
|
const props = {};
|
|
2634
|
-
for (const [propKey, register] of
|
|
2766
|
+
for (const [propKey, register] of s.prop) {
|
|
2635
2767
|
if (!isEdgePropKey(propKey)) {
|
|
2636
2768
|
continue;
|
|
2637
2769
|
}
|
|
@@ -2673,11 +2805,13 @@ export default class WarpGraph {
|
|
|
2673
2805
|
*/
|
|
2674
2806
|
async neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
|
|
2675
2807
|
await this._ensureFreshState();
|
|
2808
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2676
2809
|
|
|
2810
|
+
/** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
|
|
2677
2811
|
const neighbors = [];
|
|
2678
2812
|
|
|
2679
2813
|
// Iterate over all visible edges
|
|
2680
|
-
for (const edgeKey of orsetElements(
|
|
2814
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
2681
2815
|
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
2682
2816
|
|
|
2683
2817
|
// Filter by label if specified
|
|
@@ -2688,15 +2822,15 @@ export default class WarpGraph {
|
|
|
2688
2822
|
// Check edge direction and collect neighbors
|
|
2689
2823
|
if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
|
|
2690
2824
|
// Ensure target node is visible
|
|
2691
|
-
if (orsetContains(
|
|
2692
|
-
neighbors.push({ nodeId: to, label, direction: 'outgoing' });
|
|
2825
|
+
if (orsetContains(s.nodeAlive, to)) {
|
|
2826
|
+
neighbors.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
|
|
2693
2827
|
}
|
|
2694
2828
|
}
|
|
2695
2829
|
|
|
2696
2830
|
if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
|
|
2697
2831
|
// Ensure source node is visible
|
|
2698
|
-
if (orsetContains(
|
|
2699
|
-
neighbors.push({ nodeId: from, label, direction: 'incoming' });
|
|
2832
|
+
if (orsetContains(s.nodeAlive, from)) {
|
|
2833
|
+
neighbors.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
|
|
2700
2834
|
}
|
|
2701
2835
|
}
|
|
2702
2836
|
}
|
|
@@ -2704,6 +2838,29 @@ export default class WarpGraph {
|
|
|
2704
2838
|
return neighbors;
|
|
2705
2839
|
}
|
|
2706
2840
|
|
|
2841
|
+
/**
|
|
2842
|
+
* Returns a defensive copy of the current materialized state.
|
|
2843
|
+
*
|
|
2844
|
+
* The returned object is a shallow clone: top-level ORSet, LWW, and
|
|
2845
|
+
* VersionVector instances are copied so that mutations by the caller
|
|
2846
|
+
* cannot corrupt the internal cache.
|
|
2847
|
+
*
|
|
2848
|
+
* **Requires a cached state.** Call materialize() first if not already cached.
|
|
2849
|
+
*
|
|
2850
|
+
* @returns {Promise<import('./services/JoinReducer.js').WarpStateV5 | null>}
|
|
2851
|
+
* Cloned state, or null if no state has been materialized yet.
|
|
2852
|
+
*/
|
|
2853
|
+
async getStateSnapshot() {
|
|
2854
|
+
if (!this._cachedState && !this._autoMaterialize) {
|
|
2855
|
+
return null;
|
|
2856
|
+
}
|
|
2857
|
+
await this._ensureFreshState();
|
|
2858
|
+
if (!this._cachedState) {
|
|
2859
|
+
return null;
|
|
2860
|
+
}
|
|
2861
|
+
return cloneStateV5(/** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState));
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2707
2864
|
/**
|
|
2708
2865
|
* Gets all visible nodes in the materialized state.
|
|
2709
2866
|
*
|
|
@@ -2721,7 +2878,8 @@ export default class WarpGraph {
|
|
|
2721
2878
|
*/
|
|
2722
2879
|
async getNodes() {
|
|
2723
2880
|
await this._ensureFreshState();
|
|
2724
|
-
|
|
2881
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2882
|
+
return [...orsetElements(s.nodeAlive)];
|
|
2725
2883
|
}
|
|
2726
2884
|
|
|
2727
2885
|
/**
|
|
@@ -2744,12 +2902,13 @@ export default class WarpGraph {
|
|
|
2744
2902
|
*/
|
|
2745
2903
|
async getEdges() {
|
|
2746
2904
|
await this._ensureFreshState();
|
|
2905
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2747
2906
|
|
|
2748
2907
|
// Pre-collect edge props into a lookup: "from\0to\0label" → {propKey: value}
|
|
2749
2908
|
// Filters out stale props using full EventId ordering via compareEventIds
|
|
2750
2909
|
// against the edge's birth EventId (clean-slate semantics on re-add)
|
|
2751
2910
|
const edgePropsByKey = new Map();
|
|
2752
|
-
for (const [propKey, register] of
|
|
2911
|
+
for (const [propKey, register] of s.prop) {
|
|
2753
2912
|
if (!isEdgePropKey(propKey)) {
|
|
2754
2913
|
continue;
|
|
2755
2914
|
}
|
|
@@ -2757,7 +2916,7 @@ export default class WarpGraph {
|
|
|
2757
2916
|
const ek = encodeEdgeKey(decoded.from, decoded.to, decoded.label);
|
|
2758
2917
|
|
|
2759
2918
|
// Clean-slate filter: skip props from before the edge's current incarnation
|
|
2760
|
-
const birthEvent =
|
|
2919
|
+
const birthEvent = s.edgeBirthEvent?.get(ek);
|
|
2761
2920
|
if (birthEvent && register.eventId && compareEventIds(register.eventId, birthEvent) < 0) {
|
|
2762
2921
|
continue;
|
|
2763
2922
|
}
|
|
@@ -2771,11 +2930,11 @@ export default class WarpGraph {
|
|
|
2771
2930
|
}
|
|
2772
2931
|
|
|
2773
2932
|
const edges = [];
|
|
2774
|
-
for (const edgeKey of orsetElements(
|
|
2933
|
+
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
2775
2934
|
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
2776
2935
|
// Only include edges where both endpoints are visible
|
|
2777
|
-
if (orsetContains(
|
|
2778
|
-
orsetContains(
|
|
2936
|
+
if (orsetContains(s.nodeAlive, from) &&
|
|
2937
|
+
orsetContains(s.nodeAlive, to)) {
|
|
2779
2938
|
const props = edgePropsByKey.get(edgeKey) || {};
|
|
2780
2939
|
edges.push({ from, to, label, props });
|
|
2781
2940
|
}
|
|
@@ -2794,7 +2953,8 @@ export default class WarpGraph {
|
|
|
2794
2953
|
*/
|
|
2795
2954
|
async getPropertyCount() {
|
|
2796
2955
|
await this._ensureFreshState();
|
|
2797
|
-
|
|
2956
|
+
const s = /** @type {import('./services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
2957
|
+
return s.prop.size;
|
|
2798
2958
|
}
|
|
2799
2959
|
|
|
2800
2960
|
// ============================================================================
|
|
@@ -2913,9 +3073,9 @@ export default class WarpGraph {
|
|
|
2913
3073
|
try {
|
|
2914
3074
|
validateGraphName(resolvedForkName);
|
|
2915
3075
|
} catch (err) {
|
|
2916
|
-
throw new ForkError(`Invalid fork name: ${err.message}`, {
|
|
3076
|
+
throw new ForkError(`Invalid fork name: ${/** @type {Error} */ (err).message}`, {
|
|
2917
3077
|
code: 'E_FORK_NAME_INVALID',
|
|
2918
|
-
context: { forkName: resolvedForkName, originalError: err.message },
|
|
3078
|
+
context: { forkName: resolvedForkName, originalError: /** @type {Error} */ (err).message },
|
|
2919
3079
|
});
|
|
2920
3080
|
}
|
|
2921
3081
|
|
|
@@ -2934,9 +3094,9 @@ export default class WarpGraph {
|
|
|
2934
3094
|
try {
|
|
2935
3095
|
validateWriterId(resolvedForkWriterId);
|
|
2936
3096
|
} catch (err) {
|
|
2937
|
-
throw new ForkError(`Invalid fork writer ID: ${err.message}`, {
|
|
3097
|
+
throw new ForkError(`Invalid fork writer ID: ${/** @type {Error} */ (err).message}`, {
|
|
2938
3098
|
code: 'E_FORK_WRITER_ID_INVALID',
|
|
2939
|
-
context: { forkWriterId: resolvedForkWriterId, originalError: err.message },
|
|
3099
|
+
context: { forkWriterId: resolvedForkWriterId, originalError: /** @type {Error} */ (err).message },
|
|
2940
3100
|
});
|
|
2941
3101
|
}
|
|
2942
3102
|
|
|
@@ -2951,10 +3111,10 @@ export default class WarpGraph {
|
|
|
2951
3111
|
writerId: resolvedForkWriterId,
|
|
2952
3112
|
gcPolicy: this._gcPolicy,
|
|
2953
3113
|
adjacencyCacheSize: this._adjacencyCache?.maxSize ?? DEFAULT_ADJACENCY_CACHE_SIZE,
|
|
2954
|
-
checkpointPolicy: this._checkpointPolicy,
|
|
3114
|
+
checkpointPolicy: this._checkpointPolicy || undefined,
|
|
2955
3115
|
autoMaterialize: this._autoMaterialize,
|
|
2956
3116
|
onDeleteWithData: this._onDeleteWithData,
|
|
2957
|
-
logger: this._logger,
|
|
3117
|
+
logger: this._logger || undefined,
|
|
2958
3118
|
clock: this._clock,
|
|
2959
3119
|
crypto: this._crypto,
|
|
2960
3120
|
codec: this._codec,
|
|
@@ -2966,7 +3126,7 @@ export default class WarpGraph {
|
|
|
2966
3126
|
|
|
2967
3127
|
return forkGraph;
|
|
2968
3128
|
} catch (err) {
|
|
2969
|
-
this._logTiming('fork', t0, { error: err });
|
|
3129
|
+
this._logTiming('fork', t0, { error: /** @type {Error} */ (err) });
|
|
2970
3130
|
throw err;
|
|
2971
3131
|
}
|
|
2972
3132
|
}
|
|
@@ -3020,13 +3180,13 @@ export default class WarpGraph {
|
|
|
3020
3180
|
const t0 = this._clock.now();
|
|
3021
3181
|
|
|
3022
3182
|
try {
|
|
3023
|
-
const wormhole = await createWormholeImpl({
|
|
3183
|
+
const wormhole = await createWormholeImpl(/** @type {any} */ ({ // TODO(ts-cleanup): needs options type
|
|
3024
3184
|
persistence: this._persistence,
|
|
3025
3185
|
graphName: this._graphName,
|
|
3026
3186
|
fromSha,
|
|
3027
3187
|
toSha,
|
|
3028
3188
|
codec: this._codec,
|
|
3029
|
-
});
|
|
3189
|
+
}));
|
|
3030
3190
|
|
|
3031
3191
|
this._logTiming('createWormhole', t0, {
|
|
3032
3192
|
metrics: `${wormhole.patchCount} patches from=${fromSha.slice(0, 7)} to=${toSha.slice(0, 7)}`,
|
|
@@ -3034,7 +3194,7 @@ export default class WarpGraph {
|
|
|
3034
3194
|
|
|
3035
3195
|
return wormhole;
|
|
3036
3196
|
} catch (err) {
|
|
3037
|
-
this._logTiming('createWormhole', t0, { error: err });
|
|
3197
|
+
this._logTiming('createWormhole', t0, { error: /** @type {Error} */ (err) });
|
|
3038
3198
|
throw err;
|
|
3039
3199
|
}
|
|
3040
3200
|
}
|
|
@@ -3068,6 +3228,12 @@ export default class WarpGraph {
|
|
|
3068
3228
|
async patchesFor(entityId) {
|
|
3069
3229
|
await this._ensureFreshState();
|
|
3070
3230
|
|
|
3231
|
+
if (this._provenanceDegraded) {
|
|
3232
|
+
throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
|
|
3233
|
+
code: 'E_PROVENANCE_DEGRADED',
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3071
3237
|
if (!this._provenanceIndex) {
|
|
3072
3238
|
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
3073
3239
|
code: 'E_NO_STATE',
|
|
@@ -3129,6 +3295,12 @@ export default class WarpGraph {
|
|
|
3129
3295
|
// Ensure fresh state before accessing provenance index
|
|
3130
3296
|
await this._ensureFreshState();
|
|
3131
3297
|
|
|
3298
|
+
if (this._provenanceDegraded) {
|
|
3299
|
+
throw new QueryError('Provenance unavailable for cached seek. Re-seek with --no-persistent-cache or call materialize({ ceiling }) directly.', {
|
|
3300
|
+
code: 'E_PROVENANCE_DEGRADED',
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3132
3304
|
if (!this._provenanceIndex) {
|
|
3133
3305
|
throw new QueryError('No provenance index. Call materialize() first.', {
|
|
3134
3306
|
code: 'E_NO_STATE',
|
|
@@ -3163,7 +3335,7 @@ export default class WarpGraph {
|
|
|
3163
3335
|
this._logTiming('materializeSlice', t0, { metrics: `${sortedPatches.length} patches` });
|
|
3164
3336
|
|
|
3165
3337
|
if (collectReceipts) {
|
|
3166
|
-
const result = reduceV5(sortedPatches, undefined, { receipts: true });
|
|
3338
|
+
const result = /** @type {{state: import('./services/JoinReducer.js').WarpStateV5, receipts: import('./types/TickReceipt.js').TickReceipt[]}} */ (reduceV5(sortedPatches, undefined, { receipts: true }));
|
|
3167
3339
|
return {
|
|
3168
3340
|
state: result.state,
|
|
3169
3341
|
patchCount: sortedPatches.length,
|
|
@@ -3177,7 +3349,7 @@ export default class WarpGraph {
|
|
|
3177
3349
|
patchCount: sortedPatches.length,
|
|
3178
3350
|
};
|
|
3179
3351
|
} catch (err) {
|
|
3180
|
-
this._logTiming('materializeSlice', t0, { error: err });
|
|
3352
|
+
this._logTiming('materializeSlice', t0, { error: /** @type {Error} */ (err) });
|
|
3181
3353
|
throw err;
|
|
3182
3354
|
}
|
|
3183
3355
|
}
|
|
@@ -3214,7 +3386,7 @@ export default class WarpGraph {
|
|
|
3214
3386
|
visited.add(entityId);
|
|
3215
3387
|
|
|
3216
3388
|
// Get all patches that affected this entity
|
|
3217
|
-
const patchShas = this._provenanceIndex.patchesFor(entityId);
|
|
3389
|
+
const patchShas = /** @type {import('./services/ProvenanceIndex.js').ProvenanceIndex} */ (this._provenanceIndex).patchesFor(entityId);
|
|
3218
3390
|
|
|
3219
3391
|
for (const sha of patchShas) {
|
|
3220
3392
|
if (cone.has(sha)) {
|
|
@@ -3226,8 +3398,9 @@ export default class WarpGraph {
|
|
|
3226
3398
|
cone.set(sha, patch);
|
|
3227
3399
|
|
|
3228
3400
|
// Add read dependencies to the queue
|
|
3229
|
-
|
|
3230
|
-
|
|
3401
|
+
const patchReads = /** @type {any} */ (patch)?.reads; // TODO(ts-cleanup): type patch array
|
|
3402
|
+
if (patchReads) {
|
|
3403
|
+
for (const readEntity of patchReads) {
|
|
3231
3404
|
if (!visited.has(readEntity)) {
|
|
3232
3405
|
queue.push(readEntity);
|
|
3233
3406
|
}
|
|
@@ -3274,7 +3447,7 @@ export default class WarpGraph {
|
|
|
3274
3447
|
|
|
3275
3448
|
const patchMeta = decodePatchMessage(nodeInfo.message);
|
|
3276
3449
|
const patchBuffer = await this._persistence.readBlob(patchMeta.patchOid);
|
|
3277
|
-
return this._codec.decode(patchBuffer);
|
|
3450
|
+
return /** @type {Object} */ (this._codec.decode(patchBuffer));
|
|
3278
3451
|
}
|
|
3279
3452
|
|
|
3280
3453
|
/**
|
|
@@ -3302,8 +3475,8 @@ export default class WarpGraph {
|
|
|
3302
3475
|
* Sort order: Lamport timestamp (ascending), then writer ID, then SHA.
|
|
3303
3476
|
* This ensures deterministic ordering regardless of discovery order.
|
|
3304
3477
|
*
|
|
3305
|
-
* @param {Array<{patch:
|
|
3306
|
-
* @returns {Array<{patch:
|
|
3478
|
+
* @param {Array<{patch: any, sha: string}>} patches - Unsorted patch entries
|
|
3479
|
+
* @returns {Array<{patch: any, sha: string}>} Sorted patch entries
|
|
3307
3480
|
* @private
|
|
3308
3481
|
*/
|
|
3309
3482
|
_sortPatchesCausally(patches) {
|