@hominis/fireforge 0.49.1 → 0.50.0
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/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/dist/bin/fireforge.js +6 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/build-prelock.d.ts +20 -0
- package/dist/src/commands/build-prelock.js +50 -0
- package/dist/src/commands/build.d.ts +3 -1
- package/dist/src/commands/build.js +13 -6
- package/dist/src/commands/check-report.d.ts +28 -0
- package/dist/src/commands/check-report.js +78 -0
- package/dist/src/commands/check-run.d.ts +28 -0
- package/dist/src/commands/check-run.js +104 -0
- package/dist/src/commands/check-steps.d.ts +67 -0
- package/dist/src/commands/check-steps.js +175 -0
- package/dist/src/commands/check.d.ts +32 -0
- package/dist/src/commands/check.js +160 -0
- package/dist/src/commands/config.js +5 -6
- package/dist/src/commands/doctor-engine-eslint.js +6 -1
- package/dist/src/commands/doctor-source-pin.js +4 -1
- package/dist/src/commands/export-adopt-check.d.ts +38 -0
- package/dist/src/commands/export-adopt-check.js +98 -0
- package/dist/src/commands/export-flow.js +29 -14
- package/dist/src/commands/export-placement-gate.d.ts +6 -2
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-staged-registration.d.ts +46 -0
- package/dist/src/commands/export-staged-registration.js +91 -0
- package/dist/src/commands/forward-import-remedy.d.ts +32 -0
- package/dist/src/commands/forward-import-remedy.js +63 -0
- package/dist/src/commands/furnace/apply.js +3 -3
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
- package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
- package/dist/src/commands/furnace/chrome-doc.js +2 -2
- package/dist/src/commands/furnace/create-templates.d.ts +8 -3
- package/dist/src/commands/furnace/create-templates.js +8 -23
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
- package/dist/src/commands/furnace/deploy-named.js +107 -0
- package/dist/src/commands/furnace/deploy.d.ts +3 -2
- package/dist/src/commands/furnace/deploy.js +50 -86
- package/dist/src/commands/furnace/diff.js +26 -3
- package/dist/src/commands/furnace/index.js +66 -41
- package/dist/src/commands/furnace/init.js +13 -6
- package/dist/src/commands/furnace/override.js +4 -2
- package/dist/src/commands/furnace/preview.js +4 -3
- package/dist/src/commands/furnace/refresh.js +2 -2
- package/dist/src/commands/furnace/remove.js +4 -3
- package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
- package/dist/src/commands/furnace/rename-dry-run.js +127 -0
- package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
- package/dist/src/commands/furnace/rename-helpers.js +15 -1
- package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
- package/dist/src/commands/furnace/rename-registrations.js +93 -0
- package/dist/src/commands/furnace/rename.d.ts +2 -1
- package/dist/src/commands/furnace/rename.js +28 -66
- package/dist/src/commands/furnace/scan.d.ts +9 -4
- package/dist/src/commands/furnace/scan.js +11 -9
- package/dist/src/commands/furnace/sync.js +3 -2
- package/dist/src/commands/furnace/validate-report.d.ts +45 -0
- package/dist/src/commands/furnace/validate-report.js +91 -0
- package/dist/src/commands/furnace/validate.d.ts +1 -1
- package/dist/src/commands/furnace/validate.js +127 -114
- package/dist/src/commands/furnace/validation-output.js +14 -3
- package/dist/src/commands/lint-per-patch.js +31 -60
- package/dist/src/commands/lint-report.d.ts +8 -4
- package/dist/src/commands/lint-report.js +15 -5
- package/dist/src/commands/lint.js +15 -34
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/delete-gap-notice.js +3 -1
- package/dist/src/commands/patch/delete.d.ts +1 -1
- package/dist/src/commands/patch/delete.js +13 -98
- package/dist/src/commands/patch/index.d.ts +1 -1
- package/dist/src/commands/patch/index.js +4 -2
- package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
- package/dist/src/commands/patch/lint-ignore.js +65 -28
- package/dist/src/commands/patch/move-files-create.js +26 -17
- package/dist/src/commands/patch/move-files-into.js +5 -3
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
- package/dist/src/commands/patch/removal-dependents.js +133 -0
- package/dist/src/commands/patch/rename.js +7 -0
- package/dist/src/commands/patch/retire.d.ts +34 -0
- package/dist/src/commands/patch/retire.js +128 -0
- package/dist/src/commands/patch/split-plan.d.ts +7 -7
- package/dist/src/commands/patch/split-plan.js +45 -28
- package/dist/src/commands/patch/split-staged.d.ts +108 -0
- package/dist/src/commands/patch/split-staged.js +202 -0
- package/dist/src/commands/patch/split.d.ts +5 -0
- package/dist/src/commands/patch/split.js +51 -15
- package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
- package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
- package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
- package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
- package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
- package/dist/src/commands/patch/staged-dependency-from.js +108 -0
- package/dist/src/commands/patch/staged-dependency.js +14 -41
- package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
- package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
- package/dist/src/commands/re-export-adjacent.d.ts +45 -2
- package/dist/src/commands/re-export-adjacent.js +156 -37
- package/dist/src/commands/re-export-drift.d.ts +36 -8
- package/dist/src/commands/re-export-drift.js +73 -49
- package/dist/src/commands/re-export-files.js +11 -8
- package/dist/src/commands/re-export-lint.d.ts +2 -1
- package/dist/src/commands/re-export-lint.js +23 -10
- package/dist/src/commands/re-export-options.js +45 -4
- package/dist/src/commands/re-export-preflight.d.ts +23 -0
- package/dist/src/commands/re-export-preflight.js +160 -0
- package/dist/src/commands/re-export-register.js +42 -17
- package/dist/src/commands/re-export-removed.d.ts +12 -1
- package/dist/src/commands/re-export-removed.js +41 -11
- package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
- package/dist/src/commands/re-export-scan-owner.js +116 -0
- package/dist/src/commands/re-export-scan.d.ts +17 -9
- package/dist/src/commands/re-export-scan.js +56 -23
- package/dist/src/commands/re-export-single.js +22 -6
- package/dist/src/commands/re-export-summary.d.ts +28 -0
- package/dist/src/commands/re-export-summary.js +36 -0
- package/dist/src/commands/re-export.js +37 -24
- package/dist/src/commands/rebase/index.js +17 -17
- package/dist/src/commands/run.js +16 -2
- package/dist/src/commands/status-check.d.ts +55 -16
- package/dist/src/commands/status-check.js +92 -37
- package/dist/src/commands/status-json.d.ts +22 -9
- package/dist/src/commands/status-json.js +47 -32
- package/dist/src/commands/status-output.js +3 -1
- package/dist/src/commands/status-scope.d.ts +37 -0
- package/dist/src/commands/status-scope.js +54 -0
- package/dist/src/commands/status.js +66 -23
- package/dist/src/commands/test-browser-preflight.js +19 -3
- package/dist/src/commands/test-build-furnace.d.ts +21 -0
- package/dist/src/commands/test-build-furnace.js +48 -0
- package/dist/src/commands/test-build-phase.js +47 -41
- package/dist/src/commands/test-checkjs.js +7 -0
- package/dist/src/commands/test-diagnose.js +7 -0
- package/dist/src/commands/test-modes.d.ts +16 -1
- package/dist/src/commands/test-modes.js +24 -1
- package/dist/src/commands/test-multi-verdict.d.ts +61 -0
- package/dist/src/commands/test-multi-verdict.js +111 -0
- package/dist/src/commands/test-orchestrate.d.ts +43 -0
- package/dist/src/commands/test-orchestrate.js +222 -0
- package/dist/src/commands/test-perf-scope.d.ts +29 -0
- package/dist/src/commands/test-perf-scope.js +83 -0
- package/dist/src/commands/test-prelock.d.ts +51 -0
- package/dist/src/commands/test-prelock.js +177 -0
- package/dist/src/commands/test-register.js +54 -10
- package/dist/src/commands/test-run.js +4 -1
- package/dist/src/commands/test-stale-gate.js +57 -27
- package/dist/src/commands/test-verdict.d.ts +10 -1
- package/dist/src/commands/test-verdict.js +28 -1
- package/dist/src/commands/test.d.ts +6 -1
- package/dist/src/commands/test.js +66 -60
- package/dist/src/commands/token-lint.d.ts +14 -0
- package/dist/src/commands/token-lint.js +111 -0
- package/dist/src/commands/token.js +10 -0
- package/dist/src/commands/verify-scope.d.ts +28 -0
- package/dist/src/commands/verify-scope.js +67 -0
- package/dist/src/commands/verify.d.ts +10 -2
- package/dist/src/commands/verify.js +59 -35
- package/dist/src/core/build-baseline-types.d.ts +46 -0
- package/dist/src/core/build-baseline-validate.js +19 -0
- package/dist/src/core/build-baseline.d.ts +5 -1
- package/dist/src/core/build-baseline.js +3 -1
- package/dist/src/core/build-overwrite-guard.d.ts +50 -0
- package/dist/src/core/build-overwrite-guard.js +84 -4
- package/dist/src/core/build-prepare.d.ts +7 -0
- package/dist/src/core/build-prepare.js +14 -8
- package/dist/src/core/build-run-record.d.ts +74 -0
- package/dist/src/core/build-run-record.js +136 -0
- package/dist/src/core/checkjs-support-heads.d.ts +41 -0
- package/dist/src/core/checkjs-support-heads.js +104 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +8 -0
- package/dist/src/core/config-validate-check.d.ts +8 -0
- package/dist/src/core/config-validate-check.js +143 -0
- package/dist/src/core/config-validate-file-size.d.ts +9 -6
- package/dist/src/core/config-validate-file-size.js +91 -2
- package/dist/src/core/config-validate-history.d.ts +11 -0
- package/dist/src/core/config-validate-history.js +38 -0
- package/dist/src/core/config-validate-patch-policy.js +100 -0
- package/dist/src/core/config-validate-test-toolchains.js +7 -0
- package/dist/src/core/config-validate-token-scales.d.ts +6 -0
- package/dist/src/core/config-validate-token-scales.js +50 -0
- package/dist/src/core/config-validate.js +14 -5
- package/dist/src/core/config.d.ts +5 -0
- package/dist/src/core/config.js +16 -0
- package/dist/src/core/coverage-dispatch.d.ts +24 -0
- package/dist/src/core/coverage-dispatch.js +44 -0
- package/dist/src/core/coverage-extend.d.ts +70 -45
- package/dist/src/core/coverage-extend.js +199 -144
- package/dist/src/core/coverage-records.d.ts +97 -0
- package/dist/src/core/coverage-records.js +350 -0
- package/dist/src/core/destructive.d.ts +7 -4
- package/dist/src/core/destructive.js +10 -10
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -8
- package/dist/src/core/export-moved-code-shape.d.ts +49 -11
- package/dist/src/core/export-moved-code-shape.js +135 -44
- package/dist/src/core/file-lock-queue.d.ts +67 -0
- package/dist/src/core/file-lock-queue.js +221 -0
- package/dist/src/core/file-lock.d.ts +4 -25
- package/dist/src/core/file-lock.js +105 -138
- package/dist/src/core/furnace-apply-output.d.ts +10 -0
- package/dist/src/core/furnace-apply-output.js +31 -1
- package/dist/src/core/furnace-apply.js +12 -2
- package/dist/src/core/furnace-composed-source.d.ts +76 -0
- package/dist/src/core/furnace-composed-source.js +167 -0
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +9 -0
- package/dist/src/core/furnace-config-validate-exceptions.js +71 -0
- package/dist/src/core/furnace-config-validate.js +8 -0
- package/dist/src/core/furnace-css-fragments.d.ts +10 -0
- package/dist/src/core/furnace-css-fragments.js +26 -0
- package/dist/src/core/furnace-operation.d.ts +3 -3
- package/dist/src/core/furnace-registration-remove.d.ts +22 -1
- package/dist/src/core/furnace-registration-remove.js +53 -17
- package/dist/src/core/furnace-registration-rename.d.ts +26 -0
- package/dist/src/core/furnace-registration-rename.js +77 -0
- package/dist/src/core/furnace-registration.d.ts +14 -4
- package/dist/src/core/furnace-registration.js +44 -10
- package/dist/src/core/furnace-staleness.d.ts +7 -0
- package/dist/src/core/furnace-staleness.js +34 -24
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +10 -1
- package/dist/src/core/furnace-validate-exceptions.d.ts +40 -0
- package/dist/src/core/furnace-validate-exceptions.js +43 -0
- package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
- package/dist/src/core/furnace-validate-icon-button.js +49 -0
- package/dist/src/core/furnace-validate.js +1 -1
- package/dist/src/core/harness-port-fallback.d.ts +60 -0
- package/dist/src/core/harness-port-fallback.js +96 -0
- package/dist/src/core/history-log-file.d.ts +61 -0
- package/dist/src/core/history-log-file.js +157 -0
- package/dist/src/core/history-log.d.ts +2 -1
- package/dist/src/core/history-log.js +2 -1
- package/dist/src/core/json-preserving-edit.d.ts +22 -0
- package/dist/src/core/json-preserving-edit.js +199 -0
- package/dist/src/core/mach.js +8 -1
- package/dist/src/core/negative-control-diff.d.ts +39 -0
- package/dist/src/core/negative-control-diff.js +136 -0
- package/dist/src/core/negative-control.d.ts +138 -0
- package/dist/src/core/negative-control.js +356 -0
- package/dist/src/core/objdir-install-manifests.d.ts +73 -0
- package/dist/src/core/objdir-install-manifests.js +226 -0
- package/dist/src/core/patch-export-update.d.ts +2 -0
- package/dist/src/core/patch-export-update.js +1 -0
- package/dist/src/core/patch-export.js +3 -4
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +37 -0
- package/dist/src/core/patch-lint-checkjs-any-import.js +61 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -11
- package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
- package/dist/src/core/patch-lint-code-lines.js +105 -0
- package/dist/src/core/patch-lint-cross.js +12 -4
- package/dist/src/core/patch-lint-css.d.ts +8 -2
- package/dist/src/core/patch-lint-css.js +10 -4
- package/dist/src/core/patch-lint-file-size.d.ts +12 -5
- package/dist/src/core/patch-lint-file-size.js +22 -11
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +18 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +35 -10
- package/dist/src/core/patch-lint-forward-registration.js +10 -0
- package/dist/src/core/patch-lint-ignore.d.ts +152 -0
- package/dist/src/core/patch-lint-ignore.js +299 -0
- package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
- package/dist/src/core/patch-lint-size-thresholds.js +110 -0
- package/dist/src/core/patch-lint-size.d.ts +117 -0
- package/dist/src/core/patch-lint-size.js +323 -0
- package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
- package/dist/src/core/patch-lint-staged-registration.js +20 -0
- package/dist/src/core/patch-lint-token-scale.d.ts +39 -0
- package/dist/src/core/patch-lint-token-scale.js +109 -0
- package/dist/src/core/patch-lint.d.ts +11 -83
- package/dist/src/core/patch-lint.js +64 -286
- package/dist/src/core/patch-lock.js +7 -6
- package/dist/src/core/patch-manifest-consistency.js +7 -6
- package/dist/src/core/patch-manifest-io.d.ts +6 -3
- package/dist/src/core/patch-manifest-io.js +19 -8
- package/dist/src/core/patch-manifest-validate.js +34 -2
- package/dist/src/core/patch-order-legal.d.ts +25 -0
- package/dist/src/core/patch-order-legal.js +83 -0
- package/dist/src/core/patch-policy-content.d.ts +40 -0
- package/dist/src/core/patch-policy-content.js +216 -0
- package/dist/src/core/patch-policy.d.ts +16 -3
- package/dist/src/core/patch-policy.js +16 -9
- package/dist/src/core/patch-retired.d.ts +35 -0
- package/dist/src/core/patch-retired.js +54 -0
- package/dist/src/core/patch-subset.d.ts +16 -0
- package/dist/src/core/patch-subset.js +66 -0
- package/dist/src/core/preflight-wait.d.ts +20 -0
- package/dist/src/core/preflight-wait.js +38 -0
- package/dist/src/core/rebase-from-version.d.ts +46 -0
- package/dist/src/core/rebase-from-version.js +71 -0
- package/dist/src/core/run-log.d.ts +20 -0
- package/dist/src/core/run-log.js +89 -3
- package/dist/src/core/run-profile.d.ts +7 -0
- package/dist/src/core/run-profile.js +7 -1
- package/dist/src/core/smoke-patterns.d.ts +5 -0
- package/dist/src/core/smoke-patterns.js +13 -0
- package/dist/src/core/test-harness-crash.d.ts +18 -1
- package/dist/src/core/test-harness-crash.js +6 -4
- package/dist/src/core/test-harness-signature.d.ts +17 -0
- package/dist/src/core/test-harness-signature.js +4 -0
- package/dist/src/core/test-run-completeness.d.ts +57 -0
- package/dist/src/core/test-run-completeness.js +144 -0
- package/dist/src/core/test-stale-check.d.ts +27 -1
- package/dist/src/core/test-stale-check.js +39 -49
- package/dist/src/core/test-stale-exempt.d.ts +40 -0
- package/dist/src/core/test-stale-exempt.js +70 -0
- package/dist/src/core/token-lint.d.ts +50 -0
- package/dist/src/core/token-lint.js +126 -0
- package/dist/src/core/tree-guard.js +3 -0
- package/dist/src/core/typecheck-shim.d.ts +1 -1
- package/dist/src/core/typecheck-shim.js +90 -2
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.js +3 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +148 -21
- package/dist/src/types/commands/patches.d.ts +75 -1
- package/dist/src/types/config.d.ts +192 -0
- package/dist/src/types/config.js +9 -0
- package/dist/src/types/furnace.d.ts +43 -1
- package/dist/src/types/furnace.js +7 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +34 -6
- package/dist/src/utils/options.js +86 -5
- package/dist/src/utils/path-glob.d.ts +15 -0
- package/dist/src/utils/path-glob.js +47 -0
- package/dist/src/utils/regex.d.ts +15 -3
- package/dist/src/utils/regex.js +37 -16
- package/docs/configuration.md +282 -9
- package/docs/exit-codes.md +18 -1
- package/docs/furnace.md +97 -3
- package/docs/lifecycle-invariants.md +52 -19
- package/docs/machine-output.md +74 -12
- package/docs/patch-workflow.md +297 -22
- package/docs/run-logs.md +20 -1
- package/docs/testing.md +276 -20
- package/package.json +2 -2
|
@@ -38,12 +38,67 @@ function isSubstantiveLine(line) {
|
|
|
38
38
|
return false;
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
|
-
/** Minimum shared substantive lines before the shape is claimed. */
|
|
42
|
-
const MOVED_CODE_LINE_THRESHOLD = 3;
|
|
43
41
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* Minimum run of consecutive substantive lines, in the same order in both
|
|
43
|
+
* the new file and the source patch, before a block counts as moved code.
|
|
44
|
+
* Matching scattered single lines instead let a large unrelated patch
|
|
45
|
+
* "share" a dozen common statements with any new module and sent operators
|
|
46
|
+
* down an adopt-then-split sequence through a patch that only carried a
|
|
47
|
+
* `<script>` line for the file.
|
|
48
|
+
*/
|
|
49
|
+
const MOVED_CODE_BLOCK_THRESHOLD = 3;
|
|
50
|
+
function substantiveSequences(diff) {
|
|
51
|
+
const sequences = new Map();
|
|
52
|
+
for (const [file, lines] of extractAddedLinesPerFile(diff)) {
|
|
53
|
+
const substantive = lines.map((line) => line.trim()).filter(isSubstantiveLine);
|
|
54
|
+
if (substantive.length > 0)
|
|
55
|
+
sequences.set(file, substantive);
|
|
56
|
+
}
|
|
57
|
+
return sequences;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Longest-first, non-overlapping runs of `pending` that appear contiguously
|
|
61
|
+
* in one of `sources`' files. Anchored on exact line matches, so the work
|
|
62
|
+
* is bounded by the number of shared lines, not the product of the sizes.
|
|
63
|
+
*/
|
|
64
|
+
function findBlocks(file, pending, sources) {
|
|
65
|
+
const positions = new Map();
|
|
66
|
+
for (const [sourceFile, lines] of sources) {
|
|
67
|
+
lines.forEach((line, index) => {
|
|
68
|
+
const list = positions.get(line) ?? [];
|
|
69
|
+
list.push([sourceFile, index]);
|
|
70
|
+
positions.set(line, list);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const blocks = [];
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < pending.length) {
|
|
76
|
+
let best;
|
|
77
|
+
for (const [sourceFile, start] of positions.get(pending[i] ?? '') ?? []) {
|
|
78
|
+
const source = sources.get(sourceFile) ?? [];
|
|
79
|
+
let length = 0;
|
|
80
|
+
while (i + length < pending.length &&
|
|
81
|
+
start + length < source.length &&
|
|
82
|
+
pending[i + length] === source[start + length]) {
|
|
83
|
+
length += 1;
|
|
84
|
+
}
|
|
85
|
+
if (best === undefined || length > best.length)
|
|
86
|
+
best = { sourceFile, length };
|
|
87
|
+
}
|
|
88
|
+
if (best !== undefined && best.length >= MOVED_CODE_BLOCK_THRESHOLD) {
|
|
89
|
+
blocks.push({ file, sourceFile: best.sourceFile, lines: pending.slice(i, i + best.length) });
|
|
90
|
+
i += best.length;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
i += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return blocks;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Finds existing patches that still carry a block of substantive lines the
|
|
100
|
+
* pending diff's new files would add, the fingerprint of code moved out of
|
|
101
|
+
* an existing patch into a new file.
|
|
47
102
|
*
|
|
48
103
|
* Pure. Overlaps are returned strongest-first so the message names the
|
|
49
104
|
* most likely source patch.
|
|
@@ -53,59 +108,95 @@ const MOVED_CODE_LINE_THRESHOLD = 3;
|
|
|
53
108
|
* @returns Overlaps, or an empty list when the shape is not present
|
|
54
109
|
*/
|
|
55
110
|
export function detectMovedCodeOverlaps(pendingDiff, entries) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
for (const [file, lines] of pendingByFile) {
|
|
59
|
-
const substantive = new Set(lines.map((line) => line.trim()).filter(isSubstantiveLine));
|
|
60
|
-
if (substantive.size > 0)
|
|
61
|
-
pendingLines.set(file, substantive);
|
|
62
|
-
}
|
|
63
|
-
if (pendingLines.size === 0)
|
|
111
|
+
const pending = substantiveSequences(pendingDiff);
|
|
112
|
+
if (pending.size === 0)
|
|
64
113
|
return [];
|
|
65
114
|
const overlaps = [];
|
|
66
115
|
for (const entry of entries) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
for (const line of lines) {
|
|
70
|
-
const trimmed = line.trim();
|
|
71
|
-
if (isSubstantiveLine(trimmed))
|
|
72
|
-
existing.add(trimmed);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (existing.size === 0)
|
|
116
|
+
const sources = substantiveSequences(entry.diff);
|
|
117
|
+
if (sources.size === 0)
|
|
76
118
|
continue;
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if (fileShared > 0)
|
|
88
|
-
files.push(file);
|
|
89
|
-
}
|
|
90
|
-
if (shared.size >= MOVED_CODE_LINE_THRESHOLD && files.length > 0) {
|
|
91
|
-
overlaps.push({ sourcePatch: entry.filename, files: files.sort(), sharedLines: shared.size });
|
|
92
|
-
}
|
|
119
|
+
const blocks = [...pending].flatMap(([file, lines]) => findBlocks(file, lines, sources));
|
|
120
|
+
if (blocks.length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
overlaps.push({
|
|
123
|
+
sourcePatch: entry.filename,
|
|
124
|
+
files: [...new Set(blocks.map((block) => block.file))].sort(),
|
|
125
|
+
sharedLines: blocks.reduce((sum, block) => sum + block.lines.length, 0),
|
|
126
|
+
blocks,
|
|
127
|
+
});
|
|
93
128
|
}
|
|
94
129
|
return overlaps.sort((a, b) => b.sharedLines - a.sharedLines || a.sourcePatch.localeCompare(b.sourcePatch));
|
|
95
130
|
}
|
|
131
|
+
/** Lines shown per matched block, and blocks shown per overlap. */
|
|
132
|
+
const SHOWN_BLOCK_LINES = 4;
|
|
133
|
+
const SHOWN_BLOCKS = 3;
|
|
134
|
+
/** Renders the matched blocks of one overlap, capped, as detail lines. */
|
|
135
|
+
function formatMatchedBlocks(overlap) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
for (const block of overlap.blocks.slice(0, SHOWN_BLOCKS)) {
|
|
138
|
+
lines.push(` ${block.file} ← ${overlap.sourcePatch} (${block.sourceFile}), ${block.lines.length} line(s):`);
|
|
139
|
+
for (const line of block.lines.slice(0, SHOWN_BLOCK_LINES))
|
|
140
|
+
lines.push(` | ${line}`);
|
|
141
|
+
if (block.lines.length > SHOWN_BLOCK_LINES) {
|
|
142
|
+
lines.push(` | … ${block.lines.length - SHOWN_BLOCK_LINES} more`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (overlap.blocks.length > SHOWN_BLOCKS) {
|
|
146
|
+
lines.push(` … ${overlap.blocks.length - SHOWN_BLOCKS} more block(s)`);
|
|
147
|
+
}
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Renders the remedy lines appended to a placement refusal: "register
|
|
152
|
+
* first" when new modules lack their moz.build registration, then the
|
|
153
|
+
* adopt-then-split sequence for a real moved-code block, but only when the
|
|
154
|
+
* adopt itself would pass. Each claim names its evidence (the matched
|
|
155
|
+
* lines, the blocking findings) so it can be checked.
|
|
156
|
+
*
|
|
157
|
+
* @param input - See {@link MovedCodeAdviceInput}
|
|
158
|
+
* @returns Detail lines, or an empty list when neither shape is present
|
|
159
|
+
*/
|
|
160
|
+
export function formatMovedCodeAdvice(input) {
|
|
161
|
+
const lines = [];
|
|
162
|
+
const unregistered = input.unregisteredModules ?? [];
|
|
163
|
+
if (unregistered.length > 0) {
|
|
164
|
+
lines.push('', `Register first: ${unregistered.join(', ')} ${unregistered.length === 1 ? 'is a new system module' : 'are new system modules'} ` +
|
|
165
|
+
'no patch registers in moz.build. Add the moz.build line in the patch that owns that ' +
|
|
166
|
+
'moz.build; when its line is already in the working tree but unexported, declare it as a ' +
|
|
167
|
+
'staged registration on that patch, re-run this export, then re-export that patch.' +
|
|
168
|
+
(input.registrationResolvesAll === true
|
|
169
|
+
? ' The exported content has no other cross-patch error, so once registered the new ' +
|
|
170
|
+
'files need no adoption into another patch.'
|
|
171
|
+
: ''));
|
|
172
|
+
}
|
|
173
|
+
const strongest = input.overlaps[0];
|
|
174
|
+
if (strongest === undefined || input.registrationResolvesAll === true)
|
|
175
|
+
return lines;
|
|
176
|
+
const blockers = input.adoptBlockers ?? [];
|
|
177
|
+
const claim = `${strongest.sharedLines} substantive line(s) the new file(s) would add, in ` +
|
|
178
|
+
`${strongest.blocks.length} matching block(s), are still carried by ${strongest.sourcePatch}:`;
|
|
179
|
+
if (blockers.length > 0) {
|
|
180
|
+
lines.push('', claim, ...formatMatchedBlocks(strongest), `Adopting the new files into ${strongest.sourcePatch} first (adopt-then-split) would be refused too, so it is not offered:`, ...blockers.map((blocker) => ` - ${blocker}`));
|
|
181
|
+
return lines;
|
|
182
|
+
}
|
|
183
|
+
const adopt = formatAdoptThenSplitRemedy(strongest, input.newPatchName, input.insertionOrder);
|
|
184
|
+
if (unregistered.length > 0) {
|
|
185
|
+
adopt.splice(1, 0, 'Only if the export still fails once those modules are registered:');
|
|
186
|
+
}
|
|
187
|
+
lines.push(...adopt, `Matched lines: ${claim}`, ...formatMatchedBlocks(strongest));
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
96
190
|
/**
|
|
97
191
|
* Renders the adopt-then-split remedy for the strongest overlap, as lines
|
|
98
192
|
* appended to the refusal's detail list.
|
|
99
193
|
*
|
|
100
|
-
* @param
|
|
194
|
+
* @param strongest - The strongest overlap {@link detectMovedCodeOverlaps} found
|
|
101
195
|
* @param newPatchName - The filename the pending export would take
|
|
102
196
|
* @param insertionOrder - The order the pending export would take
|
|
103
|
-
* @returns Detail lines
|
|
197
|
+
* @returns Detail lines
|
|
104
198
|
*/
|
|
105
|
-
|
|
106
|
-
const strongest = overlaps[0];
|
|
107
|
-
if (strongest === undefined)
|
|
108
|
-
return [];
|
|
199
|
+
function formatAdoptThenSplitRemedy(strongest, newPatchName, insertionOrder) {
|
|
109
200
|
const fileArgs = strongest.files.map((file) => `--scan-file ${file}`).join(' ');
|
|
110
201
|
const moveArgs = strongest.files.map((file) => `--file ${file}`).join(' ');
|
|
111
202
|
// `patch move-files --create` takes the new patch's name, not a
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live queue state around a contended lock: how many are queued and how many
|
|
3
|
+
* are ahead of you. Waiters are granted the lock in arrival order, so `ahead`
|
|
4
|
+
* is the number of grants that happen before yours.
|
|
5
|
+
*/
|
|
6
|
+
export interface LockQueueState {
|
|
7
|
+
/** Live waiters currently queued for the lock, including this process. */
|
|
8
|
+
depth: number;
|
|
9
|
+
/** Live waiters that arrived before this process. */
|
|
10
|
+
ahead: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A ticket not refreshed for this long is skipped when the queue is read.
|
|
14
|
+
* Six heartbeats of slack: long enough that a loaded host never skips a
|
|
15
|
+
* waiter that is merely slow, short enough that a stopped waiter does not
|
|
16
|
+
* hold the head of the queue for more than half a minute.
|
|
17
|
+
*/
|
|
18
|
+
export declare const WAITER_STALE_MS = 30000;
|
|
19
|
+
/** Sibling directory holding the waiter tickets of a lock. */
|
|
20
|
+
export declare function lockWaitersDir(lockPath: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* This process's view of itself in the queue: its wait start, and its ticket
|
|
23
|
+
* when it holds one. Without a ticket the position falls back to comparing
|
|
24
|
+
* wait starts, which is what a waiter whose registration failed can know.
|
|
25
|
+
*/
|
|
26
|
+
export interface QueueSelf {
|
|
27
|
+
startedAt: number;
|
|
28
|
+
ticket?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads the live waiter queue around `lockPath`.
|
|
32
|
+
*
|
|
33
|
+
* Advisory and fail-open: an unreadable registry reports an empty queue
|
|
34
|
+
* rather than failing whatever asked. The caller's own ticket always counts
|
|
35
|
+
* as live, since this process knows it is waiting.
|
|
36
|
+
*
|
|
37
|
+
* @param lockPath - The lock directory whose queue to inspect
|
|
38
|
+
* @param self - This waiter, to compute `ahead`
|
|
39
|
+
*/
|
|
40
|
+
export declare function readLockQueue(lockPath: string, self?: QueueSelf): Promise<LockQueueState>;
|
|
41
|
+
/** A waiter's place in the queue, from joining it to leaving it. */
|
|
42
|
+
export interface WaiterRegistration {
|
|
43
|
+
/** Ticket name, or `undefined` when registration failed. */
|
|
44
|
+
ticket: string | undefined;
|
|
45
|
+
/** Refreshes the ticket when a heartbeat is due. Never throws. */
|
|
46
|
+
heartbeat: () => Promise<void>;
|
|
47
|
+
/** Leaves the queue. Always safe to call. */
|
|
48
|
+
deregister: () => Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Takes a ticket in the lock's waiter queue.
|
|
52
|
+
*
|
|
53
|
+
* Never fails the acquisition: a registry that cannot be written yields a
|
|
54
|
+
* registration without a ticket, and the waiter falls back to contending
|
|
55
|
+
* for the lock without queue order, which is how every waiter behaved
|
|
56
|
+
* before tickets decided the grant.
|
|
57
|
+
*
|
|
58
|
+
* @param lockPath - The contended lock directory
|
|
59
|
+
* @param startedAt - When this waiter started waiting (its arrival time)
|
|
60
|
+
*/
|
|
61
|
+
export declare function registerWaiter(lockPath: string, startedAt: number): Promise<WaiterRegistration>;
|
|
62
|
+
/**
|
|
63
|
+
* Renders the queue position for a wait line, with the ordering rule, so the
|
|
64
|
+
* reader knows the count is a promise and not a snapshot. Empty when nothing
|
|
65
|
+
* is queued.
|
|
66
|
+
*/
|
|
67
|
+
export declare function formatLockQueuePosition(queue: LockQueueState | undefined): string;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Waiter tickets for `withFileLock`: who is queued for a contended lock, in
|
|
4
|
+
* what order, and whose turn it is.
|
|
5
|
+
*
|
|
6
|
+
* The queue started as a display: waiters advertised themselves so the wait
|
|
7
|
+
* line could say "You are next", but the grant itself was a free-for-all of
|
|
8
|
+
* `mkdir` polls. A waiter whose poll interval had backed off to 2 s lost
|
|
9
|
+
* every free moment to later arrivals polling at 100 ms, so a run told it
|
|
10
|
+
* was next for twenty minutes watched three later runs take the lock in
|
|
11
|
+
* turn. The tickets now decide the grant: only the head of the queue may
|
|
12
|
+
* attempt the lock, so the position printed is the position honoured.
|
|
13
|
+
*
|
|
14
|
+
* A ticket is a file in `<lock>.waiters/` named
|
|
15
|
+
* `<startedAtMs>-<pid>-<uuid>`. Arrival order is the wait start, with the
|
|
16
|
+
* whole name as a tie-break so every reader sorts the queue identically.
|
|
17
|
+
* Because a ticket now blocks everyone behind it, a ticket whose owner is
|
|
18
|
+
* gone must not hold the queue: a dead PID (or a reused one, where the
|
|
19
|
+
* start-tick check can tell) drops out and its ticket is removed, and a
|
|
20
|
+
* ticket its owner has stopped refreshing drops out without being removed,
|
|
21
|
+
* which covers a stopped (^Z) waiter and a reused PID on platforms with no
|
|
22
|
+
* start tick.
|
|
23
|
+
*/
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { toError } from '../utils/errors.js';
|
|
28
|
+
import { ensureDir } from '../utils/fs.js';
|
|
29
|
+
import { verbose } from '../utils/logger.js';
|
|
30
|
+
import { isLockOwnerAlive, readProcessStartTick } from './file-lock-owner.js';
|
|
31
|
+
/**
|
|
32
|
+
* How often a waiter refreshes its ticket. Every wait loop iteration
|
|
33
|
+
* refreshes once this much time has passed, and no caller sleeps longer
|
|
34
|
+
* than a few seconds between iterations.
|
|
35
|
+
*/
|
|
36
|
+
const WAITER_HEARTBEAT_MS = 5_000;
|
|
37
|
+
/**
|
|
38
|
+
* A ticket not refreshed for this long is skipped when the queue is read.
|
|
39
|
+
* Six heartbeats of slack: long enough that a loaded host never skips a
|
|
40
|
+
* waiter that is merely slow, short enough that a stopped waiter does not
|
|
41
|
+
* hold the head of the queue for more than half a minute.
|
|
42
|
+
*/
|
|
43
|
+
export const WAITER_STALE_MS = 30_000;
|
|
44
|
+
/** Prefix of the ticket line carrying the waiter's process start tick. */
|
|
45
|
+
const START_TICK_PREFIX = 'start-tick=';
|
|
46
|
+
/** Sibling directory holding the waiter tickets of a lock. */
|
|
47
|
+
export function lockWaitersDir(lockPath) {
|
|
48
|
+
return `${lockPath}.waiters`;
|
|
49
|
+
}
|
|
50
|
+
/** Parses a ticket name: `<startedAtMs>-<pid>-<uuid>`. */
|
|
51
|
+
function parseTicketName(name) {
|
|
52
|
+
const match = /^(\d+)-(\d+)-/.exec(name);
|
|
53
|
+
const startedAt = match?.[1];
|
|
54
|
+
const pid = match?.[2];
|
|
55
|
+
if (startedAt === undefined || pid === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
return { name, startedAt: Number.parseInt(startedAt, 10), pid: Number.parseInt(pid, 10) };
|
|
58
|
+
}
|
|
59
|
+
/** Arrival order: wait start first, then the name, so every reader agrees. */
|
|
60
|
+
function compareTickets(a, b) {
|
|
61
|
+
if (a.startedAt !== b.startedAt)
|
|
62
|
+
return a.startedAt - b.startedAt;
|
|
63
|
+
if (a.name === b.name)
|
|
64
|
+
return 0;
|
|
65
|
+
return a.name < b.name ? -1 : 1;
|
|
66
|
+
}
|
|
67
|
+
/** Reads the start tick a ticket recorded, when its writer could record one. */
|
|
68
|
+
async function readTicketStartTick(path) {
|
|
69
|
+
const content = await readFile(path, 'utf-8');
|
|
70
|
+
const line = content.split('\n').find((entry) => entry.startsWith(START_TICK_PREFIX));
|
|
71
|
+
if (line === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const tick = Number.parseInt(line.slice(START_TICK_PREFIX.length), 10);
|
|
74
|
+
return Number.isFinite(tick) ? tick : undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a ticket still holds a place in the queue. A ticket whose owner is
|
|
78
|
+
* provably gone is removed so the directory does not accumulate them; one
|
|
79
|
+
* that has merely stopped being refreshed is skipped but left alone, since
|
|
80
|
+
* its owner may resume and refresh it.
|
|
81
|
+
*/
|
|
82
|
+
async function isTicketLive(dir, ticket, now) {
|
|
83
|
+
const path = join(dir, ticket.name);
|
|
84
|
+
let startTick;
|
|
85
|
+
let mtimeMs;
|
|
86
|
+
try {
|
|
87
|
+
startTick = await readTicketStartTick(path);
|
|
88
|
+
mtimeMs = (await stat(path)).mtimeMs;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Deregistered between the listing and this read.
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!isLockOwnerAlive({ pid: ticket.pid, startTick })) {
|
|
95
|
+
await rm(path, { force: true }).catch((error) => {
|
|
96
|
+
verbose(`Could not remove dead lock waiter ticket ${path}: ${toError(error).message}`);
|
|
97
|
+
});
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return now - mtimeMs <= WAITER_STALE_MS;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Reads the live waiter queue around `lockPath`.
|
|
104
|
+
*
|
|
105
|
+
* Advisory and fail-open: an unreadable registry reports an empty queue
|
|
106
|
+
* rather than failing whatever asked. The caller's own ticket always counts
|
|
107
|
+
* as live, since this process knows it is waiting.
|
|
108
|
+
*
|
|
109
|
+
* @param lockPath - The lock directory whose queue to inspect
|
|
110
|
+
* @param self - This waiter, to compute `ahead`
|
|
111
|
+
*/
|
|
112
|
+
export async function readLockQueue(lockPath, self) {
|
|
113
|
+
const dir = lockWaitersDir(lockPath);
|
|
114
|
+
let names;
|
|
115
|
+
try {
|
|
116
|
+
names = await readdir(dir);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { depth: 0, ahead: 0 };
|
|
120
|
+
}
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const tickets = names
|
|
123
|
+
.map(parseTicketName)
|
|
124
|
+
.filter((ticket) => ticket !== undefined);
|
|
125
|
+
const liveFlags = await Promise.all(tickets.map((ticket) => ticket.name === self?.ticket ? Promise.resolve(true) : isTicketLive(dir, ticket, now)));
|
|
126
|
+
const live = tickets.filter((_, index) => liveFlags[index] === true);
|
|
127
|
+
if (self === undefined)
|
|
128
|
+
return { depth: live.length, ahead: 0 };
|
|
129
|
+
if (self.ticket === undefined) {
|
|
130
|
+
return {
|
|
131
|
+
depth: live.length,
|
|
132
|
+
ahead: live.filter((ticket) => ticket.startedAt < self.startedAt).length,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const own = parseTicketName(self.ticket);
|
|
136
|
+
// Our ticket can be missing from the listing only if something removed it;
|
|
137
|
+
// we still count ourselves, and the next heartbeat recreates it.
|
|
138
|
+
const listed = live.some((ticket) => ticket.name === self.ticket);
|
|
139
|
+
return {
|
|
140
|
+
depth: live.length + (listed ? 0 : 1),
|
|
141
|
+
ahead: own === undefined
|
|
142
|
+
? 0
|
|
143
|
+
: live.filter((ticket) => ticket.name !== own.name && compareTickets(ticket, own) < 0)
|
|
144
|
+
.length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Takes a ticket in the lock's waiter queue.
|
|
149
|
+
*
|
|
150
|
+
* Never fails the acquisition: a registry that cannot be written yields a
|
|
151
|
+
* registration without a ticket, and the waiter falls back to contending
|
|
152
|
+
* for the lock without queue order, which is how every waiter behaved
|
|
153
|
+
* before tickets decided the grant.
|
|
154
|
+
*
|
|
155
|
+
* @param lockPath - The contended lock directory
|
|
156
|
+
* @param startedAt - When this waiter started waiting (its arrival time)
|
|
157
|
+
*/
|
|
158
|
+
export async function registerWaiter(lockPath, startedAt) {
|
|
159
|
+
const dir = lockWaitersDir(lockPath);
|
|
160
|
+
const ticket = `${startedAt}-${process.pid}-${randomUUID()}`;
|
|
161
|
+
const file = join(dir, ticket);
|
|
162
|
+
const startTick = readProcessStartTick(process.pid);
|
|
163
|
+
const content = `${process.pid}\n${startTick !== undefined ? `${START_TICK_PREFIX}${startTick}\n` : ''}`;
|
|
164
|
+
try {
|
|
165
|
+
await ensureDir(dir);
|
|
166
|
+
await writeFile(file, content, 'utf-8');
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
verbose(`Could not register lock waiter for ${lockPath}: ${toError(error).message}`);
|
|
170
|
+
return {
|
|
171
|
+
ticket: undefined,
|
|
172
|
+
heartbeat: async () => {
|
|
173
|
+
/* nothing to refresh */
|
|
174
|
+
},
|
|
175
|
+
deregister: async () => {
|
|
176
|
+
/* nothing to remove */
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
let lastBeatAt = Date.now();
|
|
181
|
+
return {
|
|
182
|
+
ticket,
|
|
183
|
+
heartbeat: async () => {
|
|
184
|
+
if (Date.now() - lastBeatAt < WAITER_HEARTBEAT_MS)
|
|
185
|
+
return;
|
|
186
|
+
lastBeatAt = Date.now();
|
|
187
|
+
try {
|
|
188
|
+
// A rewrite, not a utimes: it also recreates a ticket that was
|
|
189
|
+
// removed while this process was stopped long enough to look dead.
|
|
190
|
+
await ensureDir(dir);
|
|
191
|
+
await writeFile(file, content, 'utf-8');
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
verbose(`Could not refresh lock waiter ticket ${file}: ${toError(error).message}`);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
deregister: async () => {
|
|
198
|
+
try {
|
|
199
|
+
await rm(file, { force: true });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
// A leftover ticket of a process about to exit drops out on its own
|
|
203
|
+
// (dead PID), so this never needs to fail the caller.
|
|
204
|
+
verbose(`Could not remove lock waiter ticket ${file}: ${toError(error).message}`);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Renders the queue position for a wait line, with the ordering rule, so the
|
|
211
|
+
* reader knows the count is a promise and not a snapshot. Empty when nothing
|
|
212
|
+
* is queued.
|
|
213
|
+
*/
|
|
214
|
+
export function formatLockQueuePosition(queue) {
|
|
215
|
+
if (queue === undefined || queue.depth === 0)
|
|
216
|
+
return '';
|
|
217
|
+
if (queue.ahead === 0) {
|
|
218
|
+
return ` You are next in a queue of ${queue.depth} (granted in arrival order).`;
|
|
219
|
+
}
|
|
220
|
+
return ` ${queue.ahead} ahead of you (queue of ${queue.depth}, granted in arrival order).`;
|
|
221
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { LOCK_PID_FILE } from './file-lock-owner.js';
|
|
2
|
+
import { type LockQueueState } from './file-lock-queue.js';
|
|
2
3
|
export { LOCK_PID_FILE };
|
|
4
|
+
export { formatLockQueuePosition, type LockQueueState } from './file-lock-queue.js';
|
|
3
5
|
/**
|
|
4
6
|
* Snapshot of the current lock holder handed to {@link FileLockOptions.onWaitProgress}.
|
|
5
7
|
* `metadata` carries the pid file's human-diagnostic lines (line 3 onward,
|
|
@@ -11,20 +13,6 @@ export interface LockHolder {
|
|
|
11
13
|
alive: boolean;
|
|
12
14
|
metadata: string[];
|
|
13
15
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Live queue state around a contended lock.
|
|
16
|
-
*
|
|
17
|
-
* The lock itself behaves correctly under several concurrent sessions. The
|
|
18
|
-
* gap this closes is visibility. Waiters advertise themselves in a sibling
|
|
19
|
-
* directory so both the wait output and `fireforge status --lock` can say
|
|
20
|
-
* how many are queued and how many are ahead of you.
|
|
21
|
-
*/
|
|
22
|
-
export interface LockQueueState {
|
|
23
|
-
/** Live waiters currently queued for the lock, including this process. */
|
|
24
|
-
depth: number;
|
|
25
|
-
/** Live waiters that started waiting before this process. */
|
|
26
|
-
ahead: number;
|
|
27
|
-
}
|
|
28
16
|
export interface FileLockOptions {
|
|
29
17
|
timeoutMs?: number;
|
|
30
18
|
pollMs?: number;
|
|
@@ -32,6 +20,8 @@ export interface FileLockOptions {
|
|
|
32
20
|
* When set, the poll interval doubles after every contended poll (starting
|
|
33
21
|
* from `pollMs`) up to this cap. When absent, polling stays at the fixed
|
|
34
22
|
* `pollMs` interval, with zero behavior change for existing callers.
|
|
23
|
+
* Keep it far below `WAITER_STALE_MS` (30 s): a waiter refreshes its queue
|
|
24
|
+
* ticket between polls, and a ticket left unrefreshed that long is skipped.
|
|
35
25
|
*/
|
|
36
26
|
pollMaxMs?: number;
|
|
37
27
|
staleMs?: number;
|
|
@@ -97,17 +87,6 @@ export interface FileLockOptions {
|
|
|
97
87
|
}
|
|
98
88
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
99
89
|
export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
|
|
100
|
-
/**
|
|
101
|
-
* Reads the live waiter queue around `lockPath`, dropping entries whose
|
|
102
|
-
* process is gone (a killed waiter must not inflate the depth forever).
|
|
103
|
-
*
|
|
104
|
-
* Advisory and fail-open: an unreadable registry reports an empty queue
|
|
105
|
-
* rather than failing whatever asked.
|
|
106
|
-
*
|
|
107
|
-
* @param lockPath - The lock directory whose queue to inspect
|
|
108
|
-
* @param selfStartedAt - This process's wait start, to compute `ahead`
|
|
109
|
-
*/
|
|
110
|
-
export declare function readLockQueue(lockPath: string, selfStartedAt?: number): Promise<LockQueueState>;
|
|
111
90
|
/**
|
|
112
91
|
* Snapshot of a lock for `fireforge status --lock`: who holds
|
|
113
92
|
* it, for how long, and how deep the queue behind it is.
|