RayMelius Claude Sonnet 4.6 commited on
Commit
10292a0
·
1 Parent(s): 5f1b4cb

Fix real-time sync, speed, and persistence for deployed Render

Browse files

- websocket.py: send full state snapshot immediately on client connect
so new browsers instantly see the current city state (Day/time/agents)
instead of a stale Day 1 6:00 until the next tick fires
- server.py: tick delay now reads SOCI_TICK_DELAY env var (default 0.5s
instead of hardcoded 2.0s) — LLM call latency naturally paces ticks
on cloud deployments; set to 0 to run as fast as LLM allows
- database.py + snapshots.py: data directory now reads SOCI_DATA_DIR env
var (default "data") so Render's persistent disk (/var/data) can be
mounted and state survives across redeploys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

src/soci/api/server.py CHANGED
@@ -191,7 +191,10 @@ async def lifespan(app: FastAPI):
191
  _simulation = sim
192
 
193
  # Start background simulation
194
- _sim_task = asyncio.create_task(simulation_loop(sim, db, tick_delay=2.0))
 
 
 
195
 
196
  yield
197
 
 
191
  _simulation = sim
192
 
193
  # Start background simulation
194
+ # SOCI_TICK_DELAY: seconds to sleep between ticks (default 0.5).
195
+ # Set to 0 to let LLM latency pace the simulation naturally.
196
+ tick_delay = float(os.environ.get("SOCI_TICK_DELAY", "0.5"))
197
+ _sim_task = asyncio.create_task(simulation_loop(sim, db, tick_delay=tick_delay))
198
 
199
  yield
200
 
src/soci/api/websocket.py CHANGED
@@ -69,22 +69,16 @@ async def websocket_stream(websocket: WebSocket):
69
  from soci.api.server import get_simulation
70
  sim = get_simulation()
71
 
72
- # Store the original callback
73
- original_callback = sim.on_event
74
-
75
- # Add our own callback that also sends to WebSocket
76
- async def ws_event_handler(msg: str):
77
- if original_callback:
78
- original_callback(msg)
79
- await manager.broadcast({
80
- "type": "event",
81
- "message": msg,
82
- "tick": sim.clock.total_ticks,
83
- "time": sim.clock.datetime_str,
84
- })
85
-
86
- # We can't easily replace the sync callback with async,
87
- # so instead we poll the simulation state
88
  last_tick = sim.clock.total_ticks
89
  while True:
90
  try:
 
69
  from soci.api.server import get_simulation
70
  sim = get_simulation()
71
 
72
+ # Send the full current state immediately so the client is in sync
73
+ # before the next tick fires (avoids "Day 1 6:00" on fresh connects).
74
+ state = sim.get_state_summary()
75
+ await manager.send_personal(websocket, {
76
+ "type": "tick",
77
+ "tick": sim.clock.total_ticks,
78
+ "time": sim.clock.datetime_str,
79
+ "state": state,
80
+ })
81
+
 
 
 
 
 
 
82
  last_tick = sim.clock.total_ticks
83
  while True:
84
  try:
src/soci/persistence/database.py CHANGED
@@ -12,7 +12,8 @@ import aiosqlite
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
- DB_DIR = Path("data")
 
16
  DEFAULT_DB = DB_DIR / "soci.db"
17
 
18
  SCHEMA = """
 
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
+ # SOCI_DATA_DIR env var lets you point at a persistent disk (e.g. /var/data on Render).
16
+ DB_DIR = Path(os.environ.get("SOCI_DATA_DIR", "data"))
17
  DEFAULT_DB = DB_DIR / "soci.db"
18
 
19
  SCHEMA = """
src/soci/persistence/snapshots.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
 
5
  import json
6
  import logging
 
7
  from pathlib import Path
8
  from typing import Optional, TYPE_CHECKING
9
 
@@ -14,7 +15,7 @@ if TYPE_CHECKING:
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
- SNAPSHOTS_DIR = Path("data") / "snapshots"
18
 
19
 
20
  async def save_simulation(
 
4
 
5
  import json
6
  import logging
7
+ import os
8
  from pathlib import Path
9
  from typing import Optional, TYPE_CHECKING
10
 
 
15
 
16
  logger = logging.getLogger(__name__)
17
 
18
+ SNAPSHOTS_DIR = Path(os.environ.get("SOCI_DATA_DIR", "data")) / "snapshots"
19
 
20
 
21
  async def save_simulation(