/** * use-shape-fake - a stand-in for the SDK's React hook, `@ng-org/orm`'s `useShape`. * * Not a `*.test.ts`, so `bun test` does not pick it up: it is the injection the suites * share, and it is the seam the package is built on - an application hands `configure` the * real hook, and a suite hands it this. Sibling of `orm-fake.ts`, which does the same for * the shape query one floor down. * * -- Why it returns the ENVELOPE, and why that is the whole point ----------- * Every fake in this repository used to return a bare Set-like, because that is what the * hook returned in `alpha.18`. `@ng-org/orm@0.1.2-alpha.21` returns a `UseShapeResult` * AROUND the collection, and while no fake said so, a capability filter that handed three * of that envelope's members back RAW could not fail a single test. So the shape below is * read off the artifact that settles it - the installed package's * `dist/frontendAdapters/react/useShape.d.ts`, and the `dist/.../useShape.js` beside it, * which agree - and not off what this package wished the hook returned: * * { data, promise, subscription, isLoading, nextPage, previousPage } * * Each of the six is reproduced with the cardinality the declaration gives it: * - `data` is a `DeepSignalSet` for an unordered config and a `readOnlyArray` over a * `DeepSignal` array once `orderBy` is asked for (upstream `SubscriptionData`), * built with upstream's OWN constructors, and `undefined` while nothing has arrived; * - `promise` resolves to that same collection, and is `undefined` when `conf` is; * - `subscription` is the handle, carrying the items three ways of its own - * `signalObject`, `readyPromise` and the raw arrays a change listener is handed; * - `isLoading` is a boolean, the one member that carries nothing - reproduced with the * shipped EXPRESSION, `ormSubscription.isReady`, which is the INVERSE of what the * declaration beside it describes ("`true` when no data is available yet"). The * inversion is upstream's, and a fake that quietly corrected it would be measuring a * hook nobody ships - which is the exact blindness this file exists to remove; * - the two page commands exist only under a paginated config, as `WithMaybePagination` * `Omit`s them otherwise and the shipped hook leaves them `undefined`. * * What it does NOT invent: no member the declaration does not carry. A fake richer than * the target proves things about a surface nobody ships. */ // The REAL collection kinds, not stand-ins for them. `data` is what `useDeepSignal` wraps // - a `DeepSignalSet` unordered, a `readOnlyArray` over a `DeepSignal` array once an // `orderBy` is asked for - and the two behave in ways a plain `Set` and a plain array do // not: the set's `add` hands the proxy back, and the ordered one REFUSES a write to an // index, to `length`, and even a `get` of `push`. A fake that used the plain kinds was // MORE PERMISSIVE than what ships (it let a caller overwrite an item by index), having // been more CORRECT than it one round earlier. Neither is a fake's business. import { deepSignal, readOnlyArray } from "@ng-org/alien-deepsignals"; import type { BaseType } from "@ng-org/shex-orm"; import type { ShapeResult, UseShapeLike } from "../src/model/types"; /** The subscription handle the envelope carries - the members `RdfOrmSubscription` exposes * that reach the items, which is what makes handing it on a leak. */ export interface FakeSubscription { readonly signalObject: unknown; readonly readyPromise: Promise; readonly isReady: boolean; addChangeListener(l: (c: { adds: unknown[]; updates: unknown[]; removes: unknown[] }) => void): void; close(): void; } export interface FakeShapeHook { /** What goes into `configure({ useShape })`. */ hook: UseShapeLike; /** The rows the fake engine holds. A suite mutates this array in place. */ items: BaseType[]; /** The RAW collection the last call handed back - what a leak reaches, and what a test * compares the filtered view against. */ raw(): unknown; /** Pages asked for, in order, so a test can see a forwarded command arrive. */ pages: string[]; } /** * A fake `useShape` over an in-memory row list. * * `loaded` (the default) answers as a hook whose payload has arrived. Turn it off for the * state the declaration describes as "still loading": `promise` present but `data` not * there yet. Note which way `isLoading` then reads - see the note above; it follows the * shipped expression, not the shipped prose. */ export function makeUseShapeHook( options: { loaded?: boolean; paginated?: boolean } = {}, ): FakeShapeHook { const items: BaseType[] = []; const pages: string[] = []; const loaded = options.loaded !== false; const paginated = options.paginated === true; let lastRaw: unknown = undefined; const hook: UseShapeLike = (_shapeType, conf): ShapeResult => { // Upstream: "If the conf is explicitly set to `undefined`, the returned data is // `undefined` and `isLoading` is false" - and `promise`/`subscription` are typed // `CONF extends undefined ? undefined : ...`, so all three go together. if (conf === undefined) { lastRaw = undefined; return { data: undefined, promise: undefined, subscription: undefined, isLoading: false, nextPage: undefined, previousPage: undefined, }; } // `SubscriptionData`: a set with no `orderBy`, an ordered array with one. const ordered = typeof conf === "object" && conf.orderBy !== undefined; const collection: unknown = ordered ? readOnlyArray(deepSignal([...items]) as never) : deepSignal(new Set(items)); lastRaw = collection; const subscription: FakeSubscription = { signalObject: collection, readyPromise: Promise.resolve(collection), isReady: loaded, addChangeListener(l): void { l({ adds: [...items], updates: [], removes: [] }); }, close(): void {}, }; return { data: loaded ? collection : undefined, promise: Promise.resolve(collection), subscription, // Upstream, verbatim: `!ormSubscription ? false : ormSubscription.isReady`. There is // a subscription here, so this is `isReady` - and `isReady` is `loaded`. isLoading: loaded, nextPage: paginated ? (): void => void pages.push("next") : undefined, previousPage: paginated ? (): void => void pages.push("previous") : undefined, }; }; return { hook, items, raw: () => lastRaw, pages }; }