refacto: use @db/sqlite for better usage

This commit is contained in:
Julien Calixte
2026-02-17 15:29:11 +01:00
parent 1c7a4d3a11
commit c9edd63e76
6 changed files with 90 additions and 64 deletions

2
.gitignore vendored
View File

@@ -35,3 +35,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
*.db *.db
*.db-journal *.db-journal
*.db-shm
*.db-wal

View File

@@ -1,12 +1,13 @@
{ {
"tasks": { "tasks": {
"jetstream:prod": "deno run --allow-net --allow-read --allow-write --allow-env jetstream.ts", "jetstream:prod": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi jetstream.ts",
"jetstream": "deno run --watch --allow-net --allow-read --allow-write --allow-env jetstream.ts", "jetstream": "deno run --watch --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi jetstream.ts",
"server": "deno run --watch --allow-net --allow-read --allow-write --allow-env server.ts", "server": "deno run --watch --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi server.ts",
"server:prod": "deno run --allow-net --allow-read --allow-write --allow-env server.ts", "server:prod": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi server.ts",
"migrate": "deno run --allow-read --allow-write --allow-env src/migrations/init.ts" "migrate": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi --unstable-ffi src/migrations/init.ts"
}, },
"imports": { "imports": {
"@db/sqlite": "jsr:@db/sqlite@^0.13.0",
"@oak/oak": "jsr:@oak/oak@^17.2.0", "@oak/oak": "jsr:@oak/oak@^17.2.0",
"@skyware/jetstream": "npm:@skyware/jetstream@^0.2.5", "@skyware/jetstream": "npm:@skyware/jetstream@^0.2.5",
"@std/assert": "jsr:@std/assert@1" "@std/assert": "jsr:@std/assert@1"

44
deno.lock generated
View File

@@ -1,6 +1,9 @@
{ {
"version": "5", "version": "5",
"specifiers": { "specifiers": {
"jsr:@db/sqlite@*": "0.13.0",
"jsr:@db/sqlite@0.13": "0.13.0",
"jsr:@denosaurs/plug@1": "1.1.0",
"jsr:@oak/commons@1": "1.0.0", "jsr:@oak/commons@1": "1.0.0",
"jsr:@oak/oak@^17.2.0": "17.2.0", "jsr:@oak/oak@^17.2.0": "17.2.0",
"jsr:@std/assert@1": "1.0.18", "jsr:@std/assert@1": "1.0.18",
@@ -8,14 +11,35 @@
"jsr:@std/crypto@1": "1.0.3", "jsr:@std/crypto@1": "1.0.3",
"jsr:@std/encoding@1": "1.0.6", "jsr:@std/encoding@1": "1.0.6",
"jsr:@std/encoding@^1.0.5": "1.0.6", "jsr:@std/encoding@^1.0.5": "1.0.6",
"jsr:@std/fmt@1": "1.0.9",
"jsr:@std/fs@1": "1.0.19",
"jsr:@std/http@1": "1.0.12", "jsr:@std/http@1": "1.0.12",
"jsr:@std/internal@^1.0.12": "1.0.12", "jsr:@std/internal@^1.0.12": "1.0.12",
"jsr:@std/internal@^1.0.9": "1.0.12",
"jsr:@std/media-types@1": "1.1.0", "jsr:@std/media-types@1": "1.1.0",
"jsr:@std/path@1": "1.1.4", "jsr:@std/path@1": "1.1.4",
"jsr:@std/path@1.0": "1.0.9",
"jsr:@std/path@^1.1.1": "1.1.4",
"npm:@skyware/jetstream@~0.2.5": "0.2.5", "npm:@skyware/jetstream@~0.2.5": "0.2.5",
"npm:path-to-regexp@^6.3.0": "6.3.0" "npm:path-to-regexp@^6.3.0": "6.3.0"
}, },
"jsr": { "jsr": {
"@db/sqlite@0.13.0": {
"integrity": "4545c635e0b3d4ddfdc0f2240f932f24b8ad0178e9c2e3a0f9403e7b18ae2fb5",
"dependencies": [
"jsr:@denosaurs/plug",
"jsr:@std/path@1.0"
]
},
"@denosaurs/plug@1.1.0": {
"integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044",
"dependencies": [
"jsr:@std/encoding@1",
"jsr:@std/fmt",
"jsr:@std/fs",
"jsr:@std/path@1"
]
},
"@oak/commons@1.0.0": { "@oak/commons@1.0.0": {
"integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac", "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac",
"dependencies": [ "dependencies": [
@@ -35,14 +59,14 @@
"jsr:@std/bytes", "jsr:@std/bytes",
"jsr:@std/http", "jsr:@std/http",
"jsr:@std/media-types", "jsr:@std/media-types",
"jsr:@std/path", "jsr:@std/path@1",
"npm:path-to-regexp" "npm:path-to-regexp"
] ]
}, },
"@std/assert@1.0.18": { "@std/assert@1.0.18": {
"integrity": "270245e9c2c13b446286de475131dc688ca9abcd94fc5db41d43a219b34d1c78", "integrity": "270245e9c2c13b446286de475131dc688ca9abcd94fc5db41d43a219b34d1c78",
"dependencies": [ "dependencies": [
"jsr:@std/internal" "jsr:@std/internal@^1.0.12"
] ]
}, },
"@std/bytes@1.0.4": { "@std/bytes@1.0.4": {
@@ -54,6 +78,16 @@
"@std/encoding@1.0.6": { "@std/encoding@1.0.6": {
"integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069" "integrity": "ca87122c196e8831737d9547acf001766618e78cd8c33920776c7f5885546069"
}, },
"@std/fmt@1.0.9": {
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
},
"@std/fs@1.0.19": {
"integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06",
"dependencies": [
"jsr:@std/internal@^1.0.9",
"jsr:@std/path@^1.1.1"
]
},
"@std/http@1.0.12": { "@std/http@1.0.12": {
"integrity": "85246d8bfe9c8e2538518725b158bdc31f616e0869255f4a8d9e3de919cab2aa", "integrity": "85246d8bfe9c8e2538518725b158bdc31f616e0869255f4a8d9e3de919cab2aa",
"dependencies": [ "dependencies": [
@@ -66,10 +100,13 @@
"@std/media-types@1.1.0": { "@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
}, },
"@std/path@1.0.9": {
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
},
"@std/path@1.1.4": { "@std/path@1.1.4": {
"integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
"dependencies": [ "dependencies": [
"jsr:@std/internal" "jsr:@std/internal@^1.0.12"
] ]
} }
}, },
@@ -156,6 +193,7 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@db/sqlite@0.13",
"jsr:@oak/oak@^17.2.0", "jsr:@oak/oak@^17.2.0",
"jsr:@std/assert@1", "jsr:@std/assert@1",
"npm:@skyware/jetstream@~0.2.5" "npm:@skyware/jetstream@~0.2.5"

View File

@@ -77,5 +77,6 @@ jetstream.start();
setInterval(() => { setInterval(() => {
if (jetstream.cursor) { if (jetstream.cursor) {
saveCursor(jetstream.cursor); saveCursor(jetstream.cursor);
console.log(`[jetstream] updated cursor: ${jetstream.cursor}`);
} }
}, 5000); }, 5000);

View File

@@ -1,33 +1,32 @@
import { DB } from "https://deno.land/x/sqlite/mod.ts"; import { Database } from "@db/sqlite";
import type { Note } from "./note.ts"; import type { Note } from "./note.ts";
export const db = new DB(Deno.env.get("SQLITE_PATH") ?? "notes.db"); export const db = new Database(Deno.env.get("SQLITE_PATH") ?? "notes.db");
try { try {
const [[journalMode]] = db.query<[string]>("PRAGMA journal_mode=WAL"); db.exec("PRAGMA journal_mode=WAL");
db.query("PRAGMA busy_timeout=5000"); db.exec("PRAGMA busy_timeout=5000");
console.log(`[db] journal_mode=${journalMode}, busy_timeout=5000`); const [row] = db.prepare("PRAGMA journal_mode").all<{ journal_mode: string }>();
console.log(`[db] journal_mode=${row.journal_mode}, busy_timeout=5000`);
} catch (e) { } catch (e) {
console.error("[db] failed to set PRAGMAs:", e); console.error("[db] failed to set PRAGMAs:", e);
} }
export const getNotes = (cursor?: string, limit = 20) => { type NoteRow = {
const rows = cursor did: string;
? db.query<[string, string, string, string, string]>( rkey: string;
"SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE rkey < ? ORDER BY rkey DESC LIMIT ?", title: string;
[cursor, limit], publishedAt: string;
) createdAt: string;
: db.query<[string, string, string, string, string]>( };
"SELECT did, rkey, title, publishedAt, createdAt FROM note ORDER BY rkey DESC LIMIT ?",
[limit],
);
const notes = rows.map(([did, rkey, title, publishedAt, createdAt]) => ({ export const getNotes = (cursor?: string, limit = 20) => {
did, const notes = cursor
rkey, ? db.prepare(
title, "SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE rkey < ? ORDER BY rkey DESC LIMIT ?",
publishedAt, ).all<NoteRow>(cursor, limit)
createdAt, : db.prepare(
})); "SELECT did, rkey, title, publishedAt, createdAt FROM note ORDER BY rkey DESC LIMIT ?",
).all<NoteRow>(limit);
return { return {
notes, notes,
@@ -36,23 +35,13 @@ export const getNotes = (cursor?: string, limit = 20) => {
}; };
export const getNotesByDid = (did: string, cursor?: string, limit = 20) => { export const getNotesByDid = (did: string, cursor?: string, limit = 20) => {
const rows = cursor const notes = cursor
? db.query<[string, string, string, string, string]>( ? db.prepare(
"SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE did = ? AND rkey < ? ORDER BY rkey DESC LIMIT ?", "SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE did = ? AND rkey < ? ORDER BY rkey DESC LIMIT ?",
[did, cursor, limit], ).all<NoteRow>(did, cursor, limit)
) : db.prepare(
: db.query<[string, string, string, string, string]>(
"SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE did = ? ORDER BY rkey DESC LIMIT ?", "SELECT did, rkey, title, publishedAt, createdAt FROM note WHERE did = ? ORDER BY rkey DESC LIMIT ?",
[did, limit], ).all<NoteRow>(did, limit);
);
const notes = rows.map(([did, rkey, title, publishedAt, createdAt]) => ({
did,
rkey,
title,
publishedAt,
createdAt,
}));
return { return {
notes, notes,
@@ -61,35 +50,26 @@ export const getNotesByDid = (did: string, cursor?: string, limit = 20) => {
}; };
export const deleteNote = ({ did, rkey }: { did: string; rkey: string }) => { export const deleteNote = ({ did, rkey }: { did: string; rkey: string }) => {
db.query("DELETE FROM note WHERE did = ? AND rkey = ?", [did, rkey]); db.exec("DELETE FROM note WHERE did = ? AND rkey = ?", did, rkey);
}; };
export const getCursor = (): string | undefined => { export const getCursor = (): string | undefined => {
const rows = db.query<[string]>( const row = db.prepare(
"SELECT value FROM state WHERE key = 'cursor'", "SELECT value FROM state WHERE key = 'cursor'",
); ).get<{ value: string }>();
return rows[0]?.[0]; return row?.value;
}; };
export const saveCursor = (cursor: number) => { export const saveCursor = (cursor: number) => {
db.query( db.exec(
"INSERT OR REPLACE INTO state (key, value) VALUES ('cursor', ?)", "INSERT OR REPLACE INTO state (key, value) VALUES ('cursor', ?)",
[String(cursor)], String(cursor),
); );
}; };
export const upsertNote = (note: Note) => { export const upsertNote = (note: Note) => {
const now = new Date().toISOString(); const now = new Date().toISOString();
const params = [ db.exec(
note.title,
note.publishedAt ? new Date(note.publishedAt).toISOString() : now,
note.createdAt ? new Date(note.createdAt).toISOString() : now,
note.did,
note.rkey,
];
console.log(`[db] upsertNote params: ${JSON.stringify(params)}`);
db.query(
` `
INSERT INTO note ( INSERT INTO note (
title, title,
@@ -104,6 +84,10 @@ export const upsertNote = (note: Note) => {
title = excluded.title, title = excluded.title,
publishedAt = excluded.publishedAt publishedAt = excluded.publishedAt
`, `,
params, note.title,
note.publishedAt ? new Date(note.publishedAt).toISOString() : now,
note.createdAt ? new Date(note.createdAt).toISOString() : now,
note.did,
note.rkey,
); );
}; };

View File

@@ -1,6 +1,6 @@
import { db } from "../data/db.ts"; import { db } from "../data/db.ts";
db.execute(` db.exec(`
CREATE TABLE IF NOT EXISTS note ( CREATE TABLE IF NOT EXISTS note (
title TEXT NOT NULL, title TEXT NOT NULL,
publishedAt DATETIME, publishedAt DATETIME,
@@ -11,7 +11,7 @@ db.execute(`
); );
`); `);
db.execute(` db.exec(`
CREATE TABLE IF NOT EXISTS state ( CREATE TABLE IF NOT EXISTS state (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL value TEXT NOT NULL