$this->board->stations($this->ownerRef($request)), ]); } /** * Active kitchen tickets — open sales with fired-but-not-served lines. * Used for the initial load and as a fallback when SSE is unavailable. */ public function feed(Request $request): JsonResponse { return response()->json($this->board->payload($this->ownerRef($request))); } /** * Realtime feed over Server-Sent Events. Pushes the board whenever it * changes (~1s tick), with a heartbeat. Bounded lifetime so PHP-FPM workers * recycle — the browser's EventSource reconnects automatically. */ public function stream(Request $request): StreamedResponse { $owner = $this->ownerRef($request); // Release the session lock early (DB driver doesn't lock, but be safe). $request->session()->save(); $response = new StreamedResponse(function () use ($owner) { while (ob_get_level() > 0) { @ob_end_flush(); } @set_time_limit(0); $start = time(); $lastHash = null; while (true) { $payload = $this->board->payload($owner); $json = json_encode($payload); $hash = md5((string) $json); if ($hash !== $lastHash) { echo 'data: '.$json."\n\n"; $lastHash = $hash; } else { echo ": ping\n\n"; // heartbeat } @ob_flush(); @flush(); if (connection_aborted() || (time() - $start) >= 25) { break; } sleep(1); } }); $response->headers->set('Content-Type', 'text/event-stream'); $response->headers->set('Cache-Control', 'no-cache'); $response->headers->set('Connection', 'keep-alive'); $response->headers->set('X-Accel-Buffering', 'no'); // disable nginx buffering return $response; } /** * Advance one line to the next kitchen state (queued → preparing → ready → served). */ public function bump(Request $request, PosSaleLine $line): JsonResponse { return response()->json($this->board->bumpLine($line, $this->ownerRef($request))); } }