feat(tui): add zero kvstore
This commit is contained in:
@@ -5,5 +5,6 @@ const PATH = join(homedir(), ".local", "share", "money");
|
|||||||
const AUTH_PATH = join(PATH, "auth.json");
|
const AUTH_PATH = join(PATH, "auth.json");
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
dir: PATH,
|
||||||
authPath: AUTH_PATH,
|
authPath: AUTH_PATH,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { App, type Route } from "@money/ui";
|
|||||||
import { ZeroProvider } from "@rocicorp/zero/react";
|
import { ZeroProvider } from "@rocicorp/zero/react";
|
||||||
import { schema } from '@money/shared';
|
import { schema } from '@money/shared';
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { AuthClient, getAuth, layer } from "./auth";
|
import { getAuth, layer } from "./auth";
|
||||||
import { Effect, Layer } from "effect";
|
import { Effect } from "effect";
|
||||||
import { BunContext } from "@effect/platform-bun";
|
import { BunContext } from "@effect/platform-bun";
|
||||||
import type { AuthData } from "./schema";
|
import type { AuthData } from "./schema";
|
||||||
|
import { kvStore } from "./store";
|
||||||
|
|
||||||
const userID = "anon";
|
const userID = "anon";
|
||||||
const server = "http://laptop:4848";
|
const server = "http://laptop:4848";
|
||||||
@@ -16,7 +17,7 @@ function Main({ auth }: { auth: AuthData }) {
|
|||||||
const [route, setRoute] = useState<Route>("/");
|
const [route, setRoute] = useState<Route>("/");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ZeroProvider {...{ userID, auth: auth.session.token, server, schema }}>
|
<ZeroProvider {...{ userID, auth: auth.session.token, server, schema, kvStore }}>
|
||||||
<App
|
<App
|
||||||
auth={auth || null}
|
auth={auth || null}
|
||||||
route={route}
|
route={route}
|
||||||
|
|||||||
136
apps/tui/src/store.ts
Normal file
136
apps/tui/src/store.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import type { ReadonlyJSONValue, ZeroOptions } from "@rocicorp/zero";
|
||||||
|
import { config } from "./config";
|
||||||
|
|
||||||
|
type StoreProvider = ZeroOptions<any>["kvStore"];
|
||||||
|
|
||||||
|
const DATA_DIR = config.dir;
|
||||||
|
|
||||||
|
// async function ensureDir() {
|
||||||
|
// await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
function deepFreeze<T>(obj: T): T {
|
||||||
|
if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
|
||||||
|
Object.freeze(obj);
|
||||||
|
for (const value of Object.values(obj as any)) {
|
||||||
|
deepFreeze(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFile(name: string): Promise<Map<string, ReadonlyJSONValue>> {
|
||||||
|
// await ensureDir();
|
||||||
|
const filePath = path.join(DATA_DIR, `${name}.json`);
|
||||||
|
try {
|
||||||
|
const buf = await fs.readFile(filePath, "utf8");
|
||||||
|
const obj = JSON.parse(buf) as Record<string, ReadonlyJSONValue>;
|
||||||
|
const frozen = Object.fromEntries(
|
||||||
|
Object.entries(obj).map(([k, v]) => [k, deepFreeze(v)])
|
||||||
|
);
|
||||||
|
return new Map(Object.entries(frozen));
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile(name: string, data: Map<string, ReadonlyJSONValue>) {
|
||||||
|
// await ensureDir();
|
||||||
|
const filePath = path.join(DATA_DIR, `${name}.json`);
|
||||||
|
const obj = Object.fromEntries(data.entries());
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const kvStore: StoreProvider = {
|
||||||
|
create: (name: string) => {
|
||||||
|
let closed = false;
|
||||||
|
let dataPromise = loadFile(name);
|
||||||
|
|
||||||
|
const makeRead = async () => {
|
||||||
|
const data = await dataPromise;
|
||||||
|
let txClosed = false;
|
||||||
|
return {
|
||||||
|
closed: txClosed,
|
||||||
|
async has(key: string) {
|
||||||
|
if (txClosed) throw new Error("transaction closed");
|
||||||
|
return data.has(key);
|
||||||
|
},
|
||||||
|
async get(key: string) {
|
||||||
|
if (txClosed) throw new Error("transaction closed");
|
||||||
|
return data.get(key);
|
||||||
|
},
|
||||||
|
release() {
|
||||||
|
txClosed = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeWrite = async () => {
|
||||||
|
const data = await dataPromise;
|
||||||
|
let txClosed = false;
|
||||||
|
const staging = new Map<string, ReadonlyJSONValue | undefined>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
closed: txClosed,
|
||||||
|
async has(key: string) {
|
||||||
|
if (txClosed) throw new Error("transaction closed");
|
||||||
|
return staging.has(key) ? staging.get(key) !== undefined : data.has(key);
|
||||||
|
},
|
||||||
|
async get(key: string) {
|
||||||
|
if (txClosed) throw new Error("transaction closed");
|
||||||
|
return staging.has(key) ? staging.get(key) : data.get(key);
|
||||||
|
},
|
||||||
|
async put(key: string, value: ReadonlyJSONValue) {
|
||||||
|
if (txClosed) throw new Error("transaction closed");
|
||||||
|
staging.set(key, deepFreeze(value)); // 🔒 freeze before staging
|
||||||
|
},
|
||||||
|
async del(key: string) {
|
||||||
|
if (txClosed) throw new Error("transaction closed");
|
||||||
|
staging.set(key, undefined);
|
||||||
|
},
|
||||||
|
async commit() {
|
||||||
|
if (txClosed) throw new Error("transaction closed");
|
||||||
|
for (const [k, v] of staging.entries()) {
|
||||||
|
if (v === undefined) {
|
||||||
|
data.delete(k);
|
||||||
|
} else {
|
||||||
|
data.set(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveFile(name, data);
|
||||||
|
txClosed = true;
|
||||||
|
},
|
||||||
|
release() {
|
||||||
|
txClosed = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
closed,
|
||||||
|
async read() {
|
||||||
|
if (closed) throw new Error("store closed");
|
||||||
|
return makeRead();
|
||||||
|
},
|
||||||
|
async write() {
|
||||||
|
if (closed) throw new Error("store closed");
|
||||||
|
return makeWrite();
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
closed = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async drop(name: string) {
|
||||||
|
// await ensureDir();
|
||||||
|
const filePath = path.join(DATA_DIR, `${name}.json`);
|
||||||
|
await fs.rm(filePath, { force: true });
|
||||||
|
console.log("destroy db:", name);
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user