@git-stunts/git-warp 11.5.0 → 12.0.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 +145 -1
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/reindex.js +41 -0
- package/bin/cli/commands/verify-index.js +59 -0
- package/bin/cli/infrastructure.js +7 -2
- package/bin/cli/schemas.js +19 -0
- package/bin/cli/types.js +2 -0
- package/index.d.ts +49 -12
- package/package.json +2 -2
- package/src/domain/WarpGraph.js +62 -2
- package/src/domain/errors/ShardIdOverflowError.js +28 -0
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AdjacencyNeighborProvider.js +140 -0
- package/src/domain/services/BitmapIndexReader.js +32 -10
- package/src/domain/services/BitmapNeighborProvider.js +178 -0
- package/src/domain/services/CheckpointMessageCodec.js +3 -3
- package/src/domain/services/CheckpointService.js +77 -12
- package/src/domain/services/GraphTraversal.js +1239 -0
- package/src/domain/services/IncrementalIndexUpdater.js +765 -0
- package/src/domain/services/JoinReducer.js +310 -46
- package/src/domain/services/LogicalBitmapIndexBuilder.js +323 -0
- package/src/domain/services/LogicalIndexBuildService.js +108 -0
- package/src/domain/services/LogicalIndexReader.js +315 -0
- package/src/domain/services/LogicalTraversal.js +321 -202
- package/src/domain/services/MaterializedViewService.js +379 -0
- package/src/domain/services/ObserverView.js +138 -47
- package/src/domain/services/PatchBuilderV2.js +3 -3
- package/src/domain/services/PropertyIndexBuilder.js +64 -0
- package/src/domain/services/PropertyIndexReader.js +111 -0
- package/src/domain/services/SyncController.js +576 -0
- package/src/domain/services/TemporalQuery.js +128 -14
- package/src/domain/types/PatchDiff.js +90 -0
- package/src/domain/types/WarpTypesV2.js +4 -4
- package/src/domain/utils/MinHeap.js +45 -17
- package/src/domain/utils/canonicalCbor.js +36 -0
- package/src/domain/utils/fnv1a.js +20 -0
- package/src/domain/utils/roaring.js +14 -3
- package/src/domain/utils/shardKey.js +40 -0
- package/src/domain/utils/toBytes.js +17 -0
- package/src/domain/utils/validateShardOid.js +13 -0
- package/src/domain/warp/_internal.js +0 -9
- package/src/domain/warp/_wiredMethods.d.ts +8 -2
- package/src/domain/warp/checkpoint.methods.js +21 -5
- package/src/domain/warp/materialize.methods.js +17 -5
- package/src/domain/warp/materializeAdvanced.methods.js +142 -3
- package/src/domain/warp/query.methods.js +78 -12
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
- package/src/ports/BlobPort.js +1 -1
- package/src/ports/NeighborProviderPort.js +59 -0
- package/src/ports/SeekCachePort.js +4 -3
- package/src/domain/warp/sync.methods.js +0 -554
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* @see Paper IV - Echo and the WARP Core (CTL* temporal logic on histories)
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { createEmptyStateV5, join as joinPatch } from './JoinReducer.js';
|
|
29
|
+
import { createEmptyStateV5, cloneStateV5, join as joinPatch } from './JoinReducer.js';
|
|
30
30
|
import { decodePropKey } from './KeyCodec.js';
|
|
31
31
|
import { orsetContains } from '../crdt/ORSet.js';
|
|
32
32
|
|
|
@@ -83,6 +83,64 @@ function extractNodeSnapshot(state, nodeId) {
|
|
|
83
83
|
return { id: nodeId, exists, props };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Evaluates checkpoint boundary semantics for `always()`.
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} params
|
|
90
|
+
* @param {import('./JoinReducer.js').WarpStateV5} params.state
|
|
91
|
+
* @param {string} params.nodeId
|
|
92
|
+
* @param {Function} params.predicate
|
|
93
|
+
* @param {number|null} params.checkpointMaxLamport
|
|
94
|
+
* @param {number} params.since
|
|
95
|
+
* @returns {{ nodeEverExisted: boolean, shouldReturn: boolean, returnValue: boolean }}
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
function evaluateAlwaysCheckpointBoundary({
|
|
99
|
+
state,
|
|
100
|
+
nodeId,
|
|
101
|
+
predicate,
|
|
102
|
+
checkpointMaxLamport,
|
|
103
|
+
since,
|
|
104
|
+
}) {
|
|
105
|
+
if (checkpointMaxLamport !== since) {
|
|
106
|
+
return { nodeEverExisted: false, shouldReturn: false, returnValue: false };
|
|
107
|
+
}
|
|
108
|
+
const snapshot = extractNodeSnapshot(state, nodeId);
|
|
109
|
+
if (!snapshot.exists) {
|
|
110
|
+
return { nodeEverExisted: false, shouldReturn: false, returnValue: false };
|
|
111
|
+
}
|
|
112
|
+
if (!predicate(snapshot)) {
|
|
113
|
+
return { nodeEverExisted: true, shouldReturn: true, returnValue: false };
|
|
114
|
+
}
|
|
115
|
+
return { nodeEverExisted: true, shouldReturn: false, returnValue: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Evaluates checkpoint boundary semantics for `eventually()`.
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} params
|
|
122
|
+
* @param {import('./JoinReducer.js').WarpStateV5} params.state
|
|
123
|
+
* @param {string} params.nodeId
|
|
124
|
+
* @param {Function} params.predicate
|
|
125
|
+
* @param {number|null} params.checkpointMaxLamport
|
|
126
|
+
* @param {number} params.since
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
function evaluateEventuallyCheckpointBoundary({
|
|
131
|
+
state,
|
|
132
|
+
nodeId,
|
|
133
|
+
predicate,
|
|
134
|
+
checkpointMaxLamport,
|
|
135
|
+
since,
|
|
136
|
+
}) {
|
|
137
|
+
if (checkpointMaxLamport !== since) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const snapshot = extractNodeSnapshot(state, nodeId);
|
|
141
|
+
return snapshot.exists && predicate(snapshot);
|
|
142
|
+
}
|
|
143
|
+
|
|
86
144
|
/**
|
|
87
145
|
* TemporalQuery provides temporal logic operators over graph history.
|
|
88
146
|
*
|
|
@@ -94,10 +152,14 @@ export class TemporalQuery {
|
|
|
94
152
|
* @param {Object} options
|
|
95
153
|
* @param {Function} options.loadAllPatches - Async function that returns
|
|
96
154
|
* all patches as Array<{ patch, sha }> in causal order.
|
|
155
|
+
* @param {Function} [options.loadCheckpoint] - Async function returning
|
|
156
|
+
* { state: WarpStateV5, maxLamport: number } or null.
|
|
97
157
|
*/
|
|
98
|
-
constructor({ loadAllPatches }) {
|
|
158
|
+
constructor({ loadAllPatches, loadCheckpoint }) {
|
|
99
159
|
/** @type {Function} */
|
|
100
160
|
this._loadAllPatches = loadAllPatches;
|
|
161
|
+
/** @type {Function|null} */
|
|
162
|
+
this._loadCheckpoint = loadCheckpoint || null;
|
|
101
163
|
}
|
|
102
164
|
|
|
103
165
|
/**
|
|
@@ -128,19 +190,27 @@ export class TemporalQuery {
|
|
|
128
190
|
const since = options.since ?? 0;
|
|
129
191
|
const allPatches = await this._loadAllPatches();
|
|
130
192
|
|
|
131
|
-
const state =
|
|
132
|
-
|
|
193
|
+
const { state, startIdx, checkpointMaxLamport } = await this._resolveStart(allPatches, since);
|
|
194
|
+
const boundary = evaluateAlwaysCheckpointBoundary({
|
|
195
|
+
state,
|
|
196
|
+
nodeId,
|
|
197
|
+
predicate,
|
|
198
|
+
checkpointMaxLamport,
|
|
199
|
+
since,
|
|
200
|
+
});
|
|
201
|
+
if (boundary.shouldReturn) {
|
|
202
|
+
return boundary.returnValue;
|
|
203
|
+
}
|
|
204
|
+
let { nodeEverExisted } = boundary;
|
|
133
205
|
|
|
134
|
-
for (
|
|
135
|
-
|
|
206
|
+
for (let i = startIdx; i < allPatches.length; i++) {
|
|
207
|
+
const { patch, sha } = allPatches[i];
|
|
136
208
|
joinPatch(state, patch, sha);
|
|
137
209
|
|
|
138
|
-
// Skip patches before the `since` threshold
|
|
139
210
|
if (patch.lamport < since) {
|
|
140
211
|
continue;
|
|
141
212
|
}
|
|
142
213
|
|
|
143
|
-
// Extract node snapshot at this tick
|
|
144
214
|
const snapshot = extractNodeSnapshot(state, nodeId);
|
|
145
215
|
|
|
146
216
|
if (snapshot.exists) {
|
|
@@ -151,7 +221,6 @@ export class TemporalQuery {
|
|
|
151
221
|
}
|
|
152
222
|
}
|
|
153
223
|
|
|
154
|
-
// If the node never existed in the range, return false
|
|
155
224
|
return nodeEverExisted;
|
|
156
225
|
}
|
|
157
226
|
|
|
@@ -181,18 +250,26 @@ export class TemporalQuery {
|
|
|
181
250
|
const since = options.since ?? 0;
|
|
182
251
|
const allPatches = await this._loadAllPatches();
|
|
183
252
|
|
|
184
|
-
const state =
|
|
253
|
+
const { state, startIdx, checkpointMaxLamport } = await this._resolveStart(allPatches, since);
|
|
185
254
|
|
|
186
|
-
|
|
187
|
-
|
|
255
|
+
if (evaluateEventuallyCheckpointBoundary({
|
|
256
|
+
state,
|
|
257
|
+
nodeId,
|
|
258
|
+
predicate,
|
|
259
|
+
checkpointMaxLamport,
|
|
260
|
+
since,
|
|
261
|
+
})) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (let i = startIdx; i < allPatches.length; i++) {
|
|
266
|
+
const { patch, sha } = allPatches[i];
|
|
188
267
|
joinPatch(state, patch, sha);
|
|
189
268
|
|
|
190
|
-
// Skip patches before the `since` threshold
|
|
191
269
|
if (patch.lamport < since) {
|
|
192
270
|
continue;
|
|
193
271
|
}
|
|
194
272
|
|
|
195
|
-
// Extract node snapshot at this tick
|
|
196
273
|
const snapshot = extractNodeSnapshot(state, nodeId);
|
|
197
274
|
|
|
198
275
|
if (snapshot.exists && predicate(snapshot)) {
|
|
@@ -202,4 +279,41 @@ export class TemporalQuery {
|
|
|
202
279
|
|
|
203
280
|
return false;
|
|
204
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolves the initial state and start index for temporal replay.
|
|
285
|
+
*
|
|
286
|
+
* When `since > 0` and a checkpoint is available with
|
|
287
|
+
* `maxLamport <= since`, uses the checkpoint state and skips
|
|
288
|
+
* patches already covered by it. Otherwise falls back to an
|
|
289
|
+
* empty state starting from index 0.
|
|
290
|
+
*
|
|
291
|
+
* **Checkpoint `maxLamport` invariant**: The checkpoint's `maxLamport` value
|
|
292
|
+
* MUST represent a fully-closed Lamport tick — i.e. ALL patches with
|
|
293
|
+
* `lamport <= maxLamport` are included in the checkpoint state. The
|
|
294
|
+
* `findIndex` below uses strict `>` to locate the first patch *after* the
|
|
295
|
+
* checkpoint boundary. If a checkpoint were created mid-tick (some but not
|
|
296
|
+
* all patches at a given Lamport value included), this would silently skip
|
|
297
|
+
* the remaining same-tick patches. Checkpoint creators MUST guarantee the
|
|
298
|
+
* all-or-nothing inclusion property for any given Lamport tick.
|
|
299
|
+
*
|
|
300
|
+
* @param {Array<{patch: {lamport: number, [k: string]: unknown}, sha: string}>} allPatches
|
|
301
|
+
* @param {number} since - Minimum Lamport tick
|
|
302
|
+
* @returns {Promise<{state: import('./JoinReducer.js').WarpStateV5, startIdx: number, checkpointMaxLamport: number|null}>}
|
|
303
|
+
* @private
|
|
304
|
+
*/
|
|
305
|
+
async _resolveStart(allPatches, since) {
|
|
306
|
+
if (since > 0 && this._loadCheckpoint) {
|
|
307
|
+
const ck = /** @type {{ state: import('./JoinReducer.js').WarpStateV5, maxLamport: number } | null} */ (await this._loadCheckpoint());
|
|
308
|
+
if (ck && ck.state && ck.maxLamport <= since) {
|
|
309
|
+
const idx = allPatches.findIndex(
|
|
310
|
+
({ patch }) => patch.lamport > ck.maxLamport,
|
|
311
|
+
);
|
|
312
|
+
const startIdx = idx < 0 ? allPatches.length : idx;
|
|
313
|
+
// Replay mutates state in-place; isolate checkpoint provider caches from query runs.
|
|
314
|
+
return { state: cloneStateV5(ck.state), startIdx, checkpointMaxLamport: ck.maxLamport };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { state: createEmptyStateV5(), startIdx: 0, checkpointMaxLamport: null };
|
|
318
|
+
}
|
|
205
319
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatchDiff — captures alive-ness transitions during patch application.
|
|
3
|
+
*
|
|
4
|
+
* A diff entry is produced only when the alive-ness state of a node or edge
|
|
5
|
+
* actually changes, or when an LWW property winner changes. Redundant ops
|
|
6
|
+
* (e.g. NodeAdd on an already-alive node) produce no diff entries.
|
|
7
|
+
*
|
|
8
|
+
* @module domain/types/PatchDiff
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} EdgeDiffEntry
|
|
13
|
+
* @property {string} from - Source node ID
|
|
14
|
+
* @property {string} to - Target node ID
|
|
15
|
+
* @property {string} label - Edge label
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} PropDiffEntry
|
|
20
|
+
* @property {string} nodeId - Node (or edge-prop owner) ID
|
|
21
|
+
* @property {string} key - Property key
|
|
22
|
+
* @property {unknown} value - New LWW winner value
|
|
23
|
+
* @property {unknown} prevValue - Previous LWW winner value (undefined if none)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} PatchDiff
|
|
28
|
+
* @property {string[]} nodesAdded - Nodes that transitioned not-alive → alive
|
|
29
|
+
* @property {string[]} nodesRemoved - Nodes that transitioned alive → not-alive
|
|
30
|
+
* @property {EdgeDiffEntry[]} edgesAdded - Edges that transitioned not-alive → alive
|
|
31
|
+
* @property {EdgeDiffEntry[]} edgesRemoved - Edges that transitioned alive → not-alive
|
|
32
|
+
* @property {PropDiffEntry[]} propsChanged - Properties whose LWW winner actually changed
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates an empty PatchDiff.
|
|
37
|
+
*
|
|
38
|
+
* @returns {PatchDiff}
|
|
39
|
+
*/
|
|
40
|
+
export function createEmptyDiff() {
|
|
41
|
+
return {
|
|
42
|
+
nodesAdded: [],
|
|
43
|
+
nodesRemoved: [],
|
|
44
|
+
edgesAdded: [],
|
|
45
|
+
edgesRemoved: [],
|
|
46
|
+
propsChanged: [],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Merges two PatchDiff objects into a net diff by cancelling out
|
|
52
|
+
* contradictory add/remove pairs.
|
|
53
|
+
*
|
|
54
|
+
* - A node that appears in `a.nodesAdded` and `b.nodesRemoved` (or vice-versa)
|
|
55
|
+
* is dropped from both lists (the transitions cancel out).
|
|
56
|
+
* - Same logic applies to edges (keyed by `from\0to\0label`).
|
|
57
|
+
* - For `propsChanged`, only the last entry per `(nodeId, key)` is kept.
|
|
58
|
+
*
|
|
59
|
+
* @param {PatchDiff} a
|
|
60
|
+
* @param {PatchDiff} b
|
|
61
|
+
* @returns {PatchDiff}
|
|
62
|
+
*/
|
|
63
|
+
export function mergeDiffs(a, b) {
|
|
64
|
+
const allAdded = a.nodesAdded.concat(b.nodesAdded);
|
|
65
|
+
const allRemoved = a.nodesRemoved.concat(b.nodesRemoved);
|
|
66
|
+
const removedSet = new Set(allRemoved);
|
|
67
|
+
const addedSet = new Set(allAdded);
|
|
68
|
+
const nodesAdded = allAdded.filter((id) => !removedSet.has(id));
|
|
69
|
+
const nodesRemoved = allRemoved.filter((id) => !addedSet.has(id));
|
|
70
|
+
|
|
71
|
+
/** @param {EdgeDiffEntry} e */
|
|
72
|
+
const edgeKey = (e) => `${e.from}\0${e.to}\0${e.label}`;
|
|
73
|
+
const allEdgesAdded = a.edgesAdded.concat(b.edgesAdded);
|
|
74
|
+
const allEdgesRemoved = a.edgesRemoved.concat(b.edgesRemoved);
|
|
75
|
+
const edgeRemovedSet = new Set(allEdgesRemoved.map(edgeKey));
|
|
76
|
+
const edgeAddedSet = new Set(allEdgesAdded.map(edgeKey));
|
|
77
|
+
const edgesAdded = allEdgesAdded.filter((e) => !edgeRemovedSet.has(edgeKey(e)));
|
|
78
|
+
const edgesRemoved = allEdgesRemoved.filter((e) => !edgeAddedSet.has(edgeKey(e)));
|
|
79
|
+
|
|
80
|
+
// For props, deduplicate by keeping the last entry per (nodeId, key).
|
|
81
|
+
const allProps = a.propsChanged.concat(b.propsChanged);
|
|
82
|
+
/** @type {Map<string, PropDiffEntry>} */
|
|
83
|
+
const propMap = new Map();
|
|
84
|
+
for (const entry of allProps) {
|
|
85
|
+
propMap.set(`${entry.nodeId}\0${entry.key}`, entry);
|
|
86
|
+
}
|
|
87
|
+
const propsChanged = [...propMap.values()];
|
|
88
|
+
|
|
89
|
+
return { nodesAdded, nodesRemoved, edgesAdded, edgesRemoved, propsChanged };
|
|
90
|
+
}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* @typedef {Object} OpV2NodeRemove
|
|
51
51
|
* @property {'NodeRemove'} type - Operation type discriminator
|
|
52
52
|
* @property {NodeId} node - Node ID to remove
|
|
53
|
-
* @property {
|
|
53
|
+
* @property {string[]} observedDots - Encoded dot strings being removed (add events observed)
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
* @property {NodeId} from - Source node ID
|
|
71
71
|
* @property {NodeId} to - Target node ID
|
|
72
72
|
* @property {string} label - Edge label/type
|
|
73
|
-
* @property {
|
|
73
|
+
* @property {string[]} observedDots - Encoded dot strings being removed (add events observed)
|
|
74
74
|
*/
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -121,7 +121,7 @@ export function createNodeAddV2(node, dot) {
|
|
|
121
121
|
/**
|
|
122
122
|
* Creates a NodeRemove operation with observed dots
|
|
123
123
|
* @param {NodeId} node - Node ID to remove
|
|
124
|
-
* @param {
|
|
124
|
+
* @param {string[]} observedDots - Encoded dot strings being removed
|
|
125
125
|
* @returns {OpV2NodeRemove} NodeRemove operation
|
|
126
126
|
*/
|
|
127
127
|
export function createNodeRemoveV2(node, observedDots) {
|
|
@@ -145,7 +145,7 @@ export function createEdgeAddV2(from, to, label, dot) {
|
|
|
145
145
|
* @param {NodeId} from - Source node ID
|
|
146
146
|
* @param {NodeId} to - Target node ID
|
|
147
147
|
* @param {string} label - Edge label
|
|
148
|
-
* @param {
|
|
148
|
+
* @param {string[]} observedDots - Encoded dot strings being removed
|
|
149
149
|
* @returns {OpV2EdgeRemove} EdgeRemove operation
|
|
150
150
|
*/
|
|
151
151
|
export function createEdgeRemoveV2(from, to, label, observedDots) {
|
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
class MinHeap {
|
|
9
9
|
/**
|
|
10
10
|
* Creates an empty MinHeap.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} [options] - Configuration options
|
|
13
|
+
* @param {((a: T, b: T) => number)} [options.tieBreaker] - Comparator invoked when two
|
|
14
|
+
* entries have equal priority. Negative return = a wins (comes out first).
|
|
15
|
+
* When omitted, equal-priority extraction order is unspecified (heap-natural).
|
|
11
16
|
*/
|
|
12
|
-
constructor() {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
const { tieBreaker } = options || {};
|
|
13
19
|
/** @type {Array<{item: T, priority: number}>} */
|
|
14
|
-
this.
|
|
20
|
+
this._heap = [];
|
|
21
|
+
/** @type {((a: T, b: T) => number) | undefined} */
|
|
22
|
+
this._tieBreaker = tieBreaker;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
/**
|
|
@@ -22,8 +30,8 @@ class MinHeap {
|
|
|
22
30
|
* @returns {void}
|
|
23
31
|
*/
|
|
24
32
|
insert(item, priority) {
|
|
25
|
-
this.
|
|
26
|
-
this._bubbleUp(this.
|
|
33
|
+
this._heap.push({ item, priority });
|
|
34
|
+
this._bubbleUp(this._heap.length - 1);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -32,11 +40,11 @@ class MinHeap {
|
|
|
32
40
|
* @returns {T | undefined} The item with lowest priority, or undefined if empty
|
|
33
41
|
*/
|
|
34
42
|
extractMin() {
|
|
35
|
-
if (this.
|
|
36
|
-
if (this.
|
|
43
|
+
if (this._heap.length === 0) { return undefined; }
|
|
44
|
+
if (this._heap.length === 1) { return /** @type {{item: T, priority: number}} */ (this._heap.pop()).item; }
|
|
37
45
|
|
|
38
|
-
const min = this.
|
|
39
|
-
this.
|
|
46
|
+
const min = this._heap[0];
|
|
47
|
+
this._heap[0] = /** @type {{item: T, priority: number}} */ (this._heap.pop());
|
|
40
48
|
this._bubbleDown(0);
|
|
41
49
|
return min.item;
|
|
42
50
|
}
|
|
@@ -47,7 +55,7 @@ class MinHeap {
|
|
|
47
55
|
* @returns {boolean} True if empty
|
|
48
56
|
*/
|
|
49
57
|
isEmpty() {
|
|
50
|
-
return this.
|
|
58
|
+
return this._heap.length === 0;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
/**
|
|
@@ -56,7 +64,7 @@ class MinHeap {
|
|
|
56
64
|
* @returns {number} Number of items
|
|
57
65
|
*/
|
|
58
66
|
size() {
|
|
59
|
-
return this.
|
|
67
|
+
return this._heap.length;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/**
|
|
@@ -65,7 +73,27 @@ class MinHeap {
|
|
|
65
73
|
* @returns {number} The minimum priority value, or Infinity if empty
|
|
66
74
|
*/
|
|
67
75
|
peekPriority() {
|
|
68
|
-
return this.
|
|
76
|
+
return this._heap.length > 0 ? this._heap[0].priority : Infinity;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compares two heap entries. Returns negative if a should come before b.
|
|
81
|
+
*
|
|
82
|
+
* @private
|
|
83
|
+
* @param {number} idxA - Index of first entry
|
|
84
|
+
* @param {number} idxB - Index of second entry
|
|
85
|
+
* @returns {number} Negative if a < b, positive if a > b, zero if equal
|
|
86
|
+
*/
|
|
87
|
+
_compare(idxA, idxB) {
|
|
88
|
+
const a = this._heap[idxA];
|
|
89
|
+
const b = this._heap[idxB];
|
|
90
|
+
if (a.priority !== b.priority) {
|
|
91
|
+
return a.priority - b.priority;
|
|
92
|
+
}
|
|
93
|
+
if (this._tieBreaker) {
|
|
94
|
+
return this._tieBreaker(a.item, b.item);
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
/**
|
|
@@ -78,8 +106,8 @@ class MinHeap {
|
|
|
78
106
|
let current = pos;
|
|
79
107
|
while (current > 0) {
|
|
80
108
|
const parentIndex = Math.floor((current - 1) / 2);
|
|
81
|
-
if (this.
|
|
82
|
-
[this.
|
|
109
|
+
if (this._compare(parentIndex, current) <= 0) { break; }
|
|
110
|
+
[this._heap[parentIndex], this._heap[current]] = [this._heap[current], this._heap[parentIndex]];
|
|
83
111
|
current = parentIndex;
|
|
84
112
|
}
|
|
85
113
|
}
|
|
@@ -91,22 +119,22 @@ class MinHeap {
|
|
|
91
119
|
* @param {number} pos - Starting index
|
|
92
120
|
*/
|
|
93
121
|
_bubbleDown(pos) {
|
|
94
|
-
const {length} = this.
|
|
122
|
+
const {length} = this._heap;
|
|
95
123
|
let current = pos;
|
|
96
124
|
while (true) {
|
|
97
125
|
const leftChild = 2 * current + 1;
|
|
98
126
|
const rightChild = 2 * current + 2;
|
|
99
127
|
let smallest = current;
|
|
100
128
|
|
|
101
|
-
if (leftChild < length && this.
|
|
129
|
+
if (leftChild < length && this._compare(leftChild, smallest) < 0) {
|
|
102
130
|
smallest = leftChild;
|
|
103
131
|
}
|
|
104
|
-
if (rightChild < length && this.
|
|
132
|
+
if (rightChild < length && this._compare(rightChild, smallest) < 0) {
|
|
105
133
|
smallest = rightChild;
|
|
106
134
|
}
|
|
107
135
|
if (smallest === current) { break; }
|
|
108
136
|
|
|
109
|
-
[this.
|
|
137
|
+
[this._heap[current], this._heap[smallest]] = [this._heap[smallest], this._heap[current]];
|
|
110
138
|
current = smallest;
|
|
111
139
|
}
|
|
112
140
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical CBOR encoding/decoding.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to defaultCodec which already sorts keys recursively
|
|
5
|
+
* and handles Maps, null-prototype objects, and arrays.
|
|
6
|
+
*
|
|
7
|
+
* Deterministic output relies on cbor-x's key-sorting behaviour,
|
|
8
|
+
* which approximates RFC 7049 Section 3.9 (Canonical CBOR) by sorting
|
|
9
|
+
* map keys in length-first lexicographic order. This is sufficient for
|
|
10
|
+
* content-addressed equality within the WARP system but should not be
|
|
11
|
+
* assumed to match other canonical CBOR implementations byte-for-byte.
|
|
12
|
+
*
|
|
13
|
+
* @module domain/utils/canonicalCbor
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import defaultCodec from './defaultCodec.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encodes a value to canonical CBOR bytes with sorted keys.
|
|
20
|
+
*
|
|
21
|
+
* @param {unknown} value - The value to encode
|
|
22
|
+
* @returns {Uint8Array} CBOR-encoded bytes
|
|
23
|
+
*/
|
|
24
|
+
export function encodeCanonicalCbor(value) {
|
|
25
|
+
return defaultCodec.encode(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decodes CBOR bytes to a value.
|
|
30
|
+
*
|
|
31
|
+
* @param {Buffer|Uint8Array} buffer - CBOR bytes
|
|
32
|
+
* @returns {unknown} Decoded value
|
|
33
|
+
*/
|
|
34
|
+
export function decodeCanonicalCbor(buffer) {
|
|
35
|
+
return defaultCodec.decode(buffer);
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNV-1a 32-bit hash function.
|
|
3
|
+
*
|
|
4
|
+
* Used for shard key computation when the input is not a hex SHA.
|
|
5
|
+
* Uses Math.imul for correct 32-bit multiplication semantics.
|
|
6
|
+
*
|
|
7
|
+
* @note Callers with non-ASCII node IDs should normalize to NFC before
|
|
8
|
+
* hashing to ensure consistent shard placement.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} str - Input string
|
|
11
|
+
* @returns {number} Unsigned 32-bit FNV-1a hash
|
|
12
|
+
*/
|
|
13
|
+
export default function fnv1a(str) {
|
|
14
|
+
let hash = 0x811c9dc5; // FNV offset basis
|
|
15
|
+
for (let i = 0; i < str.length; i++) {
|
|
16
|
+
hash ^= str.charCodeAt(i);
|
|
17
|
+
hash = Math.imul(hash, 0x01000193); // FNV prime
|
|
18
|
+
}
|
|
19
|
+
return hash >>> 0; // Ensure unsigned
|
|
20
|
+
}
|
|
@@ -50,6 +50,7 @@ const NOT_CHECKED = Symbol('NOT_CHECKED');
|
|
|
50
50
|
* @typedef {Object} RoaringBitmapSubset
|
|
51
51
|
* @property {number} size
|
|
52
52
|
* @property {function(number): void} add
|
|
53
|
+
* @property {function(number): void} remove
|
|
53
54
|
* @property {function(number): boolean} has
|
|
54
55
|
* @property {function(Iterable<number>): void} orInPlace
|
|
55
56
|
* @property {function(boolean): Uint8Array} serialize
|
|
@@ -63,6 +64,13 @@ const NOT_CHECKED = Symbol('NOT_CHECKED');
|
|
|
63
64
|
*/
|
|
64
65
|
let roaringModule = null;
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Captures module initialization failure so callers can see the root cause.
|
|
69
|
+
* @type {unknown}
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
let initError = null;
|
|
73
|
+
|
|
66
74
|
/**
|
|
67
75
|
* Cached result of native availability check.
|
|
68
76
|
* `NOT_CHECKED` means not yet checked, `null` means indeterminate.
|
|
@@ -83,7 +91,8 @@ let nativeAvailability = NOT_CHECKED;
|
|
|
83
91
|
*/
|
|
84
92
|
function loadRoaring() {
|
|
85
93
|
if (!roaringModule) {
|
|
86
|
-
|
|
94
|
+
const cause = initError instanceof Error ? ` Caused by: ${initError.message}` : '';
|
|
95
|
+
throw new Error(`Roaring module not loaded. Call initRoaring() first or ensure top-level await import completed.${cause}`);
|
|
87
96
|
}
|
|
88
97
|
return roaringModule;
|
|
89
98
|
}
|
|
@@ -99,6 +108,7 @@ function loadRoaring() {
|
|
|
99
108
|
export async function initRoaring(mod) {
|
|
100
109
|
if (mod) {
|
|
101
110
|
roaringModule = mod;
|
|
111
|
+
initError = null;
|
|
102
112
|
return;
|
|
103
113
|
}
|
|
104
114
|
if (!roaringModule) {
|
|
@@ -113,8 +123,9 @@ export async function initRoaring(mod) {
|
|
|
113
123
|
// Auto-initialize on module load (top-level await)
|
|
114
124
|
try {
|
|
115
125
|
await initRoaring();
|
|
116
|
-
} catch {
|
|
117
|
-
// Roaring may not be installed;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
// Roaring may not be installed; keep root cause for downstream diagnostics.
|
|
128
|
+
initError = err;
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const HEX_RE = /^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/;
|
|
2
|
+
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FNV-1a 32-bit over raw bytes (Uint8Array).
|
|
7
|
+
*
|
|
8
|
+
* @param {Uint8Array} bytes
|
|
9
|
+
* @returns {number} Unsigned 32-bit hash
|
|
10
|
+
*/
|
|
11
|
+
function fnv1aBytes(bytes) {
|
|
12
|
+
let hash = 0x811c9dc5;
|
|
13
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
14
|
+
hash ^= bytes[i];
|
|
15
|
+
hash = Math.imul(hash, 0x01000193);
|
|
16
|
+
}
|
|
17
|
+
return hash >>> 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Computes a 2-character hex shard key for a given ID.
|
|
22
|
+
*
|
|
23
|
+
* For hex SHAs (exactly 40 or 64 hex chars), uses the first two characters (lowercased).
|
|
24
|
+
* For all other strings, computes FNV-1a hash over UTF-8 bytes and takes the low byte.
|
|
25
|
+
*
|
|
26
|
+
* Returns '00' for null, undefined, or non-string inputs (graceful fallback).
|
|
27
|
+
*
|
|
28
|
+
* @param {string} id - Node ID or SHA
|
|
29
|
+
* @returns {string} 2-character lowercase hex shard key (e.g. 'ab', '0f')
|
|
30
|
+
*/
|
|
31
|
+
export default function computeShardKey(id) {
|
|
32
|
+
if (id === null || id === undefined || typeof id !== 'string') {
|
|
33
|
+
return '00';
|
|
34
|
+
}
|
|
35
|
+
if (HEX_RE.test(id)) {
|
|
36
|
+
return id.substring(0, 2).toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
const hash = fnv1aBytes(encoder.encode(id));
|
|
39
|
+
return (hash & 0xff).toString(16).padStart(2, '0');
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a decoded byte payload to a Uint8Array.
|
|
3
|
+
*
|
|
4
|
+
* CBOR decoders may yield Buffer, Uint8Array, or plain number[] depending
|
|
5
|
+
* on runtime and codec implementation (e.g. cbor-x on Node vs Deno).
|
|
6
|
+
* This helper ensures Roaring bitmap deserialization and other binary APIs
|
|
7
|
+
* always receive a Uint8Array.
|
|
8
|
+
*
|
|
9
|
+
* @param {Uint8Array|ArrayLike<number>} value
|
|
10
|
+
* @returns {Uint8Array}
|
|
11
|
+
*/
|
|
12
|
+
export default function toBytes(value) {
|
|
13
|
+
if (value instanceof Uint8Array) {
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
return Uint8Array.from(value);
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a shard Object ID (hex string, 4-64 chars).
|
|
3
|
+
*
|
|
4
|
+
* The 4-character minimum accommodates abbreviated OIDs used in test
|
|
5
|
+
* fixtures and short internal IDs. Full Git SHA-1 OIDs are 40 chars;
|
|
6
|
+
* SHA-256 OIDs are 64 chars.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} oid - The OID to validate
|
|
9
|
+
* @returns {boolean} True if oid is a valid hex string of 4-64 characters
|
|
10
|
+
*/
|
|
11
|
+
export function isValidShardOid(oid) {
|
|
12
|
+
return typeof oid === 'string' && /^[0-9a-fA-F]{4,64}$/.test(oid);
|
|
13
|
+
}
|
|
@@ -10,17 +10,8 @@
|
|
|
10
10
|
// ── Error constructors ──────────────────────────────────────────────────────
|
|
11
11
|
export { default as QueryError } from '../errors/QueryError.js';
|
|
12
12
|
export { default as ForkError } from '../errors/ForkError.js';
|
|
13
|
-
export { default as SyncError } from '../errors/SyncError.js';
|
|
14
|
-
export { default as OperationAbortedError } from '../errors/OperationAbortedError.js';
|
|
15
13
|
|
|
16
14
|
// ── Shared constants ────────────────────────────────────────────────────────
|
|
17
15
|
export const DEFAULT_ADJACENCY_CACHE_SIZE = 3;
|
|
18
16
|
export const E_NO_STATE_MSG = 'No materialized state. Call materialize() before querying, or use autoMaterialize: true (the default). See https://github.com/git-stunts/git-warp#materialization';
|
|
19
17
|
export const E_STALE_STATE_MSG = 'State is stale (patches written since last materialize). Call materialize() to refresh. See https://github.com/git-stunts/git-warp#materialization';
|
|
20
|
-
|
|
21
|
-
// ── Sync constants ──────────────────────────────────────────────────────────
|
|
22
|
-
export const DEFAULT_SYNC_SERVER_MAX_BYTES = 4 * 1024 * 1024;
|
|
23
|
-
export const DEFAULT_SYNC_WITH_RETRIES = 3;
|
|
24
|
-
export const DEFAULT_SYNC_WITH_BASE_DELAY_MS = 250;
|
|
25
|
-
export const DEFAULT_SYNC_WITH_MAX_DELAY_MS = 2000;
|
|
26
|
-
export const DEFAULT_SYNC_WITH_TIMEOUT_MS = 10_000;
|