Help us shape the future of Go! Take the 2026 Go Developer Survey

message

package
v1.3.104 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

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

View Source
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.

View Source
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

func ReadFrame

func ReadFrame(r io.Reader) ([]byte, error)

ReadFrame reads exactly one length-prefixed frame. Rejects a zero-length or oversize frame — the latter usually means the peer is running the pre-framing, unbounded protocol.

func WriteFrame

func WriteFrame(w io.Writer, payload []byte) error

WriteFrame writes payload as one length-prefixed frame: a 4-byte big-endian length followed by exactly that many bytes. Rejects an empty or oversize payload.

Types

type Conn

type Conn struct {
	net.Conn
	// contains filtered or unexported fields
}

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 NewConn

func NewConn(c net.Conn) *Conn

NewConn wraps c with framing.

func (*Conn) ReadFrame

func (c *Conn) ReadFrame() ([]byte, error)

ReadFrame reads one length-prefixed frame from the underlying conn.

func (*Conn) WriteFrame

func (c *Conn) WriteFrame(payload []byte) error

WriteFrame writes a length-prefixed frame, serialized against other writers.

func (*Conn) WriteFrameDeadline

func (c *Conn) WriteFrameDeadline(payload []byte, timeout time.Duration) error

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

func ParseEnvelope(payload []byte) (env Envelope, ok bool)

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).

func (Envelope) Marshal

func (e Envelope) Marshal() ([]byte, error)

Marshal encodes the envelope as its JSON wire bytes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL