From e86e29aea0ed06b2adeb65200551303f81440026 Mon Sep 17 00:00:00 2001 From: spudone Date: Sat, 27 Jun 2026 10:13:56 -0700 Subject: [PATCH] simplify shutdown; fix market open/closed --- src/stiq/app.py | 38 +++++++++++++++++++++++++++----------- src/stiq/builder.py | 5 +++-- tests/test_builder.py | 10 +++++++++- web/script.js | 7 ++----- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/stiq/app.py b/src/stiq/app.py index aca77fc..c9ae295 100644 --- a/src/stiq/app.py +++ b/src/stiq/app.py @@ -168,6 +168,7 @@ def filter(self, record: logging.LogRecord) -> bool: app.state.cache = cache_manager app.state.builder = builder app.state.provider = provider + app.state.shutdown_timer = None # Startup — launch the Chrome app window after a brief delay # so Uvicorn can finish the lifespan and start accepting requests @@ -366,21 +367,29 @@ async def update_interval( return {"success": True, "interval": seconds_val} -async def delayed_exit(delay_secs: float): +async def delayed_exit( + delay_secs: float, usage_tracker: TiingoUsageTracker | None = None +): await asyncio.sleep(delay_secs) + if usage_tracker: + try: + usage_tracker.save() + except Exception as e: + print(f"[stiq] Error saving usage tracker: {e}", file=sys.stderr) os._exit(0) -@app.post("/api/shutdown") -async def shutdown(usage_tracker: TiingoUsageTracker = Depends(get_usage_tracker_dep)): - print("[stiq] Shutting down...") - usage_tracker.save() - asyncio.create_task(delayed_exit(0.5)) - return {"success": True} - - @app.get("/api/stream") -async def stream_events(event_bus: EventBus = Depends(get_event_bus_dep)): +async def stream_events( + request: Request, event_bus: EventBus = Depends(get_event_bus_dep) +): + # Cancel any pending shutdown timer — a client just connected + timer = getattr(request.app.state, "shutdown_timer", None) + if timer and not timer.done(): + print("[stiq] Client reconnected, cancelling pending shutdown.") + timer.cancel() + request.app.state.shutdown_timer = None + async def event_generator(): queue: asyncio.Queue = asyncio.Queue() event_bus.subscribe(queue) @@ -392,8 +401,15 @@ async def event_generator(): except asyncio.TimeoutError: yield {"comment": "ping"} except asyncio.CancelledError: + pass + finally: event_bus.unsubscribe(queue) - raise + if len(event_bus.subscribers) == 0: + print("[stiq] Last SSE client disconnected. Shutting down in 3s...") + usage_tracker = getattr(request.app.state, "usage_tracker", None) + request.app.state.shutdown_timer = asyncio.create_task( + delayed_exit(3.0, usage_tracker) + ) return EventSourceResponse(event_generator()) diff --git a/src/stiq/builder.py b/src/stiq/builder.py index b0561c7..8fe78b0 100644 --- a/src/stiq/builder.py +++ b/src/stiq/builder.py @@ -38,8 +38,9 @@ def is_market_open(self) -> bool: return False # Standard hours: 9:30 AM to 4:00 PM EST - # (Using 9 <= hour < 16 acts as 9:00 AM - 4:00 PM, which is a good baseline) - return 9 <= now_et.hour < 16 + start_time = now_et.replace(hour=9, minute=30, second=0, microsecond=0) + end_time = now_et.replace(hour=16, minute=0, second=0, microsecond=0) + return start_time <= now_et < end_time def build_realtime_quote( self, diff --git a/tests/test_builder.py b/tests/test_builder.py index c41fdf5..479099c 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -82,11 +82,19 @@ def test_is_market_open(mock_dt, builder): mock_dt.date = MagicMock() assert builder.is_market_open() is True - # 2. Test normal weekday, closed hours (Tuesday, 8:00 AM) + # 2. Test normal weekday, closed hours (Tuesday, 8:00 AM and 9:15 AM) dt_closed_early = et.localize(datetime(2026, 3, 3, 8, 0, 0)) mock_dt.now.return_value = dt_closed_early assert builder.is_market_open() is False + dt_closed_915 = et.localize(datetime(2026, 3, 3, 9, 15, 0)) + mock_dt.now.return_value = dt_closed_915 + assert builder.is_market_open() is False + + dt_open_930 = et.localize(datetime(2026, 3, 3, 9, 30, 0)) + mock_dt.now.return_value = dt_open_930 + assert builder.is_market_open() is True + # 3. Test normal weekday, closed hours (Tuesday, 5:00 PM) dt_closed_late = et.localize(datetime(2026, 3, 3, 17, 0, 0)) mock_dt.now.return_value = dt_closed_late diff --git a/web/script.js b/web/script.js index c29b955..9a85111 100644 --- a/web/script.js +++ b/web/script.js @@ -134,11 +134,8 @@ function stiq() { // Connect to Server-Sent Events stream this.setupSSE(); - // Notify backend when window is closed - window.addEventListener("beforeunload", () => { - navigator.sendBeacon("/api/shutdown"); - }); - + + if (this.quotesProvider === "tiingo") { setInterval(() => { this.lastUpdated = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });