Build apps
Caching and refresh
Decide how long an image lasts, share work between displays, and skip redrawing.
A display keeps the image your app sent and asks again when it gets old. Your render decides when that is, can tell a display to keep what it has, and can keep upstream data so that many displays share one request.
How long an image lasts
Two numbers in the render's result set an image's lifetime, in milliseconds from the moment the display receives it:
softTtlMs: once it has passed, the display asks your app again in the background and keeps showing the image until the new one arrives. It is 5 minutes unless you set it.hardTtlMs: once it has passed, the display throws the image away and shows its loading indicator until a new one arrives. It is 10 minutes unless you set it.
A display asks again for every entry of its playlist whose soft TTL has passed, not only for the one on screen, so the soft TTL is how often each display that has your app calls it, all day. Set it to how often your data really changes: a clock needs a new image every minute, a daily quote once a day.
Keep the hard TTL well above the soft TTL. The time between them is how long the display can go on showing your last image while new renders fail: it asks again every 15 seconds and keeps the image until the hard TTL runs out. With a hard TTL at or below the soft TTL, the display drops the image before it asks for the next one and shows the loading indicator in between.
Let the display keep its image
When nothing has changed, a render can answer without an image. Give each image a cacheId, a
string that names what it shows. The display sends the cacheId of the image it has as
ctx.cacheId; when it equals the one you would send, return image: null with the same
cacheId. The display keeps its image and starts its lifetime again with the TTLs of this answer,
and no image is sent.
const fahrenheit = params.unit === "Fahrenheit";
const value = fahrenheit ? celsius * 1.8 + 32 : celsius;
const degrees = `${Math.round(value)}°${fahrenheit ? "F" : "C"}`;
const freshness = {
softTtlMs: 15 * MINUTE_MS,
hardTtlMs: 120 * MINUTE_MS,
};
// The text decides the picture, so it names it: the same id, the same
// frames. A display that already shows it is told to keep it. The id
// travels in an HTTP header, which cannot hold every character of a
// place name, so it is a hash.
const cacheId = await cacheTag(params.place.desc, degrees);
if (ctx.cacheId === cacheId) {
return { image: null, cacheId, ...freshness };
}
const fonts = await loadFonts(ctx.assets, FONT_REFS);
const image = [face(fonts, params.place.desc, degrees)];
return { image, cacheId, ...freshness };- The same
cacheIdmust always mean the same frames, for one version of your app: LEDABLE's servers keep the encoded image under itscacheIdfor up to its hard TTL, at most six hours, and may send it to any display whose render returns that id. Put everything that changes the picture into it, as the example does with the place and the temperature. - Return
image: nullonly whenctx.cacheIdequals yourcacheId. Otherwise a display with no image gets nothing to show, and when you upload, the store's check render fails. - The
cacheIdtravels as an HTTP header, which cannot carry every character.cacheTag(...values)from@ledable/sdk/appturns any values into a hash that can.
realtime: true makes a display that is showing your app switch to a new image as soon as it
arrives. Without it, the display finishes the pass of the animation it is playing first.
Cache what you compute
ctx.cache.get(tag, compute, options) returns what compute returned for the same key earlier,
or calls compute, keeps its value for ttlMs milliseconds and returns it:
// One request per place for every display that shows it: the unit is
// applied when drawing, so it is left out of the cache key.
celsius = await ctx.cache.get(
"current",
async () => ({
value: await currentCelsius(params.place),
ttlMs: 15 * MINUTE_MS,
}),
{ vary: ["place"] },
);The key is made for you, from:
- your app's id and version, so a new version starts with an empty cache;
- the
tag, which names the call within your app: 1 to 64 letters, digits,-and_; - the values of your app's settings, all of them unless you name some in
vary.
By default every combination of settings has entries of its own, so nothing is ever shared
between people with different settings. vary names the settings that change the result, and
displays that agree on those share one entry. A name in vary that is not one of your settings
makes the render fail. In the example the temperature depends on the place
only, so vary: ["place"] makes every display showing Berlin share one request, whatever unit it
shows. vary: [] shares one entry among every display that has your app.
More about what is kept:
- Values go through JSON, except a
Uint8Array, which is kept as bytes. ADatecomes back as a string, and a field set toundefineddoes not come back. - When
computethrows, nothing is kept and the error reaches your render, so the next render tries again. AttlMsof 0 or less keeps nothing. - Entries are kept in each of LEDABLE's data centres separately, so two displays far apart may each cause one request.
previewhas no cache: a preview draws fixed values.
Patterns
Data that is the same for everyone, such as an exchange rate or a list of today's launches:
fetch it with vary: [], or with only the settings it depends on, and draw each display's
picture from it.
Data that belongs to one person, read with their account or credential: keep the default key.
It includes the setting that holds their account, so their data is never served to anyone else.
Never leave that setting out of vary.
Keys that are not settings: tag can be any name you build, such as one entry per day.
cacheTag(...values) hashes any values into a valid tag, which also keeps a value such as a token
out of the key:
const tag = await cacheTag("day", ctx.now.toISOString().slice(0, 10));
const events = await ctx.cache.get(tag, fetchEvents, { vary: [] });Nothing changed: return image: null with an unchanged cacheId, as above, so neither your
app nor the display does the work twice.