@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.
- package/README.md +31 -4
- package/bin/warp-graph.js +1242 -59
- package/index.d.ts +31 -0
- package/index.js +4 -0
- package/package.json +13 -3
- package/src/domain/WarpGraph.js +487 -140
- package/src/domain/crdt/LWW.js +1 -1
- package/src/domain/crdt/ORSet.js +10 -6
- package/src/domain/crdt/VersionVector.js +5 -1
- package/src/domain/errors/EmptyMessageError.js +2 -4
- package/src/domain/errors/ForkError.js +4 -0
- package/src/domain/errors/IndexError.js +4 -0
- package/src/domain/errors/OperationAbortedError.js +4 -0
- package/src/domain/errors/QueryError.js +4 -0
- package/src/domain/errors/SchemaUnsupportedError.js +4 -0
- package/src/domain/errors/ShardCorruptionError.js +2 -6
- package/src/domain/errors/ShardLoadError.js +2 -6
- package/src/domain/errors/ShardValidationError.js +2 -7
- package/src/domain/errors/StorageError.js +2 -6
- package/src/domain/errors/SyncError.js +4 -0
- package/src/domain/errors/TraversalError.js +4 -0
- package/src/domain/errors/WarpError.js +2 -4
- package/src/domain/errors/WormholeError.js +4 -0
- package/src/domain/services/AnchorMessageCodec.js +1 -4
- package/src/domain/services/BitmapIndexBuilder.js +10 -6
- package/src/domain/services/BitmapIndexReader.js +27 -21
- package/src/domain/services/BoundaryTransitionRecord.js +22 -15
- package/src/domain/services/CheckpointMessageCodec.js +1 -7
- package/src/domain/services/CheckpointSerializerV5.js +20 -19
- package/src/domain/services/CheckpointService.js +18 -18
- package/src/domain/services/CommitDagTraversalService.js +13 -1
- package/src/domain/services/DagPathFinding.js +40 -18
- package/src/domain/services/DagTopology.js +7 -6
- package/src/domain/services/DagTraversal.js +5 -3
- package/src/domain/services/Frontier.js +7 -6
- package/src/domain/services/HealthCheckService.js +15 -14
- package/src/domain/services/HookInstaller.js +64 -13
- package/src/domain/services/HttpSyncServer.js +15 -14
- package/src/domain/services/IndexRebuildService.js +12 -12
- package/src/domain/services/IndexStalenessChecker.js +13 -6
- package/src/domain/services/JoinReducer.js +28 -27
- package/src/domain/services/LogicalTraversal.js +7 -6
- package/src/domain/services/MessageCodecInternal.js +2 -0
- package/src/domain/services/ObserverView.js +6 -6
- package/src/domain/services/PatchBuilderV2.js +9 -9
- package/src/domain/services/PatchMessageCodec.js +1 -7
- package/src/domain/services/ProvenanceIndex.js +6 -8
- package/src/domain/services/ProvenancePayload.js +1 -2
- package/src/domain/services/QueryBuilder.js +29 -23
- package/src/domain/services/StateDiff.js +7 -7
- package/src/domain/services/StateSerializerV5.js +8 -6
- package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
- package/src/domain/services/SyncProtocol.js +23 -26
- package/src/domain/services/TemporalQuery.js +4 -3
- package/src/domain/services/TranslationCost.js +4 -4
- package/src/domain/services/WormholeService.js +19 -15
- package/src/domain/types/TickReceipt.js +10 -6
- package/src/domain/types/WarpTypesV2.js +2 -3
- package/src/domain/utils/CachedValue.js +1 -1
- package/src/domain/utils/LRUCache.js +3 -3
- package/src/domain/utils/MinHeap.js +2 -2
- package/src/domain/utils/RefLayout.js +106 -15
- package/src/domain/utils/WriterId.js +2 -2
- package/src/domain/utils/defaultCodec.js +9 -2
- package/src/domain/utils/defaultCrypto.js +36 -0
- package/src/domain/utils/parseCursorBlob.js +51 -0
- package/src/domain/utils/roaring.js +5 -5
- package/src/domain/utils/seekCacheKey.js +32 -0
- package/src/domain/warp/PatchSession.js +3 -3
- package/src/domain/warp/Writer.js +2 -2
- package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
- package/src/infrastructure/adapters/ClockAdapter.js +2 -2
- package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
- package/src/infrastructure/adapters/GitGraphAdapter.js +16 -27
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
- package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
- package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
- package/src/infrastructure/codecs/CborCodec.js +16 -8
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/CodecPort.js +2 -2
- package/src/ports/CommitPort.js +8 -21
- package/src/ports/ConfigPort.js +3 -3
- package/src/ports/CryptoPort.js +7 -7
- package/src/ports/GraphPersistencePort.js +12 -14
- package/src/ports/HttpServerPort.js +1 -5
- package/src/ports/IndexStoragePort.js +1 -0
- package/src/ports/LoggerPort.js +9 -9
- package/src/ports/RefPort.js +5 -5
- package/src/ports/SeekCachePort.js +73 -0
- package/src/ports/TreePort.js +3 -3
- package/src/visualization/layouts/converters.js +14 -7
- package/src/visualization/layouts/elkAdapter.js +24 -11
- package/src/visualization/layouts/elkLayout.js +23 -7
- package/src/visualization/layouts/index.js +3 -3
- package/src/visualization/renderers/ascii/check.js +30 -17
- package/src/visualization/renderers/ascii/graph.js +122 -16
- package/src/visualization/renderers/ascii/history.js +29 -90
- package/src/visualization/renderers/ascii/index.js +1 -1
- package/src/visualization/renderers/ascii/info.js +9 -7
- package/src/visualization/renderers/ascii/materialize.js +20 -16
- package/src/visualization/renderers/ascii/opSummary.js +81 -0
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +344 -0
- package/src/visualization/renderers/ascii/table.js +1 -1
- 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 {
|
|
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,
|
|
@@ -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
|
|
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');
|
|
64
|
-
* validateGraphName('
|
|
65
|
-
* validateGraphName('
|
|
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
|
|
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');
|
|
107
|
-
* validateWriterId('a/b');
|
|
108
|
-
* validateWriterId('x'.repeat(65));
|
|
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 {
|
|
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 {
|
|
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:
|
|
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?:
|
|
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 {
|
|
135
|
+
* @param {*} serveOptions
|
|
135
136
|
* @param {Function|undefined} cb - Node-style callback
|
|
136
|
-
* @returns {
|
|
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:
|
|
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
|
-
/**
|
|
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() {
|