Skip to content

Commit 8d2c776

Browse files
committed
switch better-sqlite3 --> node:sqlite, which shrinks size and removes a native dependency
1 parent b40ae85 commit 8d2c776

File tree

13 files changed

+263
-309
lines changed

13 files changed

+263
-309
lines changed

src/packages/backend/conat/persist.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ additional persist server:
99
*/
1010

1111
import "./index";
12-
import betterSqlite3 from "better-sqlite3";
12+
import sqlite from "node:sqlite";
1313
import { initContext } from "@cocalc/conat/persist/context";
1414
const { zstdCompressSync, zstdDecompressSync } = require("node:zlib");
1515
import { syncFiles } from "@cocalc/backend/data";
1616
import ensureContainingDirectoryExists from "@cocalc/backend/misc/ensure-containing-directory-exists";
1717
import { statSync, copyFileSync } from "node:fs";
1818

1919
initContext({
20-
betterSqlite3,
20+
sqlite,
2121
compress: zstdCompressSync,
2222
decompress: zstdDecompressSync,
2323
syncFiles,

src/packages/backend/conat/test/persist/backup-archive.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { pathExists } from "fs-extra";
1515
import { join } from "path";
1616
import * as fs from "fs/promises";
1717
import { messageData } from "@cocalc/conat/core/client";
18-
import sqlite from "better-sqlite3";
18+
import { DatabaseSync as sqlite } from "node:sqlite";
1919
import { openPaths } from "@cocalc/conat/persist/storage";
2020

2121
beforeAll(async () => {

src/packages/backend/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"@cocalc/util": "workspace:*",
4646
"@isaacs/ttlcache": "^1.4.1",
4747
"awaiting": "^3.0.0",
48-
"better-sqlite3": "^12.4.1",
4948
"chokidar": "^4.0.3",
5049
"debug": "^4.4.0",
5150
"fs-extra": "^11.3.1",
@@ -68,6 +67,6 @@
6867
"devDependencies": {
6968
"@types/debug": "^4.1.12",
7069
"@types/jest": "^30.0.0",
71-
"@types/node": "^18.16.14"
70+
"@types/node": "^22.10.1"
7271
}
7372
}

src/packages/backend/tcp/enable-messaging-protocol.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Enable two new functions write_mesg and recv_mesg on a TCP socket.
1111

1212
import { Buffer } from "node:buffer";
1313
import { Socket } from "node:net";
14-
1514
import getLogger from "@cocalc/backend/logger";
1615
import { error } from "@cocalc/util/message";
1716
import { from_json_socket, to_json_socket, trunc } from "@cocalc/util/misc";
@@ -147,7 +146,7 @@ export default function enable(socket: CoCalcSocket, desc: string = "") {
147146
return;
148147
} else {
149148
socket.write(length);
150-
socket.write(data, cb);
149+
socket.write(data, "utf8", cb as any);
151150
}
152151
};
153152

src/packages/conat/package.json

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,9 @@
2929
"test": "pnpm exec jest",
3030
"depcheck": "pnpx depcheck --ignores events,bufferutil,utf-8-validate"
3131
},
32-
"files": [
33-
"dist/**",
34-
"README.md",
35-
"package.json"
36-
],
32+
"files": ["dist/**", "README.md", "package.json"],
3733
"author": "SageMath, Inc.",
38-
"keywords": [
39-
"utilities",
40-
"conat",
41-
"cocalc"
42-
],
34+
"keywords": ["utilities", "conat", "cocalc"],
4335
"license": "SEE LICENSE.md",
4436
"dependencies": {
4537
"@cocalc/comm": "workspace:*",
@@ -63,9 +55,8 @@
6355
"utf-8-validate": "^6.0.5"
6456
},
6557
"devDependencies": {
66-
"@types/better-sqlite3": "^7.6.13",
6758
"@types/lodash": "^4.17.20",
68-
"@types/node": "^18.16.14"
59+
"@types/node": "^22.10.1"
6960
},
7061
"repository": {
7162
"type": "git",

src/packages/conat/persist/context.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ not from a browser. Making this explicit helps clarify the dependence
55
on the backend and make the code more unit testable.
66
*/
77

8-
import type BetterSqlite3 from "better-sqlite3";
9-
type Database = BetterSqlite3.Database;
10-
export { type Database };
8+
import type { DatabaseSync } from "node:sqlite";
9+
export type Database = DatabaseSync;
1110

12-
let betterSqlite3: any = null;
11+
let sqliteModule: { DatabaseSync: new (...args: any[]) => DatabaseSync } | null =
12+
null;
1313

1414
export let compress: (data: Buffer) => Buffer = () => {
1515
throw Error("must initialize persist context");
@@ -41,7 +41,7 @@ export let copyFileSync = (_src: string, _desc: string): void => {
4141
};
4242

4343
export function initContext(opts: {
44-
betterSqlite3;
44+
sqlite: { DatabaseSync: new (...args: any[]) => DatabaseSync };
4545
compress: (Buffer) => Buffer;
4646
decompress: (Buffer) => Buffer;
4747
syncFiles: {
@@ -54,7 +54,7 @@ export function initContext(opts: {
5454
statSync: (path: string) => { mtimeMs: number };
5555
copyFileSync: (src: string, desc: string) => void;
5656
}) {
57-
betterSqlite3 = opts.betterSqlite3;
57+
sqliteModule = opts.sqlite;
5858
compress = opts.compress;
5959
decompress = opts.decompress;
6060
syncFiles = opts.syncFiles;
@@ -64,10 +64,10 @@ export function initContext(opts: {
6464
}
6565

6666
export function createDatabase(...args): Database {
67-
if (betterSqlite3 == null) {
67+
if (sqliteModule == null) {
6868
throw Error(
69-
"conat/persist must be initialized with the better-sqlite3 module -- import from backend/conat/persist instead",
69+
"conat/persist must be initialized with node:sqlite -- import from backend/conat/persist instead",
7070
);
7171
}
72-
return new betterSqlite3(...args);
72+
return new sqliteModule.DatabaseSync(...args);
7373
}

src/packages/conat/persist/storage.ts

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ less than 1 read/write per second for each. Thus memory is critical, and
2121
supporting at least 1000 writes/second is what we need.
2222
Fortunately, this implementation can do ~50,000+ writes per second and read
2323
over 500,000 per second. Yes, it blocks the main thread, but by using
24-
better-sqlite3, we get speed increases over async code, so this is worth it.
24+
sync node:sqlite, we get speed increases over async code, so this is worth it.
2525
2626
2727
COMPRESSION:
@@ -69,6 +69,7 @@ import {
6969
decompress,
7070
statSync,
7171
copyFileSync,
72+
ensureContainingDirectoryExists,
7273
} from "./context";
7374
import type { JSONValue } from "@cocalc/util/types";
7475
import { EventEmitter } from "events";
@@ -252,6 +253,7 @@ export interface StorageOptions {
252253
export class PersistentStream extends EventEmitter {
253254
private readonly options: StorageOptions;
254255
private readonly db: Database;
256+
private readonly dbPath?: string;
255257
private readonly msgIDs = new TTL({ ttl: 2 * 60 * 1000 });
256258
private conf: Configuration;
257259
private throttledBackup?;
@@ -266,6 +268,9 @@ export class PersistentStream extends EventEmitter {
266268
const location = this.options.ephemeral
267269
? ":memory:"
268270
: this.options.path + ".db";
271+
if (location !== ":memory:") {
272+
this.dbPath = location;
273+
}
269274
this.initArchive();
270275
this.db = createDatabase(location);
271276
this.initSchema();
@@ -362,18 +367,36 @@ export class PersistentStream extends EventEmitter {
362367
if (path === undefined && !this.options.archive) {
363368
return;
364369
}
365-
path = (path ?? this.options.archive) + ".db";
370+
if (!this.dbPath) {
371+
return;
372+
}
373+
const dest = (path ?? this.options.archive) + ".db";
366374
//console.log("backup", { path });
367375
try {
368-
await this.db.backup(path);
376+
await ensureContainingDirectoryExists(dest);
377+
copyFileSync(this.dbPath, dest);
369378
} catch (err) {
370379
if (!process.env.COCALC_TEST_MODE) {
371380
console.log(err);
372381
}
373-
logger.debug("WARNING: error creating a backup", path, err);
382+
logger.debug("WARNING: error creating a backup", dest, err);
374383
}
375384
});
376385

386+
private runTransaction = <T>(fn: () => T): T => {
387+
this.db.exec("BEGIN");
388+
try {
389+
const result = fn();
390+
this.db.exec("COMMIT");
391+
return result;
392+
} catch (err) {
393+
try {
394+
this.db.exec("ROLLBACK");
395+
} catch {}
396+
throw err;
397+
}
398+
};
399+
377400
private compress = (
378401
raw: Buffer,
379402
): { raw: Buffer; compress: CompressionAlgorithm } => {
@@ -442,30 +465,25 @@ export class PersistentStream extends EventEmitter {
442465

443466
this.enforceLimits(size);
444467

445-
const tx = this.db.transaction(
446-
(time, compress, encoding, raw, headers, key, size, ttl) => {
447-
if (key !== undefined) {
448-
// insert with key -- delete all previous messages, as they will
449-
// never be needed again and waste space.
450-
this.db.prepare("DELETE FROM messages WHERE key = ?").run(key);
451-
}
452-
return this.db
453-
.prepare(
454-
"INSERT INTO messages(time, compress, encoding, raw, headers, key, size, ttl) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING seq",
455-
)
456-
.get(time / 1000, compress, encoding, raw, headers, key, size, ttl);
457-
},
458-
);
459-
const row = tx(
460-
time,
461-
compressedRaw.compress,
462-
encoding,
463-
compressedRaw.raw,
464-
serializedHeaders,
465-
key,
466-
size,
467-
ttl,
468-
);
468+
const row = this.runTransaction(() => {
469+
if (key !== undefined) {
470+
this.db.prepare("DELETE FROM messages WHERE key = ?").run(key);
471+
}
472+
return this.db
473+
.prepare(
474+
"INSERT INTO messages(time, compress, encoding, raw, headers, key, size, ttl) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING seq",
475+
)
476+
.get(
477+
time / 1000,
478+
compressedRaw.compress,
479+
encoding,
480+
compressedRaw.raw,
481+
serializedHeaders,
482+
key ?? null,
483+
size,
484+
ttl ?? null,
485+
);
486+
});
469487
const seq = Number((row as any).seq);
470488
// lastInsertRowid - is a bigint from sqlite, but we won't hit that limit
471489
this.emit("change", {
@@ -572,12 +590,11 @@ export class PersistentStream extends EventEmitter {
572590
this.db.prepare("DELETE FROM messages WHERE seq=?").run(seq);
573591
} else if (seqs0) {
574592
const statement = this.db.prepare("DELETE FROM messages WHERE seq=?");
575-
const transaction = this.db.transaction((seqs) => {
576-
for (const s of seqs) {
593+
this.runTransaction(() => {
594+
for (const s of seqs0) {
577595
statement.run(s);
578596
}
579597
});
580-
transaction(seqs0);
581598
seqs = seqs0;
582599
}
583600
this.emit("change", { op: "delete", seqs });

src/packages/lite/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"build:bundle": "pnpm build:static && cd sea && ./build-bundle.sh",
1818
"build:tarball": "pnpm build:bundle && cd build && tar Jcvf bundle.tar.xz bundle",
1919
"run:bundle": "node ./build/bundle/bundle/index.js",
20-
"sea": "cd sea && ./build-sea.sh",
20+
"sea": "pnpm build:tarball && cd sea && ./build-sea.sh",
2121
"build:all": "pnpm build && pnpm build:tarball && pnpm sea",
2222
"app": "pnpm exec cocalc-lite",
2323
"app-electron": "pnpm electron-rebuild && electron ."

src/packages/lite/sea/build-bundle.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# packages/lite/bin/start.js (which calls @cocalc/lite/main),
1111
# and copies the static frontend assets.
1212
#
13-
# Native addons copied by ncc (e.g. zeromq, better-sqlite3) are preserved.
13+
# Native addons copied by ncc (e.g. zeromq, node-pty) are preserved.
1414
# Additional assets can be copied after this script if needed.
1515

1616
set -euo pipefail

src/packages/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
"axios@<1.12.0": "^1.12.0",
3535
"tmp@<0.2.4": "^0.2.4"
3636
},
37-
"onlyBuiltDependencies": ["better-sqlite3", "node-pty"]
37+
"onlyBuiltDependencies": ["node-pty"]
3838
}
3939
}

0 commit comments

Comments
 (0)