@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
|
@@ -40,17 +40,17 @@ export default class IndexRebuildService {
|
|
|
40
40
|
* Creates an IndexRebuildService instance.
|
|
41
41
|
*
|
|
42
42
|
* @param {Object} options - Configuration options
|
|
43
|
-
* @param {
|
|
44
|
-
* Must implement `iterateNodes({ ref, limit }) => AsyncGenerator<GraphNode>`.
|
|
43
|
+
* @param {{ iterateNodes: (opts: { ref: string, limit: number }) => AsyncIterable<{ sha: string, parents: string[] }> }} options.graphService - Graph service providing node iteration.
|
|
45
44
|
* @param {import('../../ports/IndexStoragePort.js').default} options.storage - Storage adapter
|
|
46
45
|
* for persisting index blobs and trees. Typically GitGraphAdapter.
|
|
47
46
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for
|
|
48
47
|
* structured logging. Defaults to null logger (no logging).
|
|
48
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
49
49
|
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for checksums
|
|
50
50
|
* @throws {Error} If graphService is not provided
|
|
51
51
|
* @throws {Error} If storage adapter is not provided
|
|
52
52
|
*/
|
|
53
|
-
constructor({ graphService, storage, logger = nullLogger, codec, crypto }) {
|
|
53
|
+
constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
54
54
|
if (!graphService) {
|
|
55
55
|
throw new Error('IndexRebuildService requires a graphService');
|
|
56
56
|
}
|
|
@@ -156,7 +156,7 @@ export default class IndexRebuildService {
|
|
|
156
156
|
operation: 'rebuild',
|
|
157
157
|
ref,
|
|
158
158
|
mode,
|
|
159
|
-
error: err.message,
|
|
159
|
+
error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
|
|
160
160
|
durationMs,
|
|
161
161
|
});
|
|
162
162
|
throw err;
|
|
@@ -247,12 +247,12 @@ export default class IndexRebuildService {
|
|
|
247
247
|
* @private
|
|
248
248
|
*/
|
|
249
249
|
async _rebuildStreaming(ref, { limit, maxMemoryBytes, onFlush, onProgress, signal, frontier }) {
|
|
250
|
-
const builder = new StreamingBitmapIndexBuilder({
|
|
250
|
+
const builder = new StreamingBitmapIndexBuilder(/** @type {*} */ ({ // TODO(ts-cleanup): narrow port type
|
|
251
251
|
storage: this.storage,
|
|
252
252
|
maxMemoryBytes,
|
|
253
253
|
onFlush,
|
|
254
254
|
crypto: this._crypto,
|
|
255
|
-
});
|
|
255
|
+
}));
|
|
256
256
|
|
|
257
257
|
let processedNodes = 0;
|
|
258
258
|
|
|
@@ -266,7 +266,7 @@ export default class IndexRebuildService {
|
|
|
266
266
|
if (processedNodes % 10000 === 0) {
|
|
267
267
|
checkAborted(signal, 'rebuild');
|
|
268
268
|
if (onProgress) {
|
|
269
|
-
const stats = builder.getMemoryStats();
|
|
269
|
+
const stats = /** @type {any} */ (builder).getMemoryStats(); // TODO(ts-cleanup): narrow port type
|
|
270
270
|
onProgress({
|
|
271
271
|
processedNodes,
|
|
272
272
|
currentMemoryBytes: stats.estimatedBitmapBytes,
|
|
@@ -275,7 +275,7 @@ export default class IndexRebuildService {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
return await builder.finalize({ signal, frontier });
|
|
278
|
+
return await /** @type {any} */ (builder).finalize({ signal, frontier }); // TODO(ts-cleanup): narrow port type
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
@@ -302,10 +302,10 @@ export default class IndexRebuildService {
|
|
|
302
302
|
const treeStructure = await builder.serialize({ frontier });
|
|
303
303
|
const flatEntries = [];
|
|
304
304
|
for (const [path, buffer] of Object.entries(treeStructure)) {
|
|
305
|
-
const oid = await this.storage.writeBlob(buffer);
|
|
305
|
+
const oid = await /** @type {import('../../ports/BlobPort.js').default} */ (/** @type {unknown} */ (this.storage)).writeBlob(buffer);
|
|
306
306
|
flatEntries.push(`100644 blob ${oid}\t${path}`);
|
|
307
307
|
}
|
|
308
|
-
return await this.storage.writeTree(flatEntries);
|
|
308
|
+
return await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).writeTree(flatEntries);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
/**
|
|
@@ -384,12 +384,12 @@ export default class IndexRebuildService {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
const startTime = performance.now();
|
|
387
|
-
const shardOids = await this.storage.readTreeOids(treeOid);
|
|
387
|
+
const shardOids = await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).readTreeOids(treeOid);
|
|
388
388
|
const shardCount = Object.keys(shardOids).length;
|
|
389
389
|
|
|
390
390
|
// Staleness check
|
|
391
391
|
if (currentFrontier) {
|
|
392
|
-
const indexFrontier = await loadIndexFrontier(shardOids, this.storage, { codec: this._codec });
|
|
392
|
+
const indexFrontier = await loadIndexFrontier(shardOids, /** @type {*} */ (this.storage), { codec: this._codec }); // TODO(ts-cleanup): narrow port type
|
|
393
393
|
if (indexFrontier) {
|
|
394
394
|
const result = checkStaleness(indexFrontier, currentFrontier);
|
|
395
395
|
if (result.stale) {
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import defaultCodec from '../utils/defaultCodec.js';
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* @param {*} envelope
|
|
10
|
+
* @param {string} label
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
9
13
|
function validateEnvelope(envelope, label) {
|
|
10
14
|
if (!envelope || typeof envelope !== 'object' || !envelope.frontier || typeof envelope.frontier !== 'object') {
|
|
11
15
|
throw new Error(`invalid frontier envelope for ${label}`);
|
|
@@ -16,17 +20,17 @@ function validateEnvelope(envelope, label) {
|
|
|
16
20
|
* Loads the frontier from an index tree's shard OIDs.
|
|
17
21
|
*
|
|
18
22
|
* @param {Record<string, string>} shardOids - Map of path → blob OID from readTreeOids
|
|
19
|
-
* @param {import('../../ports/IndexStoragePort.js').default} storage - Storage adapter
|
|
23
|
+
* @param {import('../../ports/IndexStoragePort.js').default & import('../../ports/BlobPort.js').default} storage - Storage adapter
|
|
20
24
|
* @param {Object} [options]
|
|
21
25
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
22
26
|
* @returns {Promise<Map<string, string>|null>} Frontier map, or null if not present (legacy index)
|
|
23
27
|
*/
|
|
24
|
-
export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
|
|
28
|
+
export async function loadIndexFrontier(shardOids, storage, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
25
29
|
const c = codec || defaultCodec;
|
|
26
30
|
const cborOid = shardOids['frontier.cbor'];
|
|
27
31
|
if (cborOid) {
|
|
28
32
|
const buffer = await storage.readBlob(cborOid);
|
|
29
|
-
const envelope = c.decode(buffer);
|
|
33
|
+
const envelope = /** @type {{ frontier: Record<string, string> }} */ (c.decode(buffer));
|
|
30
34
|
validateEnvelope(envelope, 'frontier.cbor');
|
|
31
35
|
return new Map(Object.entries(envelope.frontier));
|
|
32
36
|
}
|
|
@@ -34,7 +38,7 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
|
|
|
34
38
|
const jsonOid = shardOids['frontier.json'];
|
|
35
39
|
if (jsonOid) {
|
|
36
40
|
const buffer = await storage.readBlob(jsonOid);
|
|
37
|
-
const envelope = JSON.parse(buffer.toString('utf-8'));
|
|
41
|
+
const envelope = /** @type {{ frontier: Record<string, string> }} */ (JSON.parse(buffer.toString('utf-8')));
|
|
38
42
|
validateEnvelope(envelope, 'frontier.json');
|
|
39
43
|
return new Map(Object.entries(envelope.frontier));
|
|
40
44
|
}
|
|
@@ -51,7 +55,10 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
|
|
|
51
55
|
* @property {string[]} removedWriters - Writers in index but not current
|
|
52
56
|
*/
|
|
53
57
|
|
|
54
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* @param {{ stale: boolean, advancedWriters: string[], newWriters: string[], removedWriters: string[] }} opts
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
55
62
|
function buildReason({ stale, advancedWriters, newWriters, removedWriters }) {
|
|
56
63
|
if (!stale) {
|
|
57
64
|
return 'index is current';
|
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
* @typedef {Object} WarpStateV5
|
|
30
30
|
* @property {import('../crdt/ORSet.js').ORSet} nodeAlive - ORSet of alive nodes
|
|
31
31
|
* @property {import('../crdt/ORSet.js').ORSet} edgeAlive - ORSet of alive edges
|
|
32
|
-
* @property {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
32
|
+
* @property {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} prop - Properties with LWW
|
|
33
33
|
* @property {import('../crdt/VersionVector.js').VersionVector} observedFrontier - Observed version vector
|
|
34
34
|
* @property {Map<string, import('../utils/EventId.js').EventId>} edgeBirthEvent - EdgeKey → EventId of most recent EdgeAdd (for clean-slate prop visibility)
|
|
35
35
|
*/
|
|
@@ -88,14 +88,14 @@ export function createEmptyStateV5() {
|
|
|
88
88
|
export function applyOpV2(state, op, eventId) {
|
|
89
89
|
switch (op.type) {
|
|
90
90
|
case 'NodeAdd':
|
|
91
|
-
orsetAdd(state.nodeAlive, op.node, op.dot);
|
|
91
|
+
orsetAdd(state.nodeAlive, /** @type {string} */ (op.node), /** @type {import('../crdt/Dot.js').Dot} */ (op.dot));
|
|
92
92
|
break;
|
|
93
93
|
case 'NodeRemove':
|
|
94
|
-
orsetRemove(state.nodeAlive, op.observedDots);
|
|
94
|
+
orsetRemove(state.nodeAlive, /** @type {Set<string>} */ (/** @type {unknown} */ (op.observedDots)));
|
|
95
95
|
break;
|
|
96
96
|
case 'EdgeAdd': {
|
|
97
|
-
const edgeKey = encodeEdgeKey(op.from, op.to, op.label);
|
|
98
|
-
orsetAdd(state.edgeAlive, edgeKey, op.dot);
|
|
97
|
+
const edgeKey = encodeEdgeKey(/** @type {string} */ (op.from), /** @type {string} */ (op.to), /** @type {string} */ (op.label));
|
|
98
|
+
orsetAdd(state.edgeAlive, edgeKey, /** @type {import('../crdt/Dot.js').Dot} */ (op.dot));
|
|
99
99
|
// Track the EventId at which this edge incarnation was born.
|
|
100
100
|
// On re-add after remove, the greater EventId replaces the old one,
|
|
101
101
|
// allowing the query layer to filter out stale properties.
|
|
@@ -108,13 +108,13 @@ export function applyOpV2(state, op, eventId) {
|
|
|
108
108
|
break;
|
|
109
109
|
}
|
|
110
110
|
case 'EdgeRemove':
|
|
111
|
-
orsetRemove(state.edgeAlive, op.observedDots);
|
|
111
|
+
orsetRemove(state.edgeAlive, /** @type {Set<string>} */ (/** @type {unknown} */ (op.observedDots)));
|
|
112
112
|
break;
|
|
113
113
|
case 'PropSet': {
|
|
114
114
|
// Uses EventId-based LWW, same as v4
|
|
115
|
-
const key = encodePropKey(op.node, op.key);
|
|
115
|
+
const key = encodePropKey(/** @type {string} */ (op.node), /** @type {string} */ (op.key));
|
|
116
116
|
const current = state.prop.get(key);
|
|
117
|
-
state.prop.set(key, lwwMax(current, lwwSet(eventId, op.value)));
|
|
117
|
+
state.prop.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<*>} */ (lwwMax(current, lwwSet(eventId, op.value))));
|
|
118
118
|
break;
|
|
119
119
|
}
|
|
120
120
|
default:
|
|
@@ -290,7 +290,7 @@ function edgeRemoveOutcome(orset, op) {
|
|
|
290
290
|
* - `superseded`: An existing value with higher EventId wins
|
|
291
291
|
* - `redundant`: Exact same write (identical EventId)
|
|
292
292
|
*
|
|
293
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
293
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} propMap - The properties map keyed by encoded prop keys
|
|
294
294
|
* @param {Object} op - The PropSet operation
|
|
295
295
|
* @param {string} op.node - Node ID owning the property
|
|
296
296
|
* @param {string} op.key - Property key/name
|
|
@@ -347,8 +347,8 @@ function propSetOutcome(propMap, op, eventId) {
|
|
|
347
347
|
* @param {Object} patch - The patch to apply
|
|
348
348
|
* @param {string} patch.writer - Writer ID who created this patch
|
|
349
349
|
* @param {number} patch.lamport - Lamport timestamp of this patch
|
|
350
|
-
* @param {
|
|
351
|
-
* @param {Map
|
|
350
|
+
* @param {Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: *, oid?: string}>} patch.ops - Array of operations to apply
|
|
351
|
+
* @param {Map<string, number>|{[x: string]: number}} patch.context - Version vector context (Map or serialized form)
|
|
352
352
|
* @param {string} patchSha - The Git SHA of the patch commit (used for EventId creation)
|
|
353
353
|
* @param {boolean} [collectReceipts=false] - When true, computes and returns receipt data
|
|
354
354
|
* @returns {WarpStateV5|{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}}
|
|
@@ -370,30 +370,32 @@ export function join(state, patch, patchSha, collectReceipts) {
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
// Receipt-enabled path
|
|
373
|
+
/** @type {import('../types/TickReceipt.js').OpOutcome[]} */
|
|
373
374
|
const opResults = [];
|
|
374
375
|
for (let i = 0; i < patch.ops.length; i++) {
|
|
375
376
|
const op = patch.ops[i];
|
|
376
377
|
const eventId = createEventId(patch.lamport, patch.writer, patchSha, i);
|
|
377
378
|
|
|
378
379
|
// Determine outcome BEFORE applying the op (state is pre-op)
|
|
380
|
+
/** @type {{target: string, result: string, reason?: string}} */
|
|
379
381
|
let outcome;
|
|
380
382
|
switch (op.type) {
|
|
381
383
|
case 'NodeAdd':
|
|
382
|
-
outcome = nodeAddOutcome(state.nodeAlive, op);
|
|
384
|
+
outcome = nodeAddOutcome(state.nodeAlive, /** @type {{node: string, dot: import('../crdt/Dot.js').Dot}} */ (op));
|
|
383
385
|
break;
|
|
384
386
|
case 'NodeRemove':
|
|
385
|
-
outcome = nodeRemoveOutcome(state.nodeAlive, op);
|
|
387
|
+
outcome = nodeRemoveOutcome(state.nodeAlive, /** @type {{node?: string, observedDots: string[]}} */ (op));
|
|
386
388
|
break;
|
|
387
389
|
case 'EdgeAdd': {
|
|
388
|
-
const edgeKey = encodeEdgeKey(op.from, op.to, op.label);
|
|
389
|
-
outcome = edgeAddOutcome(state.edgeAlive, op, edgeKey);
|
|
390
|
+
const edgeKey = encodeEdgeKey(/** @type {string} */ (op.from), /** @type {string} */ (op.to), /** @type {string} */ (op.label));
|
|
391
|
+
outcome = edgeAddOutcome(state.edgeAlive, /** @type {{from: string, to: string, label: string, dot: import('../crdt/Dot.js').Dot}} */ (op), edgeKey);
|
|
390
392
|
break;
|
|
391
393
|
}
|
|
392
394
|
case 'EdgeRemove':
|
|
393
|
-
outcome = edgeRemoveOutcome(state.edgeAlive, op);
|
|
395
|
+
outcome = edgeRemoveOutcome(state.edgeAlive, /** @type {{from?: string, to?: string, label?: string, observedDots: string[]}} */ (op));
|
|
394
396
|
break;
|
|
395
397
|
case 'PropSet':
|
|
396
|
-
outcome = propSetOutcome(state.prop, op, eventId);
|
|
398
|
+
outcome = propSetOutcome(state.prop, /** @type {{node: string, key: string, value: *}} */ (op), eventId);
|
|
397
399
|
break;
|
|
398
400
|
default:
|
|
399
401
|
// Unknown or BlobValue — always applied
|
|
@@ -404,12 +406,13 @@ export function join(state, patch, patchSha, collectReceipts) {
|
|
|
404
406
|
// Apply the op (mutates state)
|
|
405
407
|
applyOpV2(state, op, eventId);
|
|
406
408
|
|
|
407
|
-
const receiptOp = RECEIPT_OP_TYPE[op.type] || op.type;
|
|
409
|
+
const receiptOp = /** @type {Record<string, string>} */ (RECEIPT_OP_TYPE)[op.type] || op.type;
|
|
408
410
|
// Skip unknown/forward-compatible op types that aren't valid receipt ops
|
|
409
411
|
if (!VALID_RECEIPT_OPS.has(receiptOp)) {
|
|
410
412
|
continue;
|
|
411
413
|
}
|
|
412
|
-
|
|
414
|
+
/** @type {import('../types/TickReceipt.js').OpOutcome} */
|
|
415
|
+
const entry = { op: receiptOp, target: outcome.target, result: /** @type {'applied'|'superseded'|'redundant'} */ (outcome.result) };
|
|
413
416
|
if (outcome.reason) {
|
|
414
417
|
entry.reason = outcome.reason;
|
|
415
418
|
}
|
|
@@ -467,16 +470,16 @@ export function joinStates(a, b) {
|
|
|
467
470
|
*
|
|
468
471
|
* This is a pure function that does not mutate its inputs.
|
|
469
472
|
*
|
|
470
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
471
|
-
* @param {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
472
|
-
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
473
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} a - First property map
|
|
474
|
+
* @param {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} b - Second property map
|
|
475
|
+
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister<*>>} New map containing merged properties
|
|
473
476
|
*/
|
|
474
477
|
function mergeProps(a, b) {
|
|
475
478
|
const result = new Map(a);
|
|
476
479
|
|
|
477
480
|
for (const [key, regB] of b) {
|
|
478
481
|
const regA = result.get(key);
|
|
479
|
-
result.set(key, lwwMax(regA, regB));
|
|
482
|
+
result.set(key, /** @type {import('../crdt/LWW.js').LWWRegister<*>} */ (lwwMax(regA, regB)));
|
|
480
483
|
}
|
|
481
484
|
|
|
482
485
|
return result;
|
|
@@ -527,9 +530,7 @@ function mergeEdgeBirthEvent(a, b) {
|
|
|
527
530
|
* - When `options.receipts` is true, returns a TickReceipt per patch for
|
|
528
531
|
* provenance tracking and debugging.
|
|
529
532
|
*
|
|
530
|
-
* @param {Array<{patch:
|
|
531
|
-
* @param {Object} patches[].patch - The decoded patch object (writer, lamport, ops, context)
|
|
532
|
-
* @param {string} patches[].sha - The Git SHA of the patch commit
|
|
533
|
+
* @param {Array<{patch: {writer: string, lamport: number, ops: Array<{type: string, node?: string, dot?: import('../crdt/Dot.js').Dot, observedDots?: string[], from?: string, to?: string, label?: string, key?: string, value?: *, oid?: string}>, context: Map<string, number>|{[x: string]: number}}, sha: string}>} patches - Array of patch objects with their Git SHAs
|
|
533
534
|
* @param {WarpStateV5} [initialState] - Optional starting state (for incremental materialization from checkpoint)
|
|
534
535
|
* @param {Object} [options] - Optional configuration
|
|
535
536
|
* @param {boolean} [options.receipts=false] - When true, collect and return TickReceipts
|
|
@@ -544,7 +545,7 @@ export function reduceV5(patches, initialState, options) {
|
|
|
544
545
|
if (options && options.receipts) {
|
|
545
546
|
const receipts = [];
|
|
546
547
|
for (const { patch, sha } of patches) {
|
|
547
|
-
const result = join(state, patch, sha, true);
|
|
548
|
+
const result = /** @type {{state: WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}} */ (join(state, patch, sha, true));
|
|
548
549
|
receipts.push(result.receipt);
|
|
549
550
|
}
|
|
550
551
|
return { state, receipts };
|
|
@@ -145,14 +145,14 @@ export default class LogicalTraversal {
|
|
|
145
145
|
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
146
146
|
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
147
147
|
* @param {number} [options.maxDepth] - Maximum depth to traverse
|
|
148
|
-
* @returns {Promise<{dir: 'out'|'in'|'both', labelSet: Set<string>|null, adjacency:
|
|
148
|
+
* @returns {Promise<{dir: 'out'|'in'|'both', labelSet: Set<string>|null, adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}, depthLimit: number}>}
|
|
149
149
|
* The normalized traversal parameters
|
|
150
150
|
* @throws {TraversalError} If the start node is not found (NODE_NOT_FOUND)
|
|
151
151
|
* @throws {TraversalError} If the direction is invalid (INVALID_DIRECTION)
|
|
152
152
|
* @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
|
|
153
153
|
*/
|
|
154
154
|
async _prepare(start, { dir, labelFilter, maxDepth }) {
|
|
155
|
-
const materialized = await this._graph._materializeGraph();
|
|
155
|
+
const materialized = await /** @type {any} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
|
|
156
156
|
|
|
157
157
|
if (!(await this._graph.hasNode(start))) {
|
|
158
158
|
throw new TraversalError(`Start node not found: ${start}`, {
|
|
@@ -187,7 +187,7 @@ export default class LogicalTraversal {
|
|
|
187
187
|
const result = [];
|
|
188
188
|
|
|
189
189
|
while (queue.length > 0) {
|
|
190
|
-
const current = queue.shift();
|
|
190
|
+
const current = /** @type {{nodeId: string, depth: number}} */ (queue.shift());
|
|
191
191
|
if (visited.has(current.nodeId)) {
|
|
192
192
|
continue;
|
|
193
193
|
}
|
|
@@ -237,7 +237,7 @@ export default class LogicalTraversal {
|
|
|
237
237
|
const result = [];
|
|
238
238
|
|
|
239
239
|
while (stack.length > 0) {
|
|
240
|
-
const current = stack.pop();
|
|
240
|
+
const current = /** @type {{nodeId: string, depth: number}} */ (stack.pop());
|
|
241
241
|
if (visited.has(current.nodeId)) {
|
|
242
242
|
continue;
|
|
243
243
|
}
|
|
@@ -298,7 +298,7 @@ export default class LogicalTraversal {
|
|
|
298
298
|
visited.add(from);
|
|
299
299
|
|
|
300
300
|
while (queue.length > 0) {
|
|
301
|
-
const current = queue.shift();
|
|
301
|
+
const current = /** @type {{nodeId: string, depth: number}} */ (queue.shift());
|
|
302
302
|
if (current.depth >= depthLimit) {
|
|
303
303
|
continue;
|
|
304
304
|
}
|
|
@@ -319,10 +319,11 @@ export default class LogicalTraversal {
|
|
|
319
319
|
|
|
320
320
|
if (edge.neighborId === to) {
|
|
321
321
|
const path = [to];
|
|
322
|
+
/** @type {string|undefined} */
|
|
322
323
|
let cursor = current.nodeId;
|
|
323
324
|
while (cursor) {
|
|
324
325
|
path.push(cursor);
|
|
325
|
-
cursor = parent.get(cursor)
|
|
326
|
+
cursor = parent.get(cursor);
|
|
326
327
|
}
|
|
327
328
|
path.reverse();
|
|
328
329
|
return { found: true, path, length: path.length - 1 };
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @private
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
// @ts-expect-error -- no declaration file for @git-stunts/trailer-codec
|
|
15
16
|
import { TrailerCodec, TrailerCodecService } from '@git-stunts/trailer-codec';
|
|
16
17
|
|
|
17
18
|
// -----------------------------------------------------------------------------
|
|
@@ -62,6 +63,7 @@ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
|
62
63
|
// -----------------------------------------------------------------------------
|
|
63
64
|
|
|
64
65
|
// Lazy singleton codec instance
|
|
66
|
+
/** @type {*} */ // TODO(ts-cleanup): type lazy singleton
|
|
65
67
|
let _codec = null;
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -102,7 +102,7 @@ export default class ObserverView {
|
|
|
102
102
|
this._graph = graph;
|
|
103
103
|
|
|
104
104
|
/** @type {LogicalTraversal} */
|
|
105
|
-
this.traverse = new LogicalTraversal(this);
|
|
105
|
+
this.traverse = new LogicalTraversal(/** @type {*} */ (this)); // TODO(ts-cleanup): type observer cast
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
@@ -124,11 +124,11 @@ export default class ObserverView {
|
|
|
124
124
|
* Builds a filtered adjacency structure that only includes edges
|
|
125
125
|
* where both endpoints pass the match filter.
|
|
126
126
|
*
|
|
127
|
-
* @returns {Promise<{state: *, stateHash: string, adjacency: {outgoing: Map, incoming: Map}}>}
|
|
127
|
+
* @returns {Promise<{state: *, stateHash: string, adjacency: {outgoing: Map<string, *[]>, incoming: Map<string, *[]>}}>}
|
|
128
128
|
* @private
|
|
129
129
|
*/
|
|
130
130
|
async _materializeGraph() {
|
|
131
|
-
const materialized = await this._graph._materializeGraph();
|
|
131
|
+
const materialized = await /** @type {*} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
|
|
132
132
|
const { state, stateHash } = materialized;
|
|
133
133
|
|
|
134
134
|
// Build filtered adjacency: only edges where both endpoints match
|
|
@@ -159,8 +159,8 @@ export default class ObserverView {
|
|
|
159
159
|
incoming.get(to).push({ neighborId: from, label });
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
const sortNeighbors = (list) => {
|
|
163
|
-
list.sort((a, b) => {
|
|
162
|
+
const sortNeighbors = (/** @type {{ neighborId: string, label: string }[]} */ list) => {
|
|
163
|
+
list.sort((/** @type {{ neighborId: string, label: string }} */ a, /** @type {{ neighborId: string, label: string }} */ b) => {
|
|
164
164
|
if (a.neighborId !== b.neighborId) {
|
|
165
165
|
return a.neighborId < b.neighborId ? -1 : 1;
|
|
166
166
|
}
|
|
@@ -260,6 +260,6 @@ export default class ObserverView {
|
|
|
260
260
|
* @returns {QueryBuilder} A query builder scoped to this observer
|
|
261
261
|
*/
|
|
262
262
|
query() {
|
|
263
|
-
return new QueryBuilder(this);
|
|
263
|
+
return new QueryBuilder(/** @type {*} */ (this)); // TODO(ts-cleanup): type observer cast
|
|
264
264
|
}
|
|
265
265
|
}
|
|
@@ -85,8 +85,8 @@ export class PatchBuilderV2 {
|
|
|
85
85
|
* @param {{ warn: Function }} [options.logger] - Logger for non-fatal warnings
|
|
86
86
|
*/
|
|
87
87
|
constructor({ persistence, graphName, writerId, lamport, versionVector, getCurrentState, expectedParentSha = null, onCommitSuccess = null, onDeleteWithData = 'warn', codec, logger }) {
|
|
88
|
-
/** @type {import('../../ports/GraphPersistencePort.js').default} */
|
|
89
|
-
this._persistence = persistence;
|
|
88
|
+
/** @type {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} */
|
|
89
|
+
this._persistence = /** @type {*} */ (persistence); // TODO(ts-cleanup): narrow port type
|
|
90
90
|
|
|
91
91
|
/** @type {string} */
|
|
92
92
|
this._graphName = graphName;
|
|
@@ -214,7 +214,7 @@ export class PatchBuilderV2 {
|
|
|
214
214
|
const { edges } = findAttachedData(state, nodeId);
|
|
215
215
|
for (const edgeKey of edges) {
|
|
216
216
|
const [from, to, label] = edgeKey.split('\0');
|
|
217
|
-
const edgeDots = [...orsetGetDots(state.edgeAlive, edgeKey)];
|
|
217
|
+
const edgeDots = /** @type {import('../crdt/Dot.js').Dot[]} */ (/** @type {unknown} */ ([...orsetGetDots(state.edgeAlive, edgeKey)]));
|
|
218
218
|
this._ops.push(createEdgeRemoveV2(from, to, label, edgeDots));
|
|
219
219
|
// Provenance: cascade-generated EdgeRemove reads the edge key (to observe its dots)
|
|
220
220
|
this._reads.add(edgeKey);
|
|
@@ -251,7 +251,7 @@ export class PatchBuilderV2 {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
const observedDots = state ? [...orsetGetDots(state.nodeAlive, nodeId)] : [];
|
|
254
|
+
const observedDots = /** @type {import('../crdt/Dot.js').Dot[]} */ (/** @type {unknown} */ (state ? [...orsetGetDots(state.nodeAlive, nodeId)] : []));
|
|
255
255
|
this._ops.push(createNodeRemoveV2(nodeId, observedDots));
|
|
256
256
|
// Provenance: NodeRemove reads the node (to observe its dots)
|
|
257
257
|
this._reads.add(nodeId);
|
|
@@ -325,7 +325,7 @@ export class PatchBuilderV2 {
|
|
|
325
325
|
// Get observed dots from current state (orsetGetDots returns already-encoded dot strings)
|
|
326
326
|
const state = this._getCurrentState();
|
|
327
327
|
const edgeKey = encodeEdgeKey(from, to, label);
|
|
328
|
-
const observedDots = state ? [...orsetGetDots(state.edgeAlive, edgeKey)] : [];
|
|
328
|
+
const observedDots = /** @type {import('../crdt/Dot.js').Dot[]} */ (/** @type {unknown} */ (state ? [...orsetGetDots(state.edgeAlive, edgeKey)] : []));
|
|
329
329
|
this._ops.push(createEdgeRemoveV2(from, to, label, observedDots));
|
|
330
330
|
// Provenance: EdgeRemove reads the edge key (to observe its dots)
|
|
331
331
|
this._reads.add(edgeKey);
|
|
@@ -454,7 +454,7 @@ export class PatchBuilderV2 {
|
|
|
454
454
|
schema,
|
|
455
455
|
writer: this._writerId,
|
|
456
456
|
lamport: this._lamport,
|
|
457
|
-
context: this._vv,
|
|
457
|
+
context: /** @type {*} */ (this._vv), // TODO(ts-cleanup): narrow port type
|
|
458
458
|
ops: this._ops,
|
|
459
459
|
reads: [...this._reads].sort(),
|
|
460
460
|
writes: [...this._writes].sort(),
|
|
@@ -515,10 +515,10 @@ export class PatchBuilderV2 {
|
|
|
515
515
|
const currentRefSha = await this._persistence.readRef(writerRef);
|
|
516
516
|
|
|
517
517
|
if (currentRefSha !== this._expectedParentSha) {
|
|
518
|
-
const err = new WriterError(
|
|
518
|
+
const err = /** @type {WriterError & { expectedSha: string|null, actualSha: string|null }} */ (new WriterError(
|
|
519
519
|
'WRITER_CAS_CONFLICT',
|
|
520
520
|
'Commit failed: writer ref was updated by another process. Re-materialize and retry.'
|
|
521
|
-
);
|
|
521
|
+
));
|
|
522
522
|
err.expectedSha = this._expectedParentSha;
|
|
523
523
|
err.actualSha = currentRefSha;
|
|
524
524
|
throw err;
|
|
@@ -556,7 +556,7 @@ export class PatchBuilderV2 {
|
|
|
556
556
|
const patchCbor = this._codec.encode(patch);
|
|
557
557
|
|
|
558
558
|
// 5. Write patch.cbor blob
|
|
559
|
-
const patchBlobOid = await this._persistence.writeBlob(patchCbor);
|
|
559
|
+
const patchBlobOid = await this._persistence.writeBlob(/** @type {Buffer} */ (patchCbor));
|
|
560
560
|
|
|
561
561
|
// 6. Create tree with the blob
|
|
562
562
|
// Format for mktree: "mode type oid\tpath"
|
|
@@ -72,13 +72,7 @@ export function encodePatchMessage({ graph, writer, lamport, patchOid, schema =
|
|
|
72
72
|
* Decodes a patch commit message.
|
|
73
73
|
*
|
|
74
74
|
* @param {string} message - The raw commit message
|
|
75
|
-
* @returns {
|
|
76
|
-
* @returns {string} return.kind - Always 'patch'
|
|
77
|
-
* @returns {string} return.graph - The graph name
|
|
78
|
-
* @returns {string} return.writer - The writer ID
|
|
79
|
-
* @returns {number} return.lamport - The Lamport timestamp
|
|
80
|
-
* @returns {string} return.patchOid - The patch blob OID
|
|
81
|
-
* @returns {number} return.schema - The schema version
|
|
75
|
+
* @returns {{ kind: 'patch', graph: string, writer: string, lamport: number, patchOid: string, schema: number }} The decoded patch message
|
|
82
76
|
* @throws {Error} If the message is not a valid patch message
|
|
83
77
|
*
|
|
84
78
|
* @example
|
|
@@ -52,7 +52,6 @@ class ProvenanceIndex {
|
|
|
52
52
|
/**
|
|
53
53
|
* Internal index mapping nodeId/edgeKey to Set of patch SHAs.
|
|
54
54
|
* @type {Map<string, Set<string>>}
|
|
55
|
-
* @private
|
|
56
55
|
*/
|
|
57
56
|
#index;
|
|
58
57
|
|
|
@@ -120,7 +119,6 @@ class ProvenanceIndex {
|
|
|
120
119
|
*
|
|
121
120
|
* @param {string} entityId - The node ID or edge key
|
|
122
121
|
* @param {string} patchSha - The patch SHA
|
|
123
|
-
* @private
|
|
124
122
|
*/
|
|
125
123
|
#addEntry(entityId, patchSha) {
|
|
126
124
|
let shas = this.#index.get(entityId);
|
|
@@ -227,12 +225,12 @@ class ProvenanceIndex {
|
|
|
227
225
|
* Returns sorted entries for deterministic output.
|
|
228
226
|
*
|
|
229
227
|
* @returns {Array<[string, string[]]>} Sorted array of [entityId, sortedShas[]] pairs
|
|
230
|
-
* @private
|
|
231
228
|
*/
|
|
232
229
|
#sortedEntries() {
|
|
230
|
+
/** @type {Array<[string, string[]]>} */
|
|
233
231
|
const entries = [];
|
|
234
232
|
for (const [entityId, shas] of this.#index) {
|
|
235
|
-
entries.push([entityId, [...shas].sort()]);
|
|
233
|
+
entries.push(/** @type {[string, string[]]} */ ([entityId, [...shas].sort()]));
|
|
236
234
|
}
|
|
237
235
|
entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
238
236
|
return entries;
|
|
@@ -246,7 +244,7 @@ class ProvenanceIndex {
|
|
|
246
244
|
*
|
|
247
245
|
* @param {Object} [options]
|
|
248
246
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
249
|
-
* @returns {Buffer} CBOR-encoded index
|
|
247
|
+
* @returns {Buffer|Uint8Array} CBOR-encoded index
|
|
250
248
|
*/
|
|
251
249
|
serialize({ codec } = {}) {
|
|
252
250
|
const c = codec || defaultCodec;
|
|
@@ -258,7 +256,6 @@ class ProvenanceIndex {
|
|
|
258
256
|
*
|
|
259
257
|
* @param {Array<[string, string[]]>} entries - Array of [entityId, shas[]] pairs
|
|
260
258
|
* @returns {Map<string, Set<string>>} The built index
|
|
261
|
-
* @private
|
|
262
259
|
*/
|
|
263
260
|
static #buildIndex(entries) {
|
|
264
261
|
const index = new Map();
|
|
@@ -279,7 +276,8 @@ class ProvenanceIndex {
|
|
|
279
276
|
*/
|
|
280
277
|
static deserialize(buffer, { codec } = {}) {
|
|
281
278
|
const c = codec || defaultCodec;
|
|
282
|
-
|
|
279
|
+
/** @type {{ version?: number, entries?: Array<[string, string[]]> }} */
|
|
280
|
+
const obj = /** @type {any} */ (c.decode(buffer)); // TODO(ts-cleanup): narrow port type
|
|
283
281
|
|
|
284
282
|
if (obj.version !== 1) {
|
|
285
283
|
throw new Error(`Unsupported ProvenanceIndex version: ${obj.version}`);
|
|
@@ -304,7 +302,7 @@ class ProvenanceIndex {
|
|
|
304
302
|
/**
|
|
305
303
|
* Creates a ProvenanceIndex from a JSON representation.
|
|
306
304
|
*
|
|
307
|
-
* @param {
|
|
305
|
+
* @param {{ version?: number, entries?: Array<[string, string[]]> }} json - Object with version and entries array
|
|
308
306
|
* @returns {ProvenanceIndex} The deserialized index
|
|
309
307
|
* @throws {Error} If the JSON contains an unsupported version
|
|
310
308
|
*/
|
|
@@ -68,7 +68,6 @@ class ProvenancePayload {
|
|
|
68
68
|
/**
|
|
69
69
|
* The internal array of patch entries. Frozen after construction.
|
|
70
70
|
* @type {ReadonlyArray<PatchEntry>}
|
|
71
|
-
* @private
|
|
72
71
|
*/
|
|
73
72
|
#patches;
|
|
74
73
|
|
|
@@ -173,7 +172,7 @@ class ProvenancePayload {
|
|
|
173
172
|
// Use JoinReducer's reduceV5 for deterministic materialization.
|
|
174
173
|
// Note: reduceV5 returns { state, receipts } when options.receipts is truthy,
|
|
175
174
|
// but returns bare WarpStateV5 when no options passed (as here).
|
|
176
|
-
return reduceV5(this.#patches, initialState);
|
|
175
|
+
return /** @type {import('./JoinReducer.js').WarpStateV5} */ (reduceV5(/** @type {*} */ (this.#patches), initialState)); // TODO(ts-cleanup): type patch array
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
/**
|