Cotonic is a Javascript library which makes it possible to split the javascript code of your page into truly isolated components. By doing this a crash in one component can never affect another component.

Cotonic provides tools to make it possible for these components to cooperate by providing an MQTT publish/subscribe bus. This makes it possible for components to communicate via topics.

The project is hosted on Github. You can report bugs and discuss features on the issues page.

Cotonic is an open-source component of Zotonic.


Place the cotonic.js, cotonic-worker.js and cotonic-service-worker.js scripts on a web-server. (Right-click and use "Safe as"), or use one of the download links.

Add the following tag to the page:

<script src="//path/to/cotonic.js"


Cotonic uses Web Workers which all run in separate calling context. This means that they will not block the main user interface thread. They are also truly isolated from each other. This means that a crash or another kind of problem in worker A can't crash worker B.

You can run whatever code you like in workers, with some exceptions. You can't access the DOM, and a lot of things from the window object. This also makes them more secure because you don't have to worry that worker code from an external resource can steal the a credit-card number entered somewhere in the DOM tree.

Cotonic adds a MQTT like publish subscribe mechanism to the standard javascript web worker api. This makes it easy for web workers to communicate with each other.

// worker-a
"use strict";

self.subscribe("some/topic", function(message) {
    self.publish("model/ui/update", "<p>Worker A got message</p>")

self.publish("model/ui/insert", "<p>Worker A started</p>");
// worker-b
"use strict";

self.subscribe("some/topic", function(message) {
    self.publish("model/ui/update", "<p>Worker B got message</p>")

self.publish("model/ui/insert", "<p>Worker B started</p>");
let worker_a = cotonic.spawn("worker-a.js", [1, 2]);
let worker_a = cotonic.spawn("worker-b.js");"some/topic", "hello workers!");

Page Functions

Cotonics main functions are available in the cotonic namespace on the main page. The functions are mostly used to control and allow messaging between workers.

spawn cotonic.spawn(url, [args])
Spawn a new worker. The code of the worker should be available at url. The optional args parameter list will be passed to the worker and can be used to pass information from the page to the worker. It can be picked up with the worker_init callback. The structured clone algorithm will be used to send the args to the worker. See worker functions for more information on implementing a worker. Returns the worker-id of the newly created worker process.

=> 1
cotonic.spawn("/js/another-worker.js", ["Arg1", 1]);
=> 2

spawn_named cotonic.spawn_named(name, src_url, [base], [args])
Spawn a new named worker. Named workers are unique. Use "" or undefined to create a nameless worker. Return the worker_id of the newly spawned worker. If the worker was already running, the existing worker_id is returned.

cotonic.spawn_named("example", "example-worker.js");
=> "worker"

exit cotonic.exit(nameOrwid)
Exit terminates a worker which was previously spawned and has nameOrWid as worker-id.

const wid = cotonic.spawn("example.worker.js");
=> Worker wid is no longer running.

send cotonic.send(nameOrWid, message)
Send message message to worker nameOrWid. The parameter nameOrWid can either be the name of worker, or a worker id. The structured clone algorithm will be used to copy the message before sending it to the worker.

cotonic.send(worker_id1, "hello");

receive cotonic.receive(handler)
Receive messages from workers. The handler should be a function which takes two parameters, the message, and the worker_id.

cotonic.receive(function(message, worker_id) {
    console.log("Received", message, "from worker", worker_id);

set_worker_base_src cotonic.set_worker_base_src(baseUrl)
Set the base url to load additional importable cotonic modules from. This is used to locate the base worker script which is needed to communicate with the other workers. It is usually located in the same location as the main cotonic script.
By default the location is /lib/cotonic/cotonic-worker.js. It is also possible to configure this via the script via which cotonic itself is loaded on the page. Note: When this is not setup correctly, it will not be possible to spawn workers.

=> Cotonic will now use the '/lib/js/cotonic-worker.js' when workers are spawned.
<script data-worker-base-src="/lib/cotonic-worker.js" src="/lib/cotonic.js"></script>
=> Cotonic will use '/lib/cotonic-worker.js' when new workers are spawned.


The broker module handles all local publish subscribe connections. The subscriptions are stored in a trie datastructure allowing quick action. They are available in the namespace.

Find all subscribers below a certain topic. Used by the bridge to collect all subscriptions after a session restart. Returns a list with subscriptions."truck");
=> [
  {type: "page", wid: 0, callback: function, sub: Object, topic: "truck/+/speed"}
  {type: "page", wid: 0, callback: function, sub: Object, topic: "#"}]

Collect all subscribers which match the topic. Returns a list with subscriptions."truck/+/speed", function(msg) {
    console.log("Some trucks speed", msg);
});"truck/#", function(msg) {
    console.log("Some info of a truck", msg);
});"truck/02/speed", function(msg) {
    console.log("Speed of truck 2", msg);
=> [Object, Object] // Returns two subscriptions, truck/+/speed, and truck/#"truck/01/speed")[0]
=> {type: "page", wid: 0, callback: function, sub: Object, topic: "truck/+/speed"}"truck/02/speed");
=> [Object, Object, Object] // Returns all subscriptions"boat/02/speed");
=> [] // Has no subscribers

publish, payload, [options])
Publish the message payload on a topic. The possible options are:

Quality of service. Can be 0, 1, or 2. 0 means at most once, 1 at least once, and 2 exactly once.
When retain is true, the last message sent will be stored, and delivered immediately when a new client subscribes to the topic.
Extra properties which can be attached to the message"truck/001/temperature", 88);
=> All subscribers receive the message 88 on the topic."truck/001/speed", 74, {retain: true});
=> All subscribers receive the message 74. New subscribers will immediately receive 74.

subscribe, callback, [options])
Subscribe to the topics. The callback will be called when a message which matches one of the topics is published.

// [TODO];

unsubscribe, [options])
Unsubscribe from the topics.

// [TODO];

call, payload, [options])
Call a topic, returns a promise for the response.

// [TODO];


The mqtt module provides functions to work with mqtt topics. The broker uses this module as the basis to provide its functionality. This module also has some utility functions to easily extract information from topics.

matches cotonic.mqtt.matches(pattern, topic)
Returns true when the pattern matches the topic, false otherwise.

cotonic.mqtt.matches("truck/+/speed", "truck/01/speed");
=> true
cotonic.mqtt.matches("boat/+/speed", "truck/01/speed");
=> false 
cotonic.mqtt.matches("+/+/speed", "plane/01/speed");
=> true 
cotonic.mqtt.matches("+/+/speed", "plane/01/height");
=> false

fill cotonic.mqtt.fill(pattern, params)
Fill can use a pattern topic, and use the param object to create an mqtt topic. Returns a string with the created topic.

cotonic.mqtt.fill("truck/+truck_id/speed", {truck_id: 100});
=> "truck/100/speed"

extract cotonic.mqtt.extract(pattern, topic)
Extract values from topic into an object. The pattern +<key> matches a single level of the topic path. It places an attribute key with as value the found element in the path in the returned object. The pattern #<key> matches a multi level path. When it is matched it places that part of the path into a list. Returns an object with the found elements.

cotonic.mqtt.extract("truck/+truck_id/speed", "truck/01/speed");
=> {truck_id: "01"}
cotonic.mqtt.extract("truck/+truck_id/#params", "truck/01/speed");
=> {truck_id: "01", params: ["speed"]}

exec cotonic.mqtt.exec(pattern, topic)
When the pattern matches the topic, extract the values from the topic. Returns the extracted values when the pattern matches, null otherwise.

cotonic.mqtt.exec("truck/+truck_id/speed", "truck/01/speed");
=> {truck_id: "01"}
cotonic.mqtt.exec("boat/+truck_id/speed", "truck/01/speed");
=> null

remove_named_wildcards cotonic.mqtt.remove_named_wildcards(pattern)
Remove the special, and non mqtt compliant, wildcards from the pattern and return a compliant topic.

=> "truck/+/speed"
=> "truck/#"


This module makes it possible to bridge the broker on the local page to an external MQTT broker.

newBridge cotonic.mqtt_bridge.newBridge([remote], [options])
Create a new bridge. This is the hostname of the mqtt broker to connect to. When set to "origin" the bridge uses the hostname of the document. When the bridge is connected messages published on the topic matching bridge/+remote/# will be re-published on the remote broker. Subscriptions on the topic bridge/+remote/# will be published locally. This makes it possible to connect and communicate with all clients connected to remote mqtt brokers. Note: It is possible to connect to multiple brokers.

The protocol to use to connect to the mqtt broker. Defaults to "ws" when the page is loaded via "http", "wss" otherwise.
The pathname to use when connecting the web socket to the broker. Default: "mqtt-transport"
Default: 20
Default: 1000
The mqtt_session module which should be used. Default: cotonic.mqtt_session.

                              {protocol: "wss"});
=> Connect the local broker to via a websocket.

const decoder = new TextDecoder("utf-8");"bridge/",
    function(m, t) {
=> Subscribe to a local topic, it will be bridged from the server
   to the page. This gets the raw subtitles of bbc news24.

findBridge cotonic.mqtt_bridge.findBridge([remote])
Find bridge remote, when remote is not specified, "origin" is used. Returns the bridge, or undefined when the bridge is not found.

const b = cotonic.mqtt_bridge.findBridge("");
=> returns the bridge, or undefined

deleteBridge cotonic.mqtt_bridge.deleteBridge([remote])
Delete bridge remote, when remote is not specified, "origin" is used.



The user interface composer manages html snippets which can be placed in the DOM tree. When an updated html snippet is delivered to the composer it will render it by using Google's incremental-dom library. The updates will be applied incrementally directly to the DOM tree. The updates can be delivered as html text snippets to the interface composer.

insert cotonic.ui.insert(targetId, isInner, initialHTML, [priority])
Insert a new html snippet into the user interface composer. The snippet will be stored under the given targetId. The element will not be placed in the dom-tree immediately. This will happen when one of the render functions is called. The isInner boolean flag indicates if only innerHTML of the target element must be updated, or the outerHTML. The optional priority parameter indicates the render order of the elements. Elements with a high priority are rendedered before lower priorities. This makes it possible to nest elements.

cotonic.ui.insert("root", false, "<p>Hello World!</p>");

get cotonic.ui.get(id)
Returns the current html snippet registered at id.

let currentHTML = cotonic.ui.get("root");
=> "Hello World!"

remove cotonic.ui.remove(id)
Remove the html snippet registered at id. Note that this will not remove the element from the dom-tree, it will only remove it from the user interface composer. When the element must be removed it should first be updated and set to a blank string and a render operation should be done.


update cotonic.ui.update(id, htmlOrTokens)
Update the registered snippet for the registered element with the given id. The new snippet will be visible after a render operation.

cotonic.ui.update("root", "<p>Hello Everybody!</p>");
=> The root element on the page will be updated.

render cotonic.ui.render()
Trigger a render of all registered elements.

=> All elements will be (re)rendered.

renderId cotonic.ui.renderId(id)
Just render the element with the given id.


updateStateData cotonic.ui.updateStateData(model, states)
Communicate the state of the model to other non-cotonic components on the page. It can be used to pass model state to SPA's or other modules. It sets a data attribute on the html tag of the page. The parameter model should be a string, states is an object with values. The values of the states object are set as data attributes on the html tag like this: data-ui-<model>-<key>="<value>". When an empty object is passed all data attributes of the model is cleared.

cotonic.ui.updateStateData("auth", {authorized: true});
=> <html data-ui-state-auth-authorized="true">
cotonic.ui.updateStateData("auth", {});
=> <html">

updateStateClass cotonic.ui.updateStateClass(model, classes)
Update the class of the html tag. This makes it possible to communicate important state changes to external components like SPA's. The parameter model should be a string. Parameter classes a list of classes which must be set. The following elements will be added to the class attribute ui-state-<model>-<class>. Passing [] will clear all the class attributes of the model.

cotonic.ui.updateStateClass("auth", ["unauthorized", "pending"]);
=> <html class="ui-state-auth-pending ui-state-auth-unauthorized"> 
cotonic.ui.updateStateClass("auth", ["authorized"]);
=> <html class="ui-state-auth-authorized">

on cotonic.ui.on(topic, msg, event, [options])
Publish a DOM event on the local broker. This allows subscribers to react to user interface events. Parameter topic is the topic on which the event will be published. The parameters msg and event are included in the message which is published. The event parameter is expected to be a DOM event. The options parameter is optional, it can contain a cancel property which can be set to true, false or "preventDefault" to indicate if the event should be cancelled or prevented. The other options can be the normal options found in publish.

document.addEvenListener("click", function(e) {
    const topic ="data-topic");
    if(!topic) return;
    cotonic.ui.on(topic, {foo: "bar"}, e);
}, {passive: true})
=> When somebody clicks on an element with has a data-topic="a/topic"
   attribute, the event will be published on that topic.

Worker Functions

Workers are stand alone processes. They have no shared data with the page, nor with other workers. Their memory and calling context is isolated. They can easily communicate with other workers, the page, and servers by publising messages on topics, and subscribing to them. Cotonic provides models, modules which are loaded and ready for requests.

connect self.connect()
Connect the worker to the page. When this step succeeds the on_connect is called. on_error when it fails.

=> The worker is being connected to the page.

disconnect self.disconnect()
Disconnects the worker from the page. After this step it is no longer possible to send and receive messages from the page.

=> The worker is disconnected from the page.

is_connected self.is_connected()
Returns true iff the worker is connected to the page, false otherwise.

=> true

subscribe self.subscribe(topics, callback, ack_callback)
Subscribe the worker to the topics. When a message is received, the callback is called. Callback is a function which receives two parameters. The first parameter is the message, the second parameter an object returned by extract. This can be used to easily extract elements from topic paths in an object. The parameter topics can be a string, or a list of strings. The callback ack_callback is used when the page is subscribed, or when there is a problem. Returns nothing.

function logSpeed(msg, args) {
    if(args.boat_id) {
        console.log("boat", args.boat_id, "is now moving at", msg.payload);
    if(args.truck_id) {
        console.log("truck", args.truck_id, "is now moving at", msg.payload);
self.subscribe(["truck/+boat_id/speed", "boat/+boat_id/speed"], logSpeed);
=> The function logSpeed will be called when somebody sends a message which
   matches the topics.

unsubscribe self.unsubscribe(topics, callback, ack_callback)
Unsubscribe the worker from page. The worker will no longer receive messages from the specified topics.


publish self.publish(topic, message, options)
Publish message on topic. The options can be used to indicate the quality of service, or if the message should be retained by the broker.

self.publish("world", "hello", {retain: true});

call, message, options)
Publishes message on topic and subscribes itself to a reply topic. Returns a promise which is fulfilled when a message is received on the reply topic. When no message arrives, the promise is rejected. Returns a promise."model/document/get/all")

worker_init self.worker_init
The callback worker_init is called when the worker receives the initialization message by the page. It can take multiple arguments. The arguments are passed in via the spawn args argument list. This function can be used to initialize the worker, but it is optional.

// Worker code

let amount = null;
let targetId = null;

self.worker_init = function(n, id) {
    amount = n; 
    targetId = id;

on_connect self.on_connect
The on_connect callback will be called after a successfull connect call.

self.on_connect = function() {
    // things to do after a connect

on_error self.on_error
The on_error callback will be called after an unsuccessfull connect call.

self.on_error = function() {
    // things to do after an error


Because workers run as independent components it is not possible to directly call api's. Some api's are also not available to web workers. Models are special modules, or workers, which publish their data, or are ready to receive calls via mqtt topics.

The convention is that models provide their services via the following topic tree.

Call topics. When the model receives a publish, it returns the answer to the reply topic.
Topics used to post updates to the model.
Topics used to delete items managed by the model.
Topics which the model publishes events on.


The document model can be used to retrieve details about the current document.

Get all information on the current document. Includes screen size, cookies, user agent details."model/document/get/all")
.then(function(m) {
=> {screen_width: 1280, screen_height: 800,
    inner_width: 1047, inner_height: 292,
    is_touch: false, …}

Returns the internationalization details of the current page."model/document/get/intl")
.then(function(m) {
=>  {timezone: {cookie: "", user_agent: "Europe/Amsterdam"},
     language: {cookie: "", user_agent: "en-US", document: null}}


The location model can be used to retrieve information on the current location of the page. It also allows subscription to location changes.

Get the current href."model/location/get/href")
.then(function(m) {
=> ""

Get the current protocol"model/location/get/protocol")
.then(function(m) {
=> "https"

Get the current host (with port)."model/location/get/host")
.then(function(m) {
=> ""


Get the current hostname (without port)."model/location/get/hostname")
.then(function(m) {
=> ""

Get the current origin."model/location/get/origin")
.then(function(m) {
=> ""

Get the current pathname."model/location/get/pathname")
.then(function(m) {
=> "/"

Get the current port."model/location/get/port")
.then(function(m) {
=> "" // The default port.

A message containing the query string part of the url will be published when it changes. Note: the message is retained"model/location/event/search",
    function(m, a) {
        console.log("query string changed", m.payload);

A message containing the pathname part of the url will be published when it changes. Note: the message is retained"model/location/event/pathname",
    function(m, a) {
        console.log("pathname changed", m.payload);

A message containing the hash part of the url will be published when it changes. Note: the message is retained"model/location/event/hash",
    function(m, a) {
        console.log("hash changed", m.payload);

When the location model is enabled, a retained message is published on this topic. By subscribing to this topic it is possible to see when the model is enabled. The payload of the message pong."model/location/event/ping",
    function(m) { 
        console.log("The location model is enabled", m.payload)
=> Logs a message on the console when the location model is enabled.


Insert a new ui snippet named key into the user interface composer. Expects a message with the following properties.

The initial data to place in the dom tree.
If set to true, the ui composer will update the inner html, when set to false, the outer html
An element with a high priority will be rendered before elements with a low priority.

self.publish("model/ui/insert/root", {
    initialData: "<div class='loading'>Loading</div>",
    inner: true
    priority: 100
}, {retain: true});
=> When the ui composer receives this message it will add the

Update the contents of ui snippet named key previously registered by an insert. Expects a message with the html snippet as message.

"); => The root element is updated.

Delete ui snippet named key from the user interface composer.

=> The root element is removed from the composer, but
   stays in the DOM tree.

A message containing the user's activity status is published regularly on this topic. The message is an object which contains a is_active boolean property which indicates if the has been active over the last period between publishes.

// worker example
    function(m) {
        const a = m.payload.is_active:"active":"not active";
        console.log("The user is: ", a);
=> Displays the user's activity status.




The serviceWorker model makes it possible to communicate with other tabs from the same site. This makes it possible to communicate important state to all tabs.

Broadcast the message on channel. The message can be received via the "model/serviceWorker/event/+channel topic. This works across all open tabs.

// Publish from a worker
             {hue: "blue", brightness: 45});

Subscribe to the broadcast channel of the serviceWorker. Messages posted from other tabs or workers, including messages sent by the publisher will be received.

// Subscribe on a page"model/serviceWorker/event/broadcast/background",
    function(m) {
        console.log("Setting background to", m.payload");

When the serviceWorker model is enabled it publishes a retained pong message on this topic. This makes it possible to check if the model is enabled.

// Subscribe on a page   "model/serviceWorker/event/ping",
    function() {
        console.log("the serviceWorker model is enabled")


Gets item key from localStorage. Returns the content as payload."model/localStorage/get/a")
.then(function(m) { console.log(m.payload) }); 

Update, or insert message under key in localStorage."model/localStorage/post/a", "Hello world!");

Delete item stored under key from localStorage."model/localStorage/delete/a");

Subscribe to changes or deletions from localStorage. When the message payload is null the item is delete. Otherwise the payload is set to the newly updated value."model/localStorage/event/+key",
    function(m, a) {
        if(m.payload === null) {
            console.log("localStorage item:", a.key, "deleted");
        else {
            console.log("localStorage item:", a.key, "changed", m.payload);
=> Logs update to localStorage elements.

When the localStorage model is enabled it publishes a retained message under the topic model/localStorage/event/ping. It makes it possible to detect the localStorage model is enabled."model/localStorage/event/ping",
    function() {
        console.log("localStorage is enabled");


The sessionStorage model provids access to the sessionStorage of the browser. It makes it possible to set and retrieve values via mqtt topics. For more information about the session storage see:

Get the element stored as key from the sessionStorage. Returns the contents as payload, or null if it is not found."model/get/sessionStorage/item-1")
.then(function(m) {
    console.log("item-1", m.payload);
=> Logs the item-1 on the console, or null otherwise.

Get a sub element stored as key.subkey from the sessionStorage."model/get/sessionStorage/item-2/a")
.then(function(m) {
    console.log("item-2", m.payload);
=> Logs the item-2 on the console, or null otherwise.

post/+key Store a message under key in the sessionStorage.

self.publish("model/sessionStorage/post/item-1", "Cucumbers are sometimes green");            
=> New value stored.

Store a message under key.subkey in the sessionStorage. When no item is stored under key a new object is created, and subkey is added as sub-element. When key exists it must be an object, then subkey is added or overwritten.

self.publish("model/sessionStorage/post/item-2/a", "hello");

Delete an element stored under key from the sessionStorage.


Delete an element stored under key.subkey from the sessionStorage.


Subscribe to sessionStorage updates and deletes. When entry is updated you will get a notification.

    function(m, a) {
        console.log(m, a);
self.publish("model/sessionStorage/post/a", "hello");            
self.publish("model/sessionStorage/post/b", "world");            
=> Logs the update in the console

When the sessionStorage model is enabled a retained message is published on the model/sessionStorage/event/ping topic.

let storageReady = false;"model/sessionStorage/event/+key",
    function(msg, args) {
        case "pong": 
            storageReady = true;
=> When the storage is ready a pong message will be available

MQTT Version 5.0 - OASIS

Web Workers API

sessionStorage API

Change Log

1.0.1Feb 10, 2020DiffDocsDownload

1.0.1Jan 30, 2020DiffDocsDownload

1.0.0Jan 23, 2020DocsDownload