| lastmod | 2021-12-03T17:39:51.376Z |
|---|
Different parts of an application or various locations of services and clients need to communicate with each other. Event emitter are a proven concept for in-app communication. But pretty soon tasks become more complex or transport for communication is limited or insecure. These tools try to simplify this task.
We differentiate between the following parts:
Emitter: Classic local event handlingChannel: SimplepostMessageinterface for basic data transport without a protocolPubSub: Simple one way messages, similar toEmitterbut usingChannelas transportMessages: RPC like interface for communication viaChannel, awaits response from other sideEncoder: Transform data into a special format for transport like e.g. JSON, encrypted data, etc.
Channels are a uniform abstraction for sending and receiving data usually in binary format. It uses the commonly known MessageEvent pattern, with send via postMessage and listening on message. It is extended to optionally reflect connection states.
channel.postMessage('Hello World')
channel.on('message', (msg) => {
log(`Received data=${data}`)
})
Same API as Emitter for easily sending event like messages via a Channel. It is type safe, if you pass an interface as the generic.
interface MyProtocol {
doSomething: (using: string, count: number) => void
}
const hub = usePubSub<MyProtocol>({ channel })
hub.emit('doSomething', 'hello', 2)
hub.on('doSomething', (using, count) => {
// ...
})
You can also use the alternative more PubSub like syntax ;)
hub.publish('doSomething', 'hello', 2)
hub.subscribe('doSomething', (using, count) => {})
Usually data is sent in a binary form. Therefore, an encoding has to transform objects into a form, that both ends can understand. Specific encoders can also apply additional transforms like encryption.
Messages are a high level abstraction for communicating through channels. They provide additional benefits:
- They always return a response, so the sender knows if the message reached the other participant
- Messages are packed as
Promise, so you can await results - The message and the payload are wrapped in a method like structure resulting in a codings style close to locally calling methods on an object
- Timeouts can be set and will throw an error on failure
- If the channel is broken, it is possible to retry sending the message
Messages are defined via interface. Typescript checks for valid calls:
interface MyMessages {
echo: (data: any) => Promise<any>
pong: (data: any) => Promise<void>
}
Using the messages is easy:
const hub = useMessageHub({ channel }).send<MyMessages>()
const echoResponse = await hub.echo({ hello: 'world' })
On the receiver part implementation is also straight forward:
useMessageHub({ channel }).listen<MyMessages>({
async echo(data) {
return data
}
})
https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects
- WebSocket: Supports binary channel, see zerva-websocket
- WebRTC: Supports binary channel
- HTTP: Supports binary channel
- WebWorker: Supports ArrayBuffer
- IFrame
- BroadcastCannel
Other projects I learned from, check them out: