103 lines
2.5 KiB
TypeScript
103 lines
2.5 KiB
TypeScript
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import type { Filing, Holding, PortfolioInsight, Task, WatchlistItem } from '@/lib/types';
|
|
|
|
export type DataStore = {
|
|
counters: {
|
|
watchlist: number;
|
|
holdings: number;
|
|
filings: number;
|
|
insights: number;
|
|
};
|
|
watchlist: WatchlistItem[];
|
|
holdings: Holding[];
|
|
filings: Filing[];
|
|
tasks: Task[];
|
|
insights: PortfolioInsight[];
|
|
};
|
|
|
|
const DATA_DIR = path.join(process.cwd(), 'data');
|
|
const STORE_PATH = path.join(DATA_DIR, 'store.json');
|
|
|
|
let writeQueue = Promise.resolve();
|
|
|
|
function nowIso() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function createDefaultStore(): DataStore {
|
|
const now = nowIso();
|
|
|
|
return {
|
|
counters: {
|
|
watchlist: 0,
|
|
holdings: 0,
|
|
filings: 0,
|
|
insights: 0
|
|
},
|
|
watchlist: [],
|
|
holdings: [],
|
|
filings: [],
|
|
tasks: [],
|
|
insights: [
|
|
{
|
|
id: 1,
|
|
user_id: 1,
|
|
provider: 'local-bootstrap',
|
|
model: 'zeroclaw',
|
|
content: [
|
|
'System initialized in local-first mode.',
|
|
'Add holdings and sync filings to produce a live AI brief via OpenClaw.'
|
|
].join('\n'),
|
|
created_at: now
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
async function ensureStoreFile() {
|
|
await mkdir(DATA_DIR, { recursive: true });
|
|
|
|
try {
|
|
await readFile(STORE_PATH, 'utf8');
|
|
} catch {
|
|
const defaultStore = createDefaultStore();
|
|
defaultStore.counters.insights = defaultStore.insights.length;
|
|
await writeFile(STORE_PATH, JSON.stringify(defaultStore, null, 2), 'utf8');
|
|
}
|
|
}
|
|
|
|
async function readStore(): Promise<DataStore> {
|
|
await ensureStoreFile();
|
|
const raw = await readFile(STORE_PATH, 'utf8');
|
|
return JSON.parse(raw) as DataStore;
|
|
}
|
|
|
|
async function writeStore(store: DataStore) {
|
|
const tempPath = `${STORE_PATH}.tmp`;
|
|
await writeFile(tempPath, JSON.stringify(store, null, 2), 'utf8');
|
|
await rename(tempPath, STORE_PATH);
|
|
}
|
|
|
|
function cloneStore(store: DataStore): DataStore {
|
|
return JSON.parse(JSON.stringify(store)) as DataStore;
|
|
}
|
|
|
|
export async function getStoreSnapshot() {
|
|
const store = await readStore();
|
|
return cloneStore(store);
|
|
}
|
|
|
|
export async function withStore<T>(mutator: (store: DataStore) => T | Promise<T>): Promise<T> {
|
|
const run = async () => {
|
|
const store = await readStore();
|
|
const result = await mutator(store);
|
|
await writeStore(store);
|
|
return result;
|
|
};
|
|
|
|
const nextRun = writeQueue.then(run, run);
|
|
writeQueue = nextRun.then(() => undefined, () => undefined);
|
|
return await nextRun;
|
|
}
|