@hot-updater/server 0.28.0 → 0.29.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/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/db/ormCore.ts
CHANGED
|
@@ -7,43 +7,176 @@ import type {
|
|
|
7
7
|
Platform,
|
|
8
8
|
UpdateInfo,
|
|
9
9
|
} from "@hot-updater/core";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
12
|
+
isCohortEligibleForUpdate,
|
|
13
|
+
NIL_UUID,
|
|
14
|
+
} from "@hot-updater/core";
|
|
15
|
+
import type {
|
|
16
|
+
DatabaseBundleQueryOptions,
|
|
17
|
+
DatabaseBundleQueryWhere,
|
|
18
|
+
HotUpdaterContext,
|
|
19
|
+
} from "@hot-updater/plugin-core";
|
|
20
|
+
import { semverSatisfies } from "@hot-updater/plugin-core";
|
|
12
21
|
import type { InferFumaDB } from "fumadb";
|
|
13
22
|
import { fumadb } from "fumadb";
|
|
14
23
|
import type { FumaDBAdapter } from "fumadb/adapters";
|
|
15
24
|
import { calculatePagination } from "../calculatePagination";
|
|
16
25
|
import { v0_21_0 } from "../schema/v0_21_0";
|
|
26
|
+
import { v0_29_0 } from "../schema/v0_29_0";
|
|
17
27
|
import type { PaginationInfo } from "../types";
|
|
18
28
|
import type { DatabaseAPI } from "./types";
|
|
19
29
|
|
|
30
|
+
const parseTargetCohorts = (value: unknown): string[] | null => {
|
|
31
|
+
if (!value) return null;
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.filter((v): v is string => typeof v === "string");
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(value) as unknown;
|
|
38
|
+
if (Array.isArray(parsed)) {
|
|
39
|
+
return parsed.filter((v): v is string => typeof v === "string");
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const schemas: [typeof v0_21_0, typeof v0_29_0] = [v0_21_0, v0_29_0];
|
|
49
|
+
|
|
50
|
+
const getLastItem = <T extends unknown[]>(
|
|
51
|
+
items: T,
|
|
52
|
+
): T extends [...infer _, infer Last] ? Last : never =>
|
|
53
|
+
items.at(-1) as T extends [...infer _, infer Last] ? Last : never;
|
|
54
|
+
|
|
20
55
|
export const HotUpdaterDB = fumadb({
|
|
21
56
|
namespace: "hot_updater",
|
|
22
|
-
schemas
|
|
57
|
+
schemas,
|
|
23
58
|
});
|
|
24
|
-
|
|
25
59
|
export type HotUpdaterClient = InferFumaDB<typeof HotUpdaterDB>;
|
|
26
60
|
|
|
27
61
|
export type Migrator = ReturnType<HotUpdaterClient["createMigrator"]>;
|
|
28
62
|
|
|
29
|
-
export function createOrmDatabaseCore({
|
|
63
|
+
export function createOrmDatabaseCore<TContext = unknown>({
|
|
30
64
|
database,
|
|
31
65
|
resolveFileUrl,
|
|
32
66
|
}: {
|
|
33
67
|
database: FumaDBAdapter;
|
|
34
|
-
resolveFileUrl: (
|
|
68
|
+
resolveFileUrl: (
|
|
69
|
+
storageUri: string | null,
|
|
70
|
+
context?: HotUpdaterContext<TContext>,
|
|
71
|
+
) => Promise<string | null>;
|
|
35
72
|
}): {
|
|
36
|
-
api: DatabaseAPI
|
|
73
|
+
api: DatabaseAPI<TContext>;
|
|
37
74
|
adapterName: string;
|
|
38
75
|
createMigrator: () => Migrator;
|
|
39
76
|
generateSchema: HotUpdaterClient["generateSchema"];
|
|
40
77
|
} {
|
|
41
78
|
const client = HotUpdaterDB.client(database);
|
|
79
|
+
const UPDATE_CHECK_PAGE_SIZE = 100;
|
|
80
|
+
const isMongoAdapter = client.adapter.name.toLowerCase().includes("mongodb");
|
|
81
|
+
|
|
82
|
+
const ensureORM = async () => {
|
|
83
|
+
const latestSchema = getLastItem(schemas);
|
|
84
|
+
const lastSchemaVersion = latestSchema.version;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const migrator = client.createMigrator();
|
|
88
|
+
const currentVersion = await migrator.getVersion();
|
|
89
|
+
|
|
90
|
+
if (currentVersion === undefined) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Database is not initialized. Please run 'npx hot-updater migrate' to set up the database schema.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (currentVersion !== lastSchemaVersion) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Database schema version mismatch. Expected version ${lastSchemaVersion}, but database is on version ${currentVersion}. ` +
|
|
99
|
+
"Please run 'npx hot-updater migrate' to update your database schema.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return client.orm(lastSchemaVersion);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (
|
|
106
|
+
error instanceof Error &&
|
|
107
|
+
error.message.includes("doesn't support migration")
|
|
108
|
+
) {
|
|
109
|
+
return client.orm(lastSchemaVersion);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const buildBundleWhere = (where?: DatabaseBundleQueryWhere) => (b: any) => {
|
|
116
|
+
if (where?.id?.in && where.id.in.length === 0) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (where?.targetAppVersionIn && where.targetAppVersionIn.length === 0) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const conditions = [];
|
|
125
|
+
|
|
126
|
+
if (where?.channel !== undefined) {
|
|
127
|
+
conditions.push(b("channel", "=", where.channel));
|
|
128
|
+
}
|
|
129
|
+
if (where?.platform !== undefined) {
|
|
130
|
+
conditions.push(b("platform", "=", where.platform));
|
|
131
|
+
}
|
|
132
|
+
if (where?.enabled !== undefined) {
|
|
133
|
+
conditions.push(b("enabled", "=", where.enabled));
|
|
134
|
+
}
|
|
135
|
+
if (where?.id?.eq !== undefined) {
|
|
136
|
+
conditions.push(b("id", "=", where.id.eq));
|
|
137
|
+
}
|
|
138
|
+
if (where?.id?.gt !== undefined) {
|
|
139
|
+
conditions.push(b("id", ">", where.id.gt));
|
|
140
|
+
}
|
|
141
|
+
if (where?.id?.gte !== undefined) {
|
|
142
|
+
conditions.push(b("id", ">=", where.id.gte));
|
|
143
|
+
}
|
|
144
|
+
if (where?.id?.lt !== undefined) {
|
|
145
|
+
conditions.push(b("id", "<", where.id.lt));
|
|
146
|
+
}
|
|
147
|
+
if (where?.id?.lte !== undefined) {
|
|
148
|
+
conditions.push(b("id", "<=", where.id.lte));
|
|
149
|
+
}
|
|
150
|
+
if (where?.id?.in) {
|
|
151
|
+
conditions.push(b("id", "in", where.id.in));
|
|
152
|
+
}
|
|
153
|
+
if (where?.targetAppVersionNotNull) {
|
|
154
|
+
conditions.push(b.isNotNull("target_app_version"));
|
|
155
|
+
}
|
|
156
|
+
if (where?.targetAppVersion !== undefined) {
|
|
157
|
+
conditions.push(
|
|
158
|
+
where.targetAppVersion === null
|
|
159
|
+
? b.isNull("target_app_version")
|
|
160
|
+
: b("target_app_version", "=", where.targetAppVersion),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (where?.targetAppVersionIn) {
|
|
164
|
+
conditions.push(b("target_app_version", "in", where.targetAppVersionIn));
|
|
165
|
+
}
|
|
166
|
+
if (where?.fingerprintHash !== undefined) {
|
|
167
|
+
conditions.push(
|
|
168
|
+
where.fingerprintHash === null
|
|
169
|
+
? b.isNull("fingerprint_hash")
|
|
170
|
+
: b("fingerprint_hash", "=", where.fingerprintHash),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return conditions.length > 0 ? b.and(...conditions) : true;
|
|
175
|
+
};
|
|
42
176
|
|
|
43
|
-
const api: DatabaseAPI = {
|
|
177
|
+
const api: DatabaseAPI<TContext> = {
|
|
44
178
|
async getBundleById(id: string): Promise<Bundle | null> {
|
|
45
|
-
const
|
|
46
|
-
const orm = client.orm(version);
|
|
179
|
+
const orm = await ensureORM();
|
|
47
180
|
const result = await orm.findFirst("bundles", {
|
|
48
181
|
select: [
|
|
49
182
|
"id",
|
|
@@ -58,6 +191,8 @@ export function createOrmDatabaseCore({
|
|
|
58
191
|
"target_app_version",
|
|
59
192
|
"fingerprint_hash",
|
|
60
193
|
"metadata",
|
|
194
|
+
"rollout_cohort_count",
|
|
195
|
+
"target_cohorts",
|
|
61
196
|
],
|
|
62
197
|
where: (b) => b("id", "=", id),
|
|
63
198
|
});
|
|
@@ -74,13 +209,15 @@ export function createOrmDatabaseCore({
|
|
|
74
209
|
storageUri: result.storage_uri,
|
|
75
210
|
targetAppVersion: result.target_app_version ?? null,
|
|
76
211
|
fingerprintHash: result.fingerprint_hash ?? null,
|
|
212
|
+
rolloutCohortCount:
|
|
213
|
+
result.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
214
|
+
targetCohorts: parseTargetCohorts(result.target_cohorts),
|
|
77
215
|
};
|
|
78
216
|
return bundle;
|
|
79
217
|
},
|
|
80
218
|
|
|
81
219
|
async getUpdateInfo(args: GetBundlesArgs): Promise<UpdateInfo | null> {
|
|
82
|
-
const
|
|
83
|
-
const orm = client.orm(version);
|
|
220
|
+
const orm = await ensureORM();
|
|
84
221
|
|
|
85
222
|
type UpdateSelectRow = {
|
|
86
223
|
id: string;
|
|
@@ -88,6 +225,10 @@ export function createOrmDatabaseCore({
|
|
|
88
225
|
message: string | null;
|
|
89
226
|
storage_uri: string | null;
|
|
90
227
|
file_hash: string;
|
|
228
|
+
rollout_cohort_count?: number | null;
|
|
229
|
+
target_cohorts?: unknown | null;
|
|
230
|
+
target_app_version?: string | null;
|
|
231
|
+
fingerprint_hash?: string | null;
|
|
91
232
|
};
|
|
92
233
|
|
|
93
234
|
const toUpdateInfo = (
|
|
@@ -112,167 +253,225 @@ export function createOrmDatabaseCore({
|
|
|
112
253
|
fileHash: null,
|
|
113
254
|
};
|
|
114
255
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
where: (b) => b.and(b("platform", "=", platform)),
|
|
125
|
-
});
|
|
126
|
-
const allTargetVersions = Array.from(
|
|
127
|
-
new Set(
|
|
128
|
-
(versionRows ?? [])
|
|
129
|
-
.map((r) => r.target_app_version)
|
|
130
|
-
.filter((v): v is string => Boolean(v)),
|
|
131
|
-
),
|
|
132
|
-
);
|
|
133
|
-
const compatibleVersions = filterCompatibleAppVersions(
|
|
134
|
-
allTargetVersions,
|
|
135
|
-
appVersion,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const baseRows =
|
|
139
|
-
compatibleVersions.length === 0
|
|
140
|
-
? []
|
|
141
|
-
: await orm.findMany("bundles", {
|
|
142
|
-
select: [
|
|
143
|
-
"id",
|
|
144
|
-
"should_force_update",
|
|
145
|
-
"message",
|
|
146
|
-
"storage_uri",
|
|
147
|
-
"file_hash",
|
|
148
|
-
"channel",
|
|
149
|
-
"target_app_version",
|
|
150
|
-
"enabled",
|
|
151
|
-
],
|
|
152
|
-
where: (b) =>
|
|
153
|
-
b.and(
|
|
154
|
-
b("enabled", "=", true),
|
|
155
|
-
b("platform", "=", platform),
|
|
156
|
-
b("id", ">=", minBundleId ?? NIL_UUID),
|
|
157
|
-
b("channel", "=", channel),
|
|
158
|
-
b.isNotNull("target_app_version"),
|
|
159
|
-
),
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const candidates = (baseRows ?? []).filter((r) =>
|
|
163
|
-
r.target_app_version
|
|
164
|
-
? compatibleVersions.includes(r.target_app_version)
|
|
165
|
-
: false,
|
|
256
|
+
const isEligibleForUpdate = (
|
|
257
|
+
row: UpdateSelectRow,
|
|
258
|
+
cohort: string | undefined,
|
|
259
|
+
): boolean => {
|
|
260
|
+
return isCohortEligibleForUpdate(
|
|
261
|
+
row.id,
|
|
262
|
+
cohort,
|
|
263
|
+
row.rollout_cohort_count ?? null,
|
|
264
|
+
parseTargetCohorts(row.target_cohorts),
|
|
166
265
|
);
|
|
266
|
+
};
|
|
167
267
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
268
|
+
const findUpdateInfoByScanning = async ({
|
|
269
|
+
args,
|
|
270
|
+
where,
|
|
271
|
+
isCandidate,
|
|
272
|
+
}: {
|
|
273
|
+
args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs;
|
|
274
|
+
where: DatabaseBundleQueryWhere;
|
|
275
|
+
isCandidate: (row: UpdateSelectRow) => boolean;
|
|
276
|
+
}): Promise<UpdateInfo | null> => {
|
|
277
|
+
if (isMongoAdapter) {
|
|
278
|
+
const rows = await orm.findMany("bundles", {
|
|
279
|
+
select: [
|
|
280
|
+
"id",
|
|
281
|
+
"should_force_update",
|
|
282
|
+
"message",
|
|
283
|
+
"storage_uri",
|
|
284
|
+
"file_hash",
|
|
285
|
+
"rollout_cohort_count",
|
|
286
|
+
"target_cohorts",
|
|
287
|
+
"target_app_version",
|
|
288
|
+
"fingerprint_hash",
|
|
289
|
+
],
|
|
290
|
+
where: buildBundleWhere(where),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
rows.sort((a, b) => b.id.localeCompare(a.id));
|
|
294
|
+
|
|
295
|
+
for (const row of rows) {
|
|
296
|
+
if (!isCandidate(row)) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (args.bundleId === NIL_UUID) {
|
|
301
|
+
if (isEligibleForUpdate(row, args.cohort)) {
|
|
302
|
+
return toUpdateInfo(row, "UPDATE");
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const compareResult = row.id.localeCompare(args.bundleId);
|
|
308
|
+
|
|
309
|
+
if (compareResult > 0) {
|
|
310
|
+
if (isEligibleForUpdate(row, args.cohort)) {
|
|
311
|
+
return toUpdateInfo(row, "UPDATE");
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (compareResult === 0) {
|
|
317
|
+
if (isEligibleForUpdate(row, args.cohort)) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return toUpdateInfo(row, "ROLLBACK");
|
|
324
|
+
}
|
|
178
325
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return toUpdateInfo(latestCandidate, "UPDATE");
|
|
326
|
+
if (args.bundleId === NIL_UUID) {
|
|
327
|
+
return null;
|
|
182
328
|
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
329
|
|
|
186
|
-
if (currentBundle) {
|
|
187
330
|
if (
|
|
188
|
-
|
|
189
|
-
|
|
331
|
+
args.minBundleId &&
|
|
332
|
+
args.bundleId.localeCompare(args.minBundleId) <= 0
|
|
190
333
|
) {
|
|
191
|
-
return
|
|
334
|
+
return null;
|
|
192
335
|
}
|
|
193
|
-
|
|
336
|
+
|
|
337
|
+
return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
|
|
194
338
|
}
|
|
195
339
|
|
|
196
|
-
|
|
197
|
-
|
|
340
|
+
let offset = 0;
|
|
341
|
+
|
|
342
|
+
while (true) {
|
|
343
|
+
const rows = await orm.findMany("bundles", {
|
|
344
|
+
select: [
|
|
345
|
+
"id",
|
|
346
|
+
"should_force_update",
|
|
347
|
+
"message",
|
|
348
|
+
"storage_uri",
|
|
349
|
+
"file_hash",
|
|
350
|
+
"rollout_cohort_count",
|
|
351
|
+
"target_cohorts",
|
|
352
|
+
"target_app_version",
|
|
353
|
+
"fingerprint_hash",
|
|
354
|
+
],
|
|
355
|
+
where: buildBundleWhere(where),
|
|
356
|
+
orderBy: [["id", "desc"]],
|
|
357
|
+
limit: UPDATE_CHECK_PAGE_SIZE,
|
|
358
|
+
offset,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
for (const row of rows) {
|
|
362
|
+
if (!isCandidate(row)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (args.bundleId === NIL_UUID) {
|
|
367
|
+
if (isEligibleForUpdate(row, args.cohort)) {
|
|
368
|
+
return toUpdateInfo(row, "UPDATE");
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const compareResult = row.id.localeCompare(args.bundleId);
|
|
374
|
+
|
|
375
|
+
if (compareResult > 0) {
|
|
376
|
+
if (isEligibleForUpdate(row, args.cohort)) {
|
|
377
|
+
return toUpdateInfo(row, "UPDATE");
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (compareResult === 0) {
|
|
383
|
+
if (isEligibleForUpdate(row, args.cohort)) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return toUpdateInfo(row, "ROLLBACK");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (rows.length < UPDATE_CHECK_PAGE_SIZE) {
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
offset += UPDATE_CHECK_PAGE_SIZE;
|
|
198
397
|
}
|
|
199
|
-
|
|
200
|
-
|
|
398
|
+
|
|
399
|
+
if (args.bundleId === NIL_UUID) {
|
|
400
|
+
return null;
|
|
201
401
|
}
|
|
202
402
|
|
|
203
|
-
if (
|
|
403
|
+
if (
|
|
404
|
+
args.minBundleId &&
|
|
405
|
+
args.bundleId.localeCompare(args.minBundleId) <= 0
|
|
406
|
+
) {
|
|
204
407
|
return null;
|
|
205
408
|
}
|
|
409
|
+
|
|
206
410
|
return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
|
|
207
411
|
};
|
|
208
412
|
|
|
413
|
+
const appVersionStrategy = async ({
|
|
414
|
+
platform,
|
|
415
|
+
appVersion,
|
|
416
|
+
bundleId,
|
|
417
|
+
minBundleId = NIL_UUID,
|
|
418
|
+
channel = "production",
|
|
419
|
+
cohort,
|
|
420
|
+
}: AppVersionGetBundlesArgs): Promise<UpdateInfo | null> => {
|
|
421
|
+
return findUpdateInfoByScanning({
|
|
422
|
+
args: {
|
|
423
|
+
_updateStrategy: "appVersion",
|
|
424
|
+
platform,
|
|
425
|
+
appVersion,
|
|
426
|
+
bundleId,
|
|
427
|
+
minBundleId,
|
|
428
|
+
channel,
|
|
429
|
+
cohort,
|
|
430
|
+
},
|
|
431
|
+
where: {
|
|
432
|
+
enabled: true,
|
|
433
|
+
platform,
|
|
434
|
+
channel,
|
|
435
|
+
id: {
|
|
436
|
+
gte: minBundleId,
|
|
437
|
+
},
|
|
438
|
+
targetAppVersionNotNull: true,
|
|
439
|
+
},
|
|
440
|
+
isCandidate: (row) =>
|
|
441
|
+
!!row.target_app_version &&
|
|
442
|
+
semverSatisfies(row.target_app_version, appVersion),
|
|
443
|
+
});
|
|
444
|
+
};
|
|
445
|
+
|
|
209
446
|
const fingerprintStrategy = async ({
|
|
210
447
|
platform,
|
|
211
448
|
fingerprintHash,
|
|
212
449
|
bundleId,
|
|
213
450
|
minBundleId = NIL_UUID,
|
|
214
451
|
channel = "production",
|
|
452
|
+
cohort,
|
|
215
453
|
}: FingerprintGetBundlesArgs): Promise<UpdateInfo | null> => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
454
|
+
return findUpdateInfoByScanning({
|
|
455
|
+
args: {
|
|
456
|
+
_updateStrategy: "fingerprint",
|
|
457
|
+
platform,
|
|
458
|
+
fingerprintHash,
|
|
459
|
+
bundleId,
|
|
460
|
+
minBundleId,
|
|
461
|
+
channel,
|
|
462
|
+
cohort,
|
|
463
|
+
},
|
|
464
|
+
where: {
|
|
465
|
+
enabled: true,
|
|
466
|
+
platform,
|
|
467
|
+
channel,
|
|
468
|
+
id: {
|
|
469
|
+
gte: minBundleId,
|
|
470
|
+
},
|
|
471
|
+
fingerprintHash,
|
|
472
|
+
},
|
|
473
|
+
isCandidate: (row) => row.fingerprint_hash === fingerprintHash,
|
|
235
474
|
});
|
|
236
|
-
|
|
237
|
-
const byIdDesc = (a: { id: string }, b: { id: string }) =>
|
|
238
|
-
b.id.localeCompare(a.id);
|
|
239
|
-
const sorted = (candidates ?? []).slice().sort(byIdDesc);
|
|
240
|
-
|
|
241
|
-
const latestCandidate = sorted[0] ?? null;
|
|
242
|
-
const currentBundle = sorted.find((b) => b.id === bundleId);
|
|
243
|
-
const updateCandidate =
|
|
244
|
-
sorted.find((b) => b.id.localeCompare(bundleId) > 0) ?? null;
|
|
245
|
-
const rollbackCandidate =
|
|
246
|
-
sorted.find((b) => b.id.localeCompare(bundleId) < 0) ?? null;
|
|
247
|
-
|
|
248
|
-
if (bundleId === NIL_UUID) {
|
|
249
|
-
if (latestCandidate && latestCandidate.id !== bundleId) {
|
|
250
|
-
return toUpdateInfo(latestCandidate, "UPDATE");
|
|
251
|
-
}
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (currentBundle) {
|
|
256
|
-
if (
|
|
257
|
-
latestCandidate &&
|
|
258
|
-
latestCandidate.id.localeCompare(currentBundle.id) > 0
|
|
259
|
-
) {
|
|
260
|
-
return toUpdateInfo(latestCandidate, "UPDATE");
|
|
261
|
-
}
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (updateCandidate) {
|
|
266
|
-
return toUpdateInfo(updateCandidate, "UPDATE");
|
|
267
|
-
}
|
|
268
|
-
if (rollbackCandidate) {
|
|
269
|
-
return toUpdateInfo(rollbackCandidate, "ROLLBACK");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (minBundleId && bundleId.localeCompare(minBundleId) <= 0) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
|
|
276
475
|
};
|
|
277
476
|
|
|
278
477
|
if (args._updateStrategy === "appVersion") {
|
|
@@ -286,92 +485,117 @@ export function createOrmDatabaseCore({
|
|
|
286
485
|
|
|
287
486
|
async getAppUpdateInfo(
|
|
288
487
|
args: GetBundlesArgs,
|
|
488
|
+
context?: HotUpdaterContext<TContext>,
|
|
289
489
|
): Promise<AppUpdateInfo | null> {
|
|
290
490
|
const info = await this.getUpdateInfo(args);
|
|
291
491
|
if (!info) return null;
|
|
292
492
|
const { storageUri, ...rest } = info as UpdateInfo & {
|
|
293
493
|
storageUri: string | null;
|
|
294
494
|
};
|
|
295
|
-
const fileUrl = await resolveFileUrl(storageUri ?? null);
|
|
495
|
+
const fileUrl = await resolveFileUrl(storageUri ?? null, context);
|
|
296
496
|
return { ...rest, fileUrl };
|
|
297
497
|
},
|
|
298
498
|
|
|
299
499
|
async getChannels(): Promise<string[]> {
|
|
300
|
-
const
|
|
301
|
-
const orm = client.orm(version);
|
|
500
|
+
const orm = await ensureORM();
|
|
302
501
|
const rows = await orm.findMany("bundles", {
|
|
303
502
|
select: ["channel"],
|
|
503
|
+
orderBy: [["channel", "asc"]],
|
|
304
504
|
});
|
|
305
505
|
const set = new Set(rows?.map((r) => r.channel) ?? []);
|
|
306
506
|
return Array.from(set);
|
|
307
507
|
},
|
|
308
508
|
|
|
309
|
-
async getBundles(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const version = await client.version();
|
|
315
|
-
const orm = client.orm(version);
|
|
316
|
-
const { where, limit, offset } = options;
|
|
509
|
+
async getBundles(
|
|
510
|
+
options: DatabaseBundleQueryOptions,
|
|
511
|
+
): Promise<{ data: Bundle[]; pagination: PaginationInfo }> {
|
|
512
|
+
const orm = await ensureORM();
|
|
513
|
+
const { where, limit, offset, orderBy } = options;
|
|
317
514
|
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
"id",
|
|
321
|
-
"platform",
|
|
322
|
-
"should_force_update",
|
|
323
|
-
"enabled",
|
|
324
|
-
"file_hash",
|
|
325
|
-
"git_commit_hash",
|
|
326
|
-
"message",
|
|
327
|
-
"channel",
|
|
328
|
-
"storage_uri",
|
|
329
|
-
"target_app_version",
|
|
330
|
-
"fingerprint_hash",
|
|
331
|
-
"metadata",
|
|
332
|
-
],
|
|
333
|
-
where: (b) => {
|
|
334
|
-
const conditions = [];
|
|
335
|
-
if (where?.channel) {
|
|
336
|
-
conditions.push(b("channel", "=", where.channel));
|
|
337
|
-
}
|
|
338
|
-
if (where?.platform) {
|
|
339
|
-
conditions.push(b("platform", "=", where.platform));
|
|
340
|
-
}
|
|
341
|
-
return conditions.length > 0 ? b.and(...conditions) : true;
|
|
342
|
-
},
|
|
515
|
+
const total = await orm.count("bundles", {
|
|
516
|
+
where: buildBundleWhere(where),
|
|
343
517
|
});
|
|
344
518
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
519
|
+
const selectedColumns: Array<
|
|
520
|
+
| "id"
|
|
521
|
+
| "platform"
|
|
522
|
+
| "should_force_update"
|
|
523
|
+
| "enabled"
|
|
524
|
+
| "file_hash"
|
|
525
|
+
| "git_commit_hash"
|
|
526
|
+
| "message"
|
|
527
|
+
| "channel"
|
|
528
|
+
| "storage_uri"
|
|
529
|
+
| "target_app_version"
|
|
530
|
+
| "fingerprint_hash"
|
|
531
|
+
| "metadata"
|
|
532
|
+
| "rollout_cohort_count"
|
|
533
|
+
| "target_cohorts"
|
|
534
|
+
> = [
|
|
535
|
+
"id",
|
|
536
|
+
"platform",
|
|
537
|
+
"should_force_update",
|
|
538
|
+
"enabled",
|
|
539
|
+
"file_hash",
|
|
540
|
+
"git_commit_hash",
|
|
541
|
+
"message",
|
|
542
|
+
"channel",
|
|
543
|
+
"storage_uri",
|
|
544
|
+
"target_app_version",
|
|
545
|
+
"fingerprint_hash",
|
|
546
|
+
"metadata",
|
|
547
|
+
"rollout_cohort_count",
|
|
548
|
+
"target_cohorts",
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
const rows = isMongoAdapter
|
|
552
|
+
? (
|
|
553
|
+
await orm.findMany("bundles", {
|
|
554
|
+
select: selectedColumns,
|
|
555
|
+
where: buildBundleWhere(where),
|
|
556
|
+
})
|
|
557
|
+
)
|
|
558
|
+
.sort((a, b) => {
|
|
559
|
+
const direction = orderBy?.direction ?? "desc";
|
|
560
|
+
const result = a.id.localeCompare(b.id);
|
|
561
|
+
return direction === "asc" ? result : -result;
|
|
562
|
+
})
|
|
563
|
+
.slice(offset, offset + limit)
|
|
564
|
+
: await orm.findMany("bundles", {
|
|
565
|
+
select: selectedColumns,
|
|
566
|
+
where: buildBundleWhere(where),
|
|
567
|
+
orderBy: [[orderBy?.field ?? "id", orderBy?.direction ?? "desc"]],
|
|
568
|
+
limit,
|
|
569
|
+
offset,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const data: Bundle[] = rows.map(
|
|
573
|
+
(r): Bundle => ({
|
|
574
|
+
id: r.id,
|
|
575
|
+
platform: r.platform as Platform,
|
|
576
|
+
shouldForceUpdate: Boolean(r.should_force_update),
|
|
577
|
+
enabled: Boolean(r.enabled),
|
|
578
|
+
fileHash: r.file_hash,
|
|
579
|
+
gitCommitHash: r.git_commit_hash ?? null,
|
|
580
|
+
message: r.message ?? null,
|
|
581
|
+
channel: r.channel,
|
|
582
|
+
storageUri: r.storage_uri,
|
|
583
|
+
targetAppVersion: r.target_app_version ?? null,
|
|
584
|
+
fingerprintHash: r.fingerprint_hash ?? null,
|
|
585
|
+
rolloutCohortCount:
|
|
586
|
+
r.rollout_cohort_count ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
587
|
+
targetCohorts: parseTargetCohorts(r.target_cohorts),
|
|
588
|
+
}),
|
|
589
|
+
);
|
|
365
590
|
|
|
366
591
|
return {
|
|
367
|
-
data
|
|
592
|
+
data,
|
|
368
593
|
pagination: calculatePagination(total, { limit, offset }),
|
|
369
594
|
};
|
|
370
595
|
},
|
|
371
596
|
|
|
372
597
|
async insertBundle(bundle: Bundle): Promise<void> {
|
|
373
|
-
const
|
|
374
|
-
const orm = client.orm(version);
|
|
598
|
+
const orm = await ensureORM();
|
|
375
599
|
const values = {
|
|
376
600
|
id: bundle.id,
|
|
377
601
|
platform: bundle.platform,
|
|
@@ -385,6 +609,9 @@ export function createOrmDatabaseCore({
|
|
|
385
609
|
target_app_version: bundle.targetAppVersion,
|
|
386
610
|
fingerprint_hash: bundle.fingerprintHash,
|
|
387
611
|
metadata: bundle.metadata ?? {},
|
|
612
|
+
rollout_cohort_count:
|
|
613
|
+
bundle.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
614
|
+
target_cohorts: bundle.targetCohorts ?? null,
|
|
388
615
|
};
|
|
389
616
|
const { id, ...updateValues } = values;
|
|
390
617
|
await orm.upsert("bundles", {
|
|
@@ -398,8 +625,7 @@ export function createOrmDatabaseCore({
|
|
|
398
625
|
bundleId: string,
|
|
399
626
|
newBundle: Partial<Bundle>,
|
|
400
627
|
): Promise<void> {
|
|
401
|
-
const
|
|
402
|
-
const orm = client.orm(version);
|
|
628
|
+
const orm = await ensureORM();
|
|
403
629
|
const current = await this.getBundleById(bundleId);
|
|
404
630
|
if (!current) throw new Error("targetBundleId not found");
|
|
405
631
|
const merged: Bundle = { ...current, ...newBundle };
|
|
@@ -416,6 +642,9 @@ export function createOrmDatabaseCore({
|
|
|
416
642
|
target_app_version: merged.targetAppVersion,
|
|
417
643
|
fingerprint_hash: merged.fingerprintHash,
|
|
418
644
|
metadata: merged.metadata ?? {},
|
|
645
|
+
rollout_cohort_count:
|
|
646
|
+
merged.rolloutCohortCount ?? DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
647
|
+
target_cohorts: merged.targetCohorts ?? null,
|
|
419
648
|
};
|
|
420
649
|
const { id: id2, ...updateValues2 } = values;
|
|
421
650
|
await orm.upsert("bundles", {
|
|
@@ -426,8 +655,7 @@ export function createOrmDatabaseCore({
|
|
|
426
655
|
},
|
|
427
656
|
|
|
428
657
|
async deleteBundleById(bundleId: string): Promise<void> {
|
|
429
|
-
const
|
|
430
|
-
const orm = client.orm(version);
|
|
658
|
+
const orm = await ensureORM();
|
|
431
659
|
await orm.deleteMany("bundles", { where: (b) => b("id", "=", bundleId) });
|
|
432
660
|
},
|
|
433
661
|
};
|