COTONIC

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.

Installation

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

Add the following tag to the page:

<script src="//path/to/cotonic.js"
        data-base-worker-src="//path/to/cotonic-worker.js"></script>

Workers

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");

cotonic.broker.publish("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 on_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.

cotonic.spawn("/js/example-worker.js");
=> 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");
cotonic.exit(wid);
=> 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);
});

Configuration
The page function has one configuration option.

base_worker_src
The url for the base worker runtime for cotonic. This runtime provides the required communication primitives.

<script>
    window.cotonic = window.cotonic || {};
    window.cotonic.config = {
        base_worker_src: "/base/worker-bundle.js"
    };
</script>
<script src="cotonic.js"></script>

Broker

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 cotonic.broker namespace.

find_subscriptions_below cotonic.broker.find_subscriptions_below(topic)
Find all subscribers below a certain topic. Used by the bridge to collect all subscriptions after a session restart. Returns a list with subscriptions.

cotonic.broker.find_subscriptions_below("truck");
=> [
  {type: "page", wid: 0, callback: function, sub: Object, topic: "truck/+/speed"}
  {type: "page", wid: 0, callback: function, sub: Object, topic: "#"}]

match cotonic.broker.match(topic)
Collect all subscribers which match the topic. Returns a list with subscriptions.

cotonic.broker.subscribe("truck/+/speed", function(msg) {
    console.log("Some trucks speed", msg);
});
cotonic.broker.subscribe("truck/#", function(msg) {
    console.log("Some info of a truck", msg);
});
cotonic.broker.subscribe("truck/02/speed", function(msg) {
    console.log("Speed of truck 2", msg);
})

cotonic.broker.match("truck/01/speed");
=> [Object, Object] // Returns two subscriptions, truck/+/speed, and truck/#
cotonic.broker.match("truck/01/speed")[0]
=> {type: "page", wid: 0, callback: function, sub: Object, topic: "truck/+/speed"}
cotonic.broker.match("truck/02/speed");
=> [Object, Object, Object] // Returns all subscriptions
cotonic.broker.match("boat/02/speed");
=> [] // Has no subscribers

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

qos
Quality of service. Can be 0, 1, or 2. 0 means at most once, 1 at least once, and 2 exactly once.
retain
When retain is true, the last message sent will be stored, and delivered immediately when a new client subscribes to the topic.
properties
Extra properties which can be attached to the message

cotonic.broker.publish("truck/001/temperature", 88);
=> All subscribers receive the message 88 on the topic.
cotonic.broker.publish("truck/001/speed", 74, {retain: true});
=> All subscribers receive the message 74. New subscribers will immediately receive 74.
            

subscribe cotonic.broker.subscribe(topics, callback, [options])
Subscribe to topics. Argument topics can either be a single topic, or a list of topics. The function callback will be called when a message which matches one of the topics is published. It is a function is called with two arguments, message and info. Argument message is the received mqtt message, and info the returned information object returned by extract. This makes it possible to easily extract information from the topic. Parameter options can be

wid
The worker if used for making the scription. Can be used to differentiate subscriptions from different components on the page. Defaults to: 0
qos
The quality-of-service of the subscription. Defaults to: 0
retain_handling
[todo]
retain_as_published
[todo]
no_local
[todo]
properties
[todo]

cotonic.broker.subscribe("truck/+truck_id/speed",
    function(msg, info) {
         console.log("Truck", info.truckid, "speed:", msg.payload);
    },
    {wid: "example"});
=> The function will now be called when a truck publishes its speed.

unsubscribe cotonic.broker.unsubscribe(topics, [options])
Unsubscribe from the topics. The parameter topics can be a single topic as a string, or a list of topics. The optional parameter options is an object which has the following properties.

wid
The worker id from which to unsubscribe from. Defaults to: 0

cotonic.broker.unsubscribe("truck/+truck_id/speed",
                           {wid: "example"});
=> The subscriptions for topic "truck/+truck_id/speed" for worker "example" will not be called anymore.

call cotonic.broker.call(topic, payload, [options])
Call is a special kind of publish where the publisher expects an answer back. The payload will be published on topic using the options as described in publish. The caller will be temporarily subscribed to a reply topic. When an answer is received on this reply topic, the returned promise will be resolved. When no answer is received, the promise will be rejected with a reason. The option parameter can have the following extra options:

timeout
The timeout in milliseconds to use before rejecting the returned promise. Default: 15000, or 15 seconds.

cotonic.broker.call("model/localStorage/get/username", {}, {timeout: 1000})
.then(function(username) {
    console.log("The username is:", username");
})
.catch(function(e) {
    console.log("Could not get username within 1 second.", e);
});

MQTT

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.

cotonic.mqtt.remove_named_wildcards("truck/+truck_id/speed");
=> "truck/+/speed"
cotonic.mqtt.remove_named_wildcards("truck/#truck_info");
=> "truck/#"

mqtt_bridge

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.

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

cotonic.mqtt_bridge.newBridge("test.mosquitto.org:8081",
                              {protocol: "wss"});
=> Connect the local broker to test.mosquitto.org via a websocket.

const decoder = new TextDecoder("utf-8");
cotonic.broker.subscribe("bridge/test.mosquitto.org:8081/bbc/subtitles/bbc_news24/raw",
    function(m, t) {
        console.log(decoder.decode(m.payload));
    });
=> Subscribe to a local topic, it will be bridged from the server
   to the page. This gets the raw subtitles of bbc news24.

For windows opened by a window with a cotonic broker, a special bridge "opener" can be started with cotonic.mqtt_bridge.newBridge("opener");. This bridge is a simple one-way bridge that makes it possible for the opened window to publish to topics in the opening window. It is not (yet) possible to subscribe to topics in the opener window.

Example publishing to the opener:

cotonic.broker.publish("bridge/opener/model/sessionStorage/post/test", "Hello");

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("test.mosquitto.org:8081");
=> returns the bridge, or undefined
            

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

cotonic.mqtt_bridge.deleteBridge("test.mosquitto.org:8081");

ui

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, mode, 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. Parameter mode supports the following options:

inner or true
The html-snippets update the innerHTML of the element.
outer or false
The html-snippets update the outerHTML of the element.
shadow-open or shadow
The html-snippets update the shadow DOM of the element. The element will be the shadow host. The shadow dom will be put in "open" mode. When the shadow dow was not yet initialised, it will be initialised when the element is rendered the first time. Using the shadow-dom makes it possible to isolate the component from the css rules which are present on the page.
shadow-closed
Like "shadow-open", with the difference that the shadow dom will be initialised in "closed" mode.
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", "outer", "<p>Hello World!</p>");

cotonic.ui.insert("shadow-root", "shadow", "<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.

cotonic.ui.remove("root");

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.

cotonic.ui.render();
=> All elements will be (re)rendered.

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

cotonic.ui.renderId("root");

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 = event.target.getAttribute("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.

tokenizer

The tokenizer transforms text to html tokens. The tokenizer is used by the user interface composer in order to call the incremental-dom api. It uses incremental-dom to do in-place diffing of the dom-tree.
Note: The tokenizer does not parse or validate the html. It just tokenizes the input.

tokens cotonic.tokenizer.tokens(text)
Transforms a string with html tags into a list of tokens. The tokens are objects of the form: {type: "type", [args]}, with type being one of:

open
Represents an open tag. Contains the attribute tag which is set to the tagname of the element, and the attribute attributes which is a list of attributes of the element.
close
Represents a close tag. Contains the attribute tag which is set to the tagname of the close element.
void
Represents a void element. The attribute tag is set to the tagname of the void element.
text
Represents a test element. The attribute data is set to the text data.
doctype
Represents a doctype element. The attribute attributes is set to the attributes of the element.
pi
Represents a processing instruction element. The attribute tag is set to the tagname of the processing instruction. The attribute arguments contains the list of attributes.
comment
Represents a comment element. The attribute data contains the text in the comment element.

cotonic.tokenizer.tokens("<div class='example'>Tokenizing<br /> is cool</div>");
=> [{type: "open", tag: "div", attributes: ["class", "example"]},
    {type: "text", data: "Tokenizing"},
    {type: "void", tag: "br", attributes: []},
    {type: "text", data: " is cool"},
    {type: "close", tag: "div"}] 

charref cotonic.tokenizer.charref(text)
Transforms a html charref into a character.

cotonic.tokenizer.charref("#128540");
=> "😜"
cotonic.tokenizer.charref("amp");
=> "&"

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.

on_init self.on_init(arguments)
The callback on_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. The callback is optional.

// Page code.
cotonic.spawn("example_worker", [amount, target]);

// Worker code

let amount = null;
let targetId = null;

self.on_init = function(n, id) {
    amount = n;
    targetId = id;
}
==> Worker receives the arguments which can be used for initialization.

connect self.connect(connectOptions)
Connect the worker to the page. Returns a promise. This promise is resolved when the worker is connected to the page. When is indicated in the connectOptions that the worker depends on other services the promise is resolved after those dependencies have become available. Parameter connectOptions supports the following options:

name
The name of the worker.
depends
A list of dependencies on which the worker needs. The connect promise will be resolved, after the dependencies become available.
provides
A list of dependencies which is provided by this worker. The are published after the worker is connected.

self.connect().then(
    function() {
        console.log("Connected");
    }
);
=> The worker is being connected to the page.
self.connect({depends: ["model/ui",
                        "model/serviceWorker",
                        "worker/abc"]}).then(
    function() {
        console.log("Connected, the requested dependencies are available, and worker abc is started.");
    }
);
=> The worker is being connected to the page, and the requested dependencies are available.

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.

self.disconnect();
=> The worker is disconnected from the page.

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

self.is_connected();
=> true

whenDependencyProvided self.whenDependencyProvided(dependency)
Returns a Promise which is resolved when the external dependency is provided.

self.whenDependencyProvided("model/ui").then(
    function() {
        worker.publish("model/ui/insert/foo", {..});
    });

whenDependenciesProvided self.whenDependenciesProvided(dependencies)
Returns a Promise which is resolved when all external dependencies are provided.

self.whenDependenciesProvided(["model/ui", "model/x"]).then(
    function() {
        ...
    });
==> The promise will be resolved after the ui and x model are provided.

provides self.provides(modelsAndWorkers)
Indicate to other workers and the page that this worker provides models or workers. Other workers waiting on these dependencies will get notified when they become available. The parameter modelsAndWorkers should be a list with model or worker names this worker provides.

self.provides(["model/a", "model/b", "worker/x"])
==> When the worker connects, it will notify other workers that new models
    have become available.

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.

self.unsubscribe();

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 self.call(topic, 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.

self.call("model/document/get/all")
.then(...)
.reject(...);

Models

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.

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

model/window

The window model can be used open and close windows.

post/open
Open a window. The payload of the message can be set t

href
The url of the window. See below.
data-window
A JSON string containing properties described below.
Or
url
The url of the page to open.
name
Specifies the target attribute of the window.
_blank
opens a new window.
_parent
loads the url in the parent window.
_self
replaces the current page.
_top
replaces any frameset that maybe loaded.
name
the name of the window to load.
width
The width of the window in pixels.
height
The height of the window in pixels.
toolbar
Wether or not to display the browsers toolbar.
scrollbars
Wether or not to display scrollbars.
status
Wether or not to add a status bar.
resizable
Wether or not the window is resizable.
location
Wether or not to display the address field.
menubar
Wether or not to display the menubar.
centerBrowser
Indicate wether or not to center the new window on top of the current active browser window.
centerScreen
Indicate wether or not to center the new window on the screen.
top
The top position of the window.
left
The left position of the window.

<a href="https://example.com/logon"
   class="btn btn-social"
   data-onclick-topic="model/window/post/open">Logon in with Example.com</a>
   data-window="{'centerBrowser': true}"
==> The click on the link publishes is window open message which will open
    a centered logon window.
self.publish("model/window/post/open", {url: "https://example.com/logon",
                                        centerBrowser: true})
==> A worker opens a logon window.

post/close
Open a window.

<a href="#" class="btn btn-default"
      data-onclick-topic="model/window/post/close"> Close Window <a>
==> Closes the current window.

model/document

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

get/all
Get all information on the current document. Includes screen size, cookies, user agent details.

self.call("model/document/get/all")
.then(function(m) {
    console.log(m.payload)
});
=> {screen_width: 1280, screen_height: 800,
    inner_width: 1047, inner_height: 292,
    is_touch: false, …}

get/intl
Returns the internationalization details of the current page.

self.call("model/document/get/intl")
.then(function(m) {
    console.log(m.payload)
});
=>  {timezone: {cookie: "", user_agent: "Europe/Amsterdam"},
     language: {cookie: "", user_agent: "en-US", document: null}}

model/location

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

get/href
Get the current href.

self.call("model/location/get/href")
.then(function(m) {
    console.log(m.payload)
});
=> "https://cotonic.org/#model.location"

get/protocol
Get the current protocol

self.call("model/location/get/protocol")
.then(function(m) {
    console.log(m.payload)
});
=> "https"

get/host
Get the current host (with port).

self.call("model/location/get/host")
.then(function(m) {
    console.log(m.payload)
});
=> "cotonic.org"

get/hostname

Get the current hostname (without port).
self.call("model/location/get/hostname")
.then(function(m) {
    console.log(m.payload)
});
=> "cotonic.org"

get/origin
Get the current origin.

self.call("model/location/get/origin")
.then(function(m) {
    console.log(m.payload)
});
=> "https://cotonic.org"

get/pathname
Get the current pathname.

self.call("model/location/get/pathname")
.then(function(m) {
    console.log(m.payload)
});
=> "/"

get/port
Get the current port.

cotonic.broker.call("model/location/get/port")
.then(function(m) {
    console.log(m.payload)
})
=> "" // The default port.

post/redirect
Redirect the user to another url

cotonic.broker.publish("model/location/post/redirect", {url: "https://cotonic.org"})
=> // The user is redirect to cotonic.org.

post/redirect/back
Loads the previous URL (page) in the history list. When there is no page in the history list, a default (local) url can be passed. When no navigation occurs, the user will be redirected to this url.

cotonic.broker.publish("model/location/post/redirect/back", {url: "/home"})
=> // Go back, when there is no page in the history, move to "/home".

<a data-onclick-topic="model/location/post/redirect/back" href="/home">Go Back</a>

post/reload
Reload the current page. This can be handy when you know something on the server side has changed.

cotonic.broker.call("model/location/post/reload")
=> // The page will reload.
<button data-onclick-topic="bridge/origin/model/xyz/post/doit"
        data-onresponse-topic="model/location/reload">Do It</button>

When the xyz model on the server responds, the page will be reloaded
            

post/q
Updates the current location’s search (query) arguments with the given object of { "key": value } keys. The browsers location is modified using replaceState and the new arguments are posted to model/location/event/q and model/location/event/qlist

If you need to have multiple keys with the same name, or the order of the keys is significant then use post/qlist.

cotonic.broker.call("model/location/post/q", {"a":"1", "b":"2"} ])
=> // The browser location will be updated, the page is not reloaded
<form data-onsubmit-topic="model/location/post/q">
   <input type="text" value="">
   <input type="submit" value="Submit">
</form>
            

post/qlist
Updates the current location’s search (query) arguments with the given list of [Key, Value] pairs. The browsers location is modified using replaceState and the new arguments are posted to model/location/event/q and model/location/event/qlist

cotonic.broker.call("model/location/post/qlist", [ ["a", "1"], ["b", "2"] ])
=> // The browser location will be updated, the page is not reloaded
<form data-onsubmit-topic="model/location/post/qlist">
   <input type="text" value="">
   <input type="submit" value="Submit">
</form>
            

post/push
Updates the current location with the given url. The new location is set using pushState, so the back button will work. The location is assumed to be on the same hostname as the current page, so only the new path, search and hash are pushed.
If parts of the new location are changed then they are posted to model/location/event/pathname, model/location/event/q, model/location/event/qlist and model/location/event/hash. If the hash is set and there is an element with an id corresponding to the hash, then that element is scrolled into view.

cotonic.broker.call("model/location/post/push", { url: "/hello?w=world" });
=> // The browser location will be updated, the page is not reloaded
<a data-onclick-topic="model/location/post/push" href="?a=1">

post/replace
Updates the current location with the given url. The new location is set using replaceState, so the back button will NOT work. The location is assumed to be on the same hostname as the current page, so only the new path, search and hash are pushed.
If parts of the new location are changed then they are posted to model/location/event/pathname, model/location/event/q, model/location/event/qlist and model/location/event/hash. If the hash is set and there is an element with an id corresponding to the hash, then that element is scrolled into view.

cotonic.broker.call("model/location/post/replace", { url: "/hello?w=world" });
=> // The browser location will be updated, the page is not reloaded
<a data-onclick-topic="model/location/post/replace" href="?a=1">

post/push-silent
Updates the current location with the given url. The new location is set using pushState, so the back button will work. The location is assumed to be on the same hostname as the current page, so only the new path, search and hash are pushed.
The new location is silently changed, no location events are published.

cotonic.broker.call("model/location/post/push-silent", { url: "/hello?w=world" });
=> // The browser location will be updated, the page is not reloaded
<a data-onclick-topic="model/location/post/push-silent" href="?a=1">

post/replace-silent
Updates the current location with the given url. The new location is set using replaceState, so the back button will NOT work. The location is assumed to be on the same hostname as the current page, so only the new path, search and hash are pushed.
The new location is silently changed, no location events are published.

cotonic.broker.call("model/location/post/replace-silent", { url: "/hello?w=world" });
=> // The browser location will be updated, the page is not reloaded
<a data-onclick-topic="model/location/post/replace-silent" href="?a=1">

event/search
A message containing the query string part of the url will be published when it changes. Note: the message is retained

cotonic.broker.subscribe("model/location/event/search",
    function(m, a) {
        console.log("query string changed", m.payload);
});

event/pathname
A message containing the pathname part of the url will be published when it changes. Note: the message is retained

cotonic.broker.subscribe("model/location/event/pathname",
    function(m, a) {
        console.log("pathname changed", m.payload);
    });

event/hash
A message containing the hash part of the url will be published when it changes. Note: the message is retained

cotonic.broker.subscribe("model/location/event/hash",
    function(m, a) {
        console.log("hash changed", m.payload);
    });

event/ping
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.

cotonic.broker.subscribe("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.

event/q
A message with the current query arguments is published on this topic. By subscribing to this topic it is possible to see when the query arguments change. The payload of the message is an object with { "key": "value" } entries. Note: the message is retained

cotonic.broker.subscribe("model/location/event/q",
    function(m) {
        console.log("New query arguments", m.payload)
    })
=> Logs a message on the console when the query arguments change.

event/qlist
A message with the current query arguments is published on this topic. By subscribing to this topic it is possible to see when the query arguments change. The payload of the message is an array with [ "key", "value" ] pairs. Note: the message is retained

cotonic.broker.subscribe("model/location/event/qlist",
    function(m) {
        console.log("New query arguments", m.payload)
    })
=> Logs a message on the console when the query arguments change.

Configuration
The location has one configuration option.

pathname_search
Set to the current search parameters of the page. Alternatively this setting can can be done as data-cotonic-pathname-search attribute on the body tag.

<script>
    window.cotonic = window.cotonic || {};
    window.cotonic.config = {
        start_service_worker: true,
        pathname_search: "q=110&j=yes"
    };
</script>
<script src="cotonic.js"></script>
<body data-cotonic-pathname-search="q=110&j=yes">
...
</body>

model/ui

The ui model can be used to update the dom by publishing messages on one of the model/ui/# topics. Elements can be inserted, deleted and updated. If elements in the dom-tree are updated, then a message model/ui/event/dom-updated/+key is published. This can be used to react to dom-changes when they happen.

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

initialData
The initial data to place in the dom tree.
inner
If set to true, the ui composer will update the inner html, when set to false, the outer html
priority
An element with a high priority will be rendered before elements with a lower 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
   loading
            

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

self.publish("model/ui/update/root",
    "
"); => The root element is updated.

delete/+key
Delete ui snippet named key from the user interface composer.

self.publish("model/ui/delete/root");
=> The root element is removed from the composer, but
   stays in the DOM tree.

render
Trigger a render of all registered elements.

self.publish("model/ui/render");

render/+key
Trigger a render of the element key

self.publish("model/ui/render.root");

render-template/+key
Request rendering a template with parameters. A rendering engine must be available to respond to the render requests. The render result will update key element. Note: The element must first be inserted with cotonic.ui.insert or model/ui/insert/+key. The payload of the render request should be:

topic
The topic on which the render engine is subscribed. Can be a local or a remote topic. The response of the renderer will be updated on key
data
The data which the render engine can use to render the template.
dedup
Send the render request via a MQTT QoS 1 message. This means that we can get multiple responses. Dedup makes sure the result will be rendered only once.

self.publish("model/ui/render-template/root",
             {topic: "bridge/origin/model/template/get/render/hello.tpl",
              dedup: true,
              data: {name: "Bob"}
             });
==> The render engine listening at bridge/origin/model/template receives
    a render request. The render result will be placed in element root.

replace/+key
Replace an element with id key with a new element or elements. If the new HTML is passed as a string then the new DOM tree can have multiple elements.
The replacement is done once and no further updates are registered.

self.publish("model/ui/replace/eltid", "Hello and here we are");
            

event/recent-activity
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
self.subscribe("model/ui/event/recent-activity",
    function(m) {
        const a = m.payload.is_active?"active":"not active";
        console.log("The user is: ", a);
    }
);
=> Displays the user's activity status.

event/dom-updated/+key
When the dom-tree is updated a message will be published. The message is fired after the dom-tree is updated.

cotonic.broker.subscribe("model/ui/event/dom-updated/message-container",
    function(m) {
        const c = document.getElementById("message-container");
        c.scrollTop = c.scrollHeight
    }
);
=> Scrolls the messages bubble container to the bottom.
            

event/+model/ui-status
Events send to this topic will result in class and data attributes set on the html of the document. The model attribute is used in the data and class attributes. This can be used to signal the state of a model to the document. This can be used to signal the state of cotonic workers or models to other ui libraries like Elm or React. The payload of the message:

status
Object with model statuses. Will be set as data attributes.
classes
List with model statuses. Will be set as classes.

self.publish("model/auth/event/ui-status",  {status: {"auth": "anonymous"},
                                             classes: ["anonymous"]});
==> model.ui adds clas "ui-state-auth-anonymous" and
    adds data-ui-state-auth-auth="anonymous" to the html element of the document.

event/new-shadow-root/+key
Init the topic event listener when new shadow roots are added.

id
Element id

cotonic.broker.subscribe("model/ui/event/new-shadow-root/data-list",
    function(m) {
         console.log("Init the event listener for", "data-list")
    }
);
=> Init the topic event listener when new shadow roots are added.

model/lifecycle

Application lifecycle is a key way that modern operating systems manage resources. On Android, iOS, and recent Windows versions, apps can be started and stopped at any time by the OS. This allows these platforms to streamline and reallocate resources where they best benefit the user.

Modern browsers will also suspend and discard pages. Browsers do not provide this information in an orderly fashion. There are individual events like: load, unload, and visibilitychange. The lifecycle model publishes the different states a page can be in.

active
The page has focus, and is visible in the foreground. It is important that the page is responsive to user input. It may be needed to move non-important work from the main thread to a worker.
passive
The page has lost focus, but is still visible. It is important that ui updates and animations still run smoothly. It may be a good time to store unsaved application state.
hidden
The page has lost focus, and is no longer visible, but it still exists. It is likely that this is the end of user interaction with the page. Unsaved state should be saved. It is also a good time to stop making UI updates, since they won't be seen by user.
frozen
The page has been cached to memory by the browser. It can resume in a later stage. Javascript's timers and fetch callbacks no longer work. Browsers can freeze pages to prevent full page reloads. This is a good time to close any open Web Socket connections, close WebRTC channels because the browser want's to conserve energy.
terminated
The page will be terminated shortly.
The lifecycle model publishes the state the page is at on the model/lifecycle/event/state topic as a retained message. This makes it possible to get the current state at any moment, and be notified of changes so the application can scale its resource usage at any moment.

State changes are always done in an orderly way. When a page goes from state active to state hidden it is made sure that the passive state is also published.

State transition diagram:

Lifecycle State Changes

event/state
When the lifecycle model sees a state change in the page lifecycle state, its state is published on this topic. The message is published as a retained message, and is available when the model is active. Possible states are: active, passive, hidden, frozen, and terminated.

// Subscribe in a worker
self.subscribe("model/lifecycle/event/state",
    function(m) {
        switch(m.payload) {
            case "active":
            case "passive":
                // Speed up network usage again.
            case "hidden":
                // Slow down network usage.
                break;
        }
   });

event/online
When the lifecycle model detects a change in the online state of the browser, the state will be published on this topic. When the browser is online, true will be published false otherwise.

self.subscribe("model/lifecycle/event/online",
    function(m) {
        if(m.payload) {
            console.log("We are online");
        } else {
            console.log("We are offline");
        }
    }
);

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

// Subscribe in a worker
self.subscribe("model/lifecycle/event/ping",
    function() {
        console.log("the lifecycle model is enabled")
   });

model/autofocus

The autofocus model looks for elements with the autofocus attribute which are just added to the dom tree. When this happens, it auto focusus them. The reason for this is is that the autofocus attribute only works during document render stage, but it is very handy to have available all the time.

event/focus/+key
The element with key has just been focussed.

model/serviceWorker

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.

A Note on Security
The serviceWorker needs https to work correctly. The Chrome browser does not accept self-signed certificates for running serviceWorkers. Either run from localhost, use a real certificate, or follow the instructions on this page by Dean Hume.

Safari and Firefox accept self-signed certificates for running the serviceWorker.

post/broadcast/+channel
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
self.publish("model/serviceWorker/post/broadcast/background",
             {hue: "blue", brightness: 45});
            

event/broadcast/+channel
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
cotonic.broker.subscribe("model/serviceWorker/event/broadcast/background",
    function(m) {
        console.log("Setting background to", m.payload);
        setBackground(m.payload);
    })

event/ping
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
cotonic.broker.subscribe("model/serviceWorker/event/ping",
    function() {
        console.log("the serviceWorker model is enabled")
   });

Configuration
The service worker model has two configuration options.

start_service_worker
When set to false, the service worker will not start.
default true
service_worker_src
The url of the service worker which should be started.
default /cotonic-service-worker.js

<script>
    window.cotonic = window.cotonic || {};
    window.cotonic.config = {
        start_service_worker: true,
        service_worker_src: "/cotonic-service-worker.js"
    };
</script>
<script src="cotonic.js"></script>

model/localStorage

get/+key
Gets item key from localStorage. Returns the content as payload.

cotonic.broker.call("model/localStorage/get/a")
.then(
    function(m) {
        console.log("a is set to:", m.payload)
    }
);

post/+key
Update, or insert message under key in localStorage.

cotonic.broker.publish("model/localStorage/post/a", "Hello world!");

delete/+key
Delete item stored under key from localStorage.

cotonic.broker.publish("model/localStorage/delete/a");

event/+key
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.

cotonic.broker.subscribe("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.

event/ping
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.

cotonic.broker.subscribe("model/localStorage/event/ping",
    function() {
        console.log("localStorage is enabled");
    });

model.sessionStorage

The sessionStorage model provides 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/+key
Get the element stored as key from the sessionStorage. Returns the contents as payload, or null if it is not found.

self.call("model/sessionStorage/get/item-1")
.then(function(m) {
    console.log("item-1", m.payload);
});
=> Logs the item-1 on the console, or null otherwise.
           

get/+key/+subkey
Get a sub element stored as key.subkey from the sessionStorage.

self.call("model/sessionStorage/get/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.

post/+key/+subkey
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/+key
Delete an element stored under key from the sessionStorage.

self.publish("model/sessionStorage/delete/item-2");

delete/+key/+subkey
Delete an element stored under key.subkey from the sessionStorage.

self.publish("model/sessionStorage/delete/item-2");

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

self.subscribe("model/sessionStorage/subscribe/+key",
    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

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

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

model.dedup

The dedup model implements message deduplication. It works by sending a message to the dedup model, the dedup model then relays the message to a topic with a response topic set to the dedup model.

If a message is tried to be relayed before a message is received on the response topic then it will be saved. Only the last saved message is stored. Any previously saved messages are overwritten.

If a message is received on the dedup response topic then that message will be relayed to the original message's response topic (if any). If there is a saved message then that message will be relayed to the saved message topic.

post/message
Handles messages with the following payload:

{
    topic: "some/topic",
    payload: "the message payload",
    timeout: optionalTimeoutInMsec
}

The payload will be published to the given topic. The timeout is optional and defaults to 15000 msec. If there is no response to the dedup reponse topic within the timeout then the message is assumed to be lost and any queued message or new message is allowed to be relayed.
The de-duplication is done by generating a key. The key is the concatenation of the topic and the response topic of the message.

self.publish("model/dedup/post/message", { topic: "hello/there", payload: { a: 1 } })
=> De-duplicates messages to the hello/there topic.

post/message/+key
Exactly like post/message except that the deduplication key is not generated from the topics but predefined. This enables to use deduplication in calls, where the response topic is always unique.

self.call("model/dedup/post/message/one-at-a-time", { topic: "hello/there", payload: { a: 1 } })
.then( (msh) => console.log(msg) );
=> De-duplicates all messages using the one-at-a-time key.

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

let dedupReady = false;
cotonic.broker.subscribe("model/dedup/event/+key",
    function(msg, args) {
        switch(args.key)
        case "pong":
            dedupReady = true;
            break;
        ...
    });
=> When the dedup model is ready a pong message will be available

Introduction to Incremental DOM

MQTT Version 5.0 - OASIS

Web Workers API

sessionStorage API

Using shadow DOM

Change Log

1.6.1Dec 1, 2023DiffDocsDownload

1.6.0Nov 28, 2023DiffDocsDownload

1.5.1Nov 8, 2023DiffDocsDownload

1.5.0Nov 6, 2023DiffDocsDownload

1.4.1Sep 7, 2023DiffDocsDownload

1.4.0Aug 4, 2023DiffDocsDownload

1.3.1Jun 23, 2023DiffDocsDownload

1.3.0May 26, 2023DiffDocsDownload

1.2.2Apr 21, 2023DiffDocsDownload

1.2.1Apr 17, 2023DiffDocsDownload

1.2.0Apr 4, 2023DiffDocsDownload

1.1.9Dec 19, 2022DiffDocsDownload

1.1.8Oct 12, 2022DiffDocsDownload

1.1.7Jul 29, 2022DiffDocsDownload

1.1.6Jul 26, 2022DiffDocsDownload

1.1.5May 31, 2022DiffDocsDownload

Special thanks go to @williamthome for fixing the python dev environment.

1.1.2Jan 22, 2022DiffDocsDownload

1.1.1Dec 6, 2021DiffDocsDownload

1.1.0Oct 27, 2021DiffDocsDownload

1.0.8Aug 25, 2021DiffDocsDownload

1.0.7Jul 8, 2021DiffDocsDownload

1.0.6Mar 15, 2021DiffDocsDownload

1.0.5Mar 14, 2021DiffDocsDownload

1.0.4Nov 2, 2020DiffDocsDownload

1.0.3Mar 1, 2020DiffDocsDownload

1.0.2Feb 10, 2020DiffDocsDownload

1.0.1Jan 30, 2020DiffDocsDownload

1.0.0Jan 23, 2020DocsDownload