import type { UseQueryResult } from "@tanstack/react-query"; import type { StapelApiError } from "@stapel/core"; import type { Category, CategoryChild, CategoryFeaturesResult, CategoryTreeNode, MaxRevision } from "../api/types.js"; import type { CategoryVisibilityOptions } from "../catalog/browse.js"; import type { BuildCategoryTreeOptions, CategoryIndex } from "../catalog/tree.js"; import type { CategorySnapshot } from "../catalog/sync.js"; import type { CatalogStore } from "./catalogStore.js"; /** * Read hooks over the categories API (frontend-standard §2). Keys are * namespaced (see `categoriesQueryKeys`). * * NOT SESSION-GATED, and that is the documented exception rather than an * omission. Sibling pairs gate every read on `useActiveSessionReady` because * their endpoints need a principal; every read here is a SAFE method under * `ReadOnlyOrStaff`, which anonymous callers may make. Gating them would make * a storefront's catalogue wait for a login bootstrap that a visitor who will * never sign in has no stake in. * * ── Every browse hook filters; no cache does ─────────────────────────────── * * The three hooks that produce rows for a surface a PERSON picks from — the * catalogue tree, a category's children, the carousel — run * `catalog/browse.ts`'s predicate and take {@link CategoryVisibilityOptions} * to opt out of it. The cache underneath never does: the catalogue snapshot * stores every row the sync sent (a filtered snapshot could not apply the next * delta), and the children/carousel reads keep the server's whole answer under * their query key and project it per observer through TanStack's `select`. So * a browse mount and an admin mount of the same endpoint share ONE cache entry * and disagree only about what they show. */ /** What `useCategoryCatalog` hands back beside the tree. */ export interface CategoryCatalog { readonly index: CategoryIndex; /** The snapshot the tree was built from — cursor included, so a host can * show "catalogue as of revision N" or drive its own refresh policy. */ readonly snapshot: CategorySnapshot; /** The page budget stopped the walk. The tree is a PARTIAL catalogue: a * missing branch here is not an empty branch, and a skin that can say so * should. */ readonly truncated: boolean; /** The walk started from nothing (cold cache), rather than applying a delta. */ readonly wasFullSync: boolean; } export interface UseCategoryCatalogOptions extends BuildCategoryTreeOptions { readonly enabled?: boolean; /** Rows per sync request (server default 100, max 1000). */ readonly pageSize?: number; /** Persistence seam. Default: an app-scoped `createRepository` store. Pass * `memoryCatalogStore()` to opt out of persistence entirely. */ readonly store?: CatalogStore; /** * How long the built catalogue is considered fresh. Default: five minutes. * * A catalogue changes when somebody edits it in the admin, not while a * person browses, and every refetch is a delta request that usually returns * zero rows — so the default is generous on purpose. `0` makes every mount * re-ask. */ readonly staleTime?: number; } /** * The whole category tree, delta-synced and cached across page loads. * * This is the hook a storefront mounts once, high up: `/c/:slug` resolves its * slug against it, breadcrumbs walk it, the picker in the compose form reads * it, and none of them costs a request. Without it every page pulls the * catalogue (spec §4.3). * * ── What one "load" actually does ────────────────────────────────────────── * * read the stored snapshot (app-scoped repository; may be empty) * → GET /categories/?include_deleted=false (cold) * or GET /categories/?min_revision= (warm) * → follow `has_next` with the window pinned by `max_revision` * → fold rows + `revisions.deleted_ids` into the snapshot * → persist, then build the tree * * The load discipline is the fleet's: `loading` while the first answer is in * flight (including while the repository read is), `failed` on a refusal — a * failed sync never degrades to "the catalogue is empty". Because the stored * snapshot is read INSIDE the query function, a warm start still reports * `loading` on the very first render and then resolves in the same tick; it * does not flash an empty tree. * * `retry: false`: a sync failure is worth reporting immediately, and the * caller has `refetch`. Three silent retries only delay the sentence. */ export declare function useCategoryCatalog(options?: UseCategoryCatalogOptions): UseQueryResult; /** Options every hook that PROJECTS rows for browsing shares. */ export interface CategoryBrowseOptions extends CategoryVisibilityOptions { readonly enabled?: boolean; } /** * One category's direct children, projected for browsing. * * The synced tree already knows them; this is for the host that does not mount * the catalogue — an SSR category page, a lazily expanded branch. The server * filters `deleted` here and orders by `tn_priority` descending, but it does * NOT filter `active` and knows nothing about test rows, so a public screen * still has to — and on a live classified deployment that is the difference * between a menu of categories and a menu of end-to-end leftovers. * * The projection runs in `select`, so the QUERY CACHE still holds the server's * complete answer and a sibling admin mount reads the same entry rather than * issuing a second request for the same children. */ export declare function useCategoryChildren(id: number | null | undefined, options?: CategoryBrowseOptions): UseQueryResult; /** * ONE category row by id — the cheapest read in the contract, and the hook a * server-driven walk is built out of. * * 311 bytes and a third of a second on a live classified deployment, against * 1.4 MB and twenty seconds for the catalogue that also answers this. Every * surface that already HAS an id — a search chip naming its own narrowing, a * category landing reached by a link, a cascade reconciling a deep value — * asks this instead of mounting the sync. * * The answer carries `tn_ancestors_pks`, so it is also the whole breadcrumb * chain and the whole cascade chain: see {@link useCategoryRows}. * * NOT browse-projected. A row asked for BY ID was named by the caller, and a * hook that answered `undefined` for an inactive category would turn "you * linked to a hidden row" into "the request failed". The caller decides; * `isBrowsableCategory` is one call away. */ export declare function useCategory(id: number | null | undefined, options?: { readonly enabled?: boolean; readonly staleTime?: number; }): UseQueryResult; /** What a fan-out of small reads answers, beside the rows themselves. */ export interface CategoryFanOut { /** One entry per id asked for, in that order. `null` = not answered yet, or * answered with a refusal — `error` says which. */ readonly rows: readonly (T | null)[]; /** At least one read is still in flight. */ readonly isPending: boolean; /** The first refusal, or `null`. One error for the fan-out: a ladder with * one broken rung is a broken ladder, and four identical alerts are not * four sentences. */ readonly error: StapelApiError | null; } /** * Several category rows by id, one small request each, in parallel. * * The breadcrumb of a deep category is exactly this: `tn_ancestors_pks` gives * the ids, and the names come back in one round trip of 300-byte reads that * every other surface then finds in the cache. A tree sync answers the same * question for four thousand times the bytes. * * `ids` may be empty, and then nothing is requested — TanStack still needs the * hook to be called unconditionally, which is why this is a fan-out hook and * not a loop of {@link useCategory} at the call site. */ export declare function useCategoryRows(ids: readonly number[], options?: { readonly enabled?: boolean; readonly staleTime?: number; }): CategoryFanOut; /** * The children of several parents — ONE RUNG PER PARENT, one small request * each, in parallel. * * This is the whole server-driven walk. A cascade four levels deep is four * requests of one to four kilobytes; the catalogue that would answer the same * four questions from memory costs 1.4 MB before the first of them can be * asked, which on a 3583-row catalogue was measured at twenty seconds. * * A `null` parent means "the top of a rootless ladder", which this module * CANNOT answer from the server: there is no roots endpoint, and the list * endpoint takes no `tn_parent` filter (see MODULE.md's upstream asks). Such a * rung is left unanswered here and the caller supplies it — the one place the * catalogue sync is still the only source. * * The browse projection runs per rung, over the server's complete answer kept * in the cache, exactly as {@link useCategoryChildren} does — so an admin * mount and a storefront mount of the same rung share one cache entry. */ export declare function useCategoryLevels(parentIds: readonly (number | null)[], options?: CategoryBrowseOptions): CategoryFanOut; /** {@link useCategoryCarousel}'s options — the browse flags plus freshness. */ export interface UseCategoryCarouselOptions extends CategoryBrowseOptions { readonly staleTime?: number; } /** * The carousel strip for a landing page. * * The endpoint the server filters MOST completely (`active` AND * `carousel_enabled`) and already caches, sending * `Cache-Control: public, max-age`. `staleTime` mirrors that: the browser's * HTTP cache and the query cache should not disagree about how fresh this is. * * The browse projection still runs. Not because the server's `active` filter * is doubted, but because the two other halves of the predicate are not the * server's to apply here: a tombstone and a test row are both filtered on the * client, and this row of tiles is the most prominent browse surface the * storefront has. A surface that skipped the shared predicate would be the one * place a filtered row could still reach a person. */ export declare function useCategoryCarousel(options?: UseCategoryCarouselOptions): UseQueryResult; /** * A category's resolved feature schema — own plus inherited, deduplicated by * slug, in the category's own order. * * This is the payload `@stapel/attributes-react` draws (the compose form) and * labels facets from (the search pair's `categoryFeatures` slot). It changes * only when the catalogue is edited, hence the same generous `staleTime`. * * Since stapel-categories 0.20.1 the answer also carries `effectiveFrom`: * `"own"` (this row's own resolved schema, as always) or `"children"` (a * `chips` parent with none of its own — the intersection of its children's, * `divergent: true` beside a feature they disagree on). See * `headless/CategoryFeatures.tsx`'s `visibleFeatures` for what a renderer * does with a divergent row. */ export declare function useCategoryFeatures(id: number | null | undefined, options?: { readonly enabled?: boolean; readonly staleTime?: number; }): UseQueryResult; /** The levels a menu asks for when nobody says otherwise: roots, their * children, and one more — the three the mega-menu draws. */ export declare const DEFAULT_TREE_DEPTH = 3; /** {@link useCategoryTree}'s options. */ export interface UseCategoryTreeOptions { readonly enabled?: boolean; readonly staleTime?: number; } /** * The first `depth` levels of the catalogue, NESTED — one request, one cached * answer, and the read the desktop mega-menu is built on. * * The alternative it replaces is the reason it exists: assembled from `roots` * plus a `children` call per node, a three-level menu is one request per * branch on the coldest page of a storefront; assembled from the synced * catalogue it is the whole table (1.4 MB, twenty seconds measured) before the * first name can be drawn. This is four fields per node and the presentation * of the level below. * * NOT browse-projected, and that is not an omission: the endpoint serves * ACTIVE rows only and no tombstones at all, so the client predicate would * have nothing left to remove. It is also not a catalogue source — the nodes * carry no revision, so nothing here can seed or advance the delta snapshot. * * The server caches on the tree's own revision fingerprint and sends * `Cache-Control: public, max-age`; `staleTime` mirrors that, so the query * cache and the browser's HTTP cache do not disagree about how fresh a menu * is. */ export declare function useCategoryTree(depth?: number, options?: UseCategoryTreeOptions): UseQueryResult; /** * The catalogue's current maximum revision — the cheap freshness probe. * * A host that wants to poll for catalogue changes without transferring rows * compares this to `catalog.snapshot.cursor` and invalidates the catalogue key * when it moves. Not wired into `useCategoryCatalog` on purpose: how often to * ask is a deployment's decision, and a library that picked one would be * polling somebody's API on their behalf. */ export declare function useCategoriesRevision(options?: { readonly enabled?: boolean; readonly refetchInterval?: number; }): UseQueryResult; //# sourceMappingURL=queries.d.ts.map