@hot-updater/server 0.29.4 → 0.29.5

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.
@@ -113,6 +113,8 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
113
113
  return getPlugin().getBundleById(id, context);
114
114
  },
115
115
  async getUpdateInfo(args, context) {
116
+ const directGetUpdateInfo = getPlugin().getUpdateInfo;
117
+ if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
116
118
  const channel = args.channel ?? "production";
117
119
  const minBundleId = args.minBundleId ?? _hot_updater_core.NIL_UUID;
118
120
  const baseWhere = getBaseWhere({
@@ -113,6 +113,8 @@ function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
113
113
  return getPlugin().getBundleById(id, context);
114
114
  },
115
115
  async getUpdateInfo(args, context) {
116
+ const directGetUpdateInfo = getPlugin().getUpdateInfo;
117
+ if (directGetUpdateInfo) return context === void 0 ? await directGetUpdateInfo(args) : await directGetUpdateInfo(args, context);
116
118
  const channel = args.channel ?? "production";
117
119
  const minBundleId = args.minBundleId ?? NIL_UUID;
118
120
  const baseWhere = getBaseWhere({
package/dist/handler.cjs CHANGED
@@ -7,7 +7,7 @@ var HandlerBadRequestError = class extends Error {
7
7
  }
8
8
  };
9
9
  const handleVersion = async () => {
10
- return new Response(JSON.stringify({ version: "0.29.4" }), {
10
+ return new Response(JSON.stringify({ version: "0.29.5" }), {
11
11
  status: 200,
12
12
  headers: { "Content-Type": "application/json" }
13
13
  });
package/dist/handler.mjs CHANGED
@@ -7,7 +7,7 @@ var HandlerBadRequestError = class extends Error {
7
7
  }
8
8
  };
9
9
  const handleVersion = async () => {
10
- return new Response(JSON.stringify({ version: "0.29.4" }), {
10
+ return new Response(JSON.stringify({ version: "0.29.5" }), {
11
11
  status: 200,
12
12
  headers: { "Content-Type": "application/json" }
13
13
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/server",
3
- "version": "0.29.4",
3
+ "version": "0.29.5",
4
4
  "type": "module",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "sideEffects": false,
@@ -53,9 +53,9 @@
53
53
  "fumadb": "0.2.2",
54
54
  "rou3": "0.7.9",
55
55
  "semver": "^7.7.2",
56
- "@hot-updater/plugin-core": "0.29.4",
57
- "@hot-updater/core": "0.29.4",
58
- "@hot-updater/js": "0.29.4"
56
+ "@hot-updater/core": "0.29.5",
57
+ "@hot-updater/plugin-core": "0.29.5",
58
+ "@hot-updater/js": "0.29.5"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@electric-sql/pglite": "^0.2.17",
@@ -66,8 +66,8 @@
66
66
  "kysely-pglite-dialect": "^1.2.0",
67
67
  "msw": "^2.7.0",
68
68
  "uuidv7": "^1.0.2",
69
- "@hot-updater/standalone": "0.29.4",
70
- "@hot-updater/test-utils": "0.29.4"
69
+ "@hot-updater/standalone": "0.29.5",
70
+ "@hot-updater/test-utils": "0.29.5"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsdown",
@@ -0,0 +1,131 @@
1
+ import type { Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
2
+ import { NIL_UUID } from "@hot-updater/core";
3
+ import type {
4
+ DatabasePlugin,
5
+ RequestEnvContext,
6
+ } from "@hot-updater/plugin-core";
7
+ import { describe, expect, it, vi } from "vitest";
8
+
9
+ import { createPluginDatabaseCore } from "./pluginCore";
10
+
11
+ const baseBundle: Bundle = {
12
+ id: "00000000-0000-0000-0000-000000000001",
13
+ channel: "production",
14
+ enabled: true,
15
+ fileHash: "hash-1",
16
+ fingerprintHash: null,
17
+ gitCommitHash: null,
18
+ message: "bundle",
19
+ platform: "ios",
20
+ shouldForceUpdate: false,
21
+ storageUri: "s3://bucket/bundle.zip",
22
+ targetAppVersion: "1.0.0",
23
+ };
24
+
25
+ const updateArgs: GetBundlesArgs = {
26
+ _updateStrategy: "appVersion",
27
+ appVersion: "1.0.0",
28
+ bundleId: NIL_UUID,
29
+ platform: "ios",
30
+ };
31
+
32
+ type TestContext = RequestEnvContext<{
33
+ assetHost: string;
34
+ }>;
35
+
36
+ describe("createPluginDatabaseCore", () => {
37
+ it("prefers plugin getUpdateInfo fast-path when provided", async () => {
38
+ const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>();
39
+ const expected: UpdateInfo = {
40
+ fileHash: baseBundle.fileHash,
41
+ id: baseBundle.id,
42
+ message: baseBundle.message,
43
+ shouldForceUpdate: baseBundle.shouldForceUpdate,
44
+ status: "UPDATE",
45
+ storageUri: baseBundle.storageUri,
46
+ };
47
+ const getUpdateInfo = vi.fn<
48
+ NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
49
+ >(async () => expected);
50
+
51
+ const plugin: DatabasePlugin<TestContext> = {
52
+ name: "fast-path-plugin",
53
+ async appendBundle() {},
54
+ async commitBundle() {},
55
+ async deleteBundle() {},
56
+ async getBundleById() {
57
+ return null;
58
+ },
59
+ getBundles,
60
+ getUpdateInfo,
61
+ async getChannels() {
62
+ return ["production"];
63
+ },
64
+ async updateBundle() {},
65
+ };
66
+
67
+ const core = createPluginDatabaseCore(
68
+ () => plugin,
69
+ async () => null,
70
+ );
71
+ const context: TestContext = {
72
+ env: {
73
+ assetHost: "https://assets.example.com",
74
+ },
75
+ request: new Request("https://updates.example.com"),
76
+ };
77
+
78
+ await expect(core.api.getUpdateInfo(updateArgs, context)).resolves.toEqual(
79
+ expected,
80
+ );
81
+ expect(getUpdateInfo).toHaveBeenCalledWith(updateArgs, context);
82
+ expect(getBundles).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it("falls back to scanning when plugin getUpdateInfo is absent", async () => {
86
+ const latestBundle = {
87
+ ...baseBundle,
88
+ id: "00000000-0000-0000-0000-000000000002",
89
+ };
90
+ const getBundles = vi.fn<DatabasePlugin["getBundles"]>(async () => ({
91
+ data: [latestBundle],
92
+ pagination: {
93
+ currentPage: 1,
94
+ hasNextPage: false,
95
+ hasPreviousPage: false,
96
+ total: 1,
97
+ totalPages: 1,
98
+ },
99
+ }));
100
+
101
+ const plugin: DatabasePlugin = {
102
+ name: "scan-plugin",
103
+ async appendBundle() {},
104
+ async commitBundle() {},
105
+ async deleteBundle() {},
106
+ async getBundleById() {
107
+ return null;
108
+ },
109
+ getBundles,
110
+ async getChannels() {
111
+ return ["production"];
112
+ },
113
+ async updateBundle() {},
114
+ };
115
+
116
+ const core = createPluginDatabaseCore(
117
+ () => plugin,
118
+ async () => null,
119
+ );
120
+
121
+ await expect(core.api.getUpdateInfo(updateArgs)).resolves.toEqual({
122
+ fileHash: latestBundle.fileHash,
123
+ id: latestBundle.id,
124
+ message: latestBundle.message,
125
+ shouldForceUpdate: latestBundle.shouldForceUpdate,
126
+ status: "UPDATE",
127
+ storageUri: latestBundle.storageUri,
128
+ });
129
+ expect(getBundles).toHaveBeenCalledOnce();
130
+ });
131
+ });
@@ -269,6 +269,14 @@ export function createPluginDatabaseCore<TContext = unknown>(
269
269
  args: GetBundlesArgs,
270
270
  context?: HotUpdaterContext<TContext>,
271
271
  ): Promise<UpdateInfo | null> {
272
+ const plugin = getPlugin();
273
+ const directGetUpdateInfo = plugin.getUpdateInfo;
274
+ if (directGetUpdateInfo) {
275
+ return context === undefined
276
+ ? await directGetUpdateInfo(args)
277
+ : await directGetUpdateInfo(args, context);
278
+ }
279
+
272
280
  const channel = args.channel ?? "production";
273
281
  const minBundleId = args.minBundleId ?? NIL_UUID;
274
282
  const baseWhere = getBaseWhere({
@@ -31,6 +31,111 @@ type TestEnv = {
31
31
  type TestContext = RequestEnvContext<TestEnv>;
32
32
 
33
33
  describe("runtime createHotUpdater", () => {
34
+ it("resolves storage URLs with handler context when database fast-path is used", async () => {
35
+ const request = new Request(
36
+ "https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +
37
+ `${NIL_UUID}/${NIL_UUID}`,
38
+ );
39
+ const getBundles = vi.fn<DatabasePlugin<TestContext>["getBundles"]>();
40
+ const getUpdateInfo = vi.fn<
41
+ NonNullable<DatabasePlugin<TestContext>["getUpdateInfo"]>
42
+ >(async () => ({
43
+ fileHash: bundle.fileHash,
44
+ id: bundle.id,
45
+ message: bundle.message,
46
+ shouldForceUpdate: bundle.shouldForceUpdate,
47
+ status: "UPDATE",
48
+ storageUri: bundle.storageUri,
49
+ }));
50
+ const getDownloadUrl = vi.fn<StoragePlugin<TestContext>["getDownloadUrl"]>(
51
+ async (_storageUri, context) => {
52
+ return {
53
+ fileUrl: new URL("/bundle.zip", context?.env?.assetHost).toString(),
54
+ };
55
+ },
56
+ );
57
+
58
+ const database: DatabasePlugin<TestContext> = {
59
+ name: "testDatabase",
60
+ async appendBundle() {},
61
+ async commitBundle() {},
62
+ async deleteBundle() {},
63
+ async getBundleById(id) {
64
+ return id === bundle.id ? bundle : null;
65
+ },
66
+ getBundles,
67
+ getUpdateInfo,
68
+ async getChannels() {
69
+ return ["production"];
70
+ },
71
+ async onUnmount() {},
72
+ async updateBundle() {},
73
+ };
74
+ const storage: StoragePlugin<TestContext> = {
75
+ name: "testStorage",
76
+ supportedProtocol: "s3",
77
+ async upload(key) {
78
+ return { storageUri: `s3://test-bucket/${key}` };
79
+ },
80
+ async delete() {},
81
+ getDownloadUrl,
82
+ };
83
+
84
+ const hotUpdater = createHotUpdater({
85
+ database,
86
+ storages: [storage],
87
+ basePath: "/api/check-update",
88
+ routes: {
89
+ updateCheck: true,
90
+ bundles: false,
91
+ },
92
+ });
93
+
94
+ const response = await hotUpdater.handler(request, {
95
+ env: {
96
+ assetHost: "https://assets.example.com",
97
+ },
98
+ request,
99
+ });
100
+
101
+ expect(response.status).toBe(200);
102
+ await expect(response.json()).resolves.toEqual({
103
+ fileHash: "hash123",
104
+ fileUrl: "https://assets.example.com/bundle.zip",
105
+ id: "00000000-0000-0000-0000-000000000001",
106
+ message: "Test bundle",
107
+ shouldForceUpdate: false,
108
+ status: "UPDATE",
109
+ });
110
+ expect(getUpdateInfo).toHaveBeenCalledWith(
111
+ {
112
+ _updateStrategy: "appVersion",
113
+ appVersion: "1.0.0",
114
+ bundleId: NIL_UUID,
115
+ channel: "production",
116
+ cohort: undefined,
117
+ minBundleId: NIL_UUID,
118
+ platform: "ios",
119
+ },
120
+ expect.objectContaining({
121
+ env: {
122
+ assetHost: "https://assets.example.com",
123
+ },
124
+ request: expect.any(Request),
125
+ }),
126
+ );
127
+ expect(getBundles).not.toHaveBeenCalled();
128
+ expect(getDownloadUrl).toHaveBeenCalledWith(
129
+ "s3://test-bucket/bundles/bundle.zip",
130
+ expect.objectContaining({
131
+ env: {
132
+ assetHost: "https://assets.example.com",
133
+ },
134
+ request: expect.any(Request),
135
+ }),
136
+ );
137
+ });
138
+
34
139
  it("passes the handler context to database and storage resolution", async () => {
35
140
  const request = new Request(
36
141
  "https://updates.example.com/api/check-update/app-version/ios/1.0.0/production/" +