@ricsam/r5d-worker 0.0.100 → 0.0.103
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/README.md +16 -3
- package/dist/cjs/main.cjs +390 -322
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-mirror-fetch-policy.cjs +48 -7
- package/dist/cjs/project-mirror-refs-token.cjs +36 -2
- package/dist/cjs/project-worktrees.cjs +49 -32
- package/dist/cjs/working-tree-mirror.cjs +347 -99
- package/dist/cjs/workspace-git-sync.cjs +189 -28
- package/dist/cjs/workspace-merge-projection.cjs +5 -19
- package/dist/cjs/workspace-mutation-gate.cjs +35 -0
- package/dist/cjs/workspace-projection-ledger.cjs +81 -0
- package/dist/cjs/workspace-sync-coalescer.cjs +58 -0
- package/dist/mjs/main.mjs +395 -324
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-mirror-fetch-policy.mjs +46 -6
- package/dist/mjs/project-mirror-refs-token.mjs +30 -1
- package/dist/mjs/project-worktrees.mjs +52 -33
- package/dist/mjs/working-tree-mirror.mjs +347 -99
- package/dist/mjs/workspace-git-sync.mjs +189 -28
- package/dist/mjs/workspace-merge-projection.mjs +5 -19
- package/dist/mjs/workspace-mutation-gate.mjs +35 -0
- package/dist/mjs/workspace-projection-ledger.mjs +57 -0
- package/dist/mjs/workspace-sync-coalescer.mjs +34 -0
- package/dist/types/main.d.ts +29 -17
- package/dist/types/project-mirror-fetch-policy.d.ts +70 -15
- package/dist/types/project-mirror-refs-token.d.ts +34 -0
- package/dist/types/project-worktrees.d.ts +26 -8
- package/dist/types/working-tree-mirror.d.ts +56 -11
- package/dist/types/workspace-git-sync.d.ts +41 -4
- package/dist/types/workspace-mutation-gate.d.ts +24 -3
- package/dist/types/workspace-projection-ledger.d.ts +59 -0
- package/dist/types/workspace-sync-coalescer.d.ts +28 -0
- package/package.json +1 -1
package/dist/mjs/package.json
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
|
-
const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
|
|
2
1
|
function projectMirrorFetchDecision(input) {
|
|
3
2
|
if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
|
|
4
3
|
if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
|
|
5
4
|
if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
|
|
6
5
|
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
6
|
return { fetch: false, reason: "token_match" };
|
|
12
7
|
}
|
|
8
|
+
function planProjectMirrorObservationFetch(input) {
|
|
9
|
+
if (!input.skippingEnabled) return { scope: "project", reason: "skipping_disabled" };
|
|
10
|
+
if (input.advertisedMountTokens === void 0) {
|
|
11
|
+
const decision = projectMirrorFetchDecision({
|
|
12
|
+
advertisedToken: input.advertisedProjectToken,
|
|
13
|
+
fetchedToken: input.fetchedProjectToken,
|
|
14
|
+
skippingEnabled: true
|
|
15
|
+
});
|
|
16
|
+
return decision.fetch ? { scope: "project", reason: decision.reason } : { scope: "none" };
|
|
17
|
+
}
|
|
18
|
+
const mounts = [];
|
|
19
|
+
for (const branchName of [...input.branchNames].sort()) {
|
|
20
|
+
const decision = projectMirrorFetchDecision({
|
|
21
|
+
advertisedToken: input.advertisedMountTokens.get(branchName),
|
|
22
|
+
fetchedToken: input.fetchedMountTokens.get(branchName),
|
|
23
|
+
skippingEnabled: true
|
|
24
|
+
});
|
|
25
|
+
if (decision.fetch) mounts.push({ branchName, reason: decision.reason });
|
|
26
|
+
}
|
|
27
|
+
return mounts.length === 0 ? { scope: "none" } : { scope: "mounts", mounts };
|
|
28
|
+
}
|
|
29
|
+
const TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
30
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
31
|
+
function parseProjectMirrorRefsTokensMessage(message) {
|
|
32
|
+
const projectTokens = /* @__PURE__ */ new Map();
|
|
33
|
+
if (message.tokens && typeof message.tokens === "object") {
|
|
34
|
+
for (const [projectId, token] of Object.entries(message.tokens)) {
|
|
35
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) projectTokens.set(projectId, token);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
39
|
+
if (message.mountTokens && typeof message.mountTokens === "object") {
|
|
40
|
+
for (const [projectId, tokensByBranch] of Object.entries(message.mountTokens)) {
|
|
41
|
+
if (!tokensByBranch || typeof tokensByBranch !== "object") continue;
|
|
42
|
+
const perMount = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const [branchName, token] of Object.entries(tokensByBranch)) {
|
|
44
|
+
if (typeof token === "string" && TOKEN_RE.test(token)) perMount.set(branchName, token);
|
|
45
|
+
}
|
|
46
|
+
mountTokens.set(projectId, perMount);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const workspaceHead = typeof message.workspaceHead === "string" && GIT_OBJECT_NAME_RE.test(message.workspaceHead) ? message.workspaceHead : void 0;
|
|
50
|
+
return { projectTokens, mountTokens, workspaceHead };
|
|
51
|
+
}
|
|
13
52
|
export {
|
|
14
|
-
|
|
53
|
+
parseProjectMirrorRefsTokensMessage,
|
|
54
|
+
planProjectMirrorObservationFetch,
|
|
15
55
|
projectMirrorFetchDecision
|
|
16
56
|
};
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
const PROJECT_MIRROR_REFS_TOKEN_NAMESPACES = ["refs/heads/", "refs/r5d/server-synced/"];
|
|
2
3
|
const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
3
4
|
const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
5
|
+
const PROJECT_MIRROR_REF_LISTING_FORMAT = "--format=%(objectname) %(refname)";
|
|
6
|
+
const GIT_OBJECT_NAME_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
4
7
|
function hashProjectMirrorRefs(refs) {
|
|
5
8
|
const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
6
9
|
return createHash("sha256").update(lines.join("\n")).digest("hex");
|
|
7
10
|
}
|
|
11
|
+
function parseProjectMirrorRefListing(output, options = {}) {
|
|
12
|
+
return output.split(/\r?\n/).filter(Boolean).map((row) => {
|
|
13
|
+
const separator = row.indexOf(" ");
|
|
14
|
+
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
15
|
+
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
16
|
+
if (!GIT_OBJECT_NAME_RE.test(objectName) || refname.length === 0 || options.namespaces !== void 0 && !options.namespaces.some((prefix) => refname.startsWith(prefix))) {
|
|
17
|
+
throw new Error(`Invalid ref listing row: ${row}`);
|
|
18
|
+
}
|
|
19
|
+
return { refname, objectName };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
8
22
|
function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
9
23
|
const canonical = [];
|
|
10
24
|
for (const { refname, objectName } of refs) {
|
|
@@ -16,9 +30,24 @@ function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
|
16
30
|
}
|
|
17
31
|
return canonical;
|
|
18
32
|
}
|
|
33
|
+
function projectMirrorMountRefs(refs, mount) {
|
|
34
|
+
const headRef = `refs/heads/${mount.branchName}`;
|
|
35
|
+
const handoffPrefix = `${PROJECT_SERVER_SYNCED_REF_PREFIX}${mount.branchId}/`;
|
|
36
|
+
return refs.filter(({ refname }) => refname === headRef || refname.startsWith(handoffPrefix));
|
|
37
|
+
}
|
|
38
|
+
function projectMirrorRefsTokens(refs, mounts) {
|
|
39
|
+
const mountTokens = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const mount of mounts) mountTokens.set(mount.branchName, hashProjectMirrorRefs(projectMirrorMountRefs(refs, mount)));
|
|
41
|
+
return { project: hashProjectMirrorRefs(refs), mounts: mountTokens };
|
|
42
|
+
}
|
|
19
43
|
export {
|
|
44
|
+
PROJECT_MIRROR_REFS_TOKEN_NAMESPACES,
|
|
45
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
20
46
|
PROJECT_MIRROR_REF_PREFIX,
|
|
21
47
|
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
22
48
|
canonicalizeLocalProjectMirrorRefs,
|
|
23
|
-
hashProjectMirrorRefs
|
|
49
|
+
hashProjectMirrorRefs,
|
|
50
|
+
parseProjectMirrorRefListing,
|
|
51
|
+
projectMirrorMountRefs,
|
|
52
|
+
projectMirrorRefsTokens
|
|
24
53
|
};
|
|
@@ -6,9 +6,11 @@ import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProc
|
|
|
6
6
|
import { validateManagedBranchName } from "./managed-paths.mjs";
|
|
7
7
|
import {
|
|
8
8
|
canonicalizeLocalProjectMirrorRefs,
|
|
9
|
-
|
|
9
|
+
parseProjectMirrorRefListing,
|
|
10
|
+
PROJECT_MIRROR_REF_LISTING_FORMAT,
|
|
10
11
|
PROJECT_MIRROR_REF_PREFIX,
|
|
11
|
-
PROJECT_SERVER_SYNCED_REF_PREFIX
|
|
12
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
13
|
+
projectMirrorRefsTokens
|
|
12
14
|
} from "./project-mirror-refs-token.mjs";
|
|
13
15
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
14
16
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
@@ -1125,8 +1127,12 @@ function createLinkedProjectBranch(input) {
|
|
|
1125
1127
|
);
|
|
1126
1128
|
configureProjectBranchTracking(targetPath, input.branchName, input.branchName);
|
|
1127
1129
|
assertNoSymlinkBranchPathComponents(input.projectRoot, targetPath);
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
+
if (input.workingTree === "carry") {
|
|
1131
|
+
git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
|
|
1132
|
+
mirrorWorkingTree({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
|
|
1133
|
+
} else {
|
|
1134
|
+
git(targetPath, ["reset", "--hard", sourceHead], `check out linked worktree ${input.branchName}`);
|
|
1135
|
+
}
|
|
1130
1136
|
return { branchPath: targetPath, baseCommitHash: sourceHead };
|
|
1131
1137
|
} catch (error) {
|
|
1132
1138
|
if (branchCreated) {
|
|
@@ -1316,24 +1322,46 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1316
1322
|
throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
|
|
1317
1323
|
}
|
|
1318
1324
|
}
|
|
1325
|
+
const PROJECT_MIRROR_FETCH_REFSPECS = ["+refs/heads/*:refs/r5d/mirror/*", "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"];
|
|
1326
|
+
function projectMirrorMountFetchRefspecs(branchName, branchId) {
|
|
1327
|
+
validateManagedBranchName(branchName);
|
|
1328
|
+
if (branchId !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(branchId)) throw new Error(`Invalid branch id for ${branchName}`);
|
|
1329
|
+
return [
|
|
1330
|
+
`+refs/heads/${branchName}:refs/r5d/mirror/${branchName}`,
|
|
1331
|
+
branchId === void 0 ? "+refs/r5d/server-synced/*:refs/r5d/server-synced/*" : `+refs/r5d/server-synced/${branchId}/*:refs/r5d/server-synced/${branchId}/*`
|
|
1332
|
+
];
|
|
1333
|
+
}
|
|
1334
|
+
async function fetchProjectMirrorRefs(input) {
|
|
1335
|
+
const fetch = (refspecs) => gitResultAsync(
|
|
1336
|
+
input.primaryPath,
|
|
1337
|
+
[
|
|
1338
|
+
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1339
|
+
"fetch",
|
|
1340
|
+
"--no-recurse-submodules",
|
|
1341
|
+
"--prune",
|
|
1342
|
+
input.mirrorUrl,
|
|
1343
|
+
...refspecs
|
|
1344
|
+
],
|
|
1345
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1346
|
+
);
|
|
1347
|
+
if (input.fetchOnlyBranches && input.fetchOnlyBranches.size > 0) {
|
|
1348
|
+
const refspecs = [...input.fetchOnlyBranches].sort().flatMap((branchName) => projectMirrorMountFetchRefspecs(branchName, input.branchIds?.get(branchName)));
|
|
1349
|
+
const scoped = await fetch(refspecs);
|
|
1350
|
+
if (scoped.exitCode === 0) return;
|
|
1351
|
+
if (!/(couldn't find remote ref|no such ref)/i.test(`${scoped.stderr}
|
|
1352
|
+
${scoped.stdout}`)) {
|
|
1353
|
+
throw new Error(`Refresh project head mirror: ${scoped.stderr || scoped.stdout || `git exited ${scoped.exitCode}`}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const whole = await fetch(PROJECT_MIRROR_FETCH_REFSPECS);
|
|
1357
|
+
if (whole.exitCode !== 0) {
|
|
1358
|
+
throw new Error(`Refresh project head mirror: ${whole.stderr || whole.stdout || `git exited ${whole.exitCode}`}`);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1319
1361
|
async function observeProjectMirrorHeads(input) {
|
|
1320
1362
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1321
|
-
if (input.refreshRemote !== false) {
|
|
1322
|
-
|
|
1323
|
-
primaryPath,
|
|
1324
|
-
[
|
|
1325
|
-
...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1326
|
-
"fetch",
|
|
1327
|
-
"--no-recurse-submodules",
|
|
1328
|
-
"--prune",
|
|
1329
|
-
input.mirrorUrl,
|
|
1330
|
-
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1331
|
-
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1332
|
-
],
|
|
1333
|
-
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1334
|
-
);
|
|
1335
|
-
if (fetch.exitCode !== 0)
|
|
1336
|
-
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1363
|
+
if (input.refreshRemote !== false && !(input.fetchOnlyBranches && input.fetchOnlyBranches.size === 0)) {
|
|
1364
|
+
await fetchProjectMirrorRefs({ ...input, primaryPath });
|
|
1337
1365
|
}
|
|
1338
1366
|
const observations = [];
|
|
1339
1367
|
for (const branchName of [...input.branchNames].sort()) {
|
|
@@ -1380,23 +1408,14 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1380
1408
|
}
|
|
1381
1409
|
return observations;
|
|
1382
1410
|
}
|
|
1383
|
-
async function
|
|
1411
|
+
async function computeLocalProjectMirrorRefsTokens(input) {
|
|
1384
1412
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1385
1413
|
const rows = await gitAsync(
|
|
1386
1414
|
primaryPath,
|
|
1387
|
-
["for-each-ref",
|
|
1415
|
+
["for-each-ref", PROJECT_MIRROR_REF_LISTING_FORMAT, PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1388
1416
|
"list cached project mirror refs"
|
|
1389
1417
|
);
|
|
1390
|
-
|
|
1391
|
-
const separator = row.indexOf(" ");
|
|
1392
|
-
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
1393
|
-
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
1394
|
-
if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
|
|
1395
|
-
throw new Error(`Invalid cached mirror ref listing row: ${row}`);
|
|
1396
|
-
}
|
|
1397
|
-
return { refname, objectName };
|
|
1398
|
-
});
|
|
1399
|
-
return hashProjectMirrorRefs(canonicalizeLocalProjectMirrorRefs(refs));
|
|
1418
|
+
return projectMirrorRefsTokens(canonicalizeLocalProjectMirrorRefs(parseProjectMirrorRefListing(rows)), input.mounts);
|
|
1400
1419
|
}
|
|
1401
1420
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1402
1421
|
const results = [];
|
|
@@ -1479,7 +1498,7 @@ export {
|
|
|
1479
1498
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1480
1499
|
applyObservedProjectMirrorHeads,
|
|
1481
1500
|
assertProjectMirrorHeadsPushed,
|
|
1482
|
-
|
|
1501
|
+
computeLocalProjectMirrorRefsTokens,
|
|
1483
1502
|
createLinkedProjectBranch,
|
|
1484
1503
|
createOrRetryLinkedProjectBranch,
|
|
1485
1504
|
deleteLinkedProjectBranch,
|