@ricsam/r5d-worker 0.0.89 → 0.0.91

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.89",
3
+ "version": "0.0.91",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,135 @@
1
+ import path from "node:path";
2
+ import { spawn } from "node:child_process";
3
+ import { recoverStaleProjectWorktreeSnapshots } from "./project-worktrees.mjs";
4
+ const PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND = "__recover-project-snapshots";
5
+ function requireAbsolutePath(value, label) {
6
+ if (!value) throw new Error(`Missing ${label}`);
7
+ const resolved = path.resolve(value);
8
+ if (resolved !== value) throw new Error(`${label} must be an absolute path`);
9
+ return resolved;
10
+ }
11
+ function parseHelperArguments(args) {
12
+ let projectsRoot;
13
+ let snapshotRoot;
14
+ for (let index = 0; index < args.length; index += 1) {
15
+ const argument = args[index];
16
+ if (argument === "--projects-root") {
17
+ projectsRoot = args[index + 1];
18
+ index += 1;
19
+ continue;
20
+ }
21
+ if (argument === "--snapshot-root") {
22
+ snapshotRoot = args[index + 1];
23
+ index += 1;
24
+ continue;
25
+ }
26
+ throw new Error(`Unknown project snapshot recovery helper argument: ${argument ?? "(missing)"}`);
27
+ }
28
+ return {
29
+ projectsRoot: requireAbsolutePath(projectsRoot, "projects root"),
30
+ snapshotRoot: requireAbsolutePath(snapshotRoot, "snapshot root")
31
+ };
32
+ }
33
+ function writeHelperMessage(message) {
34
+ process.stdout.write(`${JSON.stringify(message)}
35
+ `);
36
+ }
37
+ function runProjectSnapshotRecoveryHelper(args) {
38
+ const { projectsRoot, snapshotRoot } = parseHelperArguments(args);
39
+ const result = recoverStaleProjectWorktreeSnapshots({
40
+ projectsRoot,
41
+ temporaryRoot: snapshotRoot,
42
+ onProgress: (progress) => writeHelperMessage({ type: "progress", progress })
43
+ });
44
+ writeHelperMessage({ type: "result", result });
45
+ return result.failed.length === 0 ? 0 : 1;
46
+ }
47
+ function isRecoveryProgress(value) {
48
+ if (!value || typeof value !== "object") return false;
49
+ const progress = value;
50
+ 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));
51
+ }
52
+ function isRecoveryResult(value) {
53
+ if (!value || typeof value !== "object") return false;
54
+ const result = value;
55
+ return Array.isArray(result.restored) && Array.isArray(result.removed) && Array.isArray(result.failed);
56
+ }
57
+ function recoveryChildEnvironment() {
58
+ const environment = {};
59
+ for (const name of ["PATH", "TMPDIR", "TMP", "TEMP", "SystemRoot", "WINDIR"]) {
60
+ const value = process.env[name];
61
+ if (value !== void 0) environment[name] = value;
62
+ }
63
+ return environment;
64
+ }
65
+ async function recoverProjectSnapshotsInChild(input) {
66
+ const entrypoint = process.argv[1];
67
+ if (!entrypoint) throw new Error("Cannot locate the r5d-worker runtime entrypoint for snapshot recovery");
68
+ const child = spawn(
69
+ process.execPath,
70
+ [
71
+ entrypoint,
72
+ PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
73
+ "--projects-root",
74
+ path.resolve(input.projectsRoot),
75
+ "--snapshot-root",
76
+ path.resolve(input.snapshotRoot)
77
+ ],
78
+ {
79
+ env: recoveryChildEnvironment(),
80
+ stdio: ["ignore", "pipe", "pipe"]
81
+ }
82
+ );
83
+ const terminateOnParentExit = () => {
84
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
85
+ };
86
+ const parentProcessEvents = process;
87
+ parentProcessEvents.once("exit", terminateOnParentExit);
88
+ let stdout = "";
89
+ let stderr = "";
90
+ let result;
91
+ child.stdout.setEncoding("utf8");
92
+ child.stdout.on("data", (chunk) => {
93
+ stdout += chunk;
94
+ while (true) {
95
+ const newline = stdout.indexOf("\n");
96
+ if (newline < 0) break;
97
+ const line = stdout.slice(0, newline);
98
+ stdout = stdout.slice(newline + 1);
99
+ if (!line.trim()) continue;
100
+ let message;
101
+ try {
102
+ message = JSON.parse(line);
103
+ } catch {
104
+ stderr += `Invalid recovery helper output: ${line}
105
+ `;
106
+ continue;
107
+ }
108
+ if (!message || typeof message !== "object") continue;
109
+ const record = message;
110
+ if (record.type === "progress" && isRecoveryProgress(record.progress)) input.onProgress?.(record.progress);
111
+ if (record.type === "result" && isRecoveryResult(record.result)) result = record.result;
112
+ }
113
+ });
114
+ child.stderr.setEncoding("utf8");
115
+ child.stderr.on("data", (chunk) => {
116
+ stderr = `${stderr}${chunk}`.slice(-32768);
117
+ });
118
+ const exitCode = await new Promise((resolve, reject) => {
119
+ child.once("error", reject);
120
+ child.once("close", (code, signal) => {
121
+ if (signal) reject(new Error(`Project snapshot recovery helper exited on signal ${signal}`));
122
+ else resolve(code ?? 1);
123
+ });
124
+ }).finally(() => parentProcessEvents.off("exit", terminateOnParentExit));
125
+ if (exitCode !== 0 || !result) {
126
+ const detail = stderr.trim();
127
+ throw new Error(detail || `Project snapshot recovery helper exited with status ${exitCode}`);
128
+ }
129
+ return result;
130
+ }
131
+ export {
132
+ PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
133
+ recoverProjectSnapshotsInChild,
134
+ runProjectSnapshotRecoveryHelper
135
+ };
@@ -265,7 +265,7 @@ function normalizeSnapshotManifest(value) {
265
265
  if (record.version !== PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION) {
266
266
  throw new Error("Unsupported project worktree snapshot manifest version");
267
267
  }
268
- if (record.state !== "building" && record.state !== "prepared" && record.state !== "consumed") {
268
+ if (record.state !== "building" && record.state !== "prepared" && record.state !== "restoring" && record.state !== "consumed") {
269
269
  throw new Error("Project worktree snapshot manifest has an invalid state");
270
270
  }
271
271
  if (!Number.isSafeInteger(record.ownerProcessId) || record.ownerProcessId <= 0) {
@@ -346,7 +346,7 @@ function assertSameSnapshotIdentity(expected, actual) {
346
346
  function advanceSnapshotState(snapshotRoot, expected, state) {
347
347
  const current = readSnapshotManifest(snapshotRoot);
348
348
  assertSameSnapshotIdentity(expected, current);
349
- const allowed = current.state === "building" && state === "prepared" || current.state === "prepared" && state === "consumed" || current.state === state;
349
+ 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;
350
350
  if (!allowed) throw new Error(`Project worktree snapshot cannot advance from ${current.state} to ${state}`);
351
351
  return current.state === state ? current : writeSnapshotManifest(snapshotRoot, { ...current, state });
352
352
  }
@@ -370,6 +370,20 @@ function copyCheckoutTree(sourceRoot, targetRoot) {
370
370
  verbatimSymlinks: true
371
371
  });
372
372
  }
373
+ function measureCheckoutTree(root) {
374
+ const stat = fs.lstatSync(root);
375
+ if (stat.isSymbolicLink()) return 0;
376
+ if (stat.isFile()) return stat.size;
377
+ if (!stat.isDirectory()) return 0;
378
+ let bytes = 0;
379
+ for (const entry of fs.readdirSync(root).sort()) bytes += measureCheckoutTree(path.join(root, entry));
380
+ return bytes;
381
+ }
382
+ function snapshotBranchMeasurements(branches, sourcePath) {
383
+ return new Map(
384
+ branches.map(({ branchName, snapshotPresent }) => [branchName, snapshotPresent ? measureCheckoutTree(sourcePath(branchName)) : 0])
385
+ );
386
+ }
373
387
  function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
374
388
  let entries;
375
389
  try {
@@ -392,7 +406,7 @@ function assertNoOutstandingSnapshotForProject(temporaryRoot, projectRoot) {
392
406
  }
393
407
  }
394
408
  }
395
- function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
409
+ function snapshotBranchTrees(projectRoot, branches, temporaryRoot, onProgress) {
396
410
  const resolvedProjectRoot = path.resolve(projectRoot);
397
411
  const resolvedTemporaryRoot = path.resolve(temporaryRoot ?? os.tmpdir());
398
412
  assertNoOutstandingSnapshotForProject(resolvedTemporaryRoot, resolvedProjectRoot);
@@ -419,13 +433,57 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
419
433
  branches: branchSnapshots,
420
434
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
421
435
  });
436
+ onProgress?.({
437
+ operation: "snapshot",
438
+ phase: "scanning",
439
+ projectRoot: resolvedProjectRoot,
440
+ completedBranches: 0,
441
+ totalBranches: branchSnapshots.length,
442
+ completedBytes: 0
443
+ });
444
+ const branchBytes = snapshotBranchMeasurements(branchSnapshots, (branchName) => branchPath(resolvedProjectRoot, branchName));
445
+ const totalBytes = [...branchBytes.values()].reduce((total, bytes) => total + bytes, 0);
446
+ let completedBytes = 0;
447
+ let completedBranches = 0;
422
448
  for (const { branchName, snapshotPresent } of branchSnapshots) {
423
- if (!snapshotPresent) continue;
424
- const snapshotPath = fullSnapshotPath(root, branchName);
425
- copyCheckoutTree(branchPath(resolvedProjectRoot, branchName), snapshotPath);
426
- paths.set(branchName, snapshotPath);
449
+ onProgress?.({
450
+ operation: "snapshot",
451
+ phase: "copying",
452
+ projectRoot: resolvedProjectRoot,
453
+ branchName,
454
+ completedBranches,
455
+ totalBranches: branchSnapshots.length,
456
+ completedBytes,
457
+ totalBytes
458
+ });
459
+ if (snapshotPresent) {
460
+ const snapshotPath = fullSnapshotPath(root, branchName);
461
+ copyCheckoutTree(branchPath(resolvedProjectRoot, branchName), snapshotPath);
462
+ paths.set(branchName, snapshotPath);
463
+ }
464
+ completedBytes += branchBytes.get(branchName) ?? 0;
465
+ completedBranches += 1;
466
+ onProgress?.({
467
+ operation: "snapshot",
468
+ phase: "copying",
469
+ projectRoot: resolvedProjectRoot,
470
+ branchName,
471
+ completedBranches,
472
+ totalBranches: branchSnapshots.length,
473
+ completedBytes,
474
+ totalBytes
475
+ });
427
476
  }
428
477
  const treesRoot = path.join(root, "trees");
478
+ onProgress?.({
479
+ operation: "snapshot",
480
+ phase: "syncing",
481
+ projectRoot: resolvedProjectRoot,
482
+ completedBranches,
483
+ totalBranches: branchSnapshots.length,
484
+ completedBytes,
485
+ totalBytes
486
+ });
429
487
  if (fs.existsSync(treesRoot)) fsyncTree(treesRoot);
430
488
  manifest = advanceSnapshotState(root, manifest, "prepared");
431
489
  return { root, manifest, paths };
@@ -434,33 +492,127 @@ function snapshotBranchTrees(projectRoot, branches, temporaryRoot) {
434
492
  throw error;
435
493
  }
436
494
  }
437
- function restoreProjectWorktreeSnapshot(snapshotRoot, manifest) {
438
- const current = readSnapshotManifest(snapshotRoot);
495
+ function restoreProjectWorktreeSnapshot(snapshotRoot, manifest, onProgress) {
496
+ let current = readSnapshotManifest(snapshotRoot);
439
497
  assertSameSnapshotIdentity(manifest, current);
440
- if (current.state !== "prepared") throw new Error(`Project worktree snapshot is ${current.state}, not prepared`);
498
+ if (current.state !== "prepared" && current.state !== "restoring") {
499
+ throw new Error(`Project worktree snapshot is ${current.state}, not prepared or restoring`);
500
+ }
501
+ const treesRoot = path.join(snapshotRoot, "trees");
502
+ const treesRootPresent = fs.existsSync(treesRoot);
503
+ const treesRootStat = treesRootPresent ? fs.lstatSync(treesRoot) : null;
504
+ if (treesRootStat && (!treesRootStat.isDirectory() || treesRootStat.isSymbolicLink())) {
505
+ throw new Error("Project worktree snapshot trees root must be a real directory");
506
+ }
507
+ const branchSourceRoot = current.state === "restoring" && !treesRootPresent ? current.projectRoot : treesRoot;
441
508
  for (const { branchName, snapshotPresent } of current.branches) {
442
509
  const targetPath = branchPath(current.projectRoot, branchName);
443
510
  assertNoSymlinkBranchPathComponents(current.projectRoot, targetPath);
444
- if (!snapshotPresent) continue;
445
- const sourcePath = fullSnapshotPath(snapshotRoot, branchName);
511
+ const sourcePath = path.join(branchSourceRoot, ...branchName.split("/"));
512
+ if (!snapshotPresent) {
513
+ if (current.state === "restoring" && !treesRootPresent && fs.existsSync(sourcePath)) {
514
+ throw new Error(`Restored project branch unexpectedly exists: ${branchName}`);
515
+ }
516
+ continue;
517
+ }
446
518
  const stat = fs.lstatSync(sourcePath);
447
519
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
448
520
  throw new Error(`Project worktree snapshot branch must be a real directory: ${branchName}`);
449
521
  }
450
522
  }
523
+ onProgress?.({
524
+ operation: "restore",
525
+ phase: "scanning",
526
+ projectRoot: current.projectRoot,
527
+ completedBranches: 0,
528
+ totalBranches: current.branches.length,
529
+ completedBytes: 0
530
+ });
531
+ const branchBytes = snapshotBranchMeasurements(current.branches, (branchName) => path.join(branchSourceRoot, ...branchName.split("/")));
532
+ const totalBytes = [...branchBytes.values()].reduce((total, bytes) => total + bytes, 0);
533
+ const presentBranches = current.branches.filter(({ snapshotPresent }) => snapshotPresent);
534
+ const destinationParent = path.dirname(current.projectRoot);
535
+ fs.mkdirSync(destinationParent, { recursive: true });
536
+ const canMoveWholeSnapshot = presentBranches.length > 0 && (current.state === "restoring" || treesRootStat !== null && treesRootStat.dev === fs.lstatSync(destinationParent).dev);
537
+ if (canMoveWholeSnapshot) {
538
+ if (current.state === "prepared") current = advanceSnapshotState(snapshotRoot, current, "restoring");
539
+ onProgress?.({
540
+ operation: "restore",
541
+ phase: "moving",
542
+ projectRoot: current.projectRoot,
543
+ completedBranches: treesRootPresent ? 0 : current.branches.length,
544
+ totalBranches: current.branches.length,
545
+ completedBytes: treesRootPresent ? 0 : totalBytes,
546
+ totalBytes
547
+ });
548
+ if (treesRootPresent) {
549
+ fs.rmSync(current.projectRoot, { recursive: true, force: true });
550
+ fsyncDirectoryChain(destinationParent, path.dirname(destinationParent));
551
+ fs.renameSync(treesRoot, current.projectRoot);
552
+ fsyncDirectory(snapshotRoot);
553
+ }
554
+ fsyncManagedProjectRootEntry(current.projectRoot);
555
+ onProgress?.({
556
+ operation: "restore",
557
+ phase: "moving",
558
+ projectRoot: current.projectRoot,
559
+ completedBranches: current.branches.length,
560
+ totalBranches: current.branches.length,
561
+ completedBytes: totalBytes,
562
+ totalBytes
563
+ });
564
+ return;
565
+ }
566
+ if (current.state === "restoring") {
567
+ throw new Error("Project worktree snapshot entered atomic restore state without a movable snapshot tree");
568
+ }
451
569
  for (const { branchName } of [...current.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
452
570
  const targetPath = branchPath(current.projectRoot, branchName);
453
571
  const targetParent = path.dirname(targetPath);
454
572
  fs.rmSync(targetPath, { recursive: true, force: true });
455
573
  if (fs.existsSync(targetParent)) fsyncDirectoryChain(targetParent, current.projectRoot);
456
574
  }
575
+ let completedBytes = 0;
576
+ let completedBranches = 0;
457
577
  for (const { branchName, snapshotPresent } of current.branches) {
458
- if (!snapshotPresent) continue;
459
- const targetPath = branchPath(current.projectRoot, branchName);
460
- copyCheckoutTree(fullSnapshotPath(snapshotRoot, branchName), targetPath);
461
- fsyncTree(targetPath);
462
- fsyncDirectoryChain(path.dirname(targetPath), current.projectRoot);
578
+ onProgress?.({
579
+ operation: "restore",
580
+ phase: "copying",
581
+ projectRoot: current.projectRoot,
582
+ branchName,
583
+ completedBranches,
584
+ totalBranches: current.branches.length,
585
+ completedBytes,
586
+ totalBytes
587
+ });
588
+ if (snapshotPresent) {
589
+ const targetPath = branchPath(current.projectRoot, branchName);
590
+ copyCheckoutTree(fullSnapshotPath(snapshotRoot, branchName), targetPath);
591
+ fsyncTree(targetPath);
592
+ fsyncDirectoryChain(path.dirname(targetPath), current.projectRoot);
593
+ }
594
+ completedBytes += branchBytes.get(branchName) ?? 0;
595
+ completedBranches += 1;
596
+ onProgress?.({
597
+ operation: "restore",
598
+ phase: "copying",
599
+ projectRoot: current.projectRoot,
600
+ branchName,
601
+ completedBranches,
602
+ totalBranches: current.branches.length,
603
+ completedBytes,
604
+ totalBytes
605
+ });
463
606
  }
607
+ onProgress?.({
608
+ operation: "restore",
609
+ phase: "syncing",
610
+ projectRoot: current.projectRoot,
611
+ completedBranches,
612
+ totalBranches: current.branches.length,
613
+ completedBytes,
614
+ totalBytes
615
+ });
464
616
  fs.mkdirSync(current.projectRoot, { recursive: true });
465
617
  fsyncManagedProjectRootEntry(current.projectRoot);
466
618
  }
@@ -481,7 +633,7 @@ function persistReconciledProjectAndConsumeSnapshot(snapshot, afterProjectTreeFs
481
633
  function rollbackProjectWorktreeSnapshot(snapshot) {
482
634
  const current = readSnapshotManifest(snapshot.root);
483
635
  assertSameSnapshotIdentity(snapshot.manifest, current);
484
- if (current.state === "prepared") {
636
+ if (current.state === "prepared" || current.state === "restoring") {
485
637
  restoreProjectWorktreeSnapshot(snapshot.root, current);
486
638
  consumeAndRemoveProjectWorktreeSnapshot(snapshot.root, current);
487
639
  return;
@@ -563,8 +715,8 @@ function recoverStaleProjectWorktreeSnapshots(input) {
563
715
  assertNoSymlinkPathComponents(projectsRoot, manifest.projectRoot);
564
716
  const ownedByThisProcessSession = manifest.ownerProcessId === currentProcessId && manifest.ownerSessionId === currentOwnerSessionId;
565
717
  if (ownedByThisProcessSession || ownerProcessId !== currentProcessId && processAlive(ownerProcessId)) continue;
566
- if (manifest.state === "prepared") {
567
- restoreProjectWorktreeSnapshot(candidate, manifest);
718
+ if (manifest.state === "prepared" || manifest.state === "restoring") {
719
+ restoreProjectWorktreeSnapshot(candidate, manifest, input.onProgress);
568
720
  const consumed = advanceSnapshotState(candidate, manifest, "consumed");
569
721
  removeSnapshotStorage(candidate);
570
722
  assertSameSnapshotIdentity(manifest, consumed);
@@ -750,7 +902,11 @@ async function initializeLinkedLayout(input) {
750
902
  );
751
903
  const checkoutPath = branchPath(input.projectRoot, branch.branchName);
752
904
  fs.mkdirSync(path.dirname(checkoutPath), { recursive: true });
753
- await gitAsync(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
905
+ await gitAsync(
906
+ primaryPath,
907
+ ["worktree", "add", "--force", checkoutPath, branch.branchName],
908
+ `create linked worktree ${branch.branchName}`
909
+ );
754
910
  }
755
911
  return mirrorHeads;
756
912
  }
@@ -784,7 +940,11 @@ async function reconcileLinkedLayout(input) {
784
940
  fs.rmSync(checkoutPath, { recursive: true, force: true });
785
941
  await gitAsync(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
786
942
  fs.mkdirSync(path.dirname(checkoutPath), { recursive: true });
787
- await gitAsync(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
943
+ await gitAsync(
944
+ primaryPath,
945
+ ["worktree", "add", "--force", checkoutPath, branch.branchName],
946
+ `create linked worktree ${branch.branchName}`
947
+ );
788
948
  }
789
949
  return { mirrorHeads, mutatedBranches };
790
950
  }
@@ -811,7 +971,7 @@ async function ensureProjectWorktrees(input) {
811
971
  assertNoOutstandingSnapshotForProject(snapshotStorageRoot, projectRoot);
812
972
  let snapshot;
813
973
  const ensureSnapshot = () => {
814
- snapshot ??= snapshotBranchTrees(projectRoot, input.branches, snapshotStorageRoot);
974
+ snapshot ??= snapshotBranchTrees(projectRoot, input.branches, snapshotStorageRoot, input.onSnapshotProgress);
815
975
  return snapshot;
816
976
  };
817
977
  try {
@@ -0,0 +1,10 @@
1
+ import { recoverStaleProjectWorktreeSnapshots, type ProjectWorktreeSnapshotProgress } from "./project-worktrees";
2
+ export declare const PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND = "__recover-project-snapshots";
3
+ type ProjectSnapshotRecoveryResult = ReturnType<typeof recoverStaleProjectWorktreeSnapshots>;
4
+ export declare function runProjectSnapshotRecoveryHelper(args: readonly string[]): number;
5
+ export declare function recoverProjectSnapshotsInChild(input: {
6
+ projectsRoot: string;
7
+ snapshotRoot: string;
8
+ onProgress?: (progress: ProjectWorktreeSnapshotProgress) => void;
9
+ }): Promise<ProjectSnapshotRecoveryResult>;
10
+ export {};
@@ -2,7 +2,7 @@ export declare const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-"
2
2
  declare const PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION: 1;
3
3
  type ProjectWorktreeSnapshotManifest = {
4
4
  version: typeof PROJECT_WORKTREE_SNAPSHOT_MANIFEST_VERSION;
5
- state: "building" | "prepared" | "consumed";
5
+ state: "building" | "prepared" | "restoring" | "consumed";
6
6
  ownerProcessId: number;
7
7
  ownerSessionId: string;
8
8
  projectRoot: string;
@@ -12,6 +12,16 @@ type ProjectWorktreeSnapshotManifest = {
12
12
  }>;
13
13
  createdAt: string;
14
14
  };
15
+ export type ProjectWorktreeSnapshotProgress = {
16
+ operation: "snapshot" | "restore";
17
+ phase: "scanning" | "copying" | "moving" | "syncing" | "cleaning";
18
+ projectRoot: string;
19
+ branchName?: string;
20
+ completedBranches: number;
21
+ totalBranches: number;
22
+ completedBytes: number;
23
+ totalBytes?: number;
24
+ };
15
25
  export type ProjectWorktreeBranch = {
16
26
  branchId?: string;
17
27
  branchName: string;
@@ -68,6 +78,7 @@ export declare function recoverStaleProjectWorktreeSnapshots(input: {
68
78
  processAlive?: (processId: number) => boolean;
69
79
  currentProcessId?: number;
70
80
  currentOwnerSessionId?: string;
81
+ onProgress?: (progress: ProjectWorktreeSnapshotProgress) => void;
71
82
  }): {
72
83
  restored: string[];
73
84
  removed: string[];
@@ -110,6 +121,7 @@ export declare function ensureProjectWorktrees(input: {
110
121
  } | null;
111
122
  exactMirrorBranches?: ReadonlySet<string>;
112
123
  snapshotRoot?: string;
124
+ onSnapshotProgress?: (progress: ProjectWorktreeSnapshotProgress) => void;
113
125
  }): Promise<ProjectWorktreeState[]>;
114
126
  export declare function projectWorktreeOperationInProgress(checkoutPath: string): boolean;
115
127
  export declare function createLinkedProjectBranch(input: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.89",
3
+ "version": "0.0.91",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",
@@ -1,49 +0,0 @@
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 heartbeat_exports = {};
20
- __export(heartbeat_exports, {
21
- WORKER_HEARTBEAT_INTERVAL_MS: () => WORKER_HEARTBEAT_INTERVAL_MS,
22
- WORKER_HEARTBEAT_TIMEOUT_MS: () => WORKER_HEARTBEAT_TIMEOUT_MS,
23
- grantWorkerHeartbeatBusyGrace: () => grantWorkerHeartbeatBusyGrace,
24
- hasWorkerHeartbeatTimedOut: () => hasWorkerHeartbeatTimedOut,
25
- hasWorkerHeartbeatTimedOutWithBusyGrace: () => hasWorkerHeartbeatTimedOutWithBusyGrace
26
- });
27
- module.exports = __toCommonJS(heartbeat_exports);
28
- const WORKER_HEARTBEAT_INTERVAL_MS = 15e3;
29
- const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
30
- function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
31
- return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
32
- }
33
- function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt, currentGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS, graceMs = WORKER_HEARTBEAT_INTERVAL_MS) {
34
- if (!hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs)) return currentGrace;
35
- if (currentGrace?.heartbeatAt === lastHeartbeatAt) return currentGrace;
36
- return { heartbeatAt: lastHeartbeatAt, expiresAt: now + graceMs };
37
- }
38
- function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt, busyGrace, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
39
- if (lastHeartbeatAt !== null && busyGrace?.heartbeatAt === lastHeartbeatAt && now < busyGrace.expiresAt) return false;
40
- return hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now, timeoutMs);
41
- }
42
- // Annotate the CommonJS export names for ESM import in node:
43
- 0 && (module.exports = {
44
- WORKER_HEARTBEAT_INTERVAL_MS,
45
- WORKER_HEARTBEAT_TIMEOUT_MS,
46
- grantWorkerHeartbeatBusyGrace,
47
- hasWorkerHeartbeatTimedOut,
48
- hasWorkerHeartbeatTimedOutWithBusyGrace
49
- });
@@ -1,21 +0,0 @@
1
- const WORKER_HEARTBEAT_INTERVAL_MS = 15e3;
2
- const WORKER_HEARTBEAT_TIMEOUT_MS = 45e3;
3
- function hasWorkerHeartbeatTimedOut(lastHeartbeatAt, now = Date.now(), timeoutMs = WORKER_HEARTBEAT_TIMEOUT_MS) {
4
- return lastHeartbeatAt !== null && now - lastHeartbeatAt >= timeoutMs;
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
- }
15
- export {
16
- WORKER_HEARTBEAT_INTERVAL_MS,
17
- WORKER_HEARTBEAT_TIMEOUT_MS,
18
- grantWorkerHeartbeatBusyGrace,
19
- hasWorkerHeartbeatTimedOut,
20
- hasWorkerHeartbeatTimedOutWithBusyGrace
21
- };
@@ -1,10 +0,0 @@
1
- export declare const WORKER_HEARTBEAT_INTERVAL_MS = 15000;
2
- export declare const WORKER_HEARTBEAT_TIMEOUT_MS = 45000;
3
- export declare function hasWorkerHeartbeatTimedOut(lastHeartbeatAt: number | null, now?: number, timeoutMs?: number): boolean;
4
- export type WorkerHeartbeatBusyGrace = {
5
- heartbeatAt: number;
6
- expiresAt: number;
7
- };
8
- /** Grant one bounded grace period for one actual server-heartbeat generation. */
9
- export declare function grantWorkerHeartbeatBusyGrace(lastHeartbeatAt: number | null, currentGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number, graceMs?: number): WorkerHeartbeatBusyGrace | null;
10
- export declare function hasWorkerHeartbeatTimedOutWithBusyGrace(lastHeartbeatAt: number | null, busyGrace: WorkerHeartbeatBusyGrace | null, now?: number, timeoutMs?: number): boolean;