@livestore/webmesh
Webmesh is a library for connecting multiple nodes (windows/tabs, workers, threads, ...) in a network-like topology. It helps to establish communication channels between nodes.
There are three types of channels:
- ProxyChannel: a virtual channel by proxying messages along edges (via hop nodes)
- DirectChannel: an end-to-end channel with support for transferable objects (e.g.
Uint8Array) - BroadcastChannel: a virtual channel by broadcasting messages to all connected nodes
ProxyChannels and DirectChannels have the following properties (similar to TCP):
- Has a unique name across the network
- Auto-reconnects
- Ordered messages
- Reliable (buffers messages and acks each message)
Available edge connection implementations
MessageChannelBroadcastChannel(both web and Node.js)WebSocketwindow.postMessage
Example
Scenario: For topology A <> B <> C, we want to create direct channel between A and C
import { makeMeshNode, Packet, WebChannel } from '@livestore/webmesh'
const ChannelSchema = Schema.Struct({ message: Schema.String })
// Creating shared message channels between nodes to simplify the example.
// In a real-world application, you would use e.g. a shared worker or similar to exchange the message channels between nodes.
const mcA_B = new MessageChannel()
const mcB_C = new MessageChannel()
// e.g. running in tab A of a browser
const codeOnNodeA = Effect.gen(function* () {
const nodeA = yield* makeMeshNode('A')
// create edge to node B using a MessageChannel
const edgeChannelB = yield* WebChannel.messagePortChannel({ port: mcA_B.port1, schema: Packet })
yield* nodeA.addEdge({ target: 'B', edgeChannel: edgeChannelB })
const channelToC = yield* nodeA.makeChannel({ target: 'C', schema: ChannelSchema })
yield* channelToC.send('Hello from A')
})
// e.g. running in tab B of a browser
const codeOnNodeB = Effect.gen(function* () {
const nodeB = yield* makeMeshNode('B')
const edgeChannelA = yield* WebChannel.messagePortChannel({ port: mcA_B.port2, schema: Packet })
yield* nodeB.addEdge({ target: 'A', edgeChannel: edgeChannelA })
const edgeChannelC = yield* WebChannel.messagePortChannel({ port: mcB_C.port2, schema: Packet })
yield* nodeB.addEdge({ target: 'C', edgeChannel: edgeChannelC })
})
// e.g. running in tab C of a browser
const codeOnNodeC = Effect.gen(function* () {
const nodeC = yield* makeMeshNode('C')
const edgeChannelB = yield* WebChannel.messagePortChannel({ port: mcB_C.port1, schema: Packet })
yield* nodeC.addEdge({ target: 'B', edgeChannel: edgeChannelB })
const channelToA = yield* nodeC.makeChannel({ target: 'A', schema: ChannelSchema })
const message = yield* channelToA.listen.pipe(Stream.take(1), Stream.runCollect)
console.log('message', message) // => 'Hello from A'
})
Important notes
- Each node name needs to be unique in the network.
- The node name is also used as a "tie-breaker" as part of the messaging protocol.
- It's using the
WebChannelconcept from the@livestore/utilspackage. - We assume network edges to be low-latency (a few ms)
- Webmesh is used in LiveStore as the foundation for the LiveStore devtools protocol communication.
- The implementation should avoid timeout-based "solutions" as much as possible.
Tradeoffs
- Webmesh isn't meant for larger networks
- Nodes are mostly stateless to simplify the protocol / implementation
Inspiration
- Elixir
Distribution/ OTP - Consul by HashiCorp
- Ethernet