@samebase/convex-hosting 0.3.1 → 0.4.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 +10 -128
- package/README.md +136 -398
- package/dist/cli/convex.d.ts +63 -0
- package/dist/cli/convex.js +108 -0
- package/dist/cli/convex.js.map +1 -0
- package/dist/cli/files.d.ts +18 -0
- package/dist/cli/files.js +106 -0
- package/dist/cli/files.js.map +1 -0
- package/dist/cli/index.d.ts +1 -11
- package/dist/cli/index.js +3 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/main.d.ts +5 -0
- package/dist/cli/main.js +346 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/client/index.d.ts +8 -115
- package/dist/client/index.js +65 -254
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +5 -0
- package/dist/client/version-module.d.ts +6 -0
- package/dist/component/_generated/api.d.ts +0 -5
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +20 -18
- package/dist/component/_generated/dataModel.d.ts +0 -1
- package/dist/component/_generated/server.d.ts +12 -1
- package/dist/component/_generated/server.js +2 -0
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/convex.config.d.ts +0 -1
- package/dist/component/lib.d.ts +175 -156
- package/dist/component/lib.js +396 -710
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +76 -72
- package/dist/component/schema.js +35 -59
- package/dist/component/schema.js.map +1 -1
- package/dist/workers-shared/asset-worker/src/analytics.d.ts +39 -0
- package/dist/workers-shared/asset-worker/src/analytics.js +86 -0
- package/dist/workers-shared/asset-worker/src/analytics.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/compatibility-flags.d.ts +19 -0
- package/dist/workers-shared/asset-worker/src/compatibility-flags.js +29 -0
- package/dist/workers-shared/asset-worker/src/compatibility-flags.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/configuration.d.ts +3 -0
- package/dist/workers-shared/asset-worker/src/configuration.js +30 -0
- package/dist/workers-shared/asset-worker/src/configuration.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/constants.d.ts +1 -0
- package/dist/workers-shared/asset-worker/src/constants.js +3 -0
- package/dist/workers-shared/asset-worker/src/constants.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/handler.d.ts +28 -0
- package/dist/workers-shared/asset-worker/src/handler.js +717 -0
- package/dist/workers-shared/asset-worker/src/handler.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/types.d.ts +13 -0
- package/dist/workers-shared/asset-worker/src/types.js +2 -0
- package/dist/workers-shared/asset-worker/src/types.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/utils/base-path.d.ts +29 -0
- package/dist/workers-shared/asset-worker/src/utils/base-path.js +56 -0
- package/dist/workers-shared/asset-worker/src/utils/base-path.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/utils/headers.d.ts +11 -0
- package/dist/workers-shared/asset-worker/src/utils/headers.js +77 -0
- package/dist/workers-shared/asset-worker/src/utils/headers.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/utils/rules-engine.d.ts +23 -0
- package/dist/workers-shared/asset-worker/src/utils/rules-engine.js +133 -0
- package/dist/workers-shared/asset-worker/src/utils/rules-engine.js.map +1 -0
- package/dist/workers-shared/asset-worker/src/worker.d.ts +14 -0
- package/dist/workers-shared/asset-worker/src/worker.js +12 -0
- package/dist/workers-shared/asset-worker/src/worker.js.map +1 -0
- package/dist/workers-shared/utils/base-path.d.ts +32 -0
- package/dist/workers-shared/utils/base-path.js +94 -0
- package/dist/workers-shared/utils/base-path.js.map +1 -0
- package/dist/workers-shared/utils/configuration/constants.d.ts +15 -0
- package/dist/workers-shared/utils/configuration/constants.js +16 -0
- package/dist/workers-shared/utils/configuration/constants.js.map +1 -0
- package/dist/workers-shared/utils/configuration/constructConfiguration.d.ts +12 -0
- package/dist/workers-shared/utils/configuration/constructConfiguration.js +103 -0
- package/dist/workers-shared/utils/configuration/constructConfiguration.js.map +1 -0
- package/dist/workers-shared/utils/configuration/parseHeaders.d.ts +5 -0
- package/dist/workers-shared/utils/configuration/parseHeaders.js +181 -0
- package/dist/workers-shared/utils/configuration/parseHeaders.js.map +1 -0
- package/dist/workers-shared/utils/configuration/parseRedirects.d.ts +8 -0
- package/dist/workers-shared/utils/configuration/parseRedirects.js +129 -0
- package/dist/workers-shared/utils/configuration/parseRedirects.js.map +1 -0
- package/dist/workers-shared/utils/configuration/parseStaticRouting.d.ts +2 -0
- package/dist/workers-shared/utils/configuration/parseStaticRouting.js +94 -0
- package/dist/workers-shared/utils/configuration/parseStaticRouting.js.map +1 -0
- package/dist/workers-shared/utils/configuration/types.d.ts +38 -0
- package/dist/workers-shared/utils/configuration/types.js +2 -0
- package/dist/workers-shared/utils/configuration/types.js.map +1 -0
- package/dist/workers-shared/utils/configuration/validateURL.d.ts +3 -0
- package/dist/workers-shared/utils/configuration/validateURL.js +56 -0
- package/dist/workers-shared/utils/configuration/validateURL.js.map +1 -0
- package/dist/workers-shared/utils/constants.d.ts +28 -0
- package/dist/workers-shared/utils/constants.js +34 -0
- package/dist/workers-shared/utils/constants.js.map +1 -0
- package/dist/workers-shared/utils/helpers.d.ts +22 -0
- package/dist/workers-shared/utils/helpers.js +73 -0
- package/dist/workers-shared/utils/helpers.js.map +1 -0
- package/dist/workers-shared/utils/responses.d.ts +43 -0
- package/dist/workers-shared/utils/responses.js +125 -0
- package/dist/workers-shared/utils/responses.js.map +1 -0
- package/dist/workers-shared/utils/tracing.d.ts +3 -0
- package/dist/workers-shared/utils/tracing.js +29 -0
- package/dist/workers-shared/utils/tracing.js.map +1 -0
- package/dist/workers-shared/utils/types.d.ts +131 -0
- package/dist/workers-shared/utils/types.js +72 -0
- package/dist/workers-shared/utils/types.js.map +1 -0
- package/package.json +16 -32
- package/INTEGRATION.md +0 -399
- package/dist/cli/args.d.ts +0 -28
- package/dist/cli/args.d.ts.map +0 -1
- package/dist/cli/args.js +0 -148
- package/dist/cli/args.js.map +0 -1
- package/dist/cli/argumentChunks.d.ts +0 -3
- package/dist/cli/argumentChunks.d.ts.map +0 -1
- package/dist/cli/argumentChunks.js +0 -29
- package/dist/cli/argumentChunks.js.map +0 -1
- package/dist/cli/collectFiles.d.ts +0 -10
- package/dist/cli/collectFiles.d.ts.map +0 -1
- package/dist/cli/collectFiles.js +0 -53
- package/dist/cli/collectFiles.js.map +0 -1
- package/dist/cli/commands.d.ts +0 -8
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -56
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/componentName.d.ts +0 -32
- package/dist/cli/componentName.d.ts.map +0 -1
- package/dist/cli/componentName.js +0 -43
- package/dist/cli/componentName.js.map +0 -1
- package/dist/cli/deploy.d.ts +0 -16
- package/dist/cli/deploy.d.ts.map +0 -1
- package/dist/cli/deploy.js +0 -283
- package/dist/cli/deploy.js.map +0 -1
- package/dist/cli/deployEnvironment.d.ts +0 -10
- package/dist/cli/deployEnvironment.d.ts.map +0 -1
- package/dist/cli/deployEnvironment.js +0 -12
- package/dist/cli/deployEnvironment.js.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/upload.d.ts +0 -22
- package/dist/cli/upload.d.ts.map +0 -1
- package/dist/cli/upload.js +0 -630
- package/dist/cli/upload.js.map +0 -1
- package/dist/client/_generated/_ignore.d.ts +0 -1
- package/dist/client/_generated/_ignore.d.ts.map +0 -1
- package/dist/client/_generated/_ignore.js +0 -3
- package/dist/client/_generated/_ignore.js.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/component/_generated/api.d.ts.map +0 -1
- package/dist/component/_generated/component.d.ts.map +0 -1
- package/dist/component/_generated/dataModel.d.ts.map +0 -1
- package/dist/component/_generated/server.d.ts.map +0 -1
- package/dist/component/convex.config.d.ts.map +0 -1
- package/dist/component/http.d.ts +0 -4
- package/dist/component/http.d.ts.map +0 -1
- package/dist/component/http.js +0 -125
- package/dist/component/http.js.map +0 -1
- package/dist/component/lib.d.ts.map +0 -1
- package/dist/component/schema.d.ts.map +0 -1
- package/dist/component/serving.d.ts +0 -17
- package/dist/component/serving.d.ts.map +0 -1
- package/dist/component/serving.js +0 -150
- package/dist/component/serving.js.map +0 -1
- package/dist/react/index.d.ts +0 -59
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -137
- package/dist/react/index.js.map +0 -1
- package/src/cli/args.test.ts +0 -79
- package/src/cli/args.ts +0 -153
- package/src/cli/argumentChunks.test.ts +0 -27
- package/src/cli/argumentChunks.ts +0 -40
- package/src/cli/collectFiles.test.ts +0 -26
- package/src/cli/collectFiles.ts +0 -59
- package/src/cli/commands.ts +0 -68
- package/src/cli/componentName.test.ts +0 -41
- package/src/cli/componentName.ts +0 -46
- package/src/cli/deploy.ts +0 -336
- package/src/cli/deployEnvironment.test.ts +0 -27
- package/src/cli/deployEnvironment.ts +0 -17
- package/src/cli/index.ts +0 -65
- package/src/cli/upload.ts +0 -837
- package/src/client/_generated/_ignore.ts +0 -1
- package/src/client/index.test.ts +0 -510
- package/src/client/index.ts +0 -355
- package/src/client/setup.test.ts +0 -22
- package/src/component/_generated/api.ts +0 -54
- package/src/component/_generated/component.ts +0 -55
- package/src/component/_generated/dataModel.ts +0 -60
- package/src/component/_generated/server.ts +0 -156
- package/src/component/convex.config.ts +0 -3
- package/src/component/http.test.ts +0 -223
- package/src/component/http.ts +0 -144
- package/src/component/lib.test.ts +0 -1007
- package/src/component/lib.ts +0 -862
- package/src/component/schema.ts +0 -75
- package/src/component/serving.ts +0 -158
- package/src/component/setup.test.ts +0 -11
- package/src/react/index.tsx +0 -197
- package/src/test.ts +0 -23
package/src/component/lib.ts
DELETED
|
@@ -1,862 +0,0 @@
|
|
|
1
|
-
import { v, type Infer } from "convex/values";
|
|
2
|
-
import {
|
|
3
|
-
internalMutation,
|
|
4
|
-
internalQuery,
|
|
5
|
-
type MutationCtx,
|
|
6
|
-
query,
|
|
7
|
-
type QueryCtx,
|
|
8
|
-
} from "./_generated/server.js";
|
|
9
|
-
import type { Id } from "./_generated/dataModel.js";
|
|
10
|
-
import { hasFileExtension, isHtmlContentType } from "./serving.js";
|
|
11
|
-
|
|
12
|
-
const staticAssetValidator = v.object({
|
|
13
|
-
_id: v.id("staticAssets"),
|
|
14
|
-
_creationTime: v.number(),
|
|
15
|
-
path: v.string(),
|
|
16
|
-
storageId: v.optional(v.id("_storage")),
|
|
17
|
-
blobId: v.optional(v.string()),
|
|
18
|
-
contentType: v.string(),
|
|
19
|
-
deploymentId: v.string(),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const resolvedAssetValidator = v.union(
|
|
23
|
-
staticAssetValidator,
|
|
24
|
-
staticAssetValidator.omit("_id").extend({
|
|
25
|
-
_id: v.id("retiredAssets"),
|
|
26
|
-
storageId: v.id("_storage"),
|
|
27
|
-
expiresAt: v.number(),
|
|
28
|
-
}),
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const deploymentInfoValidator = v.object({
|
|
32
|
-
_id: v.id("deploymentInfo"),
|
|
33
|
-
_creationTime: v.number(),
|
|
34
|
-
currentDeploymentId: v.string(),
|
|
35
|
-
deployedAt: v.number(),
|
|
36
|
-
spaFallback: v.optional(v.boolean()),
|
|
37
|
-
pendingBlobCleanupCount: v.optional(v.number()),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const MAX_ASSETS_PER_DEPLOYMENT = 1800;
|
|
41
|
-
const MAX_PUBLISH_MANIFEST_READS = 3800;
|
|
42
|
-
const MAX_PUBLISH_DOCUMENT_WRITES = 15500;
|
|
43
|
-
const MAX_MANIFEST_SERIALIZED_BYTES = 2 * 1024 * 1024;
|
|
44
|
-
const MAINTENANCE_PAGE_LIMIT = 256;
|
|
45
|
-
// The live manifest plus this scan is at most 3,800 documents. Remaining
|
|
46
|
-
// staged lookups use an explicit budget below Convex's 4,096 read limit.
|
|
47
|
-
const STORAGE_SCAN_LIMIT = 2000;
|
|
48
|
-
const ABANDONED_UPLOAD_AGE_MS = 24 * 60 * 60 * 1000;
|
|
49
|
-
const RETIRED_ASSET_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
50
|
-
|
|
51
|
-
const httpAssetValidator = v.object({
|
|
52
|
-
storageUrl: v.optional(v.string()),
|
|
53
|
-
blobId: v.optional(v.string()),
|
|
54
|
-
contentType: v.string(),
|
|
55
|
-
etag: v.optional(v.string()),
|
|
56
|
-
// TODO(remove in a future major): v1→v2 transitional only. Set when an
|
|
57
|
-
// inherited v1 row points at app-owned storage the component can't read, so
|
|
58
|
-
// the app-side `registerStaticRoutes` handler can serve it from its own
|
|
59
|
-
// storage during migration. Drop this field once v1 app-storage assets are
|
|
60
|
-
// no longer supported.
|
|
61
|
-
appStorageId: v.optional(v.string()),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
async function resolveAssetDocument(
|
|
65
|
-
ctx: QueryCtx,
|
|
66
|
-
path: string,
|
|
67
|
-
spaFallbackOverride?: boolean,
|
|
68
|
-
): Promise<Infer<typeof resolvedAssetValidator> | null> {
|
|
69
|
-
const exact = await ctx.db
|
|
70
|
-
.query("staticAssets")
|
|
71
|
-
.withIndex("by_path", (q) => q.eq("path", path))
|
|
72
|
-
.unique();
|
|
73
|
-
if (exact) return exact;
|
|
74
|
-
|
|
75
|
-
if (hasFileExtension(path)) {
|
|
76
|
-
// A file the current deployment replaced stays reachable for a while, so
|
|
77
|
-
// pages that are already open can still load their scripts. Old HTML never
|
|
78
|
-
// comes back.
|
|
79
|
-
const retired = await ctx.db
|
|
80
|
-
.query("retiredAssets")
|
|
81
|
-
.withIndex("by_path", (q) => q.eq("path", path))
|
|
82
|
-
.order("desc")
|
|
83
|
-
.first();
|
|
84
|
-
if (retired && !isHtmlContentType(retired.contentType)) return retired;
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const info = await ctx.db.query("deploymentInfo").first();
|
|
89
|
-
const spaFallback = spaFallbackOverride ?? info?.spaFallback ?? true;
|
|
90
|
-
if (!spaFallback) return null;
|
|
91
|
-
|
|
92
|
-
return await ctx.db
|
|
93
|
-
.query("staticAssets")
|
|
94
|
-
.withIndex("by_path", (q) => q.eq("path", "/index.html"))
|
|
95
|
-
.unique();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function deleteStorageFile(ctx: MutationCtx, storageId: Id<"_storage">) {
|
|
99
|
-
const retiredReference = await ctx.db
|
|
100
|
-
.query("retiredAssets")
|
|
101
|
-
.withIndex("by_storageId", (q) => q.eq("storageId", storageId))
|
|
102
|
-
.first();
|
|
103
|
-
if (retiredReference) return false;
|
|
104
|
-
// A v2 component mounted with the old `selfHosting` name can inherit v1 rows
|
|
105
|
-
// containing app-owned storage IDs. They are deliberately invisible in the
|
|
106
|
-
// component namespace. Checking metadata avoids relying on backend error text
|
|
107
|
-
// and treats both those foreign IDs and already-deleted files as absent.
|
|
108
|
-
const metadata = await ctx.db.system.get("_storage", storageId);
|
|
109
|
-
if (metadata === null) return false;
|
|
110
|
-
|
|
111
|
-
await ctx.storage.delete(storageId);
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function adjustPendingBlobCleanupCount(ctx: MutationCtx, delta: number): Promise<number> {
|
|
116
|
-
const state = await ctx.db.query("cleanupState").first();
|
|
117
|
-
if (state) {
|
|
118
|
-
const next = Math.max(0, state.pendingBlobCleanupCount + delta);
|
|
119
|
-
await ctx.db.patch("cleanupState", state._id, {
|
|
120
|
-
pendingBlobCleanupCount: next,
|
|
121
|
-
});
|
|
122
|
-
return next;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Seed upgrades from the old denormalized field when present. On a fresh
|
|
126
|
-
// install this also works before deploymentInfo exists.
|
|
127
|
-
const info = await ctx.db.query("deploymentInfo").first();
|
|
128
|
-
const next = Math.max(0, (info?.pendingBlobCleanupCount ?? 0) + delta);
|
|
129
|
-
await ctx.db.insert("cleanupState", { pendingBlobCleanupCount: next });
|
|
130
|
-
return next;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export const getCurrentDeployment = query({
|
|
134
|
-
args: {},
|
|
135
|
-
returns: v.union(deploymentInfoValidator, v.null()),
|
|
136
|
-
handler: async (ctx) => {
|
|
137
|
-
const info = await ctx.db.query("deploymentInfo").first();
|
|
138
|
-
if (!info) return null;
|
|
139
|
-
const state = await ctx.db.query("cleanupState").first();
|
|
140
|
-
return {
|
|
141
|
-
...info,
|
|
142
|
-
pendingBlobCleanupCount: state?.pendingBlobCleanupCount ?? info.pendingBlobCleanupCount,
|
|
143
|
-
};
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Narrow serving API used by the app-owned `registerStaticRoutes` compatibility
|
|
148
|
-
// mode. The app can resolve a public asset, but all storage and deployment
|
|
149
|
-
// management stays encapsulated in the component.
|
|
150
|
-
export const resolveAssetForHttp = query({
|
|
151
|
-
args: {
|
|
152
|
-
path: v.string(),
|
|
153
|
-
spaFallback: v.optional(v.boolean()),
|
|
154
|
-
},
|
|
155
|
-
returns: v.union(httpAssetValidator, v.null()),
|
|
156
|
-
handler: async (ctx, { path, spaFallback }) => {
|
|
157
|
-
const asset = await resolveAssetDocument(ctx, path, spaFallback);
|
|
158
|
-
if (!asset) return null;
|
|
159
|
-
|
|
160
|
-
const storageUrl = asset.storageId ? await ctx.storage.getUrl(asset.storageId) : null;
|
|
161
|
-
|
|
162
|
-
// TODO(remove in a future major): v1→v2 transitional path. During a
|
|
163
|
-
// same-name migration, inherited v1 rows point at app-owned storage the
|
|
164
|
-
// component can't fetch (storageId present but no resolvable URL and no
|
|
165
|
-
// blob). Rather than hide them (which shows the setup page until the first
|
|
166
|
-
// v2 upload), surface the raw storage id so the app-side
|
|
167
|
-
// `registerStaticRoutes` handler can serve the file from its own storage —
|
|
168
|
-
// keeping the live site up with zero downtime and no re-upload. The
|
|
169
|
-
// component-owned HTTP handler still can't reach app storage, so it
|
|
170
|
-
// continues to show its setup page for this case. Once v1 app-storage
|
|
171
|
-
// assets are no longer supported, delete `appStorageId` and return null
|
|
172
|
-
// here instead.
|
|
173
|
-
const appStorageId =
|
|
174
|
-
asset.storageId && !storageUrl && !asset.blobId ? asset.storageId : undefined;
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
...(storageUrl ? { storageUrl } : {}),
|
|
178
|
-
...(appStorageId ? { appStorageId } : {}),
|
|
179
|
-
...(asset.blobId ? { blobId: asset.blobId } : {}),
|
|
180
|
-
contentType: asset.contentType,
|
|
181
|
-
...(asset.storageId ? { etag: `"${asset.storageId}"` } : {}),
|
|
182
|
-
};
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Returns the deployment URLs visible to this component:
|
|
187
|
-
// siteUrl - CONVEX_SITE_URL (includes the component's mount prefix). Used
|
|
188
|
-
// by the CLI to derive CONVEX_HOSTING_BASE_PATH and to show
|
|
189
|
-
// where the deployed app lives.
|
|
190
|
-
// cloudUrl - CONVEX_CLOUD_URL. Used by the CLI as VITE_CONVEX_URL when
|
|
191
|
-
// building the frontend.
|
|
192
|
-
export const getUrls = internalQuery({
|
|
193
|
-
args: {},
|
|
194
|
-
returns: v.object({
|
|
195
|
-
siteUrl: v.string(),
|
|
196
|
-
cloudUrl: v.string(),
|
|
197
|
-
}),
|
|
198
|
-
handler: async () => ({
|
|
199
|
-
siteUrl: process.env.CONVEX_SITE_URL!,
|
|
200
|
-
cloudUrl: process.env.CONVEX_CLOUD_URL!,
|
|
201
|
-
}),
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
export const getByPath = internalQuery({
|
|
205
|
-
args: { path: v.string() },
|
|
206
|
-
returns: v.union(staticAssetValidator, v.null()),
|
|
207
|
-
handler: async (ctx, args) => {
|
|
208
|
-
return await ctx.db
|
|
209
|
-
.query("staticAssets")
|
|
210
|
-
.withIndex("by_path", (q) => q.eq("path", args.path))
|
|
211
|
-
.unique();
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Resolves the asset the HTTP handler should serve for a request path: the
|
|
216
|
-
// exact match, or, when SPA fallback is enabled for the current deployment
|
|
217
|
-
// and the path looks like a client-side route (no file extension), the
|
|
218
|
-
// index.html asset. Doing the fallback here keeps it to a single query.
|
|
219
|
-
export const resolveAsset = internalQuery({
|
|
220
|
-
args: { path: v.string() },
|
|
221
|
-
returns: v.union(resolvedAssetValidator, v.null()),
|
|
222
|
-
handler: async (ctx, { path }) => {
|
|
223
|
-
return await resolveAssetDocument(ctx, path);
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
export const listAssets = internalQuery({
|
|
228
|
-
args: { limit: v.optional(v.number()) },
|
|
229
|
-
returns: v.array(staticAssetValidator),
|
|
230
|
-
handler: async (ctx, args) => {
|
|
231
|
-
return await ctx.db
|
|
232
|
-
.query("staticAssets")
|
|
233
|
-
.order("asc")
|
|
234
|
-
.take(args.limit ?? 100);
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
export const generateUploadUrl = internalMutation({
|
|
239
|
-
args: {},
|
|
240
|
-
returns: v.string(),
|
|
241
|
-
handler: async (ctx) => {
|
|
242
|
-
return await ctx.storage.generateUploadUrl();
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
export const generateUploadUrls = internalMutation({
|
|
247
|
-
args: { count: v.number() },
|
|
248
|
-
returns: v.array(v.string()),
|
|
249
|
-
handler: async (ctx, { count }) => {
|
|
250
|
-
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
251
|
-
throw new Error("Upload URL batch size must be an integer from 1 to 100");
|
|
252
|
-
}
|
|
253
|
-
const urls: string[] = [];
|
|
254
|
-
for (let i = 0; i < count; i++) {
|
|
255
|
-
urls.push(await ctx.storage.generateUploadUrl());
|
|
256
|
-
}
|
|
257
|
-
return urls;
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
export const listPendingBlobCleanup = internalQuery({
|
|
262
|
-
args: { limit: v.optional(v.number()) },
|
|
263
|
-
returns: v.array(v.string()),
|
|
264
|
-
handler: async (ctx, { limit }) => {
|
|
265
|
-
return (await ctx.db.query("pendingBlobCleanup").take(Math.min(limit ?? 250, 500))).map(
|
|
266
|
-
(entry) => entry.blobId,
|
|
267
|
-
);
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
export const acknowledgeBlobCleanup = internalMutation({
|
|
272
|
-
args: { blobIds: v.array(v.string()) },
|
|
273
|
-
returns: v.object({ acknowledged: v.number() }),
|
|
274
|
-
handler: async (ctx, { blobIds }) => {
|
|
275
|
-
let acknowledged = 0;
|
|
276
|
-
for (const blobId of new Set(blobIds)) {
|
|
277
|
-
const entries = await ctx.db
|
|
278
|
-
.query("pendingBlobCleanup")
|
|
279
|
-
.withIndex("by_blobId", (q) => q.eq("blobId", blobId))
|
|
280
|
-
.collect();
|
|
281
|
-
for (const entry of entries) {
|
|
282
|
-
await ctx.db.delete("pendingBlobCleanup", entry._id);
|
|
283
|
-
acknowledged++;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (acknowledged > 0) {
|
|
288
|
-
await adjustPendingBlobCleanupCount(ctx, -acknowledged);
|
|
289
|
-
}
|
|
290
|
-
return { acknowledged };
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
export const queueBlobCleanup = internalMutation({
|
|
295
|
-
args: { blobIds: v.array(v.string()) },
|
|
296
|
-
returns: v.object({ queued: v.number() }),
|
|
297
|
-
handler: async (ctx, { blobIds }) => {
|
|
298
|
-
let queued = 0;
|
|
299
|
-
for (const blobId of new Set(blobIds)) {
|
|
300
|
-
const existing = await ctx.db
|
|
301
|
-
.query("pendingBlobCleanup")
|
|
302
|
-
.withIndex("by_blobId", (q) => q.eq("blobId", blobId))
|
|
303
|
-
.first();
|
|
304
|
-
if (existing) continue;
|
|
305
|
-
await ctx.db.insert("pendingBlobCleanup", { blobId });
|
|
306
|
-
queued++;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (queued > 0) await adjustPendingBlobCleanupCount(ctx, queued);
|
|
310
|
-
return { queued };
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
export const cleanupPendingStorage = internalMutation({
|
|
315
|
-
args: { limit: v.optional(v.number()) },
|
|
316
|
-
returns: v.object({
|
|
317
|
-
processed: v.number(),
|
|
318
|
-
deleted: v.number(),
|
|
319
|
-
needsAnotherPass: v.boolean(),
|
|
320
|
-
}),
|
|
321
|
-
handler: async (ctx, { limit }) => {
|
|
322
|
-
const batchLimit = Math.min(limit ?? MAINTENANCE_PAGE_LIMIT, MAINTENANCE_PAGE_LIMIT);
|
|
323
|
-
const expired = await ctx.db
|
|
324
|
-
.query("retiredAssets")
|
|
325
|
-
.withIndex("by_expiresAt", (q) => q.lte("expiresAt", Date.now()))
|
|
326
|
-
.take(batchLimit);
|
|
327
|
-
for (const asset of expired) {
|
|
328
|
-
await ctx.db.insert("pendingStorageCleanup", {
|
|
329
|
-
storageId: asset.storageId,
|
|
330
|
-
});
|
|
331
|
-
await ctx.db.delete("retiredAssets", asset._id);
|
|
332
|
-
}
|
|
333
|
-
const entries = await ctx.db.query("pendingStorageCleanup").take(batchLimit);
|
|
334
|
-
let deleted = 0;
|
|
335
|
-
for (const entry of entries) {
|
|
336
|
-
const [liveReference, stagedReference] = await Promise.all([
|
|
337
|
-
ctx.db
|
|
338
|
-
.query("staticAssets")
|
|
339
|
-
.withIndex("by_storageId", (q) => q.eq("storageId", entry.storageId))
|
|
340
|
-
.first(),
|
|
341
|
-
ctx.db
|
|
342
|
-
.query("stagedAssets")
|
|
343
|
-
.withIndex("by_storageId", (q) => q.eq("storageId", entry.storageId))
|
|
344
|
-
.first(),
|
|
345
|
-
]);
|
|
346
|
-
if (!liveReference && !stagedReference && (await deleteStorageFile(ctx, entry.storageId))) {
|
|
347
|
-
deleted++;
|
|
348
|
-
}
|
|
349
|
-
await ctx.db.delete("pendingStorageCleanup", entry._id);
|
|
350
|
-
}
|
|
351
|
-
return {
|
|
352
|
-
processed: entries.length,
|
|
353
|
-
deleted,
|
|
354
|
-
needsAnotherPass: entries.length === batchLimit || expired.length === batchLimit,
|
|
355
|
-
};
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
export const deleteUploadedFiles = internalMutation({
|
|
360
|
-
args: { storageIds: v.array(v.id("_storage")) },
|
|
361
|
-
returns: v.object({
|
|
362
|
-
deleted: v.number(),
|
|
363
|
-
alreadyMissing: v.number(),
|
|
364
|
-
stillReferenced: v.number(),
|
|
365
|
-
}),
|
|
366
|
-
handler: async (ctx, { storageIds }) => {
|
|
367
|
-
// A publish can commit even if the CLI loses its response. Never delete a
|
|
368
|
-
// file that the live manifest now references during failure cleanup.
|
|
369
|
-
const liveAssets = await ctx.db.query("staticAssets").collect();
|
|
370
|
-
const referencedStorageIds = new Set(
|
|
371
|
-
liveAssets.flatMap((asset) => (asset.storageId === undefined ? [] : [asset.storageId])),
|
|
372
|
-
);
|
|
373
|
-
let deleted = 0;
|
|
374
|
-
let alreadyMissing = 0;
|
|
375
|
-
let stillReferenced = 0;
|
|
376
|
-
for (const storageId of storageIds) {
|
|
377
|
-
const retiredReference = await ctx.db
|
|
378
|
-
.query("retiredAssets")
|
|
379
|
-
.withIndex("by_storageId", (q) => q.eq("storageId", storageId))
|
|
380
|
-
.first();
|
|
381
|
-
if (referencedStorageIds.has(storageId) || retiredReference) {
|
|
382
|
-
stillReferenced++;
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (await deleteStorageFile(ctx, storageId)) {
|
|
386
|
-
deleted++;
|
|
387
|
-
} else {
|
|
388
|
-
alreadyMissing++;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return { deleted, alreadyMissing, stillReferenced };
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
const recordAssetFields = {
|
|
396
|
-
path: v.string(),
|
|
397
|
-
storageId: v.optional(v.id("_storage")),
|
|
398
|
-
blobId: v.optional(v.string()),
|
|
399
|
-
contentType: v.string(),
|
|
400
|
-
deploymentId: v.string(),
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
function assertAssetLocation({
|
|
404
|
-
path,
|
|
405
|
-
storageId,
|
|
406
|
-
blobId,
|
|
407
|
-
}: {
|
|
408
|
-
path: string;
|
|
409
|
-
storageId?: Id<"_storage">;
|
|
410
|
-
blobId?: string;
|
|
411
|
-
}) {
|
|
412
|
-
if ((storageId === undefined) === (blobId === undefined)) {
|
|
413
|
-
throw new Error("Asset " + path + " must have exactly one of storageId or blobId");
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function assertAssetManifest(
|
|
418
|
-
assets: Array<{
|
|
419
|
-
path: string;
|
|
420
|
-
storageId?: Id<"_storage">;
|
|
421
|
-
blobId?: string;
|
|
422
|
-
deploymentId: string;
|
|
423
|
-
}>,
|
|
424
|
-
deploymentId?: string,
|
|
425
|
-
requireIndex = false,
|
|
426
|
-
) {
|
|
427
|
-
if (requireIndex && assets.length === 0) {
|
|
428
|
-
throw new Error("A static asset manifest cannot be empty");
|
|
429
|
-
}
|
|
430
|
-
if (requireIndex && assets.length > MAX_ASSETS_PER_DEPLOYMENT) {
|
|
431
|
-
throw new Error(
|
|
432
|
-
`A static asset manifest can contain at most ${MAX_ASSETS_PER_DEPLOYMENT} files`,
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const paths = new Set<string>();
|
|
437
|
-
for (const asset of assets) {
|
|
438
|
-
assertAssetLocation(asset);
|
|
439
|
-
if (paths.has(asset.path)) {
|
|
440
|
-
throw new Error("Duplicate asset path: " + asset.path);
|
|
441
|
-
}
|
|
442
|
-
if (deploymentId !== undefined && asset.deploymentId !== deploymentId) {
|
|
443
|
-
throw new Error("Asset " + asset.path + " has the wrong deploymentId");
|
|
444
|
-
}
|
|
445
|
-
paths.add(asset.path);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (requireIndex && !paths.has("/index.html")) {
|
|
449
|
-
throw new Error("A static asset manifest must include /index.html");
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export const stageAssets = internalMutation({
|
|
454
|
-
args: { assets: v.array(v.object(recordAssetFields)) },
|
|
455
|
-
returns: v.object({ staged: v.number() }),
|
|
456
|
-
handler: async (ctx, { assets }) => {
|
|
457
|
-
if (assets.length === 0) {
|
|
458
|
-
throw new Error("Cannot stage an empty asset chunk");
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Validate locations and deployment IDs within this chunk now. Duplicate
|
|
462
|
-
// paths across chunks are checked again when the full manifest publishes.
|
|
463
|
-
const deploymentId = assets[0].deploymentId;
|
|
464
|
-
for (const asset of assets) {
|
|
465
|
-
assertAssetLocation(asset);
|
|
466
|
-
if (asset.deploymentId !== deploymentId) {
|
|
467
|
-
throw new Error("A staged asset chunk must use one deploymentId");
|
|
468
|
-
}
|
|
469
|
-
// Validate component-storage IDs while the manifest is still split into
|
|
470
|
-
// small CLI chunks. Doing this for the complete manifest during publish
|
|
471
|
-
// would exceed Convex's index-range-read limit on large deployments.
|
|
472
|
-
if (asset.storageId && (await ctx.db.system.get("_storage", asset.storageId)) === null) {
|
|
473
|
-
throw new Error(`Staged storage file is missing for ${asset.path}`);
|
|
474
|
-
}
|
|
475
|
-
await ctx.db.insert("stagedAssets", asset);
|
|
476
|
-
}
|
|
477
|
-
return { staged: assets.length };
|
|
478
|
-
},
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
export const discardStagedDeployment = internalMutation({
|
|
482
|
-
args: { deploymentId: v.string() },
|
|
483
|
-
returns: v.object({ discarded: v.number() }),
|
|
484
|
-
handler: async (ctx, { deploymentId }) => {
|
|
485
|
-
const assets = await ctx.db
|
|
486
|
-
.query("stagedAssets")
|
|
487
|
-
.withIndex("by_deploymentId", (q) => q.eq("deploymentId", deploymentId))
|
|
488
|
-
.collect();
|
|
489
|
-
for (const asset of assets) {
|
|
490
|
-
await ctx.db.delete("stagedAssets", asset._id);
|
|
491
|
-
}
|
|
492
|
-
return { discarded: assets.length };
|
|
493
|
-
},
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
export const cleanupAbandonedStaging = internalMutation({
|
|
497
|
-
args: {
|
|
498
|
-
before: v.optional(v.number()),
|
|
499
|
-
limit: v.optional(v.number()),
|
|
500
|
-
},
|
|
501
|
-
returns: v.object({
|
|
502
|
-
discarded: v.number(),
|
|
503
|
-
deletedFiles: v.number(),
|
|
504
|
-
queuedBlobIds: v.number(),
|
|
505
|
-
}),
|
|
506
|
-
handler: async (ctx, { before, limit }) => {
|
|
507
|
-
const cutoff = before ?? Date.now() - ABANDONED_UPLOAD_AGE_MS;
|
|
508
|
-
const candidates = await ctx.db
|
|
509
|
-
.query("stagedAssets")
|
|
510
|
-
.order("asc")
|
|
511
|
-
.take(Math.min(limit ?? MAINTENANCE_PAGE_LIMIT, MAINTENANCE_PAGE_LIMIT));
|
|
512
|
-
const assets = candidates.filter((asset) => asset._creationTime < cutoff);
|
|
513
|
-
let deletedFiles = 0;
|
|
514
|
-
let queuedBlobIds = 0;
|
|
515
|
-
for (const asset of assets) {
|
|
516
|
-
if (asset.storageId) {
|
|
517
|
-
const liveReference = await ctx.db
|
|
518
|
-
.query("staticAssets")
|
|
519
|
-
.withIndex("by_storageId", (q) => q.eq("storageId", asset.storageId))
|
|
520
|
-
.first();
|
|
521
|
-
if (!liveReference && (await deleteStorageFile(ctx, asset.storageId))) {
|
|
522
|
-
deletedFiles++;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (asset.blobId) {
|
|
526
|
-
await ctx.db.insert("pendingBlobCleanup", { blobId: asset.blobId });
|
|
527
|
-
queuedBlobIds++;
|
|
528
|
-
}
|
|
529
|
-
await ctx.db.delete("stagedAssets", asset._id);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (queuedBlobIds > 0) {
|
|
533
|
-
await adjustPendingBlobCleanupCount(ctx, queuedBlobIds);
|
|
534
|
-
}
|
|
535
|
-
return {
|
|
536
|
-
discarded: assets.length,
|
|
537
|
-
deletedFiles,
|
|
538
|
-
queuedBlobIds,
|
|
539
|
-
};
|
|
540
|
-
},
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
export const cleanupUnreferencedStorage = internalMutation({
|
|
544
|
-
args: {
|
|
545
|
-
before: v.optional(v.number()),
|
|
546
|
-
limit: v.optional(v.number()),
|
|
547
|
-
order: v.optional(v.union(v.literal("asc"), v.literal("desc"))),
|
|
548
|
-
},
|
|
549
|
-
returns: v.object({
|
|
550
|
-
scanned: v.number(),
|
|
551
|
-
deleted: v.number(),
|
|
552
|
-
needsAnotherPass: v.boolean(),
|
|
553
|
-
}),
|
|
554
|
-
handler: async (ctx, { before, limit, order }) => {
|
|
555
|
-
const cutoff = before ?? Date.now() - ABANDONED_UPLOAD_AGE_MS;
|
|
556
|
-
const scanOrder = order ?? "asc";
|
|
557
|
-
// Read the live set once, then spend the remaining read budget checking
|
|
558
|
-
// staged references for non-live files. At the 1,800-file maximum this can
|
|
559
|
-
// inspect 2,000 storage rows while remaining below 4,096 document reads.
|
|
560
|
-
const [liveAssets, files] = await Promise.all([
|
|
561
|
-
ctx.db.query("staticAssets").collect(),
|
|
562
|
-
ctx.db.system.query("_storage").order(scanOrder).take(STORAGE_SCAN_LIMIT),
|
|
563
|
-
]);
|
|
564
|
-
const liveStorageIds = new Set(
|
|
565
|
-
liveAssets.flatMap((asset) => (asset.storageId === undefined ? [] : [asset.storageId])),
|
|
566
|
-
);
|
|
567
|
-
const stagedLookupBudget = Math.max(0, 3900 - liveAssets.length - files.length);
|
|
568
|
-
const deleteLimit = Math.min(limit ?? MAINTENANCE_PAGE_LIMIT, MAINTENANCE_PAGE_LIMIT);
|
|
569
|
-
let deleted = 0;
|
|
570
|
-
let scanned = 0;
|
|
571
|
-
let stagedLookups = 0;
|
|
572
|
-
let stoppedForReadBudget = false;
|
|
573
|
-
for (const file of files) {
|
|
574
|
-
if (deleted >= deleteLimit) break;
|
|
575
|
-
if (file._creationTime >= cutoff) {
|
|
576
|
-
if (scanOrder === "asc") break;
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
scanned++;
|
|
580
|
-
if (liveStorageIds.has(file._id)) continue;
|
|
581
|
-
if (stagedLookups + 2 > stagedLookupBudget) {
|
|
582
|
-
stoppedForReadBudget = true;
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
stagedLookups += 2;
|
|
586
|
-
const stagedReference = await ctx.db
|
|
587
|
-
.query("stagedAssets")
|
|
588
|
-
.withIndex("by_storageId", (q) => q.eq("storageId", file._id))
|
|
589
|
-
.first();
|
|
590
|
-
const retiredReference = await ctx.db
|
|
591
|
-
.query("retiredAssets")
|
|
592
|
-
.withIndex("by_storageId", (q) => q.eq("storageId", file._id))
|
|
593
|
-
.first();
|
|
594
|
-
if (!stagedReference && !retiredReference) {
|
|
595
|
-
await ctx.storage.delete(file._id);
|
|
596
|
-
deleted++;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return {
|
|
600
|
-
scanned,
|
|
601
|
-
deleted,
|
|
602
|
-
needsAnotherPass:
|
|
603
|
-
deleted === deleteLimit ||
|
|
604
|
-
// Without deletes, another pass would inspect the same protected prefix.
|
|
605
|
-
(deleted > 0 && (stoppedForReadBudget || files.length === STORAGE_SCAN_LIMIT)),
|
|
606
|
-
};
|
|
607
|
-
},
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
async function setCurrentDeployment(
|
|
611
|
-
ctx: MutationCtx,
|
|
612
|
-
currentDeploymentId: string,
|
|
613
|
-
spaFallback: boolean,
|
|
614
|
-
pendingBlobCleanupCount?: number,
|
|
615
|
-
) {
|
|
616
|
-
const existing = await ctx.db.query("deploymentInfo").first();
|
|
617
|
-
if (existing) {
|
|
618
|
-
await ctx.db.patch("deploymentInfo", existing._id, {
|
|
619
|
-
currentDeploymentId,
|
|
620
|
-
deployedAt: Date.now(),
|
|
621
|
-
spaFallback,
|
|
622
|
-
...(pendingBlobCleanupCount === undefined ? {} : { pendingBlobCleanupCount }),
|
|
623
|
-
});
|
|
624
|
-
} else {
|
|
625
|
-
await ctx.db.insert("deploymentInfo", {
|
|
626
|
-
currentDeploymentId,
|
|
627
|
-
deployedAt: Date.now(),
|
|
628
|
-
spaFallback,
|
|
629
|
-
...(pendingBlobCleanupCount === undefined ? {} : { pendingBlobCleanupCount }),
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
export const recordAsset = internalMutation({
|
|
635
|
-
args: recordAssetFields,
|
|
636
|
-
returns: v.union(v.string(), v.null()),
|
|
637
|
-
handler: async (ctx, args) => {
|
|
638
|
-
assertAssetLocation(args);
|
|
639
|
-
const existing = await ctx.db
|
|
640
|
-
.query("staticAssets")
|
|
641
|
-
.withIndex("by_path", (q) => q.eq("path", args.path))
|
|
642
|
-
.unique();
|
|
643
|
-
if (existing) {
|
|
644
|
-
if (existing.storageId) {
|
|
645
|
-
await deleteStorageFile(ctx, existing.storageId);
|
|
646
|
-
}
|
|
647
|
-
await ctx.db.delete("staticAssets", existing._id);
|
|
648
|
-
}
|
|
649
|
-
await ctx.db.insert("staticAssets", {
|
|
650
|
-
path: args.path,
|
|
651
|
-
...(args.storageId ? { storageId: args.storageId } : {}),
|
|
652
|
-
...(args.blobId ? { blobId: args.blobId } : {}),
|
|
653
|
-
contentType: args.contentType,
|
|
654
|
-
deploymentId: args.deploymentId,
|
|
655
|
-
});
|
|
656
|
-
return existing?.blobId ?? null;
|
|
657
|
-
},
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
export const recordAssets = internalMutation({
|
|
661
|
-
args: { assets: v.array(v.object(recordAssetFields)) },
|
|
662
|
-
returns: v.object({ blobIds: v.array(v.string()) }),
|
|
663
|
-
handler: async (ctx, { assets }) => {
|
|
664
|
-
assertAssetManifest(assets);
|
|
665
|
-
|
|
666
|
-
const blobIds: string[] = [];
|
|
667
|
-
for (const asset of assets) {
|
|
668
|
-
const existing = await ctx.db
|
|
669
|
-
.query("staticAssets")
|
|
670
|
-
.withIndex("by_path", (q) => q.eq("path", asset.path))
|
|
671
|
-
.unique();
|
|
672
|
-
if (existing) {
|
|
673
|
-
if (existing.storageId) {
|
|
674
|
-
await deleteStorageFile(ctx, existing.storageId);
|
|
675
|
-
}
|
|
676
|
-
if (existing.blobId) {
|
|
677
|
-
blobIds.push(existing.blobId);
|
|
678
|
-
}
|
|
679
|
-
await ctx.db.delete("staticAssets", existing._id);
|
|
680
|
-
}
|
|
681
|
-
await ctx.db.insert("staticAssets", {
|
|
682
|
-
path: asset.path,
|
|
683
|
-
...(asset.storageId ? { storageId: asset.storageId } : {}),
|
|
684
|
-
...(asset.blobId ? { blobId: asset.blobId } : {}),
|
|
685
|
-
contentType: asset.contentType,
|
|
686
|
-
deploymentId: asset.deploymentId,
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
return { blobIds };
|
|
690
|
-
},
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
// Publish the complete manifest, deployment metadata, and old-asset cleanup in
|
|
694
|
-
// one mutation. Until this succeeds, the previous deployment remains wholly
|
|
695
|
-
// live. Failure cleanup separately checks the live manifest before deleting
|
|
696
|
-
// newly uploaded files in case this mutation committed but its response was
|
|
697
|
-
// lost.
|
|
698
|
-
export const publishDeployment = internalMutation({
|
|
699
|
-
args: {
|
|
700
|
-
currentDeploymentId: v.string(),
|
|
701
|
-
expectedAssetCount: v.number(),
|
|
702
|
-
spaFallback: v.optional(v.boolean()),
|
|
703
|
-
},
|
|
704
|
-
returns: v.object({
|
|
705
|
-
deleted: v.number(),
|
|
706
|
-
pendingBlobCleanup: v.number(),
|
|
707
|
-
}),
|
|
708
|
-
handler: async (ctx, { currentDeploymentId, expectedAssetCount, spaFallback }) => {
|
|
709
|
-
if (
|
|
710
|
-
!Number.isInteger(expectedAssetCount) ||
|
|
711
|
-
expectedAssetCount < 1 ||
|
|
712
|
-
expectedAssetCount > MAX_ASSETS_PER_DEPLOYMENT
|
|
713
|
-
) {
|
|
714
|
-
throw new Error(
|
|
715
|
-
`expectedAssetCount must be an integer from 1 to ${MAX_ASSETS_PER_DEPLOYMENT}`,
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
const stagedAssets = await ctx.db
|
|
719
|
-
.query("stagedAssets")
|
|
720
|
-
.withIndex("by_deploymentId", (q) => q.eq("deploymentId", currentDeploymentId))
|
|
721
|
-
.take(expectedAssetCount + 1);
|
|
722
|
-
const assets = stagedAssets.map(({ path, storageId, blobId, contentType, deploymentId }) => ({
|
|
723
|
-
path,
|
|
724
|
-
storageId,
|
|
725
|
-
blobId,
|
|
726
|
-
contentType,
|
|
727
|
-
deploymentId,
|
|
728
|
-
}));
|
|
729
|
-
if (assets.length !== expectedAssetCount) {
|
|
730
|
-
throw new Error(`Staged manifest has ${assets.length} files; expected ${expectedAssetCount}`);
|
|
731
|
-
}
|
|
732
|
-
assertAssetManifest(assets, currentDeploymentId, true);
|
|
733
|
-
const manifestBytes = new TextEncoder().encode(JSON.stringify(assets)).byteLength;
|
|
734
|
-
if (manifestBytes > MAX_MANIFEST_SERIALIZED_BYTES) {
|
|
735
|
-
throw new Error(
|
|
736
|
-
`The staged manifest is ${manifestBytes} bytes, above the safe limit of ${MAX_MANIFEST_SERIALIZED_BYTES} bytes. Shorten asset paths or reduce the number of files.`,
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Leave headroom below Convex's 4,096 document-read ceiling for deployment
|
|
741
|
-
// metadata and index bookkeeping. Taking one extra old row lets oversized
|
|
742
|
-
// v1-to-v2 migrations fail clearly instead of exceeding the platform limit.
|
|
743
|
-
const oldAssetReadLimit = MAX_PUBLISH_MANIFEST_READS - assets.length + 1;
|
|
744
|
-
const oldAssets = await ctx.db.query("staticAssets").take(oldAssetReadLimit);
|
|
745
|
-
if (assets.length + oldAssets.length > MAX_PUBLISH_MANIFEST_READS) {
|
|
746
|
-
throw new Error(
|
|
747
|
-
`The old and new manifests contain more than ${MAX_PUBLISH_MANIFEST_READS} rows combined. Reduce the new build or clean up the legacy manifest before migrating.`,
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
const retiredAssets = oldAssets.flatMap((asset) => {
|
|
751
|
-
if (asset.storageId !== undefined && asset.blobId === undefined) {
|
|
752
|
-
return [
|
|
753
|
-
{
|
|
754
|
-
_id: asset._id,
|
|
755
|
-
path: asset.path,
|
|
756
|
-
storageId: asset.storageId,
|
|
757
|
-
contentType: asset.contentType,
|
|
758
|
-
deploymentId: asset.deploymentId,
|
|
759
|
-
},
|
|
760
|
-
];
|
|
761
|
-
}
|
|
762
|
-
return [];
|
|
763
|
-
});
|
|
764
|
-
const retiredIds = new Set(retiredAssets.map((asset) => asset._id));
|
|
765
|
-
const blobIds: string[] = [];
|
|
766
|
-
const storageIds = new Set<Id<"_storage">>();
|
|
767
|
-
for (const asset of oldAssets) {
|
|
768
|
-
if (retiredIds.has(asset._id)) continue;
|
|
769
|
-
if (asset.blobId) blobIds.push(asset.blobId);
|
|
770
|
-
if (asset.storageId) storageIds.add(asset.storageId);
|
|
771
|
-
}
|
|
772
|
-
const plannedDocumentWrites =
|
|
773
|
-
oldAssets.length +
|
|
774
|
-
storageIds.size +
|
|
775
|
-
blobIds.length +
|
|
776
|
-
assets.length +
|
|
777
|
-
stagedAssets.length +
|
|
778
|
-
retiredAssets.length +
|
|
779
|
-
2;
|
|
780
|
-
if (plannedDocumentWrites > MAX_PUBLISH_DOCUMENT_WRITES) {
|
|
781
|
-
throw new Error(
|
|
782
|
-
`Publishing this manifest would require ${plannedDocumentWrites} document writes, above the safe limit of ${MAX_PUBLISH_DOCUMENT_WRITES}. Reduce the number of files in the deployment.`,
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
for (const asset of oldAssets) {
|
|
787
|
-
await ctx.db.delete("staticAssets", asset._id);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
for (const { _id, ...asset } of retiredAssets) {
|
|
791
|
-
await ctx.db.insert("retiredAssets", {
|
|
792
|
-
...asset,
|
|
793
|
-
expiresAt: Date.now() + RETIRED_ASSET_AGE_MS,
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
for (const asset of assets) {
|
|
798
|
-
await ctx.db.insert("staticAssets", {
|
|
799
|
-
path: asset.path,
|
|
800
|
-
...(asset.storageId ? { storageId: asset.storageId } : {}),
|
|
801
|
-
...(asset.blobId ? { blobId: asset.blobId } : {}),
|
|
802
|
-
contentType: asset.contentType,
|
|
803
|
-
deploymentId: asset.deploymentId,
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
for (const asset of stagedAssets) {
|
|
808
|
-
await ctx.db.delete("stagedAssets", asset._id);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
for (const blobId of blobIds) {
|
|
812
|
-
await ctx.db.insert("pendingBlobCleanup", { blobId });
|
|
813
|
-
}
|
|
814
|
-
for (const storageId of storageIds) {
|
|
815
|
-
await ctx.db.insert("pendingStorageCleanup", { storageId });
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const pendingBlobCleanup = await adjustPendingBlobCleanupCount(ctx, blobIds.length);
|
|
819
|
-
await setCurrentDeployment(ctx, currentDeploymentId, spaFallback ?? true, pendingBlobCleanup);
|
|
820
|
-
// Old component-storage objects are collected in a separate bounded
|
|
821
|
-
// maintenance mutation. Keeping those deletes out of this transaction is
|
|
822
|
-
// what lets a 1,800-file manifest switch atomically below Convex's read
|
|
823
|
-
// and write limits.
|
|
824
|
-
return { deleted: 0, pendingBlobCleanup };
|
|
825
|
-
},
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
// Commits a finished upload as the current deployment: records the deployment
|
|
829
|
-
// id + SPA config, then garbage-collects assets left over from previous
|
|
830
|
-
// deployments. Returns the storage cleanup tally plus any CDN blobIds the
|
|
831
|
-
// caller should delete (component actions can't reach the /fs blobs endpoint).
|
|
832
|
-
export const commitDeployment = internalMutation({
|
|
833
|
-
args: {
|
|
834
|
-
currentDeploymentId: v.string(),
|
|
835
|
-
// Whether to serve SPA fallback for this deployment (default true).
|
|
836
|
-
spaFallback: v.optional(v.boolean()),
|
|
837
|
-
},
|
|
838
|
-
returns: v.object({
|
|
839
|
-
deleted: v.number(),
|
|
840
|
-
blobIds: v.array(v.string()),
|
|
841
|
-
}),
|
|
842
|
-
handler: async (ctx, args) => {
|
|
843
|
-
const oldAssets = await ctx.db.query("staticAssets").collect();
|
|
844
|
-
const blobIds: string[] = [];
|
|
845
|
-
let deleted = 0;
|
|
846
|
-
for (const asset of oldAssets) {
|
|
847
|
-
if (asset.deploymentId === args.currentDeploymentId) continue;
|
|
848
|
-
if (asset.storageId) {
|
|
849
|
-
if (await deleteStorageFile(ctx, asset.storageId)) {
|
|
850
|
-
deleted++;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
if (asset.blobId) {
|
|
854
|
-
blobIds.push(asset.blobId);
|
|
855
|
-
}
|
|
856
|
-
await ctx.db.delete("staticAssets", asset._id);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
await setCurrentDeployment(ctx, args.currentDeploymentId, args.spaFallback ?? true);
|
|
860
|
-
return { deleted, blobIds };
|
|
861
|
-
},
|
|
862
|
-
});
|