Signed-out page cache: a repository's kept page is served only while the repository is still public, so one made private or deleted never shows from any data centre's copy
4 files+80−50/4 viewed
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import { test } from "node:test"; | |
| 3 | + | ||
| 4 | + | import { repositoryOfPage, stillPublic } from "./public-cache.ts"; | |
| 5 | + | ||
| 6 | + | test("a project page names its repository", () => { | |
| 7 | + | assert.equal(repositoryOfPage("/flagon-io/hello"), "flagon-io/hello"); | |
| 8 | + | assert.equal(repositoryOfPage("/flagon-io/hello.data"), "flagon-io/hello"); | |
| 9 | + | assert.equal(repositoryOfPage("/Flagon-IO/Hello/issues/4"), "flagon-io/hello"); | |
| 10 | + | assert.equal(repositoryOfPage("/flagon-io/hello/blob/main/src/lib.rs"), "flagon-io/hello"); | |
| 11 | + | }); | |
| 12 | + | ||
| 13 | + | test("pages that are not a repository's name none", () => { | |
| 14 | + | assert.equal(repositoryOfPage("/"), null); | |
| 15 | + | assert.equal(repositoryOfPage("/pricing"), null); | |
| 16 | + | assert.equal(repositoryOfPage("/flagon-io"), null); | |
| 17 | + | assert.equal(repositoryOfPage("/flagon-io/-"), null); | |
| 18 | + | }); | |
| 19 | + | ||
| 20 | + | test("only a repository that is there and public is served from the cache", () => { | |
| 21 | + | assert.equal(stillPublic([{ path: "a/b", is_private: false }], "a/b"), true); | |
| 22 | + | assert.equal(stillPublic([{ path: "a/b", is_private: true }], "a/b"), false); | |
| 23 | + | // Deleted or never there: repos leaves it out. | |
| 24 | + | assert.equal(stillPublic([], "a/b"), false); | |
| 25 | + | assert.equal(stillPublic(null, "a/b"), false); | |
| 26 | + | assert.equal(stillPublic([{ path: "a/c", is_private: false }], "a/b"), false); | |
| 27 | + | }); |
| 1 | + | /** | |
| 2 | + | * The signed-out page cache (workers/app.ts) keeps a repository's pages for | |
| 3 | + | * visitors who are not signed in. Before a kept page is served, the | |
| 4 | + | * repository is asked for again: one that was made private, or deleted, | |
| 5 | + | * since the page was kept is never served from the cache, wherever it was | |
| 6 | + | * kept. This says which repository a kept page belongs to. | |
| 7 | + | */ | |
| 8 | + | ||
| 9 | + | /** `<workspace>/<repository>` of a project page's path, or null for any other page. */ | |
| 10 | + | export function repositoryOfPage(pathname: string): string | null { | |
| 11 | + | const [, workspace, rest] = /^\/([^/]+)\/([^/]+)/.exec(pathname) ?? []; | |
| 12 | + | if (!workspace || !rest || rest === "-") return null; | |
| 13 | + | const name = rest.replace(/\.data$/, ""); | |
| 14 | + | if (!name) return null; | |
| 15 | + | return `${decodeURIComponent(workspace).toLowerCase()}/${decodeURIComponent(name).toLowerCase()}`; | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | /** Whether repos' `visibility` answer says the repository is still there and public. */ | |
| 19 | + | export function stillPublic(answer: unknown, path: string): boolean { | |
| 20 | + | if (!Array.isArray(answer)) return false; | |
| 21 | + | const found = answer.find((v): v is { path: string; is_private: boolean } => { | |
| 22 | + | return typeof v === "object" && v !== null && (v as { path?: unknown }).path === path; | |
| 23 | + | }); | |
| 24 | + | return found !== undefined && found.is_private === false; | |
| 25 | + | } |
| 2 | 2 | ||
| 3 | 3 | import { finishResponse, withRequestPerf } from "../app/lib/perf.server"; | |
| 4 | 4 | import { goImport } from "../app/lib/go-get"; | |
| 5 | + | import { repositoryOfPage, stillPublic } from "../app/lib/public-cache"; | |
| 5 | 6 | import { servicePath } from "../app/lib/registry-paths"; | |
| 6 | 7 | ||
| 7 | 8 | const requestHandler = createRequestHandler( | |
| 65 | 66 | // Every page and data request says where its time went (Server-Timing) | |
| 66 | 67 | // and keeps the reader's D1 bookmarks (app/lib/perf.server.ts). | |
| 67 | 68 | const render = () => withRequestPerf(request, async () => finishResponse(request, await requestHandler(request))); | |
| 68 | − | if (anonymousPage(request, pathname)) return servePublic(request, ctx, render); | |
| 69 | + | if (anonymousPage(request, pathname)) return servePublic(env, request, ctx, render); | |
| 69 | 70 | return render(); | |
| 70 | 71 | }, | |
| 71 | 72 | } satisfies ExportedHandler<Env>; | |
| 74 | 75 | * Public pages as someone signed out sees them: the same for every such | |
| 75 | 76 | * visitor, so kept in this data centre's cache. Reserved first segments | |
| 76 | 77 | * (settings, sign-in, invitations and the like) and workspace pages (`-`) | |
| 77 | − | * are never kept; docs/PERFORMANCE.md lists the rules. | |
| 78 | + | * are never kept; docs/PERFORMANCE.md lists the rules. A repository's kept | |
| 79 | + | * page is served only while repos says the repository is still public: one | |
| 80 | + | * made private or deleted is never served from any data centre's copy. | |
| 78 | 81 | */ | |
| 79 | 82 | const PUBLIC_TOP = /^\/(?:|_root\.data|pricing|explore|security|support|policies(?:\/[a-z-]+)?)(?:\.data)?$/; | |
| 80 | 83 | const PUBLIC_PROJECT = | |
| 89 | 92 | return PUBLIC_TOP.test(pathname) || PUBLIC_PROJECT.test(pathname); | |
| 90 | 93 | } | |
| 91 | 94 | ||
| 92 | − | async function servePublic(request: Request, ctx: ExecutionContext, render: () => Promise<Response>): Promise<Response> { | |
| 95 | + | async function servePublic(env: Env, request: Request, ctx: ExecutionContext, render: () => Promise<Response>): Promise<Response> { | |
| 93 | 96 | const cache = (caches as unknown as { default: Cache }).default; | |
| 94 | 97 | const key = new Request(request.url, { method: "GET" }); | |
| 95 | − | const cached = await cache.match(key); | |
| 98 | + | const repository = PUBLIC_PROJECT.test(new URL(request.url).pathname) ? repositoryOfPage(new URL(request.url).pathname) : null; | |
| 99 | + | // Asked alongside the cache, so a hit waits for one indexed read at most. | |
| 100 | + | const [cached, visible] = await Promise.all([cache.match(key), repository ? isStillPublic(env, repository) : Promise.resolve(true)]); | |
| 101 | + | if (cached && !visible) { | |
| 102 | + | ctx.waitUntil(cache.delete(key).then(() => undefined, () => undefined)); | |
| 103 | + | return render(); | |
| 104 | + | } | |
| 96 | 105 | const keptAt = Number(cached?.headers.get("x-g1t-kept-at") ?? 0); | |
| 97 | 106 | const age = Math.round((Date.now() - keptAt) / 1000); | |
| 98 | 107 | const refresh = async () => { | |
| 122 | 131 | return refresh(); | |
| 123 | 132 | } | |
| 124 | 133 | ||
| 134 | + | /** Whether the repository is there and public; anything else, including no answer, is no. */ | |
| 135 | + | async function isStillPublic(env: Env, repository: string): Promise<boolean> { | |
| 136 | + | try { | |
| 137 | + | const answer = await env.REPOS.fetch("https://service/rpc/visibility", { | |
| 138 | + | method: "POST", | |
| 139 | + | headers: { "content-type": "application/json" }, | |
| 140 | + | body: JSON.stringify({ paths: [repository] }), | |
| 141 | + | }); | |
| 142 | + | return answer.ok && stillPublic(await answer.json(), repository); | |
| 143 | + | } catch { | |
| 144 | + | return false; | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | ||
| 125 | 148 | /** | |
| 126 | 149 | * A git request, answered by the repos service. Its `Server-Timing` header | |
| 127 | 150 | * gains `repos`: how long the answer took to start from here, so the time |
| 185 | 185 | | --- | --- | --- | --- | | |
| 186 | 186 | | Static assets (`/assets/*`) | browser and edge | a year, immutable | hashed file names | | |
| 187 | 187 | | Avatars | edge cache | a year, immutable | by content hash | | |
| 188 | − | | Public pages for people signed out | the data centre's cache (`workers/app.ts`, `servePublic`) | fresh 30 s, then served once more while a new copy is made, up to 5 min | GET, no `g1t_session` cookie, an allowlisted path (home, pricing, explore, policies, a project's pages), status 200 or 404, no `Set-Cookie`, nothing private. Reserved first segments and workspace pages (`-`) are never kept. The answer says `server-timing: cache;desc="hit, Ns old"`. | | |
| 188 | + | | Public pages for people signed out | the data centre's cache (`workers/app.ts`, `servePublic`) | fresh 30 s, then served once more while a new copy is made, up to 5 min | GET, no `g1t_session` cookie, an allowlisted path (home, pricing, explore, policies, a project's pages), status 200 or 404, no `Set-Cookie`, nothing private. Reserved first segments and workspace pages (`-`) are never kept. A project's kept page is served only after repos' `visibility` says the repository is still there and public (one indexed read, alongside the cache lookup); a repository made private or deleted is never served from any data centre's copy, and the copy is dropped. The answer says `server-timing: cache;desc="hit, Ns old"`. | | |
| 189 | 189 | | Sidebar data (projects, spend, limit, entitlements) | per isolate (`lib/cache.server.ts`) | 15 s, per person and workspace | skipped during a write and for 30 s after the person's last one; failures not kept; only settled answers kept | | |
| 190 | 190 | | Registration mode | per isolate | 60 s | | | |
| 191 | 191 | | A commit's log by hash | repos' data-centre cache | for good | history from a commit never changes; Active branches asks by hash | |