feat(daemon): Sense Runtime — Worker, IPC, Migrations, Peer Isolation #9
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
|
||||
"files": {
|
||||
"ignore": ["**/dist/**"]
|
||||
"ignore": ["**/dist/**", "false/**", "**/node_modules/**"]
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { loadavg } from "node:os";
|
||||
|
||||
import type { DrizzleDB, PeerMap } from "@uncaged/nerve-daemon";
|
||||
|
||||
import { samples } from "./schema.js";
|
||||
|
||||
/**
|
||||
* Read the 1-minute CPU load average, persist it, and emit a Signal.
|
||||
*
|
||||
* Returns `null` only if `loadavg` is unavailable (non-POSIX platforms).
|
||||
* On every successful read a row is inserted and the value is returned,
|
||||
* which causes the engine to emit a Signal.
|
||||
*/
|
||||
export async function compute(db: DrizzleDB, _peers: PeerMap): Promise<number | null> {
|
||||
const [oneMin] = loadavg();
|
||||
|
||||
if (typeof oneMin !== "number" || Number.isNaN(oneMin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await db.insert(samples).values({ ts: Date.now(), value: oneMin });
|
||||
return oneMin;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Migration: 0001_init
|
||||
-- Creates the samples table for the cpu-usage sense.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS samples (
|
||||
ts INTEGER PRIMARY KEY,
|
||||
value REAL NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { integer, real, sqliteTable } from "drizzle-orm/sqlite-core";
|
||||
|
||||
/**
|
||||
* Each row records one CPU load sample.
|
||||
* `ts` is the Unix timestamp in milliseconds (primary key, append-only).
|
||||
* `value` is the 1-minute load average from os.loadavg()[0].
|
||||
*/
|
||||
export const samples = sqliteTable("samples", {
|
||||
ts: integer("ts").primaryKey(),
|
||||
value: real("value").notNull(),
|
||||
});
|
||||
@@ -290,17 +290,11 @@ export function parseNerveConfig(raw: string): Result<NerveConfig> {
|
||||
if (!workflowsResult.ok) return workflowsResult;
|
||||
|
||||
// Cross-validate: workflow reflexes must reference defined workflows
|
||||
const workflowNames = new Set(
|
||||
workflowsResult.value ? Object.keys(workflowsResult.value) : [],
|
||||
);
|
||||
const workflowNames = new Set(workflowsResult.value ? Object.keys(workflowsResult.value) : []);
|
||||
for (let i = 0; i < reflexesResult.value.length; i++) {
|
||||
const reflex = reflexesResult.value[i];
|
||||
if (reflex.kind === "workflow" && !workflowNames.has(reflex.workflow)) {
|
||||
return err(
|
||||
new Error(
|
||||
`reflexes[${i}].workflow: "${reflex.workflow}" not found in workflows`,
|
||||
),
|
||||
);
|
||||
return err(new Error(`reflexes[${i}].workflow: "${reflex.workflow}" not found in workflows`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup"
|
||||
"build": "tsup",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/nerve-core": "workspace:*",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"vitest": "^4.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { integer, real, sqliteTable } from "drizzle-orm/sqlite-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseParentMessage } from "../ipc.js";
|
||||
import { executeCompute, openPeerDb, openSenseDb, runMigrations } from "../sense-runtime.js";
|
||||
import type { DrizzleDB, PeerMap, SenseRuntime } from "../sense-runtime.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const INIT_SQL = `
|
||||
CREATE TABLE IF NOT EXISTS samples (
|
||||
ts INTEGER PRIMARY KEY,
|
||||
value REAL NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
function makeTempMigrationsDir(sql: string): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "nerve-test-"));
|
||||
writeFileSync(join(dir, "0001_init.sql"), sql);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function makeTempMigrationsDirEmpty(): string {
|
||||
return mkdtempSync(join(tmpdir(), "nerve-test-empty-"));
|
||||
}
|
||||
|
||||
function makeTempDbPath(): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "nerve-db-"));
|
||||
return join(dir, "test.db");
|
||||
}
|
||||
|
||||
const samples = sqliteTable("samples", {
|
||||
ts: integer("ts").primaryKey(),
|
||||
value: real("value").notNull(),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// runMigrations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("runMigrations", () => {
|
||||
it("creates table via SQL migration file", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
const migrationsDir = makeTempMigrationsDir(INIT_SQL);
|
||||
const result = runMigrations(sqlite, migrationsDir);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
const row = sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='samples'")
|
||||
.get();
|
||||
expect(row).toBeDefined();
|
||||
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("runs multiple migrations in lexicographic order", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
const dir = mkdtempSync(join(tmpdir(), "nerve-multi-"));
|
||||
|
||||
writeFileSync(join(dir, "0001_init.sql"), INIT_SQL);
|
||||
writeFileSync(join(dir, "0002_add_col.sql"), "ALTER TABLE samples ADD COLUMN label TEXT;");
|
||||
|
||||
const result = runMigrations(sqlite, dir);
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
const info = sqlite.prepare("PRAGMA table_info(samples)").all() as Array<{ name: string }>;
|
||||
const cols = info.map((r) => r.name);
|
||||
expect(cols).toContain("label");
|
||||
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("returns ok when migrations directory is empty", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
const dir = makeTempMigrationsDirEmpty();
|
||||
const result = runMigrations(sqlite, dir);
|
||||
expect(result.ok).toBe(true);
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("returns err when migrations directory does not exist", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
const result = runMigrations(sqlite, "/nonexistent/path/migrations");
|
||||
expect(result.ok).toBe(false);
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("returns err when a migration SQL is invalid", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
const dir = mkdtempSync(join(tmpdir(), "nerve-bad-sql-"));
|
||||
writeFileSync(join(dir, "0001_bad.sql"), "NOT VALID SQL !!!;");
|
||||
const result = runMigrations(sqlite, dir);
|
||||
expect(result.ok).toBe(false);
|
||||
sqlite.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// openSenseDb
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("openSenseDb", () => {
|
||||
it("creates the db file and runs migrations", () => {
|
||||
const dbPath = makeTempDbPath();
|
||||
const migrationsDir = makeTempMigrationsDir(INIT_SQL);
|
||||
|
||||
const result = openSenseDb(dbPath, migrationsDir);
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
if (!result.ok) return;
|
||||
const { sqlite } = result.value;
|
||||
const row = sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='samples'")
|
||||
.get();
|
||||
expect(row).toBeDefined();
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("returns err when migrations dir is missing", () => {
|
||||
const dbPath = makeTempDbPath();
|
||||
const result = openSenseDb(dbPath, "/nonexistent/migrations");
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// openPeerDb
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("openPeerDb", () => {
|
||||
it("opens an existing db in read-only mode", () => {
|
||||
// Create a writable db first
|
||||
const dbPath = makeTempDbPath();
|
||||
const sqlite = new Database(dbPath);
|
||||
sqlite.exec(INIT_SQL);
|
||||
sqlite.prepare("INSERT INTO samples (ts, value) VALUES (1, 42.0)").run();
|
||||
sqlite.close();
|
||||
|
||||
const result = openPeerDb(dbPath);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
|
||||
// Should be able to read
|
||||
const peerDb = result.value;
|
||||
const rows = peerDb.select().from(samples).all();
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].value).toBe(42.0);
|
||||
});
|
||||
|
||||
it("returns err when db path does not exist", () => {
|
||||
const result = openPeerDb("/nonexistent/path/to/peer.db");
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// executeCompute
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("executeCompute", () => {
|
||||
function makeRuntime(computeFn: (db: DrizzleDB, peers: PeerMap) => Promise<unknown | null>): {
|
||||
runtime: SenseRuntime;
|
||||
sqlite: Database.Database;
|
||||
} {
|
||||
const sqlite = new Database(":memory:");
|
||||
sqlite.exec(INIT_SQL);
|
||||
const db = drizzle(sqlite) as DrizzleDB;
|
||||
return {
|
||||
runtime: { name: "test-sense", db, compute: computeFn },
|
||||
sqlite,
|
||||
};
|
||||
}
|
||||
|
||||
const emptyPeers: PeerMap = {};
|
||||
|
||||
it("returns the payload when compute returns a non-null value", async () => {
|
||||
const { runtime, sqlite } = makeRuntime(async (db) => {
|
||||
await db.insert(samples).values({ ts: Date.now(), value: 0.5 });
|
||||
return 0.5;
|
||||
});
|
||||
|
||||
const result = await executeCompute(runtime, emptyPeers);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value).toBe(0.5);
|
||||
|
||||
const rows = sqlite.prepare("SELECT * FROM samples").all();
|
||||
expect(rows).toHaveLength(1);
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("returns null (no signal) when compute returns null", async () => {
|
||||
const { runtime, sqlite } = makeRuntime(async () => null);
|
||||
|
||||
const result = await executeCompute(runtime, emptyPeers);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value).toBeNull();
|
||||
|
||||
const rows = sqlite.prepare("SELECT * FROM samples").all();
|
||||
expect(rows).toHaveLength(0);
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("returns err when compute throws", async () => {
|
||||
const { runtime, sqlite } = makeRuntime(async () => {
|
||||
throw new Error("something went wrong");
|
||||
});
|
||||
|
||||
const result = await executeCompute(runtime, emptyPeers);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.message).toContain("something went wrong");
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("compute can read from peers", async () => {
|
||||
// Set up a peer db with data
|
||||
const peerSqlite = new Database(":memory:");
|
||||
peerSqlite.exec(INIT_SQL);
|
||||
peerSqlite.prepare("INSERT INTO samples (ts, value) VALUES (100, 3.14)").run();
|
||||
const peerDb = drizzle(peerSqlite) as DrizzleDB;
|
||||
|
||||
const peers: PeerMap = { "other-sense": peerDb };
|
||||
|
||||
const { runtime, sqlite } = makeRuntime(async (_db, p) => {
|
||||
const rows = await p["other-sense"].select().from(samples).all();
|
||||
return rows.length > 0 ? rows[0].value : null;
|
||||
});
|
||||
|
||||
const result = await executeCompute(runtime, peers);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value).toBe(3.14);
|
||||
|
||||
peerSqlite.close();
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("write to own db does not affect peer db (isolation)", async () => {
|
||||
const peerSqlite = new Database(":memory:");
|
||||
peerSqlite.exec(INIT_SQL);
|
||||
const peerDb = drizzle(peerSqlite) as DrizzleDB;
|
||||
const peers: PeerMap = { "peer-sense": peerDb };
|
||||
|
||||
const { runtime, sqlite } = makeRuntime(async (db) => {
|
||||
await db.insert(samples).values({ ts: 999, value: 9.9 });
|
||||
return 9.9;
|
||||
});
|
||||
|
||||
await executeCompute(runtime, peers);
|
||||
|
||||
const peerRows = peerSqlite.prepare("SELECT * FROM samples").all();
|
||||
expect(peerRows).toHaveLength(0);
|
||||
|
||||
const ownRows = sqlite.prepare("SELECT * FROM samples").all();
|
||||
expect(ownRows).toHaveLength(1);
|
||||
|
||||
peerSqlite.close();
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("inserts correctly into the sense db directory path", async () => {
|
||||
const dbPath = makeTempDbPath();
|
||||
const migrationsDir = makeTempMigrationsDir(INIT_SQL);
|
||||
const dbResult = openSenseDb(dbPath, migrationsDir);
|
||||
expect(dbResult.ok).toBe(true);
|
||||
if (!dbResult.ok) return;
|
||||
|
||||
mkdirSync(join(dbPath, "..", "migrations"), { recursive: true });
|
||||
|
||||
const { sqlite: dbSqlite, db } = dbResult.value;
|
||||
const runtime: SenseRuntime = {
|
||||
name: "cpu-usage",
|
||||
db,
|
||||
compute: async (d) => {
|
||||
await d.insert(samples).values({ ts: 1000, value: 1.23 });
|
||||
return 1.23;
|
||||
},
|
||||
};
|
||||
|
||||
const result = await executeCompute(runtime, {});
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
const rows = dbSqlite.prepare("SELECT * FROM samples").all() as Array<{
|
||||
ts: number;
|
||||
value: number;
|
||||
}>;
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].ts).toBe(1000);
|
||||
expect(rows[0].value).toBe(1.23);
|
||||
dbSqlite.close();
|
||||
});
|
||||
|
||||
it("returns err when compute exceeds timeoutMs", async () => {
|
||||
const { runtime, sqlite } = makeRuntime(
|
||||
(_db, _peers, options) =>
|
||||
new Promise<null>((resolve, reject) => {
|
||||
const t = setTimeout(() => resolve(null), 5_000);
|
||||
options?.signal.addEventListener("abort", () => {
|
||||
clearTimeout(t);
|
||||
reject(new Error("aborted"));
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await executeCompute(runtime, emptyPeers, 50);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.message).toMatch(/timed out/i);
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("completes within timeout when compute is fast", async () => {
|
||||
const { runtime, sqlite } = makeRuntime(async () => 42);
|
||||
const result = await executeCompute(runtime, emptyPeers, 5_000);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value).toBe(42);
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("passes AbortSignal to compute fn", async () => {
|
||||
let capturedSignal: AbortSignal | undefined;
|
||||
const { runtime, sqlite } = makeRuntime(async (_db, _peers, options) => {
|
||||
capturedSignal = options?.signal;
|
||||
return null;
|
||||
});
|
||||
|
||||
await executeCompute(runtime, emptyPeers, 1_000);
|
||||
expect(capturedSignal).toBeInstanceOf(AbortSignal);
|
||||
sqlite.close();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// parseParentMessage (IPC validation)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("parseParentMessage", () => {
|
||||
it("accepts a valid compute message", () => {
|
||||
const result = parseParentMessage({ type: "compute", sense: "cpu" });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.type).toBe("compute");
|
||||
});
|
||||
|
||||
it("accepts a valid shutdown message", () => {
|
||||
const result = parseParentMessage({ type: "shutdown" });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.type).toBe("shutdown");
|
||||
});
|
||||
|
||||
it("returns err for non-object input", () => {
|
||||
expect(parseParentMessage(null).ok).toBe(false);
|
||||
expect(parseParentMessage("string").ok).toBe(false);
|
||||
expect(parseParentMessage(42).ok).toBe(false);
|
||||
});
|
||||
|
||||
it("returns err when type field is missing", () => {
|
||||
const result = parseParentMessage({ sense: "cpu" });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.message).toMatch(/type/);
|
||||
});
|
||||
|
||||
it("returns err for unknown type value", () => {
|
||||
const result = parseParentMessage({ type: "unknown" });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error.message).toMatch(/unknown/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// runMigrations – journal (idempotency)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("runMigrations journal", () => {
|
||||
it("does not re-run an already-applied migration", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
const dir = mkdtempSync(join(tmpdir(), "nerve-journal-"));
|
||||
writeFileSync(join(dir, "0001_init.sql"), INIT_SQL);
|
||||
|
||||
const first = runMigrations(sqlite, dir);
|
||||
expect(first.ok).toBe(true);
|
||||
|
||||
// Insert a row so we can verify second run doesn't fail on CREATE TABLE
|
||||
sqlite.exec("INSERT INTO samples (ts, value) VALUES (1, 1.0)");
|
||||
|
||||
// Run again — migration must NOT re-run (would fail without IF NOT EXISTS but
|
||||
// the journal prevents it even for non-idempotent SQL)
|
||||
const nonIdempotentSql = "CREATE TABLE samples2 (id INTEGER PRIMARY KEY)";
|
||||
writeFileSync(join(dir, "0002_samples2.sql"), nonIdempotentSql);
|
||||
|
||||
// First time: creates samples2
|
||||
const second = runMigrations(sqlite, dir);
|
||||
expect(second.ok).toBe(true);
|
||||
|
||||
// Second time: 0002 already in journal, must not re-run
|
||||
const third = runMigrations(sqlite, dir);
|
||||
expect(third.ok).toBe(true);
|
||||
|
||||
sqlite.close();
|
||||
});
|
||||
|
||||
it("tracks migrations in _migrations table", () => {
|
||||
const sqlite = new Database(":memory:");
|
||||
const dir = mkdtempSync(join(tmpdir(), "nerve-journal2-"));
|
||||
writeFileSync(join(dir, "0001_init.sql"), INIT_SQL);
|
||||
|
||||
runMigrations(sqlite, dir);
|
||||
|
||||
const rows = sqlite.prepare("SELECT name FROM _migrations ORDER BY name").all() as Array<{
|
||||
name: string;
|
||||
}>;
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].name).toBe("0001_init.sql");
|
||||
|
||||
sqlite.close();
|
||||
});
|
||||
});
|
||||
@@ -1 +1,19 @@
|
||||
// TODO: implement
|
||||
export type {
|
||||
ComputeMessage,
|
||||
ShutdownMessage,
|
||||
ParentToWorkerMessage,
|
||||
SignalMessage,
|
||||
ErrorMessage,
|
||||
ReadyMessage,
|
||||
WorkerToParentMessage,
|
||||
} from "./ipc.js";
|
||||
|
||||
export type { ComputeFn, DrizzleDB, PeerMap, SenseRuntime } from "./sense-runtime.js";
|
||||
|
||||
export {
|
||||
runMigrations,
|
||||
openSenseDb,
|
||||
openPeerDb,
|
||||
loadComputeFn,
|
||||
executeCompute,
|
||||
} from "./sense-runtime.js";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* IPC message types for parent (kernel) ↔ sense worker communication.
|
||||
* Protocol per RFC §5.2: hub-and-spoke, all messages through engine.
|
||||
*/
|
||||
|
||||
import type { Result } from "@uncaged/nerve-core";
|
||||
import { err, ok } from "@uncaged/nerve-core";
|
||||
|
||||
/** Parent → Worker: trigger one compute cycle for a sense */
|
||||
export type ComputeMessage = {
|
||||
type: "compute";
|
||||
sense: string;
|
||||
};
|
||||
|
||||
/** Parent → Worker: graceful shutdown */
|
||||
export type ShutdownMessage = {
|
||||
type: "shutdown";
|
||||
};
|
||||
|
||||
/** Union of all messages the parent sends to a worker */
|
||||
export type ParentToWorkerMessage = ComputeMessage | ShutdownMessage;
|
||||
|
||||
/** Worker → Parent: compute produced a signal */
|
||||
export type SignalMessage = {
|
||||
type: "signal";
|
||||
sense: string;
|
||||
payload: unknown;
|
||||
};
|
||||
|
||||
/** Worker → Parent: compute threw or returned an unexpected error */
|
||||
export type ErrorMessage = {
|
||||
type: "error";
|
||||
sense: string;
|
||||
error: string;
|
||||
};
|
||||
|
||||
/** Worker → Parent: worker finished bootstrap and is ready to receive compute */
|
||||
export type ReadyMessage = {
|
||||
type: "ready";
|
||||
};
|
||||
|
||||
/** Union of all messages a worker sends to the parent */
|
||||
export type WorkerToParentMessage = SignalMessage | ErrorMessage | ReadyMessage;
|
||||
|
||||
/** Validate and parse an unknown IPC message received from the parent process. */
|
||||
export function parseParentMessage(raw: unknown): Result<ParentToWorkerMessage> {
|
||||
if (raw === null || typeof raw !== "object") {
|
||||
return err(new Error("IPC message is not an object"));
|
||||
}
|
||||
const obj = raw as Record<string, unknown>;
|
||||
if (typeof obj.type !== "string") {
|
||||
return err(new Error("IPC message missing string 'type' field"));
|
||||
}
|
||||
const type = obj.type;
|
||||
if (type !== "compute" && type !== "shutdown") {
|
||||
return err(new Error(`Unknown IPC message type: "${type}"`));
|
||||
}
|
||||
return ok(raw as ParentToWorkerMessage);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
||||
|
||||
import type { Result } from "@uncaged/nerve-core";
|
||||
import { err, ok } from "@uncaged/nerve-core";
|
||||
|
||||
/** A Drizzle DB instance (schema-generic) */
|
||||
export type DrizzleDB = BetterSQLite3Database<Record<string, never>>;
|
||||
|
||||
/** Read-only map of peer sense name → their Drizzle DB */
|
||||
export type PeerMap = Readonly<Record<string, DrizzleDB>>;
|
||||
|
||||
/** Options passed to a compute function */
|
||||
export type ComputeOptions = {
|
||||
signal: AbortSignal;
|
||||
};
|
||||
|
||||
/**
|
||||
* The shape every sense's index.ts must export.
|
||||
* Engine injects `db` (read-write), `peers` (read-only), and `options`.
|
||||
* Returns T when a signal should be emitted, null for silence.
|
||||
*/
|
||||
export type ComputeFn<T = unknown> = (
|
||||
db: DrizzleDB,
|
||||
peers: PeerMap,
|
||||
options?: ComputeOptions,
|
||||
) => Promise<T | null>;
|
||||
|
||||
/** All state held for one sense inside a worker */
|
||||
export type SenseRuntime = {
|
||||
name: string;
|
||||
|
|
||||
db: DrizzleDB;
|
||||
compute: ComputeFn;
|
||||
};
|
||||
|
||||
function ensureMigrationsTable(sqlite: Database.Database): Result<void> {
|
||||
try {
|
||||
sqlite.exec(
|
||||
`CREATE TABLE IF NOT EXISTS _migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at INTEGER NOT NULL
|
||||
)`,
|
||||
);
|
||||
return ok(undefined);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return err(new Error(`Failed to create _migrations table: ${msg}`));
|
||||
}
|
||||
}
|
||||
|
||||
function listMigrationFiles(migrationsDir: string): Result<string[]> {
|
||||
try {
|
||||
const files = readdirSync(migrationsDir)
|
||||
.filter((f) => f.endsWith(".sql"))
|
||||
.sort();
|
||||
return ok(files);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return err(new Error(`Failed to read migrations directory "${migrationsDir}": ${msg}`));
|
||||
}
|
||||
}
|
||||
|
||||
function applyMigrationFile(
|
||||
sqlite: Database.Database,
|
||||
file: string,
|
||||
filePath: string,
|
||||
): Result<void> {
|
||||
let sql: string;
|
||||
try {
|
||||
sql = readFileSync(filePath, "utf8");
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return err(new Error(`Failed to read migration file "${filePath}": ${msg}`));
|
||||
}
|
||||
|
||||
const insertJournal = sqlite.prepare("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)");
|
||||
try {
|
||||
sqlite.transaction(() => {
|
||||
sqlite.exec(sql);
|
||||
insertJournal.run(file, Date.now());
|
||||
})();
|
||||
return ok(undefined);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return err(new Error(`Migration "${file}" failed: ${msg}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all *.sql migration files in the given directory against a
|
||||
* better-sqlite3 Database, in lexicographic order.
|
||||
* Tracks applied migrations in _migrations table to avoid re-running.
|
||||
*/
|
||||
export function runMigrations(sqlite: Database.Database, migrationsDir: string): Result<void> {
|
||||
const tableResult = ensureMigrationsTable(sqlite);
|
||||
if (!tableResult.ok) return tableResult;
|
||||
|
||||
const filesResult = listMigrationFiles(migrationsDir);
|
||||
if (!filesResult.ok) return filesResult;
|
||||
|
||||
const applied = new Set<string>(
|
||||
(sqlite.prepare("SELECT name FROM _migrations").all() as Array<{ name: string }>).map(
|
||||
(r) => r.name,
|
||||
),
|
||||
);
|
||||
|
||||
for (const file of filesResult.value) {
|
||||
if (applied.has(file)) continue;
|
||||
const result = applyMigrationFile(sqlite, file, join(migrationsDir, file));
|
||||
if (!result.ok) return result;
|
||||
}
|
||||
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open (or create) the SQLite file at `dbPath`, run all migrations in
|
||||
* `migrationsDir`, and wrap with Drizzle ORM.
|
||||
*/
|
||||
export function openSenseDb(
|
||||
dbPath: string,
|
||||
migrationsDir: string,
|
||||
): Result<{ sqlite: Database.Database; db: DrizzleDB }> {
|
||||
let sqlite: Database.Database;
|
||||
|
||||
try {
|
||||
sqlite = new Database(dbPath);
|
||||
// WAL mode for better concurrent read performance
|
||||
sqlite.pragma("journal_mode = WAL");
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return err(new Error(`Failed to open database "${dbPath}": ${msg}`));
|
||||
}
|
||||
|
||||
const migResult = runMigrations(sqlite, migrationsDir);
|
||||
if (!migResult.ok) return migResult;
|
||||
|
||||
const db = drizzle(sqlite) as DrizzleDB;
|
||||
return ok({ sqlite, db });
|
||||
}
|
||||
|
||||
|
xiaomo
commented
🔴 没有 timeout 机制。 RFC §5.3 要求 soft timeout abort + grace_period hard kill。建议: 🔴 **没有 timeout 机制。** RFC §5.3 要求 soft timeout abort + grace_period hard kill。建议:
```typescript
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await computeFn(db, peers, { signal: controller.signal });
// ...
} finally {
clearTimeout(timer);
}
```
|
||||
/**
|
||||
* Open a peer sense DB in read-only mode (no migrations).
|
||||
*/
|
||||
export function openPeerDb(dbPath: string): Result<DrizzleDB> {
|
||||
let sqlite: Database.Database;
|
||||
|
||||
try {
|
||||
sqlite = new Database(dbPath, { readonly: true });
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return err(new Error(`Failed to open peer database "${dbPath}" (readonly): ${msg}`));
|
||||
}
|
||||
|
||||
return ok(drizzle(sqlite) as DrizzleDB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically import the compute function from a sense's index.ts/js.
|
||||
* The module must export a named `compute` function.
|
||||
*/
|
||||
export async function loadComputeFn(senseIndexPath: string): Promise<Result<ComputeFn>> {
|
||||
let mod: unknown;
|
||||
|
||||
try {
|
||||
mod = await import(senseIndexPath);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return err(new Error(`Failed to import sense module "${senseIndexPath}": ${msg}`));
|
||||
}
|
||||
|
||||
if (
|
||||
mod === null ||
|
||||
typeof mod !== "object" ||
|
||||
!("compute" in mod) ||
|
||||
typeof (mod as Record<string, unknown>).compute !== "function"
|
||||
) {
|
||||
return err(
|
||||
new Error(`Sense module "${senseIndexPath}" must export a named "compute" function`),
|
||||
);
|
||||
}
|
||||
|
||||
return ok((mod as { compute: ComputeFn }).compute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a sense's compute function with an optional soft timeout.
|
||||
* If timeoutMs is provided and compute takes longer, the AbortSignal is
|
||||
* triggered and an error Result is returned.
|
||||
*/
|
||||
export async function executeCompute(
|
||||
runtime: SenseRuntime,
|
||||
peers: PeerMap,
|
||||
timeoutMs?: number,
|
||||
): Promise<Result<unknown | null>> {
|
||||
const controller = new AbortController();
|
||||
const options: ComputeOptions = { signal: controller.signal };
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeoutPromise =
|
||||
timeoutMs !== undefined
|
||||
? new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
controller.abort();
|
||||
reject(new Error(`compute("${runtime.name}") timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
})
|
||||
: null;
|
||||
|
||||
try {
|
||||
const computePromise = runtime.compute(runtime.db, peers, options);
|
||||
const result = timeoutPromise
|
||||
? await Promise.race([computePromise, timeoutPromise])
|
||||
: await computePromise;
|
||||
return ok(result);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (controller.signal.aborted) {
|
||||
return err(new Error(`compute("${runtime.name}") timed out after ${timeoutMs as number}ms`));
|
||||
}
|
||||
return err(new Error(`compute("${runtime.name}") threw: ${msg}`));
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Sense Worker runtime bootstrap.
|
||||
*
|
||||
* Entry point for `nerve worker sense --group <name>`.
|
||||
* Receives the group name via CLI args, reads nerve.yaml, initialises one
|
||||
* SenseRuntime per sense in the group, builds peer read-only connections,
|
||||
* then signals ready and enters the IPC event loop.
|
||||
*
|
||||
* Layout assumptions (nerve user config at `~/.uncaged-nerve/`):
|
||||
* senses/<name>/index.js ← compiled compute
|
||||
* senses/<name>/migrations/ ← SQL migration files
|
||||
* data/senses/<name>.db ← SQLite data file
|
||||
* nerve.yaml ← config
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { parseNerveConfig } from "@uncaged/nerve-core";
|
||||
import type { NerveConfig } from "@uncaged/nerve-core";
|
||||
|
||||
import type { WorkerToParentMessage } from "./ipc.js";
|
||||
import { parseParentMessage } from "./ipc.js";
|
||||
import { executeCompute, loadComputeFn, openPeerDb, openSenseDb } from "./sense-runtime.js";
|
||||
import type { DrizzleDB, PeerMap, SenseRuntime } from "./sense-runtime.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function send(msg: WorkerToParentMessage): void {
|
||||
if (process.send) {
|
||||
process.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function sendReady(): void {
|
||||
send({ type: "ready" });
|
||||
}
|
||||
|
||||
function sendSignal(sense: string, payload: unknown): void {
|
||||
send({ type: "signal", sense, payload });
|
||||
}
|
||||
|
||||
function sendError(sense: string, error: string): void {
|
||||
send({ type: "error", sense, error });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Initialisation helpers (each extracted to keep bootstrap complexity low)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function readConfig(nerveRoot: string): NerveConfig {
|
||||
const configPath = join(nerveRoot, "nerve.yaml");
|
||||
let configRaw: string;
|
||||
try {
|
||||
configRaw = readFileSync(configPath, "utf8");
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
process.stderr.write(`[sense-worker] Cannot read ${configPath}: ${msg}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configResult = parseNerveConfig(configRaw);
|
||||
if (!configResult.ok) {
|
||||
process.stderr.write(`[sense-worker] Config parse error: ${configResult.error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
return configResult.value;
|
||||
}
|
||||
|
||||
async function initSense(
|
||||
nerveRoot: string,
|
||||
senseName: string,
|
||||
): Promise<{ db: DrizzleDB; runtime: SenseRuntime }> {
|
||||
const dbPath = join(nerveRoot, "data", "senses", `${senseName}.db`);
|
||||
const migrationsDir = join(nerveRoot, "senses", senseName, "migrations");
|
||||
const senseIndexPath = resolve(join(nerveRoot, "senses", senseName, "index.js"));
|
||||
|
||||
const dbResult = openSenseDb(dbPath, migrationsDir);
|
||||
if (!dbResult.ok) {
|
||||
process.stderr.write(
|
||||
`[sense-worker] Failed to init DB for "${senseName}": ${dbResult.error.message}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const computeResult = await loadComputeFn(senseIndexPath);
|
||||
if (!computeResult.ok) {
|
||||
process.stderr.write(
|
||||
`[sense-worker] Failed to load compute for "${senseName}": ${computeResult.error.message}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { db } = dbResult.value;
|
||||
return { db, runtime: { name: senseName, db, compute: computeResult.value } };
|
||||
}
|
||||
|
||||
function buildPeers(
|
||||
nerveRoot: string,
|
||||
allSenseNames: string[],
|
||||
ownDbs: Map<string, DrizzleDB>,
|
||||
groupSenseNames: Set<string>,
|
||||
): PeerMap {
|
||||
const entries: [string, DrizzleDB][] = [];
|
||||
|
||||
|
xiaomo
commented
⚠️ 自身 sense 的 DB(read-write)不应出现在 peers map 里。要么排除,要么开独立只读连接。 ⚠️ 自身 sense 的 DB(read-write)不应出现在 peers map 里。要么排除,要么开独立只读连接。
|
||||
for (const peerName of allSenseNames) {
|
||||
// Exclude senses that belong to this worker's own group — they are not peers
|
||||
if (groupSenseNames.has(peerName)) continue;
|
||||
|
||||
const own = ownDbs.get(peerName);
|
||||
if (own !== undefined) {
|
||||
entries.push([peerName, own]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const peerDbPath = join(nerveRoot, "data", "senses", `${peerName}.db`);
|
||||
const peerResult = openPeerDb(peerDbPath);
|
||||
if (!peerResult.ok) {
|
||||
process.stderr.write(
|
||||
`[sense-worker] Warning: could not open peer DB for "${peerName}": ${peerResult.error.message}\n`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
entries.push([peerName, peerResult.value]);
|
||||
}
|
||||
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
function handleMessage(
|
||||
raw: unknown,
|
||||
|
xiaomo
commented
🔴 Unsafe cast. 🔴 **Unsafe cast.** `raw as ParentToWorkerMessage` 不做校验。建议加一个 `parseParentMessage(raw): Result<ParentToWorkerMessage>` 校验 `type` 字段。
|
||||
runtimes: Map<string, SenseRuntime>,
|
||||
peers: PeerMap,
|
||||
group: string,
|
||||
timeoutMs: number,
|
||||
inFlight: Map<string, Promise<void>>,
|
||||
): void {
|
||||
const parseResult = parseParentMessage(raw);
|
||||
if (!parseResult.ok) {
|
||||
process.stderr.write(`[sense-worker] Invalid IPC message: ${parseResult.error.message}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = parseResult.value;
|
||||
|
xiaomo
commented
⚠️ 两个问题:(1) 并发 compute 没串行化,同一 sense 连续触发会 race;(2) ⚠️ 两个问题:(1) 并发 compute 没串行化,同一 sense 连续触发会 race;(2) `.then()` 没有 `.catch()`,unhandled rejection 风险。
|
||||
|
||||
if (msg.type === "shutdown") {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (msg.type === "compute") {
|
||||
const runtime = runtimes.get(msg.sense);
|
||||
if (!runtime) {
|
||||
sendError(msg.sense, `Unknown sense "${msg.sense}" in group "${group}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Serialize computes for the same sense
|
||||
const previous = inFlight.get(msg.sense) ?? Promise.resolve();
|
||||
const next = previous.then(async () => {
|
||||
const result = await executeCompute(runtime, peers, timeoutMs);
|
||||
if (!result.ok) {
|
||||
sendError(msg.sense, result.error.message);
|
||||
return;
|
||||
}
|
||||
if (result.value !== null) {
|
||||
sendSignal(msg.sense, result.value);
|
||||
}
|
||||
});
|
||||
|
||||
const tracked = next.catch((e: unknown) => {
|
||||
const errMsg = e instanceof Error ? e.message : String(e);
|
||||
sendError(msg.sense, errMsg);
|
||||
});
|
||||
|
||||
inFlight.set(msg.sense, tracked);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bootstrap
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
|
||||
async function bootstrap(nerveRoot: string, group: string): Promise<void> {
|
||||
const config = readConfig(nerveRoot);
|
||||
|
||||
const groupSenses = Object.keys(config.senses).filter(
|
||||
(name) => config.senses[name].group === group,
|
||||
);
|
||||
|
||||
if (groupSenses.length === 0) {
|
||||
process.stderr.write(`[sense-worker] No senses found for group "${group}"\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const runtimes = new Map<string, SenseRuntime>();
|
||||
const ownDbs = new Map<string, DrizzleDB>();
|
||||
|
||||
for (const senseName of groupSenses) {
|
||||
const { db, runtime } = await initSense(nerveRoot, senseName);
|
||||
ownDbs.set(senseName, db);
|
||||
runtimes.set(senseName, runtime);
|
||||
}
|
||||
|
||||
const groupSenseNames = new Set(groupSenses);
|
||||
const peers = buildPeers(nerveRoot, Object.keys(config.senses), ownDbs, groupSenseNames);
|
||||
|
||||
// Read timeout from config (uses first group sense's config, or default)
|
||||
const firstSenseConfig = config.senses[groupSenses[0]];
|
||||
const timeoutMs =
|
||||
typeof (firstSenseConfig as Record<string, unknown>).timeoutMs === "number"
|
||||
? ((firstSenseConfig as Record<string, unknown>).timeoutMs as number)
|
||||
: DEFAULT_TIMEOUT_MS;
|
||||
|
||||
const inFlight = new Map<string, Promise<void>>();
|
||||
|
||||
sendReady();
|
||||
|
||||
process.on("message", (raw: unknown) => {
|
||||
handleMessage(raw, runtimes, peers, group, timeoutMs, inFlight);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI entrypoint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseArgs(): { nerveRoot: string; group: string } | null {
|
||||
const args = process.argv.slice(2);
|
||||
let group: string | null = null;
|
||||
let nerveRoot: string | null = null;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === "--group" && i + 1 < args.length) {
|
||||
group = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === "--root" && i + 1 < args.length) {
|
||||
nerveRoot = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!group || !nerveRoot) return null;
|
||||
return { nerveRoot, group };
|
||||
}
|
||||
|
||||
const parsed = parseArgs();
|
||||
if (!parsed) {
|
||||
process.stderr.write("Usage: sense-worker --group <name> --root <nerve-root-dir>\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
bootstrap(parsed.nerveRoot, parsed.group).catch((e) => {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
process.stderr.write(`[sense-worker] Unhandled bootstrap error: ${msg}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
Generated
+407
-8
@@ -28,9 +28,29 @@ importers:
|
||||
devDependencies:
|
||||
vitest:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(vite@8.0.9(esbuild@0.27.7)(yaml@2.8.3))
|
||||
version: 4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))
|
||||
|
||||
packages/daemon: {}
|
||||
packages/daemon:
|
||||
dependencies:
|
||||
'@uncaged/nerve-core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
better-sqlite3:
|
||||
specifier: ^11.10.0
|
||||
version: 11.10.0
|
||||
drizzle-orm:
|
||||
specifier: ^0.43.1
|
||||
version: 0.43.1(@types/better-sqlite3@7.6.13)(better-sqlite3@11.10.0)
|
||||
yaml:
|
||||
specifier: ^2.8.3
|
||||
version: 2.8.3
|
||||
devDependencies:
|
||||
'@types/better-sqlite3':
|
||||
specifier: ^7.6.13
|
||||
version: 7.6.13
|
||||
vitest:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -520,6 +540,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==}
|
||||
|
||||
@@ -529,6 +552,9 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/node@25.6.0':
|
||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||
|
||||
'@vitest/expect@4.1.5':
|
||||
resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==}
|
||||
|
||||
@@ -570,6 +596,21 @@ packages:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
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==}
|
||||
|
||||
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}
|
||||
@@ -588,6 +629,9 @@ packages:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -611,10 +655,110 @@ 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'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
drizzle-orm@0.43.1:
|
||||
resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==}
|
||||
peerDependencies:
|
||||
'@aws-sdk/client-rds-data': '>=3'
|
||||
'@cloudflare/workers-types': '>=4'
|
||||
'@electric-sql/pglite': '>=0.2.0'
|
||||
'@libsql/client': '>=0.10.0'
|
||||
'@libsql/client-wasm': '>=0.10.0'
|
||||
'@neondatabase/serverless': '>=0.10.0'
|
||||
'@op-engineering/op-sqlite': '>=2'
|
||||
'@opentelemetry/api': ^1.4.1
|
||||
'@planetscale/database': '>=1.13'
|
||||
'@prisma/client': '*'
|
||||
'@tidbcloud/serverless': '*'
|
||||
'@types/better-sqlite3': '*'
|
||||
'@types/pg': '*'
|
||||
'@types/sql.js': '*'
|
||||
'@vercel/postgres': '>=0.8.0'
|
||||
'@xata.io/client': '*'
|
||||
better-sqlite3: '>=7'
|
||||
bun-types: '*'
|
||||
expo-sqlite: '>=14.0.0'
|
||||
gel: '>=2'
|
||||
knex: '*'
|
||||
kysely: '*'
|
||||
mysql2: '>=2'
|
||||
pg: '>=8'
|
||||
postgres: '>=3'
|
||||
prisma: '*'
|
||||
sql.js: '>=1'
|
||||
sqlite3: '>=5'
|
||||
peerDependenciesMeta:
|
||||
'@aws-sdk/client-rds-data':
|
||||
optional: true
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
'@electric-sql/pglite':
|
||||
optional: true
|
||||
'@libsql/client':
|
||||
optional: true
|
||||
'@libsql/client-wasm':
|
||||
optional: true
|
||||
'@neondatabase/serverless':
|
||||
optional: true
|
||||
'@op-engineering/op-sqlite':
|
||||
optional: true
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@planetscale/database':
|
||||
optional: true
|
||||
'@prisma/client':
|
||||
optional: true
|
||||
'@tidbcloud/serverless':
|
||||
optional: true
|
||||
'@types/better-sqlite3':
|
||||
optional: true
|
||||
'@types/pg':
|
||||
optional: true
|
||||
'@types/sql.js':
|
||||
optional: true
|
||||
'@vercel/postgres':
|
||||
optional: true
|
||||
'@xata.io/client':
|
||||
optional: true
|
||||
better-sqlite3:
|
||||
optional: true
|
||||
bun-types:
|
||||
optional: true
|
||||
expo-sqlite:
|
||||
optional: true
|
||||
gel:
|
||||
optional: true
|
||||
knex:
|
||||
optional: true
|
||||
kysely:
|
||||
optional: true
|
||||
mysql2:
|
||||
optional: true
|
||||
pg:
|
||||
optional: true
|
||||
postgres:
|
||||
optional: true
|
||||
prisma:
|
||||
optional: true
|
||||
sql.js:
|
||||
optional: true
|
||||
sqlite3:
|
||||
optional: true
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||
|
||||
es-module-lexer@2.0.0:
|
||||
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
|
||||
|
||||
@@ -626,6 +770,10 @@ packages:
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
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'}
|
||||
@@ -639,14 +787,32 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -739,6 +905,16 @@ 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'}
|
||||
|
||||
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==}
|
||||
|
||||
@@ -753,6 +929,13 @@ 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==}
|
||||
|
||||
node-abi@3.89.0:
|
||||
resolution: {integrity: sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -760,6 +943,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==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@@ -799,6 +985,23 @@ 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
|
||||
|
||||
pump@3.0.4:
|
||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||
|
||||
rc@1.2.8:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
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'}
|
||||
@@ -817,9 +1020,23 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
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==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -834,11 +1051,25 @@ 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'}
|
||||
hasBin: true
|
||||
|
||||
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'}
|
||||
@@ -893,6 +1124,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -901,6 +1135,12 @@ packages:
|
||||
ufo@1.6.3:
|
||||
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
|
||||
|
||||
undici-types@7.19.2:
|
||||
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
vite@8.0.9:
|
||||
resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -990,6 +1230,9 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
yaml@2.8.3:
|
||||
resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==}
|
||||
engines: {node: '>= 14.6'}
|
||||
@@ -1282,6 +1525,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
|
||||
@@ -1291,6 +1538,10 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/node@25.6.0':
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
|
||||
'@vitest/expect@4.1.5':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
@@ -1300,13 +1551,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.5(vite@8.0.9(esbuild@0.27.7)(yaml@2.8.3))':
|
||||
'@vitest/mocker@4.1.5(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.5
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.0.9(esbuild@0.27.7)(yaml@2.8.3)
|
||||
vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3)
|
||||
|
||||
'@vitest/pretty-format@4.1.5':
|
||||
dependencies:
|
||||
@@ -1338,6 +1589,28 @@ snapshots:
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -1351,6 +1624,8 @@ snapshots:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
confbox@0.1.8: {}
|
||||
@@ -1363,8 +1638,23 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
||||
deep-extend@0.6.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
drizzle-orm@0.43.1(@types/better-sqlite3@7.6.13)(better-sqlite3@11.10.0):
|
||||
optionalDependencies:
|
||||
'@types/better-sqlite3': 7.6.13
|
||||
better-sqlite3: 11.10.0
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
es-module-lexer@2.0.0: {}
|
||||
|
||||
esbuild@0.27.7:
|
||||
@@ -1400,21 +1690,35 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
expand-template@2.0.3: {}
|
||||
|
||||
expect-type@1.3.0: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.4):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.4
|
||||
|
||||
file-uri-to-path@1.0.0: {}
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
dependencies:
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.2
|
||||
rollup: 4.60.2
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
github-from-package@0.0.0: {}
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ini@1.3.8: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
@@ -1476,6 +1780,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
mlly@1.8.2:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
@@ -1493,10 +1803,20 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
napi-build-utils@2.0.0: {}
|
||||
|
||||
node-abi@3.89.0:
|
||||
dependencies:
|
||||
semver: 7.7.4
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
obug@2.1.1: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -1524,6 +1844,39 @@ 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.89.0
|
||||
pump: 3.0.4
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.4
|
||||
tunnel-agent: 0.6.0
|
||||
|
||||
pump@3.0.4:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.5
|
||||
once: 1.4.0
|
||||
|
||||
rc@1.2.8:
|
||||
dependencies:
|
||||
deep-extend: 0.6.0
|
||||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
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: {}
|
||||
@@ -1580,8 +1933,20 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.60.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
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
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map@0.7.6: {}
|
||||
@@ -1590,6 +1955,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
|
||||
@@ -1600,6 +1971,21 @@ snapshots:
|
||||
tinyglobby: 0.2.16
|
||||
ts-interface-checker: 0.1.13
|
||||
|
||||
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
|
||||
@@ -1656,11 +2042,19 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
tunnel-agent@0.6.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.3: {}
|
||||
|
||||
vite@8.0.9(esbuild@0.27.7)(yaml@2.8.3):
|
||||
undici-types@7.19.2: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
@@ -1668,14 +2062,15 @@ snapshots:
|
||||
rolldown: 1.0.0-rc.16
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
'@types/node': 25.6.0
|
||||
esbuild: 0.27.7
|
||||
fsevents: 2.3.3
|
||||
yaml: 2.8.3
|
||||
|
||||
vitest@4.1.5(vite@8.0.9(esbuild@0.27.7)(yaml@2.8.3)):
|
||||
vitest@4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.5
|
||||
'@vitest/mocker': 4.1.5(vite@8.0.9(esbuild@0.27.7)(yaml@2.8.3))
|
||||
'@vitest/mocker': 4.1.5(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3))
|
||||
'@vitest/pretty-format': 4.1.5
|
||||
'@vitest/runner': 4.1.5
|
||||
'@vitest/snapshot': 4.1.5
|
||||
@@ -1692,8 +2087,10 @@ snapshots:
|
||||
tinyexec: 1.1.1
|
||||
tinyglobby: 0.2.16
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.9(esbuild@0.27.7)(yaml@2.8.3)
|
||||
vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(yaml@2.8.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 25.6.0
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
@@ -1702,4 +2099,6 @@ snapshots:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
yaml@2.8.3: {}
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- "@biomejs/biome"
|
||||
- better-sqlite3
|
||||
- esbuild
|
||||
|
||||
Reference in New Issue
Block a user
🔴 缺 migration journal. 每次启动全量重跑所有 SQL,非幂等 migration 会崩。建议加:
跑之前检查已执行的,跳过。