@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.
Files changed (49) hide show
  1. package/README.md +137 -10
  2. package/bin/cli/commands/registry.js +4 -0
  3. package/bin/cli/commands/reindex.js +41 -0
  4. package/bin/cli/commands/verify-index.js +59 -0
  5. package/bin/cli/infrastructure.js +7 -2
  6. package/bin/cli/schemas.js +19 -0
  7. package/bin/cli/types.js +2 -0
  8. package/index.d.ts +52 -15
  9. package/package.json +3 -2
  10. package/src/domain/WarpGraph.js +40 -0
  11. package/src/domain/errors/ShardIdOverflowError.js +28 -0
  12. package/src/domain/errors/index.js +1 -0
  13. package/src/domain/services/AdjacencyNeighborProvider.js +140 -0
  14. package/src/domain/services/BitmapNeighborProvider.js +178 -0
  15. package/src/domain/services/CheckpointMessageCodec.js +3 -3
  16. package/src/domain/services/CheckpointService.js +77 -12
  17. package/src/domain/services/GraphTraversal.js +1239 -0
  18. package/src/domain/services/IncrementalIndexUpdater.js +765 -0
  19. package/src/domain/services/JoinReducer.js +233 -5
  20. package/src/domain/services/LogicalBitmapIndexBuilder.js +323 -0
  21. package/src/domain/services/LogicalIndexBuildService.js +108 -0
  22. package/src/domain/services/LogicalIndexReader.js +315 -0
  23. package/src/domain/services/LogicalTraversal.js +321 -202
  24. package/src/domain/services/MaterializedViewService.js +379 -0
  25. package/src/domain/services/ObserverView.js +132 -69
  26. package/src/domain/services/PatchBuilderV2.js +3 -3
  27. package/src/domain/services/PropertyIndexBuilder.js +64 -0
  28. package/src/domain/services/PropertyIndexReader.js +111 -0
  29. package/src/domain/services/QueryBuilder.js +15 -44
  30. package/src/domain/services/TemporalQuery.js +128 -14
  31. package/src/domain/services/TranslationCost.js +8 -24
  32. package/src/domain/types/PatchDiff.js +90 -0
  33. package/src/domain/types/WarpTypesV2.js +4 -4
  34. package/src/domain/utils/MinHeap.js +45 -17
  35. package/src/domain/utils/canonicalCbor.js +36 -0
  36. package/src/domain/utils/fnv1a.js +20 -0
  37. package/src/domain/utils/matchGlob.js +51 -0
  38. package/src/domain/utils/roaring.js +14 -3
  39. package/src/domain/utils/shardKey.js +40 -0
  40. package/src/domain/utils/toBytes.js +17 -0
  41. package/src/domain/warp/_wiredMethods.d.ts +7 -1
  42. package/src/domain/warp/checkpoint.methods.js +21 -5
  43. package/src/domain/warp/materialize.methods.js +17 -5
  44. package/src/domain/warp/materializeAdvanced.methods.js +142 -3
  45. package/src/domain/warp/query.methods.js +83 -15
  46. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
  47. package/src/ports/BlobPort.js +1 -1
  48. package/src/ports/NeighborProviderPort.js +59 -0
  49. 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._seekCache.set(cacheKey, /** @type {Buffer} */ (buf)).catch(() => {});
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
- if ((direction === 'outgoing' || direction === 'both') && from === nodeId) {
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
- if ((direction === 'incoming' || direction === 'both') && to === nodeId) {
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
- if (!config || typeof config.match !== 'string') {
256
- throw new Error('observer config.match must be a string');
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
- return buffer;
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
- index.entries[key] = {
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
  }
@@ -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 buffer, or null on miss
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