Skip to content

Commit 660498f

Browse files
committed
fix(verify): stabilize tree-shaking and sqlite runtime compatibility
1 parent 21dfe57 commit 660498f

File tree

8 files changed

+290
-28
lines changed

8 files changed

+290
-28
lines changed

bun.lock

Lines changed: 112 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
"build": "turbo --filter @evolu/* build",
1717
"build:web": "bun run build:docs && turbo --filter web build",
1818
"build:docs": "bun run docs:generate:api",
19-
"test": "bun run test:preflight && vitest run",
19+
"test": "bun run test:preflight && bunx vitest run",
2020
"test:preflight": "bun ./scripts/ensure-better-sqlite3.mts",
21-
"test:docs": "vitest run scripts/typedoc-plugin-evolu.test.mts",
21+
"test:docs": "bunx vitest run scripts/typedoc-plugin-evolu.test.mts",
2222
"test:coverage": "bun run test:coverage:vitest && bun run test:coverage:bun",
23-
"test:coverage:vitest": "bun run test:preflight && vitest run --coverage",
23+
"test:coverage:vitest": "bun run test:preflight && bunx vitest run --coverage",
2424
"test:coverage:bun": "bun test ./packages/bun/test --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir=coverage/bun",
25-
"test:watch": "vitest",
25+
"test:watch": "bunx vitest",
2626
"start": "turbo start",
2727
"lint": "biome check",
2828
"lint-monorepo": "bunx sherif@1.6.1",

packages/common/src/local-first/Evolu.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,9 @@ export const createEvolu =
895895
[Symbol.asyncDispose]: () => {
896896
console.info("disposeEvolu");
897897
exportDatabaseDeferred?.[Symbol.dispose]();
898+
// Cancel queued microtask batches before sending dispose.
899+
mutateBatch[Symbol.dispose]();
900+
queryBatch[Symbol.dispose]();
898901
postMessage({ type: "Dispose" });
899902
return moved.disposeAsync();
900903
},

packages/common/test/TreeShaking.test.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ if (!Promise.try) {
3939
}
4040
});
4141
}
42-
require(process.argv[1]);
42+
const { pathToFileURL } = require("node:url");
43+
(async () => {
44+
const fileUrl = pathToFileURL(process.argv[1]).href;
45+
await import(fileUrl);
46+
})().catch((error) => {
47+
console.error(error);
48+
process.exitCode = 1;
49+
});
4350
`;
4451

4552
const result = spawnSync(process.execPath, ["-e", bootstrap, bundlePath], {
@@ -102,21 +109,25 @@ const bundleSize = async (fixturePath: string): Promise<BundleSize> => {
102109
return await new Promise((resolve, reject) => {
103110
compiler.run((err: Error | null, stats: Stats | undefined) => {
104111
compiler.close(() => {
105-
if (err) {
106-
reject(err);
107-
return;
108-
}
109-
if (stats?.hasErrors()) {
110-
reject(new Error(stats.toString()));
111-
return;
112+
try {
113+
if (err) {
114+
reject(err);
115+
return;
116+
}
117+
if (stats?.hasErrors()) {
118+
reject(new Error(stats.toString()));
119+
return;
120+
}
121+
const bundlePath = join(outputDir, "bundle.js");
122+
runBundle(bundlePath);
123+
const bundle = readFileSync(bundlePath);
124+
resolve({
125+
raw: bundle.byteLength,
126+
gzip: gzipSync(bundle).byteLength,
127+
});
128+
} catch (error) {
129+
reject(error);
112130
}
113-
const bundlePath = join(outputDir, "bundle.js");
114-
runBundle(bundlePath);
115-
const bundle = readFileSync(bundlePath);
116-
resolve({
117-
raw: bundle.byteLength,
118-
gzip: gzipSync(bundle).byteLength,
119-
});
120131
});
121132
});
122133
});

packages/common/test/_deps.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { timingSafeEqual } from "node:crypto";
1+
import { randomUUID, timingSafeEqual } from "node:crypto";
2+
import { readFileSync, rmSync } from "node:fs";
23
import { createRequire } from "node:module";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
36
import { assert } from "../src/Assert.js";
47
import type { TimingSafeEqual } from "../src/Crypto.js";
58
import { lazyTrue, lazyVoid } from "../src/Function.js";
@@ -121,9 +124,46 @@ interface BunSqliteModule {
121124
readonly Database: new (filename: string) => BunSqliteDbLike;
122125
}
123126

127+
interface NodeSqliteStatementLike {
128+
readonly all: (...parameters: ReadonlyArray<SqliteValue>) => Array<SqliteRow>;
129+
readonly run: (...parameters: ReadonlyArray<SqliteValue>) => {
130+
readonly changes?: number;
131+
};
132+
}
133+
134+
interface NodeSqliteDbLike {
135+
readonly prepare: (sql: string) => NodeSqliteStatementLike;
136+
readonly exec: (sql: string) => void;
137+
readonly close: () => void;
138+
}
139+
140+
interface NodeSqliteModule {
141+
readonly DatabaseSync: new (filename: string) => NodeSqliteDbLike;
142+
}
143+
124144
const isReaderSql = (sql: string): boolean =>
125145
/^\s*(select|pragma|with|explain|values)\b/i.test(sql);
126146

147+
const sqliteEscape = (value: string): string => value.replaceAll("'", "''");
148+
149+
const serializeToBytes = (exec: (sql: string) => void): Uint8Array => {
150+
const path = join(tmpdir(), `evolu-test-export-${randomUUID()}.db`);
151+
152+
try {
153+
exec(`vacuum into '${sqliteEscape(path)}'`);
154+
const file = readFileSync(path);
155+
const { buffer } = file;
156+
157+
if (buffer instanceof ArrayBuffer) {
158+
return new Uint8Array(buffer, file.byteOffset, file.byteLength);
159+
}
160+
161+
return new Uint8Array(file);
162+
} finally {
163+
rmSync(path, { force: true });
164+
}
165+
};
166+
127167
const createDb = (filename: string): DbLike => {
128168
try {
129169
const BetterSQLite = require("better-sqlite3") as BetterSqliteConstructor;
@@ -141,10 +181,9 @@ const createDb = (filename: string): DbLike => {
141181
serialize: () => db.serialize(),
142182
close: () => db.close(),
143183
};
144-
} catch (error) {
145-
const hasBunRuntime = (globalThis as Record<string, unknown>).Bun != null;
146-
if (!hasBunRuntime) throw error;
184+
} catch {}
147185

186+
try {
148187
const { Database } = require("bun:sqlite") as BunSqliteModule;
149188
const db = new Database(filename);
150189

@@ -160,7 +199,25 @@ const createDb = (filename: string): DbLike => {
160199
serialize: () => db.serialize(),
161200
close: () => db.close(),
162201
};
163-
}
202+
} catch {}
203+
204+
const { DatabaseSync } = require("node:sqlite") as NodeSqliteModule;
205+
const db = new DatabaseSync(filename);
206+
207+
return {
208+
prepare: (sql) => {
209+
const statement = db.prepare(sql);
210+
return {
211+
reader: isReaderSql(sql),
212+
all: (...parameters) => statement.all(...parameters),
213+
run: (...parameters) => ({
214+
changes: statement.run(...parameters).changes ?? 0,
215+
}),
216+
};
217+
},
218+
serialize: () => serializeToBytes((sql) => db.exec(sql)),
219+
close: () => db.close(),
220+
};
164221
};
165222

166223
// Duplicated from @evolu/nodejs because @evolu/common cannot depend on it

packages/common/test/local-first/Timestamp.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ type BetterSqliteConstructor = new () => BetterSqliteDbLike;
5050

5151
const BetterSQLite = (() => {
5252
try {
53-
return require("better-sqlite3") as BetterSqliteConstructor;
53+
const SQLite = require("better-sqlite3") as BetterSqliteConstructor;
54+
const db = new SQLite();
55+
db.prepare("select 1 as one").all();
56+
return SQLite;
5457
} catch {
5558
return null;
5659
}

packages/nodejs/src/Sqlite.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import { randomUUID } from "node:crypto";
2+
import { readFileSync, rmSync } from "node:fs";
13
import { createRequire } from "node:module";
4+
import { tmpdir } from "node:os";
5+
import { join } from "node:path";
26
import {
37
type CreateSqliteDriver,
48
createPreparedStatementsCache,
@@ -58,9 +62,46 @@ interface BunSqliteModule {
5862
readonly Database: new (filename: string) => BunSqliteDbLike;
5963
}
6064

65+
interface NodeSqliteStatementLike {
66+
readonly all: (...parameters: ReadonlyArray<SqliteValue>) => Array<SqliteRow>;
67+
readonly run: (...parameters: ReadonlyArray<SqliteValue>) => {
68+
readonly changes?: number;
69+
};
70+
}
71+
72+
interface NodeSqliteDbLike {
73+
readonly prepare: (sql: string) => NodeSqliteStatementLike;
74+
readonly exec: (sql: string) => void;
75+
readonly close: () => void;
76+
}
77+
78+
interface NodeSqliteModule {
79+
readonly DatabaseSync: new (filename: string) => NodeSqliteDbLike;
80+
}
81+
6182
const isReaderSql = (sql: string): boolean =>
6283
/^\s*(select|pragma|with|explain|values)\b/i.test(sql);
6384

85+
const sqliteEscape = (value: string): string => value.replaceAll("'", "''");
86+
87+
const serializeToBytes = (exec: (sql: string) => void): Uint8Array => {
88+
const path = join(tmpdir(), `evolu-export-${randomUUID()}.db`);
89+
90+
try {
91+
exec(`vacuum into '${sqliteEscape(path)}'`);
92+
const file = readFileSync(path);
93+
const { buffer } = file;
94+
95+
if (buffer instanceof ArrayBuffer) {
96+
return new Uint8Array(buffer, file.byteOffset, file.byteLength);
97+
}
98+
99+
return new Uint8Array(file);
100+
} finally {
101+
rmSync(path, { force: true });
102+
}
103+
};
104+
64105
const createBetterDb = (filename: string): DbLike => {
65106
const BetterSQLite = require("better-sqlite3") as BetterSqliteConstructor;
66107
const db = new BetterSQLite(filename);
@@ -97,6 +138,26 @@ const createBunDb = (filename: string): DbLike => {
97138
};
98139
};
99140

141+
const createNodeDb = (filename: string): DbLike => {
142+
const { DatabaseSync } = require("node:sqlite") as NodeSqliteModule;
143+
const db = new DatabaseSync(filename);
144+
145+
return {
146+
prepare: (sql) => {
147+
const statement = db.prepare(sql);
148+
return {
149+
reader: isReaderSql(sql),
150+
all: (...parameters) => statement.all(...parameters),
151+
run: (...parameters) => ({
152+
changes: statement.run(...parameters).changes ?? 0,
153+
}),
154+
};
155+
},
156+
serialize: () => serializeToBytes((sql) => db.exec(sql)),
157+
close: () => db.close(),
158+
};
159+
};
160+
100161
const createDb = (filename: string): DbLike => {
101162
const hasBunRuntime = (globalThis as Record<string, unknown>).Bun != null;
102163

@@ -107,12 +168,24 @@ const createDb = (filename: string): DbLike => {
107168
try {
108169
return createBetterDb(filename);
109170
} catch {
110-
throw bunError;
171+
try {
172+
return createNodeDb(filename);
173+
} catch {
174+
throw bunError;
175+
}
111176
}
112177
}
113178
}
114179

115-
return createBetterDb(filename);
180+
try {
181+
return createBetterDb(filename);
182+
} catch (betterError) {
183+
try {
184+
return createNodeDb(filename);
185+
} catch {
186+
throw betterError;
187+
}
188+
}
116189
};
117190

118191
export const createBetterSqliteDriver: CreateSqliteDriver =

packages/nodejs/test/Sqlite.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ type BetterSqliteConstructor = new (
2323

2424
const BetterSQLite = (() => {
2525
try {
26-
return require("better-sqlite3") as BetterSqliteConstructor;
26+
const SQLite = require("better-sqlite3") as BetterSqliteConstructor;
27+
const db = new SQLite(":memory:");
28+
db.close();
29+
return SQLite;
2730
} catch {
2831
return null;
2932
}

0 commit comments

Comments
 (0)