@hot-updater/server 0.27.1 → 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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { Kysely } from "kysely";
|
|
3
|
+
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
4
|
+
import { bench, describe } from "vitest";
|
|
5
|
+
import { filterCompatibleAppVersions } from "../../../../plugins/plugin-core/src";
|
|
6
|
+
import type {
|
|
7
|
+
AppVersionGetBundlesArgs,
|
|
8
|
+
Platform,
|
|
9
|
+
UpdateInfo,
|
|
10
|
+
} from "../../../core/src";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
13
|
+
isCohortEligibleForUpdate,
|
|
14
|
+
NIL_UUID,
|
|
15
|
+
} from "../../../core/src";
|
|
16
|
+
import { kyselyAdapter } from "../adapters/kysely";
|
|
17
|
+
import { createOrmDatabaseCore } from "./ormCore";
|
|
18
|
+
|
|
19
|
+
const BUNDLE_COUNT = 8_000;
|
|
20
|
+
const BENCH_APP_VERSION = "1.0.0";
|
|
21
|
+
const BENCH_PLATFORM = "ios" as const;
|
|
22
|
+
const BENCH_CHANNEL = "production";
|
|
23
|
+
|
|
24
|
+
interface BenchBundleRow {
|
|
25
|
+
id: string;
|
|
26
|
+
platform: Platform;
|
|
27
|
+
should_force_update: boolean;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
file_hash: string;
|
|
30
|
+
git_commit_hash: string;
|
|
31
|
+
message: string;
|
|
32
|
+
channel: string;
|
|
33
|
+
storage_uri: string;
|
|
34
|
+
target_app_version: string | null;
|
|
35
|
+
fingerprint_hash: string | null;
|
|
36
|
+
metadata: Record<string, unknown>;
|
|
37
|
+
rollout_cohort_count: number | null;
|
|
38
|
+
target_cohorts: string[] | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const createBundleRow = (index: number): BenchBundleRow => ({
|
|
42
|
+
id: `00000000-0000-0000-0000-${String(index).padStart(12, "0")}`,
|
|
43
|
+
platform: BENCH_PLATFORM,
|
|
44
|
+
should_force_update: false,
|
|
45
|
+
enabled: true,
|
|
46
|
+
file_hash: `hash-${index}`,
|
|
47
|
+
git_commit_hash: `commit-${index}`,
|
|
48
|
+
message: `bundle-${index}`,
|
|
49
|
+
channel: BENCH_CHANNEL,
|
|
50
|
+
storage_uri: `s3://bench/bundles/${index}.zip`,
|
|
51
|
+
target_app_version: "*",
|
|
52
|
+
fingerprint_hash: `fingerprint-${index % 10}`,
|
|
53
|
+
metadata: { index },
|
|
54
|
+
rollout_cohort_count: DEFAULT_ROLLOUT_COHORT_COUNT,
|
|
55
|
+
target_cohorts: null,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
interface BenchDatabase {
|
|
59
|
+
bundles: BenchBundleRow;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const parseTargetCohorts = (value: unknown): string[] | null => {
|
|
63
|
+
if (!value) return null;
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return value.filter((entry): entry is string => typeof entry === "string");
|
|
66
|
+
}
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(value) as unknown;
|
|
70
|
+
if (Array.isArray(parsed)) {
|
|
71
|
+
return parsed.filter(
|
|
72
|
+
(entry): entry is string => typeof entry === "string",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const oldOrmCoreGetUpdateInfo = async (
|
|
83
|
+
db: Kysely<BenchDatabase>,
|
|
84
|
+
args: AppVersionGetBundlesArgs,
|
|
85
|
+
): Promise<UpdateInfo | null> => {
|
|
86
|
+
const toUpdateInfo = (
|
|
87
|
+
row: {
|
|
88
|
+
id: string;
|
|
89
|
+
should_force_update: boolean;
|
|
90
|
+
message: string | null;
|
|
91
|
+
storage_uri: string | null;
|
|
92
|
+
file_hash: string;
|
|
93
|
+
},
|
|
94
|
+
status: "UPDATE" | "ROLLBACK",
|
|
95
|
+
): UpdateInfo => ({
|
|
96
|
+
id: row.id,
|
|
97
|
+
shouldForceUpdate:
|
|
98
|
+
status === "ROLLBACK" ? true : Boolean(row.should_force_update),
|
|
99
|
+
message: row.message ?? null,
|
|
100
|
+
status,
|
|
101
|
+
storageUri: row.storage_uri ?? null,
|
|
102
|
+
fileHash: row.file_hash ?? null,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const isEligibleForUpdate = (
|
|
106
|
+
row: {
|
|
107
|
+
id: string;
|
|
108
|
+
rollout_cohort_count?: number | null;
|
|
109
|
+
target_cohorts?: unknown | null;
|
|
110
|
+
},
|
|
111
|
+
cohort: string | undefined,
|
|
112
|
+
) => {
|
|
113
|
+
return isCohortEligibleForUpdate(
|
|
114
|
+
row.id,
|
|
115
|
+
cohort,
|
|
116
|
+
row.rollout_cohort_count ?? null,
|
|
117
|
+
parseTargetCohorts(row.target_cohorts),
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const versionRows = await db
|
|
122
|
+
.selectFrom("bundles")
|
|
123
|
+
.select("target_app_version")
|
|
124
|
+
.where("platform", "=", args.platform)
|
|
125
|
+
.execute();
|
|
126
|
+
|
|
127
|
+
const allTargetVersions = Array.from(
|
|
128
|
+
new Set(
|
|
129
|
+
versionRows
|
|
130
|
+
.map((row) => row.target_app_version)
|
|
131
|
+
.filter((value): value is string => Boolean(value)),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const compatibleVersions = filterCompatibleAppVersions(
|
|
136
|
+
allTargetVersions,
|
|
137
|
+
args.appVersion,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const baseRows =
|
|
141
|
+
compatibleVersions.length === 0
|
|
142
|
+
? []
|
|
143
|
+
: await db
|
|
144
|
+
.selectFrom("bundles")
|
|
145
|
+
.select([
|
|
146
|
+
"id",
|
|
147
|
+
"should_force_update",
|
|
148
|
+
"message",
|
|
149
|
+
"storage_uri",
|
|
150
|
+
"file_hash",
|
|
151
|
+
"rollout_cohort_count",
|
|
152
|
+
"target_cohorts",
|
|
153
|
+
"target_app_version",
|
|
154
|
+
])
|
|
155
|
+
.where("enabled", "=", true)
|
|
156
|
+
.where("platform", "=", args.platform)
|
|
157
|
+
.where("id", ">=", args.minBundleId ?? NIL_UUID)
|
|
158
|
+
.where("channel", "=", args.channel ?? BENCH_CHANNEL)
|
|
159
|
+
.where("target_app_version", "is not", null)
|
|
160
|
+
.execute();
|
|
161
|
+
|
|
162
|
+
const candidates = baseRows
|
|
163
|
+
.filter((row) =>
|
|
164
|
+
row.target_app_version
|
|
165
|
+
? compatibleVersions.includes(row.target_app_version)
|
|
166
|
+
: false,
|
|
167
|
+
)
|
|
168
|
+
.sort((a, b) => b.id.localeCompare(a.id));
|
|
169
|
+
|
|
170
|
+
const updateCandidate =
|
|
171
|
+
candidates.find(
|
|
172
|
+
(row) =>
|
|
173
|
+
row.id.localeCompare(args.bundleId) > 0 &&
|
|
174
|
+
isEligibleForUpdate(row, args.cohort),
|
|
175
|
+
) ?? null;
|
|
176
|
+
|
|
177
|
+
if (args.bundleId === NIL_UUID) {
|
|
178
|
+
return updateCandidate ? toUpdateInfo(updateCandidate, "UPDATE") : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const currentBundle = candidates.find((row) => row.id === args.bundleId);
|
|
182
|
+
const currentBundleEligible = currentBundle
|
|
183
|
+
? isEligibleForUpdate(currentBundle, args.cohort)
|
|
184
|
+
: false;
|
|
185
|
+
const rollbackCandidate =
|
|
186
|
+
candidates.find((row) => row.id.localeCompare(args.bundleId) < 0) ?? null;
|
|
187
|
+
|
|
188
|
+
if (currentBundleEligible) {
|
|
189
|
+
return updateCandidate ? toUpdateInfo(updateCandidate, "UPDATE") : null;
|
|
190
|
+
}
|
|
191
|
+
if (updateCandidate) {
|
|
192
|
+
return toUpdateInfo(updateCandidate, "UPDATE");
|
|
193
|
+
}
|
|
194
|
+
if (rollbackCandidate) {
|
|
195
|
+
return toUpdateInfo(rollbackCandidate, "ROLLBACK");
|
|
196
|
+
}
|
|
197
|
+
if (args.minBundleId && args.bundleId.localeCompare(args.minBundleId) <= 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
id: NIL_UUID,
|
|
202
|
+
message: null,
|
|
203
|
+
shouldForceUpdate: true,
|
|
204
|
+
status: "ROLLBACK",
|
|
205
|
+
storageUri: null,
|
|
206
|
+
fileHash: null,
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const pg = new PGlite();
|
|
211
|
+
const kysely = new Kysely<BenchDatabase>({ dialect: new PGliteDialect(pg) });
|
|
212
|
+
const ormCore = createOrmDatabaseCore({
|
|
213
|
+
database: kyselyAdapter({
|
|
214
|
+
db: kysely,
|
|
215
|
+
provider: "postgresql",
|
|
216
|
+
}),
|
|
217
|
+
resolveFileUrl: async () => null,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const migration = await ormCore.createMigrator().migrateToLatest({
|
|
221
|
+
mode: "from-schema",
|
|
222
|
+
updateSettings: true,
|
|
223
|
+
});
|
|
224
|
+
await migration.execute();
|
|
225
|
+
|
|
226
|
+
const rows = Array.from({ length: BUNDLE_COUNT }, (_, index) =>
|
|
227
|
+
createBundleRow(index + 1),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
for (let index = 0; index < rows.length; index += 500) {
|
|
231
|
+
await kysely
|
|
232
|
+
.insertInto("bundles")
|
|
233
|
+
.values(rows.slice(index, index + 500))
|
|
234
|
+
.execute();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
describe("orm update check benchmark", () => {
|
|
238
|
+
const args: AppVersionGetBundlesArgs = {
|
|
239
|
+
_updateStrategy: "appVersion",
|
|
240
|
+
appVersion: BENCH_APP_VERSION,
|
|
241
|
+
bundleId: NIL_UUID,
|
|
242
|
+
platform: BENCH_PLATFORM,
|
|
243
|
+
channel: BENCH_CHANNEL,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
bench(
|
|
247
|
+
"ormCore legacy appVersion scan",
|
|
248
|
+
async () => {
|
|
249
|
+
await oldOrmCoreGetUpdateInfo(kysely, args);
|
|
250
|
+
},
|
|
251
|
+
{ warmupIterations: 3, iterations: 15 },
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
bench(
|
|
255
|
+
"ormCore current paged appVersion scan",
|
|
256
|
+
async () => {
|
|
257
|
+
await ormCore.api.getUpdateInfo(args);
|
|
258
|
+
},
|
|
259
|
+
{ warmupIterations: 3, iterations: 15 },
|
|
260
|
+
);
|
|
261
|
+
});
|
package/src/db/pluginCore.ts
CHANGED
|
@@ -1,104 +1,353 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AppUpdateInfo,
|
|
3
|
+
AppVersionGetBundlesArgs,
|
|
3
4
|
Bundle,
|
|
5
|
+
FingerprintGetBundlesArgs,
|
|
4
6
|
GetBundlesArgs,
|
|
7
|
+
Platform,
|
|
5
8
|
UpdateInfo,
|
|
6
9
|
} from "@hot-updater/core";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
10
|
+
import { isCohortEligibleForUpdate, NIL_UUID } from "@hot-updater/core";
|
|
11
|
+
import {
|
|
12
|
+
type DatabaseBundleQueryOptions,
|
|
13
|
+
type DatabaseBundleQueryOrder,
|
|
14
|
+
type DatabaseBundleQueryWhere,
|
|
15
|
+
type DatabasePlugin,
|
|
16
|
+
type HotUpdaterContext,
|
|
17
|
+
semverSatisfies,
|
|
18
|
+
} from "@hot-updater/plugin-core";
|
|
9
19
|
import type { DatabaseAPI } from "./types";
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
const PAGE_SIZE = 100;
|
|
22
|
+
|
|
23
|
+
const DESC_ORDER = { field: "id", direction: "desc" } as const;
|
|
24
|
+
|
|
25
|
+
const bundleMatchesQueryWhere = (
|
|
26
|
+
bundle: Bundle,
|
|
27
|
+
where: DatabaseBundleQueryWhere | undefined,
|
|
28
|
+
) => {
|
|
29
|
+
if (!where) return true;
|
|
30
|
+
if (where.channel !== undefined && bundle.channel !== where.channel)
|
|
31
|
+
return false;
|
|
32
|
+
if (where.platform !== undefined && bundle.platform !== where.platform)
|
|
33
|
+
return false;
|
|
34
|
+
if (where.enabled !== undefined && bundle.enabled !== where.enabled)
|
|
35
|
+
return false;
|
|
36
|
+
if (where.id?.eq !== undefined && bundle.id !== where.id.eq) return false;
|
|
37
|
+
if (where.id?.gt !== undefined && bundle.id.localeCompare(where.id.gt) <= 0)
|
|
38
|
+
return false;
|
|
39
|
+
if (where.id?.gte !== undefined && bundle.id.localeCompare(where.id.gte) < 0)
|
|
40
|
+
return false;
|
|
41
|
+
if (where.id?.lt !== undefined && bundle.id.localeCompare(where.id.lt) >= 0)
|
|
42
|
+
return false;
|
|
43
|
+
if (where.id?.lte !== undefined && bundle.id.localeCompare(where.id.lte) > 0)
|
|
44
|
+
return false;
|
|
45
|
+
if (where.id?.in && !where.id.in.includes(bundle.id)) return false;
|
|
46
|
+
if (where.targetAppVersionNotNull && bundle.targetAppVersion === null) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
where.targetAppVersion !== undefined &&
|
|
51
|
+
bundle.targetAppVersion !== where.targetAppVersion
|
|
52
|
+
) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
where.targetAppVersionIn &&
|
|
57
|
+
!where.targetAppVersionIn.includes(bundle.targetAppVersion ?? "")
|
|
58
|
+
) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
where.fingerprintHash !== undefined &&
|
|
63
|
+
bundle.fingerprintHash !== where.fingerprintHash
|
|
64
|
+
) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const sortBundles = (
|
|
71
|
+
bundles: Bundle[],
|
|
72
|
+
orderBy: DatabaseBundleQueryOrder | undefined,
|
|
73
|
+
) => {
|
|
74
|
+
const direction = orderBy?.direction ?? "desc";
|
|
75
|
+
return bundles.slice().sort((a, b) => {
|
|
76
|
+
const result = a.id.localeCompare(b.id);
|
|
77
|
+
return direction === "asc" ? result : -result;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const makeResponse = (
|
|
82
|
+
bundle: Bundle,
|
|
83
|
+
status: "UPDATE" | "ROLLBACK",
|
|
84
|
+
): UpdateInfo => ({
|
|
85
|
+
id: bundle.id,
|
|
86
|
+
message: bundle.message,
|
|
87
|
+
shouldForceUpdate: status === "ROLLBACK" ? true : bundle.shouldForceUpdate,
|
|
88
|
+
status,
|
|
89
|
+
storageUri: bundle.storageUri,
|
|
90
|
+
fileHash: bundle.fileHash,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const INIT_BUNDLE_ROLLBACK_UPDATE_INFO: UpdateInfo = {
|
|
94
|
+
message: null,
|
|
95
|
+
id: NIL_UUID,
|
|
96
|
+
shouldForceUpdate: true,
|
|
97
|
+
status: "ROLLBACK",
|
|
98
|
+
storageUri: null,
|
|
99
|
+
fileHash: null,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export function createPluginDatabaseCore<TContext = unknown>(
|
|
103
|
+
plugin: DatabasePlugin<TContext>,
|
|
104
|
+
resolveFileUrl: (
|
|
105
|
+
storageUri: string | null,
|
|
106
|
+
context?: HotUpdaterContext<TContext>,
|
|
107
|
+
) => Promise<string | null>,
|
|
14
108
|
): {
|
|
15
|
-
api: DatabaseAPI
|
|
109
|
+
api: DatabaseAPI<TContext>;
|
|
16
110
|
adapterName: string;
|
|
17
111
|
createMigrator: () => never;
|
|
18
112
|
generateSchema: () => never;
|
|
19
113
|
} {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
114
|
+
const getSortedBundlePage = async (
|
|
115
|
+
options: DatabaseBundleQueryOptions,
|
|
116
|
+
context?: HotUpdaterContext<TContext>,
|
|
117
|
+
): Promise<Awaited<ReturnType<DatabasePlugin<TContext>["getBundles"]>>> => {
|
|
118
|
+
const result = await plugin.getBundles(
|
|
119
|
+
{
|
|
120
|
+
...options,
|
|
121
|
+
orderBy: options.orderBy ?? DESC_ORDER,
|
|
122
|
+
},
|
|
123
|
+
context,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...result,
|
|
128
|
+
data: sortBundles(result.data, options.orderBy ?? DESC_ORDER),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const isEligibleForUpdate = (
|
|
133
|
+
bundle: Bundle,
|
|
134
|
+
cohort: string | undefined,
|
|
135
|
+
): boolean => {
|
|
136
|
+
return isCohortEligibleForUpdate(
|
|
137
|
+
bundle.id,
|
|
138
|
+
cohort,
|
|
139
|
+
bundle.rolloutCohortCount,
|
|
140
|
+
bundle.targetCohorts,
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const findUpdateInfoByScanning = async ({
|
|
145
|
+
args,
|
|
146
|
+
queryWhere,
|
|
147
|
+
isCandidate,
|
|
148
|
+
context,
|
|
149
|
+
}: {
|
|
150
|
+
args: AppVersionGetBundlesArgs | FingerprintGetBundlesArgs;
|
|
151
|
+
queryWhere: DatabaseBundleQueryWhere;
|
|
152
|
+
isCandidate: (bundle: Bundle) => boolean;
|
|
153
|
+
context?: HotUpdaterContext<TContext>;
|
|
154
|
+
}): Promise<UpdateInfo | null> => {
|
|
155
|
+
let offset = 0;
|
|
156
|
+
|
|
157
|
+
while (true) {
|
|
158
|
+
const { data, pagination } = await getSortedBundlePage(
|
|
159
|
+
{
|
|
160
|
+
where: queryWhere,
|
|
161
|
+
limit: PAGE_SIZE,
|
|
162
|
+
offset,
|
|
163
|
+
orderBy: DESC_ORDER,
|
|
164
|
+
},
|
|
165
|
+
context,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
for (const bundle of data) {
|
|
169
|
+
if (
|
|
170
|
+
!bundleMatchesQueryWhere(bundle, queryWhere) ||
|
|
171
|
+
!isCandidate(bundle)
|
|
172
|
+
) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (args.bundleId === NIL_UUID) {
|
|
177
|
+
if (isEligibleForUpdate(bundle, args.cohort)) {
|
|
178
|
+
return makeResponse(bundle, "UPDATE");
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const compareResult = bundle.id.localeCompare(args.bundleId);
|
|
24
184
|
|
|
25
|
-
|
|
26
|
-
|
|
185
|
+
if (compareResult > 0) {
|
|
186
|
+
if (isEligibleForUpdate(bundle, args.cohort)) {
|
|
187
|
+
return makeResponse(bundle, "UPDATE");
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
27
191
|
|
|
28
|
-
|
|
29
|
-
|
|
192
|
+
if (compareResult === 0) {
|
|
193
|
+
if (isEligibleForUpdate(bundle, args.cohort)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return makeResponse(bundle, "ROLLBACK");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!pagination.hasNextPage) {
|
|
203
|
+
break;
|
|
30
204
|
}
|
|
31
205
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
206
|
+
offset += PAGE_SIZE;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (args.bundleId === NIL_UUID) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
args.minBundleId &&
|
|
215
|
+
args.bundleId.localeCompare(args.minBundleId) <= 0
|
|
216
|
+
) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return INIT_BUNDLE_ROLLBACK_UPDATE_INFO;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const getBaseWhere = ({
|
|
224
|
+
platform,
|
|
225
|
+
channel,
|
|
226
|
+
minBundleId,
|
|
227
|
+
}: {
|
|
228
|
+
platform: Platform;
|
|
229
|
+
channel: string;
|
|
230
|
+
minBundleId: string;
|
|
231
|
+
}): DatabaseBundleQueryWhere => ({
|
|
232
|
+
platform,
|
|
233
|
+
channel,
|
|
234
|
+
enabled: true,
|
|
235
|
+
id: {
|
|
236
|
+
gte: minBundleId,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const api: DatabaseAPI<TContext> = {
|
|
241
|
+
async getBundleById(
|
|
242
|
+
id: string,
|
|
243
|
+
context?: HotUpdaterContext<TContext>,
|
|
244
|
+
): Promise<Bundle | null> {
|
|
245
|
+
return plugin.getBundleById(id, context);
|
|
246
|
+
},
|
|
35
247
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
248
|
+
async getUpdateInfo(
|
|
249
|
+
args: GetBundlesArgs,
|
|
250
|
+
context?: HotUpdaterContext<TContext>,
|
|
251
|
+
): Promise<UpdateInfo | null> {
|
|
252
|
+
const channel = args.channel ?? "production";
|
|
253
|
+
const minBundleId = args.minBundleId ?? NIL_UUID;
|
|
254
|
+
const baseWhere = getBaseWhere({
|
|
255
|
+
platform: args.platform,
|
|
256
|
+
channel,
|
|
257
|
+
minBundleId,
|
|
40
258
|
});
|
|
41
259
|
|
|
42
|
-
if (
|
|
43
|
-
return
|
|
260
|
+
if (args._updateStrategy === "fingerprint") {
|
|
261
|
+
return findUpdateInfoByScanning({
|
|
262
|
+
args,
|
|
263
|
+
queryWhere: {
|
|
264
|
+
...baseWhere,
|
|
265
|
+
fingerprintHash: args.fingerprintHash,
|
|
266
|
+
},
|
|
267
|
+
context,
|
|
268
|
+
isCandidate: (bundle) => {
|
|
269
|
+
return (
|
|
270
|
+
bundle.enabled &&
|
|
271
|
+
bundle.platform === args.platform &&
|
|
272
|
+
bundle.channel === channel &&
|
|
273
|
+
bundle.id.localeCompare(minBundleId) >= 0 &&
|
|
274
|
+
bundle.fingerprintHash === args.fingerprintHash
|
|
275
|
+
);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
44
278
|
}
|
|
45
279
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
280
|
+
return findUpdateInfoByScanning({
|
|
281
|
+
args,
|
|
282
|
+
queryWhere: {
|
|
283
|
+
...baseWhere,
|
|
284
|
+
},
|
|
285
|
+
context,
|
|
286
|
+
isCandidate: (bundle) => {
|
|
287
|
+
return (
|
|
288
|
+
bundle.enabled &&
|
|
289
|
+
bundle.platform === args.platform &&
|
|
290
|
+
bundle.channel === channel &&
|
|
291
|
+
bundle.id.localeCompare(minBundleId) >= 0 &&
|
|
292
|
+
!!bundle.targetAppVersion &&
|
|
293
|
+
semverSatisfies(bundle.targetAppVersion, args.appVersion)
|
|
294
|
+
);
|
|
295
|
+
},
|
|
50
296
|
});
|
|
51
|
-
|
|
52
|
-
const bundles = data;
|
|
53
|
-
return getUpdateInfoJS(bundles, args);
|
|
54
297
|
},
|
|
55
298
|
|
|
56
299
|
async getAppUpdateInfo(
|
|
57
300
|
args: GetBundlesArgs,
|
|
301
|
+
context?: HotUpdaterContext<TContext>,
|
|
58
302
|
): Promise<AppUpdateInfo | null> {
|
|
59
|
-
const info = await this.getUpdateInfo(args);
|
|
303
|
+
const info = await this.getUpdateInfo(args, context);
|
|
60
304
|
if (!info) {
|
|
61
305
|
return null;
|
|
62
306
|
}
|
|
63
307
|
const { storageUri, ...rest } = info as UpdateInfo & {
|
|
64
308
|
storageUri: string | null;
|
|
65
309
|
};
|
|
66
|
-
const fileUrl = await resolveFileUrl(storageUri ?? null);
|
|
310
|
+
const fileUrl = await resolveFileUrl(storageUri ?? null, context);
|
|
67
311
|
return { ...rest, fileUrl };
|
|
68
312
|
},
|
|
69
313
|
|
|
70
|
-
async getChannels(
|
|
71
|
-
|
|
314
|
+
async getChannels(
|
|
315
|
+
context?: HotUpdaterContext<TContext>,
|
|
316
|
+
): Promise<string[]> {
|
|
317
|
+
return plugin.getChannels(context);
|
|
72
318
|
},
|
|
73
319
|
|
|
74
|
-
async getBundles(options
|
|
75
|
-
|
|
76
|
-
limit: number;
|
|
77
|
-
offset: number;
|
|
78
|
-
}) {
|
|
79
|
-
return plugin.getBundles(options);
|
|
320
|
+
async getBundles(options, context?: HotUpdaterContext<TContext>) {
|
|
321
|
+
return plugin.getBundles(options, context);
|
|
80
322
|
},
|
|
81
323
|
|
|
82
|
-
async insertBundle(
|
|
83
|
-
|
|
84
|
-
|
|
324
|
+
async insertBundle(
|
|
325
|
+
bundle: Bundle,
|
|
326
|
+
context?: HotUpdaterContext<TContext>,
|
|
327
|
+
): Promise<void> {
|
|
328
|
+
await plugin.appendBundle(bundle, context);
|
|
329
|
+
await plugin.commitBundle(context);
|
|
85
330
|
},
|
|
86
331
|
|
|
87
332
|
async updateBundleById(
|
|
88
333
|
bundleId: string,
|
|
89
334
|
newBundle: Partial<Bundle>,
|
|
335
|
+
context?: HotUpdaterContext<TContext>,
|
|
90
336
|
): Promise<void> {
|
|
91
|
-
await plugin.updateBundle(bundleId, newBundle);
|
|
92
|
-
await plugin.commitBundle();
|
|
337
|
+
await plugin.updateBundle(bundleId, newBundle, context);
|
|
338
|
+
await plugin.commitBundle(context);
|
|
93
339
|
},
|
|
94
340
|
|
|
95
|
-
async deleteBundleById(
|
|
96
|
-
|
|
341
|
+
async deleteBundleById(
|
|
342
|
+
bundleId: string,
|
|
343
|
+
context?: HotUpdaterContext<TContext>,
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
const bundle = await plugin.getBundleById(bundleId, context);
|
|
97
346
|
if (!bundle) {
|
|
98
347
|
return;
|
|
99
348
|
}
|
|
100
|
-
await plugin.deleteBundle(bundle);
|
|
101
|
-
await plugin.commitBundle();
|
|
349
|
+
await plugin.deleteBundle(bundle, context);
|
|
350
|
+
await plugin.commitBundle(context);
|
|
102
351
|
},
|
|
103
352
|
};
|
|
104
353
|
|