diff --git a/packages/host/app/services/store.ts b/packages/host/app/services/store.ts index ea5df5cf13..ca0d878f75 100644 --- a/packages/host/app/services/store.ts +++ b/packages/host/app/services/store.ts @@ -33,6 +33,7 @@ import { isLinkableCollectionDocument, resolveFileDefCodeRef, X_BOXEL_JOB_PRIORITY_HEADER, + userInitiatedPriority, Deferred, delay, mergeRelationships, @@ -125,18 +126,75 @@ const realmEventsLogger = logger('realm:events'); const storeLogger = logger('store'); // Companion to `jobIdHeader()` (re-exported from -// `../lib/prerender-fetch-headers`). The prerender server's -// render-runner injects `__boxelJobPriority` onto the page before -// transitioning into the render route. Outside a prerender tab the -// global is undefined and we send no header — user / API callers -// leave the realm-server's lookup paths to fall back to priority 0 -// as today. +// `../lib/prerender-fetch-headers`). Policy is two-state, gated by +// `__boxelDuringPrerender`, not by the presence of +// `__boxelJobPriority`: +// +// 1. Inside a prerender tab: forward the worker job's priority as-is. +// The render-runner injects `__boxelJobPriority` alongside +// `__boxelJobId` on each visit — a priority of 0 is meaningful +// (the originating job is system-initiated background indexing) +// and must be preserved, not upgraded. Sub-`prerenderModule` +// calls fired by `_federated-search` for a `lookupDefinition` +// cache miss inherit this priority so they don't outrun the +// parent. If `__boxelJobPriority` is missing here (older +// render-runner build, test fixture, etc.) treat as 0 — the +// safe default for prerender-context work. +// +// 2. Outside a prerender tab (the host SPA in a real user's browser): +// stamp `userInitiatedPriority` (10). User clicks driving a +// search are by definition user-initiated work and should outrank +// background indexing on the realm-server's PagePool. Without +// this, a user search whose definition lookup misses the modules +// cache would fire its sub-prerender at priority 0 and queue +// behind concurrent indexing fan-out. +// +// External (non-host) HTTP callers — anything that doesn't run in +// the host SPA's JS runtime — bypass this helper entirely and set +// `X-Boxel-Job-Priority` directly on their request if they care. +// This helper covers the host SPA only. +// +// Both globals are checked with `=== true` / strict-number rather +// than truthy coercion: `__boxelDuringPrerender` is typed as a +// boolean and a stray truthy string from a future code path +// shouldn't silently flip the policy from "user-priority" to +// "preserve 0." +// Pure resolver — exported for the unit test in +// `tests/integration/job-priority-header-test.ts`. See the comment +// above for the policy rationale; the function is the literal +// translation of that policy to numbers. +export function resolveOutboundJobPriority({ + duringPrerender, + jobPriority, +}: { + duringPrerender: unknown; + jobPriority: unknown; +}): number { + let valid = + typeof jobPriority === 'number' && + Number.isSafeInteger(jobPriority) && + jobPriority >= 0 + ? jobPriority + : undefined; + if (duringPrerender === true) { + return valid ?? 0; + } + return valid ?? userInitiatedPriority; +} + function jobPriorityHeader(): Record { - let p = (globalThis as unknown as { __boxelJobPriority?: number }) - .__boxelJobPriority; - return typeof p === 'number' && Number.isSafeInteger(p) && p >= 0 - ? { [X_BOXEL_JOB_PRIORITY_HEADER]: String(p) } - : {}; + let g = globalThis as unknown as { + __boxelDuringPrerender?: boolean; + __boxelJobPriority?: number; + }; + return { + [X_BOXEL_JOB_PRIORITY_HEADER]: String( + resolveOutboundJobPriority({ + duringPrerender: g.__boxelDuringPrerender, + jobPriority: g.__boxelJobPriority, + }), + ), + }; } const queryFieldSeedFromSearchSymbol = Symbol.for( 'cardstack-query-field-seed-from-search', diff --git a/packages/host/tests/unit/job-priority-header-test.ts b/packages/host/tests/unit/job-priority-header-test.ts new file mode 100644 index 0000000000..bc2cbd9e69 --- /dev/null +++ b/packages/host/tests/unit/job-priority-header-test.ts @@ -0,0 +1,150 @@ +import { module, test } from 'qunit'; + +import { userInitiatedPriority } from '@cardstack/runtime-common'; + +import { resolveOutboundJobPriority } from '@cardstack/host/services/store'; + +// Pure-resolver tests for the policy that decides what +// `X-Boxel-Job-Priority` value the host SPA stamps on outbound +// `_federated-search` calls. The function is module-internal logic +// extracted so its policy can be pinned without acceptance-test +// scaffolding. +// +// Two states gated by `__boxelDuringPrerender`: +// - inside prerender → forward (preserve 0) +// - outside prerender → user-initiated (10) by default +module('Unit | job-priority-header | resolveOutboundJobPriority', function () { + module('outside a prerender tab (user / API caller)', function () { + test('returns userInitiatedPriority when no global is set', function (assert) { + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: undefined, + jobPriority: undefined, + }), + userInitiatedPriority, + ); + }); + + test('returns userInitiatedPriority when __boxelDuringPrerender is false', function (assert) { + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: false, + jobPriority: undefined, + }), + userInitiatedPriority, + ); + }); + + test('honors an explicit override on __boxelJobPriority', function (assert) { + // Batch / scripting tooling running in the host SPA can set the + // global before issuing a fetch; outside a prerender tab we still + // forward what they set rather than overriding to user priority. + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: undefined, + jobPriority: 3, + }), + 3, + ); + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: false, + jobPriority: 0, + }), + 0, + 'override with 0 is preserved (not coerced to user priority)', + ); + }); + + test('rejects a truthy but non-boolean __boxelDuringPrerender — uses strict === true', function (assert) { + // If `__boxelDuringPrerender` somehow ended up as a stringy + // truthy value (e.g. set by a future code path that didn't + // coerce), the policy must NOT silently flip to "forward 0"; + // a real user-facing fetch would then queue behind background + // indexing. The check is `=== true` for exactly this reason. + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: 'yes', + jobPriority: undefined, + }), + userInitiatedPriority, + ); + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: 1, + jobPriority: undefined, + }), + userInitiatedPriority, + ); + }); + }); + + module('inside a prerender tab', function () { + test('forwards an explicit __boxelJobPriority of 10', function (assert) { + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: true, + jobPriority: 10, + }), + 10, + ); + }); + + test('forwards an explicit __boxelJobPriority of 0 — must NOT upgrade', function (assert) { + // System-initiated indexing has priority 0. A + // `_federated-search` fired by the card render must preserve + // that or its sub-prerenders would outrank the parent job. + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: true, + jobPriority: 0, + }), + 0, + ); + }); + + test('defaults to 0 when __boxelJobPriority is missing (older render-runner / test fixture)', function (assert) { + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: true, + jobPriority: undefined, + }), + 0, + ); + }); + + test('rejects malformed __boxelJobPriority values', function (assert) { + // Non-number / negative / non-integer values fall through to + // the default for the active branch. + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: true, + jobPriority: -1, + }), + 0, + ); + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: true, + jobPriority: 1.5, + }), + 0, + ); + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: true, + jobPriority: '10', + }), + 0, + ); + assert.strictEqual( + resolveOutboundJobPriority({ + duringPrerender: false, + jobPriority: -1, + }), + userInitiatedPriority, + 'malformed value outside prerender → user-initiated default', + ); + }); + }); +});