@hot-updater/server 0.29.2 → 0.29.3
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 +4 -1
- package/dist/db/index.mjs +4 -1
- package/dist/db/pluginCore.cjs +28 -14
- package/dist/db/pluginCore.mjs +28 -14
- package/dist/db/types.d.cts +2 -5
- package/dist/db/types.d.mts +2 -5
- package/dist/handler.cjs +4 -4
- package/dist/handler.d.cts +2 -5
- package/dist/handler.d.mts +2 -5
- package/dist/handler.mjs +4 -4
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/runtime.cjs +4 -2
- package/dist/runtime.mjs +4 -2
- package/dist/types/index.d.cts +9 -3
- package/dist/types/index.d.mts +9 -3
- package/package.json +6 -6
- package/src/db/index.spec.ts +92 -0
- package/src/db/index.ts +18 -4
- package/src/db/ormCore.ts +2 -2
- package/src/db/pluginCore.ts +42 -17
- package/src/db/pluginUpdateCheck.bench.ts +4 -1
- package/src/db/types.ts +2 -2
- package/src/handler-standalone.integration.spec.ts +24 -1
- package/src/handler.spec.ts +87 -17
- package/src/handler.ts +9 -4
- package/src/runtime.spec.ts +154 -0
- package/src/runtime.ts +9 -8
- package/src/types/index.ts +11 -2
package/dist/db/index.cjs
CHANGED
|
@@ -21,7 +21,10 @@ function createHotUpdater(options) {
|
|
|
21
21
|
return resolveStoragePluginUrl(storageUri, context);
|
|
22
22
|
};
|
|
23
23
|
const database = options.database;
|
|
24
|
-
const core = require_types.isDatabasePluginFactory(database) || require_types.isDatabasePlugin(database) ?
|
|
24
|
+
const core = require_types.isDatabasePluginFactory(database) || require_types.isDatabasePlugin(database) ? (() => {
|
|
25
|
+
const plugin = require_types.isDatabasePluginFactory(database) ? database() : database;
|
|
26
|
+
return require_pluginCore.createPluginDatabaseCore(() => plugin, resolveFileUrl, require_types.isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
27
|
+
})() : require_ormCore.createOrmDatabaseCore({
|
|
25
28
|
database,
|
|
26
29
|
resolveFileUrl
|
|
27
30
|
});
|
package/dist/db/index.mjs
CHANGED
|
@@ -21,7 +21,10 @@ function createHotUpdater(options) {
|
|
|
21
21
|
return resolveStoragePluginUrl(storageUri, context);
|
|
22
22
|
};
|
|
23
23
|
const database = options.database;
|
|
24
|
-
const core = isDatabasePluginFactory(database) || isDatabasePlugin(database) ?
|
|
24
|
+
const core = isDatabasePluginFactory(database) || isDatabasePlugin(database) ? (() => {
|
|
25
|
+
const plugin = isDatabasePluginFactory(database) ? database() : database;
|
|
26
|
+
return createPluginDatabaseCore(() => plugin, resolveFileUrl, isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
27
|
+
})() : createOrmDatabaseCore({
|
|
25
28
|
database,
|
|
26
29
|
resolveFileUrl
|
|
27
30
|
});
|
package/dist/db/pluginCore.cjs
CHANGED
|
@@ -46,9 +46,17 @@ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO = {
|
|
|
46
46
|
storageUri: null,
|
|
47
47
|
fileHash: null
|
|
48
48
|
};
|
|
49
|
-
function createPluginDatabaseCore(
|
|
49
|
+
function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
50
|
+
const runWithMutationPlugin = async (operation) => {
|
|
51
|
+
const plugin = options?.createMutationPlugin?.() ?? getPlugin();
|
|
52
|
+
try {
|
|
53
|
+
return await operation(plugin);
|
|
54
|
+
} finally {
|
|
55
|
+
if (options?.createMutationPlugin) await options.cleanupMutationPlugin?.(plugin);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
50
58
|
const getSortedBundlePage = async (options, context) => {
|
|
51
|
-
const result = await
|
|
59
|
+
const result = await getPlugin().getBundles({
|
|
52
60
|
...options,
|
|
53
61
|
orderBy: options.orderBy ?? DESC_ORDER
|
|
54
62
|
}, context);
|
|
@@ -102,7 +110,7 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
102
110
|
return {
|
|
103
111
|
api: {
|
|
104
112
|
async getBundleById(id, context) {
|
|
105
|
-
return
|
|
113
|
+
return getPlugin().getBundleById(id, context);
|
|
106
114
|
},
|
|
107
115
|
async getUpdateInfo(args, context) {
|
|
108
116
|
const channel = args.channel ?? "production";
|
|
@@ -143,27 +151,33 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
143
151
|
};
|
|
144
152
|
},
|
|
145
153
|
async getChannels(context) {
|
|
146
|
-
return
|
|
154
|
+
return getPlugin().getChannels(context);
|
|
147
155
|
},
|
|
148
156
|
async getBundles(options, context) {
|
|
149
|
-
return
|
|
157
|
+
return getPlugin().getBundles(options, context);
|
|
150
158
|
},
|
|
151
159
|
async insertBundle(bundle, context) {
|
|
152
|
-
await
|
|
153
|
-
|
|
160
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
161
|
+
await plugin.appendBundle(bundle, context);
|
|
162
|
+
await plugin.commitBundle(context);
|
|
163
|
+
});
|
|
154
164
|
},
|
|
155
165
|
async updateBundleById(bundleId, newBundle, context) {
|
|
156
|
-
await
|
|
157
|
-
|
|
166
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
167
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
168
|
+
await plugin.commitBundle(context);
|
|
169
|
+
});
|
|
158
170
|
},
|
|
159
171
|
async deleteBundleById(bundleId, context) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
173
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
174
|
+
if (!bundle) return;
|
|
175
|
+
await plugin.deleteBundle(bundle, context);
|
|
176
|
+
await plugin.commitBundle(context);
|
|
177
|
+
});
|
|
164
178
|
}
|
|
165
179
|
},
|
|
166
|
-
adapterName:
|
|
180
|
+
adapterName: getPlugin().name,
|
|
167
181
|
createMigrator: () => {
|
|
168
182
|
throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
|
|
169
183
|
},
|
package/dist/db/pluginCore.mjs
CHANGED
|
@@ -46,9 +46,17 @@ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO = {
|
|
|
46
46
|
storageUri: null,
|
|
47
47
|
fileHash: null
|
|
48
48
|
};
|
|
49
|
-
function createPluginDatabaseCore(
|
|
49
|
+
function createPluginDatabaseCore(getPlugin, resolveFileUrl, options) {
|
|
50
|
+
const runWithMutationPlugin = async (operation) => {
|
|
51
|
+
const plugin = options?.createMutationPlugin?.() ?? getPlugin();
|
|
52
|
+
try {
|
|
53
|
+
return await operation(plugin);
|
|
54
|
+
} finally {
|
|
55
|
+
if (options?.createMutationPlugin) await options.cleanupMutationPlugin?.(plugin);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
50
58
|
const getSortedBundlePage = async (options, context) => {
|
|
51
|
-
const result = await
|
|
59
|
+
const result = await getPlugin().getBundles({
|
|
52
60
|
...options,
|
|
53
61
|
orderBy: options.orderBy ?? DESC_ORDER
|
|
54
62
|
}, context);
|
|
@@ -102,7 +110,7 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
102
110
|
return {
|
|
103
111
|
api: {
|
|
104
112
|
async getBundleById(id, context) {
|
|
105
|
-
return
|
|
113
|
+
return getPlugin().getBundleById(id, context);
|
|
106
114
|
},
|
|
107
115
|
async getUpdateInfo(args, context) {
|
|
108
116
|
const channel = args.channel ?? "production";
|
|
@@ -143,27 +151,33 @@ function createPluginDatabaseCore(plugin, resolveFileUrl) {
|
|
|
143
151
|
};
|
|
144
152
|
},
|
|
145
153
|
async getChannels(context) {
|
|
146
|
-
return
|
|
154
|
+
return getPlugin().getChannels(context);
|
|
147
155
|
},
|
|
148
156
|
async getBundles(options, context) {
|
|
149
|
-
return
|
|
157
|
+
return getPlugin().getBundles(options, context);
|
|
150
158
|
},
|
|
151
159
|
async insertBundle(bundle, context) {
|
|
152
|
-
await
|
|
153
|
-
|
|
160
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
161
|
+
await plugin.appendBundle(bundle, context);
|
|
162
|
+
await plugin.commitBundle(context);
|
|
163
|
+
});
|
|
154
164
|
},
|
|
155
165
|
async updateBundleById(bundleId, newBundle, context) {
|
|
156
|
-
await
|
|
157
|
-
|
|
166
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
167
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
168
|
+
await plugin.commitBundle(context);
|
|
169
|
+
});
|
|
158
170
|
},
|
|
159
171
|
async deleteBundleById(bundleId, context) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
173
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
174
|
+
if (!bundle) return;
|
|
175
|
+
await plugin.deleteBundle(bundle, context);
|
|
176
|
+
await plugin.commitBundle(context);
|
|
177
|
+
});
|
|
164
178
|
}
|
|
165
179
|
},
|
|
166
|
-
adapterName:
|
|
180
|
+
adapterName: getPlugin().name,
|
|
167
181
|
createMigrator: () => {
|
|
168
182
|
throw new Error("createMigrator is only available for Kysely/Prisma/Drizzle database adapters.");
|
|
169
183
|
},
|
package/dist/db/types.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "../types/index.cjs";
|
|
2
2
|
import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
|
|
3
3
|
import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
4
4
|
import { FumaDBAdapter } from "fumadb/adapters";
|
|
@@ -11,10 +11,7 @@ interface DatabaseAPI<TContext = unknown> {
|
|
|
11
11
|
getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
|
|
12
12
|
getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
|
|
13
13
|
getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
|
|
14
|
-
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<
|
|
15
|
-
data: Bundle[];
|
|
16
|
-
pagination: PaginationInfo;
|
|
17
|
-
}>;
|
|
14
|
+
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<PaginatedResult>;
|
|
18
15
|
insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
19
16
|
updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
20
17
|
deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
|
package/dist/db/types.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "../types/index.mjs";
|
|
2
2
|
import { AppUpdateInfo, Bundle, GetBundlesArgs, UpdateInfo } from "@hot-updater/core";
|
|
3
3
|
import { DatabaseBundleQueryOptions, DatabasePlugin, HotUpdaterContext, StoragePlugin } from "@hot-updater/plugin-core";
|
|
4
4
|
import { FumaDBAdapter } from "fumadb/adapters";
|
|
@@ -11,10 +11,7 @@ interface DatabaseAPI<TContext = unknown> {
|
|
|
11
11
|
getUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<UpdateInfo | null>;
|
|
12
12
|
getAppUpdateInfo(args: GetBundlesArgs, context?: HotUpdaterContext<TContext>): Promise<AppUpdateInfo | null>;
|
|
13
13
|
getChannels(context?: HotUpdaterContext<TContext>): Promise<string[]>;
|
|
14
|
-
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<
|
|
15
|
-
data: Bundle[];
|
|
16
|
-
pagination: PaginationInfo;
|
|
17
|
-
}>;
|
|
14
|
+
getBundles(options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>): Promise<PaginatedResult>;
|
|
18
15
|
insertBundle(bundle: Bundle, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
19
16
|
updateBundleById(bundleId: string, newBundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>): Promise<void>;
|
|
20
17
|
deleteBundleById(bundleId: string, context?: HotUpdaterContext<TContext>): Promise<void>;
|
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.
|
|
10
|
+
return new Response(JSON.stringify({ version: "0.29.3" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
|
@@ -107,7 +107,7 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
107
107
|
limit,
|
|
108
108
|
offset
|
|
109
109
|
}, context);
|
|
110
|
-
return new Response(JSON.stringify(result
|
|
110
|
+
return new Response(JSON.stringify(result), {
|
|
111
111
|
status: 200,
|
|
112
112
|
headers: { "Content-Type": "application/json" }
|
|
113
113
|
});
|
|
@@ -140,8 +140,8 @@ const handleDeleteBundle = async (params, _request, api, context) => {
|
|
|
140
140
|
});
|
|
141
141
|
};
|
|
142
142
|
const handleGetChannels = async (_params, _request, api, context) => {
|
|
143
|
-
const
|
|
144
|
-
return new Response(JSON.stringify(
|
|
143
|
+
const response = { data: { channels: await api.getChannels(context) } };
|
|
144
|
+
return new Response(JSON.stringify(response), {
|
|
145
145
|
status: 200,
|
|
146
146
|
headers: { "Content-Type": "application/json" }
|
|
147
147
|
});
|
package/dist/handler.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "./types/index.cjs";
|
|
2
2
|
import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
|
|
3
3
|
import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
|
|
4
4
|
|
|
@@ -6,10 +6,7 @@ import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesA
|
|
|
6
6
|
interface HandlerAPI<TContext = unknown> {
|
|
7
7
|
getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
|
|
8
8
|
getBundleById: (id: string, context?: HotUpdaterContext<TContext>) => Promise<Bundle | null>;
|
|
9
|
-
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<
|
|
10
|
-
data: Bundle[];
|
|
11
|
-
pagination: PaginationInfo;
|
|
12
|
-
}>;
|
|
9
|
+
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<PaginatedResult>;
|
|
13
10
|
insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
14
11
|
updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
15
12
|
deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
package/dist/handler.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PaginatedResult } from "./types/index.mjs";
|
|
2
2
|
import { AppUpdateInfo, AppVersionGetBundlesArgs, Bundle, FingerprintGetBundlesArgs } from "@hot-updater/core";
|
|
3
3
|
import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plugin-core";
|
|
4
4
|
|
|
@@ -6,10 +6,7 @@ import { DatabaseBundleQueryOptions, HotUpdaterContext } from "@hot-updater/plug
|
|
|
6
6
|
interface HandlerAPI<TContext = unknown> {
|
|
7
7
|
getAppUpdateInfo: (args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs, context?: HotUpdaterContext<TContext>) => Promise<AppUpdateInfo | null>;
|
|
8
8
|
getBundleById: (id: string, context?: HotUpdaterContext<TContext>) => Promise<Bundle | null>;
|
|
9
|
-
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<
|
|
10
|
-
data: Bundle[];
|
|
11
|
-
pagination: PaginationInfo;
|
|
12
|
-
}>;
|
|
9
|
+
getBundles: (options: DatabaseBundleQueryOptions, context?: HotUpdaterContext<TContext>) => Promise<PaginatedResult>;
|
|
13
10
|
insertBundle: (bundle: Bundle, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
14
11
|
updateBundleById: (bundleId: string, bundle: Partial<Bundle>, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
|
15
12
|
deleteBundleById: (bundleId: string, context?: HotUpdaterContext<TContext>) => Promise<void>;
|
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.
|
|
10
|
+
return new Response(JSON.stringify({ version: "0.29.3" }), {
|
|
11
11
|
status: 200,
|
|
12
12
|
headers: { "Content-Type": "application/json" }
|
|
13
13
|
});
|
|
@@ -107,7 +107,7 @@ const handleGetBundles = async (_params, request, api, context) => {
|
|
|
107
107
|
limit,
|
|
108
108
|
offset
|
|
109
109
|
}, context);
|
|
110
|
-
return new Response(JSON.stringify(result
|
|
110
|
+
return new Response(JSON.stringify(result), {
|
|
111
111
|
status: 200,
|
|
112
112
|
headers: { "Content-Type": "application/json" }
|
|
113
113
|
});
|
|
@@ -140,8 +140,8 @@ const handleDeleteBundle = async (params, _request, api, context) => {
|
|
|
140
140
|
});
|
|
141
141
|
};
|
|
142
142
|
const handleGetChannels = async (_params, _request, api, context) => {
|
|
143
|
-
const
|
|
144
|
-
return new Response(JSON.stringify(
|
|
143
|
+
const response = { data: { channels: await api.getChannels(context) } };
|
|
144
|
+
return new Response(JSON.stringify(response), {
|
|
145
145
|
status: 200,
|
|
146
146
|
headers: { "Content-Type": "application/json" }
|
|
147
147
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Bundle, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.cjs";
|
|
1
|
+
import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.cjs";
|
|
2
2
|
import { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler } from "./handler.cjs";
|
|
3
3
|
import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.cjs";
|
|
4
4
|
import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.cjs";
|
|
5
|
-
export { Bundle, CreateHotUpdaterOptions, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
|
5
|
+
export { Bundle, ChannelsResponse, CreateHotUpdaterOptions, DataResponse, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Bundle, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.mjs";
|
|
1
|
+
import { Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions } from "./types/index.mjs";
|
|
2
2
|
import { HandlerAPI, HandlerOptions, HandlerRoutes, createHandler } from "./handler.mjs";
|
|
3
3
|
import { HotUpdaterClient, HotUpdaterDB, Migrator } from "./db/ormCore.mjs";
|
|
4
4
|
import { CreateHotUpdaterOptions, HotUpdaterAPI, createHotUpdater } from "./db/index.mjs";
|
|
5
|
-
export { Bundle, CreateHotUpdaterOptions, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
|
5
|
+
export { Bundle, ChannelsResponse, CreateHotUpdaterOptions, DataResponse, HandlerAPI, HandlerOptions, HandlerRoutes, HotUpdaterAPI, HotUpdaterClient, HotUpdaterDB, Migrator, Paginated, PaginatedResult, PaginationInfo, PaginationOptions, createHandler, createHotUpdater };
|
package/dist/runtime.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const require_pluginCore = require("./db/pluginCore.cjs");
|
|
|
5
5
|
const require_types = require("./db/types.cjs");
|
|
6
6
|
//#region src/runtime.ts
|
|
7
7
|
function createHotUpdater(options) {
|
|
8
|
+
const database = options.database;
|
|
8
9
|
const basePath = require_route.normalizeBasePath(options.basePath ?? "/api");
|
|
9
10
|
const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map((plugin) => typeof plugin === "function" ? plugin() : plugin);
|
|
10
11
|
const resolveStoragePluginUrl = async (storageUri, context) => {
|
|
@@ -17,8 +18,9 @@ function createHotUpdater(options) {
|
|
|
17
18
|
if (!fileUrl) throw new Error("Storage plugin returned empty fileUrl");
|
|
18
19
|
return fileUrl;
|
|
19
20
|
};
|
|
20
|
-
if (!require_types.isDatabasePluginFactory(
|
|
21
|
-
const
|
|
21
|
+
if (!require_types.isDatabasePluginFactory(database) && !require_types.isDatabasePlugin(database)) throw new Error("@hot-updater/server/runtime only supports database plugins.");
|
|
22
|
+
const plugin = require_types.isDatabasePluginFactory(database) ? database() : database;
|
|
23
|
+
const core = require_pluginCore.createPluginDatabaseCore(() => plugin, resolveStoragePluginUrl, require_types.isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
22
24
|
const api = {
|
|
23
25
|
...core.api,
|
|
24
26
|
handler: require_handler.createHandler(core.api, {
|
package/dist/runtime.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { createPluginDatabaseCore } from "./db/pluginCore.mjs";
|
|
|
4
4
|
import { isDatabasePlugin, isDatabasePluginFactory } from "./db/types.mjs";
|
|
5
5
|
//#region src/runtime.ts
|
|
6
6
|
function createHotUpdater(options) {
|
|
7
|
+
const database = options.database;
|
|
7
8
|
const basePath = normalizeBasePath(options.basePath ?? "/api");
|
|
8
9
|
const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map((plugin) => typeof plugin === "function" ? plugin() : plugin);
|
|
9
10
|
const resolveStoragePluginUrl = async (storageUri, context) => {
|
|
@@ -16,8 +17,9 @@ function createHotUpdater(options) {
|
|
|
16
17
|
if (!fileUrl) throw new Error("Storage plugin returned empty fileUrl");
|
|
17
18
|
return fileUrl;
|
|
18
19
|
};
|
|
19
|
-
if (!isDatabasePluginFactory(
|
|
20
|
-
const
|
|
20
|
+
if (!isDatabasePluginFactory(database) && !isDatabasePlugin(database)) throw new Error("@hot-updater/server/runtime only supports database plugins.");
|
|
21
|
+
const plugin = isDatabasePluginFactory(database) ? database() : database;
|
|
22
|
+
const core = createPluginDatabaseCore(() => plugin, resolveStoragePluginUrl, isDatabasePluginFactory(database) ? { createMutationPlugin: () => database() } : void 0);
|
|
21
23
|
const api = {
|
|
22
24
|
...core.api,
|
|
23
25
|
handler: createHandler(core.api, {
|
package/dist/types/index.d.cts
CHANGED
|
@@ -13,9 +13,15 @@ interface PaginationOptions {
|
|
|
13
13
|
limit: number;
|
|
14
14
|
offset: number;
|
|
15
15
|
}
|
|
16
|
-
interface
|
|
17
|
-
data:
|
|
16
|
+
interface DataResponse<TData> {
|
|
17
|
+
data: TData;
|
|
18
|
+
}
|
|
19
|
+
interface Paginated<TData> extends DataResponse<TData> {
|
|
18
20
|
pagination: PaginationInfo;
|
|
19
21
|
}
|
|
22
|
+
type PaginatedResult = Paginated<Bundle[]>;
|
|
23
|
+
type ChannelsResponse = DataResponse<{
|
|
24
|
+
channels: string[];
|
|
25
|
+
}>;
|
|
20
26
|
//#endregion
|
|
21
|
-
export { type Bundle$1 as Bundle, PaginatedResult, PaginationInfo, PaginationOptions };
|
|
27
|
+
export { type Bundle$1 as Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions };
|
package/dist/types/index.d.mts
CHANGED
|
@@ -13,9 +13,15 @@ interface PaginationOptions {
|
|
|
13
13
|
limit: number;
|
|
14
14
|
offset: number;
|
|
15
15
|
}
|
|
16
|
-
interface
|
|
17
|
-
data:
|
|
16
|
+
interface DataResponse<TData> {
|
|
17
|
+
data: TData;
|
|
18
|
+
}
|
|
19
|
+
interface Paginated<TData> extends DataResponse<TData> {
|
|
18
20
|
pagination: PaginationInfo;
|
|
19
21
|
}
|
|
22
|
+
type PaginatedResult = Paginated<Bundle[]>;
|
|
23
|
+
type ChannelsResponse = DataResponse<{
|
|
24
|
+
channels: string[];
|
|
25
|
+
}>;
|
|
20
26
|
//#endregion
|
|
21
|
-
export { type Bundle$1 as Bundle, PaginatedResult, PaginationInfo, PaginationOptions };
|
|
27
|
+
export { type Bundle$1 as Bundle, ChannelsResponse, DataResponse, Paginated, PaginatedResult, PaginationInfo, PaginationOptions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/server",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.3",
|
|
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/
|
|
57
|
-
"@hot-updater/core": "0.29.
|
|
58
|
-
"@hot-updater/js": "0.29.
|
|
56
|
+
"@hot-updater/core": "0.29.3",
|
|
57
|
+
"@hot-updater/plugin-core": "0.29.3",
|
|
58
|
+
"@hot-updater/js": "0.29.3"
|
|
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.
|
|
70
|
-
"@hot-updater/test-utils": "0.29.
|
|
69
|
+
"@hot-updater/standalone": "0.29.3",
|
|
70
|
+
"@hot-updater/test-utils": "0.29.3"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "tsdown",
|
package/src/db/index.spec.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
StoragePlugin,
|
|
6
6
|
StorageResolveContext,
|
|
7
7
|
} from "@hot-updater/plugin-core";
|
|
8
|
+
import { createDatabasePlugin } from "@hot-updater/plugin-core";
|
|
8
9
|
import {
|
|
9
10
|
setupBundleMethodsTestSuite,
|
|
10
11
|
setupGetUpdateInfoTestSuite,
|
|
@@ -361,4 +362,95 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
|
|
|
361
362
|
);
|
|
362
363
|
});
|
|
363
364
|
});
|
|
365
|
+
|
|
366
|
+
describe("database plugin factories", () => {
|
|
367
|
+
it("isolates pending mutation state between overlapping writes", async () => {
|
|
368
|
+
const committedBundleIds: string[][] = [];
|
|
369
|
+
const onUnmount = vi.fn(async () => undefined);
|
|
370
|
+
let releaseFirstCommit!: () => void;
|
|
371
|
+
let notifyFirstCommitStarted!: () => void;
|
|
372
|
+
const firstCommitStarted = new Promise<void>((resolve) => {
|
|
373
|
+
notifyFirstCommitStarted = resolve;
|
|
374
|
+
});
|
|
375
|
+
const firstCommitGate = new Promise<void>((resolve) => {
|
|
376
|
+
releaseFirstCommit = resolve;
|
|
377
|
+
});
|
|
378
|
+
let commitCount = 0;
|
|
379
|
+
|
|
380
|
+
const isolatedHotUpdater = createHotUpdater({
|
|
381
|
+
database: createDatabasePlugin({
|
|
382
|
+
name: "isolatedPlugin",
|
|
383
|
+
factory: () => ({
|
|
384
|
+
async getBundleById() {
|
|
385
|
+
return null;
|
|
386
|
+
},
|
|
387
|
+
async getBundles() {
|
|
388
|
+
return {
|
|
389
|
+
data: [],
|
|
390
|
+
pagination: {
|
|
391
|
+
hasNextPage: false,
|
|
392
|
+
hasPreviousPage: false,
|
|
393
|
+
currentPage: 1,
|
|
394
|
+
totalPages: 1,
|
|
395
|
+
total: 0,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
async getChannels() {
|
|
400
|
+
return [];
|
|
401
|
+
},
|
|
402
|
+
onUnmount,
|
|
403
|
+
async commitBundle({ changedSets }) {
|
|
404
|
+
commitCount += 1;
|
|
405
|
+
committedBundleIds.push(
|
|
406
|
+
changedSets.map((change) => change.data.id),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (commitCount === 1) {
|
|
410
|
+
notifyFirstCommitStarted();
|
|
411
|
+
await firstCommitGate;
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
}),
|
|
415
|
+
})({}),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const firstBundleId = "00000000-0000-0000-0000-000000000030";
|
|
419
|
+
const secondBundleId = "00000000-0000-0000-0000-000000000031";
|
|
420
|
+
|
|
421
|
+
const firstInsert = isolatedHotUpdater.insertBundle({
|
|
422
|
+
id: firstBundleId,
|
|
423
|
+
platform: "ios",
|
|
424
|
+
shouldForceUpdate: false,
|
|
425
|
+
enabled: true,
|
|
426
|
+
fileHash: "hash-1",
|
|
427
|
+
gitCommitHash: null,
|
|
428
|
+
message: "first bundle",
|
|
429
|
+
channel: "production",
|
|
430
|
+
storageUri: "s3://test-bucket/first.zip",
|
|
431
|
+
targetAppVersion: "1.0.0",
|
|
432
|
+
fingerprintHash: null,
|
|
433
|
+
});
|
|
434
|
+
await firstCommitStarted;
|
|
435
|
+
|
|
436
|
+
const secondInsert = isolatedHotUpdater.insertBundle({
|
|
437
|
+
id: secondBundleId,
|
|
438
|
+
platform: "ios",
|
|
439
|
+
shouldForceUpdate: false,
|
|
440
|
+
enabled: true,
|
|
441
|
+
fileHash: "hash-2",
|
|
442
|
+
gitCommitHash: null,
|
|
443
|
+
message: "second bundle",
|
|
444
|
+
channel: "production",
|
|
445
|
+
storageUri: "s3://test-bucket/second.zip",
|
|
446
|
+
targetAppVersion: "1.0.0",
|
|
447
|
+
fingerprintHash: null,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
releaseFirstCommit();
|
|
451
|
+
await Promise.all([firstInsert, secondInsert]);
|
|
452
|
+
|
|
453
|
+
expect(committedBundleIds).toEqual([[firstBundleId], [secondBundleId]]);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
364
456
|
});
|
package/src/db/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
DatabasePlugin,
|
|
2
3
|
HotUpdaterContext,
|
|
3
4
|
StoragePlugin,
|
|
4
5
|
} from "@hot-updater/plugin-core";
|
|
@@ -92,10 +93,23 @@ export function createHotUpdater<TContext = unknown>(
|
|
|
92
93
|
|
|
93
94
|
const core =
|
|
94
95
|
isDatabasePluginFactory(database) || isDatabasePlugin(database)
|
|
95
|
-
?
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
? (() => {
|
|
97
|
+
const plugin: DatabasePlugin<TContext> = isDatabasePluginFactory(
|
|
98
|
+
database,
|
|
99
|
+
)
|
|
100
|
+
? database()
|
|
101
|
+
: database;
|
|
102
|
+
|
|
103
|
+
return createPluginDatabaseCore<TContext>(
|
|
104
|
+
() => plugin,
|
|
105
|
+
resolveFileUrl,
|
|
106
|
+
isDatabasePluginFactory(database)
|
|
107
|
+
? {
|
|
108
|
+
createMutationPlugin: () => database(),
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
);
|
|
112
|
+
})()
|
|
99
113
|
: createOrmDatabaseCore<TContext>({
|
|
100
114
|
database,
|
|
101
115
|
resolveFileUrl,
|
package/src/db/ormCore.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { FumaDBAdapter } from "fumadb/adapters";
|
|
|
24
24
|
import { calculatePagination } from "../calculatePagination";
|
|
25
25
|
import { v0_21_0 } from "../schema/v0_21_0";
|
|
26
26
|
import { v0_29_0 } from "../schema/v0_29_0";
|
|
27
|
-
import type {
|
|
27
|
+
import type { Paginated } from "../types";
|
|
28
28
|
import type { DatabaseAPI } from "./types";
|
|
29
29
|
|
|
30
30
|
const parseTargetCohorts = (value: unknown): string[] | null => {
|
|
@@ -508,7 +508,7 @@ export function createOrmDatabaseCore<TContext = unknown>({
|
|
|
508
508
|
|
|
509
509
|
async getBundles(
|
|
510
510
|
options: DatabaseBundleQueryOptions,
|
|
511
|
-
): Promise<
|
|
511
|
+
): Promise<Paginated<Bundle[]>> {
|
|
512
512
|
const orm = await ensureORM();
|
|
513
513
|
const { where, limit, offset, orderBy } = options;
|
|
514
514
|
|
package/src/db/pluginCore.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
import type { DatabaseAPI } from "./types";
|
|
20
20
|
|
|
21
21
|
const PAGE_SIZE = 100;
|
|
22
|
-
|
|
23
22
|
const DESC_ORDER = { field: "id", direction: "desc" } as const;
|
|
24
23
|
|
|
25
24
|
const bundleMatchesQueryWhere = (
|
|
@@ -100,22 +99,42 @@ const INIT_BUNDLE_ROLLBACK_UPDATE_INFO: UpdateInfo = {
|
|
|
100
99
|
};
|
|
101
100
|
|
|
102
101
|
export function createPluginDatabaseCore<TContext = unknown>(
|
|
103
|
-
|
|
102
|
+
getPlugin: () => DatabasePlugin<TContext>,
|
|
104
103
|
resolveFileUrl: (
|
|
105
104
|
storageUri: string | null,
|
|
106
105
|
context?: HotUpdaterContext<TContext>,
|
|
107
106
|
) => Promise<string | null>,
|
|
107
|
+
options?: {
|
|
108
|
+
createMutationPlugin?: () => DatabasePlugin<TContext>;
|
|
109
|
+
cleanupMutationPlugin?: (
|
|
110
|
+
plugin: DatabasePlugin<TContext>,
|
|
111
|
+
) => Promise<void> | void;
|
|
112
|
+
},
|
|
108
113
|
): {
|
|
109
114
|
api: DatabaseAPI<TContext>;
|
|
110
115
|
adapterName: string;
|
|
111
116
|
createMigrator: () => never;
|
|
112
117
|
generateSchema: () => never;
|
|
113
118
|
} {
|
|
119
|
+
const runWithMutationPlugin = async <T>(
|
|
120
|
+
operation: (plugin: DatabasePlugin<TContext>) => Promise<T>,
|
|
121
|
+
): Promise<T> => {
|
|
122
|
+
const plugin = options?.createMutationPlugin?.() ?? getPlugin();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
return await operation(plugin);
|
|
126
|
+
} finally {
|
|
127
|
+
if (options?.createMutationPlugin) {
|
|
128
|
+
await options.cleanupMutationPlugin?.(plugin);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
114
133
|
const getSortedBundlePage = async (
|
|
115
134
|
options: DatabaseBundleQueryOptions,
|
|
116
135
|
context?: HotUpdaterContext<TContext>,
|
|
117
136
|
): Promise<Awaited<ReturnType<DatabasePlugin<TContext>["getBundles"]>>> => {
|
|
118
|
-
const result = await
|
|
137
|
+
const result = await getPlugin().getBundles(
|
|
119
138
|
{
|
|
120
139
|
...options,
|
|
121
140
|
orderBy: options.orderBy ?? DESC_ORDER,
|
|
@@ -242,7 +261,7 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
242
261
|
id: string,
|
|
243
262
|
context?: HotUpdaterContext<TContext>,
|
|
244
263
|
): Promise<Bundle | null> {
|
|
245
|
-
return
|
|
264
|
+
return getPlugin().getBundleById(id, context);
|
|
246
265
|
},
|
|
247
266
|
|
|
248
267
|
async getUpdateInfo(
|
|
@@ -314,19 +333,21 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
314
333
|
async getChannels(
|
|
315
334
|
context?: HotUpdaterContext<TContext>,
|
|
316
335
|
): Promise<string[]> {
|
|
317
|
-
return
|
|
336
|
+
return getPlugin().getChannels(context);
|
|
318
337
|
},
|
|
319
338
|
|
|
320
339
|
async getBundles(options, context?: HotUpdaterContext<TContext>) {
|
|
321
|
-
return
|
|
340
|
+
return getPlugin().getBundles(options, context);
|
|
322
341
|
},
|
|
323
342
|
|
|
324
343
|
async insertBundle(
|
|
325
344
|
bundle: Bundle,
|
|
326
345
|
context?: HotUpdaterContext<TContext>,
|
|
327
346
|
): Promise<void> {
|
|
328
|
-
await
|
|
329
|
-
|
|
347
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
348
|
+
await plugin.appendBundle(bundle, context);
|
|
349
|
+
await plugin.commitBundle(context);
|
|
350
|
+
});
|
|
330
351
|
},
|
|
331
352
|
|
|
332
353
|
async updateBundleById(
|
|
@@ -334,26 +355,30 @@ export function createPluginDatabaseCore<TContext = unknown>(
|
|
|
334
355
|
newBundle: Partial<Bundle>,
|
|
335
356
|
context?: HotUpdaterContext<TContext>,
|
|
336
357
|
): Promise<void> {
|
|
337
|
-
await
|
|
338
|
-
|
|
358
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
359
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
360
|
+
await plugin.commitBundle(context);
|
|
361
|
+
});
|
|
339
362
|
},
|
|
340
363
|
|
|
341
364
|
async deleteBundleById(
|
|
342
365
|
bundleId: string,
|
|
343
366
|
context?: HotUpdaterContext<TContext>,
|
|
344
367
|
): Promise<void> {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
368
|
+
await runWithMutationPlugin(async (plugin) => {
|
|
369
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
370
|
+
if (!bundle) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await plugin.deleteBundle(bundle, context);
|
|
374
|
+
await plugin.commitBundle(context);
|
|
375
|
+
});
|
|
351
376
|
},
|
|
352
377
|
};
|
|
353
378
|
|
|
354
379
|
return {
|
|
355
380
|
api,
|
|
356
|
-
adapterName:
|
|
381
|
+
adapterName: getPlugin().name,
|
|
357
382
|
createMigrator: () => {
|
|
358
383
|
throw new Error(
|
|
359
384
|
"createMigrator is only available for Kysely/Prisma/Drizzle database adapters.",
|
|
@@ -230,7 +230,10 @@ describe("plugin update check benchmark", () => {
|
|
|
230
230
|
};
|
|
231
231
|
|
|
232
232
|
const plugin = createBenchPlugin(bundles);
|
|
233
|
-
const currentApi = createPluginDatabaseCore(
|
|
233
|
+
const currentApi = createPluginDatabaseCore(
|
|
234
|
+
() => plugin,
|
|
235
|
+
async () => null,
|
|
236
|
+
).api;
|
|
234
237
|
|
|
235
238
|
bench(
|
|
236
239
|
"pluginCore legacy full fetch",
|
package/src/db/types.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
StoragePlugin,
|
|
12
12
|
} from "@hot-updater/plugin-core";
|
|
13
13
|
import type { FumaDBAdapter } from "fumadb/adapters";
|
|
14
|
-
import type {
|
|
14
|
+
import type { PaginatedResult } from "../types";
|
|
15
15
|
|
|
16
16
|
export type DatabasePluginFactory<TContext = unknown> =
|
|
17
17
|
() => DatabasePlugin<TContext>;
|
|
@@ -62,7 +62,7 @@ export interface DatabaseAPI<TContext = unknown> {
|
|
|
62
62
|
getBundles(
|
|
63
63
|
options: DatabaseBundleQueryOptions,
|
|
64
64
|
context?: HotUpdaterContext<TContext>,
|
|
65
|
-
): Promise<
|
|
65
|
+
): Promise<PaginatedResult>;
|
|
66
66
|
insertBundle(
|
|
67
67
|
bundle: Bundle,
|
|
68
68
|
context?: HotUpdaterContext<TContext>,
|
|
@@ -2,13 +2,13 @@ import { PGlite } from "@electric-sql/pglite";
|
|
|
2
2
|
import type { Bundle } from "@hot-updater/core";
|
|
3
3
|
import { NIL_UUID } from "@hot-updater/core";
|
|
4
4
|
import { createBlobDatabasePlugin } from "@hot-updater/plugin-core";
|
|
5
|
-
import { standaloneRepository } from "@hot-updater/standalone";
|
|
6
5
|
import { Kysely } from "kysely";
|
|
7
6
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
8
7
|
import { HttpResponse, http } from "msw";
|
|
9
8
|
import { setupServer } from "msw/node";
|
|
10
9
|
import { uuidv7 } from "uuidv7";
|
|
11
10
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
11
|
+
import { standaloneRepository } from "../../../plugins/standalone/src";
|
|
12
12
|
import { kyselyAdapter } from "./adapters/kysely";
|
|
13
13
|
import { createHotUpdater } from "./db";
|
|
14
14
|
|
|
@@ -66,6 +66,9 @@ beforeAll(async () => {
|
|
|
66
66
|
http.get(`${baseUrl}/hot-updater/api/bundles`, ({ request }) =>
|
|
67
67
|
handleRequest(request),
|
|
68
68
|
),
|
|
69
|
+
http.get(`${baseUrl}/hot-updater/api/bundles/channels`, ({ request }) =>
|
|
70
|
+
handleRequest(request),
|
|
71
|
+
),
|
|
69
72
|
http.get(`${baseUrl}/hot-updater/api/bundles/:id`, ({ request }) =>
|
|
70
73
|
handleRequest(request),
|
|
71
74
|
),
|
|
@@ -245,6 +248,26 @@ describe("Handler <-> Standalone Repository Integration", () => {
|
|
|
245
248
|
expect(prodResult.data).toHaveLength(2);
|
|
246
249
|
});
|
|
247
250
|
|
|
251
|
+
it("Real integration: getChannels → handler GET /bundles/channels", async () => {
|
|
252
|
+
await api.insertBundle(
|
|
253
|
+
createTestBundle({ id: uuidv7(), channel: "production" }),
|
|
254
|
+
);
|
|
255
|
+
await api.insertBundle(createTestBundle({ id: uuidv7(), channel: "beta" }));
|
|
256
|
+
await api.insertBundle(
|
|
257
|
+
createTestBundle({ id: uuidv7(), channel: "production" }),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const repo = standaloneRepository({
|
|
261
|
+
baseUrl: `${baseUrl}/hot-updater`,
|
|
262
|
+
})();
|
|
263
|
+
|
|
264
|
+
const channels = await repo.getChannels();
|
|
265
|
+
|
|
266
|
+
expect(channels).toHaveLength(2);
|
|
267
|
+
expect(channels).toContain("production");
|
|
268
|
+
expect(channels).toContain("beta");
|
|
269
|
+
});
|
|
270
|
+
|
|
248
271
|
it("Full E2E: create → retrieve → update → delete via standalone", async () => {
|
|
249
272
|
const repo = standaloneRepository({
|
|
250
273
|
baseUrl: `${baseUrl}/hot-updater`,
|
package/src/handler.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NIL_UUID } from "@hot-updater/core";
|
|
1
|
+
import { type Bundle, NIL_UUID } from "@hot-updater/core";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { createHandler, type HandlerAPI } from "./handler";
|
|
4
4
|
|
|
@@ -10,22 +10,41 @@ type TestContext = {
|
|
|
10
10
|
env: TestEnv;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
const testBundle: Bundle = {
|
|
14
|
+
id: "bundle-1",
|
|
15
|
+
platform: "ios",
|
|
16
|
+
shouldForceUpdate: false,
|
|
17
|
+
enabled: true,
|
|
18
|
+
fileHash: "hash123",
|
|
19
|
+
gitCommitHash: null,
|
|
20
|
+
message: "Test bundle",
|
|
21
|
+
channel: "production",
|
|
22
|
+
storageUri: "s3://test-bucket/bundles/bundle-1.zip",
|
|
23
|
+
targetAppVersion: "1.0.0",
|
|
24
|
+
fingerprintHash: null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const createApi = () =>
|
|
28
|
+
({
|
|
29
|
+
getAppUpdateInfo: vi
|
|
30
|
+
.fn<HandlerAPI<TestContext>["getAppUpdateInfo"]>()
|
|
31
|
+
.mockResolvedValue({
|
|
32
|
+
fileHash: null,
|
|
33
|
+
fileUrl: null,
|
|
34
|
+
id: NIL_UUID,
|
|
35
|
+
message: null,
|
|
36
|
+
shouldForceUpdate: true,
|
|
37
|
+
status: "ROLLBACK",
|
|
38
|
+
}),
|
|
39
|
+
getBundleById: vi.fn<HandlerAPI<TestContext>["getBundleById"]>(),
|
|
40
|
+
getBundles: vi.fn<HandlerAPI<TestContext>["getBundles"]>(),
|
|
41
|
+
getChannels: vi
|
|
42
|
+
.fn<HandlerAPI<TestContext>["getChannels"]>()
|
|
43
|
+
.mockResolvedValue(["production"]),
|
|
44
|
+
insertBundle: vi.fn<HandlerAPI<TestContext>["insertBundle"]>(),
|
|
45
|
+
updateBundleById: vi.fn<HandlerAPI<TestContext>["updateBundleById"]>(),
|
|
46
|
+
deleteBundleById: vi.fn<HandlerAPI<TestContext>["deleteBundleById"]>(),
|
|
47
|
+
}) satisfies HandlerAPI<TestContext>;
|
|
29
48
|
|
|
30
49
|
describe("createHandler", () => {
|
|
31
50
|
it("supports the app-version route without a cohort segment", async () => {
|
|
@@ -134,9 +153,60 @@ describe("createHandler", () => {
|
|
|
134
153
|
);
|
|
135
154
|
|
|
136
155
|
expect(channelsResponse.status).toBe(200);
|
|
156
|
+
await expect(channelsResponse.json()).resolves.toEqual({
|
|
157
|
+
data: {
|
|
158
|
+
channels: ["production"],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
expect(api.getChannels).toHaveBeenCalledWith(undefined);
|
|
137
162
|
expect(updateResponse.status).toBe(404);
|
|
138
163
|
});
|
|
139
164
|
|
|
165
|
+
it("returns paginated bundle results in the response body", async () => {
|
|
166
|
+
const api = createApi();
|
|
167
|
+
api.getBundles.mockResolvedValue({
|
|
168
|
+
data: [testBundle],
|
|
169
|
+
pagination: {
|
|
170
|
+
total: 51,
|
|
171
|
+
hasNextPage: true,
|
|
172
|
+
hasPreviousPage: true,
|
|
173
|
+
currentPage: 6,
|
|
174
|
+
totalPages: 26,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const handler = createHandler(api, { basePath: "/hot-updater" });
|
|
178
|
+
|
|
179
|
+
const response = await handler(
|
|
180
|
+
new Request(
|
|
181
|
+
"http://localhost/hot-updater/api/bundles?channel=production&platform=ios&limit=2&offset=10",
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(response.status).toBe(200);
|
|
186
|
+
expect(response.headers.get("X-Total-Count")).toBeNull();
|
|
187
|
+
await expect(response.json()).resolves.toEqual({
|
|
188
|
+
data: [testBundle],
|
|
189
|
+
pagination: {
|
|
190
|
+
total: 51,
|
|
191
|
+
hasNextPage: true,
|
|
192
|
+
hasPreviousPage: true,
|
|
193
|
+
currentPage: 6,
|
|
194
|
+
totalPages: 26,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
expect(api.getBundles).toHaveBeenCalledWith(
|
|
198
|
+
{
|
|
199
|
+
where: {
|
|
200
|
+
channel: "production",
|
|
201
|
+
platform: "ios",
|
|
202
|
+
},
|
|
203
|
+
limit: 2,
|
|
204
|
+
offset: 10,
|
|
205
|
+
},
|
|
206
|
+
undefined,
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
140
210
|
it("returns 400 when the platform route parameter is invalid", async () => {
|
|
141
211
|
const api = createApi();
|
|
142
212
|
const handler = createHandler(api, { basePath: "/hot-updater" });
|
package/src/handler.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
HotUpdaterContext,
|
|
11
11
|
} from "@hot-updater/plugin-core";
|
|
12
12
|
import { addRoute, createRouter, findRoute } from "./internalRouter";
|
|
13
|
-
import type {
|
|
13
|
+
import type { ChannelsResponse, PaginatedResult } from "./types";
|
|
14
14
|
|
|
15
15
|
declare const __VERSION__: string;
|
|
16
16
|
|
|
@@ -27,7 +27,7 @@ export interface HandlerAPI<TContext = unknown> {
|
|
|
27
27
|
getBundles: (
|
|
28
28
|
options: DatabaseBundleQueryOptions,
|
|
29
29
|
context?: HotUpdaterContext<TContext>,
|
|
30
|
-
) => Promise<
|
|
30
|
+
) => Promise<PaginatedResult>;
|
|
31
31
|
insertBundle: (
|
|
32
32
|
bundle: Bundle,
|
|
33
33
|
context?: HotUpdaterContext<TContext>,
|
|
@@ -262,7 +262,7 @@ const handleGetBundles: RouteHandler = async (
|
|
|
262
262
|
context,
|
|
263
263
|
);
|
|
264
264
|
|
|
265
|
-
return new Response(JSON.stringify(result
|
|
265
|
+
return new Response(JSON.stringify(result), {
|
|
266
266
|
status: 200,
|
|
267
267
|
headers: { "Content-Type": "application/json" },
|
|
268
268
|
});
|
|
@@ -327,8 +327,13 @@ const handleGetChannels: RouteHandler = async (
|
|
|
327
327
|
context,
|
|
328
328
|
) => {
|
|
329
329
|
const channels = await api.getChannels(context);
|
|
330
|
+
const response: ChannelsResponse = {
|
|
331
|
+
data: {
|
|
332
|
+
channels,
|
|
333
|
+
},
|
|
334
|
+
};
|
|
330
335
|
|
|
331
|
-
return new Response(JSON.stringify(
|
|
336
|
+
return new Response(JSON.stringify(response), {
|
|
332
337
|
status: 200,
|
|
333
338
|
headers: { "Content-Type": "application/json" },
|
|
334
339
|
});
|
package/src/runtime.spec.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
RequestEnvContext,
|
|
6
6
|
StoragePlugin,
|
|
7
7
|
} from "@hot-updater/plugin-core";
|
|
8
|
+
import { createDatabasePlugin } from "@hot-updater/plugin-core";
|
|
8
9
|
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
9
10
|
import { createHotUpdater } from "./runtime";
|
|
10
11
|
|
|
@@ -274,4 +275,157 @@ describe("runtime createHotUpdater", () => {
|
|
|
274
275
|
expect(response.status).toBe(200);
|
|
275
276
|
expect(getBundles).toHaveBeenCalledWith(expect.any(Object), undefined);
|
|
276
277
|
});
|
|
278
|
+
|
|
279
|
+
it("clears pending plugin changes after a failed mutation commit", async () => {
|
|
280
|
+
const committedBundles = new Map<string, Bundle>();
|
|
281
|
+
let commitAttempt = 0;
|
|
282
|
+
|
|
283
|
+
const database = createDatabasePlugin({
|
|
284
|
+
name: "failingPlugin",
|
|
285
|
+
factory: () => ({
|
|
286
|
+
async getBundleById(bundleId) {
|
|
287
|
+
return committedBundles.get(bundleId) ?? null;
|
|
288
|
+
},
|
|
289
|
+
async getBundles() {
|
|
290
|
+
return {
|
|
291
|
+
data: Array.from(committedBundles.values()),
|
|
292
|
+
pagination: {
|
|
293
|
+
hasNextPage: false,
|
|
294
|
+
hasPreviousPage: false,
|
|
295
|
+
currentPage: 1,
|
|
296
|
+
totalPages: 1,
|
|
297
|
+
total: committedBundles.size,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
async getChannels() {
|
|
302
|
+
return [];
|
|
303
|
+
},
|
|
304
|
+
async commitBundle({ changedSets }) {
|
|
305
|
+
commitAttempt += 1;
|
|
306
|
+
|
|
307
|
+
if (commitAttempt === 1) {
|
|
308
|
+
throw new Error("commit failed");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const change of changedSets) {
|
|
312
|
+
if (change.operation === "delete") {
|
|
313
|
+
committedBundles.delete(change.data.id);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
committedBundles.set(change.data.id, change.data);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
321
|
+
})({});
|
|
322
|
+
|
|
323
|
+
const hotUpdater = createHotUpdater({
|
|
324
|
+
database,
|
|
325
|
+
basePath: "/api/check-update",
|
|
326
|
+
routes: {
|
|
327
|
+
updateCheck: false,
|
|
328
|
+
bundles: false,
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const failedBundle: Bundle = {
|
|
333
|
+
...bundle,
|
|
334
|
+
id: "00000000-0000-0000-0000-000000000010",
|
|
335
|
+
message: "failed bundle",
|
|
336
|
+
};
|
|
337
|
+
const succeedingBundle: Bundle = {
|
|
338
|
+
...bundle,
|
|
339
|
+
id: "00000000-0000-0000-0000-000000000011",
|
|
340
|
+
message: "succeeding bundle",
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
await expect(hotUpdater.insertBundle(failedBundle)).rejects.toThrow(
|
|
344
|
+
"commit failed",
|
|
345
|
+
);
|
|
346
|
+
await hotUpdater.insertBundle(succeedingBundle);
|
|
347
|
+
|
|
348
|
+
expect(await hotUpdater.getBundleById(failedBundle.id)).toBeNull();
|
|
349
|
+
await expect(
|
|
350
|
+
hotUpdater.getBundleById(succeedingBundle.id),
|
|
351
|
+
).resolves.toEqual(succeedingBundle);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("isolates pending mutation state between overlapping writes", async () => {
|
|
355
|
+
const committedBundleIds: string[][] = [];
|
|
356
|
+
const onUnmount = vi.fn(async () => undefined);
|
|
357
|
+
let releaseFirstCommit!: () => void;
|
|
358
|
+
let notifyFirstCommitStarted!: () => void;
|
|
359
|
+
const firstCommitStarted = new Promise<void>((resolve) => {
|
|
360
|
+
notifyFirstCommitStarted = resolve;
|
|
361
|
+
});
|
|
362
|
+
const firstCommitGate = new Promise<void>((resolve) => {
|
|
363
|
+
releaseFirstCommit = resolve;
|
|
364
|
+
});
|
|
365
|
+
let commitCount = 0;
|
|
366
|
+
|
|
367
|
+
const database = createDatabasePlugin({
|
|
368
|
+
name: "isolatedPlugin",
|
|
369
|
+
factory: () => ({
|
|
370
|
+
async getBundleById() {
|
|
371
|
+
return null;
|
|
372
|
+
},
|
|
373
|
+
async getBundles() {
|
|
374
|
+
return {
|
|
375
|
+
data: [],
|
|
376
|
+
pagination: {
|
|
377
|
+
hasNextPage: false,
|
|
378
|
+
hasPreviousPage: false,
|
|
379
|
+
currentPage: 1,
|
|
380
|
+
totalPages: 1,
|
|
381
|
+
total: 0,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
async getChannels() {
|
|
386
|
+
return [];
|
|
387
|
+
},
|
|
388
|
+
onUnmount,
|
|
389
|
+
async commitBundle({ changedSets }) {
|
|
390
|
+
commitCount += 1;
|
|
391
|
+
committedBundleIds.push(changedSets.map((change) => change.data.id));
|
|
392
|
+
|
|
393
|
+
if (commitCount === 1) {
|
|
394
|
+
notifyFirstCommitStarted();
|
|
395
|
+
await firstCommitGate;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
}),
|
|
399
|
+
})({});
|
|
400
|
+
|
|
401
|
+
const hotUpdater = createHotUpdater({
|
|
402
|
+
database,
|
|
403
|
+
basePath: "/api/check-update",
|
|
404
|
+
routes: {
|
|
405
|
+
updateCheck: false,
|
|
406
|
+
bundles: false,
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const firstBundleId = "00000000-0000-0000-0000-000000000020";
|
|
411
|
+
const secondBundleId = "00000000-0000-0000-0000-000000000021";
|
|
412
|
+
|
|
413
|
+
const firstInsert = hotUpdater.insertBundle({
|
|
414
|
+
...bundle,
|
|
415
|
+
id: firstBundleId,
|
|
416
|
+
message: "first bundle",
|
|
417
|
+
});
|
|
418
|
+
await firstCommitStarted;
|
|
419
|
+
|
|
420
|
+
const secondInsert = hotUpdater.insertBundle({
|
|
421
|
+
...bundle,
|
|
422
|
+
id: secondBundleId,
|
|
423
|
+
message: "second bundle",
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
releaseFirstCommit();
|
|
427
|
+
await Promise.all([firstInsert, secondInsert]);
|
|
428
|
+
|
|
429
|
+
expect(committedBundleIds).toEqual([[firstBundleId], [secondBundleId]]);
|
|
430
|
+
});
|
|
277
431
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface CreateHotUpdaterOptions<TContext = unknown> {
|
|
|
34
34
|
export function createHotUpdater<TContext = unknown>(
|
|
35
35
|
options: CreateHotUpdaterOptions<TContext>,
|
|
36
36
|
): HotUpdaterAPI<TContext> {
|
|
37
|
+
const database = options.database;
|
|
37
38
|
const basePath = normalizeBasePath(options.basePath ?? "/api");
|
|
38
39
|
const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map(
|
|
39
40
|
(plugin) => (typeof plugin === "function" ? plugin() : plugin),
|
|
@@ -70,21 +71,21 @@ export function createHotUpdater<TContext = unknown>(
|
|
|
70
71
|
return fileUrl;
|
|
71
72
|
};
|
|
72
73
|
|
|
73
|
-
if (
|
|
74
|
-
!isDatabasePluginFactory(options.database) &&
|
|
75
|
-
!isDatabasePlugin(options.database)
|
|
76
|
-
) {
|
|
74
|
+
if (!isDatabasePluginFactory(database) && !isDatabasePlugin(database)) {
|
|
77
75
|
throw new Error(
|
|
78
76
|
"@hot-updater/server/runtime only supports database plugins.",
|
|
79
77
|
);
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
const plugin = isDatabasePluginFactory(
|
|
83
|
-
? options.database()
|
|
84
|
-
: options.database;
|
|
80
|
+
const plugin = isDatabasePluginFactory(database) ? database() : database;
|
|
85
81
|
const core = createPluginDatabaseCore<TContext>(
|
|
86
|
-
plugin,
|
|
82
|
+
() => plugin,
|
|
87
83
|
resolveStoragePluginUrl,
|
|
84
|
+
isDatabasePluginFactory(database)
|
|
85
|
+
? {
|
|
86
|
+
createMutationPlugin: () => database(),
|
|
87
|
+
}
|
|
88
|
+
: undefined,
|
|
88
89
|
);
|
|
89
90
|
|
|
90
91
|
const api = {
|
package/src/types/index.ts
CHANGED
|
@@ -16,7 +16,16 @@ export interface PaginationOptions {
|
|
|
16
16
|
offset: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface
|
|
20
|
-
data:
|
|
19
|
+
export interface DataResponse<TData> {
|
|
20
|
+
data: TData;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Paginated<TData> extends DataResponse<TData> {
|
|
21
24
|
pagination: PaginationInfo;
|
|
22
25
|
}
|
|
26
|
+
|
|
27
|
+
export type PaginatedResult = Paginated<Bundle[]>;
|
|
28
|
+
|
|
29
|
+
export type ChannelsResponse = DataResponse<{
|
|
30
|
+
channels: string[];
|
|
31
|
+
}>;
|