@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.
@@ -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 isNonFastForward(result) {
1123
- const output = `${result.stdout}
1124
- ${result.stderr}`.toLowerCase();
1125
- return output.includes("non-fast-forward") || output.includes("fetch first") || output.includes("[rejected]");
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
- const candidateHead = revParse(input, "HEAD");
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
- for (let pushAttempt = 0; pushAttempt < 3; pushAttempt += 1) {
1284
- const push = runGitResult(input, input.shadowRoot, ["push", "origin", `HEAD:refs/heads/${WORKSPACE_BRANCH}`]);
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 discardedPaths = reportedPaths([.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort());
1314
- const publishedHead = resetShadowToRemote(input, mirroredGitlinkProjection.opaqueRoots);
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: "conflict_reset",
1315
+ outcome: "failed",
1316
+ expectedHead,
1318
1317
  candidateHead: candidateHead ?? void 0,
1319
- publishedHead: publishedHead ?? void 0,
1320
1318
  rebaseCount,
1321
- discardedPaths,
1322
- localChangesDiscarded: true,
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: "failed",
1332
- candidateHead: candidateHead ?? void 0,
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.queue.then(task, task);
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,
@@ -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
- hasWorkerHeartbeatTimedOut
18
+ grantWorkerHeartbeatBusyGrace,
19
+ hasWorkerHeartbeatTimedOut,
20
+ hasWorkerHeartbeatTimedOutWithBusyGrace
10
21
  };