@ricsam/r5d-worker 0.0.94 → 0.0.96
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 +145 -28
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-mirror-fetch-policy.cjs +41 -0
- package/dist/cjs/project-mirror-refs-token.cjs +51 -0
- package/dist/cjs/project-worktrees.cjs +40 -15
- package/dist/cjs/workspace-git-sync.cjs +146 -14
- package/dist/mjs/main.mjs +146 -28
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-mirror-fetch-policy.mjs +16 -0
- package/dist/mjs/project-mirror-refs-token.mjs +24 -0
- package/dist/mjs/project-worktrees.mjs +44 -15
- package/dist/mjs/workspace-git-sync.mjs +146 -14
- package/dist/types/main.d.ts +9 -0
- package/dist/types/project-mirror-fetch-policy.d.ts +29 -0
- package/dist/types/project-mirror-refs-token.d.ts +19 -0
- package/dist/types/project-worktrees.d.ts +21 -0
- package/dist/types/workspace-git-sync.d.ts +14 -1
- package/package.json +1 -1
|
@@ -1483,13 +1483,38 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
|
|
|
1483
1483
|
}
|
|
1484
1484
|
return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
|
|
1485
1485
|
}
|
|
1486
|
+
async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
|
|
1487
|
+
const candidates = mounts.flatMap((mount) => {
|
|
1488
|
+
const basisHead = workspaceHydrationReceiptMountBasisHead(receipt, mount);
|
|
1489
|
+
return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
|
|
1490
|
+
});
|
|
1491
|
+
if (candidates.length === 0) return /* @__PURE__ */ new Set();
|
|
1492
|
+
const objects = await resolveGitObjectIdsAsync(
|
|
1493
|
+
workspacePath,
|
|
1494
|
+
candidates.flatMap(({ mount, basisHead }) => [
|
|
1495
|
+
workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
|
|
1496
|
+
workspaceTreeObjectName(head, mount.workspaceRelativePath)
|
|
1497
|
+
])
|
|
1498
|
+
);
|
|
1499
|
+
const unchanged = /* @__PURE__ */ new Set();
|
|
1500
|
+
for (const { mount, basisHead } of candidates) {
|
|
1501
|
+
const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
|
|
1502
|
+
const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
|
|
1503
|
+
if (before === after) unchanged.add(mount.id);
|
|
1504
|
+
}
|
|
1505
|
+
return unchanged;
|
|
1506
|
+
}
|
|
1486
1507
|
async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
1487
1508
|
const workspacePath = import_node_path.default.resolve(input.workspacePath);
|
|
1488
1509
|
const durabilityMounts = input.durabilityMounts ?? input.mounts;
|
|
1489
1510
|
const receiptMounts = input.receiptMounts ?? durabilityMounts;
|
|
1490
1511
|
const requiredMounts = input.requiredMounts ?? input.mounts;
|
|
1512
|
+
const carryMounts = input.carryMounts ?? [];
|
|
1513
|
+
if (carryMounts.length > 0 && !input.recordCurrentHead) {
|
|
1514
|
+
throw new Error("Workspace hydration can only carry unchanged mounts while recording the current head");
|
|
1515
|
+
}
|
|
1491
1516
|
const candidateMounts = [
|
|
1492
|
-
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
|
|
1517
|
+
...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts, ...carryMounts].map((mount) => [mount.id, mount])).values()
|
|
1493
1518
|
];
|
|
1494
1519
|
if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
|
|
1495
1520
|
throw new Error("Workspace checkout is not durably materialized at its current HEAD");
|
|
@@ -1505,12 +1530,13 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
|
|
|
1505
1530
|
({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
|
|
1506
1531
|
);
|
|
1507
1532
|
const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
|
|
1533
|
+
const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
|
|
1508
1534
|
return {
|
|
1509
1535
|
busyMountIds,
|
|
1510
1536
|
hydrationMounts: hydrationMounts2,
|
|
1511
1537
|
advancedDurabilityMounts: advancedDurabilityMounts2,
|
|
1512
1538
|
skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
|
|
1513
|
-
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, advancedDurabilityMounts2, receiptMounts) : null
|
|
1539
|
+
targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
|
|
1514
1540
|
};
|
|
1515
1541
|
};
|
|
1516
1542
|
const noopCandidate = computeSelection();
|
|
@@ -1686,14 +1712,26 @@ async function changedPaths(workspacePath, baseRevision, headRevision) {
|
|
|
1686
1712
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
1687
1713
|
return (await gitAsync(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths")).split("\0").filter(Boolean).sort();
|
|
1688
1714
|
}
|
|
1689
|
-
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
1715
|
+
async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, pathspecs = []) {
|
|
1690
1716
|
const base = baseRevision ?? emptyTreeHash(workspacePath);
|
|
1691
|
-
const subprocess = Bun.spawn(
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1717
|
+
const subprocess = Bun.spawn(
|
|
1718
|
+
[
|
|
1719
|
+
"git",
|
|
1720
|
+
...NON_RECURSIVE_GIT_CONFIG,
|
|
1721
|
+
"diff",
|
|
1722
|
+
"--binary",
|
|
1723
|
+
"--no-ext-diff",
|
|
1724
|
+
base,
|
|
1725
|
+
headRevision,
|
|
1726
|
+
...pathspecs.length > 0 ? ["--", ...pathspecs] : []
|
|
1727
|
+
],
|
|
1728
|
+
{
|
|
1729
|
+
cwd: workspacePath,
|
|
1730
|
+
stdout: "pipe",
|
|
1731
|
+
stderr: "pipe",
|
|
1732
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
1733
|
+
}
|
|
1734
|
+
);
|
|
1697
1735
|
const stderrPromise = new Response(subprocess.stderr).text();
|
|
1698
1736
|
const reader = subprocess.stdout.getReader();
|
|
1699
1737
|
let total = 0;
|
|
@@ -1713,6 +1751,73 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
|
|
|
1713
1751
|
if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
|
|
1714
1752
|
return total;
|
|
1715
1753
|
}
|
|
1754
|
+
async function resolveGitObjectIdsAsync(workspacePath, names) {
|
|
1755
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
1756
|
+
const unique = [...new Set(names)];
|
|
1757
|
+
if (unique.length === 0) return resolved;
|
|
1758
|
+
const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
|
|
1759
|
+
cwd: workspacePath,
|
|
1760
|
+
stdin: Buffer.from(`${unique.join("\n")}
|
|
1761
|
+
`),
|
|
1762
|
+
stdout: "pipe",
|
|
1763
|
+
stderr: "pipe",
|
|
1764
|
+
env: (0, import_git_process_environment.workerGitProcessEnvironment)()
|
|
1765
|
+
});
|
|
1766
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1767
|
+
new Response(subprocess.stdout).text(),
|
|
1768
|
+
new Response(subprocess.stderr).text(),
|
|
1769
|
+
subprocess.exited
|
|
1770
|
+
]);
|
|
1771
|
+
if (exitCode !== 0) throw new Error(`Resolve workspace objects: ${stderr.trim() || `git exited ${exitCode}`}`);
|
|
1772
|
+
const lines = stdout.split("\n").filter((line) => line.length > 0);
|
|
1773
|
+
if (lines.length !== unique.length) {
|
|
1774
|
+
throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
|
|
1775
|
+
}
|
|
1776
|
+
unique.forEach((name, index) => {
|
|
1777
|
+
const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
|
|
1778
|
+
resolved.set(name, match ? match[1] : null);
|
|
1779
|
+
});
|
|
1780
|
+
return resolved;
|
|
1781
|
+
}
|
|
1782
|
+
function workspaceTreeObjectName(revision, workspaceRelativePath) {
|
|
1783
|
+
return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
|
|
1784
|
+
}
|
|
1785
|
+
async function measureInitialPublicationDiffSize(input) {
|
|
1786
|
+
const candidates = input.mounts.filter(
|
|
1787
|
+
(mount) => typeof mount.initialPublicationSourceRelativePath === "string"
|
|
1788
|
+
);
|
|
1789
|
+
if (candidates.length === 0) return null;
|
|
1790
|
+
const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
|
|
1791
|
+
const objects = await resolveGitObjectIdsAsync(
|
|
1792
|
+
input.workspacePath,
|
|
1793
|
+
candidates.flatMap(
|
|
1794
|
+
(mount) => revisions.flatMap((revision) => [
|
|
1795
|
+
workspaceTreeObjectName(revision, mount.workspaceRelativePath),
|
|
1796
|
+
workspaceTreeObjectName(revision, mount.initialPublicationSourceRelativePath)
|
|
1797
|
+
])
|
|
1798
|
+
)
|
|
1799
|
+
);
|
|
1800
|
+
const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
|
|
1801
|
+
const initialPublications = candidates.flatMap((mount) => {
|
|
1802
|
+
const published = lookup(input.localHead, mount.workspaceRelativePath);
|
|
1803
|
+
if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
|
|
1804
|
+
const source = lookup(input.localHead, mount.initialPublicationSourceRelativePath) ?? lookup(input.remoteHead, mount.initialPublicationSourceRelativePath);
|
|
1805
|
+
return [{ mount, published, source }];
|
|
1806
|
+
});
|
|
1807
|
+
if (initialPublications.length === 0) return null;
|
|
1808
|
+
let size = await input.measure(
|
|
1809
|
+
input.workspacePath,
|
|
1810
|
+
input.remoteHead,
|
|
1811
|
+
input.localHead,
|
|
1812
|
+
input.limit,
|
|
1813
|
+
initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
|
|
1814
|
+
);
|
|
1815
|
+
for (const { published, source } of initialPublications) {
|
|
1816
|
+
if (size > input.limit) break;
|
|
1817
|
+
size += await input.measure(input.workspacePath, source, published, input.limit - size);
|
|
1818
|
+
}
|
|
1819
|
+
return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
|
|
1820
|
+
}
|
|
1716
1821
|
function conflictPaths(workspacePath) {
|
|
1717
1822
|
const result = gitResult(workspacePath, ["diff", "--name-only", "--diff-filter=U", "-z"]);
|
|
1718
1823
|
return result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
|
|
@@ -1940,12 +2045,19 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
1940
2045
|
const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
|
|
1941
2046
|
(mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
|
|
1942
2047
|
);
|
|
2048
|
+
const unchangedMountIds = currentHeadBeforeHydration ? await selectMountsWithUnchangedOuterSubtree(
|
|
2049
|
+
workspacePath,
|
|
2050
|
+
currentReceiptBeforeHydration,
|
|
2051
|
+
currentHeadBeforeHydration,
|
|
2052
|
+
uncoveredCompletionMounts
|
|
2053
|
+
) : /* @__PURE__ */ new Set();
|
|
1943
2054
|
const hydration = await hydrateWorkspaceGitMountsTransactionally({
|
|
1944
2055
|
workspacePath,
|
|
1945
|
-
mounts: uncoveredCompletionMounts,
|
|
1946
|
-
durabilityMounts: uncoveredCompletionBasisMounts,
|
|
2056
|
+
mounts: uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2057
|
+
durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
2058
|
+
carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
|
|
1947
2059
|
receiptMounts: completionReceiptMounts,
|
|
1948
|
-
requiredMounts: uncoveredRequiredLiveMounts,
|
|
2060
|
+
requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
|
|
1949
2061
|
recordCurrentHead: true
|
|
1950
2062
|
});
|
|
1951
2063
|
for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
|
|
@@ -2095,8 +2207,25 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2095
2207
|
};
|
|
2096
2208
|
}
|
|
2097
2209
|
const paths = await changedPaths(workspacePath, remoteHead, localHead);
|
|
2098
|
-
const
|
|
2210
|
+
const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
|
|
2211
|
+
let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
|
|
2099
2212
|
input.assertStillAdmitted?.();
|
|
2213
|
+
let initialPublicationMountIds;
|
|
2214
|
+
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
2215
|
+
const initialPublication = await measureInitialPublicationDiffSize({
|
|
2216
|
+
workspacePath,
|
|
2217
|
+
remoteHead,
|
|
2218
|
+
localHead,
|
|
2219
|
+
mounts: input.mounts,
|
|
2220
|
+
limit: maxDiffBytes,
|
|
2221
|
+
measure: measureDiffSize
|
|
2222
|
+
});
|
|
2223
|
+
input.assertStillAdmitted?.();
|
|
2224
|
+
if (initialPublication && initialPublication.size <= maxDiffBytes) {
|
|
2225
|
+
size = initialPublication.size;
|
|
2226
|
+
initialPublicationMountIds = initialPublication.mountIds;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2100
2229
|
if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
|
|
2101
2230
|
const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
|
|
2102
2231
|
return {
|
|
@@ -2141,7 +2270,9 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2141
2270
|
"push",
|
|
2142
2271
|
"--no-recurse-submodules"
|
|
2143
2272
|
];
|
|
2144
|
-
if (input.allowLargeDiff)
|
|
2273
|
+
if (input.allowLargeDiff || initialPublicationMountIds) {
|
|
2274
|
+
pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
|
|
2275
|
+
}
|
|
2145
2276
|
pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
|
|
2146
2277
|
const push = await gitResultAsync(workspacePath, pushArgs);
|
|
2147
2278
|
if (push.exitCode === 0) {
|
|
@@ -2163,6 +2294,7 @@ async function synchronizeWorkspaceGit(input) {
|
|
|
2163
2294
|
diffSizeBytes: size,
|
|
2164
2295
|
affectedPaths: paths,
|
|
2165
2296
|
...completedMounts,
|
|
2297
|
+
...initialPublicationMountIds ? { initialPublicationMountIds } : {},
|
|
2166
2298
|
...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
|
|
2167
2299
|
...projectionWarning ? { error: projectionWarning } : {}
|
|
2168
2300
|
};
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -58,6 +58,7 @@ import { assertDisjointManagedRoots, managedProjectRoot, validateManagedBranchNa
|
|
|
58
58
|
import {
|
|
59
59
|
applyObservedProjectMirrorHeads,
|
|
60
60
|
assertProjectMirrorHeadsPushed,
|
|
61
|
+
computeLocalProjectMirrorRefsToken,
|
|
61
62
|
createOrRetryLinkedProjectBranch,
|
|
62
63
|
deleteLinkedProjectBranch,
|
|
63
64
|
deleteProjectMirrorBranch,
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
pushProjectMirrorHeads,
|
|
71
72
|
removeProjectWorktrees
|
|
72
73
|
} from "./project-worktrees.mjs";
|
|
74
|
+
import { projectMirrorFetchDecision } from "./project-mirror-fetch-policy.mjs";
|
|
73
75
|
import {
|
|
74
76
|
PROJECT_SNAPSHOT_RECOVERY_HELPER_COMMAND,
|
|
75
77
|
recoverProjectSnapshotsInChild,
|
|
@@ -119,6 +121,7 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
|
119
121
|
const MAX_LINE_LENGTH = 2e3;
|
|
120
122
|
const WORKSPACE_GIT_QUIET_MS = 5e3;
|
|
121
123
|
const WORKSPACE_GIT_PERIODIC_MS = 6e4;
|
|
124
|
+
const PROJECT_MIRROR_OBSERVE_CONCURRENCY = 6;
|
|
122
125
|
const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
|
|
123
126
|
const PTY_INPUT_BUSY_GRACE_MS = 3e3;
|
|
124
127
|
const PTY_FOREGROUND_POLL_MS = 1e3;
|
|
@@ -1009,6 +1012,21 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
|
|
|
1009
1012
|
function canonicalProjectRemoteUrl(baseUrl, projectId) {
|
|
1010
1013
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
1011
1014
|
}
|
|
1015
|
+
async function mapWithConcurrencyLimit(items, limit, task) {
|
|
1016
|
+
const results = new Array(items.length);
|
|
1017
|
+
let nextIndex = 0;
|
|
1018
|
+
await Promise.all(
|
|
1019
|
+
Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
1020
|
+
while (true) {
|
|
1021
|
+
const index = nextIndex;
|
|
1022
|
+
nextIndex += 1;
|
|
1023
|
+
if (index >= items.length) return;
|
|
1024
|
+
results[index] = await task(items[index], index);
|
|
1025
|
+
}
|
|
1026
|
+
})
|
|
1027
|
+
);
|
|
1028
|
+
return results;
|
|
1029
|
+
}
|
|
1012
1030
|
function shellQuote(value) {
|
|
1013
1031
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
1014
1032
|
}
|
|
@@ -3468,6 +3486,10 @@ async function startWorker(options) {
|
|
|
3468
3486
|
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
3469
3487
|
const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
|
|
3470
3488
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
3489
|
+
const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3490
|
+
const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3491
|
+
const lastProjectMirrorFetchAtMs = /* @__PURE__ */ new Map();
|
|
3492
|
+
const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
|
|
3471
3493
|
const pendingMirrorDeletes = /* @__PURE__ */ new Map();
|
|
3472
3494
|
let workspaceRemoteUrl = null;
|
|
3473
3495
|
let workspaceCredentialHelper = null;
|
|
@@ -3485,6 +3507,7 @@ async function startWorker(options) {
|
|
|
3485
3507
|
let automaticSyncInFlight = false;
|
|
3486
3508
|
let terminalReplayTimer;
|
|
3487
3509
|
let workspaceSyncRequestsInFlight = 0;
|
|
3510
|
+
let lastWorkspaceSyncMirrorObservationMs = 0;
|
|
3488
3511
|
let sessionArtifactSyncRequestsInFlight = 0;
|
|
3489
3512
|
let cliUpdateInProgress = false;
|
|
3490
3513
|
let reloadAfterClose = false;
|
|
@@ -3808,6 +3831,7 @@ async function startWorker(options) {
|
|
|
3808
3831
|
const branchKey = projectBranchKey(project.projectId, branch.branchName);
|
|
3809
3832
|
const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
|
|
3810
3833
|
const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
|
|
3834
|
+
const initialPublicationSourceBranch = creatorLocalIncarnation ? branch.sourceBranchName : null;
|
|
3811
3835
|
mounts.push({
|
|
3812
3836
|
id: projectMountId(project.projectId, branch.branchName),
|
|
3813
3837
|
hydrationIncarnationKey: branch.branchId,
|
|
@@ -3817,6 +3841,7 @@ async function startWorker(options) {
|
|
|
3817
3841
|
sourceMode: "git",
|
|
3818
3842
|
hydrateDeletionMode: "git",
|
|
3819
3843
|
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3844
|
+
...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspaceProjectRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
|
|
3820
3845
|
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3821
3846
|
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3822
3847
|
mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
|
|
@@ -3831,6 +3856,7 @@ async function startWorker(options) {
|
|
|
3831
3856
|
sourceMode: "all",
|
|
3832
3857
|
hydrateDeletionMode: "all",
|
|
3833
3858
|
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3859
|
+
...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspacePlanRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
|
|
3834
3860
|
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3835
3861
|
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3836
3862
|
mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
|
|
@@ -4110,6 +4136,17 @@ async function startWorker(options) {
|
|
|
4110
4136
|
for (const result of results) {
|
|
4111
4137
|
lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
|
|
4112
4138
|
}
|
|
4139
|
+
try {
|
|
4140
|
+
fetchedProjectMirrorRefsTokens.set(
|
|
4141
|
+
project.projectId,
|
|
4142
|
+
await computeLocalProjectMirrorRefsToken({
|
|
4143
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
4144
|
+
primaryBranchName: primaryProjectBranch(project)
|
|
4145
|
+
})
|
|
4146
|
+
);
|
|
4147
|
+
} catch {
|
|
4148
|
+
fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4149
|
+
}
|
|
4113
4150
|
}
|
|
4114
4151
|
const publishedTombstoneIds = [
|
|
4115
4152
|
...new Set(
|
|
@@ -4135,6 +4172,7 @@ async function startWorker(options) {
|
|
|
4135
4172
|
credentialHelper: deletion.credentialHelper,
|
|
4136
4173
|
credentialUsername: deletion.credentialUsername
|
|
4137
4174
|
});
|
|
4175
|
+
fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4138
4176
|
}
|
|
4139
4177
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
4140
4178
|
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
@@ -4145,42 +4183,101 @@ async function startWorker(options) {
|
|
|
4145
4183
|
pendingMirrorDeletes.delete(key);
|
|
4146
4184
|
if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
|
|
4147
4185
|
removeProjectCredentialStore(deletion.projectId);
|
|
4186
|
+
advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4187
|
+
fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4188
|
+
lastProjectMirrorFetchAtMs.delete(deletion.projectId);
|
|
4148
4189
|
}
|
|
4149
4190
|
lastObservedProjectHeads.delete(key);
|
|
4150
4191
|
}
|
|
4151
4192
|
};
|
|
4193
|
+
const observeSingleProjectMirrorBundle = async (project, options2) => {
|
|
4194
|
+
const connection = projectConnection(project);
|
|
4195
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
4196
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
4197
|
+
if (options2.refreshRemote) {
|
|
4198
|
+
const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
|
|
4199
|
+
const observations = await observeProjectMirrorHeads({
|
|
4200
|
+
projectRoot,
|
|
4201
|
+
primaryBranchName,
|
|
4202
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4203
|
+
mirrorUrl: connection.mirrorUrl,
|
|
4204
|
+
credentialHelper: connection.credentialHelper,
|
|
4205
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
4206
|
+
allowNonFastForward: options2.allowNonFastForward,
|
|
4207
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4208
|
+
});
|
|
4209
|
+
if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4210
|
+
else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
|
|
4211
|
+
lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
|
|
4212
|
+
return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
|
|
4213
|
+
}
|
|
4214
|
+
return {
|
|
4215
|
+
projectId: project.projectId,
|
|
4216
|
+
projectRoot,
|
|
4217
|
+
primaryBranchName,
|
|
4218
|
+
observations: await observeProjectMirrorHeads({
|
|
4219
|
+
projectRoot,
|
|
4220
|
+
primaryBranchName,
|
|
4221
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4222
|
+
mirrorUrl: connection.mirrorUrl,
|
|
4223
|
+
credentialHelper: connection.credentialHelper,
|
|
4224
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
4225
|
+
allowNonFastForward: options2.allowNonFastForward,
|
|
4226
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
|
|
4227
|
+
refreshRemote: false
|
|
4228
|
+
})
|
|
4229
|
+
};
|
|
4230
|
+
};
|
|
4152
4231
|
const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
|
|
4153
|
-
const
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
const
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4232
|
+
const observationStartedAt = Date.now();
|
|
4233
|
+
const eligibleProjects = [...projectConfigById.values()].filter(
|
|
4234
|
+
(project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
|
|
4235
|
+
);
|
|
4236
|
+
try {
|
|
4237
|
+
if (input.failClosed) {
|
|
4238
|
+
const bundles2 = [];
|
|
4239
|
+
for (const project of eligibleProjects) {
|
|
4240
|
+
bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
|
|
4241
|
+
}
|
|
4242
|
+
return bundles2;
|
|
4243
|
+
}
|
|
4244
|
+
let fetchedCount = 0;
|
|
4245
|
+
const bundles = await mapWithConcurrencyLimit(
|
|
4246
|
+
eligibleProjects,
|
|
4247
|
+
PROJECT_MIRROR_OBSERVE_CONCURRENCY,
|
|
4248
|
+
async (project) => {
|
|
4249
|
+
try {
|
|
4250
|
+
const decision = projectMirrorFetchDecision({
|
|
4251
|
+
advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
|
|
4252
|
+
fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
|
|
4253
|
+
lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
|
|
4254
|
+
nowMs: Date.now(),
|
|
4255
|
+
skippingEnabled: projectMirrorFetchSkippingEnabled
|
|
4256
|
+
});
|
|
4257
|
+
if (decision.fetch) fetchedCount += 1;
|
|
4258
|
+
return await observeSingleProjectMirrorBundle(project, {
|
|
4259
|
+
allowNonFastForward: input.allowNonFastForward,
|
|
4260
|
+
refreshRemote: decision.fetch
|
|
4261
|
+
});
|
|
4262
|
+
} catch (error) {
|
|
4263
|
+
process.stderr.write(
|
|
4264
|
+
`[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
4265
|
+
`
|
|
4266
|
+
);
|
|
4267
|
+
return null;
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
);
|
|
4271
|
+
if (fetchedCount > 0) {
|
|
4177
4272
|
process.stderr.write(
|
|
4178
|
-
`[r5d-worker]
|
|
4273
|
+
`[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
|
|
4179
4274
|
`
|
|
4180
4275
|
);
|
|
4181
4276
|
}
|
|
4277
|
+
return bundles.filter((bundle) => bundle !== null);
|
|
4278
|
+
} finally {
|
|
4279
|
+
lastWorkspaceSyncMirrorObservationMs = Date.now() - observationStartedAt;
|
|
4182
4280
|
}
|
|
4183
|
-
return bundles;
|
|
4184
4281
|
};
|
|
4185
4282
|
const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
|
|
4186
4283
|
const active = new Set(activeMountIds);
|
|
@@ -4359,6 +4456,12 @@ async function startWorker(options) {
|
|
|
4359
4456
|
}
|
|
4360
4457
|
});
|
|
4361
4458
|
input.assertStillAdmitted?.();
|
|
4459
|
+
if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
|
|
4460
|
+
process.stderr.write(
|
|
4461
|
+
`[r5d-worker] published first checkout copies above the automatic diff-size limit, measured against their source subtrees at ${result.diffSizeBytes} bytes: ${result.initialPublicationMountIds.join(", ")}
|
|
4462
|
+
`
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4362
4465
|
if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
|
|
4363
4466
|
await applyObservedProjectHeadsAfterInboundWorkspace(
|
|
4364
4467
|
observedProjectHeads,
|
|
@@ -4449,6 +4552,7 @@ async function startWorker(options) {
|
|
|
4449
4552
|
result = await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
4450
4553
|
queueEnteredAt = Date.now();
|
|
4451
4554
|
syncStartedAt = Date.now();
|
|
4555
|
+
lastWorkspaceSyncMirrorObservationMs = 0;
|
|
4452
4556
|
if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
|
|
4453
4557
|
return await failedWorkspaceSyncResult(
|
|
4454
4558
|
attemptId,
|
|
@@ -4533,7 +4637,9 @@ async function startWorker(options) {
|
|
|
4533
4637
|
telemetry: {
|
|
4534
4638
|
totalMs: finishedAt - requestedAt,
|
|
4535
4639
|
queueMs: queueEnteredAt - requestedAt,
|
|
4536
|
-
|
|
4640
|
+
// Duration of the pre-outer mirror-head observation phase; near zero
|
|
4641
|
+
// on a cycle whose token-gated observation skipped every fetch.
|
|
4642
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
4537
4643
|
synchronizeMs: finishedAt - syncStartedAt
|
|
4538
4644
|
}
|
|
4539
4645
|
};
|
|
@@ -5251,7 +5357,8 @@ async function startWorker(options) {
|
|
|
5251
5357
|
workspaceIncidentConfigDeferralV1: true,
|
|
5252
5358
|
workspaceConfigResetToCanonicalV1: true,
|
|
5253
5359
|
projectBranchDeletionFastAckV1: true,
|
|
5254
|
-
projectMirrorLeaseV1: true
|
|
5360
|
+
projectMirrorLeaseV1: true,
|
|
5361
|
+
projectMirrorRefsTokensV1: true
|
|
5255
5362
|
},
|
|
5256
5363
|
projectRoot: projectsRoot,
|
|
5257
5364
|
artifactRoot,
|
|
@@ -5289,6 +5396,17 @@ async function startWorker(options) {
|
|
|
5289
5396
|
if (message.type === "connected") {
|
|
5290
5397
|
return;
|
|
5291
5398
|
}
|
|
5399
|
+
if (message.type === "project_mirror_refs_tokens") {
|
|
5400
|
+
advertisedProjectMirrorRefsTokens.clear();
|
|
5401
|
+
if (message.tokens && typeof message.tokens === "object") {
|
|
5402
|
+
for (const [projectId, token2] of Object.entries(message.tokens)) {
|
|
5403
|
+
if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
|
|
5404
|
+
advertisedProjectMirrorRefsTokens.set(projectId, token2);
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
return;
|
|
5409
|
+
}
|
|
5292
5410
|
if (message.type === "workspace_config") {
|
|
5293
5411
|
const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
|
|
5294
5412
|
const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
|
|
2
|
+
function projectMirrorFetchDecision(input) {
|
|
3
|
+
if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
|
|
4
|
+
if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
|
|
5
|
+
if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
|
|
6
|
+
if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
|
|
7
|
+
const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
|
|
8
|
+
if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
|
|
9
|
+
return { fetch: true, reason: "backstop_elapsed" };
|
|
10
|
+
}
|
|
11
|
+
return { fetch: false, reason: "token_match" };
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
PROJECT_MIRROR_FETCH_BACKSTOP_MS,
|
|
15
|
+
projectMirrorFetchDecision
|
|
16
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
3
|
+
const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
4
|
+
function hashProjectMirrorRefs(refs) {
|
|
5
|
+
const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
6
|
+
return createHash("sha256").update(lines.join("\n")).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
9
|
+
const canonical = [];
|
|
10
|
+
for (const { refname, objectName } of refs) {
|
|
11
|
+
if (refname.startsWith(PROJECT_MIRROR_REF_PREFIX)) {
|
|
12
|
+
canonical.push({ refname: `refs/heads/${refname.slice(PROJECT_MIRROR_REF_PREFIX.length)}`, objectName });
|
|
13
|
+
} else if (refname.startsWith(PROJECT_SERVER_SYNCED_REF_PREFIX)) {
|
|
14
|
+
canonical.push({ refname, objectName });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return canonical;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
PROJECT_MIRROR_REF_PREFIX,
|
|
21
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
22
|
+
canonicalizeLocalProjectMirrorRefs,
|
|
23
|
+
hashProjectMirrorRefs
|
|
24
|
+
};
|