@lankajs/vue 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,3 @@
1
- import "./chunk-PLDDJCW6.js";
2
-
3
1
  // src/define-lanka-composable/defineLankaComposable.ts
4
2
  import { getCurrentScope, onScopeDispose, shallowRef, triggerRef } from "vue";
5
3
  import { createLankaViewSubscription } from "lanka/extend";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/define-lanka-composable/defineLankaComposable.ts","../src/lanka-vm-to-refs/lankaVMToRefs.ts","../src/use-lanka-vm/useLankaVM.ts"],"sourcesContent":["import { getCurrentScope, onScopeDispose, shallowRef, triggerRef } from \"vue\";\nimport { createLankaViewSubscription } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * A ViewModel as Vue reads one: its own members, directly.\n *\n * `vm.rows`, `vm.load()` — no `.value`, in the script and in the template alike\n * — plus the one meta member a caller needs. `$`-prefixed, which is Pinia's\n * convention and its reason: the keys belong to the application, and a meta\n * member sharing that namespace collides the day somebody adds a `stop` of their\n * own.\n *\n * Called a ViewModel and not a store, deliberately. Pinia's word for the thing a\n * component reads is \"store\", and this reads the way one does — but what holds\n * the state, the actions and the scenario bindings is the ViewModel, and naming\n * it after the shape it wears would hide where the work lives.\n */\nexport type TLankaVueVM<TState extends object> = TState & {\n\t/** Releases the subscription. Rarely needed: a component scope does it. */\n\t$stop: () => void;\n};\n\n/**\n * How the composable answers for the ViewModel behind it.\n *\n * Its own function, because the traps are the whole mechanism and the factory\n * below is then the subscription and the Proxy.\n */\nconst readsTheViewModel = <TState extends object, TFacade extends object>(\n\tcurrent: () => TState,\n\tstop: () => void,\n): ProxyHandler<TFacade> => ({\n\tget: (_target, key) => (key === \"$stop\" ? stop : Reflect.get(current(), key)),\n\n\thas: (_target, key) => key === \"$stop\" || key in current(),\n\n\townKeys: () => Reflect.ownKeys(current()),\n\n\t/*\n\t * Reported as configurable, always.\n\t *\n\t * A Proxy must not claim a non-configurable descriptor its target lacks — the\n\t * runtime throws. The target here is a bare object while the keys live on the\n\t * state, so every descriptor this hands back is invented and must say it can\n\t * be redefined. Without it `{ ...vm }` and `Object.keys(vm)` throw rather than\n\t * read, and a Vue devtool does one of them on sight.\n\t */\n\tgetOwnPropertyDescriptor: (_target, key) =>\n\t\tkey === \"$stop\"\n\t\t\t? { value: stop, configurable: true, enumerable: false, writable: false }\n\t\t\t: { ...Reflect.getOwnPropertyDescriptor(current(), key), configurable: true },\n});\n\n/**\n * Declares the composable a Vue component reads a ViewModel through.\n *\n * ```ts\n * // todosVM.ts — at module level, the way `defineStore` is declared\n * export const useTodosVM = defineLankaComposable(todosVM);\n * ```\n *\n * ```vue\n * <script setup lang=\"ts\">\n * const todos = useTodosVM();\n * </script>\n *\n * <template><li v-for=\"row in todos.rows\" :key=\"row\">{{ row }}</li></template>\n * ```\n *\n * ## Why this exists beside `useLankaVM`\n *\n * `useLankaVM` answers a `ShallowRef`, which is the honest shape for Vue's\n * reactivity and the one every other binding on the shelf parallels. It is also\n * not what a Pinia codebase reads: there a component reads members straight off\n * what it was handed, in the template and the script alike, and `.value` appears\n * in neither. A consumer with that habit types `todos.rows`, gets `undefined`,\n * and learns that lanka is a foreign object.\n *\n * ## Why it answers a FUNCTION and not the reader itself\n *\n * Pinia's shape, and not only for the look of it. A reader built at module level\n * would open its subscription at IMPORT time, outside any component scope — so\n * nothing would ever release it, and every component would share ONE recording.\n * Two components reading different keys would then wake each other, which is the\n * whole of what access tracking exists to prevent. Measured before this shape\n * existed: the component reading only `rows` re-rendered when `unread` moved.\n *\n * So each CALL builds a reader, inside the calling component's scope, with its\n * own subscription and its own recording — and Vue releases it when that\n * component goes.\n *\n * ## Where it differs from Pinia, and why\n *\n * `useTodosVM()` in two components answers two objects, where Pinia answers one.\n * The ViewModel behind them is the same one and there is no second copy of the\n * state — what differs is the RECORDING, which belongs to whoever did the\n * reading. Sharing the object would make tracking coarse, and an idiom is not\n * allowed to change behaviour: that is the rule the parity canon sets for all of\n * them.\n *\n * Calling it outside a component is legal and gives an unscoped reader; the\n * caller then owns `$stop`.\n *\n * ## Reading is tracking\n *\n * Reads go through the access tracker, so a template reading only `rows` is not\n * woken by `isLoading`, and a ViewModel that turned tracking off is heard for\n * everything. Both are core's answers; nothing here decides either. The ref\n * holds a version counter rather than the state, because a ref holding the state\n * is a SNAPSHOT — right for a template, which re-reads when the ref changes, and\n * wrong for something also read from ordinary code at arbitrary moments.\n *\n * ## Destructuring loses reactivity, exactly as it does in Pinia\n *\n * `const { rows } = todos` copies a value out and stops tracking, which is the\n * single most common mistake in a Pinia codebase. `lankaVMToRefs(todos)` is the\n * same answer Pinia gives, under a name that says what it is reading.\n */\nexport const defineLankaComposable =\n\t<TState extends object>(viewModel: ILankaReadableVM<TState>) =>\n\t(): TLankaVueVM<TState> => {\n\t\t/*\n\t\t * `triggerRef` as well as the increment for the reason every binding on this\n\t\t * shelf carries: a tracked read hands back the SAME proxy while the state\n\t\t * object is unchanged, and a shallow ref compares by identity.\n\t\t */\n\t\tconst version = shallowRef(0);\n\n\t\tconst view = createLankaViewSubscription(viewModel, () => {\n\t\t\tversion.value += 1;\n\t\t\ttriggerRef(version);\n\t\t});\n\n\t\t// Inside a component or an `effectScope`, Vue owns the lifetime and the\n\t\t// subscription goes with it. Outside one there is nothing to attach to, and\n\t\t// `onScopeDispose` would warn — so the caller keeps `$stop`.\n\t\tif (getCurrentScope()) onScopeDispose(view.stop);\n\n\t\tconst current = (): TState => {\n\t\t\t// Read for the DEPENDENCY, discard the number. A template reading\n\t\t\t// `todos.rows` must re-render when the counter moves, and the counter is\n\t\t\t// the only reactive thing in here.\n\t\t\tvoid version.value;\n\n\t\t\treturn view.read();\n\t\t};\n\n\t\treturn new Proxy({} as TLankaVueVM<TState>, readsTheViewModel(current, view.stop));\n\t};\n","import { computed } from \"vue\";\nimport type { ComputedRef } from \"vue\";\nimport type { TLankaVueVM } from \"../define-lanka-composable/defineLankaComposable\";\n\n/** Every state member of the ViewModel, as a ref that keeps tracking. */\nexport type TLankaVMRefs<TState extends object> = {\n\t[TKey in keyof TState]: ComputedRef<TState[TKey]>;\n};\n\n/**\n * Names you can destructure, without losing the reactivity.\n *\n * ```ts\n * const todos = useTodosVM();\n * const { rows, isLoading } = lankaVMToRefs(todos);\n *\n * // in a template: {{ rows }} — in script: rows.value\n * ```\n *\n * `const { rows } = todos` reads the value ONCE and stops tracking, and it is\n * the commonest mistake in a Pinia codebase for the good reason that it looks\n * exactly like code that works: the first paint is right and nothing updates\n * after it. Pinia's answer is `storeToRefs`, so this is that answer under a name\n * a reader recognises.\n *\n * Each ref is a `computed` over the same ViewModel, so nothing is copied and nothing\n * is subscribed a second time — the composable's own subscription is still the only\n * one.\n *\n * Actions are left OUT, and that is not an oversight: an action is a stable\n * function for the life of the store, so `const { load } = todos` is correct and\n * wrapping it in a ref would make every call site write `load.value()`.\n */\nexport const lankaVMToRefs = <TState extends object>(\n\tviewModel: TLankaVueVM<TState>,\n): TLankaVMRefs<TState> => {\n\tconst refs = {} as TLankaVMRefs<TState>;\n\n\tfor (const key of Object.keys(viewModel) as (keyof TState)[]) {\n\t\tif (typeof viewModel[key] === \"function\") continue;\n\n\t\trefs[key] = computed(() => viewModel[key]);\n\t}\n\n\treturn refs;\n};\n","import {\n\tgetCurrentInstance,\n\tgetCurrentScope,\n\tonMounted,\n\tonScopeDispose,\n\tshallowRef,\n\ttriggerRef,\n} from \"vue\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { ShallowRef } from \"vue\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/** A ViewModel read from Vue: a ref, and a way to stop reading it. */\nexport interface ILankaVMRef<TValue> extends ShallowRef<TValue> {\n\t/**\n\t * Releases the subscription.\n\t *\n\t * Called for you by `onScopeDispose` inside a component or an `effectScope`.\n\t * It is published because a read made OUTSIDE a scope — at module level, in a\n\t * test — has nobody to call it, and Vue says nothing about that case.\n\t */\n\tstop: () => void;\n}\n\n/**\n * Reads a ViewModel from a Vue component.\n *\n * ```vue\n * <script setup lang=\"ts\">\n * const state = useLankaVM(todoVM);\n * </script>\n *\n * <template>\n * <li v-for=\"todo in state.todos\" :key=\"todo.id\">{{ todo.title }}</li>\n * </template>\n * ```\n *\n * Without a selector the component receives a Proxy that records which keys it\n * read, and the next change updates the ref only if one of THOSE keys moved.\n * With a selector the selector decides and tracking is bypassed.\n *\n * ## What a Vue call answers, and why React's answers differently\n *\n * A `ShallowRef`. A template unwraps it (`state.todos`) and a script does not\n * (`state.value.todos`), which is Vue's own idea of reactivity — and the one\n * thing this shelf deliberately does NOT hide. Flattening it would mean a second\n * reactivity system fighting the first, and every `watch` a consumer writes\n * would stop seeing changes.\n *\n * Everything else is the same as every other binding, and\n * `lankaViewBindingConformance` is what says so rather than this paragraph.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are\n * `createLankaAccessTracker` in core. If this file ever needs more than the port\n * gives it, the port has the defect and the fix belongs in core, for everybody.\n */\nexport function useLankaVM<TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ILankaVMRef<TState>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): ILankaVMRef<TSelected>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): ILankaVMRef<TState | TSelected> {\n\tconst tracker = createLankaAccessTracker(viewModel);\n\tconst read = (): TState | TSelected =>\n\t\tselector ? selector(viewModel.getState()) : tracker.read();\n\n\t// `as unknown` first: Vue's `shallowRef` return type is a conditional over the\n\t// value, and TypeScript cannot see that adding `stop` to it lands on this\n\t// interface. The object IS the ref — `stop` is assigned two lines below.\n\tconst state = shallowRef(read()) as unknown as ILankaVMRef<TState | TSelected>;\n\n\tconst hear = (next: TState, prev: TState): void => {\n\t\tif (selector) {\n\t\t\tconst picked = read();\n\n\t\t\t// Only when the SELECTION moved. Without this the ref is set on every\n\t\t\t// notification and the reader wakes for everything, so the same call\n\t\t\t// means one thing here and another in React — which is what the\n\t\t\t// conformance suite's selector scenes now refuse.\n\t\t\tif (Object.is(picked, state.value)) return;\n\n\t\t\tstate.value = picked;\n\t\t\ttriggerRef(state);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!tracker.shouldNotify(next, prev)) {\n\t\t\t// No update will follow. If the changed key is linked to this component\n\t\t\t// through a getter it read, the screen froze — and in development core\n\t\t\t// says so by name.\n\t\t\ttracker.reportSkipped(next, prev);\n\t\t\treturn;\n\t\t}\n\n\t\t// `triggerRef` as well as the assignment: a tracked read hands back the SAME\n\t\t// proxy while the state object is unchanged, and a shallow ref compares by\n\t\t// identity — so an assignment alone would be a no-op exactly when the\n\t\t// tracker did its job. Vue re-renders, the proxy records afresh.\n\t\tstate.value = read();\n\t\ttriggerRef(state);\n\t};\n\n\tlet stop = (): void => undefined;\n\tconst start = (): void => {\n\t\tstop = viewModel.subscribe(hear);\n\t};\n\tconst release = (): void => {\n\t\tstop();\n\t};\n\n\t/**\n\t * Inside a component the subscription starts at MOUNT; everywhere else, now.\n\t *\n\t * A server renders once and throws the tree away. Nothing is mounted and\n\t * nothing is unmounted, so the instance's scope is never stopped and\n\t * `onScopeDispose` never runs — a subscription opened in `setup` there is a\n\t * listener on a module-level ViewModel that outlives the request, and the\n\t * process collects one per request until it dies. The conformance suite's\n\t * server scene is what found it, on the day this package started answering\n\t * that scene instead of skipping it.\n\t *\n\t * `onMounted` is the seam because it is the one lifecycle a server never\n\t * reaches. Outside a component there is no mount to wait for — a module-level\n\t * read, a test, an `effectScope` — and the subscription opens immediately, as\n\t * it always did.\n\t *\n\t * The catch-up is not optional. Between `setup` and the mount the ViewModel may\n\t * have moved, and the ref still holds what `setup` saw.\n\t *\n\t * What it compares is the STATE OBJECT, not the value the reader sees. The\n\t * value was the obvious thing to compare and it is wrong on the selector arm:\n\t * a selector building a fresh object — `(s) => ({ … })`, the shape a consumer\n\t * reaches for first — is never `Object.is`-equal to anything, so every such\n\t * component rendered a second time at mount whether or not a thing had moved.\n\t * The state object is the question both arms actually mean: core answers the\n\t * same one while nothing has changed.\n\t *\n\t * ## The window this leaves, and the trade in it\n\t *\n\t * A change made synchronously in `setup` AFTER this call — a bootstrap line, a\n\t * hydration — is no longer in the first render; it lands on the next tick.\n\t * `onBeforeMount` would close that window and open a worse one: it runs inside\n\t * the hydration render, so correcting the value there makes the client paint\n\t * something the server did not send. That is a markup mismatch in a process\n\t * this framework owns no part of, and `skills/hosts/SKILL.md` §5 is the rule\n\t * it breaks — take the frame, which is a cost inside our own layer, over a\n\t * mismatch the host reports and the application cannot act on.\n\t *\n\t * It is a cost the other four bindings do not pay, which is the part worth\n\t * knowing before anyone calls it a Vue bug.\n\t */\n\tif (getCurrentInstance()) {\n\t\tconst stateAtSetup = viewModel.getState();\n\n\t\tonMounted(() => {\n\t\t\tstart();\n\n\t\t\tif (Object.is(viewModel.getState(), stateAtSetup)) return;\n\n\t\t\tstate.value = read();\n\t\t\ttriggerRef(state);\n\t\t});\n\t} else {\n\t\tstart();\n\t}\n\n\tstate.stop = release;\n\n\t// Inside a component or an `effectScope`, Vue owns the lifetime and the\n\t// subscription goes with it. Outside one there is nothing to attach to, and\n\t// `onScopeDispose` would warn — so the caller keeps `stop`.\n\tif (getCurrentScope()) onScopeDispose(release);\n\n\treturn state;\n}\n"],"mappings":";;;AAAA,SAAS,iBAAiB,gBAAgB,YAAY,kBAAkB;AACxE,SAAS,mCAAmC;AA4B5C,IAAM,oBAAoB,CACzB,SACA,UAC4B;AAAA,EAC5B,KAAK,CAAC,SAAS,QAAS,QAAQ,UAAU,OAAO,QAAQ,IAAI,QAAQ,GAAG,GAAG;AAAA,EAE3E,KAAK,CAAC,SAAS,QAAQ,QAAQ,WAAW,OAAO,QAAQ;AAAA,EAEzD,SAAS,MAAM,QAAQ,QAAQ,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWxC,0BAA0B,CAAC,SAAS,QACnC,QAAQ,UACL,EAAE,OAAO,MAAM,cAAc,MAAM,YAAY,OAAO,UAAU,MAAM,IACtE,EAAE,GAAG,QAAQ,yBAAyB,QAAQ,GAAG,GAAG,GAAG,cAAc,KAAK;AAC/E;AAmEO,IAAM,wBACZ,CAAwB,cACxB,MAA2B;AAM1B,QAAM,UAAU,WAAW,CAAC;AAE5B,QAAM,OAAO,4BAA4B,WAAW,MAAM;AACzD,YAAQ,SAAS;AACjB,eAAW,OAAO;AAAA,EACnB,CAAC;AAKD,MAAI,gBAAgB,EAAG,gBAAe,KAAK,IAAI;AAE/C,QAAM,UAAU,MAAc;AAI7B,SAAK,QAAQ;AAEb,WAAO,KAAK,KAAK;AAAA,EAClB;AAEA,SAAO,IAAI,MAAM,CAAC,GAA0B,kBAAkB,SAAS,KAAK,IAAI,CAAC;AAClF;;;ACrJD,SAAS,gBAAgB;AAiClB,IAAM,gBAAgB,CAC5B,cAC0B;AAC1B,QAAM,OAAO,CAAC;AAEd,aAAW,OAAO,OAAO,KAAK,SAAS,GAAuB;AAC7D,QAAI,OAAO,UAAU,GAAG,MAAM,WAAY;AAE1C,SAAK,GAAG,IAAI,SAAS,MAAM,UAAU,GAAG,CAAC;AAAA,EAC1C;AAEA,SAAO;AACR;;;AC7CA;AAAA,EACC;AAAA,EACA,mBAAAA;AAAA,EACA;AAAA,EACA,kBAAAC;AAAA,EACA,cAAAC;AAAA,EACA,cAAAC;AAAA,OACM;AACP,SAAS,gCAAgC;AA2DlC,SAAS,WACf,WACA,UACkC;AAClC,QAAM,UAAU,yBAAyB,SAAS;AAClD,QAAM,OAAO,MACZ,WAAW,SAAS,UAAU,SAAS,CAAC,IAAI,QAAQ,KAAK;AAK1D,QAAM,QAAQD,YAAW,KAAK,CAAC;AAE/B,QAAM,OAAO,CAAC,MAAc,SAAuB;AAClD,QAAI,UAAU;AACb,YAAM,SAAS,KAAK;AAMpB,UAAI,OAAO,GAAG,QAAQ,MAAM,KAAK,EAAG;AAEpC,YAAM,QAAQ;AACd,MAAAC,YAAW,KAAK;AAEhB;AAAA,IACD;AAEA,QAAI,CAAC,QAAQ,aAAa,MAAM,IAAI,GAAG;AAItC,cAAQ,cAAc,MAAM,IAAI;AAChC;AAAA,IACD;AAMA,UAAM,QAAQ,KAAK;AACnB,IAAAA,YAAW,KAAK;AAAA,EACjB;AAEA,MAAI,OAAO,MAAY;AACvB,QAAM,QAAQ,MAAY;AACzB,WAAO,UAAU,UAAU,IAAI;AAAA,EAChC;AACA,QAAM,UAAU,MAAY;AAC3B,SAAK;AAAA,EACN;AA2CA,MAAI,mBAAmB,GAAG;AACzB,UAAM,eAAe,UAAU,SAAS;AAExC,cAAU,MAAM;AACf,YAAM;AAEN,UAAI,OAAO,GAAG,UAAU,SAAS,GAAG,YAAY,EAAG;AAEnD,YAAM,QAAQ,KAAK;AACnB,MAAAA,YAAW,KAAK;AAAA,IACjB,CAAC;AAAA,EACF,OAAO;AACN,UAAM;AAAA,EACP;AAEA,QAAM,OAAO;AAKb,MAAIH,iBAAgB,EAAG,CAAAC,gBAAe,OAAO;AAE7C,SAAO;AACR;","names":["getCurrentScope","onScopeDispose","shallowRef","triggerRef"]}
1
+ {"version":3,"sources":["../src/define-lanka-composable/defineLankaComposable.ts","../src/lanka-vm-to-refs/lankaVMToRefs.ts","../src/use-lanka-vm/useLankaVM.ts"],"sourcesContent":["import { getCurrentScope, onScopeDispose, shallowRef, triggerRef } from \"vue\";\nimport { createLankaViewSubscription } from \"lanka/extend\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/**\n * A ViewModel as Vue reads one: its own members, directly.\n *\n * `vm.rows`, `vm.load()` — no `.value`, in the script and in the template alike\n * — plus the one meta member a caller needs. `$`-prefixed, which is Pinia's\n * convention and its reason: the keys belong to the application, and a meta\n * member sharing that namespace collides the day somebody adds a `stop` of their\n * own.\n *\n * Called a ViewModel and not a store, deliberately. Pinia's word for the thing a\n * component reads is \"store\", and this reads the way one does — but what holds\n * the state, the actions and the scenario bindings is the ViewModel, and naming\n * it after the shape it wears would hide where the work lives.\n */\nexport type TLankaVueVM<TState extends object> = TState & {\n\t/** Releases the subscription. Rarely needed: a component scope does it. */\n\t$stop: () => void;\n};\n\n/**\n * How the composable answers for the ViewModel behind it.\n *\n * Its own function, because the traps are the whole mechanism and the factory\n * below is then the subscription and the Proxy.\n */\nconst readsTheViewModel = <TState extends object, TFacade extends object>(\n\tcurrent: () => TState,\n\tstop: () => void,\n): ProxyHandler<TFacade> => ({\n\tget: (_target, key) => (key === \"$stop\" ? stop : Reflect.get(current(), key)),\n\n\thas: (_target, key) => key === \"$stop\" || key in current(),\n\n\townKeys: () => Reflect.ownKeys(current()),\n\n\t/*\n\t * Reported as configurable, always.\n\t *\n\t * A Proxy must not claim a non-configurable descriptor its target lacks — the\n\t * runtime throws. The target here is a bare object while the keys live on the\n\t * state, so every descriptor this hands back is invented and must say it can\n\t * be redefined. Without it `{ ...vm }` and `Object.keys(vm)` throw rather than\n\t * read, and a Vue devtool does one of them on sight.\n\t */\n\tgetOwnPropertyDescriptor: (_target, key) =>\n\t\tkey === \"$stop\"\n\t\t\t? { value: stop, configurable: true, enumerable: false, writable: false }\n\t\t\t: { ...Reflect.getOwnPropertyDescriptor(current(), key), configurable: true },\n});\n\n/**\n * Declares the composable a Vue component reads a ViewModel through.\n *\n * ```ts\n * // todosVM.ts — at module level, the way `defineStore` is declared\n * export const useTodosVM = defineLankaComposable(todosVM);\n * ```\n *\n * ```vue\n * <script setup lang=\"ts\">\n * const todos = useTodosVM();\n * </script>\n *\n * <template><li v-for=\"row in todos.rows\" :key=\"row\">{{ row }}</li></template>\n * ```\n *\n * ## Why this exists beside `useLankaVM`\n *\n * `useLankaVM` answers a `ShallowRef`, which is the honest shape for Vue's\n * reactivity and the one every other binding on the shelf parallels. It is also\n * not what a Pinia codebase reads: there a component reads members straight off\n * what it was handed, in the template and the script alike, and `.value` appears\n * in neither. A consumer with that habit types `todos.rows`, gets `undefined`,\n * and learns that lanka is a foreign object.\n *\n * ## Why it answers a FUNCTION and not the reader itself\n *\n * Pinia's shape, and not only for the look of it. A reader built at module level\n * would open its subscription at IMPORT time, outside any component scope — so\n * nothing would ever release it, and every component would share ONE recording.\n * Two components reading different keys would then wake each other, which is the\n * whole of what access tracking exists to prevent. Measured before this shape\n * existed: the component reading only `rows` re-rendered when `unread` moved.\n *\n * So each CALL builds a reader, inside the calling component's scope, with its\n * own subscription and its own recording — and Vue releases it when that\n * component goes.\n *\n * ## Where it differs from Pinia, and why\n *\n * `useTodosVM()` in two components answers two objects, where Pinia answers one.\n * The ViewModel behind them is the same one and there is no second copy of the\n * state — what differs is the RECORDING, which belongs to whoever did the\n * reading. Sharing the object would make tracking coarse, and an idiom is not\n * allowed to change behaviour: that is the rule the parity canon sets for all of\n * them.\n *\n * Calling it outside a component is legal and gives an unscoped reader; the\n * caller then owns `$stop`.\n *\n * ## Reading is tracking\n *\n * Reads go through the access tracker, so a template reading only `rows` is not\n * woken by `isLoading`, and a ViewModel that turned tracking off is heard for\n * everything. Both are core's answers; nothing here decides either. The ref\n * holds a version counter rather than the state, because a ref holding the state\n * is a SNAPSHOT — right for a template, which re-reads when the ref changes, and\n * wrong for something also read from ordinary code at arbitrary moments.\n *\n * ## Destructuring loses reactivity, exactly as it does in Pinia\n *\n * `const { rows } = todos` copies a value out and stops tracking, which is the\n * single most common mistake in a Pinia codebase. `lankaVMToRefs(todos)` is the\n * same answer Pinia gives, under a name that says what it is reading.\n */\nexport const defineLankaComposable =\n\t<TState extends object>(viewModel: ILankaReadableVM<TState>) =>\n\t(): TLankaVueVM<TState> => {\n\t\t/*\n\t\t * `triggerRef` as well as the increment for the reason every binding on this\n\t\t * shelf carries: a tracked read hands back the SAME proxy while the state\n\t\t * object is unchanged, and a shallow ref compares by identity.\n\t\t */\n\t\tconst version = shallowRef(0);\n\n\t\tconst view = createLankaViewSubscription(viewModel, () => {\n\t\t\tversion.value += 1;\n\t\t\ttriggerRef(version);\n\t\t});\n\n\t\t// Inside a component or an `effectScope`, Vue owns the lifetime and the\n\t\t// subscription goes with it. Outside one there is nothing to attach to, and\n\t\t// `onScopeDispose` would warn — so the caller keeps `$stop`.\n\t\tif (getCurrentScope()) onScopeDispose(view.stop);\n\n\t\tconst current = (): TState => {\n\t\t\t// Read for the DEPENDENCY, discard the number. A template reading\n\t\t\t// `todos.rows` must re-render when the counter moves, and the counter is\n\t\t\t// the only reactive thing in here.\n\t\t\tvoid version.value;\n\n\t\t\treturn view.read();\n\t\t};\n\n\t\treturn new Proxy({} as TLankaVueVM<TState>, readsTheViewModel(current, view.stop));\n\t};\n","import { computed } from \"vue\";\nimport type { ComputedRef } from \"vue\";\nimport type { TLankaVueVM } from \"../define-lanka-composable/defineLankaComposable\";\n\n/** Every state member of the ViewModel, as a ref that keeps tracking. */\nexport type TLankaVMRefs<TState extends object> = {\n\t[TKey in keyof TState]: ComputedRef<TState[TKey]>;\n};\n\n/**\n * Names you can destructure, without losing the reactivity.\n *\n * ```ts\n * const todos = useTodosVM();\n * const { rows, isLoading } = lankaVMToRefs(todos);\n *\n * // in a template: {{ rows }} — in script: rows.value\n * ```\n *\n * `const { rows } = todos` reads the value ONCE and stops tracking, and it is\n * the commonest mistake in a Pinia codebase for the good reason that it looks\n * exactly like code that works: the first paint is right and nothing updates\n * after it. Pinia's answer is `storeToRefs`, so this is that answer under a name\n * a reader recognises.\n *\n * Each ref is a `computed` over the same ViewModel, so nothing is copied and nothing\n * is subscribed a second time — the composable's own subscription is still the only\n * one.\n *\n * Actions are left OUT, and that is not an oversight: an action is a stable\n * function for the life of the store, so `const { load } = todos` is correct and\n * wrapping it in a ref would make every call site write `load.value()`.\n */\nexport const lankaVMToRefs = <TState extends object>(\n\tviewModel: TLankaVueVM<TState>,\n): TLankaVMRefs<TState> => {\n\tconst refs = {} as TLankaVMRefs<TState>;\n\n\tfor (const key of Object.keys(viewModel) as (keyof TState)[]) {\n\t\tif (typeof viewModel[key] === \"function\") continue;\n\n\t\trefs[key] = computed(() => viewModel[key]);\n\t}\n\n\treturn refs;\n};\n","import {\n\tgetCurrentInstance,\n\tgetCurrentScope,\n\tonMounted,\n\tonScopeDispose,\n\tshallowRef,\n\ttriggerRef,\n} from \"vue\";\nimport { createLankaAccessTracker } from \"lanka/extend\";\nimport type { ShallowRef } from \"vue\";\nimport type { ILankaReadableVM } from \"lanka/viewmodel\";\n\n/** A ViewModel read from Vue: a ref, and a way to stop reading it. */\nexport interface ILankaVMRef<TValue> extends ShallowRef<TValue> {\n\t/**\n\t * Releases the subscription.\n\t *\n\t * Called for you by `onScopeDispose` inside a component or an `effectScope`.\n\t * It is published because a read made OUTSIDE a scope — at module level, in a\n\t * test — has nobody to call it, and Vue says nothing about that case.\n\t */\n\tstop: () => void;\n}\n\n/**\n * Reads a ViewModel from a Vue component.\n *\n * ```vue\n * <script setup lang=\"ts\">\n * const state = useLankaVM(todoVM);\n * </script>\n *\n * <template>\n * <li v-for=\"todo in state.todos\" :key=\"todo.id\">{{ todo.title }}</li>\n * </template>\n * ```\n *\n * Without a selector the component receives a Proxy that records which keys it\n * read, and the next change updates the ref only if one of THOSE keys moved.\n * With a selector the selector decides and tracking is bypassed.\n *\n * ## What a Vue call answers, and why React's answers differently\n *\n * A `ShallowRef`. A template unwraps it (`state.todos`) and a script does not\n * (`state.value.todos`), which is Vue's own idea of reactivity — and the one\n * thing this shelf deliberately does NOT hide. Flattening it would mean a second\n * reactivity system fighting the first, and every `watch` a consumer writes\n * would stop seeing changes.\n *\n * Everything else is the same as every other binding, and\n * `lankaViewBindingConformance` is what says so rather than this paragraph.\n *\n * ## What this function does NOT contain\n *\n * The recording, the comparison and the blind-spot warning are\n * `createLankaAccessTracker` in core. If this file ever needs more than the port\n * gives it, the port has the defect and the fix belongs in core, for everybody.\n */\nexport function useLankaVM<TState extends object>(\n\tviewModel: ILankaReadableVM<TState>,\n): ILankaVMRef<TState>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector: (state: TState) => TSelected,\n): ILankaVMRef<TSelected>;\n\nexport function useLankaVM<TState extends object, TSelected>(\n\tviewModel: ILankaReadableVM<TState>,\n\tselector?: (state: TState) => TSelected,\n): ILankaVMRef<TState | TSelected> {\n\tconst tracker = createLankaAccessTracker(viewModel);\n\tconst read = (): TState | TSelected =>\n\t\tselector ? selector(viewModel.getState()) : tracker.read();\n\n\t// `as unknown` first: Vue's `shallowRef` return type is a conditional over the\n\t// value, and TypeScript cannot see that adding `stop` to it lands on this\n\t// interface. The object IS the ref — `stop` is assigned two lines below.\n\tconst state = shallowRef(read()) as unknown as ILankaVMRef<TState | TSelected>;\n\n\tconst hear = (next: TState, prev: TState): void => {\n\t\tif (selector) {\n\t\t\tconst picked = read();\n\n\t\t\t// Only when the SELECTION moved. Without this the ref is set on every\n\t\t\t// notification and the reader wakes for everything, so the same call\n\t\t\t// means one thing here and another in React — which is what the\n\t\t\t// conformance suite's selector scenes now refuse.\n\t\t\tif (Object.is(picked, state.value)) return;\n\n\t\t\tstate.value = picked;\n\t\t\ttriggerRef(state);\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (!tracker.shouldNotify(next, prev)) {\n\t\t\t// No update will follow. If the changed key is linked to this component\n\t\t\t// through a getter it read, the screen froze — and in development core\n\t\t\t// says so by name.\n\t\t\ttracker.reportSkipped(next, prev);\n\t\t\treturn;\n\t\t}\n\n\t\t// `triggerRef` as well as the assignment: a tracked read hands back the SAME\n\t\t// proxy while the state object is unchanged, and a shallow ref compares by\n\t\t// identity — so an assignment alone would be a no-op exactly when the\n\t\t// tracker did its job. Vue re-renders, the proxy records afresh.\n\t\tstate.value = read();\n\t\ttriggerRef(state);\n\t};\n\n\tlet stop = (): void => undefined;\n\tconst start = (): void => {\n\t\tstop = viewModel.subscribe(hear);\n\t};\n\tconst release = (): void => {\n\t\tstop();\n\t};\n\n\t/**\n\t * Inside a component the subscription starts at MOUNT; everywhere else, now.\n\t *\n\t * A server renders once and throws the tree away. Nothing is mounted and\n\t * nothing is unmounted, so the instance's scope is never stopped and\n\t * `onScopeDispose` never runs — a subscription opened in `setup` there is a\n\t * listener on a module-level ViewModel that outlives the request, and the\n\t * process collects one per request until it dies. The conformance suite's\n\t * server scene is what found it, on the day this package started answering\n\t * that scene instead of skipping it.\n\t *\n\t * `onMounted` is the seam because it is the one lifecycle a server never\n\t * reaches. Outside a component there is no mount to wait for — a module-level\n\t * read, a test, an `effectScope` — and the subscription opens immediately, as\n\t * it always did.\n\t *\n\t * The catch-up is not optional. Between `setup` and the mount the ViewModel may\n\t * have moved, and the ref still holds what `setup` saw.\n\t *\n\t * What it compares is the STATE OBJECT, not the value the reader sees. The\n\t * value was the obvious thing to compare and it is wrong on the selector arm:\n\t * a selector building a fresh object — `(s) => ({ … })`, the shape a consumer\n\t * reaches for first — is never `Object.is`-equal to anything, so every such\n\t * component rendered a second time at mount whether or not a thing had moved.\n\t * The state object is the question both arms actually mean: core answers the\n\t * same one while nothing has changed.\n\t *\n\t * ## The window this leaves, and the trade in it\n\t *\n\t * A change made synchronously in `setup` AFTER this call — a bootstrap line, a\n\t * hydration — is no longer in the first render; it lands on the next tick.\n\t * `onBeforeMount` would close that window and open a worse one: it runs inside\n\t * the hydration render, so correcting the value there makes the client paint\n\t * something the server did not send. That is a markup mismatch in a process\n\t * this framework owns no part of, and `skills/hosts/SKILL.md` §5 is the rule\n\t * it breaks — take the frame, which is a cost inside our own layer, over a\n\t * mismatch the host reports and the application cannot act on.\n\t *\n\t * It is a cost the other four bindings do not pay, which is the part worth\n\t * knowing before anyone calls it a Vue bug.\n\t */\n\tif (getCurrentInstance()) {\n\t\tconst stateAtSetup = viewModel.getState();\n\n\t\tonMounted(() => {\n\t\t\tstart();\n\n\t\t\tif (Object.is(viewModel.getState(), stateAtSetup)) return;\n\n\t\t\tstate.value = read();\n\t\t\ttriggerRef(state);\n\t\t});\n\t} else {\n\t\tstart();\n\t}\n\n\tstate.stop = release;\n\n\t// Inside a component or an `effectScope`, Vue owns the lifetime and the\n\t// subscription goes with it. Outside one there is nothing to attach to, and\n\t// `onScopeDispose` would warn — so the caller keeps `stop`.\n\tif (getCurrentScope()) onScopeDispose(release);\n\n\treturn state;\n}\n"],"mappings":";AAAA,SAAS,iBAAiB,gBAAgB,YAAY,kBAAkB;AACxE,SAAS,mCAAmC;AA4B5C,IAAM,oBAAoB,CACzB,SACA,UAC4B;AAAA,EAC5B,KAAK,CAAC,SAAS,QAAS,QAAQ,UAAU,OAAO,QAAQ,IAAI,QAAQ,GAAG,GAAG;AAAA,EAE3E,KAAK,CAAC,SAAS,QAAQ,QAAQ,WAAW,OAAO,QAAQ;AAAA,EAEzD,SAAS,MAAM,QAAQ,QAAQ,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWxC,0BAA0B,CAAC,SAAS,QACnC,QAAQ,UACL,EAAE,OAAO,MAAM,cAAc,MAAM,YAAY,OAAO,UAAU,MAAM,IACtE,EAAE,GAAG,QAAQ,yBAAyB,QAAQ,GAAG,GAAG,GAAG,cAAc,KAAK;AAC/E;AAmEO,IAAM,wBACZ,CAAwB,cACxB,MAA2B;AAM1B,QAAM,UAAU,WAAW,CAAC;AAE5B,QAAM,OAAO,4BAA4B,WAAW,MAAM;AACzD,YAAQ,SAAS;AACjB,eAAW,OAAO;AAAA,EACnB,CAAC;AAKD,MAAI,gBAAgB,EAAG,gBAAe,KAAK,IAAI;AAE/C,QAAM,UAAU,MAAc;AAI7B,SAAK,QAAQ;AAEb,WAAO,KAAK,KAAK;AAAA,EAClB;AAEA,SAAO,IAAI,MAAM,CAAC,GAA0B,kBAAkB,SAAS,KAAK,IAAI,CAAC;AAClF;;;ACrJD,SAAS,gBAAgB;AAiClB,IAAM,gBAAgB,CAC5B,cAC0B;AAC1B,QAAM,OAAO,CAAC;AAEd,aAAW,OAAO,OAAO,KAAK,SAAS,GAAuB;AAC7D,QAAI,OAAO,UAAU,GAAG,MAAM,WAAY;AAE1C,SAAK,GAAG,IAAI,SAAS,MAAM,UAAU,GAAG,CAAC;AAAA,EAC1C;AAEA,SAAO;AACR;;;AC7CA;AAAA,EACC;AAAA,EACA,mBAAAA;AAAA,EACA;AAAA,EACA,kBAAAC;AAAA,EACA,cAAAC;AAAA,EACA,cAAAC;AAAA,OACM;AACP,SAAS,gCAAgC;AA2DlC,SAAS,WACf,WACA,UACkC;AAClC,QAAM,UAAU,yBAAyB,SAAS;AAClD,QAAM,OAAO,MACZ,WAAW,SAAS,UAAU,SAAS,CAAC,IAAI,QAAQ,KAAK;AAK1D,QAAM,QAAQD,YAAW,KAAK,CAAC;AAE/B,QAAM,OAAO,CAAC,MAAc,SAAuB;AAClD,QAAI,UAAU;AACb,YAAM,SAAS,KAAK;AAMpB,UAAI,OAAO,GAAG,QAAQ,MAAM,KAAK,EAAG;AAEpC,YAAM,QAAQ;AACd,MAAAC,YAAW,KAAK;AAEhB;AAAA,IACD;AAEA,QAAI,CAAC,QAAQ,aAAa,MAAM,IAAI,GAAG;AAItC,cAAQ,cAAc,MAAM,IAAI;AAChC;AAAA,IACD;AAMA,UAAM,QAAQ,KAAK;AACnB,IAAAA,YAAW,KAAK;AAAA,EACjB;AAEA,MAAI,OAAO,MAAY;AACvB,QAAM,QAAQ,MAAY;AACzB,WAAO,UAAU,UAAU,IAAI;AAAA,EAChC;AACA,QAAM,UAAU,MAAY;AAC3B,SAAK;AAAA,EACN;AA2CA,MAAI,mBAAmB,GAAG;AACzB,UAAM,eAAe,UAAU,SAAS;AAExC,cAAU,MAAM;AACf,YAAM;AAEN,UAAI,OAAO,GAAG,UAAU,SAAS,GAAG,YAAY,EAAG;AAEnD,YAAM,QAAQ,KAAK;AACnB,MAAAA,YAAW,KAAK;AAAA,IACjB,CAAC;AAAA,EACF,OAAO;AACN,UAAM;AAAA,EACP;AAEA,QAAM,OAAO;AAKb,MAAIH,iBAAgB,EAAG,CAAAC,gBAAe,OAAO;AAE7C,SAAO;AACR;","names":["getCurrentScope","onScopeDispose","shallowRef","triggerRef"]}