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.
<script src="//path/to/cotonic.js" data-base-worker-src="//path/to/cotonic-worker.js"></script>
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!");
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.
<script> window.cotonic = window.cotonic || {}; window.cotonic.config = { base_worker_src: "/base/worker-bundle.js" }; </script> <script src="cotonic.js"></script>
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:
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
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.
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:
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); });
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/#"
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.
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");
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:
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.
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:
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"); => "&"
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:
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(...);
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.
The window model can be used open and close windows.
post/open
Open a window. The payload of the message can be set t
<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.
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}}
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
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.
<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>
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.
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:
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:
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.
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.
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.
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:
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") });
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.
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.
<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>
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"); });
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
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
1.6.1 β Dec 1, 2023 β
Diff β
Docs β
Download
1.6.0 β Nov 28, 2023 β
Diff β
Docs β
Download
1.5.1 β Nov 8, 2023 β
Diff β
Docs β
Download
1.5.0 β Nov 6, 2023 β
Diff β
Docs β
Download
1.4.1 β Sep 7, 2023 β
Diff β
Docs β
Download
1.4.0 β Aug 4, 2023 β
Diff β
Docs β
Download
1.3.1 β Jun 23, 2023 β
Diff β
Docs β
Download
1.3.0 β May 26, 2023 β
Diff β
Docs β
Download
1.2.2 β Apr 21, 2023 β
Diff β
Docs β
Download
1.2.1 β Apr 17, 2023 β
Diff β
Docs β
Download
1.2.0 β Apr 4, 2023 β
Diff β
Docs β
Download
1.1.9 β Dec 19, 2022 β
Diff β
Docs β
Download
1.1.8 β Oct 12, 2022 β
Diff β
Docs β
Download
1.1.7 β Jul 29, 2022 β
Diff β
Docs β
Download
1.1.6 β Jul 26, 2022 β
Diff β
Docs β
Download
1.1.5 β May 31, 2022 β
Diff β
Docs β
Download
1.1.2 β Jan 22, 2022 β
Diff β
Docs β
Download
1.1.1 β Dec 6, 2021 β
Diff β
Docs β
Download
1.1.0 β Oct 27, 2021 β
Diff β
Docs β
Download
1.0.8 β Aug 25, 2021 β
Diff β
Docs β
Download
1.0.7 β Jul 8, 2021 β
Diff β
Docs β
Download
1.0.6 β Mar 15, 2021 β
Diff β
Docs β
Download
1.0.5 β Mar 14, 2021 β
Diff β
Docs β
Download
1.0.4 β Nov 2, 2020 β
Diff β
Docs β
Download
1.0.3 β Mar 1, 2020 β
Diff β
Docs β
Download
1.0.2 β Feb 10, 2020 β
Diff β
Docs β
Download
1.0.1 β Jan 30, 2020 β
Diff β
Docs β
Download
1.0.0 β Jan 23, 2020 β
Docs β
Download