@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
@@ -0,0 +1,311 @@
1
+ /**
2
+ * CAS-backed seek materialization cache adapter.
3
+ *
4
+ * Implements SeekCachePort using @git-stunts/git-cas for persistent storage
5
+ * of serialized WarpStateV5 snapshots. Each cached state is stored as a CAS
6
+ * asset (chunked blobs + manifest tree), and an index ref tracks the mapping
7
+ * from cache keys to tree OIDs.
8
+ *
9
+ * Index ref: `refs/warp/<graphName>/seek-cache` → blob containing JSON index.
10
+ *
11
+ * Blobs are loose Git objects — `git gc` prunes them using the configured
12
+ * prune expiry (default ~2 weeks). Use vault pinning for GC-safe persistence.
13
+ *
14
+ * **Requires Node >= 22.0.0** (inherited from `@git-stunts/git-cas`).
15
+ *
16
+ * @module infrastructure/adapters/CasSeekCacheAdapter
17
+ */
18
+
19
+ import SeekCachePort from '../../ports/SeekCachePort.js';
20
+ import { buildSeekCacheRef } from '../../domain/utils/RefLayout.js';
21
+ import { Readable } from 'node:stream';
22
+
23
+ const DEFAULT_MAX_ENTRIES = 200;
24
+ const INDEX_SCHEMA_VERSION = 1;
25
+ const MAX_CAS_RETRIES = 3;
26
+
27
+ /**
28
+ * @typedef {Object} IndexEntry
29
+ * @property {string} treeOid - Git tree OID of the CAS asset
30
+ * @property {string} createdAt - ISO 8601 timestamp
31
+ * @property {number} ceiling - Lamport ceiling tick
32
+ * @property {string} frontierHash - Hex hash portion of the cache key
33
+ * @property {number} sizeBytes - Serialized state size in bytes
34
+ * @property {string} codec - Codec identifier (e.g. 'cbor-v1')
35
+ * @property {number} schemaVersion - Index entry schema version
36
+ * @property {string} [lastAccessedAt] - ISO 8601 timestamp of last read (for LRU eviction)
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} CacheIndex
41
+ * @property {number} schemaVersion - Index-level schema version
42
+ * @property {Record<string, IndexEntry>} entries - Map of cacheKey → entry
43
+ */
44
+
45
+ export default class CasSeekCacheAdapter extends SeekCachePort {
46
+ /**
47
+ * @param {{ persistence: *, plumbing: *, graphName: string, maxEntries?: number }} options
48
+ */
49
+ constructor({ persistence, plumbing, graphName, maxEntries }) {
50
+ super();
51
+ this._persistence = persistence;
52
+ this._plumbing = plumbing;
53
+ this._graphName = graphName;
54
+ this._maxEntries = maxEntries ?? DEFAULT_MAX_ENTRIES;
55
+ this._ref = buildSeekCacheRef(graphName);
56
+ this._casPromise = null;
57
+ }
58
+
59
+ /**
60
+ * Lazily initializes the ContentAddressableStore.
61
+ * @private
62
+ * @returns {Promise<*>}
63
+ */
64
+ async _getCas() {
65
+ if (!this._casPromise) {
66
+ this._casPromise = this._initCas().catch((err) => {
67
+ this._casPromise = null;
68
+ throw err;
69
+ });
70
+ }
71
+ return await this._casPromise;
72
+ }
73
+
74
+ /**
75
+ * @private
76
+ * @returns {Promise<*>}
77
+ */
78
+ async _initCas() {
79
+ const { default: ContentAddressableStore } = await import(
80
+ /* webpackIgnore: true */ '@git-stunts/git-cas'
81
+ );
82
+ return ContentAddressableStore.createCbor({ plumbing: this._plumbing });
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Index management
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Reads the current cache index from the ref.
91
+ * @private
92
+ * @returns {Promise<CacheIndex>}
93
+ */
94
+ async _readIndex() {
95
+ const oid = await this._persistence.readRef(this._ref);
96
+ if (!oid) {
97
+ return { schemaVersion: INDEX_SCHEMA_VERSION, entries: {} };
98
+ }
99
+ try {
100
+ const buf = await this._persistence.readBlob(oid);
101
+ const parsed = JSON.parse(buf.toString('utf8'));
102
+ if (parsed.schemaVersion !== INDEX_SCHEMA_VERSION) {
103
+ return { schemaVersion: INDEX_SCHEMA_VERSION, entries: {} };
104
+ }
105
+ return parsed;
106
+ } catch {
107
+ return { schemaVersion: INDEX_SCHEMA_VERSION, entries: {} };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Writes the cache index blob and updates the ref.
113
+ * @private
114
+ * @param {CacheIndex} index - The index to write
115
+ * @returns {Promise<void>}
116
+ */
117
+ async _writeIndex(index) {
118
+ const json = JSON.stringify(index);
119
+ const oid = await this._persistence.writeBlob(Buffer.from(json, 'utf8'));
120
+ await this._persistence.updateRef(this._ref, oid);
121
+ }
122
+
123
+ /**
124
+ * Mutates the index with retry on write failure.
125
+ *
126
+ * Note: this adapter is single-writer — concurrent index mutations from
127
+ * separate processes may lose updates. The retry loop handles transient
128
+ * I/O errors (e.g. temporary lock contention), not true CAS conflicts.
129
+ *
130
+ * @private
131
+ * @param {function(CacheIndex): CacheIndex} mutate - Mutation function applied to current index
132
+ * @returns {Promise<CacheIndex>} The mutated index
133
+ */
134
+ async _mutateIndex(mutate) {
135
+ /** @type {*} */ // TODO(ts-cleanup): type CAS retry error
136
+ let lastErr;
137
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
138
+ const index = await this._readIndex();
139
+ const mutated = mutate(index);
140
+ try {
141
+ await this._writeIndex(mutated);
142
+ return mutated;
143
+ } catch (err) {
144
+ lastErr = err;
145
+ // Transient write failure — retry with fresh read
146
+ if (attempt === MAX_CAS_RETRIES - 1) {
147
+ throw new Error(`CasSeekCacheAdapter: index update failed after retries: ${lastErr.message}`);
148
+ }
149
+ }
150
+ }
151
+ /* c8 ignore next - unreachable */
152
+ throw new Error('CasSeekCacheAdapter: index update failed');
153
+ }
154
+
155
+ /**
156
+ * Evicts oldest entries when index exceeds maxEntries.
157
+ * @private
158
+ * @param {CacheIndex} index
159
+ * @returns {CacheIndex}
160
+ */
161
+ _enforceMaxEntries(index) {
162
+ const keys = Object.keys(index.entries);
163
+ if (keys.length <= this._maxEntries) {
164
+ return index;
165
+ }
166
+ // Sort by last access (or creation) ascending — evict least recently used
167
+ const sorted = keys.sort((a, b) => {
168
+ const ea = index.entries[a];
169
+ const eb = index.entries[b];
170
+ const ta = ea.lastAccessedAt || ea.createdAt || '';
171
+ const tb = eb.lastAccessedAt || eb.createdAt || '';
172
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
173
+ });
174
+ const toEvict = sorted.slice(0, keys.length - this._maxEntries);
175
+ for (const k of toEvict) {
176
+ delete index.entries[k];
177
+ }
178
+ return index;
179
+ }
180
+
181
+ /**
182
+ * Parses ceiling and frontierHash from a versioned cache key.
183
+ * @private
184
+ * @param {string} key - e.g. 'v1:t42-abcdef...'
185
+ * @returns {{ ceiling: number, frontierHash: string }}
186
+ */
187
+ _parseKey(key) {
188
+ const colonIdx = key.indexOf(':');
189
+ const rest = colonIdx >= 0 ? key.slice(colonIdx + 1) : key;
190
+ const dashIdx = rest.indexOf('-');
191
+ const ceiling = parseInt(rest.slice(1, dashIdx), 10);
192
+ const frontierHash = rest.slice(dashIdx + 1);
193
+ return { ceiling, frontierHash };
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // SeekCachePort implementation
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /**
201
+ * @override
202
+ * @param {string} key
203
+ * @returns {Promise<Buffer|null>}
204
+ */
205
+ async get(key) {
206
+ const cas = await this._getCas();
207
+ const index = await this._readIndex();
208
+ const entry = index.entries[key];
209
+ if (!entry) {
210
+ return null;
211
+ }
212
+
213
+ try {
214
+ const manifest = await cas.readManifest({ treeOid: entry.treeOid });
215
+ const { buffer } = await cas.restore({ manifest });
216
+ // Update lastAccessedAt for LRU eviction ordering
217
+ await this._mutateIndex((idx) => {
218
+ if (idx.entries[key]) {
219
+ idx.entries[key].lastAccessedAt = new Date().toISOString();
220
+ }
221
+ return idx;
222
+ });
223
+ return buffer;
224
+ } catch {
225
+ // Blob GC'd or corrupted — self-heal by removing dead entry
226
+ await this._mutateIndex((idx) => {
227
+ delete idx.entries[key];
228
+ return idx;
229
+ });
230
+ return null;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * @override
236
+ * @param {string} key
237
+ * @param {Buffer} buffer
238
+ * @returns {Promise<void>}
239
+ */
240
+ async set(key, buffer) {
241
+ const cas = await this._getCas();
242
+ const { ceiling, frontierHash } = this._parseKey(key);
243
+
244
+ // Store buffer as CAS asset
245
+ const source = Readable.from([buffer]);
246
+ const manifest = await cas.store({
247
+ source,
248
+ slug: key,
249
+ filename: 'state.cbor',
250
+ });
251
+ const treeOid = await cas.createTree({ manifest });
252
+
253
+ // Update index with rich metadata
254
+ await this._mutateIndex((index) => {
255
+ index.entries[key] = {
256
+ treeOid,
257
+ createdAt: new Date().toISOString(),
258
+ ceiling,
259
+ frontierHash,
260
+ sizeBytes: buffer.length,
261
+ codec: 'cbor-v1',
262
+ schemaVersion: INDEX_SCHEMA_VERSION,
263
+ };
264
+ return this._enforceMaxEntries(index);
265
+ });
266
+ }
267
+
268
+ /**
269
+ * @override
270
+ * @param {string} key
271
+ * @returns {Promise<boolean>}
272
+ */
273
+ async has(key) {
274
+ const index = await this._readIndex();
275
+ return key in index.entries;
276
+ }
277
+
278
+ /** @override */
279
+ async keys() {
280
+ const index = await this._readIndex();
281
+ return Object.keys(index.entries);
282
+ }
283
+
284
+ /**
285
+ * @override
286
+ * @param {string} key
287
+ * @returns {Promise<boolean>}
288
+ */
289
+ async delete(key) {
290
+ let existed = false;
291
+ await this._mutateIndex((index) => {
292
+ existed = key in index.entries;
293
+ delete index.entries[key];
294
+ return index;
295
+ });
296
+ return existed;
297
+ }
298
+
299
+ /**
300
+ * Removes the index ref. CAS tree/blob objects are left as loose Git
301
+ * objects and will be pruned by `git gc` (default expiry ~2 weeks).
302
+ * @override
303
+ */
304
+ async clear() {
305
+ try {
306
+ await this._persistence.deleteRef(this._ref);
307
+ } catch {
308
+ // Ref may not exist — that's fine
309
+ }
310
+ }
311
+ }
@@ -15,7 +15,7 @@ import ClockPort from '../../ports/ClockPort.js';
15
15
  export default class ClockAdapter extends ClockPort {
16
16
  /**
17
17
  * @param {object} [options]
18
- * @param {Performance} [options.performanceImpl] - Performance API implementation.
18
+ * @param {{ now(): number }} [options.performanceImpl] - Performance API implementation.
19
19
  * Defaults to `globalThis.performance`.
20
20
  */
21
21
  constructor({ performanceImpl } = {}) {
@@ -28,7 +28,7 @@ export default class ClockAdapter extends ClockPort {
28
28
  * @returns {ClockAdapter}
29
29
  */
30
30
  static node() {
31
- return new ClockAdapter({ performanceImpl: nodePerformance });
31
+ return new ClockAdapter({ performanceImpl: /** @type {{ now(): number }} */ (nodePerformance) });
32
32
  }
33
33
 
34
34
  /**
@@ -46,9 +46,10 @@ async function readStreamBody(bodyStream) {
46
46
  * HttpServerPort request handlers.
47
47
  *
48
48
  * @param {Request} request - Deno Request object
49
- * @returns {Promise<{ method: string, url: string, headers: Object, body: Uint8Array|undefined }>}
49
+ * @returns {Promise<{ method: string, url: string, headers: Record<string, string>, body: Uint8Array|undefined }>}
50
50
  */
51
51
  async function toPlainRequest(request) {
52
+ /** @type {Record<string, string>} */
52
53
  const headers = {};
53
54
  request.headers.forEach((value, key) => {
54
55
  headers[key] = value;
@@ -75,11 +76,11 @@ async function toPlainRequest(request) {
75
76
  /**
76
77
  * Converts a plain-object response from the handler into a Deno Response.
77
78
  *
78
- * @param {{ status?: number, headers?: Object, body?: string|Uint8Array }} plain
79
+ * @param {{ status?: number, headers?: Record<string, string>, body?: string|Uint8Array|null }} plain
79
80
  * @returns {Response}
80
81
  */
81
82
  function toDenoResponse(plain) {
82
- return new Response(plain.body ?? null, {
83
+ return new Response(/** @type {BodyInit | null} */ (plain.body ?? null), {
83
84
  status: plain.status || 200,
84
85
  headers: plain.headers || {},
85
86
  });
@@ -99,7 +100,7 @@ function createHandler(requestHandler, logger) {
99
100
  const plain = await toPlainRequest(request);
100
101
  const response = await requestHandler(plain);
101
102
  return toDenoResponse(response);
102
- } catch (err) {
103
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
103
104
  if (err.status === 413) {
104
105
  const msg = new TextEncoder().encode('Payload Too Large');
105
106
  return new Response(msg, {
@@ -122,7 +123,7 @@ function createHandler(requestHandler, logger) {
122
123
  /**
123
124
  * Gracefully shuts down the Deno HTTP server.
124
125
  *
125
- * @param {object} state - Shared mutable state `{ server }`
126
+ * @param {{ server: *}} state - Shared mutable state `{ server }`
126
127
  * @param {Function} [callback]
127
128
  */
128
129
  function closeImpl(state, callback) {
@@ -139,7 +140,7 @@ function closeImpl(state, callback) {
139
140
  callback();
140
141
  }
141
142
  },
142
- (err) => {
143
+ /** @param {*} err */ (err) => {
143
144
  state.server = null;
144
145
  if (callback) {
145
146
  callback(err);
@@ -151,7 +152,7 @@ function closeImpl(state, callback) {
151
152
  /**
152
153
  * Returns the server's bound address info.
153
154
  *
154
- * @param {object} state - Shared mutable state `{ server }`
155
+ * @param {{ server: * }} state - Shared mutable state `{ server }`
155
156
  * @returns {{ address: string, port: number, family: string }|null}
156
157
  */
157
158
  function addressImpl(state) {
@@ -189,17 +190,27 @@ export default class DenoHttpAdapter extends HttpServerPort {
189
190
  this._logger = logger || noopLogger;
190
191
  }
191
192
 
192
- /** @inheritdoc */
193
+ /**
194
+ * @param {Function} requestHandler
195
+ * @returns {{ listen: Function, close: Function, address: Function }}
196
+ */
193
197
  createServer(requestHandler) {
194
198
  const handler = createHandler(requestHandler, this._logger);
199
+ /** @type {{ server: * }} */
195
200
  const state = { server: null };
196
201
 
197
202
  return {
203
+ /**
204
+ * @param {number} port
205
+ * @param {string|Function} [host]
206
+ * @param {Function} [callback]
207
+ */
198
208
  listen: (port, host, callback) => {
199
209
  const cb = typeof host === 'function' ? host : callback;
200
210
  const hostname = typeof host === 'string' ? host : undefined;
201
211
 
202
212
  try {
213
+ /** @type {*} */ // TODO(ts-cleanup): type Deno.serve options
203
214
  const serveOptions = {
204
215
  port,
205
216
  onListen() {
@@ -212,8 +223,9 @@ export default class DenoHttpAdapter extends HttpServerPort {
212
223
  serveOptions.hostname = hostname;
213
224
  }
214
225
 
226
+ // @ts-expect-error — Deno global is only available in Deno runtime
215
227
  state.server = globalThis.Deno.serve(serveOptions, handler);
216
- } catch (err) {
228
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
217
229
  if (cb) {
218
230
  cb(err);
219
231
  } else {
@@ -221,6 +233,7 @@ export default class DenoHttpAdapter extends HttpServerPort {
221
233
  }
222
234
  }
223
235
  },
236
+ /** @param {Function} [callback] */
224
237
  close: (callback) => {
225
238
  closeImpl(state, callback);
226
239
  },
@@ -73,9 +73,13 @@ const TRANSIENT_ERROR_PATTERNS = [
73
73
  'connection timed out',
74
74
  ];
75
75
 
76
+ /**
77
+ * @typedef {Error & { details?: { stderr?: string, code?: number }, exitCode?: number, code?: number }} GitError
78
+ */
79
+
76
80
  /**
77
81
  * Determines if an error is transient and safe to retry.
78
- * @param {Error} error - The error to check
82
+ * @param {GitError} error - The error to check
79
83
  * @returns {boolean} True if the error is transient
80
84
  */
81
85
  function isTransientError(error) {
@@ -102,7 +106,7 @@ const DEFAULT_RETRY_OPTIONS = {
102
106
  /**
103
107
  * Extracts the exit code from a Git command error.
104
108
  * Checks multiple possible locations where the exit code may be stored.
105
- * @param {Error} err - The error object
109
+ * @param {GitError} err - The error object
106
110
  * @returns {number|undefined} The exit code if found
107
111
  */
108
112
  function getExitCode(err) {
@@ -120,7 +124,7 @@ async function refExists(execute, ref) {
120
124
  try {
121
125
  await execute({ args: ['show-ref', '--verify', '--quiet', ref] });
122
126
  return true;
123
- } catch (err) {
127
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
124
128
  if (getExitCode(err) === 1) {
125
129
  return false;
126
130
  }
@@ -164,11 +168,6 @@ async function refExists(execute, ref) {
164
168
  * synchronization, and the retry logic handles lock contention gracefully.
165
169
  *
166
170
  * @extends GraphPersistencePort
167
- * @implements {CommitPort}
168
- * @implements {BlobPort}
169
- * @implements {TreePort}
170
- * @implements {RefPort}
171
- * @implements {ConfigPort}
172
171
  * @see {@link GraphPersistencePort} for the abstract interface contract
173
172
  * @see {@link DEFAULT_RETRY_OPTIONS} for retry configuration details
174
173
  *
@@ -198,19 +197,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
198
197
  /**
199
198
  * Creates a new GitGraphAdapter instance.
200
199
  *
201
- * @param {Object} options - Configuration options
202
- * @param {import('@git-stunts/plumbing').default} options.plumbing - The Git plumbing
203
- * instance to use for executing Git commands. Must be initialized with a valid
204
- * repository path.
205
- * @param {import('@git-stunts/alfred').RetryOptions} [options.retryOptions={}] - Custom
206
- * retry options to override the defaults. Useful for tuning retry behavior based
207
- * on deployment environment:
208
- * - `retries` (number): Maximum retry attempts (default: 3)
209
- * - `delay` (number): Initial delay in ms (default: 100)
210
- * - `maxDelay` (number): Maximum delay cap in ms (default: 2000)
211
- * - `backoff` ('exponential'|'linear'|'constant'): Backoff strategy
212
- * - `jitter` ('full'|'decorrelated'|'none'): Jitter strategy
213
- * - `shouldRetry` (function): Custom predicate for retryable errors
200
+ * @param {{ plumbing: *, retryOptions?: Object }} options - Configuration options
214
201
  *
215
202
  * @throws {Error} If plumbing is not provided
216
203
  *
@@ -447,6 +434,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
447
434
  */
448
435
  async readTree(treeOid) {
449
436
  const oids = await this.readTreeOids(treeOid);
437
+ /** @type {Record<string, Buffer>} */
450
438
  const files = {};
451
439
  // Process sequentially to avoid spawning thousands of concurrent readBlob calls
452
440
  for (const [path, oid] of Object.entries(oids)) {
@@ -468,6 +456,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
468
456
  args: ['ls-tree', '-r', '-z', treeOid]
469
457
  });
470
458
 
459
+ /** @type {Record<string, string>} */
471
460
  const oids = {};
472
461
  // NUL-separated records: "mode type oid\tpath\0"
473
462
  const records = output.split('\0');
@@ -534,7 +523,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
534
523
  args: ['rev-parse', ref]
535
524
  });
536
525
  return oid.trim();
537
- } catch (err) {
526
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
538
527
  if (getExitCode(err) === 1) {
539
528
  return null;
540
529
  }
@@ -607,7 +596,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
607
596
  try {
608
597
  await this._executeWithRetry({ args: ['cat-file', '-e', sha] });
609
598
  return true;
610
- } catch (err) {
599
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
611
600
  if (getExitCode(err) === 1) {
612
601
  return false;
613
602
  }
@@ -683,7 +672,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
683
672
  args: ['merge-base', '--is-ancestor', potentialAncestor, descendant]
684
673
  });
685
674
  return true; // Exit code 0 means it IS an ancestor
686
- } catch (err) {
675
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
687
676
  if (this._getExitCode(err) === 1) {
688
677
  return false; // Exit code 1 means it is NOT an ancestor
689
678
  }
@@ -705,7 +694,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
705
694
  });
706
695
  // Preserve empty-string values; only drop trailing newline
707
696
  return value.replace(/\n$/, '');
708
- } catch (err) {
697
+ } catch (/** @type {*} */ err) { // TODO(ts-cleanup): type error
709
698
  if (this._isConfigKeyNotFound(err)) {
710
699
  return null;
711
700
  }
@@ -757,7 +746,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
757
746
  /**
758
747
  * Extracts the exit code from a Git command error.
759
748
  * Delegates to the standalone getExitCode helper.
760
- * @param {Error} err - The error object
749
+ * @param {GitError} err - The error object
761
750
  * @returns {number|undefined} The exit code if found
762
751
  * @private
763
752
  */
@@ -768,7 +757,7 @@ export default class GitGraphAdapter extends GraphPersistencePort {
768
757
  /**
769
758
  * Checks if an error indicates a config key was not found.
770
759
  * Exit code 1 from `git config --get` means the key doesn't exist.
771
- * @param {Error} err - The error object
760
+ * @param {GitError} err - The error object
772
761
  * @returns {boolean} True if the error indicates key not found
773
762
  * @private
774
763
  */
@@ -13,19 +13,32 @@ import {
13
13
  * @extends CryptoPort
14
14
  */
15
15
  export default class NodeCryptoAdapter extends CryptoPort {
16
- /** @inheritdoc */
16
+ /**
17
+ * @param {string} algorithm
18
+ * @param {string|Buffer|Uint8Array} data
19
+ * @returns {Promise<string>}
20
+ */
17
21
  // eslint-disable-next-line @typescript-eslint/require-await -- async ensures sync throws become rejected promises
18
22
  async hash(algorithm, data) {
19
23
  return createHash(algorithm).update(data).digest('hex');
20
24
  }
21
25
 
22
- /** @inheritdoc */
26
+ /**
27
+ * @param {string} algorithm
28
+ * @param {string|Buffer|Uint8Array} key
29
+ * @param {string|Buffer|Uint8Array} data
30
+ * @returns {Promise<Buffer>}
31
+ */
23
32
  // eslint-disable-next-line @typescript-eslint/require-await -- async ensures sync throws become rejected promises
24
33
  async hmac(algorithm, key, data) {
25
34
  return createHmac(algorithm, key).update(data).digest();
26
35
  }
27
36
 
28
- /** @inheritdoc */
37
+ /**
38
+ * @param {Buffer|Uint8Array} a
39
+ * @param {Buffer|Uint8Array} b
40
+ * @returns {boolean}
41
+ */
29
42
  timingSafeEqual(a, b) {
30
43
  return nodeTimingSafeEqual(a, b);
31
44
  }
@@ -7,6 +7,9 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024;
7
7
  /**
8
8
  * Collects the request body and dispatches to the handler, returning
9
9
  * a 500 response if the handler throws.
10
+ * @param {import('node:http').IncomingMessage} req
11
+ * @param {import('node:http').ServerResponse} res
12
+ * @param {{ handler: Function, logger: { error: Function } }} options
10
13
  */
11
14
  async function dispatch(req, res, { handler, logger }) {
12
15
  try {
@@ -60,33 +63,52 @@ export default class NodeHttpAdapter extends HttpServerPort {
60
63
  this._logger = logger || noopLogger;
61
64
  }
62
65
 
63
- /** @inheritdoc */
66
+ /**
67
+ * @param {Function} requestHandler
68
+ * @returns {{ listen: Function, close: Function, address: Function }}
69
+ */
64
70
  createServer(requestHandler) {
65
71
  const logger = this._logger;
66
72
  const server = createServer((req, res) => {
67
- dispatch(req, res, { handler: requestHandler, logger }).catch((err) => {
68
- logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
69
- });
73
+ dispatch(req, res, { handler: requestHandler, logger }).catch(
74
+ /** @param {*} err */ (err) => {
75
+ logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
76
+ });
70
77
  });
71
78
 
72
79
  return {
80
+ /**
81
+ * @param {number} port
82
+ * @param {string|Function} [host]
83
+ * @param {Function} [callback]
84
+ */
73
85
  listen(port, host, callback) {
74
86
  const cb = typeof host === 'function' ? host : callback;
75
87
  const bindHost = typeof host === 'string' ? host : undefined;
88
+ /** @param {*} err */
76
89
  const onError = (err) => {
77
90
  if (cb) {
78
91
  cb(err);
79
92
  }
80
93
  };
81
94
  server.once('error', onError);
82
- const args = bindHost !== undefined ? [port, bindHost] : [port];
83
- server.listen(...args, () => {
84
- server.removeListener('error', onError);
85
- if (cb) {
86
- cb(null);
87
- }
88
- });
95
+ if (bindHost !== undefined) {
96
+ server.listen(port, bindHost, () => {
97
+ server.removeListener('error', onError);
98
+ if (cb) {
99
+ cb(null);
100
+ }
101
+ });
102
+ } else {
103
+ server.listen(port, () => {
104
+ server.removeListener('error', onError);
105
+ if (cb) {
106
+ cb(null);
107
+ }
108
+ });
109
+ }
89
110
  },
111
+ /** @param {((err?: Error) => void)} [callback] */
90
112
  close(callback) {
91
113
  server.close(callback);
92
114
  },