@git-stunts/git-warp 11.2.1 → 11.3.3
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/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 +82 -22
- package/package.json +3 -2
- 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 +2 -2
- 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 +11 -11
- 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 +42 -26
- 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 +9 -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 +6 -5
- 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 +14 -12
- 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
|
@@ -18,9 +18,19 @@ import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCo
|
|
|
18
18
|
import { Writer } from './Writer.js';
|
|
19
19
|
import { generateWriterId, resolveWriterId } from '../utils/WriterId.js';
|
|
20
20
|
|
|
21
|
+
/** @typedef {import('../types/WarpPersistence.js').CorePersistence} CorePersistence */
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Creates a new PatchBuilderV2 for this graph.
|
|
23
25
|
*
|
|
26
|
+
* In multi-writer scenarios, call `materialize()` (or a query method that
|
|
27
|
+
* auto-materializes) before creating a patch so that `_maxObservedLamport`
|
|
28
|
+
* reflects all known writers. Without this, `_nextLamport()` still produces
|
|
29
|
+
* locally-monotonic ticks (`Math.max(ownTick, _maxObservedLamport) + 1`),
|
|
30
|
+
* and `PatchBuilderV2.commit()` re-reads the writer's own ref at commit
|
|
31
|
+
* time, so correctness is preserved — but the tick may be lower than
|
|
32
|
+
* necessary, losing LWW tiebreakers against other writers.
|
|
33
|
+
*
|
|
24
34
|
* @this {import('../WarpGraph.js').default}
|
|
25
35
|
* @returns {Promise<PatchBuilderV2>} A new patch builder
|
|
26
36
|
*/
|
|
@@ -52,6 +62,9 @@ export async function createPatch() {
|
|
|
52
62
|
* Not reentrant: calling `graph.patch()` inside a callback throws.
|
|
53
63
|
* Use `createPatch()` directly for advanced multi-patch workflows.
|
|
54
64
|
*
|
|
65
|
+
* **Multi-writer note:** call `materialize()` before `patch()` so that
|
|
66
|
+
* `_maxObservedLamport` is up-to-date. See `createPatch()` for details.
|
|
67
|
+
*
|
|
55
68
|
* @this {import('../WarpGraph.js').default}
|
|
56
69
|
* @param {(p: PatchBuilderV2) => void | Promise<void>} build - Callback that adds operations to the patch
|
|
57
70
|
* @returns {Promise<string>} The commit SHA of the new patch
|
|
@@ -89,30 +102,35 @@ export async function _nextLamport() {
|
|
|
89
102
|
const writerRef = buildWriterRef(this._graphName, this._writerId);
|
|
90
103
|
const currentRefSha = await this._persistence.readRef(writerRef);
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
// First commit for this writer
|
|
94
|
-
return { lamport: 1, parentSha: null };
|
|
95
|
-
}
|
|
105
|
+
let ownTick = 0;
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
if (currentRefSha) {
|
|
108
|
+
// Read the current patch commit to get its lamport timestamp
|
|
109
|
+
const commitMessage = await this._persistence.showNode(currentRefSha);
|
|
110
|
+
const kind = detectMessageKind(commitMessage);
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
if (kind === 'patch') {
|
|
113
|
+
try {
|
|
114
|
+
const patchInfo = decodePatchMessage(commitMessage);
|
|
115
|
+
ownTick = patchInfo.lamport;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Failed to parse lamport from writer ref ${writerRef}: ` +
|
|
119
|
+
`commit ${currentRefSha} has invalid patch message format`,
|
|
120
|
+
{ cause: err }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Non-patch ref: ownTick stays 0 (fresh start), falls through to standard return.
|
|
104
125
|
}
|
|
105
126
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`commit ${currentRefSha} has invalid patch message format`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
127
|
+
// Standard Lamport clock rule: next tick = max(own chain, globally observed max) + 1.
|
|
128
|
+
// _maxObservedLamport is updated during materialize() and after each commit, so this
|
|
129
|
+
// is O(1) — no additional git reads required at commit time.
|
|
130
|
+
return {
|
|
131
|
+
lamport: Math.max(ownTick, this._maxObservedLamport) + 1,
|
|
132
|
+
parentSha: currentRefSha ?? null,
|
|
133
|
+
};
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
/**
|
|
@@ -193,19 +211,22 @@ export async function getWriterPatches(writerId, stopAtSha = null) {
|
|
|
193
211
|
*/
|
|
194
212
|
export async function _onPatchCommitted(writerId, { patch: committed, sha } = {}) {
|
|
195
213
|
vvIncrement(this._versionVector, writerId);
|
|
214
|
+
// Keep _maxObservedLamport up to date so _nextLamport() issues globally-monotonic ticks.
|
|
215
|
+
if (committed?.lamport !== undefined && committed.lamport > this._maxObservedLamport) {
|
|
216
|
+
this._maxObservedLamport = committed.lamport;
|
|
217
|
+
}
|
|
196
218
|
this._patchesSinceCheckpoint++;
|
|
197
219
|
// Eager re-materialize: apply the just-committed patch to cached state
|
|
198
220
|
// Only when the cache is clean — applying a patch to stale state would be incorrect
|
|
199
221
|
if (this._cachedState && !this._stateDirty && committed && sha) {
|
|
200
222
|
let tickReceipt = null;
|
|
201
223
|
if (this._auditService) {
|
|
202
|
-
// TODO(ts-cleanup): narrow joinPatch return + patch type to PatchV2
|
|
203
224
|
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, receipt: import('../types/TickReceipt.js').TickReceipt}} */ (
|
|
204
|
-
joinPatch(this._cachedState, /** @type {
|
|
225
|
+
joinPatch(this._cachedState, /** @type {Parameters<typeof joinPatch>[1]} */ (committed), sha, true)
|
|
205
226
|
);
|
|
206
227
|
tickReceipt = result.receipt;
|
|
207
228
|
} else {
|
|
208
|
-
joinPatch(this._cachedState, /** @type {
|
|
229
|
+
joinPatch(this._cachedState, /** @type {Parameters<typeof joinPatch>[1]} */ (committed), sha);
|
|
209
230
|
}
|
|
210
231
|
await this._setMaterializedState(this._cachedState);
|
|
211
232
|
// Update provenance index with new patch
|
|
@@ -257,14 +278,16 @@ export async function writer(writerId) {
|
|
|
257
278
|
configSet,
|
|
258
279
|
});
|
|
259
280
|
|
|
281
|
+
/** @type {CorePersistence} */
|
|
282
|
+
const persistence = this._persistence;
|
|
260
283
|
return new Writer({
|
|
261
|
-
persistence
|
|
284
|
+
persistence,
|
|
262
285
|
graphName: this._graphName,
|
|
263
286
|
writerId: resolvedWriterId,
|
|
264
287
|
versionVector: this._versionVector,
|
|
265
|
-
getCurrentState: () => /** @type {
|
|
288
|
+
getCurrentState: /** @type {() => Promise<import('../services/JoinReducer.js').WarpStateV5>} */ (/** @type {unknown} */ (() => this._cachedState)),
|
|
266
289
|
onDeleteWithData: this._onDeleteWithData,
|
|
267
|
-
onCommitSuccess: (/** @type {
|
|
290
|
+
onCommitSuccess: /** @type {(result: {patch: Object, sha: string}) => void} */ (/** @type {unknown} */ ((/** @type {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} */ opts) => this._onPatchCommitted(resolvedWriterId, opts))),
|
|
268
291
|
codec: this._codec,
|
|
269
292
|
});
|
|
270
293
|
}
|
|
@@ -313,14 +336,16 @@ export async function createWriter(opts = {}) {
|
|
|
313
336
|
await this._persistence.configSet(configKey, freshWriterId);
|
|
314
337
|
}
|
|
315
338
|
|
|
339
|
+
/** @type {CorePersistence} */
|
|
340
|
+
const writerPersistence = this._persistence;
|
|
316
341
|
return new Writer({
|
|
317
|
-
persistence:
|
|
342
|
+
persistence: writerPersistence,
|
|
318
343
|
graphName: this._graphName,
|
|
319
344
|
writerId: freshWriterId,
|
|
320
345
|
versionVector: this._versionVector,
|
|
321
|
-
getCurrentState: () => /** @type {
|
|
346
|
+
getCurrentState: /** @type {() => Promise<import('../services/JoinReducer.js').WarpStateV5>} */ (/** @type {unknown} */ (() => this._cachedState)),
|
|
322
347
|
onDeleteWithData: this._onDeleteWithData,
|
|
323
|
-
onCommitSuccess: (/** @type {
|
|
348
|
+
onCommitSuccess: /** @type {(result: {patch: Object, sha: string}) => void} */ (/** @type {unknown} */ ((/** @type {{patch?: import('../types/WarpTypesV2.js').PatchV2, sha?: string}} */ commitOpts) => this._onPatchCommitted(freshWriterId, commitOpts))),
|
|
324
349
|
codec: this._codec,
|
|
325
350
|
});
|
|
326
351
|
}
|
|
@@ -13,6 +13,8 @@ import { createEmptyStateV5, reduceV5 } from '../services/JoinReducer.js';
|
|
|
13
13
|
import { ProvenancePayload } from '../services/ProvenancePayload.js';
|
|
14
14
|
import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
|
|
15
15
|
|
|
16
|
+
/** @typedef {import('../types/WarpTypesV2.js').PatchV2} PatchV2 */
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* Returns all patch SHAs that affected a given node or edge.
|
|
18
20
|
*
|
|
@@ -184,7 +186,7 @@ export async function _computeBackwardCone(nodeId) {
|
|
|
184
186
|
cone.set(sha, patch);
|
|
185
187
|
|
|
186
188
|
// Add read dependencies to the queue
|
|
187
|
-
const patchReads = /** @type {
|
|
189
|
+
const patchReads = /** @type {{reads?: string[]}} */ (patch).reads;
|
|
188
190
|
if (patchReads) {
|
|
189
191
|
for (const readEntity of patchReads) {
|
|
190
192
|
if (!visited.has(readEntity)) {
|
|
@@ -261,8 +263,8 @@ export async function _loadPatchesBySha(shas) {
|
|
|
261
263
|
* This ensures deterministic ordering regardless of discovery order.
|
|
262
264
|
*
|
|
263
265
|
* @this {import('../WarpGraph.js').default}
|
|
264
|
-
* @param {Array<{patch:
|
|
265
|
-
* @returns {Array<{patch:
|
|
266
|
+
* @param {Array<{patch: PatchV2, sha: string}>} patches - Unsorted patch entries
|
|
267
|
+
* @returns {Array<{patch: PatchV2, sha: string}>} Sorted patch entries
|
|
266
268
|
*/
|
|
267
269
|
export function _sortPatchesCausally(patches) {
|
|
268
270
|
return [...patches].sort((a, b) => {
|
|
@@ -37,7 +37,7 @@ export async function hasNode(nodeId) {
|
|
|
37
37
|
*
|
|
38
38
|
* @this {import('../WarpGraph.js').default}
|
|
39
39
|
* @param {string} nodeId - The node ID to get properties for
|
|
40
|
-
* @returns {Promise<Map<string,
|
|
40
|
+
* @returns {Promise<Map<string, unknown>|null>} Map of property key → value, or null if node doesn't exist
|
|
41
41
|
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
42
42
|
*/
|
|
43
43
|
export async function getNodeProps(nodeId) {
|
|
@@ -66,7 +66,7 @@ export async function getNodeProps(nodeId) {
|
|
|
66
66
|
* @param {string} from - Source node ID
|
|
67
67
|
* @param {string} to - Target node ID
|
|
68
68
|
* @param {string} label - Edge label
|
|
69
|
-
* @returns {Promise<Record<string,
|
|
69
|
+
* @returns {Promise<Record<string, unknown>|null>} Object of property key → value, or null if edge doesn't exist
|
|
70
70
|
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
71
71
|
*/
|
|
72
72
|
export async function getEdgeProps(from, to, label) {
|
|
@@ -85,7 +85,7 @@ export async function getEdgeProps(from, to, label) {
|
|
|
85
85
|
|
|
86
86
|
const birthEvent = s.edgeBirthEvent?.get(edgeKey);
|
|
87
87
|
|
|
88
|
-
/** @type {Record<string,
|
|
88
|
+
/** @type {Record<string, unknown>} */
|
|
89
89
|
const props = {};
|
|
90
90
|
for (const [propKey, register] of s.prop) {
|
|
91
91
|
if (!isEdgePropKey(propKey)) {
|
|
@@ -177,7 +177,7 @@ export async function getNodes() {
|
|
|
177
177
|
* Gets all visible edges in the materialized state.
|
|
178
178
|
*
|
|
179
179
|
* @this {import('../WarpGraph.js').default}
|
|
180
|
-
* @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string,
|
|
180
|
+
* @returns {Promise<Array<{from: string, to: string, label: string, props: Record<string, unknown>}>>} Array of edge info
|
|
181
181
|
* @throws {import('../errors/QueryError.js').default} If no cached state exists (code: `E_NO_STATE`)
|
|
182
182
|
*/
|
|
183
183
|
export async function getEdges() {
|
|
@@ -275,5 +275,6 @@ export async function observer(name, config) {
|
|
|
275
275
|
*/
|
|
276
276
|
export async function translationCost(configA, configB) {
|
|
277
277
|
await this._ensureFreshState();
|
|
278
|
-
|
|
278
|
+
const s = /** @type {import('../services/JoinReducer.js').WarpStateV5} */ (this._cachedState);
|
|
279
|
+
return computeTranslationCost(configA, configB, s);
|
|
279
280
|
}
|
|
@@ -31,6 +31,9 @@ import { buildWriterRef } from '../utils/RefLayout.js';
|
|
|
31
31
|
import { collectGCMetrics } from '../services/GCMetrics.js';
|
|
32
32
|
import HttpSyncServer from '../services/HttpSyncServer.js';
|
|
33
33
|
import { signSyncRequest, canonicalizePath } from '../services/SyncAuthService.js';
|
|
34
|
+
import { isError } from '../types/WarpErrors.js';
|
|
35
|
+
|
|
36
|
+
/** @typedef {import('../types/WarpPersistence.js').CorePersistence} CorePersistence */
|
|
34
37
|
|
|
35
38
|
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
36
39
|
|
|
@@ -221,10 +224,12 @@ export async function createSyncRequest() {
|
|
|
221
224
|
*/
|
|
222
225
|
export async function processSyncRequest(request) {
|
|
223
226
|
const localFrontier = await this.getFrontier();
|
|
227
|
+
/** @type {CorePersistence} */
|
|
228
|
+
const persistence = this._persistence;
|
|
224
229
|
return await processSyncRequestImpl(
|
|
225
230
|
request,
|
|
226
231
|
localFrontier,
|
|
227
|
-
|
|
232
|
+
persistence,
|
|
228
233
|
this._graphName,
|
|
229
234
|
{ codec: this._codec }
|
|
230
235
|
);
|
|
@@ -253,7 +258,7 @@ export function applySyncResponse(response) {
|
|
|
253
258
|
});
|
|
254
259
|
}
|
|
255
260
|
|
|
256
|
-
const currentFrontier = /** @type {
|
|
261
|
+
const currentFrontier = /** @type {Map<string, string>} */ (/** @type {unknown} */ (this._cachedState.observedFrontier));
|
|
257
262
|
const result = /** @type {{state: import('../services/JoinReducer.js').WarpStateV5, frontier: Map<string, string>, applied: number}} */ (applySyncResponseImpl(response, this._cachedState, currentFrontier));
|
|
258
263
|
|
|
259
264
|
// Update cached state
|
|
@@ -352,12 +357,12 @@ export async function syncWith(remote, options = {}) {
|
|
|
352
357
|
targetUrl.hash = '';
|
|
353
358
|
}
|
|
354
359
|
let attempt = 0;
|
|
355
|
-
const emit = (/** @type {string} */ type, /** @type {Record<string,
|
|
360
|
+
const emit = (/** @type {string} */ type, /** @type {Record<string, unknown>} */ payload = {}) => {
|
|
356
361
|
if (typeof onStatus === 'function') {
|
|
357
|
-
onStatus(/** @type {
|
|
362
|
+
onStatus(/** @type {{type: string, attempt: number}} */ ({ type, attempt, ...payload }));
|
|
358
363
|
}
|
|
359
364
|
};
|
|
360
|
-
const shouldRetry = (/** @type {
|
|
365
|
+
const shouldRetry = (/** @type {unknown} */ err) => {
|
|
361
366
|
if (isDirectPeer) { return false; }
|
|
362
367
|
if (err instanceof SyncError) {
|
|
363
368
|
return ['E_SYNC_REMOTE', 'E_SYNC_TIMEOUT', 'E_SYNC_NETWORK'].includes(err.code);
|
|
@@ -400,7 +405,7 @@ export async function syncWith(remote, options = {}) {
|
|
|
400
405
|
});
|
|
401
406
|
});
|
|
402
407
|
} catch (err) {
|
|
403
|
-
if (
|
|
408
|
+
if (isError(err) && err.name === 'AbortError') {
|
|
404
409
|
throw new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
405
410
|
}
|
|
406
411
|
if (err instanceof TimeoutError) {
|
|
@@ -411,7 +416,7 @@ export async function syncWith(remote, options = {}) {
|
|
|
411
416
|
}
|
|
412
417
|
throw new SyncError('Network error', {
|
|
413
418
|
code: 'E_SYNC_NETWORK',
|
|
414
|
-
context: { message:
|
|
419
|
+
context: { message: isError(err) ? err.message : String(err) },
|
|
415
420
|
});
|
|
416
421
|
}
|
|
417
422
|
|
|
@@ -474,7 +479,7 @@ export async function syncWith(remote, options = {}) {
|
|
|
474
479
|
shouldRetry,
|
|
475
480
|
onRetry: (/** @type {Error} */ error, /** @type {number} */ attemptNumber, /** @type {number} */ delayMs) => {
|
|
476
481
|
if (typeof onStatus === 'function') {
|
|
477
|
-
onStatus(/** @type {
|
|
482
|
+
onStatus(/** @type {{type: string, attempt: number, delayMs: number, error: Error}} */ ({ type: 'retrying', attempt: attemptNumber, delayMs, error }));
|
|
478
483
|
}
|
|
479
484
|
},
|
|
480
485
|
});
|
|
@@ -488,7 +493,7 @@ export async function syncWith(remote, options = {}) {
|
|
|
488
493
|
return syncResult;
|
|
489
494
|
} catch (err) {
|
|
490
495
|
this._logTiming('syncWith', t0, { error: /** @type {Error} */ (err) });
|
|
491
|
-
if (
|
|
496
|
+
if (isError(err) && err.name === 'AbortError') {
|
|
492
497
|
const abortedError = new OperationAbortedError('syncWith', { reason: 'Signal received' });
|
|
493
498
|
if (typeof onStatus === 'function') {
|
|
494
499
|
onStatus({ type: 'failed', attempt, error: abortedError });
|
|
@@ -518,13 +523,13 @@ export async function syncWith(remote, options = {}) {
|
|
|
518
523
|
* @param {string} [options.host='127.0.0.1'] - Host to bind
|
|
519
524
|
* @param {string} [options.path='/sync'] - Path to handle sync requests
|
|
520
525
|
* @param {number} [options.maxRequestBytes=4194304] - Max request size in bytes
|
|
521
|
-
* @param {import('../../ports/HttpServerPort.js').default} options.httpPort - HTTP server adapter
|
|
526
|
+
* @param {import('../../ports/HttpServerPort.js').default} options.httpPort - HTTP server adapter
|
|
522
527
|
* @param {{ keys: Record<string, string>, mode?: 'enforce'|'log-only' }} [options.auth] - Auth configuration
|
|
523
528
|
* @returns {Promise<{close: () => Promise<void>, url: string}>} Server handle
|
|
524
529
|
* @throws {Error} If port is not a number
|
|
525
530
|
* @throws {Error} If httpPort adapter is not provided
|
|
526
531
|
*/
|
|
527
|
-
export async function serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort, auth } = /** @type {
|
|
532
|
+
export async function serve({ port, host = '127.0.0.1', path = '/sync', maxRequestBytes = DEFAULT_SYNC_SERVER_MAX_BYTES, httpPort, auth } = /** @type {{ port: number, httpPort: import('../../ports/HttpServerPort.js').default }} */ ({})) {
|
|
528
533
|
if (typeof port !== 'number') {
|
|
529
534
|
throw new Error('serve() requires a numeric port');
|
|
530
535
|
}
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ambient declarations for Deno and Bun runtime globals.
|
|
3
|
+
*
|
|
4
|
+
* These cover ONLY the APIs actually used in this codebase:
|
|
5
|
+
* - Deno.serve() (DenoHttpAdapter.js)
|
|
6
|
+
* - Deno.env.get() (bin/cli/infrastructure.js)
|
|
7
|
+
* - Bun.serve() (BunHttpAdapter.js)
|
|
8
|
+
*
|
|
9
|
+
* Do NOT install @types/deno or @types/bun — this file is intentionally
|
|
10
|
+
* narrow to avoid pulling in thousands of unrelated declarations.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
/* Deno */
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
|
|
17
|
+
interface DenoAddr {
|
|
18
|
+
transport: string;
|
|
19
|
+
hostname: string;
|
|
20
|
+
port: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DenoServer {
|
|
24
|
+
shutdown(): Promise<void>;
|
|
25
|
+
addr: DenoAddr;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DenoServeOptions {
|
|
29
|
+
port?: number;
|
|
30
|
+
hostname?: string;
|
|
31
|
+
onListen?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface DenoEnv {
|
|
35
|
+
get(name: string): string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare namespace Deno {
|
|
39
|
+
const env: DenoEnv;
|
|
40
|
+
function serve(
|
|
41
|
+
options: DenoServeOptions,
|
|
42
|
+
handler: (request: Request) => Promise<Response> | Response,
|
|
43
|
+
): DenoServer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ------------------------------------------------------------------ */
|
|
47
|
+
/* Bun */
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
|
|
50
|
+
interface BunServer {
|
|
51
|
+
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
52
|
+
hostname: string;
|
|
53
|
+
port: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface BunServeOptions {
|
|
57
|
+
port?: number;
|
|
58
|
+
hostname?: string;
|
|
59
|
+
fetch: (request: Request) => Promise<Response> | Response;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare namespace Bun {
|
|
63
|
+
function serve(options: BunServeOptions): BunServer;
|
|
64
|
+
}
|
|
@@ -106,8 +106,8 @@ function createFetchHandler(requestHandler, logger) {
|
|
|
106
106
|
const portReq = await toPortRequest(request);
|
|
107
107
|
const portRes = await requestHandler(portReq);
|
|
108
108
|
return toResponse(portRes);
|
|
109
|
-
} catch (
|
|
110
|
-
if (err.status === 413) {
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (typeof err === 'object' && err !== null && /** @type {{status?: number}} */ (err).status === 413) {
|
|
111
111
|
return new Response(PAYLOAD_TOO_LARGE, {
|
|
112
112
|
status: 413,
|
|
113
113
|
headers: { 'Content-Type': 'text/plain', 'Content-Length': PAYLOAD_TOO_LARGE_LENGTH },
|
|
@@ -125,6 +125,10 @@ function createFetchHandler(requestHandler, logger) {
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* @typedef {{ hostname: string, port: number, stop: (closeActiveConnections?: boolean) => Promise<void> }} BunServer
|
|
130
|
+
*/
|
|
131
|
+
|
|
128
132
|
/**
|
|
129
133
|
* Starts a Bun server and invokes the callback with (null) on success
|
|
130
134
|
* or (err) on failure.
|
|
@@ -132,12 +136,11 @@ function createFetchHandler(requestHandler, logger) {
|
|
|
132
136
|
* Note: Bun.serve() is synchronous, so cb fires on the same tick
|
|
133
137
|
* (unlike Node's server.listen which defers via the event loop).
|
|
134
138
|
*
|
|
135
|
-
* @param {
|
|
139
|
+
* @param {BunServeOptions} serveOptions
|
|
136
140
|
* @param {Function|undefined} cb - Node-style callback
|
|
137
|
-
* @returns {
|
|
141
|
+
* @returns {BunServer} The Bun server instance
|
|
138
142
|
*/
|
|
139
143
|
function startServer(serveOptions, cb) {
|
|
140
|
-
// @ts-expect-error — Bun global is only available in Bun runtime
|
|
141
144
|
const server = globalThis.Bun.serve(serveOptions);
|
|
142
145
|
if (cb) {
|
|
143
146
|
cb(null);
|
|
@@ -148,13 +151,15 @@ function startServer(serveOptions, cb) {
|
|
|
148
151
|
/**
|
|
149
152
|
* Safely stops a Bun server, forwarding errors to the callback.
|
|
150
153
|
*
|
|
151
|
-
* @param {{ server:
|
|
154
|
+
* @param {{ server: BunServer | null }} state - Shared mutable state
|
|
152
155
|
* @param {Function} [callback]
|
|
153
156
|
*/
|
|
154
157
|
function stopServer(state, callback) {
|
|
155
158
|
try {
|
|
156
159
|
if (state.server) {
|
|
157
|
-
|
|
160
|
+
// stop() synchronously halts the listener; the returned Promise
|
|
161
|
+
// represents draining of active connections — safe to ignore here.
|
|
162
|
+
void state.server.stop();
|
|
158
163
|
state.server = null;
|
|
159
164
|
}
|
|
160
165
|
if (callback) {
|
|
@@ -192,7 +197,7 @@ export default class BunHttpAdapter extends HttpServerPort {
|
|
|
192
197
|
*/
|
|
193
198
|
createServer(requestHandler) {
|
|
194
199
|
const fetchHandler = createFetchHandler(requestHandler, this._logger);
|
|
195
|
-
/** @type {{ server:
|
|
200
|
+
/** @type {{ server: BunServer | null }} */
|
|
196
201
|
const state = { server: null };
|
|
197
202
|
|
|
198
203
|
return {
|
|
@@ -204,7 +209,7 @@ export default class BunHttpAdapter extends HttpServerPort {
|
|
|
204
209
|
listen(port, host, callback) {
|
|
205
210
|
const cb = typeof host === 'function' ? host : callback;
|
|
206
211
|
const bindHost = typeof host === 'string' ? host : undefined;
|
|
207
|
-
/** @type {
|
|
212
|
+
/** @type {BunServeOptions} */
|
|
208
213
|
const serveOptions = { port, fetch: fetchHandler };
|
|
209
214
|
|
|
210
215
|
if (bindHost !== undefined) {
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* @module infrastructure/adapters/CasSeekCacheAdapter
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Minimal interface for the ContentAddressableStore from @git-stunts/git-cas.
|
|
21
|
+
* @typedef {{ readManifest: Function, restore: Function, store: Function, createTree: Function }} CasStore
|
|
22
|
+
*/
|
|
23
|
+
|
|
19
24
|
import SeekCachePort from '../../ports/SeekCachePort.js';
|
|
20
25
|
import { buildSeekCacheRef } from '../../domain/utils/RefLayout.js';
|
|
21
26
|
import { Readable } from 'node:stream';
|
|
@@ -59,7 +64,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
59
64
|
/**
|
|
60
65
|
* Lazily initializes the ContentAddressableStore.
|
|
61
66
|
* @private
|
|
62
|
-
* @returns {Promise
|
|
67
|
+
* @returns {Promise<CasStore>}
|
|
63
68
|
*/
|
|
64
69
|
async _getCas() {
|
|
65
70
|
if (!this._casPromise) {
|
|
@@ -73,7 +78,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
73
78
|
|
|
74
79
|
/**
|
|
75
80
|
* @private
|
|
76
|
-
* @returns {Promise
|
|
81
|
+
* @returns {Promise<CasStore>}
|
|
77
82
|
*/
|
|
78
83
|
async _initCas() {
|
|
79
84
|
const { default: ContentAddressableStore } = await import(
|
|
@@ -132,7 +137,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
132
137
|
* @returns {Promise<CacheIndex>} The mutated index
|
|
133
138
|
*/
|
|
134
139
|
async _mutateIndex(mutate) {
|
|
135
|
-
/** @type {
|
|
140
|
+
/** @type {unknown} */
|
|
136
141
|
let lastErr;
|
|
137
142
|
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
138
143
|
const index = await this._readIndex();
|
|
@@ -144,7 +149,7 @@ export default class CasSeekCacheAdapter extends SeekCachePort {
|
|
|
144
149
|
lastErr = err;
|
|
145
150
|
// Transient write failure — retry with fresh read
|
|
146
151
|
if (attempt === MAX_CAS_RETRIES - 1) {
|
|
147
|
-
throw new Error(`CasSeekCacheAdapter: index update failed after retries: ${lastErr.message}`);
|
|
152
|
+
throw new Error(`CasSeekCacheAdapter: index update failed after retries: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
}
|
|
@@ -100,8 +100,8 @@ function createHandler(requestHandler, logger) {
|
|
|
100
100
|
const plain = await toPlainRequest(request);
|
|
101
101
|
const response = await requestHandler(plain);
|
|
102
102
|
return toDenoResponse(response);
|
|
103
|
-
} catch (
|
|
104
|
-
if (err.status === 413) {
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (typeof err === 'object' && err !== null && /** @type {{status?: number}} */ (err).status === 413) {
|
|
105
105
|
const msg = new TextEncoder().encode('Payload Too Large');
|
|
106
106
|
return new Response(msg, {
|
|
107
107
|
status: 413,
|
|
@@ -140,7 +140,7 @@ function closeImpl(state, callback) {
|
|
|
140
140
|
callback();
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
-
/** @param {
|
|
143
|
+
/** @param {unknown} err */ (err) => {
|
|
144
144
|
state.server = null;
|
|
145
145
|
if (callback) {
|
|
146
146
|
callback(err);
|
|
@@ -210,7 +210,7 @@ export default class DenoHttpAdapter extends HttpServerPort {
|
|
|
210
210
|
const hostname = typeof host === 'string' ? host : undefined;
|
|
211
211
|
|
|
212
212
|
try {
|
|
213
|
-
/** @type {
|
|
213
|
+
/** @type {DenoServeOptions} */
|
|
214
214
|
const serveOptions = {
|
|
215
215
|
port,
|
|
216
216
|
onListen() {
|
|
@@ -223,9 +223,8 @@ export default class DenoHttpAdapter extends HttpServerPort {
|
|
|
223
223
|
serveOptions.hostname = hostname;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
// @ts-expect-error — Deno global is only available in Deno runtime
|
|
227
226
|
state.server = globalThis.Deno.serve(serveOptions, handler);
|
|
228
|
-
} catch (
|
|
227
|
+
} catch (err) {
|
|
229
228
|
if (cb) {
|
|
230
229
|
cb(err);
|
|
231
230
|
} else {
|
|
@@ -145,11 +145,12 @@ async function refExists(execute, ref) {
|
|
|
145
145
|
try {
|
|
146
146
|
await execute({ args: ['show-ref', '--verify', '--quiet', ref] });
|
|
147
147
|
return true;
|
|
148
|
-
} catch (
|
|
149
|
-
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const gitErr = /** @type {GitError} */ (err);
|
|
150
|
+
if (getExitCode(gitErr) === 1) {
|
|
150
151
|
return false;
|
|
151
152
|
}
|
|
152
|
-
if (isDanglingObjectError(
|
|
153
|
+
if (isDanglingObjectError(gitErr)) {
|
|
153
154
|
return false;
|
|
154
155
|
}
|
|
155
156
|
throw err;
|
|
@@ -544,11 +545,12 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
544
545
|
args: ['rev-parse', ref]
|
|
545
546
|
});
|
|
546
547
|
return oid.trim();
|
|
547
|
-
} catch (
|
|
548
|
-
|
|
548
|
+
} catch (err) {
|
|
549
|
+
const gitErr = /** @type {GitError} */ (err);
|
|
550
|
+
if (getExitCode(gitErr) === 1) {
|
|
549
551
|
return null;
|
|
550
552
|
}
|
|
551
|
-
if (isDanglingObjectError(
|
|
553
|
+
if (isDanglingObjectError(gitErr)) {
|
|
552
554
|
return null;
|
|
553
555
|
}
|
|
554
556
|
throw err;
|
|
@@ -628,8 +630,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
628
630
|
try {
|
|
629
631
|
await this._executeWithRetry({ args: ['cat-file', '-e', sha] });
|
|
630
632
|
return true;
|
|
631
|
-
} catch (
|
|
632
|
-
if (getExitCode(err) === 1) {
|
|
633
|
+
} catch (err) {
|
|
634
|
+
if (getExitCode(/** @type {GitError} */ (err)) === 1) {
|
|
633
635
|
return false;
|
|
634
636
|
}
|
|
635
637
|
throw err;
|
|
@@ -704,8 +706,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
704
706
|
args: ['merge-base', '--is-ancestor', potentialAncestor, descendant]
|
|
705
707
|
});
|
|
706
708
|
return true; // Exit code 0 means it IS an ancestor
|
|
707
|
-
} catch (
|
|
708
|
-
if (this._getExitCode(err) === 1) {
|
|
709
|
+
} catch (err) {
|
|
710
|
+
if (this._getExitCode(/** @type {GitError} */ (err)) === 1) {
|
|
709
711
|
return false; // Exit code 1 means it is NOT an ancestor
|
|
710
712
|
}
|
|
711
713
|
throw err; // Re-throw unexpected errors
|
|
@@ -726,8 +728,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
|
|
|
726
728
|
});
|
|
727
729
|
// Preserve empty-string values; only drop trailing newline
|
|
728
730
|
return value.replace(/\n$/, '');
|
|
729
|
-
} catch (
|
|
730
|
-
if (this._isConfigKeyNotFound(err)) {
|
|
731
|
+
} catch (err) {
|
|
732
|
+
if (this._isConfigKeyNotFound(/** @type {GitError} */ (err))) {
|
|
731
733
|
return null;
|
|
732
734
|
}
|
|
733
735
|
throw err;
|
|
@@ -71,7 +71,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
71
71
|
const logger = this._logger;
|
|
72
72
|
const server = createServer((req, res) => {
|
|
73
73
|
dispatch(req, res, { handler: requestHandler, logger }).catch(
|
|
74
|
-
/** @param {
|
|
74
|
+
/** @param {unknown} err */ (err) => {
|
|
75
75
|
logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
|
|
76
76
|
});
|
|
77
77
|
});
|
|
@@ -85,7 +85,7 @@ export default class NodeHttpAdapter extends HttpServerPort {
|
|
|
85
85
|
listen(port, host, callback) {
|
|
86
86
|
const cb = typeof host === 'function' ? host : callback;
|
|
87
87
|
const bindHost = typeof host === 'string' ? host : undefined;
|
|
88
|
-
/** @param {
|
|
88
|
+
/** @param {unknown} err */
|
|
89
89
|
const onError = (err) => {
|
|
90
90
|
if (cb) {
|
|
91
91
|
cb(err);
|
|
@@ -38,8 +38,8 @@ function toWebCryptoAlgo(algorithm) {
|
|
|
38
38
|
function toUint8Array(data) {
|
|
39
39
|
if (data instanceof Uint8Array) { return data; }
|
|
40
40
|
if (typeof data === 'string') { return new TextEncoder().encode(data); }
|
|
41
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(
|
|
42
|
-
const buf = /** @type {Buffer} */ (
|
|
41
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
|
|
42
|
+
const buf = /** @type {Buffer} */ (data);
|
|
43
43
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
44
44
|
}
|
|
45
45
|
throw new Error('WebCryptoAdapter: data must be string, Buffer, or Uint8Array');
|