Spaces:
Running
Running
File size: 2,527 Bytes
a896df9 664d562 a896df9 664d562 a896df9 664d562 a896df9 664d562 a896df9 664d562 a896df9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | import { z } from 'zod';
import { ExpenseSchema, IsoTimestamp, SettlementSchema } from '../../domain/schemas';
import { decodeText, encodeText } from '../storage/store';
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
const base = {
id: z.string().regex(ULID_RE),
ts: IsoTimestamp,
actor: z.string().min(1),
entityId: z.string().min(1),
version: z.number().int().positive()
};
export const LedgerEventSchema = z.union([
z.object({ ...base, kind: z.literal('expense'), op: z.literal('upsert'), data: ExpenseSchema }),
z.object({ ...base, kind: z.literal('expense'), op: z.literal('delete') }),
z.object({ ...base, kind: z.literal('settlement'), op: z.literal('upsert'), data: SettlementSchema }),
z.object({ ...base, kind: z.literal('settlement'), op: z.literal('delete') })
]);
export type LedgerEvent = z.infer<typeof LedgerEventSchema>;
export const EVENTS_PREFIX = 'events/';
export const LEDGER_PATH = 'ledger.jsonl';
export const eventKey = (id: string): string => `${EVENTS_PREFIX}${id}.json`;
export const EVENT_KEY_RE = /^events\/([0-9A-HJKMNP-TV-Z]{26})\.json$/;
export function encodeEvent(e: LedgerEvent): Uint8Array {
return encodeText(JSON.stringify(e));
}
export function decodeEvent(bytes: Uint8Array): LedgerEvent {
return LedgerEventSchema.parse(JSON.parse(decodeText(bytes)));
}
export function encodeJsonl(events: LedgerEvent[]): Uint8Array {
return encodeText(events.map((e) => JSON.stringify(e)).join('\n') + (events.length ? '\n' : ''));
}
export interface ParsedJsonl {
events: LedgerEvent[];
/** True when a trailing partial line was skipped, so the file must be rewritten rather than appended to. */
truncated: boolean;
}
/**
* Parses one event per line. A final line without a trailing newline that fails to parse is
* treated as a truncated partial write and skipped (its event file still exists in events/,
* because event files are only deleted after a successful append); any other bad line throws.
*/
export function parseJsonl(text: string): ParsedJsonl {
const events: LedgerEvent[] = [];
let truncated = false;
const lines = text.split('\n');
const lastIndex = lines.length - 1;
for (const [i, line] of lines.entries()) {
if (line.trim() === '') continue;
try {
events.push(LedgerEventSchema.parse(JSON.parse(line)));
} catch (e) {
if (i === lastIndex && !text.endsWith('\n')) {
console.warn('[ledger] ignoring truncated trailing line in ledger.jsonl');
truncated = true;
continue;
}
throw e;
}
}
return { events, truncated };
}
|