Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/core/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ function buildShard(db: SqliteDatabase, sessionId: string, tables: string[]): Se
formatVersion: 1,
session: sessionRow,
tables: {},
exportedAt: Date.now(),
// Derived from the session itself, never wall-clock time. A repeat export
// of an unchanged session must produce identical bytes, otherwise every
// push rewrites every shard and import/pull churn is unavoidable.
exportedAt: Number(sessionRow.time_updated ?? 0),
};

const projectId = sessionRow.project_id;
Expand Down
23 changes: 21 additions & 2 deletions src/core/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* disabled with a clear message rather than failing halfway through.
*/

import { createRequire } from "node:module";

export interface SqliteStatement {
all(...params: unknown[]): any[];
run(...params: unknown[]): unknown;
Expand All @@ -39,6 +41,23 @@ async function importBunSqlite(): Promise<any> {
return import(/* @vite-ignore */ /* webpackIgnore: true */ BUN_SQLITE);
}

/**
* Load the Node built-in SQLite driver.
*
* The plain dynamic import is what works under Node and Bun. Test runners such
* as vitest intercept dynamic imports and try to resolve `node:sqlite` as a
* project file, which makes the driver undetectable and silently turns every
* session test into an early return. `createRequire` bypasses the bundler and
* loads the real builtin, so those tests actually run.
*/
async function importNodeSqlite(): Promise<any> {
try {
return await import("node:sqlite");
} catch {
return createRequire(import.meta.url)("node:sqlite");
}
}

let cachedDriver: "bun" | "node" | "none" | undefined;

async function detectDriver(): Promise<"bun" | "node" | "none"> {
Expand All @@ -53,7 +72,7 @@ async function detectDriver(): Promise<"bun" | "node" | "none"> {
}
}
try {
const mod: any = await import("node:sqlite");
const mod: any = await importNodeSqlite();
if (mod?.DatabaseSync) {
cachedDriver = "node";
return cachedDriver;
Expand Down Expand Up @@ -102,7 +121,7 @@ export async function openDatabase(
}

if (driver === "node") {
const { DatabaseSync } = (await import("node:sqlite")) as any;
const { DatabaseSync } = (await importNodeSqlite()) as any;
const db = new DatabaseSync(file, options.readOnly ? { readOnly: true } : {});
return {
prepare: (sql: string) => {
Expand Down
7 changes: 4 additions & 3 deletions tests/sessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,14 @@ describe("export and import", () => {

await exportSessions(source, repo, { ...settings(), now }, silentReporter);
const shard = path.join(repo, "_sessions", "ses_recent.json.gz");
const before = fs.statSync(shard).mtimeMs;
const before = fs.readFileSync(shard);

await new Promise((resolve) => setTimeout(resolve, 20));
await exportSessions(source, repo, { ...settings(), now }, silentReporter);

// An unchanged session must not produce a new diff on every push.
expect(fs.statSync(shard).mtimeMs).toBe(before);
// An unchanged session must not produce a new diff on every push — same
// bytes, same mtime, no diff for git to pick up.
expect(fs.readFileSync(shard).equals(before)).toBe(true);
});
});

Expand Down