@astrale-os/adapter-cloudflare 0.5.0-beta.149 → 0.5.0-beta.150

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.
@@ -29,9 +29,9 @@ async function prepare(params, context, direct) {
29
29
  const name = workerName(params, context.build.schema.compiled.root.origin, context.environment);
30
30
  const revision = context.build.schema.compiled.root.revision;
31
31
  const configuredIssuer = deploymentAddressing(params);
32
- // `frontend` stays the served file list every branch below already reads; `immutable` rides
33
- // alongside it into the Worker, where it decides which of those files may be cached forever.
34
- const { files: frontend, immutable } = await frontendFiles({
32
+ // `frontend` stays the served file list every branch below already reads; `addressed` rides
33
+ // alongside it into the Worker, where it decides which of those files a browser may retain.
34
+ const { files: frontend, addressed } = await frontendFiles({
35
35
  frontend: context.build.frontend,
36
36
  projectDir: context.projectDir,
37
37
  output: join(directory, 'frontend'),
@@ -47,7 +47,7 @@ async function prepare(params, context, direct) {
47
47
  configuredIssuer,
48
48
  router,
49
49
  ...(frontend.length === 0 ? {} : { frontendRevision: frontendRevision(frontend) }),
50
- ...(immutable.length === 0 ? {} : { frontendImmutable: immutable }),
50
+ ...(addressed.length === 0 ? {} : { frontendAddressed: addressed }),
51
51
  });
52
52
  const releaseConfig = generateWranglerConfig({
53
53
  workerName: name,
@@ -8,7 +8,7 @@ export interface FrontendOutput {
8
8
  * A frontend Astrale does not build leaves this empty, which is what keeps the absence of evidence
9
9
  * indistinguishable from evidence of mutability.
10
10
  */
11
- readonly immutable: readonly string[];
11
+ readonly addressed: readonly string[];
12
12
  }
13
13
  export declare function frontendFiles(input: {
14
14
  readonly frontend: Build['frontend'];
@@ -5,9 +5,9 @@ import { buildViteFrontend } from './vite.js';
5
5
  export async function frontendFiles(input) {
6
6
  const frontend = input.frontend;
7
7
  if (frontend === undefined)
8
- return { files: [], immutable: [] };
8
+ return { files: [], addressed: [] };
9
9
  if (frontend.source.kind === 'external') {
10
- return { files: frontendBuild.files(frontend, []), immutable: [] };
10
+ return { files: frontendBuild.files(frontend, []), addressed: [] };
11
11
  }
12
12
  await rm(input.output, { recursive: true, force: true });
13
13
  const built = await buildViteFrontend({
@@ -21,7 +21,7 @@ export async function frontendFiles(input) {
21
21
  // A name the bundler reported but that never reached disk is not a served path; keeping only the
22
22
  // intersection means the evidence describes exactly what this deployment will answer with.
23
23
  const served = new Set(files.map((file) => file.path));
24
- return { files, immutable: built.immutable.filter((path) => served.has(path)) };
24
+ return { files, addressed: built.addressed.filter((path) => served.has(path)) };
25
25
  }
26
26
  async function readFiles(root, current = '') {
27
27
  const entries = await readdir(join(root, current), { withFileTypes: true });
@@ -10,5 +10,5 @@ export declare function buildViteFrontend(input: {
10
10
  readonly recordSource?: (path: string) => void;
11
11
  readonly signal: AbortSignal;
12
12
  }): Promise<{
13
- readonly immutable: readonly string[];
13
+ readonly addressed: readonly string[];
14
14
  }>;
@@ -26,11 +26,11 @@ export async function buildViteFrontend(input) {
26
26
  }
27
27
  for (const path of sources)
28
28
  input.recordSource?.(path);
29
- const immutable = JSON.parse(await readFile(addressed, 'utf8'));
30
- if (!Array.isArray(immutable) || immutable.some((path) => typeof path !== 'string')) {
29
+ const named = JSON.parse(await readFile(addressed, 'utf8'));
30
+ if (!Array.isArray(named) || named.some((path) => typeof path !== 'string')) {
31
31
  throw new TypeError('Vite preparation output evidence is invalid.');
32
32
  }
33
- return { immutable: Object.freeze([...immutable]) };
33
+ return { addressed: Object.freeze([...named]) };
34
34
  }
35
35
  finally {
36
36
  await rm(scratch, { recursive: true, force: true });
@@ -19,7 +19,7 @@ async function main() {
19
19
  const projectDir = process.cwd();
20
20
  const vite = await projectVite(projectDir);
21
21
  const sources = new Set();
22
- const immutable = new Set();
22
+ const named = new Set();
23
23
  let publicDir = false;
24
24
  let environment = {
25
25
  directory: false,
@@ -47,13 +47,13 @@ async function main() {
47
47
  sources.add(path);
48
48
  },
49
49
  // The bundler is the only party that knows which outputs it content-addressed: a served path
50
- // is safe to cache forever exactly when its name changes with its bytes. Reading that here,
51
- // from the build that produced them, is what keeps the answer a fact rather than a guess
52
- // about directory layout or file naming.
50
+ // is safe to retain exactly when its name changes with its bytes. Reading that here, from
51
+ // the build that produced them, is what keeps the answer a fact rather than a guess about
52
+ // directory layout or file naming.
53
53
  generateBundle(options, bundle) {
54
54
  for (const [fileName, entry] of Object.entries(bundle)) {
55
55
  if (addressedName(options, entry))
56
- immutable.add(fileName);
56
+ named.add(fileName);
57
57
  }
58
58
  },
59
59
  },
@@ -67,16 +67,23 @@ async function main() {
67
67
  await collectTree(publicDir, sources);
68
68
  await writeFile(evidence, JSON.stringify([...sources]));
69
69
  if (addressed !== undefined)
70
- await writeFile(addressed, JSON.stringify([...immutable].sort()));
70
+ await writeFile(addressed, JSON.stringify([...named].sort()));
71
71
  }
72
72
  /**
73
- * Whether this output's name carries a content hash, read from the template the bundler resolved.
73
+ * Whether this output's name carries a content hash.
74
74
  *
75
- * A template that is not a plain string, or that omits `[hash]`, leaves the answer unknown, and
76
- * unknown must read as "not content-addressed": losing the optimization costs a revalidation, while
77
- * claiming it wrongly pins a stale file in browsers for as long as the header says.
75
+ * Two facts are required, and each is necessary. The output must carry a `name`, which the bundler
76
+ * sets only for the outputs it named itself: a plugin emitting an explicit `fileName` bypasses the
77
+ * template entirely, and such a file can sit anywhere, including beside the bundle's own output. And
78
+ * the template that was applied must embed `[hash]`, since a project is free to strip it.
79
+ *
80
+ * Anything else leaves the answer unknown, and unknown must read as "not content-addressed": losing
81
+ * the optimization costs a revalidation, while claiming it wrongly serves a stale file for as long
82
+ * as the header says, with no way to recall it.
78
83
  */
79
84
  function addressedName(options, entry) {
85
+ if (typeof entry.name !== 'string')
86
+ return false;
80
87
  const template = entry.type === 'asset'
81
88
  ? options.assetFileNames
82
89
  : entry.isEntry === true
@@ -6,7 +6,7 @@ export interface ExecutionCodegenOptions {
6
6
  readonly workerSpecifier?: string;
7
7
  readonly build: Build;
8
8
  readonly frontendRevision?: string;
9
- readonly frontendImmutable?: readonly string[];
9
+ readonly frontendAddressed?: readonly string[];
10
10
  readonly maximumRequestBytes?: number;
11
11
  readonly invocationTimeoutMs?: number;
12
12
  readonly identityIssuer?: string;
@@ -36,7 +36,7 @@ type WorkerEnv = CloudflareEnv & CloudflareAdapterBindings & {
36
36
  type RuntimeEnv = Omit<WorkerEnv, 'ASTRALE_SIGNING_IDENTITY'>
37
37
  ${routerPredicate}
38
38
  export default generatedCloudflareWorkerEntry<WorkerEnv, RuntimeEnv>()({
39
- ${frontend}${options.frontendRevision === undefined ? '' : ` frontendRevision: ${JSON.stringify(options.frontendRevision)},\n`}${options.frontendImmutable === undefined ? '' : ` frontendImmutable: ${JSON.stringify(options.frontendImmutable)},\n`} load: async () => ({
39
+ ${frontend}${options.frontendRevision === undefined ? '' : ` frontendRevision: ${JSON.stringify(options.frontendRevision)},\n`}${options.frontendAddressed === undefined ? '' : ` frontendAddressed: ${JSON.stringify(options.frontendAddressed)},\n`} load: async () => ({
40
40
  runtime: (await import(${JSON.stringify(options.runtimeSpecifier)})).default,
41
41
  material: ${JSON.stringify(generated)} as unknown as GeneratedBuildMaterial,
42
42
  }),
@@ -10,7 +10,7 @@ export declare function prepareExecution(input: {
10
10
  readonly name: string;
11
11
  readonly direct: boolean;
12
12
  readonly frontendRevision?: string;
13
- readonly frontendImmutable?: readonly string[];
13
+ readonly frontendAddressed?: readonly string[];
14
14
  readonly configuredIssuer?: string;
15
15
  readonly router: ReturnType<typeof resolveRouter>;
16
16
  }): Promise<{
@@ -25,9 +25,9 @@ export async function prepareExecution(input) {
25
25
  workerSpecifier,
26
26
  build: input.context.build,
27
27
  ...(input.frontendRevision === undefined ? {} : { frontendRevision: input.frontendRevision }),
28
- ...(input.frontendImmutable === undefined
28
+ ...(input.frontendAddressed === undefined
29
29
  ? {}
30
- : { frontendImmutable: input.frontendImmutable }),
30
+ : { frontendAddressed: input.frontendAddressed }),
31
31
  ...(maximumRequestBytes === undefined ? {} : { maximumRequestBytes }),
32
32
  ...(invocationTimeoutMs === undefined ? {} : { invocationTimeoutMs }),
33
33
  ...(input.params.tokenExchange === undefined
@@ -20,8 +20,8 @@ export interface CloudflareWorkerConfig<RuntimeValue extends Runtime, Bindings e
20
20
  readonly frontend?: BuildMaterial['frontend'];
21
21
  /** Generated browser-file identity used only for deployment propagation checks. */
22
22
  readonly frontendRevision?: string;
23
- /** Served frontend paths the bundler content-addressed; absent means none may be cached forever. */
24
- readonly frontendImmutable?: readonly string[];
23
+ /** Served frontend paths the bundler content-addressed; absent means none may be retained. */
24
+ readonly frontendAddressed?: readonly string[];
25
25
  /** Load the exact Runtime and Build once, after the isolate has accepted a request. */
26
26
  readonly load: () => CloudflareWorkerSource<RuntimeValue, Material> | Promise<CloudflareWorkerSource<RuntimeValue, Material>>;
27
27
  readonly privateKey: JsonWebKey | ((bindings: Bindings) => JsonWebKey);
@@ -87,13 +87,13 @@ function createCloudflareWorkerEntry(restore) {
87
87
  }
88
88
  currentPublicUrl = publicUrl;
89
89
  if (config.frontend !== undefined) {
90
- const frontend = await frontendAsset(config.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendImmutable);
90
+ const frontend = await frontendAsset(config.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
91
91
  if (frontend !== undefined)
92
92
  return withProviderVersion(frontend, bindings);
93
93
  }
94
94
  const { application, build } = await realize();
95
95
  if (config.frontend === undefined) {
96
- const frontend = await frontendAsset(build.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendImmutable);
96
+ const frontend = await frontendAsset(build.frontend, bindings.ASSETS, url, request, config.frontendRevision, config.frontendAddressed);
97
97
  if (frontend !== undefined)
98
98
  return withProviderVersion(frontend, bindings);
99
99
  }
@@ -1,4 +1,4 @@
1
1
  import type { BuildMaterial } from '@astrale-os/sdk/deployment/build/material';
2
2
  import type { Fetcher } from './entry.js';
3
3
  /** Serve public frontend files without realizing the Application's execution Runtime. */
4
- export declare function frontendAsset(frontend: BuildMaterial['frontend'], assets: Fetcher | undefined, url: URL, request: Request, revision?: string, immutable?: readonly string[]): Promise<Response | undefined>;
4
+ export declare function frontendAsset(frontend: BuildMaterial['frontend'], assets: Fetcher | undefined, url: URL, request: Request, revision?: string, addressed?: readonly string[]): Promise<Response | undefined>;
@@ -1,5 +1,5 @@
1
1
  /** Serve public frontend files without realizing the Application's execution Runtime. */
2
- export async function frontendAsset(frontend, assets, url, request, revision, immutable) {
2
+ export async function frontendAsset(frontend, assets, url, request, revision, addressed) {
3
3
  if (frontend === undefined || frontend.source.kind === 'external')
4
4
  return undefined;
5
5
  if (reserved(url.pathname) || (request.method !== 'GET' && request.method !== 'HEAD')) {
@@ -19,13 +19,13 @@ export async function frontendAsset(frontend, assets, url, request, revision, im
19
19
  if (route === undefined) {
20
20
  const response = await assets.fetch(request);
21
21
  return assetPath || response.status !== 404
22
- ? publicAsset(response, revision, cacheForever(url, immutable))
22
+ ? publicAsset(response, revision, retainable(url, addressed))
23
23
  : undefined;
24
24
  }
25
25
  if (route.path !== url.pathname) {
26
26
  const response = await assets.fetch(request);
27
27
  if (response.status !== 404)
28
- return publicAsset(response, revision, cacheForever(url, immutable));
28
+ return publicAsset(response, revision, retainable(url, addressed));
29
29
  }
30
30
  // A route serves its document, never the requested path, so the document decides: it is named by
31
31
  // the route rather than by its bytes, and `publicAsset` already refuses to retain it.
@@ -38,12 +38,12 @@ export async function frontendAsset(frontend, assets, url, request, revision, im
38
38
  * path under the bundle's own directory may still have been copied there verbatim from `public/`,
39
39
  * and a content-addressed output may sit anywhere the project's configuration puts it.
40
40
  */
41
- function cacheForever(url, immutable) {
42
- if (immutable === undefined || immutable.length === 0)
41
+ function retainable(url, addressed) {
42
+ if (addressed === undefined || addressed.length === 0)
43
43
  return false;
44
- return immutable.includes(decodeURIComponent(url.pathname).replace(/^\//u, ''));
44
+ return addressed.includes(decodeURIComponent(url.pathname).replace(/^\//u, ''));
45
45
  }
46
- function publicAsset(response, revision, immutable) {
46
+ function publicAsset(response, revision, addressed) {
47
47
  // Plain Views have an opaque sandbox origin. Their modules, CSS and fonts still need
48
48
  // public reads; this neither enables credentials nor changes execution endpoint CORS.
49
49
  const readable = new Response(response.body, response);
@@ -51,15 +51,22 @@ function publicAsset(response, revision, immutable) {
51
51
  readable.headers.set('Access-Control-Allow-Origin', '*');
52
52
  if (revision !== undefined)
53
53
  readable.headers.set('X-Astrale-Frontend', revision);
54
+ // A 304 states that the copy the browser already holds is current. Answering it with `no-store`
55
+ // would tell that browser to discard what it was just told to keep, so the next visit downloads
56
+ // the file again and every revalidation costs a full transfer: worse than never caching at all.
57
+ const validated = response.status === 304;
54
58
  // Documents select hashed assets. Do not retain stale documents or transient asset misses.
55
- if (!response.ok || response.headers.get('Content-Type')?.split(';')[0] === 'text/html') {
59
+ if ((!response.ok && !validated) ||
60
+ response.headers.get('Content-Type')?.split(';')[0] === 'text/html') {
56
61
  readable.headers.set('Cache-Control', 'no-store');
57
62
  }
58
- else if (immutable) {
63
+ else if (addressed) {
59
64
  // The bytes behind this name can never change, so a revalidation could only ever confirm what
60
- // the name already states. Anything not proved content-addressed keeps the platform default,
61
- // which revalidates and is therefore never wrong, only slower.
62
- readable.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
65
+ // the name already states. `immutable` is deliberately absent: it would also suppress the
66
+ // revalidation a reload performs, which is the one remedy a reader has if this were ever
67
+ // claimed wrongly. Anything not proved content-addressed keeps the platform default, which
68
+ // revalidates and is therefore never wrong, only slower.
69
+ readable.headers.set('Cache-Control', 'public, max-age=31536000');
63
70
  }
64
71
  return readable;
65
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/adapter-cloudflare",
3
- "version": "0.5.0-beta.149",
3
+ "version": "0.5.0-beta.150",
4
4
  "description": "Deploy an Astrale application through Cloudflare Workers",
5
5
  "keywords": [
6
6
  "adapter",
@@ -53,7 +53,7 @@
53
53
  "registry": "https://registry.npmjs.org/"
54
54
  },
55
55
  "dependencies": {
56
- "@astrale-os/sdk": "0.5.0-beta.146",
56
+ "@astrale-os/sdk": "0.5.0-beta.147",
57
57
  "@noble/hashes": "2.3.0",
58
58
  "hono": "^4.6.20",
59
59
  "jose": "^6.1.3",