@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
|
@@ -12,8 +12,8 @@ const DEFAULT_PATTERN = '*';
|
|
|
12
12
|
* @typedef {Object} QueryNodeSnapshot
|
|
13
13
|
* @property {string} id - The unique identifier of the node
|
|
14
14
|
* @property {Record<string, unknown>} props - Frozen snapshot of node properties
|
|
15
|
-
* @property {
|
|
16
|
-
* @property {
|
|
15
|
+
* @property {ReadonlyArray<{label: string, to?: string, from?: string}>} edgesOut - Outgoing edges sorted by label then target
|
|
16
|
+
* @property {ReadonlyArray<{label: string, to?: string, from?: string}>} edgesIn - Incoming edges sorted by label then source
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -271,6 +271,7 @@ function cloneValue(value) {
|
|
|
271
271
|
* @private
|
|
272
272
|
*/
|
|
273
273
|
function buildPropsSnapshot(propsMap) {
|
|
274
|
+
/** @type {Record<string, unknown>} */
|
|
274
275
|
const props = {};
|
|
275
276
|
const keys = [...propsMap.keys()].sort();
|
|
276
277
|
for (const key of keys) {
|
|
@@ -299,8 +300,8 @@ function buildEdgesSnapshot(edges, directionKey) {
|
|
|
299
300
|
if (a.label !== b.label) {
|
|
300
301
|
return a.label < b.label ? -1 : 1;
|
|
301
302
|
}
|
|
302
|
-
const aPeer = a[directionKey];
|
|
303
|
-
const bPeer = b[directionKey];
|
|
303
|
+
const aPeer = /** @type {string} */ (a[directionKey]);
|
|
304
|
+
const bPeer = /** @type {string} */ (b[directionKey]);
|
|
304
305
|
return aPeer < bPeer ? -1 : aPeer > bPeer ? 1 : 0;
|
|
305
306
|
});
|
|
306
307
|
return deepFreeze(list);
|
|
@@ -493,9 +494,13 @@ export default class QueryBuilder {
|
|
|
493
494
|
*/
|
|
494
495
|
constructor(graph) {
|
|
495
496
|
this._graph = graph;
|
|
497
|
+
/** @type {string|null} */
|
|
496
498
|
this._pattern = null;
|
|
499
|
+
/** @type {Array<{type: string, fn?: (node: QueryNodeSnapshot) => boolean, label?: string, depth?: [number, number]}>} */
|
|
497
500
|
this._operations = [];
|
|
501
|
+
/** @type {string[]|null} */
|
|
498
502
|
this._select = null;
|
|
503
|
+
/** @type {AggregateSpec|null} */
|
|
499
504
|
this._aggregate = null;
|
|
500
505
|
}
|
|
501
506
|
|
|
@@ -531,7 +536,7 @@ export default class QueryBuilder {
|
|
|
531
536
|
*/
|
|
532
537
|
where(fn) {
|
|
533
538
|
assertPredicate(fn);
|
|
534
|
-
const predicate = isPlainObject(fn) ? objectToPredicate(fn) : fn;
|
|
539
|
+
const predicate = isPlainObject(fn) ? objectToPredicate(/** @type {Record<string, unknown>} */ (fn)) : /** @type {(node: QueryNodeSnapshot) => boolean} */ (fn);
|
|
535
540
|
this._operations.push({ type: 'where', fn: predicate });
|
|
536
541
|
return this;
|
|
537
542
|
}
|
|
@@ -628,11 +633,6 @@ export default class QueryBuilder {
|
|
|
628
633
|
* The "props." prefix is optional and will be stripped automatically.
|
|
629
634
|
*
|
|
630
635
|
* @param {AggregateSpec} spec - Aggregation specification
|
|
631
|
-
* @param {boolean} [spec.count] - If true, include count of matched nodes
|
|
632
|
-
* @param {string} [spec.sum] - Property path to sum
|
|
633
|
-
* @param {string} [spec.avg] - Property path to average
|
|
634
|
-
* @param {string} [spec.min] - Property path to find minimum
|
|
635
|
-
* @param {string} [spec.max] - Property path to find maximum
|
|
636
636
|
* @returns {QueryBuilder} This builder for chaining
|
|
637
637
|
* @throws {QueryError} If spec is not a plain object (code: E_QUERY_AGGREGATE_TYPE)
|
|
638
638
|
* @throws {QueryError} If numeric aggregation keys are not strings (code: E_QUERY_AGGREGATE_TYPE)
|
|
@@ -646,11 +646,12 @@ export default class QueryBuilder {
|
|
|
646
646
|
});
|
|
647
647
|
}
|
|
648
648
|
const numericKeys = ['sum', 'avg', 'min', 'max'];
|
|
649
|
+
const specAny = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (spec));
|
|
649
650
|
for (const key of numericKeys) {
|
|
650
|
-
if (
|
|
651
|
+
if (specAny[key] !== undefined && typeof specAny[key] !== 'string') {
|
|
651
652
|
throw new QueryError(`aggregate() expects ${key} to be a string path`, {
|
|
652
653
|
code: 'E_QUERY_AGGREGATE_TYPE',
|
|
653
|
-
context: { key, receivedType: typeof
|
|
654
|
+
context: { key, receivedType: typeof specAny[key] },
|
|
654
655
|
});
|
|
655
656
|
}
|
|
656
657
|
}
|
|
@@ -674,7 +675,7 @@ export default class QueryBuilder {
|
|
|
674
675
|
* @throws {QueryError} If an unknown select field is specified (code: E_QUERY_SELECT_FIELD)
|
|
675
676
|
*/
|
|
676
677
|
async run() {
|
|
677
|
-
const materialized = await this._graph._materializeGraph();
|
|
678
|
+
const materialized = await /** @type {any} */ (this._graph)._materializeGraph(); // TODO(ts-cleanup): narrow port type
|
|
678
679
|
const { adjacency, stateHash } = materialized;
|
|
679
680
|
const allNodes = sortIds(await this._graph.getNodes());
|
|
680
681
|
|
|
@@ -696,15 +697,16 @@ export default class QueryBuilder {
|
|
|
696
697
|
};
|
|
697
698
|
})
|
|
698
699
|
);
|
|
700
|
+
const predicate = /** @type {(node: QueryNodeSnapshot) => boolean} */ (op.fn);
|
|
699
701
|
const filtered = snapshots
|
|
700
|
-
.filter(({ snapshot }) =>
|
|
702
|
+
.filter(({ snapshot }) => predicate(snapshot))
|
|
701
703
|
.map(({ nodeId }) => nodeId);
|
|
702
704
|
workingSet = sortIds(filtered);
|
|
703
705
|
continue;
|
|
704
706
|
}
|
|
705
707
|
|
|
706
708
|
if (op.type === 'outgoing' || op.type === 'incoming') {
|
|
707
|
-
const [minD, maxD] = op.depth;
|
|
709
|
+
const [minD, maxD] = /** @type {[number, number]} */ (op.depth);
|
|
708
710
|
if (minD === 1 && maxD === 1) {
|
|
709
711
|
workingSet = applyHop({
|
|
710
712
|
direction: op.type,
|
|
@@ -718,7 +720,7 @@ export default class QueryBuilder {
|
|
|
718
720
|
label: op.label,
|
|
719
721
|
workingSet,
|
|
720
722
|
adjacency,
|
|
721
|
-
depth: op.depth,
|
|
723
|
+
depth: /** @type {[number, number]} */ (op.depth),
|
|
722
724
|
});
|
|
723
725
|
}
|
|
724
726
|
}
|
|
@@ -778,21 +780,24 @@ export default class QueryBuilder {
|
|
|
778
780
|
* @private
|
|
779
781
|
*/
|
|
780
782
|
async _runAggregate(workingSet, stateHash) {
|
|
781
|
-
const spec = this._aggregate;
|
|
783
|
+
const spec = /** @type {AggregateSpec} */ (this._aggregate);
|
|
784
|
+
/** @type {AggregateResult} */
|
|
782
785
|
const result = { stateHash };
|
|
786
|
+
const specRec = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (spec));
|
|
783
787
|
|
|
784
788
|
if (spec.count) {
|
|
785
789
|
result.count = workingSet.length;
|
|
786
790
|
}
|
|
787
791
|
|
|
788
792
|
const numericAggs = ['sum', 'avg', 'min', 'max'];
|
|
789
|
-
const activeAggs = numericAggs.filter((key) =>
|
|
793
|
+
const activeAggs = numericAggs.filter((key) => specRec[key]);
|
|
790
794
|
|
|
791
795
|
if (activeAggs.length > 0) {
|
|
796
|
+
/** @type {Map<string, {segments: string[], values: number[]}>} */
|
|
792
797
|
const propsByAgg = new Map();
|
|
793
798
|
for (const key of activeAggs) {
|
|
794
799
|
propsByAgg.set(key, {
|
|
795
|
-
segments:
|
|
800
|
+
segments: /** @type {string} */ (specRec[key]).replace(/^props\./, '').split('.'),
|
|
796
801
|
values: [],
|
|
797
802
|
});
|
|
798
803
|
}
|
|
@@ -800,6 +805,7 @@ export default class QueryBuilder {
|
|
|
800
805
|
for (const nodeId of workingSet) {
|
|
801
806
|
const propsMap = (await this._graph.getNodeProps(nodeId)) || new Map();
|
|
802
807
|
for (const { segments, values } of propsByAgg.values()) {
|
|
808
|
+
/** @type {*} */ // TODO(ts-cleanup): type deep property traversal
|
|
803
809
|
let value = propsMap.get(segments[0]);
|
|
804
810
|
for (let i = 1; i < segments.length; i++) {
|
|
805
811
|
if (value && typeof value === 'object') {
|
|
@@ -817,15 +823,15 @@ export default class QueryBuilder {
|
|
|
817
823
|
|
|
818
824
|
for (const [key, { values }] of propsByAgg) {
|
|
819
825
|
if (key === 'sum') {
|
|
820
|
-
result.sum = values.length > 0 ? values.reduce((a, b) => a + b, 0) : 0;
|
|
826
|
+
result.sum = values.length > 0 ? values.reduce((/** @type {number} */ a, /** @type {number} */ b) => a + b, 0) : 0;
|
|
821
827
|
} else if (key === 'avg') {
|
|
822
|
-
result.avg = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
|
828
|
+
result.avg = values.length > 0 ? values.reduce((/** @type {number} */ a, /** @type {number} */ b) => a + b, 0) / values.length : 0;
|
|
823
829
|
} else if (key === 'min') {
|
|
824
830
|
result.min =
|
|
825
|
-
values.length > 0 ? values.reduce((m, v) => (v < m ? v : m), Infinity) : 0;
|
|
831
|
+
values.length > 0 ? values.reduce((/** @type {number} */ m, /** @type {number} */ v) => (v < m ? v : m), Infinity) : 0;
|
|
826
832
|
} else if (key === 'max') {
|
|
827
833
|
result.max =
|
|
828
|
-
values.length > 0 ? values.reduce((m, v) => (v > m ? v : m), -Infinity) : 0;
|
|
834
|
+
values.length > 0 ? values.reduce((/** @type {number} */ m, /** @type {number} */ v) => (v > m ? v : m), -Infinity) : 0;
|
|
829
835
|
}
|
|
830
836
|
}
|
|
831
837
|
}
|
|
@@ -86,8 +86,8 @@ function compareProps(a, b) {
|
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
88
|
* Checks if two arrays are deeply equal.
|
|
89
|
-
* @param {Array} a
|
|
90
|
-
* @param {Array} b
|
|
89
|
+
* @param {Array<*>} a
|
|
90
|
+
* @param {Array<*>} b
|
|
91
91
|
* @returns {boolean}
|
|
92
92
|
*/
|
|
93
93
|
function arraysEqual(a, b) {
|
|
@@ -104,8 +104,8 @@ function arraysEqual(a, b) {
|
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Checks if two objects are deeply equal.
|
|
107
|
-
* @param {
|
|
108
|
-
* @param {
|
|
107
|
+
* @param {Record<string, *>} a
|
|
108
|
+
* @param {Record<string, *>} b
|
|
109
109
|
* @returns {boolean}
|
|
110
110
|
*/
|
|
111
111
|
function objectsEqual(a, b) {
|
|
@@ -155,9 +155,9 @@ function deepEqual(a, b) {
|
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
157
|
* Computes set difference: elements in `after` not in `before`.
|
|
158
|
-
* @param {Set} before
|
|
159
|
-
* @param {Set} after
|
|
160
|
-
* @returns {Array}
|
|
158
|
+
* @param {Set<string>} before
|
|
159
|
+
* @param {Set<string>} after
|
|
160
|
+
* @returns {Array<string>}
|
|
161
161
|
*/
|
|
162
162
|
function setAdded(before, after) {
|
|
163
163
|
const result = [];
|
|
@@ -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
|
}
|