The 35 files behind twelve public pages, seven admin views, two player views and three core-page extensions, ported onto `window.__rg`. Every one of them imports exactly the seven kit members plus `lib/format.js`, which is the finding §2.7.1 predicted and this confirms. `client/src/core.js` is the port mechanism, and unlike the server's it is a plain read: `window.__rg` is published before any module chunk evaluates, so there is no gap to defer around and a ported component keeps its ordinary import shape. `client/src/api.js` rebuilds the UO namespaces over the request primitive — same URLs, because §1.2 freezes the API surface. SPA paths changed and API paths did not. `/site/shard` is `/uo/shard`, and the admin paths lost their now-redundant `shard-` prefixes (`/admin/uo/ops`), a clean break being the only moment that is free. `shim/rg.js` becomes the single reader of the global, so the "core did not publish its dependencies" message is reachable from whichever module the bundler happens to touch first rather than from whichever one is imported first — a guarantee that used to last until someone sorted the imports. Co-Authored-By: Claude <noreply@anthropic.com>
55 lines
2.1 KiB
JavaScript
55 lines
2.1 KiB
JavaScript
import { useEffect, useRef, useState } from 'react'
|
|
import api from '../api.js'
|
|
|
|
// Subscribe to the public shard live-event SSE stream and keep a rolling buffer
|
|
// of the most recent events. The browser talks to our own /public/shard/stream
|
|
// route (plain HTTP EventSource) — never the sidecar's WebSocket — so the token
|
|
// stays server-side and it works through any reverse proxy.
|
|
//
|
|
// EventSource auto-reconnects on drop, so there is no manual retry loop here; a
|
|
// `connected` flag is exposed for a small live/offline indicator. `filter` (a
|
|
// Set of kinds, optional) limits which events are buffered. `max` caps the
|
|
// buffer length.
|
|
export function useShardFeed({ url, filter, max = 40 } = {}) {
|
|
const [events, setEvents] = useState([])
|
|
const [connected, setConnected] = useState(false)
|
|
// Keep the latest filter in a ref so re-renders don't tear down the stream.
|
|
const filterRef = useRef(filter)
|
|
filterRef.current = filter
|
|
const streamUrl = url || api.shardStreamUrl
|
|
|
|
useEffect(() => {
|
|
// EventSource isn't available during SSR / very old browsers — degrade to
|
|
// "no live feed" rather than throwing.
|
|
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return undefined
|
|
|
|
const es = new EventSource(streamUrl, { withCredentials: true })
|
|
|
|
es.onopen = () => setConnected(true)
|
|
es.onerror = () => setConnected(false) // EventSource will retry on its own
|
|
|
|
es.onmessage = (msg) => {
|
|
let event
|
|
try {
|
|
event = JSON.parse(msg.data)
|
|
} catch {
|
|
return
|
|
}
|
|
if (!event || !event.kind) return
|
|
const f = filterRef.current
|
|
if (f && !f.has(event.kind)) return
|
|
setEvents((prev) => {
|
|
// Tag with a stable-ish local id for React keys (events carry t but can
|
|
// collide within a ms) and cap the buffer.
|
|
const next = [{ ...event, _id: `${event.kind}-${event.t}-${prev.length}` }, ...prev]
|
|
return next.slice(0, max)
|
|
})
|
|
}
|
|
|
|
return () => es.close()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [max, streamUrl])
|
|
|
|
return { events, connected }
|
|
}
|