Build apps
How apps work
Where an app runs, when it draws, and what it may and may not do.
On this page
Where an app runs
Your code never runs on the display. A display keeps a playlist of apps, and for each one it asks
LEDABLE's servers for an image. The platform loads the version of your app that the playlist
names, calls its render, turns the frames it returns into a WebP and sends that back: a still
image for one frame, an animation for several. The display then plays the image for as long as
that playlist entry is on screen.
Each version runs in an isolate of its own: a closed-off JavaScript environment that holds your bundle and nothing else. The bundle is your code together with the parts of the SDK it imports, the drawing code included, built when you upload. A published version never changes, so it draws the same pixels for as long as displays use it, whatever happens to the platform around it.
Three functions
The entry module's default export is the object defineApp makes from three functions:
import { defineApp } from "@ledable/sdk/app";
export default defineApp({
settings: (ctx) => ({ /* every setting the app has */ }),
render: (ctx, params) => ({ /* the image for one display */ }),
preview: (ctx) => ({ /* the picture the store shows */ }),
});settingsreturns every setting the app has: what people fill in when they add it. It is code, not configuration, so it can build a list of options from data. The platform keeps what it returns for a day (24 hours) unless the app setssettingsTtlMs. See Settings.renderdraws the image for one display, from that display's settings inparamsand the request inctx. It returns the frames and, optionally, how to play them and how long they last.previewdraws the store's picture of the app. It must give the same image every time: the platform renders it once per version and keeps it.
An app with a credential setting also has verifyCredential; see
Accounts and credentials.
Each function gets its own context, with only what it may depend on:
settings | render | preview | |
|---|---|---|---|
Assets (ctx.assets) | yes | yes | yes |
Time (ctx.now) | yes | yes | no |
Cache (ctx.cache) | shared by everyone | yes | no |
Secrets (ctx.secrets) | yes | yes | no |
| The display and its settings | no | yes | no |
settings sees nothing of any one person or display, because everybody who adds the app gets the
same form. preview sees nothing that can change, because its image is kept for good.
When render runs
render runs every time a display asks for the image, and a display asks:
- when its playlist changes, such as when the app is added or its settings change;
- again once the image's soft TTL has passed. The display keeps playing the old image until the new one arrives;
- once more after a failed attempt, a little later. A display keeps the last image it got until its hard TTL has passed; after that it drops the image and shows that it is loading.
A display does this for every app in its playlist, not only the one on screen, so the next app is
ready when its turn comes. Without TTLs of your own, an image is renewed after
5 minutes and dropped after 10 minutes
without a renewal. Return softTtlMs and hardTtlMs to follow what you show: a clock asks again
at the next minute, a day count at midnight. Caching and refresh covers
TTLs, keeping the display's image when nothing changed, and sharing work between displays.
Each display asks for itself: a hundred displays with your app make a hundred renders. Keep
nothing about one display or person in module-level variables. An isolate serves every display
that runs the version and can be replaced at any time; share work through ctx.cache instead.
When render throws, or returns { ok: false, msg } to say in its own words that it cannot draw
now, the display gets no new image and keeps the one it has.
What ctx gives a render
Everything that is not a pure computation comes through ctx, so a render is a function of its
inputs and a test can supply every one of them.
| Member | What it is | More |
|---|---|---|
now | The time of this request, on the platform's clock | Testing |
timezone, language | The display's time zone (an IANA name such as Europe/Berlin) and language tag | |
resolution | The panel's width and height in pixels | Drawing |
durationMs | How long the display shows this playlist entry each time | Animation |
model, firmware, instanceId | The display's model and firmware version, and an id for this playlist entry | |
cacheId | The cache id of the image the display already has | Caching and refresh |
cache | A cache for work you do not want to repeat | Caching and refresh |
assets | The images, animations and fonts your version ships | Images and assets |
images | Decoding a PNG or JPEG you fetched | Data and the network |
secrets | Your app's own keys | Secrets |
auth, credentials | The accounts and tokens people connected | Accounts and credentials |
locations | timezoneAt(lat, lng): the time zone at a place, or null | |
news | getSummary(): a short list of current headlines LEDABLE keeps |
The whole type is RenderContext; what render returns
is RenderResult.
What an app may not do
An app runs in that isolate, and some things that work in Node.js or a browser do not exist there or are refused:
- No host APIs. The bundle runs without Node.js modules, a file system or environment
variables, and the type check that every build runs knows none of them. Files your app needs are
assets, read through
ctx.assets. - Time only from
ctx.now. Do not readDate.now()ornew Date().ctx.nowis the time the platform gives the request, and a test can set it. - Network only to declared hosts.
fetchreaches only the hostsledable.jsonlists innetwork.allowed_hosts, over HTTPS on the default port. Any other request fails in your code with the reason. People see the hosts before they add the app. See Data and the network. - No state between renders that you rely on. Each render may run in a different isolate.
Rules
What the sections above come down to:
renderreads the time only fromctx.now, andpreviewreads no time, network or settings. Tests pin the clock, and the store keeps a version's preview for good.- Images, animations and fonts are files in the project, imported as assets, never pixel data written into code, so the store can check and serve them. Tables of numbers that a computation needs may stay in code.
- Check every response from another service at the boundary, with a schema, and keep its field names as the service sends them. See Data and the network.
- Anything that changes what the app draws needs a new version number, because a published version and its assets never change.
- Everything outside your own code comes through
ctx. - Declare every host the app requests in
network.allowed_hosts; nothing else is reachable.