first GodDaemons group commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
.well-known
|
||||||
|
wssGatewayConfig.json
|
||||||
|
nodeserver.cert
|
||||||
|
nodeserver.key
|
||||||
|
package-lock.json
|
||||||
|
brol/
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
export const construct = (redisCnx) => {
|
||||||
|
const tickMs = redisCnx.gpsSrv?.getGpsSettings().collisionTickMs ?? 100
|
||||||
|
setInterval(() => {
|
||||||
|
redisCnx.gpsSrv?.tickArena()
|
||||||
|
}, tickMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const methods = {
|
||||||
|
|
||||||
|
handleAgentEvent(msg) {
|
||||||
|
const agentId = msg.sender
|
||||||
|
if(!agentId || typeof(agentId) !== 'string') {
|
||||||
|
console.warn(`[${this.redisId}] Agent event without sender`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.eventType === 'change') {
|
||||||
|
const newVector = msg.payload?.newVector
|
||||||
|
if(!newVector || typeof(newVector.x) !== 'number' || typeof(newVector.y) !== 'number' || typeof(newVector.z) !== 'number') {
|
||||||
|
console.warn(`[${this.redisId}] Invalid newVector from ${agentId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const newPosition = msg.payload?.newPosition ?? null
|
||||||
|
this.gpsSrv.onVectorChange(agentId, newVector, newPosition)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(msg.eventType === 'remove') {
|
||||||
|
this.gpsSrv.onAgentRemove(agentId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchArenaMessage(msg, chan) {
|
||||||
|
const gps = this.config.gps
|
||||||
|
if(!gps || !this.gpsSrv) return false
|
||||||
|
|
||||||
|
if(this.matchesChan(chan, gps.agentVectorChangeChannel)) {
|
||||||
|
this.handleAgentEvent(msg)
|
||||||
|
return(true)
|
||||||
|
}
|
||||||
|
return(false)
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { methods as arenaMethods, construct as arenaConstruct } from './arenaHandlers.js'
|
||||||
|
|
||||||
|
export const afterLoginMethods = [
|
||||||
|
arenaConstruct,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const meshActions = {
|
||||||
|
...arenaMethods,
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
const PROXIMITY_EPSILON = 1e-6
|
||||||
|
|
||||||
|
export function needsPrismRefresh(agent, now, prismTimeHeight, prismRefreshLeadSeconds = 0) {
|
||||||
|
return(now >= agent.since + prismTimeHeight - prismRefreshLeadSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function advanceAgentSegment(agent, now) {
|
||||||
|
agent.position = positionAt(agent, now)
|
||||||
|
agent.since = now
|
||||||
|
agent.generation = (agent.generation ?? 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function positionAt(agent, t) {
|
||||||
|
const dt = t - agent.since
|
||||||
|
return({
|
||||||
|
x: agent.position.x + agent.vector.x * dt,
|
||||||
|
y: agent.position.y + agent.vector.y * dt,
|
||||||
|
z: agent.position.z + agent.vector.z * dt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function distanceBetween(agentA, agentB, t) {
|
||||||
|
const a = positionAt(agentA, t)
|
||||||
|
const b = positionAt(agentB, t)
|
||||||
|
const dx = a.x - b.x
|
||||||
|
const dy = a.y - b.y
|
||||||
|
const dz = a.z - b.z
|
||||||
|
return(Math.sqrt(dx * dx + dy * dy + dz * dz))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPrism(agent, tStart, tEnd, nearMissDistance) {
|
||||||
|
const p0 = positionAt(agent, tStart)
|
||||||
|
const p1 = positionAt(agent, tEnd)
|
||||||
|
const pad = nearMissDistance
|
||||||
|
return({
|
||||||
|
xMin: Math.min(p0.x, p1.x) - pad,
|
||||||
|
xMax: Math.max(p0.x, p1.x) + pad,
|
||||||
|
yMin: Math.min(p0.y, p1.y) - pad,
|
||||||
|
yMax: Math.max(p0.y, p1.y) + pad,
|
||||||
|
zMin: Math.min(p0.z, p1.z) - pad,
|
||||||
|
zMax: Math.max(p0.z, p1.z) + pad,
|
||||||
|
tMin: tStart,
|
||||||
|
tMax: tEnd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prismsIntersect(a, b) {
|
||||||
|
return(
|
||||||
|
a.xMin <= b.xMax && a.xMax >= b.xMin &&
|
||||||
|
a.yMin <= b.yMax && a.yMax >= b.yMin &&
|
||||||
|
a.zMin <= b.zMax && a.zMax >= b.zMin &&
|
||||||
|
a.tMin <= b.tMax && a.tMax >= b.tMin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function minimalDistance(agentA, agentB, tStart, tEnd) {
|
||||||
|
const relPos = {
|
||||||
|
x: positionAt(agentA, tStart).x - positionAt(agentB, tStart).x,
|
||||||
|
y: positionAt(agentA, tStart).y - positionAt(agentB, tStart).y,
|
||||||
|
z: positionAt(agentA, tStart).z - positionAt(agentB, tStart).z,
|
||||||
|
}
|
||||||
|
const relVel = {
|
||||||
|
x: agentA.vector.x - agentB.vector.x,
|
||||||
|
y: agentA.vector.y - agentB.vector.y,
|
||||||
|
z: agentA.vector.z - agentB.vector.z,
|
||||||
|
}
|
||||||
|
|
||||||
|
const relVelSq = relVel.x * relVel.x + relVel.y * relVel.y + relVel.z * relVel.z
|
||||||
|
let tStar = tStart
|
||||||
|
if(relVelSq > 0) {
|
||||||
|
const dot = relPos.x * relVel.x + relPos.y * relVel.y + relPos.z * relVel.z
|
||||||
|
tStar = tStart - dot / relVelSq
|
||||||
|
}
|
||||||
|
tStar = Math.max(tStart, Math.min(tEnd, tStar))
|
||||||
|
|
||||||
|
return({
|
||||||
|
distance: distanceBetween(agentA, agentB, tStar),
|
||||||
|
time: tStar,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function firstProximityEntry(agentA, agentB, tStart, tEnd, nearMissDistance) {
|
||||||
|
const d0 = distanceBetween(agentA, agentB, tStart)
|
||||||
|
if(d0 <= nearMissDistance + PROXIMITY_EPSILON) {
|
||||||
|
return({ time: tStart, distance: d0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const relPos = {
|
||||||
|
x: positionAt(agentA, tStart).x - positionAt(agentB, tStart).x,
|
||||||
|
y: positionAt(agentA, tStart).y - positionAt(agentB, tStart).y,
|
||||||
|
z: positionAt(agentA, tStart).z - positionAt(agentB, tStart).z,
|
||||||
|
}
|
||||||
|
const relVel = {
|
||||||
|
x: agentA.vector.x - agentB.vector.x,
|
||||||
|
y: agentA.vector.y - agentB.vector.y,
|
||||||
|
z: agentA.vector.z - agentB.vector.z,
|
||||||
|
}
|
||||||
|
const relVelSq = relVel.x * relVel.x + relVel.y * relVel.y + relVel.z * relVel.z
|
||||||
|
if(relVelSq === 0) return(null)
|
||||||
|
|
||||||
|
const a = relVelSq
|
||||||
|
const b = 2 * (relPos.x * relVel.x + relPos.y * relVel.y + relPos.z * relVel.z)
|
||||||
|
const c = relPos.x * relPos.x + relPos.y * relPos.y + relPos.z * relPos.z - nearMissDistance * nearMissDistance
|
||||||
|
const D = b * b - 4 * a * c
|
||||||
|
if(D < 0) return(null)
|
||||||
|
|
||||||
|
const sqrtD = Math.sqrt(D)
|
||||||
|
const maxDt = tEnd - tStart
|
||||||
|
const roots = [(-b - sqrtD) / (2 * a), (-b + sqrtD) / (2 * a)]
|
||||||
|
.filter(dt => dt >= 0 && dt <= maxDt)
|
||||||
|
.sort((x, y) => x - y)
|
||||||
|
|
||||||
|
for(const dt of roots) {
|
||||||
|
const t = tStart + dt
|
||||||
|
const d = distanceBetween(agentA, agentB, t)
|
||||||
|
if(d > nearMissDistance + PROXIMITY_EPSILON) continue
|
||||||
|
if(dt < 1e-12) return({ time: tStart, distance: d0 })
|
||||||
|
const dBefore = distanceBetween(agentA, agentB, t - 1e-6)
|
||||||
|
if(dBefore > nearMissDistance + PROXIMITY_EPSILON) return({ time: t, distance: d })
|
||||||
|
}
|
||||||
|
return(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareWorldlinePair(agentA, agentB, prismTimeHeight, nearMissDistance, now) {
|
||||||
|
const tStart = Math.max(agentA.since, agentB.since, now)
|
||||||
|
const tEnd = Math.min(
|
||||||
|
agentA.since + prismTimeHeight,
|
||||||
|
agentB.since + prismTimeHeight
|
||||||
|
)
|
||||||
|
if(tEnd <= tStart) return(null)
|
||||||
|
|
||||||
|
const prismA = buildPrism(agentA, tStart, tEnd, nearMissDistance)
|
||||||
|
const prismB = buildPrism(agentB, tStart, tEnd, nearMissDistance)
|
||||||
|
if(!prismsIntersect(prismA, prismB)) return(null)
|
||||||
|
|
||||||
|
const min = minimalDistance(agentA, agentB, tStart, tEnd)
|
||||||
|
if(min.distance > nearMissDistance + PROXIMITY_EPSILON) return(null)
|
||||||
|
|
||||||
|
const entry = firstProximityEntry(agentA, agentB, tStart, tEnd, nearMissDistance)
|
||||||
|
if(!entry) return(null)
|
||||||
|
|
||||||
|
return({
|
||||||
|
agentA: agentA.id,
|
||||||
|
agentB: agentB.id,
|
||||||
|
time: entry.time,
|
||||||
|
distance: entry.distance,
|
||||||
|
minTime: min.time,
|
||||||
|
minDistance: min.distance,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { methods as utilities, construct as utilitiesConstruct } from './utilities.js'
|
||||||
|
import { methods as positions } from './positions.js'
|
||||||
|
|
||||||
|
export const afterLoginMethods = [
|
||||||
|
utilitiesConstruct,
|
||||||
|
]
|
||||||
|
export const meshActions = {
|
||||||
|
...utilities,
|
||||||
|
...positions,
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { publishActionReply, parseAt } from '../../actionsHelper.js'
|
||||||
|
|
||||||
|
export const methods = {
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "GETAGENTPOSITION",
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"agentId": "agent42",
|
||||||
|
"at": "2026-06-07T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event-Tx:
|
||||||
|
{
|
||||||
|
"action": "GETAGENTPOSITION",
|
||||||
|
"success": true,
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"agent": {
|
||||||
|
"id": "agent42",
|
||||||
|
"position": { "x": 1, "y": 2, "z": 3 },
|
||||||
|
"vector": { "x": 0, "y": 0, "z": 0 },
|
||||||
|
"since": 1717750800,
|
||||||
|
"generation": 2,
|
||||||
|
"at": "2026-06-07T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_GETAGENTPOSITION(action, payload, reqid, sender, roles) {
|
||||||
|
const replyOpts = {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.gps.gpsActionsReply,
|
||||||
|
}
|
||||||
|
if(!this.accessRights.canDo(roles, action)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Unauthorized action !',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = payload?.agentId
|
||||||
|
if(!agentId || typeof(agentId) !== 'string') {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Missing or invalid agentId',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const at = parseAt(payload, () => this.gpsSrv.now())
|
||||||
|
if(at === null) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Invalid at timestamp',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const agent = this.gpsSrv.getAgentPosition(agentId, at)
|
||||||
|
if(!agent) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: `Unknown agent: ${agentId}`,
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: true,
|
||||||
|
payload: { agent },
|
||||||
|
} })
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "GETAGENTSINPRISM",
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"prism": {
|
||||||
|
"xMin": -10, "xMax": 10,
|
||||||
|
"yMin": -10, "yMax": 10,
|
||||||
|
"zMin": 0, "zMax": 5
|
||||||
|
},
|
||||||
|
"at": "2026-06-07T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event-Tx:
|
||||||
|
{
|
||||||
|
"action": "GETAGENTSINPRISM",
|
||||||
|
"success": true,
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
"payload": {
|
||||||
|
"agents": [ ... ],
|
||||||
|
"at": "2026-06-07T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_GETAGENTSINPRISM(action, payload, reqid, sender, roles) {
|
||||||
|
const replyOpts = {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.gps.gpsActionsReply,
|
||||||
|
}
|
||||||
|
if(!this.accessRights.canDo(roles, action)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Unauthorized action !',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const prism = payload?.prism
|
||||||
|
if(!this.gpsSrv.isValidPrism(prism)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Missing or invalid prism bounds',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const at = parseAt(payload, () => this.gpsSrv.now())
|
||||||
|
if(at === null) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Invalid at timestamp',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const agents = this.gpsSrv.getAgentsInPrism(prism, at)
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
agents,
|
||||||
|
at: new Date(at * 1000).toISOString(),
|
||||||
|
},
|
||||||
|
} })
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { publishActionReply } from '../../actionsHelper.js'
|
||||||
|
|
||||||
|
export const construct = (redisCnx) => {
|
||||||
|
// console.log('Hello after login from utilities...')
|
||||||
|
// redisCnx.v42=0
|
||||||
|
// setInterval(redisCnx.move4243.bind(redisCnx), 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const methods = {
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "TIME"
|
||||||
|
"reqid": "6az5e4r6a"
|
||||||
|
}
|
||||||
|
Event-Tx:
|
||||||
|
{
|
||||||
|
"action": "TIME",
|
||||||
|
"success": true,
|
||||||
|
"payload" : {
|
||||||
|
gpsTime: "2022-09-01T14:42:22.603Z",
|
||||||
|
redisTime: "2022-09-01T14:42:22.603Z"
|
||||||
|
},
|
||||||
|
"reqid": "6az5e4r6a"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_TIME(action, payload, reqid, sender, roles){
|
||||||
|
publishActionReply(this, {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.gps.gpsActionsReply,
|
||||||
|
reply: {
|
||||||
|
success: true,
|
||||||
|
payload: {
|
||||||
|
gpsTime: new Date().toISOString(),
|
||||||
|
redisTime: await this.redisClient.time(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "RELOADCONFIG"
|
||||||
|
"reqid": "6az5e4r6a"
|
||||||
|
}
|
||||||
|
Event-Tx:
|
||||||
|
{
|
||||||
|
"action": "RELOADCONFIG",
|
||||||
|
"success": true,
|
||||||
|
"reqid": "6az5e4r6a"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_RELOADCONFIG(action, payload, reqid, sender, roles){
|
||||||
|
const replyOpts = {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.gps.gpsActionsReply,
|
||||||
|
}
|
||||||
|
if(!this.accessRights.canDo(roles, action)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Unauthorized action !',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.reloadAccessRights()
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: true,
|
||||||
|
} })
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Event-Rx:
|
||||||
|
{
|
||||||
|
"action": "GETCONFIG"
|
||||||
|
"reqid": "6az5e4r6a"
|
||||||
|
}
|
||||||
|
Event-Tx:
|
||||||
|
{
|
||||||
|
"action": "GETCONFIG",
|
||||||
|
"success": true,
|
||||||
|
"reqid": "6az5e4r6a",
|
||||||
|
payload: { ...the access rights, and roles... }
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
async action_GETCONFIG(action, payload, reqid, sender, roles){
|
||||||
|
const replyOpts = {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
replyChannel: this.config.gps.gpsActionsReply,
|
||||||
|
}
|
||||||
|
if(!this.accessRights.canDo(roles, action)) {
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: false,
|
||||||
|
err: 'Unauthorized action !',
|
||||||
|
} })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
publishActionReply(this, { ...replyOpts, reply: {
|
||||||
|
success: true,
|
||||||
|
payload: this.getAccessRights(),
|
||||||
|
} })
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
export function publishActionReply(redisCnx, options) {
|
||||||
|
const {
|
||||||
|
action,
|
||||||
|
reqid,
|
||||||
|
sender,
|
||||||
|
reply,
|
||||||
|
replyChannel,
|
||||||
|
senderId = 'gps',
|
||||||
|
} = options
|
||||||
|
reply.action = action
|
||||||
|
reply.sender = senderId
|
||||||
|
if(reqid) reply.reqid = reqid
|
||||||
|
const chan = replyChannel.replace(/\[UID\]/g, sender)
|
||||||
|
redisCnx.redisPublish(chan, reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAt(payload, fallbackFn) {
|
||||||
|
if(!payload?.at) return(fallbackFn())
|
||||||
|
const t = Date.parse(payload.at)
|
||||||
|
if(Number.isNaN(t)) return(null)
|
||||||
|
return(t / 1000)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
export class AgentStore {
|
||||||
|
|
||||||
|
constructor(systemCnx, storage, debug = false) {
|
||||||
|
this.cnx = systemCnx
|
||||||
|
this.storage = storage
|
||||||
|
this.debug = debug
|
||||||
|
}
|
||||||
|
|
||||||
|
agentHashKey(agentId) {
|
||||||
|
return(this.storage.agentHashKey.replace(/\[UID\]/g, agentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportSegment(agent, eventType) {
|
||||||
|
try {
|
||||||
|
const record = {
|
||||||
|
eventType,
|
||||||
|
id: agent.id,
|
||||||
|
position: { ...agent.position },
|
||||||
|
vector: { ...agent.vector },
|
||||||
|
since: agent.since,
|
||||||
|
generation: agent.generation ?? 0,
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await this.cnx.redisHset(this.agentHashKey(agent.id), 'segment', record)
|
||||||
|
await this.cnx.redisSadd(this.storage.agentsIndexKey, agent.id)
|
||||||
|
await this.cnx.redisXadd(
|
||||||
|
this.storage.positionsStream,
|
||||||
|
record,
|
||||||
|
this.storage.streamMaxLen ?? ''
|
||||||
|
)
|
||||||
|
if(this.debug) console.log(`[GPS] Exported segment ${agent.id} (${eventType})`)
|
||||||
|
} catch(err) {
|
||||||
|
console.error(`[GPS] Failed to export segment for ${agent.id}:`, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportRemove(agentId) {
|
||||||
|
try {
|
||||||
|
const record = {
|
||||||
|
eventType: 'remove',
|
||||||
|
id: agentId,
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await this.cnx.redisDel(this.agentHashKey(agentId))
|
||||||
|
await this.cnx.redisSrem(this.storage.agentsIndexKey, agentId)
|
||||||
|
await this.cnx.redisXadd(
|
||||||
|
this.storage.positionsStream,
|
||||||
|
record,
|
||||||
|
this.storage.streamMaxLen ?? ''
|
||||||
|
)
|
||||||
|
if(this.debug) console.log(`[GPS] Exported remove ${agentId}`)
|
||||||
|
} catch(err) {
|
||||||
|
console.error(`[GPS] Failed to export remove for ${agentId}:`, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
export class CollisionRegistry {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.entries = []
|
||||||
|
}
|
||||||
|
|
||||||
|
purge(agentId) {
|
||||||
|
this.entries = this.entries.filter(
|
||||||
|
e => e.agentA !== agentId && e.agentB !== agentId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(entry) {
|
||||||
|
const duplicate = this.entries.find(
|
||||||
|
e =>
|
||||||
|
(e.agentA === entry.agentA && e.agentB === entry.agentB) ||
|
||||||
|
(e.agentA === entry.agentB && e.agentB === entry.agentA)
|
||||||
|
)
|
||||||
|
if(duplicate) {
|
||||||
|
if(entry.time < duplicate.time) Object.assign(duplicate, entry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.entries.push(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
dueBefore(t) {
|
||||||
|
return(this.entries.filter(e => e.time <= t))
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(entry) {
|
||||||
|
this.entries = this.entries.filter(
|
||||||
|
e =>
|
||||||
|
!((e.agentA === entry.agentA && e.agentB === entry.agentB) ||
|
||||||
|
(e.agentA === entry.agentB && e.agentB === entry.agentA))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { AccesRights } from '../accesRights.js'
|
||||||
|
import { AgentStore } from './agentStore.js'
|
||||||
|
import { CollisionRegistry } from './collisionRegistry.js'
|
||||||
|
import {
|
||||||
|
compareWorldlinePair,
|
||||||
|
positionAt,
|
||||||
|
needsPrismRefresh,
|
||||||
|
advanceAgentSegment,
|
||||||
|
} from './actions/arena/worldline.js'
|
||||||
|
|
||||||
|
export class gpsServer {
|
||||||
|
|
||||||
|
constructor(configHelper, allRediscnx, debug) {
|
||||||
|
this.configHelper = configHelper
|
||||||
|
this.gpsConfig = configHelper.config
|
||||||
|
this.allRediscnx = allRediscnx
|
||||||
|
this.debug = debug
|
||||||
|
this.accessRights = new AccesRights(this.gpsConfig, debug)
|
||||||
|
this.agents = new Map()
|
||||||
|
this.registry = new CollisionRegistry()
|
||||||
|
this.arenaCnx = null
|
||||||
|
this.systemCnx = null
|
||||||
|
this.agentStore = null
|
||||||
|
}
|
||||||
|
|
||||||
|
getGpsSettings() {
|
||||||
|
const gps = this.gpsConfig.gps ?? {}
|
||||||
|
return({
|
||||||
|
nearMissDistance: gps.nearMissDistance ?? 1,
|
||||||
|
prismTimeHeight: gps.prismTimeHeight ?? 60,
|
||||||
|
collisionTickMs: gps.collisionTickMs ?? 100,
|
||||||
|
prismRefreshLeadSeconds: gps.prismRefreshLeadSeconds ?? 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
now() {
|
||||||
|
return(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
wireSystemConnexion(cnx) {
|
||||||
|
cnx.gpsSrv = this
|
||||||
|
cnx.accessRights = this.accessRights
|
||||||
|
cnx.reloadAccessRights = () => this.reloadAccessRights()
|
||||||
|
cnx.getAccessRights = () => this.getAccessRights()
|
||||||
|
if(!this.systemCnx || cnx.redisConfig.role === 'primary') {
|
||||||
|
this.systemCnx = cnx
|
||||||
|
this.initAgentStore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initAgentStore() {
|
||||||
|
const storage = this.gpsConfig.gps?.storage
|
||||||
|
if(storage && this.systemCnx) {
|
||||||
|
this.agentStore = new AgentStore(this.systemCnx, storage, this.debug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wireArenaConnexion(cnx) {
|
||||||
|
cnx.gpsSrv = this
|
||||||
|
this.arenaCnx = cnx
|
||||||
|
}
|
||||||
|
|
||||||
|
getAgent(agentId) {
|
||||||
|
return(this.agents.get(agentId) ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllAgents() {
|
||||||
|
return([...this.agents.values()])
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAgentSnapshot(agent, at) {
|
||||||
|
return({
|
||||||
|
id: agent.id,
|
||||||
|
position: positionAt(agent, at),
|
||||||
|
vector: { ...agent.vector },
|
||||||
|
since: agent.since,
|
||||||
|
generation: agent.generation ?? 0,
|
||||||
|
at: new Date(at * 1000).toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getAgentPosition(agentId, at = null) {
|
||||||
|
const agent = this.agents.get(agentId)
|
||||||
|
if(!agent) return(null)
|
||||||
|
const t = at ?? this.now()
|
||||||
|
return(this.buildAgentSnapshot(agent, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
isPositionInPrism(position, prism) {
|
||||||
|
return(
|
||||||
|
position.x >= prism.xMin && position.x <= prism.xMax &&
|
||||||
|
position.y >= prism.yMin && position.y <= prism.yMax &&
|
||||||
|
position.z >= prism.zMin && position.z <= prism.zMax
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidPrism(prism) {
|
||||||
|
if(!prism || typeof(prism) !== 'object') return(false)
|
||||||
|
const axes = ['xMin', 'xMax', 'yMin', 'yMax', 'zMin', 'zMax']
|
||||||
|
for(const key of axes) {
|
||||||
|
if(typeof(prism[key]) !== 'number' || Number.isNaN(prism[key])) return(false)
|
||||||
|
}
|
||||||
|
return(
|
||||||
|
prism.xMin <= prism.xMax &&
|
||||||
|
prism.yMin <= prism.yMax &&
|
||||||
|
prism.zMin <= prism.zMax
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAgentsInPrism(prism, at = null) {
|
||||||
|
const t = at ?? this.now()
|
||||||
|
const agents = []
|
||||||
|
for(const agent of this.agents.values()) {
|
||||||
|
const position = positionAt(agent, t)
|
||||||
|
if(this.isPositionInPrism(position, prism)) {
|
||||||
|
agents.push(this.buildAgentSnapshot(agent, t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return(agents)
|
||||||
|
}
|
||||||
|
|
||||||
|
upsertAgent(agentId, newVector, newPosition = null) {
|
||||||
|
const now = this.now()
|
||||||
|
let agent = this.agents.get(agentId)
|
||||||
|
if(!agent) {
|
||||||
|
agent = {
|
||||||
|
id: agentId,
|
||||||
|
position: newPosition ?? { x: 0, y: 0, z: 0 },
|
||||||
|
vector: { ...newVector },
|
||||||
|
since: now,
|
||||||
|
generation: 1,
|
||||||
|
}
|
||||||
|
this.agents.set(agentId, agent)
|
||||||
|
this.agentStore?.exportSegment(agent, 'change')
|
||||||
|
return(agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.position = positionAt(agent, now)
|
||||||
|
agent.vector = { ...newVector }
|
||||||
|
agent.since = now
|
||||||
|
agent.generation = (agent.generation ?? 0) + 1
|
||||||
|
if(newPosition) agent.position = { ...newPosition }
|
||||||
|
this.agentStore?.exportSegment(agent, 'change')
|
||||||
|
return(agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
ensurePrismValid(agentId, refreshed = new Set()) {
|
||||||
|
return(this.refreshAgentPrism(agentId, refreshed))
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAgentPrism(agentId, refreshed = new Set()) {
|
||||||
|
if(refreshed.has(agentId)) return(false)
|
||||||
|
const agent = this.agents.get(agentId)
|
||||||
|
if(!agent) return(false)
|
||||||
|
|
||||||
|
const { prismTimeHeight, prismRefreshLeadSeconds } = this.getGpsSettings()
|
||||||
|
const now = this.now()
|
||||||
|
if(!needsPrismRefresh(agent, now, prismTimeHeight, prismRefreshLeadSeconds)) return(false)
|
||||||
|
|
||||||
|
refreshed.add(agentId)
|
||||||
|
this.registry.purge(agentId)
|
||||||
|
advanceAgentSegment(agent, now)
|
||||||
|
|
||||||
|
const hits = this.scanAgentPairs(agentId, refreshed)
|
||||||
|
for(const hit of hits) this.registry.add(hit)
|
||||||
|
this.agentStore?.exportSegment(agent, 'refresh')
|
||||||
|
if(this.debug) console.log(`[GPS] Prism refresh: ${agentId}`)
|
||||||
|
return(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanAgentPairs(changedAgentId, refreshed = new Set()) {
|
||||||
|
this.ensurePrismValid(changedAgentId, refreshed)
|
||||||
|
const changed = this.agents.get(changedAgentId)
|
||||||
|
if(!changed) return([])
|
||||||
|
|
||||||
|
const { nearMissDistance, prismTimeHeight } = this.getGpsSettings()
|
||||||
|
const now = this.now()
|
||||||
|
const hits = []
|
||||||
|
for(const [otherId, other] of this.agents) {
|
||||||
|
if(otherId === changedAgentId) continue
|
||||||
|
this.ensurePrismValid(otherId, refreshed)
|
||||||
|
const hit = compareWorldlinePair(
|
||||||
|
changed,
|
||||||
|
other,
|
||||||
|
prismTimeHeight,
|
||||||
|
nearMissDistance,
|
||||||
|
now
|
||||||
|
)
|
||||||
|
if(hit) hits.push(hit)
|
||||||
|
}
|
||||||
|
return(hits)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHits(hits) {
|
||||||
|
for(const hit of hits) this.registry.add(hit)
|
||||||
|
}
|
||||||
|
|
||||||
|
onVectorChange(agentId, newVector, newPosition = null) {
|
||||||
|
this.registry.purge(agentId)
|
||||||
|
this.upsertAgent(agentId, newVector, newPosition)
|
||||||
|
const hits = this.scanAgentPairs(agentId)
|
||||||
|
this.registerHits(hits)
|
||||||
|
if(this.debug && hits.length) console.log(`[GPS] ${hits.length} proximity pair(s) for ${agentId}`)
|
||||||
|
return(hits)
|
||||||
|
}
|
||||||
|
|
||||||
|
onAgentRemove(agentId) {
|
||||||
|
this.agents.delete(agentId)
|
||||||
|
this.registry.purge(agentId)
|
||||||
|
this.agentStore?.exportRemove(agentId)
|
||||||
|
if(this.debug) console.log(`[GPS] Agent removed: ${agentId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
tickPrismRefresh() {
|
||||||
|
for(const agentId of this.agents.keys()) {
|
||||||
|
this.refreshAgentPrism(agentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildProximityBatches(due) {
|
||||||
|
const byAgent = new Map()
|
||||||
|
for(const entry of due) {
|
||||||
|
for(const agentId of [entry.agentA, entry.agentB]) {
|
||||||
|
const otherAgent = entry.agentA === agentId ? entry.agentB : entry.agentA
|
||||||
|
if(!byAgent.has(agentId)) byAgent.set(agentId, [])
|
||||||
|
byAgent.get(agentId).push({
|
||||||
|
otherAgent,
|
||||||
|
distance: entry.distance,
|
||||||
|
at: new Date(entry.time * 1000).toISOString(),
|
||||||
|
minDistance: entry.minDistance,
|
||||||
|
minAt: new Date(entry.minTime * 1000).toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return(byAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishProximityBatch(targetAgentId, pairs) {
|
||||||
|
if(!this.arenaCnx || !pairs.length) return
|
||||||
|
const chan = this.arenaCnx.config.gps.collisionsChannel.replace(/\[UID\]/g, targetAgentId)
|
||||||
|
await this.arenaCnx.redisPublish(chan, {
|
||||||
|
eventType: 'proximity',
|
||||||
|
payload: { pairs },
|
||||||
|
sender: 'gps',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async tickCollisions() {
|
||||||
|
const due = this.registry.dueBefore(this.now())
|
||||||
|
if(!due.length) return
|
||||||
|
|
||||||
|
const batches = this.buildProximityBatches(due)
|
||||||
|
for(const entry of due) this.registry.remove(entry)
|
||||||
|
for(const [agentId, pairs] of batches) {
|
||||||
|
await this.publishProximityBatch(agentId, pairs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tickArena() {
|
||||||
|
this.tickPrismRefresh()
|
||||||
|
this.tickCollisions()
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadAccessRights() {
|
||||||
|
await this.configHelper.refreshAccessRights()
|
||||||
|
this.gpsConfig.accessRights = this.configHelper.config.accessRights
|
||||||
|
this.accessRights.refreshAccessRights(this.gpsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccessRights() {
|
||||||
|
return(this.gpsConfig.accessRights)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
import yargs from 'yargs/yargs'
|
||||||
|
import { hideBin } from 'yargs/helpers'
|
||||||
|
import 'node:process'
|
||||||
|
import {RedisConnexion} from './redisConnexion.js'
|
||||||
|
import {configHelper} from '../configHelper.js'
|
||||||
|
import {gpsServer} from './gpsServer.js'
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Little improvement on console.xxx /////////////////////////////////////
|
||||||
|
const originalLog = console.log
|
||||||
|
const originalWarn = console.warn
|
||||||
|
const originalError = console.error
|
||||||
|
function logWithTimestamp(originalFn, level, ...args) {
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
originalFn(`[${timestamp}] [${level}]`, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log = (...args) => logWithTimestamp(originalLog, 'LOG', ...args)
|
||||||
|
console.warn = (...args) => logWithTimestamp(originalWarn, 'WARN', ...args)
|
||||||
|
console.error = (...args) => logWithTimestamp(originalError, 'ERROR', ...args)
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const argv = yargs(hideBin(process.argv)).command('GPS', 'Global positions and collision system for P42', {})
|
||||||
|
.options({
|
||||||
|
'debug': {
|
||||||
|
description: 'shows debug info',
|
||||||
|
alias: 'd',
|
||||||
|
defaut: false,
|
||||||
|
type: 'boolean'
|
||||||
|
},
|
||||||
|
'config': {
|
||||||
|
description: 'Points to config file (default: ../config.json)',
|
||||||
|
alias: 'c',
|
||||||
|
default: '../config.json',
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
}).help().version('1.1').argv
|
||||||
|
|
||||||
|
const debug = Boolean(process.env.DEBUG) || argv.debug
|
||||||
|
|
||||||
|
let cfgh = new configHelper({
|
||||||
|
localfile: argv.config,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function meshRedisConns(mesh, meshName, debug, rootConfig) {
|
||||||
|
const { redis, ...meshConfig } = mesh
|
||||||
|
return redis.map(cfg =>
|
||||||
|
new RedisConnexion({
|
||||||
|
debug,
|
||||||
|
config: { ...cfg, ...meshConfig, gps: rootConfig.gps },
|
||||||
|
redisId: cfg.redisId,
|
||||||
|
meshName,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startAllRedis(rootConfig, cfgh) {
|
||||||
|
if (debug) console.log('Starting all Redis instances...')
|
||||||
|
|
||||||
|
//1. instantiate all & login all
|
||||||
|
const redisConns = [
|
||||||
|
...meshRedisConns(rootConfig.systemMesh, 'system', debug, rootConfig),
|
||||||
|
...meshRedisConns(rootConfig.arenaMesh, 'arena', debug, rootConfig),
|
||||||
|
]
|
||||||
|
|
||||||
|
const srv = new gpsServer(cfgh, redisConns, debug)
|
||||||
|
for(const cnx of redisConns) {
|
||||||
|
if(cnx.meshName === 'system') srv.wireSystemConnexion(cnx)
|
||||||
|
else if(cnx.meshName === 'arena') srv.wireArenaConnexion(cnx)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginResults = await Promise.allSettled(
|
||||||
|
redisConns.map(async cnx => {
|
||||||
|
await cnx.redisLogin()
|
||||||
|
return cnx.redisId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
//2. make sure all connected before going any further
|
||||||
|
const failedLogin = loginResults.filter(r => r.status !== 'fulfilled')
|
||||||
|
if (failedLogin.length > 0) {
|
||||||
|
console.error('Redis login failures:')
|
||||||
|
failedLogin.forEach((r, i) => {
|
||||||
|
const id = redisConns[i].redisId
|
||||||
|
console.error(`chansStart failed for redis:[${id}] → ${r.reason}`)
|
||||||
|
})
|
||||||
|
throw new Error(
|
||||||
|
`Redis login failed for ${failedLogin.length}/${redisConns.length} instances`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug) console.log('All Redis logins OK')
|
||||||
|
|
||||||
|
// --- Phase 2: start channels for all (since all succeeded)
|
||||||
|
const chanResults = await Promise.allSettled(
|
||||||
|
redisConns.map(async cnx => {
|
||||||
|
await cnx.redisChansStart()
|
||||||
|
return cnx.redisId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const failedChans = chanResults.filter(r => r.status !== 'fulfilled')
|
||||||
|
if (failedChans.length > 0) {
|
||||||
|
console.error('Redis chansStart failures:')
|
||||||
|
failedChans.forEach((r, i) => {
|
||||||
|
const id = redisConns[i].redisId
|
||||||
|
console.error(`chansStart failed for redis:[${id}] → ${r.reason}`)
|
||||||
|
})
|
||||||
|
throw new Error(
|
||||||
|
`Redis chansStart failed for ${failedChans.length}/${redisConns.length} instances`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug) console.log('All Redis chansStart OK')
|
||||||
|
|
||||||
|
return { redisConns, srv }
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgh.fetchConfig().then(async rootConfig => {
|
||||||
|
if(!rootConfig) {
|
||||||
|
console.error('Cannot get a valid configuration ! Aaarrghhh...')
|
||||||
|
process.exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Debug mode : ${debug ? 'ON' : 'OFF'}`)
|
||||||
|
|
||||||
|
await startAllRedis(rootConfig, cfgh)
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "gps",
|
||||||
|
"version": "3.4.6",
|
||||||
|
"description": "Websocket-Redis Message Bus Gateway",
|
||||||
|
"main": "gps.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --verbose"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "wp42GPS"
|
||||||
|
},
|
||||||
|
"author": "Nike",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.12.0",
|
||||||
|
"pm2": "^6.0.10",
|
||||||
|
"redis": "^4.3.0",
|
||||||
|
"urldecode": "^1.0.1",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import redis from 'redis'
|
||||||
|
import * as systemMesh from './actions/system/index.js'
|
||||||
|
import * as arenaMesh from './actions/arena/index.js'
|
||||||
|
|
||||||
|
const meshModules = {
|
||||||
|
system: systemMesh,
|
||||||
|
arena: arenaMesh,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RedisConnexion {
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
this.config = options.config
|
||||||
|
this.debug = options.debug
|
||||||
|
this.redisId = options.redisId
|
||||||
|
this.redisConfig = this.config
|
||||||
|
this.meshName = options.meshName
|
||||||
|
|
||||||
|
const mesh = meshModules[this.meshName]
|
||||||
|
if(mesh?.meshActions) Object.assign(this, mesh.meshActions)
|
||||||
|
this.afterLoginMethods = mesh?.afterLoginMethods ?? []
|
||||||
|
if(!mesh) console.warn(`[${this.redisId}] Unknown meshName: ${this.meshName}`)
|
||||||
|
|
||||||
|
this.redisClient = redis.createClient({
|
||||||
|
socket: {
|
||||||
|
tls: this.redisConfig.tls,
|
||||||
|
host: this.redisConfig.host,
|
||||||
|
port: this.redisConfig.port
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redisSubscriber = null;
|
||||||
|
this.redisClient.on('error', (err) => {
|
||||||
|
console.error('Redis error: ', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis started...`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullChan(chanName){
|
||||||
|
if(!chanName.startsWith(this.redisConfig.basePrefix)) chanName = this.redisConfig.basePrefix + chanName
|
||||||
|
return(chanName)
|
||||||
|
}
|
||||||
|
|
||||||
|
matchesChan(chan, pattern) {
|
||||||
|
const fullChan = this.fullChan(chan)
|
||||||
|
const fullPattern = this.fullChan(pattern)
|
||||||
|
const re = new RegExp('^' + fullPattern.replace(/\*/g, '(.+)') + '$')
|
||||||
|
return(re.test(fullChan))
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisLogin(){
|
||||||
|
if(this.debug) console.log(`Connecting to Redis (${this.redisConfig.host}:${this.redisConfig.port}, tls:${this.redisConfig.tls?'yes':'no'})...`);
|
||||||
|
await this.redisClient.connect();
|
||||||
|
if(this.debug) console.log(`Connected to Redis ${this.redisConfig.redisId}`);
|
||||||
|
if(this.redisConfig.user) {
|
||||||
|
await this.redisClient.sendCommand(['AUTH', this.redisConfig.user, this.redisConfig.pass]);
|
||||||
|
if(this.debug) console.log(`Logged into Redis ${this.redisConfig.redisId}`);
|
||||||
|
} else {
|
||||||
|
if(this.debug) console.log(`Connected (anon) to Redis ${this.redisConfig.redisId}`);
|
||||||
|
}
|
||||||
|
if(this.debug) {
|
||||||
|
var redisTime = await this.redisClient.time();
|
||||||
|
console.log(`[${this.redisConfig.redisId}] Redis ${this.redisConfig.redisId} time:`, redisTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const method of this.afterLoginMethods){
|
||||||
|
if(typeof method != 'function') continue
|
||||||
|
method(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisChansStart(){
|
||||||
|
this.redisSubscriber = this.redisClient.duplicate();
|
||||||
|
await this.redisSubscriber.connect();
|
||||||
|
if(this.redisConfig.user) {
|
||||||
|
await this.redisSubscriber.sendCommand(['AUTH', this.redisConfig.user, this.redisConfig.pass]);
|
||||||
|
}
|
||||||
|
const allChans = this.redisConfig.basePrefix + this.redisConfig.chansNamespace+'*'
|
||||||
|
this.redisSubscriber.pSubscribe(allChans, this.redisReceive.bind(this));
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] PSubscription OK `, allChans);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisSubscribe(chanName, callBack){
|
||||||
|
if(!chanName.startsWith(this.redisConfig.basePrefix)) chanName = this.redisConfig.basePrefix + chanName
|
||||||
|
if(!chanName.startsWith(this.redisConfig.basePrefix+this.redisConfig.chansNamespace)) {
|
||||||
|
console.warn(`[${this.redisConfig.redisId}] redisSubscribe : forbidden channel range on this redis !`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.redisSubscriber.subscribe(chanName, callBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisPublish(chanName, msg){
|
||||||
|
if(typeof (msg) != 'string') msg = JSON.stringify(msg);
|
||||||
|
if(!chanName.startsWith(this.redisConfig.basePrefix)) chanName = this.redisConfig.basePrefix + chanName
|
||||||
|
if(!chanName.startsWith(this.redisConfig.basePrefix+this.redisConfig.chansNamespace)) {
|
||||||
|
console.warn(`[${this.redisConfig.redisId}] redisPublish : forbidden channel range on this redis ! (${chanName} / ${this.redisConfig.basePrefix+this.redisConfig.chansNamespace}) `)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.redisClient.publish(chanName, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisSet(k, v, exp = 0, customPrefix=null){
|
||||||
|
if(typeof(v) != 'string') v = JSON.stringify(v);
|
||||||
|
if(customPrefix!==null) k = customPrefix + k
|
||||||
|
else if(!k.startsWith(this.redisConfig.basePrefix)) k = this.redisConfig.basePrefix + k
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis SET `, k);
|
||||||
|
try { await this.redisClient.set(k, v) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis set: `, k, v) }
|
||||||
|
if(exp > 0) {
|
||||||
|
try { await this.redisClient.expire(k, exp) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis expire: `, k, exp) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisGet(k, customPrefix=null){
|
||||||
|
if(customPrefix!==null) k = customPrefix + k
|
||||||
|
else if(!k.startsWith(this.redisConfig.basePrefix)) k = this.redisConfig.basePrefix + k
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis GET`, k)
|
||||||
|
let v=null
|
||||||
|
try { v = await this.redisClient.get(k) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis get: `, k) }
|
||||||
|
return(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveKey(k, customPrefix=null){
|
||||||
|
if(customPrefix!==null) return(customPrefix + k)
|
||||||
|
if(!k.startsWith(this.redisConfig.basePrefix)) k = this.redisConfig.basePrefix + k
|
||||||
|
return(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisDel(k, customPrefix=null){
|
||||||
|
k = this.resolveKey(k, customPrefix)
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Deleting`, k)
|
||||||
|
await this.redisClient.del(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisHset(k, field, v, customPrefix=null){
|
||||||
|
if(typeof(v) != 'string') v = JSON.stringify(v)
|
||||||
|
k = this.resolveKey(k, customPrefix)
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis HSET`, k, field)
|
||||||
|
try { await this.redisClient.hSet(k, field, v) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing HSET: `, k, field, err) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisHdel(k, field, customPrefix=null){
|
||||||
|
k = this.resolveKey(k, customPrefix)
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis HDEL`, k, field)
|
||||||
|
try { await this.redisClient.hDel(k, field) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing HDEL: `, k, field, err) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisSadd(k, member, customPrefix=null){
|
||||||
|
k = this.resolveKey(k, customPrefix)
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis SADD`, k, member)
|
||||||
|
try { await this.redisClient.sAdd(k, member) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing SADD: `, k, member, err) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisSrem(k, member, customPrefix=null){
|
||||||
|
k = this.resolveKey(k, customPrefix)
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis SREM`, k, member)
|
||||||
|
try { await this.redisClient.sRem(k, member) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing SREM: `, k, member, err) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async redisGetTtl(k, customPrefix=null){
|
||||||
|
if(customPrefix!==null) k = customPrefix + k
|
||||||
|
else if(!k.startsWith(this.redisConfig.basePrefix)) k = this.redisConfig.basePrefix + k
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis Get TTL `, k)
|
||||||
|
let v=null
|
||||||
|
try { v = await this.redisClient.ttl(k) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis ttl: `, k) }
|
||||||
|
return(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisSetTtl(k, ttl, customPrefix=null){
|
||||||
|
if(customPrefix!==null) k = customPrefix + k
|
||||||
|
else if(!k.startsWith(this.redisConfig.basePrefix)) k = this.redisConfig.basePrefix + k
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis Set TTL`, k);
|
||||||
|
try { await this.redisClient.expire(k, ttl) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis expire: `, k, ttl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisXadd(streamName, kvObj, max = ''){
|
||||||
|
if(!streamName.startsWith(this.redisConfig.basePrefix)) streamName = this.redisConfig.basePrefix + streamName
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis XADD `, streamName, kvObj);
|
||||||
|
let arr = ['XADD', streamName]
|
||||||
|
if(max != '') arr = [...arr, ...['MAXLEN', '~', (1*max).toString()]]
|
||||||
|
arr.push('*')
|
||||||
|
let payload = '""'
|
||||||
|
try{ payload = JSON.stringify(kvObj) }
|
||||||
|
catch(e) { console.warn(`[${this.redisConfig.redisId}] cannot historize bad json: `,kvObj) }
|
||||||
|
arr.push('streamData')
|
||||||
|
arr.push(payload)
|
||||||
|
let sid = null
|
||||||
|
try { sid = await this.redisClient.sendCommand(arr); }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis command: `, arr, err) }
|
||||||
|
return(sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisXrange(streamName, start = '-', end = '+', withPayload = true){
|
||||||
|
if(!streamName.startsWith(this.redisConfig.basePrefix)) streamName = this.redisConfig.basePrefix + streamName
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}]Redis XRANGE `, streamName);
|
||||||
|
if(typeof(start)!='string') start = start.toString()
|
||||||
|
if(typeof(end)!='string') end = end.toString()
|
||||||
|
let arr = ['XRANGE', streamName, start, end];
|
||||||
|
let res = []
|
||||||
|
try { res = await this.redisClient.sendCommand(arr) }
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis command: `, arr, err.msg) }
|
||||||
|
|
||||||
|
if(withPayload){
|
||||||
|
let o = {};
|
||||||
|
for (let row of res) { // We'll take only the first content of the stream (the value of the key 'streamdata')
|
||||||
|
let payload = ''
|
||||||
|
try{ payload = JSON.parse(row[1][1]) }
|
||||||
|
catch(e) { console.warn(`[${this.redisConfig.redisId}] cannot unhistorize bad json: `,row[1][1]) }
|
||||||
|
o[row[0]] = payload
|
||||||
|
}
|
||||||
|
return(o);
|
||||||
|
} else {
|
||||||
|
return(res.map(row => row[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHistorizedChan(chan){
|
||||||
|
if(!chan.startsWith(this.redisConfig.basePrefix)) chan = this.redisConfig.basePrefix + chan
|
||||||
|
var matches = this.redisConfig.historizeChannels.filter((e) => {
|
||||||
|
if(!e.startsWith(this.redisConfig.basePrefix)) e = this.redisConfig.basePrefix + e
|
||||||
|
if(e.indexOf('*') > -1) {
|
||||||
|
let r = new RegExp('^'+e.replace(/\*/g,'(.+)')+'$','g')
|
||||||
|
return(chan.match(r) != null);
|
||||||
|
} else return(chan == e);
|
||||||
|
});
|
||||||
|
return(matches.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProcessInfo(){
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Redis INFO`);
|
||||||
|
try {
|
||||||
|
const rawInfo = await this.redisClient.INFO()
|
||||||
|
let arr = rawInfo.replace(/\r/g,'').split('\n')
|
||||||
|
arr = arr.filter(item => (item.trim()!='') && (!item.trim().startsWith('#')))
|
||||||
|
let infoObject = arr.reduce((acc, val) => { kv = val.split(':',2); if(kv.length>0){ acc[kv[0]] = kv[1] } return(acc) }, {})
|
||||||
|
}
|
||||||
|
catch(err) { console.error(`[${this.redisConfig.redisId}] Redis crash doing Redis INFO: `) }
|
||||||
|
return(infoObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
async redisReceive(msg, chan){
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] From Redis chan:`, chan, msg)
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(msg)
|
||||||
|
} catch {
|
||||||
|
console.warn(`[${this.redisConfig.redisId}] Ignoring non-json on channel ${chan} : ${msg}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.meshName === 'arena' && this.config.gps && typeof(this.dispatchArenaMessage) === 'function') {
|
||||||
|
if(this.dispatchArenaMessage(msg, chan)) return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.meshName === 'system' && this.config.gps?.gpsActionsChannel){
|
||||||
|
const actionsChan = this.fullChan(this.config.gps.gpsActionsChannel)
|
||||||
|
if(chan != actionsChan) return
|
||||||
|
|
||||||
|
const action = msg.action
|
||||||
|
if(!action || typeof(action) !== 'string') {
|
||||||
|
console.warn(`[${this.redisConfig.redisId}] Ignoring message without action on ${chan}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = this['action_'+action]
|
||||||
|
if(typeof(handler) != 'function') {
|
||||||
|
if(this.debug) console.warn(`[${this.redisConfig.redisId}] Unknown action ${action} on ${chan}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = ('payload' in msg) ? msg.payload : null
|
||||||
|
const reqid = ('reqid' in msg) ? msg.reqid.substr(0, 50) : null
|
||||||
|
const sender = msg.sender || null
|
||||||
|
const roles = Array.isArray(msg.roles) ? msg.roles : ['*']
|
||||||
|
|
||||||
|
if(this.debug) console.log(`[${this.redisConfig.redisId}] Dispatching action ${action} from ${sender}`)
|
||||||
|
handler.call(this, action, payload, reqid, sender, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// redis-cli -h redis.backend.eismea.eu --tls --user default
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cd /opt/p42GodDaemons/GPS/
|
||||||
|
|
||||||
|
pid=`ps -ef | grep p42Gps |grep -v grep | awk '{print $2}'`
|
||||||
|
if [ -z "$pid" ]
|
||||||
|
then
|
||||||
|
node p42Gps.js --debug > gps.log 2>&1 &
|
||||||
|
else
|
||||||
|
echo ''
|
||||||
|
echo 'Already running PID='"$pid"' (use stopGps.sh to stop it)'
|
||||||
|
echo ''
|
||||||
|
fi
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
pid=`ps -ef | grep p42Gps.js |grep -v grep | awk '{print $2}'`
|
||||||
|
if [ -n "$pid" ]
|
||||||
|
then
|
||||||
|
echo "killing pid: $pid"
|
||||||
|
kill -9 $pid
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
export class AccesRights {
|
||||||
|
|
||||||
|
constructor(config, debug){
|
||||||
|
this.debug = debug
|
||||||
|
this.config = config
|
||||||
|
this.rights = config.accessRights
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAccessRights(config){
|
||||||
|
this.rights = config.accessRights
|
||||||
|
}
|
||||||
|
|
||||||
|
mustSubscribe(uid, roles) {
|
||||||
|
if(roles.indexOf('*')<0) roles.push('*')
|
||||||
|
let chans = []
|
||||||
|
for(let myRole of roles){
|
||||||
|
for(let rightBlock of this.rights) {
|
||||||
|
if(!rightBlock.mustSubscribe) continue
|
||||||
|
if((rightBlock.roles=='*') || (rightBlock.roles.indexOf(myRole)>-1)) {
|
||||||
|
chans = this.merge(chans, rightBlock.mustSubscribe.map(item=>item.replace(/\[UID\]/g,uid)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return(chans)
|
||||||
|
}
|
||||||
|
|
||||||
|
isMandatory(uid, roles, chan){
|
||||||
|
return(this.mustSubscribe(uid, roles).filter(this.chanMatch.bind(this, chan)).length>0)
|
||||||
|
}
|
||||||
|
|
||||||
|
canSubscribe(uid, roles, myChan) {
|
||||||
|
if(roles.indexOf('*')<0) roles.push('*')
|
||||||
|
for(let myRole of roles){
|
||||||
|
for(let rightBlock of this.rights) {
|
||||||
|
if(!rightBlock.canSubscribe) continue
|
||||||
|
if((rightBlock.roles=='*') || (rightBlock.roles.indexOf(myRole)>-1)) {
|
||||||
|
let canSubList = rightBlock.canSubscribe.map(item=>item.replace(/\[UID\]/g, uid))
|
||||||
|
if(canSubList.find(this.chanMatch.bind(this, myChan))) return(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if(this.debug) console.log(`Roles : ${roles} cannot subscribe on ${myChan}`)
|
||||||
|
return(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
canPublish(uid, roles, myChan) {
|
||||||
|
if(roles.indexOf('*')<0) roles.push('*')
|
||||||
|
for(let myRole of roles){
|
||||||
|
for(let rightBlock of this.rights) {
|
||||||
|
if(!rightBlock.canPublish) continue
|
||||||
|
if((rightBlock.roles=='*') || (rightBlock.roles.indexOf(myRole)>-1)) {
|
||||||
|
let canPubList = rightBlock.canPublish.map(item=>item.replace(/\[UID\]/g, uid))
|
||||||
|
if(canPubList.find(this.chanMatch.bind(this, myChan))) return(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if(this.debug) console.log(`Roles : ${roles} cannot publish on ${myChan}`)
|
||||||
|
return(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
canSet(uid, roles, myKey){
|
||||||
|
if(roles.indexOf('*')<0) roles.push('*')
|
||||||
|
for(let myRole of roles){
|
||||||
|
for(let rightBlock of this.rights) {
|
||||||
|
if(!rightBlock.canSet) continue
|
||||||
|
if((rightBlock.roles=='*') || (rightBlock.roles.indexOf(myRole)>-1)) {
|
||||||
|
let canSetList = rightBlock.canSet.map(item=>item.replace(/\[UID\]/g, uid))
|
||||||
|
if(canSetList.find(this.chanMatch.bind(this, myKey))) return(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if(this.debug) console.log(`Roles : ${roles} cannot set ${myKey}`)
|
||||||
|
return(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
canGet(uid, roles, myKey){
|
||||||
|
if(roles.indexOf('*')<0) roles.push('*')
|
||||||
|
for(let myRole of roles){
|
||||||
|
for(let rightBlock of this.rights) {
|
||||||
|
if(!rightBlock.canGet) continue
|
||||||
|
if((rightBlock.roles=='*') || (rightBlock.roles.indexOf(myRole)>-1)) {
|
||||||
|
let canGetList = rightBlock.canGet.map(item=>item.replace(/\[UID\]/g, uid))
|
||||||
|
if(canGetList.find(this.chanMatch.bind(this, myKey))) return(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//if(this.debug) console.log(`Roles : ${roles} cannot get ${myKey}`)
|
||||||
|
return(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
canDo(roles, action, uid=null){
|
||||||
|
if(roles.indexOf('*')<0) roles.push('*')
|
||||||
|
for(let myRole of roles){
|
||||||
|
for(let rightBlock of this.rights) {
|
||||||
|
if(!rightBlock.canDo) continue
|
||||||
|
if((rightBlock.roles=='*') || (rightBlock.roles.indexOf(myRole)>-1)) {
|
||||||
|
if(rightBlock.canDo.indexOf(action)>-1) {
|
||||||
|
if((rightBlock.uuids) && Array.isArray((rightBlock.uuids))) {
|
||||||
|
// !!! Separate condition so if rightBlock.uuids but not uid in it, we don't give access !
|
||||||
|
if(rightBlock.uuids.includes(uid)) return(true) // null should never be in config uuids => old style calls are safe
|
||||||
|
} else { // no uuid block => role is sufficient to give access
|
||||||
|
return(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Anti-shoot-your-foot
|
||||||
|
if((action=='reloadAccessRights') && (roles.includes('EIC_Dev'))) {
|
||||||
|
console.error('Prevented you from shooting your foot ! \nPlease keep least EIC_Dev in reload access rights !')
|
||||||
|
return(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
//if(this.debug) console.log(`Roles : ${roles} cannot do ${action}`)
|
||||||
|
return(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
chanMatch(myChan, configChan) {
|
||||||
|
if((!myChan) || (typeof(myChan)!='string')) return(false)
|
||||||
|
let re = new RegExp('^'+configChan.replace(/\*/g,'(.+)')+'$','g')
|
||||||
|
return(myChan.match(re)!=null)
|
||||||
|
}
|
||||||
|
|
||||||
|
merge(x, y) {
|
||||||
|
let tmp = x
|
||||||
|
for(let yitem of y) if(tmp.indexOf(yitem)<0) tmp.push(yitem)
|
||||||
|
return(tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"accessRights": [
|
||||||
|
{
|
||||||
|
"canDo": [
|
||||||
|
"RELOADCONFIG",
|
||||||
|
"GETCONFIG"
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
"admin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"canDo": [
|
||||||
|
"GETAGENTPOSITION",
|
||||||
|
"GETAGENTSINPRISM"
|
||||||
|
],
|
||||||
|
"roles": "*"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"gps": {
|
||||||
|
"gpsActionsChannel": "system:requests:gps",
|
||||||
|
"gpsActionsReply": "system:replies:[UID]",
|
||||||
|
"storage": {
|
||||||
|
"agentHashKey": "system:gps:agent:[UID]",
|
||||||
|
"agentsIndexKey": "system:gps:agents",
|
||||||
|
"positionsStream": "system:gps:positions",
|
||||||
|
"streamMaxLen": 100000
|
||||||
|
},
|
||||||
|
"agentVectorChangeChannel": "arena:agents:*",
|
||||||
|
"collisionsChannel": "arena:agents:[UID]",
|
||||||
|
"nearMissDistance": 1,
|
||||||
|
"prismTimeHeight": 60,
|
||||||
|
"collisionTickMs": 100,
|
||||||
|
"prismRefreshLeadSeconds": 1
|
||||||
|
},
|
||||||
|
"systemMesh": {
|
||||||
|
"redis": [
|
||||||
|
{
|
||||||
|
"redisId": "SYS_1",
|
||||||
|
"role": "primary",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"tls": false,
|
||||||
|
"port": 6380,
|
||||||
|
"user": "",
|
||||||
|
"pass": "",
|
||||||
|
"chansNamespace": "system:",
|
||||||
|
"basePrefix": "messageBus:"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"arenaMesh": {
|
||||||
|
"redis": [
|
||||||
|
{
|
||||||
|
"redisId": "ARN_1",
|
||||||
|
"role": "primary",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"tls": false,
|
||||||
|
"port": 6379,
|
||||||
|
"user": "",
|
||||||
|
"pass": "",
|
||||||
|
"chansNamespace": "arena:",
|
||||||
|
"basePrefix": "messageBus:"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import Ajv from 'ajv'
|
||||||
|
import confSchema from './configSchema.json' with { type: 'json' }
|
||||||
|
import { pathToFileURL } from 'url'
|
||||||
|
|
||||||
|
export class configHelper {
|
||||||
|
|
||||||
|
constructor(options){
|
||||||
|
this.config = {}
|
||||||
|
this.localfile = options.localfile
|
||||||
|
this.fetchConfig = this.fetchConfigFile
|
||||||
|
this.refreshAccessRights = this.refreshAccessRightsFile
|
||||||
|
const ajv = new Ajv({
|
||||||
|
allowUnionTypes: true
|
||||||
|
})
|
||||||
|
this.configValidator = ajv.compile(confSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
isValidConfig(conf){
|
||||||
|
if(this.configValidator(conf)) return(true)
|
||||||
|
console.error('Invalid configuration: ', this.configValidator.errors)
|
||||||
|
return(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchConfigFile(){
|
||||||
|
let curConfig = this.config
|
||||||
|
const url = pathToFileURL(this.localfile).href
|
||||||
|
this.config = await import(url, {
|
||||||
|
with: { type: 'json' }
|
||||||
|
}).then(m => m.default)
|
||||||
|
if(this.isValidConfig(this.config)) return(this.config)
|
||||||
|
console.error(this.configValidator.errors)
|
||||||
|
//revert if invalid conf
|
||||||
|
this.config = curConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAccessRightsDynamo(){
|
||||||
|
let ar = await this.dynamoGet('accessRights')
|
||||||
|
this.config.accessRights = ar
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAccessRightsFile(){
|
||||||
|
let tmp
|
||||||
|
try { tmp = import(`${this.localfile}?update=${Date.now()}`, { assert: { type: 'json' } }) }
|
||||||
|
catch(err) {
|
||||||
|
console.error('Error Reloading config !! (bad json?) => Keeping current accessRights !')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(!tmp.accessRights) {
|
||||||
|
console.error('Error Reloading config !! (no accessRights !) => Keeping current accessRights !')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.config.accessRights = tmp.accessRights
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"$id": "https://nicsys.eu/god-daemons-config.schema.json",
|
||||||
|
"title": "P42 God Daemons Configuration",
|
||||||
|
"description": "Shared configuration for P42 God Daemons",
|
||||||
|
"type": "object",
|
||||||
|
"definitions": {
|
||||||
|
"redisConnection": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"redisId": { "type": "string" },
|
||||||
|
"role": { "type": "string", "enum": ["primary", "shard"] },
|
||||||
|
"host": { "type": "string" },
|
||||||
|
"tls": { "type": "boolean" },
|
||||||
|
"port": { "type": "integer" },
|
||||||
|
"user": { "type": "string" },
|
||||||
|
"pass": { "type": "string" },
|
||||||
|
"chansNamespace": { "type": "string" },
|
||||||
|
"basePrefix": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"redisId",
|
||||||
|
"chansNamespace",
|
||||||
|
"basePrefix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"redisArray": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/redisConnection" },
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"accessRights": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roles": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"array"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"canDo": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"roles",
|
||||||
|
"canDo"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gps": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"gpsActionsChannel": { "type": "string" },
|
||||||
|
"gpsActionsReply": { "type": "string" },
|
||||||
|
"storage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agentHashKey": { "type": "string" },
|
||||||
|
"agentsIndexKey": { "type": "string" },
|
||||||
|
"positionsStream": { "type": "string" },
|
||||||
|
"streamMaxLen": { "type": "integer", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"agentHashKey",
|
||||||
|
"agentsIndexKey",
|
||||||
|
"positionsStream"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"agentVectorChangeChannel": { "type": "string" },
|
||||||
|
"collisionsChannel": { "type": "string" },
|
||||||
|
"nearMissDistance": { "type": "number", "minimum": 0 },
|
||||||
|
"prismTimeHeight": { "type": "number", "minimum": 0 },
|
||||||
|
"collisionTickMs": { "type": "integer", "minimum": 1 },
|
||||||
|
"prismRefreshLeadSeconds": { "type": "number", "minimum": 0 }
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"gpsActionsChannel",
|
||||||
|
"gpsActionsReply",
|
||||||
|
"storage",
|
||||||
|
"agentVectorChangeChannel",
|
||||||
|
"collisionsChannel"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"systemMesh": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"redis": { "$ref": "#/definitions/redisArray" }
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"redis"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"arenaMesh": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"redis": { "$ref": "#/definitions/redisArray" }
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"redis"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"accessRights",
|
||||||
|
"systemMesh",
|
||||||
|
"arenaMesh"
|
||||||
|
],
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "p42GodDaemons",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user