Skip to content
Open
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
9 changes: 9 additions & 0 deletions packages/base/realm-config.gts
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,15 @@ export class RealmConfig extends CardDef {
// automatically in that case) or when an operator otherwise needs
// the full isolated render present in the index.
@field includePrerenderedDefaultRealmIndex = contains(BooleanField);
// Opt-in for the realm's GET `_screenshot/` route to trigger NEW captures
// for arbitrary capture specs. Full captureSpec power on a GET is an
// unbounded spec space reachable with only realm read, so it is off
// unless the realm turns it on; the gate blocks Chrome work only, never
// serving — any capture whose canonical spec already has a MediaCache
// ledger entry streams regardless. Read from the realm's indexed config
// at request time, so editing this takes effect with the index update,
// no restart.
@field allowArbitraryScreenshots = contains(BooleanField);

@field cardTitle = contains(StringField, {
computeVia: function (this: RealmConfig) {
Expand Down
1 change: 1 addition & 0 deletions packages/postgres/pg-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { FROM_SCRATCH_JOB_TIMEOUT_SEC } from '@cardstack/runtime-common/tasks/in
// Side-effect imports: these modules call registerQueueJobDefinition() at
// load time, so any process that constructs a PgQueuePublisher gets the
// coalesce handlers registered before publish() is called.
import '@cardstack/runtime-common/jobs/screenshot-card';
import '@cardstack/runtime-common/tasks/copy';
import '@cardstack/runtime-common/tasks/full-reindex';
import '@cardstack/runtime-common/tasks/media-cache-gc';
Expand Down
6 changes: 5 additions & 1 deletion packages/realm-server/handlers/handle-screenshot-card.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type Koa from 'koa';

import { isCaptureFormat } from '@cardstack/runtime-common';
import { enqueueScreenshotCardJob } from '@cardstack/runtime-common/jobs/screenshot-card';
import { userInitiatedPriority } from '@cardstack/runtime-common/queue';

Expand Down Expand Up @@ -64,7 +65,9 @@ export default function handleScreenshotCard({
if (!cardId || typeof cardId !== 'string') {
return sendResponseForBadRequest(ctxt, 'cardId is required');
}
if (format !== 'isolated' && format !== 'embedded') {
// Shared with the GET `_screenshot/` DSL so both surfaces accept exactly
// the same capture formats.
if (!isCaptureFormat(format)) {
return sendResponseForBadRequest(
ctxt,
'format must be "isolated" or "embedded"',
Expand All @@ -88,6 +91,7 @@ export default function handleScreenshotCard({
runAs: userId,
cardId,
format,
persist: null,
},
queue,
dbAdapter,
Expand Down
43 changes: 43 additions & 0 deletions packages/realm-server/tests/helpers/fake-media-cache-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Readable } from 'node:stream';
import type { MediaCacheAdapter } from '@cardstack/runtime-common';

// In-memory MediaCacheAdapter for tests: real bytes behind the interface,
// observable deletes, scriptable per-key delete failures, and a switch
// between the two stream shapes the serving layer handles (a node Readable,
// which streams via `nodeStream`, and a bare async iterable, which is
// buffered).
export class FakeMediaCacheAdapter implements MediaCacheAdapter {
objects = new Map<string, Uint8Array>();
deleted: string[] = [];
failDeletesFor = new Set<string>();
streamShape: 'readable' | 'iterable' = 'readable';

async put(key: string, bytes: Uint8Array, _opts: { contentType: string }) {
if (!this.objects.has(key)) {
this.objects.set(key, bytes);
}
}
async head(key: string) {
let bytes = this.objects.get(key);
return bytes ? { size: bytes.length } : undefined;
}
async getStream(key: string) {
let bytes = this.objects.get(key);
if (!bytes) {
return undefined;
}
if (this.streamShape === 'readable') {
return Readable.from(Buffer.from(bytes));
}
return (async function* () {
yield bytes;
})();
}
async delete(key: string) {
if (this.failDeletesFor.has(key)) {
throw new Error(`simulated delete failure for ${key}`);
}
this.deleted.push(key);
this.objects.delete(key);
}
}
13 changes: 12 additions & 1 deletion packages/realm-server/tests/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,7 @@ export async function createRealm({
transpileCoordinator,
fullIndexOnStartup,
mediaCacheAdapter,
screenshotSyncWaitMs,
}: {
dir: string;
definitionLookup: DefinitionLookup;
Expand Down Expand Up @@ -1274,6 +1275,9 @@ export async function createRealm({
// MediaCache object store for the realm's `_screenshot/` route; absent
// means every screenshot request serves as an uncaptured miss.
mediaCacheAdapter?: MediaCacheAdapter;
// Shrinks the `_screenshot/` route's on-demand sync-wait budget so tests
// can exercise the 503 + Retry-After path without holding real time.
screenshotSyncWaitMs?: number;
}): Promise<{ realm: Realm; adapter: RealmAdapter }> {
await insertPermissions(dbAdapter, new URL(realmURL), permissions);

Expand Down Expand Up @@ -1352,7 +1356,10 @@ export async function createRealm({
transpileCoordinator,
mediaCacheAdapter,
},
fullIndexOnStartup ? { fullIndexOnStartup: true as const } : undefined,
{
...(fullIndexOnStartup ? { fullIndexOnStartup: true as const } : {}),
...(screenshotSyncWaitMs !== undefined ? { screenshotSyncWaitMs } : {}),
},
);
if (worker) {
virtualNetwork.mount(realm.handle);
Expand Down Expand Up @@ -3062,6 +3069,7 @@ export function realmConfigCardJSON(
iconURL?: string;
backgroundURL?: string;
includePrerenderedDefaultRealmIndex?: boolean;
allowArbitraryScreenshots?: boolean;
} = {},
): string {
let attrs: Record<string, unknown> = {};
Expand All @@ -3078,6 +3086,9 @@ export function realmConfigCardJSON(
attrs.includePrerenderedDefaultRealmIndex =
config.includePrerenderedDefaultRealmIndex;
}
if (config.allowArbitraryScreenshots !== undefined) {
attrs.allowArbitraryScreenshots = config.allowArbitraryScreenshots;
}
return JSON.stringify({
data: {
type: 'card',
Expand Down
1 change: 1 addition & 0 deletions packages/realm-server/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ const ALL_TEST_FILES: string[] = [
'./media-cache-adapter-test',
'./media-cache-gc-test',
'./media-cache-serving-test',
'./media-cache-dsl-test',
'./prerender-server-test',
'./prerender-manager-test',
'./prerender-host-shell-recycle-test',
Expand Down
Loading
Loading