<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Edge Apps library 1.4.0 | Screenly</title><description>What has shipped across Screenly, newest first.</description><link>https://www.screenly.io/releases/edge-apps/1.4.0/</link><language>en</language><atom:link href="https://www.screenly.io/releases/edge-apps/1.4.0/rss.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Sat, 01 Aug 2026 00:00:00 +0000</lastBuildDate><item><title>Edge Apps library 1.4.0: Edge Apps can keep showing the last good data when a fetch fails</title><link>https://www.screenly.io/releases/edge-apps/1.4.0/last-known-good-cache/</link><guid isPermaLink="true">https://www.screenly.io/releases/edge-apps/1.4.0/last-known-good-cache/</guid><pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate><category>release</category><category>developers</category><description>&lt;p&gt;A weather app on a lobby screen that loses its API for 10 minutes has 2 choices: show an error to
everyone walking past, or keep showing the last forecast it had. &lt;code&gt;writeEdgeAppCache()&lt;/code&gt; and
&lt;code&gt;readEdgeAppCache()&lt;/code&gt; make the second one a few lines of code:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;button type="button" class="code-copy" aria-label="Copy code" data-copy-code&gt;
&lt;svg data-copy viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;rect x="9" y="9" width="11" height="11" rx="2"&gt;&lt;/rect&gt;
&lt;path d="M5 15V5a2 2 0 0 1 2-2h10"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;svg data-check viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;
&lt;path d="M20 6 9 17l-5-5"&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/button&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d0d0d0;background-color:#202020;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;import&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; getSettingWithDefault,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; readEdgeAppCache,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeEdgeAppCache,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#6ab825;font-weight:bold"&gt;from&lt;/span&gt; &lt;span style="color:#ed9d13"&gt;&amp;#39;@screenly/edge-apps&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6ab825;font-weight:bold"&gt;async&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;function&lt;/span&gt; loadWeather() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;const&lt;/span&gt; response = &lt;span style="color:#6ab825;font-weight:bold"&gt;await&lt;/span&gt; fetch(weatherApiUrl)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;if&lt;/span&gt; (!response.ok) &lt;span style="color:#6ab825;font-weight:bold"&gt;throw&lt;/span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;new&lt;/span&gt; &lt;span style="color:#24909d"&gt;Error&lt;/span&gt;(&lt;span style="color:#ed9d13"&gt;`Weather API returned &lt;/span&gt;&lt;span style="color:#ed9d13"&gt;${&lt;/span&gt;response.status&lt;span style="color:#ed9d13"&gt;}&lt;/span&gt;&lt;span style="color:#ed9d13"&gt;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;const&lt;/span&gt; weather = &lt;span style="color:#6ab825;font-weight:bold"&gt;await&lt;/span&gt; response.json()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; writeEdgeAppCache(&lt;span style="color:#ed9d13"&gt;&amp;#39;weather-app&amp;#39;&lt;/span&gt;, &lt;span style="color:#ed9d13"&gt;&amp;#39;forecast&amp;#39;&lt;/span&gt;, weather)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;return&lt;/span&gt; weather
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#6ab825;font-weight:bold"&gt;catch&lt;/span&gt; (error) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;if&lt;/span&gt; (getSettingWithDefault(&lt;span style="color:#ed9d13"&gt;&amp;#39;display_errors&amp;#39;&lt;/span&gt;, &lt;span style="color:#6ab825;font-weight:bold"&gt;false&lt;/span&gt;)) &lt;span style="color:#6ab825;font-weight:bold"&gt;throw&lt;/span&gt; error
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6ab825;font-weight:bold"&gt;return&lt;/span&gt; readEdgeAppCache(&lt;span style="color:#ed9d13"&gt;&amp;#39;weather-app&amp;#39;&lt;/span&gt;, &lt;span style="color:#ed9d13"&gt;&amp;#39;forecast&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Values are stored in the player&amp;rsquo;s &lt;code&gt;localStorage&lt;/code&gt; as JSON under &lt;code&gt;namespace:key&lt;/code&gt;, so 2 apps, or 2
caches in one app, don&amp;rsquo;t overwrite each other. Anything JSON can hold works as a value.&lt;/p&gt;
&lt;p&gt;The cache is for fallback, not freshness. Nothing expires, so read it only after a real failure,
and expect it to be empty after the player restarts. Both functions fail quietly:
&lt;code&gt;readEdgeAppCache()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; when there&amp;rsquo;s nothing usable, and &lt;code&gt;writeEdgeAppCache()&lt;/code&gt; does
nothing when storage is full or unavailable, so neither needs its own &lt;code&gt;try&lt;/code&gt;.&lt;/p&gt;</description></item></channel></rss>