@ricsam/r5d-worker 0.0.97 → 0.0.100

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/main.cjs CHANGED
@@ -97,9 +97,35 @@ function workerServerHeartbeatAction(input) {
97
97
  if (input.watchdogSweepDelayed || input.probeElapsedMs === void 0) return "start_probe";
98
98
  return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
99
99
  }
100
+ const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
101
+ function createWorkspaceHydrationLeaseHooks(send, options = {}) {
102
+ const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
103
+ const log = options.log ?? ((line) => process.stdout.write(`${line}
104
+ `));
105
+ const now = options.now ?? (() => performance.now());
106
+ const operationId = (context) => `${WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX}${context.transactionId}`;
107
+ const startedAtMs = /* @__PURE__ */ new Map();
108
+ return {
109
+ async beforeTransaction(context) {
110
+ send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: true });
111
+ startedAtMs.set(context.transactionId, now());
112
+ log(`[r5d-worker] hydrating ${context.mountIds.length} workspace mount(s) under hydration lease ${context.transactionId}`);
113
+ await yieldToEventLoop();
114
+ },
115
+ afterTransaction(context) {
116
+ const startedAt = startedAtMs.get(context.transactionId);
117
+ startedAtMs.delete(context.transactionId);
118
+ send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: false });
119
+ log(
120
+ `[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
121
+ );
122
+ }
123
+ };
124
+ }
100
125
  const workerLifecycleTestHarness = {
101
126
  boundedWorkerLifecycleProgress,
102
- workerServerHeartbeatAction
127
+ workerServerHeartbeatAction,
128
+ createWorkspaceHydrationLeaseHooks
103
129
  };
104
130
  class ProjectWorkspaceConfigurationDeferredError extends Error {
105
131
  }
@@ -4370,6 +4396,7 @@ async function startWorker(options) {
4370
4396
  error: error instanceof Error ? error.message : String(error)
4371
4397
  };
4372
4398
  };
4399
+ const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks((message) => sendWorkerMessageFromCurrentSource(ws, message));
4373
4400
  const performWorkspaceSync = async (input) => {
4374
4401
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4375
4402
  throw new Error("Worker workspace configuration has not been received");
@@ -4392,7 +4419,8 @@ async function startWorker(options) {
4392
4419
  credentialHelper: workspaceCredentialHelper,
4393
4420
  credentialUsername: workspaceCredentialUsername,
4394
4421
  gitIdentity: workspaceGitIdentity,
4395
- mounts: configuredMounts
4422
+ mounts: configuredMounts,
4423
+ hydrationHooks: workspaceHydrationTransactionHooks
4396
4424
  });
4397
4425
  await applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
4398
4426
  return {
@@ -4446,7 +4474,8 @@ async function startWorker(options) {
4446
4474
  assertStillAdmitted: input.assertStillAdmitted,
4447
4475
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4448
4476
  await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4449
- }
4477
+ },
4478
+ hydrationHooks: workspaceHydrationTransactionHooks
4450
4479
  });
4451
4480
  input.assertStillAdmitted?.();
4452
4481
  if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
@@ -5040,7 +5069,10 @@ async function startWorker(options) {
5040
5069
  sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
5041
5070
  busy: mount.busyForRecovery ?? mount.busy
5042
5071
  }));
5043
- await (0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts, { preserveStaleBases: true });
5072
+ await (0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts, {
5073
+ preserveStaleBases: true,
5074
+ hydrationHooks: workspaceHydrationTransactionHooks
5075
+ });
5044
5076
  for (const project of message.projects) {
5045
5077
  for (const { branchName } of project.preserveOnlyBranches) {
5046
5078
  if (stillPendingCreatedBranches.has((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(project.projectId, branchName))) {
@@ -5084,7 +5116,7 @@ async function startWorker(options) {
5084
5116
  // layouts can be marked ready. No process can resolve an unready
5085
5117
  // target, and preserving the fetched outer tree here prevents clean
5086
5118
  // checkout initialization from erasing synchronized dirt.
5087
- { ignoreBusy: true }
5119
+ { ignoreBusy: true, hydrationHooks: workspaceHydrationTransactionHooks }
5088
5120
  );
5089
5121
  await ensureConfiguredProjects((progress) => {
5090
5122
  sendLifecycleProgress({ phase: "configuring", operationId: configurationOperationId, ...progress });
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.97",
3
+ "version": "0.0.100",
4
4
  "type": "commonjs"
5
5
  }
@@ -29,8 +29,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  var working_tree_mirror_exports = {};
30
30
  __export(working_tree_mirror_exports, {
31
31
  assertWorkingTreeHasNoPortableGitMetadataAliases: () => assertWorkingTreeHasNoPortableGitMetadataAliases,
32
+ captureWorkingTreeMirrorPlan: () => captureWorkingTreeMirrorPlan,
33
+ fsyncWorkingTreePaths: () => fsyncWorkingTreePaths,
32
34
  inspectWorkingTree: () => inspectWorkingTree,
33
- mirrorWorkingTree: () => mirrorWorkingTree
35
+ mirrorWorkingTree: () => mirrorWorkingTree,
36
+ normalizeWorkingTreeRelativePath: () => normalizeWorkingTreeRelativePath,
37
+ planWorkingTreeMirror: () => planWorkingTreeMirror,
38
+ restoreWorkingTreeMirrorScope: () => restoreWorkingTreeMirrorScope
34
39
  });
35
40
  module.exports = __toCommonJS(working_tree_mirror_exports);
36
41
  var import_node_fs = __toESM(require("node:fs"), 1);
@@ -186,14 +191,55 @@ function addParentDirectories(entries, root) {
186
191
  }
187
192
  }
188
193
  }
189
- function inspectWorkingTree(root, mode) {
194
+ function gitlinkPaths(root) {
195
+ const result = Bun.spawnSync(
196
+ [
197
+ "git",
198
+ "-c",
199
+ "submodule.recurse=false",
200
+ "-c",
201
+ "fetch.recurseSubmodules=false",
202
+ "-c",
203
+ "push.recurseSubmodules=false",
204
+ "ls-files",
205
+ "-z",
206
+ "--stage",
207
+ "--",
208
+ "."
209
+ ],
210
+ {
211
+ cwd: root,
212
+ stdout: "pipe",
213
+ stderr: "pipe",
214
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
215
+ }
216
+ );
217
+ if (result.exitCode !== 0) {
218
+ throw new Error(`Inspect Git index gitlinks: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
219
+ }
220
+ const gitlinks = /* @__PURE__ */ new Set();
221
+ for (const record of result.stdout.toString().split("\0")) {
222
+ const match = /^160000 [0-9a-f]+ [0-3]\t(.+)$/su.exec(record);
223
+ if (!match) continue;
224
+ const normalized = normalizeRelativePath(match[1]);
225
+ if (normalized) gitlinks.add(normalized);
226
+ }
227
+ return gitlinks;
228
+ }
229
+ function directoryIsEmpty(root, relativePath) {
230
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, relativePath);
231
+ return inspected.kind === "entry" && inspected.stat.isDirectory() && import_node_fs.default.readdirSync(inspected.absolutePath).length === 0;
232
+ }
233
+ function inspectWorkingTree(root, mode, options = {}) {
190
234
  const entries = /* @__PURE__ */ new Map();
191
235
  root = import_node_path.default.resolve(root);
192
236
  if (!inspectDirectoryRoot(root, { allowMissing: true, label: "Working-tree root" })) return entries;
193
237
  if (mode === "all") {
194
238
  for (const child of import_node_fs.default.readdirSync(root).sort()) addEntry(entries, root, child, true);
195
239
  } else {
240
+ const omittedGitlinks = options.omitUnpopulatedGitlinks ? gitlinkPaths(root) : null;
196
241
  for (const relativePath of gitEligibleRoots(root)) {
242
+ if (omittedGitlinks?.has(relativePath) && directoryIsEmpty(root, relativePath)) continue;
197
243
  addEntry(entries, root, relativePath, true);
198
244
  }
199
245
  }
@@ -367,7 +413,9 @@ function mirrorWorkingTree(input) {
367
413
  }
368
414
  inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
369
415
  const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
370
- const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all");
416
+ const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
417
+ omitUnpopulatedGitlinks: input.deletionMode === "git"
418
+ });
371
419
  for (const relativePath of [...existing.keys()].sort((left, right) => right.split("/").length - left.split("/").length)) {
372
420
  if (!desired.has(relativePath)) removeEntry(targetRoot, relativePath);
373
421
  }
@@ -378,9 +426,213 @@ function mirrorWorkingTree(input) {
378
426
  }
379
427
  return { paths: [...desired.keys()].filter((relativePath) => desired.get(relativePath)?.kind !== "directory").sort() };
380
428
  }
429
+ function normalizeWorkingTreeRelativePath(value) {
430
+ return normalizeRelativePath(value);
431
+ }
432
+ const MIRROR_TEMPORARY_FILE_PATTERN = /^\.r5d-working-tree-mirror-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/u;
433
+ function relativeDepth(relativePath) {
434
+ return relativePath.split("/").length;
435
+ }
436
+ function shallowestFirst(left, right) {
437
+ return relativeDepth(left) - relativeDepth(right) || left.localeCompare(right);
438
+ }
439
+ function deepestFirst(left, right) {
440
+ return relativeDepth(right) - relativeDepth(left) || left.localeCompare(right);
441
+ }
442
+ function hasAncestorIn(relativePath, ancestors) {
443
+ let parent = import_node_path.default.posix.dirname(relativePath);
444
+ while (parent !== ".") {
445
+ if (ancestors.has(parent)) return true;
446
+ parent = import_node_path.default.posix.dirname(parent);
447
+ }
448
+ return false;
449
+ }
450
+ function treeEntryFromStat(stat, absolutePath) {
451
+ if (stat.isSymbolicLink()) return { kind: "symlink", mode: stat.mode, target: import_node_fs.default.readlinkSync(absolutePath) };
452
+ if (stat.isFile()) return { kind: "file", mode: stat.mode, size: stat.size };
453
+ if (stat.isDirectory()) return { kind: "directory", mode: stat.mode };
454
+ return null;
455
+ }
456
+ function fsyncPath(targetPath, flags) {
457
+ const descriptor = import_node_fs.default.openSync(targetPath, flags);
458
+ try {
459
+ import_node_fs.default.fsyncSync(descriptor);
460
+ } finally {
461
+ import_node_fs.default.closeSync(descriptor);
462
+ }
463
+ }
464
+ function planWorkingTreeMirror(input) {
465
+ const sourceRoot = import_node_path.default.resolve(input.sourceRoot);
466
+ const targetRoot = import_node_path.default.resolve(input.targetRoot);
467
+ const desired = inspectWorkingTree(sourceRoot, input.sourceMode);
468
+ const existing = inspectWorkingTree(targetRoot, input.deletionMode === "git" ? "git" : "all", {
469
+ omitUnpopulatedGitlinks: input.deletionMode === "git"
470
+ });
471
+ const replaced = /* @__PURE__ */ new Map();
472
+ const absent = [];
473
+ for (const [relativePath, entry] of existing) {
474
+ if (!desired.has(relativePath)) replaced.set(relativePath, entry);
475
+ }
476
+ for (const [relativePath, entry] of desired) {
477
+ const target = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
478
+ if (target.kind !== "entry") {
479
+ absent.push(relativePath);
480
+ continue;
481
+ }
482
+ const { absolutePath, stat } = target;
483
+ if (entry.kind === "directory") {
484
+ if (stat.isDirectory()) continue;
485
+ } else if (entry.kind === "symlink") {
486
+ if (stat.isSymbolicLink() && import_node_fs.default.readlinkSync(absolutePath) === entry.target) continue;
487
+ } else if (stat.isFile() && filesEqual(import_node_path.default.join(sourceRoot, ...relativePath.split("/")), absolutePath, entry)) {
488
+ continue;
489
+ }
490
+ const current = treeEntryFromStat(stat, absolutePath);
491
+ if (current) replaced.set(relativePath, current);
492
+ else absent.push(relativePath);
493
+ }
494
+ const replacedDirectories = new Set(
495
+ [...replaced].filter(([, entry]) => entry.kind === "directory").map(([relativePath]) => relativePath)
496
+ );
497
+ for (const relativePath of [...replaced.keys()]) {
498
+ if (hasAncestorIn(relativePath, replacedDirectories)) replaced.delete(relativePath);
499
+ }
500
+ return {
501
+ desired,
502
+ replaced,
503
+ absent: absent.filter((relativePath) => !hasAncestorIn(relativePath, replacedDirectories)).sort(shallowestFirst)
504
+ };
505
+ }
506
+ function captureWorkingTreeMirrorPlan(input) {
507
+ const targetRoot = import_node_path.default.resolve(input.targetRoot);
508
+ const snapshotRoot = import_node_path.default.resolve(input.snapshotRoot);
509
+ const durability = input.durability ?? "per_entry";
510
+ inspectDirectoryRoot(targetRoot, { allowMissing: false, label: "Working-tree target root" });
511
+ if (!inspectDirectoryRoot(snapshotRoot, { allowMissing: true, label: "Working-tree snapshot root" })) {
512
+ import_node_fs.default.mkdirSync(snapshotRoot, { recursive: true });
513
+ }
514
+ const trees = [];
515
+ for (const [relativePath, entry] of [...input.plan.replaced.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
516
+ if (entry.kind !== "directory") {
517
+ copyEntry(targetRoot, snapshotRoot, relativePath, entry, durability);
518
+ continue;
519
+ }
520
+ const source = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
521
+ if (source.kind !== "entry" || !source.stat.isDirectory()) {
522
+ throw new Error(`Working-tree source changed while it was being mirrored: ${import_node_path.default.join(targetRoot, ...relativePath.split("/"))}`);
523
+ }
524
+ ensureDirectory(snapshotRoot, relativePath, entry.mode);
525
+ mirrorWorkingTree({
526
+ sourceRoot: source.absolutePath,
527
+ targetRoot: import_node_path.default.join(snapshotRoot, ...relativePath.split("/")),
528
+ sourceMode: "all",
529
+ deletionMode: "all",
530
+ durability
531
+ });
532
+ trees.push(relativePath);
533
+ }
534
+ return { absent: [...input.plan.absent].sort(shallowestFirst), trees: trees.sort(shallowestFirst) };
535
+ }
536
+ function restoreWorkingTreeMirrorScope(input) {
537
+ const snapshotRoot = import_node_path.default.resolve(input.snapshotRoot);
538
+ const targetRoot = import_node_path.default.resolve(input.targetRoot);
539
+ inspectDirectoryRoot(snapshotRoot, { allowMissing: false, label: "Working-tree snapshot root" });
540
+ if (!inspectDirectoryRoot(targetRoot, { allowMissing: true, label: "Working-tree target root" })) {
541
+ import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
542
+ }
543
+ const normalize = (relativePath) => {
544
+ const normalized = normalizeRelativePath(relativePath);
545
+ if (!normalized) throw new Error(`Invalid working-tree scope path: ${relativePath}`);
546
+ return normalized;
547
+ };
548
+ const absent = input.scope.absent.map(normalize);
549
+ const trees = new Set(input.scope.trees.map(normalize));
550
+ const captured = inspectWorkingTree(snapshotRoot, "all");
551
+ for (const tree of trees) {
552
+ if (captured.get(tree)?.kind !== "directory") {
553
+ throw new Error(`Working-tree snapshot does not hold the captured directory: ${tree}`);
554
+ }
555
+ }
556
+ const touched = /* @__PURE__ */ new Set();
557
+ for (const relativePath of [...absent].sort(deepestFirst)) {
558
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
559
+ if (inspected.kind === "entry") import_node_fs.default.rmSync(inspected.absolutePath, { recursive: true, force: true });
560
+ touched.add(relativePath);
561
+ }
562
+ const writtenDirectories = /* @__PURE__ */ new Set();
563
+ for (const relativePath of absent) writtenDirectories.add(import_node_path.default.posix.dirname(relativePath));
564
+ for (const [relativePath, entry] of captured) {
565
+ if (entry.kind !== "directory" && !hasAncestorIn(relativePath, trees)) writtenDirectories.add(import_node_path.default.posix.dirname(relativePath));
566
+ }
567
+ for (const directory of writtenDirectories) {
568
+ let absoluteDirectory = targetRoot;
569
+ if (directory !== ".") {
570
+ const inspected = inspectRelativePathWithoutFollowingAncestors(targetRoot, directory);
571
+ if (inspected.kind !== "entry" || !inspected.stat.isDirectory()) continue;
572
+ absoluteDirectory = inspected.absolutePath;
573
+ }
574
+ for (const name of import_node_fs.default.readdirSync(absoluteDirectory)) {
575
+ if (!MIRROR_TEMPORARY_FILE_PATTERN.test(name)) continue;
576
+ const temporaryPath = import_node_path.default.join(absoluteDirectory, name);
577
+ if (!import_node_fs.default.lstatSync(temporaryPath).isFile()) continue;
578
+ import_node_fs.default.rmSync(temporaryPath, { force: true });
579
+ touched.add(directory === "." ? name : import_node_path.default.posix.join(directory, name));
580
+ }
581
+ }
582
+ for (const [relativePath, entry] of [...captured.entries()].sort(([left], [right]) => shallowestFirst(left, right))) {
583
+ touched.add(relativePath);
584
+ if (hasAncestorIn(relativePath, trees)) continue;
585
+ if (!trees.has(relativePath)) {
586
+ copyEntry(snapshotRoot, targetRoot, relativePath, entry, "per_entry");
587
+ continue;
588
+ }
589
+ const current = inspectRelativePathWithoutFollowingAncestors(targetRoot, relativePath);
590
+ if (current.kind === "blocked") {
591
+ throw new Error(`Refusing to restore a working-tree directory through a symlink or non-directory: ${current.blockingPath}`);
592
+ }
593
+ if (current.kind === "entry" && !current.stat.isDirectory()) {
594
+ import_node_fs.default.rmSync(current.absolutePath, { recursive: true, force: true });
595
+ }
596
+ ensureDirectory(targetRoot, relativePath, entry.mode);
597
+ mirrorWorkingTree({
598
+ sourceRoot: import_node_path.default.join(snapshotRoot, ...relativePath.split("/")),
599
+ targetRoot: import_node_path.default.join(targetRoot, ...relativePath.split("/")),
600
+ sourceMode: "all",
601
+ deletionMode: "all"
602
+ });
603
+ }
604
+ return { paths: [...touched].sort(shallowestFirst) };
605
+ }
606
+ function fsyncWorkingTreePaths(root, relativePaths) {
607
+ root = import_node_path.default.resolve(root);
608
+ inspectDirectoryRoot(root, { allowMissing: false, label: "Working-tree root" });
609
+ const directories = /* @__PURE__ */ new Set(["."]);
610
+ for (const relativePath of relativePaths) {
611
+ const normalized = normalizeRelativePath(relativePath);
612
+ if (!normalized) throw new Error(`Invalid working-tree path: ${relativePath}`);
613
+ directories.add(import_node_path.default.posix.dirname(normalized));
614
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, normalized);
615
+ if (inspected.kind !== "entry") continue;
616
+ if (inspected.stat.isDirectory()) directories.add(normalized);
617
+ else if (inspected.stat.isFile()) fsyncPath(inspected.absolutePath, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
618
+ }
619
+ for (const directory of directories) {
620
+ if (directory === ".") {
621
+ fsyncPath(root, import_node_fs.default.constants.O_RDONLY);
622
+ continue;
623
+ }
624
+ const inspected = inspectRelativePathWithoutFollowingAncestors(root, directory);
625
+ if (inspected.kind === "entry" && inspected.stat.isDirectory()) fsyncPath(inspected.absolutePath, import_node_fs.default.constants.O_RDONLY);
626
+ }
627
+ }
381
628
  // Annotate the CommonJS export names for ESM import in node:
382
629
  0 && (module.exports = {
383
630
  assertWorkingTreeHasNoPortableGitMetadataAliases,
631
+ captureWorkingTreeMirrorPlan,
632
+ fsyncWorkingTreePaths,
384
633
  inspectWorkingTree,
385
- mirrorWorkingTree
634
+ mirrorWorkingTree,
635
+ normalizeWorkingTreeRelativePath,
636
+ planWorkingTreeMirror,
637
+ restoreWorkingTreeMirrorScope
386
638
  });