Message Model¶
Foundation Topic Table Of Contents · Spec Table Of Contents · Previous: 04. Interaction Model · Next: 06. Framework API
Defines the payload codec,
MessageContext, global object reference JSON, and application metadata and ownership/size limits of the typed messages an Application sends and receives.
The framework envelope and its internal multipart encoding use the same wire schema and golden fixtures across every language, and each language's service runtime processes it on top of the raw transport. This format is never exposed to the application.
1. Typed Messages¶
The application sends a message keyed by payload type and a registered handler. The framework encodes the payload using the default typed JSON serializer and decodes it into the receiving handler's argument type. The caller never registers a codec, serializer registry, or encoder/decoder per message type.
A package that needs a separate wire format can use a framework-defined codec extension. An extension is a host-level policy — it isn't passed repeatedly into a business handler or individual send/request call. An API that handles raw bytes directly is used only for transport inspection and codec extension implementation.
2. Message Kinds and Completion¶
The public interface that starts each interaction, and how its target is selected, is owned by the Interaction Model. This section defines only the message kind and its completion condition.
| Kind | Meaning | Completion |
|---|---|---|
| Send | A one-way message delivered once to a target handler | Completes with no return data once the source-local queue accepts it; doesn't wait for the remote handler to complete |
| Request | A message where the target handler returns a reply or an error | Completes exactly once, with a reply, an error, a timeout, or a cancellation |
| Logical Multicast | A message published to matching Spots at each MeshNode of the target ChannelName | Completes with no return data once source-local capacity is secured and publish begins; doesn't tally per-target counts for monitoring |
| Classic Fanout publish | A message published to the subscribers of an independent fanout channel | Completes with no return data once the local publisher queue accepts it; doesn't confirm subscriber receipt |
| STREAM send/request | A one-way message or a reply-requiring message sent to a connected session | Follows the session sequence and lifecycle contract |
A request's reply correlation is owned by an operation ID issued by transport, or by the session sequence. Neither the packet name nor application metadata is used as the reply matching key. A reply completes with exactly one of a success payload or a framework error, and the same request can never complete twice.
Object Creation Requests¶
An object creation request is a manager-operation input distinct from a regular send/request.
The framework encodes it with the typed codec into at most a 1 MiB payload and records its immutable content reference and hash into a durable creation intent before the placement reservation.
A factory receives the logical key, ObjectGeneration, and creation attempt together, and must converge to the same result even under at-least-once execution of the same attempt.
A logical instance with an address and state that stays reachable at the same ID even when the executing node changes is called a Spot. The state after that Spot's creation and initialization finish, when it can receive application messages, is called Ready.
A CAS loser never sends the creation request as a regular message. The content reference is kept until Ready commit or fenced failure cleanup finishes.
ObjectGeneration, AuthorityOwnerGeneration (the provider-issued value indicating the order in which the authority owner changed within the same object incarnation), attempt, and the owner lease token are used only for Store fencing and never included in the application message payload or handler context.
3. MessageContext¶
A regular send/request and an Actor handler receive a common MessageContext. This
context provides the current message's nullable
MeshName — a name identifying one
RouteMesh physical connection group — nullable
ChannelName — a name identifying the Channel scope
a message is sent to — PacketName, ContentType, immutable Metadata, and a nullable
CorrelationId stored as UTF-8 bytes verbatim. CorrelationId is null on a send and non-null on a request.
MeshName is non-null on RouteMesh and Spot/Actor dispatch, and null on ClientServer/STREAM.
Connection cancellation isn't owned by a universal context — it's owned by each language's handler argument or a Session-specific context.
Node direct — sending by designating a MeshName and
target RID together — provides RouteMessageContext, Logical Multicast provides
PublishMessageContext, and STREAM dispatch provides SessionMessageContext, each
carrying that path's additional information.
There's no separate marker context per Send/Request/SpotActor. An Actor request's context carries no reply metadata/compression option, and there's no separate reply call either.
A handler filter receives a filter-specific context that carries both
the current MessageContext info and the public dispatch kind. This value only
distinguishes Node direct send/request, Channel send/request, and classic fanout. It
doesn't expose socket kind, endpoint, framework-internal owner classification, or a
dispatch descriptor. Dispatch kind isn't added to the Spot/Actor/Logical
Multicast/STREAM contexts, where a filter doesn't apply.
The Object lifecycle Context and the current message's MessageContext are separate contracts.
4. Global Object Reference JSON¶
ActorRef's and SpotRef's typed JSON contract uses the same property names and
JSON types across every language. Every property is required, and property names
are case-sensitive. A duplicate property, null, an unknown property, and an
out-of-range generation are all rejected. Deserialization doesn't normalize the ID
or route string.
{
"actorId": "player-42",
"objectGeneration": "17",
"meshName": "game",
"nodeRid": "game-node-0123456789abcdef0123456789abcdef"
}
{
"spotId": "room-42",
"objectGeneration": "9",
"meshName": "game",
"nodeRid": "game-node-0123456789abcdef0123456789abcdef"
}
actorId and spotId are global logical IDs, and meshName/nodeRid are the
location snapshot at the time of lookup. objectGeneration is a decimal string
with no leading zero, from "1" to "9223372036854775807". Numeric tokens, a
sign, a decimal point, and an exponent are not allowed.
5. The framework-json-v1 Typed Payload Profile¶
The framework's default typed application payload uses the framework-json-v1
profile in every language. This profile is the public codec contract that lets an
application payload cross a language boundary and decode to the same value.
- A UTF-8 BOM is not allowed.
- Property names and enum names are case-sensitive.
- Property order and insignificant whitespace carry no meaning.
- Duplicate properties and missing required properties are rejected.
- Unknown properties are ignored.
nullis allowed only for values the contract declares nullable.- A signed or unsigned 64-bit integer is range-checked and represented as a decimal string with no leading zero.
- An integer of 32 bits or fewer is represented as a JSON number with no fraction.
- A floating-point value must be a finite JSON number.
- A byte sequence is RFC 4648 base64 with padding.
- Date, decimal, UUID, and language-specific custom types are not converted implicitly; they use the string or DTO specified by the contract.
Where a specific framework DTO defines a stricter property set, that DTO contract takes precedence. For example, the global object reference in §4 rejects unknown properties.
Changing property or enum names, unknown/required-property handling, nullability, or numeric and byte representation is a breaking contract change because it changes cross-language decoding of an existing payload. The profile does not apply to opaque state bytes returned by an Actor or Spot relocation adapter.
6. Application Metadata¶
Application metadata is a small key-value snapshot carried separately from the business payload. Node direct, ChannelName, Spot direct (the delivery of a send or request to a Spot by designating one global Spot ID), Actor, and STREAM send/request all use the same contract.
| Item | Contract |
|---|---|
| Key and value | UTF-8, and never contain NUL |
| Total size | At most 1024 bytes, including the encoded key and value and structural overhead |
| Same key | The value most recently set on the outbound builder applies |
| Receiving | The handler context provides an unmodifiable snapshot |
| Lifetime | Valid until the handler turn ends; the application copies a value if it needs to keep it |
| Malformed input | Treated as a protocol error, without calling the handler |
| Reply | Request metadata isn't auto-copied, and a regular reply provides no metadata setter |
Metadata's internal frame layout and encoding aren't part of the public contract. The framework maintains the boundary between payload and metadata, and never makes the application assemble or parse a frame, even on a path that needs relaying.
7. Delivery Rules¶
| Path | Metadata delivery |
|---|---|
| Node direct and ChannelName | The source snapshot is delivered to the handler context of the selected MeshNode |
| Spot | Delivered to the application claim at the global Spot ID's current Ready owner |
| Logical Multicast | The same publish snapshot is delivered to each matching Spot handler |
| Actor | Delivered to the Actor handler context, without passing through a Spot callback |
| STREAM session — the server-side execution unit kept alive from accepting one STREAM connection until it closes | Delivered to the session send/request context |
| Relay from a bound session to an Actor | Only keys allowed by the root metadata policy's session-to-actor allowlist are delivered |
| Relay from an Actor to a bound session | Only keys allowed by the root metadata policy's actor-to-session allowlist are delivered |
When the framework creates a new request, it doesn't auto-copy the original metadata. It's included in the new outbound snapshot only if the caller explicitly passed the current handler's metadata. Trace information that needs auto-propagation is managed as a separate framework field by Message Flow Correlation.
8. Ownership and Size Limits¶
The caller owns the outbound builder and payload until the submit call returns. Once the framework accepts the submit, it keeps the needed payload/metadata reference or a copy for the operation's lifetime. The caller is never made to manage the lifetime of a transport buffer, native message pointer, or multipart part.
The message context, metadata, and payload view passed to a handler are read-only during the callback. The application doesn't dispose of them, and it copies whatever value it needs to keep after the callback ends. The framework cleans up the lifecycle of received payload storage, reply correlation, and the route envelope together with callback completion.
The framework deserializes a typed payload at most once for each accepted message. The message retains the value or failure produced by the first typed access, and another access with either the same or a different type does not invoke the codec again. If the retained value cannot be used as the newly requested type, the access ends with the language's type mismatch result. If the first access failed, that failure is delivered again. Obtaining a read-only raw view or explicitly asking for a byte copy does not create this typed outcome.
The same ownership rule applies while an object creation is pending. So that Location Store — the storage that holds each Spot's current owner and lifecycle state so multiple nodes can check it together — I/O and the factory don't depend on the caller's payload object or native buffer lifetime, the framework's service runtime pins the immutable encoded payload in the content store. After Ready or fenced failure, it releases, once, the payload storage owned by that attempt.
The payload's max size (by encoded size) follows the target transport's
MaxMessageSize, the byte cap on the complete
transport message a listener can receive. This value is defined per transport by
the Framework API. If the whole message exceeds this
limit, no part of it is delivered, and the entire submit or receive fails.
Per-target submission and result aggregation for Logical Multicast are defined by
Spot Messaging.
9. Verification Requirements¶
Using only MessageContext, ActorRef/SpotRef JSON, the application metadata
builder/context, and the completion values of submit/receive calls, the following
are verified. Each item maps to one test.
Message Shape
ActorRef/SpotRefJSON requiresactorId/spotId,objectGeneration,meshName, andnodeRidall together, and JSON carrying a duplicate property,null, an unknown property, or an out-of-rangeobjectGenerationis rejected.- A payload encoded with
framework-json-v1rejects a UTF-8 BOM, a duplicate property, and a missing required property, and decodes while ignoring an unknown property. - The same request never completes twice — the reply arrives exactly once, as either a success payload or a framework error.
Metadata Limits
- If the application metadata's total size, including the encoded key/value and structural overhead, exceeds 1024 bytes, it's treated as a protocol error without calling the handler.
- Setting the same key multiple times means only the value most recently set on the outbound builder arrives at the handler context.
- The metadata snapshot the handler receives is unmodifiable, and after the handler turn ends the value can't be read again unless it was copied.
- A reply doesn't auto-copy the original request metadata — only a value the caller explicitly passed appears in the new outbound snapshot.
Ownership
- The caller owns the outbound payload until the submit call returns, and after it returns the caller doesn't need to manage the buffer's lifetime.
- The message context, metadata, and payload view passed to the handler can't be kept after the callback ends — they must be copied during the callback if needed.
- Accessing the same message with typed access multiple times, whether with the same type or a different type, returns the same value (or the same failure) every time — the codec never runs again after the first access.
Size Limits
- If the whole encoded message exceeds the target transport's
MaxMessageSize, the entire submit or receive fails — not just part of it fails to be delivered.
Foundation Topic Table Of Contents · Spec Table Of Contents · Previous: 04. Interaction Model · Next: 06. Framework API