@git-stunts/git-warp 10.1.2 → 10.4.2

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 (106) hide show
  1. package/README.md +31 -4
  2. package/bin/warp-graph.js +1242 -59
  3. package/index.d.ts +31 -0
  4. package/index.js +4 -0
  5. package/package.json +13 -3
  6. package/src/domain/WarpGraph.js +487 -140
  7. package/src/domain/crdt/LWW.js +1 -1
  8. package/src/domain/crdt/ORSet.js +10 -6
  9. package/src/domain/crdt/VersionVector.js +5 -1
  10. package/src/domain/errors/EmptyMessageError.js +2 -4
  11. package/src/domain/errors/ForkError.js +4 -0
  12. package/src/domain/errors/IndexError.js +4 -0
  13. package/src/domain/errors/OperationAbortedError.js +4 -0
  14. package/src/domain/errors/QueryError.js +4 -0
  15. package/src/domain/errors/SchemaUnsupportedError.js +4 -0
  16. package/src/domain/errors/ShardCorruptionError.js +2 -6
  17. package/src/domain/errors/ShardLoadError.js +2 -6
  18. package/src/domain/errors/ShardValidationError.js +2 -7
  19. package/src/domain/errors/StorageError.js +2 -6
  20. package/src/domain/errors/SyncError.js +4 -0
  21. package/src/domain/errors/TraversalError.js +4 -0
  22. package/src/domain/errors/WarpError.js +2 -4
  23. package/src/domain/errors/WormholeError.js +4 -0
  24. package/src/domain/services/AnchorMessageCodec.js +1 -4
  25. package/src/domain/services/BitmapIndexBuilder.js +10 -6
  26. package/src/domain/services/BitmapIndexReader.js +27 -21
  27. package/src/domain/services/BoundaryTransitionRecord.js +22 -15
  28. package/src/domain/services/CheckpointMessageCodec.js +1 -7
  29. package/src/domain/services/CheckpointSerializerV5.js +20 -19
  30. package/src/domain/services/CheckpointService.js +18 -18
  31. package/src/domain/services/CommitDagTraversalService.js +13 -1
  32. package/src/domain/services/DagPathFinding.js +40 -18
  33. package/src/domain/services/DagTopology.js +7 -6
  34. package/src/domain/services/DagTraversal.js +5 -3
  35. package/src/domain/services/Frontier.js +7 -6
  36. package/src/domain/services/HealthCheckService.js +15 -14
  37. package/src/domain/services/HookInstaller.js +64 -13
  38. package/src/domain/services/HttpSyncServer.js +15 -14
  39. package/src/domain/services/IndexRebuildService.js +12 -12
  40. package/src/domain/services/IndexStalenessChecker.js +13 -6
  41. package/src/domain/services/JoinReducer.js +28 -27
  42. package/src/domain/services/LogicalTraversal.js +7 -6
  43. package/src/domain/services/MessageCodecInternal.js +2 -0
  44. package/src/domain/services/ObserverView.js +6 -6
  45. package/src/domain/services/PatchBuilderV2.js +9 -9
  46. package/src/domain/services/PatchMessageCodec.js +1 -7
  47. package/src/domain/services/ProvenanceIndex.js +6 -8
  48. package/src/domain/services/ProvenancePayload.js +1 -2
  49. package/src/domain/services/QueryBuilder.js +29 -23
  50. package/src/domain/services/StateDiff.js +7 -7
  51. package/src/domain/services/StateSerializerV5.js +8 -6
  52. package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
  53. package/src/domain/services/SyncProtocol.js +23 -26
  54. package/src/domain/services/TemporalQuery.js +4 -3
  55. package/src/domain/services/TranslationCost.js +4 -4
  56. package/src/domain/services/WormholeService.js +19 -15
  57. package/src/domain/types/TickReceipt.js +10 -6
  58. package/src/domain/types/WarpTypesV2.js +2 -3
  59. package/src/domain/utils/CachedValue.js +1 -1
  60. package/src/domain/utils/LRUCache.js +3 -3
  61. package/src/domain/utils/MinHeap.js +2 -2
  62. package/src/domain/utils/RefLayout.js +106 -15
  63. package/src/domain/utils/WriterId.js +2 -2
  64. package/src/domain/utils/defaultCodec.js +9 -2
  65. package/src/domain/utils/defaultCrypto.js +36 -0
  66. package/src/domain/utils/parseCursorBlob.js +51 -0
  67. package/src/domain/utils/roaring.js +5 -5
  68. package/src/domain/utils/seekCacheKey.js +32 -0
  69. package/src/domain/warp/PatchSession.js +3 -3
  70. package/src/domain/warp/Writer.js +2 -2
  71. package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
  72. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
  73. package/src/infrastructure/adapters/ClockAdapter.js +2 -2
  74. package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
  75. package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
  76. package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
  77. package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
  78. package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
  79. package/src/infrastructure/codecs/CborCodec.js +16 -8
  80. package/src/ports/BlobPort.js +2 -2
  81. package/src/ports/CodecPort.js +2 -2
  82. package/src/ports/CommitPort.js +8 -21
  83. package/src/ports/ConfigPort.js +3 -3
  84. package/src/ports/CryptoPort.js +7 -7
  85. package/src/ports/GraphPersistencePort.js +12 -14
  86. package/src/ports/HttpServerPort.js +1 -5
  87. package/src/ports/IndexStoragePort.js +1 -0
  88. package/src/ports/LoggerPort.js +9 -9
  89. package/src/ports/RefPort.js +5 -5
  90. package/src/ports/SeekCachePort.js +73 -0
  91. package/src/ports/TreePort.js +3 -3
  92. package/src/visualization/layouts/converters.js +14 -7
  93. package/src/visualization/layouts/elkAdapter.js +24 -11
  94. package/src/visualization/layouts/elkLayout.js +23 -7
  95. package/src/visualization/layouts/index.js +3 -3
  96. package/src/visualization/renderers/ascii/check.js +30 -17
  97. package/src/visualization/renderers/ascii/graph.js +122 -16
  98. package/src/visualization/renderers/ascii/history.js +29 -90
  99. package/src/visualization/renderers/ascii/index.js +1 -1
  100. package/src/visualization/renderers/ascii/info.js +9 -7
  101. package/src/visualization/renderers/ascii/materialize.js +20 -16
  102. package/src/visualization/renderers/ascii/opSummary.js +81 -0
  103. package/src/visualization/renderers/ascii/path.js +1 -1
  104. package/src/visualization/renderers/ascii/seek.js +344 -0
  105. package/src/visualization/renderers/ascii/table.js +1 -1
  106. package/src/visualization/renderers/svg/index.js +5 -1
@@ -25,9 +25,7 @@
25
25
 
26
26
  /**
27
27
  * Dot - causal identifier for an add operation
28
- * @typedef {Object} Dot
29
- * @property {string} writer - Writer ID that created this dot
30
- * @property {number} seq - Sequence number for this writer
28
+ * @typedef {import('../crdt/Dot.js').Dot} Dot
31
29
  */
32
30
 
33
31
  /**
@@ -182,6 +180,7 @@ export function createPropSetV2(node, key, value) {
182
180
  * @returns {PatchV2} PatchV2 object
183
181
  */
184
182
  export function createPatchV2({ schema = 2, writer, lamport, context, ops, reads, writes }) {
183
+ /** @type {PatchV2} */
185
184
  const patch = {
186
185
  schema,
187
186
  writer,
@@ -68,7 +68,7 @@ class CachedValue {
68
68
  */
69
69
  async get() {
70
70
  if (this._isValid()) {
71
- return this._value;
71
+ return /** @type {T} */ (this._value);
72
72
  }
73
73
 
74
74
  const value = await this._compute();
@@ -35,7 +35,7 @@ class LRUCache {
35
35
  return undefined;
36
36
  }
37
37
  // Move to end (most recently used) by deleting and re-inserting
38
- const value = this._cache.get(key);
38
+ const value = /** @type {V} */ (this._cache.get(key));
39
39
  this._cache.delete(key);
40
40
  this._cache.set(key, value);
41
41
  return value;
@@ -48,7 +48,7 @@ class LRUCache {
48
48
  *
49
49
  * @param {K} key - The key to set
50
50
  * @param {V} value - The value to cache
51
- * @returns {LRUCache} The cache instance for chaining
51
+ * @returns {LRUCache<K, V>} The cache instance for chaining
52
52
  */
53
53
  set(key, value) {
54
54
  // If key exists, delete it first so it moves to the end
@@ -61,7 +61,7 @@ class LRUCache {
61
61
 
62
62
  // Evict oldest entry if over capacity
63
63
  if (this._cache.size > this.maxSize) {
64
- const oldestKey = this._cache.keys().next().value;
64
+ const oldestKey = /** @type {K} */ (this._cache.keys().next().value);
65
65
  this._cache.delete(oldestKey);
66
66
  }
67
67
 
@@ -32,10 +32,10 @@ class MinHeap {
32
32
  */
33
33
  extractMin() {
34
34
  if (this.heap.length === 0) { return undefined; }
35
- if (this.heap.length === 1) { return this.heap.pop().item; }
35
+ if (this.heap.length === 1) { return /** @type {{item: *, priority: number}} */ (this.heap.pop()).item; }
36
36
 
37
37
  const min = this.heap[0];
38
- this.heap[0] = this.heap.pop();
38
+ this.heap[0] = /** @type {{item: *, priority: number}} */ (this.heap.pop());
39
39
  this._bubbleDown(0);
40
40
  return min.item;
41
41
  }
@@ -8,6 +8,8 @@
8
8
  * - refs/warp/<graph>/writers/<writer_id>
9
9
  * - refs/warp/<graph>/checkpoints/head
10
10
  * - refs/warp/<graph>/coverage/head
11
+ * - refs/warp/<graph>/cursor/active
12
+ * - refs/warp/<graph>/cursor/saved/<name>
11
13
  *
12
14
  * @module domain/utils/RefLayout
13
15
  */
@@ -49,20 +51,22 @@ const PATH_TRAVERSAL_PATTERN = /\.\./;
49
51
  * Validates a graph name and throws if invalid.
50
52
  *
51
53
  * Graph names must not contain:
52
- * - Path traversal sequences (`../`)
54
+ * - Path traversal sequences (`..`)
53
55
  * - Semicolons (`;`)
54
56
  * - Spaces
55
57
  * - Null bytes (`\0`)
56
58
  * - Empty strings
57
59
  *
58
60
  * @param {string} name - The graph name to validate
59
- * @throws {Error} If the graph name is invalid
61
+ * @throws {Error} If the name is not a string, is empty, or contains
62
+ * forbidden characters (`..`, `;`, space, `\0`)
60
63
  * @returns {void}
61
64
  *
62
65
  * @example
63
- * validateGraphName('events'); // OK
64
- * validateGraphName('../etc'); // throws
65
- * validateGraphName('my graph'); // throws
66
+ * validateGraphName('events'); // OK
67
+ * validateGraphName('team/proj'); // OK (slashes allowed)
68
+ * validateGraphName('../etc'); // throws — path traversal
69
+ * validateGraphName('my graph'); // throws — contains space
66
70
  */
67
71
  export function validateGraphName(name) {
68
72
  if (typeof name !== 'string') {
@@ -94,18 +98,20 @@ export function validateGraphName(name) {
94
98
  * Validates a writer ID and throws if invalid.
95
99
  *
96
100
  * Writer IDs must:
97
- * - Be ASCII ref-safe: only [A-Za-z0-9._-]
101
+ * - Be ASCII ref-safe: only `[A-Za-z0-9._-]`
98
102
  * - Be 1-64 characters long
99
103
  * - Not contain `/`, `..`, whitespace, or NUL
100
104
  *
101
105
  * @param {string} id - The writer ID to validate
102
- * @throws {Error} If the writer ID is invalid
106
+ * @throws {Error} If the ID is not a string, is empty, exceeds 64 characters,
107
+ * or contains forbidden characters (`/`, `..`, whitespace, NUL, non-ASCII)
103
108
  * @returns {void}
104
109
  *
105
110
  * @example
106
- * validateWriterId('node-1'); // OK
107
- * validateWriterId('a/b'); // throws (contains /)
108
- * validateWriterId('x'.repeat(65)); // throws (too long)
111
+ * validateWriterId('node-1'); // OK
112
+ * validateWriterId('a/b'); // throws contains forward slash
113
+ * validateWriterId('x'.repeat(65)); // throws exceeds max length
114
+ * validateWriterId('has space'); // throws — contains whitespace
109
115
  */
110
116
  export function validateWriterId(id) {
111
117
  if (typeof id !== 'string') {
@@ -157,7 +163,7 @@ export function validateWriterId(id) {
157
163
  *
158
164
  * @param {string} graphName - The name of the graph
159
165
  * @param {string} writerId - The writer's unique identifier
160
- * @returns {string} The full ref path
166
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/writers/<writerId>`
161
167
  * @throws {Error} If graphName or writerId is invalid
162
168
  *
163
169
  * @example
@@ -174,7 +180,7 @@ export function buildWriterRef(graphName, writerId) {
174
180
  * Builds the checkpoint head ref path for the given graph.
175
181
  *
176
182
  * @param {string} graphName - The name of the graph
177
- * @returns {string} The full ref path
183
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/checkpoints/head`
178
184
  * @throws {Error} If graphName is invalid
179
185
  *
180
186
  * @example
@@ -190,7 +196,7 @@ export function buildCheckpointRef(graphName) {
190
196
  * Builds the coverage head ref path for the given graph.
191
197
  *
192
198
  * @param {string} graphName - The name of the graph
193
- * @returns {string} The full ref path
199
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/coverage/head`
194
200
  * @throws {Error} If graphName is invalid
195
201
  *
196
202
  * @example
@@ -204,10 +210,12 @@ export function buildCoverageRef(graphName) {
204
210
 
205
211
  /**
206
212
  * Builds the writers prefix path for the given graph.
207
- * Useful for listing all writer refs under a graph.
213
+ * Useful for listing all writer refs under a graph
214
+ * (e.g. via `git for-each-ref`).
208
215
  *
209
216
  * @param {string} graphName - The name of the graph
210
- * @returns {string} The writers prefix path
217
+ * @returns {string} The writers prefix path (with trailing slash),
218
+ * e.g. `refs/warp/<graphName>/writers/`
211
219
  * @throws {Error} If graphName is invalid
212
220
  *
213
221
  * @example
@@ -219,6 +227,89 @@ export function buildWritersPrefix(graphName) {
219
227
  return `${REF_PREFIX}/${graphName}/writers/`;
220
228
  }
221
229
 
230
+ /**
231
+ * Builds the active cursor ref path for the given graph.
232
+ *
233
+ * The active cursor is a single ref that stores the current time-travel
234
+ * position used by `git warp seek`. It points to a commit SHA representing
235
+ * the materialization frontier the user has seeked to.
236
+ *
237
+ * @param {string} graphName - The name of the graph
238
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/cursor/active`
239
+ * @throws {Error} If graphName is invalid
240
+ *
241
+ * @example
242
+ * buildCursorActiveRef('events');
243
+ * // => 'refs/warp/events/cursor/active'
244
+ */
245
+ export function buildCursorActiveRef(graphName) {
246
+ validateGraphName(graphName);
247
+ return `${REF_PREFIX}/${graphName}/cursor/active`;
248
+ }
249
+
250
+ /**
251
+ * Builds a saved (named) cursor ref path for the given graph and cursor name.
252
+ *
253
+ * Saved cursors are bookmarks created by `git warp seek --save <name>`.
254
+ * Each saved cursor persists a time-travel position that can be restored
255
+ * later without re-seeking.
256
+ *
257
+ * The cursor name is validated with the same rules as a writer ID
258
+ * (ASCII ref-safe: `[A-Za-z0-9._-]`, 1-64 characters).
259
+ *
260
+ * @param {string} graphName - The name of the graph
261
+ * @param {string} name - The cursor bookmark name (validated like a writer ID)
262
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/cursor/saved/<name>`
263
+ * @throws {Error} If graphName or name is invalid
264
+ *
265
+ * @example
266
+ * buildCursorSavedRef('events', 'before-tui');
267
+ * // => 'refs/warp/events/cursor/saved/before-tui'
268
+ */
269
+ export function buildCursorSavedRef(graphName, name) {
270
+ validateGraphName(graphName);
271
+ validateWriterId(name);
272
+ return `${REF_PREFIX}/${graphName}/cursor/saved/${name}`;
273
+ }
274
+
275
+ /**
276
+ * Builds the saved cursor prefix path for the given graph.
277
+ * Useful for listing all saved cursor bookmarks under a graph
278
+ * (e.g. via `git for-each-ref`).
279
+ *
280
+ * @param {string} graphName - The name of the graph
281
+ * @returns {string} The saved cursor prefix path (with trailing slash),
282
+ * e.g. `refs/warp/<graphName>/cursor/saved/`
283
+ * @throws {Error} If graphName is invalid
284
+ *
285
+ * @example
286
+ * buildCursorSavedPrefix('events');
287
+ * // => 'refs/warp/events/cursor/saved/'
288
+ */
289
+ export function buildCursorSavedPrefix(graphName) {
290
+ validateGraphName(graphName);
291
+ return `${REF_PREFIX}/${graphName}/cursor/saved/`;
292
+ }
293
+
294
+ /**
295
+ * Builds the seek cache ref path for the given graph.
296
+ *
297
+ * The seek cache ref points to a blob containing a JSON index of
298
+ * cached materialization states, keyed by (ceiling, frontier) tuples.
299
+ *
300
+ * @param {string} graphName - The name of the graph
301
+ * @returns {string} The full ref path, e.g. `refs/warp/<graphName>/seek-cache`
302
+ * @throws {Error} If graphName is invalid
303
+ *
304
+ * @example
305
+ * buildSeekCacheRef('events');
306
+ * // => 'refs/warp/events/seek-cache'
307
+ */
308
+ export function buildSeekCacheRef(graphName) {
309
+ validateGraphName(graphName);
310
+ return `${REF_PREFIX}/${graphName}/seek-cache`;
311
+ }
312
+
222
313
  // -----------------------------------------------------------------------------
223
314
  // Parsers
224
315
  // -----------------------------------------------------------------------------
@@ -178,7 +178,7 @@ export async function resolveWriterId({ graphName, explicitWriterId, configGet,
178
178
  try {
179
179
  existing = await configGet(key);
180
180
  } catch (e) {
181
- throw new WriterIdError('CONFIG_READ_FAILED', `Failed to read git config key ${key}`, e);
181
+ throw new WriterIdError('CONFIG_READ_FAILED', `Failed to read git config key ${key}`, /** @type {Error|undefined} */ (e));
182
182
  }
183
183
 
184
184
  if (existing) {
@@ -198,7 +198,7 @@ export async function resolveWriterId({ graphName, explicitWriterId, configGet,
198
198
  try {
199
199
  await configSet(key, fresh);
200
200
  } catch (e) {
201
- throw new WriterIdError('CONFIG_WRITE_FAILED', `Failed to persist writerId to git config key ${key}`, e);
201
+ throw new WriterIdError('CONFIG_WRITE_FAILED', `Failed to persist writerId to git config key ${key}`, /** @type {Error|undefined} */ (e));
202
202
  }
203
203
 
204
204
  return fresh;
@@ -18,20 +18,27 @@ const encoder = new Encoder({
18
18
  mapsAsObjects: true,
19
19
  });
20
20
 
21
+ /**
22
+ * Recursively sorts object keys for deterministic CBOR encoding.
23
+ * @param {unknown} value - The value to sort keys of
24
+ * @returns {unknown} The value with sorted keys
25
+ */
21
26
  function sortKeys(value) {
22
27
  if (value === null || value === undefined) { return value; }
23
28
  if (Array.isArray(value)) { return value.map(sortKeys); }
24
29
  if (value instanceof Map) {
30
+ /** @type {Record<string, unknown>} */
25
31
  const sorted = {};
26
32
  for (const key of Array.from(value.keys()).sort()) {
27
33
  sorted[key] = sortKeys(value.get(key));
28
34
  }
29
35
  return sorted;
30
36
  }
31
- if (typeof value === 'object' && (value.constructor === Object || value.constructor === undefined)) {
37
+ if (typeof value === 'object' && (/** @type {Object} */ (value).constructor === Object || /** @type {Object} */ (value).constructor === undefined)) {
38
+ /** @type {Record<string, unknown>} */
32
39
  const sorted = {};
33
40
  for (const key of Object.keys(value).sort()) {
34
- sorted[key] = sortKeys(value[key]);
41
+ sorted[key] = sortKeys(/** @type {Record<string, unknown>} */ (value)[key]);
35
42
  }
36
43
  return sorted;
37
44
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Default crypto implementation for domain services.
3
+ *
4
+ * Provides SHA hashing, HMAC, and timing-safe comparison using
5
+ * node:crypto directly, avoiding concrete adapter imports from
6
+ * the infrastructure layer. This follows the same pattern as
7
+ * defaultCodec.js and defaultClock.js.
8
+ *
9
+ * Since git-warp requires Git (and therefore Node 22+, Deno, or Bun),
10
+ * node:crypto is always available.
11
+ *
12
+ * @module domain/utils/defaultCrypto
13
+ */
14
+
15
+ import {
16
+ createHash,
17
+ createHmac,
18
+ timingSafeEqual as nodeTimingSafeEqual,
19
+ } from 'node:crypto';
20
+
21
+ /** @type {import('../../ports/CryptoPort.js').default} */
22
+ const defaultCrypto = {
23
+ // eslint-disable-next-line @typescript-eslint/require-await -- async matches CryptoPort contract
24
+ async hash(algorithm, data) {
25
+ return createHash(algorithm).update(data).digest('hex');
26
+ },
27
+ // eslint-disable-next-line @typescript-eslint/require-await -- async matches CryptoPort contract
28
+ async hmac(algorithm, key, data) {
29
+ return createHmac(algorithm, key).update(data).digest();
30
+ },
31
+ timingSafeEqual(a, b) {
32
+ return nodeTimingSafeEqual(a, b);
33
+ },
34
+ };
35
+
36
+ export default defaultCrypto;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Utilities for parsing seek-cursor blobs stored as Git refs.
3
+ *
4
+ * @module parseCursorBlob
5
+ */
6
+
7
+ /**
8
+ * Parses and validates a cursor blob (Buffer) into a cursor object.
9
+ *
10
+ * The blob must contain UTF-8-encoded JSON representing a plain object with at
11
+ * minimum a finite numeric `tick` field. Any additional fields (e.g. `mode`,
12
+ * `name`) are preserved in the returned object.
13
+ *
14
+ * @param {Buffer} buf - Raw blob contents (UTF-8 encoded JSON)
15
+ * @param {string} label - Human-readable label used in error messages
16
+ * (e.g. `"active cursor"`, `"saved cursor 'foo'"`)
17
+ * @returns {{ tick: number, mode?: string, [key: string]: unknown }}
18
+ * The validated cursor object. `tick` is guaranteed to be a finite number.
19
+ * @throws {Error} If `buf` is not valid JSON
20
+ * @throws {Error} If the parsed value is not a plain JSON object (e.g. array,
21
+ * null, or primitive)
22
+ * @throws {Error} If the `tick` field is missing, non-numeric, NaN, or
23
+ * Infinity
24
+ *
25
+ * @example
26
+ * const buf = Buffer.from('{"tick":5,"mode":"lamport"}', 'utf8');
27
+ * const cursor = parseCursorBlob(buf, 'active cursor');
28
+ * // => { tick: 5, mode: 'lamport' }
29
+ *
30
+ * @example
31
+ * // Throws: "Corrupted active cursor: blob is not valid JSON"
32
+ * parseCursorBlob(Buffer.from('not json', 'utf8'), 'active cursor');
33
+ */
34
+ export function parseCursorBlob(buf, label) {
35
+ let obj;
36
+ try {
37
+ obj = JSON.parse(new TextDecoder().decode(buf));
38
+ } catch {
39
+ throw new Error(`Corrupted ${label}: blob is not valid JSON`);
40
+ }
41
+
42
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
43
+ throw new Error(`Corrupted ${label}: expected a JSON object`);
44
+ }
45
+
46
+ if (typeof obj.tick !== 'number' || !Number.isFinite(obj.tick)) {
47
+ throw new Error(`Corrupted ${label}: missing or invalid numeric tick`);
48
+ }
49
+
50
+ return obj;
51
+ }
@@ -32,7 +32,7 @@ const NOT_CHECKED = Symbol('NOT_CHECKED');
32
32
 
33
33
  /**
34
34
  * Cached reference to the loaded roaring module.
35
- * @type {Object|null}
35
+ * @type {any} // TODO(ts-cleanup): type lazy singleton
36
36
  * @private
37
37
  */
38
38
  let roaringModule = null;
@@ -51,7 +51,7 @@ let nativeAvailability = NOT_CHECKED;
51
51
  * Uses a top-level-await-friendly pattern with dynamic import.
52
52
  * The module is cached after first load.
53
53
  *
54
- * @returns {Object} The roaring module exports
54
+ * @returns {any} The roaring module exports
55
55
  * @throws {Error} If the roaring package is not installed or fails to load
56
56
  * @private
57
57
  */
@@ -151,7 +151,7 @@ export function getRoaringBitmap32() {
151
151
  */
152
152
  export function getNativeRoaringAvailable() {
153
153
  if (nativeAvailability !== NOT_CHECKED) {
154
- return nativeAvailability;
154
+ return /** @type {boolean|null} */ (nativeAvailability);
155
155
  }
156
156
 
157
157
  try {
@@ -161,13 +161,13 @@ export function getNativeRoaringAvailable() {
161
161
  // Try the method-based API first (roaring >= 2.x)
162
162
  if (typeof RoaringBitmap32.isNativelyInstalled === 'function') {
163
163
  nativeAvailability = RoaringBitmap32.isNativelyInstalled();
164
- return nativeAvailability;
164
+ return /** @type {boolean|null} */ (nativeAvailability);
165
165
  }
166
166
 
167
167
  // Fall back to property-based API (roaring 1.x)
168
168
  if (roaring.isNativelyInstalled !== undefined) {
169
169
  nativeAvailability = roaring.isNativelyInstalled;
170
- return nativeAvailability;
170
+ return /** @type {boolean|null} */ (nativeAvailability);
171
171
  }
172
172
 
173
173
  // Could not determine - leave as null (indeterminate)
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Deterministic cache key for seek materialization cache.
3
+ *
4
+ * Key format: `v1:t<ceiling>-<frontierHash>`
5
+ * where frontierHash = hex SHA-256 of sorted writerId:tipSha pairs.
6
+ *
7
+ * The `v1` prefix ensures future schema/codec changes produce distinct keys
8
+ * without needing to flush existing caches.
9
+ *
10
+ * @module domain/utils/seekCacheKey
11
+ */
12
+
13
+ import { createHash } from 'node:crypto';
14
+
15
+ const KEY_VERSION = 'v1';
16
+
17
+ /**
18
+ * Builds a deterministic, collision-resistant cache key from a ceiling tick
19
+ * and writer frontier snapshot.
20
+ *
21
+ * @param {number} ceiling - Lamport ceiling tick
22
+ * @param {Map<string, string>} frontier - Map of writerId → tip SHA
23
+ * @returns {string} Cache key, e.g. `v1:t42-a1b2c3d4...` (32+ hex chars in hash)
24
+ */
25
+ export function buildSeekCacheKey(ceiling, frontier) {
26
+ const sorted = [...frontier.entries()].sort((a, b) =>
27
+ a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0
28
+ );
29
+ const payload = sorted.map(([w, sha]) => `${w}:${sha}`).join('\n');
30
+ const hash = createHash('sha256').update(payload).digest('hex');
31
+ return `${KEY_VERSION}:t${ceiling}-${hash}`;
32
+ }
@@ -21,7 +21,7 @@ export class PatchSession {
21
21
  *
22
22
  * @param {Object} options
23
23
  * @param {import('../services/PatchBuilderV2.js').PatchBuilderV2} options.builder - Internal builder
24
- * @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git adapter
24
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/RefPort.js').default} options.persistence - Git adapter
25
25
  * @param {string} options.graphName - Graph namespace
26
26
  * @param {string} options.writerId - Writer ID
27
27
  * @param {string|null} options.expectedOldHead - Expected parent SHA for CAS
@@ -30,7 +30,7 @@ export class PatchSession {
30
30
  /** @type {import('../services/PatchBuilderV2.js').PatchBuilderV2} */
31
31
  this._builder = builder;
32
32
 
33
- /** @type {import('../../ports/GraphPersistencePort.js').default} */
33
+ /** @type {import('../../ports/GraphPersistencePort.js').default & import('../../ports/RefPort.js').default} */
34
34
  this._persistence = persistence;
35
35
 
36
36
  /** @type {string} */
@@ -176,7 +176,7 @@ export class PatchSession {
176
176
  const sha = await this._builder.commit();
177
177
  this._committed = true;
178
178
  return sha;
179
- } catch (err) {
179
+ } catch (/** @type {any} */ err) { // TODO(ts-cleanup): type error
180
180
  // Check if it's a concurrent commit error from PatchBuilderV2
181
181
  if (err.message?.includes('Concurrent commit detected') ||
182
182
  err.message?.includes('has advanced')) {
@@ -36,7 +36,7 @@ export class Writer {
36
36
  * Creates a new Writer instance.
37
37
  *
38
38
  * @param {Object} options
39
- * @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git adapter
39
+ * @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/RefPort.js').default & import('../../ports/CommitPort.js').default} options.persistence - Git adapter
40
40
  * @param {string} options.graphName - Graph namespace
41
41
  * @param {string} options.writerId - This writer's ID
42
42
  * @param {import('../crdt/VersionVector.js').VersionVector} options.versionVector - Current version vector
@@ -48,7 +48,7 @@ export class Writer {
48
48
  constructor({ persistence, graphName, writerId, versionVector, getCurrentState, onCommitSuccess, onDeleteWithData = 'warn', codec }) {
49
49
  validateWriterId(writerId);
50
50
 
51
- /** @type {import('../../ports/GraphPersistencePort.js').default} */
51
+ /** @type {import('../../ports/GraphPersistencePort.js').default & import('../../ports/RefPort.js').default & import('../../ports/CommitPort.js').default} */
52
52
  this._persistence = persistence;
53
53
 
54
54
  /** @type {string} */
@@ -50,9 +50,10 @@ async function readStreamBody(bodyStream) {
50
50
  * HttpServerPort request handlers.
51
51
  *
52
52
  * @param {Request} request - Bun fetch Request
53
- * @returns {Promise<{ method: string, url: string, headers: Object, body: Buffer|undefined }>}
53
+ * @returns {Promise<{ method: string, url: string, headers: Record<string, string>, body: Uint8Array|undefined }>}
54
54
  */
55
55
  async function toPortRequest(request) {
56
+ /** @type {Record<string, string>} */
56
57
  const headers = {};
57
58
  request.headers.forEach((value, key) => {
58
59
  headers[key] = value;
@@ -81,11 +82,11 @@ async function toPortRequest(request) {
81
82
  /**
82
83
  * Converts a plain-object port response into a Bun Response.
83
84
  *
84
- * @param {{ status?: number, headers?: Object, body?: string|Uint8Array }} portResponse
85
+ * @param {{ status?: number, headers?: Record<string, string>, body?: string|Uint8Array|null }} portResponse
85
86
  * @returns {Response}
86
87
  */
87
88
  function toResponse(portResponse) {
88
- return new Response(portResponse.body ?? null, {
89
+ return new Response(/** @type {BodyInit | null} */ (portResponse.body ?? null), {
89
90
  status: portResponse.status || 200,
90
91
  headers: portResponse.headers || {},
91
92
  });
@@ -105,7 +106,7 @@ function createFetchHandler(requestHandler, logger) {
105
106
  const portReq = await toPortRequest(request);
106
107
  const portRes = await requestHandler(portReq);
107
108
  return toResponse(portRes);
108
- } catch (err) {
109
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
109
110
  if (err.status === 413) {
110
111
  return new Response(PAYLOAD_TOO_LARGE, {
111
112
  status: 413,
@@ -131,11 +132,12 @@ function createFetchHandler(requestHandler, logger) {
131
132
  * Note: Bun.serve() is synchronous, so cb fires on the same tick
132
133
  * (unlike Node's server.listen which defers via the event loop).
133
134
  *
134
- * @param {{ port: number, hostname?: string, fetch: Function }} serveOptions
135
+ * @param {*} serveOptions
135
136
  * @param {Function|undefined} cb - Node-style callback
136
- * @returns {Object} The Bun server instance
137
+ * @returns {*} The Bun server instance
137
138
  */
138
139
  function startServer(serveOptions, cb) {
140
+ // @ts-expect-error — Bun global is only available in Bun runtime
139
141
  const server = globalThis.Bun.serve(serveOptions);
140
142
  if (cb) {
141
143
  cb(null);
@@ -146,7 +148,7 @@ function startServer(serveOptions, cb) {
146
148
  /**
147
149
  * Safely stops a Bun server, forwarding errors to the callback.
148
150
  *
149
- * @param {{ server: Object|null }} state - Shared mutable state
151
+ * @param {{ server: * }} state - Shared mutable state
150
152
  * @param {Function} [callback]
151
153
  */
152
154
  function stopServer(state, callback) {
@@ -184,15 +186,25 @@ export default class BunHttpAdapter extends HttpServerPort {
184
186
  this._logger = logger || noopLogger;
185
187
  }
186
188
 
187
- /** @inheritdoc */
189
+ /**
190
+ * @param {Function} requestHandler
191
+ * @returns {{ listen: Function, close: Function, address: Function }}
192
+ */
188
193
  createServer(requestHandler) {
189
194
  const fetchHandler = createFetchHandler(requestHandler, this._logger);
195
+ /** @type {{ server: * }} */
190
196
  const state = { server: null };
191
197
 
192
198
  return {
199
+ /**
200
+ * @param {number} port
201
+ * @param {string|Function} [host]
202
+ * @param {Function} [callback]
203
+ */
193
204
  listen(port, host, callback) {
194
205
  const cb = typeof host === 'function' ? host : callback;
195
206
  const bindHost = typeof host === 'string' ? host : undefined;
207
+ /** @type {*} */ // TODO(ts-cleanup): type Bun.serve options
196
208
  const serveOptions = { port, fetch: fetchHandler };
197
209
 
198
210
  if (bindHost !== undefined) {
@@ -208,6 +220,7 @@ export default class BunHttpAdapter extends HttpServerPort {
208
220
  }
209
221
  },
210
222
 
223
+ /** @param {Function} [callback] */
211
224
  close: (callback) => stopServer(state, callback),
212
225
 
213
226
  address() {