@samebase/convex-hosting 0.13.1 → 0.15.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +117 -73
  3. package/dist/cli/convex.d.ts +6 -0
  4. package/dist/cli/convex.js +22 -1
  5. package/dist/cli/convex.js.map +1 -1
  6. package/dist/cli/files.js +6 -1
  7. package/dist/cli/files.js.map +1 -1
  8. package/dist/cli/main.js +138 -20
  9. package/dist/cli/main.js.map +1 -1
  10. package/dist/cli/manifest.d.ts +19 -0
  11. package/dist/cli/manifest.js +71 -0
  12. package/dist/cli/manifest.js.map +1 -0
  13. package/dist/cli/site-config.d.ts +32 -0
  14. package/dist/cli/site-config.js +15 -0
  15. package/dist/cli/site-config.js.map +1 -0
  16. package/dist/client/answer.d.ts +16 -0
  17. package/dist/client/answer.js +51 -0
  18. package/dist/client/answer.js.map +1 -0
  19. package/dist/client/index.d.ts +23 -3
  20. package/dist/client/index.js +57 -78
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/manifest.d.ts +68 -0
  23. package/dist/client/manifest.js +48 -0
  24. package/dist/client/manifest.js.map +1 -0
  25. package/dist/client/version-module.d.ts +1 -1
  26. package/dist/component/_generated/component.d.ts +2 -18
  27. package/dist/component/lib.d.ts +15 -46
  28. package/dist/component/lib.js +35 -134
  29. package/dist/component/lib.js.map +1 -1
  30. package/dist/component/schema.d.ts +2 -2
  31. package/dist/component/schema.js +2 -2
  32. package/dist/workers-sdk/packages/workers-shared/asset-worker/src/assets-manifest.d.ts +14 -0
  33. package/dist/workers-sdk/packages/workers-shared/asset-worker/src/assets-manifest.js +84 -0
  34. package/dist/workers-sdk/packages/workers-shared/asset-worker/src/assets-manifest.js.map +1 -0
  35. package/package.json +7 -6
@@ -1,25 +1,51 @@
1
1
  import "./runtime.js";
2
2
  import { httpActionGeneric, ROUTABLE_HTTP_METHODS, } from "convex/server";
3
+ import { answer } from "./answer.js";
4
+ import { ASSETS_FORMAT, MARK_HEADER } from "./manifest.js";
3
5
  /**
4
- * The HTTP action that serves the site, for `routeAll`: it answers a request with the file of
5
- * version `versionId` (another method than GET or HEAD on a file answers 405), else with what
6
- * `worker` answers, else 404. `worker` is the Worker script, the object with `fetch(request)`
7
- * that the app's server entry exports: the code that runs on a miss. As on Cloudflare, the presence
8
- * of code decides how `not_found_handling` applies: with `worker`, it applies to navigations only
9
- * and `worker` answers the other misses; without it, the site is files only, so it applies to every
10
- * request.
6
+ * The HTTP action that serves the site, for `routeAll`: it answers a request with an asset (another
7
+ * method than GET or HEAD on an asset answers 405), else with what `worker` answers, else 404. As on
8
+ * Cloudflare, the presence of a Worker script decides how `not_found_handling` applies: with
9
+ * `worker`, it applies to navigations only and `worker` answers the other misses; without it, it
10
+ * applies to every request.
11
+ *
12
+ * The assets are in the pushed code, so a request calls no function of the component: a file costs
13
+ * this action and the download of its bytes, any other answer this action alone.
11
14
  *
12
15
  * A page and a 404 answer `Cache-Control: no-store`. The deployment's Cloudflare front keeps that
13
16
  * for a path without an extension and replaces it with a four-hour browser `max-age` for a path
14
17
  * with one, so a browser that asks for a script before its upload ended keeps the 404; the deploy
15
18
  * order prevents that case, because no page refers to a file that is not uploaded yet.
16
19
  */
17
- function serveSite(component, versionId, worker) {
20
+ function serveSite({ component, assets, worker }) {
18
21
  return httpActionGeneric(async (ctx, request) => {
19
- const asset = await answer(ctx, component, request, versionId, worker !== undefined);
22
+ // Read first: a mark says when its request reached this code.
23
+ const arrivedAt = Date.now();
24
+ if (assets.format !== ASSETS_FORMAT) {
25
+ return notServed(500, `.convex-hosting/version.js has format ${assets.format}, and this release of ` +
26
+ `@samebase/convex-hosting reads format ${ASSETS_FORMAT}. Deploy again.`);
27
+ }
28
+ if (request.headers.get(MARK_HEADER) === assets.markToken) {
29
+ const result = await ctx.runMutation(component.lib.markLive, {
30
+ versionId: assets.versionId,
31
+ arrivedAt,
32
+ });
33
+ return Response.json({ versionId: assets.versionId, result }, { headers: NO_STORE });
34
+ }
35
+ const { files } = assets;
36
+ // A first install pushes before its upload; the deploy pushes again when the upload ends.
37
+ if (files === null) {
38
+ return notServed(404, `Version ${assets.versionId} of the site is not uploaded yet.`);
39
+ }
40
+ // The storage URLs name the deployment they were taken from.
41
+ if (files.cloudUrl !== process.env.CONVEX_CLOUD_URL) {
42
+ return notServed(404, `Version ${assets.versionId} of the site was uploaded to ${files.cloudUrl}, not to this ` +
43
+ `deployment. Deploy to this deployment with convex-hosting deploy.`);
44
+ }
45
+ const asset = await answer(files, request, worker !== undefined);
20
46
  if (asset !== null)
21
- return asset;
22
- // `route` answers every files-only request itself, so this 404 only fails closed.
47
+ return await withBody(asset);
48
+ // `answer` answers every assets-only request itself, so this 404 only fails closed.
23
49
  if (worker === undefined)
24
50
  return new Response(null, { status: 404, headers: NO_STORE });
25
51
  const page = await worker.fetch(request);
@@ -33,87 +59,39 @@ function serveSite(component, versionId, worker) {
33
59
  * every path, as every request reaches a Cloudflare Worker with static assets. Convex answers HEAD
34
60
  * through GET.
35
61
  *
62
+ * ```ts
63
+ * routeAll({ http, component: components.hosting, assets, worker: server });
64
+ * ```
65
+ *
36
66
  * Returns the action, for the paths that a component's HTTP prefix would otherwise take: Convex
37
67
  * picks an exact route first, then the longest prefix, so an app whose session routes sit under a
38
68
  * component's prefix registers them with `http.route({ path, method, handler })`.
39
69
  */
40
- export function routeAll(http, component, versionId, worker) {
41
- const site = serveSite(component, versionId, worker);
70
+ export function routeAll(options) {
71
+ const action = serveSite(options);
42
72
  for (const method of ROUTABLE_HTTP_METHODS) {
43
- http.route({ pathPrefix: "/", method, handler: site });
73
+ options.http.route({ pathPrefix: "/", method, handler: action });
44
74
  }
45
- return site;
75
+ return action;
46
76
  }
47
- /**
48
- * Answers `request` with the asset of version `versionId` that Cloudflare's asset handler picks,
49
- * or returns `null` when no asset matches. `hasUserWorker` is Cloudflare's `has_user_worker`.
50
- *
51
- * A version that does not exist, is still uploading, or is being deleted answers 404, so a
52
- * half-installed site does not render pages without their files. The first request for a `ready`
53
- * version marks it live, which deletes the versions that were live before it; a version older than
54
- * the live one is never marked.
55
- */
56
- async function answer(ctx, component, request, versionId, hasUserWorker) {
57
- const { state, asset } = await ctx.runQuery(component.lib.route, {
58
- versionId,
59
- method: request.method,
60
- url: request.url,
61
- headers: handlerHeaders(request.headers),
62
- hasUserWorker,
63
- });
64
- if (state === null || state === "uploading" || state === "deleting") {
65
- const problem = state === null
66
- ? "does not exist"
67
- : state === "uploading"
68
- ? "is still uploading"
69
- : "is being deleted";
70
- return new Response(`Version ${versionId} of the site ${problem}.`, {
71
- status: 404,
72
- headers: NO_STORE,
73
- });
74
- }
75
- // Only the pushed code knows which version the deployment runs, so its first request reports it.
76
- if (state === "ready") {
77
- await ctx.runMutation(component.lib.markLive, { versionId });
78
- }
79
- if (asset === null)
80
- return null;
81
- const headers = new Headers();
82
- for (const { name, value } of asset.headers)
83
- headers.append(name, value);
84
- if (asset.bodyUrl === null)
85
- return new Response(null, { status: asset.status, headers });
77
+ /** The response of `asset`, with the stored file as its body when it has one. */
78
+ async function withBody(asset) {
79
+ if (asset.url === null)
80
+ return new Response(null, { status: asset.status, headers: asset.headers });
86
81
  // An app cannot read a component's storage, so the body comes from the storage URL.
87
82
  let stored;
88
83
  try {
89
- stored = await fetch(asset.bodyUrl);
84
+ stored = await fetch(asset.url);
90
85
  }
91
86
  catch {
92
- return badGateway();
87
+ return new Response(null, { status: 502, headers: NO_STORE });
93
88
  }
94
- // A deploy deleted the file between the query and the fetch.
89
+ // A deploy deleted the file: this code was replaced, and a later version's mark deleted it.
95
90
  if (stored.status === 404)
96
91
  return new Response(null, { status: 404, headers: NO_STORE });
97
92
  if (!stored.ok)
98
- return badGateway();
99
- return new Response(stored.body, { status: asset.status, headers });
100
- }
101
- /**
102
- * The request headers that Cloudflare's handler reads. It reads only the presence of
103
- * `Authorization` and `Range`, so their values (a credential, for one) stay out of the query.
104
- */
105
- function handlerHeaders(headers) {
106
- const picked = {};
107
- for (const name of ["if-none-match", "sec-fetch-mode"]) {
108
- const value = headers.get(name);
109
- if (value !== null)
110
- picked[name] = value;
111
- }
112
- for (const name of ["authorization", "range"]) {
113
- if (headers.has(name))
114
- picked[name] = "";
115
- }
116
- return picked;
93
+ return new Response(null, { status: 502, headers: NO_STORE });
94
+ return new Response(stored.body, { status: asset.status, headers: asset.headers });
117
95
  }
118
96
  /**
119
97
  * For the answers that no uploaded file gives. The deployment's Cloudflare front caches 404 answers
@@ -121,7 +99,8 @@ function handlerHeaders(headers) {
121
99
  * missing for hours (the 0.3.2 incident).
122
100
  */
123
101
  const NO_STORE = { "Cache-Control": "no-store" };
124
- function badGateway() {
125
- return new Response(null, { status: 502, headers: NO_STORE });
102
+ /** A site that cannot serve this request, named in the body. */
103
+ function notServed(status, message) {
104
+ return new Response(message, { status, headers: NO_STORE });
126
105
  }
127
106
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import type { NormalizedAssetConfig } from "../workers-sdk/packages/workers-shared/asset-worker/src/types.js";
2
+ /**
3
+ * The format of `Assets`. `serveSite` refuses assets of another format, which only a deploy with
4
+ * another release of this package writes.
5
+ */
6
+ export declare const ASSETS_FORMAT = 1;
7
+ /**
8
+ * The header with which the CLI, after the push, asks the served code to mark its version live. Its
9
+ * value is the site's `markToken`; a request without it is an ordinary request.
10
+ */
11
+ export declare const MARK_HEADER = "Convex-Hosting-Mark";
12
+ /** The path of that request, which names it in logs; the header alone decides. */
13
+ export declare const MARK_PATH = "/.convex-hosting/mark";
14
+ /**
15
+ * The static assets of one version, as `.convex-hosting/version.js` exports them under `assets`,
16
+ * Cloudflare's name for them: the version's id, and everything that serving reads, so that a
17
+ * request reads no database. `files` is `null` in the code that a first install pushes before its
18
+ * upload, which serves nothing.
19
+ */
20
+ export type Assets = {
21
+ /** `ASSETS_FORMAT` of the release that wrote it. */
22
+ format: number;
23
+ versionId: string;
24
+ /**
25
+ * The CLI sends it after the push, so that only the CLI makes the served code mark its version
26
+ * live; another request with the header pays nothing more than any other request.
27
+ */
28
+ markToken: string;
29
+ files: AssetFiles | null;
30
+ };
31
+ export type AssetFiles = {
32
+ /** The deployment's `CONVEX_CLOUD_URL` at the upload: the origin of every storage URL. */
33
+ cloudUrl: string;
34
+ /** Cloudflare's normalized `AssetConfig` for the version, as its handler takes it. */
35
+ config: NormalizedAssetConfig;
36
+ /**
37
+ * Cloudflare's asset manifest, in base64, which Cloudflare's own reader searches. The tail of an
38
+ * entry, which Cloudflare leaves zero, holds the indexes of its URL and its content type.
39
+ */
40
+ manifest: string;
41
+ contentTypes: string[];
42
+ /**
43
+ * The storage URL of each stored file is `urlPrefix`, its key, and `urlSuffix`. The keys are one
44
+ * string, `urlKeyWidth` characters each, padded with spaces: Convex evaluates the module for
45
+ * every request, and one string costs less than an array of 100,000.
46
+ */
47
+ urlPrefix: string;
48
+ urlSuffix: string;
49
+ urlKeys: string;
50
+ urlKeyWidth: number;
51
+ };
52
+ /** Where an entry's tail, after the content hash, keeps the index of its URL (a big-endian u32). */
53
+ export declare const TAIL_URL = 0;
54
+ /** Where the tail keeps the index of the content type (a big-endian u16). */
55
+ export declare const TAIL_CONTENT_TYPE = 4;
56
+ /** The content type index of an entry without a content type. */
57
+ export declare const NO_CONTENT_TYPE = 65535;
58
+ export type ManifestHit = {
59
+ /** The hex of the content hash, which is the ETag, as on Cloudflare. */
60
+ eTag: string;
61
+ url: string;
62
+ contentType: string | undefined;
63
+ };
64
+ /**
65
+ * Decodes the manifest once, like Cloudflare's `AssetsManifest`, and returns its lookup: it finds the
66
+ * entry of a path with Cloudflare's binary search, or returns `null`.
67
+ */
68
+ export declare function manifestLookup(files: AssetFiles): (path: string) => Promise<ManifestHit | null>;
@@ -0,0 +1,48 @@
1
+ import { binarySearch, hashPath, } from "../workers-sdk/packages/workers-shared/asset-worker/src/assets-manifest.js";
2
+ import { CONTENT_HASH_SIZE } from "../workers-sdk/packages/workers-shared/utils/constants.js";
3
+ /**
4
+ * The format of `Assets`. `serveSite` refuses assets of another format, which only a deploy with
5
+ * another release of this package writes.
6
+ */
7
+ export const ASSETS_FORMAT = 1;
8
+ /**
9
+ * The header with which the CLI, after the push, asks the served code to mark its version live. Its
10
+ * value is the site's `markToken`; a request without it is an ordinary request.
11
+ */
12
+ export const MARK_HEADER = "Convex-Hosting-Mark";
13
+ /** The path of that request, which names it in logs; the header alone decides. */
14
+ export const MARK_PATH = "/.convex-hosting/mark";
15
+ /** Where an entry's tail, after the content hash, keeps the index of its URL (a big-endian u32). */
16
+ export const TAIL_URL = 0;
17
+ /** Where the tail keeps the index of the content type (a big-endian u16). */
18
+ export const TAIL_CONTENT_TYPE = 4;
19
+ /** The content type index of an entry without a content type. */
20
+ export const NO_CONTENT_TYPE = 0xffff;
21
+ /**
22
+ * Decodes the manifest once, like Cloudflare's `AssetsManifest`, and returns its lookup: it finds the
23
+ * entry of a path with Cloudflare's binary search, or returns `null`.
24
+ */
25
+ export function manifestLookup(files) {
26
+ const manifest = fromBase64(files.manifest);
27
+ return async (path) => {
28
+ const contentHash = binarySearch(manifest, await hashPath(path));
29
+ if (contentHash === false)
30
+ return null;
31
+ const tail = new DataView(contentHash.buffer, contentHash.byteOffset + CONTENT_HASH_SIZE);
32
+ const key = tail.getUint32(TAIL_URL) * files.urlKeyWidth;
33
+ const contentType = tail.getUint16(TAIL_CONTENT_TYPE);
34
+ return {
35
+ eTag: Array.from(contentHash, (byte) => byte.toString(16).padStart(2, "0")).join(""),
36
+ url: files.urlPrefix +
37
+ files.urlKeys.slice(key, key + files.urlKeyWidth).trimEnd() +
38
+ files.urlSuffix,
39
+ contentType: contentType === NO_CONTENT_TYPE ? undefined : files.contentTypes[contentType],
40
+ };
41
+ };
42
+ }
43
+ /** Convex's runtime decodes base64 natively; Node before 25, where tests run, does not. */
44
+ function fromBase64(text) {
45
+ const decoded = Uint8Array.fromBase64?.(text);
46
+ return decoded ?? Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
47
+ }
48
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/client/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,QAAQ,GACT,MAAM,4EAA4E,CAAC;AAEpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,2DAA2D,CAAC;AAE9F;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC;AAE/B;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAEjD,kFAAkF;AAClF,MAAM,CAAC,MAAM,SAAS,GAAG,uBAAuB,CAAC;AA0CjD,oGAAoG;AACpG,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC;AAC1B,6EAA6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,iEAAiE;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AAStC;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,KAAiB;IAC9C,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC5C,OAAO,KAAK,EAAE,IAAY,EAA+B,EAAE;QACzD,MAAM,WAAW,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACjE,IAAI,WAAW,KAAK,KAAK;YAAE,OAAO,IAAI,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,UAAU,GAAG,iBAAiB,CAAC,CAAC;QAC1F,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC;QACzD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QACtD,OAAO;YACL,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACpF,GAAG,EACD,KAAK,CAAC,SAAS;gBACf,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,OAAO,EAAE;gBAC3D,KAAK,CAAC,SAAS;YACjB,WAAW,EAAE,WAAW,KAAK,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC;SAC3F,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,2FAA2F;AAC3F,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,OAAO,GAAI,UAA4D,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;IACjG,OAAO,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC"}
@@ -2,5 +2,5 @@
2
2
  // fresh clone, before its first deploy. Script file on purpose: a wildcard module name is only allowed
3
3
  // in an ambient context.
4
4
  declare module "*/.convex-hosting/version.js" {
5
- export const versionId: string;
5
+ export const assets: import("./manifest.js").Assets;
6
6
  }
@@ -21,24 +21,8 @@ import type { FunctionReference } from "convex/server";
21
21
  export type ComponentApi<Name extends string | undefined = string | undefined> = {
22
22
  lib: {
23
23
  markLive: FunctionReference<"mutation", "internal", {
24
+ arrivedAt: number;
24
25
  versionId: string;
25
- }, null, Name>;
26
- route: FunctionReference<"query", "internal", {
27
- hasUserWorker: boolean;
28
- headers: Record<string, string>;
29
- method: string;
30
- url: string;
31
- versionId: string;
32
- }, {
33
- asset: null | {
34
- bodyUrl: string | null;
35
- headers: Array<{
36
- name: string;
37
- value: string;
38
- }>;
39
- status: number;
40
- };
41
- state: "uploading" | "ready" | "live" | "deleting" | null;
42
- }, Name>;
26
+ }, "marked" | "live" | "superseded" | "uploading" | "deleting" | "missing", Name>;
43
27
  };
44
28
  };
@@ -95,20 +95,25 @@ export declare const finishVersion: import("convex/server").RegisteredMutation<"
95
95
  versionId: string;
96
96
  }, Promise<null>>;
97
97
  /**
98
- * Marks a `ready` version `live` and schedules `collect`. `serveSite` calls it at each request that
99
- * finds the version `ready`. An older upload never goes live over a newer one: the mark does nothing
100
- * when a live version was created after this one. So a request that started on replaced code, and
101
- * reaches its lookup or its mark after the new code's first request marked the new version, cannot
102
- * make the old version newest, and `collect` cannot delete the new version's files. A version that
103
- * is already live stays as it is, so concurrent first requests are fine.
98
+ * Marks a `ready` version `live` and schedules `collect`, and says what it did. `serveSite` calls
99
+ * it for the request that the CLI sends after the push, with the site's mark token, so only the
100
+ * code that serves marks its own version. A version that is already live stays as it is, so a
101
+ * repeated mark is fine. It never throws, so the CLI reads every outcome from the answer.
104
102
  *
105
- * Precondition: deploys to one deployment do not overlap. When two overlap and the older upload is
106
- * pushed last, its requests look like requests of replaced code: its version serves but stays
107
- * `ready`, and nothing is deleted until the next deploy.
103
+ * The request may still run when its code is replaced, so two rules refuse a mark of replaced code.
104
+ * An older upload never goes live over a newer one: the mark does nothing when a live version was
105
+ * created after this one. And a mark is late when another version went live after its request
106
+ * arrived, at `arrivedAt`: that version's code has served since. `arrivedAt` is the HTTP action's
107
+ * clock and the live time is this mutation's, so the rule holds as far as the two agree.
108
+ *
109
+ * Precondition: deploys to one deployment do not overlap. When two overlap, the mark of the
110
+ * version that serves may be refused: it then stays `ready`, and nothing is deleted until the next
111
+ * deploy. The limits are in `docs/design.md`, "Stated, not solved".
108
112
  */
109
113
  export declare const markLive: import("convex/server").RegisteredMutation<"public", {
114
+ arrivedAt: number;
110
115
  versionId: string;
111
- }, Promise<null>>;
116
+ }, Promise<"deleting" | "live" | "marked" | "missing" | "superseded" | "uploading">>;
112
117
  /**
113
118
  * One chain of calls, one batch per call, each scheduling the next with `runAfter(0)` while work
114
119
  * remains. First the `_storage` files that no entry references and that are older than
@@ -146,40 +151,4 @@ export declare const collect: import("convex/server").RegisteredMutation<"intern
146
151
  kind: "versions";
147
152
  };
148
153
  }, Promise<null>>;
149
- /**
150
- * Runs Cloudflare's asset handler for one request against one version. `state` is the kind of the
151
- * version's state, or `null` when the version does not exist. `asset` is `null` when the version is
152
- * uploading, being deleted, or does not exist, or when no asset answers.
153
- *
154
- * `hasUserWorker` is Cloudflare's `has_user_worker`: whether the app has code that answers a miss.
155
- * With it, Cloudflare's router asks `canFetch` first, which keeps `not_found_handling` for
156
- * navigations only, and the app answers the other misses. Without it, every request goes to
157
- * `handleRequest`, so `not_found_handling` applies to all of them, and an asset always answers.
158
- */
159
- export declare const route: import("convex/server").RegisteredQuery<"public", {
160
- hasUserWorker: boolean;
161
- headers: Record<string, string>;
162
- method: string;
163
- url: string;
164
- versionId: string;
165
- }, Promise<{
166
- state: null;
167
- asset: null;
168
- } | {
169
- state: "deleting" | "uploading";
170
- asset: null;
171
- } | {
172
- state: "live" | "ready";
173
- asset: null;
174
- } | {
175
- state: "live" | "ready";
176
- asset: {
177
- status: number;
178
- headers: {
179
- name: string;
180
- value: string;
181
- }[];
182
- bodyUrl: string | null;
183
- };
184
- }>>;
185
154
  export {};