@hot-updater/cloudflare 0.30.11 → 0.31.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/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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Migration number: 0005 2026-04-22T00:00:00.000Z
|
|
2
|
+
|
|
3
|
+
-- HotUpdater.bundle_artifact_columns
|
|
4
|
+
|
|
5
|
+
ALTER TABLE bundles ADD COLUMN manifest_storage_uri TEXT;
|
|
6
|
+
ALTER TABLE bundles ADD COLUMN manifest_file_hash TEXT;
|
|
7
|
+
ALTER TABLE bundles ADD COLUMN asset_base_storage_uri TEXT;
|
|
8
|
+
|
|
9
|
+
CREATE TABLE bundle_patches (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
bundle_id TEXT NOT NULL,
|
|
12
|
+
base_bundle_id TEXT NOT NULL,
|
|
13
|
+
base_file_hash TEXT NOT NULL,
|
|
14
|
+
patch_file_hash TEXT NOT NULL,
|
|
15
|
+
patch_storage_uri TEXT NOT NULL,
|
|
16
|
+
order_index INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
FOREIGN KEY (bundle_id) REFERENCES bundles(id) ON DELETE CASCADE,
|
|
18
|
+
FOREIGN KEY (base_bundle_id) REFERENCES bundles(id) ON DELETE CASCADE
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS bundle_patches_bundle_id_idx
|
|
22
|
+
ON bundle_patches(bundle_id);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS bundle_patches_base_bundle_id_idx
|
|
24
|
+
ON bundle_patches(base_bundle_id);
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
getBundlePatches,
|
|
3
|
+
type Bundle,
|
|
4
|
+
type GetBundlesArgs,
|
|
5
|
+
NIL_UUID,
|
|
6
|
+
} from "@hot-updater/core";
|
|
7
|
+
import {
|
|
8
|
+
setupBsdiffManifestUpdateInfoTestSuite,
|
|
9
|
+
setupGetUpdateInfoTestSuite,
|
|
10
|
+
} from "@hot-updater/test-utils";
|
|
3
11
|
import { env } from "cloudflare:test";
|
|
4
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
beforeAll,
|
|
14
|
+
beforeEach,
|
|
15
|
+
describe,
|
|
16
|
+
expect,
|
|
17
|
+
inject,
|
|
18
|
+
it,
|
|
19
|
+
vi,
|
|
20
|
+
} from "vitest";
|
|
5
21
|
|
|
6
22
|
import worker, { HOT_UPDATER_BASE_PATH } from "./index";
|
|
7
23
|
|
|
@@ -21,29 +37,38 @@ declare module "cloudflare:test" {
|
|
|
21
37
|
|
|
22
38
|
const PUBLIC_BASE_URL = "https://updates.example.com";
|
|
23
39
|
|
|
40
|
+
const sqlString = (value: string) => `'${value.replaceAll("'", "''")}'`;
|
|
41
|
+
|
|
24
42
|
const createInsertBundleQuery = (bundle: Bundle) => {
|
|
25
43
|
const rolloutCohortCount = bundle.rolloutCohortCount ?? 1000;
|
|
26
44
|
const targetCohorts = bundle.targetCohorts
|
|
27
|
-
?
|
|
45
|
+
? sqlString(JSON.stringify(bundle.targetCohorts))
|
|
28
46
|
: "null";
|
|
47
|
+
const metadata = sqlString(JSON.stringify(bundle.metadata ?? {}));
|
|
29
48
|
|
|
30
49
|
return `
|
|
31
50
|
INSERT INTO bundles (
|
|
32
51
|
id, file_hash, platform, target_app_version,
|
|
33
52
|
should_force_update, enabled, git_commit_hash, message, channel,
|
|
34
|
-
storage_uri, fingerprint_hash,
|
|
53
|
+
storage_uri, fingerprint_hash, metadata, manifest_storage_uri,
|
|
54
|
+
manifest_file_hash, asset_base_storage_uri, rollout_cohort_count,
|
|
55
|
+
target_cohorts
|
|
35
56
|
) VALUES (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
${bundle.targetAppVersion ?
|
|
57
|
+
${sqlString(bundle.id)},
|
|
58
|
+
${sqlString(bundle.fileHash)},
|
|
59
|
+
${sqlString(bundle.platform)},
|
|
60
|
+
${bundle.targetAppVersion ? sqlString(bundle.targetAppVersion) : "null"},
|
|
40
61
|
${bundle.shouldForceUpdate},
|
|
41
62
|
${bundle.enabled},
|
|
42
|
-
${bundle.gitCommitHash ?
|
|
43
|
-
${bundle.message ?
|
|
44
|
-
|
|
45
|
-
${bundle.storageUri ?
|
|
46
|
-
${bundle.fingerprintHash ?
|
|
63
|
+
${bundle.gitCommitHash ? sqlString(bundle.gitCommitHash) : "null"},
|
|
64
|
+
${bundle.message ? sqlString(bundle.message) : "null"},
|
|
65
|
+
${sqlString(bundle.channel)},
|
|
66
|
+
${bundle.storageUri ? sqlString(bundle.storageUri) : "null"},
|
|
67
|
+
${bundle.fingerprintHash ? sqlString(bundle.fingerprintHash) : "null"},
|
|
68
|
+
${metadata},
|
|
69
|
+
${bundle.manifestStorageUri ? sqlString(bundle.manifestStorageUri) : "null"},
|
|
70
|
+
${bundle.manifestFileHash ? sqlString(bundle.manifestFileHash) : "null"},
|
|
71
|
+
${bundle.assetBaseStorageUri ? sqlString(bundle.assetBaseStorageUri) : "null"},
|
|
47
72
|
${rolloutCohortCount},
|
|
48
73
|
${targetCohorts}
|
|
49
74
|
) ON CONFLICT(id) DO UPDATE SET
|
|
@@ -57,11 +82,44 @@ const createInsertBundleQuery = (bundle: Bundle) => {
|
|
|
57
82
|
channel = excluded.channel,
|
|
58
83
|
storage_uri = excluded.storage_uri,
|
|
59
84
|
fingerprint_hash = excluded.fingerprint_hash,
|
|
85
|
+
metadata = excluded.metadata,
|
|
86
|
+
manifest_storage_uri = excluded.manifest_storage_uri,
|
|
87
|
+
manifest_file_hash = excluded.manifest_file_hash,
|
|
88
|
+
asset_base_storage_uri = excluded.asset_base_storage_uri,
|
|
60
89
|
rollout_cohort_count = excluded.rollout_cohort_count,
|
|
61
90
|
target_cohorts = excluded.target_cohorts;
|
|
62
91
|
`;
|
|
63
92
|
};
|
|
64
93
|
|
|
94
|
+
const createInsertBundlePatchQueries = (bundle: Bundle) =>
|
|
95
|
+
getBundlePatches(bundle).map(
|
|
96
|
+
(patch, index) => `
|
|
97
|
+
INSERT INTO bundle_patches (
|
|
98
|
+
id,
|
|
99
|
+
bundle_id,
|
|
100
|
+
base_bundle_id,
|
|
101
|
+
base_file_hash,
|
|
102
|
+
patch_file_hash,
|
|
103
|
+
patch_storage_uri,
|
|
104
|
+
order_index
|
|
105
|
+
) VALUES (
|
|
106
|
+
${sqlString(`${bundle.id}:${patch.baseBundleId}`)},
|
|
107
|
+
${sqlString(bundle.id)},
|
|
108
|
+
${sqlString(patch.baseBundleId)},
|
|
109
|
+
${sqlString(patch.baseFileHash)},
|
|
110
|
+
${sqlString(patch.patchFileHash)},
|
|
111
|
+
${sqlString(patch.patchStorageUri)},
|
|
112
|
+
${index}
|
|
113
|
+
) ON CONFLICT(id) DO UPDATE SET
|
|
114
|
+
bundle_id = excluded.bundle_id,
|
|
115
|
+
base_bundle_id = excluded.base_bundle_id,
|
|
116
|
+
base_file_hash = excluded.base_file_hash,
|
|
117
|
+
patch_file_hash = excluded.patch_file_hash,
|
|
118
|
+
patch_storage_uri = excluded.patch_storage_uri,
|
|
119
|
+
order_index = excluded.order_index;
|
|
120
|
+
`,
|
|
121
|
+
);
|
|
122
|
+
|
|
65
123
|
const toRuntimeBundle = (bundle: Bundle): Bundle => {
|
|
66
124
|
return {
|
|
67
125
|
...bundle,
|
|
@@ -72,9 +130,20 @@ const toRuntimeBundle = (bundle: Bundle): Bundle => {
|
|
|
72
130
|
const seedBundles = async (bundles: Bundle[]) => {
|
|
73
131
|
for (const bundle of bundles.map(toRuntimeBundle)) {
|
|
74
132
|
await env.DB.prepare(createInsertBundleQuery(bundle)).run();
|
|
133
|
+
for (const patchSql of createInsertBundlePatchQueries(bundle)) {
|
|
134
|
+
await env.DB.prepare(patchSql).run();
|
|
135
|
+
}
|
|
75
136
|
}
|
|
76
137
|
};
|
|
77
138
|
|
|
139
|
+
const putR2Object = async (key: string, value: string, contentType: string) => {
|
|
140
|
+
await env.BUCKET.put(key, value, {
|
|
141
|
+
httpMetadata: {
|
|
142
|
+
contentType,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
78
147
|
const createCanonicalPath = (args: GetBundlesArgs) => {
|
|
79
148
|
const channel = args.channel ?? "production";
|
|
80
149
|
const minBundleId = args.minBundleId ?? NIL_UUID;
|
|
@@ -95,12 +164,11 @@ describe.sequential("cloudflare worker runtime acceptance", () => {
|
|
|
95
164
|
});
|
|
96
165
|
|
|
97
166
|
beforeEach(async () => {
|
|
167
|
+
await env.DB.prepare("DELETE FROM bundle_patches").run();
|
|
98
168
|
await env.DB.prepare("DELETE FROM bundles").run();
|
|
99
169
|
});
|
|
100
170
|
|
|
101
|
-
const
|
|
102
|
-
await seedBundles(bundles);
|
|
103
|
-
|
|
171
|
+
const requestUpdateInfo = async (args: GetBundlesArgs) => {
|
|
104
172
|
const response = await worker.fetch(
|
|
105
173
|
new Request(`${PUBLIC_BASE_URL}${createCanonicalPath(args)}`),
|
|
106
174
|
env,
|
|
@@ -109,7 +177,130 @@ describe.sequential("cloudflare worker runtime acceptance", () => {
|
|
|
109
177
|
return (await response.json()) as any;
|
|
110
178
|
};
|
|
111
179
|
|
|
112
|
-
|
|
180
|
+
const getUpdateInfo = async (bundles: Bundle[], args: GetBundlesArgs) => {
|
|
181
|
+
await seedBundles(bundles);
|
|
182
|
+
|
|
183
|
+
return requestUpdateInfo(args);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
setupGetUpdateInfoTestSuite({
|
|
187
|
+
getUpdateInfo,
|
|
188
|
+
manifestArtifacts: {
|
|
189
|
+
prepareArtifacts: async (fixture) => {
|
|
190
|
+
await Promise.all([
|
|
191
|
+
putR2Object(
|
|
192
|
+
`${fixture.currentBundleId}/manifest.json`,
|
|
193
|
+
JSON.stringify(fixture.currentManifest),
|
|
194
|
+
"application/json",
|
|
195
|
+
),
|
|
196
|
+
putR2Object(
|
|
197
|
+
`${fixture.nextBundleId}/manifest.json`,
|
|
198
|
+
JSON.stringify(fixture.nextManifest),
|
|
199
|
+
"application/json",
|
|
200
|
+
),
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
vi.stubGlobal(
|
|
204
|
+
"fetch",
|
|
205
|
+
vi.fn<typeof fetch>(async () => {
|
|
206
|
+
return new Response("worker subrequest failed", { status: 502 });
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
cleanup: () => {
|
|
212
|
+
vi.unstubAllGlobals();
|
|
213
|
+
},
|
|
214
|
+
currentArtifacts: {
|
|
215
|
+
assetBaseStorageUri: `r2://bundles/${fixture.currentBundleId}/files`,
|
|
216
|
+
manifestFileHash: "sig:manifest-current",
|
|
217
|
+
manifestStorageUri: `r2://bundles/${fixture.currentBundleId}/manifest.json`,
|
|
218
|
+
},
|
|
219
|
+
nextArtifacts: {
|
|
220
|
+
assetBaseStorageUri: `r2://bundles/${fixture.nextBundleId}/files`,
|
|
221
|
+
manifestFileHash: "sig:manifest-next",
|
|
222
|
+
manifestStorageUri: `r2://bundles/${fixture.nextBundleId}/manifest.json`,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
expectFileUrl: (fileUrl, fixture) => {
|
|
227
|
+
expect(fileUrl).toContain(
|
|
228
|
+
`/bundles/${fixture.nextBundleId}/files/${fixture.changedAssetPath}`,
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
expectManifestUrl: (manifestUrl, fixture) => {
|
|
232
|
+
expect(manifestUrl).toContain(`/${fixture.nextBundleId}/manifest.json`);
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
setupBsdiffManifestUpdateInfoTestSuite({
|
|
238
|
+
seedBundles,
|
|
239
|
+
getUpdateInfo: requestUpdateInfo,
|
|
240
|
+
prepareArtifacts: async (fixture) => {
|
|
241
|
+
await Promise.all([
|
|
242
|
+
putR2Object(
|
|
243
|
+
`${fixture.currentBundleId}/manifest.json`,
|
|
244
|
+
JSON.stringify(fixture.currentManifest),
|
|
245
|
+
"application/json",
|
|
246
|
+
),
|
|
247
|
+
putR2Object(
|
|
248
|
+
`${fixture.nextBundleId}/manifest.json`,
|
|
249
|
+
JSON.stringify(fixture.nextManifest),
|
|
250
|
+
"application/json",
|
|
251
|
+
),
|
|
252
|
+
putR2Object(
|
|
253
|
+
fixture.patchPath,
|
|
254
|
+
"patch-bytes",
|
|
255
|
+
"application/octet-stream",
|
|
256
|
+
),
|
|
257
|
+
putR2Object(
|
|
258
|
+
`${fixture.currentBundleId}/bundle.zip`,
|
|
259
|
+
"zip",
|
|
260
|
+
"application/zip",
|
|
261
|
+
),
|
|
262
|
+
putR2Object(
|
|
263
|
+
`${fixture.nextBundleId}/bundle.zip`,
|
|
264
|
+
"zip",
|
|
265
|
+
"application/zip",
|
|
266
|
+
),
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
vi.stubGlobal(
|
|
270
|
+
"fetch",
|
|
271
|
+
vi.fn<typeof fetch>(async () => {
|
|
272
|
+
return new Response("worker subrequest failed", { status: 502 });
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
cleanup: () => {
|
|
278
|
+
vi.unstubAllGlobals();
|
|
279
|
+
},
|
|
280
|
+
currentArtifacts: {
|
|
281
|
+
assetBaseStorageUri: `r2://bundles/${fixture.currentBundleId}/files`,
|
|
282
|
+
manifestFileHash: "sig:manifest-current",
|
|
283
|
+
manifestStorageUri: `r2://bundles/${fixture.currentBundleId}/manifest.json`,
|
|
284
|
+
},
|
|
285
|
+
nextArtifacts: {
|
|
286
|
+
assetBaseStorageUri: `r2://bundles/${fixture.nextBundleId}/files`,
|
|
287
|
+
manifestFileHash: "sig:manifest-next",
|
|
288
|
+
manifestStorageUri: `r2://bundles/${fixture.nextBundleId}/manifest.json`,
|
|
289
|
+
patches: [
|
|
290
|
+
{
|
|
291
|
+
baseBundleId: fixture.currentBundleId,
|
|
292
|
+
baseFileHash: "hash-old-bundle",
|
|
293
|
+
patchFileHash: "hash-bsdiff",
|
|
294
|
+
patchStorageUri: `r2://bundles/${fixture.patchPath}`,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
expectPatchUrl: (patchUrl, fixture) => {
|
|
301
|
+
expect(patchUrl).toContain(`/bundles/${fixture.patchPath}`);
|
|
302
|
+
},
|
|
303
|
+
});
|
|
113
304
|
|
|
114
305
|
it("serves canonical routes from the worker entrypoint", async () => {
|
|
115
306
|
await seedBundles([
|