@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.
Files changed (33) hide show
  1. package/README.md +16 -3
  2. package/dist/cjs/main.cjs +390 -322
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/project-mirror-fetch-policy.cjs +48 -7
  5. package/dist/cjs/project-mirror-refs-token.cjs +36 -2
  6. package/dist/cjs/project-worktrees.cjs +49 -32
  7. package/dist/cjs/working-tree-mirror.cjs +347 -99
  8. package/dist/cjs/workspace-git-sync.cjs +189 -28
  9. package/dist/cjs/workspace-merge-projection.cjs +5 -19
  10. package/dist/cjs/workspace-mutation-gate.cjs +35 -0
  11. package/dist/cjs/workspace-projection-ledger.cjs +81 -0
  12. package/dist/cjs/workspace-sync-coalescer.cjs +58 -0
  13. package/dist/mjs/main.mjs +395 -324
  14. package/dist/mjs/package.json +1 -1
  15. package/dist/mjs/project-mirror-fetch-policy.mjs +46 -6
  16. package/dist/mjs/project-mirror-refs-token.mjs +30 -1
  17. package/dist/mjs/project-worktrees.mjs +52 -33
  18. package/dist/mjs/working-tree-mirror.mjs +347 -99
  19. package/dist/mjs/workspace-git-sync.mjs +189 -28
  20. package/dist/mjs/workspace-merge-projection.mjs +5 -19
  21. package/dist/mjs/workspace-mutation-gate.mjs +35 -0
  22. package/dist/mjs/workspace-projection-ledger.mjs +57 -0
  23. package/dist/mjs/workspace-sync-coalescer.mjs +34 -0
  24. package/dist/types/main.d.ts +29 -17
  25. package/dist/types/project-mirror-fetch-policy.d.ts +70 -15
  26. package/dist/types/project-mirror-refs-token.d.ts +34 -0
  27. package/dist/types/project-worktrees.d.ts +26 -8
  28. package/dist/types/working-tree-mirror.d.ts +56 -11
  29. package/dist/types/workspace-git-sync.d.ts +41 -4
  30. package/dist/types/workspace-mutation-gate.d.ts +24 -3
  31. package/dist/types/workspace-projection-ledger.d.ts +59 -0
  32. package/dist/types/workspace-sync-coalescer.d.ts +28 -0
  33. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.100",
3
+ "version": "0.0.103",
4
4
  "type": "module"
5
5
  }
@@ -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
- PROJECT_MIRROR_FETCH_BACKSTOP_MS,
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
- hashProjectMirrorRefs,
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
- git(targetPath, ["read-tree", sourceHead], `initialize linked worktree index ${input.branchName}`);
1129
- mirrorWorkingTree({ sourceRoot: sourcePath, targetRoot: targetPath, sourceMode: "git", deletionMode: "git" });
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
- const fetch = await gitResultAsync(
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 computeLocalProjectMirrorRefsToken(input) {
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", "--format=%(objectname) %(refname)", PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
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
- const refs = rows.split(/\r?\n/).filter(Boolean).map((row) => {
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
- computeLocalProjectMirrorRefsToken,
1501
+ computeLocalProjectMirrorRefsTokens,
1483
1502
  createLinkedProjectBranch,
1484
1503
  createOrRetryLinkedProjectBranch,
1485
1504
  deleteLinkedProjectBranch,