Commands, settings, SDK functions, anything in the docs.

Build apps

Caching and refresh

Decide how long an image lasts, share work between displays, and skip redrawing.

On this page

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.

current-temperature/index.tsx
    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 cacheId must always mean the same frames, for one version of your app: LEDABLE's servers keep the encoded image under its cacheId for 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: null only when ctx.cacheId equals your cacheId. Otherwise a display with no image gets nothing to show, and when you upload, the store's check render fails.
  • The cacheId travels as an HTTP header, which cannot carry every character. cacheTag(...values) from @ledable/sdk/app turns 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:

current-temperature/index.tsx
      // 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. A Date comes back as a string, and a field set to undefined does not come back.
  • When compute throws, nothing is kept and the error reaches your render, so the next render tries again. A ttlMs of 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.
  • preview has 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:

TSX
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.