Primitives / Network
websocket
A duplex socket as a bundle. The factory takes a name (the resource tag is
per module, so one assembly holds one socket) and an optional createSocket
for tests; the placement takes { url, connectTimeoutMs? }.
import { websocket, type SocketService } from 'foldkit-primitives/net'
const ChatSocket = websocket({ name: 'ChatSocket' })
const Chat = Bundle.compose({ wantConnection: Schema.Boolean }).pipe(
Bundle.withServices<SocketService>(),
Bundle.withChild('socket', ChatSocket, {
args: { url: 'wss://example.com/chat', connectTimeoutMs: 5000 },
when: model => model.wantConnection,
}),
)status moves closed → connecting → open. The resource owns the socket and
acquires it while the placement is active (when above). Sending is a placed
helper, Chat.children.socket.helpers.send(data), a step to return from
update: it writes through the resource and yields Sent { data } on
dispatch (not delivery), or SendFailed when no socket is open, since a closed
socket's send is a silent no-op per spec. The socket service rides the
assembly into the application's resources, which is what withServices names.
Received { data } notifies without storing; project the payload into your own
field to keep it. With connectTimeoutMs, a socket still connecting when the
time runs out is closed and reported TimedOut; without one it waits as long
as the browser does.
The Model words its own errors for a reader (Failed to connect to WebSocket,
Connection error, Connection timeout, Socket unavailable), chosen by
whether the socket had opened. A view reads SocketView through
viewOf(model, wanted), one of Disconnected, Connecting, Connected, or
Error { error }, instead of keeping a connection state machine of its own;
isOpen(model) gates sends. examples/foldkit/websocket-chat
is a whole page over this bundle.
sse
The one-directional sibling: the server speaks, the Model listens. Same
shape minus send. A Failed records the error but stays connecting,
because the browser reconnects a dropped stream itself. Payloads are always
text, per the SSE spec. isLive(model) and viewOfSse(model, wanted) mirror
the socket's readers.
Broadcast
broadcastMessages(name) is an entry: posts from other tabs on the named
channel arrive as Received { data }, and the parent maps them into its own
Message. There is no bundle because a channel owns no state. postBroadcast(name, data)
is the one-shot Command, yielding Posted or BroadcastFailed. A post never
echoes to its own channel, per spec.