Compare commits

...

2 Commits

Author SHA1 Message Date
STEINNI cf292031fd threetobus now uses [UID] template, MessageBus upgraded to cnxId 2026-06-26 21:56:19 +00:00
STEINNI aefa5ab147 no local cursor rules 2026-06-26 15:39:02 +00:00
5 changed files with 81 additions and 128 deletions
-39
View File
@@ -1,39 +0,0 @@
---
description: SQL conventions — no table aliases; columns are always prefixed
globs: "**/*.{js,sql,php}"
alwaysApply: false
---
# SQL rules
All tables use **prefixed column names** (`sim_uuid`, `own_usr_id`, `usr_uuid`, `ekfs_agent_id`, …). There is never column ambiguity across joins, so **do not use table aliases**.
## Do not
- Short table aliases: `s`, `o`, `u`, `ekfs`, etc.
- Qualified columns when an alias was the only reason: `s.sim_uuid`, `o.own_sim_uuid`
## Do
- Join on bare prefixed column names.
- Use full table names (or `${qualified}` template vars) in `FROM` / `JOIN` only — no alias after the table.
```sql
-- BAD
SELECT s.sim_name, BIN_TO_UUID(s.sim_uuid) AS simulationUuid
FROM `p42SIM`.simulations s
INNER JOIN `p42GUI`.simowners o ON o.own_sim_uuid = s.sim_uuid
INNER JOIN `p42GUI`.users u ON o.own_usr_id = u.usr_id
WHERE u.usr_uuid = ? AND s.sim_uuid = UUID_TO_BIN(?)
-- GOOD
SELECT sim_name, BIN_TO_UUID(sim_uuid) AS simulationUuid
FROM `p42SIM`.simulations
INNER JOIN `p42GUI`.simowners ON own_sim_uuid = sim_uuid
INNER JOIN `p42GUI`.users ON own_usr_id = usr_id
WHERE usr_uuid = ? AND sim_uuid = UUID_TO_BIN(?)
```
`AS` on **result columns** (e.g. `AS simulationUuid` for the API) is fine — that is not a table alias.
When adding a query, read an existing join in the same repo first and match its style.
+1 -1
View File
@@ -99,7 +99,7 @@ class Snaptobus{
processBusEvent(eventType, chan, payload, userId, x){ processBusEvent(eventType, chan, payload, userId, x){
const chanObj = this._curBusConfig.find(item => item.chan==chan) const chanObj = this._curBusConfig.find(item => app.MessageBus.chanMatch(chan, item.chan))
if(!chanObj) return if(!chanObj) return
const eventObj = chanObj.events.find(item => item.eventName==eventType) const eventObj = chanObj.events.find(item => item.eventName==eventType)
if(!eventObj) return if(!eventObj) return
+2 -19
View File
@@ -33,31 +33,14 @@ export class Threetobus{
if(this._frustumWatching) this._scheduleFrustumResubscribe() if(this._frustumWatching) this._scheduleFrustumResubscribe()
} }
resolveSubscriberChan(chanTemplate) {
if(typeof(chanTemplate) !== 'string') return(null)
const uid = app.User?.identity?.uuid
if(!uid) return(null)
return(chanTemplate.replace(/\[UID\]/g, uid).replace(/\{uid\}/g, uid))
}
_resolvedEventsMapping() {
if(!Array.isArray(this._stagedEventsMapping)) return([])
return(this._stagedEventsMapping.map(chanObj => {
const chan = this.resolveSubscriberChan(chanObj.chan)
if(!chan) return(null)
return({ ...chanObj, chan })
}).filter(Boolean))
}
get EventsMapping() { return this._stagedEventsMapping } get EventsMapping() { return this._stagedEventsMapping }
get liveEventsMapping() { return this._curEventsMapping } get liveEventsMapping() { return this._curEventsMapping }
set EventsMapping(newConfig) { this._stagedEventsMapping = newConfig } set EventsMapping(newConfig) { this._stagedEventsMapping = newConfig }
async commitConfig(){ async commitConfig(){
const resolvedMapping = this._resolvedEventsMapping()
const chansToAdd = [] const chansToAdd = []
const chansToKeep = [] const chansToKeep = []
for(const chanObj of resolvedMapping){ for(const chanObj of this._stagedEventsMapping){
if(this._curEventsMapping.map(item => item.chan).includes(chanObj.chan)) chansToKeep.push(chanObj) if(this._curEventsMapping.map(item => item.chan).includes(chanObj.chan)) chansToKeep.push(chanObj)
else chansToAdd.push(chanObj) else chansToAdd.push(chanObj)
} }
@@ -91,7 +74,7 @@ export class Threetobus{
app.MessageBus.removeBusListener(eventToDel.eventName, this.processBusEvent.bind(this, eventToDel.eventName), 'threetobus') app.MessageBus.removeBusListener(eventToDel.eventName, this.processBusEvent.bind(this, eventToDel.eventName), 'threetobus')
} }
this._curEventsMapping = this.deepClone(resolvedMapping) this._curEventsMapping = this.deepClone(this._stagedEventsMapping)
} }
deepClone(obj) { // Needed because structuredClone doesn't take functions (and we have transformers) deepClone(obj) { // Needed because structuredClone doesn't take functions (and we have transformers)
+76 -69
View File
@@ -84,26 +84,26 @@ class MessageBus {
*/ */
constructor(config, userInfo){ constructor(config, userInfo){
this.config = config this.config = config
if(this.config.debug) console.log('Lauching Websocket worker...'); if(this.config.debug) console.log('Lauching Websocket worker...')
this.config.hostname = (('host' in this.config) && ( this.config.host!='')) ? this.config.host : document.location.hostname this.config.hostname = (('host' in this.config) && ( this.config.host!='')) ? this.config.host : document.location.hostname
this.userInfo = userInfo this.userInfo = userInfo
this.createWorker(); this.createWorker()
this.activeSubscriptions = []; this.activeSubscriptions = [];
this.promisesRegister = { }; this.promisesRegister = { }
this.bus2jsEventsRegister = []; // items: { eventType:'string', RegisteredCb: function, realCb: function } this.bus2jsEventsRegister = []; // items: { eventType:'string', RegisteredCb: function, realCb: function }
this.whenConnectedQ = []; this.whenConnectedQ = []
this.connected = false; this.connected = false
} }
/** /**
* *
*/ */
createWorker() { createWorker() {
if(!this.config.pathToWorker.endsWith('.js')) this.config.pathToWorker+='.js'; if(!this.config.pathToWorker.endsWith('.js')) this.config.pathToWorker+='.js'
this.MessageBusWorker = new Worker(this.config.pathToWorker+'?'+crypto.randomUUID()); this.MessageBusWorker = new Worker(this.config.pathToWorker+'?'+crypto.randomUUID())
this.MessageBusWorker.postMessage({ 'action':'start', 'config': this.config, 'userInfo': this.userInfo }); this.MessageBusWorker.postMessage({ 'action':'start', 'config': this.config, 'userInfo': this.userInfo });
this.MessageBusWorker.onmessage = this.receiveFromWorker.bind(this); this.MessageBusWorker.onmessage = this.receiveFromWorker.bind(this)
if(this.config.debug) console.log('Websocket worker launched.'); if(this.config.debug) console.log('Websocket worker launched.')
} }
/** /**
@@ -111,9 +111,9 @@ class MessageBus {
* @param {*} callBack * @param {*} callBack
*/ */
whenConnected(callBack){ whenConnected(callBack){
if(typeof(callBack) != 'function') return; if(typeof(callBack) != 'function') return
if(this.connected) callBack(); if(this.connected) callBack()
else this.whenConnectedQ.push(callBack); else this.whenConnectedQ.push(callBack)
} }
/** /**
@@ -135,8 +135,8 @@ class MessageBus {
* @param {*} callBack * @param {*} callBack
*/ */
ifConnected(callBack){ ifConnected(callBack){
if(typeof(callBack) != 'function') return; if(typeof(callBack) != 'function') return
if(this.connected) callBack(); if(this.connected) callBack()
} }
/** /**
@@ -152,17 +152,17 @@ class MessageBus {
* This method gives (and resolves) a promise, taking care of all lower-level details * This method gives (and resolves) a promise, taking care of all lower-level details
*/ */
requestWssGwAction(action, payload=null, timeOut=5000){ requestWssGwAction(action, payload=null, timeOut=5000){
if(!action) return; if(!action) return
let request = {'action':action, 'payload':payload}; let request = {'action':action, 'payload':payload}
request.reqid = crypto.randomUUID(); request.reqid = crypto.randomUUID()
return(new Promise((resolve, fail) => { return(new Promise((resolve, fail) => {
let timeOutID = setTimeout(() => { let timeOutID = setTimeout(() => {
fail(`Timeout (>${timeOut}ms) for action ${action}`); fail(`Timeout (>${timeOut}ms) for action ${action}`);
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid]) if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
}, timeOut); }, timeOut)
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]; this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]
this.MessageBusWorker.postMessage(request); this.MessageBusWorker.postMessage(request)
})); }))
} }
/** /**
@@ -172,18 +172,18 @@ class MessageBus {
* This method gives (and resolves) a promise, taking care of all lower-level details * This method gives (and resolves) a promise, taking care of all lower-level details
*/ */
requestBusAction(chan, action, payload=null, timeOut=5000){ requestBusAction(chan, action, payload=null, timeOut=5000){
if(!action) return; if(!action) return
let request = {'action':action, 'payload':payload}; let request = {'action':action, 'payload':payload}
request.reqid = crypto.randomUUID(); request.reqid = crypto.randomUUID()
return(new Promise((resolve, fail) => { return(new Promise((resolve, fail) => {
let timeOutID = setTimeout(() => { let timeOutID = setTimeout(() => {
fail(`Timeout (>${timeOut}ms) for action ${action}`); fail(`Timeout (>${timeOut}ms) for action ${action}`);
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid]) if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
}, timeOut); }, timeOut)
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]; this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]
if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan
this.send(chan, JSON.stringify(request)) this.send(chan, JSON.stringify(request))
})); }))
} }
/** /**
@@ -193,18 +193,18 @@ class MessageBus {
* This method gives (and resolves) a promise, taking care of all lower-level details * This method gives (and resolves) a promise, taking care of all lower-level details
*/ */
requestMidasAction(chan, action, data=null, timeOut=5000){ requestMidasAction(chan, action, data=null, timeOut=5000){
if(!action) return; if(!action) return
let request = {payload: {'action':action, 'data':data}} let request = {payload: {'action':action, 'data':data}}
request.reqid = crypto.randomUUID(); request.reqid = crypto.randomUUID()
return(new Promise((resolve, fail) => { return(new Promise((resolve, fail) => {
let timeOutID = setTimeout(() => { let timeOutID = setTimeout(() => {
fail(`Timeout (>${timeOut}ms) for action ${action}`); fail(`Timeout (>${timeOut}ms) for action ${action}`);
if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid]) if(this.promisesRegister[request.reqid]) delete(this.promisesRegister[request.reqid])
}, timeOut); }, timeOut)
this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]; this.promisesRegister[request.reqid] = [resolve, fail, timeOutID]
if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan if(!chan.startsWith(this.config.frontBusPrefix)) chan = this.config.frontBusPrefix+chan
this.send(chan, JSON.stringify(request)) this.send(chan, JSON.stringify(request))
})); }))
} }
/** /**
@@ -230,9 +230,9 @@ class MessageBus {
*/ */
send(chan, msg){ send(chan, msg){
// You can publish to an unsubscribed chan, userchans are the best example ! // You can publish to an unsubscribed chan, userchans are the best example !
// if(this.activeSubscriptions.indexOf(chan)<0) return; // if(this.activeSubscriptions.indexOf(chan)<0) return
var request = {'action':'PUB', 'payload': { 'chan':chan, 'msg': msg}}; var request = {'action':'PUB', 'payload': { 'chan':chan, 'msg': msg}}
this.MessageBusWorker.postMessage(request); this.MessageBusWorker.postMessage(request)
} }
/** /**
@@ -322,11 +322,13 @@ class MessageBus {
/** /**
* Helper method to match a chan with globbing * Helper method to match a chan with globbing
* *
* @param {string} myChan (no glob) * @param {string} myChan (no glob, no user expansion)
* @param {string} targetChan PATTERN (possible glob) * @param {string} targetChan PATTERN (possible glob and user expansion)
* @returns {boolean} * @returns {boolean}
*/ */
chanMatch(myChan, targetChan) { chanMatch(myChan, targetChan) {
targetChan = targetChan.replace(/\[UID\]/g, this.userInfo.uuid)
targetChan = targetChan.replace(/\[CNXID\]/g, this.cnxId)
let re = new RegExp('^'+targetChan.replace(/\*/g,'(.+)')+'$','g') let re = new RegExp('^'+targetChan.replace(/\*/g,'(.+)')+'$','g')
return(myChan.match(re)!=null) return(myChan.match(re)!=null)
} }
@@ -339,28 +341,28 @@ class MessageBus {
var workermsg = e.data; var workermsg = e.data;
if('event' in workermsg){ if('event' in workermsg){
// event "ReceiveFromServer" is the general case of a message from server, found in data, with its own struct. // event "ReceiveFromServer" is the general case of a message from server, found in data, with its own struct.
// other type og event are generated by the worker, about the connection // other type of event are generated by the worker, about the connection
switch(workermsg.event){ switch(workermsg.event){
case 'ReceiveFromServer': case 'ReceiveFromServer':
this.receiveFromServer(JSON.parse(workermsg.data)); this.receiveFromServer(JSON.parse(workermsg.data))
break; break
case 'connected': case 'connected':
this.connected = true; this.connected = true
if(this.config.debug) console.log('received connected event from worker !'); if(this.config.debug) console.log('received connected event from worker !')
this.executewhenConnectedQ(); this.executewhenConnectedQ()
app.events.trigger('MessageBus.Connected'); app.events.trigger('MessageBus.Connected')
break; break
case 'closed': case 'closed':
if(this.config.debug) console.log('received closed event from worker!'); if(this.config.debug) console.log('received closed event from worker!')
this.activeSubscriptions = []; this.activeSubscriptions = []
this.callBacksRegister = { }; this.callBacksRegister = { }
this.whenConnectedQ = []; this.whenConnectedQ = []
this.connected = false; this.connected = false;
app.events.trigger('MessageBus.Closed'); app.events.trigger('MessageBus.Closed');
break; break
default: default:
if(this.config.debug) console.warn('Unknown Websocket Worker message:', workermsg); if(this.config.debug) console.warn('Unknown Websocket Worker message:', workermsg)
} }
} }
} }
@@ -377,35 +379,40 @@ class MessageBus {
receiveFromServer(srvdata) { receiveFromServer(srvdata) {
// See protocol reminder comment at the bottom // See protocol reminder comment at the bottom
if('action' in srvdata){ // Reply to a request if('action' in srvdata){ // Reply to a request
let action = srvdata.action; let action = srvdata.action
let payload = ('payload' in srvdata) ? srvdata.payload : null; let payload = ('payload' in srvdata) ? srvdata.payload : null
// Piggyback on the results of some actions for this module internal use // Piggyback on the results of some actions for this module internal use
switch(action){ switch(action){
case 'WELCOME':
console.log('Received WSS welcome', srvdata)
this.cnxId = srvdata.cnxId
this.serverTimeDelta = Date.now() - srvdata.serverTime
break
case 'SUB': case 'SUB':
if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload); if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload)
break; break
case 'SUBLST': case 'SUBLST':
if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload); if(this.activeSubscriptions.indexOf(payload)<0) this.activeSubscriptions = this.activeSubscriptions.concat(payload)
break; break
} }
app.events.trigger('MessageBus.anyAction', srvdata); app.events.trigger('MessageBus.anyAction', srvdata);
} else { // Low-level event : Redis Event, contrary to requ/reply with wssGateway, or other later } else { // Low-level event : Redis Event, contrary to requ/reply with wssGateway, or other later
if(('event' in srvdata) && (srvdata.event == 'REDISMSG')){ if(('event' in srvdata) && (srvdata.event == 'REDISMSG')){
var payload = ('payload' in srvdata) ? srvdata.payload : null; var payload = ('payload' in srvdata) ? srvdata.payload : null
if(payload && payload.msg && (payload.msg.eventType || payload.msg.action)) { if(payload && payload.msg && (payload.msg.eventType || payload.msg.action)) {
if(payload.msg.eventType){ if(payload.msg.eventType){
var eventType = payload.msg.eventType; var eventType = payload.msg.eventType
app.events.trigger('MessageBus.event.'+eventType, { app.events.trigger('MessageBus.event.'+eventType, {
chan: payload.chan, chan: payload.chan,
sender: payload.msg.sender, sender: payload.msg.sender,
eventType: payload.msg.eventType, eventType: payload.msg.eventType,
payload: payload.msg.payload, payload: payload.msg.payload,
}); })
} else if(payload.msg.action && payload.msg.reqid) { } else if(payload.msg.action && payload.msg.reqid) {
let reqid = payload.msg.reqid; let reqid = payload.msg.reqid
let action = payload.msg.action; let action = payload.msg.action
let actionPayload = ('payload' in payload.msg) ? payload.msg.payload : null; let actionPayload = ('payload' in payload.msg) ? payload.msg.payload : null
let err = ('err' in payload.msg) ? payload.msg.err : null; let err = ('err' in payload.msg) ? payload.msg.err : null
let success = payload.msg.success; let success = payload.msg.success;
if(reqid in this.promisesRegister) { if(reqid in this.promisesRegister) {
clearTimeout(this.promisesRegister[reqid][2]); // Stop timeout timer clearTimeout(this.promisesRegister[reqid][2]); // Stop timeout timer
@@ -416,12 +423,12 @@ class MessageBus {
app.events.trigger('MessageBus.anyMessage', { app.events.trigger('MessageBus.anyMessage', {
chan: payload.chan, chan: payload.chan,
msg : payload.msg, msg : payload.msg,
}); })
} else if(payload && payload.bmsg){ } else if(payload && payload.bmsg){
app.events.trigger('MessageBus.promiscuousMessage', { // Repill msg : decapsulate & use spcific event app.events.trigger('MessageBus.promiscuousMessage', { // Repill msg : decapsulate & use spcific event
chan: payload.bmsg.chan, chan: payload.bmsg.chan,
msg : payload.bmsg.msg, msg : payload.bmsg.msg,
}); })
} }
else { else {
console.warn('Weird bus message (discarted) :', srvdata) console.warn('Weird bus message (discarted) :', srvdata)
@@ -431,9 +438,9 @@ class MessageBus {
// For request-reply, settle promise // For request-reply, settle promise
if(srvdata.reqid && (srvdata.reqid in this.promisesRegister)) { if(srvdata.reqid && (srvdata.reqid in this.promisesRegister)) {
let payload = ('payload' in srvdata) ? srvdata.payload : null; let payload = ('payload' in srvdata) ? srvdata.payload : null
let err = ('err' in srvdata) ? srvdata.err : null; let err = ('err' in srvdata) ? srvdata.err : null
let success = srvdata.success; let success = srvdata.success
clearTimeout(this.promisesRegister[srvdata.reqid][2]); // Stop timeout timer clearTimeout(this.promisesRegister[srvdata.reqid][2]); // Stop timeout timer
if(success) this.promisesRegister[srvdata.reqid][0](payload); // resolve if(success) this.promisesRegister[srvdata.reqid][0](payload); // resolve
else this.promisesRegister[srvdata.reqid][1](`MsgBus action failed.\nError: ${err}`); // Fail else this.promisesRegister[srvdata.reqid][1](`MsgBus action failed.\nError: ${err}`); // Fail
+2
View File
@@ -103,3 +103,5 @@ Maybe in the second browser he's viewing another angle, or meybe he's not viewin
Back to business. Back to business.
I slightly changed the listSims query, so we can display sim name, primordial frame name and owner name. I slightly changed the listSims query, so we can display sim name, primordial frame name and owner name.
Now that this mecanism is in place in the gateway, Let's change Observer so that it uses a combination of sender and cnxId as key for its frustum register, instead of just sender