Commit

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

syntaqxcommitted Parente902e3fBrowse files
4 files+80−50/4 viewed
+27−0
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+});
+25−0
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+}
+27−4
22
33 import { finishResponse, withRequestPerf } from "../app/lib/perf.server";
44 import { goImport } from "../app/lib/go-get";
5+import { repositoryOfPage, stillPublic } from "../app/lib/public-cache";
56 import { servicePath } from "../app/lib/registry-paths";
67
78 const requestHandler = createRequestHandler(
6566 // Every page and data request says where its time went (Server-Timing)
6667 // and keeps the reader's D1 bookmarks (app/lib/perf.server.ts).
6768 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);
6970 return render();
7071 },
7172 } satisfies ExportedHandler<Env>;
7475 * Public pages as someone signed out sees them: the same for every such
7576 * visitor, so kept in this data centre's cache. Reserved first segments
7677 * (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.
7881 */
7982 const PUBLIC_TOP = /^\/(?:|_root\.data|pricing|explore|security|support|policies(?:\/[a-z-]+)?)(?:\.data)?$/;
8083 const PUBLIC_PROJECT =
8992 return PUBLIC_TOP.test(pathname) || PUBLIC_PROJECT.test(pathname);
9093 }
9194
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> {
9396 const cache = (caches as unknown as { default: Cache }).default;
9497 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+ }
96105 const keptAt = Number(cached?.headers.get("x-g1t-kept-at") ?? 0);
97106 const age = Math.round((Date.now() - keptAt) / 1000);
98107 const refresh = async () => {
122131 return refresh();
123132 }
124133
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+
125148 /**
126149 * A git request, answered by the repos service. Its `Server-Timing` header
127150 * gains `repos`: how long the answer took to start from here, so the time
+1−1
185185 | --- | --- | --- | --- |
186186 | Static assets (`/assets/*`) | browser and edge | a year, immutable | hashed file names |
187187 | 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"`. |
189189 | 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 |
190190 | Registration mode | per isolate | 60 s | |
191191 | A commit's log by hash | repos' data-centre cache | for good | history from a commit never changes; Active branches asks by hash |