@hominis/fireforge 0.49.2 → 0.51.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 +87 -1
- 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 +41 -0
- package/dist/src/commands/check-report.js +91 -0
- package/dist/src/commands/check-run.d.ts +33 -0
- package/dist/src/commands/check-run.js +112 -0
- package/dist/src/commands/check-steps.d.ts +75 -0
- package/dist/src/commands/check-steps.js +200 -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/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- 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-placement-gate.js +8 -0
- 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.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 +98 -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 +151 -113
- package/dist/src/commands/furnace/validation-output.js +14 -3
- package/dist/src/commands/lint-per-patch.js +36 -61
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.d.ts +8 -4
- package/dist/src/commands/lint-report.js +20 -5
- package/dist/src/commands/lint.js +15 -34
- package/dist/src/commands/manifest.js +4 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -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 +24 -104
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +10 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
- package/dist/src/commands/patch/lint-ignore.js +78 -31
- package/dist/src/commands/patch/move-files-create.js +34 -18
- package/dist/src/commands/patch/move-files-into.js +15 -7
- package/dist/src/commands/patch/move-files.js +6 -2
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- 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 +20 -1
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.d.ts +34 -0
- package/dist/src/commands/patch/retire.js +133 -0
- package/dist/src/commands/patch/split-plan.d.ts +17 -8
- package/dist/src/commands/patch/split-plan.js +60 -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 +62 -17
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- 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-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +54 -139
- 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 +50 -2
- package/dist/src/commands/re-export-adjacent.js +160 -40
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- 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-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- 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 +74 -6
- 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-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +56 -17
- package/dist/src/commands/re-export-removed.d.ts +13 -2
- package/dist/src/commands/re-export-removed.js +57 -12
- 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 +19 -9
- package/dist/src/commands/re-export-scan.js +60 -24
- package/dist/src/commands/re-export-single.js +122 -46
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- 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 +71 -57
- 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 +22 -0
- package/dist/src/commands/test-build-furnace.js +54 -0
- package/dist/src/commands/test-build-phase.js +64 -41
- package/dist/src/commands/test-checkjs.js +10 -2
- package/dist/src/commands/test-diagnose.js +7 -0
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -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 +52 -0
- package/dist/src/commands/test-orchestrate.js +240 -0
- package/dist/src/commands/test-perf-scope.d.ts +50 -0
- package/dist/src/commands/test-perf-scope.js +138 -0
- package/dist/src/commands/test-prelock.d.ts +51 -0
- package/dist/src/commands/test-prelock.js +273 -0
- package/dist/src/commands/test-register.js +69 -10
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +74 -99
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +60 -28
- package/dist/src/commands/test-verdict.d.ts +11 -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 +120 -124
- package/dist/src/commands/token-lint.d.ts +14 -0
- package/dist/src/commands/token-lint.js +151 -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 +67 -36
- 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 +13 -0
- package/dist/src/core/build-prepare.js +42 -10
- 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 +10 -0
- package/dist/src/core/config-validate-check.d.ts +8 -0
- package/dist/src/core/config-validate-check.js +219 -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 +21 -0
- package/dist/src/core/config-validate-token-scales.d.ts +9 -0
- package/dist/src/core/config-validate-token-scales.js +92 -0
- package/dist/src/core/config-validate.js +20 -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 +108 -0
- package/dist/src/core/coverage-records.js +384 -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 +16 -1
- package/dist/src/core/engine-session-lock.js +38 -10
- 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-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +13 -25
- package/dist/src/core/file-lock.js +163 -140
- 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 +22 -12
- 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 +16 -0
- package/dist/src/core/furnace-config-validate-exceptions.js +102 -0
- package/dist/src/core/furnace-config-validate.js +14 -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-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- 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 +8 -4
- package/dist/src/core/furnace-validate-accessibility.js +71 -28
- package/dist/src/core/furnace-validate-exceptions.d.ts +48 -0
- package/dist/src/core/furnace-validate-exceptions.js +80 -0
- package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
- package/dist/src/core/furnace-validate-icon-button.js +78 -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 +164 -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/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +32 -10
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +51 -0
- package/dist/src/core/negative-control-diff.js +179 -0
- package/dist/src/core/negative-control.d.ts +150 -0
- package/dist/src/core/negative-control.js +399 -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/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +6 -0
- package/dist/src/core/patch-export-update.js +13 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +22 -16
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +39 -0
- package/dist/src/core/patch-lint-checkjs-any-import.js +63 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +20 -13
- 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.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +17 -4
- package/dist/src/core/patch-lint-css.d.ts +8 -2
- package/dist/src/core/patch-lint-css.js +24 -14
- 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 +23 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +36 -11
- package/dist/src/core/patch-lint-forward-registration.js +10 -0
- package/dist/src/core/patch-lint-ignore.d.ts +196 -0
- package/dist/src/core/patch-lint-ignore.js +429 -0
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -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 +331 -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 +57 -0
- package/dist/src/core/patch-lint-token-scale.js +286 -0
- package/dist/src/core/patch-lint.d.ts +11 -83
- package/dist/src/core/patch-lint.js +66 -288
- 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 +27 -9
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +61 -3
- package/dist/src/core/patch-order-legal.d.ts +26 -0
- package/dist/src/core/patch-order-legal.js +88 -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 +22 -5
- package/dist/src/core/patch-policy.js +26 -12
- package/dist/src/core/patch-retired.d.ts +57 -0
- package/dist/src/core/patch-retired.js +92 -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 +26 -0
- package/dist/src/core/run-log.js +106 -4
- 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 +19 -1
- package/dist/src/core/test-harness-crash.js +8 -9
- package/dist/src/core/test-harness-signature.d.ts +33 -0
- package/dist/src/core/test-harness-signature.js +23 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +90 -0
- package/dist/src/core/test-run-completeness.js +184 -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/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +53 -0
- package/dist/src/core/token-lint.js +131 -0
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +5 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -2
- package/dist/src/core/typecheck-shim.js +205 -4
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +14 -3
- package/dist/src/index.js +6 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +212 -24
- package/dist/src/types/commands/patches.d.ts +118 -1
- package/dist/src/types/config.d.ts +264 -0
- package/dist/src/types/config.js +9 -0
- package/dist/src/types/furnace.d.ts +63 -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 +36 -7
- package/dist/src/utils/options.js +94 -8
- 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 +369 -11
- package/docs/exit-codes.md +18 -1
- package/docs/furnace.md +139 -3
- package/docs/lifecycle-invariants.md +65 -30
- package/docs/machine-output.md +120 -12
- package/docs/patch-workflow.md +458 -22
- package/docs/run-logs.md +20 -1
- package/docs/testing.md +346 -21
- package/package.json +3 -3
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Shared holds on a FireForge directory lock.
|
|
4
|
+
*
|
|
5
|
+
* Every lock used to be exclusive, so two build-less `fireforge test` runs
|
|
6
|
+
* on one objdir, each with its own profile and (under auto ports) its own
|
|
7
|
+
* ports, still waited for each other: the gate's browser tiers ran strictly
|
|
8
|
+
* serially on a 14-core host. A shared hold lets such runs overlap while
|
|
9
|
+
* anything that writes (a build, a re-export, a test run that builds)
|
|
10
|
+
* still holds the lock alone.
|
|
11
|
+
*
|
|
12
|
+
* The scheme reuses the exclusive lock as a turnstile, so arrival order and
|
|
13
|
+
* stale-holder reaping come from the machinery that already exists:
|
|
14
|
+
*
|
|
15
|
+
* - A shared holder takes the exclusive lock in the ordinary way, records
|
|
16
|
+
* itself under `<lock>.shared/<uuid>/` (an owner record like the lock's
|
|
17
|
+
* own), and releases the exclusive lock at once.
|
|
18
|
+
* - An exclusive holder takes the exclusive lock, then waits until no live
|
|
19
|
+
* shared holder is left. While it waits it already holds the turnstile,
|
|
20
|
+
* so no new shared holder can slip in ahead of it: a writer queued
|
|
21
|
+
* behind readers is served once they finish, never starved by newcomers.
|
|
22
|
+
*
|
|
23
|
+
* A shared holder whose process died is reaped by the same PID-first
|
|
24
|
+
* liveness check the exclusive lock uses.
|
|
25
|
+
*/
|
|
26
|
+
import { randomUUID } from 'node:crypto';
|
|
27
|
+
import { mkdir, readdir, rm } from 'node:fs/promises';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
30
|
+
import { verbose } from '../utils/logger.js';
|
|
31
|
+
import { isLockOwnerAlive, readLockOwner, writeLockOwner } from './file-lock-owner.js';
|
|
32
|
+
/** The directory holding a lock's shared-holder records. */
|
|
33
|
+
export function sharedHoldersDir(lockPath) {
|
|
34
|
+
return `${lockPath}.shared`;
|
|
35
|
+
}
|
|
36
|
+
/** Holder directories this process registered, for the signal sweep. */
|
|
37
|
+
const heldShares = new Set();
|
|
38
|
+
/**
|
|
39
|
+
* Counts the live shared holders of a lock, reaping any whose process is
|
|
40
|
+
* gone. A missing directory means none.
|
|
41
|
+
*
|
|
42
|
+
* @param lockPath - The lock directory
|
|
43
|
+
* @param options - `reap: false` only counts (status reads)
|
|
44
|
+
*/
|
|
45
|
+
export async function countLiveSharedHolders(lockPath, options = {}) {
|
|
46
|
+
const dir = sharedHoldersDir(lockPath);
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(dir);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
53
|
+
return 0;
|
|
54
|
+
if (options.reap === false)
|
|
55
|
+
return 0;
|
|
56
|
+
throw toError(error);
|
|
57
|
+
}
|
|
58
|
+
let live = 0;
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const holder = join(dir, entry);
|
|
61
|
+
const owner = await readLockOwner(holder);
|
|
62
|
+
if (owner.present && isLockOwnerAlive(owner)) {
|
|
63
|
+
live += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// A record still being written has no owner yet; only an owner that
|
|
67
|
+
// names a dead process is reaped, so a racing registration survives.
|
|
68
|
+
if (owner.present && options.reap !== false) {
|
|
69
|
+
verbose(`Reaping shared hold ${holder}: PID ${owner.pid} is gone.`);
|
|
70
|
+
await rm(holder, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return live;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Records a shared hold. Call while holding the lock's exclusive turnstile.
|
|
77
|
+
*
|
|
78
|
+
* @param lockPath - The lock directory
|
|
79
|
+
* @param metadata - Owner metadata lines (`command=…`)
|
|
80
|
+
*/
|
|
81
|
+
export async function registerSharedHold(lockPath, metadata) {
|
|
82
|
+
const holder = join(sharedHoldersDir(lockPath), randomUUID());
|
|
83
|
+
await mkdir(holder, { recursive: true });
|
|
84
|
+
await writeLockOwner(holder, randomUUID(), [...(metadata ?? []), 'mode=shared']);
|
|
85
|
+
heldShares.add(holder);
|
|
86
|
+
return {
|
|
87
|
+
async release() {
|
|
88
|
+
heldShares.delete(holder);
|
|
89
|
+
try {
|
|
90
|
+
await rm(holder, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
verbose(`Could not remove shared hold ${holder}: ${toError(error).message}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Removes every shared hold this process registered, for the signal path.
|
|
100
|
+
* Never throws.
|
|
101
|
+
*
|
|
102
|
+
* @returns The holder paths removed
|
|
103
|
+
*/
|
|
104
|
+
export async function releaseSharedHoldsForSignal() {
|
|
105
|
+
const removed = [];
|
|
106
|
+
for (const holder of [...heldShares]) {
|
|
107
|
+
heldShares.delete(holder);
|
|
108
|
+
try {
|
|
109
|
+
await rm(holder, { recursive: true, force: true });
|
|
110
|
+
removed.push(holder);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Shutdown path: nothing to do.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return removed;
|
|
117
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { LOCK_PID_FILE } from './file-lock-owner.js';
|
|
2
|
+
import { type LockQueueState } from './file-lock-queue.js';
|
|
3
|
+
import { type LockMode } from './file-lock-shared.js';
|
|
2
4
|
export { LOCK_PID_FILE };
|
|
5
|
+
export { formatLockQueuePosition, type LockQueueState } from './file-lock-queue.js';
|
|
6
|
+
export type { LockMode } from './file-lock-shared.js';
|
|
3
7
|
/**
|
|
4
8
|
* Snapshot of the current lock holder handed to {@link FileLockOptions.onWaitProgress}.
|
|
5
9
|
* `metadata` carries the pid file's human-diagnostic lines (line 3 onward,
|
|
@@ -11,20 +15,6 @@ export interface LockHolder {
|
|
|
11
15
|
alive: boolean;
|
|
12
16
|
metadata: string[];
|
|
13
17
|
}
|
|
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
18
|
export interface FileLockOptions {
|
|
29
19
|
timeoutMs?: number;
|
|
30
20
|
pollMs?: number;
|
|
@@ -32,6 +22,8 @@ export interface FileLockOptions {
|
|
|
32
22
|
* When set, the poll interval doubles after every contended poll (starting
|
|
33
23
|
* from `pollMs`) up to this cap. When absent, polling stays at the fixed
|
|
34
24
|
* `pollMs` interval, with zero behavior change for existing callers.
|
|
25
|
+
* Keep it far below `WAITER_STALE_MS` (30 s): a waiter refreshes its queue
|
|
26
|
+
* ticket between polls, and a ticket left unrefreshed that long is skipped.
|
|
35
27
|
*/
|
|
36
28
|
pollMaxMs?: number;
|
|
37
29
|
staleMs?: number;
|
|
@@ -94,20 +86,14 @@ export interface FileLockOptions {
|
|
|
94
86
|
onStaleLockMessage?: (ageMs: number) => string | undefined;
|
|
95
87
|
/** Extra owner-file lines for human diagnostics. Lock mechanics ignore them. */
|
|
96
88
|
ownerMetadata?: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* `exclusive` (default) or `shared`. Shared holders overlap each other and
|
|
91
|
+
* exclude exclusive ones; see `file-lock-shared.ts`.
|
|
92
|
+
*/
|
|
93
|
+
mode?: LockMode | undefined;
|
|
97
94
|
}
|
|
98
95
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
99
96
|
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
97
|
/**
|
|
112
98
|
* Snapshot of a lock for `fireforge status --lock`: who holds
|
|
113
99
|
* it, for how long, and how deep the queue behind it is.
|
|
@@ -118,6 +104,8 @@ export interface LockStatusSnapshot {
|
|
|
118
104
|
/** Milliseconds since the lock directory was created, when held. */
|
|
119
105
|
heldForMs?: number | undefined;
|
|
120
106
|
queueDepth: number;
|
|
107
|
+
/** Live shared holders (build-less test runs under a shared lock). */
|
|
108
|
+
sharedHolders?: number | undefined;
|
|
121
109
|
}
|
|
122
110
|
/**
|
|
123
111
|
* Inspects a lock without acquiring it. Read-only and fail-open: a missing
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdir,
|
|
4
|
-
import { dirname
|
|
3
|
+
import { mkdir, rename, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
5
|
import { LockContentionError } from '../errors/base.js';
|
|
6
|
-
import { getNodeErrorCode,
|
|
6
|
+
import { getNodeErrorCode, toError } from '../utils/errors.js';
|
|
7
7
|
import { ensureDir } from '../utils/fs.js';
|
|
8
8
|
import { verbose, warn } from '../utils/logger.js';
|
|
9
9
|
import { sleep } from '../utils/sleep.js';
|
|
10
10
|
import { isLockOwnerAlive, LOCK_PID_FILE, readLockOwner, writeLockOwner, } from './file-lock-owner.js';
|
|
11
|
+
import { readLockQueue, registerWaiter, } from './file-lock-queue.js';
|
|
12
|
+
import { countLiveSharedHolders, registerSharedHold, releaseSharedHoldsForSignal, } from './file-lock-shared.js';
|
|
11
13
|
export { LOCK_PID_FILE };
|
|
14
|
+
export { formatLockQueuePosition } from './file-lock-queue.js';
|
|
12
15
|
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
13
16
|
const DEFAULT_LOCK_POLL_MS = 50;
|
|
14
17
|
const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
|
|
@@ -30,52 +33,14 @@ const WAIT_PROBE_INTERVAL_MS = 5_000;
|
|
|
30
33
|
export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
|
|
31
34
|
return `${filePath}${suffix}`;
|
|
32
35
|
}
|
|
33
|
-
/** Sibling directory in which waiters advertise themselves. */
|
|
34
|
-
function lockWaitersDir(lockPath) {
|
|
35
|
-
return `${lockPath}.waiters`;
|
|
36
|
-
}
|
|
37
|
-
/** Parsed waiter-file name: `<startedAtMs>-<pid>-<uuid>`. */
|
|
38
|
-
function parseWaiterFileName(name) {
|
|
39
|
-
const match = /^(\d+)-(\d+)-/.exec(name);
|
|
40
|
-
const startedAt = match?.[1];
|
|
41
|
-
const pid = match?.[2];
|
|
42
|
-
if (startedAt === undefined || pid === undefined)
|
|
43
|
-
return undefined;
|
|
44
|
-
return { startedAt: Number.parseInt(startedAt, 10), pid: Number.parseInt(pid, 10) };
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Reads the live waiter queue around `lockPath`, dropping entries whose
|
|
48
|
-
* process is gone (a killed waiter must not inflate the depth forever).
|
|
49
|
-
*
|
|
50
|
-
* Advisory and fail-open: an unreadable registry reports an empty queue
|
|
51
|
-
* rather than failing whatever asked.
|
|
52
|
-
*
|
|
53
|
-
* @param lockPath - The lock directory whose queue to inspect
|
|
54
|
-
* @param selfStartedAt - This process's wait start, to compute `ahead`
|
|
55
|
-
*/
|
|
56
|
-
export async function readLockQueue(lockPath, selfStartedAt) {
|
|
57
|
-
let names;
|
|
58
|
-
try {
|
|
59
|
-
names = await readdir(lockWaitersDir(lockPath));
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return { depth: 0, ahead: 0 };
|
|
63
|
-
}
|
|
64
|
-
const live = names
|
|
65
|
-
.map(parseWaiterFileName)
|
|
66
|
-
.filter((entry) => entry !== undefined)
|
|
67
|
-
.filter((entry) => isProcessAlive(entry.pid));
|
|
68
|
-
const ahead = selfStartedAt === undefined
|
|
69
|
-
? 0
|
|
70
|
-
: live.filter((entry) => entry.startedAt < selfStartedAt).length;
|
|
71
|
-
return { depth: live.length, ahead };
|
|
72
|
-
}
|
|
73
36
|
/**
|
|
74
37
|
* Inspects a lock without acquiring it. Read-only and fail-open: a missing
|
|
75
38
|
* lock is reported as free, never as an error.
|
|
76
39
|
*/
|
|
77
40
|
export async function readLockStatus(lockPath) {
|
|
78
41
|
const queue = await readLockQueue(lockPath);
|
|
42
|
+
const shared = await countLiveSharedHolders(lockPath, { reap: false });
|
|
43
|
+
const sharedPart = shared > 0 ? { sharedHolders: shared } : {};
|
|
79
44
|
let heldForMs;
|
|
80
45
|
try {
|
|
81
46
|
// Clamped: filesystem mtime resolution can round a just-created lock
|
|
@@ -84,36 +49,13 @@ export async function readLockStatus(lockPath) {
|
|
|
84
49
|
heldForMs = Math.max(0, Date.now() - (await stat(lockPath)).mtimeMs);
|
|
85
50
|
}
|
|
86
51
|
catch {
|
|
87
|
-
return { held: false, queueDepth: queue.depth };
|
|
52
|
+
return { held: false, queueDepth: queue.depth, ...sharedPart };
|
|
88
53
|
}
|
|
89
54
|
const owner = await readLockOwner(lockPath);
|
|
90
55
|
const holder = owner.present
|
|
91
56
|
? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
|
|
92
57
|
: undefined;
|
|
93
|
-
return { held: true, holder, heldForMs, queueDepth: queue.depth };
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Registers this process in the lock's waiter queue. Returns a
|
|
97
|
-
* deregistration function that is always safe to call (including when
|
|
98
|
-
* registration itself failed).
|
|
99
|
-
*/
|
|
100
|
-
async function registerWaiter(lockPath, startedAt) {
|
|
101
|
-
const dir = lockWaitersDir(lockPath);
|
|
102
|
-
const file = join(dir, `${startedAt}-${process.pid}-${randomUUID()}`);
|
|
103
|
-
try {
|
|
104
|
-
await ensureDir(dir);
|
|
105
|
-
await writeFile(file, `${process.pid}\n`, 'utf-8');
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
// Advisory only. Never fail a lock acquisition over the queue display.
|
|
109
|
-
verbose(`Could not register lock waiter for ${lockPath}: ${toError(error).message}`);
|
|
110
|
-
return async () => {
|
|
111
|
-
/* nothing to remove */
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return async () => {
|
|
115
|
-
await rm(file, { force: true });
|
|
116
|
-
};
|
|
58
|
+
return { held: true, holder, heldForMs, queueDepth: queue.depth, ...sharedPart };
|
|
117
59
|
}
|
|
118
60
|
/**
|
|
119
61
|
* Reads the contended lock's owner and queue in one go. One probe per cycle
|
|
@@ -122,12 +64,12 @@ async function registerWaiter(lockPath, startedAt) {
|
|
|
122
64
|
* degrades to `holder: undefined`, since a diagnostic must never break the
|
|
123
65
|
* wait loop.
|
|
124
66
|
*/
|
|
125
|
-
async function probeWaitState(lockPath,
|
|
67
|
+
async function probeWaitState(lockPath, self) {
|
|
126
68
|
const owner = await readLockOwner(lockPath);
|
|
127
69
|
const holder = owner.present
|
|
128
70
|
? { pid: owner.pid, alive: isLockOwnerAlive(owner), metadata: owner.metadata }
|
|
129
71
|
: undefined;
|
|
130
|
-
return { holder, queue: await readLockQueue(lockPath,
|
|
72
|
+
return { holder, queue: await readLockQueue(lockPath, self) };
|
|
131
73
|
}
|
|
132
74
|
/**
|
|
133
75
|
* Reaps a lock directory believed to be stale, safely against concurrent
|
|
@@ -345,16 +287,24 @@ function formatQueuePositionReached(queue) {
|
|
|
345
287
|
* caller asked for it. Split out of the wait loop so the loop stays a
|
|
346
288
|
* readable state machine.
|
|
347
289
|
*/
|
|
348
|
-
async function runWaitProbe(lockPath,
|
|
349
|
-
const { holder, queue } = await probeWaitState(lockPath,
|
|
290
|
+
async function runWaitProbe(lockPath, self, timeoutMs, options) {
|
|
291
|
+
const { holder, queue } = await probeWaitState(lockPath, self);
|
|
350
292
|
options.onWaitProgress?.({
|
|
351
|
-
waitedMs: Date.now() - startedAt,
|
|
293
|
+
waitedMs: Date.now() - self.startedAt,
|
|
352
294
|
timeoutMs,
|
|
353
295
|
holder,
|
|
354
296
|
queue,
|
|
355
297
|
});
|
|
356
298
|
return { queue };
|
|
357
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Whether it is this waiter's turn to contend for the lock: no live waiter
|
|
302
|
+
* arrived before it. Ties and dead or stalled tickets are settled by
|
|
303
|
+
* {@link readLockQueue}, so every waiter agrees on who is at the head.
|
|
304
|
+
*/
|
|
305
|
+
async function isWaiterTurn(lockPath, startedAt, waiter) {
|
|
306
|
+
return (await readLockQueue(lockPath, { startedAt, ticket: waiter.ticket })).ahead === 0;
|
|
307
|
+
}
|
|
358
308
|
/**
|
|
359
309
|
* Lock directories this process currently holds.
|
|
360
310
|
*
|
|
@@ -387,6 +337,7 @@ export async function forceReleaseHeldLocksForSignal() {
|
|
|
387
337
|
removed.push(lockPath);
|
|
388
338
|
}
|
|
389
339
|
}
|
|
340
|
+
removed.push(...(await releaseSharedHoldsForSignal()));
|
|
390
341
|
return removed;
|
|
391
342
|
}
|
|
392
343
|
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
@@ -404,8 +355,14 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
404
355
|
let lastProbeAt = startedAt;
|
|
405
356
|
/** Best (lowest) queue position seen so far. Used to detect an advance. */
|
|
406
357
|
let bestAhead;
|
|
407
|
-
/**
|
|
408
|
-
|
|
358
|
+
/**
|
|
359
|
+
* This waiter's ticket in the queue. Taken on first contention, or up
|
|
360
|
+
* front when others are already queued, so an uncontended acquire never
|
|
361
|
+
* writes one.
|
|
362
|
+
*/
|
|
363
|
+
let waiter;
|
|
364
|
+
/** The last `EEXIST`, carried as the cause of a timeout refusal. */
|
|
365
|
+
let contention;
|
|
409
366
|
// One cadence drives both the progress line and the deadline extension.
|
|
410
367
|
// Either feature alone is enough to want the probe. Neither is worth a
|
|
411
368
|
// second timer.
|
|
@@ -414,83 +371,97 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
414
371
|
extend !== undefined;
|
|
415
372
|
await ensureDir(dirname(lockPath));
|
|
416
373
|
try {
|
|
374
|
+
// Arrival order: a newcomer that finds others already queued takes a
|
|
375
|
+
// ticket behind them instead of racing them for the next free moment.
|
|
376
|
+
// Without this a fresh command could take the lock in the gap between
|
|
377
|
+
// the holder's release and the head waiter's next poll.
|
|
378
|
+
if ((await readLockQueue(lockPath)).depth > 0) {
|
|
379
|
+
waiter = await registerWaiter(lockPath, startedAt);
|
|
380
|
+
}
|
|
417
381
|
for (;;) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
// Advertise this waiter so the queue is visible to the wait output
|
|
427
|
-
// and to `fireforge status --lock`. Registered lazily:
|
|
428
|
-
// an uncontended acquisition never touches the registry.
|
|
429
|
-
deregisterWaiter ??= await registerWaiter(lockPath, startedAt);
|
|
430
|
-
if (lastStaleProbeAt === undefined || Date.now() - lastStaleProbeAt >= staleReprobeMs) {
|
|
431
|
-
lastStaleProbeAt = Date.now();
|
|
432
|
-
if (await removeIfStaleLock(lockPath, staleMs, options.onStaleLockMessage)) {
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
382
|
+
// Only the head of the queue contends for the lock. A waiter without
|
|
383
|
+
// a ticket (no contention seen yet, or a registry that could not be
|
|
384
|
+
// written) contends as every waiter did before tickets existed.
|
|
385
|
+
if (waiter?.ticket === undefined || (await isWaiterTurn(lockPath, startedAt, waiter))) {
|
|
386
|
+
try {
|
|
387
|
+
await mkdir(lockPath);
|
|
388
|
+
break;
|
|
435
389
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// its waiter registration and stale probe, can outlast a short budget;
|
|
440
|
-
// checked the other way round, the wait timed out having reported
|
|
441
|
-
// nothing, and an advance that probe would have seen could not extend
|
|
442
|
-
// the deadline it was about to miss.
|
|
443
|
-
if (probeWanted && Date.now() - lastProbeAt >= probeIntervalMs) {
|
|
444
|
-
lastProbeAt = Date.now();
|
|
445
|
-
// Report the budget that is currently in force, not the one
|
|
446
|
-
// originally requested: once an extension has been granted, the
|
|
447
|
-
// requested figure is stale, and a progress line that keeps
|
|
448
|
-
// quoting it reads as a timeout that does not fire.
|
|
449
|
-
const probe = await runWaitProbe(lockPath, startedAt, deadline - startedAt, options);
|
|
450
|
-
// An advance (someone ahead of us took their turn and finished)
|
|
451
|
-
// buys a fresh budget, because the wait is working. Only a queue
|
|
452
|
-
// that stops moving starves, which is the case the timeout is for.
|
|
453
|
-
//
|
|
454
|
-
// The first observation is a seed, never an advance. `bestAhead`
|
|
455
|
-
// used to start `undefined`, so the opening probe always satisfied
|
|
456
|
-
// this test and handed out a free full budget: a `--wait-lock 300`
|
|
457
|
-
// run outlived its 300 s even against a queue that never moved
|
|
458
|
-
// once, which is the "it does not reliably give up after N
|
|
459
|
-
// seconds" behaviour a downstream fork reported.
|
|
460
|
-
if (hardDeadline !== undefined) {
|
|
461
|
-
if (bestAhead === undefined) {
|
|
462
|
-
bestAhead = probe.queue.ahead;
|
|
463
|
-
}
|
|
464
|
-
else if (probe.queue.ahead < bestAhead) {
|
|
465
|
-
bestAhead = probe.queue.ahead;
|
|
466
|
-
deadline = Math.min(Date.now() + timeoutMs, hardDeadline);
|
|
467
|
-
options.onWaitExtended?.({
|
|
468
|
-
ahead: probe.queue.ahead,
|
|
469
|
-
budgetMs: deadline - startedAt,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
if (getNodeErrorCode(error) !== 'EEXIST') {
|
|
392
|
+
throw error;
|
|
472
393
|
}
|
|
394
|
+
contention = error;
|
|
473
395
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
396
|
+
}
|
|
397
|
+
// Take a ticket so the queue is visible to the wait output and to
|
|
398
|
+
// `fireforge status --lock`, and so the grant follows arrival order.
|
|
399
|
+
waiter ??= await registerWaiter(lockPath, startedAt);
|
|
400
|
+
await waiter.heartbeat();
|
|
401
|
+
const self = { startedAt, ticket: waiter.ticket };
|
|
402
|
+
if (lastStaleProbeAt === undefined || Date.now() - lastStaleProbeAt >= staleReprobeMs) {
|
|
403
|
+
lastStaleProbeAt = Date.now();
|
|
404
|
+
if (await removeIfStaleLock(lockPath, staleMs, options.onStaleLockMessage)) {
|
|
405
|
+
continue;
|
|
482
406
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
407
|
+
}
|
|
408
|
+
// The probe runs before the deadline check, so a due progress line is
|
|
409
|
+
// written before the refusal that follows it, never lost to it. On a
|
|
410
|
+
// slow host (a cold Windows CI runner) the first iteration alone, with
|
|
411
|
+
// its waiter registration and stale probe, can outlast a short budget;
|
|
412
|
+
// checked the other way round, the wait timed out having reported
|
|
413
|
+
// nothing, and an advance that probe would have seen could not extend
|
|
414
|
+
// the deadline it was about to miss.
|
|
415
|
+
if (probeWanted && Date.now() - lastProbeAt >= probeIntervalMs) {
|
|
416
|
+
lastProbeAt = Date.now();
|
|
417
|
+
// Report the budget that is currently in force, not the one
|
|
418
|
+
// originally requested: once an extension has been granted, the
|
|
419
|
+
// requested figure is stale, and a progress line that keeps
|
|
420
|
+
// quoting it reads as a timeout that does not fire.
|
|
421
|
+
const probe = await runWaitProbe(lockPath, self, deadline - startedAt, options);
|
|
422
|
+
// An advance (someone ahead of us took their turn and finished)
|
|
423
|
+
// buys a fresh budget, because the wait is working. Only a queue
|
|
424
|
+
// that stops moving starves, which is the case the timeout is for.
|
|
425
|
+
//
|
|
426
|
+
// The first observation is a seed, never an advance. `bestAhead`
|
|
427
|
+
// used to start `undefined`, so the opening probe always satisfied
|
|
428
|
+
// this test and handed out a free full budget: a `--wait-lock 300`
|
|
429
|
+
// run outlived its 300 s even against a queue that never moved
|
|
430
|
+
// once, which is the "it does not reliably give up after N
|
|
431
|
+
// seconds" behaviour a downstream fork reported.
|
|
432
|
+
if (hardDeadline !== undefined) {
|
|
433
|
+
if (bestAhead === undefined) {
|
|
434
|
+
bestAhead = probe.queue.ahead;
|
|
435
|
+
}
|
|
436
|
+
else if (probe.queue.ahead < bestAhead) {
|
|
437
|
+
bestAhead = probe.queue.ahead;
|
|
438
|
+
deadline = Math.min(Date.now() + timeoutMs, hardDeadline);
|
|
439
|
+
options.onWaitExtended?.({
|
|
440
|
+
ahead: probe.queue.ahead,
|
|
441
|
+
budgetMs: deadline - startedAt,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
486
444
|
}
|
|
487
445
|
}
|
|
446
|
+
if (Date.now() >= deadline) {
|
|
447
|
+
// The queue is read here, not carried over from the last progress
|
|
448
|
+
// probe: the position the wait reached is the one number that says
|
|
449
|
+
// whether a larger budget would have helped, and it must not depend
|
|
450
|
+
// on a probe having happened to land inside the budget. We are still
|
|
451
|
+
// registered at this point. The ticket is returned in the `finally`
|
|
452
|
+
// below, which is only entered once this refusal has been built.
|
|
453
|
+
throw await buildLockTimeoutError(lockPath, options.onTimeoutMessage, contention, await readLockQueue(lockPath, self));
|
|
454
|
+
}
|
|
455
|
+
await sleep(currentPollMs);
|
|
456
|
+
if (options.pollMaxMs !== undefined) {
|
|
457
|
+
currentPollMs = Math.min(currentPollMs * 2, options.pollMaxMs);
|
|
458
|
+
}
|
|
488
459
|
}
|
|
489
460
|
}
|
|
490
461
|
finally {
|
|
491
462
|
// Leaving the queue happens the moment we stop waiting, whether we
|
|
492
463
|
// acquired the lock or timed out.
|
|
493
|
-
await
|
|
464
|
+
await waiter?.deregister();
|
|
494
465
|
}
|
|
495
466
|
// Stamp ownership (PID + per-acquisition token + acquisition time) into
|
|
496
467
|
// the lock directory so stale-lock recovery can check liveness and release
|
|
@@ -500,8 +471,36 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
500
471
|
// `writeLockOwner` removes the just-created directory before throwing.
|
|
501
472
|
const ownerToken = randomUUID();
|
|
502
473
|
await writeLockOwner(lockPath, ownerToken, options.ownerMetadata);
|
|
474
|
+
return runWhileHeld(lockPath, ownerToken, operation, options, {
|
|
475
|
+
deadline: Math.max(deadline, Date.now() + pollMs),
|
|
476
|
+
pollMs,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Runs `operation` once the turnstile is ours: as a shared holder (turnstile
|
|
481
|
+
* released at once), or exclusively once every shared holder has drained.
|
|
482
|
+
*/
|
|
483
|
+
async function runWhileHeld(lockPath, ownerToken, operation, options, drain) {
|
|
484
|
+
if (options.mode === 'shared') {
|
|
485
|
+
// The exclusive lock was only the turnstile: record the shared hold and
|
|
486
|
+
// let the next waiter in. See file-lock-shared.ts.
|
|
487
|
+
let hold;
|
|
488
|
+
try {
|
|
489
|
+
hold = await registerSharedHold(lockPath, options.ownerMetadata);
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
await releaseLock(lockPath, ownerToken);
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
return await operation();
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
await hold.release();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
503
501
|
heldLocks.add(lockPath);
|
|
504
502
|
try {
|
|
503
|
+
await waitForSharedHoldersToDrain(lockPath, drain.deadline, drain.pollMs);
|
|
505
504
|
return await operation();
|
|
506
505
|
}
|
|
507
506
|
finally {
|
|
@@ -509,3 +508,27 @@ export async function withFileLock(lockPath, operation, options = {}) {
|
|
|
509
508
|
await releaseLock(lockPath, ownerToken);
|
|
510
509
|
}
|
|
511
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Holds an exclusive acquisition until every live shared holder has gone.
|
|
513
|
+
* The turnstile is already ours, so no new shared holder can join meanwhile.
|
|
514
|
+
*
|
|
515
|
+
* @throws LockContentionError when shared holders outlast the budget
|
|
516
|
+
*/
|
|
517
|
+
async function waitForSharedHoldersToDrain(lockPath, deadline, pollMs) {
|
|
518
|
+
let announced = false;
|
|
519
|
+
for (;;) {
|
|
520
|
+
const live = await countLiveSharedHolders(lockPath);
|
|
521
|
+
if (live === 0)
|
|
522
|
+
return;
|
|
523
|
+
if (Date.now() >= deadline) {
|
|
524
|
+
throw new LockContentionError(`Timed out waiting for file lock ${lockPath}: ${live} shared holder(s) (build-less ` +
|
|
525
|
+
'test runs under a shared lock) still hold it. Wait for them to finish, or retry ' +
|
|
526
|
+
'with a larger --wait-lock.');
|
|
527
|
+
}
|
|
528
|
+
if (!announced) {
|
|
529
|
+
verbose(`Waiting for ${live} shared holder(s) of ${lockPath} to finish.`);
|
|
530
|
+
announced = true;
|
|
531
|
+
}
|
|
532
|
+
await sleep(Math.max(pollMs, 100));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
@@ -2,6 +2,16 @@ import type { ApplyResult, DryRunAction } from '../types/furnace.js';
|
|
|
2
2
|
type ApplyResultWithActions = ApplyResult & {
|
|
3
3
|
actions?: DryRunAction[];
|
|
4
4
|
};
|
|
5
|
+
/**
|
|
6
|
+
* The reason breakdown appended to a "skipped N" summary, e.g.
|
|
7
|
+
* ` (2 already current)`, so `Applied 0, skipped 1` says whether the
|
|
8
|
+
* component was already current or skipped for another reason without
|
|
9
|
+
* diffing the deployed files by hand. Empty when nothing was skipped.
|
|
10
|
+
*
|
|
11
|
+
* @param skipped - `result.skipped` from applyAllComponents
|
|
12
|
+
* @returns The parenthesised breakdown with a leading space, or ''
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatSkipBreakdown(skipped: ApplyResult['skipped']): string;
|
|
5
15
|
/**
|
|
6
16
|
* Prints a standard summary of an apply result for both normal and dry-run flows.
|
|
7
17
|
*
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import { error, info, success, warn } from '../utils/logger.js';
|
|
2
|
+
/** Summary label and per-component explanation for each skip kind. */
|
|
3
|
+
const SKIP_KIND_TEXT = {
|
|
4
|
+
'already-current': {
|
|
5
|
+
label: 'already current',
|
|
6
|
+
detail: 'already current: source unchanged since the last recorded apply (from any session) and the engine copy matches it',
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
/** The per-component line text for one skip. */
|
|
10
|
+
function describeSkip(skipped) {
|
|
11
|
+
return skipped.kind !== undefined ? SKIP_KIND_TEXT[skipped.kind].detail : skipped.reason;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The reason breakdown appended to a "skipped N" summary, e.g.
|
|
15
|
+
* ` (2 already current)`, so `Applied 0, skipped 1` says whether the
|
|
16
|
+
* component was already current or skipped for another reason without
|
|
17
|
+
* diffing the deployed files by hand. Empty when nothing was skipped.
|
|
18
|
+
*
|
|
19
|
+
* @param skipped - `result.skipped` from applyAllComponents
|
|
20
|
+
* @returns The parenthesised breakdown with a leading space, or ''
|
|
21
|
+
*/
|
|
22
|
+
export function formatSkipBreakdown(skipped) {
|
|
23
|
+
if (skipped.length === 0)
|
|
24
|
+
return '';
|
|
25
|
+
const counts = new Map();
|
|
26
|
+
for (const entry of skipped) {
|
|
27
|
+
const label = entry.kind !== undefined ? SKIP_KIND_TEXT[entry.kind].label : entry.reason;
|
|
28
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
return ` (${[...counts].map(([label, count]) => `${count} ${label}`).join(', ')})`;
|
|
31
|
+
}
|
|
2
32
|
/**
|
|
3
33
|
* Prints a standard summary of an apply result for both normal and dry-run flows.
|
|
4
34
|
*
|
|
@@ -40,7 +70,7 @@ export function logApplyResult(result, isDryRun) {
|
|
|
40
70
|
success(`${applied.name} (${applied.type}) → ${applied.filesAffected.length} files`);
|
|
41
71
|
}
|
|
42
72
|
for (const skipped of result.skipped) {
|
|
43
|
-
info(`${skipped.name} — ${skipped
|
|
73
|
+
info(`${skipped.name} — ${describeSkip(skipped)}`);
|
|
44
74
|
}
|
|
45
75
|
for (const applied of result.applied) {
|
|
46
76
|
if (applied.stepErrors && applied.stepErrors.length > 0) {
|