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.
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
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"
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.
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");
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);
});
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.
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: "#"}]
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
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.
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.
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.
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.
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
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"
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"]}
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
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.
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");
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
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.
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>");
cotonic.ui.get(id)
Returns the current html snippet registered at id.
let currentHTML = cotonic.ui.get("root");
=> "Hello World!"
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");
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.
cotonic.ui.render()
Trigger a render of all registered elements.
cotonic.ui.render();
=> All elements will be (re)rendered.
cotonic.ui.renderId(id)
Just render the element with the given id.
cotonic.ui.renderId("root");
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">
...
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">
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.
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"}]
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.
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.
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.
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.
self.is_connected()
Returns true iff the worker is connected to the page,
false otherwise.
self.is_connected();
=> true
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", {..});
});
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.
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.
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.
self.unsubscribe(topics, callback, ack_callback)
Unsubscribe the worker from page. The worker will no longer receive messages from the specified topics.
self.unsubscribe();
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});
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.
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.
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 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, …}
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 the current href.
self.call("model/location/get/href")
.then(function(m) {
console.log(m.payload)
});
=> "https://cotonic.org/#model.location"
Get the current protocol
self.call("model/location/get/protocol")
.then(function(m) {
console.log(m.payload)
});
=> "https"
Get the current host (with port).
self.call("model/location/get/host")
.then(function(m) {
console.log(m.payload)
});
=> "cotonic.org"
Get the current hostname (without port).
self.call("model/location/get/hostname")
.then(function(m) {
console.log(m.payload)
});
=> "cotonic.org"
Get the current origin.
self.call("model/location/get/origin")
.then(function(m) {
console.log(m.payload)
});
=> "https://cotonic.org"
Get the current pathname.
self.call("model/location/get/pathname")
.then(function(m) {
console.log(m.payload)
});
=> "/"
Get the current port.
cotonic.broker.call("model/location/get/port")
.then(function(m) {
console.log(m.payload)
})
=> "" // The default port.
Redirect the user to another url
cotonic.broker.publish("model/location/post/redirect", {url: "https://cotonic.org"})
=> // The user is redirect to cotonic.org.
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>
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
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>
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>
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">
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">
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">
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">
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);
});
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);
});
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);
});
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.
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.
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.
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 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 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 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.
Trigger a render of all registered elements.
self.publish("model/ui/render");
Trigger a render of the element key
self.publish("model/ui/render.root");
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 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");
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.
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.
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.
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:
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;
}
});
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");
}
}
);
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.
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.
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.
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});
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);
})
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")
});
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
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)
}
);
Update, or insert message under key in localStorage.
cotonic.broker.publish("model/localStorage/post/a", "Hello world!");
Delete item stored under key from localStorage.
cotonic.broker.publish("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.
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.
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 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 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.
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.
self.publish("model/sessionStorage/delete/item-2");
Delete an element stored under key.subkey from the sessionStorage.
self.publish("model/sessionStorage/delete/item-2");
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
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.
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.
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.
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
Links
Introduction to Incremental DOM
MQTT Version 5.0 - OASIS
Web Workers API
sessionStorage API
Using shadow DOM
Change Log
— Dec 1, 2023 —
Diff —
Docs —
Download
- Added documentation for model/location/post/redirect/post
- Added default url to redirect/post, when no navigation occurs.
— Nov 28, 2023 —
Diff —
Docs —
Download
- Add onvisible-topic, model/ui/replace and model/location push and replace by @mworrell
- Add simple bridge to opener window by @mworrell
— Nov 8, 2023 —
Diff —
Docs —
Download
- Add onvisible-topic
- model/ui/replace, and
- model/location push and replace
— Nov 6, 2023 —
Diff —
Docs —
Download
-
Add model/ui/post/qlist with replaceState support.
— Sep 7, 2023 —
Diff —
Docs —
Download
-
Removed usage of unload event for modern browsers.
— Aug 4, 2023 —
Diff —
Docs —
Download
-
Use esbuild instead of cat to make releases.
-
Moved modules to separate es modules for better adaptability.
— Jun 23, 2023 —
Diff —
Docs —
Download
-
Fixes a problem where the remote client topic was accidentally set to the
remote routing topic.
— May 26, 2023 —
Diff —
Docs —
Download
-
Added skip node functionality to be able to integrate dom-tree changing external
libraries.
— Apr 21, 2023 —
Diff —
Docs —
Download
-
Fix an issue with renaming response topics.
— Apr 17, 2023 —
Diff —
Docs —
Download
-
Fix a bug with queued messages where the routing-id was not known yet, or was
changed.
— Apr 4, 2023 —
Diff —
Docs —
Download
-
The service worker now supports displaying web push notifications.
— Dec 19, 2022 —
Diff —
Docs —
Download
-
Added events which allows seeing when a websocket connection is openend and closed.
— Oct 12, 2022 —
Diff —
Docs —
Download
— Jul 29, 2022 —
Diff —
Docs —
Download
-
If a window close is requested and there is no window.opener then optionally redirect
to the supplied url.
— Jul 26, 2022 —
Diff —
Docs —
Download
-
Added a redirect-local in order to safely redirect to a local url.
-
Documentation and site fixes.
— May 31, 2022 —
Diff —
Docs —
Download
-
Don't de-dup subscriptions from unspecified worker ids (wid).
-
Warn when unsubscribing from a topic, without specifying a worker id (wid).
-
Fix starting the python dev server.
-
Fix a typo in the specification of the page lifecycle state-machine.
Special thanks go to
@williamthome for fixing the
python dev environment.
— Jan 22, 2022 —
Diff —
Docs —
Download
-
Changed the way onclick and and onsubmit topics are handled. When such an event
is fired, cotonic traverers the parents of the target of the event to check for
the topic attribute. When it is found, a message is published with the attributes
of the elements between the target and the element on which the topic was found.
— Dec 6, 2021 —
Diff —
Docs —
Download
-
Cleaned up another couple of IE11 workarounds, and added extra tests.
— Oct 27, 2021 —
Diff —
Docs —
Download
-
Because IE11 is no longer supported, all lazy polyfill loading code has been removed.
— Aug 25, 2021 —
Diff —
Docs —
Download
-
Added model/location/post/reload model post.
— Jul 8, 2021 —
Diff —
Docs —
Download
-
Added model/lifecycle/event/state and model/lifecycle/event/online
events.
-
Various small fixes.
— Mar 15, 2021 —
Diff —
Docs —
Download
-
Added model/ui/event/node-created/+key and model/ui/event/node-deleted/+key
events.
-
Added model/document/post/cookie/+key and model/document/get/cookie/+key
to set and retrieve cookies.
-
Small fixes.
— Mar 14, 2021 —
Diff —
Docs —
Download
-
More documentation.
-
Autofocus model
-
Extend js configuration api to more easily setup the MQTT bridge.
-
Ability to pre-connect the bridge websocket very early to speed up connecting.
-
Ability to collect click and submit events before the bridge is setup to prevent
the loss of interactivity before everything is setup. After the bridge is connected
the click and submit messages collected will be published on origin.
-
And a lot of little fixes.
— Nov 2, 2020 —
Diff —
Docs —
Download
-
More documentation.
-
A js based configuration api.
-
And a lot of little fixes.
— Mar 1, 2020 —
Diff —
Docs —
Download
-
Added more documentation and an example.
-
Load the base worker library via a standard importScripts, instead of url blob.
-
The bridge now supports connecting to any MQTT server.
-
Removed the embedded polyfills, and use an external polyfill provider when needed.
Mostly for IE11.
— Feb 10, 2020 —
Diff —
Docs —
Download
-
Added documentation, and
-
made it possible to use the bridge to connect to mqtt servers
— Jan 30, 2020 —
Diff —
Docs —
Download
-
Fixed all the tests, and
-
made the documentation more readable on mobile.
— Jan 23, 2020 —
Docs —
Download