188 lines
6.2 KiB
JavaScript
188 lines
6.2 KiB
JavaScript
import { positionAt } from '../GPS/handlers/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
|
||
}
|
||
|
||
}
|