@lankajs/react 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  "use client";
2
- import "./chunk-7D4SUZUM.js";
3
2
 
4
3
  // src/use-lanka-vm/useLankaVM.ts
5
4
  import { useCallback, useMemo, useSyncExternalStore } from "react";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/use-lanka-vm/useLankaVM.ts","../src/to-lanka-react-vm/toLankaReactVM.ts","../src/use-lanka-shallow/useLankaShallow.ts"],"sourcesContent":["import { useCallback, useMemo, useSyncExternalStore } from \"react\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * Reads a ViewModel from a React component.\n *\n * ```tsx\n * export const TodoScreen = () => {\n * \tconst { todos, isLoading, load } = useLankaVM(todoVM);\n * \t…\n * };\n * ```\n *\n * Without a selector the component receives a Proxy that records which keys it\n * read, and the next change re-renders it only if one of THOSE keys moved. With\n * a selector the selector decides and tracking is bypassed.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are\n * `createLankaAccessTracker` in core. Every binding on this shelf calls it, which\n * is what makes \"a screen re-renders for the keys it read\" a fact about lanka\n * rather than a fact about React — and what\n * `lankaViewBindingConformance` holds all of them to.\n *\n * What is left is React: a tracker per mounted component and per ViewModel, a\n * stable `subscribe`, and `useSyncExternalStore`. If this file ever needs more than the port gives it,\n * the port has the defect and the fix belongs in core, for everybody.\n *\n * ## The blind spot, unchanged\n *\n * A component re-renders only for keys it READ off the returned proxy. An action\n * that DERIVES a value reads the store through `get`, which the proxy never sees\n * — so a component whose only link to a key is such a getter never re-renders\n * for it. Set `enableAccessTrackingOptimization: false` on that ViewModel; in\n * development the mismatch announces itself by name. Canon: `skills/parity`.\n */\nexport function useLankaVM<TState extends object>(viewModel: ILankaReadableVM<TState>): TState;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): TSelected;\n\n/**\n * The overload a WRAPPER needs: a selector it was handed, which may be absent.\n *\n * The two above describe the two things a screen does, and neither accepts\n * `undefined` — so a hook that forwards its own optional argument had to branch,\n * and a branch around a hook call is the one thing React's lint rule refuses\n * outright. `toLankaReactVM` is such a wrapper, and so is every wrapper a\n * consumer writes over this one.\n *\n * The answer widens to `TState | TSelected` because it genuinely is not known\n * which: that is the price of not knowing at the type level whether a selector\n * arrived, and a caller who does know keeps one of the two overloads above.\n */\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: ((state: TState) => TSelected) | undefined,\n): TState | TSelected;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): TState | TSelected {\n\t/**\n\t * One tracker per mounted component, and per ViewModel it is pointed at.\n\t *\n\t * Two components over one ViewModel read different keys and must re-render for\n\t * different changes, so the recording belongs to the reader — which is why\n\t * this is built here and not shared.\n\t *\n\t * Keyed on the ViewModel, because `createLankaAccessTracker` closes over the\n\t * one it was given, permanently. This was a ref initialised once, and a\n\t * component handed a DIFFERENT ViewModel at the same mount point — an ordinary\n\t * prop change — kept a tracker reading the first one: `subscribe` WAS rebuilt\n\t * for the new ViewModel and woke the component correctly, and every render\n\t * then re-read the old one's state. A live subscription and a frozen screen,\n\t * with no error anywhere.\n\t *\n\t * `useMemo` rather than a ref written during render: the ref is the shape\n\t * `react-hooks/refs` allows only for initialise-once, and rightly — a ref\n\t * written on a condition during render is the impure render the rule exists to\n\t * catch. The factory runs when the ViewModel moves and at no other time, which\n\t * is exactly the lifetime the recording should have. If React ever discards\n\t * the cache it discards `subscribe` with it, so the two cannot disagree; a\n\t * fresh tracker has recorded nothing, and a reader that has read nothing is\n\t * notified of everything — more renders, never fewer.\n\t */\n\tconst tracker = useMemo(() => createLankaAccessTracker(viewModel), [viewModel]);\n\n\t/**\n\t * Whether a selector was passed, which is all `subscribe` needs to know.\n\t *\n\t * Not the selector itself. A selector is usually an inline arrow with a new\n\t * identity every render, so keying the subscription on it would tear the\n\t * subscription down and rebuild it on EVERY render — the failure measured at\n\t * 201 subscriptions for 200 renders. Whether there IS one is a boolean that\n\t * does not change at a given call site, so the subscription stands still.\n\t */\n\tconst hasSelector = selector !== undefined;\n\n\t/**\n\t * Stable identity, and the dependencies are the two things that genuinely\n\t * change what the subscription DOES.\n\t *\n\t * `useSyncExternalStore` keeps `subscribe` in an effect keyed on its identity.\n\t * A different ViewModel is a different subscription and must be rebuilt; so is\n\t * switching between tracked and selected reads. Neither moves in practice —\n\t * a ViewModel is a module-level object — so a mounted component subscribes\n\t * once and stays subscribed.\n\t */\n\tconst subscribe = useCallback(\n\t\t(onStoreChange: () => void) =>\n\t\t\tviewModel.subscribe((next, prev) => {\n\t\t\t\tif (hasSelector) {\n\t\t\t\t\tonStoreChange();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (tracker.shouldNotify(next, prev)) {\n\t\t\t\t\tonStoreChange();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Reaching here means NO re-render will follow. If the changed key is\n\t\t\t\t// linked to this component through a getter it read, the screen froze —\n\t\t\t\t// and in development core says so by name.\n\t\t\t\ttracker.reportSkipped(next, prev);\n\t\t\t}),\n\t\t// `tracker` moves only when `viewModel` does, so naming it costs no rebuild\n\t\t// the first dependency was not already going to cause.\n\t\t[viewModel, hasSelector, tracker],\n\t);\n\n\t/**\n\t * The selection, remembered against the STATE it was taken from.\n\t *\n\t * `useSyncExternalStore` reads the snapshot during render and AGAIN after\n\t * committing, and re-renders when the two differ by `Object.is`. A selector\n\t * that builds its answer — `(state) => ({ id: state.id })`, `(state) =>\n\t * rows.filter(…)`, the first shape a consumer reaches for — is never identical\n\t * to its own previous result, so the two reads never agreed and the component\n\t * rendered until React stopped it: \"Maximum update depth exceeded\", on the\n\t * commonest selector there is. The conformance suite's fresh-object scene is\n\t * what named it; the other four bindings compare the selection to the last one\n\t * and merely wake more often than they need to.\n\t *\n\t * So the selector runs once per STATE object and the answer is held. The two\n\t * reads of one commit then see the same reference, and the loop closes.\n\t *\n\t * Rebuilt when the selector's identity moves, which is what keeps a selector\n\t * computed from props honest: an inline arrow is a new function every render,\n\t * so the memo is fresh at the start of each render and warm by the time the\n\t * post-commit read arrives — which is the whole of what the comparison needs.\n\t * The subscription does not depend on it and stands still regardless.\n\t */\n\tconst selectFromState = useMemo(() => {\n\t\tif (!selector) return null;\n\n\t\tlet taken = false;\n\t\tlet takenFrom: TState;\n\t\tlet picked: TSelected;\n\n\t\treturn (state: TState): TSelected => {\n\t\t\tif (taken && Object.is(takenFrom, state)) return picked;\n\n\t\t\ttaken = true;\n\t\t\ttakenFrom = state;\n\t\t\tpicked = selector(state);\n\n\t\t\treturn picked;\n\t\t};\n\t}, [selector]);\n\n\t/**\n\t * Read during render, so it may close over this render's selector directly.\n\t *\n\t * `getSnapshot` is not kept in an effect and has no stability requirement —\n\t * which is what lets the selector stay a plain argument. The alternative was a\n\t * ref written during render to keep the latest one, and a ref written during\n\t * render is an impure render that React's own lint rule refuses.\n\t */\n\tconst readTracked = (): TState | TSelected =>\n\t\tselectFromState ? selectFromState(viewModel.getState()) : tracker.read();\n\n\t/**\n\t * The server snapshot: the state itself, never the Proxy.\n\t *\n\t * Tracking exists to skip renders a client would otherwise do; a server\n\t * renders once, and handing it a recording Proxy would only add work whose\n\t * result nothing reads.\n\t */\n\tconst readUntracked = (): TState | TSelected => {\n\t\tconst state = viewModel.getState();\n\n\t\treturn selector ? selector(state) : state;\n\t};\n\n\treturn useSyncExternalStore(subscribe, readTracked, readUntracked);\n}\n","import { useLankaVM } from \"../use-lanka-vm/useLankaVM\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * A ViewModel that is also a hook — what React called a ViewModel before the\n * port existed, and what it may go on calling one.\n *\n * Both call shapes, because both were there: no argument gives the tracked read,\n * a selector gives what the selector picked and bypasses tracking.\n */\nexport type TLankaReactVMHook<TState extends object> = {\n\t(): TState;\n\t<TSelected>(selector: (state: TState) => TSelected): TSelected;\n};\n\n/** The ViewModel it was given, plus the ability to be called like a hook. */\nexport type TLankaReactVM<TViewModel extends ILankaReadableVM<object>> = TViewModel &\n\tTLankaReactVMHook<ReturnType<TViewModel[\"getState\"]>>;\n\n/**\n * What must keep coming from the FUNCTION rather than from the ViewModel.\n *\n * Everything else a caller reads by name is the ViewModel's — including `name`,\n * which is the ViewModel's name and was the ViewModel's name before this\n * function existed, because `build()` defines it over the store.\n *\n * Symbols are excluded wholesale, and that is not tidiness. The ViewModel behind\n * this may be a LAZY proxy, which answers an unknown property with a wrapper\n * function; a wrapper handed back for `Symbol.iterator` makes the hook look\n * iterable, one for `Symbol.toPrimitive` breaks every string coercion of it, and\n * one for `$$typeof` makes React look at it as an element. None of those is a\n * member of any ViewModel, so none of them may be forwarded.\n */\nconst FUNCTION_MEMBERS: ReadonlySet<string> = new Set([\n\t\"prototype\",\n\t\"length\",\n\t\"arguments\",\n\t\"caller\",\n\t\"constructor\",\n\t\"call\",\n\t\"apply\",\n\t\"bind\",\n\t\"toString\",\n]);\n\n/**\n * How the callable answers for the ViewModel behind it.\n *\n * Its own function, because the two traps are the whole mechanism and the\n * factory above is then three lines — one hook, one Proxy, one cast. Read\n * together they were forty-two lines whose shape said \"a function doing two\n * things\", which is what the composition canon calls it.\n */\nconst forwardToViewModel = <TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ProxyHandler<(selector?: (state: object) => unknown) => unknown> => {\n\tconst members = viewModel as unknown as Record<string, unknown>;\n\n\treturn {\n\t\tget: (target, property, receiver): unknown =>\n\t\t\ttypeof property === \"symbol\" || FUNCTION_MEMBERS.has(property)\n\t\t\t\t? Reflect.get(target, property, receiver)\n\t\t\t\t: members[property],\n\n\t\t/**\n\t\t * `in` answers for the ViewModel too.\n\t\t *\n\t\t * Without this the hook would report that it has no `getState`, while\n\t\t * reading `getState` hands one back — and `\"getState\" in useTodoVM` is how a\n\t\t * devtool, a serialiser and a duck-typed helper ask. The ViewModel behind\n\t\t * this may be a lazy proxy with no `has` trap of its own, so the question is\n\t\t * answered by READING the property, which for a lazy ViewModel builds\n\t\t * nothing.\n\t\t */\n\t\thas: (target, property) =>\n\t\t\tReflect.has(target, property) ||\n\t\t\t(typeof property === \"string\" && members[property] !== undefined),\n\t};\n};\n\n/**\n * Gives a ViewModel React's own ergonomics back.\n *\n * ```ts\n * // the ViewModel, framework-free, exactly as Vue and Svelte receive it\n * const todoVM = createLankaVM({ … });\n *\n * // the same object, callable\n * export const useTodoVM = toLankaReactVM(todoVM);\n * ```\n *\n * ```tsx\n * const { todos, load } = useTodoVM();\n * const count = useTodoVM((state) => state.todos.length);\n * const todos = useTodoVM.getState().todos; // outside a component, as always\n * ```\n *\n * ## Why this exists\n *\n * Until 2.0 a ViewModel WAS a React hook: `createLankaVM` returned a callable,\n * and every screen in every application on lanka called it. Making the framework\n * framework-free took the call signature away — correctly, because four of the\n * five bindings have no use for one and core may not know what a hook is.\n *\n * That is a fact about CORE, and it was allowed to become a fact about React,\n * which it never had to be. A React consumer's familiar spelling costs one\n * wrapper in the one package that is allowed to know what a hook is, so here it\n * is: `useTodoVM()` reads, `useTodoVM(selector)` selects, `useTodoVM.getState()`\n * and `useTodoVM.subscribe()` do what they always did.\n *\n * ## What it does NOT do\n *\n * It does not change the ViewModel. There is exactly one store, and the call\n * forwards to `useLankaVM` — the same function the five bindings' conformance\n * suite drives. A ViewModel read through this and the same ViewModel read in Vue\n * answer identically, notify identically and skip identically, because it is the\n * same object either way and this adds no state of its own.\n *\n * It is also not required. `useLankaVM(todoVM)` is the portable spelling, it\n * stays the one the guides teach, and a codebase that has moved to it needs\n * nothing here.\n *\n * ## Laziness survives\n *\n * The forwarding is a Proxy rather than copied properties, so a ViewModel that\n * builds on first access still builds on first access: reading `useTodoVM.name`\n * answers from the config and constructs nothing.\n */\nexport const toLankaReactVM = <TViewModel extends ILankaReadableVM<object>>(\n\tviewModel: TViewModel,\n): TLankaReactVM<TViewModel> => {\n\t/**\n\t * The call signature, and the whole of it.\n\t *\n\t * Named `useViewModel` rather than `hook`: this IS a custom hook — it calls\n\t * one, it may only be called during a render, and a name not starting with\n\t * `use` hid both facts from every reader and from React's lint rule.\n\t *\n\t * ONE call, with the selector forwarded as it arrived — there is a\n\t * `useLankaVM` overload for exactly this. Written as a branch first, and the\n\t * lint rule was right to refuse it: a hook inside a ternary is a hook React\n\t * cannot promise to call in the same order, and the fact that both arms\n\t * happened to call the same one is not something a reader or a rule can see.\n\t *\n\t * Whether a selector was passed is a property of the CALL SITE and never\n\t * changes between renders, which is what `useLankaVM` relies on to keep one\n\t * subscription standing across a component's life.\n\t */\n\tconst useViewModel = (selector?: (state: object) => unknown): unknown =>\n\t\tuseLankaVM(viewModel, selector);\n\n\treturn new Proxy(useViewModel, forwardToViewModel(viewModel)) as TLankaReactVM<TViewModel>;\n};\n","import { useRef } from \"react\";\nimport { createLankaShallowHold } from \"lanka/viewmodel\";\n\n/**\n * Keeps a selector's answer stable when nothing in it changed.\n *\n * ```tsx\n * const { title, status } = useLankaVM(missionVM, useLankaShallow((s) => ({\n * \ttitle: s.title,\n * \tstatus: s.status,\n * })));\n * ```\n *\n * ## The waste this exists for\n *\n * `useLankaVM(vm, (s) => ({ a: s.a }))` is the commonest thing a React reader\n * writes, and unwrapped it wakes the component for EVERY change in the\n * ViewModel. The binding holds a selection against the state object it came\n * from, which is what a snapshot has to be; a fresh object is new whenever the\n * state is new, so a reader that took a selector to say \"only `a`\" is repainted\n * by a change to `z`. This is the comparison that makes the statement mean\n * something.\n *\n * A selector returning a primitive never had the problem, which is what makes\n * the waste quiet: the shape that is free and the shape that repaints on\n * everything look the same on the page.\n *\n * It used to be worse. Until `useLankaVM` ran its selector once per state\n * object, an unwrapped one CRASHED — `useSyncExternalStore` reads the snapshot\n * during render and again after committing, a fresh object disagreed with\n * itself, and the component rendered until React stopped it with \"Maximum update\n * depth exceeded\". That is closed in the binding, for everybody.\n *\n * ## What is React's here, and what is not\n *\n * The comparison is `createLankaShallowHold` in core, and every binding on the\n * shelf can reach it. It was this file's, and that made it a CAPABILITY React had\n * and four siblings did not — an idiom is a spelling, and this changes which\n * notifications reach a reader. `skills/parity/SKILL.md` 3c.\n *\n * What is left is the part only React needs. A component re-runs this hook on\n * every render, so the holding has to SURVIVE a render while the selector stays\n * the current one: the hold lives in a ref initialised once, and the closure\n * returned below closes over this render's `selector`. A selector computed from\n * props therefore stays honest, and the hold does not reset under it.\n *\n * ## Why a wrapper and not an equality argument\n *\n * `useLankaVM(vm, selector, isEqual)` was the other option, and it puts the\n * comparison in the binding for every caller — including the ones whose\n * selection is a string and pay for a comparison they cannot fail. This is opt\n * in at the call site, which is also where a reader can see it.\n *\n * The shape is React's own: a hook that returns a selector. A consumer arriving\n * from zustand has typed `useShallow` and needs no explanation, which is the\n * whole point of an idiom.\n */\nexport const useLankaShallow = <TState, TSelected>(\n\tselector: (state: TState) => TSelected,\n): ((state: TState) => TSelected) => {\n\tconst hold = useRef<((next: TSelected) => TSelected) | null>(null);\n\thold.current ??= createLankaShallowHold<TSelected>();\n\n\treturn (state: TState): TSelected => hold.current!(selector(state));\n};\n"],"mappings":";;;;AAAA,SAAS,aAAa,SAAS,4BAA4B;AAC3D,SAAS,gCAAgC;AA8DlC,SAAS,WACf,WACA,UACqB;AAyBrB,QAAM,UAAU,QAAQ,MAAM,yBAAyB,SAAS,GAAG,CAAC,SAAS,CAAC;AAW9E,QAAM,cAAc,aAAa;AAYjC,QAAM,YAAY;AAAA,IACjB,CAAC,kBACA,UAAU,UAAU,CAAC,MAAM,SAAS;AACnC,UAAI,aAAa;AAChB,sBAAc;AACd;AAAA,MACD;AAEA,UAAI,QAAQ,aAAa,MAAM,IAAI,GAAG;AACrC,sBAAc;AACd;AAAA,MACD;AAKA,cAAQ,cAAc,MAAM,IAAI;AAAA,IACjC,CAAC;AAAA;AAAA;AAAA,IAGF,CAAC,WAAW,aAAa,OAAO;AAAA,EACjC;AAwBA,QAAM,kBAAkB,QAAQ,MAAM;AACrC,QAAI,CAAC,SAAU,QAAO;AAEtB,QAAI,QAAQ;AACZ,QAAI;AACJ,QAAI;AAEJ,WAAO,CAAC,UAA6B;AACpC,UAAI,SAAS,OAAO,GAAG,WAAW,KAAK,EAAG,QAAO;AAEjD,cAAQ;AACR,kBAAY;AACZ,eAAS,SAAS,KAAK;AAEvB,aAAO;AAAA,IACR;AAAA,EACD,GAAG,CAAC,QAAQ,CAAC;AAUb,QAAM,cAAc,MACnB,kBAAkB,gBAAgB,UAAU,SAAS,CAAC,IAAI,QAAQ,KAAK;AASxE,QAAM,gBAAgB,MAA0B;AAC/C,UAAM,QAAQ,UAAU,SAAS;AAEjC,WAAO,WAAW,SAAS,KAAK,IAAI;AAAA,EACrC;AAEA,SAAO,qBAAqB,WAAW,aAAa,aAAa;AAClE;;;ACzKA,IAAM,mBAAwC,oBAAI,IAAI;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAUD,IAAM,qBAAqB,CAC1B,cACsE;AACtE,QAAM,UAAU;AAEhB,SAAO;AAAA,IACN,KAAK,CAAC,QAAQ,UAAU,aACvB,OAAO,aAAa,YAAY,iBAAiB,IAAI,QAAQ,IAC1D,QAAQ,IAAI,QAAQ,UAAU,QAAQ,IACtC,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYpB,KAAK,CAAC,QAAQ,aACb,QAAQ,IAAI,QAAQ,QAAQ,KAC3B,OAAO,aAAa,YAAY,QAAQ,QAAQ,MAAM;AAAA,EACzD;AACD;AAkDO,IAAM,iBAAiB,CAC7B,cAC+B;AAkB/B,QAAM,eAAe,CAAC,aACrB,WAAW,WAAW,QAAQ;AAE/B,SAAO,IAAI,MAAM,cAAc,mBAAmB,SAAS,CAAC;AAC7D;;;ACxJA,SAAS,cAAc;AACvB,SAAS,8BAA8B;AAwDhC,IAAM,kBAAkB,CAC9B,aACoC;AACpC,QAAM,OAAO,OAAgD,IAAI;AACjE,OAAK,YAAY,uBAAkC;AAEnD,SAAO,CAAC,UAA6B,KAAK,QAAS,SAAS,KAAK,CAAC;AACnE;","names":[]}
1
+ {"version":3,"sources":["../src/use-lanka-vm/useLankaVM.ts","../src/to-lanka-react-vm/toLankaReactVM.ts","../src/use-lanka-shallow/useLankaShallow.ts"],"sourcesContent":["import { useCallback, useMemo, useSyncExternalStore } from \"react\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * Reads a ViewModel from a React component.\n *\n * ```tsx\n * export const TodoScreen = () => {\n * \tconst { todos, isLoading, load } = useLankaVM(todoVM);\n * \t…\n * };\n * ```\n *\n * Without a selector the component receives a Proxy that records which keys it\n * read, and the next change re-renders it only if one of THOSE keys moved. With\n * a selector the selector decides and tracking is bypassed.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are\n * `createLankaAccessTracker` in core. Every binding on this shelf calls it, which\n * is what makes \"a screen re-renders for the keys it read\" a fact about lanka\n * rather than a fact about React — and what\n * `lankaViewBindingConformance` holds all of them to.\n *\n * What is left is React: a tracker per mounted component and per ViewModel, a\n * stable `subscribe`, and `useSyncExternalStore`. If this file ever needs more than the port gives it,\n * the port has the defect and the fix belongs in core, for everybody.\n *\n * ## The blind spot, unchanged\n *\n * A component re-renders only for keys it READ off the returned proxy. An action\n * that DERIVES a value reads the store through `get`, which the proxy never sees\n * — so a component whose only link to a key is such a getter never re-renders\n * for it. Set `enableAccessTrackingOptimization: false` on that ViewModel; in\n * development the mismatch announces itself by name. Canon: `skills/parity`.\n */\nexport function useLankaVM<TState extends object>(viewModel: ILankaReadableVM<TState>): TState;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): TSelected;\n\n/**\n * The overload a WRAPPER needs: a selector it was handed, which may be absent.\n *\n * The two above describe the two things a screen does, and neither accepts\n * `undefined` — so a hook that forwards its own optional argument had to branch,\n * and a branch around a hook call is the one thing React's lint rule refuses\n * outright. `toLankaReactVM` is such a wrapper, and so is every wrapper a\n * consumer writes over this one.\n *\n * The answer widens to `TState | TSelected` because it genuinely is not known\n * which: that is the price of not knowing at the type level whether a selector\n * arrived, and a caller who does know keeps one of the two overloads above.\n */\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: ((state: TState) => TSelected) | undefined,\n): TState | TSelected;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): TState | TSelected {\n\t/**\n\t * One tracker per mounted component, and per ViewModel it is pointed at.\n\t *\n\t * Two components over one ViewModel read different keys and must re-render for\n\t * different changes, so the recording belongs to the reader — which is why\n\t * this is built here and not shared.\n\t *\n\t * Keyed on the ViewModel, because `createLankaAccessTracker` closes over the\n\t * one it was given, permanently. This was a ref initialised once, and a\n\t * component handed a DIFFERENT ViewModel at the same mount point — an ordinary\n\t * prop change — kept a tracker reading the first one: `subscribe` WAS rebuilt\n\t * for the new ViewModel and woke the component correctly, and every render\n\t * then re-read the old one's state. A live subscription and a frozen screen,\n\t * with no error anywhere.\n\t *\n\t * `useMemo` rather than a ref written during render: the ref is the shape\n\t * `react-hooks/refs` allows only for initialise-once, and rightly — a ref\n\t * written on a condition during render is the impure render the rule exists to\n\t * catch. The factory runs when the ViewModel moves and at no other time, which\n\t * is exactly the lifetime the recording should have. If React ever discards\n\t * the cache it discards `subscribe` with it, so the two cannot disagree; a\n\t * fresh tracker has recorded nothing, and a reader that has read nothing is\n\t * notified of everything — more renders, never fewer.\n\t */\n\tconst tracker = useMemo(() => createLankaAccessTracker(viewModel), [viewModel]);\n\n\t/**\n\t * Whether a selector was passed, which is all `subscribe` needs to know.\n\t *\n\t * Not the selector itself. A selector is usually an inline arrow with a new\n\t * identity every render, so keying the subscription on it would tear the\n\t * subscription down and rebuild it on EVERY render — the failure measured at\n\t * 201 subscriptions for 200 renders. Whether there IS one is a boolean that\n\t * does not change at a given call site, so the subscription stands still.\n\t */\n\tconst hasSelector = selector !== undefined;\n\n\t/**\n\t * Stable identity, and the dependencies are the two things that genuinely\n\t * change what the subscription DOES.\n\t *\n\t * `useSyncExternalStore` keeps `subscribe` in an effect keyed on its identity.\n\t * A different ViewModel is a different subscription and must be rebuilt; so is\n\t * switching between tracked and selected reads. Neither moves in practice —\n\t * a ViewModel is a module-level object — so a mounted component subscribes\n\t * once and stays subscribed.\n\t */\n\tconst subscribe = useCallback(\n\t\t(onStoreChange: () => void) =>\n\t\t\tviewModel.subscribe((next, prev) => {\n\t\t\t\tif (hasSelector) {\n\t\t\t\t\tonStoreChange();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (tracker.shouldNotify(next, prev)) {\n\t\t\t\t\tonStoreChange();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Reaching here means NO re-render will follow. If the changed key is\n\t\t\t\t// linked to this component through a getter it read, the screen froze —\n\t\t\t\t// and in development core says so by name.\n\t\t\t\ttracker.reportSkipped(next, prev);\n\t\t\t}),\n\t\t// `tracker` moves only when `viewModel` does, so naming it costs no rebuild\n\t\t// the first dependency was not already going to cause.\n\t\t[viewModel, hasSelector, tracker],\n\t);\n\n\t/**\n\t * The selection, remembered against the STATE it was taken from.\n\t *\n\t * `useSyncExternalStore` reads the snapshot during render and AGAIN after\n\t * committing, and re-renders when the two differ by `Object.is`. A selector\n\t * that builds its answer — `(state) => ({ id: state.id })`, `(state) =>\n\t * rows.filter(…)`, the first shape a consumer reaches for — is never identical\n\t * to its own previous result, so the two reads never agreed and the component\n\t * rendered until React stopped it: \"Maximum update depth exceeded\", on the\n\t * commonest selector there is. The conformance suite's fresh-object scene is\n\t * what named it; the other four bindings compare the selection to the last one\n\t * and merely wake more often than they need to.\n\t *\n\t * So the selector runs once per STATE object and the answer is held. The two\n\t * reads of one commit then see the same reference, and the loop closes.\n\t *\n\t * Rebuilt when the selector's identity moves, which is what keeps a selector\n\t * computed from props honest: an inline arrow is a new function every render,\n\t * so the memo is fresh at the start of each render and warm by the time the\n\t * post-commit read arrives — which is the whole of what the comparison needs.\n\t * The subscription does not depend on it and stands still regardless.\n\t */\n\tconst selectFromState = useMemo(() => {\n\t\tif (!selector) return null;\n\n\t\tlet taken = false;\n\t\tlet takenFrom: TState;\n\t\tlet picked: TSelected;\n\n\t\treturn (state: TState): TSelected => {\n\t\t\tif (taken && Object.is(takenFrom, state)) return picked;\n\n\t\t\ttaken = true;\n\t\t\ttakenFrom = state;\n\t\t\tpicked = selector(state);\n\n\t\t\treturn picked;\n\t\t};\n\t}, [selector]);\n\n\t/**\n\t * Read during render, so it may close over this render's selector directly.\n\t *\n\t * `getSnapshot` is not kept in an effect and has no stability requirement —\n\t * which is what lets the selector stay a plain argument. The alternative was a\n\t * ref written during render to keep the latest one, and a ref written during\n\t * render is an impure render that React's own lint rule refuses.\n\t */\n\tconst readTracked = (): TState | TSelected =>\n\t\tselectFromState ? selectFromState(viewModel.getState()) : tracker.read();\n\n\t/**\n\t * The server snapshot: the state itself, never the Proxy.\n\t *\n\t * Tracking exists to skip renders a client would otherwise do; a server\n\t * renders once, and handing it a recording Proxy would only add work whose\n\t * result nothing reads.\n\t */\n\tconst readUntracked = (): TState | TSelected => {\n\t\tconst state = viewModel.getState();\n\n\t\treturn selector ? selector(state) : state;\n\t};\n\n\treturn useSyncExternalStore(subscribe, readTracked, readUntracked);\n}\n","import { useLankaVM } from \"../use-lanka-vm/useLankaVM\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * A ViewModel that is also a hook — what React called a ViewModel before the\n * port existed, and what it may go on calling one.\n *\n * Both call shapes, because both were there: no argument gives the tracked read,\n * a selector gives what the selector picked and bypasses tracking.\n */\nexport type TLankaReactVMHook<TState extends object> = {\n\t(): TState;\n\t<TSelected>(selector: (state: TState) => TSelected): TSelected;\n};\n\n/** The ViewModel it was given, plus the ability to be called like a hook. */\nexport type TLankaReactVM<TViewModel extends ILankaReadableVM<object>> = TViewModel &\n\tTLankaReactVMHook<ReturnType<TViewModel[\"getState\"]>>;\n\n/**\n * What must keep coming from the FUNCTION rather than from the ViewModel.\n *\n * Everything else a caller reads by name is the ViewModel's — including `name`,\n * which is the ViewModel's name and was the ViewModel's name before this\n * function existed, because `build()` defines it over the store.\n *\n * Symbols are excluded wholesale, and that is not tidiness. The ViewModel behind\n * this may be a LAZY proxy, which answers an unknown property with a wrapper\n * function; a wrapper handed back for `Symbol.iterator` makes the hook look\n * iterable, one for `Symbol.toPrimitive` breaks every string coercion of it, and\n * one for `$$typeof` makes React look at it as an element. None of those is a\n * member of any ViewModel, so none of them may be forwarded.\n */\nconst FUNCTION_MEMBERS: ReadonlySet<string> = new Set([\n\t\"prototype\",\n\t\"length\",\n\t\"arguments\",\n\t\"caller\",\n\t\"constructor\",\n\t\"call\",\n\t\"apply\",\n\t\"bind\",\n\t\"toString\",\n]);\n\n/**\n * How the callable answers for the ViewModel behind it.\n *\n * Its own function, because the two traps are the whole mechanism and the\n * factory above is then three lines — one hook, one Proxy, one cast. Read\n * together they were forty-two lines whose shape said \"a function doing two\n * things\", which is what the composition canon calls it.\n */\nconst forwardToViewModel = <TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ProxyHandler<(selector?: (state: object) => unknown) => unknown> => {\n\tconst members = viewModel as unknown as Record<string, unknown>;\n\n\treturn {\n\t\tget: (target, property, receiver): unknown =>\n\t\t\ttypeof property === \"symbol\" || FUNCTION_MEMBERS.has(property)\n\t\t\t\t? Reflect.get(target, property, receiver)\n\t\t\t\t: members[property],\n\n\t\t/**\n\t\t * `in` answers for the ViewModel too.\n\t\t *\n\t\t * Without this the hook would report that it has no `getState`, while\n\t\t * reading `getState` hands one back — and `\"getState\" in useTodoVM` is how a\n\t\t * devtool, a serialiser and a duck-typed helper ask. The ViewModel behind\n\t\t * this may be a lazy proxy with no `has` trap of its own, so the question is\n\t\t * answered by READING the property, which for a lazy ViewModel builds\n\t\t * nothing.\n\t\t */\n\t\thas: (target, property) =>\n\t\t\tReflect.has(target, property) ||\n\t\t\t(typeof property === \"string\" && members[property] !== undefined),\n\t};\n};\n\n/**\n * Gives a ViewModel React's own ergonomics back.\n *\n * ```ts\n * // the ViewModel, framework-free, exactly as Vue and Svelte receive it\n * const todoVM = createLankaVM({ … });\n *\n * // the same object, callable\n * export const useTodoVM = toLankaReactVM(todoVM);\n * ```\n *\n * ```tsx\n * const { todos, load } = useTodoVM();\n * const count = useTodoVM((state) => state.todos.length);\n * const todos = useTodoVM.getState().todos; // outside a component, as always\n * ```\n *\n * ## Why this exists\n *\n * Until 2.0 a ViewModel WAS a React hook: `createLankaVM` returned a callable,\n * and every screen in every application on lanka called it. Making the framework\n * framework-free took the call signature away — correctly, because four of the\n * five bindings have no use for one and core may not know what a hook is.\n *\n * That is a fact about CORE, and it was allowed to become a fact about React,\n * which it never had to be. A React consumer's familiar spelling costs one\n * wrapper in the one package that is allowed to know what a hook is, so here it\n * is: `useTodoVM()` reads, `useTodoVM(selector)` selects, `useTodoVM.getState()`\n * and `useTodoVM.subscribe()` do what they always did.\n *\n * ## What it does NOT do\n *\n * It does not change the ViewModel. There is exactly one store, and the call\n * forwards to `useLankaVM` — the same function the five bindings' conformance\n * suite drives. A ViewModel read through this and the same ViewModel read in Vue\n * answer identically, notify identically and skip identically, because it is the\n * same object either way and this adds no state of its own.\n *\n * It is also not required. `useLankaVM(todoVM)` is the portable spelling, it\n * stays the one the guides teach, and a codebase that has moved to it needs\n * nothing here.\n *\n * ## Laziness survives\n *\n * The forwarding is a Proxy rather than copied properties, so a ViewModel that\n * builds on first access still builds on first access: reading `useTodoVM.name`\n * answers from the config and constructs nothing.\n */\nexport const toLankaReactVM = <TViewModel extends ILankaReadableVM<object>>(\n\tviewModel: TViewModel,\n): TLankaReactVM<TViewModel> => {\n\t/**\n\t * The call signature, and the whole of it.\n\t *\n\t * Named `useViewModel` rather than `hook`: this IS a custom hook — it calls\n\t * one, it may only be called during a render, and a name not starting with\n\t * `use` hid both facts from every reader and from React's lint rule.\n\t *\n\t * ONE call, with the selector forwarded as it arrived — there is a\n\t * `useLankaVM` overload for exactly this. Written as a branch first, and the\n\t * lint rule was right to refuse it: a hook inside a ternary is a hook React\n\t * cannot promise to call in the same order, and the fact that both arms\n\t * happened to call the same one is not something a reader or a rule can see.\n\t *\n\t * Whether a selector was passed is a property of the CALL SITE and never\n\t * changes between renders, which is what `useLankaVM` relies on to keep one\n\t * subscription standing across a component's life.\n\t */\n\tconst useViewModel = (selector?: (state: object) => unknown): unknown =>\n\t\tuseLankaVM(viewModel, selector);\n\n\treturn new Proxy(useViewModel, forwardToViewModel(viewModel)) as TLankaReactVM<TViewModel>;\n};\n","import { useRef } from \"react\";\nimport { createLankaShallowHold } from \"lanka/viewmodel\";\n\n/**\n * Keeps a selector's answer stable when nothing in it changed.\n *\n * ```tsx\n * const { title, status } = useLankaVM(missionVM, useLankaShallow((s) => ({\n * \ttitle: s.title,\n * \tstatus: s.status,\n * })));\n * ```\n *\n * ## The waste this exists for\n *\n * `useLankaVM(vm, (s) => ({ a: s.a }))` is the commonest thing a React reader\n * writes, and unwrapped it wakes the component for EVERY change in the\n * ViewModel. The binding holds a selection against the state object it came\n * from, which is what a snapshot has to be; a fresh object is new whenever the\n * state is new, so a reader that took a selector to say \"only `a`\" is repainted\n * by a change to `z`. This is the comparison that makes the statement mean\n * something.\n *\n * A selector returning a primitive never had the problem, which is what makes\n * the waste quiet: the shape that is free and the shape that repaints on\n * everything look the same on the page.\n *\n * It used to be worse. Until `useLankaVM` ran its selector once per state\n * object, an unwrapped one CRASHED — `useSyncExternalStore` reads the snapshot\n * during render and again after committing, a fresh object disagreed with\n * itself, and the component rendered until React stopped it with \"Maximum update\n * depth exceeded\". That is closed in the binding, for everybody.\n *\n * ## What is React's here, and what is not\n *\n * The comparison is `createLankaShallowHold` in core, and every binding on the\n * shelf can reach it. It was this file's, and that made it a CAPABILITY React had\n * and four siblings did not — an idiom is a spelling, and this changes which\n * notifications reach a reader. `skills/parity/SKILL.md` 3c.\n *\n * What is left is the part only React needs. A component re-runs this hook on\n * every render, so the holding has to SURVIVE a render while the selector stays\n * the current one: the hold lives in a ref initialised once, and the closure\n * returned below closes over this render's `selector`. A selector computed from\n * props therefore stays honest, and the hold does not reset under it.\n *\n * ## Why a wrapper and not an equality argument\n *\n * `useLankaVM(vm, selector, isEqual)` was the other option, and it puts the\n * comparison in the binding for every caller — including the ones whose\n * selection is a string and pay for a comparison they cannot fail. This is opt\n * in at the call site, which is also where a reader can see it.\n *\n * The shape is React's own: a hook that returns a selector. A consumer arriving\n * from zustand has typed `useShallow` and needs no explanation, which is the\n * whole point of an idiom.\n */\nexport const useLankaShallow = <TState, TSelected>(\n\tselector: (state: TState) => TSelected,\n): ((state: TState) => TSelected) => {\n\tconst hold = useRef<((next: TSelected) => TSelected) | null>(null);\n\thold.current ??= createLankaShallowHold<TSelected>();\n\n\treturn (state: TState): TSelected => hold.current!(selector(state));\n};\n"],"mappings":";;;AAAA,SAAS,aAAa,SAAS,4BAA4B;AAC3D,SAAS,gCAAgC;AA8DlC,SAAS,WACf,WACA,UACqB;AAyBrB,QAAM,UAAU,QAAQ,MAAM,yBAAyB,SAAS,GAAG,CAAC,SAAS,CAAC;AAW9E,QAAM,cAAc,aAAa;AAYjC,QAAM,YAAY;AAAA,IACjB,CAAC,kBACA,UAAU,UAAU,CAAC,MAAM,SAAS;AACnC,UAAI,aAAa;AAChB,sBAAc;AACd;AAAA,MACD;AAEA,UAAI,QAAQ,aAAa,MAAM,IAAI,GAAG;AACrC,sBAAc;AACd;AAAA,MACD;AAKA,cAAQ,cAAc,MAAM,IAAI;AAAA,IACjC,CAAC;AAAA;AAAA;AAAA,IAGF,CAAC,WAAW,aAAa,OAAO;AAAA,EACjC;AAwBA,QAAM,kBAAkB,QAAQ,MAAM;AACrC,QAAI,CAAC,SAAU,QAAO;AAEtB,QAAI,QAAQ;AACZ,QAAI;AACJ,QAAI;AAEJ,WAAO,CAAC,UAA6B;AACpC,UAAI,SAAS,OAAO,GAAG,WAAW,KAAK,EAAG,QAAO;AAEjD,cAAQ;AACR,kBAAY;AACZ,eAAS,SAAS,KAAK;AAEvB,aAAO;AAAA,IACR;AAAA,EACD,GAAG,CAAC,QAAQ,CAAC;AAUb,QAAM,cAAc,MACnB,kBAAkB,gBAAgB,UAAU,SAAS,CAAC,IAAI,QAAQ,KAAK;AASxE,QAAM,gBAAgB,MAA0B;AAC/C,UAAM,QAAQ,UAAU,SAAS;AAEjC,WAAO,WAAW,SAAS,KAAK,IAAI;AAAA,EACrC;AAEA,SAAO,qBAAqB,WAAW,aAAa,aAAa;AAClE;;;ACzKA,IAAM,mBAAwC,oBAAI,IAAI;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAUD,IAAM,qBAAqB,CAC1B,cACsE;AACtE,QAAM,UAAU;AAEhB,SAAO;AAAA,IACN,KAAK,CAAC,QAAQ,UAAU,aACvB,OAAO,aAAa,YAAY,iBAAiB,IAAI,QAAQ,IAC1D,QAAQ,IAAI,QAAQ,UAAU,QAAQ,IACtC,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYpB,KAAK,CAAC,QAAQ,aACb,QAAQ,IAAI,QAAQ,QAAQ,KAC3B,OAAO,aAAa,YAAY,QAAQ,QAAQ,MAAM;AAAA,EACzD;AACD;AAkDO,IAAM,iBAAiB,CAC7B,cAC+B;AAkB/B,QAAM,eAAe,CAAC,aACrB,WAAW,WAAW,QAAQ;AAE/B,SAAO,IAAI,MAAM,cAAc,mBAAmB,SAAS,CAAC;AAC7D;;;ACxJA,SAAS,cAAc;AACvB,SAAS,8BAA8B;AAwDhC,IAAM,kBAAkB,CAC9B,aACoC;AACpC,QAAM,OAAO,OAAgD,IAAI;AACjE,OAAK,YAAY,uBAAkC;AAEnD,SAAO,CAAC,UAA6B,KAAK,QAAS,SAAS,KAAK,CAAC;AACnE;","names":[]}