| 591 |
591 |
|
"/api-docs/openapi.json",
|
| 592 |
592 |
|
<openapi::ApiDoc as utoipa::OpenApi>::openapi(),
|
| 593 |
593 |
|
))
|
| 594 |
|
- |
// The ES module graph, which cannot use the `?v=` fingerprint.
|
|
594 |
+ |
// The ES module graph, served under a fingerprinted DIRECTORY.
|
| 595 |
595 |
|
//
|
| 596 |
|
- |
// Every other static asset is referenced from a template with
|
| 597 |
|
- |
// `?v=<content hash>`, so a week-long cache is safe: a deploy changes
|
| 598 |
|
- |
// the URL. A module graph is the exception and it bit on 2026-08-14.
|
| 599 |
|
- |
// The templates fingerprint the ENTRY point (`core/index.js?v=...`),
|
| 600 |
|
- |
// but the entry's own `import './dispatch.js'` is a bare relative URL
|
| 601 |
|
- |
// that a deploy never changes. Cloudflare kept serving a seven-day-old
|
| 602 |
|
- |
// `dispatch.js` beside a fresh `index.js`, the two disagreed about an
|
| 603 |
|
- |
// export name, and the whole bundle died with a SyntaxError -- which
|
| 604 |
|
- |
// takes every island on the page with it, since `core/index.ts`
|
| 605 |
|
- |
// side-effect-imports all of them.
|
|
596 |
+ |
// Every other static asset is referenced from a template as
|
|
597 |
+ |
// `path?v=<hash>`, so a week-long cache is safe: a deploy changes the
|
|
598 |
+ |
// URL. A module graph is the exception, and it took the whole site's
|
|
599 |
+ |
// JavaScript down on 2026-08-14. Only the ENTRY point is named by a
|
|
600 |
+ |
// template; the entry's own `import './dispatch.js'` is a bare relative
|
|
601 |
+ |
// URL that a deploy never changes. Cloudflare went on serving a
|
|
602 |
+ |
// week-old `dispatch.js` beside a fresh `index.js`, the two disagreed
|
|
603 |
+ |
// about an export name, and the graph failed to link with a
|
|
604 |
+ |
// SyntaxError. That takes down every island on the page, because
|
|
605 |
+ |
// `core/index.ts` side-effect-imports all of them.
|
| 606 |
606 |
|
//
|
| 607 |
|
- |
// `no-cache` is "cache it, revalidate every time", not "do not cache".
|
| 608 |
|
- |
// These files are a few KB each and carry an ETag, so the common answer
|
| 609 |
|
- |
// is a 304 with no body. That is the right trade for a graph whose
|
| 610 |
|
- |
// members must agree with each other.
|
|
607 |
+ |
// Putting the hash in the directory rather than in a query is what
|
|
608 |
+ |
// fixes it, and it fixes it for imports nobody has written yet: a
|
|
609 |
+ |
// relative import resolves against the document URL, so
|
|
610 |
+ |
// `/static/dist-ab12/core/index.js` asking for `./dispatch.js` gets
|
|
611 |
+ |
// `/static/dist-ab12/core/dispatch.js` for free. Every member of the
|
|
612 |
+ |
// graph moves together, by construction, and members from two different
|
|
613 |
+ |
// deploys can never meet.
|
| 611 |
614 |
|
//
|
| 612 |
|
- |
// The alternative considered was serving the whole directory under a
|
| 613 |
|
- |
// fingerprinted path (`/static/dist-<hash>/...`), which relative
|
| 614 |
|
- |
// imports would inherit for free and which keeps the long cache. It is
|
| 615 |
|
- |
// the better answer and it is a build.rs change; filed rather than done
|
| 616 |
|
- |
// here, because this had a page down.
|
|
615 |
+ |
// The long cache comes back with it, and is now honest: these URLs are
|
|
616 |
+ |
// immutable, because the next deploy has a different directory.
|
|
617 |
+ |
//
|
|
618 |
+ |
// `?v=` was tried first and is not enough. A middle attempt set
|
|
619 |
+ |
// `no-cache` on `/static/dist`, which is correct but gives up edge
|
|
620 |
+ |
// caching on the JS and, more to the point, does nothing about an
|
|
621 |
+ |
// object the CDN already holds under the old policy.
|
|
622 |
+ |
.nest_service(
|
|
623 |
+ |
concat!("/static/dist-", env!("STATIC_VERSION")),
|
|
624 |
+ |
tower::ServiceBuilder::new()
|
|
625 |
+ |
.layer(SetResponseHeaderLayer::overriding(
|
|
626 |
+ |
axum::http::header::CACHE_CONTROL,
|
|
627 |
+ |
HeaderValue::from_static("public, max-age=604800, immutable"),
|
|
628 |
+ |
))
|
|
629 |
+ |
.service(ServeDir::new("static/dist")),
|
|
630 |
+ |
)
|
|
631 |
+ |
// The unversioned path stays, and revalidates. Nothing this build emits
|
|
632 |
+ |
// points at it; it is here for a page a browser is still holding from
|
|
633 |
+ |
// before the versioned directory existed, whose cached entry document
|
|
634 |
+ |
// still names `/static/dist/...`. Remove it once no such document can
|
|
635 |
+ |
// be in flight.
|
| 617 |
636 |
|
.nest_service(
|
| 618 |
637 |
|
"/static/dist",
|
| 619 |
638 |
|
tower::ServiceBuilder::new()
|