@git-stunts/git-warp 10.3.2 → 10.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/bin/warp-graph.js +371 -141
- package/index.d.ts +31 -0
- package/index.js +4 -0
- package/package.json +8 -3
- package/src/domain/WarpGraph.js +263 -147
- package/src/domain/crdt/LWW.js +1 -1
- package/src/domain/crdt/ORSet.js +10 -6
- package/src/domain/crdt/VersionVector.js +5 -1
- package/src/domain/errors/EmptyMessageError.js +2 -4
- package/src/domain/errors/ForkError.js +4 -0
- package/src/domain/errors/IndexError.js +4 -0
- package/src/domain/errors/OperationAbortedError.js +4 -0
- package/src/domain/errors/QueryError.js +4 -0
- package/src/domain/errors/SchemaUnsupportedError.js +4 -0
- package/src/domain/errors/ShardCorruptionError.js +2 -6
- package/src/domain/errors/ShardLoadError.js +2 -6
- package/src/domain/errors/ShardValidationError.js +2 -7
- package/src/domain/errors/StorageError.js +2 -6
- package/src/domain/errors/SyncError.js +4 -0
- package/src/domain/errors/TraversalError.js +4 -0
- package/src/domain/errors/WarpError.js +2 -4
- package/src/domain/errors/WormholeError.js +4 -0
- package/src/domain/services/AnchorMessageCodec.js +1 -4
- package/src/domain/services/BitmapIndexBuilder.js +10 -6
- package/src/domain/services/BitmapIndexReader.js +27 -21
- package/src/domain/services/BoundaryTransitionRecord.js +22 -15
- package/src/domain/services/CheckpointMessageCodec.js +1 -7
- package/src/domain/services/CheckpointSerializerV5.js +20 -19
- package/src/domain/services/CheckpointService.js +18 -18
- package/src/domain/services/CommitDagTraversalService.js +13 -1
- package/src/domain/services/DagPathFinding.js +40 -18
- package/src/domain/services/DagTopology.js +7 -6
- package/src/domain/services/DagTraversal.js +5 -3
- package/src/domain/services/Frontier.js +7 -6
- package/src/domain/services/HealthCheckService.js +15 -14
- package/src/domain/services/HookInstaller.js +64 -13
- package/src/domain/services/HttpSyncServer.js +15 -14
- package/src/domain/services/IndexRebuildService.js +12 -12
- package/src/domain/services/IndexStalenessChecker.js +13 -6
- package/src/domain/services/JoinReducer.js +28 -27
- package/src/domain/services/LogicalTraversal.js +7 -6
- package/src/domain/services/MessageCodecInternal.js +2 -0
- package/src/domain/services/ObserverView.js +6 -6
- package/src/domain/services/PatchBuilderV2.js +9 -9
- package/src/domain/services/PatchMessageCodec.js +1 -7
- package/src/domain/services/ProvenanceIndex.js +6 -8
- package/src/domain/services/ProvenancePayload.js +1 -2
- package/src/domain/services/QueryBuilder.js +29 -23
- package/src/domain/services/StateDiff.js +7 -7
- package/src/domain/services/StateSerializerV5.js +8 -6
- package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
- package/src/domain/services/SyncProtocol.js +23 -26
- package/src/domain/services/TemporalQuery.js +4 -3
- package/src/domain/services/TranslationCost.js +4 -4
- package/src/domain/services/WormholeService.js +19 -15
- package/src/domain/types/TickReceipt.js +10 -6
- package/src/domain/types/WarpTypesV2.js +2 -3
- package/src/domain/utils/CachedValue.js +1 -1
- package/src/domain/utils/LRUCache.js +3 -3
- package/src/domain/utils/MinHeap.js +2 -2
- package/src/domain/utils/RefLayout.js +19 -0
- package/src/domain/utils/WriterId.js +2 -2
- package/src/domain/utils/defaultCodec.js +9 -2
- package/src/domain/utils/defaultCrypto.js +36 -0
- package/src/domain/utils/roaring.js +5 -5
- package/src/domain/utils/seekCacheKey.js +32 -0
- package/src/domain/warp/PatchSession.js +3 -3
- package/src/domain/warp/Writer.js +2 -2
- package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
- package/src/infrastructure/adapters/ClockAdapter.js +2 -2
- package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
- package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
- package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
- package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
- package/src/infrastructure/codecs/CborCodec.js +16 -8
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/CodecPort.js +2 -2
- package/src/ports/CommitPort.js +8 -21
- package/src/ports/ConfigPort.js +3 -3
- package/src/ports/CryptoPort.js +7 -7
- package/src/ports/GraphPersistencePort.js +12 -14
- package/src/ports/HttpServerPort.js +1 -5
- package/src/ports/IndexStoragePort.js +1 -0
- package/src/ports/LoggerPort.js +9 -9
- package/src/ports/RefPort.js +5 -5
- package/src/ports/SeekCachePort.js +73 -0
- package/src/ports/TreePort.js +3 -3
- package/src/visualization/layouts/converters.js +14 -7
- package/src/visualization/layouts/elkAdapter.js +17 -4
- package/src/visualization/layouts/elkLayout.js +23 -7
- package/src/visualization/layouts/index.js +3 -3
- package/src/visualization/renderers/ascii/check.js +30 -17
- package/src/visualization/renderers/ascii/graph.js +92 -1
- package/src/visualization/renderers/ascii/history.js +28 -26
- package/src/visualization/renderers/ascii/info.js +9 -7
- package/src/visualization/renderers/ascii/materialize.js +20 -16
- package/src/visualization/renderers/ascii/opSummary.js +15 -7
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +19 -5
- package/src/visualization/renderers/ascii/table.js +1 -1
- package/src/visualization/renderers/svg/index.js +5 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import defaultCodec from '../utils/defaultCodec.js';
|
|
2
|
+
import defaultCrypto from '../utils/defaultCrypto.js';
|
|
2
3
|
import { orsetContains, orsetElements } from '../crdt/ORSet.js';
|
|
3
4
|
import { decodeEdgeKey, decodePropKey } from './KeyCodec.js';
|
|
4
5
|
|
|
@@ -75,7 +76,7 @@ export function propVisibleV5(state, propKey) {
|
|
|
75
76
|
* @param {import('./JoinReducer.js').WarpStateV5} state
|
|
76
77
|
* @param {Object} [options]
|
|
77
78
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
78
|
-
* @returns {Buffer}
|
|
79
|
+
* @returns {Buffer|Uint8Array}
|
|
79
80
|
*/
|
|
80
81
|
export function serializeStateV5(state, { codec } = {}) {
|
|
81
82
|
const c = codec || defaultCodec;
|
|
@@ -122,13 +123,14 @@ export function serializeStateV5(state, { codec } = {}) {
|
|
|
122
123
|
* Computes SHA-256 hash of canonical state bytes.
|
|
123
124
|
* @param {import('./JoinReducer.js').WarpStateV5} state
|
|
124
125
|
* @param {Object} [options] - Options
|
|
125
|
-
* @param {import('../../ports/CryptoPort.js').default} options.crypto - CryptoPort instance
|
|
126
|
+
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance
|
|
126
127
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
127
|
-
* @returns {Promise<string
|
|
128
|
+
* @returns {Promise<string>} Hex-encoded SHA-256 hash
|
|
128
129
|
*/
|
|
129
|
-
export async function computeStateHashV5(state, { crypto, codec } = {}) {
|
|
130
|
+
export async function computeStateHashV5(state, { crypto, codec } = /** @type {{crypto?: import('../../ports/CryptoPort.js').default, codec?: import('../../ports/CodecPort.js').default}} */ ({})) {
|
|
131
|
+
const c = crypto || defaultCrypto;
|
|
130
132
|
const serialized = serializeStateV5(state, { codec });
|
|
131
|
-
return
|
|
133
|
+
return await c.hash('sha256', serialized);
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
/**
|
|
@@ -141,7 +143,7 @@ export async function computeStateHashV5(state, { crypto, codec } = {}) {
|
|
|
141
143
|
*/
|
|
142
144
|
export function deserializeStateV5(buffer, { codec } = {}) {
|
|
143
145
|
const c = codec || defaultCodec;
|
|
144
|
-
return c.decode(buffer);
|
|
146
|
+
return /** @type {{nodes: string[], edges: Array<{from: string, to: string, label: string}>, props: Array<{node: string, key: string, value: *}>}} */ (c.decode(buffer));
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
// ============================================================================
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import defaultCodec from '../utils/defaultCodec.js';
|
|
2
|
+
import defaultCrypto from '../utils/defaultCrypto.js';
|
|
2
3
|
import ShardCorruptionError from '../errors/ShardCorruptionError.js';
|
|
3
4
|
import ShardValidationError from '../errors/ShardValidationError.js';
|
|
4
5
|
import nullLogger from '../utils/nullLogger.js';
|
|
@@ -36,10 +37,9 @@ const BITMAP_BASE_OVERHEAD = 64;
|
|
|
36
37
|
*
|
|
37
38
|
* @param {Object} data - The data object to checksum
|
|
38
39
|
* @param {import('../../ports/CryptoPort.js').default} crypto - CryptoPort instance
|
|
39
|
-
* @returns {Promise<string
|
|
40
|
+
* @returns {Promise<string>} Hex-encoded SHA-256 hash
|
|
40
41
|
*/
|
|
41
42
|
const computeChecksum = async (data, crypto) => {
|
|
42
|
-
if (!crypto) { return null; }
|
|
43
43
|
const json = canonicalStringify(data);
|
|
44
44
|
return await crypto.hash('sha256', json);
|
|
45
45
|
};
|
|
@@ -89,6 +89,8 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
89
89
|
* Receives { flushedBytes, totalFlushedBytes, flushCount }.
|
|
90
90
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for structured logging.
|
|
91
91
|
* Defaults to NoOpLogger (no logging).
|
|
92
|
+
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance for hashing
|
|
93
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
92
94
|
*/
|
|
93
95
|
constructor({ storage, maxMemoryBytes = DEFAULT_MAX_MEMORY_BYTES, onFlush, logger = nullLogger, crypto, codec }) {
|
|
94
96
|
if (!storage) {
|
|
@@ -99,9 +101,9 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/** @type {import('../../ports/CryptoPort.js').default} */
|
|
102
|
-
this._crypto = crypto;
|
|
104
|
+
this._crypto = crypto || defaultCrypto;
|
|
103
105
|
|
|
104
|
-
/** @type {import('../../ports/CodecPort.js').default
|
|
106
|
+
/** @type {import('../../ports/CodecPort.js').default} */
|
|
105
107
|
this._codec = codec || defaultCodec;
|
|
106
108
|
|
|
107
109
|
/** @type {Object} */
|
|
@@ -122,7 +124,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
122
124
|
/** @type {string[]} ID → SHA reverse mapping (kept in memory) */
|
|
123
125
|
this.idToSha = [];
|
|
124
126
|
|
|
125
|
-
/** @type {Map<string,
|
|
127
|
+
/** @type {Map<string, any>} Current in-memory bitmaps */
|
|
126
128
|
this.bitmaps = new Map();
|
|
127
129
|
|
|
128
130
|
/** @type {number} Estimated bytes used by current bitmaps */
|
|
@@ -137,8 +139,8 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
137
139
|
/** @type {number} Number of flush operations performed */
|
|
138
140
|
this.flushCount = 0;
|
|
139
141
|
|
|
140
|
-
/** @type {
|
|
141
|
-
this._RoaringBitmap32 = getRoaringBitmap32();
|
|
142
|
+
/** @type {any} Cached Roaring bitmap constructor */ // TODO(ts-cleanup): type lazy singleton
|
|
143
|
+
this._RoaringBitmap32 = getRoaringBitmap32(); // TODO(ts-cleanup): type lazy singleton
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
/**
|
|
@@ -189,11 +191,12 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
189
191
|
* Groups bitmaps by type ('fwd' or 'rev') and SHA prefix (first 2 hex chars).
|
|
190
192
|
* Each bitmap is serialized to a portable format and base64-encoded.
|
|
191
193
|
*
|
|
192
|
-
* @returns {
|
|
194
|
+
* @returns {Record<string, Record<string, Record<string, string>>>}
|
|
193
195
|
* Object with 'fwd' and 'rev' keys, each mapping prefix to SHA→base64Bitmap entries
|
|
194
196
|
* @private
|
|
195
197
|
*/
|
|
196
198
|
_serializeBitmapsToShards() {
|
|
199
|
+
/** @type {Record<string, Record<string, Record<string, string>>>} */
|
|
197
200
|
const bitmapShards = { fwd: {}, rev: {} };
|
|
198
201
|
for (const [key, bitmap] of this.bitmaps) {
|
|
199
202
|
const type = key.substring(0, 3);
|
|
@@ -215,7 +218,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
215
218
|
* The resulting blob OIDs are tracked in `flushedChunks` for later merging.
|
|
216
219
|
* Writes are performed in parallel for efficiency.
|
|
217
220
|
*
|
|
218
|
-
* @param {
|
|
221
|
+
* @param {Record<string, Record<string, Record<string, string>>>} bitmapShards
|
|
219
222
|
* Object with 'fwd' and 'rev' keys containing prefix-grouped bitmap data
|
|
220
223
|
* @returns {Promise<void>} Resolves when all shards have been written
|
|
221
224
|
* @async
|
|
@@ -235,11 +238,11 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
235
238
|
data: shardData,
|
|
236
239
|
};
|
|
237
240
|
const buffer = Buffer.from(JSON.stringify(envelope));
|
|
238
|
-
const oid = await this.storage.writeBlob(buffer);
|
|
241
|
+
const oid = await /** @type {any} */ (this.storage).writeBlob(buffer); // TODO(ts-cleanup): narrow port type
|
|
239
242
|
if (!this.flushedChunks.has(path)) {
|
|
240
243
|
this.flushedChunks.set(path, []);
|
|
241
244
|
}
|
|
242
|
-
this.flushedChunks.get(path).push(oid);
|
|
245
|
+
/** @type {string[]} */ (this.flushedChunks.get(path)).push(oid);
|
|
243
246
|
})
|
|
244
247
|
);
|
|
245
248
|
}
|
|
@@ -310,6 +313,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
310
313
|
* @private
|
|
311
314
|
*/
|
|
312
315
|
_buildMetaShards() {
|
|
316
|
+
/** @type {Record<string, Record<string, number>>} */
|
|
313
317
|
const idShards = {};
|
|
314
318
|
for (const [sha, id] of this.shaToId) {
|
|
315
319
|
const prefix = sha.substring(0, 2);
|
|
@@ -344,7 +348,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
344
348
|
data: map,
|
|
345
349
|
};
|
|
346
350
|
const buffer = Buffer.from(JSON.stringify(envelope));
|
|
347
|
-
const oid = await this.storage.writeBlob(buffer);
|
|
351
|
+
const oid = await /** @type {any} */ (this.storage).writeBlob(buffer); // TODO(ts-cleanup): narrow port type
|
|
348
352
|
return `100644 blob ${oid}\t${path}`;
|
|
349
353
|
})
|
|
350
354
|
);
|
|
@@ -436,18 +440,19 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
436
440
|
|
|
437
441
|
// Store frontier metadata for staleness detection
|
|
438
442
|
if (frontier) {
|
|
443
|
+
/** @type {Record<string, number|undefined>} */
|
|
439
444
|
const sorted = {};
|
|
440
445
|
for (const key of Array.from(frontier.keys()).sort()) {
|
|
441
446
|
sorted[key] = frontier.get(key);
|
|
442
447
|
}
|
|
443
448
|
const envelope = { version: 1, writerCount: frontier.size, frontier: sorted };
|
|
444
|
-
const cborOid = await this.storage.writeBlob(Buffer.from(this._codec.encode(envelope)));
|
|
449
|
+
const cborOid = await /** @type {any} */ (this.storage).writeBlob(Buffer.from(/** @type {any} */ (this._codec).encode(envelope))); // TODO(ts-cleanup): narrow port type
|
|
445
450
|
flatEntries.push(`100644 blob ${cborOid}\tfrontier.cbor`);
|
|
446
|
-
const jsonOid = await this.storage.writeBlob(Buffer.from(canonicalStringify(envelope)));
|
|
451
|
+
const jsonOid = await /** @type {any} */ (this.storage).writeBlob(Buffer.from(canonicalStringify(envelope))); // TODO(ts-cleanup): narrow port type
|
|
447
452
|
flatEntries.push(`100644 blob ${jsonOid}\tfrontier.json`);
|
|
448
453
|
}
|
|
449
454
|
|
|
450
|
-
const treeOid = await this.storage.writeTree(flatEntries);
|
|
455
|
+
const treeOid = await /** @type {any} */ (this.storage).writeTree(flatEntries); // TODO(ts-cleanup): narrow port type
|
|
451
456
|
|
|
452
457
|
this.logger.debug('Index finalized', {
|
|
453
458
|
operation: 'finalize',
|
|
@@ -501,7 +506,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
501
506
|
*/
|
|
502
507
|
_getOrCreateId(sha) {
|
|
503
508
|
if (this.shaToId.has(sha)) {
|
|
504
|
-
return this.shaToId.get(sha);
|
|
509
|
+
return /** @type {number} */ (this.shaToId.get(sha));
|
|
505
510
|
}
|
|
506
511
|
const id = this.idToSha.length;
|
|
507
512
|
this.idToSha.push(sha);
|
|
@@ -564,7 +569,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
564
569
|
* @private
|
|
565
570
|
*/
|
|
566
571
|
async _loadAndValidateChunk(oid) {
|
|
567
|
-
const buffer = await this.storage.readBlob(oid);
|
|
572
|
+
const buffer = await /** @type {any} */ (this.storage).readBlob(oid); // TODO(ts-cleanup): narrow port type
|
|
568
573
|
let envelope;
|
|
569
574
|
try {
|
|
570
575
|
envelope = JSON.parse(buffer.toString('utf-8'));
|
|
@@ -572,14 +577,13 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
572
577
|
throw new ShardCorruptionError('Failed to parse shard JSON', {
|
|
573
578
|
oid,
|
|
574
579
|
reason: 'invalid_format',
|
|
575
|
-
originalError: err.message,
|
|
580
|
+
context: { originalError: /** @type {any} */ (err).message }, // TODO(ts-cleanup): type error
|
|
576
581
|
});
|
|
577
582
|
}
|
|
578
583
|
|
|
579
584
|
// Validate version
|
|
580
585
|
if (envelope.version !== SHARD_VERSION) {
|
|
581
586
|
throw new ShardValidationError('Shard version mismatch', {
|
|
582
|
-
oid,
|
|
583
587
|
expected: SHARD_VERSION,
|
|
584
588
|
actual: envelope.version,
|
|
585
589
|
field: 'version',
|
|
@@ -610,7 +614,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
610
614
|
* it using `orInPlace` to combine edge sets.
|
|
611
615
|
*
|
|
612
616
|
* @param {Object} opts - Options object
|
|
613
|
-
* @param {
|
|
617
|
+
* @param {Record<string, any>} opts.merged - Object mapping SHA to
|
|
614
618
|
* RoaringBitmap32 instances (mutated in place)
|
|
615
619
|
* @param {string} opts.sha - The SHA key for this bitmap (40-character hex string)
|
|
616
620
|
* @param {string} opts.base64Bitmap - Base64-encoded serialized RoaringBitmap32 data
|
|
@@ -627,7 +631,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
627
631
|
throw new ShardCorruptionError('Failed to deserialize bitmap', {
|
|
628
632
|
oid,
|
|
629
633
|
reason: 'invalid_bitmap',
|
|
630
|
-
originalError: err.message,
|
|
634
|
+
context: { originalError: /** @type {any} */ (err).message }, // TODO(ts-cleanup): type error
|
|
631
635
|
});
|
|
632
636
|
}
|
|
633
637
|
|
|
@@ -671,6 +675,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
671
675
|
*/
|
|
672
676
|
async _mergeChunks(oids, { signal } = {}) {
|
|
673
677
|
// Load all chunks and merge bitmaps by SHA
|
|
678
|
+
/** @type {Record<string, any>} */
|
|
674
679
|
const merged = {};
|
|
675
680
|
|
|
676
681
|
for (const oid of oids) {
|
|
@@ -683,6 +688,7 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
683
688
|
}
|
|
684
689
|
|
|
685
690
|
// Serialize merged result
|
|
691
|
+
/** @type {Record<string, string>} */
|
|
686
692
|
const result = {};
|
|
687
693
|
for (const [sha, bitmap] of Object.entries(merged)) {
|
|
688
694
|
result[sha] = bitmap.serialize(true).toString('base64');
|
|
@@ -701,9 +707,9 @@ export default class StreamingBitmapIndexBuilder {
|
|
|
701
707
|
} catch (err) {
|
|
702
708
|
throw new ShardCorruptionError('Failed to serialize merged shard', {
|
|
703
709
|
reason: 'serialization_error',
|
|
704
|
-
originalError: err.message,
|
|
710
|
+
context: { originalError: /** @type {any} */ (err).message }, // TODO(ts-cleanup): type error
|
|
705
711
|
});
|
|
706
712
|
}
|
|
707
|
-
return this.storage.writeBlob(serialized);
|
|
713
|
+
return /** @type {any} */ (this.storage).writeBlob(serialized); // TODO(ts-cleanup): narrow port type
|
|
708
714
|
}
|
|
709
715
|
}
|
|
@@ -56,18 +56,16 @@ import { vvDeserialize } from '../crdt/VersionVector.js';
|
|
|
56
56
|
* **Mutation**: This function mutates the input patch object for efficiency.
|
|
57
57
|
* The original object reference is returned.
|
|
58
58
|
*
|
|
59
|
-
* @param {Object} patch - The raw decoded patch from CBOR
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* @param {Array} patch.ops - The patch operations (not modified)
|
|
63
|
-
* @returns {Object} The same patch object with context converted to Map
|
|
59
|
+
* @param {{ context?: Object | Map<any, any>, ops: any[] }} patch - The raw decoded patch from CBOR.
|
|
60
|
+
* If context is present as a plain object, it will be converted to a Map.
|
|
61
|
+
* @returns {{ context?: Object | Map<any, any>, ops: any[] }} The same patch object with context converted to Map
|
|
64
62
|
* @private
|
|
65
63
|
*/
|
|
66
64
|
function normalizePatch(patch) {
|
|
67
65
|
// Convert context from plain object to Map (VersionVector)
|
|
68
66
|
// CBOR deserialization returns plain objects, but join() expects a Map
|
|
69
67
|
if (patch.context && !(patch.context instanceof Map)) {
|
|
70
|
-
patch.context = vvDeserialize(patch.context);
|
|
68
|
+
patch.context = vvDeserialize(/** @type {{ [x: string]: number }} */ (patch.context));
|
|
71
69
|
}
|
|
72
70
|
return patch;
|
|
73
71
|
}
|
|
@@ -85,12 +83,12 @@ function normalizePatch(patch) {
|
|
|
85
83
|
* **Commit message format**: The message is encoded using WarpMessageCodec
|
|
86
84
|
* and contains metadata (schema version, writer info) plus the patch OID.
|
|
87
85
|
*
|
|
88
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} persistence - Git persistence layer
|
|
86
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} persistence - Git persistence layer
|
|
89
87
|
* (uses CommitPort.showNode() + BlobPort.readBlob() methods)
|
|
90
88
|
* @param {string} sha - The 40-character commit SHA to load the patch from
|
|
91
89
|
* @param {Object} [options]
|
|
92
90
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
93
|
-
* @returns {Promise<Object>} The decoded and normalized patch object containing:
|
|
91
|
+
* @returns {Promise<{ context?: Object | Map<any, any>, ops: any[] }>} The decoded and normalized patch object containing:
|
|
94
92
|
* - `ops`: Array of patch operations
|
|
95
93
|
* - `context`: VersionVector (Map) of causal dependencies
|
|
96
94
|
* - `writerId`: The writer who created this patch
|
|
@@ -101,7 +99,7 @@ function normalizePatch(patch) {
|
|
|
101
99
|
* @throws {Error} If the patch blob cannot be CBOR-decoded (corrupted data)
|
|
102
100
|
* @private
|
|
103
101
|
*/
|
|
104
|
-
async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
|
|
102
|
+
async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
105
103
|
const codec = codecOpt || defaultCodec;
|
|
106
104
|
// Read commit message to extract patch OID
|
|
107
105
|
const message = await persistence.showNode(sha);
|
|
@@ -109,7 +107,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
|
|
|
109
107
|
|
|
110
108
|
// Read and decode the patch blob
|
|
111
109
|
const patchBuffer = await persistence.readBlob(decoded.patchOid);
|
|
112
|
-
const patch = codec.decode(patchBuffer);
|
|
110
|
+
const patch = /** @type {{ context?: Object | Map<any, any>, ops: any[] }} */ (codec.decode(patchBuffer));
|
|
113
111
|
|
|
114
112
|
// Normalize the patch (convert context from object to Map)
|
|
115
113
|
return normalizePatch(patch);
|
|
@@ -129,7 +127,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
|
|
|
129
127
|
* **Performance**: O(N) where N is the number of commits between fromSha and toSha.
|
|
130
128
|
* Each commit requires two reads: commit info (for parent) and patch blob.
|
|
131
129
|
*
|
|
132
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} persistence - Git persistence layer
|
|
130
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} persistence - Git persistence layer
|
|
133
131
|
* (uses CommitPort.getNodeInfo()/showNode() + BlobPort.readBlob() methods)
|
|
134
132
|
* @param {string} graphName - Graph name (used in error messages, not for lookups)
|
|
135
133
|
* @param {string} writerId - Writer ID (used in error messages, not for lookups)
|
|
@@ -154,7 +152,7 @@ async function loadPatchFromCommit(persistence, sha, { codec: codecOpt } = {}) {
|
|
|
154
152
|
* // Load ALL patches for a new writer
|
|
155
153
|
* const patches = await loadPatchRange(persistence, 'events', 'new-writer', null, tipSha);
|
|
156
154
|
*/
|
|
157
|
-
export async function loadPatchRange(persistence, graphName, writerId, fromSha, toSha, { codec } = {}) {
|
|
155
|
+
export async function loadPatchRange(persistence, graphName, writerId, fromSha, toSha, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
158
156
|
const patches = [];
|
|
159
157
|
let cur = toSha;
|
|
160
158
|
|
|
@@ -172,9 +170,9 @@ export async function loadPatchRange(persistence, graphName, writerId, fromSha,
|
|
|
172
170
|
|
|
173
171
|
// If fromSha was specified but we didn't reach it, we have divergence
|
|
174
172
|
if (fromSha && cur === null) {
|
|
175
|
-
const err = new Error(
|
|
173
|
+
const err = /** @type {Error & { code: string }} */ (new Error(
|
|
176
174
|
`Divergence detected: ${toSha} does not descend from ${fromSha} for writer ${writerId}`
|
|
177
|
-
);
|
|
175
|
+
));
|
|
178
176
|
err.code = 'E_SYNC_DIVERGENCE';
|
|
179
177
|
throw err;
|
|
180
178
|
}
|
|
@@ -214,11 +212,7 @@ export async function loadPatchRange(persistence, graphName, writerId, fromSha,
|
|
|
214
212
|
* Maps writerId to the SHA of their latest patch commit.
|
|
215
213
|
* @param {Map<string, string>} remoteFrontier - Remote writer heads.
|
|
216
214
|
* Maps writerId to the SHA of their latest patch commit.
|
|
217
|
-
* @returns {
|
|
218
|
-
* - `needFromRemote`: Map<writerId, {from: string|null, to: string}> - Patches local needs
|
|
219
|
-
* - `needFromLocal`: Map<writerId, {from: string|null, to: string}> - Patches remote needs
|
|
220
|
-
* - `newWritersForLocal`: string[] - Writers that local has never seen
|
|
221
|
-
* - `newWritersForRemote`: string[] - Writers that remote has never seen
|
|
215
|
+
* @returns {{ needFromRemote: Map<string, {from: string|null, to: string}>, needFromLocal: Map<string, {from: string|null, to: string}>, newWritersForLocal: string[], newWritersForRemote: string[] }} Sync delta
|
|
222
216
|
*
|
|
223
217
|
* @example
|
|
224
218
|
* const local = new Map([['w1', 'sha-a'], ['w2', 'sha-b']]);
|
|
@@ -333,13 +327,14 @@ export function computeSyncDelta(localFrontier, remoteFrontier) {
|
|
|
333
327
|
*/
|
|
334
328
|
export function createSyncRequest(frontier) {
|
|
335
329
|
// Convert Map to plain object for serialization
|
|
330
|
+
/** @type {{ [x: string]: string }} */
|
|
336
331
|
const frontierObj = {};
|
|
337
332
|
for (const [writerId, sha] of frontier) {
|
|
338
333
|
frontierObj[writerId] = sha;
|
|
339
334
|
}
|
|
340
335
|
|
|
341
336
|
return {
|
|
342
|
-
type: 'sync-request',
|
|
337
|
+
type: /** @type {'sync-request'} */ ('sync-request'),
|
|
343
338
|
frontier: frontierObj,
|
|
344
339
|
};
|
|
345
340
|
}
|
|
@@ -363,7 +358,7 @@ export function createSyncRequest(frontier) {
|
|
|
363
358
|
*
|
|
364
359
|
* @param {SyncRequest} request - Incoming sync request containing the requester's frontier
|
|
365
360
|
* @param {Map<string, string>} localFrontier - Local frontier (what this node has)
|
|
366
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} persistence - Git persistence
|
|
361
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} persistence - Git persistence
|
|
367
362
|
* layer for loading patches (uses CommitPort + BlobPort methods)
|
|
368
363
|
* @param {string} graphName - Graph name for error messages and logging
|
|
369
364
|
* @returns {Promise<SyncResponse>} Response containing local frontier and patches.
|
|
@@ -379,7 +374,7 @@ export function createSyncRequest(frontier) {
|
|
|
379
374
|
* res.json(response);
|
|
380
375
|
* });
|
|
381
376
|
*/
|
|
382
|
-
export async function processSyncRequest(request, localFrontier, persistence, graphName, { codec } = {}) {
|
|
377
|
+
export async function processSyncRequest(request, localFrontier, persistence, graphName, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
383
378
|
// Convert incoming frontier from object to Map
|
|
384
379
|
const remoteFrontier = new Map(Object.entries(request.frontier));
|
|
385
380
|
|
|
@@ -406,7 +401,7 @@ export async function processSyncRequest(request, localFrontier, persistence, gr
|
|
|
406
401
|
} catch (err) {
|
|
407
402
|
// If we detect divergence, skip this writer
|
|
408
403
|
// The requester may need to handle this separately
|
|
409
|
-
if (err.code === 'E_SYNC_DIVERGENCE' || err.message
|
|
404
|
+
if (/** @type {any} */ (err).code === 'E_SYNC_DIVERGENCE' || /** @type {any} */ (err).message?.includes('Divergence detected')) { // TODO(ts-cleanup): type error
|
|
410
405
|
continue;
|
|
411
406
|
}
|
|
412
407
|
throw err;
|
|
@@ -414,13 +409,14 @@ export async function processSyncRequest(request, localFrontier, persistence, gr
|
|
|
414
409
|
}
|
|
415
410
|
|
|
416
411
|
// Convert local frontier to plain object
|
|
412
|
+
/** @type {{ [x: string]: string }} */
|
|
417
413
|
const frontierObj = {};
|
|
418
414
|
for (const [writerId, sha] of localFrontier) {
|
|
419
415
|
frontierObj[writerId] = sha;
|
|
420
416
|
}
|
|
421
417
|
|
|
422
418
|
return {
|
|
423
|
-
type: 'sync-response',
|
|
419
|
+
type: /** @type {'sync-response'} */ ('sync-response'),
|
|
424
420
|
frontier: frontierObj,
|
|
425
421
|
patches,
|
|
426
422
|
};
|
|
@@ -495,7 +491,7 @@ export function applySyncResponse(response, state, frontier) {
|
|
|
495
491
|
// will prevent silent data loss until the reader is upgraded.
|
|
496
492
|
assertOpsCompatible(normalizedPatch.ops, SCHEMA_V3);
|
|
497
493
|
// Apply patch to state
|
|
498
|
-
join(newState, normalizedPatch, sha);
|
|
494
|
+
join(newState, /** @type {*} */ (normalizedPatch), sha); // TODO(ts-cleanup): type patch array
|
|
499
495
|
applied++;
|
|
500
496
|
}
|
|
501
497
|
|
|
@@ -580,13 +576,14 @@ export function syncNeeded(localFrontier, remoteFrontier) {
|
|
|
580
576
|
* }
|
|
581
577
|
*/
|
|
582
578
|
export function createEmptySyncResponse(frontier) {
|
|
579
|
+
/** @type {{ [x: string]: string }} */
|
|
583
580
|
const frontierObj = {};
|
|
584
581
|
for (const [writerId, sha] of frontier) {
|
|
585
582
|
frontierObj[writerId] = sha;
|
|
586
583
|
}
|
|
587
584
|
|
|
588
585
|
return {
|
|
589
|
-
type: 'sync-response',
|
|
586
|
+
type: /** @type {'sync-response'} */ ('sync-response'),
|
|
590
587
|
frontier: frontierObj,
|
|
591
588
|
patches: [],
|
|
592
589
|
};
|
|
@@ -60,10 +60,11 @@ function unwrapValue(value) {
|
|
|
60
60
|
*
|
|
61
61
|
* @param {import('./JoinReducer.js').WarpStateV5} state - Current state
|
|
62
62
|
* @param {string} nodeId - Node ID to extract
|
|
63
|
-
* @returns {{ id: string, exists: boolean, props:
|
|
63
|
+
* @returns {{ id: string, exists: boolean, props: Record<string, *> }}
|
|
64
64
|
*/
|
|
65
65
|
function extractNodeSnapshot(state, nodeId) {
|
|
66
66
|
const exists = orsetContains(state.nodeAlive, nodeId);
|
|
67
|
+
/** @type {Record<string, *>} */
|
|
67
68
|
const props = {};
|
|
68
69
|
|
|
69
70
|
if (exists) {
|
|
@@ -108,7 +109,7 @@ export class TemporalQuery {
|
|
|
108
109
|
* @param {string} nodeId - The node ID to evaluate
|
|
109
110
|
* @param {Function} predicate - Predicate receiving node snapshot
|
|
110
111
|
* `{ id, exists, props }`. Should return boolean.
|
|
111
|
-
* @param {
|
|
112
|
+
* @param {Object} [options={}] - Options
|
|
112
113
|
* @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
|
|
113
114
|
* Only patches with lamport >= since are considered.
|
|
114
115
|
* @returns {Promise<boolean>} True if predicate held at every tick
|
|
@@ -161,7 +162,7 @@ export class TemporalQuery {
|
|
|
161
162
|
* @param {string} nodeId - The node ID to evaluate
|
|
162
163
|
* @param {Function} predicate - Predicate receiving node snapshot
|
|
163
164
|
* `{ id, exists, props }`. Should return boolean.
|
|
164
|
-
* @param {
|
|
165
|
+
* @param {Object} [options={}] - Options
|
|
165
166
|
* @param {number} [options.since=0] - Minimum Lamport tick (inclusive).
|
|
166
167
|
* Only patches with lamport >= since are considered.
|
|
167
168
|
* @returns {Promise<boolean>} True if predicate held at any tick
|
|
@@ -94,8 +94,8 @@ function zeroCost() {
|
|
|
94
94
|
/**
|
|
95
95
|
* Counts how many items in `source` are absent from `targetSet`.
|
|
96
96
|
*
|
|
97
|
-
* @param {Array
|
|
98
|
-
* @param {Set} targetSet - Target set to test against
|
|
97
|
+
* @param {Array<string>|Set<string>} source - Source collection
|
|
98
|
+
* @param {Set<string>} targetSet - Target set to test against
|
|
99
99
|
* @returns {number}
|
|
100
100
|
*/
|
|
101
101
|
function countMissing(source, targetSet) {
|
|
@@ -141,7 +141,7 @@ function computeEdgeLoss(state, nodesASet, nodesBSet) {
|
|
|
141
141
|
* Counts lost properties for a single node between two observer configs.
|
|
142
142
|
*
|
|
143
143
|
* @param {Map<string, boolean>} nodeProps - Property keys for the node
|
|
144
|
-
* @param {{ configA:
|
|
144
|
+
* @param {{ configA: {expose?: string[], redact?: string[]}, configB: {expose?: string[], redact?: string[]}, nodeInB: boolean }} opts
|
|
145
145
|
* @returns {{ propsInA: number, lostProps: number }}
|
|
146
146
|
*/
|
|
147
147
|
function countNodePropLoss(nodeProps, { configA, configB, nodeInB }) {
|
|
@@ -157,7 +157,7 @@ function countNodePropLoss(nodeProps, { configA, configB, nodeInB }) {
|
|
|
157
157
|
* Computes property loss across all A-visible nodes.
|
|
158
158
|
*
|
|
159
159
|
* @param {*} state - WarpStateV5
|
|
160
|
-
* @param {{ nodesA: string[], nodesBSet: Set<string>, configA:
|
|
160
|
+
* @param {{ nodesA: string[], nodesBSet: Set<string>, configA: {expose?: string[], redact?: string[]}, configB: {expose?: string[], redact?: string[]} }} opts
|
|
161
161
|
* @returns {number} propLoss fraction
|
|
162
162
|
*/
|
|
163
163
|
function computePropLoss(state, { nodesA, nodesBSet, configA, configB }) {
|
|
@@ -43,7 +43,7 @@ function validateSha(sha, paramName) {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Verifies that a SHA exists in the repository.
|
|
46
|
-
* @param {
|
|
46
|
+
* @param {{ nodeExists: (sha: string) => Promise<boolean> }} persistence - Git persistence adapter
|
|
47
47
|
* @param {string} sha - The SHA to verify
|
|
48
48
|
* @param {string} paramName - Parameter name for error messages
|
|
49
49
|
* @throws {WormholeError} If SHA doesn't exist
|
|
@@ -62,10 +62,11 @@ async function verifyShaExists(persistence, sha, paramName) {
|
|
|
62
62
|
/**
|
|
63
63
|
* Processes a single commit in the wormhole chain.
|
|
64
64
|
* @param {Object} opts - Options
|
|
65
|
-
* @param {
|
|
65
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} opts.persistence - Git persistence adapter
|
|
66
66
|
* @param {string} opts.sha - The commit SHA
|
|
67
67
|
* @param {string} opts.graphName - Expected graph name
|
|
68
68
|
* @param {string|null} opts.expectedWriter - Expected writer ID (null for first commit)
|
|
69
|
+
* @param {import('../../ports/CodecPort.js').default} [opts.codec] - Codec for deserialization
|
|
69
70
|
* @returns {Promise<{patch: Object, sha: string, writerId: string, parentSha: string|null}>}
|
|
70
71
|
* @throws {WormholeError} On validation errors
|
|
71
72
|
* @private
|
|
@@ -100,7 +101,7 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
const patchBuffer = await persistence.readBlob(patchMeta.patchOid);
|
|
103
|
-
const patch = codec.decode(patchBuffer);
|
|
104
|
+
const patch = /** @type {Object} */ (codec.decode(patchBuffer));
|
|
104
105
|
|
|
105
106
|
return {
|
|
106
107
|
patch,
|
|
@@ -135,10 +136,11 @@ async function processCommit({ persistence, sha, graphName, expectedWriter, code
|
|
|
135
136
|
* are inclusive in the wormhole.
|
|
136
137
|
*
|
|
137
138
|
* @param {Object} options - Wormhole creation options
|
|
138
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git persistence adapter
|
|
139
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} options.persistence - Git persistence adapter
|
|
139
140
|
* @param {string} options.graphName - Name of the graph
|
|
140
141
|
* @param {string} options.fromSha - SHA of the first (oldest) patch commit
|
|
141
142
|
* @param {string} options.toSha - SHA of the last (newest) patch commit
|
|
143
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
142
144
|
* @returns {Promise<WormholeEdge>} The created wormhole
|
|
143
145
|
* @throws {WormholeError} If fromSha or toSha doesn't exist (E_WORMHOLE_SHA_NOT_FOUND)
|
|
144
146
|
* @throws {WormholeError} If fromSha is not an ancestor of toSha (E_WORMHOLE_INVALID_RANGE)
|
|
@@ -156,7 +158,7 @@ export async function createWormhole({ persistence, graphName, fromSha, toSha, c
|
|
|
156
158
|
// Reverse to get oldest-first order (as required by ProvenancePayload)
|
|
157
159
|
patches.reverse();
|
|
158
160
|
|
|
159
|
-
const writerId = patches.length > 0 ? patches[0].writerId :
|
|
161
|
+
const writerId = patches.length > 0 ? patches[0].writerId : /** @type {string} */ ('');
|
|
160
162
|
// Strip writerId to match ProvenancePayload's PatchEntry typedef ({patch, sha})
|
|
161
163
|
const payload = new ProvenancePayload(patches.map(({ patch, sha }) => ({ patch, sha })));
|
|
162
164
|
|
|
@@ -170,10 +172,11 @@ export async function createWormhole({ persistence, graphName, fromSha, toSha, c
|
|
|
170
172
|
* validating each commit along the way.
|
|
171
173
|
*
|
|
172
174
|
* @param {Object} options
|
|
173
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git persistence adapter
|
|
175
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default & import('../../ports/BlobPort.js').default} options.persistence - Git persistence adapter
|
|
174
176
|
* @param {string} options.graphName - Expected graph name
|
|
175
177
|
* @param {string} options.fromSha - SHA of the first (oldest) patch commit
|
|
176
178
|
* @param {string} options.toSha - SHA of the last (newest) patch commit
|
|
179
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
177
180
|
* @returns {Promise<Array<{patch: Object, sha: string, writerId: string}>>} Patches in newest-first order
|
|
178
181
|
* @throws {WormholeError} If fromSha is not an ancestor of toSha or range is empty
|
|
179
182
|
* @private
|
|
@@ -230,7 +233,7 @@ async function collectPatchRange({ persistence, graphName, fromSha, toSha, codec
|
|
|
230
233
|
* @param {WormholeEdge} first - The earlier (older) wormhole
|
|
231
234
|
* @param {WormholeEdge} second - The later (newer) wormhole
|
|
232
235
|
* @param {Object} [options] - Composition options
|
|
233
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} [options.persistence] - Git persistence adapter (for validation)
|
|
236
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default} [options.persistence] - Git persistence adapter (for validation)
|
|
234
237
|
* @returns {Promise<WormholeEdge>} The composed wormhole
|
|
235
238
|
* @throws {WormholeError} If wormholes are from different writers (E_WORMHOLE_MULTI_WRITER)
|
|
236
239
|
* @throws {WormholeError} If wormholes are not consecutive (E_WORMHOLE_INVALID_RANGE)
|
|
@@ -318,9 +321,10 @@ export function deserializeWormhole(json) {
|
|
|
318
321
|
});
|
|
319
322
|
}
|
|
320
323
|
|
|
324
|
+
const /** @type {Record<string, *>} */ typedJson = /** @type {Record<string, *>} */ (json);
|
|
321
325
|
const requiredFields = ['fromSha', 'toSha', 'writerId', 'patchCount', 'payload'];
|
|
322
326
|
for (const field of requiredFields) {
|
|
323
|
-
if (
|
|
327
|
+
if (typedJson[field] === undefined) {
|
|
324
328
|
throw new WormholeError(`Invalid wormhole JSON: missing required field '${field}'`, {
|
|
325
329
|
code: 'E_INVALID_WORMHOLE_JSON',
|
|
326
330
|
context: { missingField: field },
|
|
@@ -328,19 +332,19 @@ export function deserializeWormhole(json) {
|
|
|
328
332
|
}
|
|
329
333
|
}
|
|
330
334
|
|
|
331
|
-
if (typeof
|
|
335
|
+
if (typeof typedJson.patchCount !== 'number' || typedJson.patchCount < 0) {
|
|
332
336
|
throw new WormholeError('Invalid wormhole JSON: patchCount must be a non-negative number', {
|
|
333
337
|
code: 'E_INVALID_WORMHOLE_JSON',
|
|
334
|
-
context: { patchCount:
|
|
338
|
+
context: { patchCount: typedJson.patchCount },
|
|
335
339
|
});
|
|
336
340
|
}
|
|
337
341
|
|
|
338
342
|
return {
|
|
339
|
-
fromSha:
|
|
340
|
-
toSha:
|
|
341
|
-
writerId:
|
|
342
|
-
patchCount:
|
|
343
|
-
payload: ProvenancePayload.fromJSON(
|
|
343
|
+
fromSha: typedJson.fromSha,
|
|
344
|
+
toSha: typedJson.toSha,
|
|
345
|
+
writerId: typedJson.writerId,
|
|
346
|
+
patchCount: typedJson.patchCount,
|
|
347
|
+
payload: ProvenancePayload.fromJSON(typedJson.payload),
|
|
344
348
|
};
|
|
345
349
|
}
|
|
346
350
|
|
|
@@ -67,11 +67,12 @@ function validateOp(op, index) {
|
|
|
67
67
|
throw new Error(`ops[${index}] must be an object`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const entry = /** @type {Record<string, *>} */ (op);
|
|
71
|
+
validateOpType(entry.op, index);
|
|
72
|
+
validateOpTarget(entry.target, index);
|
|
73
|
+
validateOpResult(entry.result, index);
|
|
73
74
|
|
|
74
|
-
if (
|
|
75
|
+
if (entry.reason !== undefined && typeof entry.reason !== 'string') {
|
|
75
76
|
throw new Error(`ops[${index}].reason must be a string or undefined`);
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -208,6 +209,7 @@ export function createTickReceipt({ patchSha, writer, lamport, ops }) {
|
|
|
208
209
|
// Build frozen op copies (defensive: don't alias caller's objects)
|
|
209
210
|
const frozenOps = Object.freeze(
|
|
210
211
|
ops.map((o) => {
|
|
212
|
+
/** @type {{ op: string, target: string, result: 'applied' | 'superseded' | 'redundant', reason?: string }} */
|
|
211
213
|
const entry = { op: o.op, target: o.target, result: o.result };
|
|
212
214
|
if (o.reason !== undefined) {
|
|
213
215
|
entry.reason = o.reason;
|
|
@@ -275,9 +277,11 @@ export function canonicalJson(receipt) {
|
|
|
275
277
|
*/
|
|
276
278
|
function sortedReplacer(_key, value) {
|
|
277
279
|
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
280
|
+
/** @type {{ [x: string]: * }} */
|
|
278
281
|
const sorted = {};
|
|
279
|
-
|
|
280
|
-
|
|
282
|
+
const obj = /** @type {{ [x: string]: * }} */ (value);
|
|
283
|
+
for (const k of Object.keys(obj).sort()) {
|
|
284
|
+
sorted[k] = obj[k];
|
|
281
285
|
}
|
|
282
286
|
return sorted;
|
|
283
287
|
}
|