// Package msgport adapts a message channel to the duplex that chaindata speaks. // // The browser has no shared memory across a worker boundary, only messages: a MessagePort, a // worker's postMessage, or a nested channel. What crosses is an ArrayBuffer that can be // transferred rather than copied, which is why the frames were made binary in the first place. // // This package is the whole adapter, and it is small on purpose. One direction pumps frames // from the port into the duplex's send side, the other pumps answers from the duplex's // receive side back out. Everything above it — the request ids, the cancel, the close — is // chaindata's, unchanged, and everything below it is whatever the platform calls a message // channel. Neither side knows the other exists. package msgport import ( "errors" "sync" "git.smesh.lol/tiny-spawn" ) // ErrClosed is a port that is gone: closed locally, or its far end died. var ErrClosed = errors.New("msgport: the port is closed") // Buffer is how many frames may wait in one direction. // // The protocol is request/answer with one request in flight per chain in practice, so this is // not a queue depth so much as a way to keep a burst of answers from blocking the JS event // loop. A port whose buffer fills is a peer that has stopped reading, and the adapter treats // that as death rather than as backpressure it cannot apply. const Buffer = 64 // Port is one end of a message channel that carries whole frames. // // Receive returns a channel that is closed when the port is gone, which is how a worker that // was terminated without a word reaches the protocol as a close instead of as silence. type Port interface { // Send delivers one frame, transferring its buffer if the platform allows it. Send(frame []byte) error // Receive is the incoming frames, closed when the port is gone. Receive() <-chan []byte // Close ends the port in both directions. Close() error } // Duplex adapts a port to the duplex chaindata speaks. // // The returned value is the caller's end: writing to Tx sends a frame out, reading from Rx is // the far side's answer. Closing Tx, a send failure, or the port closing all end the duplex // the same way — the receive side is closed, so a reader sees a channel that is done rather // than a channel that is quiet. func Duplex(port Port) tinyspawn.Duplex[[]byte] { out := make(chan []byte, Buffer) in := make(chan []byte, Buffer) var ( mu sync.Mutex dead bool ) // stop closes the receive side exactly once. It runs under the same lock a delivery // takes, so a frame can never be sent to a closed channel: deliver either wins the // lock and hands the frame over, or finds the port dead and drops it. stop := func() { mu.Lock() defer mu.Unlock() if dead { return } dead = true close(in) } deliver := func(frame []byte) bool { mu.Lock() defer mu.Unlock() if dead { return false } select { case in <- frame: return true default: // The reader is not keeping up. A protocol with one request in flight cannot // catch up later, so the honest answer is that this channel is dead. return false } } // Outgoing: the caller's writes, sent to the platform. go func() { for frame := range out { mu.Lock() gone := dead mu.Unlock() if gone { return } if err := port.Send(frame); err != nil { stop() return } } // The caller closed Tx: it has stopped sending, which is how a terminated worker // looks from the other end. stop() }() // Incoming: the platform's frames, handed to the caller. go func() { for frame := range port.Receive() { if !deliver(frame) { stop() return } } stop() }() return tinyspawn.Duplex[[]byte]{Tx: out, Rx: in} }