@cogenta/render 0.1.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/dist/astro/integration.d.ts +57 -0
- package/dist/astro/integration.d.ts.map +1 -0
- package/dist/astro/integration.js +72 -0
- package/dist/astro/integration.js.map +1 -0
- package/dist/build/index.d.ts +7 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +7 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/integration.d.ts +25 -0
- package/dist/build/integration.d.ts.map +1 -0
- package/dist/build/integration.js +41 -0
- package/dist/build/integration.js.map +1 -0
- package/dist/build/plan.d.ts +41 -0
- package/dist/build/plan.d.ts.map +1 -0
- package/dist/build/plan.js +48 -0
- package/dist/build/plan.js.map +1 -0
- package/dist/build/refusal.d.ts +14 -0
- package/dist/build/refusal.d.ts.map +1 -0
- package/dist/build/refusal.js +110 -0
- package/dist/build/refusal.js.map +1 -0
- package/dist/build/requirements.d.ts +59 -0
- package/dist/build/requirements.d.ts.map +1 -0
- package/dist/build/requirements.js +74 -0
- package/dist/build/requirements.js.map +1 -0
- package/dist/build/run-build.d.ts +43 -0
- package/dist/build/run-build.d.ts.map +1 -0
- package/dist/build/run-build.js +73 -0
- package/dist/build/run-build.js.map +1 -0
- package/dist/build/targets.d.ts +47 -0
- package/dist/build/targets.d.ts.map +1 -0
- package/dist/build/targets.js +69 -0
- package/dist/build/targets.js.map +1 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +4 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/page-cache.d.ts +64 -0
- package/dist/cache/page-cache.d.ts.map +1 -0
- package/dist/cache/page-cache.js +128 -0
- package/dist/cache/page-cache.js.map +1 -0
- package/dist/cache/recorder.d.ts +49 -0
- package/dist/cache/recorder.d.ts.map +1 -0
- package/dist/cache/recorder.js +77 -0
- package/dist/cache/recorder.js.map +1 -0
- package/dist/cache/tags.d.ts +55 -0
- package/dist/cache/tags.d.ts.map +1 -0
- package/dist/cache/tags.js +62 -0
- package/dist/cache/tags.js.map +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/content/client.d.ts +35 -0
- package/dist/content/client.d.ts.map +1 -0
- package/dist/content/client.js +155 -0
- package/dist/content/client.js.map +1 -0
- package/dist/content/types.d.ts +104 -0
- package/dist/content/types.d.ts.map +1 -0
- package/dist/content/types.js +16 -0
- package/dist/content/types.js.map +1 -0
- package/dist/context/render-context.d.ts +31 -0
- package/dist/context/render-context.d.ts.map +1 -0
- package/dist/context/render-context.js +152 -0
- package/dist/context/render-context.js.map +1 -0
- package/dist/context/types.d.ts +49 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/context/types.js +2 -0
- package/dist/context/types.js.map +1 -0
- package/dist/images/errors.d.ts +20 -0
- package/dist/images/errors.d.ts.map +1 -0
- package/dist/images/errors.js +58 -0
- package/dist/images/errors.js.map +1 -0
- package/dist/images/format.d.ts +18 -0
- package/dist/images/format.d.ts.map +1 -0
- package/dist/images/format.js +34 -0
- package/dist/images/format.js.map +1 -0
- package/dist/images/geometry.d.ts +46 -0
- package/dist/images/geometry.d.ts.map +1 -0
- package/dist/images/geometry.js +127 -0
- package/dist/images/geometry.js.map +1 -0
- package/dist/images/index.d.ts +29 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +30 -0
- package/dist/images/index.js.map +1 -0
- package/dist/images/pipeline.d.ts +51 -0
- package/dist/images/pipeline.d.ts.map +1 -0
- package/dist/images/pipeline.js +105 -0
- package/dist/images/pipeline.js.map +1 -0
- package/dist/images/sharp.d.ts +57 -0
- package/dist/images/sharp.d.ts.map +1 -0
- package/dist/images/sharp.js +139 -0
- package/dist/images/sharp.js.map +1 -0
- package/dist/images/srcset.d.ts +33 -0
- package/dist/images/srcset.d.ts.map +1 -0
- package/dist/images/srcset.js +94 -0
- package/dist/images/srcset.js.map +1 -0
- package/dist/images/store.d.ts +32 -0
- package/dist/images/store.d.ts.map +1 -0
- package/dist/images/store.js +54 -0
- package/dist/images/store.js.map +1 -0
- package/dist/images/types.d.ts +128 -0
- package/dist/images/types.d.ts.map +1 -0
- package/dist/images/types.js +22 -0
- package/dist/images/types.js.map +1 -0
- package/dist/images/wasm.d.ts +43 -0
- package/dist/images/wasm.d.ts.map +1 -0
- package/dist/images/wasm.js +128 -0
- package/dist/images/wasm.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/pwa/build.d.ts +39 -0
- package/dist/pwa/build.d.ts.map +1 -0
- package/dist/pwa/build.js +38 -0
- package/dist/pwa/build.js.map +1 -0
- package/dist/pwa/client.d.ts +90 -0
- package/dist/pwa/client.d.ts.map +1 -0
- package/dist/pwa/client.js +76 -0
- package/dist/pwa/client.js.map +1 -0
- package/dist/pwa/index.d.ts +29 -0
- package/dist/pwa/index.d.ts.map +1 -0
- package/dist/pwa/index.js +22 -0
- package/dist/pwa/index.js.map +1 -0
- package/dist/pwa/manifest.d.ts +69 -0
- package/dist/pwa/manifest.d.ts.map +1 -0
- package/dist/pwa/manifest.js +108 -0
- package/dist/pwa/manifest.js.map +1 -0
- package/dist/pwa/offline.d.ts +44 -0
- package/dist/pwa/offline.d.ts.map +1 -0
- package/dist/pwa/offline.js +94 -0
- package/dist/pwa/offline.js.map +1 -0
- package/dist/pwa/service-worker.d.ts +71 -0
- package/dist/pwa/service-worker.d.ts.map +1 -0
- package/dist/pwa/service-worker.js +119 -0
- package/dist/pwa/service-worker.js.map +1 -0
- package/dist/pwa/strategy.d.ts +36 -0
- package/dist/pwa/strategy.d.ts.map +1 -0
- package/dist/pwa/strategy.js +180 -0
- package/dist/pwa/strategy.js.map +1 -0
- package/dist/pwa/sw-runtime.d.ts +109 -0
- package/dist/pwa/sw-runtime.d.ts.map +1 -0
- package/dist/pwa/sw-runtime.js +214 -0
- package/dist/pwa/sw-runtime.js.map +1 -0
- package/dist/pwa/types.d.ts +107 -0
- package/dist/pwa/types.d.ts.map +1 -0
- package/dist/pwa/types.js +18 -0
- package/dist/pwa/types.js.map +1 -0
- package/dist/pwa/version.d.ts +45 -0
- package/dist/pwa/version.d.ts.map +1 -0
- package/dist/pwa/version.js +89 -0
- package/dist/pwa/version.js.map +1 -0
- package/dist/skin/color.d.ts +50 -0
- package/dist/skin/color.d.ts.map +1 -0
- package/dist/skin/color.js +176 -0
- package/dist/skin/color.js.map +1 -0
- package/dist/skin/css.d.ts +19 -0
- package/dist/skin/css.d.ts.map +1 -0
- package/dist/skin/css.js +69 -0
- package/dist/skin/css.js.map +1 -0
- package/dist/skin/errors.d.ts +22 -0
- package/dist/skin/errors.d.ts.map +1 -0
- package/dist/skin/errors.js +59 -0
- package/dist/skin/errors.js.map +1 -0
- package/dist/skin/index.d.ts +15 -0
- package/dist/skin/index.d.ts.map +1 -0
- package/dist/skin/index.js +11 -0
- package/dist/skin/index.js.map +1 -0
- package/dist/skin/sheet.d.ts +42 -0
- package/dist/skin/sheet.d.ts.map +1 -0
- package/dist/skin/sheet.js +36 -0
- package/dist/skin/sheet.js.map +1 -0
- package/dist/skin/tokens.d.ts +90 -0
- package/dist/skin/tokens.d.ts.map +1 -0
- package/dist/skin/tokens.js +56 -0
- package/dist/skin/tokens.js.map +1 -0
- package/dist/skin/validate.d.ts +14 -0
- package/dist/skin/validate.d.ts.map +1 -0
- package/dist/skin/validate.js +272 -0
- package/dist/skin/validate.js.map +1 -0
- package/dist/theme/load-theme.d.ts +37 -0
- package/dist/theme/load-theme.d.ts.map +1 -0
- package/dist/theme/load-theme.js +102 -0
- package/dist/theme/load-theme.js.map +1 -0
- package/dist/theme/manifest.d.ts +48 -0
- package/dist/theme/manifest.d.ts.map +1 -0
- package/dist/theme/manifest.js +36 -0
- package/dist/theme/manifest.js.map +1 -0
- package/dist/theme/verify/forbidden.d.ts +39 -0
- package/dist/theme/verify/forbidden.d.ts.map +1 -0
- package/dist/theme/verify/forbidden.js +107 -0
- package/dist/theme/verify/forbidden.js.map +1 -0
- package/dist/theme/verify/scan-file.d.ts +15 -0
- package/dist/theme/verify/scan-file.d.ts.map +1 -0
- package/dist/theme/verify/scan-file.js +151 -0
- package/dist/theme/verify/scan-file.js.map +1 -0
- package/dist/theme/verify/scanner.d.ts +52 -0
- package/dist/theme/verify/scanner.d.ts.map +1 -0
- package/dist/theme/verify/scanner.js +304 -0
- package/dist/theme/verify/scanner.js.map +1 -0
- package/dist/theme/verify/verify-theme.d.ts +44 -0
- package/dist/theme/verify/verify-theme.d.ts.map +1 -0
- package/dist/theme/verify/verify-theme.js +179 -0
- package/dist/theme/verify/verify-theme.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type BuildPlan, type PlanBuildInput } from './plan.js';
|
|
2
|
+
import type { BuildRoute } from './requirements.js';
|
|
3
|
+
import type { BuildTarget } from './targets.js';
|
|
4
|
+
/**
|
|
5
|
+
* Running a planned build.
|
|
6
|
+
*
|
|
7
|
+
* The renderer is injected, and it receives **only the route** — never the
|
|
8
|
+
* target. That is not an oversight: it is what makes "the same content
|
|
9
|
+
* produces an equivalent result on the three targets" a property of the code
|
|
10
|
+
* rather than a promise in a document. A renderer that cannot see the target
|
|
11
|
+
* cannot branch on it, so the only difference between the three targets is
|
|
12
|
+
* *when* the render runs, never *what* it produces.
|
|
13
|
+
*/
|
|
14
|
+
export type RouteRenderer = (route: BuildRoute) => Promise<string> | string;
|
|
15
|
+
export interface RunBuildInput extends PlanBuildInput {
|
|
16
|
+
readonly render: RouteRenderer;
|
|
17
|
+
/**
|
|
18
|
+
* Pages rendered at once during a prerender.
|
|
19
|
+
*
|
|
20
|
+
* A theme reads its content over HTTP (ADR-0016), so a prerender is mostly
|
|
21
|
+
* waiting on round trips; rendering one page at a time makes a thousand-page
|
|
22
|
+
* build a thousand sequential waits. Bounded rather than unbounded because
|
|
23
|
+
* the content API is a plain server and a thousand parallel reads is an
|
|
24
|
+
* outage, not a build.
|
|
25
|
+
*/
|
|
26
|
+
readonly concurrency?: number | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface BuildResult {
|
|
29
|
+
readonly target: BuildTarget;
|
|
30
|
+
readonly plan: BuildPlan;
|
|
31
|
+
/** Paths rendered ahead of time, with their HTML. */
|
|
32
|
+
readonly pages: ReadonlyMap<string, string>;
|
|
33
|
+
/** Paths left to be rendered on request. Always empty on the static target. */
|
|
34
|
+
readonly onDemand: readonly string[];
|
|
35
|
+
readonly durationMs: number;
|
|
36
|
+
/**
|
|
37
|
+
* The HTML a visitor gets for `path`, through this target's production path:
|
|
38
|
+
* a file for a prerendered route, a render for an on-demand one.
|
|
39
|
+
*/
|
|
40
|
+
serve(path: string): Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
export declare function runBuild(input: RunBuildInput): Promise<BuildResult>;
|
|
43
|
+
//# sourceMappingURL=run-build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-build.d.ts","sourceRoot":"","sources":["../../src/build/run-build.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,cAAc,EAAa,MAAM,WAAW,CAAA;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;;;;;;;GASG;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;AAE3E,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC3C,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACrC;AAID,wBAAsB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAkDzE"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
import { planBuild } from './plan.js';
|
|
3
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
4
|
+
export async function runBuild(input) {
|
|
5
|
+
const plan = planBuild(input);
|
|
6
|
+
const byPath = indexRoutes(input.routes);
|
|
7
|
+
const started = performance.now();
|
|
8
|
+
const pages = new Map();
|
|
9
|
+
const onDemand = [];
|
|
10
|
+
const toPrerender = [];
|
|
11
|
+
for (const planned of plan.routes) {
|
|
12
|
+
const route = byPath.get(planned.path);
|
|
13
|
+
if (route === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
if (planned.mode === 'prerendered')
|
|
16
|
+
toPrerender.push(route);
|
|
17
|
+
else
|
|
18
|
+
onDemand.push(planned.path);
|
|
19
|
+
}
|
|
20
|
+
const concurrency = Math.max(1, input.concurrency ?? DEFAULT_CONCURRENCY);
|
|
21
|
+
let next = 0;
|
|
22
|
+
const workers = Array.from({ length: Math.min(concurrency, toPrerender.length) }, async () => {
|
|
23
|
+
while (next < toPrerender.length) {
|
|
24
|
+
const route = toPrerender[next++];
|
|
25
|
+
if (route === undefined)
|
|
26
|
+
return;
|
|
27
|
+
pages.set(route.path, await input.render(route));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
await Promise.all(workers);
|
|
31
|
+
const durationMs = performance.now() - started;
|
|
32
|
+
return {
|
|
33
|
+
target: plan.target.target,
|
|
34
|
+
plan,
|
|
35
|
+
pages,
|
|
36
|
+
onDemand,
|
|
37
|
+
durationMs,
|
|
38
|
+
serve: async (path) => {
|
|
39
|
+
const prerendered = pages.get(path);
|
|
40
|
+
if (prerendered !== undefined)
|
|
41
|
+
return prerendered;
|
|
42
|
+
const route = byPath.get(path);
|
|
43
|
+
if (route === undefined) {
|
|
44
|
+
throw new CogentaError({
|
|
45
|
+
code: 'CONTENT_NOT_FOUND',
|
|
46
|
+
message: `No route "${path}" in this build.`,
|
|
47
|
+
hint: `The build produced ${plan.routes.length} route(s). Check the path, or the query that produced the route list.`,
|
|
48
|
+
details: { path, target: plan.target.target },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return await input.render(route);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function indexRoutes(routes) {
|
|
56
|
+
const byPath = new Map();
|
|
57
|
+
for (const route of routes) {
|
|
58
|
+
if (byPath.has(route.path)) {
|
|
59
|
+
// Refused rather than resolved: two routes on one path means one of the
|
|
60
|
+
// two pages silently never ships, which is the class of bug this whole
|
|
61
|
+
// module exists to prevent.
|
|
62
|
+
throw new CogentaError({
|
|
63
|
+
code: 'CONTENT_ROUTE_INVALID',
|
|
64
|
+
message: `Two routes claim the path "${route.path}".`,
|
|
65
|
+
hint: 'A path is produced by exactly one route. Check for a duplicate slug, or for two collections routed to the same prefix.',
|
|
66
|
+
details: { path: route.path },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
byPath.set(route.path, route);
|
|
70
|
+
}
|
|
71
|
+
return byPath;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=run-build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-build.js","sourceRoot":"","sources":["../../src/build/run-build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BlockRuntime } from '@cogenta/blocks';
|
|
2
|
+
/**
|
|
3
|
+
* The three delivery targets of ADR-0004, as a build parameter.
|
|
4
|
+
*
|
|
5
|
+
* One theme codebase, three Astro adapters. The target is chosen at build
|
|
6
|
+
* time, never branched on inside a theme: a theme that renders differently
|
|
7
|
+
* "because it is the static build" would make the acceptance criterion — the
|
|
8
|
+
* same content produces an equivalent result on the three targets — unhold-
|
|
9
|
+
* able, and would put the delivery profile back into the theme where the two
|
|
10
|
+
* planes were meant to keep it out.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BUILD_TARGETS: readonly ['static', 'node', 'edge'];
|
|
13
|
+
export type BuildTarget = (typeof BUILD_TARGETS)[number];
|
|
14
|
+
export interface TargetAdapter {
|
|
15
|
+
/** The Astro adapter this target is normally built with. */
|
|
16
|
+
readonly recommended: string;
|
|
17
|
+
readonly alternatives: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface TargetCapability {
|
|
20
|
+
readonly target: BuildTarget;
|
|
21
|
+
/** Human-readable, used in refusal messages read by someone tired. */
|
|
22
|
+
readonly label: string;
|
|
23
|
+
/** The runtime needs this target can honour (contract D, "Besoins runtime"). */
|
|
24
|
+
readonly satisfies: readonly BlockRuntime[];
|
|
25
|
+
/** What `astro.config` must declare as `output` for this target. */
|
|
26
|
+
readonly astroOutput: 'static' | 'server';
|
|
27
|
+
/** `null` for static: there is no adapter, there is a directory of files. */
|
|
28
|
+
readonly adapter: TargetAdapter | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What each target can actually do at request time.
|
|
32
|
+
*
|
|
33
|
+
* `static` satisfies `static` and nothing else — that is the whole point of a
|
|
34
|
+
* static build, and the reason the refusal below exists.
|
|
35
|
+
*
|
|
36
|
+
* `node` and `edge` both satisfy the three, because both have a request. The
|
|
37
|
+
* distinction between them is *where* the code runs, not whether a request
|
|
38
|
+
* exists: a block that needs the database at request time is served fine by an
|
|
39
|
+
* edge function calling the content API over HTTP (ADR-0016), which is exactly
|
|
40
|
+
* why "deploy it as an edge function" is one of the three ways out we offer.
|
|
41
|
+
*/
|
|
42
|
+
export declare const TARGET_CAPABILITIES: Readonly<Record<BuildTarget, TargetCapability>>;
|
|
43
|
+
export declare function isBuildTarget(value: unknown): value is BuildTarget;
|
|
44
|
+
/** Turns whatever the CLI or the configuration passed into a known target. */
|
|
45
|
+
export declare function resolveTarget(target: string): TargetCapability;
|
|
46
|
+
export declare function targetSatisfies(target: TargetCapability, runtime: BlockRuntime): boolean;
|
|
47
|
+
//# sourceMappingURL=targets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.d.ts","sourceRoot":"","sources":["../../src/build/targets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAGnD;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,YAAI,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAU,CAAA;AAEhE,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAExD,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,gFAAgF;IAChF,QAAQ,CAAC,SAAS,EAAE,SAAS,YAAY,EAAE,CAAA;IAC3C,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,QAAQ,GAAG,QAAQ,CAAA;IACzC,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;CACvC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAyB/E,CAAA;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAU9D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAExF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
/**
|
|
3
|
+
* The three delivery targets of ADR-0004, as a build parameter.
|
|
4
|
+
*
|
|
5
|
+
* One theme codebase, three Astro adapters. The target is chosen at build
|
|
6
|
+
* time, never branched on inside a theme: a theme that renders differently
|
|
7
|
+
* "because it is the static build" would make the acceptance criterion — the
|
|
8
|
+
* same content produces an equivalent result on the three targets — unhold-
|
|
9
|
+
* able, and would put the delivery profile back into the theme where the two
|
|
10
|
+
* planes were meant to keep it out.
|
|
11
|
+
*/
|
|
12
|
+
export const BUILD_TARGETS = ['static', 'node', 'edge'];
|
|
13
|
+
/**
|
|
14
|
+
* What each target can actually do at request time.
|
|
15
|
+
*
|
|
16
|
+
* `static` satisfies `static` and nothing else — that is the whole point of a
|
|
17
|
+
* static build, and the reason the refusal below exists.
|
|
18
|
+
*
|
|
19
|
+
* `node` and `edge` both satisfy the three, because both have a request. The
|
|
20
|
+
* distinction between them is *where* the code runs, not whether a request
|
|
21
|
+
* exists: a block that needs the database at request time is served fine by an
|
|
22
|
+
* edge function calling the content API over HTTP (ADR-0016), which is exactly
|
|
23
|
+
* why "deploy it as an edge function" is one of the three ways out we offer.
|
|
24
|
+
*/
|
|
25
|
+
export const TARGET_CAPABILITIES = {
|
|
26
|
+
static: {
|
|
27
|
+
target: 'static',
|
|
28
|
+
label: 'static (HTML files on a CDN)',
|
|
29
|
+
satisfies: ['static'],
|
|
30
|
+
astroOutput: 'static',
|
|
31
|
+
adapter: null,
|
|
32
|
+
},
|
|
33
|
+
node: {
|
|
34
|
+
target: 'node',
|
|
35
|
+
label: 'Node SSR (a Node server rendering on request)',
|
|
36
|
+
satisfies: ['static', 'server', 'edge'],
|
|
37
|
+
astroOutput: 'server',
|
|
38
|
+
adapter: { recommended: '@astrojs/node', alternatives: [] },
|
|
39
|
+
},
|
|
40
|
+
edge: {
|
|
41
|
+
target: 'edge',
|
|
42
|
+
label: 'edge (functions at the CDN border)',
|
|
43
|
+
satisfies: ['static', 'server', 'edge'],
|
|
44
|
+
astroOutput: 'server',
|
|
45
|
+
adapter: {
|
|
46
|
+
recommended: '@astrojs/cloudflare',
|
|
47
|
+
alternatives: ['@astrojs/vercel', '@astrojs/netlify'],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export function isBuildTarget(value) {
|
|
52
|
+
return typeof value === 'string' && BUILD_TARGETS.includes(value);
|
|
53
|
+
}
|
|
54
|
+
/** Turns whatever the CLI or the configuration passed into a known target. */
|
|
55
|
+
export function resolveTarget(target) {
|
|
56
|
+
if (!isBuildTarget(target)) {
|
|
57
|
+
throw new CogentaError({
|
|
58
|
+
code: 'BUILD_TARGET_UNKNOWN',
|
|
59
|
+
message: `"${target}" is not a build target.`,
|
|
60
|
+
hint: `Use one of: ${BUILD_TARGETS.join(', ')}.`,
|
|
61
|
+
details: { target, known: BUILD_TARGETS },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return TARGET_CAPABILITIES[target];
|
|
65
|
+
}
|
|
66
|
+
export function targetSatisfies(target, runtime) {
|
|
67
|
+
return target.satisfies.includes(runtime);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=targets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.js","sourceRoot":"","sources":["../../src/build/targets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAU,CAAA;AAsBhE;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAoD;IAClF,MAAM,EAAE;QACN,MAAM,EAAE,QAAQ;QAChB,KAAK,EAAE,8BAA8B;QACrC,SAAS,EAAE,CAAC,QAAQ,CAAC;QACrB,WAAW,EAAE,QAAQ;QACrB,OAAO,EAAE,IAAI;KACd;IACD,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,+CAA+C;QACtD,SAAS,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;QACvC,WAAW,EAAE,QAAQ;QACrB,OAAO,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,EAAE,EAAE;KAC5D;IACD,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM;QACd,KAAK,EAAE,oCAAoC;QAC3C,SAAS,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;QACvC,WAAW,EAAE,QAAQ;QACrB,OAAO,EAAE;YACP,WAAW,EAAE,qBAAqB;YAClC,YAAY,EAAE,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;SACtD;KACF;CACF,CAAA;AAED,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,aAAmC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC1F,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,IAAI,MAAM,0BAA0B;YAC7C,IAAI,EAAE,eAAe,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YAChD,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE;SAC1C,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAA;AACpC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAwB,EAAE,OAAqB;IAC7E,OAAO,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;AAC3C,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { PageRequest, RenderCache, RenderCacheOptions, RenderResult, } from './page-cache.js';
|
|
2
|
+
export { createRenderCache, pageCacheKey } from './page-cache.js';
|
|
3
|
+
export type { ReadRecorder } from './recorder.js';
|
|
4
|
+
export { createReadRecorder, recordingContentClient, recordingRenderContext } from './recorder.js';
|
|
5
|
+
export type { ContentChange } from './tags.js';
|
|
6
|
+
export { collectionTag, entryTag, mediaTag, normalisePath, pathTag, tagsForChange, tagsForChanges, } from './tags.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,YAAY,GACb,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACjE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAClG,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAC9C,OAAO,EACL,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,OAAO,EACP,aAAa,EACb,cAAc,GACf,MAAM,WAAW,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createRenderCache, pageCacheKey } from './page-cache.js';
|
|
2
|
+
export { createReadRecorder, recordingContentClient, recordingRenderContext } from './recorder.js';
|
|
3
|
+
export { collectionTag, entryTag, mediaTag, normalisePath, pathTag, tagsForChange, tagsForChanges, } from './tags.js';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAEjE,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAElG,OAAO,EACL,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,aAAa,EACb,OAAO,EACP,aAAa,EACb,cAAc,GACf,MAAM,WAAW,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type CacheDriver, CogentaError, type Logger } from '@cogenta/core';
|
|
2
|
+
import { type ReadRecorder } from './recorder.js';
|
|
3
|
+
import { type ContentChange } from './tags.js';
|
|
4
|
+
/**
|
|
5
|
+
* The render cache: a page in, the content it read out, tags in between.
|
|
6
|
+
*
|
|
7
|
+
* It takes a `CacheDriver` and never names one. Memory, file and Redis all
|
|
8
|
+
* implement `invalidateTags`, so the render cache is correct on all three and
|
|
9
|
+
* a site that has no Redis loses shared storage, not correctness (rule R1).
|
|
10
|
+
*
|
|
11
|
+
* Since ADR-0016 a theme reads over HTTP, so a miss costs a round trip per
|
|
12
|
+
* read. This cache is what pays that back: a hit costs one `get`.
|
|
13
|
+
*/
|
|
14
|
+
/** What identifies a rendered page. Anything that changes the HTML belongs here. */
|
|
15
|
+
export interface PageRequest {
|
|
16
|
+
readonly locale: string;
|
|
17
|
+
readonly url: URL | string;
|
|
18
|
+
/**
|
|
19
|
+
* Extra discriminators — active skin, build target, device class. A page
|
|
20
|
+
* that varies on something absent from here is a page served wrong to half
|
|
21
|
+
* its readers, so hosts must declare every axis they vary on.
|
|
22
|
+
*/
|
|
23
|
+
readonly variant?: Readonly<Record<string, string>> | undefined;
|
|
24
|
+
}
|
|
25
|
+
export interface RenderCacheOptions {
|
|
26
|
+
readonly cache: CacheDriver;
|
|
27
|
+
readonly logger?: Logger | undefined;
|
|
28
|
+
/** Seconds. Omitted means "until invalidated", which is the point of tags. */
|
|
29
|
+
readonly ttl?: number | undefined;
|
|
30
|
+
/** Key prefix. Bump it to drop every page without touching other cache users. */
|
|
31
|
+
readonly namespace?: string | undefined;
|
|
32
|
+
/** Called on every swallowed cache failure. Injected by hosts with alerting. */
|
|
33
|
+
readonly onError?: ((error: CogentaError) => void) | undefined;
|
|
34
|
+
}
|
|
35
|
+
export interface RenderResult<T> {
|
|
36
|
+
readonly value: T;
|
|
37
|
+
/** True when the value came from the cache and nothing was rendered. */
|
|
38
|
+
readonly cached: boolean;
|
|
39
|
+
/** Dependencies of the page. Read back from the entry on a hit. */
|
|
40
|
+
readonly tags: readonly string[];
|
|
41
|
+
readonly key: string;
|
|
42
|
+
}
|
|
43
|
+
export interface RenderCache {
|
|
44
|
+
/** The key a request maps to. Exposed so a host can purge one page by hand. */
|
|
45
|
+
keyFor(request: PageRequest): string;
|
|
46
|
+
render<T>(request: PageRequest, produce: (recorder: ReadRecorder) => Promise<T>): Promise<RenderResult<T>>;
|
|
47
|
+
/** Drops one page, whatever its dependencies. */
|
|
48
|
+
forget(request: PageRequest): Promise<void>;
|
|
49
|
+
/** Publication, unpublication, deletion. Throws if the cache refuses. */
|
|
50
|
+
invalidate(changes: ContentChange | readonly ContentChange[]): Promise<void>;
|
|
51
|
+
invalidateTags(tags: readonly string[]): Promise<void>;
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export declare function createRenderCache(options: RenderCacheOptions): RenderCache;
|
|
55
|
+
/**
|
|
56
|
+
* The cache key of a page.
|
|
57
|
+
*
|
|
58
|
+
* Built from origin-independent parts — one site is one cache — and from the
|
|
59
|
+
* search string, because a query parameter that changes the HTML must change
|
|
60
|
+
* the key. Parameters are sorted so that `?a=1&b=2` and `?b=2&a=1` are one
|
|
61
|
+
* entry rather than two copies that expire independently.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pageCacheKey(namespace: string, request: PageRequest): string;
|
|
64
|
+
//# sourceMappingURL=page-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-cache.d.ts","sourceRoot":"","sources":["../../src/cache/page-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AACrE,OAAO,EAAE,KAAK,aAAa,EAAiC,MAAM,WAAW,CAAA;AAE7E;;;;;;;;;GASG;AAEH,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;CAChE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,iFAAiF;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;CAC/D;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;IACjB,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAAA;IACpC,MAAM,CAAC,CAAC,EACN,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAC9C,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3B,iDAAiD;IACjD,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,yEAAyE;IACzE,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5E,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAgBD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAqF1E;AAmCD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,CAY5E"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { CogentaError } from '@cogenta/core';
|
|
2
|
+
import { createReadRecorder } from './recorder.js';
|
|
3
|
+
import { normalisePath, tagsForChanges } from './tags.js';
|
|
4
|
+
const DEFAULT_NAMESPACE = 'render';
|
|
5
|
+
export function createRenderCache(options) {
|
|
6
|
+
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
|
|
7
|
+
const logger = options.logger;
|
|
8
|
+
const cache = options.cache;
|
|
9
|
+
/**
|
|
10
|
+
* A cache that throws is worse than no cache: it turns a slow page into a
|
|
11
|
+
* broken one. Every read and write is therefore swallowed, reported, and the
|
|
12
|
+
* render carries on as if the cache were empty.
|
|
13
|
+
*
|
|
14
|
+
* Invalidation is the deliberate exception, and it is handled outside this
|
|
15
|
+
* helper: a publish that silently failed to invalidate leaves stale pages up
|
|
16
|
+
* with nothing to notice it, so the caller must hear about it.
|
|
17
|
+
*/
|
|
18
|
+
async function attempt(operation, key, run) {
|
|
19
|
+
try {
|
|
20
|
+
return await run();
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
const error = cacheFailure(operation, key, cause);
|
|
24
|
+
logger?.warn('render cache unavailable, serving without it', {
|
|
25
|
+
code: error.code,
|
|
26
|
+
operation,
|
|
27
|
+
key,
|
|
28
|
+
});
|
|
29
|
+
options.onError?.(error);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function keyFor(request) {
|
|
34
|
+
return pageCacheKey(namespace, request);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
keyFor,
|
|
38
|
+
render: async (request, produce) => {
|
|
39
|
+
const key = keyFor(request);
|
|
40
|
+
const hit = await attempt('get', key, () => cache.get(key));
|
|
41
|
+
if (hit !== undefined && hit !== null) {
|
|
42
|
+
return { value: hit.value, cached: true, tags: hit.tags, key };
|
|
43
|
+
}
|
|
44
|
+
// Outside `attempt`: a failing render is a real failure and must surface.
|
|
45
|
+
// Only the cache is allowed to fail quietly here.
|
|
46
|
+
const recorder = createReadRecorder();
|
|
47
|
+
const value = await produce(recorder);
|
|
48
|
+
const tags = recorder.tags();
|
|
49
|
+
await attempt('set', key, () => cache.set(key, { value, tags }, { tags, ...(options.ttl === undefined ? {} : { ttl: options.ttl }) }));
|
|
50
|
+
return { value, cached: false, tags, key };
|
|
51
|
+
},
|
|
52
|
+
forget: async (request) => {
|
|
53
|
+
const key = keyFor(request);
|
|
54
|
+
await attempt('delete', key, () => cache.delete(key));
|
|
55
|
+
},
|
|
56
|
+
invalidate: async (changes) => {
|
|
57
|
+
await invalidate(cache, tagsForChanges(asChanges(changes)));
|
|
58
|
+
},
|
|
59
|
+
invalidateTags: async (tags) => {
|
|
60
|
+
await invalidate(cache, tags);
|
|
61
|
+
},
|
|
62
|
+
clear: async () => {
|
|
63
|
+
await attempt('clear', namespace, () => cache.clear());
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function isSingleChange(input) {
|
|
68
|
+
return !Array.isArray(input);
|
|
69
|
+
}
|
|
70
|
+
function asChanges(input) {
|
|
71
|
+
return isSingleChange(input) ? [input] : input;
|
|
72
|
+
}
|
|
73
|
+
async function invalidate(cache, tags) {
|
|
74
|
+
if (tags.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
await cache.invalidateTags(tags);
|
|
78
|
+
}
|
|
79
|
+
catch (cause) {
|
|
80
|
+
throw new CogentaError({
|
|
81
|
+
code: 'CACHE_FAILED',
|
|
82
|
+
message: `The render cache could not invalidate ${tags.length} tag(s) after a content change.`,
|
|
83
|
+
hint: 'Pages that embed this content will keep serving the old version until the cache is reachable again. Check the cache driver, then re-run the invalidation or clear the cache.',
|
|
84
|
+
cause,
|
|
85
|
+
details: { tags },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function cacheFailure(operation, key, cause) {
|
|
90
|
+
return new CogentaError({
|
|
91
|
+
code: 'CACHE_FAILED',
|
|
92
|
+
message: `The render cache failed to ${operation} "${key}".`,
|
|
93
|
+
hint: 'The page was rendered anyway. Check the cache driver — an unreachable cache costs latency, not correctness.',
|
|
94
|
+
cause,
|
|
95
|
+
details: { operation, key },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The cache key of a page.
|
|
100
|
+
*
|
|
101
|
+
* Built from origin-independent parts — one site is one cache — and from the
|
|
102
|
+
* search string, because a query parameter that changes the HTML must change
|
|
103
|
+
* the key. Parameters are sorted so that `?a=1&b=2` and `?b=2&a=1` are one
|
|
104
|
+
* entry rather than two copies that expire independently.
|
|
105
|
+
*/
|
|
106
|
+
export function pageCacheKey(namespace, request) {
|
|
107
|
+
const url = typeof request.url === 'string' ? parsePath(request.url) : request.url;
|
|
108
|
+
const search = sortedQuery(url.searchParams);
|
|
109
|
+
const parts = [namespace, 'page', request.locale, normalisePath(url.pathname) + search];
|
|
110
|
+
const variant = request.variant;
|
|
111
|
+
if (variant !== undefined) {
|
|
112
|
+
const entries = Object.entries(variant).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
113
|
+
for (const [name, value] of entries)
|
|
114
|
+
parts.push(`${name}=${value}`);
|
|
115
|
+
}
|
|
116
|
+
return parts.join('|');
|
|
117
|
+
}
|
|
118
|
+
/** A path, possibly with a query, and possibly a full URL. Base is irrelevant. */
|
|
119
|
+
function parsePath(value) {
|
|
120
|
+
return new URL(value, 'https://page.invalid');
|
|
121
|
+
}
|
|
122
|
+
function sortedQuery(parameters) {
|
|
123
|
+
const pairs = [...parameters.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
124
|
+
if (pairs.length === 0)
|
|
125
|
+
return '';
|
|
126
|
+
return `?${pairs.map(([name, value]) => `${name}=${value}`).join('&')}`;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=page-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-cache.js","sourceRoot":"","sources":["../../src/cache/page-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAe,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAqB,MAAM,eAAe,CAAA;AACrE,OAAO,EAAsB,aAAa,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AA4D7E,MAAM,iBAAiB,GAAG,QAAQ,CAAA;AAclC,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,iBAAiB,CAAA;IACxD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;IAE3B;;;;;;;;OAQG;IACH,KAAK,UAAU,OAAO,CACpB,SAAiB,EACjB,GAAW,EACX,GAAqB;QAErB,IAAI,CAAC;YACH,OAAO,MAAM,GAAG,EAAE,CAAA;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,KAAK,GAAG,YAAY,CAAC,SAAS,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;YACjD,MAAM,EAAE,IAAI,CAAC,8CAA8C,EAAE;gBAC3D,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,SAAS;gBACT,GAAG;aACJ,CAAC,CAAA;YACF,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAA;YACxB,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC;IAED,SAAS,MAAM,CAAC,OAAoB;QAClC,OAAO,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;IAED,OAAO;QACL,MAAM;QAEN,MAAM,EAAE,KAAK,EACX,OAAoB,EACpB,OAA+C,EACrB,EAAE;YAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;YAE3B,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,CAAC,CAAC,CAAA;YAC1E,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;gBACtC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAA;YAChE,CAAC;YAED,0EAA0E;YAC1E,kDAAkD;YAClD,MAAM,QAAQ,GAAG,kBAAkB,EAAE,CAAA;YACrC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAA;YACrC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAA;YAE5B,MAAM,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,CAC7B,KAAK,CAAC,GAAG,CACP,GAAG,EACH,EAAE,KAAK,EAAE,IAAI,EAAE,EACf,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,CACrE,CACF,CAAA;YAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAA;QAC5C,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACxB,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;YAC3B,MAAM,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QACvD,CAAC;QAED,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YAC5B,MAAM,UAAU,CAAC,KAAK,EAAE,cAAc,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;QAC7D,CAAC;QAED,cAAc,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC7B,MAAM,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QAC/B,CAAC;QAED,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;QACxD,CAAC;KACF,CAAA;AACH,CAAC;AAED,SAAS,cAAc,CAAC,KAA+C;IACrE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAED,SAAS,SAAS,CAAC,KAA+C;IAChE,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AAChD,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,KAAkB,EAAE,IAAuB;IACnE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAM;IAC7B,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,YAAY,CAAC;YACrB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,yCAAyC,IAAI,CAAC,MAAM,iCAAiC;YAC9F,IAAI,EAAE,8KAA8K;YACpL,KAAK;YACL,OAAO,EAAE,EAAE,IAAI,EAAE;SAClB,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,SAAiB,EAAE,GAAW,EAAE,KAAc;IAClE,OAAO,IAAI,YAAY,CAAC;QACtB,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,8BAA8B,SAAS,KAAK,GAAG,IAAI;QAC5D,IAAI,EAAE,6GAA6G;QACnH,KAAK;QACL,OAAO,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE;KAC5B,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB,EAAE,OAAoB;IAClE,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAA;IAClF,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAC5C,MAAM,KAAK,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAA;IAEvF,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAA;IAC/B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACxF,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAA;IACrE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACxB,CAAC;AAED,kFAAkF;AAClF,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,IAAI,GAAG,CAAC,KAAK,EAAE,sBAAsB,CAAC,CAAA;AAC/C,CAAC;AAED,SAAS,WAAW,CAAC,UAA2B;IAC9C,MAAM,KAAK,GAAG,CAAC,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACxF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACjC,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;AACzE,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ContentClient, ResponseDependencies } from '../content/types.js';
|
|
2
|
+
import type { RenderContext } from '../context/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Collects, during a render, what the page actually read.
|
|
5
|
+
*
|
|
6
|
+
* This is the part that has to be automatic. A theme declaring its own
|
|
7
|
+
* dependencies is wrong the first time someone adds a `ctx.content.list()` and
|
|
8
|
+
* forgets to update the declaration — and the symptom is a page serving stale
|
|
9
|
+
* content for as long as the cache lives, which nobody notices until a reader
|
|
10
|
+
* complains. Wrapping the two doors to data (`ctx.content` and `ctx.image`)
|
|
11
|
+
* means the dependency set is a consequence of the render, not a claim about
|
|
12
|
+
* it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ReadRecorder {
|
|
15
|
+
/** Tags accumulated so far. A fresh array on each call — never the live set. */
|
|
16
|
+
tags(): readonly string[];
|
|
17
|
+
recordEntry(id: string): void;
|
|
18
|
+
recordCollection(collection: string): void;
|
|
19
|
+
recordMedia(id: string): void;
|
|
20
|
+
recordPath(path: string): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Feeds a response's declared dependencies into a recorder.
|
|
24
|
+
*
|
|
25
|
+
* This is the other half of what `recordingContentClient` cannot see: server-
|
|
26
|
+
* side relation expansion inlines an author into an article without the
|
|
27
|
+
* author's id ever crossing the client as a request of its own, so wrapping
|
|
28
|
+
* `ContentClient` alone misses it. Wire this into the concrete HTTP client's
|
|
29
|
+
* `onDependencies` option (see `createContentClient`) and both halves land in
|
|
30
|
+
* the same set.
|
|
31
|
+
*/
|
|
32
|
+
export declare function recordDependencies(recorder: ReadRecorder, dependencies: ResponseDependencies): void;
|
|
33
|
+
export declare function createReadRecorder(): ReadRecorder;
|
|
34
|
+
/**
|
|
35
|
+
* A content client that records every read, and changes nothing else.
|
|
36
|
+
*
|
|
37
|
+
* Note what `list` records: the **collection**, on top of the entries it
|
|
38
|
+
* returned. Tagging only the returned entries is the classic mistake — the
|
|
39
|
+
* front page of a blog would then survive the publication of a new article,
|
|
40
|
+
* because that article was, by definition, not in the cached page. A list
|
|
41
|
+
* depends on the shape of the collection, not on the rows it happened to show.
|
|
42
|
+
*/
|
|
43
|
+
export declare function recordingContentClient(client: ContentClient, recorder: ReadRecorder): ContentClient;
|
|
44
|
+
/**
|
|
45
|
+
* The render context a page is given while its dependencies are being
|
|
46
|
+
* collected. Same contract D surface, same behaviour; it only remembers.
|
|
47
|
+
*/
|
|
48
|
+
export declare function recordingRenderContext(context: RenderContext, recorder: ReadRecorder): RenderContext;
|
|
49
|
+
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/cache/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAIb,oBAAoB,EACrB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAA6B,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,IAAI,IAAI,SAAS,MAAM,EAAE,CAAA;IACzB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,oBAAoB,GACjC,IAAI,CAIN;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAUjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,YAAY,GACrB,aAAa,CAuBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,YAAY,GACrB,aAAa,CASf"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { collectionTag, entryTag, mediaTag, pathTag } from './tags.js';
|
|
2
|
+
/**
|
|
3
|
+
* Feeds a response's declared dependencies into a recorder.
|
|
4
|
+
*
|
|
5
|
+
* This is the other half of what `recordingContentClient` cannot see: server-
|
|
6
|
+
* side relation expansion inlines an author into an article without the
|
|
7
|
+
* author's id ever crossing the client as a request of its own, so wrapping
|
|
8
|
+
* `ContentClient` alone misses it. Wire this into the concrete HTTP client's
|
|
9
|
+
* `onDependencies` option (see `createContentClient`) and both halves land in
|
|
10
|
+
* the same set.
|
|
11
|
+
*/
|
|
12
|
+
export function recordDependencies(recorder, dependencies) {
|
|
13
|
+
for (const id of dependencies.entries)
|
|
14
|
+
recorder.recordEntry(id);
|
|
15
|
+
for (const id of dependencies.media)
|
|
16
|
+
recorder.recordMedia(id);
|
|
17
|
+
for (const collection of dependencies.collections)
|
|
18
|
+
recorder.recordCollection(collection);
|
|
19
|
+
}
|
|
20
|
+
export function createReadRecorder() {
|
|
21
|
+
const tags = new Set();
|
|
22
|
+
return {
|
|
23
|
+
tags: () => [...tags],
|
|
24
|
+
recordEntry: (id) => void tags.add(entryTag(id)),
|
|
25
|
+
recordCollection: (collection) => void tags.add(collectionTag(collection)),
|
|
26
|
+
recordMedia: (id) => void tags.add(mediaTag(id)),
|
|
27
|
+
recordPath: (path) => void tags.add(pathTag(path)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A content client that records every read, and changes nothing else.
|
|
32
|
+
*
|
|
33
|
+
* Note what `list` records: the **collection**, on top of the entries it
|
|
34
|
+
* returned. Tagging only the returned entries is the classic mistake — the
|
|
35
|
+
* front page of a blog would then survive the publication of a new article,
|
|
36
|
+
* because that article was, by definition, not in the cached page. A list
|
|
37
|
+
* depends on the shape of the collection, not on the rows it happened to show.
|
|
38
|
+
*/
|
|
39
|
+
export function recordingContentClient(client, recorder) {
|
|
40
|
+
return {
|
|
41
|
+
entry: async (collection, id) => {
|
|
42
|
+
// Recorded before awaiting, and whatever comes back: a page that renders
|
|
43
|
+
// "not found" for a missing entry must be dropped when that entry appears.
|
|
44
|
+
recorder.recordEntry(id);
|
|
45
|
+
return client.entry(collection, id);
|
|
46
|
+
},
|
|
47
|
+
byPath: async (path) => {
|
|
48
|
+
recorder.recordPath(path);
|
|
49
|
+
const entry = await client.byPath(path);
|
|
50
|
+
if (entry !== null)
|
|
51
|
+
recorder.recordEntry(entry.id);
|
|
52
|
+
return entry;
|
|
53
|
+
},
|
|
54
|
+
list: async (request) => {
|
|
55
|
+
recorder.recordCollection(request.collection);
|
|
56
|
+
const page = await client.list(request);
|
|
57
|
+
for (const item of page.items)
|
|
58
|
+
recorder.recordEntry(item.id);
|
|
59
|
+
return page;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The render context a page is given while its dependencies are being
|
|
65
|
+
* collected. Same contract D surface, same behaviour; it only remembers.
|
|
66
|
+
*/
|
|
67
|
+
export function recordingRenderContext(context, recorder) {
|
|
68
|
+
return {
|
|
69
|
+
...context,
|
|
70
|
+
content: recordingContentClient(context.content, recorder),
|
|
71
|
+
image: (media, options) => {
|
|
72
|
+
recorder.recordMedia(media.id);
|
|
73
|
+
return context.image(media, options);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/cache/recorder.ts"],"names":[],"mappings":"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"}
|