Documentation
¶
Overview ¶
Package dm pkg/skychat/dm/controller.go c4-app-chat
Controller is the shared 1:1 (direct-message) skychat core: it owns the per-peer framed connections, the listen/accept + per-conn read loops, the chat-msg / chat-ack / quoted-reply envelope handling, the outbound send path (with optional peer-receipt ack-wait and cross-network fallback), and best- effort persistence. It is transport-agnostic (a small Client seam satisfied by *app.Client on both the native visor and the wasm visor) and surfaces every message — inbound and the outbound mirror — through one OnEvent callback, so a consumer's UI/SSE layer stays out of the wire mechanics.
This is the extraction of what used to be duplicated: the native app's dialAndCache / tryNetworkFallback / handleConn / listenLoop / messageHandler send-core / ack-waiter machinery, and the wasm visor's ad-hoc conns/readChatConn/sendChat. The wire codec is pkg/skychat/message; the persistence backend is pkg/skychat/history (both js-safe), so this package compiles under GOOS=js as well as native.
Index ¶
- type Client
- type Config
- type Controller
- func (c *Controller) Close() error
- func (c *Controller) Connect(ctx context.Context, pk cipher.PubKey, netType appnet.Type) error
- func (c *Controller) Conns() int
- func (c *Controller) HasConn(pk cipher.PubKey) bool
- func (c *Controller) NoteInbound()
- func (c *Controller) Peers() []string
- func (c *Controller) Send(ctx context.Context, pk cipher.PubKey, netType appnet.Type, text string, ...) (SendResult, error)
- func (c *Controller) SendDelete(ctx context.Context, pk cipher.PubKey, id string) error
- func (c *Controller) SendRaw(ctx context.Context, pk cipher.PubKey, payload []byte) error
- func (c *Controller) SendRead(ctx context.Context, pk cipher.PubKey, id string) error
- func (c *Controller) Serve(conn net.Conn)
- func (c *Controller) Start(ctx context.Context) error
- func (c *Controller) Stats() Stats
- type Event
- type SendOpts
- type SendResult
- type Stats
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client interface {
Dial(appnet.Addr) (net.Conn, error)
Listen(appnet.Type, routing.Port) (net.Listener, error)
}
Client is the transport seam — the subset of *app.Client the controller needs. Both the native and the wasm visor supply an *app.Client.
type Config ¶
type Config struct {
// Client is the transport (dial/listen). Required.
Client Client
// Store persists messages (nil = ephemeral; delivery is never blocked).
Store history.Store
// Networks the controller listens + sends on (e.g. dmsg, skynet).
Networks []appnet.Type
// Port the controller listens on / dials. Zero = the well-known chat port (1).
Port routing.Port
// OnEvent is called for every surfaced message (inbound + outbound mirror).
// Never nil in practice; a nil callback simply drops surfacing.
OnEvent func(Event)
// OnReceipt is called for an inbound receipt about a message WE sent: a
// chat-ack (the peer's app received it) or a chat-read (the peer actually
// displayed it). id is the envelope id of the original message, so a UI can
// advance that bubble's delivery status; peer is who the receipt came from.
// Receipts are consumed here — they are never surfaced as chat lines.
// Nil = the app doesn't track delivery status.
OnReceipt func(peer, id, kind string)
// OnDelete is called for an inbound chat-delete: the peer asks us to drop
// (tombstone) a message THEY sent us, identified by its envelope id. Like a
// receipt it is consumed here, never surfaced as a chat line; the app
// replaces the message with a "deleted" placeholder. Nil = deletes ignored.
OnDelete func(peer, id string)
// StaleAckWindow bounds how long a RequestAck send waits for the peer's
// chat-ack before concluding the cached conn is half-open and evicting it,
// so the next send redials. A live peer acks in well under a second; this
// is generous so a momentarily-slow peer doesn't cost a healthy conn.
// Zero disables the check.
StaleAckWindow time.Duration
// DialRetry, if set, wraps each dial attempt (e.g. a netutil.Retrier's Do)
// to preserve the native app's retry behavior. Nil = single attempt.
DialRetry func(context.Context, func() error) error
// PreHandleFrame, if set, is offered every inbound frame before chat
// decoding; returning true consumes the frame (used by the native app for
// its pair-control envelopes). Nil = no interception.
PreHandleFrame func(peer cipher.PubKey, payload []byte) bool
// RawText sends a plain (no-reply, no-ack) message as bare text instead of
// a chat-msg envelope with a minted id. Off by default: every message then
// carries a stable id, which is what quoted replies, read ticks and
// delete-for-everyone key on, natively and in the wasm visor alike. Set it
// only to keep a default send byte-identical on the wire for pre-envelope
// peers; inbound bare text is always accepted regardless.
RawText bool
// Log is an optional structured logger sink.
Log func(format string, args ...interface{})
}
Config configures a Controller.
type Controller ¶
type Controller struct {
// contains filtered or unexported fields
}
Controller is the shared DM core. Create with New; bring listeners up with Start; send with Send; tear down with Close.
func New ¶
func New(cfg Config) *Controller
New builds a Controller. It does not open any connection until Start.
func (*Controller) Close ¶
func (c *Controller) Close() error
Close stops accept/read loops and closes every cached conn. Idempotent.
func (*Controller) Connect ¶
Connect makes sure there is a conn to pk, dialing and caching one over netType when there is not. A cached conn on ANY network satisfies it — the question this answers is "can a message leave for this peer right now", and the send path reuses whatever is cached regardless of which network carried it.
Send does this implicitly, inside the request that carries the first message. That is the right default for a CLI and the wrong one for a person: setting up a route can take the better part of a minute, and until it resolves the sender has no way to tell a slow handshake from an app that swallowed what they typed. Exported so the UI can ask for the handshake on its own, up front, and say so while it runs.
func (*Controller) Conns ¶
func (c *Controller) Conns() int
Conns returns the number of cached peer connections (for status probes).
func (*Controller) HasConn ¶
func (c *Controller) HasConn(pk cipher.PubKey) bool
HasConn reports whether a live cached connection to pk exists.
func (*Controller) NoteInbound ¶
func (c *Controller) NoteInbound()
NoteInbound records an inbound message that arrived outside the framed-conn read loop (e.g. the native app's CXO feed path) so the /status counters stay accurate for those transports too.
func (*Controller) Peers ¶
func (c *Controller) Peers() []string
Peers returns the PK hexes of every cached peer connection (for /status).
func (*Controller) Send ¶
func (c *Controller) Send(ctx context.Context, pk cipher.PubKey, netType appnet.Type, text string, opt SendOpts) (SendResult, error)
Send delivers text to pk over net. It mirrors the native send path: build the wire payload (plain, or a chat-msg envelope when a reply or ack is needed), (re)use a cached conn, write with a deadline, redial+retry once on a stale cached-conn write, fall back to the alternate network once, persist, surface the outbound mirror, and — for a WaitAck send — block on the peer's chat-ack.
func (*Controller) SendDelete ¶
SendDelete sends a delete-for-everyone command for the message id to pk over a cached-or-dialed conn. On the peer, OnDelete fires and the message is tombstoned. Best-effort like a receipt — a peer that's offline simply never applies it (the sender's own copy is deleted locally by the caller).
func (*Controller) SendRaw ¶
SendRaw writes a pre-framed raw payload to pk over a cached or freshly-dialed conn, trying each configured network in order. No envelope, persistence, or surfacing — it's for out-of-band control frames (the native app's pair-invite / pair-ack) whose inbound side is consumed by PreHandleFrame.
func (*Controller) SendRead ¶
SendRead sends a read receipt for the message id to pk (the message's original sender), over a cached-or-dialed conn. On the sender, OnReceipt fires with kind "read" so their bubble's tick advances to read. Best-effort like SendDelete — a peer that's offline simply never advances the tick.
func (*Controller) Serve ¶
func (c *Controller) Serve(conn net.Conn)
Serve runs a conn (whose RemoteAddr() reports an appnet.Addr) through the cache + read loop. It is the accept path above, and also the entry point for transports the Client seam doesn't cover — the native app's TCP-direct handshake hands its conn here. Blocks until the conn closes (the caller decides whether to run it in a goroutine).
func (*Controller) Start ¶
func (c *Controller) Start(ctx context.Context) error
Start opens a listener on each configured network and accepts inbound connections until ctx is canceled or Close is called. Returns an error only if NO listener could be started (a per-network failure is logged and skipped, matching the native + wasm behavior of keeping whatever listeners came up).
func (*Controller) Stats ¶
func (c *Controller) Stats() Stats
Stats returns a snapshot of the message counters.
type Event ¶
type Event struct {
ID string // message envelope id ("" for a plain, pre-envelope message)
Dir string // "in" | "out"
Peer string // the OTHER end's PK hex (sender for in, recipient for out)
Network string // "dmsg" | "skynet" | ...
Text string // message body
ReplyTo string // quoted-reply target id ("" if not a reply)
TS time.Time // receive (in) or send (out) time, UTC
}
Event is one surfaced direct message — an inbound message or the mirror of a local send. The consumer maps it into its own UI/SSE shape.
type SendOpts ¶
type SendOpts struct {
// ReplyTo, when set, quotes the message with this id (threaded reply).
ReplyTo string
// WaitAck > 0 requests a peer-receipt ack: the message rides a chat-msg
// envelope with ack=true and Send blocks up to WaitAck for the peer's
// chat-ack. Zero = fire-and-forget (default).
WaitAck time.Duration
// RequestAck asks the peer for a chat-ack WITHOUT blocking: the message
// rides an id'd envelope with ack=true exactly like WaitAck, but Send
// returns as soon as the write lands and the ack surfaces later through
// OnReceipt. This is the message-status lifecycle a UI wants (sent →
// received → read) as opposed to a CLI's synchronous --wait. Ignored when
// WaitAck is set, which already asks for the ack and waits for it.
RequestAck bool
// Auto picks the network automatically instead of using the netType arg:
// reuse a warm conn if one exists (so replies also follow the peer's chosen
// path), else send over dmsg immediately — no route setup, instant first
// contact — and warm a skynet route in the background so subsequent messages
// upgrade to the faster/steadier routed transport. netType is ignored.
Auto bool
}
SendOpts tunes one send.
type SendResult ¶
type SendResult struct {
ID string // the envelope id assigned to this message ("" for a plain send)
Acked bool // true if a WaitAck send received its ack in time
Elapsed time.Duration // time from write to ack (only meaningful when Acked)
Network appnet.Type // the network the message actually went out on (may differ via fallback)
}
SendResult reports the outcome of a send.
type Stats ¶
type Stats struct {
InboundMsgs int64
OutboundMsgs int64
InboundDrops int64 // conn read errors (peer/transport dropped)
OutboundFails int64 // sends that failed even after retry + fallback
OutboundRetry int64 // stale-cached-conn writes rescued by a redial+retry
OutboundFallbk int64 // sends rescued by the alternate network
LastRxAt time.Time
LastTxAt time.Time
}
Stats is a snapshot of the controller's message counters, for a status probe.