@hot-updater/server 0.28.0 → 0.29.1
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/adapters/drizzle.cjs +7 -7
- package/dist/adapters/drizzle.mjs +2 -0
- package/dist/adapters/kysely.cjs +7 -7
- package/dist/adapters/kysely.mjs +2 -0
- package/dist/adapters/mongodb.cjs +7 -7
- package/dist/adapters/mongodb.mjs +2 -0
- package/dist/adapters/prisma.cjs +7 -7
- package/dist/adapters/prisma.mjs +2 -0
- package/dist/calculatePagination.cjs +1 -3
- package/dist/{calculatePagination.js → calculatePagination.mjs} +1 -2
- package/dist/db/index.cjs +24 -15
- package/dist/db/index.d.cts +12 -9
- package/dist/db/index.d.mts +30 -0
- package/dist/db/index.mjs +45 -0
- package/dist/db/ormCore.cjs +247 -138
- package/dist/db/ormCore.d.cts +35 -17
- package/dist/db/ormCore.d.mts +44 -0
- package/dist/db/ormCore.mjs +386 -0
- package/dist/db/pluginCore.cjs +145 -40
- package/dist/db/pluginCore.mjs +176 -0
- package/dist/db/types.cjs +1 -3
- package/dist/db/types.d.cts +14 -21
- package/dist/db/types.d.mts +24 -0
- package/dist/db/{types.js → types.mjs} +1 -2
- package/dist/handler.cjs +117 -48
- package/dist/handler.d.cts +28 -18
- package/dist/handler.d.mts +47 -0
- package/dist/handler.mjs +217 -0
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +4 -0
- package/dist/internalRouter.cjs +54 -0
- package/dist/internalRouter.mjs +52 -0
- package/dist/node.cjs +2 -3
- package/dist/node.d.cts +0 -1
- package/dist/{node.d.ts → node.d.mts} +1 -2
- package/dist/{node.js → node.mjs} +1 -2
- package/dist/route.cjs +7 -0
- package/dist/route.mjs +7 -0
- package/dist/runtime.cjs +42 -0
- package/dist/runtime.d.cts +21 -0
- package/dist/runtime.d.mts +21 -0
- package/dist/runtime.mjs +40 -0
- package/dist/schema/v0_21_0.cjs +1 -5
- package/dist/schema/{v0_21_0.js → v0_21_0.mjs} +1 -3
- package/dist/schema/v0_29_0.cjs +24 -0
- package/dist/schema/v0_29_0.mjs +24 -0
- package/dist/types/{index.d.ts → index.d.mts} +1 -1
- package/package.json +18 -18
- package/src/db/index.spec.ts +64 -29
- package/src/db/index.ts +55 -35
- package/src/db/ormCore.ts +438 -210
- package/src/db/ormUpdateCheck.bench.ts +261 -0
- package/src/db/pluginCore.ts +298 -49
- package/src/db/pluginUpdateCheck.bench.ts +250 -0
- package/src/db/types.ts +52 -27
- package/src/{handler-standalone-integration.spec.ts → handler-standalone.integration.spec.ts} +106 -0
- package/src/handler.spec.ts +156 -0
- package/src/handler.ts +296 -77
- package/src/internalRouter.ts +104 -0
- package/src/route.ts +7 -0
- package/src/runtime.spec.ts +277 -0
- package/src/runtime.ts +121 -0
- package/src/schema/v0_29_0.ts +26 -0
- package/dist/_virtual/rolldown_runtime.cjs +0 -25
- package/dist/adapters/drizzle.js +0 -3
- package/dist/adapters/kysely.js +0 -3
- package/dist/adapters/mongodb.js +0 -3
- package/dist/adapters/prisma.js +0 -3
- package/dist/db/index.d.ts +0 -27
- package/dist/db/index.js +0 -36
- package/dist/db/ormCore.d.ts +0 -26
- package/dist/db/ormCore.js +0 -273
- package/dist/db/pluginCore.js +0 -69
- package/dist/db/types.d.ts +0 -31
- package/dist/handler.d.ts +0 -37
- package/dist/handler.js +0 -146
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -5
- /package/dist/adapters/{drizzle.d.ts → drizzle.d.mts} +0 -0
- /package/dist/adapters/{kysely.d.ts → kysely.d.mts} +0 -0
- /package/dist/adapters/{mongodb.d.ts → mongodb.d.mts} +0 -0
- /package/dist/adapters/{prisma.d.ts → prisma.d.mts} +0 -0
package/src/handler.ts
CHANGED
|
@@ -3,26 +3,45 @@ import type {
|
|
|
3
3
|
AppVersionGetBundlesArgs,
|
|
4
4
|
Bundle,
|
|
5
5
|
FingerprintGetBundlesArgs,
|
|
6
|
+
Platform,
|
|
6
7
|
} from "@hot-updater/core";
|
|
7
|
-
import {
|
|
8
|
+
import type {
|
|
9
|
+
DatabaseBundleQueryOptions,
|
|
10
|
+
HotUpdaterContext,
|
|
11
|
+
} from "@hot-updater/plugin-core";
|
|
12
|
+
import { addRoute, createRouter, findRoute } from "./internalRouter";
|
|
8
13
|
import type { PaginationInfo } from "./types";
|
|
9
14
|
|
|
10
15
|
declare const __VERSION__: string;
|
|
11
16
|
|
|
12
17
|
// Narrow API surface needed by the handler to avoid circular types
|
|
13
|
-
export interface HandlerAPI {
|
|
18
|
+
export interface HandlerAPI<TContext = unknown> {
|
|
14
19
|
getAppUpdateInfo: (
|
|
15
20
|
args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs,
|
|
21
|
+
context?: HotUpdaterContext<TContext>,
|
|
16
22
|
) => Promise<AppUpdateInfo | null>;
|
|
17
|
-
getBundleById: (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
getBundleById: (
|
|
24
|
+
id: string,
|
|
25
|
+
context?: HotUpdaterContext<TContext>,
|
|
26
|
+
) => Promise<Bundle | null>;
|
|
27
|
+
getBundles: (
|
|
28
|
+
options: DatabaseBundleQueryOptions,
|
|
29
|
+
context?: HotUpdaterContext<TContext>,
|
|
30
|
+
) => Promise<{ data: Bundle[]; pagination: PaginationInfo }>;
|
|
31
|
+
insertBundle: (
|
|
32
|
+
bundle: Bundle,
|
|
33
|
+
context?: HotUpdaterContext<TContext>,
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
updateBundleById: (
|
|
36
|
+
bundleId: string,
|
|
37
|
+
bundle: Partial<Bundle>,
|
|
38
|
+
context?: HotUpdaterContext<TContext>,
|
|
39
|
+
) => Promise<void>;
|
|
40
|
+
deleteBundleById: (
|
|
41
|
+
bundleId: string,
|
|
42
|
+
context?: HotUpdaterContext<TContext>,
|
|
43
|
+
) => Promise<void>;
|
|
44
|
+
getChannels: (context?: HotUpdaterContext<TContext>) => Promise<string[]>;
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
export interface HandlerOptions {
|
|
@@ -31,14 +50,38 @@ export interface HandlerOptions {
|
|
|
31
50
|
* @default "/api"
|
|
32
51
|
*/
|
|
33
52
|
basePath?: string;
|
|
53
|
+
routes?: HandlerRoutes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface HandlerRoutes {
|
|
57
|
+
/**
|
|
58
|
+
* Controls whether update-check routes are mounted.
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
updateCheck?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Controls whether bundle management routes are mounted.
|
|
64
|
+
* This includes `/version` and `/api/bundles*`, which are used by the
|
|
65
|
+
* CLI `standaloneRepository` plugin.
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
bundles?: boolean;
|
|
34
69
|
}
|
|
35
70
|
|
|
36
|
-
type RouteHandler = (
|
|
71
|
+
type RouteHandler<TContext = unknown> = (
|
|
37
72
|
params: Record<string, string>,
|
|
38
73
|
request: Request,
|
|
39
|
-
api: HandlerAPI
|
|
74
|
+
api: HandlerAPI<TContext>,
|
|
75
|
+
context?: HotUpdaterContext<TContext>,
|
|
40
76
|
) => Promise<Response>;
|
|
41
77
|
|
|
78
|
+
class HandlerBadRequestError extends Error {
|
|
79
|
+
constructor(message: string) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "HandlerBadRequestError";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
42
85
|
// Route handlers
|
|
43
86
|
const handleVersion: RouteHandler = async () => {
|
|
44
87
|
return new Response(JSON.stringify({ version: __VERSION__ }), {
|
|
@@ -47,15 +90,88 @@ const handleVersion: RouteHandler = async () => {
|
|
|
47
90
|
});
|
|
48
91
|
};
|
|
49
92
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
const decodeMaybe = (value: string | undefined): string | undefined => {
|
|
94
|
+
if (value === undefined) return undefined;
|
|
95
|
+
try {
|
|
96
|
+
return decodeURIComponent(value);
|
|
97
|
+
} catch {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const isPlatform = (value: string): value is Platform => {
|
|
103
|
+
return value === "ios" || value === "android";
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const requireRouteParam = (
|
|
107
|
+
params: Record<string, string>,
|
|
108
|
+
key: string,
|
|
109
|
+
): string => {
|
|
110
|
+
const value = params[key];
|
|
111
|
+
if (!value) {
|
|
112
|
+
throw new HandlerBadRequestError(`Missing route parameter: ${key}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return value;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const requirePlatformParam = (params: Record<string, string>): Platform => {
|
|
119
|
+
const platform = requireRouteParam(params, "platform");
|
|
120
|
+
|
|
121
|
+
if (!isPlatform(platform)) {
|
|
122
|
+
throw new HandlerBadRequestError(
|
|
123
|
+
`Invalid platform: ${platform}. Expected 'ios' or 'android'.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return platform;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type BundlePatchPayload = Partial<Bundle> & {
|
|
131
|
+
id?: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const requireBundlePatchPayload = (
|
|
135
|
+
payload: unknown,
|
|
136
|
+
bundleId: string,
|
|
137
|
+
): Partial<Bundle> => {
|
|
138
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
139
|
+
throw new HandlerBadRequestError("Invalid bundle payload");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const bundlePatch = payload as BundlePatchPayload;
|
|
143
|
+
if (bundlePatch.id !== undefined && bundlePatch.id !== bundleId) {
|
|
144
|
+
throw new HandlerBadRequestError("Bundle id mismatch");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { id: _ignoredId, ...rest } = bundlePatch;
|
|
148
|
+
return rest;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleFingerprintUpdateWithCohort: RouteHandler = async (
|
|
152
|
+
params,
|
|
153
|
+
_request,
|
|
154
|
+
api,
|
|
155
|
+
context,
|
|
156
|
+
) => {
|
|
157
|
+
const platform = requirePlatformParam(params);
|
|
158
|
+
const fingerprintHash = requireRouteParam(params, "fingerprintHash");
|
|
159
|
+
const channel = requireRouteParam(params, "channel");
|
|
160
|
+
const minBundleId = requireRouteParam(params, "minBundleId");
|
|
161
|
+
const bundleId = requireRouteParam(params, "bundleId");
|
|
162
|
+
|
|
163
|
+
const updateInfo = await api.getAppUpdateInfo(
|
|
164
|
+
{
|
|
165
|
+
_updateStrategy: "fingerprint",
|
|
166
|
+
platform,
|
|
167
|
+
fingerprintHash,
|
|
168
|
+
channel,
|
|
169
|
+
minBundleId,
|
|
170
|
+
bundleId,
|
|
171
|
+
cohort: decodeMaybe(params.cohort),
|
|
172
|
+
},
|
|
173
|
+
context,
|
|
174
|
+
);
|
|
59
175
|
|
|
60
176
|
return new Response(JSON.stringify(updateInfo), {
|
|
61
177
|
status: 200,
|
|
@@ -63,15 +179,30 @@ const handleFingerprintUpdate: RouteHandler = async (params, _request, api) => {
|
|
|
63
179
|
});
|
|
64
180
|
};
|
|
65
181
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
182
|
+
const handleAppVersionUpdateWithCohort: RouteHandler = async (
|
|
183
|
+
params,
|
|
184
|
+
_request,
|
|
185
|
+
api,
|
|
186
|
+
context,
|
|
187
|
+
) => {
|
|
188
|
+
const platform = requirePlatformParam(params);
|
|
189
|
+
const appVersion = requireRouteParam(params, "appVersion");
|
|
190
|
+
const channel = requireRouteParam(params, "channel");
|
|
191
|
+
const minBundleId = requireRouteParam(params, "minBundleId");
|
|
192
|
+
const bundleId = requireRouteParam(params, "bundleId");
|
|
193
|
+
|
|
194
|
+
const updateInfo = await api.getAppUpdateInfo(
|
|
195
|
+
{
|
|
196
|
+
_updateStrategy: "appVersion",
|
|
197
|
+
platform,
|
|
198
|
+
appVersion,
|
|
199
|
+
channel,
|
|
200
|
+
minBundleId,
|
|
201
|
+
bundleId,
|
|
202
|
+
cohort: decodeMaybe(params.cohort),
|
|
203
|
+
},
|
|
204
|
+
context,
|
|
205
|
+
);
|
|
75
206
|
|
|
76
207
|
return new Response(JSON.stringify(updateInfo), {
|
|
77
208
|
status: 200,
|
|
@@ -79,8 +210,14 @@ const handleAppVersionUpdate: RouteHandler = async (params, _request, api) => {
|
|
|
79
210
|
});
|
|
80
211
|
};
|
|
81
212
|
|
|
82
|
-
const handleGetBundle: RouteHandler = async (
|
|
83
|
-
|
|
213
|
+
const handleGetBundle: RouteHandler = async (
|
|
214
|
+
params,
|
|
215
|
+
_request,
|
|
216
|
+
api,
|
|
217
|
+
context,
|
|
218
|
+
) => {
|
|
219
|
+
const bundleId = requireRouteParam(params, "id");
|
|
220
|
+
const bundle = await api.getBundleById(bundleId, context);
|
|
84
221
|
|
|
85
222
|
if (!bundle) {
|
|
86
223
|
return new Response(JSON.stringify({ error: "Bundle not found" }), {
|
|
@@ -95,21 +232,35 @@ const handleGetBundle: RouteHandler = async (params, _request, api) => {
|
|
|
95
232
|
});
|
|
96
233
|
};
|
|
97
234
|
|
|
98
|
-
const handleGetBundles: RouteHandler = async (
|
|
235
|
+
const handleGetBundles: RouteHandler = async (
|
|
236
|
+
_params,
|
|
237
|
+
request,
|
|
238
|
+
api,
|
|
239
|
+
context,
|
|
240
|
+
) => {
|
|
99
241
|
const url = new URL(request.url);
|
|
100
242
|
const channel = url.searchParams.get("channel") ?? undefined;
|
|
101
|
-
const platform = url.searchParams.get("platform")
|
|
243
|
+
const platform = url.searchParams.get("platform");
|
|
102
244
|
const limit = Number(url.searchParams.get("limit")) || 50;
|
|
103
245
|
const offset = Number(url.searchParams.get("offset")) || 0;
|
|
104
246
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
247
|
+
if (platform !== null && !isPlatform(platform)) {
|
|
248
|
+
throw new HandlerBadRequestError(
|
|
249
|
+
`Invalid platform: ${platform}. Expected 'ios' or 'android'.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const result = await api.getBundles(
|
|
254
|
+
{
|
|
255
|
+
where: {
|
|
256
|
+
...(channel && { channel }),
|
|
257
|
+
...(platform && { platform }),
|
|
258
|
+
},
|
|
259
|
+
limit,
|
|
260
|
+
offset,
|
|
109
261
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
262
|
+
context,
|
|
263
|
+
);
|
|
113
264
|
|
|
114
265
|
return new Response(JSON.stringify(result.data), {
|
|
115
266
|
status: 200,
|
|
@@ -117,12 +268,17 @@ const handleGetBundles: RouteHandler = async (_params, request, api) => {
|
|
|
117
268
|
});
|
|
118
269
|
};
|
|
119
270
|
|
|
120
|
-
const handleCreateBundles: RouteHandler = async (
|
|
271
|
+
const handleCreateBundles: RouteHandler = async (
|
|
272
|
+
_params,
|
|
273
|
+
request,
|
|
274
|
+
api,
|
|
275
|
+
context,
|
|
276
|
+
) => {
|
|
121
277
|
const body = await request.json();
|
|
122
278
|
const bundles = Array.isArray(body) ? body : [body];
|
|
123
279
|
|
|
124
280
|
for (const bundle of bundles) {
|
|
125
|
-
await api.insertBundle(bundle as Bundle);
|
|
281
|
+
await api.insertBundle(bundle as Bundle, context);
|
|
126
282
|
}
|
|
127
283
|
|
|
128
284
|
return new Response(JSON.stringify({ success: true }), {
|
|
@@ -131,8 +287,32 @@ const handleCreateBundles: RouteHandler = async (_params, request, api) => {
|
|
|
131
287
|
});
|
|
132
288
|
};
|
|
133
289
|
|
|
134
|
-
const
|
|
135
|
-
|
|
290
|
+
const handleUpdateBundle: RouteHandler = async (
|
|
291
|
+
params,
|
|
292
|
+
request,
|
|
293
|
+
api,
|
|
294
|
+
context,
|
|
295
|
+
) => {
|
|
296
|
+
const bundleId = requireRouteParam(params, "id");
|
|
297
|
+
const body = await request.json();
|
|
298
|
+
const payload = Array.isArray(body) ? body[0] : body;
|
|
299
|
+
const bundlePatch = requireBundlePatchPayload(payload, bundleId);
|
|
300
|
+
await api.updateBundleById(bundleId, bundlePatch, context);
|
|
301
|
+
|
|
302
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
303
|
+
status: 200,
|
|
304
|
+
headers: { "Content-Type": "application/json" },
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const handleDeleteBundle: RouteHandler = async (
|
|
309
|
+
params,
|
|
310
|
+
_request,
|
|
311
|
+
api,
|
|
312
|
+
context,
|
|
313
|
+
) => {
|
|
314
|
+
const bundleId = requireRouteParam(params, "id");
|
|
315
|
+
await api.deleteBundleById(bundleId, context);
|
|
136
316
|
|
|
137
317
|
return new Response(JSON.stringify({ success: true }), {
|
|
138
318
|
status: 200,
|
|
@@ -140,8 +320,13 @@ const handleDeleteBundle: RouteHandler = async (params, _request, api) => {
|
|
|
140
320
|
});
|
|
141
321
|
};
|
|
142
322
|
|
|
143
|
-
const handleGetChannels: RouteHandler = async (
|
|
144
|
-
|
|
323
|
+
const handleGetChannels: RouteHandler = async (
|
|
324
|
+
_params,
|
|
325
|
+
_request,
|
|
326
|
+
api,
|
|
327
|
+
context,
|
|
328
|
+
) => {
|
|
329
|
+
const channels = await api.getChannels(context);
|
|
145
330
|
|
|
146
331
|
return new Response(JSON.stringify({ channels }), {
|
|
147
332
|
status: 200,
|
|
@@ -150,52 +335,79 @@ const handleGetChannels: RouteHandler = async (_params, _request, api) => {
|
|
|
150
335
|
};
|
|
151
336
|
|
|
152
337
|
// Route handlers map
|
|
153
|
-
const routes: Record<string, RouteHandler
|
|
338
|
+
const routes: Record<string, RouteHandler<any>> = {
|
|
154
339
|
version: handleVersion,
|
|
155
|
-
|
|
156
|
-
|
|
340
|
+
fingerprintUpdateWithCohort: handleFingerprintUpdateWithCohort,
|
|
341
|
+
appVersionUpdateWithCohort: handleAppVersionUpdateWithCohort,
|
|
157
342
|
getBundle: handleGetBundle,
|
|
158
343
|
getBundles: handleGetBundles,
|
|
159
344
|
createBundles: handleCreateBundles,
|
|
345
|
+
updateBundle: handleUpdateBundle,
|
|
160
346
|
deleteBundle: handleDeleteBundle,
|
|
161
347
|
getChannels: handleGetChannels,
|
|
162
348
|
};
|
|
163
349
|
|
|
164
350
|
/**
|
|
165
351
|
* Creates a Web Standard Request handler for Hot Updater API
|
|
166
|
-
* This handler is framework-agnostic and works with any
|
|
167
|
-
*
|
|
352
|
+
* This handler is framework-agnostic and works with any runtime that
|
|
353
|
+
* supports standard Request/Response objects.
|
|
168
354
|
*/
|
|
169
|
-
export function createHandler(
|
|
170
|
-
api: HandlerAPI
|
|
355
|
+
export function createHandler<TContext = unknown>(
|
|
356
|
+
api: HandlerAPI<TContext>,
|
|
171
357
|
options: HandlerOptions = {},
|
|
172
|
-
): (
|
|
358
|
+
): (
|
|
359
|
+
request: Request,
|
|
360
|
+
context?: HotUpdaterContext<TContext>,
|
|
361
|
+
) => Promise<Response> {
|
|
173
362
|
const basePath = options.basePath ?? "/api";
|
|
363
|
+
const updateCheckEnabled = options.routes?.updateCheck ?? true;
|
|
364
|
+
const bundlesEnabled = options.routes?.bundles ?? true;
|
|
174
365
|
|
|
175
366
|
// Create and configure router
|
|
176
367
|
const router = createRouter();
|
|
177
368
|
|
|
178
369
|
// Register routes
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
370
|
+
if (updateCheckEnabled) {
|
|
371
|
+
addRoute(
|
|
372
|
+
router,
|
|
373
|
+
"GET",
|
|
374
|
+
"/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId",
|
|
375
|
+
"fingerprintUpdateWithCohort",
|
|
376
|
+
);
|
|
377
|
+
addRoute(
|
|
378
|
+
router,
|
|
379
|
+
"GET",
|
|
380
|
+
"/fingerprint/:platform/:fingerprintHash/:channel/:minBundleId/:bundleId/:cohort",
|
|
381
|
+
"fingerprintUpdateWithCohort",
|
|
382
|
+
);
|
|
383
|
+
addRoute(
|
|
384
|
+
router,
|
|
385
|
+
"GET",
|
|
386
|
+
"/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId",
|
|
387
|
+
"appVersionUpdateWithCohort",
|
|
388
|
+
);
|
|
389
|
+
addRoute(
|
|
390
|
+
router,
|
|
391
|
+
"GET",
|
|
392
|
+
"/app-version/:platform/:appVersion/:channel/:minBundleId/:bundleId/:cohort",
|
|
393
|
+
"appVersionUpdateWithCohort",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (bundlesEnabled) {
|
|
398
|
+
addRoute(router, "GET", "/version", "version");
|
|
399
|
+
addRoute(router, "GET", "/api/bundles/channels", "getChannels");
|
|
400
|
+
addRoute(router, "GET", "/api/bundles/:id", "getBundle");
|
|
401
|
+
addRoute(router, "GET", "/api/bundles", "getBundles");
|
|
402
|
+
addRoute(router, "POST", "/api/bundles", "createBundles");
|
|
403
|
+
addRoute(router, "PATCH", "/api/bundles/:id", "updateBundle");
|
|
404
|
+
addRoute(router, "DELETE", "/api/bundles/:id", "deleteBundle");
|
|
405
|
+
}
|
|
197
406
|
|
|
198
|
-
return async (
|
|
407
|
+
return async (
|
|
408
|
+
request: Request,
|
|
409
|
+
context?: HotUpdaterContext<TContext>,
|
|
410
|
+
): Promise<Response> => {
|
|
199
411
|
try {
|
|
200
412
|
const url = new URL(request.url);
|
|
201
413
|
const path = url.pathname;
|
|
@@ -217,7 +429,7 @@ export function createHandler(
|
|
|
217
429
|
}
|
|
218
430
|
|
|
219
431
|
// Get handler and execute
|
|
220
|
-
const handler = routes[match.data as string]
|
|
432
|
+
const handler = routes[match.data as string] as RouteHandler<TContext>;
|
|
221
433
|
if (!handler) {
|
|
222
434
|
return new Response(JSON.stringify({ error: "Handler not found" }), {
|
|
223
435
|
status: 500,
|
|
@@ -225,8 +437,15 @@ export function createHandler(
|
|
|
225
437
|
});
|
|
226
438
|
}
|
|
227
439
|
|
|
228
|
-
return await handler(match.params || {}, request, api);
|
|
440
|
+
return await handler(match.params || {}, request, api, context);
|
|
229
441
|
} catch (error) {
|
|
442
|
+
if (error instanceof HandlerBadRequestError) {
|
|
443
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
444
|
+
status: 400,
|
|
445
|
+
headers: { "Content-Type": "application/json" },
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
230
449
|
console.error("Hot Updater handler error:", error);
|
|
231
450
|
return new Response(
|
|
232
451
|
JSON.stringify({
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
interface RouteRecord<T> {
|
|
2
|
+
data: T;
|
|
3
|
+
method: string;
|
|
4
|
+
paramNames: string[];
|
|
5
|
+
segments: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface RouteMatch<T> {
|
|
9
|
+
data: T;
|
|
10
|
+
params: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Router<T> {
|
|
14
|
+
routes: RouteRecord<T>[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalizePath = (path: string) => {
|
|
18
|
+
if (!path) {
|
|
19
|
+
return "/";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (path === "/") {
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const withLeadingSlash = path.startsWith("/") ? path : `/${path}`;
|
|
27
|
+
return withLeadingSlash.endsWith("/")
|
|
28
|
+
? withLeadingSlash.slice(0, -1)
|
|
29
|
+
: withLeadingSlash;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const toSegments = (path: string) => {
|
|
33
|
+
const normalized = normalizePath(path);
|
|
34
|
+
return normalized === "/" ? [] : normalized.slice(1).split("/");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function createRouter<T>(): Router<T> {
|
|
38
|
+
return { routes: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function addRoute<T>(
|
|
42
|
+
router: Router<T>,
|
|
43
|
+
method: string,
|
|
44
|
+
path: string,
|
|
45
|
+
data: T,
|
|
46
|
+
) {
|
|
47
|
+
const segments = toSegments(path);
|
|
48
|
+
const paramNames = segments
|
|
49
|
+
.filter((segment) => segment.startsWith(":"))
|
|
50
|
+
.map((segment) => segment.slice(1));
|
|
51
|
+
|
|
52
|
+
router.routes.push({
|
|
53
|
+
data,
|
|
54
|
+
method: method.toUpperCase(),
|
|
55
|
+
paramNames,
|
|
56
|
+
segments,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function findRoute<T>(
|
|
61
|
+
router: Router<T>,
|
|
62
|
+
method: string,
|
|
63
|
+
path: string,
|
|
64
|
+
): RouteMatch<T> | undefined {
|
|
65
|
+
const normalizedMethod = method.toUpperCase();
|
|
66
|
+
const pathSegments = toSegments(path);
|
|
67
|
+
|
|
68
|
+
for (const route of router.routes) {
|
|
69
|
+
if (route.method !== normalizedMethod) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (route.segments.length !== pathSegments.length) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const params: Record<string, string> = {};
|
|
78
|
+
let matched = true;
|
|
79
|
+
|
|
80
|
+
for (let index = 0; index < route.segments.length; index += 1) {
|
|
81
|
+
const routeSegment = route.segments[index];
|
|
82
|
+
const pathSegment = pathSegments[index];
|
|
83
|
+
|
|
84
|
+
if (routeSegment.startsWith(":")) {
|
|
85
|
+
params[routeSegment.slice(1)] = pathSegment;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (routeSegment !== pathSegment) {
|
|
90
|
+
matched = false;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (matched) {
|
|
96
|
+
return {
|
|
97
|
+
data: route.data,
|
|
98
|
+
params,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|