Web Socket Call Flow
This guide is a source-oriented execution trace for the built-in a-socket module.
Read this together with:
Use the practical split:
- Web Socket Guide for architecture
- Web Socket Usage Guide for server-side authoring patterns
- Web Socket Protocol Guide for the client-visible wire format
- this page for source tracing and debugging
Why this call-flow view matters
The main Web Socket guide explains the architecture and extension model.
This page answers a different question:
- what methods actually run, and in what order, when a socket starts, receives a packet, sends a message, or closes?
That matters when you are:
- debugging connection lifecycle behavior
- deciding whether an extension belongs in
socketNamespace,socketConnection, orsocketPacket - tracing authentication, origin checks, instance initialization, or packet execution from source
- verifying how local delivery and cross-worker broadcast fit together
Key source files
Use these files as the primary trace surface:
vona/src/suite-vendor/a-cabloy/modules/a-socket/src/monkey.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/service/socket.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/service/socketEvent.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/bean.socket.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/socketConnection.*.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/socketPacket.*.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/types/socketEvent.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/lib/const.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/hmr.socketConnection.tsvona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/hmr.socketPacket.ts
Startup call flow
At backend startup, the call path is:
Monkey.appReady()ServiceSocket.appReady()- create
WebSocketServer - register the
connectionlistener
Representative shape:
async appReady() {
if (!this.app.server) return;
this.app.wss = new WebSocketServer({ server: this.app.server });
this.app.wss.on('connection', (ws, req) => {
this._onConnection(ws, req);
});
}A practical interpretation is:
- monkey lifecycle integration connects
a-socketto ordinary backend startup a-socketdoes not create a separate standalone server- the Web Socket server is attached to the existing backend HTTP server
Connection setup call flow
When a client connects, the main path is:
WebSocketServeremitsconnectionServiceSocket._onConnection(ws, req)starts- reject immediately if the app is already closing
- parse the request URL
- read query values for instance, locale, and timezone
- create a request-scoped backend context through
app.bean.executor.newCtx(...) - define
ctx.ws - derive
ws.namespace - assign
ws.id - add the client to
bean.socket - execute the connection onion chain with
{ method: 'enter', ws } - install
onclose,onmessage, andonerror - send
sysReady
Connection setup as a source trace
A compact trace looks like this:
Monkey.appReady()
-> ServiceSocket.appReady()
-> new WebSocketServer({ server: app.server })
-> wss.on('connection', (ws, req) => _onConnection(ws, req))
connection
-> ServiceSocket._onConnection(ws, req)
-> URL.parse(req.url, ...)
-> app.bean.executor.newCtx(...)
-> define ctx.ws getter
-> ws.namespace = getNamespace()
-> ws.id = uuidv4()
-> bean.socket.addClient(ws)
-> _getComposeSocketConnections(ws.namespace)({ method: 'enter', ws })
-> install ws.onclose / ws.onmessage / ws.onerror
-> ws.sendEvent('sysReady')This is the first source path to read when a connection is accepted but later behavior is not what you expect.
Namespace resolution call flow
Namespace identity is derived inside ServiceSocket.getNamespace().
The flow is:
- read
this.ctx.path - remove the configured global prefix
- if the remaining path is empty, use
/ - cast the result to a namespace key
A useful interpretation is:
- transport path and logical namespace are intentionally coupled
- namespace selection happens before packet handling
- namespace-specific socket behavior is therefore determined at connection time
Representative mapping:
/ws->//ws/ssrhmr->/ssrhmr
Connection onion composition flow
The connection chain is not hardcoded as a manual list in service/socket.ts.
Instead, the flow is:
ServiceSocket._getComposeSocketConnections(namespace)- fetch the namespace cache from
getCacheSocketConnections(app) - if the namespace is not cached:
- ask
this.bean.onion.socketConnection.getOnionsEnabledWrapped(...)for enabled onion slices - wrap each slice through
_wrapOnionConnection(...) - compose the wrapped handlers through
compose(...)
- ask
- execute the composed chain
Representative shape:
const connections = this.bean.onion.socketConnection.getOnionsEnabledWrapped(item => {
return this._wrapOnionConnection(item);
});
cacheSocketConnections[namespace] = compose(connections);This matters because the runtime chain is framework-driven:
- enabled onion metadata determines what participates
- wrappers resolve the real bean instance from the container
- the composed result is cached per namespace
Built-in connection onion execution order
The default built-in connection path is:
alive -> app -> instance -> cors -> event -> passport -> readyThe runtime enters each bean through:
beanInstance.enter(ws, options, next)on connection setupbeanInstance.exit(ws, options, next)on connection teardown
A practical responsibility map is:
alive-> heartbeat and stale-socket detectionapp-> app-ready and closing checksinstance-> instance initializationcors-> origin validationevent-> attachws.sendEvent(...)passport-> token or anonymous sign-inready-> terminal ordered stage
Packet composition flow
Inbound messages follow the same overall pattern as connection onions.
The runtime path is:
ws.onmessageServiceSocket._getComposeSocketPackets(ws.namespace)- fetch the namespace cache from
getCacheSocketPackets(app) - if missing, load enabled packet onions, wrap them, and compose them
- execute the composed chain with
{ data: event.data, ws }
The packet wrapper calls:
beanInstance.execute(data.data, data.ws, options, _patchPacketNext(...))
That patching step lets packet handlers pass a transformed packet onward while the composed chain keeps the socket attached.
Built-in packet execution order
The default built-in packet path is:
event -> performActionStep 1: event decoding
SocketPacketEvent.execute(...) checks whether the payload is an event-formatted string.
If so, it:
- strips the configured prefix
'_:' - parses JSON
- reverse-maps short codes such as
_a,_b,_c - forwards
[eventName, data]
Otherwise it forwards:
[undefined, rawData]
This is the source transition from raw transport data into normalized packet data.
Step 2: perform-action handling
SocketPacketPerformAction.execute(...) checks whether the normalized event name is:
sysPerformAction
If not, it calls next().
If yes, it:
- extracts compact request fields from the packet
- calls
this.$scope.executor.service.executor.performActionInner(...) - sends
sysPerformActionBackon success or failure
A compact trace looks like this:
ws.onmessage
-> composed socketPacket chain
-> SocketPacketEvent.execute(raw)
-> normalized packet [eventName, data]
-> SocketPacketPerformAction.execute(packet)
-> performActionInner(method, path, { query, body, headers })
-> ws.sendEvent('sysPerformActionBack', resultOrError)Outbound send call flow
The simplest server-to-client path is direct send by client id.
The runtime path is:
- some backend code calls
scope.socket.service.socketEvent.send(...) ServiceSocketEvent.send(...)callssendWorker(...)locallysendWorker(...)looks up the socket inbean.socket.clientsws.sendEvent(...)serializes and sends the packetServiceSocketEvent.send(...)also emits thesendbroadcast bean for cross-worker propagation
A compact trace looks like this:
caller
-> ServiceSocketEvent.send(id, eventName, data, options)
-> sendWorker(id, ...)
-> bean.socket.clients[id]?.sendEvent(...)
-> ws.send('_:' + JSON.stringify([eventNameInner, data]))
-> scope.broadcast.send.emit(...)This is why even a targeted send is not only a local socket write. It also has a distributed propagation path.
Outbound broadcast call flow
Namespace broadcast follows the same two-level model.
The runtime path is:
- some backend code calls
broadcast(namespace, eventName, data, options) ServiceSocketEvent.broadcast(...)callsbroadcastWorker(...)locallybroadcastWorker(...)reads client ids frombean.socket.clientsNamespace[namespace]- each local client sends through
ws.sendEvent(...) ServiceSocketEvent.broadcast(...)emits thebroadcastbean for other workers
A compact trace looks like this:
caller
-> ServiceSocketEvent.broadcast(namespace, eventName, data, options)
-> broadcastWorker(namespace, ...)
-> ids = bean.socket.clientsNamespace[namespace]
-> for each id: bean.socket.clients[id]?.sendEvent(...)
-> scope.broadcast.broadcast.emit(...)A practical interpretation is:
- local clients receive the event immediately
- other workers replay the same delivery logic for their local namespace members
a-socketreuses the distributed broadcast layer instead of inventing a separate cross-worker socket bus
Namespace-bean call flow
Most application code should not call low-level socket registries directly.
The usual path is a namespace bean extending BeanSocketNamespaceBase.
That path is:
- define a namespace bean with
@SocketNamespace(...) - call
send(...)orbroadcast(...)on that bean BeanSocketNamespaceBaseforwards toServiceSocketEventServiceSocketEventperforms local delivery and broadcast propagation
Representative trace:
BeanSsrHmr.reload()
-> this.scope.socketNamespace.ssrHmr.broadcast('reload')
-> BeanSocketNamespaceBase.broadcast(...)
-> this.$scope.socket.service.socketEvent.broadcast(namespace, ...)
-> local namespace delivery
-> broadcast fan-out to other workersThis is the normal extension path for feature modules.
Disconnect call flow
When a connection closes, the runtime path is:
ws.onclose- run the connection onion chain again with
{ method: 'exit', ws } - remove the client from the registry in
bean.socket.removeClient(ws) - resolve the connection promise
Representative trace:
ws.onclose
-> _getComposeSocketConnections(ws.namespace)({ method: 'exit', ws })
-> bean.socket.removeClient(ws)
-> resolve()This matters because cleanup is not only a transport event. It also reuses the onion lifecycle so custom connection beans can attach exit-time behavior.
Shutdown call flow
At backend shutdown, the path is:
Monkey.appClose()ServiceSocket.appClose()app.wss.close()bean.socket.close()- terminate all tracked clients
A compact trace looks like this:
Monkey.appClose()
-> ServiceSocket.appClose()
-> app.wss.close()
-> bean.socket.close()
-> for each client: terminate()This keeps runtime shutdown and socket cleanup aligned.
HMR and cache invalidation flow
a-socket caches composed connection and packet chains in app.meta through:
SymbolCacheSocketConnectionsSymbolCacheSocketPackets
The related flow is:
- first connection or packet execution composes and caches the chain
- later traffic reuses the cached composed function
- HMR reload triggers
HmrSocketConnection.reload(...)orHmrSocketPacket.reload(...) - those HMR beans clear the corresponding cache from
app.meta - the next execution rebuilds the composed chain from current metadata
A compact trace looks like this:
first use
-> _getComposeSocketConnections(namespace)
-> getCacheSocketConnections(app)
-> compose(...) and cache
HMR reload
-> HmrSocketConnection.reload(...)
-> clearAllCacheSocketConnections(app)
next use
-> rebuild composed chainThis is the important source path when onion changes appear stale during development.
When to trace which path
Use these call paths depending on the problem you are investigating:
- startup problem ->
monkey.tsandServiceSocket.appReady() - connection rejected too early ->
_onConnection(...)plusapp,instance,cors, andpassport - client never receives ready ->
eventconnection onion plussysReady - inbound action fails ->
socketPacket.event.ts,socketPacket.performAction.ts, andperformActionInner(...) - targeted delivery fails ->
ServiceSocketEvent.send(...)andbean.socket.clients - namespace broadcast fails ->
ServiceSocketEvent.broadcast(...)andclientsNamespace - development reload seems stale ->
lib/const.tsand the HMR beans
Related guides
If you need the broader context next, read:
- Web Socket Guide for the architecture and extension model
- Web Socket Usage Guide for server-side authoring patterns
- Web Socket Protocol Guide for the client-visible wire format
- Broadcast Guide and Worker Guide for cross-worker delivery context