@hot-updater/cloudflare 1.0.0-rc.2 → 1.0.0-rc.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/iac/index.cjs +281 -161
- package/dist/iac/index.d.cts +1 -5
- package/dist/iac/index.d.mts +1 -5
- package/dist/iac/index.mjs +277 -157
- package/dist/index.cjs +69 -14
- package/dist/index.d.cts +1 -3
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +67 -12
- package/dist/worker/index.cjs +71 -15
- package/dist/worker/index.d.cts +2 -3
- package/dist/worker/index.d.mts +2 -3
- package/dist/worker/index.mjs +71 -15
- package/package.json +6 -6
- package/sql/bundles.sql +29 -7
- package/src/d1Database.spec.ts +99 -0
- package/src/d1Database.ts +7 -1
- package/src/d1Implementation.spec.ts +90 -0
- package/src/d1Implementation.ts +42 -3
- package/src/d1Query.ts +3 -0
- package/src/d1Rows.spec.ts +32 -0
- package/src/d1Rows.ts +35 -0
- package/src/worker/d1Database.spec.ts +36 -0
- package/src/worker/d1Database.ts +9 -1
- package/worker/dist/README.md +1 -1
- package/worker/dist/index.js +997 -659
- package/worker/dist/index.js.map +4 -4
- package/worker/migrations/0001_hot-updater_1.0.0.sql +30 -13
- package/worker/src/databaseMigration.integration.spec.ts +62 -1
- package/worker/src/databasePlugins.integration.spec.ts +35 -1
- package/worker/src/insightsMovement.integration.spec.ts +73 -0
|
@@ -151,6 +151,27 @@ CREATE TABLE bundle_events (
|
|
|
151
151
|
CONSTRAINT bundle_events_received_at_check CHECK (received_at_ms >= 0)
|
|
152
152
|
);
|
|
153
153
|
|
|
154
|
+
CREATE TABLE bundle_installations (
|
|
155
|
+
install_id TEXT PRIMARY KEY NOT NULL,
|
|
156
|
+
id TEXT NOT NULL,
|
|
157
|
+
user_id TEXT,
|
|
158
|
+
username TEXT,
|
|
159
|
+
to_bundle_id TEXT NOT NULL,
|
|
160
|
+
type TEXT NOT NULL,
|
|
161
|
+
platform TEXT NOT NULL,
|
|
162
|
+
app_version TEXT NOT NULL,
|
|
163
|
+
channel TEXT NOT NULL,
|
|
164
|
+
cohort TEXT NOT NULL,
|
|
165
|
+
received_at_ms REAL NOT NULL,
|
|
166
|
+
CONSTRAINT bundle_installations_type_check CHECK (
|
|
167
|
+
type IN ('UPDATE_APPLIED', 'RECOVERED', 'RELEASE_ADOPTED', 'UNCHANGED')
|
|
168
|
+
),
|
|
169
|
+
CONSTRAINT bundle_installations_platform_check CHECK (
|
|
170
|
+
platform IN ('ios', 'android')
|
|
171
|
+
),
|
|
172
|
+
CONSTRAINT bundle_installations_received_at_check CHECK (received_at_ms >= 0)
|
|
173
|
+
);
|
|
174
|
+
|
|
154
175
|
CREATE TABLE api_keys (
|
|
155
176
|
id TEXT PRIMARY KEY NOT NULL,
|
|
156
177
|
hash TEXT NOT NULL,
|
|
@@ -183,19 +204,15 @@ CREATE INDEX release_catalogs_channel_idx ON release_catalogs(channel_id);
|
|
|
183
204
|
CREATE INDEX bundle_events_received_at_idx
|
|
184
205
|
ON bundle_events(received_at_ms, id);
|
|
185
206
|
CREATE INDEX bundle_events_install_idx
|
|
186
|
-
ON bundle_events(install_id, received_at_ms, id);
|
|
187
|
-
CREATE INDEX
|
|
188
|
-
ON
|
|
189
|
-
CREATE INDEX
|
|
190
|
-
ON
|
|
191
|
-
CREATE INDEX
|
|
192
|
-
|
|
193
|
-
CREATE INDEX
|
|
194
|
-
|
|
195
|
-
CREATE INDEX bundle_events_to_release_idx
|
|
196
|
-
ON bundle_events(type, to_release_id, received_at_ms, id);
|
|
197
|
-
CREATE INDEX bundle_events_from_release_idx
|
|
198
|
-
ON bundle_events(type, from_release_id, received_at_ms, id);
|
|
207
|
+
ON bundle_events(install_id, type, received_at_ms, id);
|
|
208
|
+
CREATE INDEX bundle_installations_user_id_idx
|
|
209
|
+
ON bundle_installations(user_id, install_id);
|
|
210
|
+
CREATE INDEX bundle_installations_received_at_idx
|
|
211
|
+
ON bundle_installations(received_at_ms);
|
|
212
|
+
CREATE INDEX bundle_events_from_bundle_idx ON bundle_events(type, platform, channel, from_bundle_id, received_at_ms, id);
|
|
213
|
+
CREATE INDEX bundle_events_to_bundle_idx ON bundle_events(type, platform, channel, to_bundle_id, received_at_ms, id);
|
|
214
|
+
CREATE INDEX bundle_installations_scope_idx ON bundle_installations(platform, channel, received_at_ms);
|
|
215
|
+
CREATE INDEX bundle_installations_bundle_idx ON bundle_installations(platform, channel, to_bundle_id, received_at_ms);
|
|
199
216
|
CREATE UNIQUE INDEX api_keys_hash_key ON api_keys(hash);
|
|
200
217
|
CREATE INDEX api_keys_created_at_idx
|
|
201
218
|
ON api_keys(created_at_ms, id);
|
|
@@ -10,7 +10,7 @@ declare module "vitest" {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
it("ships a single
|
|
13
|
+
it("ships a single 1.0.0 initialization migration", () => {
|
|
14
14
|
expect(inject("d1Migrations").map(({ name }) => name)).toEqual([
|
|
15
15
|
"0001_hot-updater_1.0.0.sql",
|
|
16
16
|
]);
|
|
@@ -47,6 +47,7 @@ it("creates the current schema with required artifact sizes", async () => {
|
|
|
47
47
|
expect(tables.results.map(({ name }) => name)).toEqual(
|
|
48
48
|
expect.arrayContaining([
|
|
49
49
|
"bundle_events",
|
|
50
|
+
"bundle_installations",
|
|
50
51
|
"bundle_patches",
|
|
51
52
|
"bundles",
|
|
52
53
|
"channels",
|
|
@@ -96,6 +97,66 @@ it("creates the current schema with required artifact sizes", async () => {
|
|
|
96
97
|
byte_size: 3_000_000_002,
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
await env.DB.prepare(`
|
|
101
|
+
INSERT INTO bundle_installations (
|
|
102
|
+
install_id, id, user_id, username, to_bundle_id, type, platform,
|
|
103
|
+
app_version, channel, cohort, received_at_ms
|
|
104
|
+
) VALUES (
|
|
105
|
+
'install-1', 'event-1', 'user-1', 'Demo User',
|
|
106
|
+
'00000000-0000-0000-0000-000000000001', 'UPDATE_APPLIED', 'ios',
|
|
107
|
+
'1.0.0', 'production', 'cohort-1', 100
|
|
108
|
+
)
|
|
109
|
+
`).run();
|
|
110
|
+
await expect(
|
|
111
|
+
env.DB.prepare(
|
|
112
|
+
"SELECT install_id, received_at_ms FROM bundle_installations",
|
|
113
|
+
).first(),
|
|
114
|
+
).resolves.toEqual({ install_id: "install-1", received_at_ms: 100 });
|
|
115
|
+
|
|
116
|
+
const installationIndexes = await env.DB.prepare(`
|
|
117
|
+
SELECT name FROM sqlite_master
|
|
118
|
+
WHERE type = 'index' AND tbl_name = 'bundle_installations'
|
|
119
|
+
ORDER BY name
|
|
120
|
+
`).all<{ name: string }>();
|
|
121
|
+
expect(installationIndexes.results.map(({ name }) => name)).toEqual(
|
|
122
|
+
expect.arrayContaining([
|
|
123
|
+
"bundle_installations_received_at_idx",
|
|
124
|
+
"bundle_installations_user_id_idx",
|
|
125
|
+
"bundle_installations_scope_idx",
|
|
126
|
+
"bundle_installations_bundle_idx",
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const movementIndex = await env.DB.prepare(
|
|
131
|
+
"PRAGMA index_info(bundle_events_install_idx)",
|
|
132
|
+
).all<{ name: string }>();
|
|
133
|
+
expect(movementIndex.results.map(({ name }) => name)).toEqual([
|
|
134
|
+
"install_id",
|
|
135
|
+
"type",
|
|
136
|
+
"received_at_ms",
|
|
137
|
+
"id",
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
for (const [direction, type] of [
|
|
141
|
+
["from", "RECOVERED"],
|
|
142
|
+
["to", "UPDATE_APPLIED"],
|
|
143
|
+
]) {
|
|
144
|
+
const plan = await env.DB.prepare(`
|
|
145
|
+
EXPLAIN QUERY PLAN SELECT * FROM bundle_events
|
|
146
|
+
WHERE type = ? AND platform = 'ios' AND channel = 'production'
|
|
147
|
+
AND ${direction}_bundle_id = ? AND received_at_ms >= 100 AND received_at_ms < 200
|
|
148
|
+
ORDER BY received_at_ms DESC, id DESC LIMIT 101
|
|
149
|
+
`)
|
|
150
|
+
.bind(type, "00000000-0000-0000-0000-000000000001")
|
|
151
|
+
.all<{ detail: string }>();
|
|
152
|
+
expect(plan.results.map(({ detail }) => detail).join("\n")).toContain(
|
|
153
|
+
`bundle_events_${direction}_bundle_idx`,
|
|
154
|
+
);
|
|
155
|
+
expect(plan.results.map(({ detail }) => detail).join("\n")).not.toContain(
|
|
156
|
+
"TEMP B-TREE",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
99
160
|
for (const size of [-1, Number.MAX_SAFE_INTEGER + 1, null]) {
|
|
100
161
|
await expect(
|
|
101
162
|
env.DB.prepare("UPDATE bundles SET archive_byte_size = ?")
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ChannelRow,
|
|
4
4
|
ReleaseRow,
|
|
5
5
|
} from "@hot-updater/plugin-core";
|
|
6
|
+
import { toInsightsInstallationRow } from "@hot-updater/plugin-core";
|
|
6
7
|
import { setupDatabasePluginTestSuite } from "@hot-updater/test-utils";
|
|
7
8
|
import { env } from "cloudflare:test";
|
|
8
9
|
import {
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
vi,
|
|
17
18
|
} from "vitest";
|
|
18
19
|
|
|
20
|
+
import { createBundleEventRowFixture } from "../../../../packages/test-utils/src/databaseTestFixtures";
|
|
19
21
|
import { d1Database } from "../../src/d1Database";
|
|
20
22
|
import { d1Database as d1RuntimeDatabase } from "../../src/worker";
|
|
21
23
|
|
|
@@ -92,7 +94,7 @@ vi.mock("cloudflare", () => ({
|
|
|
92
94
|
const reset = async (): Promise<void> => {
|
|
93
95
|
await getDb()
|
|
94
96
|
.prepare(
|
|
95
|
-
"DELETE FROM bundle_events; DELETE FROM api_keys; DELETE FROM bundle_patches; DELETE FROM release_catalogs; DELETE FROM releases; DELETE FROM bundles; DELETE FROM channels;",
|
|
97
|
+
"DELETE FROM bundle_events; DELETE FROM bundle_installations; DELETE FROM api_keys; DELETE FROM bundle_patches; DELETE FROM release_catalogs; DELETE FROM releases; DELETE FROM bundles; DELETE FROM channels;",
|
|
96
98
|
)
|
|
97
99
|
.run();
|
|
98
100
|
};
|
|
@@ -209,6 +211,38 @@ describe.each([
|
|
|
209
211
|
).resolves.toEqual({ deleted: false, reason: "not_found" });
|
|
210
212
|
});
|
|
211
213
|
|
|
214
|
+
it("rolls back the snapshot when the later event insert fails, then safely retries", async () => {
|
|
215
|
+
const plugin = createPlugin();
|
|
216
|
+
const event = createBundleEventRowFixture("9101", 100);
|
|
217
|
+
const input = { event, installation: toInsightsInstallationRow(event) };
|
|
218
|
+
await env.DB.prepare(`
|
|
219
|
+
CREATE TRIGGER fail_insights_event BEFORE INSERT ON bundle_events
|
|
220
|
+
BEGIN SELECT RAISE(ABORT, 'injected event failure'); END;
|
|
221
|
+
`).run();
|
|
222
|
+
try {
|
|
223
|
+
await expect(plugin.models.insights.record(input)).rejects.toThrow(
|
|
224
|
+
"injected event failure",
|
|
225
|
+
);
|
|
226
|
+
expect(
|
|
227
|
+
(await env.DB.prepare("SELECT * FROM bundle_events").all()).results,
|
|
228
|
+
).toEqual([]);
|
|
229
|
+
expect(
|
|
230
|
+
(await env.DB.prepare("SELECT * FROM bundle_installations").all())
|
|
231
|
+
.results,
|
|
232
|
+
).toEqual([]);
|
|
233
|
+
} finally {
|
|
234
|
+
await env.DB.prepare("DROP TRIGGER fail_insights_event").run();
|
|
235
|
+
}
|
|
236
|
+
await plugin.models.insights.record(input);
|
|
237
|
+
await plugin.models.insights.record(input);
|
|
238
|
+
await expect(
|
|
239
|
+
plugin.models.insights.findInstallations({ installId: event.install_id }),
|
|
240
|
+
).resolves.toEqual([input.installation]);
|
|
241
|
+
expect(
|
|
242
|
+
(await env.DB.prepare("SELECT id FROM bundle_events").all()).results,
|
|
243
|
+
).toEqual([{ id: event.id }]);
|
|
244
|
+
});
|
|
245
|
+
|
|
212
246
|
it("rejects direct and committed deletion while a Release references the channel", async () => {
|
|
213
247
|
const plugin = createPlugin();
|
|
214
248
|
const channel = createChannelRow("active");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { env } from "cloudflare:test";
|
|
2
|
+
import { expect, inject, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { d1Database } from "../../src/worker";
|
|
5
|
+
|
|
6
|
+
it("pages sparse movements beyond 50,000 reports through ordered native index ranges", async () => {
|
|
7
|
+
await env.DB.prepare(inject("prepareSql")).run();
|
|
8
|
+
await env.DB.prepare(`
|
|
9
|
+
WITH RECURSIVE reports(n) AS (
|
|
10
|
+
SELECT 1 UNION ALL SELECT n + 1 FROM reports WHERE n < 60000
|
|
11
|
+
)
|
|
12
|
+
INSERT INTO bundle_events (
|
|
13
|
+
id, type, install_id, from_bundle_id, to_bundle_id, platform,
|
|
14
|
+
app_version, channel, cohort, update_strategy, received_at_ms
|
|
15
|
+
)
|
|
16
|
+
SELECT '00000000-0000-7000-8000-' || printf('%012d', n),
|
|
17
|
+
CASE n % 800 WHEN 0 THEN 'UPDATE_APPLIED' WHEN 1 THEN 'RECOVERED' ELSE 'UNCHANGED' END,
|
|
18
|
+
'sparse-movement-install',
|
|
19
|
+
CASE WHEN n % 800 IN (0, 1) THEN '00000000-0000-7000-8000-000000001001' ELSE NULL END,
|
|
20
|
+
'00000000-0000-7000-8000-000000001002', 'ios', '1.0.0', 'production', '0',
|
|
21
|
+
CASE WHEN n % 800 IN (0, 1) THEN 'appVersion' ELSE NULL END,
|
|
22
|
+
n / 2
|
|
23
|
+
FROM reports
|
|
24
|
+
`).run();
|
|
25
|
+
|
|
26
|
+
const queries: { sql: string; params: readonly unknown[] }[] = [];
|
|
27
|
+
const plugin = d1Database({
|
|
28
|
+
prepare(sql) {
|
|
29
|
+
return {
|
|
30
|
+
bind(...params) {
|
|
31
|
+
queries.push({ sql, params });
|
|
32
|
+
return env.DB.prepare(sql).bind(...params);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
batch: () =>
|
|
37
|
+
Promise.reject(new Error("Unexpected write in movement query")),
|
|
38
|
+
});
|
|
39
|
+
const input = {
|
|
40
|
+
filter: {
|
|
41
|
+
kind: "installationMovement",
|
|
42
|
+
installId: "sparse-movement-install",
|
|
43
|
+
},
|
|
44
|
+
beforeReceivedAtMs: 30001,
|
|
45
|
+
limit: 101,
|
|
46
|
+
} as const;
|
|
47
|
+
const first = await plugin.models.insights.listEvents(input);
|
|
48
|
+
expect(first).toHaveLength(101);
|
|
49
|
+
const last = first[first.length - 1]!;
|
|
50
|
+
const second = await plugin.models.insights.listEvents({
|
|
51
|
+
...input,
|
|
52
|
+
after: { receivedAtMs: last.received_at_ms, id: last.id },
|
|
53
|
+
});
|
|
54
|
+
expect(second).toHaveLength(49);
|
|
55
|
+
const expected = Array.from({ length: 60000 }, (_, index) => index + 1)
|
|
56
|
+
.filter((index) => index % 800 === 0 || index % 800 === 1)
|
|
57
|
+
.reverse()
|
|
58
|
+
.map(
|
|
59
|
+
(index) => `00000000-0000-7000-8000-${String(index).padStart(12, "0")}`,
|
|
60
|
+
);
|
|
61
|
+
expect([...first, ...second].map(({ id }) => id)).toEqual(expected);
|
|
62
|
+
expect(queries.length).toBeLessThanOrEqual(6);
|
|
63
|
+
for (const { sql, params } of queries) {
|
|
64
|
+
expect(sql).toContain("LIMIT");
|
|
65
|
+
expect(params[params.length - 1]).toBe("0");
|
|
66
|
+
const plan = await env.DB.prepare(`EXPLAIN QUERY PLAN ${sql}`)
|
|
67
|
+
.bind(...params)
|
|
68
|
+
.all<{ detail: string }>();
|
|
69
|
+
const details = plan.results.map(({ detail }) => detail).join("\n");
|
|
70
|
+
expect(details).toContain("bundle_events_install_idx");
|
|
71
|
+
expect(details).not.toContain("TEMP B-TREE");
|
|
72
|
+
}
|
|
73
|
+
});
|