@git-stunts/git-warp 11.5.1 → 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 +142 -10
- 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 +40 -0
- 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/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 +233 -5
- 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/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/warp/_wiredMethods.d.ts +7 -1
- 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
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless service that computes dirty shard buffers from a PatchDiff.
|
|
3
|
+
*
|
|
4
|
+
* Given a diff of alive-ness transitions + a shard loader for the existing
|
|
5
|
+
* index tree, produces only the shard buffers that changed. The caller
|
|
6
|
+
* merges them back into the tree via `{ ...existingTree, ...dirtyShards }`.
|
|
7
|
+
*
|
|
8
|
+
* @module domain/services/IncrementalIndexUpdater
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import defaultCodec from '../utils/defaultCodec.js';
|
|
12
|
+
import computeShardKey from '../utils/shardKey.js';
|
|
13
|
+
import toBytes from '../utils/toBytes.js';
|
|
14
|
+
import { getRoaringBitmap32 } from '../utils/roaring.js';
|
|
15
|
+
import { orsetContains, orsetElements } from '../crdt/ORSet.js';
|
|
16
|
+
import { decodeEdgeKey } from './KeyCodec.js';
|
|
17
|
+
import { ShardIdOverflowError } from '../errors/index.js';
|
|
18
|
+
|
|
19
|
+
/** Maximum local IDs per shard (2^24). */
|
|
20
|
+
const MAX_LOCAL_ID = 1 << 24;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} MetaShard
|
|
24
|
+
* @property {Array<[string, number]>} nodeToGlobal
|
|
25
|
+
* @property {number} nextLocalId
|
|
26
|
+
* @property {import('../utils/roaring.js').RoaringBitmapSubset} aliveBitmap
|
|
27
|
+
* @property {Map<number, string>} globalToNode - Reverse lookup: globalId → nodeId (O(1))
|
|
28
|
+
* @property {Map<string, number>} nodeToGlobalMap - Forward lookup: nodeId → globalId (O(1))
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Record<string, Record<string, Uint8Array>>} EdgeShardData
|
|
33
|
+
* Keyed by bucket ("all" or labelId string), then by globalId string → serialised bitmap.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export default class IncrementalIndexUpdater {
|
|
37
|
+
/**
|
|
38
|
+
* @param {Object} [options]
|
|
39
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec]
|
|
40
|
+
*/
|
|
41
|
+
constructor({ codec } = {}) {
|
|
42
|
+
this._codec = codec || defaultCodec;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Computes only the dirty shards from a PatchDiff.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} params
|
|
49
|
+
* @param {import('../types/PatchDiff.js').PatchDiff} params.diff
|
|
50
|
+
* @param {import('./JoinReducer.js').WarpStateV5} params.state
|
|
51
|
+
* @param {(path: string) => Uint8Array|undefined} params.loadShard
|
|
52
|
+
* @returns {Record<string, Uint8Array>} dirty shard buffers (path -> Uint8Array)
|
|
53
|
+
*/
|
|
54
|
+
computeDirtyShards({ diff, state, loadShard }) {
|
|
55
|
+
const dirtyKeys = this._collectDirtyShardKeys(diff);
|
|
56
|
+
if (dirtyKeys.size === 0) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @type {Map<string, MetaShard>} */
|
|
61
|
+
const metaCache = new Map();
|
|
62
|
+
/** @type {Record<string, Uint8Array>} */
|
|
63
|
+
const out = {};
|
|
64
|
+
|
|
65
|
+
const labels = this._loadLabels(loadShard);
|
|
66
|
+
let labelsDirty = false;
|
|
67
|
+
|
|
68
|
+
for (const nodeId of diff.nodesAdded) {
|
|
69
|
+
this._handleNodeAdd(nodeId, metaCache, loadShard);
|
|
70
|
+
}
|
|
71
|
+
for (const nodeId of diff.nodesRemoved) {
|
|
72
|
+
this._handleNodeRemove(nodeId, metaCache, loadShard);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @type {Map<string, EdgeShardData>} */
|
|
76
|
+
const fwdCache = new Map();
|
|
77
|
+
/** @type {Map<string, EdgeShardData>} */
|
|
78
|
+
const revCache = new Map();
|
|
79
|
+
|
|
80
|
+
// Purge edge bitmaps for removed nodes (dangling edge elimination).
|
|
81
|
+
// An edge whose endpoint is dead must not appear in the index, even
|
|
82
|
+
// if the edge itself is still alive in the ORSet.
|
|
83
|
+
for (const nodeId of diff.nodesRemoved) {
|
|
84
|
+
this._purgeNodeEdges(nodeId, metaCache, fwdCache, revCache, labels, loadShard);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Filter edgesAdded by endpoint alive-ness (matches edgeVisibleV5).
|
|
88
|
+
for (const edge of diff.edgesAdded) {
|
|
89
|
+
if (!orsetContains(state.nodeAlive, edge.from) || !orsetContains(state.nodeAlive, edge.to)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
labelsDirty = this._ensureLabel(edge.label, labels) || labelsDirty;
|
|
93
|
+
this._handleEdgeAdd(edge, labels, metaCache, fwdCache, revCache, loadShard);
|
|
94
|
+
}
|
|
95
|
+
for (const edge of diff.edgesRemoved) {
|
|
96
|
+
this._handleEdgeRemove(edge, labels, metaCache, fwdCache, revCache, loadShard);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Restore edges for re-added nodes. When a node transitions
|
|
100
|
+
// not-alive -> alive, edges touching it that are alive in the ORSet
|
|
101
|
+
// become visible again. The diff only tracks explicit EdgeAdd ops,
|
|
102
|
+
// not these implicit visibility transitions.
|
|
103
|
+
//
|
|
104
|
+
// Known O(E) worst-case: scans all alive edges. For genuinely new nodes
|
|
105
|
+
// (not re-adds), this scan is unnecessary since they can't have pre-existing
|
|
106
|
+
// edges. _findGlobalId returns undefined for new nodes, so this could be
|
|
107
|
+
// short-circuited — deferred for a future optimization pass.
|
|
108
|
+
if (diff.nodesAdded.length > 0) {
|
|
109
|
+
const addedSet = new Set(diff.nodesAdded);
|
|
110
|
+
const diffEdgeSet = new Set(
|
|
111
|
+
diff.edgesAdded.map((e) => `${e.from}\0${e.to}\0${e.label}`),
|
|
112
|
+
);
|
|
113
|
+
for (const edgeKey of orsetElements(state.edgeAlive)) {
|
|
114
|
+
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
115
|
+
if (!addedSet.has(from) && !addedSet.has(to)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!orsetContains(state.nodeAlive, from) || !orsetContains(state.nodeAlive, to)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const diffKey = `${from}\0${to}\0${label}`;
|
|
122
|
+
if (diffEdgeSet.has(diffKey)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
labelsDirty = this._ensureLabel(label, labels) || labelsDirty;
|
|
126
|
+
this._handleEdgeAdd({ from, to, label }, labels, metaCache, fwdCache, revCache, loadShard);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this._flushMeta(metaCache, out);
|
|
131
|
+
this._flushEdgeShards(fwdCache, 'fwd', out);
|
|
132
|
+
this._flushEdgeShards(revCache, 'rev', out);
|
|
133
|
+
|
|
134
|
+
if (labelsDirty) {
|
|
135
|
+
out['labels.cbor'] = this._saveLabels(labels);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this._handleProps(diff.propsChanged, loadShard, out);
|
|
139
|
+
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Dirty shard key collection ────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Collects all shard keys touched by the diff.
|
|
147
|
+
*
|
|
148
|
+
* @param {import('../types/PatchDiff.js').PatchDiff} diff
|
|
149
|
+
* @returns {Set<string>}
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
_collectDirtyShardKeys(diff) {
|
|
153
|
+
const keys = new Set();
|
|
154
|
+
for (const nid of diff.nodesAdded) {
|
|
155
|
+
keys.add(computeShardKey(nid));
|
|
156
|
+
}
|
|
157
|
+
for (const nid of diff.nodesRemoved) {
|
|
158
|
+
keys.add(computeShardKey(nid));
|
|
159
|
+
}
|
|
160
|
+
for (const e of diff.edgesAdded) {
|
|
161
|
+
keys.add(computeShardKey(e.from));
|
|
162
|
+
keys.add(computeShardKey(e.to));
|
|
163
|
+
}
|
|
164
|
+
for (const e of diff.edgesRemoved) {
|
|
165
|
+
keys.add(computeShardKey(e.from));
|
|
166
|
+
keys.add(computeShardKey(e.to));
|
|
167
|
+
}
|
|
168
|
+
for (const p of diff.propsChanged) {
|
|
169
|
+
keys.add(computeShardKey(p.nodeId));
|
|
170
|
+
}
|
|
171
|
+
return keys;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Node operations ───────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handles a NodeAdd: allocate or reactivate globalId, set alive bit.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} nodeId
|
|
180
|
+
* @param {Map<string, MetaShard>} metaCache
|
|
181
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_handleNodeAdd(nodeId, metaCache, loadShard) {
|
|
185
|
+
const sk = computeShardKey(nodeId);
|
|
186
|
+
const meta = this._getOrLoadMeta(sk, metaCache, loadShard);
|
|
187
|
+
const existing = this._findGlobalId(meta, nodeId);
|
|
188
|
+
if (existing !== undefined) {
|
|
189
|
+
meta.aliveBitmap.add(existing);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (meta.nextLocalId >= MAX_LOCAL_ID) {
|
|
193
|
+
throw new ShardIdOverflowError(
|
|
194
|
+
`Shard ${sk} exceeded 2^24 local IDs`,
|
|
195
|
+
{ shardKey: sk, nextLocalId: meta.nextLocalId },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const shardByte = parseInt(sk, 16);
|
|
199
|
+
const globalId = ((shardByte << 24) | meta.nextLocalId) >>> 0;
|
|
200
|
+
meta.nextLocalId++;
|
|
201
|
+
meta.nodeToGlobal.push([nodeId, globalId]);
|
|
202
|
+
meta.globalToNode.set(globalId, nodeId);
|
|
203
|
+
meta.nodeToGlobalMap.set(nodeId, globalId);
|
|
204
|
+
meta.aliveBitmap.add(globalId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handles a NodeRemove: clear alive bit but keep globalId stable.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} nodeId
|
|
211
|
+
* @param {Map<string, MetaShard>} metaCache
|
|
212
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
_handleNodeRemove(nodeId, metaCache, loadShard) {
|
|
216
|
+
const meta = this._getOrLoadMeta(computeShardKey(nodeId), metaCache, loadShard);
|
|
217
|
+
const gid = this._findGlobalId(meta, nodeId);
|
|
218
|
+
if (gid !== undefined) {
|
|
219
|
+
meta.aliveBitmap.remove(gid);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Purges all edge bitmap entries that reference a removed node.
|
|
225
|
+
*
|
|
226
|
+
* When a node is removed, edges that touch it become invisible (even if the
|
|
227
|
+
* edge itself is still alive in the ORSet). The full rebuild skips these via
|
|
228
|
+
* edgeVisibleV5; the incremental path must explicitly purge them.
|
|
229
|
+
*
|
|
230
|
+
* Scans the alive edges in the ORSet for any that reference the dead node,
|
|
231
|
+
* then removes those entries from the forward and reverse edge bitmaps.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} deadNodeId
|
|
234
|
+
* @param {Map<string, MetaShard>} metaCache
|
|
235
|
+
* @param {Map<string, EdgeShardData>} fwdCache
|
|
236
|
+
* @param {Map<string, EdgeShardData>} revCache
|
|
237
|
+
* @param {Record<string, number>} labels
|
|
238
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
_purgeNodeEdges(deadNodeId, metaCache, fwdCache, revCache, labels, loadShard) {
|
|
242
|
+
const deadMeta = this._getOrLoadMeta(computeShardKey(deadNodeId), metaCache, loadShard);
|
|
243
|
+
const deadGid = this._findGlobalId(deadMeta, deadNodeId);
|
|
244
|
+
if (deadGid === undefined) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Purge the dead node's own fwd/rev shard entries by zeroing out its
|
|
249
|
+
// bitmap rows and clearing its globalId from peer bitmaps.
|
|
250
|
+
const shardKey = computeShardKey(deadNodeId);
|
|
251
|
+
const RoaringBitmap32 = getRoaringBitmap32();
|
|
252
|
+
|
|
253
|
+
// Clear the dead node's own outgoing edges (forward shard)
|
|
254
|
+
const fwdData = this._getOrLoadEdgeShard(fwdCache, 'fwd', shardKey, loadShard);
|
|
255
|
+
for (const bucket of Object.keys(fwdData)) {
|
|
256
|
+
const gidStr = String(deadGid);
|
|
257
|
+
if (fwdData[bucket] && fwdData[bucket][gidStr]) {
|
|
258
|
+
// Before clearing, find the targets so we can clean reverse bitmaps
|
|
259
|
+
const targets = RoaringBitmap32.deserialize(
|
|
260
|
+
toBytes(fwdData[bucket][gidStr]),
|
|
261
|
+
true,
|
|
262
|
+
).toArray();
|
|
263
|
+
|
|
264
|
+
// Clear this node's outgoing bitmap
|
|
265
|
+
const empty = new RoaringBitmap32();
|
|
266
|
+
fwdData[bucket][gidStr] = empty.serialize(true);
|
|
267
|
+
|
|
268
|
+
// Remove deadGid from each target's reverse bitmap
|
|
269
|
+
for (const targetGid of targets) {
|
|
270
|
+
const targetNodeId = this._findNodeIdByGlobal(targetGid, metaCache, loadShard);
|
|
271
|
+
if (targetNodeId) {
|
|
272
|
+
const targetShard = computeShardKey(targetNodeId);
|
|
273
|
+
const revData = this._getOrLoadEdgeShard(revCache, 'rev', targetShard, loadShard);
|
|
274
|
+
const targetGidStr = String(targetGid);
|
|
275
|
+
if (revData[bucket] && revData[bucket][targetGidStr]) {
|
|
276
|
+
const bm = RoaringBitmap32.deserialize(
|
|
277
|
+
toBytes(revData[bucket][targetGidStr]),
|
|
278
|
+
true,
|
|
279
|
+
);
|
|
280
|
+
bm.remove(deadGid);
|
|
281
|
+
revData[bucket][targetGidStr] = bm.serialize(true);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Clear the dead node's own incoming edges (reverse shard)
|
|
289
|
+
const revData = this._getOrLoadEdgeShard(revCache, 'rev', shardKey, loadShard);
|
|
290
|
+
for (const bucket of Object.keys(revData)) {
|
|
291
|
+
const gidStr = String(deadGid);
|
|
292
|
+
if (revData[bucket] && revData[bucket][gidStr]) {
|
|
293
|
+
const sources = RoaringBitmap32.deserialize(
|
|
294
|
+
toBytes(revData[bucket][gidStr]),
|
|
295
|
+
true,
|
|
296
|
+
).toArray();
|
|
297
|
+
|
|
298
|
+
const empty = new RoaringBitmap32();
|
|
299
|
+
revData[bucket][gidStr] = empty.serialize(true);
|
|
300
|
+
|
|
301
|
+
// Remove deadGid from each source's forward bitmap
|
|
302
|
+
for (const sourceGid of sources) {
|
|
303
|
+
const sourceNodeId = this._findNodeIdByGlobal(sourceGid, metaCache, loadShard);
|
|
304
|
+
if (sourceNodeId) {
|
|
305
|
+
const sourceShard = computeShardKey(sourceNodeId);
|
|
306
|
+
const fwdDataPeer = this._getOrLoadEdgeShard(fwdCache, 'fwd', sourceShard, loadShard);
|
|
307
|
+
const sourceGidStr = String(sourceGid);
|
|
308
|
+
if (fwdDataPeer[bucket] && fwdDataPeer[bucket][sourceGidStr]) {
|
|
309
|
+
const bm = RoaringBitmap32.deserialize(
|
|
310
|
+
toBytes(fwdDataPeer[bucket][sourceGidStr]),
|
|
311
|
+
true,
|
|
312
|
+
);
|
|
313
|
+
bm.remove(deadGid);
|
|
314
|
+
fwdDataPeer[bucket][sourceGidStr] = bm.serialize(true);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Reverse-looks up a nodeId from a globalId using the pre-built reverse map.
|
|
324
|
+
*
|
|
325
|
+
* @param {number} globalId
|
|
326
|
+
* @param {Map<string, MetaShard>} metaCache
|
|
327
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
328
|
+
* @returns {string|undefined}
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
_findNodeIdByGlobal(globalId, metaCache, loadShard) {
|
|
332
|
+
// The shard key is encoded in the upper byte of the globalId
|
|
333
|
+
const shardByte = (globalId >>> 24) & 0xff;
|
|
334
|
+
const shardKey = shardByte.toString(16).padStart(2, '0');
|
|
335
|
+
const meta = this._getOrLoadMeta(shardKey, metaCache, loadShard);
|
|
336
|
+
return meta.globalToNode.get(globalId);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Label operations ──────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Ensures a label exists in the registry; returns true if newly added.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} label
|
|
345
|
+
* @param {Record<string, number>} labels
|
|
346
|
+
* @returns {boolean}
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
_ensureLabel(label, labels) {
|
|
350
|
+
if (Object.prototype.hasOwnProperty.call(labels, label)) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
let maxId = -1;
|
|
354
|
+
for (const id of Object.values(labels)) {
|
|
355
|
+
if (id > maxId) {
|
|
356
|
+
maxId = id;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
labels[label] = maxId + 1;
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Edge operations ───────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @param {{from: string, to: string, label: string}} edge
|
|
367
|
+
* @param {Record<string, number>} labels
|
|
368
|
+
* @param {Map<string, MetaShard>} metaCache
|
|
369
|
+
* @param {Map<string, EdgeShardData>} fwdCache
|
|
370
|
+
* @param {Map<string, EdgeShardData>} revCache
|
|
371
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
372
|
+
* @private
|
|
373
|
+
*/
|
|
374
|
+
_handleEdgeAdd(edge, labels, metaCache, fwdCache, revCache, loadShard) {
|
|
375
|
+
const fromMeta = this._getOrLoadMeta(computeShardKey(edge.from), metaCache, loadShard);
|
|
376
|
+
const toMeta = this._getOrLoadMeta(computeShardKey(edge.to), metaCache, loadShard);
|
|
377
|
+
const fromGid = this._findGlobalId(fromMeta, edge.from);
|
|
378
|
+
const toGid = this._findGlobalId(toMeta, edge.to);
|
|
379
|
+
if (fromGid === undefined || toGid === undefined) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const labelId = String(labels[edge.label]);
|
|
384
|
+
const fromShard = computeShardKey(edge.from);
|
|
385
|
+
const toShard = computeShardKey(edge.to);
|
|
386
|
+
|
|
387
|
+
this._addToEdgeBitmap(fwdCache, { shardKey: fromShard, bucket: 'all', owner: fromGid, target: toGid, dir: 'fwd' }, loadShard);
|
|
388
|
+
this._addToEdgeBitmap(fwdCache, { shardKey: fromShard, bucket: labelId, owner: fromGid, target: toGid, dir: 'fwd' }, loadShard);
|
|
389
|
+
this._addToEdgeBitmap(revCache, { shardKey: toShard, bucket: 'all', owner: toGid, target: fromGid, dir: 'rev' }, loadShard);
|
|
390
|
+
this._addToEdgeBitmap(revCache, { shardKey: toShard, bucket: labelId, owner: toGid, target: fromGid, dir: 'rev' }, loadShard);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {{from: string, to: string, label: string}} edge
|
|
395
|
+
* @param {Record<string, number>} labels
|
|
396
|
+
* @param {Map<string, MetaShard>} metaCache
|
|
397
|
+
* @param {Map<string, EdgeShardData>} fwdCache
|
|
398
|
+
* @param {Map<string, EdgeShardData>} revCache
|
|
399
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
400
|
+
* @private
|
|
401
|
+
*/
|
|
402
|
+
_handleEdgeRemove(edge, labels, metaCache, fwdCache, revCache, loadShard) {
|
|
403
|
+
const fromMeta = this._getOrLoadMeta(computeShardKey(edge.from), metaCache, loadShard);
|
|
404
|
+
const toMeta = this._getOrLoadMeta(computeShardKey(edge.to), metaCache, loadShard);
|
|
405
|
+
const fromGid = this._findGlobalId(fromMeta, edge.from);
|
|
406
|
+
const toGid = this._findGlobalId(toMeta, edge.to);
|
|
407
|
+
if (fromGid === undefined || toGid === undefined) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (labels[edge.label] === undefined) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const labelId = String(labels[edge.label]);
|
|
416
|
+
const fromShard = computeShardKey(edge.from);
|
|
417
|
+
const toShard = computeShardKey(edge.to);
|
|
418
|
+
|
|
419
|
+
this._removeFromEdgeBitmap(fwdCache, { shardKey: fromShard, bucket: labelId, owner: fromGid, target: toGid, dir: 'fwd' }, loadShard);
|
|
420
|
+
this._removeFromEdgeBitmap(revCache, { shardKey: toShard, bucket: labelId, owner: toGid, target: fromGid, dir: 'rev' }, loadShard);
|
|
421
|
+
|
|
422
|
+
this._recomputeAllBucket(fwdCache, fromShard, fromGid, labels, loadShard, 'fwd');
|
|
423
|
+
this._recomputeAllBucket(revCache, toShard, toGid, labels, loadShard, 'rev');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Edge bitmap helpers ───────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @param {Map<string, EdgeShardData>} cache
|
|
430
|
+
* @param {{ shardKey: string, bucket: string, owner: number, target: number, dir: string }} opts
|
|
431
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
432
|
+
* @private
|
|
433
|
+
*/
|
|
434
|
+
_addToEdgeBitmap(cache, opts, loadShard) {
|
|
435
|
+
const { shardKey, bucket, owner, target, dir } = opts;
|
|
436
|
+
const data = this._getOrLoadEdgeShard(cache, dir, shardKey, loadShard);
|
|
437
|
+
const bm = this._deserializeBitmap(data, bucket, String(owner));
|
|
438
|
+
bm.add(target);
|
|
439
|
+
if (!data[bucket]) { data[bucket] = {}; }
|
|
440
|
+
data[bucket][String(owner)] = bm.serialize(true);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @param {Map<string, EdgeShardData>} cache
|
|
445
|
+
* @param {{ shardKey: string, bucket: string, owner: number, target: number, dir: string }} opts
|
|
446
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
449
|
+
_removeFromEdgeBitmap(cache, opts, loadShard) {
|
|
450
|
+
const { shardKey, bucket, owner, target, dir } = opts;
|
|
451
|
+
const data = this._getOrLoadEdgeShard(cache, dir, shardKey, loadShard);
|
|
452
|
+
const bm = this._deserializeBitmap(data, bucket, String(owner));
|
|
453
|
+
bm.remove(target);
|
|
454
|
+
if (!data[bucket]) { data[bucket] = {}; }
|
|
455
|
+
data[bucket][String(owner)] = bm.serialize(true);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Recomputes the 'all' bucket for a given owner by OR-ing all per-label bitmaps.
|
|
460
|
+
*
|
|
461
|
+
* @param {Map<string, EdgeShardData>} cache
|
|
462
|
+
* @param {string} shardKey
|
|
463
|
+
* @param {number} owner
|
|
464
|
+
* @param {Record<string, number>} labels
|
|
465
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
466
|
+
* @param {string} dir
|
|
467
|
+
* @private
|
|
468
|
+
*/
|
|
469
|
+
_recomputeAllBucket(cache, shardKey, owner, labels, loadShard, dir) {
|
|
470
|
+
const data = this._getOrLoadEdgeShard(cache, dir, shardKey, loadShard);
|
|
471
|
+
const RoaringBitmap32 = getRoaringBitmap32();
|
|
472
|
+
const merged = new RoaringBitmap32();
|
|
473
|
+
const ownerStr = String(owner);
|
|
474
|
+
|
|
475
|
+
for (const labelId of Object.values(labels)) {
|
|
476
|
+
const bucket = String(labelId);
|
|
477
|
+
if (data[bucket] && data[bucket][ownerStr]) {
|
|
478
|
+
const bm = RoaringBitmap32.deserialize(
|
|
479
|
+
toBytes(data[bucket][ownerStr]),
|
|
480
|
+
true,
|
|
481
|
+
);
|
|
482
|
+
merged.orInPlace(bm);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (!data.all) { data.all = {}; }
|
|
487
|
+
data.all[ownerStr] = merged.serialize(true);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Property operations ───────────────────────────────────────────────────
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Handles PropSet entries and flushes dirty props shards.
|
|
494
|
+
*
|
|
495
|
+
* @param {import('../types/PatchDiff.js').PropDiffEntry[]} propsChanged
|
|
496
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
497
|
+
* @param {Record<string, Uint8Array>} out
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
_handleProps(propsChanged, loadShard, out) {
|
|
501
|
+
if (propsChanged.length === 0) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** @type {Map<string, Map<string, Record<string, unknown>>>} */
|
|
506
|
+
const shardMap = new Map();
|
|
507
|
+
|
|
508
|
+
for (const prop of propsChanged) {
|
|
509
|
+
const shardKey = computeShardKey(prop.nodeId);
|
|
510
|
+
if (!shardMap.has(shardKey)) {
|
|
511
|
+
shardMap.set(shardKey, this._loadProps(shardKey, loadShard));
|
|
512
|
+
}
|
|
513
|
+
const shard = /** @type {Map<string, Record<string, unknown>>} */ (shardMap.get(shardKey));
|
|
514
|
+
let nodeProps = shard.get(prop.nodeId);
|
|
515
|
+
if (!nodeProps) {
|
|
516
|
+
nodeProps = Object.create(null);
|
|
517
|
+
shard.set(prop.nodeId, /** @type {Record<string, unknown>} */ (nodeProps));
|
|
518
|
+
} else if (Object.getPrototypeOf(nodeProps) !== null) {
|
|
519
|
+
const safeProps = Object.assign(Object.create(null), nodeProps);
|
|
520
|
+
shard.set(prop.nodeId, safeProps);
|
|
521
|
+
nodeProps = safeProps;
|
|
522
|
+
}
|
|
523
|
+
/** @type {Record<string, unknown>} */ (nodeProps)[prop.key] = prop.value;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for (const [shardKey, shard] of shardMap) {
|
|
527
|
+
out[`props_${shardKey}.cbor`] = this._saveProps(shard);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── Meta shard I/O ────────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* @param {string} shardKey
|
|
535
|
+
* @param {Map<string, MetaShard>} cache
|
|
536
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
537
|
+
* @returns {MetaShard}
|
|
538
|
+
* @private
|
|
539
|
+
*/
|
|
540
|
+
_getOrLoadMeta(shardKey, cache, loadShard) {
|
|
541
|
+
const cached = cache.get(shardKey);
|
|
542
|
+
if (cached) {
|
|
543
|
+
return cached;
|
|
544
|
+
}
|
|
545
|
+
const meta = this._loadMeta(shardKey, loadShard);
|
|
546
|
+
cache.set(shardKey, meta);
|
|
547
|
+
return meta;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* @param {string} shardKey
|
|
552
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
553
|
+
* @returns {MetaShard}
|
|
554
|
+
* @private
|
|
555
|
+
*/
|
|
556
|
+
_loadMeta(shardKey, loadShard) {
|
|
557
|
+
const RoaringBitmap32 = getRoaringBitmap32();
|
|
558
|
+
const buf = loadShard(`meta_${shardKey}.cbor`);
|
|
559
|
+
if (!buf) {
|
|
560
|
+
return {
|
|
561
|
+
nodeToGlobal: [],
|
|
562
|
+
nextLocalId: 0,
|
|
563
|
+
aliveBitmap: new RoaringBitmap32(),
|
|
564
|
+
globalToNode: new Map(),
|
|
565
|
+
nodeToGlobalMap: new Map(),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const raw = /** @type {{ nodeToGlobal: Array<[string, number]> | Record<string, number>, alive: Uint8Array | number[], nextLocalId: number }} */ (this._codec.decode(buf));
|
|
569
|
+
const entries = Array.isArray(raw.nodeToGlobal)
|
|
570
|
+
? raw.nodeToGlobal
|
|
571
|
+
: Object.entries(raw.nodeToGlobal);
|
|
572
|
+
const alive = raw.alive && raw.alive.length > 0
|
|
573
|
+
? RoaringBitmap32.deserialize(toBytes(raw.alive), true)
|
|
574
|
+
: new RoaringBitmap32();
|
|
575
|
+
|
|
576
|
+
// Build O(1) lookup maps from the entries array
|
|
577
|
+
/** @type {Map<number, string>} */
|
|
578
|
+
const globalToNode = new Map();
|
|
579
|
+
/** @type {Map<string, number>} */
|
|
580
|
+
const nodeToGlobalMap = new Map();
|
|
581
|
+
for (const [nodeId, gid] of entries) {
|
|
582
|
+
globalToNode.set(Number(gid), nodeId);
|
|
583
|
+
nodeToGlobalMap.set(nodeId, Number(gid));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return { nodeToGlobal: entries, nextLocalId: raw.nextLocalId, aliveBitmap: alive, globalToNode, nodeToGlobalMap };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Serialises and flushes all dirty meta shards into `out`.
|
|
591
|
+
*
|
|
592
|
+
* @param {Map<string, MetaShard>} metaCache
|
|
593
|
+
* @param {Record<string, Uint8Array>} out
|
|
594
|
+
* @private
|
|
595
|
+
*/
|
|
596
|
+
_flushMeta(metaCache, out) {
|
|
597
|
+
for (const [shardKey, meta] of metaCache) {
|
|
598
|
+
const shard = {
|
|
599
|
+
nodeToGlobal: meta.nodeToGlobal,
|
|
600
|
+
nextLocalId: meta.nextLocalId,
|
|
601
|
+
alive: meta.aliveBitmap.serialize(true),
|
|
602
|
+
};
|
|
603
|
+
out[`meta_${shardKey}.cbor`] = this._codec.encode(shard).slice();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ── Edge shard I/O ────────────────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* @param {Map<string, EdgeShardData>} cache
|
|
611
|
+
* @param {string} dir
|
|
612
|
+
* @param {string} shardKey
|
|
613
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
614
|
+
* @returns {EdgeShardData}
|
|
615
|
+
* @private
|
|
616
|
+
*/
|
|
617
|
+
_getOrLoadEdgeShard(cache, dir, shardKey, loadShard) {
|
|
618
|
+
const cacheKey = `${dir}_${shardKey}`;
|
|
619
|
+
const cached = cache.get(cacheKey);
|
|
620
|
+
if (cached) {
|
|
621
|
+
return cached;
|
|
622
|
+
}
|
|
623
|
+
const data = this._loadEdgeShard(dir, shardKey, loadShard);
|
|
624
|
+
cache.set(cacheKey, data);
|
|
625
|
+
return data;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @param {string} dir
|
|
630
|
+
* @param {string} shardKey
|
|
631
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
632
|
+
* @returns {EdgeShardData}
|
|
633
|
+
* @private
|
|
634
|
+
*/
|
|
635
|
+
_loadEdgeShard(dir, shardKey, loadShard) {
|
|
636
|
+
const buf = loadShard(`${dir}_${shardKey}.cbor`);
|
|
637
|
+
if (!buf) {
|
|
638
|
+
return {};
|
|
639
|
+
}
|
|
640
|
+
return /** @type {EdgeShardData} */ (this._codec.decode(buf));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Flushes all dirty edge shards for a direction into `out`.
|
|
645
|
+
*
|
|
646
|
+
* @param {Map<string, EdgeShardData>} cache
|
|
647
|
+
* @param {string} dir
|
|
648
|
+
* @param {Record<string, Uint8Array>} out
|
|
649
|
+
* @private
|
|
650
|
+
*/
|
|
651
|
+
_flushEdgeShards(cache, dir, out) {
|
|
652
|
+
const prefix = `${dir}_`;
|
|
653
|
+
for (const [cacheKey, data] of cache) {
|
|
654
|
+
if (!cacheKey.startsWith(prefix)) {
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const path = `${cacheKey}.cbor`;
|
|
658
|
+
out[path] = this._codec.encode(data).slice();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ── Labels I/O ────────────────────────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
666
|
+
* @returns {Record<string, number>}
|
|
667
|
+
* @private
|
|
668
|
+
*/
|
|
669
|
+
_loadLabels(loadShard) {
|
|
670
|
+
const buf = loadShard('labels.cbor');
|
|
671
|
+
if (!buf) {
|
|
672
|
+
return Object.create(null);
|
|
673
|
+
}
|
|
674
|
+
const decoded = /** @type {Record<string, number>|Array<[string, number]>} */ (this._codec.decode(buf));
|
|
675
|
+
/** @type {Record<string, number>} */
|
|
676
|
+
const labels = Object.create(null);
|
|
677
|
+
const entries = Array.isArray(decoded) ? decoded : Object.entries(decoded);
|
|
678
|
+
for (const [label, id] of entries) {
|
|
679
|
+
labels[label] = id;
|
|
680
|
+
}
|
|
681
|
+
return labels;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* @param {Record<string, number>} labels
|
|
686
|
+
* @returns {Uint8Array}
|
|
687
|
+
* @private
|
|
688
|
+
*/
|
|
689
|
+
_saveLabels(labels) {
|
|
690
|
+
const entries = Object.entries(labels).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
691
|
+
return this._codec.encode(entries).slice();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ── Props I/O ─────────────────────────────────────────────────────────────
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* @param {string} shardKey
|
|
698
|
+
* @param {(path: string) => Uint8Array|undefined} loadShard
|
|
699
|
+
* @returns {Map<string, Record<string, unknown>>}
|
|
700
|
+
* @private
|
|
701
|
+
*/
|
|
702
|
+
_loadProps(shardKey, loadShard) {
|
|
703
|
+
const buf = loadShard(`props_${shardKey}.cbor`);
|
|
704
|
+
/** @type {Map<string, Record<string, unknown>>} */
|
|
705
|
+
const map = new Map();
|
|
706
|
+
if (!buf) {
|
|
707
|
+
return map;
|
|
708
|
+
}
|
|
709
|
+
const decoded = this._codec.decode(buf);
|
|
710
|
+
if (Array.isArray(decoded)) {
|
|
711
|
+
for (const [nodeId, props] of decoded) {
|
|
712
|
+
const safeProps = Object.assign(
|
|
713
|
+
Object.create(null),
|
|
714
|
+
(props && typeof props === 'object') ? props : {},
|
|
715
|
+
);
|
|
716
|
+
map.set(nodeId, safeProps);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return map;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* @param {Map<string, Record<string, unknown>>} shard
|
|
724
|
+
* @returns {Uint8Array}
|
|
725
|
+
* @private
|
|
726
|
+
*/
|
|
727
|
+
_saveProps(shard) {
|
|
728
|
+
const entries = [...shard.entries()];
|
|
729
|
+
return this._codec.encode(entries).slice();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ── Utility ───────────────────────────────────────────────────────────────
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Finds the globalId for a nodeId in a MetaShard via the O(1) forward map.
|
|
736
|
+
*
|
|
737
|
+
* @param {MetaShard} meta
|
|
738
|
+
* @param {string} nodeId
|
|
739
|
+
* @returns {number|undefined}
|
|
740
|
+
* @private
|
|
741
|
+
*/
|
|
742
|
+
_findGlobalId(meta, nodeId) {
|
|
743
|
+
return meta.nodeToGlobalMap.get(nodeId);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Deserializes a bitmap from edge shard data, or creates a new one.
|
|
748
|
+
*
|
|
749
|
+
* @param {EdgeShardData} data
|
|
750
|
+
* @param {string} bucket
|
|
751
|
+
* @param {string} ownerStr
|
|
752
|
+
* @returns {import('../utils/roaring.js').RoaringBitmapSubset}
|
|
753
|
+
* @private
|
|
754
|
+
*/
|
|
755
|
+
_deserializeBitmap(data, bucket, ownerStr) {
|
|
756
|
+
const RoaringBitmap32 = getRoaringBitmap32();
|
|
757
|
+
if (data[bucket] && data[bucket][ownerStr]) {
|
|
758
|
+
return RoaringBitmap32.deserialize(
|
|
759
|
+
toBytes(data[bucket][ownerStr]),
|
|
760
|
+
true,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
return new RoaringBitmap32();
|
|
764
|
+
}
|
|
765
|
+
}
|