@ricsam/r5d-worker 0.0.53 → 0.0.55
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/dist/cjs/heartbeat.cjs +15 -2
- package/dist/cjs/main.cjs +186 -145
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-incident-state.cjs +39 -0
- package/dist/cjs/workspace-mutation-gate.cjs +92 -0
- package/dist/cjs/workspace-sync.cjs +73 -41
- package/dist/mjs/heartbeat.mjs +12 -1
- package/dist/mjs/main.mjs +191 -146
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-incident-state.mjs +14 -0
- package/dist/mjs/workspace-mutation-gate.mjs +68 -0
- package/dist/mjs/workspace-sync.mjs +71 -41
- package/dist/types/heartbeat.d.ts +7 -0
- package/dist/types/workspace-incident-state.d.ts +8 -0
- package/dist/types/workspace-mutation-gate.d.ts +19 -0
- package/dist/types/workspace-sync.d.ts +14 -1
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var workspace_mutation_gate_exports = {};
|
|
20
|
+
__export(workspace_mutation_gate_exports, {
|
|
21
|
+
WorkspaceMutationGate: () => WorkspaceMutationGate
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(workspace_mutation_gate_exports);
|
|
24
|
+
class WorkspaceMutationGate {
|
|
25
|
+
activeMutations = 0;
|
|
26
|
+
syncActive = false;
|
|
27
|
+
waiters = [];
|
|
28
|
+
acquireMutation() {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
this.waiters.push({ kind: "mutation", resolve });
|
|
31
|
+
this.drain();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async runMutation(operation) {
|
|
35
|
+
const release = await this.acquireMutation();
|
|
36
|
+
try {
|
|
37
|
+
return await operation();
|
|
38
|
+
} finally {
|
|
39
|
+
release();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async runSync(operation) {
|
|
43
|
+
const release = await new Promise((resolve) => {
|
|
44
|
+
this.waiters.push({ kind: "sync", resolve });
|
|
45
|
+
this.drain();
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
return await operation();
|
|
49
|
+
} finally {
|
|
50
|
+
release();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
drain() {
|
|
54
|
+
if (this.syncActive || this.waiters.length === 0) return;
|
|
55
|
+
const first = this.waiters[0];
|
|
56
|
+
if (first?.kind === "sync") {
|
|
57
|
+
if (this.activeMutations > 0) return;
|
|
58
|
+
this.waiters.shift();
|
|
59
|
+
this.syncActive = true;
|
|
60
|
+
first.resolve(this.releaseSync());
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
while (this.waiters[0]?.kind === "mutation") {
|
|
64
|
+
const waiter = this.waiters.shift();
|
|
65
|
+
if (!waiter || waiter.kind !== "mutation") break;
|
|
66
|
+
this.activeMutations += 1;
|
|
67
|
+
waiter.resolve(this.releaseMutation());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
releaseMutation() {
|
|
71
|
+
let released = false;
|
|
72
|
+
return () => {
|
|
73
|
+
if (released) return;
|
|
74
|
+
released = true;
|
|
75
|
+
this.activeMutations -= 1;
|
|
76
|
+
this.drain();
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
releaseSync() {
|
|
80
|
+
let released = false;
|
|
81
|
+
return () => {
|
|
82
|
+
if (released) return;
|
|
83
|
+
released = true;
|
|
84
|
+
this.syncActive = false;
|
|
85
|
+
this.drain();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
90
|
+
0 && (module.exports = {
|
|
91
|
+
WorkspaceMutationGate
|
|
92
|
+
});
|
|
@@ -30,10 +30,12 @@ var workspace_sync_exports = {};
|
|
|
30
30
|
__export(workspace_sync_exports, {
|
|
31
31
|
MAX_WORKSPACE_SYNC_DIFF_BYTES: () => MAX_WORKSPACE_SYNC_DIFF_BYTES,
|
|
32
32
|
WORKSPACE_BRANCH: () => WORKSPACE_BRANCH,
|
|
33
|
+
WORKSPACE_INTENT_REF_PREFIX: () => WORKSPACE_INTENT_REF_PREFIX,
|
|
33
34
|
WORKSPACE_NATIVE_PLANS_RELATIVE_PATH: () => WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
|
|
34
35
|
WORKSPACE_PERIODIC_SCAN_INTERVAL_MS: () => WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
|
|
35
36
|
WorkspaceSyncSingleFlight: () => WorkspaceSyncSingleFlight,
|
|
36
37
|
assertCanonicalCheckoutIndexMatchesWorktree: () => assertCanonicalCheckoutIndexMatchesWorktree,
|
|
38
|
+
assertWorkspaceQuarantineRef: () => assertWorkspaceQuarantineRef,
|
|
37
39
|
calculateWorkspaceDiffFingerprint: () => calculateWorkspaceDiffFingerprint,
|
|
38
40
|
encodeWorkspaceBranch: () => encodeWorkspaceBranch,
|
|
39
41
|
mirrorShadowWorkspaceToVisible: () => mirrorShadowWorkspaceToVisible,
|
|
@@ -49,7 +51,9 @@ var import_node_crypto = require("node:crypto");
|
|
|
49
51
|
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
50
52
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
51
53
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
54
|
+
var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
|
|
52
55
|
const WORKSPACE_BRANCH = "main";
|
|
56
|
+
const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
|
|
53
57
|
const MAX_WORKSPACE_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
54
58
|
const WORKSPACE_PERIODIC_SCAN_INTERVAL_MS = 5e3;
|
|
55
59
|
const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
|
|
@@ -1119,10 +1123,27 @@ function commitMessage(input) {
|
|
|
1119
1123
|
...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
|
|
1120
1124
|
});
|
|
1121
1125
|
}
|
|
1122
|
-
function
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
+
function assertWorkspaceQuarantineRef(quarantineRef) {
|
|
1127
|
+
const suffix = quarantineRef.slice(WORKSPACE_INTENT_REF_PREFIX.length);
|
|
1128
|
+
if (!quarantineRef.startsWith(WORKSPACE_INTENT_REF_PREFIX) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(suffix)) {
|
|
1129
|
+
throw new Error(`Workspace candidate ref must be below ${WORKSPACE_INTENT_REF_PREFIX}`);
|
|
1130
|
+
}
|
|
1131
|
+
const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
|
|
1132
|
+
stdout: "pipe",
|
|
1133
|
+
stderr: "pipe",
|
|
1134
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
1135
|
+
});
|
|
1136
|
+
if (result.exitCode !== 0) {
|
|
1137
|
+
throw new Error(`Invalid workspace candidate ref ${JSON.stringify(quarantineRef)}`);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function uploadWorkspaceCandidate(input, candidateHead) {
|
|
1141
|
+
const quarantineRef = input.quarantineRef;
|
|
1142
|
+
if (!quarantineRef) {
|
|
1143
|
+
throw new Error("A server-issued workspace candidate ref is required before uploading a canonical workspace candidate");
|
|
1144
|
+
}
|
|
1145
|
+
assertWorkspaceQuarantineRef(quarantineRef);
|
|
1146
|
+
return runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:${quarantineRef}`]);
|
|
1126
1147
|
}
|
|
1127
1148
|
function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
|
|
1128
1149
|
runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch canonical workspace before reset");
|
|
@@ -1141,6 +1162,7 @@ function baseResult(input, startingHead) {
|
|
|
1141
1162
|
workerLabel: input.workerLabel,
|
|
1142
1163
|
trigger: input.trigger,
|
|
1143
1164
|
startingHead,
|
|
1165
|
+
expectedHead: startingHead,
|
|
1144
1166
|
rebaseCount: 0,
|
|
1145
1167
|
diffSizeBytes: 0,
|
|
1146
1168
|
gitStatus: "",
|
|
@@ -1244,7 +1266,7 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1244
1266
|
if (stagedWorkingPaths.length > 0) {
|
|
1245
1267
|
runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
|
|
1246
1268
|
}
|
|
1247
|
-
|
|
1269
|
+
let candidateHead = revParse(input, "HEAD");
|
|
1248
1270
|
if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
|
|
1249
1271
|
const hasRemoteMain = Boolean(revParse(input, `origin/${WORKSPACE_BRANCH}`));
|
|
1250
1272
|
const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
|
|
@@ -1280,58 +1302,59 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1280
1302
|
};
|
|
1281
1303
|
}
|
|
1282
1304
|
let rebaseCount = 0;
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
if (push.exitCode === 0) {
|
|
1286
|
-
const publishedHead = revParse(input, "HEAD") ?? candidateHead;
|
|
1287
|
-
if (publishedHead) assertPublishedCanonicalCheckoutTree(input, publishedHead, sampledCanonicalCheckoutTreeHash);
|
|
1288
|
-
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
|
|
1289
|
-
return {
|
|
1290
|
-
...observed,
|
|
1291
|
-
outcome: "published",
|
|
1292
|
-
candidateHead: candidateHead ?? void 0,
|
|
1293
|
-
publishedHead: publishedHead ?? void 0,
|
|
1294
|
-
rebaseCount,
|
|
1295
|
-
gitStatus: gitStatus(input)
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
if (!isNonFastForward(push)) {
|
|
1299
|
-
return {
|
|
1300
|
-
...observed,
|
|
1301
|
-
outcome: "failed",
|
|
1302
|
-
candidateHead: candidateHead ?? void 0,
|
|
1303
|
-
rebaseCount,
|
|
1304
|
-
error: push.stderr || push.stdout || "Workspace push failed",
|
|
1305
|
-
gitStatus: gitStatus(input)
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch workspace after push race");
|
|
1305
|
+
const expectedHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
|
|
1306
|
+
if (candidateHead && expectedHead && !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead])) {
|
|
1309
1307
|
const rebase = runGitResult(input, input.shadowRoot, ["rebase", `origin/${WORKSPACE_BRANCH}`]);
|
|
1310
1308
|
rebaseCount += 1;
|
|
1311
1309
|
if (rebase.exitCode !== 0) {
|
|
1312
1310
|
const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1311
|
+
const conflictPaths = reportedPaths([.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort());
|
|
1312
|
+
runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
|
|
1315
1313
|
return {
|
|
1316
1314
|
...observed,
|
|
1317
|
-
outcome: "
|
|
1315
|
+
outcome: "failed",
|
|
1316
|
+
expectedHead,
|
|
1318
1317
|
candidateHead: candidateHead ?? void 0,
|
|
1319
|
-
publishedHead: publishedHead ?? void 0,
|
|
1320
1318
|
rebaseCount,
|
|
1321
|
-
|
|
1322
|
-
|
|
1319
|
+
affectedPaths: conflictPaths,
|
|
1320
|
+
error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`,
|
|
1323
1321
|
gitStatus: gitStatus(input)
|
|
1324
1322
|
};
|
|
1325
1323
|
}
|
|
1324
|
+
candidateHead = revParse(input, "HEAD");
|
|
1326
1325
|
assertPublishedCanonicalCheckoutTree(input, "HEAD", sampledCanonicalCheckoutTreeHash);
|
|
1327
1326
|
assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, "HEAD");
|
|
1328
1327
|
}
|
|
1328
|
+
if (!candidateHead) {
|
|
1329
|
+
return {
|
|
1330
|
+
...observed,
|
|
1331
|
+
outcome: "failed",
|
|
1332
|
+
rebaseCount,
|
|
1333
|
+
error: "Workspace candidate does not have a commit",
|
|
1334
|
+
gitStatus: gitStatus(input)
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
const upload = uploadWorkspaceCandidate(input, candidateHead);
|
|
1338
|
+
if (upload.exitCode !== 0) {
|
|
1339
|
+
return {
|
|
1340
|
+
...observed,
|
|
1341
|
+
outcome: "failed",
|
|
1342
|
+
candidateHead,
|
|
1343
|
+
quarantineRef: input.quarantineRef,
|
|
1344
|
+
rebaseCount,
|
|
1345
|
+
error: upload.stderr || upload.stdout || "Workspace candidate upload failed",
|
|
1346
|
+
gitStatus: gitStatus(input)
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
|
|
1350
|
+
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots);
|
|
1329
1351
|
return {
|
|
1330
1352
|
...observed,
|
|
1331
|
-
outcome: "
|
|
1332
|
-
|
|
1353
|
+
outcome: "candidate_ready",
|
|
1354
|
+
expectedHead,
|
|
1355
|
+
candidateHead,
|
|
1356
|
+
quarantineRef: input.quarantineRef,
|
|
1333
1357
|
rebaseCount,
|
|
1334
|
-
error: "Workspace push did not converge after three attempts",
|
|
1335
1358
|
gitStatus: gitStatus(input)
|
|
1336
1359
|
};
|
|
1337
1360
|
} catch (error) {
|
|
@@ -1374,9 +1397,10 @@ function calculateWorkspaceDiffFingerprint(rawInput) {
|
|
|
1374
1397
|
return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify({ stagedTree, localHead, remoteHead })).digest("hex");
|
|
1375
1398
|
}
|
|
1376
1399
|
class WorkspaceSyncSingleFlight {
|
|
1400
|
+
mutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
|
|
1377
1401
|
queue = Promise.resolve();
|
|
1378
1402
|
enqueue(task) {
|
|
1379
|
-
const queued = this.
|
|
1403
|
+
const queued = this.mutationGate.runSync(task);
|
|
1380
1404
|
this.queue = queued.then(
|
|
1381
1405
|
() => void 0,
|
|
1382
1406
|
() => void 0
|
|
@@ -1395,6 +1419,12 @@ class WorkspaceSyncSingleFlight {
|
|
|
1395
1419
|
fingerprintPrepared(prepare) {
|
|
1396
1420
|
return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
|
|
1397
1421
|
}
|
|
1422
|
+
runMutation(operation) {
|
|
1423
|
+
return this.mutationGate.runMutation(operation);
|
|
1424
|
+
}
|
|
1425
|
+
acquireMutation() {
|
|
1426
|
+
return this.mutationGate.acquireMutation();
|
|
1427
|
+
}
|
|
1398
1428
|
afterCurrent() {
|
|
1399
1429
|
return this.queue;
|
|
1400
1430
|
}
|
|
@@ -1403,10 +1433,12 @@ class WorkspaceSyncSingleFlight {
|
|
|
1403
1433
|
0 && (module.exports = {
|
|
1404
1434
|
MAX_WORKSPACE_SYNC_DIFF_BYTES,
|
|
1405
1435
|
WORKSPACE_BRANCH,
|
|
1436
|
+
WORKSPACE_INTENT_REF_PREFIX,
|
|
1406
1437
|
WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
|
|
1407
1438
|
WORKSPACE_PERIODIC_SCAN_INTERVAL_MS,
|
|
1408
1439
|
WorkspaceSyncSingleFlight,
|
|
1409
1440
|
assertCanonicalCheckoutIndexMatchesWorktree,
|
|
1441
|
+
assertWorkspaceQuarantineRef,
|
|
1410
1442
|
calculateWorkspaceDiffFingerprint,
|
|
1411
1443
|
encodeWorkspaceBranch,
|
|
1412
1444
|
mirrorShadowWorkspaceToVisible,
|
package/dist/mjs/heartbeat.mjs
CHANGED
|
@@ -3,8 +3,19 @@ const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
|
|
|
3
3
|
function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
|
|
4
4
|
return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
|
|
5
5
|
}
|
|
6
|
+
function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
|
|
7
|
+
if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
|
|
8
|
+
if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
|
|
9
|
+
return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
|
|
10
|
+
}
|
|
11
|
+
function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
|
|
12
|
+
if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
|
|
13
|
+
return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
|
|
14
|
+
}
|
|
6
15
|
export {
|
|
7
16
|
WORKER_HEARTBEAT_INTERVAL_MS,
|
|
8
17
|
WORKER_HEARTBEAT_TIMEOUT_MS,
|
|
9
|
-
|
|
18
|
+
grantWorkerHeartbeatBusyGrace,
|
|
19
|
+
hasWorkerHeartbeatTimedOut,
|
|
20
|
+
hasWorkerHeartbeatTimedOutWithBusyGrace
|
|
10
21
|
};
|