@ricsam/r5d-worker 0.0.90 → 0.0.92
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 +3 -1
- package/dist/cjs/main.cjs +288 -46
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-snapshot-recovery-runner.cjs +171 -0
- package/dist/cjs/project-worktrees.cjs +183 -23
- package/dist/cjs/supervisor.cjs +15 -5
- package/dist/mjs/main.mjs +293 -49
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-snapshot-recovery-runner.mjs +135 -0
- package/dist/mjs/project-worktrees.mjs +183 -23
- package/dist/mjs/supervisor.mjs +12 -4
- package/dist/types/main.d.ts +24 -0
- package/dist/types/project-snapshot-recovery-runner.d.ts +10 -0
- package/dist/types/project-worktrees.d.ts +13 -1
- package/dist/types/supervisor.d.ts +4 -1
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var project_snapshot_recovery_runner_exports = {};
|
|
30
|
+
__export(project_snapshot_recovery_runner_exports, {
|
|
31
|
+
PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND: () => PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
|
|
32
|
+
recoverProjectSnapshotsInChild: () => recoverProjectSnapshotsInChild,
|
|
33
|
+
runProjectSnapshotRecoveryHelper: () => runProjectSnapshotRecoveryHelper
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(project_snapshot_recovery_runner_exports);
|
|
36
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
37
|
+
var import_node_child_process = require("node:child_process");
|
|
38
|
+
var import_project_worktrees = require("./project-worktrees.cjs");
|
|
39
|
+
const PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND = "__recover-project-snapshots";
|
|
40
|
+
function requireAbsolutePath(value, label) {
|
|
41
|
+
if (!value) throw new Error(`Missing ${label}`);
|
|
42
|
+
const resolved = import_node_path.default.resolve(value);
|
|
43
|
+
if (resolved !== value) throw new Error(`${label} must be an absolute path`);
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
function parseHelperArguments(args) {
|
|
47
|
+
let projectsRoot;
|
|
48
|
+
let snapshotRoot;
|
|
49
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
50
|
+
const argument = args[index];
|
|
51
|
+
if (argument === "--projects-root") {
|
|
52
|
+
projectsRoot = args[index + 1];
|
|
53
|
+
index += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (argument === "--snapshot-root") {
|
|
57
|
+
snapshotRoot = args[index + 1];
|
|
58
|
+
index += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Unknown project snapshot recovery helper argument: ${argument ?? "(missing)"}`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
projectsRoot: requireAbsolutePath(projectsRoot, "projects root"),
|
|
65
|
+
snapshotRoot: requireAbsolutePath(snapshotRoot, "snapshot root")
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function writeHelperMessage(message) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(message)}
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
function runProjectSnapshotRecoveryHelper(args) {
|
|
73
|
+
const { projectsRoot, snapshotRoot } = parseHelperArguments(args);
|
|
74
|
+
const result = (0, import_project_worktrees.recoverStaleProjectWorktreeSnapshots)({
|
|
75
|
+
projectsRoot,
|
|
76
|
+
temporaryRoot: snapshotRoot,
|
|
77
|
+
onProgress: (progress) => writeHelperMessage({ type: "progress", progress })
|
|
78
|
+
});
|
|
79
|
+
writeHelperMessage({ type: "result", result });
|
|
80
|
+
return result.failed.length === 0 ? 0 : 1;
|
|
81
|
+
}
|
|
82
|
+
function isRecoveryProgress(value) {
|
|
83
|
+
if (!value || typeof value !== "object") return false;
|
|
84
|
+
const progress = value;
|
|
85
|
+
return progress.operation === "restore" && (progress.phase === "scanning" || progress.phase === "copying" || progress.phase === "moving" || progress.phase === "syncing" || progress.phase === "cleaning") && typeof progress.projectRoot === "string" && Number.isSafeInteger(progress.completedBranches) && Number.isSafeInteger(progress.totalBranches) && typeof progress.completedBytes === "number" && Number.isFinite(progress.completedBytes) && (progress.totalBytes === void 0 || typeof progress.totalBytes === "number" && Number.isFinite(progress.totalBytes));
|
|
86
|
+
}
|
|
87
|
+
function isRecoveryResult(value) {
|
|
88
|
+
if (!value || typeof value !== "object") return false;
|
|
89
|
+
const result = value;
|
|
90
|
+
return Array.isArray(result.restored) && Array.isArray(result.removed) && Array.isArray(result.failed);
|
|
91
|
+
}
|
|
92
|
+
function recoveryChildEnvironment() {
|
|
93
|
+
const environment = {};
|
|
94
|
+
for (const name of ["PATH", "TMPDIR", "TMP", "TEMP", "SystemRoot", "WINDIR"]) {
|
|
95
|
+
const value = process.env[name];
|
|
96
|
+
if (value !== void 0) environment[name] = value;
|
|
97
|
+
}
|
|
98
|
+
return environment;
|
|
99
|
+
}
|
|
100
|
+
async function recoverProjectSnapshotsInChild(input) {
|
|
101
|
+
const entrypoint = process.argv[1];
|
|
102
|
+
if (!entrypoint) throw new Error("Cannot locate the r5d-worker runtime entrypoint for snapshot recovery");
|
|
103
|
+
const child = (0, import_node_child_process.spawn)(
|
|
104
|
+
process.execPath,
|
|
105
|
+
[
|
|
106
|
+
entrypoint,
|
|
107
|
+
PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
|
|
108
|
+
"--projects-root",
|
|
109
|
+
import_node_path.default.resolve(input.projectsRoot),
|
|
110
|
+
"--snapshot-root",
|
|
111
|
+
import_node_path.default.resolve(input.snapshotRoot)
|
|
112
|
+
],
|
|
113
|
+
{
|
|
114
|
+
env: recoveryChildEnvironment(),
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
const terminateOnParentExit = () => {
|
|
119
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
120
|
+
};
|
|
121
|
+
const parentProcessEvents = process;
|
|
122
|
+
parentProcessEvents.once("exit", terminateOnParentExit);
|
|
123
|
+
let stdout = "";
|
|
124
|
+
let stderr = "";
|
|
125
|
+
let result;
|
|
126
|
+
child.stdout.setEncoding("utf8");
|
|
127
|
+
child.stdout.on("data", (chunk) => {
|
|
128
|
+
stdout += chunk;
|
|
129
|
+
while (true) {
|
|
130
|
+
const newline = stdout.indexOf("\n");
|
|
131
|
+
if (newline < 0) break;
|
|
132
|
+
const line = stdout.slice(0, newline);
|
|
133
|
+
stdout = stdout.slice(newline + 1);
|
|
134
|
+
if (!line.trim()) continue;
|
|
135
|
+
let message;
|
|
136
|
+
try {
|
|
137
|
+
message = JSON.parse(line);
|
|
138
|
+
} catch {
|
|
139
|
+
stderr += `Invalid recovery helper output: ${line}
|
|
140
|
+
`;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!message || typeof message !== "object") continue;
|
|
144
|
+
const record = message;
|
|
145
|
+
if (record.type === "progress" && isRecoveryProgress(record.progress)) input.onProgress?.(record.progress);
|
|
146
|
+
if (record.type === "result" && isRecoveryResult(record.result)) result = record.result;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
child.stderr.setEncoding("utf8");
|
|
150
|
+
child.stderr.on("data", (chunk) => {
|
|
151
|
+
stderr = `${stderr}${chunk}`.slice(-32768);
|
|
152
|
+
});
|
|
153
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
154
|
+
child.once("error", reject);
|
|
155
|
+
child.once("close", (code, signal) => {
|
|
156
|
+
if (signal) reject(new Error(`Project snapshot recovery helper exited on signal ${signal}`));
|
|
157
|
+
else resolve(code ?? 1);
|
|
158
|
+
});
|
|
159
|
+
}).finally(() => parentProcessEvents.off("exit", terminateOnParentExit));
|
|
160
|
+
if (exitCode !== 0 || !result) {
|
|
161
|
+
const detail = stderr.trim();
|
|
162
|
+
throw new Error(detail || `Project snapshot recovery helper exited with status ${exitCode}`);
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
167
|
+
0 && (module.exports = {
|
|
168
|
+
PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
|
|
169
|
+
recoverProjectSnapshotsInChild,
|
|
170
|
+
runProjectSnapshotRecoveryHelper
|
|
171
|
+
});
|
|
@@ -318,7 +318,7 @@ function normalizeSnapshotManifest(value) {
|
|
|
318
318
|
if (record.version !== PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION) {
|
|
319
319
|
throw new Error("Unsupported project worktree snapshot manifest version");
|
|
320
320
|
}
|
|
321
|
-
if (record.state !== "building" && record.state !== "prepared" && record.state !== "consumed") {
|
|
321
|
+
if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
|
|
322
322
|
throw new Error("Project worktree snapshot manifest has an invalid state");
|
|
323
323
|
}
|
|
324
324
|
if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
|
|
@@ -399,7 +399,7 @@ function assertSameSnapshotIdentity(expected, actual) {
|
|
|
399
399
|
function advanceSnapshotState(snapshotRoot, expected, state) {
|
|
400
400
|
const current = readSnapshotManifest(snapshotRoot);
|
|
401
401
|
assertSameSnapshotIdentity(expected, current);
|
|
402
|
-
const allowed = current.state === "building" && state === "prepared" || current.state === "prepared" && state === "consumed" || current.state === state;
|
|
402
|
+
const allowed = current.state === "building" && state === "prepared" || current.state === "prepared" && state === "restoring" || current.state === "prepared" && state === "consumed" || current.state === "restoring" && state === "consumed" || current.state === state;
|
|
403
403
|
if (!allowed) throw new Error(`Project worktree snapshot cannot advance from ${current.state} to ${state}`);
|
|
404
404
|
return current.state === state ? current : writeSnapshotManifest(snapshotRoot, { ...current, state });
|
|
405
405
|
}
|
|
@@ -423,6 +423,20 @@ function copyCheckoutTree(sourceRoot, targetRoot) {
|
|
|
423
423
|
verbatimSymlinks: true
|
|
424
424
|
});
|
|
425
425
|
}
|
|
426
|
+
function measureCheckoutTree(root) {
|
|
427
|
+
const stat = import_node_fs.default.lstatSync(root);
|
|
428
|
+
if (stat.isSymbolicLink()) return 0;
|
|
429
|
+
if (stat.isFile()) return stat.size;
|
|
430
|
+
if (!stat.isDirectory()) return 0;
|
|
431
|
+
let bytes = 0;
|
|
432
|
+
for (const entry of import_node_fs.default.readdirSync(root).sort()) bytes += measureCheckoutTree(import_node_path.default.join(root, entry));
|
|
433
|
+
return bytes;
|
|
434
|
+
}
|
|
435
|
+
function snapshotBranchMeasurements(branches, sourcePath) {
|
|
436
|
+
return new Map(
|
|
437
|
+
branches.map(({ branchName, snapshotPresent }) => [branchName, snapshotPresent ? measureCheckoutTree(sourcePath(branchName)) : 0])
|
|
438
|
+
);
|
|
439
|
+
}
|
|
426
440
|
function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
427
441
|
let entries;
|
|
428
442
|
try {
|
|
@@ -445,7 +459,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
|
|
|
445
459
|
}
|
|
446
460
|
}
|
|
447
461
|
}
|
|
448
|
-
function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
|
|
462
|
+
function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
|
|
449
463
|
const resolvedProjectRoot = import_node_path.default.resolve(projectRoot);
|
|
450
464
|
const resolvedTemporaryRoot = import_node_path.default.resolve(temporaryRoot ?? import_node_os.default.tmpdir());
|
|
451
465
|
assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
|
|
@@ -472,13 +486,57 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
|
|
|
472
486
|
branches: branchSnapshots,
|
|
473
487
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
474
488
|
});
|
|
489
|
+
onProgress?.({
|
|
490
|
+
operation: "snapshot",
|
|
491
|
+
phase: "scanning",
|
|
492
|
+
projectRoot: resolvedProjectRoot,
|
|
493
|
+
completedBranches: 0,
|
|
494
|
+
totalBranches: branchSnapshots.length,
|
|
495
|
+
completedBytes: 0
|
|
496
|
+
});
|
|
497
|
+
const branchBytes = snapshotBranchMeasurements(branchSnapshots, (branchName) => branchPath(resolvedProjectRoot, branchName));
|
|
498
|
+
const totalBytes = [...branchBytes.values()].reduce((total, bytes) => total + bytes, 0);
|
|
499
|
+
let completedBytes = 0;
|
|
500
|
+
let completedBranches = 0;
|
|
475
501
|
for (const { branchName, snapshotPresent } of branchSnapshots) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
502
|
+
onProgress?.({
|
|
503
|
+
operation: "snapshot",
|
|
504
|
+
phase: "copying",
|
|
505
|
+
projectRoot: resolvedProjectRoot,
|
|
506
|
+
branchName,
|
|
507
|
+
completedBranches,
|
|
508
|
+
totalBranches: branchSnapshots.length,
|
|
509
|
+
completedBytes,
|
|
510
|
+
totalBytes
|
|
511
|
+
});
|
|
512
|
+
if (snapshotPresent) {
|
|
513
|
+
const snapshotPath = fullSnapshotPath(root, branchName);
|
|
514
|
+
copyCheckoutTree(branchPath(resolvedProjectRoot, branchName), snapshotPath);
|
|
515
|
+
paths.set(branchName, snapshotPath);
|
|
516
|
+
}
|
|
517
|
+
completedBytes += branchBytes.get(branchName) ?? 0;
|
|
518
|
+
completedBranches += 1;
|
|
519
|
+
onProgress?.({
|
|
520
|
+
operation: "snapshot",
|
|
521
|
+
phase: "copying",
|
|
522
|
+
projectRoot: resolvedProjectRoot,
|
|
523
|
+
branchName,
|
|
524
|
+
completedBranches,
|
|
525
|
+
totalBranches: branchSnapshots.length,
|
|
526
|
+
completedBytes,
|
|
527
|
+
totalBytes
|
|
528
|
+
});
|
|
480
529
|
}
|
|
481
530
|
const treesRoot = import_node_path.default.join(root, "trees");
|
|
531
|
+
onProgress?.({
|
|
532
|
+
operation: "snapshot",
|
|
533
|
+
phase: "syncing",
|
|
534
|
+
projectRoot: resolvedProjectRoot,
|
|
535
|
+
completedBranches,
|
|
536
|
+
totalBranches: branchSnapshots.length,
|
|
537
|
+
completedBytes,
|
|
538
|
+
totalBytes
|
|
539
|
+
});
|
|
482
540
|
if (import_node_fs.default.existsSync(treesRoot)) fsyncTree(treesRoot);
|
|
483
541
|
manifest = advanceSnapshotState(root, manifest, "prepared");
|
|
484
542
|
return { root, manifest, paths };
|
|
@@ -487,33 +545,127 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
|
|
|
487
545
|
throw error;
|
|
488
546
|
}
|
|
489
547
|
}
|
|
490
|
-
function restoreProjectWorktreeSnapshot(snapshotRoot, manifest) {
|
|
491
|
-
|
|
548
|
+
function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
|
|
549
|
+
let current = readSnapshotManifest(snapshotRoot);
|
|
492
550
|
assertSameSnapshotIdentity(manifest, current);
|
|
493
|
-
if (current.state !== "prepared"
|
|
551
|
+
if (current.state !== "prepared" && current.state !== "restoring") {
|
|
552
|
+
throw new Error(`Project worktree snapshot is ${current.state}, not prepared or restoring`);
|
|
553
|
+
}
|
|
554
|
+
const treesRoot = import_node_path.default.join(snapshotRoot, "trees");
|
|
555
|
+
const treesRootPresent = import_node_fs.default.existsSync(treesRoot);
|
|
556
|
+
const treesRootStat = treesRootPresent ? import_node_fs.default.lstatSync(treesRoot) : null;
|
|
557
|
+
if (treesRootStat && (!treesRootStat.isDirectory() || treesRootStat.isSymbolicLink())) {
|
|
558
|
+
throw new Error("Project worktree snapshot trees root must be a real directory");
|
|
559
|
+
}
|
|
560
|
+
const branchSourceRoot = current.state === "restoring" && !treesRootPresent ? current.projectRoot : treesRoot;
|
|
494
561
|
for (const { branchName, snapshotPresent } of current.branches) {
|
|
495
562
|
const targetPath = branchPath(current.projectRoot, branchName);
|
|
496
563
|
assertNoSymlinkBranchPathComponents(current.projectRoot, targetPath);
|
|
497
|
-
|
|
498
|
-
|
|
564
|
+
const sourcePath = import_node_path.default.join(branchSourceRoot, ...branchName.split("/"));
|
|
565
|
+
if (!snapshotPresent) {
|
|
566
|
+
if (current.state === "restoring" && !treesRootPresent && import_node_fs.default.existsSync(sourcePath)) {
|
|
567
|
+
throw new Error(`Restored project branch unexpectedly exists: ${branchName}`);
|
|
568
|
+
}
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
499
571
|
const stat = import_node_fs.default.lstatSync(sourcePath);
|
|
500
572
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
501
573
|
throw new Error(`Project worktree snapshot branch must be a real directory: ${branchName}`);
|
|
502
574
|
}
|
|
503
575
|
}
|
|
576
|
+
onProgress?.({
|
|
577
|
+
operation: "restore",
|
|
578
|
+
phase: "scanning",
|
|
579
|
+
projectRoot: current.projectRoot,
|
|
580
|
+
completedBranches: 0,
|
|
581
|
+
totalBranches: current.branches.length,
|
|
582
|
+
completedBytes: 0
|
|
583
|
+
});
|
|
584
|
+
const branchBytes = snapshotBranchMeasurements(current.branches, (branchName) => import_node_path.default.join(branchSourceRoot, ...branchName.split("/")));
|
|
585
|
+
const totalBytes = [...branchBytes.values()].reduce((total, bytes) => total + bytes, 0);
|
|
586
|
+
const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
|
|
587
|
+
const destinationParent = import_node_path.default.dirname(current.projectRoot);
|
|
588
|
+
import_node_fs.default.mkdirSync(destinationParent, { recursive: true });
|
|
589
|
+
const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === import_node_fs.default.lstatSync(destinationParent).dev);
|
|
590
|
+
if (canMoveWholeSnapshot) {
|
|
591
|
+
if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
|
|
592
|
+
onProgress?.({
|
|
593
|
+
operation: "restore",
|
|
594
|
+
phase: "moving",
|
|
595
|
+
projectRoot: current.projectRoot,
|
|
596
|
+
completedBranches: treesRootPresent ? 0 : current.branches.length,
|
|
597
|
+
totalBranches: current.branches.length,
|
|
598
|
+
completedBytes: treesRootPresent ? 0 : totalBytes,
|
|
599
|
+
totalBytes
|
|
600
|
+
});
|
|
601
|
+
if (treesRootPresent) {
|
|
602
|
+
import_node_fs.default.rmSync(current.projectRoot, { recursive: true, force: true });
|
|
603
|
+
fsyncDirectoryChain(destinationParent, import_node_path.default.dirname(destinationParent));
|
|
604
|
+
import_node_fs.default.renameSync(treesRoot, current.projectRoot);
|
|
605
|
+
fsyncDirectory(snapshotRoot);
|
|
606
|
+
}
|
|
607
|
+
fsyncManagedProjectRootEntry(current.projectRoot);
|
|
608
|
+
onProgress?.({
|
|
609
|
+
operation: "restore",
|
|
610
|
+
phase: "moving",
|
|
611
|
+
projectRoot: current.projectRoot,
|
|
612
|
+
completedBranches: current.branches.length,
|
|
613
|
+
totalBranches: current.branches.length,
|
|
614
|
+
completedBytes: totalBytes,
|
|
615
|
+
totalBytes
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (current.state === "restoring") {
|
|
620
|
+
throw new Error("Project worktree snapshot entered atomic restore state without a movable snapshot tree");
|
|
621
|
+
}
|
|
504
622
|
for (const { branchName } of [...current.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
|
|
505
623
|
const targetPath = branchPath(current.projectRoot, branchName);
|
|
506
624
|
const targetParent = import_node_path.default.dirname(targetPath);
|
|
507
625
|
import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
|
|
508
626
|
if (import_node_fs.default.existsSync(targetParent)) fsyncDirectoryChain(targetParent, current.projectRoot);
|
|
509
627
|
}
|
|
628
|
+
let completedBytes = 0;
|
|
629
|
+
let completedBranches = 0;
|
|
510
630
|
for (const { branchName, snapshotPresent } of current.branches) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
631
|
+
onProgress?.({
|
|
632
|
+
operation: "restore",
|
|
633
|
+
phase: "copying",
|
|
634
|
+
projectRoot: current.projectRoot,
|
|
635
|
+
branchName,
|
|
636
|
+
completedBranches,
|
|
637
|
+
totalBranches: current.branches.length,
|
|
638
|
+
completedBytes,
|
|
639
|
+
totalBytes
|
|
640
|
+
});
|
|
641
|
+
if (snapshotPresent) {
|
|
642
|
+
const targetPath = branchPath(current.projectRoot, branchName);
|
|
643
|
+
copyCheckoutTree(fullSnapshotPath(snapshotRoot, branchName), targetPath);
|
|
644
|
+
fsyncTree(targetPath);
|
|
645
|
+
fsyncDirectoryChain(import_node_path.default.dirname(targetPath), current.projectRoot);
|
|
646
|
+
}
|
|
647
|
+
completedBytes += branchBytes.get(branchName) ?? 0;
|
|
648
|
+
completedBranches += 1;
|
|
649
|
+
onProgress?.({
|
|
650
|
+
operation: "restore",
|
|
651
|
+
phase: "copying",
|
|
652
|
+
projectRoot: current.projectRoot,
|
|
653
|
+
branchName,
|
|
654
|
+
completedBranches,
|
|
655
|
+
totalBranches: current.branches.length,
|
|
656
|
+
completedBytes,
|
|
657
|
+
totalBytes
|
|
658
|
+
});
|
|
516
659
|
}
|
|
660
|
+
onProgress?.({
|
|
661
|
+
operation: "restore",
|
|
662
|
+
phase: "syncing",
|
|
663
|
+
projectRoot: current.projectRoot,
|
|
664
|
+
completedBranches,
|
|
665
|
+
totalBranches: current.branches.length,
|
|
666
|
+
completedBytes,
|
|
667
|
+
totalBytes
|
|
668
|
+
});
|
|
517
669
|
import_node_fs.default.mkdirSync(current.projectRoot, { recursive: true });
|
|
518
670
|
fsyncManagedProjectRootEntry(current.projectRoot);
|
|
519
671
|
}
|
|
@@ -534,7 +686,7 @@ function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFs
|
|
|
534
686
|
function rollbackProjectWorktreeSnapshot(snapshot) {
|
|
535
687
|
const current = readSnapshotManifest(snapshot.root);
|
|
536
688
|
assertSameSnapshotIdentity(snapshot.manifest, current);
|
|
537
|
-
if (current.state === "prepared") {
|
|
689
|
+
if (current.state === "prepared" || current.state === "restoring") {
|
|
538
690
|
restoreProjectWorktreeSnapshot(snapshot.root, current);
|
|
539
691
|
consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, current);
|
|
540
692
|
return;
|
|
@@ -616,8 +768,8 @@ function recoverStaleProjectWorktreeSnapshots(input) {
|
|
|
616
768
|
assertNoSymlinkPathComponents(projectsRoot, manifest.projectRoot);
|
|
617
769
|
const ownedByThisProcessSession = manifest.ownerProcessId === currentProcessId && manifest.ownerSessionId === currentOwnerSessionId;
|
|
618
770
|
if (ownedByThisProcessSession || ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
|
|
619
|
-
if (manifest.state === "prepared") {
|
|
620
|
-
restoreProjectWorktreeSnapshot(candidate, manifest);
|
|
771
|
+
if (manifest.state === "prepared" || manifest.state === "restoring") {
|
|
772
|
+
restoreProjectWorktreeSnapshot(candidate, manifest, input.onProgress);
|
|
621
773
|
const consumed = advanceSnapshotState(candidate, manifest, "consumed");
|
|
622
774
|
removeSnapshotStorage(candidate);
|
|
623
775
|
assertSameSnapshotIdentity(manifest, consumed);
|
|
@@ -803,7 +955,11 @@ async function initializeLinkedLayout(input) {
|
|
|
803
955
|
);
|
|
804
956
|
const checkoutPath = branchPath(input.projectRoot, branch.branchName);
|
|
805
957
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
|
|
806
|
-
await gitAsync(
|
|
958
|
+
await gitAsync(
|
|
959
|
+
primaryPath,
|
|
960
|
+
["worktree", "add", "--force", checkoutPath, branch.branchName],
|
|
961
|
+
`create linked worktree ${branch.branchName}`
|
|
962
|
+
);
|
|
807
963
|
}
|
|
808
964
|
return mirrorHeads;
|
|
809
965
|
}
|
|
@@ -837,7 +993,11 @@ async function reconcileLinkedLayout(input) {
|
|
|
837
993
|
import_node_fs.default.rmSync(checkoutPath, { recursive: true, force: true });
|
|
838
994
|
await gitAsync(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
|
|
839
995
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(checkoutPath), { recursive: true });
|
|
840
|
-
await gitAsync(
|
|
996
|
+
await gitAsync(
|
|
997
|
+
primaryPath,
|
|
998
|
+
["worktree", "add", "--force", checkoutPath, branch.branchName],
|
|
999
|
+
`create linked worktree ${branch.branchName}`
|
|
1000
|
+
);
|
|
841
1001
|
}
|
|
842
1002
|
return { mirrorHeads, mutatedBranches };
|
|
843
1003
|
}
|
|
@@ -864,7 +1024,7 @@ async function ensureProjectWorktrees(input) {
|
|
|
864
1024
|
assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
|
|
865
1025
|
let snapshot;
|
|
866
1026
|
const ensureSnapshot = () => {
|
|
867
|
-
snapshot ??= snapshotBranchTrees(projectRoot, input.branches, snapshotStorageRoot);
|
|
1027
|
+
snapshot ??= snapshotBranchTrees(projectRoot, input.branches, snapshotStorageRoot, input.onSnapshotProgress);
|
|
868
1028
|
return snapshot;
|
|
869
1029
|
};
|
|
870
1030
|
try {
|
package/dist/cjs/supervisor.cjs
CHANGED
|
@@ -21,10 +21,12 @@ __export(supervisor_exports, {
|
|
|
21
21
|
WORKER_CREDENTIAL_RESTART_EXIT_CODE: () => WORKER_CREDENTIAL_RESTART_EXIT_CODE,
|
|
22
22
|
WORKER_RECONNECT_DELAY_MS: () => WORKER_RECONNECT_DELAY_MS,
|
|
23
23
|
WORKER_RECONNECT_EXIT_CODE: () => WORKER_RECONNECT_EXIT_CODE,
|
|
24
|
+
WORKER_RECONNECT_JITTER_MS: () => WORKER_RECONNECT_JITTER_MS,
|
|
24
25
|
WORKER_RELOAD_EXIT_CODE: () => WORKER_RELOAD_EXIT_CODE,
|
|
25
26
|
WORKER_RUNTIME_ENV: () => WORKER_RUNTIME_ENV,
|
|
26
27
|
isRetryableWorkerServerStatus: () => isRetryableWorkerServerStatus,
|
|
27
|
-
superviseWorkerRuntime: () => superviseWorkerRuntime
|
|
28
|
+
superviseWorkerRuntime: () => superviseWorkerRuntime,
|
|
29
|
+
workerReconnectDelayMs: () => workerReconnectDelayMs
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(supervisor_exports);
|
|
30
32
|
var import_node_os = require("node:os");
|
|
@@ -34,6 +36,7 @@ const WORKER_RELOAD_EXIT_CODE = 75;
|
|
|
34
36
|
const WORKER_RECONNECT_EXIT_CODE = 76;
|
|
35
37
|
const WORKER_CREDENTIAL_RESTART_EXIT_CODE = 77;
|
|
36
38
|
const WORKER_RECONNECT_DELAY_MS = 2e3;
|
|
39
|
+
const WORKER_RECONNECT_JITTER_MS = 3e3;
|
|
37
40
|
const SUPERVISOR_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
38
41
|
function defaultRuntimeSpawner(argv, options) {
|
|
39
42
|
return Bun.spawn(argv, options);
|
|
@@ -41,6 +44,10 @@ function defaultRuntimeSpawner(argv, options) {
|
|
|
41
44
|
function defaultRuntimeDelay(delayMs) {
|
|
42
45
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
43
46
|
}
|
|
47
|
+
function workerReconnectDelayMs(random = Math.random) {
|
|
48
|
+
const sample = Math.max(0, Math.min(1, random()));
|
|
49
|
+
return WORKER_RECONNECT_DELAY_MS + Math.floor(sample * WORKER_RECONNECT_JITTER_MS);
|
|
50
|
+
}
|
|
44
51
|
async function defaultRuntimeReaper(runtime) {
|
|
45
52
|
await (0, import_process_tree.terminateProcessTree)(runtime);
|
|
46
53
|
}
|
|
@@ -53,7 +60,7 @@ function signalExitCode(signal) {
|
|
|
53
60
|
function osSignalNumber(signal) {
|
|
54
61
|
return import_node_os.constants.signals[signal];
|
|
55
62
|
}
|
|
56
|
-
async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay, reapCredentialRuntime = defaultRuntimeReaper) {
|
|
63
|
+
async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = defaultRuntimeSpawner, waitBeforeReconnect = defaultRuntimeDelay, reapCredentialRuntime = defaultRuntimeReaper, reconnectDelay = workerReconnectDelayMs) {
|
|
57
64
|
if (!process.argv[1]) {
|
|
58
65
|
throw new Error("Cannot locate the r5d-worker entrypoint");
|
|
59
66
|
}
|
|
@@ -105,9 +112,10 @@ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = de
|
|
|
105
112
|
continue;
|
|
106
113
|
}
|
|
107
114
|
if (exitCode === WORKER_RECONNECT_EXIT_CODE) {
|
|
108
|
-
|
|
115
|
+
const reconnectDelayMs = reconnectDelay();
|
|
116
|
+
process.stderr.write(`[r5d-worker] reconnecting in ${reconnectDelayMs}ms
|
|
109
117
|
`);
|
|
110
|
-
await waitBeforeReconnect(
|
|
118
|
+
await waitBeforeReconnect(reconnectDelayMs);
|
|
111
119
|
if (receivedSignal) return signalExitCode(receivedSignal);
|
|
112
120
|
continue;
|
|
113
121
|
}
|
|
@@ -123,8 +131,10 @@ async function superviseWorkerRuntime(argv, env = process.env, spawnRuntime = de
|
|
|
123
131
|
WORKER_CREDENTIAL_RESTART_EXIT_CODE,
|
|
124
132
|
WORKER_RECONNECT_DELAY_MS,
|
|
125
133
|
WORKER_RECONNECT_EXIT_CODE,
|
|
134
|
+
WORKER_RECONNECT_JITTER_MS,
|
|
126
135
|
WORKER_RELOAD_EXIT_CODE,
|
|
127
136
|
WORKER_RUNTIME_ENV,
|
|
128
137
|
isRetryableWorkerServerStatus,
|
|
129
|
-
superviseWorkerRuntime
|
|
138
|
+
superviseWorkerRuntime,
|
|
139
|
+
workerReconnectDelayMs
|
|
130
140
|
});
|