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
413 changes: 397 additions & 16 deletions packages/host/app/resources/search-entries.ts

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions packages/host/app/resources/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,13 @@ export class SearchResource<
return;
}
// Re-run on incremental index events (the search doc changed)
// and on prerender_html events (fresh HTML / corrected
// full-text membership landed on its own channel).
// and on prerender_html events. The latter matter even to
// structured queries: this search excludes rows with an
// effective error, and a render error lands on the
// prerendered_html channel at-or-above the row's index
// generation — so membership can flip on a prerender_html event
// with no index event announcing it. (Full-text `matches`
// membership rides that channel too, via `markdown`.)
let isIncrementalIndex =
event.eventName === 'index' &&
(!('indexType' in event) || event.indexType === 'incremental');
Expand Down
82 changes: 82 additions & 0 deletions packages/host/app/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ import {
type CardResource,
type SearchEntryResults,
type SearchEntryWireQuery,
type EntrySingleDocument,
isEntrySingleDocument,
type PrerenderedHtmlFormat,
type ResolvedCodeRef,
type RealmIdentifier,
type RealmResourceIdentifier,
type Saved,
Expand Down Expand Up @@ -1339,6 +1343,84 @@ export default class StoreService extends Service implements StoreInterface {
return json;
}

// Conditional single-instance card+html GET: fetch one `entry` sourced by
// URL (the single-instance counterpart of `_search`), with the rendering
// selection spelled as query params and the client's held composite
// validator as `If-None-Match`. A `304` means the client's rendering is
// current; a `200` returns the fresh entry (with an `item` fallback when no
// rendering exists). The live-search selective refresh uses this to bring one
// member's HTML up to date without re-querying the whole search. Nothing is
// hydrated into the store.
async fetchCardEntry(
url: string,
opts: {
kind: StoreReadType;
format?: PrerenderedHtmlFormat;
renderType?: ResolvedCodeRef;
// `html` | `item` | `html,item`; omit for the default resolution (the
// selected rendering, falling back to `item` where none matched).
fields?: string;
ifNoneMatch?: string;
},
): Promise<
{ notModified: true } | { notModified: false; doc: EntrySingleDocument }
> {
let requestURL = new URL(url);
if (opts.format) {
requestURL.searchParams.set('format', opts.format);
}
if (opts.renderType) {
requestURL.searchParams.set(
'renderType',
`${opts.renderType.module}/${opts.renderType.name}`,
);
}
if (opts.fields) {
requestURL.searchParams.set('fields', opts.fields);
}
let headers: Record<string, string> = {
Accept:
opts.kind === 'file-meta'
? SupportedMimeType.FileMetaHtml
: SupportedMimeType.CardHtml,
...duringPrerenderHeaders(),
...consumingRealmHeader(),
...jobIdHeader(),
...jobPriorityHeader(),
...loggingCorrelationIdHeader(),
};
if (opts.ifNoneMatch) {
headers['If-None-Match'] = opts.ifNoneMatch;
}
let response = await this.network.authedFetch(requestURL.href, {
method: 'GET',
headers,
});
if (response.status === 304) {
return { notModified: true };
}
if (!response.ok) {
let responseText = await response.text();
let err = new Error(
`status: ${response.status} - ${response.statusText}. ${responseText}`,
) as any;
err.status = response.status;
err.responseText = responseText;
err.responseHeaders = response.headers;
throw err;
}
// The response content-type is the negotiated `application/vnd.card+html`
// (not `+json`), but the body is a JSON:API document — parse the text.
let json = JSON.parse(await response.text());
if (!isEntrySingleDocument(json)) {
throw new Error(
`The card+html response was not a valid entry single document:
${JSON.stringify(json, null, 2)}`,
);
}
return { notModified: false, doc: json };
}

getSearchResource<T extends CardDef | FileDef = CardDef>(
parent: object,
getQuery: () => Query | undefined,
Expand Down
Loading
Loading