@git-stunts/git-warp 11.2.1 → 11.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/bin/cli/commands/check.js +2 -2
- package/bin/cli/commands/doctor/checks.js +12 -12
- package/bin/cli/commands/doctor/index.js +2 -2
- package/bin/cli/commands/doctor/types.js +1 -1
- package/bin/cli/commands/history.js +12 -5
- package/bin/cli/commands/install-hooks.js +5 -5
- package/bin/cli/commands/materialize.js +2 -2
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +4 -4
- package/bin/cli/commands/query.js +54 -13
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/seek.js +17 -11
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +3 -3
- package/bin/cli/commands/verify-audit.js +8 -7
- package/bin/cli/commands/view.js +6 -5
- package/bin/cli/infrastructure.js +26 -12
- package/bin/cli/shared.js +2 -2
- package/bin/cli/types.js +19 -8
- package/bin/presenters/index.js +35 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +155 -33
- package/index.d.ts +118 -22
- package/index.js +2 -0
- package/package.json +5 -3
- package/src/domain/WarpGraph.js +4 -1
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/services/AuditReceiptService.js +6 -6
- package/src/domain/services/AuditVerifierService.js +52 -38
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +22 -3
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +92 -41
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +26 -11
- package/src/domain/services/KeyCodec.js +7 -0
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +1 -1
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +96 -30
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +3 -2
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +3 -3
- package/src/domain/trust/TrustEvaluator.js +18 -3
- package/src/domain/trust/TrustRecordService.js +30 -23
- package/src/domain/trust/TrustStateBuilder.js +21 -8
- package/src/domain/trust/canonical.js +6 -6
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +40 -18
- package/src/domain/warp/_wiredMethods.d.ts +199 -45
- package/src/domain/warp/checkpoint.methods.js +5 -1
- package/src/domain/warp/fork.methods.js +2 -2
- package/src/domain/warp/materialize.methods.js +55 -5
- package/src/domain/warp/materializeAdvanced.methods.js +15 -4
- package/src/domain/warp/patch.methods.js +54 -29
- package/src/domain/warp/provenance.methods.js +5 -3
- package/src/domain/warp/query.methods.js +89 -6
- package/src/domain/warp/sync.methods.js +16 -11
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +18 -13
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
|
@@ -25,7 +25,7 @@ import { createORSet, orsetAdd, orsetCompact } from '../crdt/ORSet.js';
|
|
|
25
25
|
import { createDot } from '../crdt/Dot.js';
|
|
26
26
|
import { createVersionVector } from '../crdt/VersionVector.js';
|
|
27
27
|
import { cloneStateV5, reduceV5 } from './JoinReducer.js';
|
|
28
|
-
import { encodeEdgeKey, encodePropKey } from './KeyCodec.js';
|
|
28
|
+
import { encodeEdgeKey, encodePropKey, CONTENT_PROPERTY_KEY, decodePropKey, isEdgePropKey, decodeEdgePropKey } from './KeyCodec.js';
|
|
29
29
|
import { ProvenanceIndex } from './ProvenanceIndex.js';
|
|
30
30
|
|
|
31
31
|
// ============================================================================
|
|
@@ -132,6 +132,20 @@ export async function createV5({
|
|
|
132
132
|
provenanceIndexBlobOid = await persistence.writeBlob(/** @type {Buffer} */ (provenanceIndexBuffer));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// 6c. Collect content blob OIDs from state properties for GC anchoring.
|
|
136
|
+
// If patch commits are ever pruned, content blobs remain reachable via
|
|
137
|
+
// the checkpoint tree. Without this, git gc would nuke content blobs
|
|
138
|
+
// whose only anchor was the (now-pruned) patch commit tree.
|
|
139
|
+
const contentOids = new Set();
|
|
140
|
+
for (const [propKey, register] of checkpointState.prop) {
|
|
141
|
+
const { propKey: decodedKey } = isEdgePropKey(propKey)
|
|
142
|
+
? decodeEdgePropKey(propKey)
|
|
143
|
+
: decodePropKey(propKey);
|
|
144
|
+
if (decodedKey === CONTENT_PROPERTY_KEY && typeof register.value === 'string') {
|
|
145
|
+
contentOids.add(register.value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
135
149
|
// 7. Create tree with sorted entries
|
|
136
150
|
const treeEntries = [
|
|
137
151
|
`100644 blob ${appliedVVBlobOid}\tappliedVV.cbor`,
|
|
@@ -145,6 +159,11 @@ export async function createV5({
|
|
|
145
159
|
treeEntries.push(`100644 blob ${provenanceIndexBlobOid}\tprovenanceIndex.cbor`);
|
|
146
160
|
}
|
|
147
161
|
|
|
162
|
+
// Add content blob anchors
|
|
163
|
+
for (const oid of contentOids) {
|
|
164
|
+
treeEntries.push(`100644 blob ${oid}\t_content_${oid}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
// Sort entries by filename for deterministic tree (git requires sorted entries by path)
|
|
149
168
|
treeEntries.sort((a, b) => {
|
|
150
169
|
const filenameA = a.split('\t')[1];
|
|
@@ -196,7 +215,7 @@ export async function createV5({
|
|
|
196
215
|
* @returns {Promise<{state: import('./JoinReducer.js').WarpStateV5, frontier: import('./Frontier.js').Frontier, stateHash: string, schema: number, appliedVV: Map<string, number>|null, provenanceIndex?: import('./ProvenanceIndex.js').ProvenanceIndex}>} The loaded checkpoint data
|
|
197
216
|
* @throws {Error} If checkpoint is schema:1 (migration required)
|
|
198
217
|
*/
|
|
199
|
-
export async function loadCheckpoint(persistence, checkpointSha, { codec } =
|
|
218
|
+
export async function loadCheckpoint(persistence, checkpointSha, { codec } = {}) {
|
|
200
219
|
// 1. Read commit message and decode
|
|
201
220
|
const message = await persistence.showNode(checkpointSha);
|
|
202
221
|
const decoded = /** @type {{ schema: number, stateHash: string, indexOid: string }} */ (decodeCheckpointMessage(message));
|
|
@@ -327,7 +346,7 @@ export async function materializeIncremental({
|
|
|
327
346
|
* @param {Object} visibleProjection - The checkpoint's visible projection
|
|
328
347
|
* @param {string[]} visibleProjection.nodes - Visible node IDs
|
|
329
348
|
* @param {Array<{from: string, to: string, label: string}>} visibleProjection.edges - Visible edges
|
|
330
|
-
* @param {Array<{node: string, key: string, value:
|
|
349
|
+
* @param {Array<{node: string, key: string, value: unknown}>} visibleProjection.props - Visible properties
|
|
331
350
|
* @returns {import('./JoinReducer.js').WarpStateV5} Reconstructed WarpStateV5
|
|
332
351
|
* @public
|
|
333
352
|
*/
|
|
@@ -39,7 +39,7 @@ export default class CommitDagTraversalService {
|
|
|
39
39
|
* @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
|
|
40
40
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
|
|
41
41
|
*/
|
|
42
|
-
constructor({ indexReader, logger = nullLogger }
|
|
42
|
+
constructor({ indexReader, logger = nullLogger }) {
|
|
43
43
|
if (!indexReader) {
|
|
44
44
|
throw new Error('CommitDagTraversalService requires an indexReader');
|
|
45
45
|
}
|
|
@@ -56,7 +56,7 @@ export default class CommitDagTraversalService {
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Breadth-first traversal from a starting node.
|
|
59
|
-
* @param {
|
|
59
|
+
* @param {Parameters<import('./DagTraversal.js').default['bfs']>[0]} options
|
|
60
60
|
* @see DagTraversal#bfs
|
|
61
61
|
*/
|
|
62
62
|
bfs(options) {
|
|
@@ -65,7 +65,7 @@ export default class CommitDagTraversalService {
|
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Depth-first pre-order traversal from a starting node.
|
|
68
|
-
* @param {
|
|
68
|
+
* @param {Parameters<import('./DagTraversal.js').default['dfs']>[0]} options
|
|
69
69
|
* @see DagTraversal#dfs
|
|
70
70
|
*/
|
|
71
71
|
dfs(options) {
|
|
@@ -74,7 +74,7 @@ export default class CommitDagTraversalService {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Yields all ancestors of a node.
|
|
77
|
-
* @param {
|
|
77
|
+
* @param {Parameters<import('./DagTraversal.js').default['ancestors']>[0]} options
|
|
78
78
|
* @see DagTraversal#ancestors
|
|
79
79
|
*/
|
|
80
80
|
ancestors(options) {
|
|
@@ -83,7 +83,7 @@ export default class CommitDagTraversalService {
|
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Yields all descendants of a node.
|
|
86
|
-
* @param {
|
|
86
|
+
* @param {Parameters<import('./DagTraversal.js').default['descendants']>[0]} options
|
|
87
87
|
* @see DagTraversal#descendants
|
|
88
88
|
*/
|
|
89
89
|
descendants(options) {
|
|
@@ -92,7 +92,7 @@ export default class CommitDagTraversalService {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* Checks if there is any path from one node to another.
|
|
95
|
-
* @param {
|
|
95
|
+
* @param {Parameters<import('./DagTraversal.js').default['isReachable']>[0]} options
|
|
96
96
|
* @see DagTraversal#isReachable
|
|
97
97
|
*/
|
|
98
98
|
isReachable(options) {
|
|
@@ -103,7 +103,7 @@ export default class CommitDagTraversalService {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Finds ANY path between two nodes using BFS.
|
|
106
|
-
* @param {
|
|
106
|
+
* @param {Parameters<import('./DagPathFinding.js').default['findPath']>[0]} options
|
|
107
107
|
* @see DagPathFinding#findPath
|
|
108
108
|
*/
|
|
109
109
|
findPath(options) {
|
|
@@ -112,7 +112,7 @@ export default class CommitDagTraversalService {
|
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Finds the shortest path using bidirectional BFS.
|
|
115
|
-
* @param {
|
|
115
|
+
* @param {Parameters<import('./DagPathFinding.js').default['shortestPath']>[0]} options
|
|
116
116
|
* @see DagPathFinding#shortestPath
|
|
117
117
|
*/
|
|
118
118
|
shortestPath(options) {
|
|
@@ -121,7 +121,7 @@ export default class CommitDagTraversalService {
|
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Finds shortest path using Dijkstra's algorithm.
|
|
124
|
-
* @param {
|
|
124
|
+
* @param {Parameters<import('./DagPathFinding.js').default['weightedShortestPath']>[0]} options
|
|
125
125
|
* @see DagPathFinding#weightedShortestPath
|
|
126
126
|
*/
|
|
127
127
|
weightedShortestPath(options) {
|
|
@@ -130,7 +130,7 @@ export default class CommitDagTraversalService {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* Finds shortest path using A* with heuristic guidance.
|
|
133
|
-
* @param {
|
|
133
|
+
* @param {Parameters<import('./DagPathFinding.js').default['aStarSearch']>[0]} options
|
|
134
134
|
* @see DagPathFinding#aStarSearch
|
|
135
135
|
*/
|
|
136
136
|
aStarSearch(options) {
|
|
@@ -139,7 +139,7 @@ export default class CommitDagTraversalService {
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Bi-directional A* search.
|
|
142
|
-
* @param {
|
|
142
|
+
* @param {Parameters<import('./DagPathFinding.js').default['bidirectionalAStar']>[0]} options
|
|
143
143
|
* @see DagPathFinding#bidirectionalAStar
|
|
144
144
|
*/
|
|
145
145
|
bidirectionalAStar(options) {
|
|
@@ -150,7 +150,7 @@ export default class CommitDagTraversalService {
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Finds common ancestors of multiple nodes.
|
|
153
|
-
* @param {
|
|
153
|
+
* @param {Parameters<import('./DagTopology.js').default['commonAncestors']>[0]} options
|
|
154
154
|
* @see DagTopology#commonAncestors
|
|
155
155
|
*/
|
|
156
156
|
commonAncestors(options) {
|
|
@@ -159,7 +159,7 @@ export default class CommitDagTraversalService {
|
|
|
159
159
|
|
|
160
160
|
/**
|
|
161
161
|
* Yields nodes in topological order using Kahn's algorithm.
|
|
162
|
-
* @param {
|
|
162
|
+
* @param {Parameters<import('./DagTopology.js').default['topologicalSort']>[0]} options
|
|
163
163
|
* @see DagTopology#topologicalSort
|
|
164
164
|
*/
|
|
165
165
|
topologicalSort(options) {
|
|
@@ -41,7 +41,7 @@ export default class DagPathFinding {
|
|
|
41
41
|
* @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
|
|
42
42
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
|
|
43
43
|
*/
|
|
44
|
-
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {
|
|
44
|
+
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {{ indexReader: import('./BitmapIndexReader.js').default }} */ ({})) {
|
|
45
45
|
if (!indexReader) {
|
|
46
46
|
throw new Error('DagPathFinding requires an indexReader');
|
|
47
47
|
}
|
|
@@ -228,7 +228,7 @@ export default class DagPathFinding {
|
|
|
228
228
|
checkAborted(signal, 'weightedShortestPath');
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
const current = pq.extractMin();
|
|
231
|
+
const current = /** @type {string} */ (pq.extractMin());
|
|
232
232
|
|
|
233
233
|
if (visited.has(current)) {
|
|
234
234
|
continue;
|
|
@@ -314,7 +314,7 @@ export default class DagPathFinding {
|
|
|
314
314
|
checkAborted(signal, 'aStarSearch');
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
const current = pq.extractMin();
|
|
317
|
+
const current = /** @type {string} */ (pq.extractMin());
|
|
318
318
|
|
|
319
319
|
if (visited.has(current)) {
|
|
320
320
|
continue;
|
|
@@ -463,7 +463,7 @@ export default class DagPathFinding {
|
|
|
463
463
|
* Expands the forward frontier by one node in bidirectional A*.
|
|
464
464
|
*
|
|
465
465
|
* @param {Object} state - Forward expansion state
|
|
466
|
-
* @param {import('../utils/MinHeap.js').default} state.fwdHeap
|
|
466
|
+
* @param {import('../utils/MinHeap.js').default<string>} state.fwdHeap
|
|
467
467
|
* @param {Set<string>} state.fwdVisited
|
|
468
468
|
* @param {Map<string, number>} state.fwdGScore
|
|
469
469
|
* @param {Map<string, string>} state.fwdPrevious
|
|
@@ -483,7 +483,7 @@ export default class DagPathFinding {
|
|
|
483
483
|
weightProvider, forwardHeuristic, to,
|
|
484
484
|
mu: inputMu, meetingPoint: inputMeeting,
|
|
485
485
|
}) {
|
|
486
|
-
const current = fwdHeap.extractMin();
|
|
486
|
+
const current = /** @type {string} */ (fwdHeap.extractMin());
|
|
487
487
|
let explored = 0;
|
|
488
488
|
let bestMu = inputMu;
|
|
489
489
|
let bestMeeting = inputMeeting;
|
|
@@ -536,7 +536,7 @@ export default class DagPathFinding {
|
|
|
536
536
|
* Expands the backward frontier by one node in bidirectional A*.
|
|
537
537
|
*
|
|
538
538
|
* @param {Object} state - Backward expansion state
|
|
539
|
-
* @param {import('../utils/MinHeap.js').default} state.bwdHeap
|
|
539
|
+
* @param {import('../utils/MinHeap.js').default<string>} state.bwdHeap
|
|
540
540
|
* @param {Set<string>} state.bwdVisited
|
|
541
541
|
* @param {Map<string, number>} state.bwdGScore
|
|
542
542
|
* @param {Map<string, string>} state.bwdNext
|
|
@@ -556,7 +556,7 @@ export default class DagPathFinding {
|
|
|
556
556
|
weightProvider, backwardHeuristic, from,
|
|
557
557
|
mu: inputMu, meetingPoint: inputMeeting,
|
|
558
558
|
}) {
|
|
559
|
-
const current = bwdHeap.extractMin();
|
|
559
|
+
const current = /** @type {string} */ (bwdHeap.extractMin());
|
|
560
560
|
let explored = 0;
|
|
561
561
|
let bestMu = inputMu;
|
|
562
562
|
let bestMeeting = inputMeeting;
|
|
@@ -37,7 +37,7 @@ export default class DagTopology {
|
|
|
37
37
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
|
|
38
38
|
* @param {import('./DagTraversal.js').default} [options.traversal] - Traversal service for ancestor enumeration
|
|
39
39
|
*/
|
|
40
|
-
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default, traversal?: import('./DagTraversal.js').default }} */ { indexReader, logger = nullLogger, traversal } = /** @type {
|
|
40
|
+
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default, traversal?: import('./DagTraversal.js').default }} */ { indexReader, logger = nullLogger, traversal } = /** @type {{ indexReader: import('./BitmapIndexReader.js').default }} */ ({})) {
|
|
41
41
|
if (!indexReader) {
|
|
42
42
|
throw new Error('DagTopology requires an indexReader');
|
|
43
43
|
}
|
|
@@ -43,7 +43,7 @@ export default class DagTraversal {
|
|
|
43
43
|
* @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
|
|
44
44
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
|
|
45
45
|
*/
|
|
46
|
-
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {
|
|
46
|
+
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {{ indexReader: import('./BitmapIndexReader.js').default }} */ ({})) {
|
|
47
47
|
if (!indexReader) {
|
|
48
48
|
throw new Error('DagTraversal requires an indexReader');
|
|
49
49
|
}
|
|
@@ -191,7 +191,7 @@ export default class HealthCheckService {
|
|
|
191
191
|
} catch (err) {
|
|
192
192
|
this._logger.warn('Repository ping failed', {
|
|
193
193
|
operation: 'checkRepository',
|
|
194
|
-
error:
|
|
194
|
+
error: err instanceof Error ? err.message : String(err),
|
|
195
195
|
});
|
|
196
196
|
return {
|
|
197
197
|
status: /** @type {'healthy'|'unhealthy'} */ (HealthStatus.UNHEALTHY),
|
|
@@ -80,7 +80,7 @@ export class HookInstaller {
|
|
|
80
80
|
* @param {string} deps.templateDir - Directory containing hook templates
|
|
81
81
|
* @param {PathUtils} deps.path - Path utilities (join and resolve)
|
|
82
82
|
*/
|
|
83
|
-
constructor({ fs, execGitConfig, version, templateDir, path }
|
|
83
|
+
constructor({ fs, execGitConfig, version, templateDir, path }) {
|
|
84
84
|
/** @type {FsAdapter} */
|
|
85
85
|
this._fs = fs;
|
|
86
86
|
/** @type {(repoPath: string, key: string) => string|null} */
|
|
@@ -8,15 +8,56 @@
|
|
|
8
8
|
* @module domain/services/HttpSyncServer
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { z } from 'zod';
|
|
11
12
|
import SyncAuthService from './SyncAuthService.js';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
const MAX_REQUEST_BYTES_CEILING = 128 * 1024 * 1024; // 134217728
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Zod schema for HttpSyncServer constructor options.
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
const authSchema = z.object({
|
|
22
|
+
mode: z.enum(['enforce', 'log-only']).default('enforce'),
|
|
23
|
+
keys: z.record(z.string()).refine(
|
|
24
|
+
(obj) => Object.keys(obj).length > 0,
|
|
25
|
+
'auth.keys must not be empty',
|
|
26
|
+
),
|
|
27
|
+
crypto: /** @type {z.ZodType<import('../../ports/CryptoPort.js').default>} */ (z.custom((v) => v === undefined || (typeof v === 'object' && v !== null))).optional(),
|
|
28
|
+
logger: /** @type {z.ZodType<import('../../ports/LoggerPort.js').default>} */ (z.custom((v) => v === undefined || (typeof v === 'object' && v !== null))).optional(),
|
|
29
|
+
wallClockMs: /** @type {z.ZodType<() => number>} */ (z.custom((v) => v === undefined || typeof v === 'function')).optional(),
|
|
30
|
+
}).strict();
|
|
31
|
+
|
|
32
|
+
const optionsSchema = z.object({
|
|
33
|
+
httpPort: /** @type {z.ZodType<import('../../ports/HttpServerPort.js').default>} */ (z.custom(
|
|
34
|
+
(v) => v !== null && v !== undefined && typeof v === 'object',
|
|
35
|
+
'httpPort must be a non-null object',
|
|
36
|
+
)),
|
|
37
|
+
graph: /** @type {z.ZodType<import('../WarpGraph.js').default>} */ (z.custom(
|
|
38
|
+
(v) => v !== null && v !== undefined && typeof v === 'object',
|
|
39
|
+
'graph must be a non-null object',
|
|
40
|
+
)),
|
|
41
|
+
maxRequestBytes: z.number().int().positive().max(MAX_REQUEST_BYTES_CEILING).default(DEFAULT_MAX_REQUEST_BYTES),
|
|
42
|
+
path: z.string().startsWith('/').default('/sync'),
|
|
43
|
+
host: z.string().min(1).default('127.0.0.1'),
|
|
44
|
+
auth: authSchema.optional(),
|
|
45
|
+
allowedWriters: z.array(z.string()).optional(),
|
|
46
|
+
}).strict().superRefine((data, ctx) => {
|
|
47
|
+
if (data.allowedWriters && data.allowedWriters.length > 0 && !data.auth) {
|
|
48
|
+
ctx.addIssue({
|
|
49
|
+
code: z.ZodIssueCode.custom,
|
|
50
|
+
message: 'allowedWriters requires auth.keys to be configured',
|
|
51
|
+
path: ['allowedWriters'],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
14
55
|
|
|
15
56
|
/**
|
|
16
57
|
* Recursively sorts object keys for deterministic JSON output.
|
|
17
58
|
*
|
|
18
|
-
* @param {
|
|
19
|
-
* @returns {
|
|
59
|
+
* @param {unknown} value - Any JSON-serializable value
|
|
60
|
+
* @returns {unknown} The canonicalized value with sorted object keys
|
|
20
61
|
* @private
|
|
21
62
|
*/
|
|
22
63
|
function canonicalizeJson(value) {
|
|
@@ -24,10 +65,10 @@ function canonicalizeJson(value) {
|
|
|
24
65
|
return value.map(canonicalizeJson);
|
|
25
66
|
}
|
|
26
67
|
if (value && typeof value === 'object') {
|
|
27
|
-
/** @type {{ [x: string]:
|
|
68
|
+
/** @type {{ [x: string]: unknown }} */
|
|
28
69
|
const sorted = {};
|
|
29
70
|
for (const key of Object.keys(value).sort()) {
|
|
30
|
-
sorted[key] = canonicalizeJson(/** @type {{ [x: string]:
|
|
71
|
+
sorted[key] = canonicalizeJson(/** @type {{ [x: string]: unknown }} */ (value)[key]);
|
|
31
72
|
}
|
|
32
73
|
return sorted;
|
|
33
74
|
}
|
|
@@ -37,7 +78,7 @@ function canonicalizeJson(value) {
|
|
|
37
78
|
/**
|
|
38
79
|
* Produces a canonical JSON string with sorted keys.
|
|
39
80
|
*
|
|
40
|
-
* @param {
|
|
81
|
+
* @param {unknown} value - Any JSON-serializable value
|
|
41
82
|
* @returns {string} Canonical JSON string
|
|
42
83
|
* @private
|
|
43
84
|
*/
|
|
@@ -64,7 +105,7 @@ function errorResponse(status, message) {
|
|
|
64
105
|
/**
|
|
65
106
|
* Builds a JSON success response with canonical key ordering.
|
|
66
107
|
*
|
|
67
|
-
* @param {
|
|
108
|
+
* @param {unknown} data - Response payload
|
|
68
109
|
* @returns {{ status: number, headers: Object, body: string }}
|
|
69
110
|
* @private
|
|
70
111
|
*/
|
|
@@ -79,7 +120,7 @@ function jsonResponse(data) {
|
|
|
79
120
|
/**
|
|
80
121
|
* Validates that a sync request object has the expected shape.
|
|
81
122
|
*
|
|
82
|
-
* @param {
|
|
123
|
+
* @param {unknown} parsed - Parsed JSON body
|
|
83
124
|
* @returns {boolean} True if valid
|
|
84
125
|
* @private
|
|
85
126
|
*/
|
|
@@ -87,10 +128,11 @@ function isValidSyncRequest(parsed) {
|
|
|
87
128
|
if (!parsed || typeof parsed !== 'object') {
|
|
88
129
|
return false;
|
|
89
130
|
}
|
|
90
|
-
|
|
131
|
+
const rec = /** @type {Record<string, unknown>} */ (parsed);
|
|
132
|
+
if (rec.type !== 'sync-request') {
|
|
91
133
|
return false;
|
|
92
134
|
}
|
|
93
|
-
if (!
|
|
135
|
+
if (!rec.frontier || typeof rec.frontier !== 'object' || Array.isArray(rec.frontier)) {
|
|
94
136
|
return false;
|
|
95
137
|
}
|
|
96
138
|
return true;
|
|
@@ -160,7 +202,7 @@ function checkBodySize(body, maxBytes) {
|
|
|
160
202
|
* Parses and validates the request body as a sync request.
|
|
161
203
|
*
|
|
162
204
|
* @param {Buffer|undefined} body
|
|
163
|
-
* @returns {{ error: { status: number, headers: Object, body: string }, parsed: null } | { error: null, parsed:
|
|
205
|
+
* @returns {{ error: { status: number, headers: Object, body: string }, parsed: null } | { error: null, parsed: import('./SyncProtocol.js').SyncRequest }}
|
|
164
206
|
* @private
|
|
165
207
|
*/
|
|
166
208
|
function parseBody(body) {
|
|
@@ -183,19 +225,14 @@ function parseBody(body) {
|
|
|
183
225
|
/**
|
|
184
226
|
* Initializes auth service from config if present.
|
|
185
227
|
*
|
|
186
|
-
* @param {
|
|
228
|
+
* @param {z.infer<typeof authSchema>} [auth]
|
|
187
229
|
* @param {string[]} [allowedWriters]
|
|
188
230
|
* @returns {{ auth: SyncAuthService|null, authMode: string|null }}
|
|
189
231
|
* @private
|
|
190
232
|
*/
|
|
191
233
|
function initAuth(auth, allowedWriters) {
|
|
192
|
-
if (auth
|
|
193
|
-
|
|
194
|
-
const mode = auth.mode || 'enforce';
|
|
195
|
-
if (!VALID_MODES.has(mode)) {
|
|
196
|
-
throw new Error(`Invalid auth.mode: '${mode}'. Must be 'enforce' or 'log-only'.`);
|
|
197
|
-
}
|
|
198
|
-
return { auth: new SyncAuthService({ ...auth, allowedWriters }), authMode: mode };
|
|
234
|
+
if (auth) {
|
|
235
|
+
return { auth: new SyncAuthService({ ...auth, allowedWriters }), authMode: auth.mode };
|
|
199
236
|
}
|
|
200
237
|
return { auth: null, authMode: null };
|
|
201
238
|
}
|
|
@@ -204,26 +241,35 @@ export default class HttpSyncServer {
|
|
|
204
241
|
/**
|
|
205
242
|
* @param {Object} options
|
|
206
243
|
* @param {import('../../ports/HttpServerPort.js').default} options.httpPort - HTTP server port abstraction
|
|
207
|
-
* @param {{ processSyncRequest:
|
|
244
|
+
* @param {{ processSyncRequest: Function }} options.graph - WarpGraph instance (must expose processSyncRequest)
|
|
208
245
|
* @param {string} [options.path='/sync'] - URL path to handle sync requests on
|
|
209
246
|
* @param {string} [options.host='127.0.0.1'] - Host to bind
|
|
210
247
|
* @param {number} [options.maxRequestBytes=4194304] - Maximum request body size in bytes
|
|
211
248
|
* @param {{ keys: Record<string, string>, mode?: 'enforce'|'log-only', crypto?: import('../../ports/CryptoPort.js').default, logger?: import('../../ports/LoggerPort.js').default, wallClockMs?: () => number }} [options.auth] - Auth configuration
|
|
212
249
|
* @param {string[]} [options.allowedWriters] - Optional whitelist of allowed writer IDs
|
|
213
250
|
*/
|
|
214
|
-
constructor(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
251
|
+
constructor(options) {
|
|
252
|
+
/** @type {z.infer<typeof optionsSchema>} */
|
|
253
|
+
let parsed;
|
|
254
|
+
try {
|
|
255
|
+
parsed = optionsSchema.parse(options);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (err instanceof z.ZodError) {
|
|
258
|
+
const messages = err.issues.map((i) => i.message).join('; ');
|
|
259
|
+
throw new Error(`HttpSyncServer config: ${messages}`);
|
|
260
|
+
}
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this._httpPort = parsed.httpPort;
|
|
265
|
+
this._graph = parsed.graph;
|
|
266
|
+
this._path = parsed.path;
|
|
267
|
+
this._host = parsed.host;
|
|
268
|
+
this._maxRequestBytes = parsed.maxRequestBytes;
|
|
220
269
|
this._server = null;
|
|
221
|
-
const authInit = initAuth(auth, allowedWriters);
|
|
270
|
+
const authInit = initAuth(parsed.auth, parsed.allowedWriters);
|
|
222
271
|
this._auth = authInit.auth;
|
|
223
272
|
this._authMode = authInit.authMode;
|
|
224
|
-
if (allowedWriters && !authInit.auth) {
|
|
225
|
-
throw new Error('allowedWriters requires auth.keys to be configured');
|
|
226
|
-
}
|
|
227
273
|
}
|
|
228
274
|
|
|
229
275
|
/**
|
|
@@ -234,7 +280,7 @@ export default class HttpSyncServer {
|
|
|
234
280
|
* null so the request proceeds.
|
|
235
281
|
*
|
|
236
282
|
* @param {{ method: string, url: string, headers: { [x: string]: string }, body: Buffer|undefined }} request
|
|
237
|
-
* @param {
|
|
283
|
+
* @param {Record<string, unknown>} parsed - Parsed sync request body
|
|
238
284
|
* @returns {Promise<{ status: number, headers: Object, body: string }|null>}
|
|
239
285
|
* @private
|
|
240
286
|
*/
|
|
@@ -264,29 +310,31 @@ export default class HttpSyncServer {
|
|
|
264
310
|
return null;
|
|
265
311
|
}
|
|
266
312
|
|
|
267
|
-
/** @param {{ method: string, url: string, headers:
|
|
313
|
+
/** @param {{ method: string, url: string, headers: Object, body: Buffer|undefined }} request */
|
|
268
314
|
async _handleRequest(request) {
|
|
269
|
-
|
|
315
|
+
/** @type {{ method: string, url: string, headers: Record<string, string>, body: Buffer|undefined }} */
|
|
316
|
+
const req = { ...request, headers: /** @type {Record<string, string>} */ (request.headers) };
|
|
317
|
+
const contentTypeError = checkContentType(req.headers);
|
|
270
318
|
if (contentTypeError) {
|
|
271
319
|
return contentTypeError;
|
|
272
320
|
}
|
|
273
321
|
|
|
274
|
-
const routeError = validateRoute(
|
|
322
|
+
const routeError = validateRoute(req, this._path, this._host);
|
|
275
323
|
if (routeError) {
|
|
276
324
|
return routeError;
|
|
277
325
|
}
|
|
278
326
|
|
|
279
|
-
const sizeError = checkBodySize(
|
|
327
|
+
const sizeError = checkBodySize(req.body, this._maxRequestBytes);
|
|
280
328
|
if (sizeError) {
|
|
281
329
|
return sizeError;
|
|
282
330
|
}
|
|
283
331
|
|
|
284
|
-
const { error, parsed } = parseBody(
|
|
332
|
+
const { error, parsed } = parseBody(req.body);
|
|
285
333
|
if (error) {
|
|
286
334
|
return error;
|
|
287
335
|
}
|
|
288
336
|
|
|
289
|
-
const authError = await this._authorize(
|
|
337
|
+
const authError = await this._authorize(req, parsed);
|
|
290
338
|
if (authError) {
|
|
291
339
|
return authError;
|
|
292
340
|
}
|
|
@@ -294,8 +342,8 @@ export default class HttpSyncServer {
|
|
|
294
342
|
try {
|
|
295
343
|
const response = await this._graph.processSyncRequest(parsed);
|
|
296
344
|
return jsonResponse(response);
|
|
297
|
-
} catch (err) {
|
|
298
|
-
return errorResponse(500,
|
|
345
|
+
} catch (/** @type {unknown} */ err) {
|
|
346
|
+
return errorResponse(500, err instanceof Error ? err.message : 'Sync failed');
|
|
299
347
|
}
|
|
300
348
|
}
|
|
301
349
|
|
|
@@ -311,11 +359,14 @@ export default class HttpSyncServer {
|
|
|
311
359
|
throw new Error('listen() requires a numeric port');
|
|
312
360
|
}
|
|
313
361
|
|
|
314
|
-
|
|
362
|
+
/** @type {{ listen: Function, close: Function, address: Function }} */
|
|
363
|
+
const server = this._httpPort.createServer(
|
|
364
|
+
(/** @type {{ method: string, url: string, headers: Object, body: Buffer|undefined }} */ request) => this._handleRequest(request),
|
|
365
|
+
);
|
|
315
366
|
this._server = server;
|
|
316
367
|
|
|
317
368
|
await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
318
|
-
server.listen(port, this._host, (/** @type {
|
|
369
|
+
server.listen(port, this._host, (/** @type {Error|null} */ err) => {
|
|
319
370
|
if (err) {
|
|
320
371
|
reject(err);
|
|
321
372
|
} else {
|
|
@@ -332,7 +383,7 @@ export default class HttpSyncServer {
|
|
|
332
383
|
url,
|
|
333
384
|
close: () =>
|
|
334
385
|
/** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
335
|
-
server.close((/** @type {
|
|
386
|
+
server.close((/** @type {Error|null} */ err) => {
|
|
336
387
|
if (err) {
|
|
337
388
|
reject(err);
|
|
338
389
|
} else {
|
|
@@ -50,7 +50,7 @@ export default class IndexRebuildService {
|
|
|
50
50
|
* @throws {Error} If graphService is not provided
|
|
51
51
|
* @throws {Error} If storage adapter is not provided
|
|
52
52
|
*/
|
|
53
|
-
constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {
|
|
53
|
+
constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {{ graphService: { iterateNodes: (opts: { ref: string, limit: number }) => AsyncIterable<{ sha: string, parents: string[] }> }, storage: import('../../ports/IndexStoragePort.js').default }} */ ({})) {
|
|
54
54
|
if (!graphService) {
|
|
55
55
|
throw new Error('IndexRebuildService requires a graphService');
|
|
56
56
|
}
|
|
@@ -156,7 +156,7 @@ export default class IndexRebuildService {
|
|
|
156
156
|
operation: 'rebuild',
|
|
157
157
|
ref,
|
|
158
158
|
mode,
|
|
159
|
-
error:
|
|
159
|
+
error: err instanceof Error ? err.message : String(err),
|
|
160
160
|
durationMs,
|
|
161
161
|
});
|
|
162
162
|
throw err;
|
|
@@ -247,12 +247,12 @@ export default class IndexRebuildService {
|
|
|
247
247
|
* @private
|
|
248
248
|
*/
|
|
249
249
|
async _rebuildStreaming(ref, { limit, maxMemoryBytes, onFlush, onProgress, signal, frontier }) {
|
|
250
|
-
const builder = new StreamingBitmapIndexBuilder(
|
|
250
|
+
const builder = new StreamingBitmapIndexBuilder({
|
|
251
251
|
storage: this.storage,
|
|
252
252
|
maxMemoryBytes,
|
|
253
253
|
onFlush,
|
|
254
254
|
crypto: this._crypto,
|
|
255
|
-
})
|
|
255
|
+
});
|
|
256
256
|
|
|
257
257
|
let processedNodes = 0;
|
|
258
258
|
|
|
@@ -266,7 +266,7 @@ export default class IndexRebuildService {
|
|
|
266
266
|
if (processedNodes % 10000 === 0) {
|
|
267
267
|
checkAborted(signal, 'rebuild');
|
|
268
268
|
if (onProgress) {
|
|
269
|
-
const stats = /** @type {
|
|
269
|
+
const stats = /** @type {{ estimatedBitmapBytes: number }} */ (builder.getMemoryStats());
|
|
270
270
|
onProgress({
|
|
271
271
|
processedNodes,
|
|
272
272
|
currentMemoryBytes: stats.estimatedBitmapBytes,
|
|
@@ -275,7 +275,7 @@ export default class IndexRebuildService {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
return await
|
|
278
|
+
return await builder.finalize({ signal, frontier });
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
@@ -389,7 +389,7 @@ export default class IndexRebuildService {
|
|
|
389
389
|
|
|
390
390
|
// Staleness check
|
|
391
391
|
if (currentFrontier) {
|
|
392
|
-
const indexFrontier = await loadIndexFrontier(shardOids, /** @type {
|
|
392
|
+
const indexFrontier = await loadIndexFrontier(shardOids, /** @type {import('../../ports/IndexStoragePort.js').default & import('../../ports/BlobPort.js').default} */ (this.storage), { codec: this._codec });
|
|
393
393
|
if (indexFrontier) {
|
|
394
394
|
const result = checkStaleness(indexFrontier, currentFrontier);
|
|
395
395
|
if (result.stale) {
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
import defaultCodec from '../utils/defaultCodec.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @param {
|
|
9
|
+
* @param {unknown} envelope
|
|
10
10
|
* @param {string} label
|
|
11
11
|
* @private
|
|
12
12
|
*/
|
|
13
13
|
function validateEnvelope(envelope, label) {
|
|
14
|
-
|
|
14
|
+
const rec = /** @type {Record<string, unknown>} */ (envelope);
|
|
15
|
+
if (!rec || typeof rec !== 'object' || !rec.frontier || typeof rec.frontier !== 'object') {
|
|
15
16
|
throw new Error(`invalid frontier envelope for ${label}`);
|
|
16
17
|
}
|
|
17
18
|
}
|
|
@@ -25,7 +26,7 @@ function validateEnvelope(envelope, label) {
|
|
|
25
26
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
26
27
|
* @returns {Promise<Map<string, string>|null>} Frontier map, or null if not present (legacy index)
|
|
27
28
|
*/
|
|
28
|
-
export async function loadIndexFrontier(shardOids, storage, { codec } =
|
|
29
|
+
export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
|
|
29
30
|
const c = codec || defaultCodec;
|
|
30
31
|
const cborOid = shardOids['frontier.cbor'];
|
|
31
32
|
if (cborOid) {
|