a lot of refactos

This commit is contained in:
STEINNI
2026-06-21 21:08:46 +00:00
parent 3066a54a4c
commit 4c9e989bda
16 changed files with 472 additions and 60 deletions
+60 -2
View File
@@ -28,6 +28,8 @@ export class gpsServer {
this.state = SimState.IDLE this.state = SimState.IDLE
this.simulationId = null this.simulationId = null
this.bigBangEpoch = null this.bigBangEpoch = null
this.resumeEpoch = null
this.pausedAt = null
this.ignoredChangeCount = 0 this.ignoredChangeCount = 0
} }
@@ -62,17 +64,29 @@ export class gpsServer {
return(this.state === SimState.LIVE) return(this.state === SimState.LIVE)
} }
isPaused() {
return(this.state === SimState.PAUSED)
}
canQuerySim() {
return(this.isLive() || this.isPaused())
}
isPrepare() { isPrepare() {
return(this.state === SimState.PREPARE) return(this.state === SimState.PREPARE)
} }
simNow() { simNow() {
if(this.bigBangEpoch === null) return(null) if(this.bigBangEpoch === null) return(null)
if(this.resumeEpoch !== null) {
return(this.pausedAt + (performance.now() - this.resumeEpoch) / 1000)
}
return((performance.now() - this.bigBangEpoch) / 1000) return((performance.now() - this.bigBangEpoch) / 1000)
} }
now() { now() {
if(this.isLive()) return(this.simNow()) if(this.isLive()) return(this.simNow())
if(this.isPaused()) return(this.pausedAt)
if(this.isPrepare()) return(0) if(this.isPrepare()) return(0)
return(null) return(null)
} }
@@ -124,7 +138,7 @@ export class gpsServer {
} }
getAgentPosition(agentId, at = null) { getAgentPosition(agentId, at = null) {
if(!this.isLive()) return(null) if(!this.canQuerySim()) return(null)
const agent = this.agents.get(agentId) const agent = this.agents.get(agentId)
if(!agent) return(null) if(!agent) return(null)
const t = at ?? this.now() const t = at ?? this.now()
@@ -154,7 +168,7 @@ export class gpsServer {
} }
getAgentsInPrism(prism, at = null) { getAgentsInPrism(prism, at = null) {
if(!this.isLive()) return([]) if(!this.canQuerySim()) return([])
const t = at ?? this.now() const t = at ?? this.now()
if(t === null) return([]) if(t === null) return([])
const agents = [] const agents = []
@@ -172,6 +186,8 @@ export class gpsServer {
this.registry = new CollisionRegistry() this.registry = new CollisionRegistry()
this.simulationId = null this.simulationId = null
this.bigBangEpoch = null this.bigBangEpoch = null
this.resumeEpoch = null
this.pausedAt = null
this.ignoredChangeCount = 0 this.ignoredChangeCount = 0
this.state = SimState.IDLE this.state = SimState.IDLE
await this.agentStore?.clearAll() await this.agentStore?.clearAll()
@@ -269,6 +285,8 @@ export class gpsServer {
} }
this.bigBangEpoch = performance.now() this.bigBangEpoch = performance.now()
this.resumeEpoch = null
this.pausedAt = null
this.state = SimState.LIVE this.state = SimState.LIVE
for(const agent of this.agents.values()) { for(const agent of this.agents.values()) {
@@ -278,6 +296,46 @@ export class gpsServer {
if(this.debug) console.log(`[GPS] LIVE: bigBangEpoch set, simulationId=${this.simulationId}`) if(this.debug) console.log(`[GPS] LIVE: bigBangEpoch set, simulationId=${this.simulationId}`)
} }
onSimulationPaused(payload = {}) {
if(this.state === SimState.IDLE || this.state === SimState.PAUSED) return
if(payload.simulationId && this.simulationId && payload.simulationId !== this.simulationId) {
console.error('[GPS] simulationPaused rejected: simulationId mismatch')
return
}
const t = (typeof(payload.t) === 'number' && !Number.isNaN(payload.t))
? payload.t
: this.now()
this.pausedAt = t
this.state = SimState.PAUSED
if(this.debug) console.log(`[GPS] PAUSED at t=${t}, simulationId=${this.simulationId}`)
}
async onSimulationStopped(payload = {}) {
if(this.state === SimState.IDLE) return
if(payload.simulationId && this.simulationId && payload.simulationId !== this.simulationId) {
console.error('[GPS] simulationStopped rejected: simulationId mismatch')
return
}
if(this.debug) console.log(`[GPS] STOPPED (reset), simulationId=${this.simulationId}`)
await this.resetSimulation()
}
onSimulationResumed(payload = {}) {
if(this.state !== SimState.PAUSED) {
console.error(`[GPS] simulationResumed rejected: expected PAUSED, got ${this.state}`)
return
}
if(payload.simulationId && this.simulationId && payload.simulationId !== this.simulationId) {
console.error('[GPS] simulationResumed rejected: simulationId mismatch')
return
}
this.resumeEpoch = performance.now()
this.state = SimState.LIVE
if(this.debug) console.log(`[GPS] RESUMED at t=${this.pausedAt}, simulationId=${this.simulationId}`)
}
upsertAgent(agentId, newVector, newPosition = null) { upsertAgent(agentId, newVector, newPosition = null) {
const now = this.now() const now = this.now()
let agent = this.agents.get(agentId) let agent = this.agents.get(agentId)
+13
View File
@@ -19,5 +19,18 @@ export const eventHandlers = {
bigBang(msg, chan) { bigBang(msg, chan) {
this.gpsSrv?.onBigBang(msg.payload ?? {}) this.gpsSrv?.onBigBang(msg.payload ?? {})
}, },
simulationPaused(msg, chan) {
this.gpsSrv?.onSimulationPaused(msg.payload ?? {})
},
simulationResumed(msg, chan) {
this.gpsSrv?.onSimulationResumed(msg.payload ?? {})
},
simulationStopped(msg, chan) {
const srv = this.gpsSrv
if(!srv) return
srv.onSimulationStopped(msg.payload ?? {}).catch(err => {
console.error(`[${this.redisId}] simulationStopped failed:`, err)
})
},
}, },
} }
+1
View File
@@ -2,6 +2,7 @@ export const SimState = {
IDLE: 'idle', IDLE: 'idle',
PREPARE: 'prepare', PREPARE: 'prepare',
LIVE: 'live', LIVE: 'live',
PAUSED: 'paused',
} }
export function validateAgentSets(expected, found) { export function validateAgentSets(expected, found) {
+52
View File
@@ -30,6 +30,37 @@ export const actions = {
keyframeId: result.keyframeId, keyframeId: result.keyframeId,
infraId: result.infraId, infraId: result.infraId,
agentIds: result.agentIds, agentIds: result.agentIds,
resumed: result.resumed ?? false,
state: result.state ?? null,
},
})
},
async action_PAUSESIMULATION(action, payload, reqid, sender, roles) {
if(!isValidUuid(sender)) {
replyToAction(this, { action, reqid, sender, success: false, err: 'Missing or invalid sender (user UUID)' })
return
}
if(!payload?.simulationUuid) {
replyToAction(this, { action, reqid, sender, success: false, err: 'Missing simulationUuid' })
return
}
const result = await this.maestroSrv.pauseSimulation(sender, payload)
if(!result.ok) {
replyToAction(this, { action, reqid, sender, success: false, err: result.err })
return
}
replyToAction(this, {
action,
reqid,
sender,
success: true,
payload: {
simulationId: result.simulationId,
t: result.t,
}, },
}) })
}, },
@@ -60,4 +91,25 @@ export const actions = {
}) })
}, },
async action_GETSIMULATIONSSTATUS(action, payload, reqid, sender, roles) {
if(!isValidUuid(sender)) {
replyToAction(this, { action, reqid, sender, success: false, err: 'Missing or invalid sender (user UUID)' })
return
}
const result = await this.maestroSrv.getSimulationsStatus(sender)
if(!result.ok) {
replyToAction(this, { action, reqid, sender, success: false, err: result.err })
return
}
replyToAction(this, {
action,
reqid,
sender,
success: true,
payload: result.simulations,
})
},
} }
+213 -15
View File
@@ -24,6 +24,11 @@ export class maestroServer {
this.orchestrationState = MaestroState.IDLE this.orchestrationState = MaestroState.IDLE
this.simulationId = null this.simulationId = null
this.agentIds = [] this.agentIds = []
this.keyframeId = null
this.infraId = null
this.bigBangEpoch = null
this.resumeEpoch = null
this.pausedAt = null
} }
getMaestroSettings() { getMaestroSettings() {
@@ -54,6 +59,18 @@ export class maestroServer {
return(this.orchestrationState === MaestroState.LIVE) return(this.orchestrationState === MaestroState.LIVE)
} }
isPaused() {
return(this.orchestrationState === MaestroState.PAUSED)
}
simNow() {
if(this.bigBangEpoch === null) return(null)
if(this.resumeEpoch !== null) {
return(this.pausedAt + (performance.now() - this.resumeEpoch) / 1000)
}
return((performance.now() - this.bigBangEpoch) / 1000)
}
async init() { async init() {
const mysqlCfg = this.maestroConfig.mysql const mysqlCfg = this.maestroConfig.mysql
if(!mysqlCfg) { if(!mysqlCfg) {
@@ -111,6 +128,11 @@ export class maestroServer {
this.orchestrationState = MaestroState.IDLE this.orchestrationState = MaestroState.IDLE
this.simulationId = null this.simulationId = null
this.agentIds = [] this.agentIds = []
this.keyframeId = null
this.infraId = null
this.bigBangEpoch = null
this.resumeEpoch = null
this.pausedAt = null
this.prepareQuorum?.cancel() this.prepareQuorum?.cancel()
} }
@@ -120,24 +142,58 @@ export class maestroServer {
return(this.prepareQuorum.handleMessage(msg, chan)) return(this.prepareQuorum.handleMessage(msg, chan))
} }
async publishLifecycle(eventType, payload) { async publishLifecycle(eventType, payload, state = null) {
if(!this.arenaCnx) throw(new Error('No arena Redis connection')) if(!this.arenaCnx) throw(new Error('No arena Redis connection'))
const { arenaChannel, senderId } = this.getMaestroSettings().lifecycle const { arenaChannel, senderId } = this.getMaestroSettings().lifecycle
const resolvedState = state ?? this.orchestrationStateFor(payload?.simulationId ?? this.simulationId)
await this.arenaCnx.redisPublish(arenaChannel, { await this.arenaCnx.redisPublish(arenaChannel, {
eventType, eventType,
sender: senderId, sender: senderId,
payload, payload,
}) })
await this.publishSystemLifecycle(eventType, payload, resolvedState)
if(this.debug) console.log(`[Maestro] Published ${eventType} simulationId=${payload.simulationId}`) if(this.debug) console.log(`[Maestro] Published ${eventType} simulationId=${payload.simulationId}`)
} }
async publishSystemLifecycle(eventType, payload, state) {
if(!this.systemCnx || !this.simRepo) return
const simulationId = payload?.simulationId
if(!simulationId) return
const ownersResult = await this.simRepo.listSimulationOwnerUuids(simulationId)
if(!ownersResult.ok || !ownersResult.ownerUuids.length) return
const maestro = this.maestroConfig.maestro ?? {}
const channelTemplate = maestro.systemLifecycleChannel ?? 'system:maestro:lifecycle:[UID]'
const senderId = maestro.senderId ?? 'maestro'
const msg = {
eventType,
sender: senderId,
payload: { ...payload, state },
}
for(const uid of ownersResult.ownerUuids) {
const chan = channelTemplate.replace(/\[UID\]/g, uid)
await this.systemCnx.redisPublish(chan, msg)
}
if(this.debug) {
console.log(`[Maestro] Published system ${eventType} state=${state} simulationId=${simulationId}`)
}
}
async startSimulation(userUuid, payload) { async startSimulation(userUuid, payload) {
if(!this.simRepo) return({ ok: false, err: 'Database not initialized' }) if(!this.simRepo) return({ ok: false, err: 'Database not initialized' })
if(!this.arenaGroom) return({ ok: false, err: 'No arena Redis connection' }) if(!this.arenaGroom) return({ ok: false, err: 'No arena Redis connection' })
const simulationUuid = payload?.simulationUuid
if(this.isPaused()) {
return(this.resumeSimulation(userUuid, simulationUuid))
}
if(!this.prepareQuorum) return({ ok: false, err: 'No prepare quorum (arena Redis not wired)' }) if(!this.prepareQuorum) return({ ok: false, err: 'No prepare quorum (arena Redis not wired)' })
if(!this.isIdle()) return({ ok: false, err: 'A simulation is already in progress' }) if(!this.isIdle()) return({ ok: false, err: 'A simulation is already in progress' })
const simulationUuid = payload?.simulationUuid
const infraId = payload?.infraId ?? null const infraId = payload?.infraId ?? null
const access = await this.simRepo.validateSimulationAccess(userUuid, simulationUuid) const access = await this.simRepo.validateSimulationAccess(userUuid, simulationUuid)
@@ -152,6 +208,8 @@ export class maestroServer {
this.simulationId = simulationUuid this.simulationId = simulationUuid
this.agentIds = agentsResult.agents.map(a => a.id) this.agentIds = agentsResult.agents.map(a => a.id)
this.keyframeId = keyframeId
this.infraId = infraId
this.orchestrationState = MaestroState.PREPARING this.orchestrationState = MaestroState.PREPARING
const lifecyclePayload = { const lifecyclePayload = {
@@ -166,30 +224,131 @@ export class maestroServer {
await this.publishLifecycle('onYourMarks', lifecyclePayload) await this.publishLifecycle('onYourMarks', lifecyclePayload)
const quorum = await readyWait this.continueStartSimulation(simulationUuid, readyWait).catch(err => {
if(!quorum.ok) { console.error('[Maestro] continueStartSimulation failed:', err)
await this.arenaGroom.clearArena()
this.resetOrchestration()
return(quorum)
}
await this.publishLifecycle('bigBang', {
simulationId: this.simulationId,
agentIds: this.agentIds,
}) })
this.orchestrationState = MaestroState.LIVE
if(this.debug) console.log(`[Maestro] LIVE simulationId=${this.simulationId}`)
return({ return({
ok: true, ok: true,
simulationId: this.simulationId, simulationId: this.simulationId,
keyframeId, keyframeId,
infraId, infraId,
agentIds: this.agentIds, agentIds: this.agentIds,
resumed: false,
state: MaestroState.PREPARING,
}) })
} }
async continueStartSimulation(simulationUuid, readyWait) {
try {
const quorum = await readyWait
if(!quorum.ok) {
if(this.orchestrationState === MaestroState.PREPARING && this.simulationId === simulationUuid) {
await this.publishSystemLifecycle('simulationPrepareFailed', {
simulationId: simulationUuid,
err: quorum.err,
}, MaestroState.IDLE)
await this.arenaGroom.clearArena()
this.resetOrchestration()
}
if(this.debug) {
console.warn(`[Maestro] Prepare failed simulationId=${simulationUuid}: ${quorum.err}`)
}
return
}
if(this.orchestrationState !== MaestroState.PREPARING || this.simulationId !== simulationUuid) {
return
}
this.bigBangEpoch = performance.now()
this.resumeEpoch = null
this.pausedAt = null
this.orchestrationState = MaestroState.LIVE
await this.publishLifecycle('bigBang', {
simulationId: this.simulationId,
agentIds: this.agentIds,
})
if(this.debug) console.log(`[Maestro] LIVE simulationId=${this.simulationId}`)
} catch(err) {
console.error(`[Maestro] continueStartSimulation error simulationId=${simulationUuid}:`, err)
if(this.simulationId === simulationUuid && this.orchestrationState === MaestroState.PREPARING) {
await this.publishSystemLifecycle('simulationPrepareFailed', {
simulationId: simulationUuid,
err: err.message ?? 'Prepare failed',
}, MaestroState.IDLE)
await this.arenaGroom?.clearArena()
this.resetOrchestration()
}
}
}
async resumeSimulation(userUuid, simulationUuid) {
if(this.simulationId !== simulationUuid) {
return({ ok: false, err: 'Another simulation is paused' })
}
const access = await this.simRepo.validateSimulationAccess(userUuid, simulationUuid)
if(!access.ok) return(access)
await this.publishLifecycle('simulationResumed', {
simulationId: simulationUuid,
agentIds: [...this.agentIds],
t: this.pausedAt,
})
this.resumeEpoch = performance.now()
this.orchestrationState = MaestroState.LIVE
if(this.debug) console.log(`[Maestro] RESUMED at t=${this.pausedAt}, simulationId=${simulationUuid}`)
return({
ok: true,
simulationId: this.simulationId,
keyframeId: this.keyframeId,
infraId: this.infraId,
agentIds: [...this.agentIds],
resumed: true,
state: MaestroState.LIVE,
})
}
async pauseSimulation(userUuid, payload) {
if(!this.simRepo) return({ ok: false, err: 'Database not initialized' })
const simulationUuid = payload?.simulationUuid
const access = await this.simRepo.validateSimulationAccess(userUuid, simulationUuid)
if(!access.ok) return(access)
if(this.simulationId !== simulationUuid) {
if(this.simulationId) return({ ok: false, err: 'Another simulation is active' })
return({ ok: false, err: 'Simulation is not running' })
}
if(this.orchestrationState === MaestroState.PAUSED) {
return({ ok: true, simulationId: simulationUuid, t: this.pausedAt })
}
if(this.orchestrationState !== MaestroState.LIVE && this.orchestrationState !== MaestroState.PREPARING) {
return({ ok: false, err: 'Simulation is not running' })
}
const t = this.orchestrationState === MaestroState.LIVE ? this.simNow() : 0
await this.publishLifecycle('simulationPaused', {
simulationId: simulationUuid,
agentIds: [...this.agentIds],
t,
})
this.pausedAt = t
this.orchestrationState = MaestroState.PAUSED
if(this.debug) console.log(`[Maestro] PAUSED at t=${t}, simulationId=${simulationUuid}`)
return({ ok: true, simulationId: simulationUuid, t })
}
async stopSimulation(userUuid, payload) { async stopSimulation(userUuid, payload) {
if(!this.simRepo) return({ ok: false, err: 'Database not initialized' }) if(!this.simRepo) return({ ok: false, err: 'Database not initialized' })
if(!this.arenaGroom) return({ ok: false, err: 'No arena Redis connection' }) if(!this.arenaGroom) return({ ok: false, err: 'No arena Redis connection' })
@@ -202,6 +361,26 @@ export class maestroServer {
return({ ok: false, err: 'Another simulation is active' }) return({ ok: false, err: 'Another simulation is active' })
} }
if(this.simulationId === simulationUuid) {
let t = null
if(this.orchestrationState === MaestroState.LIVE) {
t = this.simNow()
} else if(this.orchestrationState === MaestroState.PREPARING) {
t = 0
} else if(this.orchestrationState === MaestroState.PAUSED) {
t = this.pausedAt
}
await this.publishLifecycle('simulationStopped', {
simulationId: simulationUuid,
agentIds: [...this.agentIds],
t,
}, MaestroState.IDLE)
}
if(this.simulationId === simulationUuid && this.orchestrationState === MaestroState.PREPARING) {
this.prepareQuorum?.abortPending({ ok: false, err: 'Simulation stopped' })
}
await this.arenaGroom.clearArena() await this.arenaGroom.clearArena()
this.resetOrchestration() this.resetOrchestration()
@@ -210,6 +389,25 @@ export class maestroServer {
return({ ok: true, simulationId: simulationUuid }) return({ ok: true, simulationId: simulationUuid })
} }
orchestrationStateFor(simulationId) {
if(this.simulationId !== simulationId) return(MaestroState.IDLE)
return(this.orchestrationState)
}
async getSimulationsStatus(userUuid) {
if(!this.simRepo) return({ ok: false, err: 'Database not initialized' })
const listResult = await this.simRepo.listOwnerSimulations(userUuid)
if(!listResult.ok) return(listResult)
const simulations = listResult.simulationIds.map(simulationId => ({
simulationId,
state: this.orchestrationStateFor(simulationId),
}))
return({ ok: true, simulations })
}
async reloadAccessRights() { async reloadAccessRights() {
await this.configHelper.refreshAccessRights() await this.configHelper.refreshAccessRights()
this.maestroConfig.accessRights = this.configHelper.config.accessRights this.maestroConfig.accessRights = this.configHelper.config.accessRights
+1
View File
@@ -2,4 +2,5 @@ export const MaestroState = {
IDLE: 'idle', IDLE: 'idle',
PREPARING: 'preparing', PREPARING: 'preparing',
LIVE: 'live', LIVE: 'live',
PAUSED: 'paused',
} }
+13 -1
View File
@@ -83,6 +83,18 @@ export class PrepareQuorum {
} }
cancel() { cancel() {
this.#cleanup()
}
abortPending(result) {
const resolve = this.resolve
this.#cleanup()
if(typeof(resolve) !== 'function') return(false)
resolve(result)
return(true)
}
#cleanup() {
if(this.timer) { if(this.timer) {
clearTimeout(this.timer) clearTimeout(this.timer)
this.timer = null this.timer = null
@@ -96,7 +108,7 @@ export class PrepareQuorum {
#finish(result) { #finish(result) {
const resolve = this.resolve const resolve = this.resolve
this.cancel() this.#cleanup()
if(typeof(resolve) === 'function') resolve(result) if(typeof(resolve) === 'function') resolve(result)
} }
+50 -16
View File
@@ -58,14 +58,14 @@ export class SimRepository {
if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' }) if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' })
const rows = await MySQLClient.poolExecute(this.db, ` const rows = await MySQLClient.poolExecute(this.db, `
SELECT s.sim_id, SELECT sim_id,
BIN_TO_UUID(s.sim_uuid) AS sim_uuid, BIN_TO_UUID(sim_uuid) AS sim_uuid,
BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid BIN_TO_UUID(sim_root_kf_uuid) AS sim_root_kf_uuid
FROM ${this.#qualify(this.simDb, 'simulations')} s FROM ${this.#qualify(this.simDb, 'simulations')}
INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} o ON o.own_sim_uuid = s.sim_uuid INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} ON own_sim_uuid = sim_uuid
INNER JOIN ${this.#qualify(this.guiDb, 'users')} u ON o.own_usr_id = u.usr_id INNER JOIN ${this.#qualify(this.guiDb, 'users')} ON own_usr_id = usr_id
WHERE u.usr_uuid = ? WHERE usr_uuid = ?
AND s.sim_uuid = UUID_TO_BIN(?) AND sim_uuid = UUID_TO_BIN(?)
`, [userUuid, simulationUuid]) `, [userUuid, simulationUuid])
if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' }) if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' })
@@ -88,20 +88,54 @@ export class SimRepository {
if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' }) if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' })
const rows = await MySQLClient.poolExecute(this.db, ` const rows = await MySQLClient.poolExecute(this.db, `
SELECT s.sim_id, SELECT sim_id,
BIN_TO_UUID(s.sim_uuid) AS sim_uuid, BIN_TO_UUID(sim_uuid) AS sim_uuid,
BIN_TO_UUID(s.sim_root_kf_uuid) AS sim_root_kf_uuid BIN_TO_UUID(sim_root_kf_uuid) AS sim_root_kf_uuid
FROM ${this.#qualify(this.simDb, 'simulations')} s FROM ${this.#qualify(this.simDb, 'simulations')}
INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} o ON o.own_sim_uuid = s.sim_uuid INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} ON own_sim_uuid = sim_uuid
INNER JOIN ${this.#qualify(this.guiDb, 'users')} u ON o.own_usr_id = u.usr_id INNER JOIN ${this.#qualify(this.guiDb, 'users')} ON own_usr_id = usr_id
WHERE u.usr_uuid = ? WHERE usr_uuid = ?
AND s.sim_uuid = UUID_TO_BIN(?) AND sim_uuid = UUID_TO_BIN(?)
`, [userUuid, simulationUuid]) `, [userUuid, simulationUuid])
if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' }) if(!rows.length) return({ ok: false, err: 'Simulation not found or access denied' })
return({ ok: true, sim: rows[0] }) return({ ok: true, sim: rows[0] })
} }
async listSimulationOwnerUuids(simulationUuid) {
if(!isValidUuid(simulationUuid)) return({ ok: false, err: 'Invalid simulation UUID' })
const rows = await MySQLClient.poolExecute(this.db, `
SELECT BIN_TO_UUID(usr_uuid) AS owner_uuid
FROM ${this.#qualify(this.guiDb, 'simowners')}
INNER JOIN ${this.#qualify(this.guiDb, 'users')} ON own_usr_id = usr_id
WHERE own_sim_uuid = UUID_TO_BIN(?)
`, [simulationUuid])
return({
ok: true,
ownerUuids: rows.map(row => row.owner_uuid),
})
}
async listOwnerSimulations(userUuid) {
if(!isValidUuid(userUuid)) return({ ok: false, err: 'Invalid user UUID' })
const rows = await MySQLClient.poolExecute(this.db, `
SELECT BIN_TO_UUID(sim_uuid) AS simulation_id
FROM ${this.#qualify(this.simDb, 'simulations')}
INNER JOIN ${this.#qualify(this.guiDb, 'simowners')} ON own_sim_uuid = sim_uuid
INNER JOIN ${this.#qualify(this.guiDb, 'users')} ON own_usr_id = usr_id
WHERE usr_uuid = ?
ORDER BY sim_id
`, [userUuid])
return({
ok: true,
simulationIds: rows.map(row => row.simulation_id),
})
}
async loadKeyframeAgents(keyframeId) { async loadKeyframeAgents(keyframeId) {
const rows = await MySQLClient.poolExecute(this.db, ` const rows = await MySQLClient.poolExecute(this.db, `
SELECT BIN_TO_UUID(ekfs_agent_id) AS agent_id, SELECT BIN_TO_UUID(ekfs_agent_id) AS agent_id,
+3
View File
@@ -7,5 +7,8 @@ export const eventHandlers = {
bigBang(msg, chan) { bigBang(msg, chan) {
this.observerSrv?.onBigBang() this.observerSrv?.onBigBang()
}, },
simulationStopped(msg, chan) {
this.observerSrv?.onSimulationStopped(msg.payload ?? {})
},
}, },
} }
-1
View File
@@ -107,7 +107,6 @@ export const actions = {
success: true, success: true,
payload: { payload: {
frequency: result.frequency, frequency: result.frequency,
agents: result.agents,
t: result.t, t: result.t,
}, },
}) })
+43 -11
View File
@@ -1,7 +1,6 @@
import { AccesRights } from '../accesRights.js' import { AccesRights } from '../accesRights.js'
import { GpsStorageReader } from './gpsStorageReader.js' import { GpsStorageReader } from './gpsStorageReader.js'
import { RequestorRegistry } from './requestorRegistry.js' import { RequestorRegistry } from './requestorRegistry.js'
import { replyToAction } from '../bus/publishActionReply.js'
import { SimState } from '../GPS/simulationState.js' import { SimState } from '../GPS/simulationState.js'
export class observerServer { export class observerServer {
@@ -26,6 +25,8 @@ export class observerServer {
return({ return({
senderId: observer.senderId ?? 'observer', senderId: observer.senderId ?? 'observer',
scanIntervalMs: observer.scanIntervalMs ?? 300, scanIntervalMs: observer.scanIntervalMs ?? 300,
frustumEventsChannel: observer.observerFrustumEventsChannel
?? 'system:observer:subscribed[UID]:agents',
lifecycle: { lifecycle: {
arenaChannel: observer.lifecycle?.arenaChannel ?? 'arena:lifecycle', arenaChannel: observer.lifecycle?.arenaChannel ?? 'arena:lifecycle',
godsReadyChannel: observer.lifecycle?.godsReadyChannel ?? 'arena:gods:ready', godsReadyChannel: observer.lifecycle?.godsReadyChannel ?? 'arena:gods:ready',
@@ -53,20 +54,44 @@ export class observerServer {
this.gpsStorageReader, this.gpsStorageReader,
() => this.now(), () => this.now(),
scanIntervalMs, scanIntervalMs,
(sender, payload) => this.publishFrustumUpdate(sender, payload), (subscriberId, payload) => this.publishFrustumAgentEvents(
subscriberId,
payload.agents,
payload.t
),
this.debug this.debug
) )
} }
publishFrustumUpdate(sender, payload) { async publishFrustumAgentEvents(subscriberId, agents, t) {
if(!this.systemCnx || !sender) return if(!this.systemCnx || !subscriberId) return
const observer = this.observerConfig.observer ?? {} if(!Array.isArray(agents) || !agents.length) return
replyToAction(this.systemCnx, {
action: 'GETAGENTSINFRUSTUM', const { frustumEventsChannel } = this.getObserverSettings()
sender, const chan = frustumEventsChannel.replace(/\[UID\]/g, subscriberId)
success: true, const senderId = this.getObserverSettings().senderId
payload,
}) for(const agent of agents) {
if(!agent?.id || !agent?.position) continue
await this.systemCnx.redisPublish(chan, {
eventType: 'move',
sender: senderId,
payload: {
aid: agent.id,
coords: {
x: agent.position.x,
y: agent.position.y,
z: agent.position.z,
},
t,
},
})
}
if(this.debug) {
console.log(`[Observer] Frustum events: ${agents.length} agent(s) on ${chan} at t=${t}`)
}
} }
isLive() { isLive() {
@@ -94,6 +119,13 @@ export class observerServer {
this.state = SimState.LIVE this.state = SimState.LIVE
} }
onSimulationStopped(payload = {}) {
this.requestorRegistry?.clear()
this.state = SimState.IDLE
this.bigBangEpoch = null
if(this.debug) console.log(`[Observer] simulationStopped at t=${payload.t}`)
}
wireSystemConnexion(cnx) { wireSystemConnexion(cnx) {
cnx.observerSrv = this cnx.observerSrv = this
cnx.accessRights = this.accessRights cnx.accessRights = this.accessRights
+4 -4
View File
@@ -56,11 +56,11 @@ export class RequestorRegistry {
updatedAt: Date.now(), updatedAt: Date.now(),
}) })
this.#ensureTick() this.#ensureTick()
this.#pushAgentEvents(this.requestors.get(id))
return({ return({
ok: true, ok: true,
frequency: frequencyMs, frequency: frequencyMs,
agents: matching,
t, t,
}) })
} }
@@ -130,9 +130,9 @@ export class RequestorRegistry {
return(matching) return(matching)
} }
#pushUpdate(requestor) { #pushAgentEvents(requestor) {
if(typeof(this.onPush) !== 'function') return if(typeof(this.onPush) !== 'function') return
this.onPush(requestor.id, { void this.onPush(requestor.id, {
agents: [...requestor.agents], agents: [...requestor.agents],
t: requestor.t, t: requestor.t,
}) })
@@ -160,7 +160,7 @@ export class RequestorRegistry {
requestor.tickCounter++ requestor.tickCounter++
if(requestor.tickCounter >= requestor.pushEveryNTicks) { if(requestor.tickCounter >= requestor.pushEveryNTicks) {
requestor.tickCounter = 0 requestor.tickCounter = 0
this.#pushUpdate(requestor) this.#pushAgentEvents(requestor)
} }
} }
if(this.debug) console.log(`[Observer] Scanned ${agents.size} agent(s) for ${this.requestors.size} requestor(s)`) if(this.debug) console.log(`[Observer] Scanned ${agents.size} agent(s) for ${this.requestors.size} requestor(s)`)
+5 -1
View File
@@ -20,7 +20,9 @@
{ {
"canDo": [ "canDo": [
"STARTSIMULATION", "STARTSIMULATION",
"STOPSIMULATION" "PAUSESIMULATION",
"STOPSIMULATION",
"GETSIMULATIONSSTATUS"
], ],
"roles": "*" "roles": "*"
} }
@@ -59,6 +61,7 @@
"arenaChannel": "arena:lifecycle", "arenaChannel": "arena:lifecycle",
"godsReadyChannel": "arena:gods:ready" "godsReadyChannel": "arena:gods:ready"
}, },
"systemLifecycleChannel": "system:maestro:lifecycle:[UID]",
"readyTimeoutMs": 30000 "readyTimeoutMs": 30000
}, },
"mysql": { "mysql": {
@@ -70,6 +73,7 @@
"primordialDaemon": false, "primordialDaemon": false,
"observerActionsChannel": "system:requests:observer", "observerActionsChannel": "system:requests:observer",
"observerActionsReply": "system:replies:[UID]", "observerActionsReply": "system:replies:[UID]",
"observerFrustumEventsChannel": "system:observer:subscribed[UID]:agents",
"senderId": "observer", "senderId": "observer",
"scanIntervalMs": 300, "scanIntervalMs": 300,
"lifecycle": { "lifecycle": {
+1
View File
@@ -157,6 +157,7 @@
"primordialDaemon": { "type": "boolean" }, "primordialDaemon": { "type": "boolean" },
"observerActionsChannel": { "type": "string" }, "observerActionsChannel": { "type": "string" },
"observerActionsReply": { "type": "string" }, "observerActionsReply": { "type": "string" },
"observerFrustumEventsChannel": { "type": "string" },
"senderId": { "type": "string" }, "senderId": { "type": "string" },
"scanIntervalMs": { "type": "integer", "minimum": 50 }, "scanIntervalMs": { "type": "integer", "minimum": 50 },
"lifecycle": { "lifecycle": {
+5 -1
View File
@@ -20,7 +20,9 @@
{ {
"canDo": [ "canDo": [
"STARTSIMULATION", "STARTSIMULATION",
"STOPSIMULATION" "PAUSESIMULATION",
"STOPSIMULATION",
"GETSIMULATIONSSTATUS"
], ],
"roles": "*" "roles": "*"
} }
@@ -59,6 +61,7 @@
"arenaChannel": "arena:lifecycle", "arenaChannel": "arena:lifecycle",
"godsReadyChannel": "arena:gods:ready" "godsReadyChannel": "arena:gods:ready"
}, },
"systemLifecycleChannel": "system:maestro:lifecycle:[UID]",
"readyTimeoutMs": 30000 "readyTimeoutMs": 30000
}, },
"mysql": { "mysql": {
@@ -70,6 +73,7 @@
"primordialDaemon": false, "primordialDaemon": false,
"observerActionsChannel": "system:requests:observer", "observerActionsChannel": "system:requests:observer",
"observerActionsReply": "system:replies:[UID]", "observerActionsReply": "system:replies:[UID]",
"observerFrustumEventsChannel": "system:observer:subscribed[UID]:agents",
"senderId": "observer", "senderId": "observer",
"scanIntervalMs": 300, "scanIntervalMs": 300,
"lifecycle": { "lifecycle": {
+8 -8
View File
@@ -40,14 +40,14 @@ function vectorsEqual(a, b) {
async function findSimulationFixture(ctx) { async function findSimulationFixture(ctx) {
const { guiDatabase, simDatabase } = ctx.databases const { guiDatabase, simDatabase } = ctx.databases
const rows = await MySQLClient.poolExecute(ctx.db, ` const rows = await MySQLClient.poolExecute(ctx.db, `
SELECT u.usr_uuid AS user_uuid, SELECT usr_uuid AS user_uuid,
BIN_TO_UUID(s.sim_uuid) AS simulation_uuid BIN_TO_UUID(sim_uuid) AS simulation_uuid
FROM \`${guiDatabase}\`.users u FROM \`${guiDatabase}\`.users
INNER JOIN \`${guiDatabase}\`.simowners o ON o.own_usr_id = u.usr_id INNER JOIN \`${guiDatabase}\`.simowners ON own_usr_id = usr_id
INNER JOIN \`${simDatabase}\`.simulations s ON o.own_sim_uuid = s.sim_uuid INNER JOIN \`${simDatabase}\`.simulations ON own_sim_uuid = sim_uuid
INNER JOIN \`${simDatabase}\`.edited_kf_store ekfs ON ekfs.ekfs_ekf_uuid = s.sim_root_kf_uuid INNER JOIN \`${simDatabase}\`.edited_kf_store ON ekfs_ekf_uuid = sim_root_kf_uuid
GROUP BY u.usr_uuid, s.sim_uuid GROUP BY usr_uuid, sim_uuid
HAVING COUNT(ekfs.ekfs_agent_id) > 0 HAVING COUNT(ekfs_agent_id) > 0
LIMIT 1 LIMIT 1
`) `)