Broadcast channel in Deno
Introduction
In its continual support for web APIs, Deno now supports web standard BroadcastChannel API too. The broadcast channel API is used to:
- Create & subscribe to a broadcast channel
- Send & receive broadcast messages
The idea behind broadcast channel is similar to JavaScript’s global addEventListener function in which interested parties can provide a callback to get notified about a certain global event. The broadcast channel works pretty much the same. Although it’s not limited by standard, usually there would be one producer & one or more receivers. The producer/broadcaster isn’t aware of the receivers (just like message queue).
A great documentation of broadcast channel is present at MDN. Instead of repeating it, we’ll just take a small overview of three useful functions, followed by a use case of config file updates.
Creation
The creation of broadcast channel happens by creating an object of BroadcastChannel with the name of the channel. If a broadcast channel exists with the same name, the existing channel would be returned. Otherwise, a new broadcast channel would be created.
const channel=new BroadcastChannel("generalAnnouncement");
const channel=new BroadcastChannel("panicAnnouncement");
PostMessage
The postMessage function can be used to broadcast a message to all the receivers.
channel.postMessage("Here is a message");
channel.postMessage({a: 1, b: 2});
The caller of postMessage isn’t aware of the receivers.
OnMessage
The onMessage function can be used to receive a broadcast. This function needs a callback from the user. The input to the callback function is MessageEvent that contains the data sent through postMesssage.
channel.onMessage=e=>console.log(e.data);
Now that we’ve gone through the basics of broadcast channel, let’s take a suitable use-case for demonstration: monitoring & propagating config file updates.
Use case — Config file updates
Some applications have a config file (could be JSON formatted) that is usually read at the startup. The individual modules inside the application read parts of the config file and save relevant data. While up & running, the application needs to track config file changes and reload data without requiring an application restart. We can use broadcast channel to propagate config file updates to the receivers.
First, we’ve the config file (config.json):
//config.json{
"opt1": "one",
"opt2": "two",
"opt12": "three four"
}
Next, we’ve the constants file (appConsts.ts):
//appConsts.tsexport const CONFIG_FILE='./config.json';
export const CONFIG_CHANNEL='configChange';
Next, we’ve the main module (app.ts). The main module creates AppMod1, the broadcast channel, and a config file (‘./config.json’) monitor (watchFs). Whenever config file changes, it reloads it and then post it to the broadcast channel.
//app.tsimport {AppMod1} from "./appMod1.ts";
import * as c from "./appConsts.ts";const getConfig = async () => {
return JSON.parse(await Deno.readTextFile(c.CONFIG_FILE));
};const cfg=await getConfig();
new AppMod1(cfg);
watchConfig();async function watchConfig() {
const notifier=new BroadcastChannel(c.CONFIG_CHANNEL);
for await (const w of Deno.watchFs(c.CONFIG_FILE)) {
if(w.kind==='modify') {
console.log('Notifying listeners ', w);
notifier.postMessage(await getConfig());
}
}
}
Next, we’ve the module: appMod1. This one saves relevant data (opt1 & opt12) from received config data, and creates mod2. AppMod1 is a consumer of the broadcast, therefore it defines an onMessage callback. The onMessage callback saves the relevant data from received config data.
//appMod1.tsimport {CONFIG_CHANNEL} from "./appConsts.ts";
import {AppMod2} from "./appMod2.ts";export class AppMod1 {
#opt1:string="";
#opt12:string="";
#appMod2:AppMod2;constructor(cfg:Record<string, string>) {
this.#appMod2=new AppMod2(cfg);
this.loadConfig(cfg);
const notifier=new BroadcastChannel(CONFIG_CHANNEL);
notifier.onmessage=e=>this.loadConfig(e.data);
}loadConfig(cfg:Record<string, string>) {
this.#opt1=cfg.opt1 || "";
this.#opt12=cfg.opt12 || "";
console.log('AppMod1: Config loaded. opt1=', this.#opt1, ', opt12=', this.#opt12);
}
};
Next, we’ve the module: appMod2. This one saves relevant data (opt2 & opt12) from received config data. AppMod2 is also a consumer of the broadcast, therefore it defines an onMessage callback. The onMessage callback saves the relevant data from received config data.
import {CONFIG_CHANNEL} from "./appConsts.ts";export class AppMod2 {
#opt2:string="";
#opt12:string="";constructor(cfg:Record<string, string>) {
this.loadConfig(cfg);
const notifier=new BroadcastChannel(CONFIG_CHANNEL);
notifier.onmessage=e=>this.loadConfig(e.data);
}loadConfig(cfg:Record<string, string>) {
this.#opt2=cfg.opt2 || "";
this.#opt12=cfg.opt12 || "";
console.log('AppMod2: Config loaded. opt1=', this.#opt2, ', opt12=', this.#opt12);
}
};
That’s all about the code. Now, let’s do a sample run. When the application starts, initial config data would be loaded into the modules (mod1 saves opt1 & opt12, while mod2 saves opt2 & opt12).
> deno run --allow-read --unstable app.ts
AppMod2: Config loaded. opt1= two , opt12= one two
AppMod1: Config loaded. opt1= one , opt12= one two
Now, let’s update the config.json file using sed command:
> sed -i '' -e 's/two/three/g' config.json
> cat config.json
{
"opt1": "one",
"opt2": "three",
"opt12": "one three"
}
The sed command results in two modify events to app.ts that propagates them to the receivers.
Not sure why there are two modify events. Perhaps sed command updates the file two times?
deno run --allow-read --unstable app.ts
AppMod2: Config loaded. opt1= two , opt12= one two
AppMod1: Config loaded. opt1= one , opt12= one twoNotifying listeners { kind: "modify", paths: [ "/Users/mayankc/Work/source/denoExamples/config.json" ] }
Notifying listeners { kind: "modify", paths: [ "/Users/mayankc/Work/source/denoExamples/config.json" ] }
AppMod2: Config loaded. opt1= three , opt12= one three
AppMod1: Config loaded. opt1= one , opt12= one three
AppMod2: Config loaded. opt1= three , opt12= one three
AppMod1: Config loaded. opt1= one , opt12= one three