@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 +37 -5
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/working-tree-mirror.cjs +256 -4
- package/dist/cjs/workspace-git-sync.cjs +179 -87
- package/dist/mjs/main.mjs +37 -5
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/working-tree-mirror.mjs +250 -3
- package/dist/mjs/workspace-git-sync.mjs +188 -88
- package/dist/types/main.d.ts +181 -0
- package/dist/types/working-tree-mirror.d.ts +80 -1
- package/dist/types/workspace-git-sync.d.ts +25 -0
- package/package.json +1 -1
|
@@ -6,7 +6,15 @@ import {
|
|
|
6
6
|
mergeWorkspaceProjectionMount,
|
|
7
7
|
workspaceMergeProjectionSupport
|
|
8
8
|
} from "./workspace-merge-projection.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
captureWorkingTreeMirrorPlan,
|
|
11
|
+
fsyncWorkingTreePaths,
|
|
12
|
+
inspectWorkingTree,
|
|
13
|
+
mirrorWorkingTree,
|
|
14
|
+
normalizeWorkingTreeRelativePath,
|
|
15
|
+
planWorkingTreeMirror,
|
|
16
|
+
restoreWorkingTreeMirrorScope
|
|
17
|
+
} from "./working-tree-mirror.mjs";
|
|
10
18
|
import { assertManagedDirectoryPath } from "./workspace-mount-boundary.mjs";
|
|
11
19
|
const WORKSPACE_GIT_BRANCH = "main";
|
|
12
20
|
const MAX_WORKSPACE_GIT_DIFF_BYTES = 5 * 1024 * 1024;
|
|
@@ -909,7 +917,7 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
909
917
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
910
918
|
}
|
|
911
919
|
const candidate = parsed;
|
|
912
|
-
if (candidate.version !== 2 && candidate.version !== 3 || !Array.isArray(candidate.mounts)) {
|
|
920
|
+
if (candidate.version !== 2 && candidate.version !== 3 && candidate.version !== 4 || !Array.isArray(candidate.mounts)) {
|
|
913
921
|
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
914
922
|
}
|
|
915
923
|
const parseReceipt = (rawReceipt) => {
|
|
@@ -922,6 +930,25 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
922
930
|
};
|
|
923
931
|
const targetReceipt = parseReceipt(candidate.targetReceipt);
|
|
924
932
|
const receiptBefore = parseReceipt(candidate.receiptBefore);
|
|
933
|
+
const parseScope = (rawScope, existed) => {
|
|
934
|
+
if (rawScope === void 0) return void 0;
|
|
935
|
+
if (candidate.version !== 4 || !existed || !rawScope || typeof rawScope !== "object") {
|
|
936
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
937
|
+
}
|
|
938
|
+
const parsePaths = (rawPaths) => {
|
|
939
|
+
if (!Array.isArray(rawPaths)) throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
940
|
+
const seen = /* @__PURE__ */ new Set();
|
|
941
|
+
return rawPaths.map((rawPath) => {
|
|
942
|
+
if (typeof rawPath !== "string" || normalizeWorkingTreeRelativePath(rawPath) !== rawPath || seen.has(rawPath)) {
|
|
943
|
+
throw new Error(`Workspace hydration transaction manifest is invalid: ${manifestPath}`);
|
|
944
|
+
}
|
|
945
|
+
seen.add(rawPath);
|
|
946
|
+
return rawPath;
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
const { absent, trees } = rawScope;
|
|
950
|
+
return { absent: parsePaths(absent), trees: parsePaths(trees) };
|
|
951
|
+
};
|
|
925
952
|
const seenIds = /* @__PURE__ */ new Set();
|
|
926
953
|
const seenSnapshots = /* @__PURE__ */ new Set();
|
|
927
954
|
const mounts = candidate.mounts.map((rawMount) => {
|
|
@@ -938,10 +965,20 @@ function parseHydrationTransactionManifest(transactionPath) {
|
|
|
938
965
|
}
|
|
939
966
|
seenIds.add(mount.id);
|
|
940
967
|
seenSnapshots.add(mount.snapshotName);
|
|
941
|
-
|
|
968
|
+
const scope = parseScope(rawMount.scope, mount.existed);
|
|
969
|
+
return {
|
|
970
|
+
id: mount.id,
|
|
971
|
+
incarnationKey: mount.incarnationKey,
|
|
972
|
+
sourcePath: mount.sourcePath,
|
|
973
|
+
durabilityRootPath: mount.durabilityRootPath,
|
|
974
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
975
|
+
existed: mount.existed,
|
|
976
|
+
snapshotName: mount.snapshotName,
|
|
977
|
+
...scope ? { scope } : {}
|
|
978
|
+
};
|
|
942
979
|
});
|
|
943
980
|
return {
|
|
944
|
-
version: 3,
|
|
981
|
+
version: candidate.version === 4 ? 4 : 3,
|
|
945
982
|
targetReceipt,
|
|
946
983
|
receiptBefore,
|
|
947
984
|
mounts
|
|
@@ -963,6 +1000,7 @@ function hydrationTransactionCommitted(transactionPath) {
|
|
|
963
1000
|
}
|
|
964
1001
|
function restoreHydrationTransaction(workspacePath, manifest) {
|
|
965
1002
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1003
|
+
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
966
1004
|
for (const mount of manifest.mounts) {
|
|
967
1005
|
assertManagedDirectoryPath({
|
|
968
1006
|
trustedRoot: mount.durabilityRootPath,
|
|
@@ -978,6 +1016,11 @@ function restoreHydrationTransaction(workspacePath, manifest) {
|
|
|
978
1016
|
if (!snapshotStatus.isDirectory() || snapshotStatus.isSymbolicLink()) {
|
|
979
1017
|
throw new Error(`Workspace hydration snapshot is not a regular directory: ${snapshotPath}`);
|
|
980
1018
|
}
|
|
1019
|
+
if (mount.scope) {
|
|
1020
|
+
const restored = restoreWorkingTreeMirrorScope({ snapshotRoot: snapshotPath, targetRoot: mount.sourcePath, scope: mount.scope });
|
|
1021
|
+
durabilityScopes.set(mount.id, restored.paths);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
981
1024
|
mirrorWorkingTree({ sourceRoot: snapshotPath, targetRoot: mount.sourcePath, sourceMode: "all", deletionMode: "all" });
|
|
982
1025
|
}
|
|
983
1026
|
fsyncHydratedWorkspaceMounts(
|
|
@@ -986,7 +1029,8 @@ function restoreHydrationTransaction(workspacePath, manifest) {
|
|
|
986
1029
|
hydrationIncarnationKey: mount.incarnationKey,
|
|
987
1030
|
sourceMode: "all",
|
|
988
1031
|
hydrateDeletionMode: "all"
|
|
989
|
-
}))
|
|
1032
|
+
})),
|
|
1033
|
+
durabilityScopes
|
|
990
1034
|
);
|
|
991
1035
|
}
|
|
992
1036
|
function recoverPendingHydrationTransaction(workspacePath) {
|
|
@@ -1030,57 +1074,80 @@ function beginHydrationTransaction(workspacePath, mounts, targetReceipt) {
|
|
|
1030
1074
|
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1031
1075
|
const stagingPath = path.join(stateDirectory, `.workspace-hydration-transaction.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1032
1076
|
fs.mkdirSync(stagingPath, { mode: 448 });
|
|
1077
|
+
const durabilityScopes = /* @__PURE__ */ new Map();
|
|
1033
1078
|
try {
|
|
1034
1079
|
const snapshotRoot = path.join(stagingPath, "mounts");
|
|
1035
1080
|
fs.mkdirSync(snapshotRoot, { mode: 448 });
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1081
|
+
const receiptBefore = readHydratedWorkspaceReceipt(workspacePath);
|
|
1082
|
+
const manifestMounts = mounts.map((mount, index) => {
|
|
1083
|
+
const sourcePath = path.resolve(mount.sourcePath);
|
|
1084
|
+
const durabilityRootPath = path.resolve(mount.durabilityRootPath);
|
|
1085
|
+
assertManagedDirectoryPath({
|
|
1086
|
+
trustedRoot: durabilityRootPath,
|
|
1087
|
+
candidate: sourcePath,
|
|
1088
|
+
label: `Workspace hydration snapshot mount ${mount.id}`
|
|
1089
|
+
});
|
|
1090
|
+
const sourceStatus = lstatIfExists(sourcePath);
|
|
1091
|
+
if (sourceStatus && (!sourceStatus.isDirectory() || sourceStatus.isSymbolicLink())) {
|
|
1092
|
+
throw new Error(`Workspace hydration snapshot source is not a regular directory: ${sourcePath}`);
|
|
1093
|
+
}
|
|
1094
|
+
const snapshotName = String(index);
|
|
1095
|
+
const snapshotPath = path.join(snapshotRoot, snapshotName);
|
|
1096
|
+
fs.mkdirSync(snapshotPath, { mode: 448 });
|
|
1097
|
+
let scope;
|
|
1098
|
+
if (sourceStatus && mount.hydrateDeletionMode === "git") {
|
|
1099
|
+
const outerPath = workspaceMountOuterPath(workspacePath, mount);
|
|
1043
1100
|
assertManagedDirectoryPath({
|
|
1044
|
-
trustedRoot:
|
|
1045
|
-
candidate:
|
|
1046
|
-
label: `Workspace
|
|
1101
|
+
trustedRoot: workspacePath,
|
|
1102
|
+
candidate: outerPath,
|
|
1103
|
+
label: `Workspace mount ${mount.id} outer source`
|
|
1047
1104
|
});
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1105
|
+
const plan = planWorkingTreeMirror({ sourceRoot: outerPath, targetRoot: sourcePath, sourceMode: "all", deletionMode: "git" });
|
|
1106
|
+
scope = captureWorkingTreeMirrorPlan({
|
|
1107
|
+
plan,
|
|
1108
|
+
targetRoot: sourcePath,
|
|
1109
|
+
snapshotRoot: snapshotPath,
|
|
1110
|
+
// Same private-staging deferral as the complete snapshot below.
|
|
1111
|
+
durability: "deferred_private_staging"
|
|
1112
|
+
});
|
|
1113
|
+
durabilityScopes.set(mount.id, [...plan.desired.keys()]);
|
|
1114
|
+
} else if (sourceStatus) {
|
|
1115
|
+
mirrorWorkingTree({
|
|
1116
|
+
sourceRoot: sourcePath,
|
|
1117
|
+
targetRoot: snapshotPath,
|
|
1118
|
+
sourceMode: "all",
|
|
1119
|
+
deletionMode: "all",
|
|
1120
|
+
// This tree is still private and the source authority is untouched.
|
|
1121
|
+
// The single recursive barrier below makes the complete snapshot
|
|
1122
|
+
// durable before its directory entry is published or hydration starts.
|
|
1123
|
+
durability: "deferred_private_staging"
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
id: mount.id,
|
|
1128
|
+
incarnationKey: mount.hydrationIncarnationKey,
|
|
1129
|
+
sourcePath,
|
|
1130
|
+
durabilityRootPath,
|
|
1131
|
+
workspaceRelativePath: mount.workspaceRelativePath,
|
|
1132
|
+
existed: Boolean(sourceStatus),
|
|
1133
|
+
snapshotName,
|
|
1134
|
+
...scope ? { scope } : {}
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
const manifest = {
|
|
1138
|
+
// Older workers reject version 4, so a scoped snapshot can never be
|
|
1139
|
+
// replayed as a complete copy after a downgrade.
|
|
1140
|
+
version: manifestMounts.some((mount) => mount.scope) ? 4 : 3,
|
|
1141
|
+
targetReceipt,
|
|
1142
|
+
receiptBefore,
|
|
1143
|
+
mounts: manifestMounts
|
|
1077
1144
|
};
|
|
1078
1145
|
writePrivateFileDurably(path.join(stagingPath, "manifest.json"), `${JSON.stringify(manifest)}
|
|
1079
1146
|
`);
|
|
1080
1147
|
fsyncTree(stagingPath);
|
|
1081
1148
|
fs.renameSync(stagingPath, transactionPath);
|
|
1082
1149
|
fsyncDirectory(stateDirectory);
|
|
1083
|
-
return manifest;
|
|
1150
|
+
return { manifest, durabilityScopes };
|
|
1084
1151
|
} catch (error) {
|
|
1085
1152
|
fs.rmSync(stagingPath, { recursive: true, force: true });
|
|
1086
1153
|
fsyncDirectory(stateDirectory);
|
|
@@ -1092,7 +1159,11 @@ function markHydrationTransactionDurable(workspacePath) {
|
|
|
1092
1159
|
writePrivateFileDurably(path.join(transactionPath, "committed"), "committed\n");
|
|
1093
1160
|
fsyncDirectory(transactionPath);
|
|
1094
1161
|
}
|
|
1095
|
-
function
|
|
1162
|
+
function gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus) {
|
|
1163
|
+
if (mount.hydrateDeletionMode !== "git" || !sourceStatus.isDirectory() || !lstatIfExists(path.join(sourcePath, ".git"))) return null;
|
|
1164
|
+
return [...inspectWorkingTree(sourcePath, "git").keys()];
|
|
1165
|
+
}
|
|
1166
|
+
function fsyncHydratedWorkspaceMount(mount, scope) {
|
|
1096
1167
|
assertManagedDirectoryPath({
|
|
1097
1168
|
trustedRoot: mount.durabilityRootPath,
|
|
1098
1169
|
candidate: mount.sourcePath,
|
|
@@ -1117,7 +1188,17 @@ function fsyncHydratedWorkspaceMount(mount) {
|
|
|
1117
1188
|
fs.closeSync(descriptor);
|
|
1118
1189
|
}
|
|
1119
1190
|
};
|
|
1120
|
-
|
|
1191
|
+
const sourcePath = path.resolve(mount.sourcePath);
|
|
1192
|
+
const sourceStatus = lstatIfExists(sourcePath);
|
|
1193
|
+
if (sourceStatus) {
|
|
1194
|
+
const paths = scope ?? gitVisibleDurabilityPaths(mount, sourcePath, sourceStatus);
|
|
1195
|
+
if (paths) {
|
|
1196
|
+
if (!sourceStatus.isDirectory()) throw new Error(`Workspace mount ${mount.id} source is not a regular directory: ${sourcePath}`);
|
|
1197
|
+
fsyncWorkingTreePaths(sourcePath, paths);
|
|
1198
|
+
} else {
|
|
1199
|
+
syncEntry(sourcePath, true);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1121
1202
|
const durabilityRoot = path.resolve(mount.durabilityRootPath);
|
|
1122
1203
|
let boundaryParent = path.dirname(path.resolve(mount.sourcePath));
|
|
1123
1204
|
while (true) {
|
|
@@ -1136,8 +1217,8 @@ function fsyncHydratedWorkspaceMount(mount) {
|
|
|
1136
1217
|
boundaryParent = nextParent;
|
|
1137
1218
|
}
|
|
1138
1219
|
}
|
|
1139
|
-
function fsyncHydratedWorkspaceMounts(mounts) {
|
|
1140
|
-
for (const mount of mounts) fsyncHydratedWorkspaceMount(mount);
|
|
1220
|
+
function fsyncHydratedWorkspaceMounts(mounts, scopes = /* @__PURE__ */ new Map()) {
|
|
1221
|
+
for (const mount of mounts) fsyncHydratedWorkspaceMount(mount, scopes.get(mount.id));
|
|
1141
1222
|
}
|
|
1142
1223
|
function workspaceIsShallow(workspacePath) {
|
|
1143
1224
|
return gitResult(workspacePath, ["rev-parse", "--is-shallow-repository"]).stdout === "true";
|
|
@@ -1386,6 +1467,9 @@ function restoreDeferredWorkspaceMountsFromHead(workspacePath, mounts) {
|
|
|
1386
1467
|
}
|
|
1387
1468
|
}
|
|
1388
1469
|
}
|
|
1470
|
+
function workspaceMountOuterPath(workspacePath, mount) {
|
|
1471
|
+
return path.join(workspacePath, ...mount.workspaceRelativePath.replace(/\\/g, "/").split("/"));
|
|
1472
|
+
}
|
|
1389
1473
|
function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
1390
1474
|
const hydratedMountIds = [];
|
|
1391
1475
|
const skippedMountIds = [];
|
|
@@ -1394,7 +1478,7 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1394
1478
|
skippedMountIds.push(mount.id);
|
|
1395
1479
|
continue;
|
|
1396
1480
|
}
|
|
1397
|
-
const sourceRoot =
|
|
1481
|
+
const sourceRoot = workspaceMountOuterPath(workspacePath, mount);
|
|
1398
1482
|
assertManagedDirectoryPath({
|
|
1399
1483
|
trustedRoot: workspacePath,
|
|
1400
1484
|
candidate: sourceRoot,
|
|
@@ -1480,56 +1564,67 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1480
1564
|
const busyMountIds = new Set(input.ignoreBusy ? [] : candidateMounts.filter((mount) => mount.busy?.()).map(({ id }) => id));
|
|
1481
1565
|
const requestedIds = new Set(input.mounts.map(({ id }) => id));
|
|
1482
1566
|
const requiredIds = new Set(requiredMounts.map(({ id }) => id));
|
|
1483
|
-
const
|
|
1484
|
-
const
|
|
1567
|
+
const hydrationMounts = input.mounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1568
|
+
const advancedDurabilityMounts = durabilityMounts.filter(
|
|
1485
1569
|
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1486
1570
|
);
|
|
1487
1571
|
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1488
1572
|
const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1489
1573
|
return {
|
|
1490
1574
|
busyMountIds,
|
|
1491
|
-
hydrationMounts
|
|
1492
|
-
advancedDurabilityMounts
|
|
1575
|
+
hydrationMounts,
|
|
1576
|
+
advancedDurabilityMounts,
|
|
1493
1577
|
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
|
|
1494
|
-
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...
|
|
1578
|
+
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts, ...carriedMounts], receiptMounts) : null
|
|
1495
1579
|
};
|
|
1496
1580
|
};
|
|
1497
1581
|
const noopCandidate = computeSelection();
|
|
1498
1582
|
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)) {
|
|
1499
1583
|
return { hydratedMountIds: [], skippedMountIds: noopCandidate.skippedMountIds };
|
|
1500
1584
|
}
|
|
1501
|
-
const
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1585
|
+
const hooks = input.hydrationHooks;
|
|
1586
|
+
const context = {
|
|
1587
|
+
transactionId: crypto.randomUUID(),
|
|
1588
|
+
workspacePath,
|
|
1589
|
+
mountIds: [...new Set([...noopCandidate.hydrationMounts, ...noopCandidate.advancedDurabilityMounts].map(({ id }) => id))].sort()
|
|
1590
|
+
};
|
|
1591
|
+
if (hooks) await hooks.beforeTransaction(context);
|
|
1504
1592
|
try {
|
|
1505
|
-
const
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
throw new Error("Workspace hydration did not include every required mount");
|
|
1509
|
-
}
|
|
1510
|
-
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts);
|
|
1511
|
-
markHydrationTransactionDurable(workspacePath);
|
|
1512
|
-
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1513
|
-
removeHydrationTransaction(workspacePath);
|
|
1514
|
-
return {
|
|
1515
|
-
hydratedMountIds: hydration.hydratedMountIds,
|
|
1516
|
-
skippedMountIds: selection.skippedMountIds
|
|
1517
|
-
};
|
|
1518
|
-
} catch (error) {
|
|
1519
|
-
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1520
|
-
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1521
|
-
throw error;
|
|
1522
|
-
}
|
|
1593
|
+
const selection = computeSelection();
|
|
1594
|
+
const { hydrationMounts, advancedDurabilityMounts, targetReceipt } = selection;
|
|
1595
|
+
const { manifest, durabilityScopes } = beginHydrationTransaction(workspacePath, hydrationMounts, targetReceipt);
|
|
1523
1596
|
try {
|
|
1524
|
-
|
|
1597
|
+
const hydration = hydrateWorkspaceGitMountsRaw(workspacePath, hydrationMounts, { ignoreBusy: true });
|
|
1598
|
+
const expectedHydratedIds = hydrationMounts.map(({ id }) => id).sort();
|
|
1599
|
+
if (hydration.skippedMountIds.length > 0 || hydration.hydratedMountIds.length !== expectedHydratedIds.length || hydration.hydratedMountIds.some((id, index) => id !== expectedHydratedIds[index])) {
|
|
1600
|
+
throw new Error("Workspace hydration did not include every required mount");
|
|
1601
|
+
}
|
|
1602
|
+
fsyncHydratedWorkspaceMounts(advancedDurabilityMounts, durabilityScopes);
|
|
1603
|
+
markHydrationTransactionDurable(workspacePath);
|
|
1604
|
+
if (targetReceipt) updateHydratedWorkspaceReceipt(workspacePath, targetReceipt);
|
|
1525
1605
|
removeHydrationTransaction(workspacePath);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1606
|
+
return {
|
|
1607
|
+
hydratedMountIds: hydration.hydratedMountIds,
|
|
1608
|
+
skippedMountIds: selection.skippedMountIds
|
|
1609
|
+
};
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
const transactionPath = hydrationTransactionPath(workspacePath);
|
|
1612
|
+
if (lstatIfExists(transactionPath) && hydrationTransactionCommitted(transactionPath)) {
|
|
1613
|
+
throw error;
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
restoreHydrationTransaction(workspacePath, manifest);
|
|
1617
|
+
removeHydrationTransaction(workspacePath);
|
|
1618
|
+
} catch (restoreError) {
|
|
1619
|
+
throw new AggregateError(
|
|
1620
|
+
[error, restoreError],
|
|
1621
|
+
`Workspace hydration failed and its durable snapshot could not be restored: ${error instanceof Error ? error.message : String(error)}`
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
throw error;
|
|
1531
1625
|
}
|
|
1532
|
-
|
|
1626
|
+
} finally {
|
|
1627
|
+
hooks?.afterTransaction(context);
|
|
1533
1628
|
}
|
|
1534
1629
|
}
|
|
1535
1630
|
async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
@@ -1541,7 +1636,8 @@ async function hydrateWorkspaceGitMounts(workspacePath, mounts, options = {}) {
|
|
|
1541
1636
|
return hydrateWorkspaceGitMountsTransactionally({
|
|
1542
1637
|
workspacePath,
|
|
1543
1638
|
mounts,
|
|
1544
|
-
ignoreBusy: options.ignoreBusy
|
|
1639
|
+
ignoreBusy: options.ignoreBusy,
|
|
1640
|
+
hydrationHooks: options.hydrationHooks
|
|
1545
1641
|
});
|
|
1546
1642
|
}
|
|
1547
1643
|
async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {}) {
|
|
@@ -1581,7 +1677,8 @@ async function recoverWorkspaceGitHydration(workspacePath, mounts, options = {})
|
|
|
1581
1677
|
receiptMounts: configuredBasisMounts,
|
|
1582
1678
|
requiredMounts: hydrationTargets,
|
|
1583
1679
|
ignoreBusy: options.ignoreBusy,
|
|
1584
|
-
recordCurrentHead: true
|
|
1680
|
+
recordCurrentHead: true,
|
|
1681
|
+
hydrationHooks: options.hydrationHooks
|
|
1585
1682
|
});
|
|
1586
1683
|
void hydration;
|
|
1587
1684
|
}
|
|
@@ -1632,7 +1729,8 @@ async function resetWorkspaceGit(input) {
|
|
|
1632
1729
|
durabilityMounts: [...requiredLiveMounts, ...selected.tombstones, ...durablyAbsentMounts],
|
|
1633
1730
|
receiptMounts: input.mounts,
|
|
1634
1731
|
requiredMounts: requiredLiveMounts,
|
|
1635
|
-
recordCurrentHead: true
|
|
1732
|
+
recordCurrentHead: true,
|
|
1733
|
+
hydrationHooks: input.hydrationHooks
|
|
1636
1734
|
});
|
|
1637
1735
|
const requiredLiveIds = requiredLiveMounts.map(({ id }) => id).sort();
|
|
1638
1736
|
const resetHead = revParse(workspacePath, "HEAD");
|
|
@@ -1887,7 +1985,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1887
1985
|
await recoverWorkspaceGitHydration(workspacePath, input.mounts, {
|
|
1888
1986
|
...deferredMountIds.size > 0 ? { deferMountIds: deferredMountIds } : {},
|
|
1889
1987
|
preserveResolutionInProgress,
|
|
1890
|
-
preserveStaleBases: true
|
|
1988
|
+
preserveStaleBases: true,
|
|
1989
|
+
hydrationHooks: input.hydrationHooks
|
|
1891
1990
|
});
|
|
1892
1991
|
const selected = activeMounts(input.mounts);
|
|
1893
1992
|
const projectionMutationTokens = new Map(
|
|
@@ -2013,7 +2112,8 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2013
2112
|
carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
|
|
2014
2113
|
receiptMounts: completionReceiptMounts,
|
|
2015
2114
|
requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2016
|
-
recordCurrentHead: true
|
|
2115
|
+
recordCurrentHead: true,
|
|
2116
|
+
hydrationHooks: input.hydrationHooks
|
|
2017
2117
|
});
|
|
2018
2118
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
2019
2119
|
refreshCycleSkippedMounts(completionReceiptMounts);
|
package/dist/types/main.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
3
|
import type { WorkerGitIdentity } from "./git-identity";
|
|
4
4
|
import { configureGitHubRegistryAuthFiles, type PreparedPrivateAuthFileGeneration } from "./registry-auth";
|
|
5
|
+
import { type WorkspaceHydrationTransactionHooks } from "./workspace-git-sync";
|
|
5
6
|
type WorkerProjectConfig = {
|
|
6
7
|
projectId: string;
|
|
7
8
|
checkoutPathSegments: [namespace: string, project: string];
|
|
@@ -23,6 +24,11 @@ type WorkerProjectConfig = {
|
|
|
23
24
|
baseCommitHash: string;
|
|
24
25
|
}>;
|
|
25
26
|
};
|
|
27
|
+
type WorkerAheadOfOriginBranch = {
|
|
28
|
+
projectId: string;
|
|
29
|
+
branchName: string;
|
|
30
|
+
ahead: number;
|
|
31
|
+
};
|
|
26
32
|
type WorkerLifecycleProgress = {
|
|
27
33
|
phase: "starting" | "recovering" | "configuring" | "syncing" | "ready" | "failed";
|
|
28
34
|
operationId: string;
|
|
@@ -39,9 +45,28 @@ declare function workerServerHeartbeatAction(input: {
|
|
|
39
45
|
timeoutMs: number;
|
|
40
46
|
watchdogSweepDelayed: boolean;
|
|
41
47
|
}): "wait" | "start_probe" | "terminate";
|
|
48
|
+
/**
|
|
49
|
+
* A hydration transaction blocks this event loop until it ends, so no pong,
|
|
50
|
+
* progress, or result can leave the process while it runs; on a slow disk
|
|
51
|
+
* that is hours. Declare a hydration heartbeat lease first: the server keeps
|
|
52
|
+
* the control socket open and defers the affected request deadlines while the
|
|
53
|
+
* lease is active instead of reading the silence as a dead worker, which is
|
|
54
|
+
* what turned one slow hydration into a mandatory canonical reset (incident
|
|
55
|
+
* 3bccab03). Bun writes the frame to the socket synchronously; the extra
|
|
56
|
+
* event-loop turn lets control messages that were already queued run before
|
|
57
|
+
* the busy observation that precedes the transaction body.
|
|
58
|
+
*/
|
|
59
|
+
declare function createWorkspaceHydrationLeaseHooks(send: (message: Extract<WorkerClientMessage, {
|
|
60
|
+
type: "heartbeat_lease";
|
|
61
|
+
}>) => void, options?: {
|
|
62
|
+
yieldToEventLoop?: () => Promise<void>;
|
|
63
|
+
log?: (line: string) => void;
|
|
64
|
+
now?: () => number;
|
|
65
|
+
}): WorkspaceHydrationTransactionHooks;
|
|
42
66
|
export declare const workerLifecycleTestHarness: {
|
|
43
67
|
boundedWorkerLifecycleProgress: typeof boundedWorkerLifecycleProgress;
|
|
44
68
|
workerServerHeartbeatAction: typeof workerServerHeartbeatAction;
|
|
69
|
+
createWorkspaceHydrationLeaseHooks: typeof createWorkspaceHydrationLeaseHooks;
|
|
45
70
|
};
|
|
46
71
|
type WorkspaceSyncTrigger = {
|
|
47
72
|
type: "connect" | "inbound_head" | "write" | "edit" | "shell_inline" | "process_terminal" | "process_cancel" | "periodic" | "manual" | "remediation" | "remediation_confirm" | "remediation_reset";
|
|
@@ -103,6 +128,144 @@ export type WorkerSessionTarget = {
|
|
|
103
128
|
ownerUserId: string;
|
|
104
129
|
rootProfile: "visible_projects" | "canonical_sync";
|
|
105
130
|
};
|
|
131
|
+
type WorkerClientMessage = {
|
|
132
|
+
type: "hello";
|
|
133
|
+
hostInfo?: Record<string, unknown>;
|
|
134
|
+
startupReady?: boolean;
|
|
135
|
+
} | {
|
|
136
|
+
type: "worker_progress";
|
|
137
|
+
progress: WorkerLifecycleProgress;
|
|
138
|
+
} | {
|
|
139
|
+
type: "heartbeat_lease";
|
|
140
|
+
operationId: string;
|
|
141
|
+
active: boolean;
|
|
142
|
+
/** A hydration transaction may block the worker for hours; the server bounds it separately from preparation leases. */
|
|
143
|
+
kind?: "hydration";
|
|
144
|
+
} | {
|
|
145
|
+
type: "startup_ready";
|
|
146
|
+
} | {
|
|
147
|
+
type: "active_processes";
|
|
148
|
+
processes: Array<{
|
|
149
|
+
runId: string;
|
|
150
|
+
target: WorkerSessionTarget;
|
|
151
|
+
sessionId: string;
|
|
152
|
+
platform: string;
|
|
153
|
+
arch: string;
|
|
154
|
+
mode: "foreground" | "detached";
|
|
155
|
+
pid?: number;
|
|
156
|
+
processGroupId?: number;
|
|
157
|
+
credentialId?: string;
|
|
158
|
+
argv: string[];
|
|
159
|
+
command: string;
|
|
160
|
+
cwd?: string;
|
|
161
|
+
startedAt: string;
|
|
162
|
+
interactive?: boolean;
|
|
163
|
+
workspaceEffect?: "none";
|
|
164
|
+
}>;
|
|
165
|
+
} | {
|
|
166
|
+
type: "pty_opened";
|
|
167
|
+
requestId: string;
|
|
168
|
+
ptyId: string;
|
|
169
|
+
} | {
|
|
170
|
+
type: "pty_output";
|
|
171
|
+
ptyId: string;
|
|
172
|
+
data: string;
|
|
173
|
+
} | {
|
|
174
|
+
type: "pty_exit";
|
|
175
|
+
ptyId: string;
|
|
176
|
+
exitCode: number;
|
|
177
|
+
signal?: number | string;
|
|
178
|
+
} | {
|
|
179
|
+
type: "pty_error";
|
|
180
|
+
ptyId: string;
|
|
181
|
+
requestId?: string;
|
|
182
|
+
error: string;
|
|
183
|
+
} | {
|
|
184
|
+
type: "operation_result";
|
|
185
|
+
requestId: string;
|
|
186
|
+
result?: WorkerOperationResult;
|
|
187
|
+
error?: string;
|
|
188
|
+
} | {
|
|
189
|
+
type: "exec_result";
|
|
190
|
+
requestId: string;
|
|
191
|
+
stdout: string;
|
|
192
|
+
stderr: string;
|
|
193
|
+
exitCode: number;
|
|
194
|
+
durationMs?: number;
|
|
195
|
+
error?: string;
|
|
196
|
+
} | {
|
|
197
|
+
type: "exec_accepted";
|
|
198
|
+
requestId: string;
|
|
199
|
+
runId: string;
|
|
200
|
+
} | {
|
|
201
|
+
type: "exec_started";
|
|
202
|
+
requestId: string;
|
|
203
|
+
runId: string;
|
|
204
|
+
cwd: string;
|
|
205
|
+
pid?: number;
|
|
206
|
+
processGroupId?: number;
|
|
207
|
+
} | {
|
|
208
|
+
type: "exec_start_error";
|
|
209
|
+
requestId: string;
|
|
210
|
+
runId: string;
|
|
211
|
+
error: string;
|
|
212
|
+
} | {
|
|
213
|
+
type: "exec_output";
|
|
214
|
+
runId: string;
|
|
215
|
+
stream: "stdout" | "stderr";
|
|
216
|
+
data: string;
|
|
217
|
+
} | {
|
|
218
|
+
type: "exec_exit";
|
|
219
|
+
runId: string;
|
|
220
|
+
exitCode: number;
|
|
221
|
+
durationMs?: number;
|
|
222
|
+
timedOut?: boolean;
|
|
223
|
+
workspaceEffect?: "none";
|
|
224
|
+
canonicalWorkspaceHead?: string;
|
|
225
|
+
} | {
|
|
226
|
+
type: "exec_error";
|
|
227
|
+
runId: string;
|
|
228
|
+
error: string;
|
|
229
|
+
durationMs?: number;
|
|
230
|
+
workspaceEffect?: "none";
|
|
231
|
+
canonicalWorkspaceHead?: string;
|
|
232
|
+
} | {
|
|
233
|
+
type: "cancel_result";
|
|
234
|
+
requestId: string;
|
|
235
|
+
runId: string;
|
|
236
|
+
cancelled: boolean;
|
|
237
|
+
message?: string;
|
|
238
|
+
} | {
|
|
239
|
+
type: "pong";
|
|
240
|
+
} | {
|
|
241
|
+
type: "workspace_configured";
|
|
242
|
+
requestId: string;
|
|
243
|
+
result: WorkspaceSyncResult;
|
|
244
|
+
pendingCheckouts: Array<{
|
|
245
|
+
projectId: string;
|
|
246
|
+
branchName: string;
|
|
247
|
+
}>;
|
|
248
|
+
aheadOfOriginBranches: WorkerAheadOfOriginBranch[];
|
|
249
|
+
deferredWorkspaceSyncForIncidentId?: string;
|
|
250
|
+
} | {
|
|
251
|
+
type: "workspace_sync_started";
|
|
252
|
+
attemptId: string;
|
|
253
|
+
} | {
|
|
254
|
+
type: "workspace_sync_result";
|
|
255
|
+
requestId?: string;
|
|
256
|
+
result: WorkspaceSyncResult;
|
|
257
|
+
} | {
|
|
258
|
+
type: "session_artifacts_sync_result";
|
|
259
|
+
requestId: string;
|
|
260
|
+
sessionId: string;
|
|
261
|
+
error?: string;
|
|
262
|
+
} | {
|
|
263
|
+
type: "update_clis_result";
|
|
264
|
+
requestId: string;
|
|
265
|
+
installedWorkerVersion?: string;
|
|
266
|
+
installedR5dctlVersion?: string;
|
|
267
|
+
error?: string;
|
|
268
|
+
};
|
|
106
269
|
type WorkerServerMessage = {
|
|
107
270
|
type: "connected";
|
|
108
271
|
label: string;
|
|
@@ -371,6 +534,23 @@ type WorkerViewFileBytesResult = {
|
|
|
371
534
|
width: number;
|
|
372
535
|
height: number;
|
|
373
536
|
};
|
|
537
|
+
type WorkerExecStdinAck = {
|
|
538
|
+
type: "exec_stdin";
|
|
539
|
+
runId: string;
|
|
540
|
+
bytesWritten: number;
|
|
541
|
+
eof: boolean;
|
|
542
|
+
};
|
|
543
|
+
type WorkerCreateProjectBranchResult = {
|
|
544
|
+
type: "create_project_branch";
|
|
545
|
+
branchId: string;
|
|
546
|
+
branchName: string;
|
|
547
|
+
baseCommitHash: string;
|
|
548
|
+
};
|
|
549
|
+
type WorkerDeleteProjectBranchResult = {
|
|
550
|
+
type: "delete_project_branch";
|
|
551
|
+
branchId: string;
|
|
552
|
+
branchName: string;
|
|
553
|
+
};
|
|
374
554
|
type WorkerCodeEntry = {
|
|
375
555
|
name: string;
|
|
376
556
|
type: "file" | "directory";
|
|
@@ -389,6 +569,7 @@ type WorkerCodeReadResult = {
|
|
|
389
569
|
mtime: string;
|
|
390
570
|
base64: string;
|
|
391
571
|
};
|
|
572
|
+
type WorkerOperationResult = WorkerReadFileResult | WorkerWriteFileResult | WorkerEditFileResult | WorkerGrepResult | WorkerFindResult | WorkerLsResult | WorkerViewFileBytesResult | WorkerExecStdinAck | WorkerCreateProjectBranchResult | WorkerDeleteProjectBranchResult | WorkerCodeListResult | WorkerCodeReadResult;
|
|
392
573
|
declare class WorkspaceSyncAdmissionChangedError extends Error {
|
|
393
574
|
readonly name = "WorkspaceSyncAdmissionChangedError";
|
|
394
575
|
}
|