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
: SimplepostMessage
interface for basic data transport without a protocolPubSub
: Simple one way messages, similar toEmitter
but usingChannel
as 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: