From 190625133e82fb02f79306095d6ba22ec2920ebe Mon Sep 17 00:00:00 2001 From: Alexei Mochalov Date: Mon, 31 Aug 2026 18:56:35 +0200 Subject: [PATCH] fix(data-products): load the fixture adapter lazily `model/DataProducts/hooks` is on the volumes landing's import path through `useProducts`, so its static `fixtureAdapter` import put the whole 36K demo fixture corpus in the chunk every deployment downloads there -- including the ones with the `data-products` preview off, which never read it. Reach the adapter through a dynamic import instead, in the shape `Tabulator.tsx` already uses for `ConfigEditor`. The corpus gets its own on-demand chunk; the resource keys and the hook signatures do not change. `useAdapter` goes with it: it returned the adapter synchronously, which a lazy adapter cannot do, and it had no callers. --- catalog/CHANGELOG.md | 1 + catalog/app/model/DataProducts/hooks.ts | 47 ++++++++++++------------- catalog/app/model/DataProducts/index.ts | 1 - 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/catalog/CHANGELOG.md b/catalog/CHANGELOG.md index e953f850f88..d6727388858 100644 --- a/catalog/CHANGELOG.md +++ b/catalog/CHANGELOG.md @@ -21,6 +21,7 @@ complete sentence without it. ## Changes +- [Changed] The `data-products` demo fixture data no longer ships in the bundles a browser downloads on the volumes landing; it loads only when the preview is on ([#5259](https://github.com/quiltdata/quilt/pull/5259)) - [Fixed] Quilt+ URI parsing retains the informational `catalog` field, reads a raw `+` in an unencoded path as a `+` rather than a space, and shares its compatibility corpus with quilt3 ([#5255](https://github.com/quiltdata/quilt/pull/5255)) - [Fixed] Quilt+ URIs: a package path containing a literal `%` no longer breaks the URI, and one containing a literal `%20` no longer decodes to a space and points at the wrong entry. Paths from producers that do not percent-encode now resolve instead of failing, and a path that genuinely cannot be decoded reports a real error rather than the literal text `unknown error: ${e}` ([#5256](https://github.com/quiltdata/quilt/pull/5256)) - [Changed] Search: error states are separate, more strictly typed components, safer to extend than one component behind a `kind` prop ([#5238](https://github.com/quiltdata/quilt/pull/5238)) diff --git a/catalog/app/model/DataProducts/hooks.ts b/catalog/app/model/DataProducts/hooks.ts index 59340ca4268..9b78a839ce6 100644 --- a/catalog/app/model/DataProducts/hooks.ts +++ b/catalog/app/model/DataProducts/hooks.ts @@ -20,7 +20,6 @@ import * as Cache from 'utils/ResourceCache' import type { ContentsResult, DataProductAdapter, EntryBodyResult } from './adapter' import { supportsBrowsing, supportsFetching } from './adapter' -import { fixtureAdapter } from './fixtureAdapter' import type { AccessRequest } from './requests' import type { Connection } from './connections' import type { DataProduct } from './types' @@ -28,16 +27,22 @@ import type { DataProduct } from './types' /** * Which adapter the hooks read from. * - * A module-level constant rather than a React context, deliberately: there is + * A module-level choice rather than a React context, deliberately: there is * exactly one adapter per deployment, chosen by what the registry can serve, and * nothing in the UI ever wants two at once. A context would invite per-subtree * overriding -- which sounds flexible and in practice means two screens * disagreeing about what exists. * + * Imported dynamically because this module is on the volumes landing's import + * path (through `useProducts`), and a static import puts the whole fixture + * corpus in that chunk -- including for deployments with the preview off, which + * never read it. + * * When a GraphQL-backed adapter lands this becomes a build- or config-time * choice here, and no container changes. That is the whole point of the port. */ -const adapter: DataProductAdapter = fixtureAdapter +const loadAdapter = (): Promise => + import('./fixtureAdapter').then((m) => m.fixtureAdapter) // The cache keys on `input`; these resources take none beyond the ids below, so // `key` is explicit rather than relying on `R.identity` over an object. @@ -55,7 +60,7 @@ const adapter: DataProductAdapter = fixtureAdapter const ProductsResource = Cache.createResource({ name: 'DataProducts.list', fetch: ({ enabled }: { enabled: boolean }) => - enabled ? adapter.listProducts() : Promise.resolve([]), + enabled ? loadAdapter().then((a) => a.listProducts()) : Promise.resolve([]), // @ts-expect-error key: ({ enabled }: { enabled: boolean }) => enabled, }) @@ -63,21 +68,22 @@ const ProductsResource = Cache.createResource({ const ConnectionsResource = Cache.createResource({ name: 'DataProducts.connections', fetch: ({ enabled }: { enabled: boolean }) => - enabled ? adapter.listConnections() : Promise.resolve([]), + enabled ? loadAdapter().then((a) => a.listConnections()) : Promise.resolve([]), // @ts-expect-error key: ({ enabled }: { enabled: boolean }) => enabled, }) const ProductResource = Cache.createResource({ name: 'DataProducts.product', - fetch: ({ id }: { id: string }) => adapter.getProduct(id), + fetch: ({ id }: { id: string }) => loadAdapter().then((a) => a.getProduct(id)), // @ts-expect-error key: ({ id }: { id: string }) => id, }) const RequestsResource = Cache.createResource({ name: 'DataProducts.requests', - fetch: ({ productId }: { productId: string }) => adapter.listRequests(productId), + fetch: ({ productId }: { productId: string }) => + loadAdapter().then((a) => a.listRequests(productId)), // @ts-expect-error key: ({ productId }: { productId: string }) => productId, }) @@ -94,9 +100,11 @@ const ContentsResource = Cache.createResource({ // enumerate contents at all. NOT_FOUND is the honest answer -- we have no way // to look, so we did not find anything -- and it keeps the UI on one code // path rather than branching on adapter shape at every call site. - supportsBrowsing(adapter) - ? adapter.listContents(productId, member) - : Promise.resolve({ ok: false, reason: 'NOT_FOUND' }), + loadAdapter().then((a) => + supportsBrowsing(a) + ? a.listContents(productId, member) + : ({ ok: false, reason: 'NOT_FOUND' } as ContentsResult), + ), // @ts-expect-error key: ({ productId, member }: { productId: string; member: string }) => `${productId}::${member}`, @@ -110,9 +118,11 @@ const EntryBodyResource = Cache.createResource({ // Same reasoning as contents: an adapter that cannot fetch is a real shape, // not a broken one, and NOT_FOUND keeps the UI on one path rather than // branching on adapter capability at the call site. - supportsFetching(adapter) - ? adapter.fetchEntry(productId, member, logicalKey) - : Promise.resolve({ ok: false, reason: 'NOT_FOUND' }), + loadAdapter().then((a) => + supportsFetching(a) + ? a.fetchEntry(productId, member, logicalKey) + : ({ ok: false, reason: 'NOT_FOUND' } as EntryBodyResult), + ), // @ts-expect-error key: ({ productId, member, logicalKey }: EntryInput) => `${productId}::${member}::${logicalKey}`, @@ -197,14 +207,3 @@ export function useContents(productId: string, member: string): ContentsResult { { suspend: true }, ) as ContentsResult } - -/** - * The adapter itself, for the one thing hooks cannot express: asking whether a - * write path exists. - * - * Exposed so a container can call `supportsRequests(useAdapter())` and disable - * its submit affordance honestly, instead of hardcoding "no adapter yet". - */ -export function useAdapter(): DataProductAdapter { - return adapter -} diff --git a/catalog/app/model/DataProducts/index.ts b/catalog/app/model/DataProducts/index.ts index 6584145c1d8..40d7a279906 100644 --- a/catalog/app/model/DataProducts/index.ts +++ b/catalog/app/model/DataProducts/index.ts @@ -32,7 +32,6 @@ export type { } from './adapter' export { supportsBrowsing, supportsFetching, supportsRequests } from './adapter' export { - useAdapter, useConnections, useContents, useEntryBody,