Files
ng-eventually/packages/polyfill/test/use-shape-fake.ts
Sylvain Duchesne eb956c880d fix: les trois mutations posent la meme question, et la vue porte ses propres ecritures
L-ASYMETRIE, une fois de plus, et c-est elle le defaut. `delete` et `clear`
interrogeaient `keep`, `add` non - ce qui rendait la phrase publiee vraie par
coincidence : le `add` non garde FABRIQUAIT l-etat que les deux autres ne
traitaient qu-a moitie. Bob plante un objet dans le document d-ALICE, ne le
revoit jamais (la vue le refiltre), la liaison bidirectionnelle le commite
la-bas, puis `clear()` le laisse derriere. Les trois posent maintenant la meme
question ; ce qu-elles repondent differe parce que les membres different :
`delete` a `false`, qui est le mot du contrat Set pour "il n-y etait pas" et qui
est vrai de la vue ; `add` n-a aucune valeur de ce sens, donc il leve.

LA VUE PORTE SES PROPRES ECRITURES. Aucun piege `set`/`deleteProperty`/
`defineProperty` : contre un tableau ordonne, `view[0] = x`, `delete view[0]` et
`view.length = 0` passaient - le dernier defaisant `clear` entierement. Ce qui
est livre est un `readOnlyArray` qui les refuse, donc la production etait
protegee PAR LA CIBLE : la vue s-appuyait sur une garantie qu-elle ne donne pas,
sur une forme qu-elle ne controle pas. C-est l-hypothese empruntee qui est
derriere chaque defaut de cette famille.

LE PROPRIETAIRE EST SERVI. 18 membres declares d-un resultat ordonne levaient
pour celui dont ce sont les donnees, plus `includes` et les sept combinateurs
d-ensemble : une fuite echangee contre une panne. Tout ce que
`@ng-org/alien-deepsignals` declare non mutant est servi sur les objets
lisibles, et un test relit le fichier livre pour que la liste restee ici ne
derive pas.

Aussi : le descripteur d-un ACCESSEUR routait vers la cible ; le faux construit
maintenant les VRAIES collections (`deepSignal`, `readOnlyArray`) au lieu d-un
Set et d-un tableau nus, ce qui le rendait plus permissif que ce qui est livre
apres l-avoir rendu plus correct au round precedent.
2026-09-24 19:38:14 +02:00

133 lines
6.5 KiB
TypeScript

/**
* 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<T, CONF>`),
* 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<unknown>;
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<T, CONF>`: 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 };
}