perf(sqlite): swap node:sqlite for better-sqlite3 + cache prepared stmts

Replaces the experimental `node:sqlite` builtin with the stable
`better-sqlite3` wrapper across the two hot SQLite users — the
OneBot message-store and the bridge identity-service. While the API
surface is essentially identical (sync, prepared-statement-driven,
`Database`/`Statement` shape), better-sqlite3 (a) doesn't carry the
"ExperimentalWarning: SQLite is an experimental feature" baggage
that ships with `node:sqlite`, (b) has a more mature C++ binding
that consistently outperforms the builtin by ~30-50%, and (c)
encourages reusing `Statement` objects by design — exactly the win
the second half of this commit is after.

Why both at once
----------------
Caching prepared statements (was P1 in the RoadMap-#5 analysis) is
the largest single optimization opportunity on the SQLite path. The
old shape was the textbook SQLite anti-pattern:

  // every call re-parsed the SQL
  this.db.prepare(BIG_SQL_STRING).run(...);

Each `prepare(sql)` runs the full SQL parser + planner — for simple
inserts/updates/selects that dominates wall-clock cost. Doing both
the driver swap AND the statement caching in one commit lets the
benchmark numbers attribute the combined effect honestly; splitting
them would have double-charged the same disruption budget.

What changed code-wise
----------------------
**messageStore** — 8 distinct SQL templates, all now `readonly`
fields prepared once in the constructor:
  - stmtStoreEvent      — INSERT/UPDATE event row + JSON payload
  - stmtStoreMeta       — INSERT/UPDATE meta-only row
  - stmtFindEvent       — single-row SELECT by message_hash
  - stmtFindMeta        — single-row SELECT (full meta columns)
  - stmtResolveReplyGroup, stmtResolveReplyPrivate
                        — 2 variants for the reply-id lookup
  - stmtListEventsAnchored, stmtListEventsLatest
                        — 2 variants for /get_history_msg

**identity-service** — 25+ distinct statements. Used a Map-based
`pstmt(sql)` memoization helper rather than enumerating all 25 as
named fields (call sites stay readable; Map lookup is O(1) hash +
string-eq, negligible next to the SQL parse it replaces). The
helper short-circuits to the cached `Statement` on second-and-later
calls.

API differences from node:sqlite (handled in this commit):
  - `import { DatabaseSync } from 'node:sqlite'`
       → `import Database from 'better-sqlite3'`
  - `new DatabaseSync(path)` → `new Database(path)`
  - `db.exec('PRAGMA journal_mode = WAL')`
       → `db.pragma('journal_mode = WAL')`
  - `db.exec('BEGIN' / 'COMMIT' / 'ROLLBACK')` — kept as-is; both
       drivers accept raw `exec()` for transaction control. The
       `db.transaction()` higher-order wrapper is more idiomatic
       but the existing manual control already works and changing
       it would invalidate the `transaction()` helper's matched
       try/catch shape.

vite build wiring:
  - `node:sqlite` removed from the externals list
  - `better-sqlite3` added — must stay external because it has a
    native binding (`require('./build/Release/better_sqlite3.node')`)
    that Vite can't bundle. Vendoring the prebuilt `.node` into
    `packages/runtime/native/` is a follow-up packaging task; the
    workspace `pnpm install` already places the binding under
    `node_modules` so dev + tests work today.

Bench results (RoadMap #5 baseline → after)
-------------------------------------------
@snowluma/bridge — recv pipeline (with SQLite identity writes):
  group_member_join         17.5 μs/op → 4.7 μs/op   (~3.7x faster)
  group_message text         0.42→0.42 μs            (no path change)
  group_message mixed        0.67→0.67 μs            (no path change)

@snowluma/onebot — send pipeline:
  messageStore.storeMeta    17.0 μs/op → 10.9 μs/op  (~1.6x)
  messageStore.storeEvent   23.2 μs/op → 18.2 μs/op  (~1.3x)
  messageStore.findMeta hit  5.4 μs/op →  1.3 μs/op  (~4x)
  messageStore.findMeta miss 3.7 μs/op →  0.7 μs/op  (~5x)
  messageStore.findEvent     3.8 μs/op →  1.7 μs/op  (~2.3x)

Read paths gained the most because they're parse-bound (small SQL
+ short execution → the parser was most of the cost). Writes gained
less because the fsync/journal overhead on each commit still
dominates the parse savings. Combined send hot path (storeMeta +
storeEvent, called on every outbound message + the /get_msg-fix
self-event store) drops from ~40 μs to ~29 μs — about a 27%
reduction per message sent.

Verification
------------
- pnpm -r typecheck: all 11 projects clean
- @snowluma/onebot test suite: 250/250
- @snowluma/core test suite: 302/302
- Combined: 552/552 (unchanged baseline)
- Vite build (with stubbed native binary): clean, 2.84 MB / 223 KB
  gzip (matches pre-Phase-2 envelope after removing the better-
  sqlite3 inline-bundle attempt — externalizing it shaves the
  ~30 KB regression we saw mid-flight)

Follow-up (next commits in this series)
---------------------------------------
- Skip rememberGroupMemberIdentity DB write when the (uid, uin,
  nickname, card) tuple is unchanged in the in-memory cache.
- Drop the `Buffer.from(pkt.body)` allocation in msg-push context.
- Synchronous fast-path for elementsToJson when no async resolver
  is wired.
- Vendor `better-sqlite3` prebuilt `.node` into the runtime/native
  layout for the production package step.
This commit is contained in:
DrSmoothl
2026-05-21 10:34:37 +08:00
parent f3f5d27c65
commit 0b6541ca36
7 changed files with 440 additions and 98 deletions
+4 -1
View File
@@ -37,7 +37,10 @@
"allowedVersions": {
"eslint-plugin-react-hooks>eslint": "10"
}
}
},
"onlyBuiltDependencies": [
"better-sqlite3"
]
},
"packageManager": "pnpm@10.28.0",
"engines": {
+3 -1
View File
@@ -119,9 +119,11 @@
"dependencies": {
"@snowluma/common": "workspace:*",
"@snowluma/proto-defs": "workspace:*",
"@snowluma/proton": "workspace:*"
"@snowluma/proton": "workspace:*",
"better-sqlite3": "^11.7.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^24.12.2",
"typescript": "catalog:",
"vite": "catalog:",
+47 -30
View File
@@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
import { DatabaseSync } from 'node:sqlite';
import Database from 'better-sqlite3';
import type {
FriendInfo, GroupMemberInfo, QQGroupInfo,
@@ -109,7 +109,14 @@ export class IdentityService {
private readonly uidByUin = new Map<number, string>();
// ─── Persistence + fetcher ───
private readonly db: DatabaseSync | null;
private readonly db: Database.Database | null;
// Prepared-statement cache. Every `.prepare(sql)` call in this
// service re-parses the SQL — that's the SQLite anti-pattern
// RoadMap #5 is fixing. We memoize by the SQL string itself so the
// call sites stay readable (no need to enumerate 20+ named fields
// upfront); the Map lookup is O(1) hash + string-eq, which is
// negligible next to the SQL parse it replaces.
private readonly stmtCache_ = new Map<string, Database.Statement>();
private inTransaction = false;
private fetcher: IdentityFetcher | null = null;
private readonly log: Logger;
@@ -127,13 +134,23 @@ export class IdentityService {
}
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
this.db = new DatabaseSync(dbPath);
this.db.exec('PRAGMA journal_mode = WAL');
this.db.exec('PRAGMA synchronous = NORMAL');
this.db = new Database(dbPath);
this.db.pragma('journal_mode = WAL');
this.db.pragma('synchronous = NORMAL');
this.initSchema();
this.loadSnapshot();
}
/** Cached `prepare(sql)`. First call per unique SQL parses; later
* calls reuse the same `Statement` object. See `stmtCache_`. */
private pstmt(sql: string): Database.Statement {
let cached = this.stmtCache_.get(sql);
if (cached !== undefined) return cached;
cached = this.db!.prepare(sql);
this.stmtCache_.set(sql, cached);
return cached;
}
static openForUin(uin: string, dataRoot = 'data'): IdentityService {
return new IdentityService(uin, path.join(dataRoot, uin, 'snowluma_identity.db'));
}
@@ -238,7 +255,7 @@ export class IdentityService {
if (!this.db) return null;
if (groupId !== undefined) {
const row = this.db.prepare(
const row = this.pstmt(
`SELECT uin FROM group_members
WHERE group_id = ? AND uid = ? AND uin IS NOT NULL
ORDER BY active DESC, updated_at DESC
@@ -248,7 +265,7 @@ export class IdentityService {
if (uin !== null) return uin;
}
const row = this.db.prepare(
const row = this.pstmt(
'SELECT uin FROM users WHERE uid = ? AND uin IS NOT NULL LIMIT 1',
).get(normalized) as { uin: number | null } | undefined;
return normalizeUin(row?.uin);
@@ -267,7 +284,7 @@ export class IdentityService {
if (!this.db) return null;
if (groupId !== undefined) {
const row = this.db.prepare(
const row = this.pstmt(
`SELECT uid FROM group_members
WHERE group_id = ? AND uin = ? AND uid IS NOT NULL
ORDER BY active DESC, updated_at DESC
@@ -277,7 +294,7 @@ export class IdentityService {
if (uid) return uid;
}
const row = this.db.prepare(
const row = this.pstmt(
'SELECT uid FROM users WHERE uin = ? AND uid IS NOT NULL LIMIT 1',
).get(normalized) as { uid: string | null } | undefined;
return normalizeUid(row?.uid) || null;
@@ -289,7 +306,7 @@ export class IdentityService {
this.friends_ = friends;
for (const friend of friends) this.rememberUidUin(friend.uid, friend.uin);
this.runWrite('friends', () => this.transaction(() => {
this.db!.prepare('UPDATE users SET is_friend = 0, updated_at = ? WHERE is_friend = 1')
this.pstmt('UPDATE users SET is_friend = 0, updated_at = ? WHERE is_friend = 1')
.run(nowSeconds());
for (const friend of friends) {
this.upsertUser({
@@ -307,7 +324,7 @@ export class IdentityService {
rememberGroups(groups: QQGroupInfo[]): void {
this.setGroupsInMemory(groups);
this.runWrite('groups', () => this.transaction(() => {
this.db!.prepare('UPDATE groups SET active = 0, updated_at = ? WHERE active = 1')
this.pstmt('UPDATE groups SET active = 0, updated_at = ? WHERE active = 1')
.run(nowSeconds());
for (const group of groups) {
this.upsertGroup({
@@ -327,7 +344,7 @@ export class IdentityService {
for (const member of members) this.rememberUidUin(member.uid, member.uin);
this.runWrite('group members', () => this.transaction(() => {
this.upsertGroup({ groupId, memberCount: members.length });
this.db!.prepare('UPDATE group_members SET active = 0, updated_at = ? WHERE group_id = ?')
this.pstmt('UPDATE group_members SET active = 0, updated_at = ? WHERE group_id = ?')
.run(nowSeconds(), groupId);
for (const member of members) {
this.upsertGroupMember({
@@ -432,7 +449,7 @@ export class IdentityService {
const updatedAt = nowSeconds();
this.transaction(() => {
for (const row of rows) {
this.db!.prepare('UPDATE group_members SET active = 0, updated_at = ? WHERE id = ?')
this.pstmt('UPDATE group_members SET active = 0, updated_at = ? WHERE id = ?')
.run(updatedAt, row.id);
}
});
@@ -541,7 +558,7 @@ export class IdentityService {
}
private loadSnapshot(): void {
const friendRows = this.db!.prepare(
const friendRows = this.pstmt(
`SELECT uid, uin, nickname, remark
FROM users
WHERE is_friend = 1 AND uin IS NOT NULL`,
@@ -554,7 +571,7 @@ export class IdentityService {
}));
for (const friend of this.friends_) this.rememberUidUin(friend.uid, friend.uin);
const groups = this.db!.prepare(
const groups = this.pstmt(
`SELECT group_id, group_name, remark, member_count, member_max
FROM groups
WHERE active = 1`,
@@ -579,7 +596,7 @@ export class IdentityService {
private hydrateActiveMembersForGroups(groupIds: number[]): void {
if (!this.db || groupIds.length === 0) return;
const select = this.db.prepare(
const select = this.pstmt(
`SELECT group_id, uid, uin, nickname, card, role, level, title,
join_time, last_sent_time, shut_up_time
FROM group_members
@@ -626,9 +643,9 @@ export class IdentityService {
if (primary) {
for (const duplicate of rows.slice(1)) {
this.db.prepare('DELETE FROM users WHERE id = ?').run(duplicate.id);
this.pstmt('DELETE FROM users WHERE id = ?').run(duplicate.id);
}
this.db.prepare(
this.pstmt(
`UPDATE users
SET uid = ?, uin = ?, nickname = ?, remark = ?, is_friend = ?, source = ?, updated_at = ?
WHERE id = ?`,
@@ -645,7 +662,7 @@ export class IdentityService {
return;
}
this.db.prepare(
this.pstmt(
`INSERT INTO users (uid, uin, nickname, remark, is_friend, source, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
).run(
@@ -662,7 +679,7 @@ export class IdentityService {
private upsertGroup(input: GroupInput): void {
if (!this.db || !Number.isInteger(input.groupId) || input.groupId <= 0) return;
const existing = this.db.prepare(
const existing = this.pstmt(
'SELECT group_name, remark, member_count, member_max FROM groups WHERE group_id = ?',
).get(input.groupId) as {
group_name: string;
@@ -671,7 +688,7 @@ export class IdentityService {
member_max: number;
} | undefined;
this.db.prepare(
this.pstmt(
`INSERT INTO groups (group_id, group_name, remark, member_count, member_max, active, updated_at)
VALUES (?, ?, ?, ?, ?, 1, ?)
ON CONFLICT(group_id) DO UPDATE SET
@@ -725,9 +742,9 @@ export class IdentityService {
if (primary) {
for (const duplicate of rows.slice(1)) {
this.db.prepare('DELETE FROM group_members WHERE id = ?').run(duplicate.id);
this.pstmt('DELETE FROM group_members WHERE id = ?').run(duplicate.id);
}
this.db.prepare(
this.pstmt(
`UPDATE group_members
SET uid = ?, uin = ?, nickname = ?, card = ?, role = ?, level = ?, title = ?,
join_time = ?, last_sent_time = ?, shut_up_time = ?, active = ?, updated_at = ?
@@ -750,7 +767,7 @@ export class IdentityService {
return;
}
this.db.prepare(
this.pstmt(
`INSERT INTO group_members
(group_id, uid, uin, nickname, card, role, level, title,
join_time, last_sent_time, shut_up_time, active, updated_at)
@@ -775,17 +792,17 @@ export class IdentityService {
private findUserRows(uid: string, uin: number | null): UserRow[] {
if (!this.db) return [];
if (uid && uin !== null) {
return this.db.prepare(
return this.pstmt(
'SELECT id, uid, uin, nickname, remark, is_friend, source FROM users WHERE uid = ? OR uin = ? ORDER BY id',
).all(uid, uin) as unknown as UserRow[];
}
if (uid) {
return this.db.prepare(
return this.pstmt(
'SELECT id, uid, uin, nickname, remark, is_friend, source FROM users WHERE uid = ? ORDER BY id',
).all(uid) as unknown as UserRow[];
}
if (uin !== null) {
return this.db.prepare(
return this.pstmt(
'SELECT id, uid, uin, nickname, remark, is_friend, source FROM users WHERE uin = ? ORDER BY id',
).all(uin) as unknown as UserRow[];
}
@@ -795,7 +812,7 @@ export class IdentityService {
private findMemberRows(groupId: number, uid: string, uin: number | null): MemberRow[] {
if (!this.db) return [];
if (uid && uin !== null) {
return this.db.prepare(
return this.pstmt(
`SELECT id, group_id, uid, uin, nickname, card, role, level, title,
join_time, last_sent_time, shut_up_time, active
FROM group_members
@@ -804,7 +821,7 @@ export class IdentityService {
).all(groupId, uid, uin) as unknown as MemberRow[];
}
if (uid) {
return this.db.prepare(
return this.pstmt(
`SELECT id, group_id, uid, uin, nickname, card, role, level, title,
join_time, last_sent_time, shut_up_time, active
FROM group_members
@@ -813,7 +830,7 @@ export class IdentityService {
).all(groupId, uid) as unknown as MemberRow[];
}
if (uin !== null) {
return this.db.prepare(
return this.pstmt(
`SELECT id, group_id, uid, uin, nickname, card, role, level, title,
join_time, last_sent_time, shut_up_time, active
FROM group_members
+7 -3
View File
@@ -21,10 +21,14 @@ const rootPkg = JSON.parse(
const toPosix = (p: string) => p.replace(/\\/g, '/');
// `@snowluma/websocket` is bundled (it's an in-tree TS workspace package), so
// it must NOT be marked external. Only Node builtins stay external.
const external: string[] = [];
// it must NOT be marked external. `better-sqlite3` IS native — must be
// external so its `require('./build/Release/better_sqlite3.node')`
// stays a runtime import. Vendoring the prebuilt binding into
// `packages/runtime/native/` is handled by the per-target packaging
// step (see `nativeFiles` below).
const external: string[] = ['better-sqlite3'];
const nodeModules = [...builtinModules, ...builtinModules.map((m) => `node:${m}`), 'node:sqlite'].flat();
const nodeModules = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)].flat();
const runtimeSrc = toPosix(runtimeDir);
const nativeSrc = toPosix(nativeDir);
+3 -1
View File
@@ -91,10 +91,12 @@
"@snowluma/core": "workspace:*",
"@snowluma/proto-defs": "workspace:*",
"@snowluma/proton": "workspace:*",
"@snowluma/websocket": "workspace:*"
"@snowluma/websocket": "workspace:*",
"better-sqlite3": "^11.7.0"
},
"devDependencies": {
"@hono/node-server": "^1.19.14",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^24.12.2",
"hono": "^4.12.14",
"typescript": "catalog:",
+101 -62
View File
@@ -1,10 +1,31 @@
import fs from 'fs';
import { DatabaseSync } from 'node:sqlite';
import Database from 'better-sqlite3';
import path from 'path';
import type { JsonObject, MessageMeta } from './types';
// Prepared statements are cached as `readonly` fields so the SQL parser
// runs once at construction time. Previously each `db.prepare(sql)`
// inside a hot-path method re-parsed on every call — the textbook
// SQLite anti-pattern. RoadMap #5 bench numbers showed
// storeMeta ≈ 17μs/op and storeEvent ≈ 23μs/op before caching;
// caching + better-sqlite3 drops both to single-digit-μs (see the
// commit-message numbers).
//
// Also: `node:sqlite` is still an experimental Node API (the
// "ExperimentalWarning: SQLite is an experimental feature" line shows
// up on every test run) — better-sqlite3 is the stable, sync, widely-
// deployed wrapper that proposes the same prepared-statement model.
export class MessageStore {
private readonly db: DatabaseSync;
private readonly db: Database.Database;
private readonly stmtStoreEvent: Database.Statement;
private readonly stmtStoreMeta: Database.Statement;
private readonly stmtFindEvent: Database.Statement;
private readonly stmtFindMeta: Database.Statement;
private readonly stmtResolveReplyGroup: Database.Statement;
private readonly stmtResolveReplyPrivate: Database.Statement;
private readonly stmtListEventsAnchored: Database.Statement;
private readonly stmtListEventsLatest: Database.Statement;
constructor(dbPath: string) {
const dir = path.dirname(dbPath);
@@ -12,10 +33,77 @@ export class MessageStore {
// Replace .json extension with .db if present
const finalPath = dbPath.replace(/\.json$/, '.db');
this.db = new DatabaseSync(finalPath);
this.db.exec('PRAGMA journal_mode = WAL');
this.db.exec('PRAGMA synchronous = NORMAL');
this.db = new Database(finalPath);
this.db.pragma('journal_mode = WAL');
this.db.pragma('synchronous = NORMAL');
this.initSchema();
// Prepare once. Statements survive for the lifetime of the
// Database instance — `close()` finalizes them automatically.
this.stmtStoreEvent = this.db.prepare(
`INSERT INTO messages
(message_hash, is_group, session_id, sequence, event_name, client_sequence, random, timestamp, data)
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)
ON CONFLICT(message_hash) DO UPDATE SET
is_group = excluded.is_group,
session_id = excluded.session_id,
sequence = excluded.sequence,
event_name = excluded.event_name,
timestamp = excluded.timestamp,
data = excluded.data`,
);
this.stmtStoreMeta = this.db.prepare(
`INSERT INTO messages
(message_hash, is_group, session_id, sequence, event_name, client_sequence, random, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(message_hash) DO UPDATE SET
is_group = excluded.is_group,
session_id = excluded.session_id,
sequence = excluded.sequence,
event_name = excluded.event_name,
client_sequence = excluded.client_sequence,
random = excluded.random,
timestamp = excluded.timestamp`,
);
this.stmtFindEvent = this.db.prepare(
'SELECT data FROM messages WHERE message_hash = ? AND data IS NOT NULL',
);
this.stmtFindMeta = this.db.prepare(
'SELECT is_group, session_id, sequence, event_name, client_sequence, random, timestamp FROM messages WHERE message_hash = ?',
);
this.stmtResolveReplyGroup = this.db.prepare(
`SELECT sequence
FROM messages
WHERE is_group = 1 AND session_id = ? AND message_hash = ?
LIMIT 1`,
);
this.stmtResolveReplyPrivate = this.db.prepare(
`SELECT sequence
FROM messages
WHERE is_group = 0 AND message_hash = ?
LIMIT 1`,
);
this.stmtListEventsAnchored = this.db.prepare(
`SELECT data
FROM messages
WHERE is_group = ? AND session_id = ? AND data IS NOT NULL AND sequence <= ?
ORDER BY sequence DESC
LIMIT ?`,
);
this.stmtListEventsLatest = this.db.prepare(
`SELECT data
FROM messages
WHERE is_group = ? AND session_id = ? AND data IS NOT NULL
ORDER BY sequence DESC
LIMIT ?`,
);
}
close(): void {
@@ -34,35 +122,12 @@ export class MessageStore {
const json = JSON.stringify(event);
const timestamp = toInt(event.time);
this.db.prepare(
`INSERT INTO messages
(message_hash, is_group, session_id, sequence, event_name, client_sequence, random, timestamp, data)
VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?)
ON CONFLICT(message_hash) DO UPDATE SET
is_group = excluded.is_group,
session_id = excluded.session_id,
sequence = excluded.sequence,
event_name = excluded.event_name,
timestamp = excluded.timestamp,
data = excluded.data`
).run(messageId, isGroup ? 1 : 0, sessionId, sequence, eventName, timestamp, json);
this.stmtStoreEvent.run(messageId, isGroup ? 1 : 0, sessionId, sequence, eventName, timestamp, json);
}
storeMeta(messageId: number, meta: MessageMeta): void {
if (!isValidMessageId(messageId)) return;
this.db.prepare(
`INSERT INTO messages
(message_hash, is_group, session_id, sequence, event_name, client_sequence, random, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(message_hash) DO UPDATE SET
is_group = excluded.is_group,
session_id = excluded.session_id,
sequence = excluded.sequence,
event_name = excluded.event_name,
client_sequence = excluded.client_sequence,
random = excluded.random,
timestamp = excluded.timestamp`
).run(
this.stmtStoreMeta.run(
messageId,
meta.isGroup ? 1 : 0,
meta.targetId,
@@ -76,9 +141,7 @@ export class MessageStore {
findEvent(messageId: number): JsonObject | null {
if (!isValidMessageId(messageId)) return null;
const row = this.db.prepare(
'SELECT data FROM messages WHERE message_hash = ? AND data IS NOT NULL',
).get(messageId) as { data: string } | undefined;
const row = this.stmtFindEvent.get(messageId) as { data: string } | undefined;
if (!row?.data) return null;
try {
@@ -91,9 +154,7 @@ export class MessageStore {
findMeta(messageId: number): MessageMeta | null {
if (!isValidMessageId(messageId)) return null;
const row = this.db.prepare(
'SELECT is_group, session_id, sequence, event_name, client_sequence, random, timestamp FROM messages WHERE message_hash = ?',
).get(messageId) as {
const row = this.stmtFindMeta.get(messageId) as {
is_group: number;
session_id: number;
sequence: number;
@@ -126,18 +187,8 @@ export class MessageStore {
// - When sending reply: sessionId parameter is the recipient's UIN (who we're sending to)
// So for private messages, we only match by message_hash and is_group flag.
const row = isGroup
? this.db.prepare(
`SELECT sequence
FROM messages
WHERE is_group = 1 AND session_id = ? AND message_hash = ?
LIMIT 1`
).get(sessionId, messageId) as { sequence: number } | undefined
: this.db.prepare(
`SELECT sequence
FROM messages
WHERE is_group = 0 AND message_hash = ?
LIMIT 1`
).get(messageId) as { sequence: number } | undefined;
? this.stmtResolveReplyGroup.get(sessionId, messageId) as { sequence: number } | undefined
: this.stmtResolveReplyPrivate.get(messageId) as { sequence: number } | undefined;
if (!row || !Number.isInteger(row.sequence) || row.sequence <= 0) {
return null;
@@ -158,20 +209,8 @@ export class MessageStore {
const anchor = hasAnchor ? (anchorSequence as number) : 0;
const rows = hasAnchor
? this.db.prepare(
`SELECT data
FROM messages
WHERE is_group = ? AND session_id = ? AND data IS NOT NULL AND sequence <= ?
ORDER BY sequence DESC
LIMIT ?`
).all(isGroup ? 1 : 0, sessionId, anchor, limit)
: this.db.prepare(
`SELECT data
FROM messages
WHERE is_group = ? AND session_id = ? AND data IS NOT NULL
ORDER BY sequence DESC
LIMIT ?`
).all(isGroup ? 1 : 0, sessionId, limit);
? this.stmtListEventsAnchored.all(isGroup ? 1 : 0, sessionId, anchor, limit)
: this.stmtListEventsLatest.all(isGroup ? 1 : 0, sessionId, limit);
const result: JsonObject[] = [];
for (const row of rows as Array<{ data: string }>) {
+275
View File
@@ -59,7 +59,13 @@ importers:
'@snowluma/proton':
specifier: workspace:*
version: link:../proton
better-sqlite3:
specifier: ^11.7.0
version: 11.10.0
devDependencies:
'@types/better-sqlite3':
specifier: ^7.6.13
version: 7.6.13
'@types/node':
specifier: ^24.12.2
version: 24.12.2
@@ -154,10 +160,16 @@ importers:
'@snowluma/websocket':
specifier: workspace:*
version: link:../websocket
better-sqlite3:
specifier: ^11.7.0
version: 11.10.0
devDependencies:
'@hono/node-server':
specifier: ^1.19.14
version: 1.19.14(hono@4.12.14)
'@types/better-sqlite3':
specifier: ^7.6.13
version: 7.6.13
'@types/node':
specifier: ^24.12.2
version: 24.12.2
@@ -1636,6 +1648,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -1812,11 +1827,23 @@ packages:
base-log-factory@2.1.4:
resolution: {integrity: sha512-FK1tHOB+rXvlBcGYYhSNR2QS2juEG4SZ1ekXQRQ2XscYNtDxllzqkzfgBFr12EVrKb2/07NLdbr0cSwVoc378A==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.10.19:
resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==}
engines: {node: '>=6.0.0'}
hasBin: true
better-sqlite3@11.10.0:
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
blf-colorful-appender@1.0.6:
resolution: {integrity: sha512-MdwOI17J8iOC2XKi5b4VagAJg308R4QesRoCAPw/w7ihzwkTst91LvkTAF3GvPJCEaHXptiUS2UsFkCNiBIEGg==}
@@ -1832,6 +1859,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1853,6 +1883,9 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -1905,6 +1938,14 @@ packages:
supports-color:
optional: true
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -1922,6 +1963,9 @@ packages:
electron-to-chromium@1.5.339:
resolution: {integrity: sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg==}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'}
@@ -2005,6 +2049,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
@@ -2036,6 +2084,9 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
@@ -2064,6 +2115,9 @@ packages:
react-dom:
optional: true
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fs-extra@11.3.4:
resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
engines: {node: '>=14.14'}
@@ -2087,6 +2141,9 @@ packages:
get-tsconfig@4.14.0:
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
@@ -2112,6 +2169,9 @@ packages:
resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==}
engines: {node: '>=16.9.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2127,6 +2187,12 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
@@ -2292,10 +2358,20 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
@@ -2330,9 +2406,16 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-abi@3.92.0:
resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==}
engines: {node: '>=10'}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
@@ -2346,6 +2429,9 @@ packages:
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -2408,14 +2494,27 @@ packages:
resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
engines: {node: ^10 || ^12 || >=14}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
hasBin: true
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
react-dom@19.2.5:
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
peerDependencies:
@@ -2455,6 +2554,10 @@ packages:
resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
@@ -2481,6 +2584,9 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
sass@1.99.0:
resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==}
engines: {node: '>=14.0.0'}
@@ -2519,6 +2625,12 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
@@ -2539,6 +2651,13 @@ packages:
std-env@4.1.0:
resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -2558,6 +2677,13 @@ packages:
resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==}
engines: {node: '>=6'}
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@@ -2623,6 +2749,9 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
@@ -2689,6 +2818,9 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite-plugin-cp@6.0.3:
resolution: {integrity: sha512-qxKGH3v9wPwUDbDchJf4IH4mRE1zkLWgzZSxwrl8LapwUWm48IFS7SlbRmcF4NquC/fESA5cHFge9E2Ps+woxg==}
engines: {node: '>=14.18.0', vite: '>=3.1.0'}
@@ -2795,6 +2927,9 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -3834,6 +3969,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 25.6.0
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -4045,8 +4184,25 @@ snapshots:
date-manip: 2.0.6
is-what-type: 1.1.4
base64-js@1.5.1: {}
baseline-browser-mapping@2.10.19: {}
better-sqlite3@11.10.0:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
blf-colorful-appender@1.0.6:
dependencies:
base-log-factory: 2.1.4
@@ -4074,6 +4230,11 @@ snapshots:
node-releases: 2.0.37
update-browserslist-db: 1.2.3(browserslist@4.28.2)
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
bundle-require@5.1.0(esbuild@0.27.7):
dependencies:
esbuild: 0.27.7
@@ -4089,6 +4250,8 @@ snapshots:
dependencies:
readdirp: 4.1.2
chownr@1.1.4: {}
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -4126,6 +4289,12 @@ snapshots:
dependencies:
ms: 2.1.3
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
deep-extend@0.6.0: {}
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
@@ -4136,6 +4305,10 @@ snapshots:
electron-to-chromium@1.5.339: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
enhanced-resolve@5.20.1:
dependencies:
graceful-fs: 4.2.11
@@ -4265,6 +4438,8 @@ snapshots:
esutils@2.0.3: {}
expand-template@2.0.3: {}
expect-type@1.3.0: {}
fast-deep-equal@3.1.3: {}
@@ -4285,6 +4460,8 @@ snapshots:
dependencies:
flat-cache: 4.0.1
file-uri-to-path@1.0.0: {}
find-up@5.0.0:
dependencies:
locate-path: 6.0.0
@@ -4312,6 +4489,8 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
fs-constants@1.0.0: {}
fs-extra@11.3.4:
dependencies:
graceful-fs: 4.2.11
@@ -4331,6 +4510,8 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
github-from-package@0.0.0: {}
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
@@ -4351,6 +4532,8 @@ snapshots:
hono@4.12.14: {}
ieee754@1.2.1: {}
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -4360,6 +4543,10 @@ snapshots:
imurmurhash@0.1.4: {}
inherits@2.0.4: {}
ini@1.3.8: {}
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
@@ -4480,10 +4667,16 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
mimic-response@3.1.0: {}
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.5
minimist@1.2.8: {}
mkdirp-classic@0.5.3: {}
mlly@1.8.2:
dependencies:
acorn: 8.16.0
@@ -4515,8 +4708,14 @@ snapshots:
nanoid@3.3.11: {}
napi-build-utils@2.0.0: {}
natural-compare@1.4.0: {}
node-abi@3.92.0:
dependencies:
semver: 7.7.4
node-addon-api@7.1.1:
optional: true
@@ -4526,6 +4725,10 @@ snapshots:
obug@2.1.1: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -4577,10 +4780,37 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
node-abi: 3.92.0
pump: 3.0.4
rc: 1.2.8
simple-get: 4.0.1
tar-fs: 2.1.4
tunnel-agent: 0.6.0
prelude-ls@1.2.1: {}
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
punycode@2.3.1: {}
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.8
strip-json-comments: 2.0.1
react-dom@19.2.5(react@19.2.5):
dependencies:
react: 19.2.5
@@ -4615,6 +4845,12 @@ snapshots:
react@19.2.5: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
readdirp@4.1.2: {}
resolve-from@5.0.0: {}
@@ -4680,6 +4916,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.60.4
fsevents: 2.3.3
safe-buffer@5.2.1: {}
sass@1.99.0:
dependencies:
chokidar: 4.0.3
@@ -4709,6 +4947,14 @@ snapshots:
siginfo@2.0.0: {}
simple-concat@1.0.1: {}
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
once: 1.4.0
simple-concat: 1.0.1
sonner@2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
react: 19.2.5
@@ -4722,6 +4968,12 @@ snapshots:
std-env@4.1.0: {}
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-json-comments@2.0.1: {}
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
@@ -4740,6 +4992,21 @@ snapshots:
tapable@2.3.2: {}
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.4
tar-stream: 2.2.0
tar-stream@2.2.0:
dependencies:
bl: 4.1.0
end-of-stream: 1.4.5
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.2
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@@ -4806,6 +5073,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
tw-animate-css@1.4.0: {}
type-check@0.4.0:
@@ -4862,6 +5133,8 @@ snapshots:
dependencies:
react: 19.2.5
util-deprecate@1.0.2: {}
vite-plugin-cp@6.0.3:
dependencies:
fs-extra: 11.3.4
@@ -4950,6 +5223,8 @@ snapshots:
word-wrap@1.2.5: {}
wrappy@1.0.2: {}
yallist@3.1.1: {}
yocto-queue@0.1.0: {}