-
Notifications
You must be signed in to change notification settings - Fork 12
Skip the link-closure readback on card writes #6162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7743,12 +7743,15 @@ export class Realm { | |
| lastModified, | ||
| ); | ||
| } else { | ||
| // The write response is read only for the primary card's assigned id | ||
| // and realm-info; the client discards its attributes, relationships and | ||
| // `included[]` (see `persistAndUpdate` in host store.ts). Skip the | ||
| // transitive `loadLinks` closure and query-backed expansion — assembling | ||
| // a link graph nothing reads is wasted work (the closure walk is the | ||
| // bulk of read-handler time), and side-loaded children round-trip their | ||
| // ids through their `lid`s, not this response. | ||
|
Comment on lines
+7746
to
+7752
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Claude Code 🤖] "the client discards its attributes, relationships and That is what both host failures are. Ask: update both tests to the new shape, and reword this comment, its twin on the PATCH branch, and the description's "relationships … returned exactly as before" to say the relationships lose their linkage too. Regression, blocking. |
||
| let entry = await this.#realmIndexQueryEngine.cardDocument( | ||
| new URL(newURL), | ||
| { | ||
| loadLinks: true, | ||
| skipQueryBackedExpansion: false, | ||
| }, | ||
| ); | ||
| if (!entry || entry?.type === 'error') { | ||
| let err = entry | ||
|
|
@@ -7803,8 +7806,7 @@ export class Realm { | |
| let duringPrerender = isDuringPrerenderRequest(request); | ||
| // A skip-index-wait caller (see SKIP_INDEX_WAIT_HEADER) takes the same | ||
| // write-side path as a prerender write — index deferred, answer from the | ||
| // serialized echo — without the prerender-only serialization tweaks | ||
| // (skipQueryBackedExpansion) that stay gated on `duringPrerender` below. | ||
| // serialized echo rather than a readback. | ||
| let answerFromEcho = duringPrerender || isSkipIndexWaitRequest(request); | ||
|
|
||
| let { data: patch, included: maybeIncluded } = await request.json(); | ||
|
|
@@ -7945,12 +7947,12 @@ export class Realm { | |
| // If the patch makes no semantic changes and doesn't include side-loaded | ||
| // resources, short-circuit to avoid touching the file (and changing mtime). | ||
| if (included.length === 0 && isEqual(primaryResource, original)) { | ||
| // No links closure: the PATCH response is read only for the primary | ||
| // card's id and realm-info, and this readback runs inside the | ||
| // realm-wide write lock every other writer queues on (see the | ||
| // non-short-circuit readback below for the full rationale). | ||
| let entry = await this.#realmIndexQueryEngine.cardDocument( | ||
| new URL(instanceURL), | ||
| { | ||
| loadLinks: true, | ||
| skipQueryBackedExpansion: duringPrerender, | ||
| }, | ||
| ); | ||
| if (entry && entry.type !== 'error') { | ||
| let existingDoc = merge({}, entry.doc, { | ||
|
|
@@ -7962,11 +7964,10 @@ export class Realm { | |
| let createdAt = await this.getCreatedTime( | ||
| this.paths.local(url) + '.json', | ||
| ); | ||
| // The PATCH echo is the same served representation as a GET — | ||
| // including the joined `meta.screenshots` (the store replaces an | ||
| // instance's meta wholesale from a save response, so an echo | ||
| // without it would wipe the key client-side until the next GET) | ||
| // and the same validator components. | ||
| // The PATCH echo carries the joined `meta.screenshots` a GET would | ||
| // (the store replaces an instance's meta wholesale from a save | ||
| // response, so an echo without it would wipe the key client-side | ||
| // until the next GET). | ||
| if (entry.screenshots) { | ||
| existingDoc.data.meta = { | ||
| ...existingDoc.data.meta, | ||
|
|
@@ -7976,27 +7977,18 @@ export class Realm { | |
| }), | ||
| }; | ||
| } | ||
| // entry.doc came from cardDocument(), which already called | ||
| // attachRealmInfo() and (re)populated the realm-info cache — | ||
| // so the cached hash is current as of this response. | ||
| await this.getRealmInfo(); | ||
| let foreignDeps = this.hasForeignRealmDeps(entry.deps); | ||
| let etag = foreignDeps | ||
| ? undefined | ||
| : buildCardJsonEtag( | ||
| entry.indexedAt, | ||
| this.getCachedRealmInfoHash(), | ||
| screenshotsEtagFingerprint(entry.screenshots), | ||
| ); | ||
| // No validator on the write echo: the echo omits the link closure a | ||
| // GET assembles, so it is a different representation than the GET | ||
| // whose ETag it would otherwise share — emitting one would let a | ||
| // conditional GET 304 onto this closure-less body. The client | ||
| // discards the echo body anyway, so it has no validator to gain. | ||
| this.#serveInstanceIdsAsRRI(existingDoc); | ||
| return createResponse({ | ||
| body: JSON.stringify(existingDoc, null, 2), | ||
| init: { | ||
| headers: { | ||
| 'content-type': SupportedMimeType.CardJson, | ||
| 'cache-control': this.cardJsonCacheControl(requestContext), | ||
| ...(etag ? { etag } : {}), | ||
| ...etagSuppressedHeader(foreignDeps), | ||
| ...lastModifiedHeader(existingDoc), | ||
| ...(createdAt != null | ||
| ? { 'x-created': formatRFC7231(createdAt * 1000) } | ||
|
|
@@ -8101,12 +8093,15 @@ export class Realm { | |
| requestContext, | ||
| }); | ||
| } | ||
| // The write response is read only for the primary card's assigned id | ||
| // and realm-info; the client discards its attributes, relationships and | ||
| // `included[]` (see `persistAndUpdate` in host store.ts). Skip the | ||
| // transitive `loadLinks` closure and query-backed expansion — assembling | ||
| // a link graph nothing reads is wasted work, and here it is wasted | ||
| // inside the realm-wide write lock every other writer on this realm is | ||
| // serialized behind. | ||
| let entry = await this.#realmIndexQueryEngine.cardDocument( | ||
| new URL(instanceURL), | ||
| { | ||
| loadLinks: true, | ||
| skipQueryBackedExpansion: false, | ||
| }, | ||
| ); | ||
| if (!entry || entry?.type === 'error') { | ||
| if ( | ||
|
|
@@ -8155,32 +8150,18 @@ export class Realm { | |
| }; | ||
| } | ||
| } | ||
| // Same rationale as the no-op short-circuit branch above: | ||
| // cardDocument() above primed the realm-info cache via | ||
| // attachRealmInfo(), but only when entry was a non-error doc. | ||
| // On the error fallback we may still need to populate it. | ||
| await this.getRealmInfo(); | ||
| let foreignDeps = | ||
| entry && entry.type !== 'error' | ||
| ? this.hasForeignRealmDeps(entry.deps) | ||
| : false; | ||
| let etag = | ||
| entry && entry.type !== 'error' && !foreignDeps | ||
| ? buildCardJsonEtag( | ||
| entry.indexedAt, | ||
| this.getCachedRealmInfoHash(), | ||
| screenshotsEtagFingerprint(entry.screenshots), | ||
| ) | ||
| : undefined; | ||
| // No validator on the write echo (same rationale as the short-circuit | ||
| // branch above): the echo omits the link closure a GET assembles, so it | ||
| // is a different representation than the GET whose ETag it would share — | ||
| // emitting one would let a conditional GET 304 onto this closure-less | ||
| // body. The client discards the echo body anyway. | ||
| this.#serveInstanceIdsAsRRI(doc); | ||
| return createResponse({ | ||
| body: JSON.stringify(doc, null, 2), | ||
| init: { | ||
| headers: { | ||
| 'content-type': SupportedMimeType.CardJson, | ||
| 'cache-control': this.cardJsonCacheControl(requestContext), | ||
| ...(etag ? { etag } : {}), | ||
| ...etagSuppressedHeader(foreignDeps), | ||
| ...lastModifiedHeader(doc), | ||
| ...(created ? { 'x-created': formatRFC7231(created * 1000) } : {}), | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code 🤖] This 500s on the first POST in CI, so the test never reaches the
includedassertions it exists to make — the contract it is here to pin is unpinned.rri('./friend.gts')resolves against the created card's own URL, and a POST to/lands the card at<realm>/Friend/<uuid>.json, so the ref resolves to<realm>/Friend/friend.gts:fileSerializationthrowsFilterRefersToNonexistentTypeError(Module entry not found for URL) and the handler turns it into a 500. The other POST-to-/tests in this file spell the ref as a prefixed module (rri('@cardstack/base/card-api')).While you're in here: both PATCH echoes change too, and neither the write branch nor the no-op short-circuit has a test asserting the closure is absent.
Regression, blocking.