@hot-updater/cloudflare 0.30.12 → 0.31.1
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/dist/iac/index.cjs +0 -1
- package/dist/iac/index.mjs +0 -1
- package/dist/index.cjs +164 -57
- package/dist/index.d.cts +1 -21
- package/dist/index.d.mts +1 -21
- package/dist/index.mjs +174 -67
- package/dist/worker/index.cjs +109 -25
- package/dist/worker/index.d.cts +6 -1
- package/dist/worker/index.d.mts +6 -1
- package/dist/worker/index.mjs +111 -27
- package/package.json +7 -7
- package/sql/bundles.sql +23 -3
- package/src/cloudflareWorkerDatabase.ts +198 -15
- package/src/d1Database.spec.ts +85 -3
- package/src/d1Database.ts +206 -19
- package/src/r2Storage.spec.ts +85 -0
- package/src/r2Storage.ts +33 -37
- package/src/r2WorkerStorage.spec.ts +50 -0
- package/src/r2WorkerStorage.ts +52 -20
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +3038 -247
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0005_hot-updater_0.31.0.sql +24 -0
- package/worker/src/index.integration.spec.ts +209 -18
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
3
|
-
|
|
3
|
+
getAssetBaseStorageUri,
|
|
4
|
+
getBundlePatches,
|
|
5
|
+
getManifestFileHash,
|
|
6
|
+
getManifestStorageUri,
|
|
7
|
+
stripBundleArtifactMetadata,
|
|
4
8
|
} from "@hot-updater/core";
|
|
5
9
|
import type {
|
|
6
10
|
Bundle,
|
|
@@ -48,6 +52,36 @@ interface BuildQueryResult {
|
|
|
48
52
|
params: unknown[];
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
interface D1WorkerBundleRow {
|
|
56
|
+
id: string;
|
|
57
|
+
channel: string;
|
|
58
|
+
enabled: number | boolean;
|
|
59
|
+
should_force_update: number | boolean;
|
|
60
|
+
file_hash: string;
|
|
61
|
+
git_commit_hash: string | null;
|
|
62
|
+
message: string | null;
|
|
63
|
+
platform: "ios" | "android";
|
|
64
|
+
target_app_version: string | null;
|
|
65
|
+
storage_uri: string;
|
|
66
|
+
fingerprint_hash: string | null;
|
|
67
|
+
metadata: unknown;
|
|
68
|
+
manifest_storage_uri?: string | null;
|
|
69
|
+
manifest_file_hash?: string | null;
|
|
70
|
+
asset_base_storage_uri?: string | null;
|
|
71
|
+
rollout_cohort_count: number | null;
|
|
72
|
+
target_cohorts: string | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface D1WorkerBundlePatchRow {
|
|
76
|
+
id: string;
|
|
77
|
+
bundle_id: string;
|
|
78
|
+
base_bundle_id: string;
|
|
79
|
+
base_file_hash: string;
|
|
80
|
+
patch_file_hash: string;
|
|
81
|
+
patch_storage_uri: string;
|
|
82
|
+
order_index: number | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
51
85
|
function buildWhereClause(
|
|
52
86
|
conditions: QueryConditions | undefined,
|
|
53
87
|
): BuildQueryResult {
|
|
@@ -168,7 +202,54 @@ function parseTargetCohorts(value: unknown): string[] | null {
|
|
|
168
202
|
return null;
|
|
169
203
|
}
|
|
170
204
|
|
|
171
|
-
|
|
205
|
+
const parseMetadata = (value: unknown): Bundle["metadata"] => {
|
|
206
|
+
if (!value) return undefined;
|
|
207
|
+
if (typeof value === "string") {
|
|
208
|
+
try {
|
|
209
|
+
return parseMetadata(JSON.parse(value) as unknown);
|
|
210
|
+
} catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return typeof value === "object" && !Array.isArray(value)
|
|
215
|
+
? (value as Bundle["metadata"])
|
|
216
|
+
: undefined;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const buildBundlePatchId = (bundleId: string, baseBundleId: string) =>
|
|
220
|
+
`${bundleId}:${baseBundleId}`;
|
|
221
|
+
|
|
222
|
+
const bundleToPatchRows = (bundle: Bundle): D1WorkerBundlePatchRow[] =>
|
|
223
|
+
getBundlePatches(bundle).map((patch, index) => ({
|
|
224
|
+
id: buildBundlePatchId(bundle.id, patch.baseBundleId),
|
|
225
|
+
bundle_id: bundle.id,
|
|
226
|
+
base_bundle_id: patch.baseBundleId,
|
|
227
|
+
base_file_hash: patch.baseFileHash,
|
|
228
|
+
patch_file_hash: patch.patchFileHash,
|
|
229
|
+
patch_storage_uri: patch.patchStorageUri,
|
|
230
|
+
order_index: index,
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
function transformRowToBundle(
|
|
234
|
+
row: D1WorkerBundleRow,
|
|
235
|
+
patchRows: D1WorkerBundlePatchRow[] = [],
|
|
236
|
+
): Bundle {
|
|
237
|
+
const rawMetadata = parseMetadata(row.metadata);
|
|
238
|
+
const patches = patchRows
|
|
239
|
+
.slice()
|
|
240
|
+
.sort(
|
|
241
|
+
(left, right) =>
|
|
242
|
+
(left.order_index ?? 0) - (right.order_index ?? 0) ||
|
|
243
|
+
left.base_bundle_id.localeCompare(right.base_bundle_id),
|
|
244
|
+
)
|
|
245
|
+
.map((patch) => ({
|
|
246
|
+
baseBundleId: patch.base_bundle_id,
|
|
247
|
+
baseFileHash: patch.base_file_hash,
|
|
248
|
+
patchFileHash: patch.patch_file_hash,
|
|
249
|
+
patchStorageUri: patch.patch_storage_uri,
|
|
250
|
+
}));
|
|
251
|
+
const primaryPatch = patches[0] ?? null;
|
|
252
|
+
|
|
172
253
|
return {
|
|
173
254
|
id: row.id,
|
|
174
255
|
channel: row.channel,
|
|
@@ -181,7 +262,15 @@ function transformRowToBundle(row: SnakeCaseBundle): Bundle {
|
|
|
181
262
|
targetAppVersion: row.target_app_version,
|
|
182
263
|
storageUri: row.storage_uri,
|
|
183
264
|
fingerprintHash: row.fingerprint_hash,
|
|
184
|
-
metadata:
|
|
265
|
+
metadata: stripBundleArtifactMetadata(rawMetadata),
|
|
266
|
+
manifestStorageUri: row.manifest_storage_uri ?? null,
|
|
267
|
+
manifestFileHash: row.manifest_file_hash ?? null,
|
|
268
|
+
assetBaseStorageUri: row.asset_base_storage_uri ?? null,
|
|
269
|
+
patches,
|
|
270
|
+
patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
|
|
271
|
+
patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
|
|
272
|
+
patchFileHash: primaryPatch?.patchFileHash ?? null,
|
|
273
|
+
patchStorageUri: primaryPatch?.patchStorageUri ?? null,
|
|
185
274
|
rolloutCohortCount:
|
|
186
275
|
(row.rollout_cohort_count as number | null) ??
|
|
187
276
|
DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
@@ -236,12 +325,43 @@ export const d1WorkerDatabase = <
|
|
|
236
325
|
return result ?? null;
|
|
237
326
|
};
|
|
238
327
|
|
|
328
|
+
const getPatchMap = async (
|
|
329
|
+
bundleIds: string[],
|
|
330
|
+
context?: HotUpdaterContext<TContext>,
|
|
331
|
+
) => {
|
|
332
|
+
const patchMap = new Map<string, D1WorkerBundlePatchRow[]>();
|
|
333
|
+
|
|
334
|
+
if (bundleIds.length === 0) {
|
|
335
|
+
return patchMap;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const placeholders = bundleIds.map(() => "?").join(", ");
|
|
339
|
+
const rows = await queryAll<D1WorkerBundlePatchRow>(
|
|
340
|
+
`
|
|
341
|
+
SELECT *
|
|
342
|
+
FROM bundle_patches
|
|
343
|
+
WHERE bundle_id IN (${placeholders})
|
|
344
|
+
ORDER BY order_index ASC, base_bundle_id ASC
|
|
345
|
+
`,
|
|
346
|
+
bundleIds,
|
|
347
|
+
context,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
for (const row of rows) {
|
|
351
|
+
const current = patchMap.get(row.bundle_id) ?? [];
|
|
352
|
+
current.push(row);
|
|
353
|
+
patchMap.set(row.bundle_id, current);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return patchMap;
|
|
357
|
+
};
|
|
358
|
+
|
|
239
359
|
const queryBundlesForUpdateInfo = async (
|
|
240
360
|
conditions: QueryConditions,
|
|
241
361
|
context?: HotUpdaterContext<TContext>,
|
|
242
362
|
): Promise<Bundle[]> => {
|
|
243
363
|
const { sql: whereClause, params } = buildWhereClause(conditions);
|
|
244
|
-
const rows = await queryAll<
|
|
364
|
+
const rows = await queryAll<D1WorkerBundleRow>(
|
|
245
365
|
`
|
|
246
366
|
SELECT * FROM bundles
|
|
247
367
|
${whereClause}
|
|
@@ -249,8 +369,14 @@ export const d1WorkerDatabase = <
|
|
|
249
369
|
params,
|
|
250
370
|
context,
|
|
251
371
|
);
|
|
372
|
+
const patchMap = await getPatchMap(
|
|
373
|
+
rows.map((row) => row.id),
|
|
374
|
+
context,
|
|
375
|
+
);
|
|
252
376
|
|
|
253
|
-
return rows.map(
|
|
377
|
+
return rows.map((row) =>
|
|
378
|
+
transformRowToBundle(row, patchMap.get(row.id)),
|
|
379
|
+
);
|
|
254
380
|
};
|
|
255
381
|
|
|
256
382
|
const getTargetAppVersionsForUpdateInfo = async (
|
|
@@ -324,13 +450,16 @@ export const d1WorkerDatabase = <
|
|
|
324
450
|
}),
|
|
325
451
|
|
|
326
452
|
async getBundleById(bundleId, context) {
|
|
327
|
-
const row = await
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
453
|
+
const [row, patchMap] = await Promise.all([
|
|
454
|
+
queryFirst<D1WorkerBundleRow>(
|
|
455
|
+
"SELECT * FROM bundles WHERE id = ? LIMIT 1",
|
|
456
|
+
[bundleId],
|
|
457
|
+
context,
|
|
458
|
+
),
|
|
459
|
+
getPatchMap([bundleId], context),
|
|
460
|
+
]);
|
|
332
461
|
|
|
333
|
-
return row ? transformRowToBundle(row) : null;
|
|
462
|
+
return row ? transformRowToBundle(row, patchMap.get(bundleId)) : null;
|
|
334
463
|
},
|
|
335
464
|
|
|
336
465
|
async getBundles(options, context) {
|
|
@@ -352,13 +481,19 @@ export const d1WorkerDatabase = <
|
|
|
352
481
|
);
|
|
353
482
|
const total = countRows[0]?.total ?? 0;
|
|
354
483
|
|
|
355
|
-
const rows = await queryAll<
|
|
484
|
+
const rows = await queryAll<D1WorkerBundleRow>(
|
|
356
485
|
`SELECT * FROM bundles${whereClause} ${orderSql} LIMIT ? OFFSET ?`,
|
|
357
486
|
[...params, limit, offset],
|
|
358
487
|
context,
|
|
359
488
|
);
|
|
360
489
|
|
|
361
|
-
const
|
|
490
|
+
const patchMap = await getPatchMap(
|
|
491
|
+
rows.map((row) => row.id),
|
|
492
|
+
context,
|
|
493
|
+
);
|
|
494
|
+
const bundles = rows.map((row) =>
|
|
495
|
+
transformRowToBundle(row, patchMap.get(row.id)),
|
|
496
|
+
);
|
|
362
497
|
|
|
363
498
|
const paginationOptions: PaginationOptions = { limit, offset };
|
|
364
499
|
return {
|
|
@@ -385,6 +520,14 @@ export const d1WorkerDatabase = <
|
|
|
385
520
|
|
|
386
521
|
for (const operation of changedSets) {
|
|
387
522
|
if (operation.operation === "delete") {
|
|
523
|
+
await db
|
|
524
|
+
.prepare("DELETE FROM bundle_patches WHERE bundle_id = ?")
|
|
525
|
+
.bind(operation.data.id)
|
|
526
|
+
.run();
|
|
527
|
+
await db
|
|
528
|
+
.prepare("DELETE FROM bundle_patches WHERE base_bundle_id = ?")
|
|
529
|
+
.bind(operation.data.id)
|
|
530
|
+
.run();
|
|
388
531
|
await db
|
|
389
532
|
.prepare("DELETE FROM bundles WHERE id = ?")
|
|
390
533
|
.bind(operation.data.id)
|
|
@@ -408,10 +551,13 @@ export const d1WorkerDatabase = <
|
|
|
408
551
|
storage_uri,
|
|
409
552
|
fingerprint_hash,
|
|
410
553
|
metadata,
|
|
554
|
+
manifest_storage_uri,
|
|
555
|
+
manifest_file_hash,
|
|
556
|
+
asset_base_storage_uri,
|
|
411
557
|
rollout_cohort_count,
|
|
412
558
|
target_cohorts
|
|
413
559
|
)
|
|
414
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
560
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
415
561
|
`)
|
|
416
562
|
.bind(
|
|
417
563
|
bundle.id,
|
|
@@ -425,13 +571,50 @@ export const d1WorkerDatabase = <
|
|
|
425
571
|
bundle.targetAppVersion,
|
|
426
572
|
bundle.storageUri,
|
|
427
573
|
bundle.fingerprintHash,
|
|
428
|
-
JSON.stringify(
|
|
574
|
+
JSON.stringify(
|
|
575
|
+
stripBundleArtifactMetadata(bundle.metadata) ?? {},
|
|
576
|
+
),
|
|
577
|
+
getManifestStorageUri(bundle),
|
|
578
|
+
getManifestFileHash(bundle),
|
|
579
|
+
getAssetBaseStorageUri(bundle),
|
|
429
580
|
bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
430
581
|
bundle.targetCohorts
|
|
431
582
|
? JSON.stringify(bundle.targetCohorts)
|
|
432
583
|
: null,
|
|
433
584
|
)
|
|
434
585
|
.run();
|
|
586
|
+
|
|
587
|
+
await db
|
|
588
|
+
.prepare("DELETE FROM bundle_patches WHERE bundle_id = ?")
|
|
589
|
+
.bind(bundle.id)
|
|
590
|
+
.run();
|
|
591
|
+
|
|
592
|
+
const patchRows = bundleToPatchRows(bundle);
|
|
593
|
+
for (const patchRow of patchRows) {
|
|
594
|
+
await db
|
|
595
|
+
.prepare(`
|
|
596
|
+
INSERT OR REPLACE INTO bundle_patches (
|
|
597
|
+
id,
|
|
598
|
+
bundle_id,
|
|
599
|
+
base_bundle_id,
|
|
600
|
+
base_file_hash,
|
|
601
|
+
patch_file_hash,
|
|
602
|
+
patch_storage_uri,
|
|
603
|
+
order_index
|
|
604
|
+
)
|
|
605
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
606
|
+
`)
|
|
607
|
+
.bind(
|
|
608
|
+
patchRow.id,
|
|
609
|
+
patchRow.bundle_id,
|
|
610
|
+
patchRow.base_bundle_id,
|
|
611
|
+
patchRow.base_file_hash,
|
|
612
|
+
patchRow.patch_file_hash,
|
|
613
|
+
patchRow.patch_storage_uri,
|
|
614
|
+
patchRow.order_index ?? 0,
|
|
615
|
+
)
|
|
616
|
+
.run();
|
|
617
|
+
}
|
|
435
618
|
}
|
|
436
619
|
},
|
|
437
620
|
};
|
package/src/d1Database.spec.ts
CHANGED
|
@@ -20,12 +20,26 @@ type D1Row = {
|
|
|
20
20
|
storage_uri: string;
|
|
21
21
|
fingerprint_hash: string | null;
|
|
22
22
|
metadata: string;
|
|
23
|
+
manifest_storage_uri?: string | null;
|
|
24
|
+
manifest_file_hash?: string | null;
|
|
25
|
+
asset_base_storage_uri?: string | null;
|
|
23
26
|
rollout_cohort_count: number | null;
|
|
24
27
|
target_cohorts: string | null;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
type D1PatchRow = {
|
|
31
|
+
id: string;
|
|
32
|
+
bundle_id: string;
|
|
33
|
+
base_bundle_id: string;
|
|
34
|
+
base_file_hash: string;
|
|
35
|
+
patch_file_hash: string;
|
|
36
|
+
patch_storage_uri: string;
|
|
37
|
+
order_index: number | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const { rows, patchRows } = vi.hoisted(() => ({
|
|
28
41
|
rows: new Map<string, D1Row>(),
|
|
42
|
+
patchRows: new Map<string, D1PatchRow>(),
|
|
29
43
|
}));
|
|
30
44
|
|
|
31
45
|
vi.mock("pg-minify", () => ({
|
|
@@ -170,6 +184,25 @@ vi.mock("cloudflare", () => ({
|
|
|
170
184
|
return createPage(row ? [row] : []);
|
|
171
185
|
}
|
|
172
186
|
|
|
187
|
+
if (
|
|
188
|
+
normalizedSql.startsWith(
|
|
189
|
+
"select * from bundle_patches where bundle_id in",
|
|
190
|
+
)
|
|
191
|
+
) {
|
|
192
|
+
const selectedBundleIds = new Set(
|
|
193
|
+
params.map((value) => String(value)),
|
|
194
|
+
);
|
|
195
|
+
const result = Array.from(patchRows.values())
|
|
196
|
+
.filter((row) => selectedBundleIds.has(row.bundle_id))
|
|
197
|
+
.sort(
|
|
198
|
+
(a, b) =>
|
|
199
|
+
Number(a.order_index ?? 0) - Number(b.order_index ?? 0) ||
|
|
200
|
+
a.base_bundle_id.localeCompare(b.base_bundle_id),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return createPage(result);
|
|
204
|
+
}
|
|
205
|
+
|
|
173
206
|
if (normalizedSql.startsWith("select * from bundles")) {
|
|
174
207
|
const { filteredRows, index } = getFilteredRows(sql, params);
|
|
175
208
|
const limit = Number(params[index] ?? filteredRows.length);
|
|
@@ -214,6 +247,34 @@ vi.mock("cloudflare", () => ({
|
|
|
214
247
|
return createPage([]);
|
|
215
248
|
}
|
|
216
249
|
|
|
250
|
+
if (
|
|
251
|
+
normalizedSql.startsWith(
|
|
252
|
+
"delete from bundle_patches where bundle_id = ?",
|
|
253
|
+
)
|
|
254
|
+
) {
|
|
255
|
+
const bundleId = String(params[0]);
|
|
256
|
+
for (const [id, row] of patchRows.entries()) {
|
|
257
|
+
if (row.bundle_id === bundleId) {
|
|
258
|
+
patchRows.delete(id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return createPage([]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (
|
|
265
|
+
normalizedSql.startsWith(
|
|
266
|
+
"delete from bundle_patches where base_bundle_id = ?",
|
|
267
|
+
)
|
|
268
|
+
) {
|
|
269
|
+
const baseBundleId = String(params[0]);
|
|
270
|
+
for (const [id, row] of patchRows.entries()) {
|
|
271
|
+
if (row.base_bundle_id === baseBundleId) {
|
|
272
|
+
patchRows.delete(id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return createPage([]);
|
|
276
|
+
}
|
|
277
|
+
|
|
217
278
|
if (normalizedSql.startsWith("insert or replace into bundles")) {
|
|
218
279
|
const row: D1Row = {
|
|
219
280
|
id: params[0],
|
|
@@ -228,13 +289,32 @@ vi.mock("cloudflare", () => ({
|
|
|
228
289
|
storage_uri: params[9],
|
|
229
290
|
fingerprint_hash: params[10],
|
|
230
291
|
metadata: params[11],
|
|
231
|
-
|
|
232
|
-
|
|
292
|
+
manifest_storage_uri: params[12],
|
|
293
|
+
manifest_file_hash: params[13],
|
|
294
|
+
asset_base_storage_uri: params[14],
|
|
295
|
+
rollout_cohort_count: params[15],
|
|
296
|
+
target_cohorts: params[16],
|
|
233
297
|
};
|
|
234
298
|
rows.set(row.id, row);
|
|
235
299
|
return createPage([]);
|
|
236
300
|
}
|
|
237
301
|
|
|
302
|
+
if (
|
|
303
|
+
normalizedSql.startsWith("insert or replace into bundle_patches")
|
|
304
|
+
) {
|
|
305
|
+
const row: D1PatchRow = {
|
|
306
|
+
id: params[0],
|
|
307
|
+
bundle_id: params[1],
|
|
308
|
+
base_bundle_id: params[2],
|
|
309
|
+
base_file_hash: params[3],
|
|
310
|
+
patch_file_hash: params[4],
|
|
311
|
+
patch_storage_uri: params[5],
|
|
312
|
+
order_index: Number(params[6] ?? 0),
|
|
313
|
+
};
|
|
314
|
+
patchRows.set(row.id, row);
|
|
315
|
+
return createPage([]);
|
|
316
|
+
}
|
|
317
|
+
|
|
238
318
|
throw new Error(`Unsupported SQL in D1 mock: ${sql}`);
|
|
239
319
|
},
|
|
240
320
|
},
|
|
@@ -247,6 +327,7 @@ describe("d1Database plugin", () => {
|
|
|
247
327
|
|
|
248
328
|
beforeEach(() => {
|
|
249
329
|
rows.clear();
|
|
330
|
+
patchRows.clear();
|
|
250
331
|
plugin = d1Database({
|
|
251
332
|
databaseId: "test-db-id",
|
|
252
333
|
accountId: "test-account-id",
|
|
@@ -279,6 +360,7 @@ describe("d1Database plugin", () => {
|
|
|
279
360
|
setupGetUpdateInfoTestSuite({
|
|
280
361
|
getUpdateInfo: async (bundles, args) => {
|
|
281
362
|
rows.clear();
|
|
363
|
+
patchRows.clear();
|
|
282
364
|
|
|
283
365
|
for (const bundle of bundles) {
|
|
284
366
|
await plugin.appendBundle(bundle);
|