@hot-updater/cloudflare 1.0.0-rc.2 → 1.0.0-rc.21

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 (51) hide show
  1. package/dist/index.cjs +95 -870
  2. package/dist/index.d.cts +724 -6
  3. package/dist/index.d.mts +724 -6
  4. package/dist/index.mjs +93 -870
  5. package/dist/init/index.cjs +11768 -0
  6. package/dist/init/index.d.cts +5 -1
  7. package/dist/init/index.d.mts +5 -1
  8. package/dist/init/index.mjs +11761 -1
  9. package/dist/worker/index.cjs +59 -859
  10. package/dist/worker/index.d.cts +725 -11
  11. package/dist/worker/index.d.mts +725 -11
  12. package/dist/worker/index.mjs +62 -863
  13. package/package.json +9 -13
  14. package/sql/bundles.sql +109 -190
  15. package/src/d1Database.ts +61 -40
  16. package/src/d1Executor.ts +84 -0
  17. package/src/d1Schema.ts +26 -0
  18. package/src/index.ts +1 -0
  19. package/src/plugins.ts +8 -0
  20. package/src/r2S3Storage.ts +4 -4
  21. package/src/r2Storage.ts +2 -2
  22. package/src/r2WorkerStorage.ts +4 -4
  23. package/src/worker/d1Database.ts +21 -27
  24. package/src/worker/index.ts +1 -0
  25. package/worker/dist/README.md +1 -1
  26. package/worker/dist/index.js +9682 -6206
  27. package/worker/dist/index.js.map +4 -4
  28. package/worker/migrations/0001_hot-updater_1.0.0.sql +109 -205
  29. package/worker/src/index.ts +2 -2
  30. package/dist/iac/index.cjs +0 -12566
  31. package/dist/iac/index.d.cts +0 -9
  32. package/dist/iac/index.d.mts +0 -9
  33. package/dist/iac/index.mjs +0 -12558
  34. package/src/d1Database.spec.ts +0 -178
  35. package/src/d1Implementation.spec.ts +0 -252
  36. package/src/d1Implementation.ts +0 -742
  37. package/src/d1Query.spec.ts +0 -46
  38. package/src/d1Query.ts +0 -89
  39. package/src/d1Rows.spec.ts +0 -111
  40. package/src/d1Rows.ts +0 -326
  41. package/src/d1Sql.spec.ts +0 -30
  42. package/src/d1Sql.ts +0 -175
  43. package/src/r2Storage.spec.ts +0 -346
  44. package/src/r2WorkerStorage.spec.ts +0 -184
  45. package/src/utils/createWrangler.spec.ts +0 -75
  46. package/src/worker/d1Database.spec.ts +0 -33
  47. package/worker/src/databaseMigration.integration.spec.ts +0 -130
  48. package/worker/src/databasePlugins.integration.spec.ts +0 -292
  49. package/worker/src/index.integration.spec.ts +0 -190
  50. package/worker/src/storagePlugin.integration.spec.ts +0 -59
  51. package/worker/src/testWorker.ts +0 -5
@@ -1,872 +1,71 @@
1
- import { createDatabasePlugin, createStorageDownloadUrl, createStorageKeyBuilder, createStoragePlugin, createStorageUri, isDatabaseMetadataObject, parseStorageUri } from "@hot-updater/plugin-core";
2
- import { createDatabasePluginAdapter } from "@hot-updater/plugin-core/internal";
3
- //#region src/d1Rows.ts
4
- var InvalidD1RowError = class extends Error {
5
- name = "InvalidD1RowError";
6
- model;
7
- constructor(model) {
8
- super(`D1 returned an invalid ${model} row`);
9
- this.model = model;
10
- }
11
- };
12
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
- const stringValue = (row, field, model) => {
14
- const value = row[field];
15
- if (typeof value !== "string") throw new InvalidD1RowError(model);
16
- return value;
17
- };
18
- const nullableString = (row, field, model) => {
19
- const value = row[field];
20
- if (value === null) return null;
21
- if (typeof value !== "string") throw new InvalidD1RowError(model);
22
- return value;
23
- };
24
- const numberValue = (row, field, model) => {
25
- const value = row[field];
26
- if (typeof value !== "number") throw new InvalidD1RowError(model);
27
- return value;
28
- };
29
- const byteSizeValue = (row, field, model) => {
30
- const value = row[field];
31
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new InvalidD1RowError(model);
32
- return value;
33
- };
34
- const booleanValue = (row, field, model) => {
35
- const value = row[field];
36
- if (value === true || value === 1) return true;
37
- if (value === false || value === 0) return false;
38
- throw new InvalidD1RowError(model);
39
- };
40
- const jsonValue = (value, model) => {
41
- if (typeof value !== "string") throw new InvalidD1RowError(model);
42
- try {
43
- return JSON.parse(value);
44
- } catch (error) {
45
- if (error instanceof SyntaxError) throw new InvalidD1RowError(model);
46
- throw error;
47
- }
48
- };
49
- const metadata = (row) => {
50
- const value = jsonValue(row["metadata"], "bundles");
51
- if (!isDatabaseMetadataObject(value)) throw new InvalidD1RowError("bundles");
52
- return value;
53
- };
54
- const bundleRow = (row) => {
55
- const platform = stringValue(row, "platform", "bundles");
56
- if (platform !== "ios" && platform !== "android") throw new InvalidD1RowError("bundles");
57
- return {
58
- id: stringValue(row, "id", "bundles"),
59
- platform,
60
- file_hash: stringValue(row, "file_hash", "bundles"),
61
- git_commit_hash: nullableString(row, "git_commit_hash", "bundles"),
62
- storage_uri: stringValue(row, "storage_uri", "bundles"),
63
- archive_byte_size: row["archive_byte_size"] === void 0 ? 0 : byteSizeValue(row, "archive_byte_size", "bundles"),
64
- metadata: metadata(row),
65
- manifest_storage_uri: nullableString(row, "manifest_storage_uri", "bundles"),
66
- manifest_file_hash: nullableString(row, "manifest_file_hash", "bundles"),
67
- asset_base_storage_uri: nullableString(row, "asset_base_storage_uri", "bundles")
68
- };
69
- };
70
- const patchRow = (row) => ({
71
- id: stringValue(row, "id", "bundle_patches"),
72
- bundle_id: stringValue(row, "bundle_id", "bundle_patches"),
73
- base_bundle_id: stringValue(row, "base_bundle_id", "bundle_patches"),
74
- base_file_hash: stringValue(row, "base_file_hash", "bundle_patches"),
75
- patch_file_hash: stringValue(row, "patch_file_hash", "bundle_patches"),
76
- patch_storage_uri: stringValue(row, "patch_storage_uri", "bundle_patches"),
77
- byte_size: byteSizeValue(row, "byte_size", "bundle_patches"),
78
- order_index: numberValue(row, "order_index", "bundle_patches")
79
- });
80
- const channelRow = (row) => ({
81
- id: stringValue(row, "id", "channels"),
82
- name: stringValue(row, "name", "channels")
83
- });
84
- const eventRow = (row) => {
85
- const type = stringValue(row, "type", "bundle_events");
86
- const platform = stringValue(row, "platform", "bundle_events");
87
- const fromBundleId = nullableString(row, "from_bundle_id", "bundle_events");
88
- const updateStrategy = nullableString(row, "update_strategy", "bundle_events");
89
- if (platform !== "ios" && platform !== "android" || !((type === "UPDATE_APPLIED" || type === "RECOVERED" || type === "RELEASE_ADOPTED") && typeof fromBundleId === "string" && (updateStrategy === "fingerprint" || updateStrategy === "appVersion") || type === "UNCHANGED" && fromBundleId === null && updateStrategy === null)) throw new InvalidD1RowError("bundle_events");
90
- return {
91
- id: stringValue(row, "id", "bundle_events"),
92
- type,
93
- install_id: stringValue(row, "install_id", "bundle_events"),
94
- user_id: nullableString(row, "user_id", "bundle_events"),
95
- username: nullableString(row, "username", "bundle_events"),
96
- from_release_id: nullableString(row, "from_release_id", "bundle_events"),
97
- from_bundle_id: fromBundleId,
98
- to_release_id: nullableString(row, "to_release_id", "bundle_events"),
99
- to_bundle_id: stringValue(row, "to_bundle_id", "bundle_events"),
100
- platform,
101
- app_version: stringValue(row, "app_version", "bundle_events"),
102
- channel: stringValue(row, "channel", "bundle_events"),
103
- cohort: stringValue(row, "cohort", "bundle_events"),
104
- update_strategy: updateStrategy,
105
- fingerprint_hash: nullableString(row, "fingerprint_hash", "bundle_events"),
106
- sdk_version: nullableString(row, "sdk_version", "bundle_events"),
107
- received_at_ms: numberValue(row, "received_at_ms", "bundle_events")
108
- };
109
- };
110
- const apiKeyRow = (row) => {
111
- const role = stringValue(row, "role", "api_keys");
112
- if (role !== "client") throw new InvalidD1RowError("api_keys");
113
- const revokedAt = row["revoked_at_ms"];
114
- return {
115
- id: stringValue(row, "id", "api_keys"),
116
- hash: stringValue(row, "hash", "api_keys"),
117
- name: stringValue(row, "name", "api_keys"),
118
- prefix: stringValue(row, "prefix", "api_keys"),
119
- role,
120
- created_at_ms: numberValue(row, "created_at_ms", "api_keys"),
121
- revoked_at_ms: revokedAt === null ? null : numberValue(row, "revoked_at_ms", "api_keys")
122
- };
123
- };
124
- const releaseTargetCohorts = (row) => {
125
- const value = jsonValue(row["target_cohorts"], "releases");
126
- if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new InvalidD1RowError("releases");
127
- return value.filter((item) => typeof item === "string");
128
- };
129
- const releaseRow = (row) => {
130
- const platform = stringValue(row, "platform", "releases");
131
- const kind = stringValue(row, "kind", "releases");
132
- const strategy = stringValue(row, "strategy", "releases");
133
- const operation = stringValue(row, "operation", "releases");
134
- if (platform !== "ios" && platform !== "android" || kind !== "BUNDLE" && kind !== "EMBEDDED" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT" || operation !== "DEPLOY" && operation !== "PROMOTE" && operation !== "ROLLBACK") throw new InvalidD1RowError("releases");
135
- return {
136
- id: stringValue(row, "id", "releases"),
137
- revision: numberValue(row, "revision", "releases"),
138
- scope_key: stringValue(row, "scope_key", "releases"),
139
- channel_id: stringValue(row, "channel_id", "releases"),
140
- platform,
141
- kind,
142
- bundle_id: nullableString(row, "bundle_id", "releases"),
143
- strategy,
144
- target_app_version: nullableString(row, "target_app_version", "releases"),
145
- fingerprint_hash: nullableString(row, "fingerprint_hash", "releases"),
146
- enabled: booleanValue(row, "enabled", "releases"),
147
- should_force_update: booleanValue(row, "should_force_update", "releases"),
148
- message: nullableString(row, "message", "releases"),
149
- rollout_cohort_count: numberValue(row, "rollout_cohort_count", "releases"),
150
- target_cohorts: releaseTargetCohorts(row),
151
- operation,
152
- source_release_id: nullableString(row, "source_release_id", "releases"),
153
- created_at_ms: numberValue(row, "created_at_ms", "releases"),
154
- updated_at_ms: numberValue(row, "updated_at_ms", "releases")
155
- };
156
- };
157
- const releaseCatalogRow = (row) => {
158
- const platform = stringValue(row, "platform", "release_catalogs");
159
- const strategy = stringValue(row, "strategy", "release_catalogs");
160
- if (platform !== "ios" && platform !== "android" || strategy !== "APP_VERSION" && strategy !== "FINGERPRINT") throw new InvalidD1RowError("release_catalogs");
161
- return {
162
- scope_key: stringValue(row, "scope_key", "release_catalogs"),
163
- catalog_id: stringValue(row, "catalog_id", "release_catalogs"),
164
- strategy,
165
- channel_id: stringValue(row, "channel_id", "release_catalogs"),
166
- channel_key: stringValue(row, "channel_key", "release_catalogs"),
167
- platform,
168
- fingerprint_hash: nullableString(row, "fingerprint_hash", "release_catalogs"),
169
- generation: numberValue(row, "generation", "release_catalogs"),
170
- payload: stringValue(row, "payload", "release_catalogs"),
171
- catalog_hash: stringValue(row, "catalog_hash", "release_catalogs"),
172
- byte_size: numberValue(row, "byte_size", "release_catalogs"),
173
- is_tombstone: booleanValue(row, "is_tombstone", "release_catalogs"),
174
- updated_at_ms: numberValue(row, "updated_at_ms", "release_catalogs")
175
- };
176
- };
177
- function parseD1Row(model, value) {
178
- if (!isRecord(value)) throw new InvalidD1RowError(model);
179
- switch (model) {
180
- case "bundles": return bundleRow(value);
181
- case "bundle_patches": return patchRow(value);
182
- case "channels": return channelRow(value);
183
- case "bundle_events": return eventRow(value);
184
- case "api_keys": return apiKeyRow(value);
185
- case "releases": return releaseRow(value);
186
- case "release_catalogs": return releaseCatalogRow(value);
187
- }
188
- }
189
- //#endregion
190
- //#region src/d1Sql.ts
191
- var InvalidD1PredicateError = class extends Error {
192
- name = "InvalidD1PredicateError";
193
- };
194
- const encodeD1Value = (value) => {
195
- const encoded = JSON.stringify(value);
196
- if (encoded === void 0) throw new TypeError("D1 values must be JSON-serializable.");
197
- return encoded;
198
- };
199
- const bind = (value) => ({
200
- sql: "json_extract(?, '$')",
201
- params: [encodeD1Value(value)]
202
- });
203
- const escapeLikePattern = (value) => value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
204
- const stringPredicate = (condition, operator) => {
205
- if (typeof condition.value !== "string") throw new InvalidD1PredicateError();
206
- if (condition.mode !== "insensitive") {
207
- const parameter = bind(condition.value);
208
- const column = `${condition.field} COLLATE BINARY`;
209
- const value = `${parameter.sql} COLLATE BINARY`;
210
- if (operator === "contains" || operator === "starts_with") return {
211
- sql: `instr(${column}, ${value}) ${operator === "contains" ? "> 0" : "= 1"}`,
212
- params: parameter.params
213
- };
214
- return {
215
- sql: `substr(${column}, length(${condition.field}) - length(${parameter.sql}) + 1) = ${value}`,
216
- params: [...parameter.params, ...parameter.params]
217
- };
218
- }
219
- const literal = escapeLikePattern(condition.value);
220
- const parameter = bind(operator === "contains" ? `%${literal}%` : operator === "starts_with" ? `${literal}%` : `%${literal}`);
221
- return {
222
- sql: `lower(${condition.field}) LIKE lower(${parameter.sql}) ESCAPE '\\'`,
223
- params: parameter.params
224
- };
225
- };
226
- const predicate = (condition) => {
227
- const operator = condition.operator ?? "eq";
228
- switch (operator) {
229
- case "eq":
230
- case "ne": {
231
- if (condition.value === null) return {
232
- sql: `${condition.field} IS ${operator === "ne" ? "NOT " : ""}NULL`,
233
- params: []
234
- };
235
- const parameter = bind(condition.value);
236
- const insensitive = "mode" in condition && condition.mode === "insensitive";
237
- const column = insensitive ? `lower(${condition.field})` : condition.field;
238
- const value = insensitive ? `lower(${parameter.sql})` : parameter.sql;
239
- return {
240
- sql: `${column} ${operator === "eq" ? "=" : "<>"} ${value}`,
241
- params: parameter.params
242
- };
243
- }
244
- case "gt":
245
- case "gte":
246
- case "lt":
247
- case "lte": {
248
- const sqlOperator = {
249
- gt: ">",
250
- gte: ">=",
251
- lt: "<",
252
- lte: "<="
253
- }[operator];
254
- const parameter = bind(condition.value);
255
- return {
256
- sql: `${condition.field} ${sqlOperator} ${parameter.sql}`,
257
- params: parameter.params
258
- };
259
- }
260
- case "in":
261
- case "not_in":
262
- if (!Array.isArray(condition.value)) throw new InvalidD1PredicateError();
263
- if (condition.value.length === 0) return {
264
- sql: operator === "not_in" ? "1 = 1" : "1 = 0",
265
- params: []
266
- };
267
- return {
268
- sql: `${condition.field} ${operator === "not_in" ? "NOT " : ""}IN (SELECT value FROM json_each(?))`,
269
- params: [encodeD1Value(condition.value)]
270
- };
271
- case "contains":
272
- case "starts_with":
273
- case "ends_with": return stringPredicate(condition, operator);
274
- }
275
- };
276
- const buildD1Where = (where) => {
277
- const [first, ...rest] = where ?? [];
278
- if (first === void 0) return {
279
- sql: "",
280
- params: []
281
- };
282
- const initial = predicate(first);
283
- let sql = initial.sql;
284
- const params = [...initial.params];
285
- for (const condition of rest) {
286
- const next = predicate(condition);
287
- sql = `(${sql} ${condition.connector === "OR" ? "OR" : "AND"} ${next.sql})`;
288
- params.push(...next.params);
289
- }
1
+ import { createEngineDatabase, createSqlAdapter, createStorageAdapter, createStorageDownloadUrl, createStorageKeyBuilder, createStorageUri, parseStorageUri } from "@hot-updater/plugin-core";
2
+ import { apiKeys } from "@hot-updater/server/plugins/api-keys";
3
+ import { insights } from "@hot-updater/server/plugins/insights";
4
+ //#region src/d1Executor.ts
5
+ var D1ExecutionError = class extends Error {
6
+ name = "D1ExecutionError";
7
+ constructor() {
8
+ super("D1 did not successfully execute every requested statement");
9
+ }
10
+ };
11
+ const toSqlResult = (result) => {
12
+ if (result.success === false) throw new D1ExecutionError();
290
13
  return {
291
- sql: ` WHERE ${sql}`,
292
- params
14
+ rows: result.results ?? [],
15
+ changes: result.meta?.changes ?? 0
293
16
  };
294
17
  };
295
- const buildD1Order = (orderBy) => orderBy === void 0 ? "" : ` ORDER BY ${orderBy.map((clause) => `${clause.field} ${clause.direction.toUpperCase()}${clause.nulls ? ` NULLS ${clause.nulls.toUpperCase()}` : ""}`).join(", ")}`;
296
- const d1Placeholders = (count) => Array.from({ length: count }, () => "json_extract(?, '$')").join(", ");
297
- const encodeD1Values = (values) => values.map(encodeD1Value);
298
- //#endregion
299
- //#region src/d1Query.ts
300
- const d1TableNames = {
301
- bundles: "bundles",
302
- bundle_patches: "bundle_patches",
303
- channels: "channels",
304
- bundle_events: "bundle_events",
305
- api_keys: "api_keys",
306
- release_catalogs: "release_catalogs",
307
- releases: "releases"
308
- };
309
- var D1QueryResultError = class extends Error {
310
- name = "D1QueryResultError";
311
- operation = "count";
312
- constructor(result) {
313
- super("D1 count query returned a malformed first row");
314
- this.result = result;
315
- }
316
- };
317
- const countD1Rows = async (executor, input) => {
318
- const where = buildD1Where(input.where);
319
- const table = d1TableNames[input.model];
320
- const source = input.distinct === void 0 ? `${table}${where.sql}` : `(SELECT DISTINCT ${input.distinct.join(", ")} FROM ${table}${where.sql}) AS distinct_rows`;
321
- const row = (await executor.query(`SELECT COUNT(*) AS count FROM ${source}`, where.params))[0];
322
- if (typeof row !== "object" || row === null || Array.isArray(row)) throw new D1QueryResultError(row);
323
- const count = Reflect.get(row, "count");
324
- if (typeof count !== "number" || !Number.isFinite(count)) throw new D1QueryResultError(row);
325
- return count;
326
- };
327
- const findManyD1Rows = async (executor, input) => {
328
- const where = buildD1Where(input.where);
329
- const orderBy = input.orderBy;
330
- const order = buildD1Order(orderBy);
331
- const table = d1TableNames[input.model];
332
- const source = input.distinctOn === void 0 ? `SELECT * FROM ${table}${where.sql}${order}` : `SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY ${input.distinctOn.fields.join(", ")}${order}) AS __hot_updater_rank FROM ${table}${where.sql}) AS distinct_rows WHERE __hot_updater_rank = 1${order}`;
333
- const pageParams = encodeD1Values([input.limit, input.offset]);
334
- const rows = await executor.query(`${source} LIMIT json_extract(?, '$') OFFSET json_extract(?, '$')`, [...where.params, ...pageParams]);
335
- switch (input.model) {
336
- case "bundles": return rows.map((row) => parseD1Row("bundles", row));
337
- case "bundle_patches": return rows.map((row) => parseD1Row("bundle_patches", row));
338
- case "channels": return rows.map((row) => parseD1Row("channels", row));
339
- case "bundle_events": return rows.map((row) => parseD1Row("bundle_events", row));
340
- case "api_keys": return rows.map((row) => parseD1Row("api_keys", row));
341
- case "releases": return rows.map((row) => parseD1Row("releases", row));
342
- case "release_catalogs": return rows.map((row) => parseD1Row("release_catalogs", row));
343
- }
344
- };
345
- //#endregion
346
- //#region src/d1Implementation.ts
347
- const EXPECTATION_CONFLICT_MARKER = "HOT_UPDATER_COMMIT_EXPECTATION_CONFLICT";
348
- const expectationGuard = (expectations) => {
349
- const clauses = [];
350
- const params = [];
351
- for (const expectation of expectations) {
352
- const isRelease = expectation.model === "releases";
353
- const table = isRelease ? "releases" : "release_catalogs";
354
- const keyField = isRelease ? "id" : "scope_key";
355
- const versionField = isRelease ? "revision" : "generation";
356
- const key = isRelease ? expectation.id : expectation.scopeKey;
357
- const version = isRelease ? expectation.revision : expectation.generation;
358
- if (version === null) {
359
- clauses.push(`NOT EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$'))`);
360
- params.push(...encodeD1Values([key]));
361
- } else {
362
- clauses.push(`EXISTS (SELECT 1 FROM ${table} WHERE ${keyField} = json_extract(?, '$') AND ${versionField} = json_extract(?, '$'))`);
363
- params.push(...encodeD1Values([key, version]));
364
- }
365
- }
366
- return {
367
- sql: clauses.join(" AND ") || "1",
368
- params
369
- };
370
- };
371
- const readVersion = (row, field) => {
372
- if (typeof row !== "object" || row === null) return null;
373
- const value = Reflect.get(row, field);
374
- return typeof value === "number" ? value : null;
375
- };
376
- const expectationConflict = async (executor, expectations) => {
377
- for (const expectation of expectations) {
378
- const isRelease = expectation.model === "releases";
379
- const table = isRelease ? "releases" : "release_catalogs";
380
- const keyField = isRelease ? "id" : "scope_key";
381
- const versionField = isRelease ? "revision" : "generation";
382
- const key = isRelease ? expectation.id : expectation.scopeKey;
383
- const expectedVersion = isRelease ? expectation.revision : expectation.generation;
384
- const actualVersion = readVersion((await executor.query(`SELECT ${versionField} FROM ${table} WHERE ${keyField} = json_extract(?, '$') LIMIT 1`, encodeD1Values([key])))[0], versionField);
385
- if (actualVersion !== expectedVersion) return {
386
- committed: false,
387
- conflict: {
388
- actualVersion,
389
- changeIndex: -1,
390
- expectedVersion,
391
- key,
392
- model: expectation.model,
393
- reason: "version_conflict"
394
- }
395
- };
396
- }
397
- return null;
398
- };
399
- const isExpectationConflictError = (error) => {
18
+ /** D1 reports SQLite errors in the message; the SQL core classifies codes. */
19
+ const withSqliteCode = (error) => {
400
20
  const message = error instanceof Error ? error.message : String(error);
401
- return message.includes(EXPECTATION_CONFLICT_MARKER) || message.toLowerCase().includes("malformed json");
402
- };
403
- const bundleValues = (row) => [
404
- row.id,
405
- row.platform,
406
- row.file_hash,
407
- row.git_commit_hash,
408
- row.storage_uri,
409
- row.archive_byte_size,
410
- row.metadata,
411
- row.manifest_storage_uri,
412
- row.manifest_file_hash,
413
- row.asset_base_storage_uri
414
- ];
415
- const patchValues = (row) => [
416
- row.id,
417
- row.bundle_id,
418
- row.base_bundle_id,
419
- row.base_file_hash,
420
- row.patch_file_hash,
421
- row.patch_storage_uri,
422
- row.byte_size,
423
- row.order_index
424
- ];
425
- const channelValues = (row) => [row.id, row.name];
426
- const insertQuery = (input, guard, conflictMode = "returnExisting") => {
427
- let columns;
428
- let values;
429
- switch (input.model) {
430
- case "bundles":
431
- columns = [
432
- "id",
433
- "platform",
434
- "file_hash",
435
- "git_commit_hash",
436
- "storage_uri",
437
- "archive_byte_size",
438
- "metadata",
439
- "manifest_storage_uri",
440
- "manifest_file_hash",
441
- "asset_base_storage_uri"
442
- ];
443
- values = bundleValues(input.data);
444
- break;
445
- case "bundle_patches":
446
- columns = [
447
- "id",
448
- "bundle_id",
449
- "base_bundle_id",
450
- "base_file_hash",
451
- "patch_file_hash",
452
- "patch_storage_uri",
453
- "byte_size",
454
- "order_index"
455
- ];
456
- values = patchValues(input.data);
457
- break;
458
- case "channels":
459
- columns = ["id", "name"];
460
- values = channelValues(input.data);
461
- break;
462
- case "bundle_events":
463
- case "api_keys":
464
- case "release_catalogs":
465
- case "releases":
466
- columns = Object.keys(input.data);
467
- values = Object.values(input.data);
468
- break;
469
- }
470
- const conflict = input.onConflict !== "ignore" ? "" : input.model === "channels" ? conflictMode === "ignore" ? " ON CONFLICT(name) DO NOTHING" : " ON CONFLICT(name) DO UPDATE SET name = excluded.name" : input.model === "api_keys" ? conflictMode === "ignore" ? " ON CONFLICT(hash) DO NOTHING" : " ON CONFLICT(hash) DO UPDATE SET hash = excluded.hash" : "";
471
- return {
472
- sql: `INSERT INTO ${d1TableNames[input.model]} (${columns.join(", ")}) ${guard === void 0 ? `VALUES (${d1Placeholders(values.length)})` : `SELECT ${d1Placeholders(values.length)} WHERE ${guard.sql}`}${conflict} RETURNING *`,
473
- params: [...encodeD1Values(values), ...guard?.params ?? []]
474
- };
475
- };
476
- const updateEntries = (update) => Object.entries(update);
477
- const updateQuery = (input, guard) => {
478
- const entries = updateEntries(input.update);
479
- const where = buildD1Where(input.where);
480
- if (entries.length === 0) return {
481
- sql: `SELECT * FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""} LIMIT 1`,
482
- params: [...where.params, ...guard?.params ?? []]
483
- };
484
- const assignments = entries.map(([field]) => `${field} = json_extract(?, '$')`).join(", ");
485
- return {
486
- sql: `UPDATE ${d1TableNames[input.model]} SET ${assignments}${where.sql}${guard ? ` AND ${guard.sql}` : ""} RETURNING *`,
487
- params: [
488
- ...encodeD1Values(entries.map(([, value]) => value)),
489
- ...where.params,
490
- ...guard?.params ?? []
491
- ]
492
- };
493
- };
494
- const deleteQuery = (input, guard) => {
495
- const where = buildD1Where(input.where);
496
- return {
497
- sql: `DELETE FROM ${d1TableNames[input.model]}${where.sql}${guard ? ` AND ${guard.sql}` : ""}`,
498
- params: [...where.params, ...guard?.params ?? []]
499
- };
500
- };
501
- const insertedKey = (change) => {
502
- if (change.operation !== "insert") return void 0;
503
- if (change.model === "bundles") return `bundles:${change.row.id}`;
504
- if (change.model === "apiKeys") return `api_keys:${change.row.id}`;
505
- if (change.model === "releases") return `releases:${change.row.id}`;
506
- };
507
- const requiredRow = (change, changeIndex) => {
508
- if (change.operation !== "update") return void 0;
509
- if (change.model === "bundles") return {
510
- model: "bundles",
511
- id: change.where.id,
512
- changeIndex
513
- };
514
- if (change.model === "apiKeys") return {
515
- model: "api_keys",
516
- id: change.where.id,
517
- changeIndex
518
- };
519
- if (change.model === "releases") return {
520
- model: "releases",
521
- id: change.where.id,
522
- changeIndex
523
- };
524
- };
525
- const channelDeletePrecondition = (changes, changeIndex, channelId) => {
526
- const releaseEffects = /* @__PURE__ */ new Map();
527
- for (const change of changes.slice(0, changeIndex)) {
528
- if (change.model !== "releases") continue;
529
- switch (change.operation) {
530
- case "insert":
531
- releaseEffects.set(change.row.id, change.row.channel_id);
532
- break;
533
- case "update": break;
534
- case "delete":
535
- releaseEffects.set(change.where.id, null);
536
- break;
537
- }
538
- }
539
- return {
540
- id: channelId,
541
- excludedReleaseIds: [...releaseEffects.keys()],
542
- addedReferenceReleaseIds: [...releaseEffects].filter(([, finalChannelId]) => finalChannelId === channelId).map(([releaseId]) => releaseId)
543
- };
544
- };
545
- const changeQuery = (change, guard) => {
546
- switch (change.model) {
547
- case "bundles": switch (change.operation) {
548
- case "insert": return insertQuery({
549
- model: "bundles",
550
- data: change.row
551
- }, guard);
552
- case "update": return updateQuery({
553
- model: "bundles",
554
- where: [{
555
- field: "id",
556
- value: change.where.id
557
- }],
558
- update: change.update
559
- }, guard);
560
- case "delete": return deleteQuery({
561
- model: "bundles",
562
- where: [{
563
- field: "id",
564
- value: change.where.id
565
- }]
566
- }, guard);
567
- }
568
- case "bundlePatches": return change.operation === "insert" ? insertQuery({
569
- model: "bundle_patches",
570
- data: change.row
571
- }, guard) : deleteQuery({
572
- model: "bundle_patches",
573
- where: [{
574
- field: "bundle_id",
575
- value: change.where.bundleId
576
- }]
577
- }, guard);
578
- case "releases": switch (change.operation) {
579
- case "insert": return insertQuery({
580
- model: "releases",
581
- data: change.row
582
- }, guard);
583
- case "update": return updateQuery({
584
- model: "releases",
585
- where: [{
586
- field: "id",
587
- value: change.where.id
588
- }],
589
- update: change.update
590
- }, guard);
591
- case "delete": return deleteQuery({
592
- model: "releases",
593
- where: [{
594
- field: "id",
595
- value: change.where.id
596
- }]
597
- }, guard);
598
- }
599
- case "releaseCatalogs": return {
600
- sql: `INSERT INTO release_catalogs (${Object.keys(change.row).join(", ")}) SELECT ${d1Placeholders(Object.keys(change.row).length)} WHERE ${guard.sql} ON CONFLICT(scope_key) DO UPDATE SET ${Object.keys(change.row).filter((field) => field !== "scope_key").map((field) => `${field} = excluded.${field}`).join(", ")} RETURNING *`,
601
- params: [...encodeD1Values(Object.values(change.row)), ...guard.params]
602
- };
603
- case "channels": return change.operation === "insert" ? insertQuery({
604
- model: "channels",
605
- data: change.row,
606
- onConflict: change.onConflict
607
- }, guard, "ignore") : {
608
- sql: `DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) AND ${guard.sql}`,
609
- params: [...encodeD1Values([change.where.id, change.where.id]), ...guard.params]
610
- };
611
- case "insights": return insertQuery({
612
- model: "bundle_events",
613
- data: change.row
614
- }, guard);
615
- case "apiKeys": return change.operation === "insert" ? insertQuery({
616
- model: "api_keys",
617
- data: change.row,
618
- onConflict: change.onConflict
619
- }, guard, "ignore") : updateQuery({
620
- model: "api_keys",
621
- where: [{
622
- field: "id",
623
- value: change.where.id
624
- }],
625
- update: { revoked_at_ms: change.update.revokedAtMs }
626
- }, guard);
627
- }
628
- };
629
- const createCommitPlan = (input) => {
630
- const checks = [];
631
- const statements = [];
632
- const expectations = input.expectations ?? [];
633
- if (expectations.length > 0) {
634
- const guard = expectationGuard(expectations);
635
- statements.push({
636
- sql: `SELECT CASE WHEN ${guard.sql} THEN 1 ELSE json_extract('${EXPECTATION_CONFLICT_MARKER}', '$') END AS expectation_guard`,
637
- params: guard.params
638
- });
639
- }
640
- const requiredRows = [];
641
- const channelDeletes = [];
642
- const inserted = /* @__PURE__ */ new Set();
643
- for (const [changeIndex, change] of input.changes.entries()) {
644
- const required = requiredRow(change, changeIndex);
645
- if (required !== void 0) {
646
- if (!inserted.has(`${required.model}:${required.id}`)) {
647
- requiredRows.push(required);
648
- checks.push({
649
- changeIndex,
650
- resultIndex: statements.length,
651
- conflictWhen: "empty",
652
- reason: "not_found"
653
- });
654
- statements.push({
655
- sql: `SELECT id FROM ${d1TableNames[required.model]} WHERE id = json_extract(?, '$') LIMIT 1`,
656
- params: encodeD1Values([required.id])
657
- });
658
- }
659
- }
660
- if (change.model === "channels" && change.operation === "delete") {
661
- const precondition = channelDeletePrecondition(input.changes, changeIndex, change.where.id);
662
- channelDeletes.push(precondition);
663
- checks.push({
664
- changeIndex,
665
- resultIndex: statements.length,
666
- conflictWhen: "nonempty",
667
- reason: "referenced"
668
- });
669
- statements.push({
670
- sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') AND id NOT IN (SELECT value FROM json_each(?)) UNION ALL SELECT value AS id FROM json_each(?) LIMIT 1",
671
- params: encodeD1Values([
672
- precondition.id,
673
- precondition.excludedReleaseIds,
674
- precondition.addedReferenceReleaseIds
675
- ])
676
- });
677
- }
678
- const key = insertedKey(change);
679
- if (key !== void 0) inserted.add(key);
680
- }
681
- const guard = {
682
- sql: `NOT EXISTS (
683
- SELECT 1 FROM json_each(?) AS required
684
- WHERE NOT EXISTS (
685
- SELECT 1 FROM bundles
686
- WHERE json_extract(required.value, '$.model') = 'bundles'
687
- AND bundles.id = json_extract(required.value, '$.id')
688
- UNION ALL
689
- SELECT 1 FROM api_keys
690
- WHERE json_extract(required.value, '$.model') = 'api_keys'
691
- AND api_keys.id = json_extract(required.value, '$.id')
692
- UNION ALL
693
- SELECT 1 FROM channels
694
- WHERE json_extract(required.value, '$.model') = 'channels'
695
- AND channels.id = json_extract(required.value, '$.id')
696
- UNION ALL
697
- SELECT 1 FROM releases
698
- WHERE json_extract(required.value, '$.model') = 'releases'
699
- AND releases.id = json_extract(required.value, '$.id')
700
- )
701
- ) AND NOT EXISTS (
702
- SELECT 1 FROM json_each(?) AS channel_delete
703
- WHERE EXISTS (
704
- SELECT 1 FROM releases
705
- WHERE releases.channel_id = json_extract(channel_delete.value, '$.id')
706
- AND releases.id NOT IN (
707
- SELECT value FROM json_each(
708
- json_extract(channel_delete.value, '$.excludedReleaseIds')
709
- )
710
- )
711
- UNION ALL
712
- SELECT value FROM json_each(
713
- json_extract(channel_delete.value, '$.addedReferenceReleaseIds')
714
- )
715
- )
716
- )`,
717
- params: encodeD1Values([requiredRows.map(({ model, id }) => ({
718
- model,
719
- id
720
- })), channelDeletes])
721
- };
722
- statements.push(...input.changes.map((change) => changeQuery(change, guard)));
723
- return {
724
- checks,
725
- statements
726
- };
727
- };
728
- const resultForPlan = (plan, results) => {
729
- const missing = plan.checks.find(({ conflictWhen, resultIndex }) => conflictWhen === "empty" ? (results[resultIndex]?.length ?? 0) === 0 : (results[resultIndex]?.length ?? 0) > 0);
730
- return missing === void 0 ? { committed: true } : {
731
- committed: false,
732
- conflict: {
733
- changeIndex: missing.changeIndex,
734
- reason: missing.reason
735
- }
736
- };
737
- };
738
- const insertChannel = async (executor, input) => {
739
- const insert = insertQuery({
740
- model: "channels",
741
- data: input.row,
742
- onConflict: "ignore"
743
- }, void 0, "ignore");
744
- const select = {
745
- sql: "SELECT id, name FROM channels WHERE name = json_extract(?, '$') LIMIT 1",
746
- params: encodeD1Values([input.row.name])
747
- };
748
- const [insertedRows = [], canonicalRows = []] = await executor.batch([insert, select]);
749
- return {
750
- row: parseD1Row("channels", canonicalRows[0]),
751
- inserted: insertedRows.length > 0
752
- };
753
- };
754
- const deleteChannel = async (executor, input) => {
755
- const [channelRows = [], referenceRows = [], deletedRows = []] = await executor.batch([
756
- {
757
- sql: "SELECT id FROM channels WHERE id = json_extract(?, '$') LIMIT 1",
758
- params: encodeD1Values([input.id])
759
- },
760
- {
761
- sql: "SELECT id FROM releases WHERE channel_id = json_extract(?, '$') LIMIT 1",
762
- params: encodeD1Values([input.id])
763
- },
764
- {
765
- sql: "DELETE FROM channels WHERE id = json_extract(?, '$') AND NOT EXISTS (SELECT 1 FROM releases WHERE channel_id = json_extract(?, '$')) RETURNING id",
766
- params: encodeD1Values([input.id, input.id])
767
- }
768
- ]);
769
- if (channelRows.length === 0) return {
770
- deleted: false,
771
- reason: "not_found"
772
- };
773
- if (referenceRows.length > 0) return {
774
- deleted: false,
775
- reason: "not_empty"
776
- };
777
- return deletedRows.length > 0 ? { deleted: true } : {
778
- deleted: false,
779
- reason: "not_empty"
780
- };
781
- };
782
- const createD1Implementation = (executor) => ({
783
- async create(input) {
784
- const query = insertQuery(input);
785
- const rows = await executor.query(query.sql, query.params);
786
- switch (input.model) {
787
- case "bundles": return parseD1Row("bundles", rows[0]);
788
- case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
789
- case "channels": return parseD1Row("channels", rows[0]);
790
- case "bundle_events": return parseD1Row("bundle_events", rows[0]);
791
- case "api_keys": return parseD1Row("api_keys", rows[0]);
792
- case "releases": return parseD1Row("releases", rows[0]);
793
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
794
- }
795
- },
796
- async update(input) {
797
- const query = updateQuery(input);
798
- const rows = await executor.query(query.sql, query.params);
799
- if (rows[0] === void 0) return null;
800
- switch (input.model) {
801
- case "bundles": return parseD1Row("bundles", rows[0]);
802
- case "api_keys": return parseD1Row("api_keys", rows[0]);
803
- case "releases": return parseD1Row("releases", rows[0]);
804
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
805
- }
806
- },
807
- async delete(input) {
808
- const query = deleteQuery(input);
809
- await executor.query(query.sql, query.params);
810
- },
811
- count: (input) => countD1Rows(executor, input),
812
- async findOne(input) {
813
- const where = buildD1Where(input.where);
814
- const rows = await executor.query(`SELECT * FROM ${d1TableNames[input.model]}${where.sql} LIMIT 1`, where.params);
815
- if (rows[0] === void 0) return null;
816
- switch (input.model) {
817
- case "bundles": return parseD1Row("bundles", rows[0]);
818
- case "bundle_patches": return parseD1Row("bundle_patches", rows[0]);
819
- case "channels": return parseD1Row("channels", rows[0]);
820
- case "api_keys": return parseD1Row("api_keys", rows[0]);
821
- case "releases": return parseD1Row("releases", rows[0]);
822
- case "release_catalogs": return parseD1Row("release_catalogs", rows[0]);
823
- }
824
- },
825
- findMany: (input) => findManyD1Rows(executor, input),
826
- insertChannel: (input) => insertChannel(executor, input),
827
- deleteChannel: (input) => deleteChannel(executor, input),
828
- async commit(input) {
829
- if (input.changes.length === 0) return { committed: true };
830
- const expectations = input.expectations ?? [];
831
- const conflict = await expectationConflict(executor, expectations);
832
- if (conflict !== null) return conflict;
833
- const plan = createCommitPlan(input);
834
- try {
835
- return resultForPlan(plan, await executor.batch(plan.statements));
836
- } catch (error) {
837
- if (expectations.length === 0 || !isExpectationConflictError(error)) throw error;
838
- return await expectationConflict(executor, expectations) ?? {
839
- committed: false,
840
- conflict: {
841
- actualVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
842
- changeIndex: -1,
843
- expectedVersion: expectations[0].model === "releases" ? expectations[0].revision : expectations[0].generation,
844
- key: expectations[0].model === "releases" ? expectations[0].id : expectations[0].scopeKey,
845
- model: expectations[0].model,
846
- reason: "version_conflict"
21
+ const code = /UNIQUE constraint failed|constraint failed: .*PRIMARY/iu.test(message) ? "SQLITE_CONSTRAINT_UNIQUE" : /SQLITE_BUSY|database is locked/iu.test(message) ? "SQLITE_BUSY" : void 0;
22
+ return code === void 0 ? error : Object.assign(new Error(message, { cause: error }), { code });
23
+ };
24
+ /**
25
+ * Hot Updater's database on D1: the storage engine through the shared SQL
26
+ * core, fenced by the schema settings. D1 has no interactive transactions,
27
+ * so each write is one atomic batch that evaluates its guards first
28
+ * (`_hu_write`).
29
+ */
30
+ const createD1Database = (runner) => {
31
+ return createEngineDatabase({
32
+ name: "d1Database",
33
+ adapter: createSqlAdapter({
34
+ executor: {
35
+ dialect: "sqlite",
36
+ execute: (statement) => runner.query(statement).catch((error) => {
37
+ throw withSqliteCode(error);
38
+ }),
39
+ batch: (statements) => runner.batch(statements).catch((error) => {
40
+ throw withSqliteCode(error);
41
+ }),
42
+ transaction: () => {
43
+ throw new Error("D1 has no interactive transactions; writes are batches.");
847
44
  }
848
- };
849
- }
45
+ },
46
+ maxOps: 450,
47
+ maxParams: 100
48
+ })
49
+ });
50
+ };
51
+ //#endregion
52
+ //#region src/worker/d1Database.ts
53
+ /** Hot Updater's database on a D1 binding, inside a Worker. */
54
+ const d1Database = (database) => createD1Database({
55
+ query: async ({ sql, params }) => toSqlResult(await database.prepare(sql).bind(...params).all()),
56
+ async batch(statements) {
57
+ const results = await database.batch(statements.map(({ sql, params }) => database.prepare(sql).bind(...params)));
58
+ if (results.length !== statements.length) throw new D1ExecutionError();
59
+ return results.map(toSqlResult);
850
60
  }
851
61
  });
852
62
  //#endregion
853
- //#region src/worker/d1Database.ts
854
- const d1Database = (database) => {
855
- const adapter = createDatabasePluginAdapter("d1Database", createD1Implementation({
856
- async query(sql, params) {
857
- return (await database.prepare(sql).bind(...params).all()).results ?? [];
858
- },
859
- async batch(statements) {
860
- return (await database.batch(statements.map(({ sql, params }) => database.prepare(sql).bind(...params)))).map(({ results }) => results ?? []);
861
- }
862
- }));
863
- return createDatabasePlugin({
864
- name: "d1Database",
865
- models: adapter.models,
866
- commit: adapter.commit,
867
- ...adapter.dispose ? { dispose: adapter.dispose } : {}
868
- });
869
- };
63
+ //#region src/plugins.ts
64
+ /**
65
+ * The plugins the managed Cloudflare server runs: Insights, and API keys on every
66
+ * client route. Its Worker imports this list.
67
+ */
68
+ const plugins = [insights(), apiKeys()];
870
69
  //#endregion
871
70
  //#region src/r2WorkerStorage.ts
872
71
  const r2WorkerStorage = (config) => {
@@ -877,7 +76,7 @@ const r2WorkerStorage = (config) => {
877
76
  if (parsed.bucket !== config.bucketName) throw new Error(`Bucket name mismatch: expected "${config.bucketName}", but found "${parsed.bucket}".`);
878
77
  return parsed;
879
78
  };
880
- return createStoragePlugin({
79
+ return createStorageAdapter({
881
80
  name: "r2Storage",
882
81
  protocol: "r2",
883
82
  async put({ key, body, contentLength, contentType }) {
@@ -925,4 +124,4 @@ const r2WorkerStorage = (config) => {
925
124
  });
926
125
  };
927
126
  //#endregion
928
- export { d1Database, r2WorkerStorage as r2Storage };
127
+ export { d1Database, plugins, r2WorkerStorage as r2Storage };