Documentation
¶
Overview ¶
Package message pkg/skychat/message/message.go c4-app-chat format: length-prefixed frames and the chat-msg/chat-ack envelope.
Before this package the same framing (a 4-byte big-endian length followed by that many payload bytes, capped at 64 KiB) was implemented three times — the native app (cmd/apps/skychat framedConn), the browser-tab wasm visor (cmd/wasm-visor chatConn), and the group relay (group/session.go) — each with its own constant and write mutex, free to drift. They are now one codec.
The package is pure Go (only io/net/encoding/sync/time), so it compiles under GOOS=js GOARCH=wasm as well as native — the wasm visor and the native app share it.
Wire history: pre-2026-05-12 the protocol was "one Write = one Read" (a raw byte slice per message). That held on dmsg (noise-framed) but broke on skynet routes, where a VStream can split one Write across several Reads at arbitrary boundaries, so a long message arrived split into two chat entries. The length-prefixed frame fixed that; old unframed binaries can't talk to framed ones. The chat-msg/chat-ack envelope (opt-in, JSON) rides inside a frame; a default send writes the plain-text body with no envelope, keeping the wire byte-identical for ordinary chat.
Index ¶
Constants ¶
const ( TypeMsg = "chat-msg" TypeAck = "chat-ack" TypeRead = "chat-read" // TypeDelete is a delete-for-everyone command: {"type":"chat-delete","id":"<hex>"}. // The sender asks the recipient to drop (tombstone) the message with that id // — the id the sender minted for it (chat-msg ID). It carries no body and is // consumed like a receipt, never surfaced as a chat line; the recipient's app // replaces the message with a "deleted" placeholder. additive + backward- // compatible (a peer predating it ignores the unknown type). TypeDelete = "chat-delete" )
Envelope type values.
const MaxFrameSize = 64 * 1024
MaxFrameSize bounds a single framed payload. A larger frame on read is rejected as an out-of-sync or hostile peer (or an old, unframed binary).
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Conn ¶
Conn wraps a net.Conn with length-prefixed framing and a write mutex. The write mutex is required because two callers can race to write to the same underlying conn (e.g. an outbound message and the ack reply for an inbound one); interleaving one frame's length prefix with another's payload would desync the receiver permanently. The read path is expected to have a single owner, so there is no read mutex.
func (*Conn) WriteFrame ¶
WriteFrame writes a length-prefixed frame, serialized against other writers.
func (*Conn) WriteFrameDeadline ¶
WriteFrameDeadline is WriteFrame bounded by a per-call write deadline, so a stalled peer (transport half-dead, peer not draining) can't pin the caller's goroutine indefinitely while holding the write mutex. timeout <= 0 falls back to an unbounded WriteFrame. The deadline is acquired AFTER the write mutex so its scope is the on-wire write rather than time spent queued behind another writer, and reset to "no deadline" (time.Time{}) on the way out so a later call isn't pre-expired.
type Envelope ¶
type Envelope struct {
Type string `json:"type"`
ID string `json:"id"`
Body string `json:"body,omitempty"`
Ack bool `json:"ack,omitempty"` // chat-msg only: request ack-on-receipt
// ReplyTo, when set on a chat-msg, is the ID of the message this one quotes
// (a threaded reply). omitempty keeps the wire byte-identical for non-reply
// messages, so a peer that predates this field simply ignores it — the field
// is additive and backward-compatible. Both the native app and the wasm-visor
// set/read it, so quoted replies now propagate to the RECIPIENT (previously
// reply-threading was sender-local only).
ReplyTo string `json:"reply_to,omitempty"`
}
Envelope is the JSON payload shape for the opt-in peer-receipt protocol:
chat-msg: {"type":"chat-msg","id":"<hex>","body":"...","ack":true}
chat-ack: {"type":"chat-ack","id":"<hex>"}
chat-read: {"type":"chat-read","id":"<hex>"}
A default (no --wait) send writes the plain-text body with no envelope, so the wire stays byte-identical for ordinary chat; the envelope is opt-in at the sender.
The three types drive the message-status lifecycle. chat-msg with ack=true asks the recipient's app to send a chat-ack back on receipt (→ "received" status at the sender). chat-read is sent later, when the recipient's UI actually displays the message (→ "read" status). chat-ack/chat-read carry only the id of the chat-msg they refer to.
func ParseEnvelope ¶
ParseEnvelope conservatively recognizes a chat-msg/chat-ack envelope in a frame payload: after trimming surrounding whitespace the payload must begin with '{', be valid JSON, and carry a known chat-* type. Anything else returns ok=false so a plain-text message that happens to look like JSON still reaches its peer as text (only known types are consumed as envelopes).