@git-stunts/git-warp 11.5.1 → 12.1.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 +137 -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 +52 -15
- package/package.json +3 -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 +132 -69
- 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/QueryBuilder.js +15 -44
- package/src/domain/services/TemporalQuery.js +128 -14
- package/src/domain/services/TranslationCost.js +8 -24
- 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/matchGlob.js +51 -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 +83 -15
- 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
|
@@ -18,6 +18,7 @@ import { serializeFullStateV5, deserializeFullStateV5 } from '../services/Checkp
|
|
|
18
18
|
import { buildSeekCacheKey } from '../utils/seekCacheKey.js';
|
|
19
19
|
import { materializeIncremental } from '../services/CheckpointService.js';
|
|
20
20
|
import { createFrontier, updateFrontier } from '../services/Frontier.js';
|
|
21
|
+
import BitmapNeighborProvider from '../services/BitmapNeighborProvider.js';
|
|
21
22
|
|
|
22
23
|
/** @typedef {import('../types/WarpPersistence.js').CorePersistence} CorePersistence */
|
|
23
24
|
/** @typedef {import('../services/JoinReducer.js').WarpStateV5} WarpStateV5 */
|
|
@@ -106,10 +107,11 @@ export function _buildAdjacency(state) {
|
|
|
106
107
|
*
|
|
107
108
|
* @this {import('../WarpGraph.js').default}
|
|
108
109
|
* @param {import('../services/JoinReducer.js').WarpStateV5} state
|
|
110
|
+
* @param {import('../types/PatchDiff.js').PatchDiff} [diff] - Optional diff for incremental index
|
|
109
111
|
* @returns {Promise<MaterializedResult>}
|
|
110
112
|
* @private
|
|
111
113
|
*/
|
|
112
|
-
export async function _setMaterializedState(state) {
|
|
114
|
+
export async function _setMaterializedState(state, diff) {
|
|
113
115
|
this._cachedState = state;
|
|
114
116
|
this._stateDirty = false;
|
|
115
117
|
this._versionVector = vvClone(state.observedFrontier);
|
|
@@ -128,9 +130,58 @@ export async function _setMaterializedState(state) {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
this._materializedGraph = { state, stateHash, adjacency };
|
|
133
|
+
this._buildView(state, stateHash, diff);
|
|
131
134
|
return this._materializedGraph;
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Builds the MaterializedView (logicalIndex + propertyReader) and attaches
|
|
139
|
+
* a BitmapNeighborProvider to the materialized graph. Skips rebuild when
|
|
140
|
+
* the stateHash matches the previous build. Uses incremental update when
|
|
141
|
+
* a diff and cached index tree are available.
|
|
142
|
+
*
|
|
143
|
+
* @this {import('../WarpGraph.js').default}
|
|
144
|
+
* @param {import('../services/JoinReducer.js').WarpStateV5} state
|
|
145
|
+
* @param {string} stateHash
|
|
146
|
+
* @param {import('../types/PatchDiff.js').PatchDiff} [diff] - Optional diff for incremental update
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
export function _buildView(state, stateHash, diff) {
|
|
150
|
+
if (this._cachedViewHash === stateHash) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
/** @type {import('../services/MaterializedViewService.js').BuildResult} */
|
|
155
|
+
let result;
|
|
156
|
+
if (diff && this._cachedIndexTree) {
|
|
157
|
+
result = this._viewService.applyDiff({
|
|
158
|
+
existingTree: this._cachedIndexTree,
|
|
159
|
+
diff,
|
|
160
|
+
state,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
result = this._viewService.build(state);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this._logicalIndex = result.logicalIndex;
|
|
167
|
+
this._propertyReader = result.propertyReader;
|
|
168
|
+
this._cachedViewHash = stateHash;
|
|
169
|
+
this._cachedIndexTree = result.tree;
|
|
170
|
+
|
|
171
|
+
const provider = new BitmapNeighborProvider({ logicalIndex: result.logicalIndex });
|
|
172
|
+
if (this._materializedGraph) {
|
|
173
|
+
this._materializedGraph.provider = provider;
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
this._logger?.warn('[warp] index build failed, falling back to linear scan', {
|
|
177
|
+
error: /** @type {Error} */ (err).message,
|
|
178
|
+
});
|
|
179
|
+
this._logicalIndex = null;
|
|
180
|
+
this._propertyReader = null;
|
|
181
|
+
this._cachedIndexTree = null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
134
185
|
/**
|
|
135
186
|
* Materializes the graph with a Lamport ceiling (time-travel).
|
|
136
187
|
*
|
|
@@ -196,12 +247,15 @@ export async function _materializeWithCeiling(ceiling, collectReceipts, t0) {
|
|
|
196
247
|
const cached = await this._seekCache.get(cacheKey);
|
|
197
248
|
if (cached) {
|
|
198
249
|
try {
|
|
199
|
-
const state = deserializeFullStateV5(cached, { codec: this._codec });
|
|
250
|
+
const state = deserializeFullStateV5(cached.buffer, { codec: this._codec });
|
|
200
251
|
this._provenanceIndex = new ProvenanceIndex();
|
|
201
252
|
this._provenanceDegraded = true;
|
|
202
253
|
await this._setMaterializedState(state);
|
|
203
254
|
this._cachedCeiling = ceiling;
|
|
204
255
|
this._cachedFrontier = frontier;
|
|
256
|
+
if (cached.indexTreeOid) {
|
|
257
|
+
await this._restoreIndexFromCache(cached.indexTreeOid);
|
|
258
|
+
}
|
|
205
259
|
this._logTiming('materialize', t0, { metrics: `cache hit (ceiling=${ceiling})` });
|
|
206
260
|
return state;
|
|
207
261
|
} catch {
|
|
@@ -258,7 +312,8 @@ export async function _materializeWithCeiling(ceiling, collectReceipts, t0) {
|
|
|
258
312
|
cacheKey = buildSeekCacheKey(ceiling, frontier);
|
|
259
313
|
}
|
|
260
314
|
const buf = serializeFullStateV5(state, { codec: this._codec });
|
|
261
|
-
this.
|
|
315
|
+
this._persistSeekCacheEntry(cacheKey, /** @type {Buffer} */ (buf), state)
|
|
316
|
+
.catch(() => {});
|
|
262
317
|
}
|
|
263
318
|
|
|
264
319
|
// Skip auto-checkpoint and GC — this is an exploratory read
|
|
@@ -270,6 +325,62 @@ export async function _materializeWithCeiling(ceiling, collectReceipts, t0) {
|
|
|
270
325
|
return state;
|
|
271
326
|
}
|
|
272
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Persists a seek cache entry with an optional index tree snapshot.
|
|
330
|
+
*
|
|
331
|
+
* Builds the bitmap index tree from the materialized state, writes it
|
|
332
|
+
* to Git storage, and includes the resulting tree OID in the cache
|
|
333
|
+
* entry metadata. Index persistence failure is non-fatal — the state
|
|
334
|
+
* buffer is still cached without the index.
|
|
335
|
+
*
|
|
336
|
+
* @this {import('../WarpGraph.js').default}
|
|
337
|
+
* @param {string} cacheKey - Seek cache key
|
|
338
|
+
* @param {Buffer} buf - Serialized WarpStateV5 buffer
|
|
339
|
+
* @param {import('../services/JoinReducer.js').WarpStateV5} state
|
|
340
|
+
* @returns {Promise<void>}
|
|
341
|
+
* @private
|
|
342
|
+
*/
|
|
343
|
+
export async function _persistSeekCacheEntry(cacheKey, buf, state) {
|
|
344
|
+
/** @type {{ indexTreeOid?: string }} */
|
|
345
|
+
const opts = {};
|
|
346
|
+
try {
|
|
347
|
+
const { tree } = this._viewService.build(state);
|
|
348
|
+
opts.indexTreeOid = await this._viewService.persistIndexTree(
|
|
349
|
+
tree,
|
|
350
|
+
this._persistence,
|
|
351
|
+
);
|
|
352
|
+
} catch {
|
|
353
|
+
// Non-fatal — cache the state without the index
|
|
354
|
+
}
|
|
355
|
+
if (this._seekCache) {
|
|
356
|
+
await this._seekCache.set(cacheKey, buf, opts);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Restores a LogicalIndex and PropertyReader from a cached index tree OID.
|
|
362
|
+
*
|
|
363
|
+
* Reads the tree entries from Git storage and delegates hydration to
|
|
364
|
+
* the MaterializedViewService. Failure is non-fatal — the in-memory
|
|
365
|
+
* index built by `_buildView` remains as fallback.
|
|
366
|
+
*
|
|
367
|
+
* @this {import('../WarpGraph.js').default}
|
|
368
|
+
* @param {string} indexTreeOid - Git tree OID of the bitmap index snapshot
|
|
369
|
+
* @returns {Promise<void>}
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
export async function _restoreIndexFromCache(indexTreeOid) {
|
|
373
|
+
try {
|
|
374
|
+
const shardOids = await this._persistence.readTreeOids(indexTreeOid);
|
|
375
|
+
const { logicalIndex, propertyReader } =
|
|
376
|
+
await this._viewService.loadFromOids(shardOids, this._persistence);
|
|
377
|
+
this._logicalIndex = logicalIndex;
|
|
378
|
+
this._propertyReader = propertyReader;
|
|
379
|
+
} catch {
|
|
380
|
+
// Non-fatal — fall back to in-memory index from _buildView
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
273
384
|
/**
|
|
274
385
|
* Materializes the graph state at a specific checkpoint.
|
|
275
386
|
*
|
|
@@ -348,3 +459,31 @@ export async function materializeAt(checkpointSha) {
|
|
|
348
459
|
await this._setMaterializedState(state);
|
|
349
460
|
return state;
|
|
350
461
|
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Verifies the bitmap index against adjacency ground truth.
|
|
465
|
+
*
|
|
466
|
+
* @this {import('../WarpGraph.js').default}
|
|
467
|
+
* @param {{ seed?: number, sampleRate?: number }} [options]
|
|
468
|
+
* @returns {{ passed: number, failed: number, errors: Array<{nodeId: string, direction: string, expected: string[], actual: string[]}> }}
|
|
469
|
+
*/
|
|
470
|
+
export function verifyIndex(options) {
|
|
471
|
+
if (!this._logicalIndex || !this._cachedState || !this._viewService) {
|
|
472
|
+
throw new Error('Cannot verify index: graph not materialized or index not built');
|
|
473
|
+
}
|
|
474
|
+
return this._viewService.verifyIndex({
|
|
475
|
+
state: this._cachedState,
|
|
476
|
+
logicalIndex: this._logicalIndex,
|
|
477
|
+
options,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Clears the cached bitmap index, forcing a full rebuild on next materialize.
|
|
483
|
+
*
|
|
484
|
+
* @this {import('../WarpGraph.js').default}
|
|
485
|
+
*/
|
|
486
|
+
export function invalidateIndex() {
|
|
487
|
+
this._cachedIndexTree = null;
|
|
488
|
+
this._cachedViewHash = null;
|
|
489
|
+
}
|
|
@@ -42,6 +42,18 @@ export async function hasNode(nodeId) {
|
|
|
42
42
|
*/
|
|
43
43
|
export async function getNodeProps(nodeId) {
|
|
44
44
|
await this._ensureFreshState();
|
|
45
|
+
|
|
46
|
+
// ── Indexed fast path (positive results only; stale index falls through) ──
|
|
47
|
+
if (this._propertyReader && this._logicalIndex?.isAlive(nodeId)) {
|
|
48
|
+
try {
|
|
49
|
+
const record = await this._propertyReader.getNodeProps(nodeId);
|
|
50
|
+
return record ? new Map(Object.entries(record)) : new Map();
|
|
51
|
+
} catch {
|
|
52
|
+
// Fall through to linear scan on index read failures.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Linear scan fallback ─────────────────────────────────────────────
|
|
45
57
|
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
46
58
|
|
|
47
59
|
if (!orsetContains(s.nodeAlive, nodeId)) {
|
|
@@ -103,6 +115,17 @@ export async function getEdgeProps(from, to, label) {
|
|
|
103
115
|
return props;
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Converts NeighborEdge[] to the query-method shape with a direction tag.
|
|
120
|
+
*
|
|
121
|
+
* @param {Array<{neighborId: string, label: string}>} edges
|
|
122
|
+
* @param {'outgoing' | 'incoming'} dir
|
|
123
|
+
* @returns {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>}
|
|
124
|
+
*/
|
|
125
|
+
function tagDirection(edges, dir) {
|
|
126
|
+
return edges.map((e) => ({ nodeId: e.neighborId, label: e.label, direction: dir }));
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
/**
|
|
107
130
|
* Gets neighbors of a node from the materialized state.
|
|
108
131
|
*
|
|
@@ -115,28 +138,71 @@ export async function getEdgeProps(from, to, label) {
|
|
|
115
138
|
*/
|
|
116
139
|
export async function neighbors(nodeId, direction = 'both', edgeLabel = undefined) {
|
|
117
140
|
await this._ensureFreshState();
|
|
118
|
-
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
119
141
|
|
|
142
|
+
// ── Indexed fast path (only when node is in index; stale falls through) ──
|
|
143
|
+
const provider = this._materializedGraph?.provider;
|
|
144
|
+
if (provider && this._logicalIndex?.isAlive(nodeId)) {
|
|
145
|
+
try {
|
|
146
|
+
const opts = edgeLabel ? { labels: new Set([edgeLabel]) } : undefined;
|
|
147
|
+
return await _indexedNeighbors(provider, nodeId, direction, opts);
|
|
148
|
+
} catch {
|
|
149
|
+
// Fall through to linear scan on index/provider failures.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Linear scan fallback ─────────────────────────────────────────────
|
|
154
|
+
return _linearNeighbors(/** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState), nodeId, direction, edgeLabel);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Indexed neighbor lookup using BitmapNeighborProvider.
|
|
159
|
+
*
|
|
160
|
+
* @param {import('../../ports/NeighborProviderPort.js').default} provider
|
|
161
|
+
* @param {string} nodeId
|
|
162
|
+
* @param {'outgoing' | 'incoming' | 'both'} direction
|
|
163
|
+
* @param {import('../../ports/NeighborProviderPort.js').NeighborOptions} [opts]
|
|
164
|
+
* @returns {Promise<Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>>}
|
|
165
|
+
*/
|
|
166
|
+
async function _indexedNeighbors(provider, nodeId, direction, opts) {
|
|
167
|
+
if (direction === 'both') {
|
|
168
|
+
const [outEdges, inEdges] = await Promise.all([
|
|
169
|
+
provider.getNeighbors(nodeId, 'out', opts),
|
|
170
|
+
provider.getNeighbors(nodeId, 'in', opts),
|
|
171
|
+
]);
|
|
172
|
+
return [...tagDirection(outEdges, 'outgoing'), ...tagDirection(inEdges, 'incoming')];
|
|
173
|
+
}
|
|
174
|
+
const dir = direction === 'outgoing' ? 'out' : 'in';
|
|
175
|
+
const edges = await provider.getNeighbors(nodeId, dir, opts);
|
|
176
|
+
const tag = direction === 'outgoing' ? /** @type {const} */ ('outgoing') : /** @type {const} */ ('incoming');
|
|
177
|
+
return tagDirection(edges, tag);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Linear-scan neighbor lookup from raw CRDT state.
|
|
182
|
+
*
|
|
183
|
+
* @param {import('../services/JoinReducer.js').WarpStateV5} cachedState
|
|
184
|
+
* @param {string} nodeId
|
|
185
|
+
* @param {'outgoing' | 'incoming' | 'both'} direction
|
|
186
|
+
* @param {string} [edgeLabel]
|
|
187
|
+
* @returns {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>}
|
|
188
|
+
*/
|
|
189
|
+
function _linearNeighbors(cachedState, nodeId, direction, edgeLabel) {
|
|
190
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (cachedState);
|
|
120
191
|
/** @type {Array<{nodeId: string, label: string, direction: 'outgoing' | 'incoming'}>} */
|
|
121
192
|
const result = [];
|
|
193
|
+
const checkOut = direction === 'outgoing' || direction === 'both';
|
|
194
|
+
const checkIn = direction === 'incoming' || direction === 'both';
|
|
122
195
|
|
|
123
196
|
for (const edgeKey of orsetElements(s.edgeAlive)) {
|
|
124
197
|
const { from, to, label } = decodeEdgeKey(edgeKey);
|
|
125
|
-
|
|
126
198
|
if (edgeLabel !== undefined && label !== edgeLabel) {
|
|
127
199
|
continue;
|
|
128
200
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (orsetContains(s.nodeAlive, to)) {
|
|
132
|
-
result.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
|
|
133
|
-
}
|
|
201
|
+
if (checkOut && from === nodeId && orsetContains(s.nodeAlive, to)) {
|
|
202
|
+
result.push({ nodeId: to, label, direction: /** @type {const} */ ('outgoing') });
|
|
134
203
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (orsetContains(s.nodeAlive, from)) {
|
|
138
|
-
result.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
|
|
139
|
-
}
|
|
204
|
+
if (checkIn && to === nodeId && orsetContains(s.nodeAlive, from)) {
|
|
205
|
+
result.push({ nodeId: from, label, direction: /** @type {const} */ ('incoming') });
|
|
140
206
|
}
|
|
141
207
|
}
|
|
142
208
|
|
|
@@ -246,14 +312,16 @@ export function query() {
|
|
|
246
312
|
* @this {import('../WarpGraph.js').default}
|
|
247
313
|
* @param {string} name - Observer name
|
|
248
314
|
* @param {Object} config - Observer configuration
|
|
249
|
-
* @param {string} config.match - Glob pattern for visible nodes
|
|
315
|
+
* @param {string|string[]} config.match - Glob pattern(s) for visible nodes
|
|
250
316
|
* @param {string[]} [config.expose] - Property keys to include
|
|
251
317
|
* @param {string[]} [config.redact] - Property keys to exclude
|
|
252
318
|
* @returns {Promise<import('../services/ObserverView.js').default>} A read-only observer view
|
|
253
319
|
*/
|
|
254
320
|
export async function observer(name, config) {
|
|
255
|
-
|
|
256
|
-
|
|
321
|
+
/** @param {unknown} m */
|
|
322
|
+
const isValidMatch = (m) => typeof m === 'string' || (Array.isArray(m) && m.every(/** @param {unknown} i */ i => typeof i === 'string'));
|
|
323
|
+
if (!config || !isValidMatch(config.match)) {
|
|
324
|
+
throw new Error('observer config.match must be a string or array of strings');
|
|
257
325
|
}
|
|
258
326
|
await this._ensureFreshState();
|
|
259
327
|
return new ObserverView({ name, config, graph: this });
|
|
@@ -39,6 +39,7 @@ const MAX_CAS_RETRIES = 3;
|
|
|
39
39
|
* @property {string} codec - Codec identifier (e.g. 'cbor-v1')
|
|
40
40
|
* @property {number} schemaVersion - Index entry schema version
|
|
41
41
|
* @property {string} [lastAccessedAt] - ISO 8601 timestamp of last read (for LRU eviction)
|
|
42
|
+
* @property {string} [indexTreeOid] - Git tree OID of the bitmap index snapshot
|
|
42
43
|
*/
|
|
43
44
|
|
|
44
45
|
/**
|
|
@@ -203,9 +204,18 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
203
204
|
// ---------------------------------------------------------------------------
|
|
204
205
|
|
|
205
206
|
/**
|
|
207
|
+
* Retrieves a cached state buffer by key.
|
|
208
|
+
*
|
|
209
|
+
* Note: This method reads the index twice — once here for the entry lookup,
|
|
210
|
+
* and again inside `_mutateIndex` for the `lastAccessedAt` update. The
|
|
211
|
+
* double-read is a known trade-off: `_mutateIndex` re-reads to provide
|
|
212
|
+
* CAS-safe retry semantics, and deduplicating the reads would complicate
|
|
213
|
+
* the retry logic without meaningful performance impact (the index is a
|
|
214
|
+
* single small JSON blob).
|
|
215
|
+
*
|
|
206
216
|
* @override
|
|
207
217
|
* @param {string} key
|
|
208
|
-
* @returns {Promise<Buffer|null>}
|
|
218
|
+
* @returns {Promise<{ buffer: Buffer|Uint8Array, indexTreeOid?: string } | null>}
|
|
209
219
|
*/
|
|
210
220
|
async get(key) {
|
|
211
221
|
const cas = await this._getCas();
|
|
@@ -225,7 +235,12 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
225
235
|
}
|
|
226
236
|
return idx;
|
|
227
237
|
});
|
|
228
|
-
|
|
238
|
+
/** @type {{ buffer: Buffer|Uint8Array, indexTreeOid?: string }} */
|
|
239
|
+
const result = { buffer };
|
|
240
|
+
if (entry.indexTreeOid) {
|
|
241
|
+
result.indexTreeOid = entry.indexTreeOid;
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
229
244
|
} catch {
|
|
230
245
|
// Blob GC'd or corrupted — self-heal by removing dead entry
|
|
231
246
|
await this._mutateIndex((idx) => {
|
|
@@ -239,10 +254,11 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
239
254
|
/**
|
|
240
255
|
* @override
|
|
241
256
|
* @param {string} key
|
|
242
|
-
* @param {Buffer} buffer
|
|
257
|
+
* @param {Buffer|Uint8Array} buffer
|
|
258
|
+
* @param {{ indexTreeOid?: string }} [options]
|
|
243
259
|
* @returns {Promise<void>}
|
|
244
260
|
*/
|
|
245
|
-
async set(key, buffer) {
|
|
261
|
+
async set(key, buffer, options) {
|
|
246
262
|
const cas = await this._getCas();
|
|
247
263
|
const { ceiling, frontierHash } = this._parseKey(key);
|
|
248
264
|
|
|
@@ -257,7 +273,8 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
257
273
|
|
|
258
274
|
// Update index with rich metadata
|
|
259
275
|
await this._mutateIndex((index) => {
|
|
260
|
-
|
|
276
|
+
/** @type {IndexEntry} */
|
|
277
|
+
const entry = {
|
|
261
278
|
treeOid,
|
|
262
279
|
createdAt: new Date().toISOString(),
|
|
263
280
|
ceiling,
|
|
@@ -266,6 +283,10 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
266
283
|
codec: 'cbor-v1',
|
|
267
284
|
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
268
285
|
};
|
|
286
|
+
if (options?.indexTreeOid) {
|
|
287
|
+
entry.indexTreeOid = options.indexTreeOid;
|
|
288
|
+
}
|
|
289
|
+
index.entries[key] = entry;
|
|
269
290
|
return this._enforceMaxEntries(index);
|
|
270
291
|
});
|
|
271
292
|
}
|
package/src/ports/BlobPort.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export default class BlobPort {
|
|
11
11
|
/**
|
|
12
12
|
* Writes content as a Git blob and returns its OID.
|
|
13
|
-
* @param {Buffer|string} _content - The blob content to write
|
|
13
|
+
* @param {Uint8Array|Buffer|string} _content - The blob content to write
|
|
14
14
|
* @returns {Promise<string>} The Git OID of the created blob
|
|
15
15
|
* @throws {Error} If not implemented by a concrete adapter
|
|
16
16
|
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port interface for neighbor lookups on any graph.
|
|
3
|
+
*
|
|
4
|
+
* Concrete providers back this with in-memory adjacency maps, bitmap indexes,
|
|
5
|
+
* or remote APIs. All providers MUST return edges sorted by (neighborId, label)
|
|
6
|
+
* using strict codepoint comparison (never localeCompare).
|
|
7
|
+
*
|
|
8
|
+
* @abstract
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @typedef {'out' | 'in' | 'both'} Direction */
|
|
12
|
+
/** @typedef {{ labels?: Set<string> }} NeighborOptions */
|
|
13
|
+
/** @typedef {{ neighborId: string, label: string }} NeighborEdge */
|
|
14
|
+
|
|
15
|
+
export default class NeighborProviderPort {
|
|
16
|
+
/**
|
|
17
|
+
* Returns neighbor edges for a node, sorted by (neighborId, label).
|
|
18
|
+
*
|
|
19
|
+
* For direction 'both', returns the union of out and in edges
|
|
20
|
+
* deduped by (neighborId, label). A consumer cannot tell if an
|
|
21
|
+
* edge was outgoing or incoming — this is intentionally lossy.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} _nodeId - The node to look up
|
|
24
|
+
* @param {Direction} _direction - Edge direction: 'out', 'in', or 'both'
|
|
25
|
+
* @param {NeighborOptions} [_options] - Optional label filter
|
|
26
|
+
* @returns {Promise<NeighborEdge[]>} Sorted by (neighborId, label) via codepoint comparison
|
|
27
|
+
* @throws {Error} If not implemented by a concrete provider
|
|
28
|
+
*/
|
|
29
|
+
async getNeighbors(_nodeId, _direction, _options) {
|
|
30
|
+
throw new Error('NeighborProviderPort.getNeighbors() not implemented');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks whether a node is alive in this view.
|
|
35
|
+
*
|
|
36
|
+
* Semantics: "alive in this view" (visible projection), NOT "ever existed."
|
|
37
|
+
*
|
|
38
|
+
* @param {string} _nodeId - The node to check
|
|
39
|
+
* @returns {Promise<boolean>} True if the node is alive
|
|
40
|
+
* @throws {Error} If not implemented by a concrete provider
|
|
41
|
+
*/
|
|
42
|
+
async hasNode(_nodeId) {
|
|
43
|
+
throw new Error('NeighborProviderPort.hasNode() not implemented');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the latency class of this provider.
|
|
48
|
+
*
|
|
49
|
+
* Used by GraphTraversal to decide whether to enable neighbor memoization.
|
|
50
|
+
* - 'sync': in-memory, no benefit from caching (e.g., AdjacencyNeighborProvider)
|
|
51
|
+
* - 'async-local': disk-backed, caching avoids repeated reads (e.g., BitmapNeighborProvider)
|
|
52
|
+
* - 'async-remote': network-backed, caching critical
|
|
53
|
+
*
|
|
54
|
+
* @returns {'sync' | 'async-local' | 'async-remote'}
|
|
55
|
+
*/
|
|
56
|
+
get latencyClass() {
|
|
57
|
+
return 'async-local';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -14,7 +14,7 @@ export default class SeekCachePort {
|
|
|
14
14
|
/**
|
|
15
15
|
* Retrieves a cached state buffer by key.
|
|
16
16
|
* @param {string} _key - Cache key (e.g., 'v1:t42-<frontierHash>')
|
|
17
|
-
* @returns {Promise<Buffer|null>} The cached
|
|
17
|
+
* @returns {Promise<{ buffer: Buffer|Uint8Array, indexTreeOid?: string } | null>} The cached entry, or null on miss
|
|
18
18
|
* @throws {Error} If not implemented by a concrete adapter
|
|
19
19
|
*/
|
|
20
20
|
async get(_key) {
|
|
@@ -24,11 +24,12 @@ export default class SeekCachePort {
|
|
|
24
24
|
/**
|
|
25
25
|
* Stores a state buffer under the given key.
|
|
26
26
|
* @param {string} _key - Cache key
|
|
27
|
-
* @param {Buffer} _buffer - Serialized state to cache
|
|
27
|
+
* @param {Buffer|Uint8Array} _buffer - Serialized state to cache
|
|
28
|
+
* @param {{ indexTreeOid?: string }} [_options] - Optional metadata
|
|
28
29
|
* @returns {Promise<void>}
|
|
29
30
|
* @throws {Error} If not implemented by a concrete adapter
|
|
30
31
|
*/
|
|
31
|
-
async set(_key, _buffer) {
|
|
32
|
+
async set(_key, _buffer, _options) {
|
|
32
33
|
throw new Error('SeekCachePort.set() not implemented');
|
|
33
34
|
}
|
|
34
35
|
|