Files
company-haness/.claude/hooks/orgos/state/event_store.py
T

95 lines
3.0 KiB
Python

"""Append-only JSONL primitives for the Org OS state kernel."""
from __future__ import annotations
import json
import os
from contextlib import nullcontext
from typing import Any, Callable, ContextManager, Iterable
def read_jsonl(path: str | None, on_error: Callable[[str], None] | None = None) -> list[dict[str, Any]]:
if not path or not os.path.exists(path):
return []
rows: list[dict[str, Any]] = []
try:
with open(path, encoding="utf-8") as handle:
for line in handle:
try:
value = json.loads(line)
except Exception:
continue
if isinstance(value, dict):
rows.append(value)
except Exception as exc:
if on_error:
on_error(f"event 원장 읽기 실패({path}): {exc}")
return rows
def append_jsonl(
path: str | None,
event: dict[str, Any],
*,
file_lock: bool = False,
on_error: Callable[[str], None] | None = None,
) -> bool:
if not path:
return False
try:
with open(path, "a", encoding="utf-8") as handle:
if file_lock:
try:
import fcntl
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
except Exception:
pass
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
handle.flush()
os.fsync(handle.fileno())
return True
except Exception as exc:
if on_error:
on_error(f"event append 실패({path}): {exc}")
return False
def atomic_append(
entries: Iterable[tuple[str | None, dict[str, Any]]],
*,
transaction_lock: ContextManager[Any] | None = None,
) -> None:
"""Append a group of events and truncate every participating tail on failure."""
normalized = list(entries)
handles: list[tuple[Any, int]] = []
with (transaction_lock or nullcontext()):
try:
for path, _event in normalized:
if not path:
raise OSError("event path 해석 실패")
handle = open(path, "a+", encoding="utf-8")
try:
import fcntl
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
except Exception:
pass
handle.seek(0, os.SEEK_END)
handles.append((handle, handle.tell()))
try:
for (handle, _offset), (_path, event) in zip(handles, normalized):
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
handle.flush()
os.fsync(handle.fileno())
except Exception:
for handle, offset in handles:
handle.seek(offset)
handle.truncate()
handle.flush()
raise
finally:
for handle, _offset in handles:
try:
handle.close()
except Exception:
pass