From b653c205d49b554dccd9972a69a1cf182bcf4f58 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 17 Sep 2026 11:23:36 +0200 Subject: [PATCH 1/3] Pin what a serialized instance carries when its link graph is resident MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing case covers a card whose links are one level deep and plural. Add the singular, two-level one — a person linking to a person who links to three pets — and assert the whole document: the target is a reference, and neither it nor its own links appear. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/acceptance/prerender-meta-test.gts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/host/tests/acceptance/prerender-meta-test.gts b/packages/host/tests/acceptance/prerender-meta-test.gts index 5cf777514a4..2ac4939f623 100644 --- a/packages/host/tests/acceptance/prerender-meta-test.gts +++ b/packages/host/tests/acceptance/prerender-meta-test.gts @@ -346,6 +346,54 @@ module('Acceptance | prerender | meta', function (hooks) { ); }); + test('a serialized instance stops at its own resource, however deep the resident graph', async function (assert) { + // Jade links to Hassan, who links to three pets. The searchable settle + // leaves that whole graph resident, so this is the case where walking it + // would cost the most and contribute the least: a link target is a + // reference here, and its own links are not the card's to carry. + let url = `${testRealmURL}Person/jade.json`; + await visit(renderPath(url, '/meta')); + let { value } = await capturePrerenderResult('textContent'); + let meta: PrerenderMeta = JSON.parse(value); + assert.deepEqual( + meta.serialized, + { + data: { + type: 'card', + id: testRRI('Person/jade'), + attributes: { + name: 'Jade', + cardTitle: 'Jade', + cardInfo: { + name: null, + summary: null, + cardThumbnailURL: null, + notes: null, + }, + cardDescription: null, + cardThumbnailURL: null, + numOfPets: '0', + }, + relationships: { + friend: { + links: { + self: './hassan', + }, + }, + }, + meta: { + adoptsFrom: { + module: rri('../person'), + name: 'Person', + }, + realmURL: testRealmURL, + }, + }, + }, + 'the link target is a reference, and neither it nor its own links ride along', + ); + }); + test('can generate display name', async function (assert) { let url = `${testRealmURL}Pet/paper.json`; await visit(renderPath(url, '/meta')); From 23679cc91f6d198af4e57546fd51424eb694d2a4 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 17 Sep 2026 11:23:49 +0200 Subject: [PATCH 2/3] Stop building an index serialization's link graph in order to discard it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A card file holds the card's own resource and no linked neighbors, which is why this route strips `included` and every resolved relationship `data` from what it serializes. Building that graph first is pure cost: the searchable settle leaves the whole graph resident, so the serializer walks each link target — and each target's own targets — into an `included[]` with no reader. Ask for the document directly instead. An excluded target still emits its relationship entry, so the serialized instance is unchanged; the walk behind it is not taken. The `included` delete stays as a guard, now against a card whose own serialize hook pushes one. Co-Authored-By: Claude Opus 5 (1M context) --- packages/host/app/routes/render/meta.ts | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/host/app/routes/render/meta.ts b/packages/host/app/routes/render/meta.ts index 104df8a126b..f0f512b4aa8 100644 --- a/packages/host/app/routes/render/meta.ts +++ b/packages/host/app/routes/render/meta.ts @@ -229,12 +229,18 @@ export default class RenderMetaRoute extends Route { let vn = this.network.virtualNetwork; serialized = api.serializeCard(instance, { includeComputeds: true, + // A card file holds the card's own resource and no linked neighbors, + // so no link target's resource belongs in this document. Saying that + // up front is what keeps it cheap: the searchable settle above leaves + // the link graph resident, and at any wider scope the serializer would + // walk all of it — each target, and each target's own targets — to + // build an `included[]` this route then discards. An excluded target + // still emits its relationship entry, so the document is unchanged. + includedScope: 'none', // A query-backed field is resolved live and the index can't invalidate - // it, so its serialized value would always be stale — and deep- - // serializing the query closure into `included[]` is what wedges a - // densely cross-linked realm. Membership comes from the file's own - // relationships, so omit query fields here (the relationship data is - // stripped below regardless). + // it, so its serialized value would always be stale. Membership comes + // from the file's own relationships, so omit query fields here (the + // relationship data is stripped below regardless). omitQueryFields: true, maybeRelativeReference: (reference: string) => maybeRelativeReference( @@ -244,18 +250,18 @@ export default class RenderMetaRoute extends Route { ), }) as SingleCardDocument; serializeMs = performance.now() - serializeStart; - // Emulate the on-disk file serialization: a card file holds only the - // card's own resource — relationship slots keep their `links` but drop - // the resolved `data`, and no linked neighbors ride along in `included`. - // The searchable settle above may have loaded link targets into the - // store, and `serializeCard` walks whatever is resident into `included`; - // strip both so the serialized instance is a pure function of the card's - // own data, independent of which targets happen to be loaded. + // The rest of emulating the on-disk file serialization: a relationship + // slot keeps its `links` but drops the resolved `data`, so the serialized + // instance is a pure function of the card's own data rather than of which + // targets happen to be loaded. for (let { relationship } of relationshipEntries( serialized.data.relationships, )) { delete relationship.data; } + // `includedScope: 'none'` builds no `included`; the delete holds the + // no-neighbors contract against a card whose own `serialize` hook pushes + // one regardless. delete serialized.included; } finally { if (passOpen && typeof api.endComputePass === 'function') { From d639a0f922a6b030ada7e7883b9d0ad70f6f37ce Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Thu, 17 Sep 2026 12:34:57 +0200 Subject: [PATCH 3/3] Describe the walk as bounded by residency, not by the realm's graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A prerender's reads already come back links-only, so the store holds the link targets the search doc pulled rather than the card's whole graph. The walk the scope avoids is over that resident set, transitively — which is what the comment this replaces claimed more of than it should. Co-Authored-By: Claude Opus 5 (1M context) --- packages/host/app/routes/render/meta.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/host/app/routes/render/meta.ts b/packages/host/app/routes/render/meta.ts index f0f512b4aa8..65607d3946e 100644 --- a/packages/host/app/routes/render/meta.ts +++ b/packages/host/app/routes/render/meta.ts @@ -232,10 +232,11 @@ export default class RenderMetaRoute extends Route { // A card file holds the card's own resource and no linked neighbors, // so no link target's resource belongs in this document. Saying that // up front is what keeps it cheap: the searchable settle above leaves - // the link graph resident, and at any wider scope the serializer would - // walk all of it — each target, and each target's own targets — to - // build an `included[]` this route then discards. An excluded target - // still emits its relationship entry, so the document is unchanged. + // link targets resident, and at any wider scope the serializer would + // walk whatever it finds there — each resident target, and + // transitively each target's own — to build an `included[]` this + // route then discards. An excluded target still emits its + // relationship entry, so the document is unchanged. includedScope: 'none', // A query-backed field is resolved live and the index can't invalidate // it, so its serialized value would always be stale. Membership comes