import { positionAt } from '../GPS/actions/arena/worldline.js' import { Frustum } from './frustum.js' export class RequestorRegistry { constructor(reader, getNow, scanIntervalMs, onPush, debug = false) { this.reader = reader this.getNow = getNow this.scanIntervalMs = scanIntervalMs this.onPush = onPush this.debug = debug this.requestors = new Map() } #tickTimer = null #scanInFlight = false #adjustFrequency(requestedMs) { if(typeof(requestedMs) !== 'number' || Number.isNaN(requestedMs)) return(null) if(requestedMs < 300 || requestedMs > 10000) return(null) const tickMs = this.scanIntervalMs const ticks = Math.round(requestedMs / tickMs) const minTicks = Math.ceil(300 / tickMs) const maxTicks = Math.floor(10000 / tickMs) const clampedTicks = Math.max(minTicks, Math.min(maxTicks, ticks)) return(clampedTicks * tickMs) } async subscribeFrustum(id, { planes, frequency }) { if(!id || typeof(id) !== 'string') return({ ok: false, err: 'Invalid requestor id' }) const frustum = Frustum.fromPlanes(planes) if(!frustum) return({ ok: false, err: 'Invalid frustum planes' }) const frequencyMs = this.#adjustFrequency(frequency) if(frequencyMs === null) { return({ ok: false, err: 'Invalid frequency (expected 300–10000 ms)' }) } const t = this.getNow() if(t === null) return({ ok: false, err: 'Simulation not live' }) const agents = await this.reader.loadAllAgents() const matching = this.#matchAgents(agents, frustum, t) const pushEveryNTicks = frequencyMs / this.scanIntervalMs this.requestors.set(id, { id, frustum, tMode: 'live', subscription: true, frequencyMs, pushEveryNTicks, tickCounter: 0, agents: matching, t, updatedAt: Date.now(), }) this.#ensureTick() return({ ok: true, frequency: frequencyMs, agents: matching, t, }) } updateRequestor(id, spec) { const requestor = this.requestors.get(id) if(!requestor) return({ ok: false, err: 'Unknown requestor' }) if(Array.isArray(spec?.planes)) { const frustum = Frustum.fromPlanes(spec.planes) if(!frustum) return({ ok: false, err: 'Invalid frustum planes' }) requestor.frustum = frustum } if(typeof(spec?.frequency) === 'number') { const frequencyMs = this.#adjustFrequency(spec.frequency) if(frequencyMs === null) return({ ok: false, err: 'Invalid frequency (expected 300–10000 ms)' }) requestor.frequencyMs = frequencyMs requestor.pushEveryNTicks = frequencyMs / this.scanIntervalMs requestor.tickCounter = 0 } return({ ok: true, frequency: requestor.frequencyMs }) } unregisterRequestor(id) { this.requestors.delete(id) if(this.requestors.size === 0) this.#stopTick() return({ ok: true }) } getRequestorAgents(id) { const requestor = this.requestors.get(id) if(!requestor) return(null) return({ agents: [...requestor.agents], t: requestor.t, frequency: requestor.frequencyMs ?? null, updatedAt: requestor.updatedAt, }) } clear() { this.requestors.clear() this.#stopTick() } async evaluateOnce({ frustum, t }) { if(!frustum || !(frustum instanceof Frustum)) { return({ ok: false, err: 'Invalid frustum', agents: [] }) } if(typeof(t) !== 'number' || Number.isNaN(t)) return({ ok: false, err: 'Invalid simulation time', agents: [] }) const agents = await this.reader.loadAllAgents() const matching = this.#matchAgents(agents, frustum, t) return({ ok: true, agents: matching, t }) } #resolveT(requestor) { if(requestor.tMode === 'fixed') return(requestor.fixedT) return(this.getNow()) } #matchAgents(agents, frustum, t) { const matching = [] for(const agent of agents.values()) { const position = positionAt(agent, t) if(!frustum.containsPoint(position)) continue matching.push(this.reader.buildAgentSnapshot(agent, t)) } return(matching) } #pushUpdate(requestor) { if(typeof(this.onPush) !== 'function') return this.onPush(requestor.id, { agents: [...requestor.agents], t: requestor.t, }) } async #scanAll() { if(this.#scanInFlight || this.requestors.size === 0) return this.#scanInFlight = true try { const agents = await this.reader.loadAllAgents() for(const requestor of this.requestors.values()) { const t = this.#resolveT(requestor) if(t === null) { requestor.agents = [] requestor.t = null requestor.updatedAt = Date.now() continue } requestor.agents = this.#matchAgents(agents, requestor.frustum, t) requestor.t = t requestor.updatedAt = Date.now() if(!requestor.subscription) continue requestor.tickCounter++ if(requestor.tickCounter >= requestor.pushEveryNTicks) { requestor.tickCounter = 0 this.#pushUpdate(requestor) } } if(this.debug) console.log(`[Observer] Scanned ${agents.size} agent(s) for ${this.requestors.size} requestor(s)`) } catch(err) { console.error('[Observer] Requestor scan failed:', err) } finally { this.#scanInFlight = false } } #ensureTick() { if(this.#tickTimer) return this.#tickTimer = setInterval(() => { this.#scanAll() }, this.scanIntervalMs) } #stopTick() { if(!this.#tickTimer) return clearInterval(this.#tickTimer) this.#tickTimer = null } }