Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions packages/host/app/routes/render/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,19 @@ export default class RenderMetaRoute extends Route<Model> {
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
// link targets resident, and at any wider scope the serializer would
// walk whatever it finds there — each resident target, and
Comment on lines +235 to +236

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] "at any wider scope" overstates it — only 'all' takes that walk. isExcludedByIncludedScope in card-api.gts reads value.id ? includedScope !== 'all' : includedScope === 'none', so a target with an id is excluded under every scope but 'all', and on this path every resident target is saved. 'local' here would serialize exactly what 'none' does.

As written the paragraph tells the next reader that 'local' is dangerous on this path too, which is the opposite of the write path's reasoning in card-service.ts. "at the default 'all' scope" covers it.

Non-blocking; comment accuracy.


Generated by Claude Code

// 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 — 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(
Expand All @@ -244,18 +251,18 @@ export default class RenderMetaRoute extends Route<Model> {
),
}) 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') {
Expand Down
48 changes: 48 additions & 0 deletions packages/host/tests/acceptance/prerender-meta-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Comment on lines +350 to +355

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] "The searchable settle leaves that whole graph resident" is the only thing that makes this a two-level case, and it isn't the searchable walk that delivers it — that walk never reaches the pets.

friend is searchable: true, which seeds the one-hop route friend, and searchDocFromFields reads field.searchable only on the indexed card, so friend.pets is never a route. In searchable.ts an unmatched link takes the if (!matched) return { id: makeAbsoluteURL(rawValue.reference) } branch — the reference straight off the NotLoadedValue, no load. The existing can generate search doc that includes linksTo field case shows the result: friend.pets comes out as three bare { id } entries.

What pulls the pets in is Person.numOfPets, a computed reading this.pets, firing getter loads that the settle loop waits out — the category meta.ts itself calls out as bypassing the generator's collector. So the depth this test is named for rests on an incidental computed in the fixture: change numOfPets to stop reading this.pets and the test silently degrades to the one-level case can generate serialized instance already covers, still passing and still claiming "however deep the resident graph".

Since the assertion can't distinguish the scopes on its own, residency is the whole of what makes it a guard. This file already has the idiom for establishing it deliberately — can generate search doc that includes linksTo field visits /html/isolated/0 first and says that's what pulls the linked fields. Either do the same here, or name numOfPets in the comment so whoever next edits the fixture knows this test depends on it.

Non-blocking; the gap is in the test's coverage claim, not in the change.


Generated by Claude Code

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'));
Expand Down
Loading