@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.
@@ -954,7 +954,7 @@ function parseHydrationTransactionManifest(transactionPath) {
954
954
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
955
955
  }
956
956
  const candidate = parsed;
957
- if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
957
+ if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
958
958
  throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
959
959
  }
960
960
  const parseReceipt = (rawReceipt) => {
@@ -967,6 +967,25 @@ function parseHydrationTransactionManifest(transactionPath) {
967
967
  };
968
968
  const targetReceipt = parseReceipt(candidate.targetReceipt);
969
969
  const receiptBefore = parseReceipt(candidate.receiptBefore);
970
+ const parseScope = (rawScope, existed) => {
971
+ if (rawScope === void 0) return void 0;
972
+ if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
973
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
974
+ }
975
+ const parsePaths = (rawPaths) => {
976
+ if (!Array.isArray(rawPaths)) throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
977
+ const seen = /* @__PURE__ */ new Set();
978
+ return rawPaths.map((rawPath) => {
979
+ if (typeof rawPath !== "string" || (0, import_working_tree_mirror.normalizeWorkingTreeRelativePath)(rawPath) !== rawPath || seen.has(rawPath)) {
980
+ throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
981
+ }
982
+ seen.add(rawPath);
983
+ return rawPath;
984
+ });
985
+ };
986
+ const { absent, trees } = rawScope;
987
+ return { absent: parsePaths(absent), trees: parsePaths(trees) };
988
+ };
970
989
  const seenIds = /* @__PURE__ */ new Set();
971
990
  const seenSnapshots = /* @__PURE__ */ new Set();
972
991
  const mounts = candidate.mounts.map((rawMount) => {
@@ -983,10 +1002,20 @@ function parseHydrationTransactionManifest(transactionPath) {
983
1002
  }
984
1003
  seenIds.add(mount.id);
985
1004
  seenSnapshots.add(mount.snapshotName);
986
- return mount;
1005
+ const scope = parseScope(rawMount.scope, mount.existed);
1006
+ return {
1007
+ id: mount.id,
1008
+ incarnationKey: mount.incarnationKey,
1009
+ sourcePath: mount.sourcePath,
1010
+ durabilityRootPath: mount.durabilityRootPath,
1011
+ workspaceRelativePath: mount.workspaceRelativePath,
1012
+ existed: mount.existed,
1013
+ snapshotName: mount.snapshotName,
1014
+ ...scope ? { scope } : {}
1015
+ };
987
1016
  });
988
1017
  return {
989
- version: 3,
1018
+ version: candidate.version === 4 ? 4 : 3,
990
1019
  targetReceipt,
991
1020
  receiptBefore,
992
1021
  mounts
@@ -1008,6 +1037,7 @@ function hydrationTransactionCommitted(transactionPath) {
1008
1037
  }
1009
1038
  function restoreHydrationTransaction(workspacePath, manifest) {
1010
1039
  const transactionPath = hydrationTransactionPath(workspacePath);
1040
+ const durabilityScopes = /* @__PURE__ */ new Map();
1011
1041
  for (const mount of manifest.mounts) {
1012
1042
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1013
1043
  trustedRoot: mount.durabilityRootPath,
@@ -1023,6 +1053,11 @@ function restoreHydrationTransaction(workspacePath, manifest) {
1023
1053
  if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
1024
1054
  throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
1025
1055
  }
1056
+ if (mount.scope) {
1057
+ const restored = (0, import_working_tree_mirror.restoreWorkingTreeMirrorScope)({ snapshotRoot: snapshotPath, targetRoot: mount.sourcePath, scope: mount.scope });
1058
+ durabilityScopes.set(mount.id, restored.paths);
1059
+ continue;
1060
+ }
1026
1061
  (0, import_working_tree_mirror.mirrorWorkingTree)({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
1027
1062
  }
1028
1063
  fsyncHydratedWorkspaceMounts(
@@ -1031,7 +1066,8 @@ function restoreHydrationTransaction(workspacePath, manifest) {
1031
1066
  hydrationIncarnationKey: mount.incarnationKey,
1032
1067
  sourceMode: "all",
1033
1068
  hydrateDeletionMode: "all"
1034
- }))
1069
+ })),
1070
+ durabilityScopes
1035
1071
  );
1036
1072
  }
1037
1073
  function recoverPendingHydrationTransaction(workspacePath) {
@@ -1075,57 +1111,80 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
1075
1111
  const transactionPath = hydrationTransactionPath(workspacePath);
1076
1112
  const stagingPath = import_node_path.default.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
1077
1113
  import_node_fs.default.mkdirSync(stagingPath, { mode: 448 });
1114
+ const durabilityScopes = /* @__PURE__ */ new Map();
1078
1115
  try {
1079
1116
  const snapshotRoot = import_node_path.default.join(stagingPath, "mounts");
1080
1117
  import_node_fs.default.mkdirSync(snapshotRoot, { mode: 448 });
1081
- const manifest = {
1082
- version: 3,
1083
- targetReceipt,
1084
- receiptBefore: readHydratedWorkspaceReceipt(workspacePath),
1085
- mounts: mounts.map((mount, index) => {
1086
- const sourcePath = import_node_path.default.resolve(mount.sourcePath);
1087
- const durabilityRootPath = import_node_path.default.resolve(mount.durabilityRootPath);
1118
+ const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
1119
+ const manifestMounts = mounts.map((mount, index) => {
1120
+ const sourcePath = import_node_path.default.resolve(mount.sourcePath);
1121
+ const durabilityRootPath = import_node_path.default.resolve(mount.durabilityRootPath);
1122
+ (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1123
+ trustedRoot: durabilityRootPath,
1124
+ candidate: sourcePath,
1125
+ label: `Workspace hydration snapshot mount ${mount.id}`
1126
+ });
1127
+ const sourceStatus = lstatIfExists(sourcePath);
1128
+ if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1129
+ throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1130
+ }
1131
+ const snapshotName = String(index);
1132
+ const snapshotPath = import_node_path.default.join(snapshotRoot, snapshotName);
1133
+ import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
1134
+ let scope;
1135
+ if (sourceStatus && mount.hydrateDeletionMode === "git") {
1136
+ const outerPath = workspaceMountOuterPath(workspacePath, mount);
1088
1137
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1089
- trustedRoot: durabilityRootPath,
1090
- candidate: sourcePath,
1091
- label: `Workspace hydration snapshot mount ${mount.id}`
1138
+ trustedRoot: workspacePath,
1139
+ candidate: outerPath,
1140
+ label: `Workspace mount ${mount.id} outer source`
1092
1141
  });
1093
- const sourceStatus = lstatIfExists(sourcePath);
1094
- if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
1095
- throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
1096
- }
1097
- const snapshotName = String(index);
1098
- const snapshotPath = import_node_path.default.join(snapshotRoot, snapshotName);
1099
- import_node_fs.default.mkdirSync(snapshotPath, { mode: 448 });
1100
- if (sourceStatus) {
1101
- (0, import_working_tree_mirror.mirrorWorkingTree)({
1102
- sourceRoot: sourcePath,
1103
- targetRoot: snapshotPath,
1104
- sourceMode: "all",
1105
- deletionMode: "all",
1106
- // This tree is still private and the source authority is untouched.
1107
- // The single recursive barrier below makes the complete snapshot
1108
- // durable before its directory entry is published or hydration starts.
1109
- durability: "deferred_private_staging"
1110
- });
1111
- }
1112
- return {
1113
- id: mount.id,
1114
- incarnationKey: mount.hydrationIncarnationKey,
1115
- sourcePath,
1116
- durabilityRootPath,
1117
- workspaceRelativePath: mount.workspaceRelativePath,
1118
- existed: Boolean(sourceStatus),
1119
- snapshotName
1120
- };
1121
- })
1142
+ const plan = (0, import_working_tree_mirror.planWorkingTreeMirror)({ sourceRoot: outerPath, targetRoot: sourcePath, sourceMode: "all", deletionMode: "git" });
1143
+ scope = (0, import_working_tree_mirror.captureWorkingTreeMirrorPlan)({
1144
+ plan,
1145
+ targetRoot: sourcePath,
1146
+ snapshotRoot: snapshotPath,
1147
+ // Same private-staging deferral as the complete snapshot below.
1148
+ durability: "deferred_private_staging"
1149
+ });
1150
+ durabilityScopes.set(mount.id, [...plan.desired.keys()]);
1151
+ } else if (sourceStatus) {
1152
+ (0, import_working_tree_mirror.mirrorWorkingTree)({
1153
+ sourceRoot: sourcePath,
1154
+ targetRoot: snapshotPath,
1155
+ sourceMode: "all",
1156
+ deletionMode: "all",
1157
+ // This tree is still private and the source authority is untouched.
1158
+ // The single recursive barrier below makes the complete snapshot
1159
+ // durable before its directory entry is published or hydration starts.
1160
+ durability: "deferred_private_staging"
1161
+ });
1162
+ }
1163
+ return {
1164
+ id: mount.id,
1165
+ incarnationKey: mount.hydrationIncarnationKey,
1166
+ sourcePath,
1167
+ durabilityRootPath,
1168
+ workspaceRelativePath: mount.workspaceRelativePath,
1169
+ existed: Boolean(sourceStatus),
1170
+ snapshotName,
1171
+ ...scope ? { scope } : {}
1172
+ };
1173
+ });
1174
+ const manifest = {
1175
+ // Older workers reject version 4, so a scoped snapshot can never be
1176
+ // replayed as a complete copy after a downgrade.
1177
+ version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
1178
+ targetReceipt,
1179
+ receiptBefore,
1180
+ mounts: manifestMounts
1122
1181
  };
1123
1182
  writePrivateFileDurably(import_node_path.default.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
1124
1183
  `);
1125
1184
  fsyncTree(stagingPath);
1126
1185
  import_node_fs.default.renameSync(stagingPath, transactionPath);
1127
1186
  fsyncDirectory(stateDirectory);
1128
- return manifest;
1187
+ return { manifest, durabilityScopes };
1129
1188
  } catch (error) {
1130
1189
  import_node_fs.default.rmSync(stagingPath, { recursive: true, force: true });
1131
1190
  fsyncDirectory(stateDirectory);
@@ -1137,7 +1196,11 @@ function markHydrationTransactionDurable(workspacePath) {
1137
1196
  writePrivateFileDurably(import_node_path.default.join(transactionPath, "committed"), "committed\n");
1138
1197
  fsyncDirectory(transactionPath);
1139
1198
  }
1140
- function fsyncHydratedWorkspaceMount(mount) {
1199
+ function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
1200
+ if (mount.hydrateDeletionMode !== "git" || !sourceStatus.isDirectory() || !lstatIfExists(import_node_path.default.join(sourcePath, ".git"))) return null;
1201
+ return [...(0, import_working_tree_mirror.inspectWorkingTree)(sourcePath, "git").keys()];
1202
+ }
1203
+ function fsyncHydratedWorkspaceMount(mount, scope) {
1141
1204
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1142
1205
  trustedRoot: mount.durabilityRootPath,
1143
1206
  candidate: mount.sourcePath,
@@ -1162,7 +1225,17 @@ function fsyncHydratedWorkspaceMount(mount) {
1162
1225
  import_node_fs.default.closeSync(descriptor);
1163
1226
  }
1164
1227
  };
1165
- if (import_node_fs.default.existsSync(mount.sourcePath)) syncEntry(mount.sourcePath, true);
1228
+ const sourcePath = import_node_path.default.resolve(mount.sourcePath);
1229
+ const sourceStatus = lstatIfExists(sourcePath);
1230
+ if (sourceStatus) {
1231
+ const paths = scope ?? gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus);
1232
+ if (paths) {
1233
+ if (!sourceStatus.isDirectory()) throw new Error(`Workspace mount ${mount.id} source is not a regular directory: ${sourcePath}`);
1234
+ (0, import_working_tree_mirror.fsyncWorkingTreePaths)(sourcePath, paths);
1235
+ } else {
1236
+ syncEntry(sourcePath, true);
1237
+ }
1238
+ }
1166
1239
  const durabilityRoot = import_node_path.default.resolve(mount.durabilityRootPath);
1167
1240
  let boundaryParent = import_node_path.default.dirname(import_node_path.default.resolve(mount.sourcePath));
1168
1241
  while (true) {
@@ -1181,8 +1254,8 @@ function fsyncHydratedWorkspaceMount(mount) {
1181
1254
  boundaryParent = nextParent;
1182
1255
  }
1183
1256
  }
1184
- function fsyncHydratedWorkspaceMounts(mounts) {
1185
- for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
1257
+ function fsyncHydratedWorkspaceMounts(mounts, scopes = /* @__PURE__ */ new Map()) {
1258
+ for (const mount of mounts) fsyncHydratedWorkspaceMount(mount, scopes.get(mount.id));
1186
1259
  }
1187
1260
  function workspaceIsShallow(workspacePath) {
1188
1261
  return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
@@ -1431,6 +1504,9 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
1431
1504
  }
1432
1505
  }
1433
1506
  }
1507
+ function workspaceMountOuterPath(workspacePath, mount) {
1508
+ return import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1509
+ }
1434
1510
  function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1435
1511
  const hydratedMountIds = [];
1436
1512
  const skippedMountIds = [];
@@ -1439,7 +1515,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1439
1515
  skippedMountIds.push(mount.id);
1440
1516
  continue;
1441
1517
  }
1442
- const sourceRoot = import_node_path.default.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
1518
+ const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
1443
1519
  (0, import_workspace_mount_boundary.assertManagedDirectoryPath)({
1444
1520
  trustedRoot: workspacePath,
1445
1521
  candidate: sourceRoot,
@@ -1525,56 +1601,67 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1525
1601
  const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
1526
1602
  const requestedIds = new Set(input.mounts.map(({ id }) => id));
1527
1603
  const requiredIds = new Set(requiredMounts.map(({ id }) => id));
1528
- const hydrationMounts2 = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1529
- const advancedDurabilityMounts2 = durabilityMounts.filter(
1604
+ const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
1605
+ const advancedDurabilityMounts = durabilityMounts.filter(
1530
1606
  ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1531
1607
  );
1532
1608
  const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1533
1609
  const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
1534
1610
  return {
1535
1611
  busyMountIds,
1536
- hydrationMounts: hydrationMounts2,
1537
- advancedDurabilityMounts: advancedDurabilityMounts2,
1612
+ hydrationMounts,
1613
+ advancedDurabilityMounts,
1538
1614
  skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
1539
- targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
1615
+ targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts, ...carriedMounts], receiptMounts) : null
1540
1616
  };
1541
1617
  };
1542
1618
  const noopCandidate = computeSelection();
1543
1619
  if (noopCandidate.hydrationMounts.length === 0 && noopCandidate.advancedDurabilityMounts.length === 0 && noopCandidate.targetReceipt && lstatIfExists(hydrationTransactionPath(workspacePath)) === null && workspaceHydrationReceiptsEqual(receiptBeforeTransaction, noopCandidate.targetReceipt) && hydratedWorkspaceReceiptFileIsV3(workspacePath) && await workspaceBasisRefsAreCurrentAsync(workspacePath, noopCandidate.targetReceipt)) {
1544
1620
  return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
1545
1621
  }
1546
- const selection = computeSelection();
1547
- const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1548
- const manifest = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1622
+ const hooks = input.hydrationHooks;
1623
+ const context = {
1624
+ transactionId: crypto.randomUUID(),
1625
+ workspacePath,
1626
+ mountIds: [...new Set([...noopCandidate.hydrationMounts, ...noopCandidate.advancedDurabilityMounts].map(({ id }) => id))].sort()
1627
+ };
1628
+ if (hooks) await hooks.beforeTransaction(context);
1549
1629
  try {
1550
- const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1551
- const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1552
- if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1553
- throw new Error("Workspace hydration did not include every required mount");
1554
- }
1555
- fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
1556
- markHydrationTransactionDurable(workspacePath);
1557
- if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1558
- removeHydrationTransaction(workspacePath);
1559
- return {
1560
- hydratedMountIds: hydration.hydratedMountIds,
1561
- skippedMountIds: selection.skippedMountIds
1562
- };
1563
- } catch (error) {
1564
- const transactionPath = hydrationTransactionPath(workspacePath);
1565
- if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1566
- throw error;
1567
- }
1630
+ const selection = computeSelection();
1631
+ const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
1632
+ const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
1568
1633
  try {
1569
- restoreHydrationTransaction(workspacePath, manifest);
1634
+ const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
1635
+ const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
1636
+ if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
1637
+ throw new Error("Workspace hydration did not include every required mount");
1638
+ }
1639
+ fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
1640
+ markHydrationTransactionDurable(workspacePath);
1641
+ if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
1570
1642
  removeHydrationTransaction(workspacePath);
1571
- } catch (restoreError) {
1572
- throw new AggregateError(
1573
- [error, restoreError],
1574
- `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1575
- );
1643
+ return {
1644
+ hydratedMountIds: hydration.hydratedMountIds,
1645
+ skippedMountIds: selection.skippedMountIds
1646
+ };
1647
+ } catch (error) {
1648
+ const transactionPath = hydrationTransactionPath(workspacePath);
1649
+ if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
1650
+ throw error;
1651
+ }
1652
+ try {
1653
+ restoreHydrationTransaction(workspacePath, manifest);
1654
+ removeHydrationTransaction(workspacePath);
1655
+ } catch (restoreError) {
1656
+ throw new AggregateError(
1657
+ [error, restoreError],
1658
+ `Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
1659
+ );
1660
+ }
1661
+ throw error;
1576
1662
  }
1577
- throw error;
1663
+ } finally {
1664
+ hooks?.afterTransaction(context);
1578
1665
  }
1579
1666
  }
1580
1667
  async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
@@ -1586,7 +1673,8 @@ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
1586
1673
  return hydrateWorkspaceGitMountsTransactionally({
1587
1674
  workspacePath,
1588
1675
  mounts,
1589
- ignoreBusy: options.ignoreBusy
1676
+ ignoreBusy: options.ignoreBusy,
1677
+ hydrationHooks: options.hydrationHooks
1590
1678
  });
1591
1679
  }
1592
1680
  async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
@@ -1626,7 +1714,8 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
1626
1714
  receiptMounts: configuredBasisMounts,
1627
1715
  requiredMounts: hydrationTargets,
1628
1716
  ignoreBusy: options.ignoreBusy,
1629
- recordCurrentHead: true
1717
+ recordCurrentHead: true,
1718
+ hydrationHooks: options.hydrationHooks
1630
1719
  });
1631
1720
  void hydration;
1632
1721
  }
@@ -1677,7 +1766,8 @@ async function resetWorkspaceGit(input) {
1677
1766
  durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
1678
1767
  receiptMounts: input.mounts,
1679
1768
  requiredMounts: requiredLiveMounts,
1680
- recordCurrentHead: true
1769
+ recordCurrentHead: true,
1770
+ hydrationHooks: input.hydrationHooks
1681
1771
  });
1682
1772
  const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
1683
1773
  const resetHead = revParse(workspacePath, "HEAD");
@@ -1932,7 +2022,8 @@ async function synchronizeWorkspaceGit(input) {
1932
2022
  await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
1933
2023
  ...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
1934
2024
  preserveResolutionInProgress,
1935
- preserveStaleBases: true
2025
+ preserveStaleBases: true,
2026
+ hydrationHooks: input.hydrationHooks
1936
2027
  });
1937
2028
  const selected = activeMounts(input.mounts);
1938
2029
  const projectionMutationTokens = new Map(
@@ -2058,7 +2149,8 @@ async function synchronizeWorkspaceGit(input) {
2058
2149
  carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
2059
2150
  receiptMounts: completionReceiptMounts,
2060
2151
  requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2061
- recordCurrentHead: true
2152
+ recordCurrentHead: true,
2153
+ hydrationHooks: input.hydrationHooks
2062
2154
  });
2063
2155
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
2064
2156
  refreshCycleSkippedMounts(completionReceiptMounts);
package/dist/mjs/main.mjs CHANGED
@@ -104,9 +104,35 @@ function workerServerHeartbeatAction(input) {
104
104
  if (input.watchdogSweepDelayed || input.probeElapsedMs === void 0) return "start_probe";
105
105
  return input.probeElapsedMs >= input.timeoutMs ? "terminate" : "wait";
106
106
  }
107
+ const WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX = "hydration:";
108
+ function createWorkspaceHydrationLeaseHooks(send, options = {}) {
109
+ const yieldToEventLoop = options.yieldToEventLoop ?? (() => new Promise((resolve) => setImmediate(resolve)));
110
+ const log = options.log ?? ((line) => process.stdout.write(`${line}
111
+ `));
112
+ const now = options.now ?? (() => performance.now());
113
+ const operationId = (context) => `${WORKSPACE_HYDRATION_LEASE_OPERATION_PREFIX}${context.transactionId}`;
114
+ const startedAtMs = /* @__PURE__ */ new Map();
115
+ return {
116
+ async beforeTransaction(context) {
117
+ send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: true });
118
+ startedAtMs.set(context.transactionId, now());
119
+ log(`[r5d-worker] hydrating ${context.mountIds.length} workspace mount(s) under hydration lease ${context.transactionId}`);
120
+ await yieldToEventLoop();
121
+ },
122
+ afterTransaction(context) {
123
+ const startedAt = startedAtMs.get(context.transactionId);
124
+ startedAtMs.delete(context.transactionId);
125
+ send({ type: "heartbeat_lease", operationId: operationId(context), kind: "hydration", active: false });
126
+ log(
127
+ `[r5d-worker] released hydration lease ${context.transactionId} after ${startedAt === void 0 ? "?" : Math.round(now() - startedAt)}ms`
128
+ );
129
+ }
130
+ };
131
+ }
107
132
  const workerLifecycleTestHarness = {
108
133
  boundedWorkerLifecycleProgress,
109
- workerServerHeartbeatAction
134
+ workerServerHeartbeatAction,
135
+ createWorkspaceHydrationLeaseHooks
110
136
  };
111
137
  class ProjectWorkspaceConfigurationDeferredError extends Error {
112
138
  }
@@ -4377,6 +4403,7 @@ async function startWorker(options) {
4377
4403
  error: error instanceof Error ? error.message : String(error)
4378
4404
  };
4379
4405
  };
4406
+ const workspaceHydrationTransactionHooks = createWorkspaceHydrationLeaseHooks((message) => sendWorkerMessageFromCurrentSource(ws, message));
4380
4407
  const performWorkspaceSync = async (input) => {
4381
4408
  if (!workspaceRemoteUrl || !workspaceCredentialHelper || !workspaceCredentialUsername || !workspaceGitIdentity) {
4382
4409
  throw new Error("Worker workspace configuration has not been received");
@@ -4399,7 +4426,8 @@ async function startWorker(options) {
4399
4426
  credentialHelper: workspaceCredentialHelper,
4400
4427
  credentialUsername: workspaceCredentialUsername,
4401
4428
  gitIdentity: workspaceGitIdentity,
4402
- mounts: configuredMounts
4429
+ mounts: configuredMounts,
4430
+ hydrationHooks: workspaceHydrationTransactionHooks
4403
4431
  });
4404
4432
  await applyObservedProjectHeadsAfterInboundWorkspace(observedProjectHeads2, reset.activeMountIds, true);
4405
4433
  return {
@@ -4453,7 +4481,8 @@ async function startWorker(options) {
4453
4481
  assertStillAdmitted: input.assertStillAdmitted,
4454
4482
  afterWorkspacePublished: outerRemediation ? void 0 : async ({ activeMountIds, publishedHead }) => {
4455
4483
  await pushChangedProjectHeads(activeMountIds, publishedHead, publicationHeads, expectedMirrorHeads, inboundMoveMountIds);
4456
- }
4484
+ },
4485
+ hydrationHooks: workspaceHydrationTransactionHooks
4457
4486
  });
4458
4487
  input.assertStillAdmitted?.();
4459
4488
  if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
@@ -5047,7 +5076,10 @@ async function startWorker(options) {
5047
5076
  sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
5048
5077
  busy: mount.busyForRecovery ?? mount.busy
5049
5078
  }));
5050
- await recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts, { preserveStaleBases: true });
5079
+ await recoverWorkspaceGitHydration(workspaceShadowRoot, recoveryMounts, {
5080
+ preserveStaleBases: true,
5081
+ hydrationHooks: workspaceHydrationTransactionHooks
5082
+ });
5051
5083
  for (const project of message.projects) {
5052
5084
  for (const { branchName } of project.preserveOnlyBranches) {
5053
5085
  if (stillPendingCreatedBranches.has(pendingCreatedBranchKey(project.projectId, branchName))) {
@@ -5091,7 +5123,7 @@ async function startWorker(options) {
5091
5123
  // layouts can be marked ready. No process can resolve an unready
5092
5124
  // target, and preserving the fetched outer tree here prevents clean
5093
5125
  // checkout initialization from erasing synchronized dirt.
5094
- { ignoreBusy: true }
5126
+ { ignoreBusy: true, hydrationHooks: workspaceHydrationTransactionHooks }
5095
5127
  );
5096
5128
  await ensureConfiguredProjects((progress) => {
5097
5129
  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": "module"
5
5
  }