@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
|
@@ -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({ indexReader, logger = nullLogger, traversal } = {}) {
|
|
40
|
+
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default, traversal?: import('./DagTraversal.js').default }} */ { indexReader, logger = nullLogger, traversal } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
41
41
|
if (!indexReader) {
|
|
42
42
|
throw new Error('DagTopology requires an indexReader');
|
|
43
43
|
}
|
|
@@ -76,9 +76,10 @@ export default class DagTopology {
|
|
|
76
76
|
*/
|
|
77
77
|
async commonAncestors({ shas, maxResults = 100, maxDepth = DEFAULT_MAX_DEPTH, signal }) {
|
|
78
78
|
if (shas.length === 0) { return []; }
|
|
79
|
+
const traversal = /** @type {import('./DagTraversal.js').default} */ (this._traversal);
|
|
79
80
|
if (shas.length === 1) {
|
|
80
81
|
const ancestors = [];
|
|
81
|
-
for await (const node of
|
|
82
|
+
for await (const node of traversal.ancestors({ sha: shas[0], maxNodes: maxResults, maxDepth, signal })) {
|
|
82
83
|
ancestors.push(node.sha);
|
|
83
84
|
}
|
|
84
85
|
return ancestors;
|
|
@@ -92,7 +93,7 @@ export default class DagTopology {
|
|
|
92
93
|
for (const sha of shas) {
|
|
93
94
|
checkAborted(signal, 'commonAncestors');
|
|
94
95
|
const visited = new Set();
|
|
95
|
-
for await (const node of
|
|
96
|
+
for await (const node of traversal.ancestors({ sha, maxDepth, signal })) {
|
|
96
97
|
if (!visited.has(node.sha)) {
|
|
97
98
|
visited.add(node.sha);
|
|
98
99
|
ancestorCounts.set(node.sha, (ancestorCounts.get(node.sha) || 0) + 1);
|
|
@@ -149,7 +150,7 @@ export default class DagTopology {
|
|
|
149
150
|
checkAborted(signal, 'topologicalSort');
|
|
150
151
|
}
|
|
151
152
|
|
|
152
|
-
const sha = queue.shift();
|
|
153
|
+
const sha = /** @type {string} */ (queue.shift());
|
|
153
154
|
const neighbors = await this._getNeighbors(sha, direction);
|
|
154
155
|
edges.set(sha, neighbors);
|
|
155
156
|
|
|
@@ -182,7 +183,7 @@ export default class DagTopology {
|
|
|
182
183
|
checkAborted(signal, 'topologicalSort');
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
const sha = ready.shift();
|
|
186
|
+
const sha = /** @type {string} */ (ready.shift());
|
|
186
187
|
const depth = depthMap.get(sha) || 0;
|
|
187
188
|
|
|
188
189
|
nodesYielded++;
|
|
@@ -190,7 +191,7 @@ export default class DagTopology {
|
|
|
190
191
|
|
|
191
192
|
const neighbors = edges.get(sha) || [];
|
|
192
193
|
for (const neighbor of neighbors) {
|
|
193
|
-
const newDegree = inDegree.get(neighbor) - 1;
|
|
194
|
+
const newDegree = /** @type {number} */ (inDegree.get(neighbor)) - 1;
|
|
194
195
|
inDegree.set(neighbor, newDegree);
|
|
195
196
|
|
|
196
197
|
if (!depthMap.has(neighbor)) {
|
|
@@ -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({ indexReader, logger = nullLogger } = {}) {
|
|
46
|
+
constructor(/** @type {{ indexReader: import('./BitmapIndexReader.js').default, logger?: import('../../ports/LoggerPort.js').default }} */ { indexReader, logger = nullLogger } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
47
47
|
if (!indexReader) {
|
|
48
48
|
throw new Error('DagTraversal requires an indexReader');
|
|
49
49
|
}
|
|
@@ -89,6 +89,7 @@ export default class DagTraversal {
|
|
|
89
89
|
signal,
|
|
90
90
|
}) {
|
|
91
91
|
const visited = new Set();
|
|
92
|
+
/** @type {TraversalNode[]} */
|
|
92
93
|
const queue = [{ sha: start, depth: 0, parent: null }];
|
|
93
94
|
let nodesYielded = 0;
|
|
94
95
|
|
|
@@ -99,7 +100,7 @@ export default class DagTraversal {
|
|
|
99
100
|
checkAborted(signal, 'bfs');
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
const current = queue.shift();
|
|
103
|
+
const current = /** @type {TraversalNode} */ (queue.shift());
|
|
103
104
|
|
|
104
105
|
if (visited.has(current.sha)) { continue; }
|
|
105
106
|
if (current.depth > maxDepth) { continue; }
|
|
@@ -142,6 +143,7 @@ export default class DagTraversal {
|
|
|
142
143
|
signal,
|
|
143
144
|
}) {
|
|
144
145
|
const visited = new Set();
|
|
146
|
+
/** @type {TraversalNode[]} */
|
|
145
147
|
const stack = [{ sha: start, depth: 0, parent: null }];
|
|
146
148
|
let nodesYielded = 0;
|
|
147
149
|
|
|
@@ -152,7 +154,7 @@ export default class DagTraversal {
|
|
|
152
154
|
checkAborted(signal, 'dfs');
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
const current = stack.pop();
|
|
157
|
+
const current = /** @type {TraversalNode} */ (stack.pop());
|
|
156
158
|
|
|
157
159
|
if (visited.has(current.sha)) { continue; }
|
|
158
160
|
if (current.depth > maxDepth) { continue; }
|
|
@@ -50,12 +50,13 @@ export function getWriters(frontier) {
|
|
|
50
50
|
* Keys are sorted for determinism.
|
|
51
51
|
* @param {Frontier} frontier
|
|
52
52
|
* @param {Object} [options]
|
|
53
|
-
* @param {import('../../ports/CodecPort.js').default} options.codec - Codec for serialization
|
|
54
|
-
* @returns {Buffer}
|
|
53
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
54
|
+
* @returns {Buffer|Uint8Array}
|
|
55
55
|
*/
|
|
56
|
-
export function serializeFrontier(frontier, { codec } = {}) {
|
|
56
|
+
export function serializeFrontier(frontier, { codec } = /** @type {{codec?: import('../../ports/CodecPort.js').default}} */ ({})) {
|
|
57
57
|
const c = codec || defaultCodec;
|
|
58
58
|
// Convert Map to sorted object for deterministic encoding
|
|
59
|
+
/** @type {Record<string, string|undefined>} */
|
|
59
60
|
const obj = {};
|
|
60
61
|
const sortedKeys = Array.from(frontier.keys()).sort();
|
|
61
62
|
for (const key of sortedKeys) {
|
|
@@ -68,12 +69,12 @@ export function serializeFrontier(frontier, { codec } = {}) {
|
|
|
68
69
|
* Deserializes frontier from CBOR bytes.
|
|
69
70
|
* @param {Buffer} buffer
|
|
70
71
|
* @param {Object} [options]
|
|
71
|
-
* @param {import('../../ports/CodecPort.js').default} options.codec - Codec for deserialization
|
|
72
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
72
73
|
* @returns {Frontier}
|
|
73
74
|
*/
|
|
74
|
-
export function deserializeFrontier(buffer, { codec } = {}) {
|
|
75
|
+
export function deserializeFrontier(buffer, { codec } = /** @type {{codec?: import('../../ports/CodecPort.js').default}} */ ({})) {
|
|
75
76
|
const c = codec || defaultCodec;
|
|
76
|
-
const obj = c.decode(buffer);
|
|
77
|
+
const obj = /** @type {Record<string, string>} */ (c.decode(buffer));
|
|
77
78
|
const frontier = new Map();
|
|
78
79
|
for (const [writerId, patchSha] of Object.entries(obj)) {
|
|
79
80
|
frontier.set(writerId, patchSha);
|
|
@@ -46,7 +46,7 @@ export default class HealthCheckService {
|
|
|
46
46
|
/**
|
|
47
47
|
* Creates a HealthCheckService instance.
|
|
48
48
|
* @param {Object} options
|
|
49
|
-
* @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Persistence port for repository checks
|
|
49
|
+
* @param {import('../../ports/GraphPersistencePort.js').default & import('../../ports/CommitPort.js').default} options.persistence - Persistence port for repository checks
|
|
50
50
|
* @param {import('../../ports/ClockPort.js').default} options.clock - Clock port for timing operations
|
|
51
51
|
* @param {number} [options.cacheTtlMs=5000] - How long to cache health results in milliseconds
|
|
52
52
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for structured logging
|
|
@@ -132,22 +132,23 @@ export default class HealthCheckService {
|
|
|
132
132
|
*/
|
|
133
133
|
async getHealth() {
|
|
134
134
|
const { value, cachedAt, fromCache } = await this._healthCache.getWithMetadata();
|
|
135
|
+
const result = /** @type {HealthResult} */ (value);
|
|
135
136
|
|
|
136
137
|
if (cachedAt) {
|
|
137
|
-
return { ...
|
|
138
|
+
return { ...result, cachedAt };
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
// Log only for fresh computations
|
|
141
142
|
if (!fromCache) {
|
|
142
143
|
this._logger.debug('Health check completed', {
|
|
143
144
|
operation: 'getHealth',
|
|
144
|
-
status:
|
|
145
|
-
repositoryStatus:
|
|
146
|
-
indexStatus:
|
|
145
|
+
status: result.status,
|
|
146
|
+
repositoryStatus: result.components.repository.status,
|
|
147
|
+
indexStatus: result.components.index.status,
|
|
147
148
|
});
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
return
|
|
151
|
+
return result;
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
/**
|
|
@@ -184,16 +185,16 @@ export default class HealthCheckService {
|
|
|
184
185
|
try {
|
|
185
186
|
const pingResult = await this._persistence.ping();
|
|
186
187
|
return {
|
|
187
|
-
status: pingResult.ok ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY,
|
|
188
|
+
status: /** @type {'healthy'|'unhealthy'} */ (pingResult.ok ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY),
|
|
188
189
|
latencyMs: Math.round(pingResult.latencyMs * 100) / 100, // Round to 2 decimal places
|
|
189
190
|
};
|
|
190
191
|
} catch (err) {
|
|
191
192
|
this._logger.warn('Repository ping failed', {
|
|
192
193
|
operation: 'checkRepository',
|
|
193
|
-
error: err.message,
|
|
194
|
+
error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
|
|
194
195
|
});
|
|
195
196
|
return {
|
|
196
|
-
status: HealthStatus.UNHEALTHY,
|
|
197
|
+
status: /** @type {'healthy'|'unhealthy'} */ (HealthStatus.UNHEALTHY),
|
|
197
198
|
latencyMs: 0,
|
|
198
199
|
};
|
|
199
200
|
}
|
|
@@ -207,7 +208,7 @@ export default class HealthCheckService {
|
|
|
207
208
|
_checkIndex() {
|
|
208
209
|
if (!this._indexReader) {
|
|
209
210
|
return {
|
|
210
|
-
status: HealthStatus.DEGRADED,
|
|
211
|
+
status: /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.DEGRADED),
|
|
211
212
|
loaded: false,
|
|
212
213
|
};
|
|
213
214
|
}
|
|
@@ -216,7 +217,7 @@ export default class HealthCheckService {
|
|
|
216
217
|
const shardCount = this._indexReader.shardOids?.size ?? 0;
|
|
217
218
|
|
|
218
219
|
return {
|
|
219
|
-
status: HealthStatus.HEALTHY,
|
|
220
|
+
status: /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.HEALTHY),
|
|
220
221
|
loaded: true,
|
|
221
222
|
shardCount,
|
|
222
223
|
};
|
|
@@ -232,15 +233,15 @@ export default class HealthCheckService {
|
|
|
232
233
|
_computeOverallStatus(repositoryHealth, indexHealth) {
|
|
233
234
|
// If repository is unhealthy, overall is unhealthy
|
|
234
235
|
if (repositoryHealth.status === HealthStatus.UNHEALTHY) {
|
|
235
|
-
return HealthStatus.UNHEALTHY;
|
|
236
|
+
return /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.UNHEALTHY);
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
// If index is degraded (not loaded), overall is degraded
|
|
239
240
|
if (indexHealth.status === HealthStatus.DEGRADED) {
|
|
240
|
-
return HealthStatus.DEGRADED;
|
|
241
|
+
return /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.DEGRADED);
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
// All components healthy
|
|
244
|
-
return HealthStatus.HEALTHY;
|
|
245
|
+
return /** @type {'healthy'|'degraded'|'unhealthy'} */ (HealthStatus.HEALTHY);
|
|
245
246
|
}
|
|
246
247
|
}
|
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
* @module domain/services/HookInstaller
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} FsAdapter
|
|
12
|
+
* @property {(path: string, content: string | Buffer, options?: Object) => void} writeFileSync
|
|
13
|
+
* @property {(path: string, mode: number) => void} chmodSync
|
|
14
|
+
* @property {(path: string, encoding?: string) => string} readFileSync
|
|
15
|
+
* @property {(path: string) => boolean} existsSync
|
|
16
|
+
* @property {(path: string, options?: Object) => void} mkdirSync
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} PathUtils
|
|
21
|
+
* @property {(...segments: string[]) => string} join
|
|
22
|
+
* @property {(...segments: string[]) => string} resolve
|
|
23
|
+
*/
|
|
24
|
+
|
|
10
25
|
const DELIMITER_START_PREFIX = '# --- @git-stunts/git-warp post-merge hook';
|
|
11
26
|
const DELIMITER_END = '# --- end @git-stunts/git-warp ---';
|
|
12
27
|
const VERSION_MARKER_PREFIX = '# warp-hook-version:';
|
|
@@ -59,17 +74,22 @@ export class HookInstaller {
|
|
|
59
74
|
* Creates a new HookInstaller.
|
|
60
75
|
*
|
|
61
76
|
* @param {Object} deps - Injected dependencies
|
|
62
|
-
* @param {
|
|
77
|
+
* @param {FsAdapter} deps.fs - Filesystem adapter with methods: readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync
|
|
63
78
|
* @param {(repoPath: string, key: string) => string|null} deps.execGitConfig - Function to read git config values
|
|
64
79
|
* @param {string} deps.version - Package version
|
|
65
80
|
* @param {string} deps.templateDir - Directory containing hook templates
|
|
66
|
-
* @param {
|
|
81
|
+
* @param {PathUtils} deps.path - Path utilities (join and resolve)
|
|
67
82
|
*/
|
|
68
|
-
constructor({ fs, execGitConfig, version, templateDir, path } = {}) {
|
|
83
|
+
constructor({ fs, execGitConfig, version, templateDir, path } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
84
|
+
/** @type {FsAdapter} */
|
|
69
85
|
this._fs = fs;
|
|
86
|
+
/** @type {(repoPath: string, key: string) => string|null} */
|
|
70
87
|
this._execGitConfig = execGitConfig;
|
|
88
|
+
/** @type {string} */
|
|
71
89
|
this._templateDir = templateDir;
|
|
90
|
+
/** @type {string} */
|
|
72
91
|
this._version = version;
|
|
92
|
+
/** @type {PathUtils} */
|
|
73
93
|
this._path = path;
|
|
74
94
|
}
|
|
75
95
|
|
|
@@ -134,7 +154,11 @@ export class HookInstaller {
|
|
|
134
154
|
throw new Error(`Unknown install strategy: ${strategy}`);
|
|
135
155
|
}
|
|
136
156
|
|
|
137
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} hookPath
|
|
159
|
+
* @param {string} content
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
138
162
|
_freshInstall(hookPath, content) {
|
|
139
163
|
this._fs.writeFileSync(hookPath, content, { mode: 0o755 });
|
|
140
164
|
this._fs.chmodSync(hookPath, 0o755);
|
|
@@ -145,13 +169,17 @@ export class HookInstaller {
|
|
|
145
169
|
};
|
|
146
170
|
}
|
|
147
171
|
|
|
148
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} hookPath
|
|
174
|
+
* @param {string} stamped
|
|
175
|
+
* @private
|
|
176
|
+
*/
|
|
149
177
|
_upgradeInstall(hookPath, stamped) {
|
|
150
178
|
const existing = this._readFile(hookPath);
|
|
151
179
|
const classification = classifyExistingHook(existing);
|
|
152
180
|
|
|
153
181
|
if (classification.appended) {
|
|
154
|
-
const updated = replaceDelimitedSection(existing, stamped);
|
|
182
|
+
const updated = replaceDelimitedSection(/** @type {string} */ (existing), stamped);
|
|
155
183
|
// If delimiters were corrupted, replaceDelimitedSection returns unchanged content — fall back to overwrite
|
|
156
184
|
if (updated === existing) {
|
|
157
185
|
this._fs.writeFileSync(hookPath, stamped, { mode: 0o755 });
|
|
@@ -170,7 +198,11 @@ export class HookInstaller {
|
|
|
170
198
|
};
|
|
171
199
|
}
|
|
172
200
|
|
|
173
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* @param {string} hookPath
|
|
203
|
+
* @param {string} stamped
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
174
206
|
_appendInstall(hookPath, stamped) {
|
|
175
207
|
const existing = this._readFile(hookPath) || '';
|
|
176
208
|
const body = stripShebang(stamped);
|
|
@@ -184,7 +216,11 @@ export class HookInstaller {
|
|
|
184
216
|
};
|
|
185
217
|
}
|
|
186
218
|
|
|
187
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* @param {string} hookPath
|
|
221
|
+
* @param {string} stamped
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
188
224
|
_replaceInstall(hookPath, stamped) {
|
|
189
225
|
const existing = this._readFile(hookPath);
|
|
190
226
|
let backupPath;
|
|
@@ -210,12 +246,18 @@ export class HookInstaller {
|
|
|
210
246
|
return this._fs.readFileSync(templatePath, 'utf8');
|
|
211
247
|
}
|
|
212
248
|
|
|
213
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* @param {string} template
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
214
253
|
_stampVersion(template) {
|
|
215
254
|
return template.replaceAll(VERSION_PLACEHOLDER, this._version);
|
|
216
255
|
}
|
|
217
256
|
|
|
218
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* @param {string} repoPath
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
219
261
|
_resolveHooksDir(repoPath) {
|
|
220
262
|
const customPath = this._execGitConfig(repoPath, 'core.hooksPath');
|
|
221
263
|
if (customPath) {
|
|
@@ -230,12 +272,18 @@ export class HookInstaller {
|
|
|
230
272
|
return this._path.join(repoPath, '.git', 'hooks');
|
|
231
273
|
}
|
|
232
274
|
|
|
233
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* @param {string} repoPath
|
|
277
|
+
* @private
|
|
278
|
+
*/
|
|
234
279
|
_resolveHookPath(repoPath) {
|
|
235
280
|
return this._path.join(this._resolveHooksDir(repoPath), 'post-merge');
|
|
236
281
|
}
|
|
237
282
|
|
|
238
|
-
/**
|
|
283
|
+
/**
|
|
284
|
+
* @param {string} filePath
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
239
287
|
_readFile(filePath) {
|
|
240
288
|
try {
|
|
241
289
|
return this._fs.readFileSync(filePath, 'utf8');
|
|
@@ -244,7 +292,10 @@ export class HookInstaller {
|
|
|
244
292
|
}
|
|
245
293
|
}
|
|
246
294
|
|
|
247
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* @param {string} dirPath
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
248
299
|
_ensureDir(dirPath) {
|
|
249
300
|
if (!this._fs.existsSync(dirPath)) {
|
|
250
301
|
this._fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -22,9 +22,10 @@ function canonicalizeJson(value) {
|
|
|
22
22
|
return value.map(canonicalizeJson);
|
|
23
23
|
}
|
|
24
24
|
if (value && typeof value === 'object') {
|
|
25
|
+
/** @type {{ [x: string]: * }} */
|
|
25
26
|
const sorted = {};
|
|
26
27
|
for (const key of Object.keys(value).sort()) {
|
|
27
|
-
sorted[key] = canonicalizeJson(value[key]);
|
|
28
|
+
sorted[key] = canonicalizeJson(/** @type {{ [x: string]: * }} */ (value)[key]);
|
|
28
29
|
}
|
|
29
30
|
return sorted;
|
|
30
31
|
}
|
|
@@ -97,7 +98,7 @@ function isValidSyncRequest(parsed) {
|
|
|
97
98
|
* Checks the content-type header. Returns an error response if the
|
|
98
99
|
* content type is present but not application/json, otherwise null.
|
|
99
100
|
*
|
|
100
|
-
* @param {
|
|
101
|
+
* @param {{ [x: string]: string }} headers - Request headers
|
|
101
102
|
* @returns {{ status: number, headers: Object, body: string }|null}
|
|
102
103
|
* @private
|
|
103
104
|
*/
|
|
@@ -113,7 +114,7 @@ function checkContentType(headers) {
|
|
|
113
114
|
* Parses the request URL and validates the path and method.
|
|
114
115
|
* Returns an error response on failure, or null if valid.
|
|
115
116
|
*
|
|
116
|
-
* @param {{ method: string, url: string, headers:
|
|
117
|
+
* @param {{ method: string, url: string, headers: { [x: string]: string } }} request
|
|
117
118
|
* @param {string} expectedPath
|
|
118
119
|
* @param {string} defaultHost
|
|
119
120
|
* @returns {{ status: number, headers: Object, body: string }|null}
|
|
@@ -171,12 +172,12 @@ export default class HttpSyncServer {
|
|
|
171
172
|
/**
|
|
172
173
|
* @param {Object} options
|
|
173
174
|
* @param {import('../../ports/HttpServerPort.js').default} options.httpPort - HTTP server port abstraction
|
|
174
|
-
* @param {
|
|
175
|
+
* @param {{ processSyncRequest: (request: *) => Promise<*> }} options.graph - WarpGraph instance (must expose processSyncRequest)
|
|
175
176
|
* @param {string} [options.path='/sync'] - URL path to handle sync requests on
|
|
176
177
|
* @param {string} [options.host='127.0.0.1'] - Host to bind
|
|
177
178
|
* @param {number} [options.maxRequestBytes=4194304] - Maximum request body size in bytes
|
|
178
179
|
*/
|
|
179
|
-
constructor({ httpPort, graph, path = '/sync', host = '127.0.0.1', maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES } = {}) {
|
|
180
|
+
constructor({ httpPort, graph, path = '/sync', host = '127.0.0.1', maxRequestBytes = DEFAULT_MAX_REQUEST_BYTES } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
180
181
|
this._httpPort = httpPort;
|
|
181
182
|
this._graph = graph;
|
|
182
183
|
this._path = path && path.startsWith('/') ? path : `/${path || 'sync'}`;
|
|
@@ -188,7 +189,7 @@ export default class HttpSyncServer {
|
|
|
188
189
|
/**
|
|
189
190
|
* Handles an incoming HTTP request through the port abstraction.
|
|
190
191
|
*
|
|
191
|
-
* @param {{ method: string, url: string, headers:
|
|
192
|
+
* @param {{ method: string, url: string, headers: { [x: string]: string }, body: Buffer|undefined }} request
|
|
192
193
|
* @returns {Promise<{ status: number, headers: Object, body: string }>}
|
|
193
194
|
* @private
|
|
194
195
|
*/
|
|
@@ -212,7 +213,7 @@ export default class HttpSyncServer {
|
|
|
212
213
|
const response = await this._graph.processSyncRequest(parsed);
|
|
213
214
|
return jsonResponse(response);
|
|
214
215
|
} catch (err) {
|
|
215
|
-
return errorResponse(500, err?.message || 'Sync failed');
|
|
216
|
+
return errorResponse(500, /** @type {any} */ (err)?.message || 'Sync failed'); // TODO(ts-cleanup): type error
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
|
|
@@ -228,18 +229,18 @@ export default class HttpSyncServer {
|
|
|
228
229
|
throw new Error('listen() requires a numeric port');
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
const server = this._httpPort.createServer((request) => this._handleRequest(request));
|
|
232
|
+
const server = this._httpPort.createServer((/** @type {*} */ request) => this._handleRequest(request)); // TODO(ts-cleanup): type http callback
|
|
232
233
|
this._server = server;
|
|
233
234
|
|
|
234
|
-
await new Promise((resolve, reject) => {
|
|
235
|
-
server.listen(port, this._host, (err) => {
|
|
235
|
+
await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
236
|
+
server.listen(port, this._host, (/** @type {*} */ err) => { // TODO(ts-cleanup): type http callback
|
|
236
237
|
if (err) {
|
|
237
238
|
reject(err);
|
|
238
239
|
} else {
|
|
239
240
|
resolve();
|
|
240
241
|
}
|
|
241
242
|
});
|
|
242
|
-
});
|
|
243
|
+
}));
|
|
243
244
|
|
|
244
245
|
const address = server.address();
|
|
245
246
|
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
@@ -248,15 +249,15 @@ export default class HttpSyncServer {
|
|
|
248
249
|
return {
|
|
249
250
|
url,
|
|
250
251
|
close: () =>
|
|
251
|
-
new Promise((resolve, reject) => {
|
|
252
|
-
server.close((err) => {
|
|
252
|
+
/** @type {Promise<void>} */ (new Promise((resolve, reject) => {
|
|
253
|
+
server.close((/** @type {*} */ err) => { // TODO(ts-cleanup): type http callback
|
|
253
254
|
if (err) {
|
|
254
255
|
reject(err);
|
|
255
256
|
} else {
|
|
256
257
|
resolve();
|
|
257
258
|
}
|
|
258
259
|
});
|
|
259
|
-
}),
|
|
260
|
+
})),
|
|
260
261
|
};
|
|
261
262
|
}
|
|
262
263
|
}
|
|
@@ -40,17 +40,17 @@ export default class IndexRebuildService {
|
|
|
40
40
|
* Creates an IndexRebuildService instance.
|
|
41
41
|
*
|
|
42
42
|
* @param {Object} options - Configuration options
|
|
43
|
-
* @param {
|
|
44
|
-
* Must implement `iterateNodes({ ref, limit }) => AsyncGenerator<GraphNode>`.
|
|
43
|
+
* @param {{ iterateNodes: (opts: { ref: string, limit: number }) => AsyncIterable<{ sha: string, parents: string[] }> }} options.graphService - Graph service providing node iteration.
|
|
45
44
|
* @param {import('../../ports/IndexStoragePort.js').default} options.storage - Storage adapter
|
|
46
45
|
* for persisting index blobs and trees. Typically GitGraphAdapter.
|
|
47
46
|
* @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger for
|
|
48
47
|
* structured logging. Defaults to null logger (no logging).
|
|
48
|
+
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for serialization
|
|
49
49
|
* @param {import('../../ports/CryptoPort.js').default} [options.crypto] - Crypto adapter for checksums
|
|
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 }) {
|
|
53
|
+
constructor({ graphService, storage, logger = nullLogger, codec, crypto } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
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: err.message,
|
|
159
|
+
error: /** @type {any} */ (err).message, // TODO(ts-cleanup): type error
|
|
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(/** @type {*} */ ({ // TODO(ts-cleanup): narrow port type
|
|
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 = builder.getMemoryStats();
|
|
269
|
+
const stats = /** @type {any} */ (builder).getMemoryStats(); // TODO(ts-cleanup): narrow port type
|
|
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 builder.finalize({ signal, frontier });
|
|
278
|
+
return await /** @type {any} */ (builder).finalize({ signal, frontier }); // TODO(ts-cleanup): narrow port type
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
@@ -302,10 +302,10 @@ export default class IndexRebuildService {
|
|
|
302
302
|
const treeStructure = await builder.serialize({ frontier });
|
|
303
303
|
const flatEntries = [];
|
|
304
304
|
for (const [path, buffer] of Object.entries(treeStructure)) {
|
|
305
|
-
const oid = await this.storage.writeBlob(buffer);
|
|
305
|
+
const oid = await /** @type {import('../../ports/BlobPort.js').default} */ (/** @type {unknown} */ (this.storage)).writeBlob(buffer);
|
|
306
306
|
flatEntries.push(`100644 blob ${oid}\t${path}`);
|
|
307
307
|
}
|
|
308
|
-
return await this.storage.writeTree(flatEntries);
|
|
308
|
+
return await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).writeTree(flatEntries);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
/**
|
|
@@ -384,12 +384,12 @@ export default class IndexRebuildService {
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
const startTime = performance.now();
|
|
387
|
-
const shardOids = await this.storage.readTreeOids(treeOid);
|
|
387
|
+
const shardOids = await /** @type {import('../../ports/TreePort.js').default} */ (/** @type {unknown} */ (this.storage)).readTreeOids(treeOid);
|
|
388
388
|
const shardCount = Object.keys(shardOids).length;
|
|
389
389
|
|
|
390
390
|
// Staleness check
|
|
391
391
|
if (currentFrontier) {
|
|
392
|
-
const indexFrontier = await loadIndexFrontier(shardOids, this.storage, { codec: this._codec });
|
|
392
|
+
const indexFrontier = await loadIndexFrontier(shardOids, /** @type {*} */ (this.storage), { codec: this._codec }); // TODO(ts-cleanup): narrow port type
|
|
393
393
|
if (indexFrontier) {
|
|
394
394
|
const result = checkStaleness(indexFrontier, currentFrontier);
|
|
395
395
|
if (result.stale) {
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import defaultCodec from '../utils/defaultCodec.js';
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* @param {*} envelope
|
|
10
|
+
* @param {string} label
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
9
13
|
function validateEnvelope(envelope, label) {
|
|
10
14
|
if (!envelope || typeof envelope !== 'object' || !envelope.frontier || typeof envelope.frontier !== 'object') {
|
|
11
15
|
throw new Error(`invalid frontier envelope for ${label}`);
|
|
@@ -16,17 +20,17 @@ function validateEnvelope(envelope, label) {
|
|
|
16
20
|
* Loads the frontier from an index tree's shard OIDs.
|
|
17
21
|
*
|
|
18
22
|
* @param {Record<string, string>} shardOids - Map of path → blob OID from readTreeOids
|
|
19
|
-
* @param {import('../../ports/IndexStoragePort.js').default} storage - Storage adapter
|
|
23
|
+
* @param {import('../../ports/IndexStoragePort.js').default & import('../../ports/BlobPort.js').default} storage - Storage adapter
|
|
20
24
|
* @param {Object} [options]
|
|
21
25
|
* @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for deserialization
|
|
22
26
|
* @returns {Promise<Map<string, string>|null>} Frontier map, or null if not present (legacy index)
|
|
23
27
|
*/
|
|
24
|
-
export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
|
|
28
|
+
export async function loadIndexFrontier(shardOids, storage, { codec } = /** @type {*} */ ({})) { // TODO(ts-cleanup): needs options type
|
|
25
29
|
const c = codec || defaultCodec;
|
|
26
30
|
const cborOid = shardOids['frontier.cbor'];
|
|
27
31
|
if (cborOid) {
|
|
28
32
|
const buffer = await storage.readBlob(cborOid);
|
|
29
|
-
const envelope = c.decode(buffer);
|
|
33
|
+
const envelope = /** @type {{ frontier: Record<string, string> }} */ (c.decode(buffer));
|
|
30
34
|
validateEnvelope(envelope, 'frontier.cbor');
|
|
31
35
|
return new Map(Object.entries(envelope.frontier));
|
|
32
36
|
}
|
|
@@ -34,7 +38,7 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
|
|
|
34
38
|
const jsonOid = shardOids['frontier.json'];
|
|
35
39
|
if (jsonOid) {
|
|
36
40
|
const buffer = await storage.readBlob(jsonOid);
|
|
37
|
-
const envelope = JSON.parse(buffer.toString('utf-8'));
|
|
41
|
+
const envelope = /** @type {{ frontier: Record<string, string> }} */ (JSON.parse(buffer.toString('utf-8')));
|
|
38
42
|
validateEnvelope(envelope, 'frontier.json');
|
|
39
43
|
return new Map(Object.entries(envelope.frontier));
|
|
40
44
|
}
|
|
@@ -51,7 +55,10 @@ export async function loadIndexFrontier(shardOids, storage, { codec } = {}) {
|
|
|
51
55
|
* @property {string[]} removedWriters - Writers in index but not current
|
|
52
56
|
*/
|
|
53
57
|
|
|
54
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* @param {{ stale: boolean, advancedWriters: string[], newWriters: string[], removedWriters: string[] }} opts
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
55
62
|
function buildReason({ stale, advancedWriters, newWriters, removedWriters }) {
|
|
56
63
|
if (!stale) {
|
|
57
64
|
return 'index is current';
|