Headless apps.

The same app, the same window.Arda, with nothing to draw on: a box that syncs and prints, a sensor that pushes readings. The page's scripts run in the player's own JavaScript runtime.

Working — for an app folder given on the command line

Running one

shell
$ ardaplayer -headless myapp/
level=INFO msg="headless: running" script=main.js
level=INFO msg=app msg="started"

The player loads app/index.html and runs its classic <script> elements in order (inline or src from the folder). -dev changes nothing here (the player says so); arda.dev.json's settings and webhooks still apply to a local run.

A deployed app on a device with no screen

In progress — released in ArdaPlayer v1.1.17, not yet proved on a device

From v1.1.17 a device with no screen — a kiosk that finds none, or ardaplayer -headless with no folder — runs the app deployed to it through this same runtime, by the same rule as a folder: index.html's classic <script> elements, inline or a local src, in document order. Modules and scripts from another site are skipped.

  • Arda.headless is true before the first line, and Arda.config is the device's settings.
  • An app already installed when the device starts runs at once. The first app pushed to a new device waits until its needForBoot data has arrived, as a screen's boot screen does.
  • Each update restarts the app on the new version. The update negotiation is the same as on a screen: 'updateStart' is heard, pause() and apply() work.
  • location.reload() starts the app again. Locking the device in the portal stops it until it is unlocked.
  • Not in developer mode: -dev runs a folder.
!
Not yet proved on a device This has run on a desk against a test server (push, update, lock and unlock, restart). On a device it has so far only been seen starting an app built for a screen, which stopped at its first DOM call as described below; no headless app has run on one yet. Up to v1.1.16 a device with no screen stays synced and recoverable from the portal but does not start the deployed app, and its log says so.

What differs: there is no DOM

app/main.js
// Arda.headless is true before your first line; undefined in a page.
if (!Arda.headless) {
  document.getElementById('total').textContent = '0';   // only with a screen
}

function tick() {
  readSensor().then(function (v) {
    return Arda.db.push('readings', { value: v, at: new Date().toISOString() }, 'r-' + Date.now());
  }).catch(function (e) { console.error('reading failed:', e.message); });
}
setInterval(tick, 60000);
console.log('started');
  • document is not stubbed: touching it throws, with a message naming the fix. A stub that did nothing would give an app that seems to run and achieves nothing.
  • Put the logic that must run without a screen outside your DOM code, in classic scripts. <script type="module"> is skipped with a warning, and so is a script from another site.
  • The engine is ES5.1 plus much of ES6. ?? works, so the Arda.config idiom is safe.
  • console.* goes to the player's log (on a device, the journal); it is the only way a headless app is heard. See logging.
  • An uncaught throw is logged and the runtime keeps going, so a screenless device stays connected to report it.
  • Arda.keyboard() rejects; there is nothing to draw on.

fetch and WebSocket

app/main.js
// Your own servers, the same way a page would (a per-device setting holds the token)
fetch('https://api.example.com/orders', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + (Arda.config.api_token ?? ''), 'Content-Type': 'application/json' },
  body: JSON.stringify({ till: Arda.config.till_name ?? 'till' })
}).then(function (r) { return r.ok ? r.json() : null; })
  .then(function (orders) { if (orders) console.log('orders', orders.length); });

function connect() {
  var ws = new WebSocket('wss://live.example.com/station');
  ws.onmessage = function (e) { /* e.data: a string, or an ArrayBuffer for binary */ };
  ws.onclose = function () { setTimeout(connect, 2000); };   // your app reconnects
}
connect();
  • fetch takes method, headers, body (string, ArrayBuffer or typed array) and signal; the response has ok, status, statusText, url, headers.get(), text(), json(), arrayBuffer(). A relative URL is the player itself (/storage/…). No CORS, no cookies; a request gives up after 2 minutes.
  • WebSocket is the browser's API. Binary messages arrive as an ArrayBuffer (there is no Blob). Headless only, a third argument {headers: {…}} adds handshake headers.
  • setTimeout, setInterval, EventSource, AbortController, TextEncoder and TextDecoder (UTF-8) are there.
  • A socket does not survive the device going offline; a pushed record does. Keep what must not be lost in a dataset.

Checked: the examples on this site's other pages were run in this runtime (ardaplayer -dev -headless on a test folder): Arda.headless, Arda.config from arda.dev.json, Arda.db, fetch('/storage/…'), webhooks, and the document error. The v1.1.17 deployed-app behaviour is from the player's source (cmd/ardaplayer/headless.go, internal/jsrun/supervise.go) and its desk run against a test server; it has not run on a device. The fetch to an outside server and the WebSocket example were not run here.