@hot-updater/cloudflare 0.30.12 → 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
package/dist/worker/index.cjs
CHANGED
|
@@ -79,7 +79,34 @@ function parseTargetCohorts(value) {
|
|
|
79
79
|
}
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
const parseMetadata = (value) => {
|
|
83
|
+
if (!value) return void 0;
|
|
84
|
+
if (typeof value === "string") try {
|
|
85
|
+
return parseMetadata(JSON.parse(value));
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
return typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
90
|
+
};
|
|
91
|
+
const buildBundlePatchId = (bundleId, baseBundleId) => `${bundleId}:${baseBundleId}`;
|
|
92
|
+
const bundleToPatchRows = (bundle) => (0, _hot_updater_core.getBundlePatches)(bundle).map((patch, index) => ({
|
|
93
|
+
id: buildBundlePatchId(bundle.id, patch.baseBundleId),
|
|
94
|
+
bundle_id: bundle.id,
|
|
95
|
+
base_bundle_id: patch.baseBundleId,
|
|
96
|
+
base_file_hash: patch.baseFileHash,
|
|
97
|
+
patch_file_hash: patch.patchFileHash,
|
|
98
|
+
patch_storage_uri: patch.patchStorageUri,
|
|
99
|
+
order_index: index
|
|
100
|
+
}));
|
|
101
|
+
function transformRowToBundle(row, patchRows = []) {
|
|
102
|
+
const rawMetadata = parseMetadata(row.metadata);
|
|
103
|
+
const patches = patchRows.slice().sort((left, right) => (left.order_index ?? 0) - (right.order_index ?? 0) || left.base_bundle_id.localeCompare(right.base_bundle_id)).map((patch) => ({
|
|
104
|
+
baseBundleId: patch.base_bundle_id,
|
|
105
|
+
baseFileHash: patch.base_file_hash,
|
|
106
|
+
patchFileHash: patch.patch_file_hash,
|
|
107
|
+
patchStorageUri: patch.patch_storage_uri
|
|
108
|
+
}));
|
|
109
|
+
const primaryPatch = patches[0] ?? null;
|
|
83
110
|
return {
|
|
84
111
|
id: row.id,
|
|
85
112
|
channel: row.channel,
|
|
@@ -92,7 +119,15 @@ function transformRowToBundle(row) {
|
|
|
92
119
|
targetAppVersion: row.target_app_version,
|
|
93
120
|
storageUri: row.storage_uri,
|
|
94
121
|
fingerprintHash: row.fingerprint_hash,
|
|
95
|
-
metadata:
|
|
122
|
+
metadata: (0, _hot_updater_core.stripBundleArtifactMetadata)(rawMetadata),
|
|
123
|
+
manifestStorageUri: row.manifest_storage_uri ?? null,
|
|
124
|
+
manifestFileHash: row.manifest_file_hash ?? null,
|
|
125
|
+
assetBaseStorageUri: row.asset_base_storage_uri ?? null,
|
|
126
|
+
patches,
|
|
127
|
+
patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
|
|
128
|
+
patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
|
|
129
|
+
patchFileHash: primaryPatch?.patchFileHash ?? null,
|
|
130
|
+
patchStorageUri: primaryPatch?.patchStorageUri ?? null,
|
|
96
131
|
rolloutCohortCount: row.rollout_cohort_count ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
97
132
|
targetCohorts: parseTargetCohorts(row.target_cohorts)
|
|
98
133
|
};
|
|
@@ -111,12 +146,30 @@ const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin
|
|
|
111
146
|
const queryFirst = async (sql, params = [], context) => {
|
|
112
147
|
return await config.getDb(context).prepare(sql).bind(...params).first() ?? null;
|
|
113
148
|
};
|
|
149
|
+
const getPatchMap = async (bundleIds, context) => {
|
|
150
|
+
const patchMap = /* @__PURE__ */ new Map();
|
|
151
|
+
if (bundleIds.length === 0) return patchMap;
|
|
152
|
+
const rows = await queryAll(`
|
|
153
|
+
SELECT *
|
|
154
|
+
FROM bundle_patches
|
|
155
|
+
WHERE bundle_id IN (${bundleIds.map(() => "?").join(", ")})
|
|
156
|
+
ORDER BY order_index ASC, base_bundle_id ASC
|
|
157
|
+
`, bundleIds, context);
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
const current = patchMap.get(row.bundle_id) ?? [];
|
|
160
|
+
current.push(row);
|
|
161
|
+
patchMap.set(row.bundle_id, current);
|
|
162
|
+
}
|
|
163
|
+
return patchMap;
|
|
164
|
+
};
|
|
114
165
|
const queryBundlesForUpdateInfo = async (conditions, context) => {
|
|
115
166
|
const { sql: whereClause, params } = buildWhereClause(conditions);
|
|
116
|
-
|
|
167
|
+
const rows = await queryAll(`
|
|
117
168
|
SELECT * FROM bundles
|
|
118
169
|
${whereClause}
|
|
119
|
-
`, params, context)
|
|
170
|
+
`, params, context);
|
|
171
|
+
const patchMap = await getPatchMap(rows.map((row) => row.id), context);
|
|
172
|
+
return rows.map((row) => transformRowToBundle(row, patchMap.get(row.id)));
|
|
120
173
|
};
|
|
121
174
|
const getTargetAppVersionsForUpdateInfo = async ({ platform, channel, minBundleId }, context) => {
|
|
122
175
|
return (await queryAll(`
|
|
@@ -157,8 +210,8 @@ const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin
|
|
|
157
210
|
}
|
|
158
211
|
}),
|
|
159
212
|
async getBundleById(bundleId, context) {
|
|
160
|
-
const row = await queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context);
|
|
161
|
-
return row ? transformRowToBundle(row) : null;
|
|
213
|
+
const [row, patchMap] = await Promise.all([queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context), getPatchMap([bundleId], context)]);
|
|
214
|
+
return row ? transformRowToBundle(row, patchMap.get(bundleId)) : null;
|
|
162
215
|
},
|
|
163
216
|
async getBundles(options, context) {
|
|
164
217
|
const { where, limit, orderBy } = options;
|
|
@@ -166,12 +219,14 @@ const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin
|
|
|
166
219
|
const { sql: whereClause, params } = buildWhereClause(where);
|
|
167
220
|
const orderSql = orderBy?.direction === "asc" ? "ORDER BY id ASC" : "ORDER BY id DESC";
|
|
168
221
|
const total = (await queryAll(`SELECT COUNT(*) as total FROM bundles${whereClause}`, params, context))[0]?.total ?? 0;
|
|
222
|
+
const rows = await queryAll(`SELECT * FROM bundles${whereClause} ${orderSql} LIMIT ? OFFSET ?`, [
|
|
223
|
+
...params,
|
|
224
|
+
limit,
|
|
225
|
+
offset
|
|
226
|
+
], context);
|
|
227
|
+
const patchMap = await getPatchMap(rows.map((row) => row.id), context);
|
|
169
228
|
return {
|
|
170
|
-
data: (
|
|
171
|
-
...params,
|
|
172
|
-
limit,
|
|
173
|
-
offset
|
|
174
|
-
], context)).map(transformRowToBundle),
|
|
229
|
+
data: rows.map((row) => transformRowToBundle(row, patchMap.get(row.id))),
|
|
175
230
|
pagination: (0, _hot_updater_plugin_core.calculatePagination)(total, {
|
|
176
231
|
limit,
|
|
177
232
|
offset
|
|
@@ -186,6 +241,8 @@ const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin
|
|
|
186
241
|
const db = config.getDb(context);
|
|
187
242
|
for (const operation of changedSets) {
|
|
188
243
|
if (operation.operation === "delete") {
|
|
244
|
+
await db.prepare("DELETE FROM bundle_patches WHERE bundle_id = ?").bind(operation.data.id).run();
|
|
245
|
+
await db.prepare("DELETE FROM bundle_patches WHERE base_bundle_id = ?").bind(operation.data.id).run();
|
|
189
246
|
await db.prepare("DELETE FROM bundles WHERE id = ?").bind(operation.data.id).run();
|
|
190
247
|
continue;
|
|
191
248
|
}
|
|
@@ -204,11 +261,28 @@ const d1WorkerDatabase = () => (0, _hot_updater_plugin_core.createDatabasePlugin
|
|
|
204
261
|
storage_uri,
|
|
205
262
|
fingerprint_hash,
|
|
206
263
|
metadata,
|
|
264
|
+
manifest_storage_uri,
|
|
265
|
+
manifest_file_hash,
|
|
266
|
+
asset_base_storage_uri,
|
|
207
267
|
rollout_cohort_count,
|
|
208
268
|
target_cohorts
|
|
209
269
|
)
|
|
210
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
211
|
-
`).bind(bundle.id, bundle.channel, bundle.enabled ? 1 : 0, bundle.shouldForceUpdate ? 1 : 0, bundle.fileHash, bundle.gitCommitHash || null, bundle.message || null, bundle.platform, bundle.targetAppVersion, bundle.storageUri, bundle.fingerprintHash, JSON.stringify(bundle.metadata ?? {}), bundle.rolloutCohortCount ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT, bundle.targetCohorts ? JSON.stringify(bundle.targetCohorts) : null).run();
|
|
270
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
271
|
+
`).bind(bundle.id, bundle.channel, bundle.enabled ? 1 : 0, bundle.shouldForceUpdate ? 1 : 0, bundle.fileHash, bundle.gitCommitHash || null, bundle.message || null, bundle.platform, bundle.targetAppVersion, bundle.storageUri, bundle.fingerprintHash, JSON.stringify((0, _hot_updater_core.stripBundleArtifactMetadata)(bundle.metadata) ?? {}), (0, _hot_updater_core.getManifestStorageUri)(bundle), (0, _hot_updater_core.getManifestFileHash)(bundle), (0, _hot_updater_core.getAssetBaseStorageUri)(bundle), bundle.rolloutCohortCount ?? _hot_updater_core.DEFAULT_ROLLOUT_COHORT_COUNT, bundle.targetCohorts ? JSON.stringify(bundle.targetCohorts) : null).run();
|
|
272
|
+
await db.prepare("DELETE FROM bundle_patches WHERE bundle_id = ?").bind(bundle.id).run();
|
|
273
|
+
const patchRows = bundleToPatchRows(bundle);
|
|
274
|
+
for (const patchRow of patchRows) await db.prepare(`
|
|
275
|
+
INSERT OR REPLACE INTO bundle_patches (
|
|
276
|
+
id,
|
|
277
|
+
bundle_id,
|
|
278
|
+
base_bundle_id,
|
|
279
|
+
base_file_hash,
|
|
280
|
+
patch_file_hash,
|
|
281
|
+
patch_storage_uri,
|
|
282
|
+
order_index
|
|
283
|
+
)
|
|
284
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
285
|
+
`).bind(patchRow.id, patchRow.bundle_id, patchRow.base_bundle_id, patchRow.base_file_hash, patchRow.patch_file_hash, patchRow.patch_storage_uri, patchRow.order_index ?? 0).run();
|
|
212
286
|
}
|
|
213
287
|
}
|
|
214
288
|
};
|
|
@@ -224,21 +298,31 @@ const resolveJwtSecretFromContext = (context) => {
|
|
|
224
298
|
if (!jwtSecret) throw new Error("r2WorkerStorage requires env.JWT_SECRET in the hot updater context.");
|
|
225
299
|
return jwtSecret;
|
|
226
300
|
};
|
|
301
|
+
const resolveR2BucketFromContext = (context) => {
|
|
302
|
+
const bucket = context?.env?.BUCKET;
|
|
303
|
+
if (!bucket) throw new Error("r2WorkerStorage requires env.BUCKET in the hot updater context.");
|
|
304
|
+
return bucket;
|
|
305
|
+
};
|
|
306
|
+
const createPublicObjectPath = (storageUrl) => `${storageUrl.host}${storageUrl.pathname}`;
|
|
307
|
+
const createR2ObjectKey = (storageUrl) => storageUrl.pathname.replace(/^\/+/, "");
|
|
227
308
|
const r2WorkerStorage = (config) => {
|
|
228
|
-
return ()
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
async
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
309
|
+
return (0, _hot_updater_plugin_core.createRuntimeStoragePlugin)({
|
|
310
|
+
name: "r2WorkerStorage",
|
|
311
|
+
supportedProtocol: "r2",
|
|
312
|
+
factory: (config) => ({
|
|
313
|
+
async readText(storageUri, context) {
|
|
314
|
+
const storageUrl = new URL(storageUri);
|
|
315
|
+
if (storageUrl.protocol !== "r2:") throw new Error("Invalid R2 storage URI protocol");
|
|
316
|
+
const bucket = resolveR2BucketFromContext(context);
|
|
317
|
+
const key = createR2ObjectKey(storageUrl);
|
|
318
|
+
const object = await bucket.get(key);
|
|
319
|
+
if (!object) return null;
|
|
320
|
+
return object.text();
|
|
237
321
|
},
|
|
238
322
|
async getDownloadUrl(storageUri, context) {
|
|
239
323
|
const storageUrl = new URL(storageUri);
|
|
240
324
|
if (storageUrl.protocol !== "r2:") throw new Error("Invalid R2 storage URI protocol");
|
|
241
|
-
const key =
|
|
325
|
+
const key = createPublicObjectPath(storageUrl);
|
|
242
326
|
const [jwtSecret, publicBaseUrl] = await Promise.all([resolveContextValue(config.jwtSecret ?? resolveJwtSecretFromContext, context), resolveContextValue(config.publicBaseUrl, context)]);
|
|
243
327
|
const token = await (0, _hot_updater_js.signToken)(key, jwtSecret);
|
|
244
328
|
const url = new URL(publicBaseUrl);
|
|
@@ -247,8 +331,8 @@ const r2WorkerStorage = (config) => {
|
|
|
247
331
|
url.searchParams.set("token", token);
|
|
248
332
|
return { fileUrl: url.toString() };
|
|
249
333
|
}
|
|
250
|
-
}
|
|
251
|
-
};
|
|
334
|
+
})
|
|
335
|
+
})(config);
|
|
252
336
|
};
|
|
253
337
|
//#endregion
|
|
254
338
|
//#region src/worker/index.ts
|
package/dist/worker/index.d.cts
CHANGED
|
@@ -23,6 +23,11 @@ interface CloudflareWorkerDatabaseEnv {
|
|
|
23
23
|
//#region src/r2WorkerStorage.d.ts
|
|
24
24
|
interface CloudflareWorkerStorageEnv {
|
|
25
25
|
JWT_SECRET: string;
|
|
26
|
+
BUCKET: {
|
|
27
|
+
get: (key: string) => Promise<{
|
|
28
|
+
text: () => Promise<string>;
|
|
29
|
+
} | null>;
|
|
30
|
+
};
|
|
26
31
|
}
|
|
27
32
|
type ContextResolver<TContext, TValue> = (context?: HotUpdaterContext<TContext>) => TValue | Promise<TValue>;
|
|
28
33
|
interface CloudflareWorkerStorageConfig<TContext extends RequestEnvContext$1<CloudflareWorkerStorageEnv>> {
|
|
@@ -34,6 +39,6 @@ interface CloudflareWorkerStorageConfig<TContext extends RequestEnvContext$1<Clo
|
|
|
34
39
|
interface CloudflareWorkerRuntimeEnv extends CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv {}
|
|
35
40
|
type RequestEnvContext<TEnv = CloudflareWorkerRuntimeEnv> = RequestEnvContext$1<TEnv>;
|
|
36
41
|
declare const d1Database: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>() => () => _$_hot_updater_plugin_core0.DatabasePlugin<TContext>;
|
|
37
|
-
declare const r2Storage: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>(config: CloudflareWorkerStorageConfig<TContext>) => () => _$_hot_updater_plugin_core0.
|
|
42
|
+
declare const r2Storage: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>(config: CloudflareWorkerStorageConfig<TContext>) => () => _$_hot_updater_plugin_core0.RuntimeStoragePlugin<TContext>;
|
|
38
43
|
//#endregion
|
|
39
44
|
export { type CloudflareWorkerDatabaseEnv, CloudflareWorkerRuntimeEnv, type CloudflareWorkerStorageEnv, RequestEnvContext, d1Database, r2Storage, verifyJwtSignedUrl };
|
package/dist/worker/index.d.mts
CHANGED
|
@@ -23,6 +23,11 @@ interface CloudflareWorkerDatabaseEnv {
|
|
|
23
23
|
//#region src/r2WorkerStorage.d.ts
|
|
24
24
|
interface CloudflareWorkerStorageEnv {
|
|
25
25
|
JWT_SECRET: string;
|
|
26
|
+
BUCKET: {
|
|
27
|
+
get: (key: string) => Promise<{
|
|
28
|
+
text: () => Promise<string>;
|
|
29
|
+
} | null>;
|
|
30
|
+
};
|
|
26
31
|
}
|
|
27
32
|
type ContextResolver<TContext, TValue> = (context?: HotUpdaterContext<TContext>) => TValue | Promise<TValue>;
|
|
28
33
|
interface CloudflareWorkerStorageConfig<TContext extends RequestEnvContext$1<CloudflareWorkerStorageEnv>> {
|
|
@@ -34,6 +39,6 @@ interface CloudflareWorkerStorageConfig<TContext extends RequestEnvContext$1<Clo
|
|
|
34
39
|
interface CloudflareWorkerRuntimeEnv extends CloudflareWorkerDatabaseEnv, CloudflareWorkerStorageEnv {}
|
|
35
40
|
type RequestEnvContext<TEnv = CloudflareWorkerRuntimeEnv> = RequestEnvContext$1<TEnv>;
|
|
36
41
|
declare const d1Database: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>() => () => _$_hot_updater_plugin_core0.DatabasePlugin<TContext>;
|
|
37
|
-
declare const r2Storage: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>(config: CloudflareWorkerStorageConfig<TContext>) => () => _$_hot_updater_plugin_core0.
|
|
42
|
+
declare const r2Storage: <TContext extends RequestEnvContext<CloudflareWorkerRuntimeEnv> = RequestEnvContext<CloudflareWorkerRuntimeEnv>>(config: CloudflareWorkerStorageConfig<TContext>) => () => _$_hot_updater_plugin_core0.RuntimeStoragePlugin<TContext>;
|
|
38
43
|
//#endregion
|
|
39
44
|
export { type CloudflareWorkerDatabaseEnv, CloudflareWorkerRuntimeEnv, type CloudflareWorkerStorageEnv, RequestEnvContext, d1Database, r2Storage, verifyJwtSignedUrl };
|
package/dist/worker/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { signToken, verifyJwtSignedUrl } from "@hot-updater/js";
|
|
2
|
-
import { DEFAULT_ROLLOUT_COHORT_COUNT } from "@hot-updater/core";
|
|
3
|
-
import { calculatePagination, createDatabasePlugin, createDatabasePluginGetUpdateInfo } from "@hot-updater/plugin-core";
|
|
2
|
+
import { DEFAULT_ROLLOUT_COHORT_COUNT, getAssetBaseStorageUri, getBundlePatches, getManifestFileHash, getManifestStorageUri, stripBundleArtifactMetadata } from "@hot-updater/core";
|
|
3
|
+
import { calculatePagination, createDatabasePlugin, createDatabasePluginGetUpdateInfo, createRuntimeStoragePlugin } from "@hot-updater/plugin-core";
|
|
4
4
|
//#region src/cloudflareWorkerDatabase.ts
|
|
5
5
|
function buildWhereClause(conditions) {
|
|
6
6
|
if (!conditions) return {
|
|
@@ -78,7 +78,34 @@ function parseTargetCohorts(value) {
|
|
|
78
78
|
}
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
const parseMetadata = (value) => {
|
|
82
|
+
if (!value) return void 0;
|
|
83
|
+
if (typeof value === "string") try {
|
|
84
|
+
return parseMetadata(JSON.parse(value));
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
return typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
89
|
+
};
|
|
90
|
+
const buildBundlePatchId = (bundleId, baseBundleId) => `${bundleId}:${baseBundleId}`;
|
|
91
|
+
const bundleToPatchRows = (bundle) => getBundlePatches(bundle).map((patch, index) => ({
|
|
92
|
+
id: buildBundlePatchId(bundle.id, patch.baseBundleId),
|
|
93
|
+
bundle_id: bundle.id,
|
|
94
|
+
base_bundle_id: patch.baseBundleId,
|
|
95
|
+
base_file_hash: patch.baseFileHash,
|
|
96
|
+
patch_file_hash: patch.patchFileHash,
|
|
97
|
+
patch_storage_uri: patch.patchStorageUri,
|
|
98
|
+
order_index: index
|
|
99
|
+
}));
|
|
100
|
+
function transformRowToBundle(row, patchRows = []) {
|
|
101
|
+
const rawMetadata = parseMetadata(row.metadata);
|
|
102
|
+
const patches = patchRows.slice().sort((left, right) => (left.order_index ?? 0) - (right.order_index ?? 0) || left.base_bundle_id.localeCompare(right.base_bundle_id)).map((patch) => ({
|
|
103
|
+
baseBundleId: patch.base_bundle_id,
|
|
104
|
+
baseFileHash: patch.base_file_hash,
|
|
105
|
+
patchFileHash: patch.patch_file_hash,
|
|
106
|
+
patchStorageUri: patch.patch_storage_uri
|
|
107
|
+
}));
|
|
108
|
+
const primaryPatch = patches[0] ?? null;
|
|
82
109
|
return {
|
|
83
110
|
id: row.id,
|
|
84
111
|
channel: row.channel,
|
|
@@ -91,7 +118,15 @@ function transformRowToBundle(row) {
|
|
|
91
118
|
targetAppVersion: row.target_app_version,
|
|
92
119
|
storageUri: row.storage_uri,
|
|
93
120
|
fingerprintHash: row.fingerprint_hash,
|
|
94
|
-
metadata:
|
|
121
|
+
metadata: stripBundleArtifactMetadata(rawMetadata),
|
|
122
|
+
manifestStorageUri: row.manifest_storage_uri ?? null,
|
|
123
|
+
manifestFileHash: row.manifest_file_hash ?? null,
|
|
124
|
+
assetBaseStorageUri: row.asset_base_storage_uri ?? null,
|
|
125
|
+
patches,
|
|
126
|
+
patchBaseBundleId: primaryPatch?.baseBundleId ?? null,
|
|
127
|
+
patchBaseFileHash: primaryPatch?.baseFileHash ?? null,
|
|
128
|
+
patchFileHash: primaryPatch?.patchFileHash ?? null,
|
|
129
|
+
patchStorageUri: primaryPatch?.patchStorageUri ?? null,
|
|
95
130
|
rolloutCohortCount: row.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
96
131
|
targetCohorts: parseTargetCohorts(row.target_cohorts)
|
|
97
132
|
};
|
|
@@ -110,12 +145,30 @@ const d1WorkerDatabase = () => createDatabasePlugin({
|
|
|
110
145
|
const queryFirst = async (sql, params = [], context) => {
|
|
111
146
|
return await config.getDb(context).prepare(sql).bind(...params).first() ?? null;
|
|
112
147
|
};
|
|
148
|
+
const getPatchMap = async (bundleIds, context) => {
|
|
149
|
+
const patchMap = /* @__PURE__ */ new Map();
|
|
150
|
+
if (bundleIds.length === 0) return patchMap;
|
|
151
|
+
const rows = await queryAll(`
|
|
152
|
+
SELECT *
|
|
153
|
+
FROM bundle_patches
|
|
154
|
+
WHERE bundle_id IN (${bundleIds.map(() => "?").join(", ")})
|
|
155
|
+
ORDER BY order_index ASC, base_bundle_id ASC
|
|
156
|
+
`, bundleIds, context);
|
|
157
|
+
for (const row of rows) {
|
|
158
|
+
const current = patchMap.get(row.bundle_id) ?? [];
|
|
159
|
+
current.push(row);
|
|
160
|
+
patchMap.set(row.bundle_id, current);
|
|
161
|
+
}
|
|
162
|
+
return patchMap;
|
|
163
|
+
};
|
|
113
164
|
const queryBundlesForUpdateInfo = async (conditions, context) => {
|
|
114
165
|
const { sql: whereClause, params } = buildWhereClause(conditions);
|
|
115
|
-
|
|
166
|
+
const rows = await queryAll(`
|
|
116
167
|
SELECT * FROM bundles
|
|
117
168
|
${whereClause}
|
|
118
|
-
`, params, context)
|
|
169
|
+
`, params, context);
|
|
170
|
+
const patchMap = await getPatchMap(rows.map((row) => row.id), context);
|
|
171
|
+
return rows.map((row) => transformRowToBundle(row, patchMap.get(row.id)));
|
|
119
172
|
};
|
|
120
173
|
const getTargetAppVersionsForUpdateInfo = async ({ platform, channel, minBundleId }, context) => {
|
|
121
174
|
return (await queryAll(`
|
|
@@ -156,8 +209,8 @@ const d1WorkerDatabase = () => createDatabasePlugin({
|
|
|
156
209
|
}
|
|
157
210
|
}),
|
|
158
211
|
async getBundleById(bundleId, context) {
|
|
159
|
-
const row = await queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context);
|
|
160
|
-
return row ? transformRowToBundle(row) : null;
|
|
212
|
+
const [row, patchMap] = await Promise.all([queryFirst("SELECT * FROM bundles WHERE id = ? LIMIT 1", [bundleId], context), getPatchMap([bundleId], context)]);
|
|
213
|
+
return row ? transformRowToBundle(row, patchMap.get(bundleId)) : null;
|
|
161
214
|
},
|
|
162
215
|
async getBundles(options, context) {
|
|
163
216
|
const { where, limit, orderBy } = options;
|
|
@@ -165,12 +218,14 @@ const d1WorkerDatabase = () => createDatabasePlugin({
|
|
|
165
218
|
const { sql: whereClause, params } = buildWhereClause(where);
|
|
166
219
|
const orderSql = orderBy?.direction === "asc" ? "ORDER BY id ASC" : "ORDER BY id DESC";
|
|
167
220
|
const total = (await queryAll(`SELECT COUNT(*) as total FROM bundles${whereClause}`, params, context))[0]?.total ?? 0;
|
|
221
|
+
const rows = await queryAll(`SELECT * FROM bundles${whereClause} ${orderSql} LIMIT ? OFFSET ?`, [
|
|
222
|
+
...params,
|
|
223
|
+
limit,
|
|
224
|
+
offset
|
|
225
|
+
], context);
|
|
226
|
+
const patchMap = await getPatchMap(rows.map((row) => row.id), context);
|
|
168
227
|
return {
|
|
169
|
-
data: (
|
|
170
|
-
...params,
|
|
171
|
-
limit,
|
|
172
|
-
offset
|
|
173
|
-
], context)).map(transformRowToBundle),
|
|
228
|
+
data: rows.map((row) => transformRowToBundle(row, patchMap.get(row.id))),
|
|
174
229
|
pagination: calculatePagination(total, {
|
|
175
230
|
limit,
|
|
176
231
|
offset
|
|
@@ -185,6 +240,8 @@ const d1WorkerDatabase = () => createDatabasePlugin({
|
|
|
185
240
|
const db = config.getDb(context);
|
|
186
241
|
for (const operation of changedSets) {
|
|
187
242
|
if (operation.operation === "delete") {
|
|
243
|
+
await db.prepare("DELETE FROM bundle_patches WHERE bundle_id = ?").bind(operation.data.id).run();
|
|
244
|
+
await db.prepare("DELETE FROM bundle_patches WHERE base_bundle_id = ?").bind(operation.data.id).run();
|
|
188
245
|
await db.prepare("DELETE FROM bundles WHERE id = ?").bind(operation.data.id).run();
|
|
189
246
|
continue;
|
|
190
247
|
}
|
|
@@ -203,11 +260,28 @@ const d1WorkerDatabase = () => createDatabasePlugin({
|
|
|
203
260
|
storage_uri,
|
|
204
261
|
fingerprint_hash,
|
|
205
262
|
metadata,
|
|
263
|
+
manifest_storage_uri,
|
|
264
|
+
manifest_file_hash,
|
|
265
|
+
asset_base_storage_uri,
|
|
206
266
|
rollout_cohort_count,
|
|
207
267
|
target_cohorts
|
|
208
268
|
)
|
|
209
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
210
|
-
`).bind(bundle.id, bundle.channel, bundle.enabled ? 1 : 0, bundle.shouldForceUpdate ? 1 : 0, bundle.fileHash, bundle.gitCommitHash || null, bundle.message || null, bundle.platform, bundle.targetAppVersion, bundle.storageUri, bundle.fingerprintHash, JSON.stringify(bundle.metadata ?? {}), bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT, bundle.targetCohorts ? JSON.stringify(bundle.targetCohorts) : null).run();
|
|
269
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
270
|
+
`).bind(bundle.id, bundle.channel, bundle.enabled ? 1 : 0, bundle.shouldForceUpdate ? 1 : 0, bundle.fileHash, bundle.gitCommitHash || null, bundle.message || null, bundle.platform, bundle.targetAppVersion, bundle.storageUri, bundle.fingerprintHash, JSON.stringify(stripBundleArtifactMetadata(bundle.metadata) ?? {}), getManifestStorageUri(bundle), getManifestFileHash(bundle), getAssetBaseStorageUri(bundle), bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT, bundle.targetCohorts ? JSON.stringify(bundle.targetCohorts) : null).run();
|
|
271
|
+
await db.prepare("DELETE FROM bundle_patches WHERE bundle_id = ?").bind(bundle.id).run();
|
|
272
|
+
const patchRows = bundleToPatchRows(bundle);
|
|
273
|
+
for (const patchRow of patchRows) await db.prepare(`
|
|
274
|
+
INSERT OR REPLACE INTO bundle_patches (
|
|
275
|
+
id,
|
|
276
|
+
bundle_id,
|
|
277
|
+
base_bundle_id,
|
|
278
|
+
base_file_hash,
|
|
279
|
+
patch_file_hash,
|
|
280
|
+
patch_storage_uri,
|
|
281
|
+
order_index
|
|
282
|
+
)
|
|
283
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
284
|
+
`).bind(patchRow.id, patchRow.bundle_id, patchRow.base_bundle_id, patchRow.base_file_hash, patchRow.patch_file_hash, patchRow.patch_storage_uri, patchRow.order_index ?? 0).run();
|
|
211
285
|
}
|
|
212
286
|
}
|
|
213
287
|
};
|
|
@@ -223,21 +297,31 @@ const resolveJwtSecretFromContext = (context) => {
|
|
|
223
297
|
if (!jwtSecret) throw new Error("r2WorkerStorage requires env.JWT_SECRET in the hot updater context.");
|
|
224
298
|
return jwtSecret;
|
|
225
299
|
};
|
|
300
|
+
const resolveR2BucketFromContext = (context) => {
|
|
301
|
+
const bucket = context?.env?.BUCKET;
|
|
302
|
+
if (!bucket) throw new Error("r2WorkerStorage requires env.BUCKET in the hot updater context.");
|
|
303
|
+
return bucket;
|
|
304
|
+
};
|
|
305
|
+
const createPublicObjectPath = (storageUrl) => `${storageUrl.host}${storageUrl.pathname}`;
|
|
306
|
+
const createR2ObjectKey = (storageUrl) => storageUrl.pathname.replace(/^\/+/, "");
|
|
226
307
|
const r2WorkerStorage = (config) => {
|
|
227
|
-
return (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
async
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
308
|
+
return createRuntimeStoragePlugin({
|
|
309
|
+
name: "r2WorkerStorage",
|
|
310
|
+
supportedProtocol: "r2",
|
|
311
|
+
factory: (config) => ({
|
|
312
|
+
async readText(storageUri, context) {
|
|
313
|
+
const storageUrl = new URL(storageUri);
|
|
314
|
+
if (storageUrl.protocol !== "r2:") throw new Error("Invalid R2 storage URI protocol");
|
|
315
|
+
const bucket = resolveR2BucketFromContext(context);
|
|
316
|
+
const key = createR2ObjectKey(storageUrl);
|
|
317
|
+
const object = await bucket.get(key);
|
|
318
|
+
if (!object) return null;
|
|
319
|
+
return object.text();
|
|
236
320
|
},
|
|
237
321
|
async getDownloadUrl(storageUri, context) {
|
|
238
322
|
const storageUrl = new URL(storageUri);
|
|
239
323
|
if (storageUrl.protocol !== "r2:") throw new Error("Invalid R2 storage URI protocol");
|
|
240
|
-
const key =
|
|
324
|
+
const key = createPublicObjectPath(storageUrl);
|
|
241
325
|
const [jwtSecret, publicBaseUrl] = await Promise.all([resolveContextValue(config.jwtSecret ?? resolveJwtSecretFromContext, context), resolveContextValue(config.publicBaseUrl, context)]);
|
|
242
326
|
const token = await signToken(key, jwtSecret);
|
|
243
327
|
const url = new URL(publicBaseUrl);
|
|
@@ -246,8 +330,8 @@ const r2WorkerStorage = (config) => {
|
|
|
246
330
|
url.searchParams.set("token", token);
|
|
247
331
|
return { fileUrl: url.toString() };
|
|
248
332
|
}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
333
|
+
})
|
|
334
|
+
})(config);
|
|
251
335
|
};
|
|
252
336
|
//#endregion
|
|
253
337
|
//#region src/worker/index.ts
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/cloudflare",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.31.0",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"cloudflare": "4.2.0",
|
|
51
51
|
"hono": "4.12.9",
|
|
52
52
|
"uuidv7": "^1.0.2",
|
|
53
|
-
"@hot-updater/core": "0.
|
|
54
|
-
"@hot-updater/cli-tools": "0.
|
|
55
|
-
"@hot-updater/
|
|
56
|
-
"@hot-updater/
|
|
57
|
-
"@hot-updater/server": "0.
|
|
53
|
+
"@hot-updater/core": "0.31.0",
|
|
54
|
+
"@hot-updater/cli-tools": "0.31.0",
|
|
55
|
+
"@hot-updater/plugin-core": "0.31.0",
|
|
56
|
+
"@hot-updater/js": "0.31.0",
|
|
57
|
+
"@hot-updater/server": "0.31.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@cloudflare/vitest-pool-workers": "0.13.0",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"vitest": "4.1.4",
|
|
72
72
|
"wrangler": "^4.5.0",
|
|
73
73
|
"xdg-app-paths": "^8.3.0",
|
|
74
|
-
"@hot-updater/test-utils": "0.
|
|
74
|
+
"@hot-updater/test-utils": "0.31.0"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "tsdown && pnpm build:worker",
|
package/sql/bundles.sql
CHANGED
|
@@ -9,14 +9,34 @@ CREATE TABLE bundles (
|
|
|
9
9
|
file_hash TEXT NOT NULL,
|
|
10
10
|
git_commit_hash TEXT,
|
|
11
11
|
message TEXT,
|
|
12
|
-
channel TEXT NOT NULL,
|
|
13
|
-
storage_uri TEXT,
|
|
12
|
+
channel TEXT NOT NULL DEFAULT 'production',
|
|
13
|
+
storage_uri TEXT NOT NULL,
|
|
14
14
|
fingerprint_hash TEXT,
|
|
15
15
|
metadata JSONB DEFAULT '{}',
|
|
16
|
+
manifest_storage_uri TEXT,
|
|
17
|
+
manifest_file_hash TEXT,
|
|
18
|
+
asset_base_storage_uri TEXT,
|
|
16
19
|
rollout_cohort_count INTEGER DEFAULT 1000
|
|
17
20
|
CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000),
|
|
18
|
-
target_cohorts TEXT
|
|
21
|
+
target_cohorts TEXT,
|
|
22
|
+
CHECK ((target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL))
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE bundle_patches (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
bundle_id TEXT NOT NULL,
|
|
28
|
+
base_bundle_id TEXT NOT NULL,
|
|
29
|
+
base_file_hash TEXT NOT NULL,
|
|
30
|
+
patch_file_hash TEXT NOT NULL,
|
|
31
|
+
patch_storage_uri TEXT NOT NULL,
|
|
32
|
+
order_index INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
FOREIGN KEY (bundle_id) REFERENCES bundles(id) ON DELETE CASCADE,
|
|
34
|
+
FOREIGN KEY (base_bundle_id) REFERENCES bundles(id) ON DELETE CASCADE
|
|
19
35
|
);
|
|
20
36
|
|
|
21
37
|
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|
|
22
38
|
CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
|
|
39
|
+
CREATE INDEX bundles_channel_idx ON bundles(channel);
|
|
40
|
+
CREATE INDEX bundles_rollout_idx ON bundles(rollout_cohort_count);
|
|
41
|
+
CREATE INDEX bundle_patches_bundle_id_idx ON bundle_patches(bundle_id);
|
|
42
|
+
CREATE INDEX bundle_patches_base_bundle_id_idx ON bundle_patches(base_bundle_id);
|