@hot-updater/server 0.32.0 → 0.33.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/db/index.cjs +7 -10
- package/dist/db/index.mjs +7 -10
- package/dist/db/pluginCore.cjs +136 -96
- package/dist/db/pluginCore.mjs +137 -97
- package/dist/db/requestBundleIdentityMap.cjs +29 -0
- package/dist/db/requestBundleIdentityMap.mjs +29 -0
- package/dist/db/types.d.cts +2 -1
- package/dist/db/types.d.mts +2 -1
- package/dist/packages/server/package.cjs +1 -1
- package/dist/packages/server/package.mjs +1 -1
- package/dist/runtime.cjs +10 -12
- package/dist/runtime.mjs +10 -12
- package/package.json +7 -7
- package/src/db/index.spec.ts +36 -0
- package/src/db/index.ts +6 -10
- package/src/db/pluginCore.spec.ts +334 -0
- package/src/db/pluginCore.ts +63 -7
- package/src/db/requestBundleIdentityMap.spec.ts +56 -0
- package/src/db/requestBundleIdentityMap.ts +61 -0
- package/src/db/types.ts +2 -0
- package/src/runtime.spec.ts +39 -0
- package/src/runtime.ts +10 -12
package/dist/runtime.cjs
CHANGED
|
@@ -22,23 +22,21 @@ function createHotUpdater(options) {
|
|
|
22
22
|
createMutationPlugin: () => database(),
|
|
23
23
|
readStorageText
|
|
24
24
|
} : { readStorageText });
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
routes: options.routes
|
|
30
|
-
}),
|
|
31
|
-
adapterName: core.adapterName
|
|
32
|
-
};
|
|
25
|
+
const internalHandler = require_handler.createHandler(core.api, {
|
|
26
|
+
basePath,
|
|
27
|
+
routes: options.routes
|
|
28
|
+
});
|
|
33
29
|
const handler = (request, context, ...extraArgs) => {
|
|
34
|
-
if (extraArgs.length > 0) return
|
|
35
|
-
return
|
|
30
|
+
if (extraArgs.length > 0) return internalHandler(request);
|
|
31
|
+
return internalHandler(request, context);
|
|
36
32
|
};
|
|
37
|
-
|
|
38
|
-
...api,
|
|
33
|
+
const api = {
|
|
39
34
|
basePath,
|
|
35
|
+
adapterName: core.adapterName,
|
|
40
36
|
handler
|
|
41
37
|
};
|
|
38
|
+
Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
|
|
39
|
+
return api;
|
|
42
40
|
}
|
|
43
41
|
//#endregion
|
|
44
42
|
exports.HOT_UPDATER_SERVER_VERSION = require_version.HOT_UPDATER_SERVER_VERSION;
|
package/dist/runtime.mjs
CHANGED
|
@@ -20,23 +20,21 @@ function createHotUpdater(options) {
|
|
|
20
20
|
createMutationPlugin: () => database(),
|
|
21
21
|
readStorageText
|
|
22
22
|
} : { readStorageText });
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
routes: options.routes
|
|
28
|
-
}),
|
|
29
|
-
adapterName: core.adapterName
|
|
30
|
-
};
|
|
23
|
+
const internalHandler = createHandler(core.api, {
|
|
24
|
+
basePath,
|
|
25
|
+
routes: options.routes
|
|
26
|
+
});
|
|
31
27
|
const handler = (request, context, ...extraArgs) => {
|
|
32
|
-
if (extraArgs.length > 0) return
|
|
33
|
-
return
|
|
28
|
+
if (extraArgs.length > 0) return internalHandler(request);
|
|
29
|
+
return internalHandler(request, context);
|
|
34
30
|
};
|
|
35
|
-
|
|
36
|
-
...api,
|
|
31
|
+
const api = {
|
|
37
32
|
basePath,
|
|
33
|
+
adapterName: core.adapterName,
|
|
38
34
|
handler
|
|
39
35
|
};
|
|
36
|
+
Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
|
|
37
|
+
return api;
|
|
40
38
|
}
|
|
41
39
|
//#endregion
|
|
42
40
|
export { HOT_UPDATER_SERVER_VERSION, createHandler, createHotUpdater };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"mongodb": "6.20.0",
|
|
56
56
|
"rou3": "0.7.9",
|
|
57
57
|
"semver": "^7.7.2",
|
|
58
|
-
"@hot-updater/
|
|
59
|
-
"@hot-updater/
|
|
60
|
-
"@hot-updater/plugin-core": "0.
|
|
61
|
-
"@hot-updater/js": "0.
|
|
58
|
+
"@hot-updater/core": "0.33.0",
|
|
59
|
+
"@hot-updater/bsdiff": "0.33.0",
|
|
60
|
+
"@hot-updater/plugin-core": "0.33.0",
|
|
61
|
+
"@hot-updater/js": "0.33.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@electric-sql/pglite": "0.2.17",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"kysely-pglite-dialect": "1.2.0",
|
|
69
69
|
"msw": "^2.7.0",
|
|
70
70
|
"uuidv7": "^1.0.2",
|
|
71
|
-
"@hot-updater/standalone": "0.
|
|
72
|
-
"@hot-updater/test-utils": "0.
|
|
71
|
+
"@hot-updater/standalone": "0.33.0",
|
|
72
|
+
"@hot-updater/test-utils": "0.33.0"
|
|
73
73
|
},
|
|
74
74
|
"inlinedDependencies": {
|
|
75
75
|
"@noble/hashes": "1.8.0",
|
package/src/db/index.spec.ts
CHANGED
|
@@ -866,6 +866,42 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
|
|
|
866
866
|
});
|
|
867
867
|
|
|
868
868
|
describe("database plugin factories", () => {
|
|
869
|
+
it("keeps optional maintenance capabilities lazy", () => {
|
|
870
|
+
const factory = vi.fn(() => ({
|
|
871
|
+
async getBundleById() {
|
|
872
|
+
return null;
|
|
873
|
+
},
|
|
874
|
+
async getBundles() {
|
|
875
|
+
return {
|
|
876
|
+
data: [],
|
|
877
|
+
pagination: {
|
|
878
|
+
hasNextPage: false,
|
|
879
|
+
hasPreviousPage: false,
|
|
880
|
+
currentPage: 1,
|
|
881
|
+
totalPages: 1,
|
|
882
|
+
total: 0,
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
},
|
|
886
|
+
async getChannels() {
|
|
887
|
+
return [];
|
|
888
|
+
},
|
|
889
|
+
async commitBundle() {},
|
|
890
|
+
}));
|
|
891
|
+
const hotUpdater = createHotUpdater({
|
|
892
|
+
database: createDatabasePlugin({
|
|
893
|
+
name: "lazyPlugin",
|
|
894
|
+
factory,
|
|
895
|
+
})({}),
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
expect(factory).not.toHaveBeenCalled();
|
|
899
|
+
expect(hotUpdater.diagnostics).toBeUndefined();
|
|
900
|
+
expect(factory).not.toHaveBeenCalled();
|
|
901
|
+
expect(hotUpdater.diagnostics).toBeUndefined();
|
|
902
|
+
expect(factory).not.toHaveBeenCalled();
|
|
903
|
+
});
|
|
904
|
+
|
|
869
905
|
it("isolates pending mutation state between overlapping writes", async () => {
|
|
870
906
|
const committedBundleIds: string[][] = [];
|
|
871
907
|
const onUnmount = vi.fn(async () => undefined);
|
package/src/db/index.ts
CHANGED
|
@@ -104,19 +104,15 @@ export function createHotUpdater<TContext = unknown>(
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
const api = {
|
|
107
|
-
|
|
107
|
+
basePath,
|
|
108
|
+
adapterName: core.adapterName,
|
|
109
|
+
createMigrator: core.createMigrator,
|
|
110
|
+
generateSchema: core.generateSchema,
|
|
108
111
|
handler: createHandler(core.api, {
|
|
109
112
|
basePath,
|
|
110
113
|
routes: options.routes,
|
|
111
114
|
}),
|
|
112
|
-
adapterName: core.adapterName,
|
|
113
|
-
createMigrator: core.createMigrator,
|
|
114
|
-
generateSchema: core.generateSchema,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
...api,
|
|
119
|
-
basePath,
|
|
120
|
-
handler: api.handler,
|
|
121
115
|
};
|
|
116
|
+
Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
|
|
117
|
+
return api as HotUpdaterAPI<TContext>;
|
|
122
118
|
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DatabasePlugin,
|
|
5
5
|
RequestEnvContext,
|
|
6
6
|
} from "@hot-updater/plugin-core";
|
|
7
|
+
import { createDatabasePluginGetUpdateInfo } from "@hot-updater/plugin-core";
|
|
7
8
|
import { describe, expect, it, vi } from "vitest";
|
|
8
9
|
|
|
9
10
|
import { createPluginDatabaseCore } from "./pluginCore";
|
|
@@ -195,6 +196,339 @@ describe("createPluginDatabaseCore", () => {
|
|
|
195
196
|
});
|
|
196
197
|
});
|
|
197
198
|
|
|
199
|
+
it("uses request bundle identity map for manifest artifact lookups", async () => {
|
|
200
|
+
const currentBundle = {
|
|
201
|
+
...baseBundle,
|
|
202
|
+
id: "00000000-0000-0000-0000-000000000001",
|
|
203
|
+
manifestStorageUri: "r2://bucket/current/manifest.json",
|
|
204
|
+
manifestFileHash: "sig:current-manifest",
|
|
205
|
+
assetBaseStorageUri: "r2://bucket/current/files",
|
|
206
|
+
};
|
|
207
|
+
const targetBundle = {
|
|
208
|
+
...baseBundle,
|
|
209
|
+
id: "00000000-0000-0000-0000-000000000002",
|
|
210
|
+
fileHash: "hash-2",
|
|
211
|
+
manifestStorageUri: "r2://bucket/target/manifest.json",
|
|
212
|
+
manifestFileHash: "sig:target-manifest",
|
|
213
|
+
assetBaseStorageUri: "r2://bucket/target/files",
|
|
214
|
+
};
|
|
215
|
+
const manifests = new Map([
|
|
216
|
+
[
|
|
217
|
+
currentBundle.manifestStorageUri,
|
|
218
|
+
JSON.stringify({
|
|
219
|
+
bundleId: currentBundle.id,
|
|
220
|
+
assets: {
|
|
221
|
+
"index.ios.bundle": {
|
|
222
|
+
fileHash: "old-bundle-hash",
|
|
223
|
+
},
|
|
224
|
+
"shared.png": {
|
|
225
|
+
fileHash: "same-image-hash",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
}),
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
targetBundle.manifestStorageUri,
|
|
232
|
+
JSON.stringify({
|
|
233
|
+
bundleId: targetBundle.id,
|
|
234
|
+
assets: {
|
|
235
|
+
"index.ios.bundle": {
|
|
236
|
+
fileHash: "target-bundle-hash",
|
|
237
|
+
},
|
|
238
|
+
"shared.png": {
|
|
239
|
+
fileHash: "same-image-hash",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
243
|
+
],
|
|
244
|
+
]);
|
|
245
|
+
const getBundleById = vi.fn<DatabasePlugin<TestContext>["getBundleById"]>(
|
|
246
|
+
async (bundleId) => {
|
|
247
|
+
if (bundleId === currentBundle.id) return currentBundle;
|
|
248
|
+
if (bundleId === targetBundle.id) return targetBundle;
|
|
249
|
+
return null;
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
const getUpdateInfo = vi.fn<
|
|
253
|
+
NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
|
|
254
|
+
>(async () => ({
|
|
255
|
+
fileHash: targetBundle.fileHash,
|
|
256
|
+
id: targetBundle.id,
|
|
257
|
+
message: targetBundle.message,
|
|
258
|
+
shouldForceUpdate: targetBundle.shouldForceUpdate,
|
|
259
|
+
status: "UPDATE",
|
|
260
|
+
storageUri: targetBundle.storageUri,
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
const plugin: DatabasePlugin<TestContext> = {
|
|
264
|
+
name: "identity-map-plugin",
|
|
265
|
+
async appendBundle() {},
|
|
266
|
+
async commitBundle() {},
|
|
267
|
+
async deleteBundle() {},
|
|
268
|
+
getBundleById,
|
|
269
|
+
getUpdateInfo,
|
|
270
|
+
async getBundles() {
|
|
271
|
+
return {
|
|
272
|
+
data: [targetBundle],
|
|
273
|
+
pagination: {
|
|
274
|
+
currentPage: 1,
|
|
275
|
+
hasNextPage: false,
|
|
276
|
+
hasPreviousPage: false,
|
|
277
|
+
total: 1,
|
|
278
|
+
totalPages: 1,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
async getChannels() {
|
|
283
|
+
return ["production"];
|
|
284
|
+
},
|
|
285
|
+
async updateBundle() {},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const core = createPluginDatabaseCore(
|
|
289
|
+
() => plugin,
|
|
290
|
+
async (storageUri) => {
|
|
291
|
+
if (!storageUri) return null;
|
|
292
|
+
const url = new URL(storageUri);
|
|
293
|
+
return `https://assets.example.com/${url.host}${url.pathname}`;
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
readStorageText: async (storageUri) =>
|
|
297
|
+
manifests.get(storageUri) ?? null,
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const updateInfo = await core.api.getAppUpdateInfo({
|
|
302
|
+
...updateArgs,
|
|
303
|
+
bundleId: currentBundle.id,
|
|
304
|
+
});
|
|
305
|
+
expect(updateInfo).not.toBeNull();
|
|
306
|
+
if (!updateInfo) {
|
|
307
|
+
throw new Error("expected app update info");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
expect(updateInfo).toMatchObject({
|
|
311
|
+
changedAssets: {
|
|
312
|
+
"index.ios.bundle": {
|
|
313
|
+
file: {
|
|
314
|
+
compression: "br",
|
|
315
|
+
url: "https://assets.example.com/bucket/target/files/index.ios.bundle.br",
|
|
316
|
+
},
|
|
317
|
+
fileHash: "target-bundle-hash",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
manifestFileHash: "sig:target-manifest",
|
|
321
|
+
manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
|
|
322
|
+
});
|
|
323
|
+
expect(updateInfo.changedAssets).not.toHaveProperty("shared.png");
|
|
324
|
+
expect(getUpdateInfo).toHaveBeenCalledOnce();
|
|
325
|
+
expect(getBundleById).toHaveBeenCalledTimes(2);
|
|
326
|
+
expect(getBundleById).toHaveBeenCalledWith(targetBundle.id, undefined);
|
|
327
|
+
expect(getBundleById).toHaveBeenCalledWith(currentBundle.id, undefined);
|
|
328
|
+
expect(Object.keys(updateInfo)).not.toContain("__hotUpdaterBundle");
|
|
329
|
+
expect(Object.keys(updateInfo)).not.toContain("__hotUpdaterCurrentBundle");
|
|
330
|
+
expect(JSON.stringify(updateInfo)).not.toContain("__hotUpdaterBundle");
|
|
331
|
+
expect(JSON.stringify(updateInfo)).not.toContain(
|
|
332
|
+
"__hotUpdaterCurrentBundle",
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("seeds request bundle identity map from provider update lookups", async () => {
|
|
337
|
+
const targetBundle = {
|
|
338
|
+
...baseBundle,
|
|
339
|
+
id: "00000000-0000-0000-0000-000000000002",
|
|
340
|
+
fileHash: "hash-2",
|
|
341
|
+
manifestStorageUri: "s3://bucket/target/manifest.json",
|
|
342
|
+
manifestFileHash: "sig:target-manifest",
|
|
343
|
+
assetBaseStorageUri: "s3://bucket/target/files",
|
|
344
|
+
};
|
|
345
|
+
const manifests = new Map([
|
|
346
|
+
[
|
|
347
|
+
targetBundle.manifestStorageUri,
|
|
348
|
+
JSON.stringify({
|
|
349
|
+
bundleId: targetBundle.id,
|
|
350
|
+
assets: {
|
|
351
|
+
"index.ios.bundle": {
|
|
352
|
+
fileHash: "target-bundle-hash",
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
],
|
|
357
|
+
]);
|
|
358
|
+
const getBundleById = vi.fn<DatabasePlugin<TestContext>["getBundleById"]>(
|
|
359
|
+
async () => {
|
|
360
|
+
throw new Error("unexpected provider bundle reread");
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
const getUpdateInfo = createDatabasePluginGetUpdateInfo<TestContext>({
|
|
364
|
+
getBundlesByFingerprint: async () => [],
|
|
365
|
+
getBundlesByTargetAppVersions: async () => [targetBundle],
|
|
366
|
+
listTargetAppVersions: async () => ["1.0.0"],
|
|
367
|
+
});
|
|
368
|
+
const plugin: DatabasePlugin<TestContext> = {
|
|
369
|
+
name: "seeded-fast-path-plugin",
|
|
370
|
+
async appendBundle() {},
|
|
371
|
+
async commitBundle() {},
|
|
372
|
+
async deleteBundle() {},
|
|
373
|
+
getBundleById,
|
|
374
|
+
async getBundles() {
|
|
375
|
+
return {
|
|
376
|
+
data: [targetBundle],
|
|
377
|
+
pagination: {
|
|
378
|
+
currentPage: 1,
|
|
379
|
+
hasNextPage: false,
|
|
380
|
+
hasPreviousPage: false,
|
|
381
|
+
total: 1,
|
|
382
|
+
totalPages: 1,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
getUpdateInfo,
|
|
387
|
+
async getChannels() {
|
|
388
|
+
return ["production"];
|
|
389
|
+
},
|
|
390
|
+
async updateBundle() {},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const core = createPluginDatabaseCore(
|
|
394
|
+
() => plugin,
|
|
395
|
+
async (storageUri) => {
|
|
396
|
+
if (!storageUri) return null;
|
|
397
|
+
const url = new URL(storageUri);
|
|
398
|
+
return `https://assets.example.com/${url.host}${url.pathname}`;
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
readStorageText: async (storageUri) =>
|
|
402
|
+
manifests.get(storageUri) ?? null,
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
const context: TestContext = {
|
|
406
|
+
env: {
|
|
407
|
+
assetHost: "https://assets.example.com",
|
|
408
|
+
},
|
|
409
|
+
request: new Request("https://updates.example.com"),
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const updateInfo = await core.api.getAppUpdateInfo(updateArgs, context);
|
|
413
|
+
|
|
414
|
+
expect(updateInfo).toMatchObject({
|
|
415
|
+
changedAssets: {
|
|
416
|
+
"index.ios.bundle": {
|
|
417
|
+
file: {
|
|
418
|
+
compression: "br",
|
|
419
|
+
url: "https://assets.example.com/bucket/target/files/index.ios.bundle.br",
|
|
420
|
+
},
|
|
421
|
+
fileHash: "target-bundle-hash",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
id: targetBundle.id,
|
|
425
|
+
manifestFileHash: "sig:target-manifest",
|
|
426
|
+
manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
|
|
427
|
+
});
|
|
428
|
+
expect(getBundleById).not.toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("does not reread providers for current bundles outside direct update lookup results", async () => {
|
|
432
|
+
const targetBundle = {
|
|
433
|
+
...baseBundle,
|
|
434
|
+
id: "00000000-0000-0000-0000-000000000003",
|
|
435
|
+
fileHash: "hash-3",
|
|
436
|
+
manifestStorageUri: "s3://bucket/target/manifest.json",
|
|
437
|
+
manifestFileHash: "sig:target-manifest",
|
|
438
|
+
assetBaseStorageUri: "s3://bucket/target/files",
|
|
439
|
+
};
|
|
440
|
+
const manifests = new Map([
|
|
441
|
+
[
|
|
442
|
+
targetBundle.manifestStorageUri,
|
|
443
|
+
JSON.stringify({
|
|
444
|
+
bundleId: targetBundle.id,
|
|
445
|
+
assets: {
|
|
446
|
+
"index.ios.bundle": {
|
|
447
|
+
fileHash: "target-bundle-hash",
|
|
448
|
+
},
|
|
449
|
+
"image.png": {
|
|
450
|
+
fileHash: "target-image-hash",
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
}),
|
|
454
|
+
],
|
|
455
|
+
]);
|
|
456
|
+
const getBundleById = vi.fn<DatabasePlugin<TestContext>["getBundleById"]>(
|
|
457
|
+
async () => {
|
|
458
|
+
throw new Error("unexpected provider current bundle reread");
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
const getUpdateInfo = createDatabasePluginGetUpdateInfo<TestContext>({
|
|
462
|
+
getBundlesByFingerprint: async () => [],
|
|
463
|
+
getBundlesByTargetAppVersions: async () => [targetBundle],
|
|
464
|
+
listTargetAppVersions: async () => ["1.0.0"],
|
|
465
|
+
});
|
|
466
|
+
const plugin: DatabasePlugin<TestContext> = {
|
|
467
|
+
name: "seeded-current-miss-plugin",
|
|
468
|
+
async appendBundle() {},
|
|
469
|
+
async commitBundle() {},
|
|
470
|
+
async deleteBundle() {},
|
|
471
|
+
getBundleById,
|
|
472
|
+
async getBundles() {
|
|
473
|
+
throw new Error("unexpected provider scan");
|
|
474
|
+
},
|
|
475
|
+
getUpdateInfo,
|
|
476
|
+
async getChannels() {
|
|
477
|
+
return ["production"];
|
|
478
|
+
},
|
|
479
|
+
async updateBundle() {},
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const core = createPluginDatabaseCore(
|
|
483
|
+
() => plugin,
|
|
484
|
+
async (storageUri) => {
|
|
485
|
+
if (!storageUri) return null;
|
|
486
|
+
const url = new URL(storageUri);
|
|
487
|
+
return `https://assets.example.com/${url.host}${url.pathname}`;
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
readStorageText: async (storageUri) =>
|
|
491
|
+
manifests.get(storageUri) ?? null,
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
const context: TestContext = {
|
|
495
|
+
env: {
|
|
496
|
+
assetHost: "https://assets.example.com",
|
|
497
|
+
},
|
|
498
|
+
request: new Request("https://updates.example.com"),
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const updateInfo = await core.api.getAppUpdateInfo(
|
|
502
|
+
{
|
|
503
|
+
...updateArgs,
|
|
504
|
+
bundleId: "00000000-0000-0000-0000-0000000000aa",
|
|
505
|
+
},
|
|
506
|
+
context,
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
expect(updateInfo).toMatchObject({
|
|
510
|
+
changedAssets: {
|
|
511
|
+
"image.png": {
|
|
512
|
+
file: {
|
|
513
|
+
url: "https://assets.example.com/bucket/target/files/image.png",
|
|
514
|
+
},
|
|
515
|
+
fileHash: "target-image-hash",
|
|
516
|
+
},
|
|
517
|
+
"index.ios.bundle": {
|
|
518
|
+
file: {
|
|
519
|
+
compression: "br",
|
|
520
|
+
url: "https://assets.example.com/bucket/target/files/index.ios.bundle.br",
|
|
521
|
+
},
|
|
522
|
+
fileHash: "target-bundle-hash",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
id: targetBundle.id,
|
|
526
|
+
manifestFileHash: "sig:target-manifest",
|
|
527
|
+
manifestUrl: "https://assets.example.com/bucket/target/manifest.json",
|
|
528
|
+
});
|
|
529
|
+
expect(getBundleById).not.toHaveBeenCalled();
|
|
530
|
+
});
|
|
531
|
+
|
|
198
532
|
it("resolves manifest changed assets from deterministic content-addressed storage", async () => {
|
|
199
533
|
const currentBundle = {
|
|
200
534
|
...baseBundle,
|
package/src/db/pluginCore.ts
CHANGED
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
type DatabaseBundleQueryOrder,
|
|
14
14
|
type DatabaseBundleQueryWhere,
|
|
15
15
|
type DatabasePlugin,
|
|
16
|
+
getRequestUpdateBundleSeeds,
|
|
16
17
|
type HotUpdaterContext,
|
|
17
18
|
semverSatisfies,
|
|
18
19
|
} from "@hot-updater/plugin-core";
|
|
19
20
|
|
|
21
|
+
import { createRequestBundleIdentityMap } from "./requestBundleIdentityMap";
|
|
20
22
|
import { assertBundlePersistenceConstraints } from "./schemaEnhancements";
|
|
21
23
|
import type { DatabaseAPI } from "./types";
|
|
22
24
|
import { resolveManifestArtifacts } from "./updateArtifacts";
|
|
@@ -347,9 +349,7 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
347
349
|
if (!info) {
|
|
348
350
|
return null;
|
|
349
351
|
}
|
|
350
|
-
const { storageUri, ...rest } = info
|
|
351
|
-
storageUri: string | null;
|
|
352
|
-
};
|
|
352
|
+
const { storageUri, ...rest } = info;
|
|
353
353
|
|
|
354
354
|
const readStorageText = options?.readStorageText;
|
|
355
355
|
if (info.id === NIL_UUID || !readStorageText) {
|
|
@@ -358,12 +358,29 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
358
358
|
return baseResponse;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
const requestBundleSeeds = getRequestUpdateBundleSeeds(context);
|
|
362
|
+
const requestBundles = createRequestBundleIdentityMap({
|
|
363
|
+
context,
|
|
364
|
+
loadBundleById: (bundleId, requestContext) =>
|
|
365
|
+
getPlugin().getBundleById(bundleId, requestContext),
|
|
366
|
+
seeds: requestBundleSeeds,
|
|
367
|
+
});
|
|
368
|
+
const getCurrentBundle = () => {
|
|
369
|
+
if (args.bundleId === NIL_UUID) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const seededCurrentBundle = requestBundles.peek(args.bundleId);
|
|
374
|
+
if (seededCurrentBundle || requestBundleSeeds.length > 0) {
|
|
375
|
+
return seededCurrentBundle;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return requestBundles.get(args.bundleId);
|
|
379
|
+
};
|
|
361
380
|
const [fileUrl, targetBundle, currentBundle] = await Promise.all([
|
|
362
381
|
resolveFileUrl(storageUri ?? null, context),
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
? getPlugin().getBundleById(args.bundleId, context)
|
|
366
|
-
: null,
|
|
382
|
+
requestBundles.get(info.id),
|
|
383
|
+
getCurrentBundle(),
|
|
367
384
|
]);
|
|
368
385
|
const baseResponse: AppUpdateAvailableInfo = { ...rest, fileUrl };
|
|
369
386
|
const manifestArtifacts = await resolveManifestArtifacts({
|
|
@@ -435,6 +452,45 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
435
452
|
},
|
|
436
453
|
};
|
|
437
454
|
|
|
455
|
+
Object.defineProperty(api, "diagnostics", {
|
|
456
|
+
configurable: true,
|
|
457
|
+
enumerable: true,
|
|
458
|
+
get(this: DatabaseAPI<TContext>) {
|
|
459
|
+
const diagnostics = getPlugin().diagnostics;
|
|
460
|
+
if (!diagnostics) {
|
|
461
|
+
Object.defineProperty(this, "diagnostics", {
|
|
462
|
+
configurable: true,
|
|
463
|
+
enumerable: true,
|
|
464
|
+
value: undefined,
|
|
465
|
+
});
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const wrappedDiagnostics: NonNullable<
|
|
470
|
+
DatabaseAPI<TContext>["diagnostics"]
|
|
471
|
+
> = {};
|
|
472
|
+
if (diagnostics.bundleIndex) {
|
|
473
|
+
wrappedDiagnostics.bundleIndex = {
|
|
474
|
+
check: (context?: HotUpdaterContext<TContext>) =>
|
|
475
|
+
getPlugin().diagnostics!.bundleIndex!.check(context),
|
|
476
|
+
...(diagnostics.bundleIndex.repair
|
|
477
|
+
? {
|
|
478
|
+
repair: (context?: HotUpdaterContext<TContext>) =>
|
|
479
|
+
getPlugin().diagnostics!.bundleIndex!.repair!(context),
|
|
480
|
+
}
|
|
481
|
+
: {}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
Object.defineProperty(this, "diagnostics", {
|
|
486
|
+
configurable: true,
|
|
487
|
+
enumerable: true,
|
|
488
|
+
value: wrappedDiagnostics,
|
|
489
|
+
});
|
|
490
|
+
return wrappedDiagnostics;
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
|
|
438
494
|
return {
|
|
439
495
|
api,
|
|
440
496
|
adapterName: getPlugin().name,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Bundle } from "@hot-updater/core";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { createRequestBundleIdentityMap } from "./requestBundleIdentityMap";
|
|
5
|
+
|
|
6
|
+
const baseBundle: Bundle = {
|
|
7
|
+
channel: "production",
|
|
8
|
+
enabled: true,
|
|
9
|
+
fileHash: "file-hash",
|
|
10
|
+
fingerprintHash: null,
|
|
11
|
+
gitCommitHash: null,
|
|
12
|
+
id: "00000000-0000-0000-0000-000000000001",
|
|
13
|
+
message: "bundle",
|
|
14
|
+
platform: "ios",
|
|
15
|
+
shouldForceUpdate: false,
|
|
16
|
+
storageUri: "s3://bucket/bundle.zip",
|
|
17
|
+
targetAppVersion: "1.0.0",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("createRequestBundleIdentityMap", () => {
|
|
21
|
+
it("returns seeded bundles without loading them again", async () => {
|
|
22
|
+
const loadBundleById = vi.fn(async () => null);
|
|
23
|
+
const identityMap = createRequestBundleIdentityMap({
|
|
24
|
+
loadBundleById,
|
|
25
|
+
seeds: [baseBundle],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await expect(identityMap.get(baseBundle.id)).resolves.toBe(baseBundle);
|
|
29
|
+
expect(loadBundleById).not.toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("shares one fallback lookup for repeated bundle reads", async () => {
|
|
33
|
+
const loadedBundle: Bundle = {
|
|
34
|
+
...baseBundle,
|
|
35
|
+
id: "00000000-0000-0000-0000-000000000002",
|
|
36
|
+
message: "loaded",
|
|
37
|
+
};
|
|
38
|
+
const loadBundleById = vi.fn(async () => loadedBundle);
|
|
39
|
+
const identityMap = createRequestBundleIdentityMap({
|
|
40
|
+
loadBundleById,
|
|
41
|
+
seeds: [],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const [first, second] = await Promise.all([
|
|
45
|
+
identityMap.get(loadedBundle.id),
|
|
46
|
+
identityMap.get(loadedBundle.id),
|
|
47
|
+
]);
|
|
48
|
+
const third = await identityMap.get(loadedBundle.id);
|
|
49
|
+
|
|
50
|
+
expect(first).toBe(loadedBundle);
|
|
51
|
+
expect(second).toBe(loadedBundle);
|
|
52
|
+
expect(third).toBe(loadedBundle);
|
|
53
|
+
expect(loadBundleById).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(loadBundleById).toHaveBeenCalledWith(loadedBundle.id, undefined);
|
|
55
|
+
});
|
|
56
|
+
});
|