@ricsam/r5d-worker 0.0.94 → 0.0.95
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 +136 -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/mjs/main.mjs +137 -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/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/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -81,6 +81,7 @@ var import_repository_transition_policy = require("./repository-transition-polic
|
|
|
81
81
|
var import_supervisor = require("./supervisor.cjs");
|
|
82
82
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
83
83
|
var import_project_worktrees = require("./project-worktrees.cjs");
|
|
84
|
+
var import_project_mirror_fetch_policy = require("./project-mirror-fetch-policy.cjs");
|
|
84
85
|
var import_project_snapshot_recovery_runner = require("./project-snapshot-recovery-runner.cjs");
|
|
85
86
|
var import_project_workspace_state = require("./project-workspace-state.cjs");
|
|
86
87
|
var import_workspace_git_sync = require("./workspace-git-sync.cjs");
|
|
@@ -113,6 +114,7 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
|
113
114
|
const MAX_LINE_LENGTH = 2e3;
|
|
114
115
|
const WORKSPACE_GIT_QUIET_MS = 5e3;
|
|
115
116
|
const WORKSPACE_GIT_PERIODIC_MS = 6e4;
|
|
117
|
+
const PROJECT_MIRROR_OBSERVE_CONCURRENCY = 6;
|
|
116
118
|
const WORKSPACE_INCIDENT_CONFIG_REFRESH_TIMEOUT_MS = 6e4;
|
|
117
119
|
const PTY_INPUT_BUSY_GRACE_MS = 3e3;
|
|
118
120
|
const PTY_FOREGROUND_POLL_MS = 1e3;
|
|
@@ -1003,6 +1005,21 @@ function resolveWorkerFilePath(branchPath, inputPath, builtInPaths) {
|
|
|
1003
1005
|
function canonicalProjectRemoteUrl(baseUrl, projectId) {
|
|
1004
1006
|
return new URL(`/git/${projectId}.git`, baseUrl).toString();
|
|
1005
1007
|
}
|
|
1008
|
+
async function mapWithConcurrencyLimit(items, limit, task) {
|
|
1009
|
+
const results = new Array(items.length);
|
|
1010
|
+
let nextIndex = 0;
|
|
1011
|
+
await Promise.all(
|
|
1012
|
+
Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
1013
|
+
while (true) {
|
|
1014
|
+
const index = nextIndex;
|
|
1015
|
+
nextIndex += 1;
|
|
1016
|
+
if (index >= items.length) return;
|
|
1017
|
+
results[index] = await task(items[index], index);
|
|
1018
|
+
}
|
|
1019
|
+
})
|
|
1020
|
+
);
|
|
1021
|
+
return results;
|
|
1022
|
+
}
|
|
1006
1023
|
function shellQuote(value) {
|
|
1007
1024
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
1008
1025
|
}
|
|
@@ -3462,6 +3479,10 @@ async function startWorker(options) {
|
|
|
3462
3479
|
const readyProjectIds = /* @__PURE__ */ new Set();
|
|
3463
3480
|
const reconciledProjectConfigFingerprints = /* @__PURE__ */ new Map();
|
|
3464
3481
|
const lastObservedProjectHeads = /* @__PURE__ */ new Map();
|
|
3482
|
+
const advertisedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3483
|
+
const fetchedProjectMirrorRefsTokens = /* @__PURE__ */ new Map();
|
|
3484
|
+
const lastProjectMirrorFetchAtMs = /* @__PURE__ */ new Map();
|
|
3485
|
+
const projectMirrorFetchSkippingEnabled = process.env.R5D_PROJECT_MIRROR_FETCH_SKIP !== "0";
|
|
3465
3486
|
const pendingMirrorDeletes = /* @__PURE__ */ new Map();
|
|
3466
3487
|
let workspaceRemoteUrl = null;
|
|
3467
3488
|
let workspaceCredentialHelper = null;
|
|
@@ -3479,6 +3500,7 @@ async function startWorker(options) {
|
|
|
3479
3500
|
let automaticSyncInFlight = false;
|
|
3480
3501
|
let terminalReplayTimer;
|
|
3481
3502
|
let workspaceSyncRequestsInFlight = 0;
|
|
3503
|
+
let lastWorkspaceSyncMirrorObservationMs = 0;
|
|
3482
3504
|
let sessionArtifactSyncRequestsInFlight = 0;
|
|
3483
3505
|
let cliUpdateInProgress = false;
|
|
3484
3506
|
let reloadAfterClose = false;
|
|
@@ -4104,6 +4126,17 @@ async function startWorker(options) {
|
|
|
4104
4126
|
for (const result of results) {
|
|
4105
4127
|
lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
|
|
4106
4128
|
}
|
|
4129
|
+
try {
|
|
4130
|
+
fetchedProjectMirrorRefsTokens.set(
|
|
4131
|
+
project.projectId,
|
|
4132
|
+
await (0, import_project_worktrees.computeLocalProjectMirrorRefsToken)({
|
|
4133
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
4134
|
+
primaryBranchName: primaryProjectBranch(project)
|
|
4135
|
+
})
|
|
4136
|
+
);
|
|
4137
|
+
} catch {
|
|
4138
|
+
fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4139
|
+
}
|
|
4107
4140
|
}
|
|
4108
4141
|
const publishedTombstoneIds = [
|
|
4109
4142
|
...new Set(
|
|
@@ -4129,6 +4162,7 @@ async function startWorker(options) {
|
|
|
4129
4162
|
credentialHelper: deletion.credentialHelper,
|
|
4130
4163
|
credentialUsername: deletion.credentialUsername
|
|
4131
4164
|
});
|
|
4165
|
+
fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4132
4166
|
}
|
|
4133
4167
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
4134
4168
|
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
@@ -4139,42 +4173,101 @@ async function startWorker(options) {
|
|
|
4139
4173
|
pendingMirrorDeletes.delete(key);
|
|
4140
4174
|
if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
|
|
4141
4175
|
removeProjectCredentialStore(deletion.projectId);
|
|
4176
|
+
advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4177
|
+
fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4178
|
+
lastProjectMirrorFetchAtMs.delete(deletion.projectId);
|
|
4142
4179
|
}
|
|
4143
4180
|
lastObservedProjectHeads.delete(key);
|
|
4144
4181
|
}
|
|
4145
4182
|
};
|
|
4183
|
+
const observeSingleProjectMirrorBundle = async (project, options2) => {
|
|
4184
|
+
const connection = projectConnection(project);
|
|
4185
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
4186
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
4187
|
+
if (options2.refreshRemote) {
|
|
4188
|
+
const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
|
|
4189
|
+
const observations = await (0, import_project_worktrees.observeProjectMirrorHeads)({
|
|
4190
|
+
projectRoot,
|
|
4191
|
+
primaryBranchName,
|
|
4192
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4193
|
+
mirrorUrl: connection.mirrorUrl,
|
|
4194
|
+
credentialHelper: connection.credentialHelper,
|
|
4195
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
4196
|
+
allowNonFastForward: options2.allowNonFastForward,
|
|
4197
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4198
|
+
});
|
|
4199
|
+
if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4200
|
+
else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
|
|
4201
|
+
lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
|
|
4202
|
+
return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
|
|
4203
|
+
}
|
|
4204
|
+
return {
|
|
4205
|
+
projectId: project.projectId,
|
|
4206
|
+
projectRoot,
|
|
4207
|
+
primaryBranchName,
|
|
4208
|
+
observations: await (0, import_project_worktrees.observeProjectMirrorHeads)({
|
|
4209
|
+
projectRoot,
|
|
4210
|
+
primaryBranchName,
|
|
4211
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4212
|
+
mirrorUrl: connection.mirrorUrl,
|
|
4213
|
+
credentialHelper: connection.credentialHelper,
|
|
4214
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
4215
|
+
allowNonFastForward: options2.allowNonFastForward,
|
|
4216
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
|
|
4217
|
+
refreshRemote: false
|
|
4218
|
+
})
|
|
4219
|
+
};
|
|
4220
|
+
};
|
|
4146
4221
|
const observeProjectHeadsBeforeOuterWorkspace = async (input) => {
|
|
4147
|
-
const
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
const
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4222
|
+
const observationStartedAt = Date.now();
|
|
4223
|
+
const eligibleProjects = [...projectConfigById.values()].filter(
|
|
4224
|
+
(project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
|
|
4225
|
+
);
|
|
4226
|
+
try {
|
|
4227
|
+
if (input.failClosed) {
|
|
4228
|
+
const bundles2 = [];
|
|
4229
|
+
for (const project of eligibleProjects) {
|
|
4230
|
+
bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
|
|
4231
|
+
}
|
|
4232
|
+
return bundles2;
|
|
4233
|
+
}
|
|
4234
|
+
let fetchedCount = 0;
|
|
4235
|
+
const bundles = await mapWithConcurrencyLimit(
|
|
4236
|
+
eligibleProjects,
|
|
4237
|
+
PROJECT_MIRROR_OBSERVE_CONCURRENCY,
|
|
4238
|
+
async (project) => {
|
|
4239
|
+
try {
|
|
4240
|
+
const decision = (0, import_project_mirror_fetch_policy.projectMirrorFetchDecision)({
|
|
4241
|
+
advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
|
|
4242
|
+
fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
|
|
4243
|
+
lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
|
|
4244
|
+
nowMs: Date.now(),
|
|
4245
|
+
skippingEnabled: projectMirrorFetchSkippingEnabled
|
|
4246
|
+
});
|
|
4247
|
+
if (decision.fetch) fetchedCount += 1;
|
|
4248
|
+
return await observeSingleProjectMirrorBundle(project, {
|
|
4249
|
+
allowNonFastForward: input.allowNonFastForward,
|
|
4250
|
+
refreshRemote: decision.fetch
|
|
4251
|
+
});
|
|
4252
|
+
} catch (error) {
|
|
4253
|
+
process.stderr.write(
|
|
4254
|
+
`[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
4255
|
+
`
|
|
4256
|
+
);
|
|
4257
|
+
return null;
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
);
|
|
4261
|
+
if (fetchedCount > 0) {
|
|
4171
4262
|
process.stderr.write(
|
|
4172
|
-
`[r5d-worker]
|
|
4263
|
+
`[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
|
|
4173
4264
|
`
|
|
4174
4265
|
);
|
|
4175
4266
|
}
|
|
4267
|
+
return bundles.filter((bundle) => bundle !== null);
|
|
4268
|
+
} finally {
|
|
4269
|
+
lastWorkspaceSyncMirrorObservationMs = Date.now() - observationStartedAt;
|
|
4176
4270
|
}
|
|
4177
|
-
return bundles;
|
|
4178
4271
|
};
|
|
4179
4272
|
const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
|
|
4180
4273
|
const active = new Set(activeMountIds);
|
|
@@ -4443,6 +4536,7 @@ async function startWorker(options) {
|
|
|
4443
4536
|
result = await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
4444
4537
|
queueEnteredAt = Date.now();
|
|
4445
4538
|
syncStartedAt = Date.now();
|
|
4539
|
+
lastWorkspaceSyncMirrorObservationMs = 0;
|
|
4446
4540
|
if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
|
|
4447
4541
|
return await failedWorkspaceSyncResult(
|
|
4448
4542
|
attemptId,
|
|
@@ -4527,7 +4621,9 @@ async function startWorker(options) {
|
|
|
4527
4621
|
telemetry: {
|
|
4528
4622
|
totalMs: finishedAt - requestedAt,
|
|
4529
4623
|
queueMs: queueEnteredAt - requestedAt,
|
|
4530
|
-
|
|
4624
|
+
// Duration of the pre-outer mirror-head observation phase; near zero
|
|
4625
|
+
// on a cycle whose token-gated observation skipped every fetch.
|
|
4626
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
4531
4627
|
synchronizeMs: finishedAt - syncStartedAt
|
|
4532
4628
|
}
|
|
4533
4629
|
};
|
|
@@ -5245,7 +5341,8 @@ async function startWorker(options) {
|
|
|
5245
5341
|
workspaceIncidentConfigDeferralV1: true,
|
|
5246
5342
|
workspaceConfigResetToCanonicalV1: true,
|
|
5247
5343
|
projectBranchDeletionFastAckV1: true,
|
|
5248
|
-
projectMirrorLeaseV1: true
|
|
5344
|
+
projectMirrorLeaseV1: true,
|
|
5345
|
+
projectMirrorRefsTokensV1: true
|
|
5249
5346
|
},
|
|
5250
5347
|
projectRoot: projectsRoot,
|
|
5251
5348
|
artifactRoot,
|
|
@@ -5283,6 +5380,17 @@ async function startWorker(options) {
|
|
|
5283
5380
|
if (message.type === "connected") {
|
|
5284
5381
|
return;
|
|
5285
5382
|
}
|
|
5383
|
+
if (message.type === "project_mirror_refs_tokens") {
|
|
5384
|
+
advertisedProjectMirrorRefsTokens.clear();
|
|
5385
|
+
if (message.tokens && typeof message.tokens === "object") {
|
|
5386
|
+
for (const [projectId, token2] of Object.entries(message.tokens)) {
|
|
5387
|
+
if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
|
|
5388
|
+
advertisedProjectMirrorRefsTokens.set(projectId, token2);
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
return;
|
|
5393
|
+
}
|
|
5286
5394
|
if (message.type === "workspace_config") {
|
|
5287
5395
|
const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
|
|
5288
5396
|
const refreshesDeferredConfiguration = deferredWorkspaceConfiguration !== null && activeWorkspaceIncidentId === null && message.deferWorkspaceSyncForIncidentId === void 0;
|
package/dist/cjs/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var project_mirror_fetch_policy_exports = {};
|
|
20
|
+
__export(project_mirror_fetch_policy_exports, {
|
|
21
|
+
PROJECT_MIRROR_FETCH_BACKSTOP_MS: () => PROJECT_MIRROR_FETCH_BACKSTOP_MS,
|
|
22
|
+
projectMirrorFetchDecision: () => projectMirrorFetchDecision
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(project_mirror_fetch_policy_exports);
|
|
25
|
+
const PROJECT_MIRROR_FETCH_BACKSTOP_MS = 10 * 6e4;
|
|
26
|
+
function projectMirrorFetchDecision(input) {
|
|
27
|
+
if (!input.skippingEnabled) return { fetch: true, reason: "skipping_disabled" };
|
|
28
|
+
if (input.advertisedToken === void 0) return { fetch: true, reason: "no_advertised_token" };
|
|
29
|
+
if (input.fetchedToken === void 0) return { fetch: true, reason: "never_fetched" };
|
|
30
|
+
if (input.advertisedToken !== input.fetchedToken) return { fetch: true, reason: "token_changed" };
|
|
31
|
+
const backstopMs = input.backstopMs ?? PROJECT_MIRROR_FETCH_BACKSTOP_MS;
|
|
32
|
+
if (input.lastFetchAtMs === void 0 || input.nowMs - input.lastFetchAtMs >= backstopMs) {
|
|
33
|
+
return { fetch: true, reason: "backstop_elapsed" };
|
|
34
|
+
}
|
|
35
|
+
return { fetch: false, reason: "token_match" };
|
|
36
|
+
}
|
|
37
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
38
|
+
0 && (module.exports = {
|
|
39
|
+
PROJECT_MIRROR_FETCH_BACKSTOP_MS,
|
|
40
|
+
projectMirrorFetchDecision
|
|
41
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var project_mirror_refs_token_exports = {};
|
|
20
|
+
__export(project_mirror_refs_token_exports, {
|
|
21
|
+
PROJECT_MIRROR_REF_PREFIX: () => PROJECT_MIRROR_REF_PREFIX,
|
|
22
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX: () => PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
23
|
+
canonicalizeLocalProjectMirrorRefs: () => canonicalizeLocalProjectMirrorRefs,
|
|
24
|
+
hashProjectMirrorRefs: () => hashProjectMirrorRefs
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(project_mirror_refs_token_exports);
|
|
27
|
+
var import_node_crypto = require("node:crypto");
|
|
28
|
+
const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
29
|
+
const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
30
|
+
function hashProjectMirrorRefs(refs) {
|
|
31
|
+
const lines = refs.map(({ objectName, refname }) => `${objectName} ${refname}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
32
|
+
return (0, import_node_crypto.createHash)("sha256").update(lines.join("\n")).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
function canonicalizeLocalProjectMirrorRefs(refs) {
|
|
35
|
+
const canonical = [];
|
|
36
|
+
for (const { refname, objectName } of refs) {
|
|
37
|
+
if (refname.startsWith(PROJECT_MIRROR_REF_PREFIX)) {
|
|
38
|
+
canonical.push({ refname: `refs/heads/${refname.slice(PROJECT_MIRROR_REF_PREFIX.length)}`, objectName });
|
|
39
|
+
} else if (refname.startsWith(PROJECT_SERVER_SYNCED_REF_PREFIX)) {
|
|
40
|
+
canonical.push({ refname, objectName });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return canonical;
|
|
44
|
+
}
|
|
45
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
46
|
+
0 && (module.exports = {
|
|
47
|
+
PROJECT_MIRROR_REF_PREFIX,
|
|
48
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX,
|
|
49
|
+
canonicalizeLocalProjectMirrorRefs,
|
|
50
|
+
hashProjectMirrorRefs
|
|
51
|
+
});
|
|
@@ -32,6 +32,7 @@ __export(project_worktrees_exports, {
|
|
|
32
32
|
ProjectBranchCreationRollbackIncompleteError: () => ProjectBranchCreationRollbackIncompleteError,
|
|
33
33
|
applyObservedProjectMirrorHeads: () => applyObservedProjectMirrorHeads,
|
|
34
34
|
assertProjectMirrorHeadsPushed: () => assertProjectMirrorHeadsPushed,
|
|
35
|
+
computeLocalProjectMirrorRefsToken: () => computeLocalProjectMirrorRefsToken,
|
|
35
36
|
createLinkedProjectBranch: () => createLinkedProjectBranch,
|
|
36
37
|
createOrRetryLinkedProjectBranch: () => createOrRetryLinkedProjectBranch,
|
|
37
38
|
deleteLinkedProjectBranch: () => deleteLinkedProjectBranch,
|
|
@@ -57,6 +58,7 @@ var import_node_os = __toESM(require("node:os"), 1);
|
|
|
57
58
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
58
59
|
var import_git_process_environment = require("./git-process-environment.cjs");
|
|
59
60
|
var import_managed_paths = require("./managed-paths.cjs");
|
|
61
|
+
var import_project_mirror_refs_token = require("./project-mirror-refs-token.cjs");
|
|
60
62
|
var import_working_tree_mirror = require("./working-tree-mirror.cjs");
|
|
61
63
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
62
64
|
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
|
|
@@ -1294,6 +1296,7 @@ ${deleted.stdout}`)) {
|
|
|
1294
1296
|
`Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
|
|
1295
1297
|
);
|
|
1296
1298
|
}
|
|
1299
|
+
await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
|
|
1297
1300
|
}
|
|
1298
1301
|
async function pushProjectMirrorHeads(input) {
|
|
1299
1302
|
const results = [];
|
|
@@ -1351,6 +1354,7 @@ async function pushProjectMirrorHeads(input) {
|
|
|
1351
1354
|
if (pushed.exitCode !== 0) {
|
|
1352
1355
|
throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
|
|
1353
1356
|
}
|
|
1357
|
+
await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
|
|
1354
1358
|
results.push({ branchName, head, pushed: true });
|
|
1355
1359
|
}
|
|
1356
1360
|
return results;
|
|
@@ -1363,21 +1367,23 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1363
1367
|
}
|
|
1364
1368
|
async function observeProjectMirrorHeads(input) {
|
|
1365
1369
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1370
|
+
if (input.refreshRemote !== false) {
|
|
1371
|
+
const fetch = await gitResultAsync(
|
|
1372
|
+
primaryPath,
|
|
1373
|
+
[
|
|
1374
|
+
...(0, import_git_process_environment.gitTransportSecurityArgs)(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
|
|
1375
|
+
"fetch",
|
|
1376
|
+
"--no-recurse-submodules",
|
|
1377
|
+
"--prune",
|
|
1378
|
+
input.mirrorUrl,
|
|
1379
|
+
"+refs/heads/*:refs/r5d/mirror/*",
|
|
1380
|
+
"+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
|
|
1381
|
+
],
|
|
1382
|
+
PROJECT_GIT_TRANSPORT_TIMEOUT_MS
|
|
1383
|
+
);
|
|
1384
|
+
if (fetch.exitCode !== 0)
|
|
1385
|
+
throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
|
|
1386
|
+
}
|
|
1381
1387
|
const observations = [];
|
|
1382
1388
|
for (const branchName of [...input.branchNames].sort()) {
|
|
1383
1389
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
@@ -1423,6 +1429,24 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1423
1429
|
}
|
|
1424
1430
|
return observations;
|
|
1425
1431
|
}
|
|
1432
|
+
async function computeLocalProjectMirrorRefsToken(input) {
|
|
1433
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1434
|
+
const rows = await gitAsync(
|
|
1435
|
+
primaryPath,
|
|
1436
|
+
["for-each-ref", "--format=%(objectname) %(refname)", import_project_mirror_refs_token.PROJECT_MIRROR_REF_PREFIX, import_project_mirror_refs_token.PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1437
|
+
"list cached project mirror refs"
|
|
1438
|
+
);
|
|
1439
|
+
const refs = rows.split(/\r?\n/).filter(Boolean).map((row) => {
|
|
1440
|
+
const separator = row.indexOf(" ");
|
|
1441
|
+
const objectName = separator < 0 ? "" : row.slice(0, separator);
|
|
1442
|
+
const refname = separator < 0 ? "" : row.slice(separator + 1);
|
|
1443
|
+
if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
|
|
1444
|
+
throw new Error(`Invalid cached mirror ref listing row: ${row}`);
|
|
1445
|
+
}
|
|
1446
|
+
return { refname, objectName };
|
|
1447
|
+
});
|
|
1448
|
+
return (0, import_project_mirror_refs_token.hashProjectMirrorRefs)((0, import_project_mirror_refs_token.canonicalizeLocalProjectMirrorRefs)(refs));
|
|
1449
|
+
}
|
|
1426
1450
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1427
1451
|
const results = [];
|
|
1428
1452
|
for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
@@ -1505,6 +1529,7 @@ const projectWorktreesTestHarness = {
|
|
|
1505
1529
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1506
1530
|
applyObservedProjectMirrorHeads,
|
|
1507
1531
|
assertProjectMirrorHeadsPushed,
|
|
1532
|
+
computeLocalProjectMirrorRefsToken,
|
|
1508
1533
|
createLinkedProjectBranch,
|
|
1509
1534
|
createOrRetryLinkedProjectBranch,
|
|
1510
1535
|
deleteLinkedProjectBranch,
|
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;
|
|
@@ -4110,6 +4133,17 @@ async function startWorker(options) {
|
|
|
4110
4133
|
for (const result of results) {
|
|
4111
4134
|
lastObservedProjectHeads.set(projectBranchKey(project.projectId, result.branchName), result.head);
|
|
4112
4135
|
}
|
|
4136
|
+
try {
|
|
4137
|
+
fetchedProjectMirrorRefsTokens.set(
|
|
4138
|
+
project.projectId,
|
|
4139
|
+
await computeLocalProjectMirrorRefsToken({
|
|
4140
|
+
projectRoot: configuredProjectRoot(projectsRoot, project),
|
|
4141
|
+
primaryBranchName: primaryProjectBranch(project)
|
|
4142
|
+
})
|
|
4143
|
+
);
|
|
4144
|
+
} catch {
|
|
4145
|
+
fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4146
|
+
}
|
|
4113
4147
|
}
|
|
4114
4148
|
const publishedTombstoneIds = [
|
|
4115
4149
|
...new Set(
|
|
@@ -4135,6 +4169,7 @@ async function startWorker(options) {
|
|
|
4135
4169
|
credentialHelper: deletion.credentialHelper,
|
|
4136
4170
|
credentialUsername: deletion.credentialUsername
|
|
4137
4171
|
});
|
|
4172
|
+
fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4138
4173
|
}
|
|
4139
4174
|
if (deletion.tombstoneId && !deletion.projectDeleted) {
|
|
4140
4175
|
projectWorkspaceState = projectWorkspaceStateStore.recordMirrorRefDeletion({
|
|
@@ -4145,42 +4180,101 @@ async function startWorker(options) {
|
|
|
4145
4180
|
pendingMirrorDeletes.delete(key);
|
|
4146
4181
|
if (deletion.projectDeleted && !projectConfigById.has(deletion.projectId) && ![...pendingMirrorDeletes.values()].some((pending) => pending.projectId === deletion.projectId)) {
|
|
4147
4182
|
removeProjectCredentialStore(deletion.projectId);
|
|
4183
|
+
advertisedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4184
|
+
fetchedProjectMirrorRefsTokens.delete(deletion.projectId);
|
|
4185
|
+
lastProjectMirrorFetchAtMs.delete(deletion.projectId);
|
|
4148
4186
|
}
|
|
4149
4187
|
lastObservedProjectHeads.delete(key);
|
|
4150
4188
|
}
|
|
4151
4189
|
};
|
|
4190
|
+
const observeSingleProjectMirrorBundle = async (project, options2) => {
|
|
4191
|
+
const connection = projectConnection(project);
|
|
4192
|
+
const projectRoot = configuredProjectRoot(projectsRoot, project);
|
|
4193
|
+
const primaryBranchName = primaryProjectBranch(project);
|
|
4194
|
+
if (options2.refreshRemote) {
|
|
4195
|
+
const advertisedBeforeFetch = advertisedProjectMirrorRefsTokens.get(project.projectId);
|
|
4196
|
+
const observations = await observeProjectMirrorHeads({
|
|
4197
|
+
projectRoot,
|
|
4198
|
+
primaryBranchName,
|
|
4199
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4200
|
+
mirrorUrl: connection.mirrorUrl,
|
|
4201
|
+
credentialHelper: connection.credentialHelper,
|
|
4202
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
4203
|
+
allowNonFastForward: options2.allowNonFastForward,
|
|
4204
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId]))
|
|
4205
|
+
});
|
|
4206
|
+
if (advertisedBeforeFetch === void 0) fetchedProjectMirrorRefsTokens.delete(project.projectId);
|
|
4207
|
+
else fetchedProjectMirrorRefsTokens.set(project.projectId, advertisedBeforeFetch);
|
|
4208
|
+
lastProjectMirrorFetchAtMs.set(project.projectId, Date.now());
|
|
4209
|
+
return { projectId: project.projectId, projectRoot, primaryBranchName, observations };
|
|
4210
|
+
}
|
|
4211
|
+
return {
|
|
4212
|
+
projectId: project.projectId,
|
|
4213
|
+
projectRoot,
|
|
4214
|
+
primaryBranchName,
|
|
4215
|
+
observations: await observeProjectMirrorHeads({
|
|
4216
|
+
projectRoot,
|
|
4217
|
+
primaryBranchName,
|
|
4218
|
+
branchNames: project.branches.map(({ branchName }) => branchName),
|
|
4219
|
+
mirrorUrl: connection.mirrorUrl,
|
|
4220
|
+
credentialHelper: connection.credentialHelper,
|
|
4221
|
+
credentialUsername: connection.mirrorCredentialUsername,
|
|
4222
|
+
allowNonFastForward: options2.allowNonFastForward,
|
|
4223
|
+
branchIds: new Map(project.branches.map(({ branchId, branchName }) => [branchName, branchId])),
|
|
4224
|
+
refreshRemote: false
|
|
4225
|
+
})
|
|
4226
|
+
};
|
|
4227
|
+
};
|
|
4152
4228
|
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
|
-
|
|
4229
|
+
const observationStartedAt = Date.now();
|
|
4230
|
+
const eligibleProjects = [...projectConfigById.values()].filter(
|
|
4231
|
+
(project) => !project.executionDisabled && readyProjectIds.has(project.projectId) && project.branches.length > 0
|
|
4232
|
+
);
|
|
4233
|
+
try {
|
|
4234
|
+
if (input.failClosed) {
|
|
4235
|
+
const bundles2 = [];
|
|
4236
|
+
for (const project of eligibleProjects) {
|
|
4237
|
+
bundles2.push(await observeSingleProjectMirrorBundle(project, { allowNonFastForward: input.allowNonFastForward, refreshRemote: true }));
|
|
4238
|
+
}
|
|
4239
|
+
return bundles2;
|
|
4240
|
+
}
|
|
4241
|
+
let fetchedCount = 0;
|
|
4242
|
+
const bundles = await mapWithConcurrencyLimit(
|
|
4243
|
+
eligibleProjects,
|
|
4244
|
+
PROJECT_MIRROR_OBSERVE_CONCURRENCY,
|
|
4245
|
+
async (project) => {
|
|
4246
|
+
try {
|
|
4247
|
+
const decision = projectMirrorFetchDecision({
|
|
4248
|
+
advertisedToken: advertisedProjectMirrorRefsTokens.get(project.projectId),
|
|
4249
|
+
fetchedToken: fetchedProjectMirrorRefsTokens.get(project.projectId),
|
|
4250
|
+
lastFetchAtMs: lastProjectMirrorFetchAtMs.get(project.projectId),
|
|
4251
|
+
nowMs: Date.now(),
|
|
4252
|
+
skippingEnabled: projectMirrorFetchSkippingEnabled
|
|
4253
|
+
});
|
|
4254
|
+
if (decision.fetch) fetchedCount += 1;
|
|
4255
|
+
return await observeSingleProjectMirrorBundle(project, {
|
|
4256
|
+
allowNonFastForward: input.allowNonFastForward,
|
|
4257
|
+
refreshRemote: decision.fetch
|
|
4258
|
+
});
|
|
4259
|
+
} catch (error) {
|
|
4260
|
+
process.stderr.write(
|
|
4261
|
+
`[r5d-worker] failed to observe project mirror heads before workspace update: ${error instanceof Error ? error.message : String(error)}
|
|
4262
|
+
`
|
|
4263
|
+
);
|
|
4264
|
+
return null;
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
);
|
|
4268
|
+
if (fetchedCount > 0) {
|
|
4177
4269
|
process.stderr.write(
|
|
4178
|
-
`[r5d-worker]
|
|
4270
|
+
`[r5d-worker] project mirror observation fetched ${fetchedCount} of ${eligibleProjects.length} mirror(s)
|
|
4179
4271
|
`
|
|
4180
4272
|
);
|
|
4181
4273
|
}
|
|
4274
|
+
return bundles.filter((bundle) => bundle !== null);
|
|
4275
|
+
} finally {
|
|
4276
|
+
lastWorkspaceSyncMirrorObservationMs = Date.now() - observationStartedAt;
|
|
4182
4277
|
}
|
|
4183
|
-
return bundles;
|
|
4184
4278
|
};
|
|
4185
4279
|
const applyObservedProjectHeadsAfterInboundWorkspace = async (bundles, activeMountIds, requireObservedHeads, integratedWorkspaceHead) => {
|
|
4186
4280
|
const active = new Set(activeMountIds);
|
|
@@ -4449,6 +4543,7 @@ async function startWorker(options) {
|
|
|
4449
4543
|
result = await workspaceSyncSingleFlight.runExclusive(async () => {
|
|
4450
4544
|
queueEnteredAt = Date.now();
|
|
4451
4545
|
syncStartedAt = Date.now();
|
|
4546
|
+
lastWorkspaceSyncMirrorObservationMs = 0;
|
|
4452
4547
|
if (!deferredWorkspaceSyncTriggerIsAllowed(deferredWorkspaceConfiguration, activeWorkspaceIncidentId, input.trigger)) {
|
|
4453
4548
|
return await failedWorkspaceSyncResult(
|
|
4454
4549
|
attemptId,
|
|
@@ -4533,7 +4628,9 @@ async function startWorker(options) {
|
|
|
4533
4628
|
telemetry: {
|
|
4534
4629
|
totalMs: finishedAt - requestedAt,
|
|
4535
4630
|
queueMs: queueEnteredAt - requestedAt,
|
|
4536
|
-
|
|
4631
|
+
// Duration of the pre-outer mirror-head observation phase; near zero
|
|
4632
|
+
// on a cycle whose token-gated observation skipped every fetch.
|
|
4633
|
+
prepareMs: lastWorkspaceSyncMirrorObservationMs,
|
|
4537
4634
|
synchronizeMs: finishedAt - syncStartedAt
|
|
4538
4635
|
}
|
|
4539
4636
|
};
|
|
@@ -5251,7 +5348,8 @@ async function startWorker(options) {
|
|
|
5251
5348
|
workspaceIncidentConfigDeferralV1: true,
|
|
5252
5349
|
workspaceConfigResetToCanonicalV1: true,
|
|
5253
5350
|
projectBranchDeletionFastAckV1: true,
|
|
5254
|
-
projectMirrorLeaseV1: true
|
|
5351
|
+
projectMirrorLeaseV1: true,
|
|
5352
|
+
projectMirrorRefsTokensV1: true
|
|
5255
5353
|
},
|
|
5256
5354
|
projectRoot: projectsRoot,
|
|
5257
5355
|
artifactRoot,
|
|
@@ -5289,6 +5387,17 @@ async function startWorker(options) {
|
|
|
5289
5387
|
if (message.type === "connected") {
|
|
5290
5388
|
return;
|
|
5291
5389
|
}
|
|
5390
|
+
if (message.type === "project_mirror_refs_tokens") {
|
|
5391
|
+
advertisedProjectMirrorRefsTokens.clear();
|
|
5392
|
+
if (message.tokens && typeof message.tokens === "object") {
|
|
5393
|
+
for (const [projectId, token2] of Object.entries(message.tokens)) {
|
|
5394
|
+
if (typeof token2 === "string" && /^[0-9a-f]{64}$/.test(token2)) {
|
|
5395
|
+
advertisedProjectMirrorRefsTokens.set(projectId, token2);
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
return;
|
|
5400
|
+
}
|
|
5292
5401
|
if (message.type === "workspace_config") {
|
|
5293
5402
|
const receiptGeneration = ++workspaceConfigurationReceiptGeneration;
|
|
5294
5403
|
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
|
+
};
|
|
@@ -4,6 +4,12 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
|
|
6
6
|
import { validateManagedBranchName } from "./managed-paths.mjs";
|
|
7
|
+
import {
|
|
8
|
+
canonicalizeLocalProjectMirrorRefs,
|
|
9
|
+
hashProjectMirrorRefs,
|
|
10
|
+
PROJECT_MIRROR_REF_PREFIX,
|
|
11
|
+
PROJECT_SERVER_SYNCED_REF_PREFIX
|
|
12
|
+
} from "./project-mirror-refs-token.mjs";
|
|
7
13
|
import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
|
|
8
14
|
const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
|
|
9
15
|
const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
|
|
@@ -1241,6 +1247,7 @@ ${deleted.stdout}`)) {
|
|
|
1241
1247
|
`Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
|
|
1242
1248
|
);
|
|
1243
1249
|
}
|
|
1250
|
+
await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
|
|
1244
1251
|
}
|
|
1245
1252
|
async function pushProjectMirrorHeads(input) {
|
|
1246
1253
|
const results = [];
|
|
@@ -1298,6 +1305,7 @@ async function pushProjectMirrorHeads(input) {
|
|
|
1298
1305
|
if (pushed.exitCode !== 0) {
|
|
1299
1306
|
throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
|
|
1300
1307
|
}
|
|
1308
|
+
await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
|
|
1301
1309
|
results.push({ branchName, head, pushed: true });
|
|
1302
1310
|
}
|
|
1303
1311
|
return results;
|
|
@@ -1310,21 +1318,23 @@ function assertProjectMirrorHeadsPushed(results) {
|
|
|
1310
1318
|
}
|
|
1311
1319
|
async function observeProjectMirrorHeads(input) {
|
|
1312
1320
|
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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}`}`);
|
|
1337
|
+
}
|
|
1328
1338
|
const observations = [];
|
|
1329
1339
|
for (const branchName of [...input.branchNames].sort()) {
|
|
1330
1340
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
@@ -1370,6 +1380,24 @@ async function observeProjectMirrorHeads(input) {
|
|
|
1370
1380
|
}
|
|
1371
1381
|
return observations;
|
|
1372
1382
|
}
|
|
1383
|
+
async function computeLocalProjectMirrorRefsToken(input) {
|
|
1384
|
+
const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
|
|
1385
|
+
const rows = await gitAsync(
|
|
1386
|
+
primaryPath,
|
|
1387
|
+
["for-each-ref", "--format=%(objectname) %(refname)", PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
|
|
1388
|
+
"list cached project mirror refs"
|
|
1389
|
+
);
|
|
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));
|
|
1400
|
+
}
|
|
1373
1401
|
async function applyObservedProjectMirrorHeads(input) {
|
|
1374
1402
|
const results = [];
|
|
1375
1403
|
for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
|
|
@@ -1451,6 +1479,7 @@ export {
|
|
|
1451
1479
|
ProjectBranchCreationRollbackIncompleteError,
|
|
1452
1480
|
applyObservedProjectMirrorHeads,
|
|
1453
1481
|
assertProjectMirrorHeadsPushed,
|
|
1482
|
+
computeLocalProjectMirrorRefsToken,
|
|
1454
1483
|
createLinkedProjectBranch,
|
|
1455
1484
|
createOrRetryLinkedProjectBranch,
|
|
1456
1485
|
deleteLinkedProjectBranch,
|
package/dist/types/main.d.ts
CHANGED
|
@@ -125,6 +125,15 @@ type WorkerServerMessage = {
|
|
|
125
125
|
confirmationReason?: string;
|
|
126
126
|
resetToCanonical?: boolean;
|
|
127
127
|
requiredAncestorHeads?: string[];
|
|
128
|
+
} | {
|
|
129
|
+
/**
|
|
130
|
+
* Content-derived tokens naming each canonical project repository's
|
|
131
|
+
* observable ref state, keyed by project id. Replace-all: projects
|
|
132
|
+
* absent from a message have no advertised token and observation falls
|
|
133
|
+
* back to fetching. See `project-mirror-fetch-policy.ts`.
|
|
134
|
+
*/
|
|
135
|
+
type: "project_mirror_refs_tokens";
|
|
136
|
+
tokens: Record<string, string>;
|
|
128
137
|
} | {
|
|
129
138
|
type: "create_project_branch";
|
|
130
139
|
requestId: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a periodic observation must fetch a project's hidden mirror
|
|
3
|
+
* or may reuse the locally cached mirror refs. The server advertises a
|
|
4
|
+
* content-derived token per project (hash of the exact ref namespaces the
|
|
5
|
+
* observation fetch would transfer); when the advertised token equals the
|
|
6
|
+
* token the local refs already cover, the fetch would be a no-op.
|
|
7
|
+
*
|
|
8
|
+
* Skipping is fail-open: any missing or mismatched information fetches. The
|
|
9
|
+
* backstop bounds the damage of a server-side token bug (for example a token
|
|
10
|
+
* computed against the wrong repository path) to one interval; every
|
|
11
|
+
* git-transport writer is observed by the server's on-disk recompute, so the
|
|
12
|
+
* backstop is insurance against bugs, not against known unobserved writers.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROJECT_MIRROR_FETCH_BACKSTOP_MS: number;
|
|
15
|
+
export type ProjectMirrorFetchDecision = {
|
|
16
|
+
fetch: true;
|
|
17
|
+
reason: "skipping_disabled" | "no_advertised_token" | "never_fetched" | "token_changed" | "backstop_elapsed";
|
|
18
|
+
} | {
|
|
19
|
+
fetch: false;
|
|
20
|
+
reason: "token_match";
|
|
21
|
+
};
|
|
22
|
+
export declare function projectMirrorFetchDecision(input: {
|
|
23
|
+
advertisedToken: string | undefined;
|
|
24
|
+
fetchedToken: string | undefined;
|
|
25
|
+
lastFetchAtMs: number | undefined;
|
|
26
|
+
nowMs: number;
|
|
27
|
+
skippingEnabled: boolean;
|
|
28
|
+
backstopMs?: number;
|
|
29
|
+
}): ProjectMirrorFetchDecision;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
|
|
2
|
+
export declare const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
|
|
3
|
+
export type ProjectMirrorRef = {
|
|
4
|
+
refname: string;
|
|
5
|
+
objectName: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Hash a ref listing into a mirror-refs token. Both sides sort here rather
|
|
9
|
+
* than trusting git's listing order: renaming `refs/r5d/mirror/*` back to
|
|
10
|
+
* `refs/heads/*` changes sort neighborhoods.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hashProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): string;
|
|
13
|
+
/**
|
|
14
|
+
* Rename locally fetched mirror refs into the namespaces the server hashes.
|
|
15
|
+
* Rows outside the two observed namespaces are dropped; the caller lists only
|
|
16
|
+
* those namespaces, so a dropped row means an unexpected listing, and dropping
|
|
17
|
+
* it can only make tokens differ — which fails open into a fetch.
|
|
18
|
+
*/
|
|
19
|
+
export declare function canonicalizeLocalProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): ProjectMirrorRef[];
|
|
@@ -212,11 +212,32 @@ export type ProjectMirrorHeadObservation = {
|
|
|
212
212
|
* Fetch and pin the exact hidden-mirror tips that may be applied after a
|
|
213
213
|
* causally later outer-workspace fetch and hydration. Observation deliberately
|
|
214
214
|
* does not mutate a live branch or its index.
|
|
215
|
+
*
|
|
216
|
+
* With `refreshRemote: false` the network fetch is skipped and the tips come
|
|
217
|
+
* from the locally cached `refs/r5d/mirror/*` / `refs/r5d/server-synced/*`
|
|
218
|
+
* refs of an earlier fetch. That is safe in the stale direction (a newer
|
|
219
|
+
* mirror tip waits for a later cycle, exactly like a tip that lands mid-cycle
|
|
220
|
+
* today) and in the fresh direction (hidden-mirror publication is tree-first,
|
|
221
|
+
* so any locally cached tip's covering outer tree was published before the
|
|
222
|
+
* fetch that cached it, hence before this cycle's outer fetch).
|
|
215
223
|
*/
|
|
216
224
|
export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadUpdateInput, "branchMutationAllowed"> & {
|
|
217
225
|
allowNonFastForward: boolean;
|
|
218
226
|
branchIds?: ReadonlyMap<string, string>;
|
|
227
|
+
refreshRemote?: boolean;
|
|
219
228
|
}): Promise<ProjectMirrorHeadObservation[]>;
|
|
229
|
+
/**
|
|
230
|
+
* Compute the mirror-refs token for the locally cached mirror state — the
|
|
231
|
+
* token the server will advertise once it hashes the same refs. After a
|
|
232
|
+
* successful lease push (which updates `refs/r5d/mirror/*` above), this
|
|
233
|
+
* matches the server's next advertisement unless something else also moved
|
|
234
|
+
* the canonical repository, in which case the mismatch fails open into a
|
|
235
|
+
* fetch. Formula twin: `server/git/project-mirror-refs-token.ts`.
|
|
236
|
+
*/
|
|
237
|
+
export declare function computeLocalProjectMirrorRefsToken(input: {
|
|
238
|
+
projectRoot: string;
|
|
239
|
+
primaryBranchName: string;
|
|
240
|
+
}): Promise<string>;
|
|
220
241
|
/** Apply only previously observed object ids; this function never refetches. */
|
|
221
242
|
export declare function applyObservedProjectMirrorHeads(input: {
|
|
222
243
|
projectRoot: string;
|