export type Account = { id: string name: string createdAt: number } export type TxnType = 'credit' | 'debit' export type Transaction = { id: string accountId: string type: TxnType amount: number note: string createdAt: number } export type Session = { key: 'current' user: string loggedInAt: number } const DB_NAME = 'ledger' const DB_VERSION = 1 const STORE_ACCOUNTS = 'accounts' const STORE_TXNS = 'transactions' const STORE_SESSION = 'session' let dbPromise: Promise | null = null function openDb(): Promise { if (dbPromise) return dbPromise dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION) req.onupgradeneeded = () => { const db = req.result if (!db.objectStoreNames.contains(STORE_ACCOUNTS)) { const s = db.createObjectStore(STORE_ACCOUNTS, { keyPath: 'id' }) s.createIndex('createdAt', 'createdAt') s.createIndex('name', 'name', { unique: true }) } if (!db.objectStoreNames.contains(STORE_TXNS)) { const s = db.createObjectStore(STORE_TXNS, { keyPath: 'id' }) s.createIndex('accountId', 'accountId') s.createIndex('createdAt', 'createdAt') } if (!db.objectStoreNames.contains(STORE_SESSION)) { db.createObjectStore(STORE_SESSION, { keyPath: 'key' }) } } req.onsuccess = () => resolve(req.result) req.onerror = () => reject(req.error) }) return dbPromise } function tx( db: IDBDatabase, stores: string | string[], mode: IDBTransactionMode, ) { return db.transaction(stores, mode) } function reqAsPromise(req: IDBRequest): Promise { return new Promise((resolve, reject) => { req.onsuccess = () => resolve(req.result) req.onerror = () => reject(req.error) }) } export function makeId(): string { if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { return crypto.randomUUID() } return ( Date.now().toString(36) + Math.random().toString(36).slice(2, 10) ) } export async function listAccounts(): Promise { const db = await openDb() const store = tx(db, STORE_ACCOUNTS, 'readonly').objectStore(STORE_ACCOUNTS) const all = await reqAsPromise(store.getAll() as IDBRequest) return all.sort((a, b) => a.createdAt - b.createdAt) } export async function getAccount(id: string): Promise { const db = await openDb() const store = tx(db, STORE_ACCOUNTS, 'readonly').objectStore(STORE_ACCOUNTS) return reqAsPromise(store.get(id) as IDBRequest) } export async function accountNameExists(name: string): Promise { const db = await openDb() const idx = tx(db, STORE_ACCOUNTS, 'readonly') .objectStore(STORE_ACCOUNTS) .index('name') const key = await reqAsPromise(idx.getKey(name)) return key !== undefined } export async function createAccount(name: string): Promise { const trimmed = name.trim() if (!trimmed) throw new Error('Name is required') if (trimmed.length > 40) throw new Error('Name is too long (max 40)') if (await accountNameExists(trimmed)) { throw new Error('An account with that name already exists') } const account: Account = { id: makeId(), name: trimmed, createdAt: Date.now(), } const db = await openDb() const t = tx(db, STORE_ACCOUNTS, 'readwrite') await reqAsPromise(t.objectStore(STORE_ACCOUNTS).add(account)) return account } export async function listTransactions( accountId: string, ): Promise { const db = await openDb() const idx = tx(db, STORE_TXNS, 'readonly') .objectStore(STORE_TXNS) .index('accountId') const all = await reqAsPromise( idx.getAll(IDBKeyRange.only(accountId)) as IDBRequest, ) return all.sort((a, b) => b.createdAt - a.createdAt) } export async function listAllTransactions(): Promise { const db = await openDb() const store = tx(db, STORE_TXNS, 'readonly').objectStore(STORE_TXNS) return reqAsPromise(store.getAll() as IDBRequest) } export async function createTransaction(input: { accountId: string type: TxnType amount: number note: string }): Promise { if (!Number.isInteger(input.amount)) { throw new Error('Amount must be a whole number of cents') } if (input.amount <= 0) { throw new Error('Amount must be greater than zero') } if (input.type !== 'credit' && input.type !== 'debit') { throw new Error('Invalid transaction type') } const acc = await getAccount(input.accountId) if (!acc) throw new Error('Account not found') const txn: Transaction = { id: makeId(), accountId: input.accountId, type: input.type, amount: input.amount, note: input.note.trim().slice(0, 80), createdAt: Date.now(), } const db = await openDb() const t = tx(db, STORE_TXNS, 'readwrite') await reqAsPromise(t.objectStore(STORE_TXNS).add(txn)) return txn } export async function getSession(): Promise { const db = await openDb() const store = tx(db, STORE_SESSION, 'readonly').objectStore(STORE_SESSION) return reqAsPromise(store.get('current') as IDBRequest) } export async function setSession(user: string): Promise { const session: Session = { key: 'current', user, loggedInAt: Date.now(), } const db = await openDb() const t = tx(db, STORE_SESSION, 'readwrite') await reqAsPromise(t.objectStore(STORE_SESSION).put(session)) return session } export async function clearSession(): Promise { const db = await openDb() const t = tx(db, STORE_SESSION, 'readwrite') await reqAsPromise(t.objectStore(STORE_SESSION).delete('current')) }