@git-stunts/git-warp 11.2.1 → 11.5.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 +24 -1
- package/bin/cli/commands/check.js +2 -2
- package/bin/cli/commands/doctor/checks.js +12 -12
- package/bin/cli/commands/doctor/index.js +2 -2
- package/bin/cli/commands/doctor/types.js +1 -1
- package/bin/cli/commands/history.js +12 -5
- package/bin/cli/commands/install-hooks.js +5 -5
- package/bin/cli/commands/materialize.js +2 -2
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +4 -4
- package/bin/cli/commands/query.js +54 -13
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/seek.js +17 -11
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +3 -3
- package/bin/cli/commands/verify-audit.js +8 -7
- package/bin/cli/commands/view.js +6 -5
- package/bin/cli/infrastructure.js +26 -12
- package/bin/cli/shared.js +2 -2
- package/bin/cli/types.js +19 -8
- package/bin/presenters/index.js +35 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +155 -33
- package/index.d.ts +118 -22
- package/index.js +2 -0
- package/package.json +5 -3
- package/src/domain/WarpGraph.js +4 -1
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/services/AuditReceiptService.js +6 -6
- package/src/domain/services/AuditVerifierService.js +52 -38
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +22 -3
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +92 -41
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +26 -11
- package/src/domain/services/KeyCodec.js +7 -0
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +1 -1
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +96 -30
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +3 -2
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +3 -3
- package/src/domain/trust/TrustEvaluator.js +18 -3
- package/src/domain/trust/TrustRecordService.js +30 -23
- package/src/domain/trust/TrustStateBuilder.js +21 -8
- package/src/domain/trust/canonical.js +6 -6
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +40 -18
- package/src/domain/warp/_wiredMethods.d.ts +199 -45
- package/src/domain/warp/checkpoint.methods.js +5 -1
- package/src/domain/warp/fork.methods.js +2 -2
- package/src/domain/warp/materialize.methods.js +55 -5
- package/src/domain/warp/materializeAdvanced.methods.js +15 -4
- package/src/domain/warp/patch.methods.js +54 -29
- package/src/domain/warp/provenance.methods.js +5 -3
- package/src/domain/warp/query.methods.js +89 -6
- package/src/domain/warp/sync.methods.js +16 -11
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
|
@@ -16,6 +16,19 @@
|
|
|
16
16
|
* @see docs/specs/AUDIT_RECEIPT.md Section 8
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} AuditReceipt
|
|
21
|
+
* @property {number} version
|
|
22
|
+
* @property {string} graphName
|
|
23
|
+
* @property {string} writerId
|
|
24
|
+
* @property {string} dataCommit
|
|
25
|
+
* @property {string} opsDigest
|
|
26
|
+
* @property {string} prevAuditCommit
|
|
27
|
+
* @property {number} tickStart
|
|
28
|
+
* @property {number} tickEnd
|
|
29
|
+
* @property {number} timestamp
|
|
30
|
+
*/
|
|
31
|
+
|
|
19
32
|
import { buildAuditPrefix, buildAuditRef } from '../utils/RefLayout.js';
|
|
20
33
|
import { decodeAuditMessage } from './AuditMessageCodec.js';
|
|
21
34
|
import { TrustRecordService } from '../trust/TrustRecordService.js';
|
|
@@ -67,14 +80,15 @@ function validateOidFormat(value) {
|
|
|
67
80
|
|
|
68
81
|
/**
|
|
69
82
|
* Checks whether a receipt object has the expected 9 fields with correct types.
|
|
70
|
-
* @param {
|
|
83
|
+
* @param {unknown} receipt
|
|
71
84
|
* @returns {string|null} Error message or null if valid
|
|
72
85
|
*/
|
|
73
86
|
function validateReceiptSchema(receipt) {
|
|
74
87
|
if (!receipt || typeof receipt !== 'object') {
|
|
75
88
|
return 'receipt is not an object';
|
|
76
89
|
}
|
|
77
|
-
const
|
|
90
|
+
const rec = /** @type {Record<string, unknown>} */ (receipt);
|
|
91
|
+
const keys = Object.keys(rec);
|
|
78
92
|
if (keys.length !== 9) {
|
|
79
93
|
return `expected 9 fields, got ${keys.length}`;
|
|
80
94
|
}
|
|
@@ -83,46 +97,46 @@ function validateReceiptSchema(receipt) {
|
|
|
83
97
|
'tickEnd', 'tickStart', 'timestamp', 'version', 'writerId',
|
|
84
98
|
];
|
|
85
99
|
for (const k of required) {
|
|
86
|
-
if (!(k in
|
|
100
|
+
if (!(k in rec)) {
|
|
87
101
|
return `missing field: ${k}`;
|
|
88
102
|
}
|
|
89
103
|
}
|
|
90
|
-
if (
|
|
91
|
-
return `unsupported version: ${
|
|
104
|
+
if (rec.version !== 1) {
|
|
105
|
+
return `unsupported version: ${rec.version}`;
|
|
92
106
|
}
|
|
93
|
-
if (typeof
|
|
107
|
+
if (typeof rec.graphName !== 'string' || rec.graphName.length === 0) {
|
|
94
108
|
return 'graphName must be a non-empty string';
|
|
95
109
|
}
|
|
96
|
-
if (typeof
|
|
110
|
+
if (typeof rec.writerId !== 'string' || rec.writerId.length === 0) {
|
|
97
111
|
return 'writerId must be a non-empty string';
|
|
98
112
|
}
|
|
99
|
-
if (typeof
|
|
113
|
+
if (typeof rec.dataCommit !== 'string') {
|
|
100
114
|
return 'dataCommit must be a string';
|
|
101
115
|
}
|
|
102
|
-
if (typeof
|
|
116
|
+
if (typeof rec.opsDigest !== 'string') {
|
|
103
117
|
return 'opsDigest must be a string';
|
|
104
118
|
}
|
|
105
|
-
if (typeof
|
|
119
|
+
if (typeof rec.prevAuditCommit !== 'string') {
|
|
106
120
|
return 'prevAuditCommit must be a string';
|
|
107
121
|
}
|
|
108
|
-
if (!Number.isInteger(
|
|
109
|
-
return `tickStart must be integer >= 1, got ${
|
|
122
|
+
if (!Number.isInteger(rec.tickStart) || /** @type {number} */ (rec.tickStart) < 1) {
|
|
123
|
+
return `tickStart must be integer >= 1, got ${rec.tickStart}`;
|
|
110
124
|
}
|
|
111
|
-
if (!Number.isInteger(
|
|
112
|
-
return `tickEnd must be integer >= tickStart, got ${
|
|
125
|
+
if (!Number.isInteger(rec.tickEnd) || /** @type {number} */ (rec.tickEnd) < /** @type {number} */ (rec.tickStart)) {
|
|
126
|
+
return `tickEnd must be integer >= tickStart, got ${rec.tickEnd}`;
|
|
113
127
|
}
|
|
114
|
-
if (
|
|
115
|
-
return `v1 requires tickStart === tickEnd, got ${
|
|
128
|
+
if (rec.version === 1 && rec.tickStart !== rec.tickEnd) {
|
|
129
|
+
return `v1 requires tickStart === tickEnd, got ${rec.tickStart} !== ${rec.tickEnd}`;
|
|
116
130
|
}
|
|
117
|
-
if (!Number.isInteger(
|
|
118
|
-
return `timestamp must be non-negative integer, got ${
|
|
131
|
+
if (!Number.isInteger(rec.timestamp) || /** @type {number} */ (rec.timestamp) < 0) {
|
|
132
|
+
return `timestamp must be non-negative integer, got ${rec.timestamp}`;
|
|
119
133
|
}
|
|
120
134
|
return null;
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
/**
|
|
124
138
|
* Validates trailers against the CBOR receipt fields.
|
|
125
|
-
* @param {
|
|
139
|
+
* @param {AuditReceipt} receipt
|
|
126
140
|
* @param {{ graph: string, writer: string, dataCommit: string, opsDigest: string, schema: number }} decoded
|
|
127
141
|
* @returns {string|null} Error message or null if consistent
|
|
128
142
|
*/
|
|
@@ -312,7 +326,7 @@ export class AuditVerifierService {
|
|
|
312
326
|
*/
|
|
313
327
|
async _walkChain(graphName, writerId, tip, since, result) {
|
|
314
328
|
let current = tip;
|
|
315
|
-
/** @type {
|
|
329
|
+
/** @type {AuditReceipt|null} */ let prevReceipt = null;
|
|
316
330
|
/** @type {number|null} */ let chainOidLen = null;
|
|
317
331
|
|
|
318
332
|
while (current) {
|
|
@@ -322,8 +336,8 @@ export class AuditVerifierService {
|
|
|
322
336
|
let commitInfo;
|
|
323
337
|
try {
|
|
324
338
|
commitInfo = await this._persistence.getNodeInfo(current);
|
|
325
|
-
} catch (
|
|
326
|
-
this._addError(result, 'MISSING_RECEIPT_BLOB', `Cannot read commit ${current}: ${err
|
|
339
|
+
} catch (err) {
|
|
340
|
+
this._addError(result, 'MISSING_RECEIPT_BLOB', `Cannot read commit ${current}: ${err instanceof Error ? err.message : String(err)}`, current);
|
|
327
341
|
return;
|
|
328
342
|
}
|
|
329
343
|
|
|
@@ -456,7 +470,7 @@ export class AuditVerifierService {
|
|
|
456
470
|
* @param {string} commitSha
|
|
457
471
|
* @param {{ message: string }} commitInfo
|
|
458
472
|
* @param {ChainResult} result
|
|
459
|
-
* @returns {Promise<{ receipt:
|
|
473
|
+
* @returns {Promise<{ receipt: AuditReceipt, decodedTrailers: { graph: string, writer: string, dataCommit: string, opsDigest: string, schema: number } }|null>}
|
|
460
474
|
* @private
|
|
461
475
|
*/
|
|
462
476
|
async _readReceipt(commitSha, commitInfo, result) {
|
|
@@ -464,9 +478,9 @@ export class AuditVerifierService {
|
|
|
464
478
|
let treeOid;
|
|
465
479
|
try {
|
|
466
480
|
treeOid = await this._persistence.getCommitTree(commitSha);
|
|
467
|
-
} catch (
|
|
481
|
+
} catch (err) {
|
|
468
482
|
this._addError(result, 'MISSING_RECEIPT_BLOB',
|
|
469
|
-
`Cannot read tree for ${commitSha}: ${err
|
|
483
|
+
`Cannot read tree for ${commitSha}: ${err instanceof Error ? err.message : String(err)}`, commitSha);
|
|
470
484
|
return null;
|
|
471
485
|
}
|
|
472
486
|
|
|
@@ -474,9 +488,9 @@ export class AuditVerifierService {
|
|
|
474
488
|
let treeEntries;
|
|
475
489
|
try {
|
|
476
490
|
treeEntries = await this._persistence.readTreeOids(treeOid);
|
|
477
|
-
} catch (
|
|
491
|
+
} catch (err) {
|
|
478
492
|
this._addError(result, 'RECEIPT_TREE_INVALID',
|
|
479
|
-
`Cannot read tree ${treeOid}: ${err
|
|
493
|
+
`Cannot read tree ${treeOid}: ${err instanceof Error ? err.message : String(err)}`, commitSha);
|
|
480
494
|
return null;
|
|
481
495
|
}
|
|
482
496
|
|
|
@@ -493,19 +507,19 @@ export class AuditVerifierService {
|
|
|
493
507
|
let blobContent;
|
|
494
508
|
try {
|
|
495
509
|
blobContent = await this._persistence.readBlob(blobOid);
|
|
496
|
-
} catch (
|
|
510
|
+
} catch (err) {
|
|
497
511
|
this._addError(result, 'MISSING_RECEIPT_BLOB',
|
|
498
|
-
`Cannot read receipt blob ${blobOid}: ${err
|
|
512
|
+
`Cannot read receipt blob ${blobOid}: ${err instanceof Error ? err.message : String(err)}`, commitSha);
|
|
499
513
|
return null;
|
|
500
514
|
}
|
|
501
515
|
|
|
502
516
|
// Decode CBOR
|
|
503
517
|
let receipt;
|
|
504
518
|
try {
|
|
505
|
-
receipt = this._codec.decode(blobContent);
|
|
506
|
-
} catch (
|
|
519
|
+
receipt = /** @type {AuditReceipt} */ (this._codec.decode(blobContent));
|
|
520
|
+
} catch (err) {
|
|
507
521
|
this._addError(result, 'CBOR_DECODE_FAILED',
|
|
508
|
-
`CBOR decode failed: ${err
|
|
522
|
+
`CBOR decode failed: ${err instanceof Error ? err.message : String(err)}`, commitSha);
|
|
509
523
|
result.status = STATUS_ERROR;
|
|
510
524
|
return null;
|
|
511
525
|
}
|
|
@@ -514,9 +528,9 @@ export class AuditVerifierService {
|
|
|
514
528
|
let decodedTrailers;
|
|
515
529
|
try {
|
|
516
530
|
decodedTrailers = decodeAuditMessage(commitInfo.message);
|
|
517
|
-
} catch (
|
|
531
|
+
} catch (err) {
|
|
518
532
|
this._addError(result, 'TRAILER_MISMATCH',
|
|
519
|
-
`Trailer decode failed: ${err
|
|
533
|
+
`Trailer decode failed: ${err instanceof Error ? err.message : String(err)}`, commitSha);
|
|
520
534
|
result.status = STATUS_DATA_MISMATCH;
|
|
521
535
|
return null;
|
|
522
536
|
}
|
|
@@ -526,7 +540,7 @@ export class AuditVerifierService {
|
|
|
526
540
|
|
|
527
541
|
/**
|
|
528
542
|
* Validates OID format for dataCommit, prevAuditCommit, and opsDigest.
|
|
529
|
-
* @param {
|
|
543
|
+
* @param {AuditReceipt} receipt
|
|
530
544
|
* @param {ChainResult} result
|
|
531
545
|
* @param {string} commitSha
|
|
532
546
|
* @returns {boolean} true if valid
|
|
@@ -556,8 +570,8 @@ export class AuditVerifierService {
|
|
|
556
570
|
|
|
557
571
|
/**
|
|
558
572
|
* Validates chain linking between current and previous (newer) receipt.
|
|
559
|
-
* @param {
|
|
560
|
-
* @param {
|
|
573
|
+
* @param {AuditReceipt} currentReceipt - The older receipt being validated
|
|
574
|
+
* @param {AuditReceipt} prevReceipt - The newer receipt (closer to tip)
|
|
561
575
|
* @param {string} commitSha
|
|
562
576
|
* @param {ChainResult} result
|
|
563
577
|
* @returns {boolean} true if valid
|
|
@@ -645,7 +659,7 @@ export class AuditVerifierService {
|
|
|
645
659
|
* @param {Object} [options]
|
|
646
660
|
* @param {string} [options.pin] - Pinned trust chain commit SHA
|
|
647
661
|
* @param {string} [options.mode] - Policy mode ('warn' or 'enforce')
|
|
648
|
-
* @returns {Promise<
|
|
662
|
+
* @returns {Promise<import('../trust/TrustEvaluator.js').TrustAssessment>}
|
|
649
663
|
*/
|
|
650
664
|
async evaluateTrust(graphName, options = {}) {
|
|
651
665
|
const recordService = new TrustRecordService({
|
|
@@ -103,7 +103,7 @@ export default class BitmapIndexBuilder {
|
|
|
103
103
|
this.shaToId = new Map();
|
|
104
104
|
/** @type {string[]} */
|
|
105
105
|
this.idToSha = [];
|
|
106
|
-
/** @type {Map<string,
|
|
106
|
+
/** @type {Map<string, import('../utils/roaring.js').RoaringBitmapSubset>} */
|
|
107
107
|
this.bitmaps = new Map();
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -178,7 +178,7 @@ export default class BitmapIndexBuilder {
|
|
|
178
178
|
bitmapShards[type][prefix] = {};
|
|
179
179
|
}
|
|
180
180
|
// Encode bitmap as base64 for JSON storage
|
|
181
|
-
bitmapShards[type][prefix][sha] = bitmap.serialize(true).toString('base64');
|
|
181
|
+
bitmapShards[type][prefix][sha] = Buffer.from(bitmap.serialize(true)).toString('base64');
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
for (const type of ['fwd', 'rev']) {
|
|
@@ -224,6 +224,6 @@ export default class BitmapIndexBuilder {
|
|
|
224
224
|
const RoaringBitmap32 = ensureRoaringBitmap32();
|
|
225
225
|
this.bitmaps.set(key, new RoaringBitmap32());
|
|
226
226
|
}
|
|
227
|
-
this.bitmaps.get(key).add(id);
|
|
227
|
+
/** @type {import('../utils/roaring.js').RoaringBitmapSubset} */ (this.bitmaps.get(key)).add(id);
|
|
228
228
|
}
|
|
229
229
|
}
|
|
@@ -6,6 +6,7 @@ import { getRoaringBitmap32 } from '../utils/roaring.js';
|
|
|
6
6
|
import { canonicalStringify } from '../utils/canonicalStringify.js';
|
|
7
7
|
|
|
8
8
|
/** @typedef {import('../../ports/IndexStoragePort.js').default} IndexStoragePort */
|
|
9
|
+
/** @typedef {import('../types/WarpPersistence.js').IndexStorage} IndexStorage */
|
|
9
10
|
/** @typedef {import('../../ports/LoggerPort.js').default} LoggerPort */
|
|
10
11
|
/** @typedef {import('../../ports/CryptoPort.js').default} CryptoPort */
|
|
11
12
|
|
|
@@ -89,11 +90,11 @@ export default class BitmapIndexReader {
|
|
|
89
90
|
* When exceeded, least recently used shards are evicted to free memory.
|
|
90
91
|
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - CryptoPort instance for checksum verification.
|
|
91
92
|
*/
|
|
92
|
-
constructor({ storage, strict = false, logger = nullLogger, maxCachedShards = DEFAULT_MAX_CACHED_SHARDS, crypto } = /** @type {
|
|
93
|
+
constructor({ storage, strict = false, logger = nullLogger, maxCachedShards = DEFAULT_MAX_CACHED_SHARDS, crypto } = /** @type {{ storage: IndexStoragePort, strict?: boolean, logger?: LoggerPort, maxCachedShards?: number, crypto?: CryptoPort }} */ ({})) {
|
|
93
94
|
if (!storage) {
|
|
94
95
|
throw new Error('BitmapIndexReader requires a storage adapter');
|
|
95
96
|
}
|
|
96
|
-
this.storage = storage;
|
|
97
|
+
this.storage = /** @type {IndexStorage} */ (storage);
|
|
97
98
|
this.strict = strict;
|
|
98
99
|
this.logger = logger;
|
|
99
100
|
this.maxCachedShards = maxCachedShards;
|
|
@@ -144,7 +145,8 @@ export default class BitmapIndexReader {
|
|
|
144
145
|
async lookupId(sha) {
|
|
145
146
|
const prefix = sha.substring(0, 2);
|
|
146
147
|
const path = `meta_${prefix}.json`;
|
|
147
|
-
|
|
148
|
+
// Meta shards always map SHA→numeric ID (built by BitmapIndexBuilder)
|
|
149
|
+
const idMap = /** @type {Record<string, number>} */ (await this._getOrLoadShard(path, 'json'));
|
|
148
150
|
return idMap[sha];
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -176,7 +178,8 @@ export default class BitmapIndexReader {
|
|
|
176
178
|
async _getEdges(sha, type) {
|
|
177
179
|
const prefix = sha.substring(0, 2);
|
|
178
180
|
const shardPath = `shards_${type}_${prefix}.json`;
|
|
179
|
-
|
|
181
|
+
// Bitmap shards always map SHA→base64-encoded bitmap data
|
|
182
|
+
const shard = /** @type {Record<string, string>} */ (await this._getOrLoadShard(shardPath, 'json'));
|
|
180
183
|
|
|
181
184
|
const encoded = shard[sha];
|
|
182
185
|
if (!encoded) {
|
|
@@ -195,7 +198,7 @@ export default class BitmapIndexReader {
|
|
|
195
198
|
shardPath,
|
|
196
199
|
oid: this.shardOids.get(shardPath),
|
|
197
200
|
reason: 'bitmap_deserialize_error',
|
|
198
|
-
context: { originalError:
|
|
201
|
+
context: { originalError: err instanceof Error ? err.message : String(err) },
|
|
199
202
|
});
|
|
200
203
|
this._handleShardError(corruptionError, {
|
|
201
204
|
path: shardPath,
|
|
@@ -224,7 +227,8 @@ export default class BitmapIndexReader {
|
|
|
224
227
|
|
|
225
228
|
for (const [path] of this.shardOids) {
|
|
226
229
|
if (path.startsWith('meta_') && path.endsWith('.json')) {
|
|
227
|
-
|
|
230
|
+
// Meta shards always map SHA→numeric ID (built by BitmapIndexBuilder)
|
|
231
|
+
const shard = /** @type {Record<string, number>} */ (await this._getOrLoadShard(path, 'json'));
|
|
228
232
|
for (const [sha, id] of Object.entries(shard)) {
|
|
229
233
|
this._idToShaCache[id] = sha;
|
|
230
234
|
}
|
|
@@ -247,10 +251,10 @@ export default class BitmapIndexReader {
|
|
|
247
251
|
/**
|
|
248
252
|
* Validates a shard envelope for version and checksum integrity.
|
|
249
253
|
*
|
|
250
|
-
* @param {{ data?:
|
|
254
|
+
* @param {{ data?: Record<string, string | number>, version?: number, checksum?: string }} envelope - The shard envelope to validate
|
|
251
255
|
* @param {string} path - Shard path (for error context)
|
|
252
256
|
* @param {string} oid - Object ID (for error context)
|
|
253
|
-
* @returns {Promise<
|
|
257
|
+
* @returns {Promise<Record<string, string | number>>} The validated data from the envelope
|
|
254
258
|
* @throws {ShardCorruptionError} If envelope format is invalid
|
|
255
259
|
* @throws {ShardValidationError} If version or checksum validation fails
|
|
256
260
|
* @private
|
|
@@ -299,7 +303,7 @@ export default class BitmapIndexReader {
|
|
|
299
303
|
* @param {string} context.path - Shard path
|
|
300
304
|
* @param {string} context.oid - Object ID
|
|
301
305
|
* @param {string} context.format - 'json' or 'bitmap'
|
|
302
|
-
* @returns {
|
|
306
|
+
* @returns {Record<string, string | number> | import('../utils/roaring.js').RoaringBitmapSubset} Empty shard (non-strict mode only)
|
|
303
307
|
* @throws {ShardCorruptionError|ShardValidationError} In strict mode
|
|
304
308
|
* @private
|
|
305
309
|
*/
|
|
@@ -307,17 +311,21 @@ export default class BitmapIndexReader {
|
|
|
307
311
|
if (this.strict) {
|
|
308
312
|
throw err;
|
|
309
313
|
}
|
|
310
|
-
/** @type {
|
|
311
|
-
const
|
|
314
|
+
/** @type {string|undefined} */
|
|
315
|
+
const field = err instanceof ShardValidationError ? err.field : undefined;
|
|
316
|
+
/** @type {unknown} */
|
|
317
|
+
const expected = err instanceof ShardValidationError ? err.expected : undefined;
|
|
318
|
+
/** @type {unknown} */
|
|
319
|
+
const actual = err instanceof ShardValidationError ? err.actual : undefined;
|
|
312
320
|
this.logger.warn('Shard validation warning', {
|
|
313
321
|
operation: 'loadShard',
|
|
314
322
|
shardPath: path,
|
|
315
323
|
oid,
|
|
316
324
|
error: err.message,
|
|
317
325
|
code: err.code,
|
|
318
|
-
field
|
|
319
|
-
expected
|
|
320
|
-
actual
|
|
326
|
+
field,
|
|
327
|
+
expected,
|
|
328
|
+
actual,
|
|
321
329
|
});
|
|
322
330
|
const emptyShard = format === 'json' ? {} : new (getRoaringBitmap32())();
|
|
323
331
|
this.loadedShards.set(path, emptyShard);
|
|
@@ -329,7 +337,7 @@ export default class BitmapIndexReader {
|
|
|
329
337
|
* @param {Buffer} buffer - Raw shard buffer
|
|
330
338
|
* @param {string} path - Shard path (for error context)
|
|
331
339
|
* @param {string} oid - Object ID (for error context)
|
|
332
|
-
* @returns {Promise<
|
|
340
|
+
* @returns {Promise<Record<string, string | number>>} The validated data from the shard
|
|
333
341
|
* @throws {ShardCorruptionError} If parsing fails or format is invalid
|
|
334
342
|
* @throws {ShardValidationError} If version or checksum validation fails
|
|
335
343
|
* @private
|
|
@@ -349,7 +357,7 @@ export default class BitmapIndexReader {
|
|
|
349
357
|
*/
|
|
350
358
|
async _loadShardBuffer(path, oid) {
|
|
351
359
|
try {
|
|
352
|
-
return await
|
|
360
|
+
return await this.storage.readBlob(oid);
|
|
353
361
|
} catch (cause) {
|
|
354
362
|
throw new ShardLoadError('Failed to load shard from storage', {
|
|
355
363
|
shardPath: path,
|
|
@@ -382,15 +390,16 @@ export default class BitmapIndexReader {
|
|
|
382
390
|
/**
|
|
383
391
|
* Attempts to handle a shard error based on its type.
|
|
384
392
|
* Returns handled result for validation/corruption errors, null otherwise.
|
|
385
|
-
* @param {
|
|
393
|
+
* @param {unknown} err - The error to handle
|
|
386
394
|
* @param {Object} context - Error context
|
|
387
395
|
* @param {string} context.path - Shard path
|
|
388
396
|
* @param {string} context.oid - Object ID
|
|
389
397
|
* @param {string} context.format - 'json' or 'bitmap'
|
|
390
|
-
* @returns {
|
|
398
|
+
* @returns {Record<string, string | number> | import('../utils/roaring.js').RoaringBitmapSubset | null} Handled result or null if error should be re-thrown
|
|
391
399
|
* @private
|
|
392
400
|
*/
|
|
393
401
|
_tryHandleShardError(err, context) {
|
|
402
|
+
if (!(err instanceof Error)) { return null; }
|
|
394
403
|
const wrappedErr = this._wrapParseError(err, context.path, context.oid);
|
|
395
404
|
const isHandleable = wrappedErr instanceof ShardCorruptionError ||
|
|
396
405
|
wrappedErr instanceof ShardValidationError;
|
|
@@ -406,7 +415,7 @@ export default class BitmapIndexReader {
|
|
|
406
415
|
*
|
|
407
416
|
* @param {string} path - Shard path
|
|
408
417
|
* @param {string} format - 'json' or 'bitmap'
|
|
409
|
-
* @returns {Promise<
|
|
418
|
+
* @returns {Promise<Record<string, string | number> | import('../utils/roaring.js').RoaringBitmapSubset>}
|
|
410
419
|
* @throws {ShardLoadError} When storage.readBlob fails
|
|
411
420
|
* @throws {ShardCorruptionError} When shard format is invalid (strict mode only)
|
|
412
421
|
* @throws {ShardValidationError} When version or checksum validation fails (strict mode only)
|
|
@@ -82,7 +82,7 @@ const BTR_VERSION = 1;
|
|
|
82
82
|
* @param {string} fields.h_in - Hash of input state
|
|
83
83
|
* @param {string} fields.h_out - Hash of output state
|
|
84
84
|
* @param {Uint8Array} fields.U_0 - Serialized initial state
|
|
85
|
-
* @param {Array
|
|
85
|
+
* @param {Array<unknown>} fields.P - Serialized provenance payload
|
|
86
86
|
* @param {string} fields.t - ISO timestamp
|
|
87
87
|
* @param {string|Uint8Array} key - HMAC key
|
|
88
88
|
* @param {{ crypto: import('../../ports/CryptoPort.js').default, codec?: import('../../ports/CodecPort.js').default }} deps - Dependencies
|
|
@@ -111,7 +111,7 @@ async function computeHmac(fields, key, { crypto, codec }) {
|
|
|
111
111
|
* @property {string} h_in - Hash of input state (hex SHA-256)
|
|
112
112
|
* @property {string} h_out - Hash of output state (hex SHA-256)
|
|
113
113
|
* @property {Uint8Array} U_0 - Serialized initial state (CBOR)
|
|
114
|
-
* @property {Array
|
|
114
|
+
* @property {Array<unknown>} P - Serialized provenance payload
|
|
115
115
|
* @property {string} t - ISO 8601 timestamp
|
|
116
116
|
* @property {string} kappa - Authentication tag (hex HMAC-SHA256)
|
|
117
117
|
*/
|
|
@@ -189,7 +189,7 @@ const REQUIRED_FIELDS = ['version', 'h_in', 'h_out', 'U_0', 'P', 't', 'kappa'];
|
|
|
189
189
|
/**
|
|
190
190
|
* Validates BTR structure and returns failure reason if invalid.
|
|
191
191
|
*
|
|
192
|
-
* @param {
|
|
192
|
+
* @param {unknown} btr - The BTR object to validate
|
|
193
193
|
* @returns {string|null} Error message if invalid, null if valid
|
|
194
194
|
* @private
|
|
195
195
|
*/
|
|
@@ -197,13 +197,14 @@ function validateBTRStructure(btr) {
|
|
|
197
197
|
if (!btr || typeof btr !== 'object') {
|
|
198
198
|
return 'BTR must be an object';
|
|
199
199
|
}
|
|
200
|
+
const rec = /** @type {Record<string, unknown>} */ (btr);
|
|
200
201
|
for (const field of REQUIRED_FIELDS) {
|
|
201
|
-
if (!(field in
|
|
202
|
+
if (!(field in rec)) {
|
|
202
203
|
return `Missing required field: ${field}`;
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
|
-
if (
|
|
206
|
-
return `Unsupported BTR version: ${
|
|
206
|
+
if (rec.version !== BTR_VERSION) {
|
|
207
|
+
return `Unsupported BTR version: ${rec.version} (expected ${BTR_VERSION})`;
|
|
207
208
|
}
|
|
208
209
|
return null;
|
|
209
210
|
}
|
|
@@ -250,7 +251,7 @@ async function verifyHmac(btr, key, { crypto, codec }) {
|
|
|
250
251
|
* @returns {Promise<string|null>} Error message if replay mismatch, null if valid
|
|
251
252
|
* @private
|
|
252
253
|
*/
|
|
253
|
-
async function verifyReplayHash(btr, { crypto, codec } =
|
|
254
|
+
async function verifyReplayHash(btr, { crypto, codec } = {}) {
|
|
254
255
|
try {
|
|
255
256
|
const result = await replayBTR(btr, { crypto, codec });
|
|
256
257
|
if (result.h_out !== btr.h_out) {
|
|
@@ -258,7 +259,7 @@ async function verifyReplayHash(btr, { crypto, codec } = /** @type {*} */ ({}))
|
|
|
258
259
|
}
|
|
259
260
|
return null;
|
|
260
261
|
} catch (err) {
|
|
261
|
-
return `Replay failed: ${
|
|
262
|
+
return `Replay failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
|
|
@@ -280,7 +281,7 @@ async function verifyReplayHash(btr, { crypto, codec } = /** @type {*} */ ({}))
|
|
|
280
281
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
281
282
|
* @returns {Promise<VerificationResult>} Verification result with valid flag and optional reason
|
|
282
283
|
*/
|
|
283
|
-
export async function verifyBTR(btr, key, options =
|
|
284
|
+
export async function verifyBTR(btr, key, options = {}) {
|
|
284
285
|
const { crypto, codec } = options;
|
|
285
286
|
|
|
286
287
|
const structureError = validateBTRStructure(btr);
|
|
@@ -323,13 +324,13 @@ export async function verifyBTR(btr, key, options = /** @type {*} */ ({})) { //
|
|
|
323
324
|
* The final state and its hash
|
|
324
325
|
* @throws {Error} If replay fails
|
|
325
326
|
*/
|
|
326
|
-
export async function replayBTR(btr, { crypto, codec } =
|
|
327
|
+
export async function replayBTR(btr, { crypto, codec } = {}) {
|
|
327
328
|
// Deserialize initial state from U_0
|
|
328
329
|
// Note: U_0 is the full serialized state (via serializeFullStateV5)
|
|
329
330
|
const initialState = deserializeInitialState(btr.U_0, { codec });
|
|
330
331
|
|
|
331
332
|
// Reconstruct payload
|
|
332
|
-
const payload = ProvenancePayload.fromJSON(btr.P);
|
|
333
|
+
const payload = ProvenancePayload.fromJSON(/** @type {import('./ProvenancePayload.js').PatchEntry[]} */ (btr.P));
|
|
333
334
|
|
|
334
335
|
// Replay
|
|
335
336
|
const finalState = payload.replay(initialState);
|
|
@@ -355,7 +356,7 @@ export async function replayBTR(btr, { crypto, codec } = /** @type {*} */ ({}))
|
|
|
355
356
|
* @returns {import('./JoinReducer.js').WarpStateV5} The deserialized state
|
|
356
357
|
* @private
|
|
357
358
|
*/
|
|
358
|
-
function deserializeInitialState(U_0, { codec } =
|
|
359
|
+
function deserializeInitialState(U_0, { codec } = {}) {
|
|
359
360
|
return deserializeFullStateV5(U_0, { codec });
|
|
360
361
|
}
|
|
361
362
|
|
|
@@ -370,7 +371,7 @@ function deserializeInitialState(U_0, { codec } = /** @type {*} */ ({})) { // TO
|
|
|
370
371
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
371
372
|
* @returns {Uint8Array} CBOR-encoded BTR
|
|
372
373
|
*/
|
|
373
|
-
export function serializeBTR(btr, { codec } =
|
|
374
|
+
export function serializeBTR(btr, { codec } = {}) {
|
|
374
375
|
const c = codec || defaultCodec;
|
|
375
376
|
return c.encode({
|
|
376
377
|
version: btr.version,
|
|
@@ -392,9 +393,9 @@ export function serializeBTR(btr, { codec } = /** @type {*} */ ({})) { // TODO(t
|
|
|
392
393
|
* @returns {BTR} The deserialized BTR
|
|
393
394
|
* @throws {Error} If the bytes are not valid CBOR or missing required fields
|
|
394
395
|
*/
|
|
395
|
-
export function deserializeBTR(bytes, { codec } =
|
|
396
|
+
export function deserializeBTR(bytes, { codec } = {}) {
|
|
396
397
|
const c = codec || defaultCodec;
|
|
397
|
-
const obj = /** @type {Record<string,
|
|
398
|
+
const obj = /** @type {Record<string, unknown>} */ (c.decode(bytes));
|
|
398
399
|
|
|
399
400
|
// Validate structure (reuse module-level constant for consistency with validateBTRStructure)
|
|
400
401
|
for (const field of REQUIRED_FIELDS) {
|
|
@@ -403,7 +404,7 @@ export function deserializeBTR(bytes, { codec } = /** @type {*} */ ({})) { // TO
|
|
|
403
404
|
}
|
|
404
405
|
}
|
|
405
406
|
|
|
406
|
-
return {
|
|
407
|
+
return /** @type {BTR} */ ({
|
|
407
408
|
version: obj.version,
|
|
408
409
|
h_in: obj.h_in,
|
|
409
410
|
h_out: obj.h_out,
|
|
@@ -411,7 +412,7 @@ export function deserializeBTR(bytes, { codec } = /** @type {*} */ ({})) { // TO
|
|
|
411
412
|
P: obj.P,
|
|
412
413
|
t: obj.t,
|
|
413
414
|
kappa: obj.kappa,
|
|
414
|
-
};
|
|
415
|
+
});
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
/**
|
|
@@ -39,7 +39,7 @@ import { createEmptyStateV5 } from './JoinReducer.js';
|
|
|
39
39
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
40
40
|
* @returns {Buffer|Uint8Array} CBOR-encoded full state
|
|
41
41
|
*/
|
|
42
|
-
export function serializeFullStateV5(state, { codec } =
|
|
42
|
+
export function serializeFullStateV5(state, { codec } = {}) {
|
|
43
43
|
const c = codec || defaultCodec;
|
|
44
44
|
// Serialize ORSets using existing serialization
|
|
45
45
|
const nodeAliveObj = orsetSerialize(state.nodeAlive);
|
|
@@ -90,14 +90,14 @@ export function serializeFullStateV5(state, { codec } = /** @type {*} */ ({})) {
|
|
|
90
90
|
* @returns {import('./JoinReducer.js').WarpStateV5}
|
|
91
91
|
*/
|
|
92
92
|
// eslint-disable-next-line complexity
|
|
93
|
-
export function deserializeFullStateV5(buffer, { codec: codecOpt } =
|
|
93
|
+
export function deserializeFullStateV5(buffer, { codec: codecOpt } = {}) {
|
|
94
94
|
const codec = codecOpt || defaultCodec;
|
|
95
95
|
// Handle null/undefined buffer before attempting decode
|
|
96
96
|
if (buffer === null || buffer === undefined) {
|
|
97
97
|
return createEmptyStateV5();
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const obj = /** @type {Record<string,
|
|
100
|
+
const obj = /** @type {Record<string, unknown>} */ (codec.decode(buffer));
|
|
101
101
|
|
|
102
102
|
// Handle null/undefined decoded result: return empty state
|
|
103
103
|
if (obj === null || obj === undefined) {
|
|
@@ -107,16 +107,17 @@ export function deserializeFullStateV5(buffer, { codec: codecOpt } = /** @type {
|
|
|
107
107
|
// Handle version mismatch: throw with diagnostic info
|
|
108
108
|
// Accept both 'full-v5' and missing version (for backward compatibility with pre-versioned data)
|
|
109
109
|
if (obj.version !== undefined && obj.version !== 'full-v5') {
|
|
110
|
+
const ver = /** @type {string} */ (obj.version);
|
|
110
111
|
throw new Error(
|
|
111
|
-
`Unsupported full state version: expected 'full-v5', got '${
|
|
112
|
+
`Unsupported full state version: expected 'full-v5', got '${ver}'`
|
|
112
113
|
);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
return {
|
|
116
117
|
nodeAlive: orsetDeserialize(obj.nodeAlive || {}),
|
|
117
118
|
edgeAlive: orsetDeserialize(obj.edgeAlive || {}),
|
|
118
|
-
prop: deserializeProps(obj.prop),
|
|
119
|
-
observedFrontier: vvDeserialize(obj.observedFrontier || {}),
|
|
119
|
+
prop: deserializeProps(/** @type {[string, unknown][]} */ (obj.prop)),
|
|
120
|
+
observedFrontier: vvDeserialize(/** @type {{[x: string]: number}} */ (obj.observedFrontier || {})),
|
|
120
121
|
edgeBirthEvent: /** @type {Map<string, import('../utils/EventId.js').EventId>} */ (deserializeEdgeBirthEvent(obj)),
|
|
121
122
|
};
|
|
122
123
|
}
|
|
@@ -172,7 +173,7 @@ export function computeAppliedVV(state) {
|
|
|
172
173
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
173
174
|
* @returns {Buffer|Uint8Array} CBOR-encoded version vector
|
|
174
175
|
*/
|
|
175
|
-
export function serializeAppliedVV(vv, { codec } =
|
|
176
|
+
export function serializeAppliedVV(vv, { codec } = {}) {
|
|
176
177
|
const c = codec || defaultCodec;
|
|
177
178
|
const obj = vvSerialize(vv);
|
|
178
179
|
return c.encode(obj);
|
|
@@ -186,7 +187,7 @@ export function serializeAppliedVV(vv, { codec } = /** @type {*} */ ({})) { // T
|
|
|
186
187
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
187
188
|
* @returns {Map<string, number>} Version vector
|
|
188
189
|
*/
|
|
189
|
-
export function deserializeAppliedVV(buffer, { codec } =
|
|
190
|
+
export function deserializeAppliedVV(buffer, { codec } = {}) {
|
|
190
191
|
const c = codec || defaultCodec;
|
|
191
192
|
const obj = /** @type {{ [x: string]: number }} */ (c.decode(buffer));
|
|
192
193
|
return vvDeserialize(obj);
|
|
@@ -198,14 +199,14 @@ export function deserializeAppliedVV(buffer, { codec } = /** @type {*} */ ({}))
|
|
|
198
199
|
|
|
199
200
|
/**
|
|
200
201
|
* Deserializes the props array from checkpoint format.
|
|
201
|
-
* @param {Array
|
|
202
|
-
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister
|
|
202
|
+
* @param {Array<[string, unknown]>} propArray - Array of [key, registerObj] pairs
|
|
203
|
+
* @returns {Map<string, import('../crdt/LWW.js').LWWRegister<unknown>>}
|
|
203
204
|
*/
|
|
204
205
|
function deserializeProps(propArray) {
|
|
205
206
|
const prop = new Map();
|
|
206
207
|
if (propArray && Array.isArray(propArray)) {
|
|
207
208
|
for (const [key, registerObj] of propArray) {
|
|
208
|
-
prop.set(key, deserializeLWWRegister(registerObj));
|
|
209
|
+
prop.set(key, deserializeLWWRegister(/** @type {{ eventId: { lamport: number, writerId: string, patchSha: string, opIndex: number }, value: unknown } | null} */ (registerObj)));
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
return prop;
|
|
@@ -213,7 +214,7 @@ function deserializeProps(propArray) {
|
|
|
213
214
|
|
|
214
215
|
/**
|
|
215
216
|
* Deserializes edge birth event data, supporting both legacy and current formats.
|
|
216
|
-
* @param {Record<string,
|
|
217
|
+
* @param {Record<string, unknown>} obj - The decoded checkpoint object
|
|
217
218
|
* @returns {Map<string, import('../utils/EventId.js').EventId>}
|
|
218
219
|
*/
|
|
219
220
|
function deserializeEdgeBirthEvent(obj) {
|
|
@@ -240,8 +241,8 @@ function deserializeEdgeBirthEvent(obj) {
|
|
|
240
241
|
* Serializes an LWW register for CBOR encoding.
|
|
241
242
|
* EventId is serialized as a plain object with sorted keys.
|
|
242
243
|
*
|
|
243
|
-
* @param {import('../crdt/LWW.js').LWWRegister
|
|
244
|
-
* @returns {{ eventId: { lamport: number, opIndex: number, patchSha: string, writerId: string }, value:
|
|
244
|
+
* @param {import('../crdt/LWW.js').LWWRegister<unknown>} register
|
|
245
|
+
* @returns {{ eventId: { lamport: number, opIndex: number, patchSha: string, writerId: string }, value: unknown } | null}
|
|
245
246
|
*/
|
|
246
247
|
function serializeLWWRegister(register) {
|
|
247
248
|
if (!register) {
|
|
@@ -262,8 +263,8 @@ function serializeLWWRegister(register) {
|
|
|
262
263
|
/**
|
|
263
264
|
* Deserializes an LWW register from CBOR.
|
|
264
265
|
*
|
|
265
|
-
* @param {{ eventId: { lamport: number, writerId: string, patchSha: string, opIndex: number }, value:
|
|
266
|
-
* @returns {import('../crdt/LWW.js').LWWRegister
|
|
266
|
+
* @param {{ eventId: { lamport: number, writerId: string, patchSha: string, opIndex: number }, value: unknown } | null} obj
|
|
267
|
+
* @returns {import('../crdt/LWW.js').LWWRegister<unknown> | null}
|
|
267
268
|
*/
|
|
268
269
|
function deserializeLWWRegister(obj) {
|
|
269
270
|
if (!obj) {
|