Implement local SQLite backend and reactive UI

This commit is contained in:
2026-05-14 21:28:32 -04:00
parent 4aa3f7b362
commit f95b0ae912
35 changed files with 5444 additions and 2009 deletions

View File

@@ -12,6 +12,8 @@ export type IpcMethod<Input, Output> = {
input: z.ZodType<Input>;
output: z.ZodType<Output>;
handler: (input: Input) => Promise<Output> | Output;
rawArgs?: boolean;
rawResult?: boolean;
};
function validationDetail(error: z.ZodError): unknown {
@@ -22,10 +24,11 @@ function validationDetail(error: z.ZodError): unknown {
}
export function registerIpcMethod<Input, Output>(method: IpcMethod<Input, Output>): void {
ipcMain.handle(method.channel, async (_event, rawInput): Promise<IpcResult<Output>> => {
ipcMain.handle(method.channel, async (_event, ...rawArgs): Promise<IpcResult<Output> | Output> => {
const rawInput = method.rawArgs ? rawArgs : rawArgs[0];
const parsedInput = method.input.safeParse(rawInput);
if (!parsedInput.success) {
return {
const errorResult: IpcResult<Output> = {
ok: false,
error: {
code: "VALIDATION_ERROR",
@@ -33,29 +36,32 @@ export function registerIpcMethod<Input, Output>(method: IpcMethod<Input, Output
detail: validationDetail(parsedInput.error),
},
};
return method.rawResult ? errorResult as Output : errorResult;
}
try {
const output = await method.handler(parsedInput.data);
const parsedOutput = method.output.safeParse(output);
if (!parsedOutput.success) {
return {
const errorResult: IpcResult<Output> = {
ok: false,
error: {
code: "INTERNAL_ERROR",
message: "Invalid IPC response.",
},
};
return method.rawResult ? errorResult as Output : errorResult;
}
return { ok: true, data: parsedOutput.data };
return method.rawResult ? parsedOutput.data : { ok: true, data: parsedOutput.data };
} catch (error) {
return {
const errorResult: IpcResult<Output> = {
ok: false,
error: {
code: "INTERNAL_ERROR",
message: error instanceof Error ? error.message : "Unhandled IPC failure.",
},
};
return method.rawResult ? errorResult as Output : errorResult;
}
});
}

View File

@@ -8,9 +8,10 @@ import { startAutoSnapshot } from "./snapshotService.js";
import { registerFileHandlers } from "./fileHandler.js";
import type { ClientSettings, RpcMethod, RpcResult } from "@mosaiciq/contracts/rpc";
import { isRpcMethod, parseRpcRequest, parseRpcResponse } from "@mosaiciq/contracts/rpcSchemas";
import { getDatabase, seedDatabase, getDataDir, updateClientSettings, getClientSettings } from "@mosaiciq/shared/db";
import { getDatabase, bootstrapDatabase, getDataDir, updateClientSettings, getClientSettings } from "@mosaiciq/shared/db";
import { eventEmitter } from "@mosaiciq/shared/agents";
import { z } from "zod";
import { registerIpcMethod } from "./ipc.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const isDev = process.env.VITE_DEV_SERVER_URL || !app.isPackaged;
@@ -44,34 +45,49 @@ function rpcValidationDetail(error: z.ZodError): unknown {
return error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }));
}
ipcMain.handle("rpc:call", async (_event, method: unknown, payload: unknown): Promise<RpcResult<RpcMethod>> => {
if (!isRpcMethod(method)) {
return { ok: false, error: { code: "VALIDATION_ERROR", message: "Unknown RPC method." } };
}
registerIpcMethod<unknown[], RpcResult<RpcMethod>>({
channel: "rpc:call",
input: z.tuple([z.unknown(), z.unknown()]),
output: z.custom<RpcResult<RpcMethod>>((value) => {
if (!value || typeof value !== "object" || !("ok" in value)) return false;
const result = value as RpcResult<RpcMethod>;
return result.ok ? "data" in result : "error" in result;
}),
rawArgs: true,
rawResult: true,
handler: async ([method, payload]): Promise<RpcResult<RpcMethod>> => {
if (!isRpcMethod(method)) {
return { ok: false, error: { code: "VALIDATION_ERROR", message: "Unknown RPC method." } };
}
let parsedPayload;
try {
parsedPayload = parseRpcRequest(method, payload);
} catch (error) {
return {
ok: false,
error: {
code: "VALIDATION_ERROR",
message: "Invalid RPC payload.",
let parsedPayload;
try {
parsedPayload = parseRpcRequest(method, payload);
} catch (error) {
return {
ok: false,
error: {
code: "VALIDATION_ERROR",
message: "Invalid RPC payload.",
detail: error instanceof z.ZodError ? rpcValidationDetail(error) : undefined,
},
};
}
const result = await handleRpc(method, parsedPayload);
if (!result.ok) return result;
try {
parseRpcResponse(method, result.data);
return result;
} catch (error) {
console.error("[RPC] Invalid response:", {
method,
detail: error instanceof z.ZodError ? rpcValidationDetail(error) : undefined,
},
};
}
const result = await handleRpc(method, parsedPayload);
if (!result.ok) return result;
try {
parseRpcResponse(method, result.data);
return result;
} catch {
return { ok: false, error: { code: "INTERNAL_ERROR", message: "Invalid RPC response." } };
}
});
return { ok: false, error: { code: "INTERNAL_ERROR", message: "Invalid RPC response." } };
}
},
});
app.whenReady().then(async () => {
@@ -91,11 +107,7 @@ app.whenReady().then(async () => {
}
}
// Seed demo data if empty (check if any companies exist)
const companyCount = db.prepare("SELECT COUNT(*) as count FROM companies").get() as { count: number };
if (companyCount.count === 0) {
seedDatabase(db);
}
bootstrapDatabase(db);
const win = await createWindow();