Skip to content

Web Socket Protocol Guide

This guide explains the built-in wire protocol used by a-socket in Vona within the Cabloy monorepo.

Read this together with:

Why this protocol guide exists

The architecture guide explains how the Web Socket runtime is structured, and the usage guide explains how backend modules should use it.

This page answers a narrower question:

  • what bytes and payload shapes travel over the socket when the built-in protocol is used?

That matters when you are:

  • writing a browser or non-browser client against the current server contract
  • debugging why one side cannot parse an event
  • understanding the sysReady, sysPerformAction, and sysPerformActionBack system events
  • tracing how ws.sendEvent(...) encodes outbound data and how packet decoding restores it on inbound traffic

Source-truth files

The protocol contract is grounded in these files:

  • vona/src/suite-vendor/a-cabloy/modules/a-socket/src/types/socketEvent.ts
  • vona/src/suite-vendor/a-cabloy/modules/a-socket/src/config/config.ts
  • vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/socketConnection.event.ts
  • vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/socketPacket.event.ts
  • vona/src/suite-vendor/a-cabloy/modules/a-socket/src/bean/socketPacket.performAction.ts

If this guide and source ever disagree, prefer the current source.

The event transport model

The built-in protocol wraps event messages as:

  • a string prefix
  • followed by JSON

The default prefix is:

  • '_:'

The JSON body is serialized as a two-element array:

  • [eventNameOrShortCode, data]

So the wire model is:

text
_:[eventNameOrShortCode, data]

More precisely, the server sends:

typescript
`${eventPrefix}${JSON.stringify([eventNameInner, data])}`;

That means the Web Socket protocol is event-oriented rather than frame-schema-oriented.

Prefix behavior

The event prefix comes from module config:

typescript
return {
  eventPrefix: '_:',
};

A practical interpretation is:

  • if an inbound string begins with '_:', a-socket treats it as an event packet
  • if it does not begin with '_:', the built-in event decoder forwards it as ordinary raw data

This is why the prefix is the first protocol boundary.

Outbound event encoding

The outbound helper is attached during the connection onion event stage through:

  • ws.sendEvent(...)

Its behavior is:

  1. take eventName and data
  2. map the event name to a short code if one exists in socketEventRecord
  3. serialize [mappedName, data] with JSON.stringify(...)
  4. prepend the eventPrefix
  5. send the final string with ws.send(...)

Representative shape:

typescript
const eventNameInner = socketEventRecord[eventName] ?? eventName;
ws.send(`${this.scope.config.eventPrefix}${JSON.stringify([eventNameInner, data])}`);

This means custom namespace events usually travel with their original event names, while built-in system events may travel with compact short codes.

Inbound event decoding

Inbound decoding happens in socketPacket.event.ts.

Its behavior is:

  1. check whether the raw payload is a string
  2. check whether the string starts with the configured event prefix
  3. remove the prefix
  4. parse the remaining JSON
  5. reverse-map built-in short codes through socketEventRecordReverse
  6. forward a normalized packet [eventName, data]

If the raw payload is not an event-formatted string, the decoder forwards:

  • [undefined, rawData]

A practical interpretation is:

  • event packets become normalized (eventName, data) packets
  • non-event packets remain possible, but they do not participate in the built-in event-name protocol unless another packet handler interprets them

Built-in system event names and short codes

The built-in system event mapping is:

  • sysReady -> _a
  • sysPerformAction -> _b
  • sysPerformActionBack -> _c

Reverse mapping is:

  • _a -> sysReady
  • _b -> sysPerformAction
  • _c -> sysPerformActionBack

A useful rule is:

  • use the human-readable names when discussing the protocol logically
  • expect the short forms on the wire for those built-in events

Protocol frame examples

Example 1: server ready signal

Logical event:

text
sysReady

Wire packet:

text
_: ["_a",null]

Because sysReady has payload type never, the built-in sender still serializes the packet as a two-slot array, so the second slot is null in the JSON wire form shown here.

The important contract point is:

  • the first slot is _a
  • the packet means the server-side connection setup completed

Example 2: custom namespace event

Suppose a namespace event name is:

  • messageCreated

and the payload is:

json
{ "roomId": "r1", "messageId": "m8", "text": "hello" }

Then a representative wire packet is:

text
_:["messageCreated",{"roomId":"r1","messageId":"m8","text":"hello"}]

Because this is not one of the built-in short-coded system events, the event name stays as messageCreated.

Example 3: perform-action request

Logical event name:

  • sysPerformAction

Representative logical payload:

json
{
  "i": 1,
  "m": "get",
  "p": "/training/student/findOne",
  "q": { "id": 3 },
  "h": { "x-demo": "1" }
}

Representative wire packet:

text
_:["_b",{"i":1,"m":"get","p":"/training/student/findOne","q":{"id":3},"h":{"x-demo":"1"}}]

sysReady

sysReady is the built-in server-ready handshake event.

It is sent by the server after:

  • the socket was accepted
  • the connection onion enter chain completed successfully
  • handlers for close, message, and error were installed

A client can interpret sysReady as:

  • the connection is alive at the protocol level
  • the backend-side startup for this socket completed
  • request-like packet traffic can begin

sysPerformAction

sysPerformAction is the built-in request-like event for invoking backend actions over Web Socket.

Its inner payload shape is:

typescript
interface ISocketEventPerformActionOptionsInner {
  i: number;
  m: 'get' | 'post' | 'delete' | 'put' | 'patch';
  q?: object;
  p: string;
  b?: any;
  h?: object;
}

Field meanings:

  • i -> request id chosen by the client
  • m -> method
  • p -> backend path
  • q -> query object
  • b -> body
  • h -> headers object

A practical interpretation is:

  • this is a compact RPC-like request envelope
  • it reuses the backend action execution path rather than inventing a second business-dispatch model

sysPerformActionBack

sysPerformActionBack is the reply event for sysPerformAction.

On success, the built-in server handler sends:

json
{ "i": 1, "c": 0, "d": <result> }

On failure, it sends:

json
{ "i": 1, "c": <errorCode>, "m": "error message" }

Field meanings in practice:

  • i -> matches the original request id
  • c -> status code, with 0 meaning success
  • d -> result data on success
  • m -> error message on failure

A client should therefore match replies to requests by i.

Request-response pairing model

The built-in protocol does not create a separate reply channel name per action.

Instead, the pairing model is:

  1. client sends one sysPerformAction packet with request id i
  2. server executes the action
  3. server sends one sysPerformActionBack packet with the same i
  4. client resolves or rejects the in-flight request by matching that id

That means the request id is the essential correlation field.

Custom event naming model

For application-defined namespace events, a practical rule is:

  • choose stable event names such as messageCreated, typing, reload, or orderCreated
  • keep the namespace path for channel identity
  • keep the event name for action identity
  • keep the payload object for data

This keeps the protocol understandable on both sides.

A useful split is:

  • namespace path -> which channel
  • event name -> which signal
  • payload -> which data

JSON compatibility considerations

Because the built-in protocol uses JSON.stringify(...), payloads should stay JSON-friendly.

Prefer payloads such as:

  • objects
  • arrays
  • strings
  • numbers
  • booleans
  • null

Be careful with values such as:

  • functions
  • class instances that rely on prototype behavior
  • circular objects
  • non-JSON-native runtime values

A practical rule is:

  • if you would not confidently send it through ordinary JSON serialization, do not assume it is a good socket event payload either

Relationship to non-event packets

The built-in decoder only interprets the event protocol when:

  • the inbound data is a string
  • and the string starts with the configured event prefix

Otherwise the packet is forwarded as raw data through [undefined, rawData].

This means:

  • the built-in event protocol is the normal and preferred path
  • custom packet behavior can still exist through additional socketPacket handlers
  • but if you want interoperability with the built-in helpers, stay inside the prefixed JSON event format

Practical client checklist

When implementing a client against the current server contract, ask:

  1. does the connection URL use the correct /ws/... namespace path?
  2. does the client wait for sysReady before assuming the socket is ready?
  3. are outbound event packets prefixed with '_:'?
  4. are built-in system event short codes handled correctly?
  5. if using sysPerformAction, does each request carry a unique i and does the reply matcher use the same field?

If those answers are correct, the client-side protocol integration will usually align with the current a-socket implementation.

Read this page together with:

Use the practical split:

Released under the MIT License.