main.py (5146 lines, 106 routes) becomes an assembly only: one package per domain — core, files, settings, dashboards, conversations, diff, notifications, forms, runs, accounts, workers, models, cron, agents, projects, services, goals, memories, plans, templates — each exposing an APIRouter; the flat domain modules move into their package behind a barrel that keeps the old `import conversations` / `import projects` spellings. The shared singletons (store, meta_store, hub, indexer, …) are built once by core.state.build_state() and attached to app.state.ai; routes take them as the `deps: State` dependency and helpers as an explicit `deps: AppState`. conversations/pricing.py carries the per-model rates out of the parser. Verified: route table and OpenAPI byte-identical; 90 read endpoints golden-diffed against the monolith on a copy of the live data (identical); write routes smoke-tested; 66 backend tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
31 lines
1.0 KiB
Python
31 lines
1.0 KiB
Python
"""A build-once-per-version memo shared by the catalog rollups."""
|
||
|
||
import threading
|
||
|
||
|
||
class _Memo:
|
||
"""One expensive whole-catalog build, memoized on a version key.
|
||
|
||
Every list/detail refetch the SSE bus triggers lands right after a
|
||
transcript write (a version bump), so several requests used to miss the
|
||
cache together and each run the same full build in parallel — N clients =
|
||
N× the CPU on one GIL. Callers with the same key now share a single build:
|
||
the first builds, the rest wait on the lock and take its result."""
|
||
|
||
def __init__(self, build):
|
||
self._build = build
|
||
self._lock = threading.Lock()
|
||
self._hit: tuple | None = None # (key, value), swapped atomically
|
||
|
||
def get(self, key, *args):
|
||
hit = self._hit
|
||
if hit and hit[0] == key:
|
||
return hit[1]
|
||
with self._lock:
|
||
hit = self._hit
|
||
if hit and hit[0] == key:
|
||
return hit[1]
|
||
val = self._build(*args)
|
||
self._hit = (key, val)
|
||
return val
|