<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://runspired.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://runspired.com/" rel="alternate" type="text/html" /><updated>2025-05-25T22:02:34+00:00</updated><id>https://runspired.com/feed.xml</id><title type="html">runspired.com</title><subtitle>Personal Log, Stardate 0</subtitle><author><name>Chris Thoburn</name></author><entry><title type="html">In Defense of Machine Exchangeable Formats</title><link href="https://runspired.com/2025/05/25/in-defense-of-machine-formats.html" rel="alternate" type="text/html" title="In Defense of Machine Exchangeable Formats" /><published>2025-05-25T00:00:00+00:00</published><updated>2025-05-25T00:00:00+00:00</updated><id>https://runspired.com/2025/05/25/in-defense-of-machine-formats</id><content type="html" xml:base="https://runspired.com/2025/05/25/in-defense-of-machine-formats.html"><![CDATA[<h1 id="in-defense-of-machine-exchangeable-formats">In Defense of Machine Exchangeable Formats</h1>

<blockquote>
  <p>Or: <strong>Why a custom DSL is the last thing you should add to your project</strong></p>
</blockquote>

<p>I’ve been thinking on DSLs lately and how many of them invert the problem space.</p>

<p>In <a href="https://warp-drive.io"><em>Warp</em><strong>Drive</strong></a>, we chose to remove our custom DSL in favor of JSON, a decision I’ll discuss a bit after detailing some of the underlying thought processes that led to this decision.</p>

<p><img src="../../../assets/images/machine-exchange-format.png" width="100%" alt="Lots of machines all talking with each other" /></p>

<h2 id="stability-of-the-output-is-more-important-than-stability-of-the-input">Stability of the Output is more important than Stability of the Input</h2>

<p>Often DSLs are seen as a way to improve authoring DX and enable a stable syntax overtop a shifting underlying output.</p>

<p>For instance, take an ORM using decorators to define
fields on classes to generate schemas for records.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">hasOne</span><span class="p">,</span> <span class="nx">hasMany</span><span class="p">,</span> <span class="nx">primaryKey</span><span class="p">,</span> <span class="nx">text</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@orm/schema</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">TrackedUpdates</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@orm/traits</span><span class="dl">'</span><span class="p">;</span>

<span class="p">@</span><span class="nd">TrackedUpdates</span>
<span class="kd">class</span> <span class="nx">User</span> <span class="p">{</span>
  <span class="p">@</span><span class="nd">primaryKey</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">uuid-v7</span><span class="dl">'</span> <span class="p">})</span>
  <span class="nx">id</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">text</span> <span class="nx">name</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">hasOne</span><span class="p">(</span><span class="dl">'</span><span class="s1">team</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">fk</span><span class="p">:</span> <span class="dl">'</span><span class="s1">manager</span><span class="dl">'</span> <span class="p">})</span>
  <span class="nx">managedTeam</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">hasOne</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">fk</span><span class="p">:</span> <span class="dl">'</span><span class="s1">reports</span><span class="dl">'</span> <span class="p">})</span>
  <span class="nx">manager</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">hasMany</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">fk</span><span class="p">:</span> <span class="dl">'</span><span class="s1">manager</span><span class="dl">'</span> <span class="p">})</span>
  <span class="nx">reports</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="p">{</span> <span class="nx">User</span> <span class="p">};</span>
</code></pre></div></div>

<p>One reason server-side ORMs approach schemas via DSL is to be able to normalize the input into multiple potential outputs (table schemas for SQLite, PostgreSQL, MySQL, MongoDB etc.).</p>

<p>This works because for a given input + given database there is a stable underlying format the DSL decomposes to such as a flavor of SQL.</p>

<p>If there wasn’t a stable underlying output, this decision on the part of server-side ORMs would be a disaster. It generally is not, because when the ORM’s DSL becomes a point of friction teams can eject and write SQL.</p>

<p>The ability to both <strong>access</strong> and <strong>directly use</strong> the actual format is key.</p>

<p>While DSLs are nice for authoring sugar, if they don’t decompose to an agnostic machine exchangeable format - or they don’t expose that format for direct use - they become too restrictive and present a significant long-term maintenance risk.</p>

<p>As is the order of operations: these custom ORM DSLs only became possible <em>because</em> there were <strong>existing</strong> stable formats for them to decompose into.</p>

<h2 id="great-dsls">Great DSLs</h2>

<p>The crucial point the above discussion is teasing is that the DSL should not be the spec. Stable representations come first and define a spec, and DSLs produce spec compliant output.</p>

<p>This is one of several common patterns that in my experience all great DSLs follow.</p>

<ul>
  <li>they document the underlying representation they use</li>
  <li>they allow access to the underlying representation</li>
  <li>they allow direct use of the underlying representation instead of the DSL when needed</li>
  <li>they use an underlying representation that is broadly exchangeable</li>
  <li>the DSL is not the spec, the underlying representation is</li>
  <li>there underlying representation can be produced by any number of DSLs, its not specific to one.</li>
</ul>

<p>Note how none of these is at all about the authoring DX the DSL provides: it doesn’t matter how great the authoring experience is if its also an obstacle.</p>

<p>Which brings me to my next point..</p>

<h2 id="authoring-dx-is-sometimes-vastly-overrated">Authoring DX Is (sometimes) Vastly Overrated</h2>

<p>DSLs work best when they simplify something done regularly.</p>

<p>Defining a schema for a table in your database isn’t something done regularly, as compared to say GraphQL queries where you might find yourself writing several new queries every day.</p>

<p>I see a lot of projects create custom DSLs for something done once as part of configuration and then rarely touched. To me, this seems bad.</p>

<p>I’ve also seen a lot of developers evaluate a tool or library based on whether they like the DSL for the configuration thing they will do once. This also seems bad.</p>

<p>This isn’t to say a great DSLs can’t make it easier to get started configuring a project - they absolutely can - just that the human addiction to sugar shouldn’t be trusted. Sugar is sweet, but it can also kill you over time.</p>

<h2 id="embracing-json">Embracing JSON</h2>

<p>Handy-wavy over-simplification, <em>Warp</em><strong>Drive</strong> is a library intended for use in web clients that combines a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">fetch</a> pipeline with ORM-like capabilities for relational data. If you are unfamiliar, the project <a href="https://docs.warp-drive.io/guide/">introduction</a> gives a better synopsis.</p>

<p>This means <em>Warp</em><strong>Drive</strong> encounters two of the most common areas in which DSLs arise:</p>

<ul>
  <li>request building (think sql generators in your ORM of choice, or GraphQL queries)</li>
  <li>resource schemas</li>
</ul>

<p>While these are the same problems server ORMs must often solve, there is a crucial distinction to make.</p>

<p><em>Warp</em><strong>Drive</strong> is a client-side library. Requests aren’t constructing SQL they are constructing messages to be sent via http or websocket. And since there’s no underlying database there’s no underlying SQL format that schemas decompose to and which you could eject and use.</p>

<p>If <em>Warp</em><strong>Drive</strong> wants to give developers the same escape valves to <em>just use the platform</em> it MUST similarly expose the ability for users to interact with the actual underlying representation when the DSL doesn’t cut it.</p>

<p>In both of these areas <em>Warp</em><strong>Drive</strong>’s predecessor <em>Ember</em><strong>Data</strong> used to provide a DSL</p>

<ul>
  <li>Methods such as <code class="language-plaintext highlighter-rouge">store.findRecord</code> that would magically build a request for your API</li>
  <li>Models that were used to provide resource schemas</li>
</ul>

<p>Both of these DSLs repeatedly proved to be overly restrictive. They both decomposed to JSON, but that underlying representation was neither accessible nor directly usable.</p>

<p>Eventually escape hatches were built, but they felt like clumsy work arounds because they in fact were: for instance <code class="language-plaintext highlighter-rouge">adapterOptions</code> as a way to channel arbitrary information through to adapters on requests to enable folks to eject and write their own behaviors.</p>

<p>This is ultimately why schemas in <a href="https://warp-drive.io"><em>Warp</em><strong>Drive</strong></a> ejected from the more common ORM pattern of using decorators on classes. We were decomposing to JSON anyway, and by making the exchangeable format the spec we ensure greater flexibility and easier maintenance.</p>

<p>It is also why <code class="language-plaintext highlighter-rouge">store.request</code> just takes a JSON object following the <code class="language-plaintext highlighter-rouge">RequestInit</code> interface with minimal additional decoration for <em>Warp</em><strong>Drive</strong> specific behaviors.</p>

<p>For both of these APIs, folks that liked the DSL approach have complained about authoring in JSON. Generally, if you liked the prior DSLs it meant your usage fell in a very narrow range that didn’t hit the friction of their restrictive nature.</p>

<p>This isn’t to say we couldn’t have a DSL for these things. In fact we will likely ship one eventually,
both for requests and for schemas. We had one planned for schemas early in the development of our new reactive objects, but opted against in the interest of time and because we found it added little value.</p>

<p>Why little value?</p>

<p>Because creating resource schemas is something you don’t do regularly. But also because the JSON schemas are fully typed which results in both editor autocomplete and AI prompt completions assisting you far more easily than they could in the class approach.</p>

<p>This is beside the point though, DSLs are still allowed. It’s just important that the underlying representation is JSON and is directly usable. By doing this, we’ve set up <em>Warp</em><strong>Drive</strong> for greater short term and long term success.</p>

<p>Say (for instance) that you wanted to use GraphQL with WarpDrive. To make this happen you need three things:</p>

<ul>
  <li>for the template-tag function implementation to produce a valid <code class="language-plaintext highlighter-rouge">RequestInit</code> (and ideally type!)</li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">store</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">gql</span><span class="s2">`{
    user(id: 5) {
      firstName
      lastName
    }
}`</span><span class="p">);</span>
</code></pre></div></div>

<ul>
  <li>for the parsed query to provide resource schemas for the contained data that can be loaded into the store’s schema service</li>
  <li>a request handler that normalizes the GraphQL response into a more cacheable format (this sounds harder than it is)</li>
</ul>

<p>In the GraphQL example, GraphQL is serving as both the schema and the request DSL. Because <em>Warp</em><strong>Drive</strong> operates on a JSON representation this DSL can easily decompose to, integration with GraphQL doesn’t require the library itself to know anything about GraphQL. Imagine trying to bolt this onto Models…</p>

<p>And the same goes for tons of other DSLs and mechanisms of producing schemas. What happens when you want to supply your data schemas from SQL table schemas or OpenAPI specs, or compile them from types, or deliver them on-demand from your API to ensure they are always consistent with what your API is providing?</p>

<p>While its likely that <em>Warp</em><strong>Drive</strong> will eventually ship with one or more DSLs for requests and schemas, this architecture ensures flexibility and maintainability over time.</p>

<p>A custom DSL is the last thing you should add to your project. Some folks might interpret this to mean you shouldn’t use a DSL or should never add a DSL. That isn’t what I’m saying. DSLs are like optimizing the last 100ft of a delivery coming from a hundred miles away. It matters, but the spec and underlying representation need to be solid first.</p>

<h2 id="a-parting-thought-on-mcp-in-the-context-of-dsls">A Parting Thought on MCP in the Context of DSLs</h2>

<p>My suspicion is that the Model Context Protocol is shaping up to be a great DSL. Not based on usage or rigourous study of the specification, but because it chose to explicitly define the underlying representation as JSON, following a schema, and communicated via json-rpc.</p>

<p>There are those who wonder if HTML or XML wouldn’t have been a better choice. They may be right in some ways, but personally I’ve found that working with structured JSON is a better choice due to the ease with which tooling supports consuming JSON across languages and ecosystems and in how JSON maps close-enough to the object instances a language will parse it into.</p>

<p>MCP follows most of the patterns that I feel lead to great DSLs, so I see no reason why it couldn’t become one (or one couldn’t develop for it). I am far less worried around the speed of its development given these things than the speed at which the JavaScript language was invented.</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[In Defense of Machine Exchangeable Formats]]></summary></entry><entry><title type="html">Exploring Advanced Request Handlers in WarpDrive</title><link href="https://runspired.com/2025/02/26/exploring-advanced-handlers.html" rel="alternate" type="text/html" title="Exploring Advanced Request Handlers in WarpDrive" /><published>2025-02-26T00:00:00+00:00</published><updated>2025-02-26T00:00:00+00:00</updated><id>https://runspired.com/2025/02/26/exploring-advanced-handlers</id><content type="html" xml:base="https://runspired.com/2025/02/26/exploring-advanced-handlers.html"><![CDATA[<h1 id="exploring-advanced-request-handlers-in-warpdrive">Exploring Advanced Request Handlers in WarpDrive</h1>

<p><a href="https://warp-drive.io">WarpDrive</a> provides applications with a managed fetch pipeline
following the chain-of-responsibility principle. When a request is issued it triggers the
registered pipline.</p>

<p>Pipelines can be simple, for instance below is the setup for a pipeline that just calls
fetch with minimal processing.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">manager</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">RequestManager</span><span class="p">()</span>
  <span class="p">.</span><span class="nx">use</span><span class="p">([</span><span class="nx">Fetch</span><span class="p">]);</span>
</code></pre></div></div>

<blockquote>
  <p><strong>TIP</strong></p>

  <p>For a good introduction to managed fetch in WarpDrive, read <a href="https://runspired.com/2024/01/31/modern-ember-data.html">this post</a> about modern request paradigms in EmberData (WarpDrive is the modern evolution of EmberData).</p>
</blockquote>

<p>Most often, handlers should be simple. Providing minimal decoration for universal responsibilities
like setting up authentication tokens or adding tracing for observability tooling.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">API_ROOT</span> <span class="o">=</span> <span class="s2">`https://</span><span class="p">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">hostname</span><span class="p">}</span><span class="s2">/api/`</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">isOwnAPI</span><span class="p">(</span><span class="nx">url</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">url</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="nx">API_ROOT</span><span class="p">)</span> <span class="o">||</span> <span class="nx">url</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">decorateOwnRequest</span><span class="p">(</span><span class="nx">session</span><span class="p">,</span> <span class="nx">request</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">);</span>
  <span class="nx">headers</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">X-Amzn-Trace-Id</span><span class="dl">'</span><span class="p">,</span> <span class="s2">`Root=</span><span class="p">${</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">randomUUID</span><span class="p">()}</span><span class="s2">`</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">session</span><span class="p">?.</span><span class="nx">token</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">headers</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">token</span><span class="dl">'</span><span class="p">,</span> <span class="nx">token</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">request</span><span class="p">,</span> <span class="p">{</span> <span class="nx">headers</span> <span class="p">});</span>
<span class="p">}</span>


<span class="k">export</span> <span class="kd">class</span> <span class="nx">AuthHandler</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">(</span><span class="nx">sessionService</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">session</span> <span class="o">=</span> <span class="nx">sessionService</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">request</span><span class="p">({</span> <span class="nx">request</span> <span class="p">},</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span> <span class="o">||</span> <span class="o">!</span><span class="nx">isOwnAPI</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">decorateOwnRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">));</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In even this simple example, we can start to see the rumblings of why handlers can be advanced:</p>

<ul>
  <li>Requests passed to handlers are immutable, and so <code class="language-plaintext highlighter-rouge">decorateOwnRequest</code> generates a new request
from the original to apply the additional headers.</li>
  <li>requests have the ability to see the result of calling <code class="language-plaintext highlighter-rouge">next</code> (this is why we return the result of the call).</li>
  <li>next can be called with a different request than the handler received</li>
</ul>

<p>There’s a fourth feature of handlers thats less obvious: a handler can call <code class="language-plaintext highlighter-rouge">next</code> as many times
as it wants, but it can only return a single response.</p>

<p>These four features – immutability, access to the response, ability to pass along a different request,
and the ability to call <code class="language-plaintext highlighter-rouge">next</code> any number of times – work together to allow applications to build highly
advanced handlers to  fulfill requests however they best see fit.</p>

<p>Below, lets dive into two such advanced handlers that I recently worked on: a <code class="language-plaintext highlighter-rouge">pagination engine</code> and
a <code class="language-plaintext highlighter-rouge">recommendation engine</code>.</p>

<h2 id="implementing-a-pagination-engine">Implementing a Pagination Engine</h2>

<p>When using <a href="https://jsonapi.org/format/#fetching-pagination">{JSON:API}</a>, pagination is implemented using
links at the top level of the response which describe where to get other pages of data related to the
current response.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com/api/users"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"prev"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"next"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com/api/users?page[offset]=25&amp;page[limit]=25"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"first"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com/api/users?page[offset]=0&amp;page[limit]=25"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"last"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://example.com/api/users?page[offset]=75&amp;page[limit]=25"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="err">//</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="err">list</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="err">users</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="err">//</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>WarpDrive adopted this convention as part of its Cache API. When a cache receives a request, it returns a
“response document” that describes what it found in the content for that request. While the cache does not
need to be in <a href="https://jsonapi.org/format/#fetching-pagination">{JSON:API}</a> format, this response document
needs to be in a universal format and so WarpDrive adopted the top-level structure from <a href="https://jsonapi.org/format/#fetching-pagination">{JSON:API}</a>
to use due to its expressiveness.</p>

<p>When the WarpDrive Store creates a reactive wrapper for this response document, it exposes utility methods
for working with the request. For instance, to fetch the next page, we can call <code class="language-plaintext highlighter-rouge">next</code></p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="na">content</span><span class="p">:</span> <span class="nx">currentPage</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">store</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span> <span class="na">url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/api/users</span><span class="dl">'</span> <span class="p">});</span>
<span class="kd">const</span> <span class="p">{</span> <span class="na">content</span><span class="p">:</span> <span class="nx">nextPage</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">currentPage</span><span class="p">.</span><span class="nx">next</span><span class="p">();</span>
</code></pre></div></div>

<p>This feature allows applications to construct advanced pagination logic in a highly conventional
manner by abstracting the mechanics of pagination as an unimportant detail of the underlying request,
but it seemingly breaks down if an API does not provide pagination links 🙈</p>

<p>Handlers to the rescue!</p>

<p>Our PaginationEngine is going to process our requests for two separate scenarios:</p>

<ul>
  <li>GET based pagination using queryParams</li>
  <li>POST based pagination using params in the body (for QUERY via POST semantics)</li>
</ul>

<p>For this example, lets assume our <code class="language-plaintext highlighter-rouge">GET</code> endpoints (e.g. <code class="language-plaintext highlighter-rouge">GET /api/users</code>) accept
the params <code class="language-plaintext highlighter-rouge">limit</code> and <code class="language-plaintext highlighter-rouge">offset</code> and in the response meta we receive back this
limit, offset and total:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nl">meta</span><span class="p">:</span> <span class="p">{</span> <span class="na">total</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">limit</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">offset</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span>
</code></pre></div></div>

<p>Lets assume our <code class="language-plaintext highlighter-rouge">POST</code> endpoints (e.g. <code class="language-plaintext highlighter-rouge">POST /api/users</code>) accepts the same <code class="language-plaintext highlighter-rouge">limit</code> and <code class="language-plaintext highlighter-rouge">offset</code>
top level, e.g.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/users</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
  <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="na">limit</span><span class="p">:</span> <span class="mi">25</span><span class="p">,</span> <span class="na">offset</span><span class="p">:</span> <span class="mi">0</span> <span class="p">})</span>
<span class="p">}).</span><span class="nx">toJSON</span><span class="p">();</span>
</code></pre></div></div>

<p>In both cases, we assume that other parameters unrelated to pagination may be present. Let’s dive
into a implementing the simpler <code class="language-plaintext highlighter-rouge">GET</code> case first.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">API_ROOT</span> <span class="o">=</span> <span class="s2">`https://</span><span class="p">${</span><span class="nx">location</span><span class="p">.</span><span class="nx">hostname</span><span class="p">}</span><span class="s2">/api/`</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">isOwnAPI</span><span class="p">(</span><span class="nx">url</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">boolean</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">url</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="nx">API_ROOT</span><span class="p">)</span> <span class="o">||</span> <span class="nx">url</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">maybeAddPaginationLinks</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">doc</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">doc</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">meta</span><span class="p">?.</span><span class="nx">total</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">links</span> <span class="o">=</span> <span class="nx">doc</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">links</span> <span class="o">=</span> <span class="nx">doc</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">links</span> <span class="o">??</span> <span class="p">{};</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">limit</span><span class="p">,</span> <span class="nx">offset</span><span class="p">,</span> <span class="nx">total</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">doc</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">meta</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">originalUrl</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Url</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
    
    <span class="c1">// add current</span>
    <span class="nx">originalUrl</span><span class="p">.</span><span class="nx">queryParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">limit</span><span class="dl">'</span><span class="p">,</span> <span class="nx">limit</span><span class="p">);</span>
    <span class="nx">originalUrl</span><span class="p">.</span><span class="nx">queryParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">offset</span><span class="dl">'</span><span class="p">,</span> <span class="nx">offset</span><span class="p">);</span>
    <span class="nx">links</span><span class="p">.</span><span class="nb">self</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">originalUrl</span><span class="p">);</span>

    <span class="c1">// add first</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">offset</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">links</span><span class="p">.</span><span class="nx">first</span> <span class="o">=</span> <span class="nx">links</span><span class="p">.</span><span class="nb">self</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">originalUrl</span><span class="p">.</span><span class="nx">queryParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">offset</span><span class="dl">'</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
      <span class="nx">links</span><span class="p">.</span><span class="nx">first</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">originalUrl</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// add prev</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">offset</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">originalUrl</span><span class="p">.</span><span class="nx">queryParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">offset</span><span class="dl">'</span><span class="p">,</span> <span class="nx">offset</span> <span class="o">-</span> <span class="nx">limit</span><span class="p">);</span>
      <span class="nx">links</span><span class="p">.</span><span class="nx">prev</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">originalUrl</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">links</span><span class="p">.</span><span class="nx">prev</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="c1">// add next</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">offset</span> <span class="o">+</span> <span class="nx">limit</span> <span class="o">&lt;</span> <span class="nx">total</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">originalUrl</span><span class="p">.</span><span class="nx">queryParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">offset</span><span class="dl">'</span><span class="p">,</span> <span class="nx">offset</span> <span class="o">+</span> <span class="nx">limit</span><span class="p">);</span>
      <span class="nx">links</span><span class="p">.</span><span class="nx">next</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">originalUrl</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="nx">links</span><span class="p">.</span><span class="nx">next</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// add last</span>
    <span class="kd">const</span> <span class="nx">lastOffset</span> <span class="o">=</span> <span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="mi">51</span> <span class="o">/</span> <span class="mi">25</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="nx">limit</span><span class="p">;</span>
    <span class="nx">originalUrl</span><span class="p">.</span><span class="nx">queryParams</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">offset</span><span class="dl">'</span><span class="p">,</span> <span class="nx">lastOffset</span><span class="p">);</span>
    <span class="nx">links</span><span class="p">.</span><span class="nx">last</span> <span class="o">=</span> <span class="nb">String</span><span class="p">(</span><span class="nx">originalUrl</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="nx">doc</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">const</span> <span class="nx">BasicPaginationHandler</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">request</span><span class="p">({</span> <span class="nx">request</span> <span class="p">},</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span> <span class="o">||</span> <span class="o">!</span><span class="nx">isOwnAPI</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span>
      <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">doc</span> <span class="o">=&gt;</span> <span class="nx">maybeAddPaginationLinks</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">doc</span><span class="p">));</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And that’s it! Now our response has the full range of links it can use for pagination
features derived from our params based contract. In many cases, an API may not provide
access to meta like this, but we can use limit and offset information from the original
URL, and provide <code class="language-plaintext highlighter-rouge">null</code> for the <code class="language-plaintext highlighter-rouge">last</code> link, allowing cursor based navigation via next/prev
as well as a return to start, which for most features is more than enough.</p>

<p>But what happens when our endpoint requires the use of <code class="language-plaintext highlighter-rouge">POST</code> ? Let’s explore!</p>

<p>For this challenge, we are going to take advantage of the fact that the <em>first</em> request
issued by the APP is issued directly (e.g. not via <code class="language-plaintext highlighter-rouge">await currentPage.next()</code> or similar
but via <code class="language-plaintext highlighter-rouge">store.request({ url: '/users', method: 'POST', body })</code>).</p>

<p>This gives us a key distinction to make when handling a request</p>

<ul>
  <li>requests that are the <em>first</em> in a series</li>
  <li>requests that are a <em>continuation</em> of a series</li>
</ul>

<p>In this approach we’ll want some state for book-keeping so we are going to implement a class
for our handler and store a Map that holds on to a bit of info from first requests that
we will need for a continuation request.</p>

<p>The general strategy is that we generate a link (really just a unique string) that the app can
use to make a <code class="language-plaintext highlighter-rouge">GET</code> request conceptually that works with <code class="language-plaintext highlighter-rouge">await page.next()</code>, but switch the
request out for a <code class="language-plaintext highlighter-rouge">POST</code> request when we encounter the generated link in our handler.</p>

<p>We also need a way to know that a request wants to opt into this pagination behavior,
there’s a number of heuristics we could use but for this example we’re going to be
explicit and pass an option into the initial request that tells us to make use of this
feature.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">store</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
  <span class="na">url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/api/users</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="na">limit</span><span class="p">:</span> <span class="mi">25</span><span class="p">,</span> <span class="na">offset</span><span class="p">:</span> <span class="mi">0</span> <span class="p">}),</span>
  <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">usePaginationEngine</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">QUERYPaginationEngine</span> <span class="p">{</span>
  <span class="nx">urlMap</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">();</span>

  <span class="nx">request</span><span class="p">({</span> <span class="nx">request</span> <span class="p">},</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span> <span class="o">||</span> <span class="o">!</span><span class="nx">isOwnAPI</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
    <span class="p">}</span>
  
    <span class="k">if</span> <span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">options</span><span class="p">?.</span><span class="nx">usePaginationEngine</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">handleInitialRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">urlMap</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">urlMap</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">handleContinuationRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">urlMap</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Above, if the request targets our own API and has requested to use the
pagination engine, we handle the request as an “initial” request.</p>

<p>If the url is a url in our url map, we handle it as a “continuation request”,
else we pass along this request since we aren’t being asked to handle it.</p>

<p>For this blog post I am only going to implement <code class="language-plaintext highlighter-rouge">next</code> link behavior, though
as with the first <code class="language-plaintext highlighter-rouge">GET</code> example above we can follow this pattern for every
pagination link we may want.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">function</span> <span class="nx">handleInitialRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="nx">urlMap</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">method</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span>
      <span class="s2">`The PaginationEngine handler expects usePaginationEngine to only be used with POST requests`</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="nx">handleRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="nx">urlMap</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">handleRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="nx">urlMap</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">identifier</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">identifierCache</span><span class="p">.</span><span class="nx">getOrCreateDocumentIdentifier</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">identifier</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span>
      <span class="dl">'</span><span class="s1">The PaginationEngine handler expects the request to utilize a cache-key, but none was provided</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">nextLink</span> <span class="o">=</span> <span class="s2">`{@psuedo-link:next}//</span><span class="p">${</span><span class="nx">identifier</span><span class="p">.</span><span class="nx">lid</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">links</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">links</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">.</span><span class="nx">links</span> <span class="o">??</span> <span class="p">{};</span>
  <span class="nx">links</span><span class="p">.</span><span class="nx">next</span> <span class="o">=</span> <span class="nx">nextLink</span><span class="p">;</span>
  <span class="nx">urlMap</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">nextLink</span><span class="p">,</span> <span class="nx">request</span><span class="p">);</span>

  <span class="k">return</span> <span class="nx">response</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Above, we create a fake url for our “next” link by generating a string that roughly says “I represent the next link for the quest with the following cache-key”. Then we key the original request in our map to that
link and move on. (Sidenote: if you want this handler to work with the PersistedCache experiment we will
also need to setup a way to restore this map from cache, this is left for discussion at another time.)</p>

<p>When the app calls <code class="language-plaintext highlighter-rouge">await page.next()</code>, it will effectively generate the request:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">store</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
  <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">url</span><span class="p">:</span> <span class="s2">`{@psuedo-link:next}//</span><span class="p">${</span><span class="nx">identifier</span><span class="p">.</span><span class="nx">lid</span><span class="p">}</span><span class="s2">`</span>
<span class="p">})</span>
</code></pre></div></div>

<p>When we see this link in the handler, we invoke <code class="language-plaintext highlighter-rouge">handleContinuationRequest</code>.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">handleContinuationRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="nx">urlMap</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">requestInfo</span> <span class="o">=</span> <span class="nx">urlMap</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">upgradedRequest</span> <span class="o">=</span> <span class="nx">buildPostRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">requestInfo</span><span class="p">);</span>

  <span class="k">return</span> <span class="nx">handleRequest</span><span class="p">(</span><span class="nx">upgradedRequest</span><span class="p">,</span> <span class="nx">next</span><span class="p">,</span> <span class="nx">urlMap</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">buildPostRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">:</span> <span class="nx">StoreRequest</span><span class="p">,</span> <span class="nx">parentRequest</span><span class="p">:</span> <span class="nx">PaginatedRequestInfo</span><span class="p">):</span> <span class="nx">StoreRequest</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">url</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">parentRequest</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">overrides</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nx">unknown</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">url</span><span class="p">,</span>
    <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">};</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">parentRequest</span><span class="p">.</span><span class="nx">headers</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">headers</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">(</span><span class="nx">parentRequest</span><span class="p">.</span><span class="nx">headers</span><span class="p">);</span>
    <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">?.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">value</span><span class="p">,</span> <span class="nx">key</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">headers</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">));</span>
    <span class="nx">overrides</span><span class="p">.</span><span class="nx">headers</span> <span class="o">=</span> <span class="nx">headers</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">// update the offset</span>
  <span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">parentRequest</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
  <span class="nx">body</span><span class="p">.</span><span class="nx">offset</span> <span class="o">+=</span> <span class="nx">body</span><span class="p">.</span><span class="nx">limit</span><span class="p">;</span>
  <span class="nx">overrides</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">body</span><span class="p">);</span>

  <span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">request</span><span class="p">,</span> <span class="nx">overrides</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And there we have it, our POST based API convention now plays along seamlessly with the
simpler mental model of calling <code class="language-plaintext highlighter-rouge">await page.next()</code> on any collection.</p>

<h2 id="implementing-a-recommendation-engine">Implementing a Recommendation Engine</h2>

<p>On the surface, it seems like WarpDrive’s managed fetch pipeline treats requests as 1:1
with a call to an API endpoint or a query on some other source (like a local database).</p>

<p>But the reality is more nuanced: WarpDrive thinks about requests as 1:1 with a response.</p>

<p>Catch that? The nuance is very subtle. Possibly imperceptible.</p>

<p>It helps to explore this via a real world example.</p>

<p>Lets say you want to request options for a select, and in that list of options you also
want to include a few “recommended” options near the top that based on some criteria are
more likely to be what the user is searching for.</p>

<p>The naive way of implementing this is as two requests, one for options (perhaps filtered
and sorted) and another for recommendations. Then in the app we splice the two arrays
together and pass them into our select component.</p>

<p>Its not bad, but it is often a lot of work to transform the shapes of the two responses into
the same shape, and doing so defeats much of the power of using reactive cache-driven resources.</p>

<p>Taking a step back though we see that while we have two requests, conceptually we really have
just one “request” for options with recommendations and one “response” or result being the
merger of these.</p>

<p>Lets implement a handler that understands this requirement, and does so in a way that any
request can make a “recommendations” request alongside the “options” request.</p>

<p>For this example, we’ll assume we have two endpoints:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">POST /api/recommendations</code> which returns recommendations for resources of type <code class="language-plaintext highlighter-rouge">&lt;entity&gt;</code> in <a href="https://jsonapi.org/format/#fetching-pagination">{JSON:API}</a> format.</li>
  <li><code class="language-plaintext highlighter-rouge">GET /api/&lt;entity&gt;</code> which returns a page of results of resource type <code class="language-plaintext highlighter-rouge">&lt;entity&gt;</code> in <a href="https://jsonapi.org/format/#fetching-pagination">{JSON:API}</a> format</li>
</ul>

<p>When we receive a request that wants to also fetch recommendations, we’ll issue two
requests and “mux” (combine) the response.</p>

<p>Unlike above where we used <code class="language-plaintext highlighter-rouge">options</code> to pass the <code class="language-plaintext highlighter-rouge">usePaginationEngine</code> flag to the handler
to activate it, here we are going to use a header (<code class="language-plaintext highlighter-rouge">X-Include-Recommendations</code>) and check
for the value <code class="language-plaintext highlighter-rouge">'fetch'</code>. We’re implementing it this way so that later we can refactor our
API itself to accept this header as a way to know call our recommendations service to
generate recommendations for the request and include them in the response.</p>

<p>In this way, handlers can function to ponyfill how we want our API to behave even
before we’ve had the time or ability to do so.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">shouldMakeRecommendationsRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">request</span><span class="p">.</span><span class="nx">headers</span><span class="p">?.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">X-Include-Recommendations</span><span class="dl">'</span><span class="p">)</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">fetch</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">invokeAndProcessMuxSuccess</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// ... implemented below later ... //</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">RecommendationsHandler</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">request</span><span class="p">({</span> <span class="nx">request</span> <span class="p">},</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span> <span class="o">||</span> <span class="o">!</span><span class="nx">isOwnAPI</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">shouldMakeRecommendationsRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">invokeAndProcessMuxSuccess</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">next</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In order to split our request into two requests we need a bit of extra data for
the body of the request to send to our recommendations service. For this we will
use <code class="language-plaintext highlighter-rouge">request.data.recommendations</code> to provide the additional options.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">invokeAndProcessMuxSuccess</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span><span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">recommendationsRequest</span> <span class="o">=</span> <span class="nx">buildRecommendationsRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">dataPromise</span> <span class="o">=</span> <span class="nx">next</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">recPromise</span> <span class="o">=</span> <span class="nx">next</span><span class="p">(</span><span class="nx">recommendationsRequest</span><span class="p">);</span>

  <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">allSettled</span><span class="p">([</span><span class="nx">recPromise</span><span class="p">,</span> <span class="nx">dataPromise</span><span class="p">]).</span><span class="nx">then</span><span class="p">(</span>
    <span class="p">([</span><span class="nx">recs</span><span class="p">,</span> <span class="nx">data</span><span class="p">])</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="c1">// if data request errors, we error</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">rejected</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="nx">data</span><span class="p">.</span><span class="nx">reason</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="c1">// if ML request errors, we return data</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">recs</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">rejected</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">data</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
      <span class="p">}</span>

      <span class="c1">// if both succeed, we combine them into one response</span>
      <span class="k">return</span> <span class="nx">combineWithResult</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">content</span><span class="p">,</span> <span class="nx">recs</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">content</span><span class="p">);</span>
    <span class="p">},</span>
  <span class="p">);</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">buildRecommendationsRequest</span><span class="p">(</span><span class="nx">request</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span>
    <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({</span> <span class="na">limit</span><span class="p">:</span> <span class="mi">5</span> <span class="p">},</span> <span class="nx">request</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">recommendations</span><span class="p">)</span>
  <span class="p">);</span>
  
  <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">buildBaseURL</span><span class="p">({</span> <span class="na">resourcePath</span><span class="p">:</span> <span class="dl">'</span><span class="s1">recommendations/match</span><span class="dl">'</span> <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">cacheKey</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">::</span><span class="p">${</span><span class="nx">body</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="nx">url</span><span class="p">,</span>
    <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">key</span><span class="p">:</span> <span class="nx">cacheKey</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="na">headers</span><span class="p">:</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">({</span>
      <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">Accept</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/vnd.api+json</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">}),</span>
    <span class="nx">body</span><span class="p">,</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">combineWithResult</span><span class="p">(</span><span class="nx">primaryData</span><span class="p">,</span> <span class="nx">recs</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">meta</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">primaryData</span><span class="p">.</span><span class="nx">meta</span><span class="p">,</span> <span class="p">{</span> <span class="na">ml</span><span class="p">:</span> <span class="nx">recs</span><span class="p">.</span><span class="nx">meta</span> <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">included</span> <span class="o">=</span> <span class="nx">primaryData</span><span class="p">.</span><span class="nx">included</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="nx">recs</span><span class="p">.</span><span class="nx">included</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">recs</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="nx">primaryData</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">links</span> <span class="o">=</span> <span class="nx">primaryData</span><span class="p">.</span><span class="nx">links</span> <span class="o">??</span> <span class="p">{};</span>

  <span class="k">return</span> <span class="p">{</span>
    <span class="nx">meta</span><span class="p">,</span>
    <span class="nx">links</span><span class="p">,</span>
    <span class="nx">data</span><span class="p">,</span>
    <span class="nx">included</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And finally, what does using this look like in practice?</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">store</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
  <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GET</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">url</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/api/users</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">headers</span><span class="p">:</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">({</span>
    <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">Accept</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/vnd.api+json</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">X-Include-Recommendations</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">fetch</span><span class="dl">'</span>
  <span class="p">}),</span>
  <span class="na">data</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">recommendations</span><span class="p">:</span> <span class="p">{</span>
      <span class="c1">// various inputs for generating the recommendations</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">limit</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
      <span class="na">context</span><span class="p">:</span> <span class="dl">'</span><span class="s1">control-tester</span><span class="dl">'</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Now, lets sugar this!</p>

<p>Builders are great for sugar because on top of enabling easy reuse of common requests
they can compose with each other to decorate requests.</p>

<p>For the request above, we might use the <code class="language-plaintext highlighter-rouge">query('user')</code> as our base.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">query</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/json-api/request</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<p>Then write a simple builder that decorates our query requests:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">withRecommendations</span><span class="p">(</span><span class="nx">queryInit</span><span class="p">,</span> <span class="nx">payload</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">queryInit</span><span class="p">.</span><span class="nx">headers</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="dl">'</span><span class="s1">X-Include-Recommendations</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">fetch</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">queryInit</span><span class="p">.</span><span class="nx">data</span> <span class="o">=</span> <span class="nx">queryInit</span><span class="p">.</span><span class="nx">data</span> <span class="o">||</span> <span class="p">{};</span>
  <span class="nx">queryInit</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">recommendations</span> <span class="o">=</span> <span class="nx">payload</span><span class="p">;</span>

  <span class="k">return</span> <span class="nx">queryInit</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then to use it:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">store</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">withRecommendations</span><span class="p">(</span><span class="nx">query</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">),</span> <span class="p">{</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">limit</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
  <span class="na">context</span><span class="p">:</span> <span class="dl">'</span><span class="s1">control-tester</span><span class="dl">'</span>
<span class="p">}));</span>
</code></pre></div></div>

<p>Enjoy!</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[Exploring Advanced Request Handlers in WarpDrive]]></summary></entry><entry><title type="html">Exploring transformed and derived values in @warp-drive/schema-record</title><link href="https://runspired.com/2025/02/06/exploring-transformed-and-derivied-values-in-schema-record.html" rel="alternate" type="text/html" title="Exploring transformed and derived values in @warp-drive/schema-record" /><published>2025-02-06T00:00:00+00:00</published><updated>2025-02-06T00:00:00+00:00</updated><id>https://runspired.com/2025/02/06/exploring-transformed-and-derivied-values-in-schema-record</id><content type="html" xml:base="https://runspired.com/2025/02/06/exploring-transformed-and-derivied-values-in-schema-record.html"><![CDATA[<h1 id="exploring-transformed-and-derived-values-in-warp-driveschema-record">Exploring transformed and derived values in @warp-drive/schema-record</h1>

<p>With <a href="https://github.com/emberjs/data/tree/main/packages/schema-record#readme">@warp-drive/schema-record</a> approaching its first stable release, now felt like as good a time as any to start writing about some of the key differences from <a href="https://github.com/emberjs/data/blob/main/packages/model/README.md">@ember-data/model</a> in its approach to reactive-data.</p>

<p>The first thing most will notice is that the authoring format has changed from javascript classes to json schemas (and optionally types).</p>

<p><strong>Model (before)</strong></p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">cached</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@glimmer/tracking</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Model</span><span class="p">,</span> <span class="p">{</span> <span class="nx">attr</span><span class="p">,</span> <span class="nx">belongsTo</span><span class="p">,</span> <span class="nx">hasMany</span><span class="p">,</span> <span class="kd">type</span> <span class="nx">HasMany</span><span class="p">,</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/model</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="nx">User</span> <span class="kd">extends</span> <span class="nx">Model</span> <span class="p">{</span>
  <span class="p">@</span><span class="nd">attr</span><span class="p">(</span><span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">)</span> <span class="kr">declare</span> <span class="nx">firstName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="p">@</span><span class="nd">attr</span><span class="p">(</span><span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">)</span> <span class="kr">declare</span> <span class="nx">lastName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">hasMany</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">inverse</span><span class="p">:</span> <span class="dl">'</span><span class="s1">friends</span><span class="dl">'</span> <span class="p">})</span>
  <span class="kr">declare</span> <span class="nx">friends</span><span class="p">:</span> <span class="nx">HasMany</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">belongsTo</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">inverse</span><span class="p">:</span> <span class="kc">null</span> <span class="p">})</span>
  <span class="kr">declare</span> <span class="nx">favoritePerson</span><span class="p">:</span> <span class="nx">User</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">cached</span>
  <span class="kd">get</span> <span class="nx">fullName</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">firstName</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">lastName</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Schema (after)</strong></p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">UserSchema</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">identity</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">@id</span><span class="dl">'</span> <span class="p">},</span>
  <span class="na">fields</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">firstName</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">lastName</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span> <span class="p">},</span>
    <span class="p">{</span> 
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">friends</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">collection</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">inverse</span><span class="p">:</span> <span class="dl">'</span><span class="s1">friends</span><span class="dl">'</span> <span class="p">}</span>
    <span class="p">},</span>
    <span class="p">{</span> 
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">favoritePerson</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">resource</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">async</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">inverse</span><span class="p">:</span> <span class="kc">null</span> <span class="p">}</span>
    <span class="p">},</span>
    <span class="p">{</span> 
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">fullName</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">derived</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">concat</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">fields</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">firstName</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">lastName</span><span class="dl">'</span><span class="p">],</span> <span class="na">separator</span><span class="p">:</span> <span class="dl">'</span><span class="s1"> </span><span class="dl">'</span> <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">]</span>
<span class="p">};</span>

<span class="kd">type</span> <span class="nx">User</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">firstName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">lastName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">friends</span><span class="p">:</span> <span class="nx">User</span><span class="p">[];</span>
  <span class="nl">favoritePerson</span><span class="p">:</span> <span class="nx">User</span><span class="p">;</span>
  <span class="nl">fullName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>There are a lot of key behavioral differences between SchemaRecord and Model: from a shift to immutability, built-in change buffering, deeply reactive object and array fields, to (still under construction) a whole new relationship paradigm. Today, I want to focus on one key area of change: transformed and derived values.</p>

<p>In the world of Models, engineers could use the class to add additional behaviors and derived (computed or calculated) values in addition to the schema fields defined via decorator. When using SchemaRecord, the only fields allowed are those defined via schema. In other words, <strong>SchemaRecord places a fairly massive new constraint on just how much you can do.</strong></p>

<p>I was motivated to write about this constraint after a discussion in <a href="https://discord.com/channels/480462759797063690/1337069566940942388/1337143861964968017">emberjs discord</a> wanted to know more about how to approach solving for it. In this post, I want to explore this new constraint: why we added it — how it helps developers fall into the pit-of-success — and whether there are any alternatives when using SchemaRecord (<em>spoiler alert yes</em>).</p>

<h2 id="the-world-according-to-schema">The World According to Schema</h2>

<blockquote>
  <p>On the sixth day of the second month of the 19th year of our library data, in the evening we lifted our eyes and low under the night-shift of the monitor we looked upon the git respository and from it issued forth a decree that henceforth our records must respect the boundaries of the data they represent.</p>

  <p><em>— i WarpDrive iv.11 2025 Edition</em></p>
</blockquote>

<p>In the world of Schema, every behavior of a record is defined by its schema and derived from the data in the cache.</p>

<p><strong>every</strong>. <strong>behavior</strong>.</p>

<p>If the property or method is not included in your schema for the resource, it doesn’t exist. Every SchemaRecord begins as a completely clean slate.</p>

<p>You may (or more likely probably do not yet) know that SchemaRecord has a special “legacy” mode that allows it to emulate the default Model behaviors. Everything from props on the instance like <code class="language-plaintext highlighter-rouge">isDestroyed</code> and <code class="language-plaintext highlighter-rouge">isDestroying</code> to default fields like <code class="language-plaintext highlighter-rouge">id</code>, the state-machine (<code class="language-plaintext highlighter-rouge">currentState</code>) and its friends like <code class="language-plaintext highlighter-rouge">isDeleted</code>, <code class="language-plaintext highlighter-rouge">isError</code>, <code class="language-plaintext highlighter-rouge">isLoaded</code>, and <code class="language-plaintext highlighter-rouge">isNew</code>, to methods such as <code class="language-plaintext highlighter-rouge">reload</code>, <code class="language-plaintext highlighter-rouge">rollbackAttributes</code>, <code class="language-plaintext highlighter-rouge">save</code>, and <code class="language-plaintext highlighter-rouge">unloadRecord</code>. We even emulate <code class="language-plaintext highlighter-rouge">constructor.modelName</code>.</p>

<p>Every single one of these, yes <em>every single one</em> is implemented by adding a schema-field to the definition for the resource, the result of the <code class="language-plaintext highlighter-rouge">withDefaults</code> call below.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">withDefaults</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/model/migration-support</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">User</span> <span class="o">=</span> <span class="nx">withDefaults</span><span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">fields</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">firstName</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span> <span class="p">}</span>
  <span class="p">]</span>
<span class="p">})</span>
</code></pre></div></div>

<p>Outside of a small special group referred to as <code class="language-plaintext highlighter-rouge">locals</code>, all of these fields are created by a <code class="language-plaintext highlighter-rouge">derivation</code>, which implies that derivations can do all sorts of things beyond simple calcs, even so far as adding methods!</p>

<p>So you see, while you can’t just quickly slap a getter or a method on a class like you could with Model, the world is your oyster! <em>(please, please pretend I didn’t just say that.. lest you steer your app into a miserable place)</em></p>

<p>As the saying goes: <strong>just because you could, doesn’t mean you should!</strong></p>

<h2 id="why-is-everything-schema">Why is Everything Schema?</h2>

<p>To understand why you shouldn’t just write a whole bunch of new field capabilities and derivations to keep on doing whatever, lets take a moment to understand why SchemaRecord isn’t implemented as a class you can extend in the first-place.</p>

<p>Javascript applications, especially those that target browsers, need to balance a lot of factors to maintain great performance characteristics, a few of the big buckets:</p>

<ul>
  <li>asset download time</li>
  <li>asset parse time</li>
  <li>JS eval time</li>
  <li>runtime memory overhead</li>
</ul>

<p>The Model approach to reactive-data performed poorly in all four of these categories.</p>

<p>As apps grow, the number of Models required and the size of their definitions also grows, leading to larger and larger assets and thus larger download and parse times.</p>

<p>Since those Models contain the schema, they are often needed synchronously at unpredictable times, leading to them generally being eval’d early during an app-boot cycle.</p>

<p>Since Models are subclasses (of at least Model and thus also EmberObject and its chain) and often use Mixins and lots of defineProperty calls (from the decorators), parsing them, evaluating and instantiating them is particularly expensive from both a compute and memory perspective.</p>

<p>In short, using Models for reactive-data is expensive and doesn’t scale well with the demands of your application.</p>

<h3 id="so-is-schemarecord-just-about-performance">So is SchemaRecord just about performance?</h3>

<p>No, actually. There are ways for us to optimize record instances in ways that outperform the current SchemaRecord implementation (and we may introduce those as special modes in the future, they have different tradeoffs, we are currently balanced in favor of program correctness and helping developers catch accidental mistakes).</p>

<p>SchemaRecord is equally about <em>flexibility</em>. By having our record instances consume JSON schema to derive their behaviors, we gain not only the ability to deliver smaller, easier to parse JSON payloads that scale better <strong>but also</strong> the ability to deliver the definitions only when we need them and from whatever source is most optimal.</p>

<p>For instance, embedding these JSON schemas in your JS bundle, having them be separate JSON files you load alongside your JS bundle – or just-in-time (JIT), or having them be part of response payloads from requests you make to your API are all equally valid ways of delivering schema. And these modalities can be mixed as needed for apps to tune themselves. <em>flexibility</em>.</p>

<p><em>flexibility</em> is also about what schemas mean from the perspective of partial-data and typescript. In the Model world, the Models were typically used as the type. This leads to friction where in some contexts fields are optional or invalid (such as during a create), while in others they are readonly.</p>

<p>In the SchemaRecord world, types are the types. They vanish from your runtime, and can be tailored to the context of specific edit, create or partial-data scenarios. For more about this I recommend reading the <a href="https://github.com/emberjs/data/blob/main/guides/typescript/2-why-brands.md">TypeScript Guide</a>.</p>

<h3 id="so-is-schemarecord-just-about-performance-and-flexibility">So is SchemaRecord just about performance and flexibility?</h3>

<p>No, actually. It is equally about program correctness (I hinted at this above). Over the years, we have had the opportunity to watch developers intentionally and unintentionally misuse models in ways that lead to frustrating application bugs.</p>

<p>One of the most basic mistakes is treating a record as a convenient storage location for local component state. For instance, we’ll often see records in a list get mutated to add booleans like “isSelected” “isFocused” and “isExpanded”. At first this works and feels easy: then later this creates confusing bugs when the record is used by a different component trying to add and stash its own state.</p>

<p>With SchemaRecord, if a property isn’t in the schema, accessing (or worse attempting to set it) will immediately throw an error. This ensures you aren’t leaking unintended side-effects elsewhere in your code.</p>

<p>The full set of ways SchemaRecord is helping to steer you towards program correctness and protect against wierd and spooky bugs is fairly vast, and probably left for a blog post of its own. As a teaser, the way it goes about immutability and mutation is also designed to guide you to write more correct programs. Suffice it to say though: its important to realize that this is one of its primary goals. And it is likely to be one of the goals that developers (you) struggle with the most.</p>

<p>As programmers we’ve been programmed to hack at things until they work. With Models, we often could just hack until it seemed to work. SchemaRecord demands that you step back and think through how the thing <em>should</em> work and where various behaviors and state <em>should</em> belong.</p>

<h3 id="the-pit-of-success">The Pit of Success</h3>

<p>A basic principle of SchemaRecord is that a little friction and the right constraints go a long way towards steering apps into patterns that are performant, scalable, and correct by default.</p>

<p>Removing the ability to easily and quickly extend a Model with new behaviors is a key part of that principle.</p>

<p>If you find yourself asking “why do I have to write so many custom derivations” or “why am I writing so many custom transformations” or “why does this feel so hard to do” there’s a decent chance the answer is “its meant to be”.</p>

<p>Equally though, just because something is hard does not mean “you should never”. Our goal as a library is to steer you towards what is usually best. Your goal, as a developer, is to know when to steer against the current.</p>

<p>And so with that prolongued introduction, lets explore two categories of steering against the current: transformed and derived values.</p>

<h2 id="exploring-fieldschemas">Exploring FieldSchemas</h2>

<p>The original question which prompted this discussion was asking whether accessing services on records was still possible, and if so how to have a field on a SchemaRecord that changed based on a user selected language set in the <code class="language-plaintext highlighter-rouge">intl</code> service.</p>

<p>In the Model world, this was solved with the following setup:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Model</span><span class="p">,</span> <span class="p">{</span> <span class="nx">attr</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/model</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">service</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember/service</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="nx">House</span> <span class="kd">extends</span> <span class="nx">Model</span> <span class="p">{</span>
  <span class="p">@</span><span class="nd">service</span> <span class="nx">intl</span><span class="p">;</span>

  <span class="p">@</span><span class="nd">attr</span> <span class="kr">declare</span> <span class="nx">houseDescription</span><span class="p">:</span> <span class="p">{</span>
    <span class="nl">en</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Great House</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">es</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Buena Casa</span><span class="dl">'</span>
  <span class="p">},</span>
  
  <span class="kd">get</span> <span class="nx">description</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">lang</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">intl</span><span class="p">.</span><span class="nx">lang</span><span class="p">;</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">houseDescription</span><span class="p">[</span><span class="nx">lang</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Today, lets focus on three specific kinds of FieldSchemas exploring how each might be used to solve this use case:</p>

<ul>
  <li><a href="https://github.com/emberjs/data/blob/33193bf9097a122c1e51a543ea4ebf6a1a2a74d4/packages/core-types/src/schema/fields.ts#L3-L36">transformed fields</a></li>
  <li><a href="https://github.com/emberjs/data/blob/33193bf9097a122c1e51a543ea4ebf6a1a2a74d4/packages/core-types/src/schema/fields.ts#L407-L457">derived fields</a></li>
  <li><a href="https://github.com/emberjs/data/blob/33193bf9097a122c1e51a543ea4ebf6a1a2a74d4/packages/core-types/src/schema/fields.ts#L38-L87">aliased fields</a></li>
</ul>

<h3 id="transformed-fields">Transformed Fields</h3>

<p>You may have heard of transforms when using Models. If so, you understand the rough idea of transformed fields, but transformed fields are very different from the legacy transforms that could be defined via Model attributes.</p>

<p>Defining a transform on a Model looked like this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">User</span> <span class="kd">extends</span> <span class="nx">Model</span> <span class="p">{</span>

  <span class="p">@</span><span class="nd">attr</span><span class="p">(</span><span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">)</span> <span class="nx">name</span><span class="p">;</span>
  <span class="c1">//      ^ 'string' is the transform</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This exact field definition can be defined in JSON as:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"attribute"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"options"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>It may surprise you to know that this basically did nothing in the Model world … unless you happened to be extending from one of the serializers provided by the package <code class="language-plaintext highlighter-rouge">@ember-data/serializer</code>, in which case <em>as long as you did not override the wrong normalization or serialization hook</em> would use Ember’s resolver to lookup the transform to help normalize or serialize the payload.</p>

<p>Key takeaways about legacy transforms:</p>

<ul>
  <li>operated on data to/from the Cache and your API</li>
  <li>weren’t guaranteed to operate at all</li>
  <li>are definitely not type info (despite how many have tried to treat them)</li>
</ul>

<p>A common pitfall that developers hit with legacy transforms is that they don’t run when you mutate a record.</p>

<p>For instance, say you use the <code class="language-plaintext highlighter-rouge">'date'</code> transform to convert string dates to <code class="language-plaintext highlighter-rouge">Date</code> instances. Your API sends down a string, the serializer transforms the field, and the value in the cache is now a <code class="language-plaintext highlighter-rouge">Date</code> instance.</p>

<p>Now, say you are creating a new record with <code class="language-plaintext highlighter-rouge">store.createRecord('user', { birthday })</code>. What do you pass for <code class="language-plaintext highlighter-rouge">birthday</code>, a string or a Date instance? The answer is a <code class="language-plaintext highlighter-rouge">Date</code>, though often folks will set a string instead.</p>

<p>This gets really pernicious with the boolean and number transforms, because while the values coming from the API are converted into the proper form, if you update the value by binding it to a text-input… the value in the cache will now be a string.</p>

<p>Enough about the faults of legacy transforms though (and there are many). SchemaRecord guides us towards correctness, and one of the ways it does so is by introducing a complete rework of transforms. We’ll call the new transforms <code class="language-plaintext highlighter-rouge">Transformations</code>.</p>

<p>Transformations:</p>

<ul>
  <li>operate on data to/from the cache and your app code (they run when the value is accessed or set on the record)</li>
  <li>are guaranteed to operate</li>
  <li>could well be type info (but still aren’t, use types for types)</li>
</ul>

<p>To see how this works, lets create and register a Date transformation (note, heavily recommend something immutable like luxon for Date values instead of raw Date)</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">Transformation</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/schema-record/schema</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Type</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/core-types/symbols</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">DateTransform</span><span class="p">:</span> <span class="nx">Transformation</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nb">Date</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">serialize</span><span class="p">(</span><span class="na">value</span><span class="p">:</span> <span class="nb">Date</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">value</span><span class="p">.</span><span class="nx">toUTCString</span><span class="p">();</span>
  <span class="p">},</span>
  <span class="nx">hydrate</span><span class="p">(</span><span class="na">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Date</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">value</span><span class="p">);</span>
  <span class="p">},</span>
  <span class="p">[</span><span class="nx">Type</span><span class="p">]:</span> <span class="dl">'</span><span class="s1">date</span><span class="dl">'</span><span class="p">,</span>
<span class="p">};</span>

<span class="nx">store</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">registerTransformation</span><span class="p">(</span><span class="nx">DateTransform</span><span class="p">);</span>
</code></pre></div></div>

<p>We register the transformation so that there is no ember-resolver magic. Like schemas, Transformations can be registered Just-In-Time, which means that if desired you can fetch and load transformations asynchrously alongside schemas. As long as the Transformation is registered by the time you access the field on the record instance, you’re good to go.</p>

<p>In addition to some of the common scenarios like Date and Enum, we expect due to their guarantee to run that some folks will choose to use them to write validation layers for fields used in forms.</p>

<p>This is explicitly allowed, though not necessarily sensible as often form validation errors are best handled with other patterns. Validation purposes aside, throwing errors from transforms (especially in dev mode) for malformed data can be an effective way to enforce good habits and prevent sneaky bugs from occurring like integers getting coerced into strings.</p>

<p>Lets address the original question about mapped translations using a transformed field.</p>

<h3 id="implementing-mapped-translations-using-a-transformed-field">Implementing Mapped Translations Using a Transformed Field</h3>

<p>First, lets create a schema and a type to match the data we will have:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">withDefaults</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/schema-record/schema</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">House</span> <span class="o">=</span> <span class="nx">withDefaults</span><span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">house</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">fields</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">houseDescription</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span><span class="p">,</span>
      <span class="c1">// ^ using 'field' instead of 'attribute' ensures we use</span>
      <span class="c1">// the new transformations behavior and not the legacy one.</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mapped-translation</span><span class="dl">'</span><span class="p">,</span>
      <span class="c1">// ^ This declares what transformation to use</span>
    <span class="p">}</span>
  <span class="p">]</span>
<span class="p">});</span>

<span class="kd">type</span> <span class="nx">TranslationMap</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">en</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">es</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">type</span> <span class="nx">HouseRecord</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">$type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">house</span><span class="dl">'</span><span class="p">;</span>
  <span class="nl">houseDescription</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="c1">// NOT TranslationMap!</span>
<span class="p">};</span>

<span class="nx">store</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">registerResource</span><span class="p">(</span><span class="nx">House</span><span class="p">);</span>
</code></pre></div></div>

<p>Now, for the <code class="language-plaintext highlighter-rouge">mapped-translation</code> implementation.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">getOwner</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember/owner</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Type</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/core-types/symbols</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">MappedTranslationTransform</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">serialize</span><span class="p">(</span><span class="na">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">options</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="na">record</span><span class="p">:</span> <span class="nx">SchemaRecord</span><span class="p">):</span> <span class="nx">TranslationMap</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">lang</span> <span class="o">=</span> <span class="nx">getOwner</span><span class="p">(</span><span class="nx">record</span><span class="p">).</span><span class="nx">lookup</span><span class="p">(</span><span class="dl">'</span><span class="s1">service:intl</span><span class="dl">'</span><span class="p">).</span><span class="nx">lang</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">;</span>
    <span class="k">return</span> <span class="p">{</span> <span class="p">[</span><span class="nx">lang</span><span class="p">]:</span> <span class="nx">value</span> <span class="p">};</span>
  <span class="p">},</span>
  <span class="nx">hydrate</span><span class="p">(</span><span class="na">value</span><span class="p">:</span> <span class="nx">TranslationMap</span><span class="p">,</span> <span class="na">options</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span> <span class="na">record</span><span class="p">:</span> <span class="nx">SchemaRecord</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">lang</span> <span class="o">=</span> <span class="nx">getOwner</span><span class="p">(</span><span class="nx">record</span><span class="p">).</span><span class="nx">lookup</span><span class="p">(</span><span class="dl">'</span><span class="s1">service:intl</span><span class="dl">'</span><span class="p">).</span><span class="nx">lang</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">value</span><span class="p">[</span><span class="nx">lang</span><span class="p">]</span> <span class="o">??</span> <span class="dl">''</span><span class="p">;</span>
  <span class="p">},</span>
  <span class="p">[</span><span class="nx">Type</span><span class="p">]:</span> <span class="dl">'</span><span class="s1">mapped-translation</span><span class="dl">'</span><span class="p">,</span>
<span class="p">};</span>

<span class="nx">store</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">registerTransformation</span><span class="p">(</span><span class="nx">MappedTranslationTransform</span><span class="p">);</span>
</code></pre></div></div>

<p>With the above, when we access the <code class="language-plaintext highlighter-rouge">houseDescription</code> property we get the
correct description for our current language. Whenever the current language changes,
or whenever the cache updates with a new value for houseDescription the value on
our record will recompute.</p>

<p>Whenever we set the property, we update the cache with the new value. However,
in this approach the mutation is dangerous:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="p">{</span> <span class="p">[</span><span class="nx">lang</span><span class="p">]:</span> <span class="nx">value</span> <span class="p">};</span>
</code></pre></div></div>

<p>This will mean that the mutated state in the cache will lose any other languages that
had values. In some cases, this may be desired, but if we wanted to patch just the one
language we’d need a bit more info.</p>

<p>The downside of the transformation approach is that we don’t give the schema for the field
being operated on to the serialize or hydrate methods. This was by design to avoid folks
getting too creative inside of transformations, though in a scenario like this it might be useful.</p>

<p>Lets say the options arg gave you access to the field-schema instead of just fieldSchema.options. Then we could do a merge in the cache during serialization to avoid removing other languages. We could also do this by duplicating a small amount of field information in the schema definition.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">recordIdentifierFor</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/store</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">getOwner</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember/owner</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Type</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/core-types/symbols</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">MappedTranslationTransform</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">serialize</span><span class="p">(</span><span class="na">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">field</span><span class="p">:</span> <span class="nx">FieldSchema</span><span class="p">,</span> <span class="na">record</span><span class="p">:</span> <span class="nx">SchemaRecord</span><span class="p">):</span> <span class="nx">TranslationMap</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">owner</span> <span class="o">=</span> <span class="nx">getOwner</span><span class="p">(</span><span class="nx">record</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">cache</span> <span class="o">=</span> <span class="nx">owner</span><span class="p">.</span><span class="nx">lookup</span><span class="p">(</span><span class="dl">'</span><span class="s1">service:store</span><span class="dl">'</span><span class="p">).</span><span class="nx">cache</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">identifier</span> <span class="o">=</span> <span class="nx">recordIdentifierFor</span><span class="p">(</span><span class="nx">record</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">lang</span> <span class="o">=</span> <span class="nx">owner</span><span class="p">.</span><span class="nx">lookup</span><span class="p">(</span><span class="dl">'</span><span class="s1">service:intl</span><span class="dl">'</span><span class="p">).</span><span class="nx">lang</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">currentValue</span> <span class="o">=</span> <span class="nx">cache</span><span class="p">.</span><span class="nx">getAttr</span><span class="p">(</span><span class="nx">identifier</span><span class="p">,</span> <span class="nx">field</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>

    <span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="nx">currentValue</span><span class="p">,</span> <span class="p">{</span> <span class="p">[</span><span class="nx">lang</span><span class="p">]:</span> <span class="nx">value</span> <span class="p">});</span>
  <span class="p">},</span>
  <span class="nx">hydrate</span><span class="p">(</span><span class="na">value</span><span class="p">:</span> <span class="nx">TranslationMap</span><span class="p">,</span> <span class="na">field</span><span class="p">:</span> <span class="nx">FieldSchema</span><span class="p">,</span> <span class="na">record</span><span class="p">:</span> <span class="nx">SchemaRecord</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">lang</span> <span class="o">=</span> <span class="nx">getOwner</span><span class="p">(</span><span class="nx">record</span><span class="p">).</span><span class="nx">lookup</span><span class="p">(</span><span class="dl">'</span><span class="s1">service:intl</span><span class="dl">'</span><span class="p">).</span><span class="nx">lang</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">;</span>
    <span class="k">return</span> <span class="nx">value</span><span class="p">[</span><span class="nx">lang</span><span class="p">]</span> <span class="o">??</span> <span class="dl">''</span><span class="p">;</span>
  <span class="p">},</span>
  <span class="p">[</span><span class="nx">Type</span><span class="p">]:</span> <span class="dl">'</span><span class="s1">mapped-translation</span><span class="dl">'</span><span class="p">,</span>
<span class="p">};</span>

<span class="nx">store</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">registerTransformation</span><span class="p">(</span><span class="nx">MappedTranslationTransform</span><span class="p">);</span>
</code></pre></div></div>

<p>Perhaps with time and feedback this is a restriction we will lift. The primary reason this restriction was put in place is to try to prevent transformations that compute off of additional fields, as this can lead to difficult to reason about differences between what the record presents and what is in the cache.</p>

<p>A bit of friction to steer folks the right way by default … but with a work around via padding additional info into options if the correct course is to steer against the stream.</p>

<p>Ok, so now for the <code class="language-plaintext highlighter-rouge">alias</code> approach.</p>

<h3 id="aliased-fields">Aliased Fields</h3>

<p>An AliasField can be used to alias one key to another key present in the cache version of the resource.</p>

<p>Unlike DerivedField (which we will see next), an AliasField may write to its source when a record is in an editable mode.</p>

<p>AliasFields may utilize a transformation, specified by type, to pre/post process the field.</p>

<p>An AliasField may also specify a <code class="language-plaintext highlighter-rouge">kind</code> via options. <code class="language-plaintext highlighter-rouge">kind</code> may be any other valid field kind
other than:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">@hash</code></li>
  <li><code class="language-plaintext highlighter-rouge">@id</code></li>
  <li><code class="language-plaintext highlighter-rouge">@local</code></li>
  <li><code class="language-plaintext highlighter-rouge">derived</code></li>
</ul>

<p>This allows an AliasField to rename any field in the cache.</p>

<p>Alias fields are generally intended to be used to support migrating between different schemas, though there are times where they are useful as a form of advanced derivation when used with a transform.</p>

<p>For instance, an AliasField could be used to expose both a string and a Date version of the
same field, with both being capable of being written to.</p>

<h3 id="implementing-mapped-translations-using-an-aliased-field">Implementing Mapped Translations Using an Aliased Field</h3>

<p>In the alias approach, you retain exposing two fields like the original Model had, and you still write the transformation described above. The primary advantage is retaining access to the original field.</p>

<p>Here are our new House schema and types.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">withDefaults</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/schema-record/schema</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">MappedTranslationObject</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mapped-translation-object</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">identity</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="c1">// ^ resource schemas with no identity field are used to describe reusable</span>
  <span class="c1">// data structures contained within our primary resource types</span>
  <span class="na">fields</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">es</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span> <span class="p">}</span>
  <span class="p">]</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">House</span> <span class="o">=</span> <span class="nx">withDefaults</span><span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">house</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">fields</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">houseDescription</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">schema-object</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mapped-translation-object</span><span class="dl">'</span><span class="p">,</span>
      <span class="c1">// ^ this means the resource-schema for this object is 'mapped-translation-object'</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">description</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">alias</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">type</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
        <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">houseDescription</span><span class="dl">'</span><span class="p">,</span>
        <span class="c1">// ^ means this field will source its data from the field right above</span>
        <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mapped-translation</span><span class="dl">'</span><span class="p">,</span>
        <span class="c1">// ^ means this field will use the transform we defined before</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">]</span>
<span class="p">});</span>

<span class="kd">type</span> <span class="nx">TranslationMap</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">en</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">es</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">};</span>

<span class="kd">type</span> <span class="nx">HouseRecord</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">$type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">house</span><span class="dl">'</span><span class="p">;</span>
  <span class="nl">houseDescription</span><span class="p">:</span> <span class="nx">TranslationMap</span><span class="p">;</span>
  <span class="nl">description</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">};</span>

<span class="nx">store</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">registerResources</span><span class="p">([</span><span class="nx">House</span><span class="p">,</span> <span class="nx">MappedTranslationsObject</span><span class="p">]);</span>
</code></pre></div></div>

<p>This works exactly the same as the transformation approach except now we use <code class="language-plaintext highlighter-rouge">record.description</code> to get the description in the currently active language and can still access and update <code class="language-plaintext highlighter-rouge">houseDescription</code> directly.</p>

<p>One advantage of this is that because <code class="language-plaintext highlighter-rouge">houseDescription</code> is a <code class="language-plaintext highlighter-rouge">schema-object</code>, mutating it instead of <code class="language-plaintext highlighter-rouge">description</code> is both deeply-reactive and granular (the cache knows how to perform and store deep changes to schema-objects).</p>

<p>Finally, the derivation approach:</p>

<h3 id="derived-fields">Derived Fields</h3>

<p>A DerivedField is a field whose value is derived
from other fields in the schema.</p>

<p>The value is read-only, and is not stored in the cache,
nor is it sent to the server.</p>

<p>Usage of derived fields should be minimized to scenarios where the derivation is known to be safe.</p>

<p>For instance, derivations that required fields that are not always loaded or that require access to related resources that may not be loaded should be avoided.</p>

<h3 id="implementing-mapped-translations-using-a-derived-field">Implementing Mapped Translations Using a Derived Field</h3>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">MappedTranslationObject</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mapped-translation-object</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">identity</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
  <span class="na">fields</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">es</span><span class="dl">'</span><span class="p">,</span> <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">field</span><span class="dl">'</span> <span class="p">}</span>
  <span class="p">]</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">House</span> <span class="o">=</span> <span class="nx">withDefaults</span><span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">house</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">fields</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">houseDescription</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">schema-object</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mapped-translation-object</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="p">{</span>
      <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">description</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">kind</span><span class="p">:</span> <span class="dl">'</span><span class="s1">derived</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mapped-translation</span><span class="dl">'</span>
      <span class="c1">// ^ the name of our derivation</span>
      <span class="c1">// this is not our transformation from before,</span>
      <span class="c1">// we will define this below</span>
      <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">field</span><span class="p">:</span> <span class="dl">'</span><span class="s1">houseDescription</span><span class="dl">'</span> <span class="p">}</span>
  <span class="p">]</span>
<span class="p">});</span>
</code></pre></div></div>

<p>You’ll notice that the above looks a lot like the alias approach. The main difference is that alias fields can be mutated, derived fields can never be set.</p>

<p>And here’s what that derivation would look like:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">getOwner</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember/owner</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Type</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/core-types/symbols</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">mappedTranslation</span><span class="p">(</span>
  <span class="nx">record</span><span class="p">:</span> <span class="nx">SchemaRecord</span> <span class="o">&amp;</span> <span class="p">{</span> <span class="p">[</span><span class="na">key</span><span class="p">:</span> <span class="kr">string</span><span class="p">]:</span> <span class="nx">unknown</span> <span class="p">},</span>
  <span class="nx">options</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nx">unknown</span><span class="o">&gt;</span> <span class="o">|</span> <span class="kc">null</span><span class="p">,</span>
  <span class="nx">_prop</span><span class="p">:</span> <span class="kr">string</span>
<span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">options</span><span class="p">?.</span><span class="nx">field</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`options.field is required`</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">opts</span> <span class="o">=</span> <span class="nx">options</span> <span class="k">as</span> <span class="p">{</span> <span class="na">field</span><span class="p">:</span> <span class="kr">string</span> <span class="p">};</span>
  <span class="kd">const</span> <span class="nx">lang</span> <span class="o">=</span> <span class="nx">getOwner</span><span class="p">(</span><span class="nx">record</span><span class="p">).</span><span class="nx">lookup</span><span class="p">(</span><span class="dl">'</span><span class="s1">service:intl</span><span class="dl">'</span><span class="p">).</span><span class="nx">lang</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">;</span>

  <span class="k">return</span> <span class="nx">record</span><span class="p">[</span><span class="nx">options</span><span class="p">.</span><span class="nx">field</span><span class="p">][</span><span class="nx">lang</span><span class="p">]</span> <span class="o">??</span> <span class="dl">''</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">mappedTranslation</span><span class="p">[</span><span class="nx">Type</span><span class="p">]</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">mapped-translation</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">store</span><span class="p">.</span><span class="nx">schema</span><span class="p">.</span><span class="nx">registerDerivation</span><span class="p">(</span><span class="nx">mappedTranslation</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="parting-thoughts">Parting Thoughts</h2>

<p>You can write a Derivation that gives you access to services, but it is generally something I would avoid except for key-data-concerns.</p>

<p>key-data-concerns might be things like:</p>

<ul>
  <li>the intl service for use by derivations or transforms</li>
  <li>a clock service for use by time based derivations or transforms</li>
</ul>

<p>But I would not do something like give the record access to a Store or Request service. Side-effects and Async in particular are things to avoid in Derivations.</p>

<p>If you keep to a few well-thought-out Transformations and Derivations you can go far and fast, but if you try to put tons of unique computations onto your records, you will find it intentionally frustrating.</p>

<p>The point is to steer you into putting these calculations into more correct locations in your code.</p>

<p>That said, translations like in the above example are a great use-case for using them because they have high utility and reusability across both fields and resource types and perform relatively simple calcs.</p>

<p>A side-effect of this pattern that is valuable for some apps is that Transformations and Derivations follow a contract and pattern simple and descriptive enough that it lets them cross the client/server boundary. If your API provides your schemas and your data, it follows that it could use them to make the same derivations when needed (say for creating a PDF report or CSV export).</p>

<p>A curious footnote to this discussion, Transformations and Derivations may not remain the only way to customize SchemaRecord: there is a <a href="https://github.com/emberjs/data/issues/9534#issue-2534328361">proposal</a> being floated to allow apps to also define what kinds of fields are valid.</p>

<p>Currently, each field “kind” (<code class="language-plaintext highlighter-rouge">schema-object</code>,<code class="language-plaintext highlighter-rouge">field</code>,<code class="language-plaintext highlighter-rouge">derived</code>,<code class="language-plaintext highlighter-rouge">collection</code> etc.) is implemented as a function following a nearly identical signature. It feels like a natural progression of the schema-verse to allow registering <code class="language-plaintext highlighter-rouge">Kind</code> functions just like you can register Derivations and Transformations, continuing our theme of small composable primitives.</p>

<p>Constrained. But Powerful.</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[Exploring transformed and derived values in @warp-drive/schema-record]]></summary></entry><entry><title type="html">Creating NPX compatible cli tools with Bun</title><link href="https://runspired.com/2025/01/25/npx-executables-with-bun.html" rel="alternate" type="text/html" title="Creating NPX compatible cli tools with Bun" /><published>2025-01-25T00:00:00+00:00</published><updated>2025-01-25T00:00:00+00:00</updated><id>https://runspired.com/2025/01/25/npx-executables-with-bun</id><content type="html" xml:base="https://runspired.com/2025/01/25/npx-executables-with-bun.html"><![CDATA[<h1 id="creating-npx-compatible-cli-tools-with-bun">Creating NPX compatible cli tools with Bun</h1>

<blockquote>
  <p>This blog post is an exercise in exploration. Read to the end for why I wouldn’t necessarily suggest doing this.</p>
</blockquote>

<p>Users want to run scripts using tools already available in their project, usually this means installing a package and running its executable via <code class="language-plaintext highlighter-rouge">npx</code>, <code class="language-plaintext highlighter-rouge">npm run</code>, <code class="language-plaintext highlighter-rouge">pnpm dlx</code>, <code class="language-plaintext highlighter-rouge">bunx</code> or similar.</p>

<p>Script authors (like myself) want to create scripts using the best tools at their disposal. These past few years I’ve been using <a href="https://bun.sh/">Bun</a> for creating most of my scripts because as an all-in-one toolchain+sdk it makes it ridiculously easy to build complex scripts with minimal time spent on project configuration, and many of its APIs like <code class="language-plaintext highlighter-rouge">file</code>, <code class="language-plaintext highlighter-rouge">glob</code> and <code class="language-plaintext highlighter-rouge">spawn</code> are ideal for building the kinds of tools I tend to work on.</p>

<p>I also like to author scripts that are fast and lightweight, which Bun’s focus on performance and builtins helps make happen. I can often author complex scripts with zero or close-to-zero dependencies.</p>

<p>One challenge I’ve encountered has been how to create tools that work for users that don’t use Bun or perhaps have an older version.</p>

<p>While our script can use a shebang (<code class="language-plaintext highlighter-rouge">#!/usr/bin/env bun</code>) to request running our script with <code class="language-plaintext highlighter-rouge">bun</code>, this will only work if the user has already adopted bun into their tooling stack, and has a recent enough version of bun on their machine to support the APIs we use.</p>

<p>Eventually, bun intends to enable building a project with <code class="language-plaintext highlighter-rouge">node</code> as a target in a way which includes any bun-specific APIs in the output. Whenever this lands it will immediately be incredibly useful, as most commonly the people trying to run scripts authored with bun APIs without bun are running node.</p>

<p>…but what if it didn’t matter at all what tools and versions were locally available to the dev?</p>

<p>Below I’ll outline a series of concepts that together allow us to do this.</p>

<h2 id="compiling-our-script">Compiling our Script</h2>

<p>Often we don’t think to bundle the node scripts we write. Or if we do its to support things like typescript, or esm/cjs interop. With bun, typescript and esm/cjs interop “just work”. Thankfully, these ideas are now entering node as well so this particular advantage bun holds will fade away.</p>

<p>Sometimes, especially with larger node apps, we bundle just to reduce start-up time.
This is something bun makes easy, and it will even make optimizations like minification and inlining happen for you while it does.</p>

<p>For small CLI scripts, especially those meant for remote use by tools like <code class="language-plaintext highlighter-rouge">npx</code>, I think bundling is ideal as it lets us reduce the boot cost of our tool and potentially ship an even smaller package.</p>

<p>But bun optionally takes bundling two huge steps further:</p>

<ul>
  <li><a href="https://bun.sh/docs/bundler/executables#bytecode-compilation"><code class="language-plaintext highlighter-rouge">--bytecode</code></a> improves startup time of a bundle by pre-parsing the JS and storing that in the JS instead.</li>
  <li><a href="https://bun.sh/docs/bundler/executables"><code class="language-plaintext highlighter-rouge">--compile</code></a> produces a single-file executable binary containing all necessary code and the bun runtime.</li>
</ul>

<p>By default, bun compiles for the machine you are on, but we can adjust the target to compile for <a href="https://bun.sh/docs/bundler/executables#supported-targets">any architecture that bun supports</a>.</p>

<p>Assuming the entrypoint for our script is <code class="language-plaintext highlighter-rouge">./src/index.ts</code>, then we can build a simple script to generate compiled versions for each supported platform.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">KnownArchitectures</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">arm64</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">x64</span><span class="dl">'</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">KnownOperatingSystems</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">linux</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">windows</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">darwin</span><span class="dl">'</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">InvalidCombinations</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">windows-arm64</span><span class="dl">'</span><span class="p">];</span>

<span class="nl">outer</span><span class="p">:</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">arch</span> <span class="k">of</span> <span class="nx">KnownArchitectures</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">platform</span> <span class="k">of</span> <span class="nx">KnownOperatingSystems</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">InvalidCombinations</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">platform</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">arch</span><span class="p">}</span><span class="s2">`</span><span class="p">))</span> <span class="k">continue</span><span class="p">;</span>

    <span class="c1">// compile the given architecture</span>
    <span class="kd">const</span> <span class="nx">target</span> <span class="o">=</span> <span class="s2">`bun-</span><span class="p">${</span><span class="nx">platform</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">arch</span><span class="p">}</span><span class="s2">-modern`</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">args</span> <span class="o">=</span> <span class="p">[</span>
      <span class="dl">'</span><span class="s1">bun</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">build</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">./src/index.ts</span><span class="dl">'</span><span class="p">,</span>
      <span class="s2">`--outfile=dist/</span><span class="p">${</span><span class="nx">target</span><span class="p">}</span><span class="s2">-npx-bun-demo`</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">--compile</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">--minify</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">--bytecode</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">--env=disable</span><span class="dl">'</span><span class="p">,</span>
      <span class="dl">'</span><span class="s1">--sourcemap=none</span><span class="dl">'</span><span class="p">,</span>
      <span class="s2">`--target=</span><span class="p">${</span><span class="nx">target</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
    <span class="p">];</span>

    <span class="kd">const</span> <span class="nx">proc</span> <span class="o">=</span> <span class="nx">Bun</span><span class="p">.</span><span class="nx">spawn</span><span class="p">(</span><span class="nx">args</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">env</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">,</span>
      <span class="na">cwd</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">cwd</span><span class="p">(),</span>
      <span class="na">stderr</span><span class="p">:</span> <span class="dl">'</span><span class="s1">inherit</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">stdout</span><span class="p">:</span> <span class="dl">'</span><span class="s1">inherit</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">stdin</span><span class="p">:</span> <span class="dl">'</span><span class="s1">inherit</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">proc</span><span class="p">.</span><span class="nx">exited</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">result</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">break</span> <span class="nx">outer</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="setting-up-our-package">setting up our package</h2>

<h3 id="1-move-all-dependencies-to-devdependencies">1. move all dependencies to devDependencies</h3>

<p>Since the code we need from our dependencies is all bundled into the executable, we don’t want any tools to install them when trying to run our script.</p>

<p>So anything listed in <code class="language-plaintext highlighter-rouge">dependencies</code> should be moved to <code class="language-plaintext highlighter-rouge">devDependencies</code> in the <code class="language-plaintext highlighter-rouge">package.json</code>.</p>

<p>If you were using bun to build your API, this feature simplifies your deployment immensely! Zero-install / extremely lightweight image deploys!</p>

<h3 id="2-ensure-we-compile-whenever-we-package-up-our-project">2. Ensure we compile whenever we package up our project</h3>

<p>In our <code class="language-plaintext highlighter-rouge">package.json</code> file, we setup the <code class="language-plaintext highlighter-rouge">prepack</code> script to ensure we build the compiled versions.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bun run build.ts"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"prepack"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bun run build"</span><span class="w">
  </span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<h3 id="3-setup-a-bin-script-for-the-package">3. setup a <code class="language-plaintext highlighter-rouge">bin</code> script for the package</h3>

<p>Since we want our script to be usable from tools like <code class="language-plaintext highlighter-rouge">npx</code>, we need to add a <code class="language-plaintext highlighter-rouge">bin</code> entry to our <code class="language-plaintext highlighter-rouge">package.json</code> file.</p>

<p>NPM doesn’t provide a utility for switching which asset to use for a bin script based on the consumer’s architecture (though you could use node-gyp to achieve this). Below, I’ll detail how to setup a script that juggles this for you, we’ll name that file <code class="language-plaintext highlighter-rouge">arch-switch.mjs</code>.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"bin"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"npx-bun-demo"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./arch-switch.mjs"</span><span class="w">
  </span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<h3 id="4-setup-what-files-to-publish-with-our-package">4. setup what files to publish with our package</h3>

<p>Our script above outputs the compiled versions into <code class="language-plaintext highlighter-rouge">dist</code>, and requires the bin script <code class="language-plaintext highlighter-rouge">arch-switch.mjs</code>, nothing else is needed to use the tool, so these will be the only entries in our <code class="language-plaintext highlighter-rouge">package.json</code> file.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"files"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"dist"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"arch-switch.mjs"</span><span class="w">
  </span><span class="p">]</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>

<h2 id="creating-the-bin-script">Creating the bin script</h2>

<p>Our <code class="language-plaintext highlighter-rouge">arch-switch</code> script will select which pre-compiled binary to execute to perform the <code class="language-plaintext highlighter-rouge">npx-bun-demo</code> command by detecting the platform and architecture of the machine in use.</p>

<p>For this, we want to make sure to use only features that are available to both node and bun, which generally means “use only node-available APIs” as bun largely has implemented support for anything node offers (being able to use any npm package and node built-in with bun is another of its superpowers).</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">spawn</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">child_process</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">arch</span> <span class="k">as</span> <span class="nx">osArch</span><span class="p">,</span> <span class="nx">platform</span> <span class="k">as</span> <span class="nx">osPlatform</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">os</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">KnownArchitectures</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">arm64</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">x64</span><span class="dl">'</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">KnownOperatingSystems</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">linux</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">windows</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">darwin</span><span class="dl">'</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">InvalidCombinations</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">windows-arm64</span><span class="dl">'</span><span class="p">];</span>

<span class="kd">const</span> <span class="nx">arch</span> <span class="o">=</span> <span class="nx">osArch</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">_platform</span> <span class="o">=</span> <span class="nx">osPlatform</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">platform</span> <span class="o">=</span> <span class="nx">_platform</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">win32</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">windows</span><span class="dl">'</span> <span class="p">:</span> <span class="nx">_platform</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">ExitError</span><span class="p">(</span><span class="nx">str</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`\n\n`</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span>
    <span class="dl">'</span><span class="se">\</span><span class="s1">x1b[1m</span><span class="se">\</span><span class="s1">x1b[4m</span><span class="se">\</span><span class="s1">x1b[31m</span><span class="se">\</span><span class="s1">x1b[40m</span><span class="dl">'</span><span class="p">,</span>
    <span class="s2">`\t\t</span><span class="p">${</span><span class="nx">str</span><span class="p">}</span><span class="s2">\t\t`</span>
  <span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">`\n\n`</span><span class="p">);</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">KnownArchitectures</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">arch</span><span class="p">))</span> <span class="p">{</span>
  <span class="nx">ExitError</span><span class="p">(</span><span class="s2">`Unsupported architecture '</span><span class="p">${</span><span class="nx">arch</span><span class="p">}</span><span class="s2">'`</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">KnownOperatingSystems</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">platform</span><span class="p">))</span> <span class="p">{</span>
  <span class="nx">ExitError</span><span class="p">(</span><span class="s2">`Unsupported platform '</span><span class="p">${</span><span class="nx">platform</span><span class="p">}</span><span class="s2">'`</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">executableName</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">platform</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">arch</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">InvalidCombinations</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">executableName</span><span class="p">))</span> <span class="p">{</span>
  <span class="nx">ExitError</span><span class="p">(</span><span class="s2">`Unsupported architecture '</span><span class="p">${</span><span class="nx">arch</span><span class="p">}</span><span class="s2">' for current platform '</span><span class="p">${</span><span class="nx">platform</span><span class="p">}</span><span class="s2">'`</span><span class="p">);</span>
<span class="p">}</span>

<span class="nx">spawn</span><span class="p">(</span>
  <span class="s2">`./dist/bun-</span><span class="p">${</span><span class="nx">executableName</span><span class="p">}</span><span class="s2">-modern-npx-bun-demo</span><span class="p">${</span><span class="nx">platform</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">windows</span><span class="dl">'</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">.exe</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">''</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
  <span class="nx">process</span><span class="p">.</span><span class="nx">argv</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span>
  <span class="p">{</span>
    <span class="na">cwd</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">cwd</span><span class="p">(),</span>
    <span class="na">env</span><span class="p">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">,</span>
    <span class="na">stdio</span><span class="p">:</span> <span class="dl">'</span><span class="s1">inherit</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="conditionally-running-bun-via-shebang">Conditionally Running Bun via shebang</h3>

<p>Our bin script above is missing just one piece: the shebang.</p>

<p>Usually, a bin script will specify <code class="language-plaintext highlighter-rouge">#!/usr/bin/env node</code> at the top of the file to ensure tools execute it with the node runtime.</p>

<p>We can alternatively specify <code class="language-plaintext highlighter-rouge">#!/usr/bin/env bun</code> instead to make use of the much faster start-up time and runtime offered by <code class="language-plaintext highlighter-rouge">bun</code>, but remember, we aren’t sure our user has bun (or node for that matter) and we want to work either way.</p>

<p>Enter a conditional shebang, a bit of sorcery I leave to google (or lets be honest probably ChatGPT) to explain:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh -</span>
<span class="s1">':'</span><span class="p">;</span> /<span class="k">*</span>-
<span class="nv">test1</span><span class="o">=</span><span class="si">$(</span>bun <span class="nt">--version</span> 2&gt;&amp;1<span class="si">)</span> <span class="o">&amp;&amp;</span> <span class="nb">exec </span>bun <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
<span class="nv">test2</span><span class="o">=</span><span class="si">$(</span>node <span class="nt">--version</span> 2&gt;&amp;1<span class="si">)</span> <span class="o">&amp;&amp;</span> <span class="nb">exec </span>node <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
<span class="nb">exec printf</span> <span class="s1">'%s\n'</span> <span class="s2">"</span><span class="nv">$test1</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$test2</span><span class="s2">"</span> 1&gt;&amp;2
<span class="k">*</span>/
</code></pre></div></div>

<p>We can add a console log to the top of our file after this to help see in practice that this works:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#!/bin/sh -
</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">;</span> <span class="cm">/*-
test1=$(bun --version 2&gt;&amp;1) &amp;&amp; exec bun "$0" "$@"
test2=$(node --version 2&gt;&amp;1) &amp;&amp; exec node "$0" "$@"
exec printf '%s\n' "$test1" "$test2" 1&gt;&amp;2
*/</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span>
  <span class="s2">`\n\nRunning '</span><span class="p">${[</span><span class="dl">'</span><span class="s1">npx-bun-demo (arch-switch) |&gt;</span><span class="dl">'</span><span class="p">,</span> <span class="p">...</span><span class="nx">process</span><span class="p">.</span><span class="nx">argv</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">2</span><span class="p">)].</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="p">)}</span><span class="s2">' with: </span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">isBun</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">bun</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">node</span><span class="dl">'</span><span class="p">}</span><span class="s2">`</span>
<span class="p">);</span>
</code></pre></div></div>

<p>The general gist is that this uses various comment syntaxes and strings in a manner that a shell-script interprets it as a command and the JS runtime ignores it. If we find a bun command in our current path, we use it, else we try to use node.</p>

<p>And there you have it!</p>

<p>The source-code for this demo is <a href="https://github.com/runspired/npx-bun-demo">available on github</a>.</p>

<p>The example app details can be tried via <code class="language-plaintext highlighter-rouge">npx npx-bun-demo@1.0.0</code> or a similar tool of your choosing.</p>

<h3 id="what-i-dont-like">What I don’t like</h3>

<p>The downside to the approach detailed here is the package size that results from producing the matrix of compiled binaries.</p>

<p>If all we cared about was a single platform and architecture, our package would be <code class="language-plaintext highlighter-rouge">56.8 MB</code>. This is viewable via <code class="language-plaintext highlighter-rouge">npm view npx-bun-demo@1.0.1</code></p>

<p>This is larger than I’d like, but considering that is the runtime, dependencies and source-code I find it acceptable (most small scripts are much larger than this once dependency sizes are taken into account, before even considering the size of node itself).</p>

<p>With the full matrix of current targets, our package is <code class="language-plaintext highlighter-rouge">422.4 MB</code>. This is viewable via <code class="language-plaintext highlighter-rouge">npm view npx-bun-demo@1.0.0</code> Oof 🙈</p>

<p>There are times where this is probably acceptable (at least, in the short-haul until bun gives us the ability to consume its APIs outside of a bun environment and/or npm gives us the ability to pre-build separate tarballs for distinct architectures).</p>

<p>If you want, you could risk dangerously relying on the postinstall hook (seriously folks, run your package installation with scripts disabled please) to detect and download the desired executable. The easy path for that would be to check the compiled versions into github and have the switch script fetch the file if not already present.</p>

<p>I really don’t recommend this except <em>maybe</em> for scripts that are only ever used via a mechanism like <code class="language-plaintext highlighter-rouge">npx</code>.</p>

<p>This approach is demoable via <code class="language-plaintext highlighter-rouge">npx npx-bun-demo@1.0.7</code> for everything but windows (since github won’t allow files &gt; 100MB).</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[Creating NPX compatible cli tools with Bun]]></summary></entry><entry><title type="html">EdgePipes | The Alternative to SSR and RSCs</title><link href="https://runspired.com/2024/12/01/edge-pipes.html" rel="alternate" type="text/html" title="EdgePipes | The Alternative to SSR and RSCs" /><published>2024-12-01T00:00:00+00:00</published><updated>2024-12-01T00:00:00+00:00</updated><id>https://runspired.com/2024/12/01/edge-pipes</id><content type="html" xml:base="https://runspired.com/2024/12/01/edge-pipes.html"><![CDATA[<h1 id="edgepipes--the-alternative-to-ssr-and-rscs">EdgePipes | The Alternative to SSR and RSCs</h1>

<p>If you’ve talked with me at any point over the past few years (in person, at conferences, or listened in anywhere someone unwisely chose to give me a platform) you’ve probably heard me mention “SSD” or “Server Side Data”.</p>

<p>The basic idea is that instead of SSR (Server Side Rendering) we would hoist <strong>just the fetch hooks and router</strong> out of an SPA or MPA, and embed them in a SharedWorker in the browser or deploy them as an Edge Function.</p>

<p>Three design requirements are pre-requisites for this idea:</p>

<ul>
  <li>the router and fetch hooks should be fully isolatable from any rendering requirements</li>
  <li>the fetch hooks should be non-blocking by design (waterfalls / linear requests are acceptable via chaining fetch hooks if required, but ideally some annotation may be used to differentiate those that block due to need vs due to ux considerations)</li>
  <li>the fetch process needs to be managed on the client</li>
</ul>

<p>Unlike SSR and RSCs where the goal is to output the page and data in a state ready for the consumer to read, our goal with this architecture is purely to tackle optimizations around waterfalls, latency, and network reliability. The output artifact is not a page, but a streamable payload containing a fully replayable response for each request made by the fetch hooks.</p>

<p>This design is premised on a few key ideas:</p>

<ol>
  <li>
    <p>The existence of “The Backbone Effect”: Network connections are more stable, throughput is higher and latency is lower between the server running an edge function and the data-center hosting your backend than between the device and your backend.</p>
  </li>
  <li>
    <p>That booting an edge-function with a simple optimized router and running the fetch hooks is significantly faster than the time it would take to await asset load on the client, boot the full application, begin routing and run all fetch-hooks in parallel there.</p>
  </li>
  <li>
    <p>That we are able to manage network requests intelligently-enough in the client so that we can make use of the response from this edge-function.</p>
  </li>
  <li>
    <p>That delivering N request responses through one pipe or via server-push is in aggregate faster and more reliable than sending N separate requests.</p>
  </li>
  <li>
    <p>That the application has intelligent enough state and routing management to take advantage of this edge-function to optimistically prefetch data for pages the users might go to next, and can delegate this responsibility off-thread.</p>
  </li>
</ol>

<p>As I’ve thought more on this idea I’ve started rebranding it as “EdgePipes” instead of SSD to better communicate what it does and where it differentiates.</p>

<h2 id="whats-different">What’s different?</h2>

<p>Unlike SSR, EdgePipes should be fairly resource-consumption friendly. Like SSR, EdgePipes can be used to progressively enhance the performance or experience of an application.</p>

<p>Unlike SSR, EdgePipes optimize page-loads even after the initial page.</p>

<p>Unlike SSR and RSCs, EdgePipes don’t create a double-request or rehydration problem, which should lead to applications being <em>actually</em> interactive faster, not just appearing to be.</p>

<p>Unlike RSCs EdgePipes don’t change your security model, make you think about where your servers are in relation to your database, or ask you to write your API in JS.</p>

<p>Unlike RSCs, EdgePipes actually make sense on the edge, because they don’t need to be located close to your microservices and databases.</p>

<p>I also suspect that EdgePipes would be easy to hyper-optimize. Their restricted nature likely means they can be compiled into executables with the likes of bun and static hermes. They could potentially rewrite responses into more streamable and compressible forms for final delivery (if the plugin for doing so is paired with a plugin in the client that understands how to re-expand).</p>

<p>Since routes are often nested, the client could easily send a hint for which fetch-hooks it already has (and thus can be skipped).</p>

<p>Using it for prefetch doesn’t even have to deliver the response all-the-way to the client: it could just prefetch to a cache on the edge so that the cost to the user’s device is only paid if they navigate to the associated page, solving elegantly a common problem seen with sites that attempt to use prefetch aggressively for every link and button.</p>

<h2 id="but-why">But Why?</h2>

<p>I started thinking about this architecture while working at LinkedIn, where quite often we ran our SSR (fastboot) in a “data only” mode somewhat similar to this and for similar reasons. But (for lots of reasons) that implementation always left way more to be desired than it solved problems.</p>

<p>I’ve never liked SSR. Okay that might be a bit of a lie there was a short period when SSR first burst onto the scene and EmberJS added one of the first out-of-the-box SSR solutions with fastboot that I thought it was awesome. But the rose colored lenses faded to grey quickly: the real world evidence mostly showed that SSR for anything except pre-rendering html for static sites was both cost-prohibitive and net-negative to user experiences and performance. While I’d never considered it a silver bullet, I did at least think at the time it would <em>improve</em> user experiences and maybe lead to some interesting pre-render-in-a-worker-like concepts.</p>

<p>Deadends. Useful deadends mind you, but deadends. Useful because I really like the tooling and ideas in many new frameworks like Astro, the built-in SSR mode of vite, and the exploration of minimalism that lit and qwik and htmx are driving (and I don’t think we fully get there without the failure of SSR as a performance fix).</p>

<p>But there were ideas in SSR I did like: the idea that we could multiplex all the requests associated to a given page visit. The idea that we could flatten waterfalls. The idea that we could leverage how the internet backbone prioritizes data. The idea that we could shift some compute closer to the user to boost their experience. The idea that we could do this progressively overtop of existing applications as little more than an opt-in enhancement without requiring teams to adopt a whole new paradigm or language.</p>

<p>And what I like most about the EdgePipe approach: it even should work for fairly lightweight minimal apps that do a lot of their work on the server.. because it is fundamentally around optimizing the fetch pipeline wherever one exists.</p>

<p>Anyhew, so those are EdgePipes and if you’ve been full of WTFs wondering what I meant talking about SSD these past few years now you know 💜</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[EdgePipes | The Alternative to SSR and RSCs]]></summary></entry><entry><title type="html">Adventures in WarpDrive | Cascade on Delete</title><link href="https://runspired.com/2024/11/29/cascade-on-delete.html" rel="alternate" type="text/html" title="Adventures in WarpDrive | Cascade on Delete" /><published>2024-11-29T00:00:00+00:00</published><updated>2024-11-29T00:00:00+00:00</updated><id>https://runspired.com/2024/11/29/cascade-on-delete</id><content type="html" xml:base="https://runspired.com/2024/11/29/cascade-on-delete.html"><![CDATA[<h1 id="adventures-in-warpdrive--cascade-on-delete">Adventures in WarpDrive | Cascade on Delete</h1>

<p>Recently at <a href="https://www.auditboard.com/">AuditBoard</a> we had a case come up where we needed to perform some additional cleanup whenever certain records were deleted.</p>

<p>For instance: imagine you have both a <code class="language-plaintext highlighter-rouge">user</code> and one or more <code class="language-plaintext highlighter-rouge">search-result</code> resources, where <code class="language-plaintext highlighter-rouge">search-result</code> contains a link to the full user and a few fields related to a search query or used as a table row. When <code class="language-plaintext highlighter-rouge">user:1</code> is deleted, you want to ensure that any <code class="language-plaintext highlighter-rouge">search-result:X</code> related to <code class="language-plaintext highlighter-rouge">user:1</code> is also deleted, because their existence no longer makes sense.</p>

<p>This could be achieved by writing a function <code class="language-plaintext highlighter-rouge">deleteUser</code> that you use anywhere a user is deleted that handles deleting both the user and iterating available search-results and deleting any that pointed at the user, or by manually handling this logic in each location in the code that requires it.</p>

<p>This approach doesn’t scale well. Performance falls off the more kinds of search-results you might need to handle, and the more data you have in cache. In large systems, this can also become brittle: a developer may forget to handle the extra deletions or minor variations in the code that does so might create divergent expectations over time.</p>

<p>Faced with this problem recently I recognized it as a variant of “cascade on delete”, a feature some users of EmberData/WarpDrive have wanted for ages. I also recognized this was doable now over some of the newer public APIs, and could be made performant by using some of the (still private but iterating towards public) Graph APIs.</p>

<h2 id="step-1-extending-our-store-with-the-new-behavior">Step 1: Extending our Store with the new behavior</h2>

<p>I figured <code class="language-plaintext highlighter-rouge">ResourcePolicy</code> was a good name for the primitive I wanted (to mirror <code class="language-plaintext highlighter-rouge">CachePolicy</code>)
and set out to write one. I started with scaffolding the shape of the policy and integrating it
with our store.</p>

<p>For this, I wanted to support both an upfront static config and the ability to dynamically
add to the config overtime (we deliver configuration from our API alongside schema information
for the types of records we typically care about this feature for).</p>

<p><em>./resource-policy.ts</em></p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">Store</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./store</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">type</span> <span class="nx">ResourcePolicyConfig</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">onDeleteAssociated</span><span class="p">:</span> <span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="nb">Set</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;&gt;</span><span class="p">;</span>
<span class="p">};</span>

<span class="cm">/**
 * The ResourcePolicy manages rules for cleanup whenever a record is deleted,
 * allowing for more advanced behaviors like cascading or associated deletes.
 */</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">ResourcePolicy</span> <span class="p">{</span>
  <span class="nl">store</span><span class="p">:</span> <span class="nx">Store</span><span class="p">;</span>
  <span class="nl">policy</span><span class="p">:</span> <span class="nx">ResourcePolicyConfig</span><span class="p">;</span>

  <span class="kd">constructor</span><span class="p">(</span><span class="nx">store</span><span class="p">:</span> <span class="nx">Store</span><span class="p">,</span> <span class="nx">policy</span><span class="p">:</span> <span class="nx">ResourcePolicyConfig</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">store</span> <span class="o">=</span> <span class="nx">store</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">policy</span> <span class="o">=</span> <span class="nx">policy</span><span class="p">;</span>
  <span class="p">}</span>

    <span class="cm">/**
   * Adds a rule to attempt deletion of records of the `associatedType` when
   * a record of the `type` is deleted. This will only work for 1:none relationships
   * where the associated record has a one-way relationship to the primary type.
   *
   * Thus this is a very limited feature and should be used with caution, it is
   * primarily intended for use as a cascade delete on implicit relationships of
   * dynamically generated records.
   */</span>
  <span class="nx">addAssociatedDelete</span><span class="p">(</span><span class="kd">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">associatedType</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">policy</span><span class="p">.</span><span class="nx">onDeleteAssociated</span><span class="p">[</span><span class="kd">type</span><span class="p">]</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">policy</span><span class="p">.</span><span class="nx">onDeleteAssociated</span><span class="p">[</span><span class="kd">type</span><span class="p">]</span> <span class="o">??</span> <span class="k">new</span> <span class="nb">Set</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">policy</span><span class="p">.</span><span class="nx">onDeleteAssociated</span><span class="p">[</span><span class="kd">type</span><span class="p">].</span><span class="nx">add</span><span class="p">(</span><span class="nx">associatedType</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><em>./store.ts</em></p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Store</span><span class="p">,</span> <span class="p">{</span> <span class="nx">recordIdentifierFor</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/store</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ResourcePolicy</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./resource-policy.ts</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppStore</span> <span class="kd">extends</span> <span class="nx">Store</span> <span class="p">{</span>
  <span class="cm">/**
   * The ResourcePolicy manages rules for cleanup whenever a record is deleted,
   * 
   * You should not need to interact with this policy directly.
   */</span>
  <span class="nx">resourcePolicy</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ResourcePolicy</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="p">{</span>
    <span class="c1">// Note: this can be dynamically populared via handler based on request response meta</span>
    <span class="na">onDeleteAssociated</span><span class="p">:</span> <span class="p">{},</span>
  <span class="p">});</span>

  <span class="c1">// .. other store config</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="subscribing-to-cache-updates">Subscribing to Cache Updates</h2>

<p>One of the newer features in EmberData/WarpDrive is the <a href="https://api.emberjs.com/ember-data/release/classes/NotificationManager">NotificationManager</a>. By “newer” this feature has existed since the mid-3.x series, but its capabilities have expanded with time and it is not a feature that has generally been surfaced for general use (we should probably change that, consider this post your introduction).</p>

<p>The NotificationManager is actually how WarpDrive manages intelligent reactivity. Each UI Object that the store creates for the application (records, record arrays, documents etc.) uses this API to subscribe to the cache for updates. When an update occurs, the UI Object dirties any reactive signals for state that has changed.</p>

<p>This is also how the EmberInspector currently integrates with the store to watch for changes to the cache for its own use.</p>

<p>In addition to being able to subscribe to the changes to a specific document or resource, the NotificationManager allows subscribing to changes to <code class="language-plaintext highlighter-rouge">'added' | 'removed' | 'updated' | 'state'</code> for any resource or document. We’re going to make use of that for this feature:</p>

<p>First, I updated the constructor to give us somewhere to store information about data that has
been recently removed and method call to kickoff our subscription handling.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gi">+import type { StableRecordIdentifier } from '@warp-drive/core-types';
+import type { CacheOperation } from '@ember-data/store';
</span> import type { Store } from './store';

 type ResourcePolicyConfig = {
   onDeleteAssociated: Record&lt;string, Set&lt;string&gt;&gt;;
 };

 /**
  * The ResourcePolicy manages rules for cleanup whenever a record is deleted,
  * allowing for more advanced behaviors like cascading or associated deletes.
  */
 export class ResourcePolicy {
   store: Store;
   policy: ResourcePolicyConfig;
<span class="gi">+  recentlyRemoved: WeakSet&lt;StableRecordIdentifier&gt;;
</span>
   constructor(store: Store, policy: ResourcePolicyConfig) {
     this.store = store;
     this.policy = policy;
<span class="gi">+    this.recentlyRemoved = new WeakSet();
</span>
+    this._setup();
   }

   // ... more below
 }
</code></pre></div></div>

<p>Then I setup our subscriptions:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">class</span> <span class="nx">ResourcePolicy</span> <span class="p">{</span>

   <span class="c1">// .. more between</span>
  
  <span class="nx">_setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">notifications</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">;</span>

    <span class="c1">// any time a resource change occurs</span>
  <span class="nx">notifications</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="dl">'</span><span class="s1">resource</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">identifier</span><span class="p">:</span> <span class="nx">StableRecordIdentifier</span><span class="p">,</span> <span class="kd">type</span><span class="p">:</span> <span class="nx">CacheOperation</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="c1">// don't do any special handling for newly created, unsaved records</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">identifier</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">switch</span> <span class="p">(</span><span class="kd">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// if the change is a deletion, consider if we need to delete associated records</span>
    <span class="k">case</span> <span class="dl">'</span><span class="s1">removed</span><span class="dl">'</span><span class="p">:</span>
      <span class="k">void</span> <span class="k">this</span><span class="p">.</span><span class="nx">_onDeleteAssociated</span><span class="p">(</span><span class="nx">identifier</span><span class="p">);</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="p">});</span>
    <span class="p">}</span>

  <span class="c1">// .. more below</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="performing-the-cascadeassociated-delete">Performing the Cascade/Associated Delete</h2>

<p>Ok so this part is going to get a little messy. Here’s the full implementation of <code class="language-plaintext highlighter-rouge">_onDeleteAssociated</code> to get oriented with. It’s annotated but afterwards I’ll also walk through a few of the salient points:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">assert</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember/debug</span><span class="dl">'</span><span class="p">;</span>

<span class="k">import</span> <span class="kd">type</span> <span class="p">{</span> <span class="nx">GraphEdge</span><span class="p">,</span> <span class="nx">ImplicitEdge</span><span class="p">,</span> <span class="nx">ResourceEdge</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/graph/-private</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">peekGraph</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/graph/-private</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// ... more between</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">ResourcePolicy</span> <span class="p">{</span>

  <span class="c1">// ... more between</span>

  <span class="nx">_onDeleteAssociated</span><span class="p">(</span><span class="nx">identifier</span><span class="p">:</span> <span class="nx">StableRecordIdentifier</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// This guards against multiple notifications for removal of the same</span>
    <span class="c1">// record, which occurs in (at least) 4.12 due to multiple parts of the</span>
    <span class="c1">// cache independently reporting the removal during cleanup.</span>
    <span class="c1">//</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">recentlyRemoved</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">identifier</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
    
    <span class="k">this</span><span class="p">.</span><span class="nx">recentlyRemoved</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">identifier</span><span class="p">);</span>

    <span class="nx">assert</span><span class="p">(</span><span class="dl">'</span><span class="s1">identifier must have an id</span><span class="dl">'</span><span class="p">,</span> <span class="nx">identifier</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">store</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="kd">type</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">identifier</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">associated</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">policy</span><span class="p">.</span><span class="nx">onDeleteAssociated</span><span class="p">?.[</span><span class="kd">type</span><span class="p">];</span>


    <span class="c1">// if we have no rule for this type, no cleanup to attempt</span>
    <span class="c1">//</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">associated</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>


    <span class="c1">// for our app, the 1:1 case is simple because our API endpoints</span>
    <span class="c1">// re-use the ID ala `query-result-user` and `user` share the same ID.</span>
    <span class="c1">// if we were to start using this logic for more than that case, we would</span>
    <span class="c1">// remove this optimization</span>
    <span class="c1">//</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">associated</span><span class="p">.</span><span class="nx">size</span> <span class="o">===</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">associatedType</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">associated</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span><span class="o">!</span><span class="p">;</span>
      <span class="kd">const</span> <span class="nx">record</span> <span class="o">=</span> <span class="nx">store</span><span class="p">.</span><span class="nx">peekRecord</span><span class="p">(</span><span class="nx">associatedType</span><span class="p">,</span> <span class="nx">identifier</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">record</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">store</span><span class="p">.</span><span class="nx">unloadRecord</span><span class="p">(</span><span class="nx">record</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// we need to find the implicitly related record</span>
    <span class="c1">// and then determine if all of its relationships are now empty</span>
    <span class="c1">// and only remove it if so: we use the graph to determine this.</span>
    <span class="c1">//</span>
    <span class="kd">const</span> <span class="nx">graph</span> <span class="o">=</span> <span class="nx">peekGraph</span><span class="p">(</span><span class="nx">store</span><span class="p">)</span><span class="o">!</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">edgeStorage</span> <span class="o">=</span> <span class="nx">graph</span><span class="p">?.</span><span class="nx">identifiers</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">identifier</span><span class="p">);</span>

    <span class="c1">// If there are no edges, there are no relationships</span>
    <span class="c1">//</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">edgeStorage</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>

    
    <span class="c1">// for our app's specific scenario, we only wanted to unload the record</span>
    <span class="c1">// if all associated relationships were now empty</span>
    <span class="c1">//</span>
    <span class="kd">const</span> <span class="nx">toUnload</span> <span class="o">=</span> <span class="p">[];</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">associatedType</span> <span class="k">of</span> <span class="nx">associated</span><span class="p">)</span> <span class="p">{</span>

      <span class="c1">// implicit keys match the pattern `implicit-${associatedType}:${inverseName}${randomNumber}`</span>
      <span class="c1">// gaining access to implicit keys via an explicit API is a feature we need to add when we</span>
      <span class="c1">// mark the Graph as a fully public API</span>
      <span class="c1">//</span>
      <span class="kd">const</span> <span class="nx">keys</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">edgeStorage</span><span class="p">).</span><span class="nx">filter</span><span class="p">(</span>
        <span class="p">(</span><span class="nx">key</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">key</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="s2">`implicit-</span><span class="p">${</span><span class="nx">associatedType</span><span class="p">}</span><span class="s2">:`</span><span class="p">)</span>
      <span class="p">);</span>
      <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">keys</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
      <span class="nx">assert</span><span class="p">(</span><span class="dl">'</span><span class="s1">expected to find a key</span><span class="dl">'</span><span class="p">,</span> <span class="nx">key</span><span class="p">);</span>
      <span class="nx">assert</span><span class="p">(</span><span class="s2">`expected to only find one key, found </span><span class="p">${</span><span class="nx">keys</span><span class="p">.</span><span class="nx">length</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="nx">keys</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">1</span><span class="p">);</span>

      <span class="kd">const</span> <span class="nx">edge</span> <span class="o">=</span> <span class="nx">edgeStorage</span><span class="p">[</span><span class="nx">key</span><span class="p">];</span>
      <span class="nx">assert</span><span class="p">(</span><span class="dl">'</span><span class="s1">expected to find an implicit edge</span><span class="dl">'</span><span class="p">,</span> <span class="nx">edge</span> <span class="o">&amp;&amp;</span> <span class="nx">isImplicitEdge</span><span class="p">(</span><span class="nx">edge</span><span class="p">));</span>
      <span class="kd">const</span> <span class="nx">associatedIdentifers</span> <span class="o">=</span> <span class="nx">edge</span><span class="p">.</span><span class="nx">remoteMembers</span><span class="p">;</span>

      <span class="c1">// yup, that's a label. I hate me too but they are useful in this scenario.</span>
      <span class="nl">gc</span><span class="p">:</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">associatedIdentifier</span> <span class="k">of</span> <span class="nx">associatedIdentifers</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// for each associated identifier, if all of it's own</span>
        <span class="c1">// relationships are empty (not including the one we're</span>
        <span class="c1">// deleting as it may not have been cleaned up yet),</span>
        <span class="c1">// then we can remove it.</span>
        <span class="c1">//</span>
        <span class="kd">const</span> <span class="nx">associatedStorage</span> <span class="o">=</span> <span class="nx">graph</span><span class="p">.</span><span class="nx">identifiers</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">associatedIdentifier</span><span class="p">);</span>
        <span class="nx">assert</span><span class="p">(</span>
          <span class="s2">`expected to find associated storage for </span><span class="p">${</span><span class="nx">associatedIdentifier</span><span class="p">.</span><span class="nx">lid</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
          <span class="nx">associatedStorage</span>
        <span class="p">);</span>

        <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">assocKey</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">associatedStorage</span><span class="p">))</span> <span class="p">{</span>
          <span class="kd">const</span> <span class="nx">assocEdge</span><span class="p">:</span> <span class="nx">GraphEdge</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="o">=</span> <span class="nx">associatedStorage</span><span class="p">[</span><span class="nx">assocKey</span><span class="p">];</span>
          <span class="nx">assert</span><span class="p">(</span><span class="dl">'</span><span class="s1">expected to find a belongsTo edge</span><span class="dl">'</span><span class="p">,</span> <span class="nx">assocEdge</span> <span class="o">&amp;&amp;</span> <span class="nx">isBelongsToEdge</span><span class="p">(</span><span class="nx">assocEdge</span><span class="p">));</span>

          <span class="k">if</span> <span class="p">(</span><span class="nx">assocEdge</span><span class="p">.</span><span class="nx">remoteState</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
            <span class="c1">// if this edge is the edge that kicked off the deletion,</span>
            <span class="c1">// we treat it as removed even though the state is still</span>
            <span class="c1">// present in the graph.</span>
            <span class="c1">//</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">assocEdge</span><span class="p">.</span><span class="nx">remoteState</span> <span class="o">===</span> <span class="nx">identifier</span><span class="p">)</span>
              <span class="k">continue</span><span class="p">;</span>


            <span class="c1">// if we have remoteState that is not the originating</span>
            <span class="c1">// identifier, then this record cannot be removed, so</span>
            <span class="c1">// we break out both the inner and the outer loop.</span>
            <span class="c1">//</span>
            <span class="k">break</span> <span class="nx">gc</span><span class="p">;</span>
          <span class="p">}</span>
        <span class="p">}</span>

        <span class="c1">// if we made here, then all of the associated record's</span>
        <span class="c1">// relationships are empty and we can remove the record.</span>
        <span class="c1">// </span>
        <span class="kd">const</span> <span class="nx">record</span> <span class="o">=</span> <span class="nx">store</span><span class="p">.</span><span class="nx">peekRecord</span><span class="p">(</span><span class="nx">associatedIdentifier</span><span class="p">);</span>
        <span class="nx">assert</span><span class="p">(</span><span class="dl">'</span><span class="s1">expected to find a record</span><span class="dl">'</span><span class="p">,</span> <span class="nx">record</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">record</span><span class="p">)</span> <span class="p">{</span>
          <span class="nx">toUnload</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">record</span><span class="p">);</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">toUnload</span><span class="p">.</span><span class="nx">length</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">record</span> <span class="k">of</span> <span class="nx">toUnload</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">store</span><span class="p">.</span><span class="nx">unloadRecord</span><span class="p">(</span><span class="nx">record</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">isBelongsToEdge</span><span class="p">(</span><span class="nx">edge</span><span class="p">:</span> <span class="nx">GraphEdge</span><span class="p">):</span> <span class="nx">edge</span> <span class="k">is</span> <span class="nx">ResourceEdge</span> <span class="p">{</span>
	<span class="k">return</span> <span class="nx">edge</span><span class="p">.</span><span class="nx">definition</span><span class="p">.</span><span class="nx">kind</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">belongsTo</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">isImplicitEdge</span><span class="p">(</span><span class="nx">edge</span><span class="p">:</span> <span class="nx">GraphEdge</span><span class="p">):</span> <span class="nx">edge</span> <span class="k">is</span> <span class="nx">ImplicitEdge</span> <span class="p">{</span>
	<span class="k">return</span> <span class="nx">edge</span><span class="p">.</span><span class="nx">definition</span><span class="p">.</span><span class="nx">isImplicit</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Ok, so in this implementation a few things were specific to AuditBoard’s use cases:</p>

<ul>
  <li>the associated records for which we cascade the delete only ever utilize belongsTo relationships, never hasMany relationships.</li>
  <li>the associated records sometimes (but not always) contain more than one belongsTo relationship, in the single case there’s a simple fully public API to quickly perform the removal</li>
  <li>these belongsTo relationships are all implemented as one-directional (<code class="language-plaintext highlighter-rouge">inverse: null</code>)</li>
</ul>

<p>All this means for you is that if you want to implement a similar concept in your app, its best to first understand the requirements your app has around cascading deletes. There are ways to handle hasMany relationships, cascade-on-delete for regular relationships, cascade-on-delete for completely unrelated records etc: you just need to know the rules and shape of the problem as it pertains to your app.</p>

<p>That is what is great about the NotificationManager, its a simple API but it lets us quickly write code that can do complex things within our specific app-domain logic.</p>

<p>The two questions you probably have come from the code handling what happens when one of our associated records relates to more than one type: what is the graph, and what are implicit edges.</p>

<h2 id="the-graph">The Graph</h2>

<p>The Graph is a library used by the JSON:API Cache which we built so that cache implementations (for any format) could delegate one of the hardest problems (managing relationships) to a primitive built and optimized for it.</p>

<p>We’ve kept the API for it private to this point because we expect to change the implementation of it quite a bit while adding support for paginated relationships, and there are some open questions around whether it should also become how documents are stored (the answer is probably “yes”).</p>

<p>The Graph is just that, a graph: it stores the relationships (directional Edges) between resource CacheKeys (identifiers).</p>

<h2 id="implicit-edges">Implicit Edges</h2>

<p>When the Graph encounters a one-direction relationship e.g. something like a belongsTo or a hasMany with <code class="language-plaintext highlighter-rouge">inverse: null</code>, it creates an implicit inverse relationship for convenience and performance.</p>

<p>For instance, lets say a user has a car, the car gets into an accident and is totaled, and now the user does not have a car.</p>

<p>We might model this as a hasMany (<code class="language-plaintext highlighter-rouge">user.cars</code>) pointing at a belongsTo (<code class="language-plaintext highlighter-rouge">car.owner</code>), or we might model this with <code class="language-plaintext highlighter-rouge">inverse: null</code> (e.g. just <code class="language-plaintext highlighter-rouge">user.cars</code>, and no inverse on the car).</p>

<p>In the case there is no inverse, when the car is totaled and thus deleted, we need to know what relationships that car was in to remove its entry. That’s where the implicit edge comes in, it tells us exactly what relationships currently point at the car, and so following them backwards we can quickly remove the car from all associated relationships.</p>

<p>This feature is one of the many small niceties of working with relationships in EmberData/WarpDrive, and it happens to sound exactly like the information we need for cascade-on-delete!</p>

<p>To be clear, we could implement this with public API by iterating all records of the associated type and checking the current values of their relationships, but that is a far more expensive operation as it requires iterating far more values, and potentially creating ui objects for them and their relationships.</p>

<p>So in our case with <code class="language-plaintext highlighter-rouge">user</code> and <code class="language-plaintext highlighter-rouge">search-result</code>, we reach in to the list of implicit edges in the graph for the user that got deleted, get the list, and for each implicit relationship if it matches our associated type (<code class="language-plaintext highlighter-rouge">search-result</code>) we perform the deletion check. Elegant really, though I wish we were closer to making this information publically and conveniently accessible. I don’t like reaching into private APIs either 🙈</p>

<p>This exploration though lets me feel out what information should be public and what (future) public APIs are missing from the Graph in its current state.</p>

<hr />

<p>This has been an Adventure in WarpDrive. Stay tuned for our next episode!</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[Adventures in WarpDrive | Cascade on Delete]]></summary></entry><entry><title type="html">Web Reflections</title><link href="https://runspired.com/2024/11/27/web-reflections.html" rel="alternate" type="text/html" title="Web Reflections" /><published>2024-11-27T00:00:00+00:00</published><updated>2024-11-27T00:00:00+00:00</updated><id>https://runspired.com/2024/11/27/web-reflections</id><content type="html" xml:base="https://runspired.com/2024/11/27/web-reflections.html"><![CDATA[<h1 id="web-reflections">Web Reflections</h1>

<p>I recently came back upon this 2017 blog post from Tom Dale considering whether <a href="https://tomdale.net/2017/09/compilers-are-the-new-frameworks/">compilers are the new frameworks</a>. If you haven’t read it, its short, I recommend doing so and before popping back over here to continue.</p>

<p>A few of his points:</p>
<ul>
  <li>We’ve reached the point of diminishing returns on improving runtime JS performance</li>
  <li>We’ll transpile, compile and minify <em>even more</em> as time goes on</li>
  <li>JS (in 2017) is about to have the building blocks for new app paradigms: WebAssembly, SharedArrayBuffer, Atomics, etc</li>
  <li>To predict the future of the web, look at what high-performance native systems do</li>
  <li>This will lead to better performance and bundle size</li>
</ul>

<p>What is fun about a post like this, now seven years old, is we can see the results of those predictions and insights.</p>

<p>In my opinion, Tom was about 90% right on how things were going to evolve, but way off on the conclusion of it leading to a better more performant web.</p>

<p>The investment in compilers and tooling pipelines has largely not led to a reduction in bytes sent to the browser, but an increase. Performance hasn’t improved from the move to compilation, and its maybe decreased.</p>

<p>In fact, the bigger your toolchain for compilation the more likely it has become that you ship more JS than you should. Why did this happen? And was this outcome forseeable? I think it was, and I think three things were at play that Tom didn’t seem to consider:</p>

<h3 id="1-human-factors">1. Human Factors</h3>

<p>We tend to get what we want, and what we want is generally to ship features and product. More powerful toolchains allowing app sizes to scale with the size of the product and team just meant we could ship more features and product.</p>

<p>This is basically the same observation that even while CPUs have gotten more powerful our programs seem to be slowing down. These tools and compilers can result in better performance, but only if we <em>don’t look at the new space in the budget and immediately overspend it.</em></p>

<h3 id="2-the-platform">2. The Platform</h3>

<p>Looking back now, its evident the platform had not stopped evolving, and the performance and bundle size impact of many newer platform APIs is not only massive, but significantly larger than a compiler could ever achieve.</p>

<p>Animations are really great example of this. The APIs for WebAnimations, ViewTransitions and generators together replaced the need for the animation libraries that previously were among our largest dependencies.</p>

<p>There’s an important lesson here: <em>No compiler can beat shipping zero-bytes.</em></p>

<blockquote>
  <p>No compiler can beat shipping zero-bytes</p>
</blockquote>

<p>The examples of this sort of platform win are everywhere: CSS layers, variables, advanced selectors, improved media query capabilities. Each of these replaced JS-runtime or CSS-compiler based solutions that had high runtime or bundlesize costs.</p>

<p>These wins happened even at the language level: browsers finally implemented generator functions, fields, and private fields natively. Currently they’ve started implementation of decorators and accessors. These have played a massive role in reducing the amount of bytes we need to ship in addition to generally being faster implementations.</p>

<p>And at the JS SDK Level: 10 different implementations of UUID in your bundle? Use crypto.randomUUID. A lot of logic to diff arrays and sets? There are APIs for unions, intersections, and more now.</p>

<p>I work on an SPA that is over 12 years old. More often than not when I’m refactoring something it looks like deleting a lot of code and replacing it with a simpler, smaller platform API.</p>

<p>So to sum up, I think betting on compilers has done much less than betting on the platform. And personally I believe there are still a lot of gains to be had in that area!</p>

<p>Maybe Tom was right though, and maybe we’ve done exactly as he thought we should: <em>we looked at what made high-performance native code great, and realized it was the platform.</em></p>

<h3 id="3-complexity">3. Complexity</h3>

<p>There’s a key point missing in Tom’s post. A point I think was able to be seen then, and even addressed then but was nonetheless missed. A point that I think the web community as a whole missed for the better part of a decade: <em>If you want faster apps, reduce their complexity.</em></p>

<blockquote>
  <p>If you want faster apps, reduce their complexity.</p>
</blockquote>

<p>Tom’s ideas mostly related to how to optimize complexity that already exists: but in the same way that no compiler can ever beat shipping 0 bytes, no compiler can optimize a for-loop to be faster than no-loop (unless of course for the case where the loop has no outputs and can just be safely deleted).</p>

<p>This incidentally has been where my own chips have been pushed. Complex frontend JavaScript apps are just a symptom of an underlying disease: JS is not the problem.</p>

<p>I think a lot of our performance issues trace back to poor architectural decisions around how we model, retrieve, transform and mutate state. I don’t just mean within apps either: there’s been a huge failure of imagination when it comes to designing API Frameworks and formats.</p>

<p>It’s amazing how we’ll spend hours fighting over which framework renders a component millionths of a second faster, and then dump in a MB of JSON we’ve given almost no thought to the shape of without a care in the world, probably in a waterfall pattern.</p>

<p>The rise of RSCs, HTMX, Qwik, LiveWire and (to a lesser extent) Astro are in my mind at least in part a reaction to realizing that the performance issue wasn’t actually a JS issue but a data issue.</p>

<p>So too are the many local-first DB offerings like Replicache: they just went the other way on the same problem and decided “if we’re sending it all there anyway, why not just keep it there”.</p>

<p>But in my opinion these choices are too often trying to sidestep the problem without really solving it. This does not make these bad solutions or bad tools in any way, some of them I even think are amazing and will shape the architecture of apps for the next decade in a positive way. But this does mean there’s more out there for us to work to solve still.</p>

<h3 id="compilers-are-ok">Compilers Are OK</h3>

<p>Don’t mistake this for a screed against compilers. I think compilers are doing amazing things and will continue to play a critical role in the evolution of the web. Its a mistake though to think we can compile away all our problems or to treat them as the panacea for performance.</p>

<p>So here’s my advice for anyone who wants to make a dent in the future of web development: invest in the platform, and be thoughtful in your data design.</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[Web Reflections]]></summary></entry><entry><title type="html">Polaris | What I’m Working On</title><link href="https://runspired.com/2024/11/27/what-im-working-on.html" rel="alternate" type="text/html" title="Polaris | What I’m Working On" /><published>2024-11-27T00:00:00+00:00</published><updated>2024-11-27T00:00:00+00:00</updated><id>https://runspired.com/2024/11/27/what-im-working-on</id><content type="html" xml:base="https://runspired.com/2024/11/27/what-im-working-on.html"><![CDATA[<h1 id="polaris-what-im-working-on">Polaris: What I’m Working On</h1>

<p>You’d think being on a core team (emberjs) I’d know what other core team members are exploring, but the reality is often I know just as much as the next person about where other team members have wandered off to poke and prod. So it was refreshing to read <a href="https://yehudakatz.com/2024/10/28/polaris-what-im-working-on/">Yehuda’s blog post</a> covering his recent meanderings, and I figured I’d add my own musings to the mix.</p>

<h2 id="shipping-ember-data-413">Shipping ember-data 4.13</h2>

<p>Polaris isn’t a version, its a concept. A set of APIs, paradigms, and patterns that flow together cohesively. If it were just a point-in-time version what I’m going to say next would be <em>really strange</em>.</p>

<p>I’ve been working hard on shipping the Polaris story for <a href="https://github.com/emberjs/data/pull/9598">ember-data v4</a>. Yes, V4 - the prior major cycle that ended 20 months ago.</p>

<p>As we’ve worked to evolve EmberData into WarpDrive in the 5.x cycle we’ve been keeping a steady flow of improvements into 4.12 to attempt to maintain enough alignment that apps can make the jump seamlessly.</p>

<p>But two of the biggest improvements in the 5.x cycle (namely types and vite support) couldn’t be easily backported: and because they couldn’t be backported it was getting exponentially harder to backport fixes and improvements to the cache, schema service, and request-manager needed to enable applications using 4.12 to be compatible with SchemaRecord and the full feature suite offered by <code class="language-plaintext highlighter-rouge">@warp-drive/ember</code>.</p>

<p>We want apps stuck in 4.x to be able to use as many Polaris idioms as possible, being able to do so makes the upgrade path and adoption process simpler, and it gives extended breathing room to larger apps that need more time to upgrade.</p>

<p>And so, we have reactivated the v4 release line and will soon release 4.13. 4.13 will</p>

<ul>
  <li>have its own native types</li>
  <li>be compatible with vite</li>
  <li>contain all bug fixes and improvements from the 5.x series</li>
  <li>maintain all deprecated features from the 4.x series (duh)</li>
  <li>allow activation of deprecations from the 5.x series (these are off by default, so no new deprecations are getting added, but you can choose to opt-in early)</li>
</ul>

<h2 id="shipping-ember-data-54">Shipping ember-data 5.4</h2>

<p>The 5.4 release has been long overdue, but we didn’t want to run away from 4.x users looking for an easier migration path until we had more of a cohesive vision implemented of what the final destination for them was going to look like.</p>

<p>Now we do: we’re putting the finishing touches on the first release of SchemaRecord and expect to ship by EOY.</p>

<p>What may have gone unnoticed though is that while we haven’t shipped a new minor of <code class="language-plaintext highlighter-rouge">ember-data</code> in a long time, we have continued to ship new patches of <code class="language-plaintext highlighter-rouge">5.3</code>. In fact, every improvement and fix in <code class="language-plaintext highlighter-rouge">5.4</code> is already in <code class="language-plaintext highlighter-rouge">5.3</code>! This is because <code class="language-plaintext highlighter-rouge">5.4</code> is symbolic more than anything, when we ship and start regular minor cadence again it will signify that the new paradigm is ready for you to try out.</p>

<p>This means that 5.3 supports vite 💜</p>

<h2 id="planning-the-replacement-for-unloadall-and-unloadrecord">Planning the replacement for unloadAll and unloadRecord</h2>

<p><code class="language-plaintext highlighter-rouge">unloadRecord</code> and <code class="language-plaintext highlighter-rouge">unloadAll</code> are two APIs that barely made sense in the resource-centric past of EmberData and make no-sense in the request-centric future of WarpDrive. They’ve been crutches for apps that really needed to care about memory management.</p>

<p>But there’s been no design push to replace them because its not been totally clear what a good general-purpose replacement for them would be that doesn’t immediately fall victim to the same issues they have around accidentally creating broken state in the app.</p>

<p>With that in mind, I’ve been exploring an alternative idea around how to implement a robust but performant Garbage Collection (GC) feature for WarpDrive on-and-off for the past several years.</p>

<p>Two of the sticking points were:</p>
<ul>
  <li>in the legacy setup, relationships MUST be retainers which often makes GC near impossible</li>
  <li>it seems to require a reference-counting approach, which is known to be algorithmically slow and error prone</li>
  <li>freeing instances and data from the cache isn’t a great decision if you’re going to want that data again really quickly thereafter.</li>
</ul>

<p>To that end, I finally realized that fully-embracing the request-centric world allows us to treat requests as roots and thus implement a tracing-gc approach. Moreover, in this paradigm we no longer need to strictly consider relationships as retainers (there’s a couple of edge cases around mutation where you do).</p>

<p>If you are curious I wrote up a length explanation of how this will work <a href="https://github.com/emberjs/data/pull/9612/commits/3352349217368bdc9635175586bc06923273f7f1">here</a>, which I plan to turn into an RFC soon after we ship 5.4</p>

<p>The third point is solved by integration of the GC with the CachePolicy (we’d respect the soft-expiry as a retention policy more than likely, or add an explicit hook to make it fully configurable), as well as by the PersistedCache experiment.</p>

<h2 id="thinking-about-schemas">Thinking about Schemas</h2>

<p>When I first started talking about SchemaRecord (then called SchemaModel) I envisioned pairing it with an optional Schema DSL to make authoring schemas and types a quick unified experience.</p>

<p>In the interest of time, that DSL has been indefinitely delayed. I’ve come to think of it as a “nice to have” but not an essential, because the experience of authoring JSON schemas and standalone types has so far felt pretty easy and enjoyable.</p>

<p>I’m not sure yet if that feeling I have will extend to everyone coming from the Model world though, and if others want to help take on the work I still think it has a lot of utility.</p>

<h2 id="thinking-about-crdts">Thinking about CRDTs</h2>

<p>So far: I feel they are more hype than useful. My mental model of them is probably not good enough yet. From where I stand though, value-added to the diffs you still need seems low while the implementation and mental model costs feel high. This might change with more familiarity.</p>

<h2 id="thinking-about-persistedcache">Thinking about PersistedCache</h2>

<p>I really want to get mutations into the DataWorker and PersistedCache permitives soon, to do so we need named stores. (There’s also some cache API changes we will want, but those can be experimented with separately).</p>

<p>It would be a fairly small RFC and implementation, I’m tempted to ship one in the next few weeks.</p>

<p>If you haven’t seen DataWorker and PersistedCache, I’d recommend watching my talk from <a href="https://www.youtube.com/watch?v=BCv8OgcG5vM">EmberFest 2024</a></p>

<h2 id="the-scheduler">The Scheduler</h2>

<p>I remain convinced that the <a href="https://github.com/emberjs/rfcs/pull/957">Render Aware Scheduler Interface</a> is one of the largest lifts to DX ember can deliver for Polaris. And I don’t think it would be hard, but none of us have had the time to focus on it. Imagine: no more backburner, no more run loops, no more error traces and call stacks deep in framework code trying to find where your own code with the mistake is.</p>

<h2 id="routing">Routing</h2>

<p>The state of routing in Ember has become a huge bottleneck to shipping new features in WarpDrive. I have so many things waiting in the wings dependent on it: so I’ve been doing a lot of ruminating on how to push exploration there forward: I’m extremely tempted to ship a whole new router, and I don’t think that’s a good thing.</p>

<p>A few of the things I’m looking to explore:</p>

<ul>
  <li>An EdgeServer that eliminates fetch waterfalls and allows any app to utilize the big-pipe approach</li>
  <li>route request pre-fetch via the DataWorker</li>
  <li>Turning Ember apps into MPAs</li>
  <li>using context to provide route requests to component trees</li>
</ul>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Route</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/ember</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">params</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// return value can either be a request Future, promise or an object.</span>
  <span class="c1">// if it is an object, each key of the object should point at</span>
  <span class="c1">// either a request Future, promise or a value.</span>
  <span class="c1">// the function should not be marked `async`</span>
  <span class="c1">// the return will become the `@route` arg provided to the template.</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">MyRoute</span> <span class="o">=</span> <span class="o">&lt;</span><span class="nx">template</span><span class="o">&gt;</span>
  <span class="nx">access</span> <span class="nx">the</span> <span class="nx">result</span> <span class="k">of</span> <span class="nx">the</span> <span class="nx">fetch</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">unresolved</span><span class="p">)</span>
  <span class="p">{{@</span><span class="nd">route</span><span class="p">}}</span>

  <span class="nx">component</span> <span class="nx">trees</span> <span class="nx">invoked</span> <span class="nx">here</span> <span class="nx">or</span> <span class="nx">within</span>
  <span class="nx">the</span> <span class="k">yield</span> <span class="nx">would</span> <span class="nx">be</span> <span class="nx">able</span> <span class="nx">to</span> <span class="nx">access</span> <span class="nx">the</span>
  <span class="nx">route</span> <span class="nx">object</span> <span class="nx">via</span> <span class="s2">`consume('@route')`</span>
  <span class="nx">standard</span> <span class="k">yield</span> <span class="nx">also</span> <span class="nx">works</span>

  <span class="p">{{</span><span class="k">yield</span><span class="p">}}</span>
<span class="o">&lt;</span><span class="sr">/template&gt;</span><span class="err">;
</span>
</code></pre></div></div>

<ul>
  <li>using context to provide request results to a component tree</li>
</ul>

<div class="language-hbs highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
const MyRoute = <span class="nt">&lt;template&gt;</span>
  <span class="nt">&lt;Request</span> <span class="err">@</span><span class="na">request=</span><span class="k">{{</span><span class="na">@route</span><span class="p">.</span><span class="nv">someRequest</span><span class="k">}}</span> <span class="err">@</span><span class="na">key=</span><span class="s">"awesomeSauce"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;AwesomeSauceConsumer</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/Request&gt;</span>
<span class="nt">&lt;/template&gt;</span>;

</code></pre></div></div>

<h2 id="testing">Testing</h2>

<p>WarpDrive should offer an integrating testing experience. I’ve been exploring ideas for that with <a href="https://github.com/emberjs/data/tree/main/packages/holodeck#readme">Holodeck</a> and <a href="https://github.com/emberjs/data/tree/main/packages/diagnostic#readme">Diagnostic</a>. The next step is to add a simple router (likely I’ll just use something layer on honojs, which is what I’ve used for a POC of this) and re-use WarpDrive as the ORM Layer.</p>

<p>There’s a few approaches to the setup with differing tradeoffs, all of them still better than the state of things with Mirage or MSW. A big unanswered question is around how to handle API specs. I was hoping to generate from OpenAPI Specs if an API has them, but these seem to lose way too much information vital to robust mocking.</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[Polaris: What I’m Working On]]></summary></entry><entry><title type="html">Modern Ember Data</title><link href="https://runspired.com/2024/01/31/modern-ember-data.html" rel="alternate" type="text/html" title="Modern Ember Data" /><published>2024-01-31T00:00:00+00:00</published><updated>2024-01-31T00:00:00+00:00</updated><id>https://runspired.com/2024/01/31/modern-ember-data</id><content type="html" xml:base="https://runspired.com/2024/01/31/modern-ember-data.html"><![CDATA[<h1 id="not-your-parents-emberdata">Not Your Parent’s EmberData</h1>

<blockquote>
  <p>How RequestManager flips the script on everything</p>
</blockquote>

<hr />

<p>EmberData’s legacy <a href="https://github.com/sproutcore/sproutcore/commit/f6248b1650a688a401cc6eea135fbe983e20cd12">turns 18 this year</a>.</p>

<p>What is remarkable (besides a Javascript project surviving for this long) is how long the 
project lasted without any significant revisiting of its architecture before RequestManager
was added in 2023 for the 4.12 release.</p>

<p>Ok, that’s not totally the truth. We paved the way to RequestManager years before, and began
the internal evolution that would allow for it with the
<a href="https://rfcs.emberjs.com/id/0403-ember-data-identifiers/">RFC for identifiers</a> in 2018. 
But until RequestManager, much of the power that identifiers unlocked remained largely out 
of sight.</p>

<p>In this post, I want to walk through one of the key changes that comes with RequestManager
that highlights the advantages of the changing architecture, as well as tease a bit of what
is still to come: <strong>All requests should use EmberData.</strong></p>

<p>But first, a short overview of the shift to RequestManager.</p>

<h2 id="what-changed">What Changed</h2>

<p>In the past, the request layer in EmberData was an abstraction hidden from the application
developer. Whether and when the store would decide to build and issue a request via an 
adapter vs resolve from cache was a bit of magic.</p>

<p>The good was that this allowed for a uniform, stable API for requesting data to work with.
The bad was that how to update the cache or invalidate it was a bit mysterious.. and at times
downright frustrating.</p>

<p>Regardless of whether you were experiencing the fun or the frustrating aspects of working
with the store, these interactions were <em>resource</em> centric: “find me this record”, “query for
records matching X”, “give me all records of this type”, “save this record”.</p>

<p>Consider the (simplified) signatures of the historical approach:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">Store</span> <span class="p">{</span>
    <span class="nx">findRecord</span><span class="p">(</span><span class="kd">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">RecordInstance</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nx">findAll</span><span class="p">(</span><span class="kd">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">RecordInstance</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nx">queryRecord</span><span class="p">(</span><span class="kd">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">query</span><span class="p">:</span> <span class="nx">object</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">RecordInstance</span><span class="o">&gt;</span><span class="p">;</span>
    <span class="nx">query</span><span class="p">(</span><span class="kd">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">query</span><span class="p">:</span> <span class="nx">object</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">RecordInstance</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Compared to the (again, simplified) signature of the new approach:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">Result</span> <span class="p">{</span>
    <span class="nl">request</span><span class="p">:</span> <span class="nx">FetchInit</span><span class="p">;</span>
    <span class="nl">response</span><span class="p">:</span> <span class="nx">Response</span><span class="p">;</span>
    <span class="nl">content</span><span class="p">:</span> <span class="nx">Document</span><span class="p">;</span>
<span class="p">}</span>

<span class="kr">interface</span> <span class="nx">Store</span> <span class="p">{</span>
    <span class="nx">request</span><span class="p">(</span><span class="nx">options</span><span class="p">:</span> <span class="nx">FetchInit</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">Result</span><span class="o">&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>These interfaces present a rough sketch of the shape of the change, take note these are not
the exact types 😅!</p>

<p>As its name implies: the RequestManager is instead <em>request</em> centric. Instead of answering
questions about specific records or types of records like we used to ask the store in the past, we ask it about the status of a specific request.</p>

<p>Ok, so how does this new API change how we build our applications?</p>

<h2 id="all-requests-should-use-emberdata">All Requests Should Use EmberData</h2>

<blockquote>
  <p>Me: If your application makes even one fetch request, your app should use EmberData.</p>

  <p>Person 2: Wait, really?!</p>

  <p>Me: Yes. Really.</p>

  <p>Person 2: Explain…</p>
</blockquote>

<p>I will sum up. Convert this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">MyService</span> <span class="p">{</span>
  <span class="k">async</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`/api/v1/company`</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
          <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
    <span class="p">});</span>
    <span class="k">return</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Into this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">MyService</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">service</span> <span class="nx">requestManager</span><span class="p">;</span>

    <span class="k">async</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">requestManager</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
        <span class="na">url</span><span class="p">:</span> <span class="s2">`/api/v1/company`</span><span class="p">,</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
          <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
      <span class="p">});</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Note, the above is the 1:1 conversion to keep things “exactly” as they were in your app, and as
it stands it already provides huge albeit hidden value. Below we will unpack that value and
then begin to iteratively migrate to unlock even more value.</p>

<p>Because of its history, there is a temptation to think of EmberData as a <em>resource centric</em>
library, along the lines of an ORM (or worse, a full fledged database).</p>

<p>But as we discussed in the last section, EmberData is now a <em>request centric</em> library. The
extent to which this is true goes far beyond just porting the requests that EmberData used to make magically via adapters into the new paradigm.</p>

<p>If you are doubting right now how EmberData being <em>request centric</em> means you should use it
for all your requests then first, I applaud you for your skepticism. It is invaluable to 
critically analyze the claims a library (or its author) makes.</p>

<p>Second, I feel you. 18 years of doing things one way is a <em>lot</em> of history, built context
and emotions to suddenly toss away. But before you head over to my <a href="https://www.threads.net/@runspired/">threads</a> account 
to drag me for how bad a take this is, hear me out. Then please do so at your earliest 
convenience.</p>

<p>You probably fall into one of two camps:</p>

<ol>
  <li>You use EmberData but you have requests for which you do not use it. If this is you then probably you are more willing to hear me out.</li>
  <li>You don’t use EmberData – perhaps because you ripped it out of your app in a burning fit of passion, and you have no idea outside of either sick revenge or disaster porn why you are reading this post … I don’t know either but I hope you keep reading …</li>
</ol>

<p>Those of you in the first camp, this will probably be an easier sell: you can drop usage of ember-ajax, jquery.ajax, ember-fetch and probably a dozen homegrown internal things you have and instead use a nice uniform interface for managing all manner of requests. I know that doesn’t sway your skepticism that this is possible, but hopefully its at least a small carrot.</p>

<p><strong><em>But WHYyyyyyy?</em></strong></p>

<h3 id="lets-start-with-what-you-gained-in-this-simple-migration">Lets start with what you gained in this simple migration</h3>

<p>I mentioned huge albeit hidden value.</p>

<p>The most basic answer is that RequestManager provides a simple, stable unified interface for 
how we request or mutate data. Developers that love EmberData historically have largely loved
it for this aspect. Yes, you still have to construct fetch requests, but we’ll get to that 
part below in a moment.</p>

<p>A stable unified interface gives you three things.</p>

<p>First, an easier ability to refactor (by lots of different mechanisms).</p>

<p>Similarly – second, an easy integration point to make things happen universally when needed: whether a shared abstraction to reduce cognitive overhead or for implementing a sweeping API change that you can make feel like a tiny bump instead of a major rewrite.</p>

<p>Third, unified expectations of behavior.</p>

<p>In this code example we see all three of these things in play.</p>

<p>First, by unifying on the platform, it was quick to convert our standard fetch request into the request manager paradigm.</p>

<p>Second, we gained all the functionality of whatever handlers we registered for use. Most commonly, this will be the <a href="https://github.com/emberjs/data/blob/5a48f52a08587e59cb529575577880daf678ae00/packages/request/src/fetch.ts">Fetch Handler</a>.</p>

<p>This means that immediately our code example started handling a number of scenarios that it didn’t before, despite no seeming change. Lets unpack what we get that we didn’t have before. In no particular order:</p>

<ul>
  <li>The fetch code is SSR ready</li>
  <li>We account for libraries like mirage that will try to dynamically swap out <code class="language-plaintext highlighter-rouge">fetch</code> for its own version</li>
  <li>A test waiter is automatically installed to help prevent leaky or flakey tests</li>
  <li>AbortController is automatically wired-in</li>
  <li>Streaming Responses are automatically prepared for</li>
  <li>Vague network level errors are converted into meaningful error objects</li>
  <li>The <code class="language-plaintext highlighter-rouge">date</code> header is autoset on the response if not already present to ensure the ability to check request age</li>
  <li>Responses that represent errors are converted into meaningful error objects
    <ul>
      <li>statusText is normalized for http1.1/2/3</li>
      <li>the error message is set to a meaningful string that will be helpful to observability tooling (like Sentry) and avoids common pitfalls of errors being too similar</li>
    </ul>
  </li>
  <li>the handling of parsing the response to JSON is done for you</li>
</ul>

<p>Before this change, had our fetch request failed, the most likely
outcome in many applications would have been no error surfaced to
the application or to observability tooling like Sentry (because <code class="language-plaintext highlighter-rouge">fetch</code> always resolves), or a very confusing and poorly differentiated error
along the lines of <code class="language-plaintext highlighter-rouge">cannot parse token &lt; at json line:0</code>.</p>

<p>And third, we gained expectations of behavior.</p>

<p>Everything that the Fetch handler does is something every developer 
otherwise must do each time individually. But often in the interests of time, terseness, overconfidence in network stability, or due to lack of awareness these things will not be done.</p>

<p>The value of an abstraction like EmberData is that it is able to reduce cognitive and implementation burden on product engineers for these sorts
of considerations, in many cases eliminating that burden entirely.</p>

<p>Whether or not you use EmberData to manage your requests doesn’t change the fact that <em>they need managed</em>. Even just a single, relatively simple request has this need.</p>

<p>But the value doesn’t end here. Lets take our migration further.</p>

<h3 id="taking-our-migration-even-further">Taking our Migration Even Further</h3>

<p>Above we had refactored our service into what is copied again below:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">MyService</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">service</span> <span class="nx">requestManager</span><span class="p">;</span>

    <span class="k">async</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">requestManager</span><span class="p">.</span><span class="nx">request</span><span class="p">({</span>
        <span class="na">url</span><span class="p">:</span> <span class="s2">`/api/v1/company`</span><span class="p">,</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
          <span class="dl">'</span><span class="s1">content-type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
      <span class="p">});</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>It might have been invoked something like this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Route</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">service</span> <span class="nx">myService</span><span class="p">;</span>

    <span class="k">async</span> <span class="nx">model</span><span class="p">({</span> <span class="nx">search</span><span class="p">,</span> <span class="nx">offset</span> <span class="p">})</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nx">search</span><span class="p">,</span>
            <span class="na">sort</span><span class="p">:</span> <span class="dl">'</span><span class="s1">name:asc</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">limit</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
            <span class="na">offset</span><span class="p">:</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">offset</span><span class="p">)</span>
        <span class="p">};</span>
        <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">myService</span><span class="p">.</span><span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>

        <span class="k">return</span> <span class="p">{</span>
            <span class="nx">search</span><span class="p">,</span>
            <span class="nx">offset</span><span class="p">,</span>
            <span class="nx">data</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This service is doing three things for us:</p>

<ol>
  <li>choosing the url</li>
  <li>generating the fetch options</li>
  <li>ensuring a json response</li>
</ol>

<p>Lets start by refactoring it to make use of the RequestManager’s encouraged
pattern of builders and handlers.</p>

<blockquote>
  <p>Builders setup requests, they are functions that may understand the app state and context in which a request is being generated</p>

  <p>Handlers help to fulfill requests, by processing requests or responses in ways that are broadly applicable</p>
</blockquote>

<p>In our current service, everything within the call to <code class="language-plaintext highlighter-rouge">request</code> is something that is immediately a candidate for a builder or a handler.</p>

<p>Lets write just a builder for today. Generally we name builders to follow the natural
meaning of what the request being constructed is intended to do.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="nx">resourcePath</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="na">url</span><span class="p">:</span> <span class="s2">`/api/v1/</span><span class="p">${</span><span class="nx">resourcePath</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That makes our service now look like this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">queryData</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./builders</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">class</span> <span class="nx">MyService</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">service</span> <span class="nx">requestManager</span><span class="p">;</span>

    <span class="k">async</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">requestManager</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span>
        <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="dl">'</span><span class="s1">company</span><span class="dl">'</span><span class="p">)</span>
      <span class="p">);</span>
      <span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">content</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>At which point we realize this custom fetch-wrapping service is no longer useful.</p>

<p>Lets delete it and use RequestManager directly.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gi">+import { queryData } from './builders';
+
</span><span class="p">class Route {
</span><span class="gd">-    @service myService;
</span><span class="gi">+    @service requestManager;
</span>
    async model({ search, offset }) {
<span class="gd">-        const query = {
</span><span class="gi">+        const query = queryData({
</span>            search,
            sort: 'name:asc',
            limit: 50,
            offset: Number(offset)
<span class="gd">-        };
</span><span class="gi">+        }, 'company');
</span><span class="gd">-        const data = await this.myService.queryData(query);
</span><span class="gi">+        const result = await this.requestManager.request(query);
</span>
        return {
            search,
            offset,
<span class="gd">-            data
</span><span class="gi">+            data: result.content
</span>        };
    }
 }
</code></pre></div></div>

<p>Which results in:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">dataQuery</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./builders</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">class</span> <span class="nx">Route</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">service</span> <span class="nx">requestManager</span><span class="p">;</span>

    <span class="k">async</span> <span class="nx">model</span><span class="p">({</span> <span class="nx">search</span><span class="p">,</span> <span class="nx">offset</span> <span class="p">})</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="nx">dataQuery</span><span class="p">({</span>
            <span class="nx">search</span><span class="p">,</span>
            <span class="na">sort</span><span class="p">:</span> <span class="dl">'</span><span class="s1">name:asc</span><span class="dl">'</span><span class="p">,</span>
            <span class="na">limit</span><span class="p">:</span> <span class="mi">50</span><span class="p">,</span>
            <span class="na">offset</span><span class="p">:</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">offset</span><span class="p">)</span>
        <span class="p">},</span> <span class="dl">'</span><span class="s1">company</span><span class="dl">'</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">requestManager</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>

        <span class="k">return</span> <span class="p">{</span>
            <span class="nx">search</span><span class="p">,</span>
            <span class="nx">offset</span><span class="p">,</span>
            <span class="na">data</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">content</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>While very slightly more verbose, this is immediately better than the custom service we had
before in a bunch of ways.</p>

<p>Now the developer knows this is a managed request instead of that context being hidden
behind the extra service, so it level sets expectations of what they can expect to need
to do.</p>

<p>The developer also now gets direct access to the response of the request, which gives them access
to the wired-in AbortController, additional request and response information, and the ability
to stream the response if they choose. Our wrapper while useful was previously discarding the capabilities from being accessible to the developer. “Use the platform” has a friend: “Use the framework”. These things feel invisible, but they are there ready for when they are needed, no workarounds necessary.</p>

<h3 id="incrementally-migrating-with-builders">Incrementally Migrating With Builders</h3>

<p>Finally, we’ve now introduced a very nifty refactoring nicety. When we had our custom service, had we wanted to migrate the requests to use a new API we had three choices available.</p>

<ol>
  <li>We could have migrated all requests simultaneously</li>
  <li>We could have introduced an overload to the method to take in whether or not to use the updated API</li>
  <li>We could have added an additional method name. E.g. <code class="language-plaintext highlighter-rouge">queryDataV2</code>.</li>
</ol>

<p>The trouble with (1) is it carries inherent risk for anything but small apps. The trouble with (2) is that you quickly grow the cognitive and implementation complexity of your method.
The trouble with (3) is that if you don’t choose a good name, you introduce even more cognitive complexity, and even if you do choose a good name the odds are that the original method name is both easier to remember, faster to autocomplete, and intuitively preferred.</p>

<p>We’ve all been there with (3). Naming things is hard, and teaching folks to migrate their habits is too.</p>

<p>The neat thing about RequestManager is that its a simple chain-of-command executor. It doesn’t care much about what your requests are, just that it can execute them. It is <em>interface</em> driven instead of imperative. Thus we remove the complexity attached to the method name and signature, replacing it instead with an interface that rarely if ever will change.</p>

<p>Which means our refactor still may take three forms, but the change looks different.</p>

<ol>
  <li>We could update the existing builder to migrate all requests simultaneously</li>
  <li>We could change the builder signature to have an options argument to which we pass the version</li>
  <li>We could implement a whole new builder.</li>
</ol>

<p>Which answer is best for you will vary. In my app at work I have three API migrations planned for the year that utilize builders in their migration strategy.</p>

<p>The first will migrate all requests simultaneously. It is simply updating the underlying format in a way that won’t be product affecting.</p>

<p>The second will migrate requests incrementally to a new format, it is updating the underlying transfer format in a way that affects product code, but not changing the overall semantics of the API. For this I have considered passing in an option to the builder, but I am more likely to take the new-builder approach as it makes tracking the status of the incremental migration with static analysis much easier. Either approach is very valid.</p>

<p>The third will migrate requests to a new API version that changes semantics significantly. This is a high risk migration, and so I will use a completely new builder.</p>

<p>With this in mind, lets iterate on the builder we have in our example above and show how this pattern provides value. First, by refactoring in a way that doesn’t result in a product code change, then in one which does.</p>

<p>As a reminder, this was where we left off with our builder before:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="nx">resourcePath</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="na">url</span><span class="p">:</span> <span class="s2">`/api/v1/</span><span class="p">${</span><span class="nx">resourcePath</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The first thing we probably want to do is have our builder respect a configurable default host
and namespace. This prepares us for our site and API not being on the same domain (a decision you do not want to have to piecemeal figure out how to account for later).</p>

<p>EmberData provides users a global config mechanism for host and namespace. Typically you will want to do this either in your store file or app file.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">setBuildURLConfig</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/request-utils</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">setBuildURLConfig</span><span class="p">({</span>
  <span class="na">host</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://api.example.com</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// no trailing slash, though '/' is valid</span>
  <span class="na">namespace</span><span class="p">:</span> <span class="dl">'</span><span class="s1">api/v1</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// no leading slash and again no trailing slash</span>
<span class="p">});</span>
</code></pre></div></div>

<p>What this does is set the default host and namespace for use by the request-utils package,
which provides a number of utility methods for constructing builders.</p>

<p>Next, lets update our builder to make use of this:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">buildBaseURL</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/request-utils</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="nx">resourcePath</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">{</span>
        <span class="na">url</span><span class="p">:</span> <span class="nx">buildBaseURL</span><span class="p">({</span> <span class="nx">resourcePath</span> <span class="p">}),</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>Note: the signature of what is passed to buildBaseURL is simpler above than in the current
EmberData 5.3 release, and reflects updates that relaxed the signature which will be in 5.4</p>
</blockquote>

<p>Awesome! Now we are ready for deployment to CDNs! Or maybe your application is single-tenant
and has a per-customer api domain that needs to be configured globally, easy-peasy. This can all be handled without the product code needing to be changed or consider it.</p>

<p>Now lets say we want this request builder to build requests that are <em>also</em> able to be cached when using EmberData’s CacheHandler?</p>

<p>For that, we need a cacheKey, and for good measure an op-code (some op-codes are special –this one is not– but I will explain more about op-codes in another post) as well as an identifier.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">buildBaseURL</span><span class="p">,</span> <span class="nx">buildQueryParams</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@ember-data/request-utils</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">queryData</span><span class="p">(</span><span class="nx">query</span><span class="p">,</span> <span class="nx">resourcePath</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">buildBaseURL</span><span class="p">({</span> <span class="nx">resourcePath</span> <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">queryData</span> <span class="o">=</span> <span class="nx">structuredClone</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">url</span><span class="p">}</span><span class="s2">?</span><span class="p">${</span><span class="nx">buildQueryParams</span><span class="p">(</span><span class="nx">queryData</span><span class="p">)}</span><span class="s2">`</span><span class="p">;</span>

    <span class="k">return</span> <span class="p">{</span>
        <span class="nx">url</span><span class="p">,</span>
        <span class="na">op</span><span class="p">:</span> <span class="dl">'</span><span class="s1">query</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">identifier</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="nx">resourcePath</span> <span class="p">},</span>
        <span class="na">cacheOptions</span><span class="p">:</span> <span class="p">{</span> <span class="nx">key</span> <span class="p">},</span>
        <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
            <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span>
        <span class="p">},</span>
        <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>Note: whether resourcePath and identifier.type match is API specific, in many APIs they do, in equally many they do not. A simple mapping function is useful if they do not.</p>
</blockquote>

<p>Ok so what all did we do here?</p>

<p>First, we add <code class="language-plaintext highlighter-rouge">op: 'query'</code>, which is a hint to the CacheHandler about how to treat this request. Then we added <code class="language-plaintext highlighter-rouge">identifier: { type: resourcePath },</code> which will hint to it the
primary resource type this request pertains to, which is useful for many cache invalidation strategies. There are additional properties that may be added if desired for that reason, I won’t go into them all here.</p>

<p>Finally, we activated caching and told it the key to use. This was important because by default the CacheHandler will only cache GET requests with a url by their url, but we want
to use POST as QUERY here. If we blindly used the URL we would have a bug in our application.</p>

<p>So we serialize the query to be part of the cache-key, and we do this using the <code class="language-plaintext highlighter-rouge">buildQueryParams</code> utility. This utility performs a bit of wizardy to produce a stable key.</p>

<p>In case you didn’t know <code class="language-plaintext highlighter-rouge">JSON.stringify({ a: '1', b: '2' })</code> is not the same as <code class="language-plaintext highlighter-rouge">JSON.stringify({ b: '2', a: '1' })</code>. Key insertion order is respected during JSON serialization. However, this is rarely useful for creating a cache-key because objects are often dynamically generated. What we care about is whether they have a (deep) equivalent value, which this function helps us to achieve in more cases. Yet another thing where an annoying and complicated problem vanishes with a good framework and good infra.</p>

<p>What if you don’t want to trust serializing the query like this to get a cache-key? Use any string key you would like, just make sure that its uniqueness validly describes the query.</p>

<p>The reasons for why this is so important will go into my next post which will dive into caching.</p>

<h4 id="migrations-that-affect-product">Migrations That Affect Product</h4>

<p>Lets say we wanted to change our API from using ActiveModel to using JSON:API as its format. The response in both cases is JSON but the shape is very different. How would
we handle this with builders? For this exercise, lets assume the API version stays the
same and the new format is controlled by JSON:API’s expected header.</p>

<blockquote>
  <p>Note: We are glossing over that JSON:API doesn’t have a post-as-query capability in
the spec, most real-world implementations still implement it. Here we care only about
updating headers to get the new API response format, no other changes in API semantics.</p>
</blockquote>

<p>For this I would write a new builder. I would start by copying the original, and then
adjust the headers as desired. I would also account for the format in the cache-key
because it is something that affects the response but is not captured by the URL itself.</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">import { buildBaseURL, buildQueryParams } from '@ember-data/request-utils';
</span>
export function queryData(query, resourcePath) {
    const url = buildBaseURL({ resourcePath });
    const queryData = structuredClone(query);
<span class="gd">-    const key = `${url}?${buildQueryParams(queryData)}`;
</span><span class="gi">+    const key = `[JSON:API]${url}?${buildQueryParams(queryData)}`;
</span>
    return {
        url,
        op: 'query',
        identifier: { type: resourcePath },
        cacheOptions: { key },
        method: 'POST',
        headers: {
<span class="gi">+            'Accepts': 'application/vnd.api+json',
</span><span class="gd">-            'Content-Type': 'application/json'
</span><span class="gi">+            'Content-Type': 'application/vnd.api+json'
</span>        },
        body: JSON.stringify(query)
    }
 }
</code></pre></div></div>

<p>Now lets migrate our product code usage:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">-import { dataQuery } from './builders';
</span><span class="gi">+import { dataQuery } from './builders-v2';
</span>
class Route {
    @service requestManager;

    async model({ search, offset }) {
        const query = dataQuery({
            search,
            sort: 'name:asc',
            limit: 50,
            offset: Number(offset)
        }, 'company');
        const result = await this.requestManager.request(query);

        return {
            search,
            offset,
            data: result.content
        };
    }
 }
</code></pre></div></div>

<p>Obviously we will then need to make additional changes to our code to account for the changed json shape, but our request code is stable, our migration state is easy to statically analyze, and our brain doesn’t hate any weirdly named methods.</p>

<p>Of note: if you were using EmberData’s Store here and not just the RequestManager, you
would not need to migrate code using <code class="language-plaintext highlighter-rouge">result.content</code> as that would already be
record instances!</p>

<p>The only difference in code to have seamlessly absorbed such a major migration would be this!</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">import { dataQuery } from './builders-v2';
</span>
class Route {
<span class="gd">-    @service requestManager;
</span><span class="gi">+    @service store;
</span>
    async model({ search, offset }) {
        const query = dataQuery({
            search,
            sort: 'name:asc',
            limit: 50,
            offset: Number(offset)
        }, 'company');
<span class="gd">-        const result = await this.requestManager.request(query);
</span><span class="gi">+        const result = await this.store.request(query);
</span>
        return {
            search,
            offset,
            data: result.content
        };
    }
 }
</code></pre></div></div>

<h3 id="where-to-from-here">Where To From Here?</h3>

<p>Builders and RequestManager are but the first stepping stone in what promises to be a big evolution in how you manage querying and mutating data in your application.</p>

<p>We see the future as one that is schema and spec driven. Specs describe API endpoints and explain how they operate on the resources your schemas describe. This information then feeds
into tooling to automatically produce your API mocks for tests, eliminates Models, provides
strong end-to-end typing guarantees, and allows lint and runtime verification of query validity.</p>

<p>The hints of this are already throughought the EmberData codebase. Alpha versions of ideas like the request mocking library (which can do things no other mocking library does due to RequestManager) are under construction and even being used by the library’s own test suite to dogfood their development.</p>

<p>All this to say, we think builders will end up a typechecked, typed response thing that for
most apps looks a bit like this for the example we’ve been using.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">dataQuery</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./builders</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">aql</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@warp-drive/aql</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">class</span> <span class="nx">Route</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">service</span> <span class="nx">requestManager</span><span class="p">;</span>

    <span class="k">async</span> <span class="nx">model</span><span class="p">({</span> <span class="nx">search</span><span class="p">,</span> <span class="nx">offset</span> <span class="p">})</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">requestManager</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">aql</span><span class="s2">`
          QUERY company {
            data {}
            filter {
              @arg search = </span><span class="p">${</span><span class="nx">search</span><span class="p">}</span><span class="s2">
            }
            sort [
                name = "asc"
            ]
            page {
                @arg offset = </span><span class="p">${</span><span class="nb">Number</span><span class="p">(</span><span class="nx">offset</span><span class="p">)}</span><span class="s2">
                limit = 50
            }
          }
        `</span><span class="p">);</span>

        <span class="k">return</span> <span class="p">{</span>
            <span class="nx">search</span><span class="p">,</span>
            <span class="nx">offset</span><span class="p">,</span>
            <span class="na">data</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">content</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With benefits that stretch for miles beyond the simplicity of the interface.</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[Not Your Parent’s EmberData]]></summary></entry><entry><title type="html">Hello World</title><link href="https://runspired.com/2024/01/30/hello-world.html" rel="alternate" type="text/html" title="Hello World" /><published>2024-01-30T00:00:00+00:00</published><updated>2024-01-30T00:00:00+00:00</updated><id>https://runspired.com/2024/01/30/hello-world</id><content type="html" xml:base="https://runspired.com/2024/01/30/hello-world.html"><![CDATA[<h1 id="-hello-world">👋 Hello World!</h1>

<p>I’ve decided it was long since past due to have a place I could
periodically post my thoughts in long-form.</p>

<p>I encourage you to expect very little, and check back even less.</p>

<p>If I happen to write something that isn’t horrible, or especially
if it is horrible you can put me on blast on threads <a href="https://www.threads.net/@runspired/">(@runspired)</a>.</p>]]></content><author><name>Chris Thoburn</name></author><summary type="html"><![CDATA[👋 Hello World!]]></summary></entry></feed>