@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.
- package/CHANGELOG.md +39 -0
- package/README.md +117 -73
- package/dist/cli/convex.d.ts +6 -0
- package/dist/cli/convex.js +22 -1
- package/dist/cli/convex.js.map +1 -1
- package/dist/cli/files.js +6 -1
- package/dist/cli/files.js.map +1 -1
- package/dist/cli/main.js +138 -20
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/manifest.d.ts +19 -0
- package/dist/cli/manifest.js +71 -0
- package/dist/cli/manifest.js.map +1 -0
- package/dist/cli/site-config.d.ts +32 -0
- package/dist/cli/site-config.js +15 -0
- package/dist/cli/site-config.js.map +1 -0
- package/dist/client/answer.d.ts +16 -0
- package/dist/client/answer.js +51 -0
- package/dist/client/answer.js.map +1 -0
- package/dist/client/index.d.ts +23 -3
- package/dist/client/index.js +57 -78
- package/dist/client/index.js.map +1 -1
- package/dist/client/manifest.d.ts +68 -0
- package/dist/client/manifest.js +48 -0
- package/dist/client/manifest.js.map +1 -0
- package/dist/client/version-module.d.ts +1 -1
- package/dist/component/_generated/component.d.ts +2 -18
- package/dist/component/lib.d.ts +15 -46
- package/dist/component/lib.js +35 -134
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +2 -2
- package/dist/component/schema.js +2 -2
- package/dist/workers-sdk/packages/workers-shared/asset-worker/src/assets-manifest.d.ts +14 -0
- package/dist/workers-sdk/packages/workers-shared/asset-worker/src/assets-manifest.js +84 -0
- package/dist/workers-sdk/packages/workers-shared/asset-worker/src/assets-manifest.js.map +1 -0
- package/package.json +7 -6
package/dist/client/index.js
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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,
|
|
20
|
+
function serveSite({ component, assets, worker }) {
|
|
18
21
|
return httpActionGeneric(async (ctx, request) => {
|
|
19
|
-
|
|
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
|
-
// `
|
|
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(
|
|
41
|
-
const
|
|
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:
|
|
73
|
+
options.http.route({ pathPrefix: "/", method, handler: action });
|
|
44
74
|
}
|
|
45
|
-
return
|
|
75
|
+
return action;
|
|
46
76
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
84
|
+
stored = await fetch(asset.url);
|
|
90
85
|
}
|
|
91
86
|
catch {
|
|
92
|
-
return
|
|
87
|
+
return new Response(null, { status: 502, headers: NO_STORE });
|
|
93
88
|
}
|
|
94
|
-
// A deploy deleted the file
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
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
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AACtB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,
|
|
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
|
|
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
|
-
},
|
|
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
|
};
|
package/dist/component/lib.d.ts
CHANGED
|
@@ -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
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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<
|
|
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 {};
|