@legionworks/facet 1.9.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/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +23 -0
- package/README.md +158 -0
- package/dist/gallery/chunk-4g8rem85.css +656 -0
- package/dist/gallery/chunk-c6s9k9ty.js +5970 -0
- package/dist/gallery/frame/artifact.css +8424 -0
- package/dist/gallery/frame/chunks/abnfDiagram-N423BO3Z-jj74szbm.js +155 -0
- package/dist/gallery/frame/chunks/architecture-TIHT7OUA-cvdjn7dt.js +10 -0
- package/dist/gallery/frame/chunks/architectureDiagram-T3A2C74G-n4z77eg6.js +8675 -0
- package/dist/gallery/frame/chunks/blockDiagram-VBNYF7ZC-7xsqzrd1.js +3529 -0
- package/dist/gallery/frame/chunks/c4Diagram-5PPSVZJV-z2zjs7wx.js +2413 -0
- package/dist/gallery/frame/chunks/classDiagram-JCYQIIEL-ve8rngqp.js +47 -0
- package/dist/gallery/frame/chunks/classDiagram-v2-OCEON4UE-8cfaxb8z.js +47 -0
- package/dist/gallery/frame/chunks/cose-bilkent-JH36ORCC-sn92qexx.js +4822 -0
- package/dist/gallery/frame/chunks/cynefin-VYW2F7L2-tqjv913d.js +10 -0
- package/dist/gallery/frame/chunks/cynefinDiagram-MW4NZA55-c1c0dj12.js +500 -0
- package/dist/gallery/frame/chunks/dagre-VZM6K2ZE-7tyg4yt2.js +476 -0
- package/dist/gallery/frame/chunks/diagram-7IWD3JNH-y4eng9sc.js +526 -0
- package/dist/gallery/frame/chunks/diagram-B4RE2ZJO-rn3p7j5h.js +673 -0
- package/dist/gallery/frame/chunks/diagram-LBJQPF4R-w4a2cm9q.js +255 -0
- package/dist/gallery/frame/chunks/diagram-Q27KOJAE-aqc2v7wy.js +575 -0
- package/dist/gallery/frame/chunks/diagram-UB23O5K3-xncy8nyd.js +355 -0
- package/dist/gallery/frame/chunks/ebnfDiagram-BXEA7PRR-acwgxz16.js +175 -0
- package/dist/gallery/frame/chunks/erDiagram-JOGREHBK-mzp5nswt.js +1332 -0
- package/dist/gallery/frame/chunks/eventmodeling-45OFAUF4-sg07qr0c.js +10 -0
- package/dist/gallery/frame/chunks/flowDiagram-UKHOOZJN-2e5ygs44.js +29 -0
- package/dist/gallery/frame/chunks/ganttDiagram-PKOTCBZU-p2hhh7hr.js +2620 -0
- package/dist/gallery/frame/chunks/gitGraph-TEB2WS4Q-j6kw0naa.js +10 -0
- package/dist/gallery/frame/chunks/gitGraphDiagram-DS77QQ5N-4z9z8wjm.js +1347 -0
- package/dist/gallery/frame/chunks/info-DKCQHKI2-mpt80jd3.js +10 -0
- package/dist/gallery/frame/chunks/infoDiagram-6WML65LV-5rqm3mgg.js +65 -0
- package/dist/gallery/frame/chunks/ishikawaDiagram-WSZJBQD7-b51z96jy.js +975 -0
- package/dist/gallery/frame/chunks/journeyDiagram-NVQOT4AX-9jbfp0pk.js +1272 -0
- package/dist/gallery/frame/chunks/kanban-definition-27J2QSJJ-4x0vx4se.js +1116 -0
- package/dist/gallery/frame/chunks/katex-xfgk95tv.js +14034 -0
- package/dist/gallery/frame/chunks/markdown-1s5vwkf2.js +9 -0
- package/dist/gallery/frame/chunks/markdown-2h6bwsms.js +3239 -0
- package/dist/gallery/frame/chunks/markdown-2x113yh8.js +107 -0
- package/dist/gallery/frame/chunks/markdown-30b7rwmp.js +1943 -0
- package/dist/gallery/frame/chunks/markdown-356bfj4g.js +2031 -0
- package/dist/gallery/frame/chunks/markdown-44n9ea69.js +2406 -0
- package/dist/gallery/frame/chunks/markdown-56enrf2r.js +39 -0
- package/dist/gallery/frame/chunks/markdown-5avymm8t.js +725 -0
- package/dist/gallery/frame/chunks/markdown-6r4svars.js +33 -0
- package/dist/gallery/frame/chunks/markdown-6zz8efx2.js +24 -0
- package/dist/gallery/frame/chunks/markdown-7jp6prnb.js +19 -0
- package/dist/gallery/frame/chunks/markdown-809k7q41.js +55 -0
- package/dist/gallery/frame/chunks/markdown-82xmzhzd.js +1076 -0
- package/dist/gallery/frame/chunks/markdown-92n37gd4.js +7627 -0
- package/dist/gallery/frame/chunks/markdown-ag2sssf8.js +36 -0
- package/dist/gallery/frame/chunks/markdown-b4s7ntq2.js +19 -0
- package/dist/gallery/frame/chunks/markdown-c9k0fvp1.js +100 -0
- package/dist/gallery/frame/chunks/markdown-ceefymn8.js +125 -0
- package/dist/gallery/frame/chunks/markdown-d1wfbs70.js +243 -0
- package/dist/gallery/frame/chunks/markdown-dy5sgjhc.js +11022 -0
- package/dist/gallery/frame/chunks/markdown-dz3z5w30.js +30868 -0
- package/dist/gallery/frame/chunks/markdown-eg6ngz7y.js +30340 -0
- package/dist/gallery/frame/chunks/markdown-ff5wvb43.js +1149 -0
- package/dist/gallery/frame/chunks/markdown-ffg06nfz.js +86 -0
- package/dist/gallery/frame/chunks/markdown-fvfncadf.js +472 -0
- package/dist/gallery/frame/chunks/markdown-gvyszqpv.js +19 -0
- package/dist/gallery/frame/chunks/markdown-gx1p0ew0.js +88 -0
- package/dist/gallery/frame/chunks/markdown-gxye5frz.js +65 -0
- package/dist/gallery/frame/chunks/markdown-h7pzv34e.js +105 -0
- package/dist/gallery/frame/chunks/markdown-hab789t9.js +47 -0
- package/dist/gallery/frame/chunks/markdown-hjp6sj8t.js +125 -0
- package/dist/gallery/frame/chunks/markdown-jka64qgp.js +1993 -0
- package/dist/gallery/frame/chunks/markdown-k8xkn8mw.js +56 -0
- package/dist/gallery/frame/chunks/markdown-m7frq8x8.js +112 -0
- package/dist/gallery/frame/chunks/markdown-p740v4qm.js +36 -0
- package/dist/gallery/frame/chunks/markdown-pxfeh231.js +57 -0
- package/dist/gallery/frame/chunks/markdown-pzgd8z3s.js +87 -0
- package/dist/gallery/frame/chunks/markdown-qx3cngz2.js +1673 -0
- package/dist/gallery/frame/chunks/markdown-rawhfvvc.js +85 -0
- package/dist/gallery/frame/chunks/markdown-rd8w0ng9.js +49 -0
- package/dist/gallery/frame/chunks/markdown-rm2yxfjj.js +2773 -0
- package/dist/gallery/frame/chunks/markdown-s1jatrnk.js +993 -0
- package/dist/gallery/frame/chunks/markdown-sgfg5vvq.js +59 -0
- package/dist/gallery/frame/chunks/markdown-t4ethv9d.js +409 -0
- package/dist/gallery/frame/chunks/markdown-tnmabqvz.js +36 -0
- package/dist/gallery/frame/chunks/markdown-tvdpppw8.js +5292 -0
- package/dist/gallery/frame/chunks/markdown-v0x27twr.js +22 -0
- package/dist/gallery/frame/chunks/markdown-v14td68x.js +36 -0
- package/dist/gallery/frame/chunks/markdown-w6sgd54z.js +36 -0
- package/dist/gallery/frame/chunks/markdown-xw8dsk8b.js +2125 -0
- package/dist/gallery/frame/chunks/markdown-y18mve73.js +85 -0
- package/dist/gallery/frame/chunks/markdown-yengsms5.js +427 -0
- package/dist/gallery/frame/chunks/markdown-z1wc0v0d.js +1826 -0
- package/dist/gallery/frame/chunks/markdown-zazx9h4n.js +329 -0
- package/dist/gallery/frame/chunks/mermaid-de7p4v0x.js +30 -0
- package/dist/gallery/frame/chunks/mindmap-definition-FAOFIHXS-ra44cbem.js +1216 -0
- package/dist/gallery/frame/chunks/packet-7NZHBO7P-xppm5913.js +10 -0
- package/dist/gallery/frame/chunks/pegDiagram-VL7TDLO6-a2a4wg03.js +163 -0
- package/dist/gallery/frame/chunks/pie-RZYD4A2V-hnbxk152.js +10 -0
- package/dist/gallery/frame/chunks/pieDiagram-7S7Q4E2Y-f1vxjtmx.js +311 -0
- package/dist/gallery/frame/chunks/quadrantDiagram-CIZ2JOQS-ywafa6kh.js +1390 -0
- package/dist/gallery/frame/chunks/radar-I7S5WNFK-ewqd27se.js +10 -0
- package/dist/gallery/frame/chunks/railroad-3IZDKUUU-5hy4bdx3.js +10 -0
- package/dist/gallery/frame/chunks/railroad-abnf-AHOZXSZD-bh5sxz0d.js +10 -0
- package/dist/gallery/frame/chunks/railroad-ebnf-EBAXGLYW-6b9c3zeh.js +10 -0
- package/dist/gallery/frame/chunks/railroad-peg-LSFZ7HO6-avc8v39s.js +10 -0
- package/dist/gallery/frame/chunks/railroadDiagram-AXF67PYL-qc61wax4.js +131 -0
- package/dist/gallery/frame/chunks/requirementDiagram-LRYGKXZP-9bs4786c.js +1295 -0
- package/dist/gallery/frame/chunks/sankeyDiagram-W5VNT64P-awt1em8b.js +1339 -0
- package/dist/gallery/frame/chunks/sequenceDiagram-SI44F4Z6-qaf9mz3t.js +4324 -0
- package/dist/gallery/frame/chunks/sizeCapture-X5ZJPWSS-9fq685xd.js +64 -0
- package/dist/gallery/frame/chunks/stateDiagram-OKZ733FA-vx7a7hh0.js +456 -0
- package/dist/gallery/frame/chunks/stateDiagram-v2-UEYNNEHI-x4m6ws5t.js +46 -0
- package/dist/gallery/frame/chunks/swimlanes-SLNWSIFB-e4mxm1gh.js +8287 -0
- package/dist/gallery/frame/chunks/swimlanesDiagram-ULZ7WXOC-0h93e2qq.js +42 -0
- package/dist/gallery/frame/chunks/timeline-definition-Z64GVDOM-wp0bhbzg.js +1549 -0
- package/dist/gallery/frame/chunks/treeView-QDETBFTQ-hcx7n0vt.js +10 -0
- package/dist/gallery/frame/chunks/treemap-6X3UGDF4-av02skdw.js +10 -0
- package/dist/gallery/frame/chunks/vennDiagram-T6HMQDX7-pmvk1p7n.js +2566 -0
- package/dist/gallery/frame/chunks/wardley-OPB4EBWU-njsp6e40.js +10 -0
- package/dist/gallery/frame/chunks/wardleyDiagram-T6FBY63Y-eggmnv4z.js +979 -0
- package/dist/gallery/frame/chunks/xychartDiagram-ELKLHX3M-q40tnpe5.js +1954 -0
- package/dist/gallery/frame/frame.css +183 -0
- package/dist/gallery/frame/runtime/chart.js +42744 -0
- package/dist/gallery/frame/runtime/html.js +14 -0
- package/dist/gallery/frame/runtime/markdown.js +1403 -0
- package/dist/gallery/frame/runtime/mermaid.js +32 -0
- package/dist/gallery/frame/runtime/svg.js +14 -0
- package/dist/gallery/frame/runtime/tsx.js +52 -0
- package/dist/gallery/index.html +104 -0
- package/docs/reference/cli.md +163 -0
- package/docs/reference/export.md +106 -0
- package/docs/reference/html.md +215 -0
- package/docs/reference/http.md +85 -0
- package/docs/reference/mcp.md +78 -0
- package/docs/reference/security.md +119 -0
- package/docs/reference/storage.md +93 -0
- package/docs/reference/tsx.md +101 -0
- package/docs/reference/validation.md +212 -0
- package/package.json +89 -0
- package/scripts/build-gallery.ts +127 -0
- package/scripts/launch-netns.sh +67 -0
- package/skills/facet/SKILL.md +100 -0
- package/src/cli/client.ts +311 -0
- package/src/cli/commands/create.ts +49 -0
- package/src/cli/commands/doctor.ts +249 -0
- package/src/cli/commands/export.ts +179 -0
- package/src/cli/commands/instantiate.ts +35 -0
- package/src/cli/commands/list.ts +49 -0
- package/src/cli/commands/open.ts +75 -0
- package/src/cli/commands/pin.ts +39 -0
- package/src/cli/commands/promote.ts +49 -0
- package/src/cli/commands/publish.ts +122 -0
- package/src/cli/commands/read-back.ts +56 -0
- package/src/cli/commands/status.ts +114 -0
- package/src/cli/commands/watch.ts +146 -0
- package/src/cli/main.ts +522 -0
- package/src/cli/output.ts +106 -0
- package/src/cli/parser.ts +376 -0
- package/src/cli/presenter.ts +212 -0
- package/src/cli/service-metadata.ts +134 -0
- package/src/cli/spawn-service.ts +326 -0
- package/src/css.d.ts +4 -0
- package/src/gallery-web/app.ts +1581 -0
- package/src/gallery-web/export-menu.ts +121 -0
- package/src/gallery-web/export.ts +112 -0
- package/src/gallery-web/favicon.ts +54 -0
- package/src/gallery-web/frame/entries/chart.ts +6 -0
- package/src/gallery-web/frame/entries/html.ts +6 -0
- package/src/gallery-web/frame/entries/markdown.ts +6 -0
- package/src/gallery-web/frame/entries/mermaid.ts +6 -0
- package/src/gallery-web/frame/entries/svg.ts +6 -0
- package/src/gallery-web/frame/entries/tsx.ts +6 -0
- package/src/gallery-web/frame/frame-payload.ts +91 -0
- package/src/gallery-web/frame/renderer-validation.ts +13 -0
- package/src/gallery-web/frame/renderers/chart.ts +182 -0
- package/src/gallery-web/frame/renderers/dompurify-shim.ts +112 -0
- package/src/gallery-web/frame/renderers/html.ts +48 -0
- package/src/gallery-web/frame/renderers/markdown.ts +81 -0
- package/src/gallery-web/frame/renderers/mermaid.ts +120 -0
- package/src/gallery-web/frame/renderers/registry.ts +260 -0
- package/src/gallery-web/frame/renderers/svg.ts +393 -0
- package/src/gallery-web/frame/renderers/tsx.ts +61 -0
- package/src/gallery-web/frame/runtime.ts +501 -0
- package/src/gallery-web/frame/styles/artifact.css +8424 -0
- package/src/gallery-web/frame/styles/frame.css +183 -0
- package/src/gallery-web/frame/styles/html-source.css +23 -0
- package/src/gallery-web/frame/view-box.ts +33 -0
- package/src/gallery-web/frame-html.ts +72 -0
- package/src/gallery-web/index.html +104 -0
- package/src/gallery-web/session.ts +107 -0
- package/src/gallery-web/sse-client.ts +212 -0
- package/src/gallery-web/styles/app.css +420 -0
- package/src/gallery-web/styles/tokens.css +112 -0
- package/src/gallery-web/styles/verdict.css +186 -0
- package/src/gallery-web/swap.ts +64 -0
- package/src/gallery-web/theme.ts +40 -0
- package/src/gallery-web/view-state.ts +93 -0
- package/src/harness-adapters/claude-code/README.md +10 -0
- package/src/harness-adapters/claude-code/facet.sh +4 -0
- package/src/harness-adapters/codex/README.md +10 -0
- package/src/harness-adapters/codex/facet.sh +4 -0
- package/src/harness-adapters/mcp/README.md +13 -0
- package/src/harness-adapters/mcp/cli-bridge.ts +216 -0
- package/src/harness-adapters/mcp/main.ts +15 -0
- package/src/harness-adapters/mcp/server.ts +164 -0
- package/src/harness-adapters/mcp/tool-schemas.ts +53 -0
- package/src/harness-adapters/opencode/README.md +10 -0
- package/src/harness-adapters/opencode/facet.sh +4 -0
- package/src/runtime/compiled-entrypoints.ts +69 -0
- package/src/service/dispatcher.ts +717 -0
- package/src/service/export.ts +218 -0
- package/src/service/http-utils.ts +55 -0
- package/src/service/lexical/expectations.ts +210 -0
- package/src/service/lifecycle/idle-controller.ts +125 -0
- package/src/service/lifecycle/orphan-cleanup.ts +128 -0
- package/src/service/lifecycle/process-lock.ts +235 -0
- package/src/service/main.ts +136 -0
- package/src/service/process.ts +240 -0
- package/src/service/router-guards.ts +239 -0
- package/src/service/router.ts +627 -0
- package/src/service/security/auth.ts +168 -0
- package/src/service/security/host-origin.ts +138 -0
- package/src/service/security/http-guards.ts +13 -0
- package/src/service/security/leases.ts +182 -0
- package/src/service/security/token-store.ts +138 -0
- package/src/service/server.ts +356 -0
- package/src/service/store/database.ts +61 -0
- package/src/service/store/evidence-retention.ts +234 -0
- package/src/service/store/migrations.ts +167 -0
- package/src/service/store/repository-lifecycle.ts +121 -0
- package/src/service/store/repository.ts +732 -0
- package/src/service/store/schema.ts +218 -0
- package/src/service/stored-verdict.ts +97 -0
- package/src/service/stream.ts +301 -0
- package/src/service/verdict-enrichment.ts +45 -0
- package/src/shared/build/frame-bundle-plugins.ts +61 -0
- package/src/shared/build-mode.ts +28 -0
- package/src/shared/config/evidence-read.ts +98 -0
- package/src/shared/config/limits.ts +56 -0
- package/src/shared/config/paths.ts +96 -0
- package/src/shared/contracts/artifact-types.ts +2 -0
- package/src/shared/contracts/artifact.ts +98 -0
- package/src/shared/contracts/commands/_shared.ts +59 -0
- package/src/shared/contracts/commands/guards.ts +86 -0
- package/src/shared/contracts/commands/index.ts +204 -0
- package/src/shared/contracts/commands/names.ts +35 -0
- package/src/shared/contracts/commands/requests.ts +149 -0
- package/src/shared/contracts/commands/results.ts +200 -0
- package/src/shared/contracts/envelope.ts +140 -0
- package/src/shared/contracts/events.ts +41 -0
- package/src/shared/contracts/observed-counts.ts +19 -0
- package/src/shared/contracts/renderers.ts +6 -0
- package/src/shared/contracts/validation.ts +296 -0
- package/src/shared/errors/facet-error.ts +127 -0
- package/src/shared/errors/store-error.ts +68 -0
- package/src/shared/evidence-image.ts +37 -0
- package/src/shared/export.ts +31 -0
- package/src/shared/html/policy.ts +108 -0
- package/src/shared/html/style-vocabulary.ts +352 -0
- package/src/shared/logging/logger.ts +108 -0
- package/src/shared/logging/redact.ts +49 -0
- package/src/shared/security/frozen-csp.ts +13 -0
- package/src/shared/storage-version.ts +2 -0
- package/src/shared/tsx/execution.ts +29 -0
- package/src/shared/tsx/import-policy.ts +94 -0
- package/src/shared/util/dir-permissions.ts +94 -0
- package/src/shared/util/mermaid-nodes.ts +230 -0
- package/src/shared/util/process.ts +67 -0
- package/src/shared/util/time.ts +21 -0
- package/src/shared/version.ts +3 -0
- package/src/validation/sandbox/limits.ts +41 -0
- package/src/validation/sandbox/netns.ts +177 -0
- package/src/validation/tier0/chart.ts +177 -0
- package/src/validation/tier0/dom-shim.ts +54 -0
- package/src/validation/tier0/html.ts +408 -0
- package/src/validation/tier0/markdown.ts +285 -0
- package/src/validation/tier0/mermaid.ts +98 -0
- package/src/validation/tier0/runner.ts +504 -0
- package/src/validation/tier0/svg.ts +321 -0
- package/src/validation/tier0/tsx/allowlist-resolver.ts +77 -0
- package/src/validation/tier0/tsx/ast-policy-capabilities.ts +238 -0
- package/src/validation/tier0/tsx/ast-policy-shared.ts +71 -0
- package/src/validation/tier0/tsx/ast-policy.ts +181 -0
- package/src/validation/tier0/tsx/compiler.ts +156 -0
- package/src/validation/tier0/worker-dispatch.ts +211 -0
- package/src/validation/tier0/worker-entry.ts +96 -0
- package/src/validation/tier0/worker-input.ts +75 -0
- package/src/validation/tier1/browser-process.ts +147 -0
- package/src/validation/tier1/cdp-pipe.ts +281 -0
- package/src/validation/tier1/entries/chart.ts +5 -0
- package/src/validation/tier1/entries/html.ts +6 -0
- package/src/validation/tier1/entries/markdown.ts +5 -0
- package/src/validation/tier1/entries/mermaid.ts +5 -0
- package/src/validation/tier1/entries/svg.ts +5 -0
- package/src/validation/tier1/entries/tsx.ts +5 -0
- package/src/validation/tier1/frame-target.ts +162 -0
- package/src/validation/tier1/harness-entry.ts +261 -0
- package/src/validation/tier1/harness.ts +215 -0
- package/src/validation/tier1/isolated-probe.ts +94 -0
- package/src/validation/tier1/launcher.ts +177 -0
- package/src/validation/tier1/limits.ts +80 -0
- package/src/validation/tier1/nonce.ts +17 -0
- package/src/validation/tier1/protocol-probe.ts +519 -0
- package/src/validation/tier1/runner.ts +1122 -0
- package/src/validation/tier1/verdict.ts +246 -0
- package/src/validation/tier1/webp.ts +69 -0
- package/templates/README.md +88 -0
- package/templates/bar-compare.vl.json +63 -0
- package/templates/capacity-report.tsx +102 -0
- package/templates/decision-record.md +24 -0
- package/templates/deployment-state.mmd +40 -0
- package/templates/exemplar.md +91 -0
- package/templates/facet-story.tsx +799 -0
- package/templates/fleet-dashboard.html +141 -0
- package/templates/html-release-ledger.html +91 -0
- package/templates/html-status-report.html +114 -0
- package/templates/incident-console.tsx +147 -0
- package/templates/legion-boundaries.mmd +23 -0
- package/templates/legion-flow.mmd +15 -0
- package/templates/legion-sequence.mmd +14 -0
- package/templates/legion-state.mmd +7 -0
- package/templates/metric-card.svg +29 -0
- package/templates/observability-map.svg +101 -0
- package/templates/pipeline-audit.md +108 -0
- package/templates/release-metrics.vl.json +98 -0
- package/templates/service-topology.mmd +47 -0
- package/templates/status-report.md +32 -0
- package/templates/system-map.svg +53 -0
- package/templates/timeseries.vl.json +71 -0
- package/templates/tsx-interactive-counter.tsx +19 -0
- package/templates/tsx-status-report.tsx +31 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service metadata helpers shared by the lazy spawn path and any
|
|
3
|
+
* future CLI surface that needs to peek at the live loopback service.
|
|
4
|
+
*
|
|
5
|
+
* Three responsibilities:
|
|
6
|
+
*
|
|
7
|
+
* 1. Read the install token from disk (the CLI never auto-generates
|
|
8
|
+
* tokens; the service does on its first start).
|
|
9
|
+
* 2. Read the validated metadata record and classify it as live,
|
|
10
|
+
* stale, cross-version, or missing. The classification drives
|
|
11
|
+
* every spawn decision.
|
|
12
|
+
* 3. Wait for the metadata record to become ready (port > 0, live
|
|
13
|
+
* pid, matching contract version) under a bounded deadline.
|
|
14
|
+
* Never a fixed sleep.
|
|
15
|
+
*
|
|
16
|
+
* Extracted from `spawn-service.ts` so the orchestrator stays under
|
|
17
|
+
* the 300-line hard cap; the helpers here are the kind that grow as
|
|
18
|
+
* new edge cases get defended.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
+
|
|
23
|
+
import { FacetError } from "../shared/errors/facet-error";
|
|
24
|
+
import { FACET_SCHEMA_VERSION } from "../shared/contracts/envelope";
|
|
25
|
+
import {
|
|
26
|
+
isLockStale,
|
|
27
|
+
readLockMetadata,
|
|
28
|
+
type LockMetadata,
|
|
29
|
+
} from "../service/lifecycle/process-lock";
|
|
30
|
+
import { isPidAlive } from "../service/lifecycle/process-lock";
|
|
31
|
+
import type { FacetRuntimePaths } from "../shared/config/paths";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the typed contract-mismatch error used by every code path
|
|
35
|
+
* that observes a cross-version lock. Lives in one place so the
|
|
36
|
+
* error body shape cannot drift between the fast-path and the
|
|
37
|
+
* wait-for-ready path.
|
|
38
|
+
*/
|
|
39
|
+
export function contractMismatchError(meta: {
|
|
40
|
+
pid: number;
|
|
41
|
+
port: number;
|
|
42
|
+
contractVersion: string;
|
|
43
|
+
}): FacetError {
|
|
44
|
+
return new FacetError(
|
|
45
|
+
"internal",
|
|
46
|
+
`Service contract version mismatch: service=${meta.contractVersion}, cli=${FACET_SCHEMA_VERSION}`,
|
|
47
|
+
{
|
|
48
|
+
retryable: false,
|
|
49
|
+
details: {
|
|
50
|
+
reason: "contract_version_mismatch",
|
|
51
|
+
service: meta.contractVersion,
|
|
52
|
+
cli: FACET_SCHEMA_VERSION,
|
|
53
|
+
pid: meta.pid,
|
|
54
|
+
port: meta.port,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read the install token from disk. The token is created by the
|
|
62
|
+
* service during its first start; if the lock is missing the token
|
|
63
|
+
* is too. The CLI never auto-generates tokens — that is a
|
|
64
|
+
* service-side responsibility.
|
|
65
|
+
*/
|
|
66
|
+
export function readInstallToken(paths: FacetRuntimePaths): string {
|
|
67
|
+
const tokenPath = paths.token.replace(/promote\.token$/, "install.token");
|
|
68
|
+
if (!existsSync(tokenPath)) {
|
|
69
|
+
throw new FacetError(
|
|
70
|
+
"internal",
|
|
71
|
+
`Install token not found at ${tokenPath}; the service must start before the CLI can talk to it`,
|
|
72
|
+
{ retryable: true, details: { tokenPath } },
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const value = readFileSync(tokenPath, "utf8").trim();
|
|
76
|
+
if (value.length === 0) {
|
|
77
|
+
throw new FacetError("internal", `Install token at ${tokenPath} is empty`, {
|
|
78
|
+
retryable: false,
|
|
79
|
+
details: { tokenPath },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read a usable metadata record from disk. Returns the record only
|
|
87
|
+
* when it points at a live, contract-version-matching service.
|
|
88
|
+
* Returns null in every other case (missing, stale, dead pid) so the
|
|
89
|
+
* caller can decide whether to spawn or surface a typed error.
|
|
90
|
+
*
|
|
91
|
+
* Cross-version live records are reported as `null` here — the caller
|
|
92
|
+
* uses `readLiveMetadata` only for the same-version fast path and
|
|
93
|
+
* branches on contract version separately to surface the typed
|
|
94
|
+
* mismatch error.
|
|
95
|
+
*/
|
|
96
|
+
export function readLiveMetadata(paths: FacetRuntimePaths): LockMetadata | null {
|
|
97
|
+
const meta = readLockMetadata(paths.lock);
|
|
98
|
+
if (meta === null) return null;
|
|
99
|
+
if (meta.contractVersion !== FACET_SCHEMA_VERSION) return null;
|
|
100
|
+
if (!isPidAlive(meta.pid)) return null;
|
|
101
|
+
if (isLockStale(meta)) return null;
|
|
102
|
+
return meta;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Wait for the metadata record to appear with a non-zero port —
|
|
107
|
+
* `startFacetService` writes the port AFTER the kernel-assigned
|
|
108
|
+
* bind succeeds, so a port>0 record means a serving loopback.
|
|
109
|
+
* Poll-with-deadline — never a fixed sleep. Throws a typed
|
|
110
|
+
* `contract_version_mismatch` if the record appears with a future
|
|
111
|
+
* schema version (a build upgrade) and a typed `internal` on
|
|
112
|
+
* deadline.
|
|
113
|
+
*/
|
|
114
|
+
export async function waitForReady(
|
|
115
|
+
paths: FacetRuntimePaths,
|
|
116
|
+
timeoutMs: number,
|
|
117
|
+
intervalMs: number,
|
|
118
|
+
): Promise<LockMetadata> {
|
|
119
|
+
const deadline = Date.now() + timeoutMs;
|
|
120
|
+
while (Date.now() < deadline) {
|
|
121
|
+
const meta = readLockMetadata(paths.lock);
|
|
122
|
+
if (meta !== null && meta.port > 0) {
|
|
123
|
+
if (meta.contractVersion !== FACET_SCHEMA_VERSION) {
|
|
124
|
+
throw contractMismatchError(meta);
|
|
125
|
+
}
|
|
126
|
+
if (isPidAlive(meta.pid)) return meta;
|
|
127
|
+
}
|
|
128
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
129
|
+
}
|
|
130
|
+
throw new FacetError("internal", "Service did not become ready before deadline", {
|
|
131
|
+
retryable: true,
|
|
132
|
+
details: { timeoutMs, lockPath: paths.lock },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy service spawn.
|
|
3
|
+
*
|
|
4
|
+
* First cold CLI call reads the validated service metadata record
|
|
5
|
+
* written by `startFacetService` (see
|
|
6
|
+
* `src/service/lifecycle/process-lock.ts`). If the record is absent
|
|
7
|
+
* or stale, the spawner reaps the stale lock and forks a DETACHED
|
|
8
|
+
* `bun run src/service/main.ts` child carrying the same `FACET_HOME`
|
|
9
|
+
* so the child sees the same paths the parent did.
|
|
10
|
+
*
|
|
11
|
+
* Concurrency: a single in-process `inflight` Promise is shared
|
|
12
|
+
* across every cold caller. The Promise resolves only when the
|
|
13
|
+
* metadata record reports a live, contract-version-matching service
|
|
14
|
+
* (poll-with-deadline — never a fixed sleep). A second caller that
|
|
15
|
+
* arrives while the first is waiting joins the SAME Promise, so
|
|
16
|
+
* 20 concurrent cold calls result in exactly ONE spawn and ONE
|
|
17
|
+
* metadata record on disk.
|
|
18
|
+
*
|
|
19
|
+
* The child is started with `child_process.spawn` detached + `unref`,
|
|
20
|
+
* so the parent can exit without leaving the service as a zombie;
|
|
21
|
+
* the service owns its own lifecycle via the idle controller.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import type { ChildProcess } from "node:child_process";
|
|
26
|
+
import { unlinkSync } from "node:fs";
|
|
27
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { FACET_SCHEMA_VERSION } from "../shared/contracts/envelope";
|
|
30
|
+
import { isCompiledRuntime, type ServiceLaunch } from "../shared/build-mode";
|
|
31
|
+
import { isLockStale, readLockMetadata } from "../service/lifecycle/process-lock";
|
|
32
|
+
import { computeFacetPaths, type FacetRuntimePaths } from "../shared/config/paths";
|
|
33
|
+
import { isPidAlive } from "../service/lifecycle/process-lock";
|
|
34
|
+
import type { LockMetadata } from "../service/lifecycle/process-lock";
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
contractMismatchError,
|
|
38
|
+
readInstallToken,
|
|
39
|
+
readLiveMetadata,
|
|
40
|
+
waitForReady,
|
|
41
|
+
} from "./service-metadata";
|
|
42
|
+
|
|
43
|
+
export interface SpawnServiceOptions {
|
|
44
|
+
readonly env: NodeJS.ProcessEnv;
|
|
45
|
+
/** Override the runtime paths; defaults to `computeFacetPaths(env)`. */
|
|
46
|
+
readonly paths?: FacetRuntimePaths;
|
|
47
|
+
/** Override the service entrypoint (default: `src/service/main.ts`). */
|
|
48
|
+
readonly entrypoint?: string;
|
|
49
|
+
/** Override the bun binary (default: `process.execPath`). */
|
|
50
|
+
readonly bunPath?: string;
|
|
51
|
+
/** Polling deadline in ms (default: 15000). */
|
|
52
|
+
readonly readyTimeoutMs?: number;
|
|
53
|
+
/** Poll interval in ms (default: 25). */
|
|
54
|
+
readonly pollIntervalMs?: number;
|
|
55
|
+
/** Override the service idle window; used by lifecycle probes and tests. */
|
|
56
|
+
readonly idleTimeoutMs?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Override the Tier 0 runner module path. Defaults to
|
|
59
|
+
* `src/validation/tier0/runner.ts` resolved relative to this file.
|
|
60
|
+
* The CLI owns this path because only the CLI may import the
|
|
61
|
+
* validation module directly; the service child dynamic-imports
|
|
62
|
+
* the path passed here.
|
|
63
|
+
*/
|
|
64
|
+
readonly tier0RunnerPath?: string;
|
|
65
|
+
/** Override the Tier 1 runner module path for insecure boots. */
|
|
66
|
+
readonly tier1RunnerPath?: string;
|
|
67
|
+
/** Explicit process launch mode; defaults to the current Bun build mode. */
|
|
68
|
+
readonly launch?: ServiceLaunch;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Test-side hooks. The `onServiceSpawn` callback fires once per
|
|
73
|
+
* REAL `child_process.spawn` call in the cold-start path; the
|
|
74
|
+
* concurrency test counts these to assert that 20 concurrent
|
|
75
|
+
* cold callers result in exactly ONE spawn (not 20). The
|
|
76
|
+
* `bypassInflight` flag skips the in-process inflight wait map
|
|
77
|
+
* so the test can prove the "exactly one" assertion is meaningful
|
|
78
|
+
* (with bypass on, the same 20 callers produce 20 spawns).
|
|
79
|
+
*/
|
|
80
|
+
export interface ServiceHooks {
|
|
81
|
+
readonly onServiceSpawn?: (argv?: readonly string[], stderr?: "ignore" | "inherit") => void;
|
|
82
|
+
readonly bypassInflight?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ResolvedService {
|
|
86
|
+
readonly metadata: LockMetadata;
|
|
87
|
+
readonly installToken: string;
|
|
88
|
+
readonly baseUrl: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const DEFAULT_READY_TIMEOUT_MS = 15_000;
|
|
92
|
+
const DEFAULT_POLL_INTERVAL_MS = 25;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One-process registry of in-flight spawn waits. The CLI is
|
|
96
|
+
* short-lived (one invocation per process), so the table holds at
|
|
97
|
+
* most ONE entry at a time in practice. The map exists so the
|
|
98
|
+
* test that races 20 cold callers can observe the join path:
|
|
99
|
+
* callers 2..N must NOT acquire the lock or fork a new process.
|
|
100
|
+
*/
|
|
101
|
+
const inflight = new Map<string, Promise<ResolvedService>>();
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the service child's argv. Extracted from `spawnChild` so the
|
|
105
|
+
* parent/child path-agreement contract is unit-testable without a real
|
|
106
|
+
* spawn: every per-path override the parent passes on the command line
|
|
107
|
+
* (db, tokens, lock, evidence) must be present and equal the parent's
|
|
108
|
+
* `paths` so a future change to `computeFacetPaths` cannot desync them.
|
|
109
|
+
*/
|
|
110
|
+
export function buildServiceSpawnArgs(
|
|
111
|
+
paths: FacetRuntimePaths,
|
|
112
|
+
options: ServiceLaunch & { readonly idleTimeoutMs?: number },
|
|
113
|
+
): string[] {
|
|
114
|
+
const args = [
|
|
115
|
+
...(options.mode === "compiled" ? ["--facet-internal-service"] : [options.entrypoint]),
|
|
116
|
+
"--db-path",
|
|
117
|
+
paths.database,
|
|
118
|
+
"--install-token-path",
|
|
119
|
+
paths.token.replace(/promote\.token$/, "install.token"),
|
|
120
|
+
"--promote-token-path",
|
|
121
|
+
paths.token,
|
|
122
|
+
"--lock-path",
|
|
123
|
+
paths.lock,
|
|
124
|
+
"--evidence-path",
|
|
125
|
+
paths.evidence,
|
|
126
|
+
...(options.mode === "source"
|
|
127
|
+
? [
|
|
128
|
+
"--tier0-runner-path",
|
|
129
|
+
options.tier0RunnerPath,
|
|
130
|
+
"--tier1-runner-path",
|
|
131
|
+
options.tier1RunnerPath,
|
|
132
|
+
]
|
|
133
|
+
: []),
|
|
134
|
+
];
|
|
135
|
+
if (options.idleTimeoutMs !== undefined) {
|
|
136
|
+
args.push("--idle-timeout-ms", String(options.idleTimeoutMs));
|
|
137
|
+
}
|
|
138
|
+
return args;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Spawn the service as a detached child. The child inherits
|
|
143
|
+
* `FACET_HOME` so its paths match the parent's; the parent passes
|
|
144
|
+
* the same paths on the command line so a future change to
|
|
145
|
+
* `computeFacetPaths` cannot desync parent and child.
|
|
146
|
+
*/
|
|
147
|
+
function spawnChild(
|
|
148
|
+
paths: FacetRuntimePaths,
|
|
149
|
+
options: SpawnServiceOptions,
|
|
150
|
+
onSpawn?: (argv?: readonly string[], stderr?: "ignore" | "inherit") => void,
|
|
151
|
+
): ChildProcess {
|
|
152
|
+
const bunPath = options.bunPath ?? process.execPath;
|
|
153
|
+
const facetHome = paths.database
|
|
154
|
+
? resolvePath(dirname(dirname(paths.database)))
|
|
155
|
+
: (options.env.FACET_HOME ?? "");
|
|
156
|
+
const childEnv: NodeJS.ProcessEnv = {
|
|
157
|
+
...options.env,
|
|
158
|
+
FACET_HOME: facetHome,
|
|
159
|
+
};
|
|
160
|
+
// Pass per-path overrides as flags so the child does not have to
|
|
161
|
+
// recompute paths from FACET_HOME; this keeps parent + child
|
|
162
|
+
// identical even when an operator sets XDG_* vars.
|
|
163
|
+
const launch = options.launch ?? resolveServiceLaunch(options);
|
|
164
|
+
const args = buildServiceSpawnArgs(paths, {
|
|
165
|
+
...launch,
|
|
166
|
+
...(options.idleTimeoutMs !== undefined ? { idleTimeoutMs: options.idleTimeoutMs } : {}),
|
|
167
|
+
});
|
|
168
|
+
const insecureBoot =
|
|
169
|
+
["1", "2", "3"].includes(options.env.FACET_INSECURE ?? "") ||
|
|
170
|
+
options.env.FACET_INSECURE_AUTO === "1";
|
|
171
|
+
const stderr = insecureBoot ? "inherit" : "ignore";
|
|
172
|
+
onSpawn?.(args, stderr);
|
|
173
|
+
return spawn(bunPath, args, {
|
|
174
|
+
env: childEnv,
|
|
175
|
+
stdio: ["ignore", "ignore", stderr],
|
|
176
|
+
detached: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveServiceLaunch(options: SpawnServiceOptions): ServiceLaunch {
|
|
181
|
+
if (isCompiledRuntime()) return { mode: "compiled" };
|
|
182
|
+
return {
|
|
183
|
+
mode: "source",
|
|
184
|
+
entrypoint: options.entrypoint ?? resolvePath(import.meta.dir, "..", "service", "main.ts"),
|
|
185
|
+
tier0RunnerPath: options.tier0RunnerPath ?? resolveDefaultTier0RunnerPath(),
|
|
186
|
+
tier1RunnerPath: options.tier1RunnerPath ?? resolveDefaultTier1RunnerPath(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Default Tier 0 runner module path. Resolved relative to this file
|
|
192
|
+
* (which lives in `src/cli/`) so the import is `../validation/tier0/runner`.
|
|
193
|
+
* Callers can override via `SpawnServiceOptions.tier0RunnerPath` for
|
|
194
|
+
* tests or for custom builds.
|
|
195
|
+
*/
|
|
196
|
+
function resolveDefaultTier0RunnerPath(): string {
|
|
197
|
+
return resolvePath(import.meta.dir, "..", "validation", "tier0", "runner.ts");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveDefaultTier1RunnerPath(): string {
|
|
201
|
+
return resolvePath(import.meta.dir, "..", "validation", "tier1", "runner.ts");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reap a stale lock whose owner is dead, so the spawn path can
|
|
206
|
+
* fork a fresh service. Best-effort: a parallel cold start may
|
|
207
|
+
* race and either win the lock or retry; both outcomes are safe.
|
|
208
|
+
*/
|
|
209
|
+
function reapStaleLock(lockPath: string): void {
|
|
210
|
+
const existing = readLockMetadata(lockPath);
|
|
211
|
+
if (existing === null) return;
|
|
212
|
+
if (isPidAlive(existing.pid) && !isLockStale(existing)) return;
|
|
213
|
+
try {
|
|
214
|
+
unlinkSync(lockPath);
|
|
215
|
+
} catch {
|
|
216
|
+
// ignore — a parallel cold start will retry.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Cold-start: fork the child + wait for ready. Throws typed
|
|
222
|
+
* `FacetError` on contract-mismatch and on ready-timeout.
|
|
223
|
+
*
|
|
224
|
+
* The CLI does NOT call `acquireLock` under its own pid — that
|
|
225
|
+
* would be a foreign-pid lock the child could never reclaim without
|
|
226
|
+
* killing the parent. Instead the CLI reaps a stale lock and forks
|
|
227
|
+
* the child, which acquires the lock on its own startup path via
|
|
228
|
+
* `startFacetService`.
|
|
229
|
+
*
|
|
230
|
+
* The `onServiceSpawn` hook fires once before each real
|
|
231
|
+
* `child_process.spawn` call; the concurrency test uses it to
|
|
232
|
+
* assert that 20 concurrent cold callers produce exactly ONE
|
|
233
|
+
* spawn (not 20).
|
|
234
|
+
*/
|
|
235
|
+
async function coldStart(
|
|
236
|
+
options: SpawnServiceOptions,
|
|
237
|
+
hooks: ServiceHooks = {},
|
|
238
|
+
): Promise<ResolvedService> {
|
|
239
|
+
const env = options.env.FACET_HOME !== undefined ? { facetHome: options.env.FACET_HOME } : {};
|
|
240
|
+
const paths = options.paths ?? computeFacetPaths(env);
|
|
241
|
+
reapStaleLock(paths.lock);
|
|
242
|
+
// Pre-spawn guard: a live same-version foreign-pid owner means a
|
|
243
|
+
// peer service is already running. The CLI joins that peer via
|
|
244
|
+
// the metadata record instead of forking a second instance.
|
|
245
|
+
const precheck = readLockMetadata(paths.lock);
|
|
246
|
+
if (precheck !== null && isPidAlive(precheck.pid) && !isLockStale(precheck)) {
|
|
247
|
+
if (precheck.contractVersion !== FACET_SCHEMA_VERSION) {
|
|
248
|
+
throw contractMismatchError(precheck);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
metadata: precheck,
|
|
252
|
+
installToken: readInstallToken(paths),
|
|
253
|
+
baseUrl: `http://127.0.0.1:${precheck.port}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const child = spawnChild(paths, options, hooks.onServiceSpawn);
|
|
257
|
+
// Detach the child from the parent's lifecycle — when the CLI
|
|
258
|
+
// exits, the service keeps running until its idle window closes.
|
|
259
|
+
child.unref();
|
|
260
|
+
void child;
|
|
261
|
+
const meta = await waitForReady(
|
|
262
|
+
paths,
|
|
263
|
+
options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
|
|
264
|
+
options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
265
|
+
);
|
|
266
|
+
return {
|
|
267
|
+
metadata: meta,
|
|
268
|
+
installToken: readInstallToken(paths),
|
|
269
|
+
baseUrl: `http://127.0.0.1:${meta.port}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Ensure a live, contract-version-matching service is running. If
|
|
275
|
+
* one is already running, return its info; if not, lazily start one
|
|
276
|
+
* and share the wait across concurrent callers.
|
|
277
|
+
*
|
|
278
|
+
* Returns the resolved service's baseUrl, metadata, and install
|
|
279
|
+
* token. Throws typed `FacetError` on contract-version mismatch
|
|
280
|
+
* and on the ready deadline.
|
|
281
|
+
*/
|
|
282
|
+
export async function ensureService(
|
|
283
|
+
options: SpawnServiceOptions,
|
|
284
|
+
hooks: ServiceHooks = {},
|
|
285
|
+
): Promise<ResolvedService> {
|
|
286
|
+
const env = options.env.FACET_HOME !== undefined ? { facetHome: options.env.FACET_HOME } : {};
|
|
287
|
+
const paths = options.paths ?? computeFacetPaths(env);
|
|
288
|
+
const key = paths.lock;
|
|
289
|
+
|
|
290
|
+
// Fast path: a usable same-version record already exists.
|
|
291
|
+
const live = readLiveMetadata(paths);
|
|
292
|
+
if (live !== null) {
|
|
293
|
+
return {
|
|
294
|
+
metadata: live,
|
|
295
|
+
installToken: readInstallToken(paths),
|
|
296
|
+
baseUrl: `http://127.0.0.1:${live.port}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// A live foreign owner cannot be used or reclaimed. A dead owner falls
|
|
301
|
+
// through so coldStart can reap its stale lock before spawning.
|
|
302
|
+
const raw = readLockMetadata(paths.lock);
|
|
303
|
+
if (raw !== null) {
|
|
304
|
+
if (raw.contractVersion !== FACET_SCHEMA_VERSION && isPidAlive(raw.pid)) {
|
|
305
|
+
throw contractMismatchError(raw);
|
|
306
|
+
}
|
|
307
|
+
// Same-version but stale (dead pid): fall through to the cold
|
|
308
|
+
// path so a fresh service replaces it.
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Concurrent-caller fast path: another caller in this process is
|
|
312
|
+
// already spawning. Join the same wait. `bypassInflight` is a
|
|
313
|
+
// test-only flag that disables this join so the concurrency
|
|
314
|
+
// test can prove the join path is doing real work.
|
|
315
|
+
if (!hooks.bypassInflight) {
|
|
316
|
+
const pending = inflight.get(key);
|
|
317
|
+
if (pending !== undefined) return pending;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Cold path: become the spawner.
|
|
321
|
+
const promise = coldStart(options, hooks).finally(() => {
|
|
322
|
+
inflight.delete(key);
|
|
323
|
+
});
|
|
324
|
+
inflight.set(key, promise);
|
|
325
|
+
return promise;
|
|
326
|
+
}
|
package/src/css.d.ts
ADDED