mvc-kit 2.13.2 → 2.14.0

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.
Files changed (46) hide show
  1. package/BEST_PRACTICES.md +53 -4
  2. package/agent-config/claude-code/skills/mvc-kit/SKILL.md +1 -1
  3. package/agent-config/claude-code/skills/mvc-kit/anti-patterns.md +47 -0
  4. package/agent-config/claude-code/skills/mvc-kit/patterns.md +11 -0
  5. package/agent-config/claude-code/skills/mvc-kit/recipes.md +103 -22
  6. package/agent-config/claude-code/skills/mvc-kit/testing.md +3 -2
  7. package/agent-config/claude-code/skills/mvc-kit-review/checklist.md +7 -6
  8. package/agent-config/copilot/copilot-instructions.md +34 -0
  9. package/agent-config/cursor/cursorrules +34 -0
  10. package/dist/react/components/DataTable.cjs +5 -9
  11. package/dist/react/components/DataTable.cjs.map +1 -1
  12. package/dist/react/components/DataTable.d.ts.map +1 -1
  13. package/dist/react/components/DataTable.js +5 -9
  14. package/dist/react/components/DataTable.js.map +1 -1
  15. package/dist/react/use-instance.cjs +6 -3
  16. package/dist/react/use-instance.cjs.map +1 -1
  17. package/dist/react/use-instance.d.ts.map +1 -1
  18. package/dist/react/use-instance.js +6 -3
  19. package/dist/react/use-instance.js.map +1 -1
  20. package/dist/react/use-local.cjs +1 -0
  21. package/dist/react/use-local.cjs.map +1 -1
  22. package/dist/react/use-local.js +1 -0
  23. package/dist/react/use-local.js.map +1 -1
  24. package/dist/react/use-model.cjs +34 -8
  25. package/dist/react/use-model.cjs.map +1 -1
  26. package/dist/react/use-model.d.ts.map +1 -1
  27. package/dist/react/use-model.js +34 -8
  28. package/dist/react/use-model.js.map +1 -1
  29. package/dist/react/use-subscribe-only.cjs +3 -2
  30. package/dist/react/use-subscribe-only.cjs.map +1 -1
  31. package/dist/react/use-subscribe-only.d.ts.map +1 -1
  32. package/dist/react/use-subscribe-only.js +3 -2
  33. package/dist/react/use-subscribe-only.js.map +1 -1
  34. package/examples/react/AuthExample/src/components/AdminPage.tsx +3 -1
  35. package/examples/react/AuthExample/src/components/DashboardPage.tsx +4 -2
  36. package/examples/react/AuthExample/src/components/ProfilePage.tsx +2 -1
  37. package/package.json +1 -1
  38. package/src/Model.md +55 -6
  39. package/src/Service.md +4 -1
  40. package/src/react/components/DataTable.tsx +9 -13
  41. package/src/react/use-instance.ts +14 -3
  42. package/src/react/use-local.ts +2 -1
  43. package/src/react/use-model.md +51 -4
  44. package/src/react/use-model.test.tsx +86 -0
  45. package/src/react/use-model.ts +44 -15
  46. package/src/react/use-subscribe-only.ts +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/react/components/DataTable.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type {\n Column,\n SortHeaderProps,\n SelectionState,\n SelectionHelper,\n PaginationState,\n PaginationHelper,\n PaginationInfo,\n SortingHelper,\n AsyncStateProps,\n} from './types';\nimport { isSelectionHelper, isPaginationHelper, isSortingHelper } from './types';\nimport type { SortDescriptor } from '../../Sorting';\n\n// ── Prop resolution helpers ──\n\ninterface ResolvedSelection {\n selected: ReadonlySet<any>;\n onToggle: (key: any) => void;\n onToggleAll: (allKeys: any[]) => void;\n}\n\nfunction resolveSelectionProp(selection: SelectionState | SelectionHelper): ResolvedSelection {\n if (isSelectionHelper(selection)) {\n return {\n selected: selection.selected,\n onToggle: (key) => selection.toggle(key),\n onToggleAll: (allKeys) => selection.toggleAll(allKeys),\n };\n }\n return {\n selected: selection.selected,\n onToggle: selection.onToggle,\n onToggleAll: (allKeys) => selection.onToggleAll(allKeys),\n };\n}\n\nfunction resolveSortProp(\n sort: readonly SortDescriptor[] | SortingHelper,\n onSort?: (key: string) => void,\n): { sorts: readonly SortDescriptor[]; onSort: ((key: string) => void) | undefined } {\n if (isSortingHelper(sort)) {\n return { sorts: sort.sorts, onSort: (key) => sort.toggle(key) };\n }\n return { sorts: sort, onSort };\n}\n\nfunction resolvePaginationProp(\n pagination: PaginationState | PaginationHelper,\n pageSize?: number,\n paginationTotal?: number,\n): PaginationInfo {\n if (isPaginationHelper(pagination)) {\n const total = paginationTotal ?? 0;\n const ps = pagination.pageSize;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: (p) => pagination.setPage(p),\n goPrev: () => pagination.setPage(page - 1),\n goNext: () => pagination.setPage(page + 1),\n };\n }\n const total = pagination.total;\n const ps = pageSize ?? 10;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: pagination.onPageChange,\n goPrev: () => pagination.onPageChange(page - 1),\n goNext: () => pagination.onPageChange(page + 1),\n };\n}\n\n/** Props for the DataTable headless component. */\nexport interface DataTableProps<T> extends AsyncStateProps {\n items: T[];\n columns: Column<T>[];\n keyOf?: (item: T) => string | number;\n pageSize?: number;\n\n // Controlled state — accepts object-literal OR helper instance\n sort?: readonly SortDescriptor[] | SortingHelper;\n onSort?: (key: string) => void;\n selection?: SelectionState | SelectionHelper;\n pagination?: PaginationState | PaginationHelper;\n paginationTotal?: number;\n\n // Render slots\n renderEmpty?: () => ReactNode;\n renderLoading?: () => ReactNode;\n renderError?: (error: string) => ReactNode;\n renderSortIndicator?: (props: SortHeaderProps) => ReactNode;\n renderRow?: (item: T, index: number, defaultCells: ReactNode) => ReactNode;\n renderPagination?: (info: PaginationInfo) => ReactNode;\n\n className?: string;\n 'aria-label'?: string;\n}\n\nconst defaultKeyOf = (item: any) => item.id;\n\nfunction getAriaSortValue(key: string, sorts: readonly SortDescriptor[] | undefined): 'ascending' | 'descending' | 'none' {\n if (!sorts) return 'none';\n const desc = sorts.find(s => s.key === key);\n if (!desc) return 'none';\n return desc.direction === 'asc' ? 'ascending' : 'descending';\n}\n\n/**\n * Headless data table with sort headers, selection checkboxes, and pagination slots.\n * Renders semantic HTML (`<table>`) with data attributes for styling.\n * Accepts Sorting/Selection/Pagination helpers directly via duck-typing.\n */\nexport function DataTable<T>({\n items,\n columns,\n keyOf = defaultKeyOf,\n pageSize,\n sort,\n onSort,\n selection,\n loading,\n error,\n pagination,\n paginationTotal,\n renderEmpty,\n renderLoading,\n renderError,\n renderSortIndicator,\n renderRow,\n renderPagination,\n className,\n 'aria-label': ariaLabel,\n}: DataTableProps<T>) {\n if (loading && renderLoading) return <>{renderLoading()}</>;\n if (error && renderError) return <>{renderError(error)}</>;\n if (items.length === 0 && renderEmpty) return <>{renderEmpty()}</>;\n\n // ── Resolve props ──\n const resolvedSelection = selection ? resolveSelectionProp(selection) : undefined;\n\n let resolvedSorts: readonly SortDescriptor[] | undefined;\n let resolvedOnSort: ((key: string) => void) | undefined;\n if (sort) {\n const resolved = resolveSortProp(sort, onSort);\n resolvedSorts = resolved.sorts;\n resolvedOnSort = resolved.onSort;\n }\n\n let displayItems = items;\n let paginationInfo: PaginationInfo | undefined;\n if (pagination) {\n paginationInfo = resolvePaginationProp(pagination, pageSize, paginationTotal);\n } else if (pageSize && !pagination) {\n displayItems = items.slice(0, pageSize);\n }\n\n // Selection: check indeterminate state\n const allKeys = resolvedSelection ? displayItems.map(item => keyOf(item)) : [];\n const allSelected = resolvedSelection && allKeys.length > 0 && allKeys.every(k => resolvedSelection!.selected.has(k));\n const someSelected = resolvedSelection && allKeys.some(k => resolvedSelection!.selected.has(k));\n\n return (\n <div data-component=\"data-table\" className={className}>\n <table role=\"grid\" aria-label={ariaLabel}>\n <thead>\n <tr>\n {resolvedSelection && (\n <th data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!allSelected}\n ref={(el) => {\n if (el) el.indeterminate = !!someSelected && !allSelected;\n }}\n onChange={() => resolvedSelection!.onToggleAll(allKeys)}\n aria-label=\"Select all\"\n />\n </th>\n )}\n {columns.map((col) => {\n const isSortable = col.sortable && resolvedOnSort;\n const sortDesc = resolvedSorts?.find(s => s.key === col.key);\n const isActive = !!sortDesc;\n const sortIndex = resolvedSorts ? resolvedSorts.findIndex(s => s.key === col.key) : -1;\n\n return (\n <th\n key={col.key}\n data-sortable={isSortable ? '' : undefined}\n data-sorted={isActive ? '' : undefined}\n data-align={col.align}\n style={col.width ? { width: col.width } : undefined}\n aria-sort={isSortable ? getAriaSortValue(col.key, resolvedSorts) : undefined}\n >\n {isSortable ? (\n <button type=\"button\" onClick={() => resolvedOnSort!(col.key)}>\n {col.header}\n {renderSortIndicator?.({\n active: isActive,\n direction: sortDesc?.direction ?? 'asc',\n index: sortIndex,\n onToggle: () => resolvedOnSort!(col.key),\n })}\n </button>\n ) : (\n col.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {displayItems.map((item, index) => {\n const key = keyOf(item);\n const isSelected = resolvedSelection?.selected.has(key);\n\n const cells = (\n <>\n {resolvedSelection && (\n <td data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!isSelected}\n onChange={() => resolvedSelection!.onToggle(key)}\n aria-label={`Select row ${key}`}\n />\n </td>\n )}\n {columns.map((col) => (\n <td\n key={col.key}\n data-align={col.align}\n >\n {col.render(item, index)}\n </td>\n ))}\n </>\n );\n\n return (\n <tr key={key} data-selected={isSelected ? '' : undefined}>\n {renderRow ? renderRow(item, index, cells) : cells}\n </tr>\n );\n })}\n </tbody>\n </table>\n {paginationInfo && renderPagination?.(paginationInfo)}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/react/components/DataTable.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport type {\n Column,\n SortHeaderProps,\n SelectionState,\n SelectionHelper,\n PaginationState,\n PaginationHelper,\n PaginationInfo,\n SortingHelper,\n AsyncStateProps,\n} from './types';\nimport { isSelectionHelper, isPaginationHelper, isSortingHelper } from './types';\nimport type { SortDescriptor } from '../../Sorting';\n\n// ── Prop resolution helpers ──\n\ninterface ResolvedSelection {\n selected: ReadonlySet<any>;\n onToggle: (key: any) => void;\n onToggleAll: (allKeys: any[]) => void;\n}\n\nfunction resolveSelectionProp(selection: SelectionState | SelectionHelper): ResolvedSelection {\n if (isSelectionHelper(selection)) {\n return {\n selected: selection.selected,\n onToggle: (key) => selection.toggle(key),\n onToggleAll: (allKeys) => selection.toggleAll(allKeys),\n };\n }\n return {\n selected: selection.selected,\n onToggle: selection.onToggle,\n onToggleAll: (allKeys) => selection.onToggleAll(allKeys),\n };\n}\n\nfunction resolveSortProp(\n sort: readonly SortDescriptor[] | SortingHelper,\n onSort?: (key: string) => void,\n): { sorts: readonly SortDescriptor[]; onSort: ((key: string) => void) | undefined } {\n if (isSortingHelper(sort)) {\n return { sorts: sort.sorts, onSort: (key) => sort.toggle(key) };\n }\n return { sorts: sort, onSort };\n}\n\nfunction resolvePaginationProp(\n pagination: PaginationState | PaginationHelper,\n pageSize?: number,\n paginationTotal?: number,\n): PaginationInfo {\n if (isPaginationHelper(pagination)) {\n const total = paginationTotal ?? 0;\n const ps = pagination.pageSize;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: (p) => pagination.setPage(p),\n goPrev: () => pagination.setPage(page - 1),\n goNext: () => pagination.setPage(page + 1),\n };\n }\n const total = pagination.total;\n const ps = pageSize ?? 10;\n const pageCount = Math.max(1, Math.ceil(total / ps));\n const page = pagination.page;\n return {\n page,\n pageCount,\n total,\n pageSize: ps,\n hasPrev: page > 1,\n hasNext: page < pageCount,\n goToPage: pagination.onPageChange,\n goPrev: () => pagination.onPageChange(page - 1),\n goNext: () => pagination.onPageChange(page + 1),\n };\n}\n\n/** Props for the DataTable headless component. */\nexport interface DataTableProps<T> extends AsyncStateProps {\n items: T[];\n columns: Column<T>[];\n keyOf?: (item: T) => string | number;\n pageSize?: number;\n\n // Controlled state — accepts object-literal OR helper instance\n sort?: readonly SortDescriptor[] | SortingHelper;\n onSort?: (key: string) => void;\n selection?: SelectionState | SelectionHelper;\n pagination?: PaginationState | PaginationHelper;\n paginationTotal?: number;\n\n // Render slots\n renderEmpty?: () => ReactNode;\n renderLoading?: () => ReactNode;\n renderError?: (error: string) => ReactNode;\n renderSortIndicator?: (props: SortHeaderProps) => ReactNode;\n renderRow?: (item: T, index: number, defaultCells: ReactNode) => ReactNode;\n renderPagination?: (info: PaginationInfo) => ReactNode;\n\n className?: string;\n 'aria-label'?: string;\n}\n\nconst defaultKeyOf = (item: any) => item.id;\n\nfunction getAriaSortValue(key: string, sorts: readonly SortDescriptor[] | undefined): 'ascending' | 'descending' | 'none' {\n if (!sorts) return 'none';\n const desc = sorts.find(s => s.key === key);\n if (!desc) return 'none';\n return desc.direction === 'asc' ? 'ascending' : 'descending';\n}\n\n/**\n * Headless data table with sort headers, selection checkboxes, and pagination slots.\n * Renders semantic HTML (`<table>`) with data attributes for styling.\n * Accepts Sorting/Selection/Pagination helpers directly via duck-typing.\n */\nexport function DataTable<T>({\n items,\n columns,\n keyOf = defaultKeyOf,\n pageSize,\n sort,\n onSort,\n selection,\n loading,\n error,\n pagination,\n paginationTotal,\n renderEmpty,\n renderLoading,\n renderError,\n renderSortIndicator,\n renderRow,\n renderPagination,\n className,\n 'aria-label': ariaLabel,\n}: DataTableProps<T>) {\n if (loading && renderLoading) return <>{renderLoading()}</>;\n if (error && renderError) return <>{renderError(error)}</>;\n if (items.length === 0 && renderEmpty) return <>{renderEmpty()}</>;\n\n // ── Resolve props ──\n const resolvedSelection = selection ? resolveSelectionProp(selection) : undefined;\n\n const resolvedSort = sort ? resolveSortProp(sort, onSort) : undefined;\n const resolvedSorts = resolvedSort?.sorts;\n const resolvedOnSort = resolvedSort?.onSort;\n\n let displayItems = items;\n let paginationInfo: PaginationInfo | undefined;\n if (pagination) {\n paginationInfo = resolvePaginationProp(pagination, pageSize, paginationTotal);\n } else if (pageSize && !pagination) {\n displayItems = items.slice(0, pageSize);\n }\n\n // Selection: check indeterminate state\n const allKeys = resolvedSelection ? displayItems.map(item => keyOf(item)) : [];\n const allSelected = !!resolvedSelection && allKeys.length > 0 && allKeys.every(k => resolvedSelection.selected.has(k));\n const someSelected = !!resolvedSelection && allKeys.some(k => resolvedSelection.selected.has(k));\n\n return (\n <div data-component=\"data-table\" className={className}>\n <table role=\"grid\" aria-label={ariaLabel}>\n <thead>\n <tr>\n {resolvedSelection && (\n <th data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!allSelected}\n ref={(el) => {\n if (el) el.indeterminate = !!someSelected && !allSelected;\n }}\n onChange={() => resolvedSelection.onToggleAll(allKeys)}\n aria-label=\"Select all\"\n />\n </th>\n )}\n {columns.map((col) => {\n const isSortable = col.sortable && resolvedOnSort;\n const sortDesc = resolvedSorts?.find(s => s.key === col.key);\n const isActive = !!sortDesc;\n const sortIndex = resolvedSorts ? resolvedSorts.findIndex(s => s.key === col.key) : -1;\n\n return (\n <th\n key={col.key}\n data-sortable={isSortable ? '' : undefined}\n data-sorted={isActive ? '' : undefined}\n data-align={col.align}\n style={col.width ? { width: col.width } : undefined}\n aria-sort={isSortable ? getAriaSortValue(col.key, resolvedSorts) : undefined}\n >\n {isSortable ? (\n <button type=\"button\" onClick={() => resolvedOnSort(col.key)}>\n {col.header}\n {renderSortIndicator?.({\n active: isActive,\n direction: sortDesc?.direction ?? 'asc',\n index: sortIndex,\n onToggle: () => resolvedOnSort(col.key),\n })}\n </button>\n ) : (\n col.header\n )}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {displayItems.map((item, index) => {\n const key = keyOf(item);\n const isSelected = resolvedSelection?.selected.has(key);\n\n const cells = (\n <>\n {resolvedSelection && (\n <td data-column=\"select\">\n <input\n type=\"checkbox\"\n checked={!!isSelected}\n onChange={() => resolvedSelection.onToggle(key)}\n aria-label={`Select row ${key}`}\n />\n </td>\n )}\n {columns.map((col) => (\n <td\n key={col.key}\n data-align={col.align}\n >\n {col.render(item, index)}\n </td>\n ))}\n </>\n );\n\n return (\n <tr key={key} data-selected={isSelected ? '' : undefined}>\n {renderRow ? renderRow(item, index, cells) : cells}\n </tr>\n );\n })}\n </tbody>\n </table>\n {paginationInfo && renderPagination?.(paginationInfo)}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  let react = require("react");
2
2
  //#region src/react/use-instance.ts
3
+ var __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
3
4
  function hasAsyncSubscription(obj) {
4
5
  return obj !== null && typeof obj === "object" && typeof obj.subscribeAsync === "function";
5
6
  }
@@ -13,22 +14,23 @@ var SERVER_SNAPSHOT = () => 0;
13
14
  * trigger React re-renders.
14
15
  */
15
16
  function useInstance(subscribable) {
17
+ if (__DEV__ && !subscribable) throw new Error("useInstance: received an undefined/null subscribable. Make sure the instance is created synchronously (in a parent ViewModel's constructor or as a singleton) before the component that calls useInstance renders. If it's created in onInit(), guard the component with `if (!vm.child) return <Spinner />` first.");
16
18
  const ref = (0, react.useRef)(null);
17
19
  if (!ref.current || ref.current.subscribable !== subscribable) {
18
20
  const version = { current: ref.current?.version ?? 0 };
19
- ref.current = {
21
+ const entry = {
20
22
  version: version.current,
21
23
  subscribable,
22
24
  subscribe: (onStoreChange) => {
23
25
  const unsub1 = subscribable.subscribe(() => {
24
26
  version.current++;
25
- ref.current.version = version.current;
27
+ entry.version = version.current;
26
28
  onStoreChange();
27
29
  });
28
30
  let unsub2;
29
31
  if (hasAsyncSubscription(subscribable)) unsub2 = subscribable.subscribeAsync(() => {
30
32
  version.current++;
31
- ref.current.version = version.current;
33
+ entry.version = version.current;
32
34
  onStoreChange();
33
35
  });
34
36
  return () => {
@@ -38,6 +40,7 @@ function useInstance(subscribable) {
38
40
  },
39
41
  getSnapshot: () => version.current
40
42
  };
43
+ ref.current = entry;
41
44
  }
42
45
  (0, react.useSyncExternalStore)(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);
43
46
  return subscribable.state;
@@ -1 +1 @@
1
- {"version":3,"file":"use-instance.cjs","names":[],"sources":["../../src/react/use-instance.ts"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\nimport type { Subscribable } from '../types';\n\nfunction hasAsyncSubscription(obj: unknown): obj is { subscribeAsync(cb: () => void): () => void } {\n return (\n obj !== null &&\n typeof obj === 'object' &&\n typeof (obj as any).subscribeAsync === 'function'\n );\n}\n\nconst SERVER_SNAPSHOT = () => 0;\n\ninterface InstanceRef<S> {\n version: number;\n subscribable: Subscribable<S>;\n subscribe: (onStoreChange: () => void) => () => void;\n getSnapshot: () => number;\n}\n\n/**\n * Subscribe to an existing Subscribable instance.\n * No ownership - caller manages the instance lifecycle.\n *\n * If the instance has a `subscribeAsync` method (duck-typed),\n * a combined subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): S {\n const ref = useRef<InstanceRef<S> | null>(null);\n\n if (!ref.current || ref.current.subscribable !== subscribable) {\n const version = { current: ref.current?.version ?? 0 };\n ref.current = {\n version: version.current,\n subscribable,\n subscribe: (onStoreChange: () => void) => {\n const unsub1 = subscribable.subscribe(() => {\n version.current++;\n ref.current!.version = version.current;\n onStoreChange();\n });\n let unsub2: (() => void) | undefined;\n if (hasAsyncSubscription(subscribable)) {\n unsub2 = subscribable.subscribeAsync(() => {\n version.current++;\n ref.current!.version = version.current;\n onStoreChange();\n });\n }\n return () => { unsub1(); unsub2?.(); };\n },\n getSnapshot: () => version.current,\n };\n }\n\n useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);\n\n return subscribable.state;\n}\n"],"mappings":";;AAGA,SAAS,qBAAqB,KAAqE;AACjG,QACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;;AAI3C,IAAM,wBAAwB;;;;;;;;;AAiB9B,SAAgB,YAAe,cAAkC;CAC/D,MAAM,OAAA,GAAA,MAAA,QAAoC,KAAK;AAE/C,KAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,iBAAiB,cAAc;EAC7D,MAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,GAAG;AACtD,MAAI,UAAU;GACZ,SAAS,QAAQ;GACjB;GACA,YAAY,kBAA8B;IACxC,MAAM,SAAS,aAAa,gBAAgB;AAC1C,aAAQ;AACR,SAAI,QAAS,UAAU,QAAQ;AAC/B,oBAAe;MACf;IACF,IAAI;AACJ,QAAI,qBAAqB,aAAa,CACpC,UAAS,aAAa,qBAAqB;AACzC,aAAQ;AACR,SAAI,QAAS,UAAU,QAAQ;AAC/B,oBAAe;MACf;AAEJ,iBAAa;AAAE,aAAQ;AAAE,eAAU;;;GAErC,mBAAmB,QAAQ;GAC5B;;AAGH,EAAA,GAAA,MAAA,sBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,gBAAgB;AAErF,QAAO,aAAa"}
1
+ {"version":3,"file":"use-instance.cjs","names":[],"sources":["../../src/react/use-instance.ts"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\nimport type { Subscribable } from '../types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nfunction hasAsyncSubscription(obj: unknown): obj is { subscribeAsync(cb: () => void): () => void } {\n return (\n obj !== null &&\n typeof obj === 'object' &&\n typeof (obj as any).subscribeAsync === 'function'\n );\n}\n\nconst SERVER_SNAPSHOT = () => 0;\n\ninterface InstanceRef<S> {\n version: number;\n subscribable: Subscribable<S>;\n subscribe: (onStoreChange: () => void) => () => void;\n getSnapshot: () => number;\n}\n\n/**\n * Subscribe to an existing Subscribable instance.\n * No ownership - caller manages the instance lifecycle.\n *\n * If the instance has a `subscribeAsync` method (duck-typed),\n * a combined subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): S {\n if (__DEV__ && !subscribable) {\n throw new Error(\n 'useInstance: received an undefined/null subscribable. ' +\n 'Make sure the instance is created synchronously (in a parent ViewModel\\'s constructor or as a singleton) ' +\n 'before the component that calls useInstance renders. ' +\n 'If it\\'s created in onInit(), guard the component with `if (!vm.child) return <Spinner />` first.',\n );\n }\n const ref = useRef<InstanceRef<S> | null>(null);\n\n if (!ref.current || ref.current.subscribable !== subscribable) {\n const version = { current: ref.current?.version ?? 0 };\n const entry: InstanceRef<S> = {\n version: version.current,\n subscribable,\n subscribe: (onStoreChange: () => void) => {\n const unsub1 = subscribable.subscribe(() => {\n version.current++;\n entry.version = version.current;\n onStoreChange();\n });\n let unsub2: (() => void) | undefined;\n if (hasAsyncSubscription(subscribable)) {\n unsub2 = subscribable.subscribeAsync(() => {\n version.current++;\n entry.version = version.current;\n onStoreChange();\n });\n }\n return () => { unsub1(); unsub2?.(); };\n },\n getSnapshot: () => version.current,\n };\n ref.current = entry;\n }\n\n useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);\n\n return subscribable.state;\n}\n"],"mappings":";;AAGA,IAAM,UAAU,OAAO,oBAAoB,eAAe;AAE1D,SAAS,qBAAqB,KAAqE;AACjG,QACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;;AAI3C,IAAM,wBAAwB;;;;;;;;;AAiB9B,SAAgB,YAAe,cAAkC;AAC/D,KAAI,WAAW,CAAC,aACd,OAAM,IAAI,MACR,sTAID;CAEH,MAAM,OAAA,GAAA,MAAA,QAAoC,KAAK;AAE/C,KAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,iBAAiB,cAAc;EAC7D,MAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,GAAG;EACtD,MAAM,QAAwB;GAC5B,SAAS,QAAQ;GACjB;GACA,YAAY,kBAA8B;IACxC,MAAM,SAAS,aAAa,gBAAgB;AAC1C,aAAQ;AACR,WAAM,UAAU,QAAQ;AACxB,oBAAe;MACf;IACF,IAAI;AACJ,QAAI,qBAAqB,aAAa,CACpC,UAAS,aAAa,qBAAqB;AACzC,aAAQ;AACR,WAAM,UAAU,QAAQ;AACxB,oBAAe;MACf;AAEJ,iBAAa;AAAE,aAAQ;AAAE,eAAU;;;GAErC,mBAAmB,QAAQ;GAC5B;AACD,MAAI,UAAU;;AAGhB,EAAA,GAAA,MAAA,sBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,gBAAgB;AAErF,QAAO,aAAa"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-instance.d.ts","sourceRoot":"","sources":["../../src/react/use-instance.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAmB7C;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CA+B/D"}
1
+ {"version":3,"file":"use-instance.d.ts","sourceRoot":"","sources":["../../src/react/use-instance.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAqB7C;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAwC/D"}
@@ -1,5 +1,6 @@
1
1
  import { useRef, useSyncExternalStore } from "react";
2
2
  //#region src/react/use-instance.ts
3
+ var __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
3
4
  function hasAsyncSubscription(obj) {
4
5
  return obj !== null && typeof obj === "object" && typeof obj.subscribeAsync === "function";
5
6
  }
@@ -13,22 +14,23 @@ var SERVER_SNAPSHOT = () => 0;
13
14
  * trigger React re-renders.
14
15
  */
15
16
  function useInstance(subscribable) {
17
+ if (__DEV__ && !subscribable) throw new Error("useInstance: received an undefined/null subscribable. Make sure the instance is created synchronously (in a parent ViewModel's constructor or as a singleton) before the component that calls useInstance renders. If it's created in onInit(), guard the component with `if (!vm.child) return <Spinner />` first.");
16
18
  const ref = useRef(null);
17
19
  if (!ref.current || ref.current.subscribable !== subscribable) {
18
20
  const version = { current: ref.current?.version ?? 0 };
19
- ref.current = {
21
+ const entry = {
20
22
  version: version.current,
21
23
  subscribable,
22
24
  subscribe: (onStoreChange) => {
23
25
  const unsub1 = subscribable.subscribe(() => {
24
26
  version.current++;
25
- ref.current.version = version.current;
27
+ entry.version = version.current;
26
28
  onStoreChange();
27
29
  });
28
30
  let unsub2;
29
31
  if (hasAsyncSubscription(subscribable)) unsub2 = subscribable.subscribeAsync(() => {
30
32
  version.current++;
31
- ref.current.version = version.current;
33
+ entry.version = version.current;
32
34
  onStoreChange();
33
35
  });
34
36
  return () => {
@@ -38,6 +40,7 @@ function useInstance(subscribable) {
38
40
  },
39
41
  getSnapshot: () => version.current
40
42
  };
43
+ ref.current = entry;
41
44
  }
42
45
  useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);
43
46
  return subscribable.state;
@@ -1 +1 @@
1
- {"version":3,"file":"use-instance.js","names":[],"sources":["../../src/react/use-instance.ts"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\nimport type { Subscribable } from '../types';\n\nfunction hasAsyncSubscription(obj: unknown): obj is { subscribeAsync(cb: () => void): () => void } {\n return (\n obj !== null &&\n typeof obj === 'object' &&\n typeof (obj as any).subscribeAsync === 'function'\n );\n}\n\nconst SERVER_SNAPSHOT = () => 0;\n\ninterface InstanceRef<S> {\n version: number;\n subscribable: Subscribable<S>;\n subscribe: (onStoreChange: () => void) => () => void;\n getSnapshot: () => number;\n}\n\n/**\n * Subscribe to an existing Subscribable instance.\n * No ownership - caller manages the instance lifecycle.\n *\n * If the instance has a `subscribeAsync` method (duck-typed),\n * a combined subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): S {\n const ref = useRef<InstanceRef<S> | null>(null);\n\n if (!ref.current || ref.current.subscribable !== subscribable) {\n const version = { current: ref.current?.version ?? 0 };\n ref.current = {\n version: version.current,\n subscribable,\n subscribe: (onStoreChange: () => void) => {\n const unsub1 = subscribable.subscribe(() => {\n version.current++;\n ref.current!.version = version.current;\n onStoreChange();\n });\n let unsub2: (() => void) | undefined;\n if (hasAsyncSubscription(subscribable)) {\n unsub2 = subscribable.subscribeAsync(() => {\n version.current++;\n ref.current!.version = version.current;\n onStoreChange();\n });\n }\n return () => { unsub1(); unsub2?.(); };\n },\n getSnapshot: () => version.current,\n };\n }\n\n useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);\n\n return subscribable.state;\n}\n"],"mappings":";;AAGA,SAAS,qBAAqB,KAAqE;AACjG,QACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;;AAI3C,IAAM,wBAAwB;;;;;;;;;AAiB9B,SAAgB,YAAe,cAAkC;CAC/D,MAAM,MAAM,OAA8B,KAAK;AAE/C,KAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,iBAAiB,cAAc;EAC7D,MAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,GAAG;AACtD,MAAI,UAAU;GACZ,SAAS,QAAQ;GACjB;GACA,YAAY,kBAA8B;IACxC,MAAM,SAAS,aAAa,gBAAgB;AAC1C,aAAQ;AACR,SAAI,QAAS,UAAU,QAAQ;AAC/B,oBAAe;MACf;IACF,IAAI;AACJ,QAAI,qBAAqB,aAAa,CACpC,UAAS,aAAa,qBAAqB;AACzC,aAAQ;AACR,SAAI,QAAS,UAAU,QAAQ;AAC/B,oBAAe;MACf;AAEJ,iBAAa;AAAE,aAAQ;AAAE,eAAU;;;GAErC,mBAAmB,QAAQ;GAC5B;;AAGH,sBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,gBAAgB;AAErF,QAAO,aAAa"}
1
+ {"version":3,"file":"use-instance.js","names":[],"sources":["../../src/react/use-instance.ts"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\nimport type { Subscribable } from '../types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nfunction hasAsyncSubscription(obj: unknown): obj is { subscribeAsync(cb: () => void): () => void } {\n return (\n obj !== null &&\n typeof obj === 'object' &&\n typeof (obj as any).subscribeAsync === 'function'\n );\n}\n\nconst SERVER_SNAPSHOT = () => 0;\n\ninterface InstanceRef<S> {\n version: number;\n subscribable: Subscribable<S>;\n subscribe: (onStoreChange: () => void) => () => void;\n getSnapshot: () => number;\n}\n\n/**\n * Subscribe to an existing Subscribable instance.\n * No ownership - caller manages the instance lifecycle.\n *\n * If the instance has a `subscribeAsync` method (duck-typed),\n * a combined subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): S {\n if (__DEV__ && !subscribable) {\n throw new Error(\n 'useInstance: received an undefined/null subscribable. ' +\n 'Make sure the instance is created synchronously (in a parent ViewModel\\'s constructor or as a singleton) ' +\n 'before the component that calls useInstance renders. ' +\n 'If it\\'s created in onInit(), guard the component with `if (!vm.child) return <Spinner />` first.',\n );\n }\n const ref = useRef<InstanceRef<S> | null>(null);\n\n if (!ref.current || ref.current.subscribable !== subscribable) {\n const version = { current: ref.current?.version ?? 0 };\n const entry: InstanceRef<S> = {\n version: version.current,\n subscribable,\n subscribe: (onStoreChange: () => void) => {\n const unsub1 = subscribable.subscribe(() => {\n version.current++;\n entry.version = version.current;\n onStoreChange();\n });\n let unsub2: (() => void) | undefined;\n if (hasAsyncSubscription(subscribable)) {\n unsub2 = subscribable.subscribeAsync(() => {\n version.current++;\n entry.version = version.current;\n onStoreChange();\n });\n }\n return () => { unsub1(); unsub2?.(); };\n },\n getSnapshot: () => version.current,\n };\n ref.current = entry;\n }\n\n useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);\n\n return subscribable.state;\n}\n"],"mappings":";;AAGA,IAAM,UAAU,OAAO,oBAAoB,eAAe;AAE1D,SAAS,qBAAqB,KAAqE;AACjG,QACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;;AAI3C,IAAM,wBAAwB;;;;;;;;;AAiB9B,SAAgB,YAAe,cAAkC;AAC/D,KAAI,WAAW,CAAC,aACd,OAAM,IAAI,MACR,sTAID;CAEH,MAAM,MAAM,OAA8B,KAAK;AAE/C,KAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,iBAAiB,cAAc;EAC7D,MAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,GAAG;EACtD,MAAM,QAAwB;GAC5B,SAAS,QAAQ;GACjB;GACA,YAAY,kBAA8B;IACxC,MAAM,SAAS,aAAa,gBAAgB;AAC1C,aAAQ;AACR,WAAM,UAAU,QAAQ;AACxB,oBAAe;MACf;IACF,IAAI;AACJ,QAAI,qBAAqB,aAAa,CACpC,UAAS,aAAa,qBAAqB;AACzC,aAAQ;AACR,WAAM,UAAU,QAAQ;AACxB,oBAAe;MACf;AAEJ,iBAAa;AAAE,aAAQ;AAAE,eAAU;;;GAErC,mBAAmB,QAAQ;GAC5B;AACD,MAAI,UAAU;;AAGhB,sBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,gBAAgB;AAErF,QAAO,aAAa"}
@@ -31,6 +31,7 @@ function useLocal(classOrFactory, ...rest) {
31
31
  else instanceRef.current = classOrFactory();
32
32
  (0, react.useEffect)(() => {
33
33
  const instance = instanceRef.current;
34
+ if (!instance) return;
34
35
  mountedRef.current = true;
35
36
  if (require_guards.isInitializable(instance)) instance.init();
36
37
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"use-local.cjs","names":[],"sources":["../../src/react/use-local.ts"],"sourcesContent":["import { useRef, useEffect } from 'react';\nimport type { DependencyList } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { isSubscribable, isSubscribeOnly, isInitializable } from './guards';\nimport { useInstance } from './use-instance';\nimport { useSubscribeOnly } from './use-subscribe-only';\nimport type { StateOf } from './types';\n\nfunction depsChanged(prev: DependencyList | undefined, next: DependencyList): boolean {\n if (prev === undefined) return false;\n if (prev.length !== next.length) return true;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return true;\n }\n return false;\n}\n\n// ── With deps (class + initialState + deps) ────────────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<any> & Disposable>(\n Class: new (initialState: StateOf<T>) => T,\n initialState: StateOf<T>,\n deps: DependencyList,\n): [StateOf<T>, T];\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T,\n deps: DependencyList,\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(\n factory: () => T,\n deps: DependencyList,\n): T;\n\n// ── Without deps (existing overloads, unchanged) ───────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(factory: () => T): T;\n\n// ── Implementation ─────────────────────────────────────────────────\n\nexport function useLocal<T extends Disposable, S = StateOf<T>>(\n classOrFactory: (new (...args: unknown[]) => T) | (() => T),\n ...rest: unknown[]\n): [S, T] | T {\n // ── Detect deps: last arg is an array → treat as deps ──\n let args: unknown[];\n let deps: DependencyList | undefined;\n\n if (rest.length > 0 && Array.isArray(rest[rest.length - 1])) {\n deps = rest[rest.length - 1] as DependencyList;\n args = rest.slice(0, -1);\n } else {\n args = rest;\n deps = undefined;\n }\n\n const instanceRef = useRef<T | null>(null);\n const mountedRef = useRef(false);\n const prevDepsRef = useRef<DependencyList | undefined>(undefined);\n\n // ── Render phase: dep-change detection ──\n if (deps !== undefined && depsChanged(prevDepsRef.current, deps)) {\n instanceRef.current?.dispose();\n instanceRef.current = null;\n }\n if (deps !== undefined) {\n prevDepsRef.current = deps;\n }\n\n // ── Create instance if needed ──\n if (!instanceRef.current || instanceRef.current.disposed) {\n const isClass =\n typeof classOrFactory === 'function' &&\n classOrFactory.prototype &&\n classOrFactory.prototype.constructor === classOrFactory;\n\n if (isClass) {\n instanceRef.current = new (classOrFactory as new (...a: unknown[]) => T)(...args);\n } else {\n instanceRef.current = (classOrFactory as () => T)();\n }\n }\n\n // ── Effect: init + deferred cleanup ──\n useEffect(() => {\n const instance = instanceRef.current!; // capture for cleanup closure\n mountedRef.current = true;\n if (isInitializable(instance)) {\n instance.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n instance.dispose();\n }\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps ?? []);\n\n // ── Subscribe to state if Subscribable ──\n if (isSubscribable(instanceRef.current)) {\n const state = useInstance(instanceRef.current as unknown as Subscribable<S>);\n return [state, instanceRef.current];\n }\n\n // ── Subscribe for re-renders if subscribe-only (e.g., Trackable) ──\n if (isSubscribeOnly(instanceRef.current)) {\n useSubscribeOnly(instanceRef.current);\n return instanceRef.current;\n }\n\n return instanceRef.current;\n}\n"],"mappings":";;;;;AAQA,SAAS,YAAY,MAAkC,MAA+B;AACpF,KAAI,SAAS,KAAA,EAAW,QAAO;AAC/B,KAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAC/B,KAAI,CAAC,OAAO,GAAG,KAAK,IAAI,KAAK,GAAG,CAAE,QAAO;AAE3C,QAAO;;AA4ET,SAAgB,SACd,gBACA,GAAG,MACS;CAEZ,IAAI;CACJ,IAAI;AAEJ,KAAI,KAAK,SAAS,KAAK,MAAM,QAAQ,KAAK,KAAK,SAAS,GAAG,EAAE;AAC3D,SAAO,KAAK,KAAK,SAAS;AAC1B,SAAO,KAAK,MAAM,GAAG,GAAG;QACnB;AACL,SAAO;AACP,SAAO,KAAA;;CAGT,MAAM,eAAA,GAAA,MAAA,QAA+B,KAAK;CAC1C,MAAM,cAAA,GAAA,MAAA,QAAoB,MAAM;CAChC,MAAM,eAAA,GAAA,MAAA,QAAiD,KAAA,EAAU;AAGjE,KAAI,SAAS,KAAA,KAAa,YAAY,YAAY,SAAS,KAAK,EAAE;AAChE,cAAY,SAAS,SAAS;AAC9B,cAAY,UAAU;;AAExB,KAAI,SAAS,KAAA,EACX,aAAY,UAAU;AAIxB,KAAI,CAAC,YAAY,WAAW,YAAY,QAAQ,SAM9C,KAJE,OAAO,mBAAmB,cAC1B,eAAe,aACf,eAAe,UAAU,gBAAgB,eAGzC,aAAY,UAAU,IAAK,eAA8C,GAAG,KAAK;KAEjF,aAAY,UAAW,gBAA4B;AAKvD,EAAA,GAAA,MAAA,iBAAgB;EACd,MAAM,WAAW,YAAY;AAC7B,aAAW,UAAU;AACrB,MAAI,eAAA,gBAAgB,SAAS,CAC3B,UAAS,MAAM;AAEjB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS;MAEnB,EAAE;;IAGN,QAAQ,EAAE,CAAC;AAGd,KAAI,eAAA,eAAe,YAAY,QAAQ,CAErC,QAAO,CADO,qBAAA,YAAY,YAAY,QAAsC,EAC7D,YAAY,QAAQ;AAIrC,KAAI,eAAA,gBAAgB,YAAY,QAAQ,EAAE;AACxC,6BAAA,iBAAiB,YAAY,QAAQ;AACrC,SAAO,YAAY;;AAGrB,QAAO,YAAY"}
1
+ {"version":3,"file":"use-local.cjs","names":[],"sources":["../../src/react/use-local.ts"],"sourcesContent":["import { useRef, useEffect } from 'react';\nimport type { DependencyList } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { isSubscribable, isSubscribeOnly, isInitializable } from './guards';\nimport { useInstance } from './use-instance';\nimport { useSubscribeOnly } from './use-subscribe-only';\nimport type { StateOf } from './types';\n\nfunction depsChanged(prev: DependencyList | undefined, next: DependencyList): boolean {\n if (prev === undefined) return false;\n if (prev.length !== next.length) return true;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return true;\n }\n return false;\n}\n\n// ── With deps (class + initialState + deps) ────────────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<any> & Disposable>(\n Class: new (initialState: StateOf<T>) => T,\n initialState: StateOf<T>,\n deps: DependencyList,\n): [StateOf<T>, T];\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T,\n deps: DependencyList,\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(\n factory: () => T,\n deps: DependencyList,\n): T;\n\n// ── Without deps (existing overloads, unchanged) ───────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(factory: () => T): T;\n\n// ── Implementation ─────────────────────────────────────────────────\n\nexport function useLocal<T extends Disposable, S = StateOf<T>>(\n classOrFactory: (new (...args: unknown[]) => T) | (() => T),\n ...rest: unknown[]\n): [S, T] | T {\n // ── Detect deps: last arg is an array → treat as deps ──\n let args: unknown[];\n let deps: DependencyList | undefined;\n\n if (rest.length > 0 && Array.isArray(rest[rest.length - 1])) {\n deps = rest[rest.length - 1] as DependencyList;\n args = rest.slice(0, -1);\n } else {\n args = rest;\n deps = undefined;\n }\n\n const instanceRef = useRef<T | null>(null);\n const mountedRef = useRef(false);\n const prevDepsRef = useRef<DependencyList | undefined>(undefined);\n\n // ── Render phase: dep-change detection ──\n if (deps !== undefined && depsChanged(prevDepsRef.current, deps)) {\n instanceRef.current?.dispose();\n instanceRef.current = null;\n }\n if (deps !== undefined) {\n prevDepsRef.current = deps;\n }\n\n // ── Create instance if needed ──\n if (!instanceRef.current || instanceRef.current.disposed) {\n const isClass =\n typeof classOrFactory === 'function' &&\n classOrFactory.prototype &&\n classOrFactory.prototype.constructor === classOrFactory;\n\n if (isClass) {\n instanceRef.current = new (classOrFactory as new (...a: unknown[]) => T)(...args);\n } else {\n instanceRef.current = (classOrFactory as () => T)();\n }\n }\n\n // ── Effect: init + deferred cleanup ──\n useEffect(() => {\n const instance = instanceRef.current;\n if (!instance) return;\n mountedRef.current = true;\n if (isInitializable(instance)) {\n instance.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n instance.dispose();\n }\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps ?? []);\n\n // ── Subscribe to state if Subscribable ──\n if (isSubscribable(instanceRef.current)) {\n const state = useInstance(instanceRef.current as unknown as Subscribable<S>);\n return [state, instanceRef.current];\n }\n\n // ── Subscribe for re-renders if subscribe-only (e.g., Trackable) ──\n if (isSubscribeOnly(instanceRef.current)) {\n useSubscribeOnly(instanceRef.current);\n return instanceRef.current;\n }\n\n return instanceRef.current;\n}\n"],"mappings":";;;;;AAQA,SAAS,YAAY,MAAkC,MAA+B;AACpF,KAAI,SAAS,KAAA,EAAW,QAAO;AAC/B,KAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAC/B,KAAI,CAAC,OAAO,GAAG,KAAK,IAAI,KAAK,GAAG,CAAE,QAAO;AAE3C,QAAO;;AA4ET,SAAgB,SACd,gBACA,GAAG,MACS;CAEZ,IAAI;CACJ,IAAI;AAEJ,KAAI,KAAK,SAAS,KAAK,MAAM,QAAQ,KAAK,KAAK,SAAS,GAAG,EAAE;AAC3D,SAAO,KAAK,KAAK,SAAS;AAC1B,SAAO,KAAK,MAAM,GAAG,GAAG;QACnB;AACL,SAAO;AACP,SAAO,KAAA;;CAGT,MAAM,eAAA,GAAA,MAAA,QAA+B,KAAK;CAC1C,MAAM,cAAA,GAAA,MAAA,QAAoB,MAAM;CAChC,MAAM,eAAA,GAAA,MAAA,QAAiD,KAAA,EAAU;AAGjE,KAAI,SAAS,KAAA,KAAa,YAAY,YAAY,SAAS,KAAK,EAAE;AAChE,cAAY,SAAS,SAAS;AAC9B,cAAY,UAAU;;AAExB,KAAI,SAAS,KAAA,EACX,aAAY,UAAU;AAIxB,KAAI,CAAC,YAAY,WAAW,YAAY,QAAQ,SAM9C,KAJE,OAAO,mBAAmB,cAC1B,eAAe,aACf,eAAe,UAAU,gBAAgB,eAGzC,aAAY,UAAU,IAAK,eAA8C,GAAG,KAAK;KAEjF,aAAY,UAAW,gBAA4B;AAKvD,EAAA,GAAA,MAAA,iBAAgB;EACd,MAAM,WAAW,YAAY;AAC7B,MAAI,CAAC,SAAU;AACf,aAAW,UAAU;AACrB,MAAI,eAAA,gBAAgB,SAAS,CAC3B,UAAS,MAAM;AAEjB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS;MAEnB,EAAE;;IAGN,QAAQ,EAAE,CAAC;AAGd,KAAI,eAAA,eAAe,YAAY,QAAQ,CAErC,QAAO,CADO,qBAAA,YAAY,YAAY,QAAsC,EAC7D,YAAY,QAAQ;AAIrC,KAAI,eAAA,gBAAgB,YAAY,QAAQ,EAAE;AACxC,6BAAA,iBAAiB,YAAY,QAAQ;AACrC,SAAO,YAAY;;AAGrB,QAAO,YAAY"}
@@ -31,6 +31,7 @@ function useLocal(classOrFactory, ...rest) {
31
31
  else instanceRef.current = classOrFactory();
32
32
  useEffect(() => {
33
33
  const instance = instanceRef.current;
34
+ if (!instance) return;
34
35
  mountedRef.current = true;
35
36
  if (isInitializable(instance)) instance.init();
36
37
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"use-local.js","names":[],"sources":["../../src/react/use-local.ts"],"sourcesContent":["import { useRef, useEffect } from 'react';\nimport type { DependencyList } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { isSubscribable, isSubscribeOnly, isInitializable } from './guards';\nimport { useInstance } from './use-instance';\nimport { useSubscribeOnly } from './use-subscribe-only';\nimport type { StateOf } from './types';\n\nfunction depsChanged(prev: DependencyList | undefined, next: DependencyList): boolean {\n if (prev === undefined) return false;\n if (prev.length !== next.length) return true;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return true;\n }\n return false;\n}\n\n// ── With deps (class + initialState + deps) ────────────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<any> & Disposable>(\n Class: new (initialState: StateOf<T>) => T,\n initialState: StateOf<T>,\n deps: DependencyList,\n): [StateOf<T>, T];\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T,\n deps: DependencyList,\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(\n factory: () => T,\n deps: DependencyList,\n): T;\n\n// ── Without deps (existing overloads, unchanged) ───────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(factory: () => T): T;\n\n// ── Implementation ─────────────────────────────────────────────────\n\nexport function useLocal<T extends Disposable, S = StateOf<T>>(\n classOrFactory: (new (...args: unknown[]) => T) | (() => T),\n ...rest: unknown[]\n): [S, T] | T {\n // ── Detect deps: last arg is an array → treat as deps ──\n let args: unknown[];\n let deps: DependencyList | undefined;\n\n if (rest.length > 0 && Array.isArray(rest[rest.length - 1])) {\n deps = rest[rest.length - 1] as DependencyList;\n args = rest.slice(0, -1);\n } else {\n args = rest;\n deps = undefined;\n }\n\n const instanceRef = useRef<T | null>(null);\n const mountedRef = useRef(false);\n const prevDepsRef = useRef<DependencyList | undefined>(undefined);\n\n // ── Render phase: dep-change detection ──\n if (deps !== undefined && depsChanged(prevDepsRef.current, deps)) {\n instanceRef.current?.dispose();\n instanceRef.current = null;\n }\n if (deps !== undefined) {\n prevDepsRef.current = deps;\n }\n\n // ── Create instance if needed ──\n if (!instanceRef.current || instanceRef.current.disposed) {\n const isClass =\n typeof classOrFactory === 'function' &&\n classOrFactory.prototype &&\n classOrFactory.prototype.constructor === classOrFactory;\n\n if (isClass) {\n instanceRef.current = new (classOrFactory as new (...a: unknown[]) => T)(...args);\n } else {\n instanceRef.current = (classOrFactory as () => T)();\n }\n }\n\n // ── Effect: init + deferred cleanup ──\n useEffect(() => {\n const instance = instanceRef.current!; // capture for cleanup closure\n mountedRef.current = true;\n if (isInitializable(instance)) {\n instance.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n instance.dispose();\n }\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps ?? []);\n\n // ── Subscribe to state if Subscribable ──\n if (isSubscribable(instanceRef.current)) {\n const state = useInstance(instanceRef.current as unknown as Subscribable<S>);\n return [state, instanceRef.current];\n }\n\n // ── Subscribe for re-renders if subscribe-only (e.g., Trackable) ──\n if (isSubscribeOnly(instanceRef.current)) {\n useSubscribeOnly(instanceRef.current);\n return instanceRef.current;\n }\n\n return instanceRef.current;\n}\n"],"mappings":";;;;;AAQA,SAAS,YAAY,MAAkC,MAA+B;AACpF,KAAI,SAAS,KAAA,EAAW,QAAO;AAC/B,KAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAC/B,KAAI,CAAC,OAAO,GAAG,KAAK,IAAI,KAAK,GAAG,CAAE,QAAO;AAE3C,QAAO;;AA4ET,SAAgB,SACd,gBACA,GAAG,MACS;CAEZ,IAAI;CACJ,IAAI;AAEJ,KAAI,KAAK,SAAS,KAAK,MAAM,QAAQ,KAAK,KAAK,SAAS,GAAG,EAAE;AAC3D,SAAO,KAAK,KAAK,SAAS;AAC1B,SAAO,KAAK,MAAM,GAAG,GAAG;QACnB;AACL,SAAO;AACP,SAAO,KAAA;;CAGT,MAAM,cAAc,OAAiB,KAAK;CAC1C,MAAM,aAAa,OAAO,MAAM;CAChC,MAAM,cAAc,OAAmC,KAAA,EAAU;AAGjE,KAAI,SAAS,KAAA,KAAa,YAAY,YAAY,SAAS,KAAK,EAAE;AAChE,cAAY,SAAS,SAAS;AAC9B,cAAY,UAAU;;AAExB,KAAI,SAAS,KAAA,EACX,aAAY,UAAU;AAIxB,KAAI,CAAC,YAAY,WAAW,YAAY,QAAQ,SAM9C,KAJE,OAAO,mBAAmB,cAC1B,eAAe,aACf,eAAe,UAAU,gBAAgB,eAGzC,aAAY,UAAU,IAAK,eAA8C,GAAG,KAAK;KAEjF,aAAY,UAAW,gBAA4B;AAKvD,iBAAgB;EACd,MAAM,WAAW,YAAY;AAC7B,aAAW,UAAU;AACrB,MAAI,gBAAgB,SAAS,CAC3B,UAAS,MAAM;AAEjB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS;MAEnB,EAAE;;IAGN,QAAQ,EAAE,CAAC;AAGd,KAAI,eAAe,YAAY,QAAQ,CAErC,QAAO,CADO,YAAY,YAAY,QAAsC,EAC7D,YAAY,QAAQ;AAIrC,KAAI,gBAAgB,YAAY,QAAQ,EAAE;AACxC,mBAAiB,YAAY,QAAQ;AACrC,SAAO,YAAY;;AAGrB,QAAO,YAAY"}
1
+ {"version":3,"file":"use-local.js","names":[],"sources":["../../src/react/use-local.ts"],"sourcesContent":["import { useRef, useEffect } from 'react';\nimport type { DependencyList } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { isSubscribable, isSubscribeOnly, isInitializable } from './guards';\nimport { useInstance } from './use-instance';\nimport { useSubscribeOnly } from './use-subscribe-only';\nimport type { StateOf } from './types';\n\nfunction depsChanged(prev: DependencyList | undefined, next: DependencyList): boolean {\n if (prev === undefined) return false;\n if (prev.length !== next.length) return true;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return true;\n }\n return false;\n}\n\n// ── With deps (class + initialState + deps) ────────────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<any> & Disposable>(\n Class: new (initialState: StateOf<T>) => T,\n initialState: StateOf<T>,\n deps: DependencyList,\n): [StateOf<T>, T];\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T,\n deps: DependencyList,\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(\n factory: () => T,\n deps: DependencyList,\n): T;\n\n// ── Without deps (existing overloads, unchanged) ───────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T\n): [S, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(factory: () => T): T;\n\n// ── Implementation ─────────────────────────────────────────────────\n\nexport function useLocal<T extends Disposable, S = StateOf<T>>(\n classOrFactory: (new (...args: unknown[]) => T) | (() => T),\n ...rest: unknown[]\n): [S, T] | T {\n // ── Detect deps: last arg is an array → treat as deps ──\n let args: unknown[];\n let deps: DependencyList | undefined;\n\n if (rest.length > 0 && Array.isArray(rest[rest.length - 1])) {\n deps = rest[rest.length - 1] as DependencyList;\n args = rest.slice(0, -1);\n } else {\n args = rest;\n deps = undefined;\n }\n\n const instanceRef = useRef<T | null>(null);\n const mountedRef = useRef(false);\n const prevDepsRef = useRef<DependencyList | undefined>(undefined);\n\n // ── Render phase: dep-change detection ──\n if (deps !== undefined && depsChanged(prevDepsRef.current, deps)) {\n instanceRef.current?.dispose();\n instanceRef.current = null;\n }\n if (deps !== undefined) {\n prevDepsRef.current = deps;\n }\n\n // ── Create instance if needed ──\n if (!instanceRef.current || instanceRef.current.disposed) {\n const isClass =\n typeof classOrFactory === 'function' &&\n classOrFactory.prototype &&\n classOrFactory.prototype.constructor === classOrFactory;\n\n if (isClass) {\n instanceRef.current = new (classOrFactory as new (...a: unknown[]) => T)(...args);\n } else {\n instanceRef.current = (classOrFactory as () => T)();\n }\n }\n\n // ── Effect: init + deferred cleanup ──\n useEffect(() => {\n const instance = instanceRef.current;\n if (!instance) return;\n mountedRef.current = true;\n if (isInitializable(instance)) {\n instance.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n instance.dispose();\n }\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps ?? []);\n\n // ── Subscribe to state if Subscribable ──\n if (isSubscribable(instanceRef.current)) {\n const state = useInstance(instanceRef.current as unknown as Subscribable<S>);\n return [state, instanceRef.current];\n }\n\n // ── Subscribe for re-renders if subscribe-only (e.g., Trackable) ──\n if (isSubscribeOnly(instanceRef.current)) {\n useSubscribeOnly(instanceRef.current);\n return instanceRef.current;\n }\n\n return instanceRef.current;\n}\n"],"mappings":";;;;;AAQA,SAAS,YAAY,MAAkC,MAA+B;AACpF,KAAI,SAAS,KAAA,EAAW,QAAO;AAC/B,KAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAC/B,KAAI,CAAC,OAAO,GAAG,KAAK,IAAI,KAAK,GAAG,CAAE,QAAO;AAE3C,QAAO;;AA4ET,SAAgB,SACd,gBACA,GAAG,MACS;CAEZ,IAAI;CACJ,IAAI;AAEJ,KAAI,KAAK,SAAS,KAAK,MAAM,QAAQ,KAAK,KAAK,SAAS,GAAG,EAAE;AAC3D,SAAO,KAAK,KAAK,SAAS;AAC1B,SAAO,KAAK,MAAM,GAAG,GAAG;QACnB;AACL,SAAO;AACP,SAAO,KAAA;;CAGT,MAAM,cAAc,OAAiB,KAAK;CAC1C,MAAM,aAAa,OAAO,MAAM;CAChC,MAAM,cAAc,OAAmC,KAAA,EAAU;AAGjE,KAAI,SAAS,KAAA,KAAa,YAAY,YAAY,SAAS,KAAK,EAAE;AAChE,cAAY,SAAS,SAAS;AAC9B,cAAY,UAAU;;AAExB,KAAI,SAAS,KAAA,EACX,aAAY,UAAU;AAIxB,KAAI,CAAC,YAAY,WAAW,YAAY,QAAQ,SAM9C,KAJE,OAAO,mBAAmB,cAC1B,eAAe,aACf,eAAe,UAAU,gBAAgB,eAGzC,aAAY,UAAU,IAAK,eAA8C,GAAG,KAAK;KAEjF,aAAY,UAAW,gBAA4B;AAKvD,iBAAgB;EACd,MAAM,WAAW,YAAY;AAC7B,MAAI,CAAC,SAAU;AACf,aAAW,UAAU;AACrB,MAAI,gBAAgB,SAAS,CAC3B,UAAS,MAAM;AAEjB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS;MAEnB,EAAE;;IAGN,QAAQ,EAAE,CAAC;AAGd,KAAI,eAAe,YAAY,QAAQ,CAErC,QAAO,CADO,YAAY,YAAY,QAAsC,EAC7D,YAAY,QAAQ;AAIrC,KAAI,gBAAgB,YAAY,QAAQ,EAAE;AACxC,mBAAiB,YAAY,QAAQ;AACrC,SAAO,YAAY;;AAGrB,QAAO,YAAY"}
@@ -1,19 +1,38 @@
1
1
  const require_guards = require("./guards.cjs");
2
2
  let react = require("react");
3
3
  //#region src/react/use-model.ts
4
+ var __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
5
+ function assertModelExists(model, hookName) {
6
+ if (model) return;
7
+ throw new Error(`${hookName}: received an undefined/null model. This usually means the component rendered before the ViewModel finished creating the model. Either (a) create the model in the ViewModel constructor when the initial data is available (so it exists from the first render), or (b) if the model is created in onInit() after an async fetch, guard the component render with \`if (!vm.model) return <Spinner />\` before calling this hook. Do not use \`public model!: FormModel\` — the \`!\` lies to TypeScript about runtime state.`);
8
+ }
4
9
  /**
5
10
  * Bind to a component-scoped Model with validation and dirty state exposed.
6
11
  */
7
12
  function useModel(factory) {
8
13
  const modelRef = (0, react.useRef)(null);
9
14
  const mountedRef = (0, react.useRef)(false);
10
- if (!modelRef.current || modelRef.current.disposed) modelRef.current = factory();
11
- const modelSubscribe = (0, react.useCallback)((onStoreChange) => modelRef.current.subscribe(onStoreChange), []);
12
- const modelSnapshot = (0, react.useCallback)(() => modelRef.current.state, []);
15
+ let model = modelRef.current;
16
+ if (!model || model.disposed) {
17
+ model = factory();
18
+ if (__DEV__) assertModelExists(model, "useModel");
19
+ modelRef.current = model;
20
+ }
21
+ const modelSubscribe = (0, react.useCallback)((onStoreChange) => {
22
+ const m = modelRef.current;
23
+ return m ? m.subscribe(onStoreChange) : () => {};
24
+ }, []);
25
+ const modelSnapshot = (0, react.useCallback)(() => {
26
+ const m = modelRef.current;
27
+ if (!m) throw new Error("useModel: model accessed before creation");
28
+ return m.state;
29
+ }, []);
13
30
  (0, react.useSyncExternalStore)(modelSubscribe, modelSnapshot, modelSnapshot);
14
31
  (0, react.useEffect)(() => {
32
+ const m = modelRef.current;
33
+ if (!m) return;
15
34
  mountedRef.current = true;
16
- if (require_guards.isInitializable(modelRef.current)) modelRef.current.init();
35
+ if (require_guards.isInitializable(m)) m.init();
17
36
  return () => {
18
37
  mountedRef.current = false;
19
38
  setTimeout(() => {
@@ -21,7 +40,6 @@ function useModel(factory) {
21
40
  }, 0);
22
41
  };
23
42
  }, []);
24
- const model = modelRef.current;
25
43
  return {
26
44
  state: model.state,
27
45
  errors: model.errors,
@@ -41,10 +59,17 @@ function useModel(factory) {
41
59
  function useModelRef(factory) {
42
60
  const modelRef = (0, react.useRef)(null);
43
61
  const mountedRef = (0, react.useRef)(false);
44
- if (!modelRef.current || modelRef.current.disposed) modelRef.current = factory();
62
+ let model = modelRef.current;
63
+ if (!model || model.disposed) {
64
+ model = factory();
65
+ if (__DEV__) assertModelExists(model, "useModelRef");
66
+ modelRef.current = model;
67
+ }
45
68
  (0, react.useEffect)(() => {
69
+ const m = modelRef.current;
70
+ if (!m) return;
46
71
  mountedRef.current = true;
47
- if (require_guards.isInitializable(modelRef.current)) modelRef.current.init();
72
+ if (require_guards.isInitializable(m)) m.init();
48
73
  return () => {
49
74
  mountedRef.current = false;
50
75
  setTimeout(() => {
@@ -52,12 +77,13 @@ function useModelRef(factory) {
52
77
  }, 0);
53
78
  };
54
79
  }, []);
55
- return modelRef.current;
80
+ return model;
56
81
  }
57
82
  /**
58
83
  * Bind to a single Model field with surgical re-renders.
59
84
  */
60
85
  function useField(model, field) {
86
+ if (__DEV__) assertModelExists(model, "useField");
61
87
  const getSnapshot = (0, react.useCallback)(() => {
62
88
  return {
63
89
  value: model.state[field],
@@ -1 +1 @@
1
- {"version":3,"file":"use-model.cjs","names":[],"sources":["../../src/react/use-model.ts"],"sourcesContent":["import { useRef, useEffect, useSyncExternalStore, useCallback } from 'react';\nimport type { Model } from '../Model';\nimport type { ValidationErrors } from '../types';\nimport type { StateOf } from './types';\nimport { isInitializable } from './guards';\n\n/** Return type of `useModel`, providing state, validation, and model access. */\nexport interface ModelHandle<S extends object, M extends Model<S>> {\n state: S;\n errors: ValidationErrors<S>;\n valid: boolean;\n dirty: boolean;\n model: M;\n}\n\n/**\n * Bind to a component-scoped Model with validation and dirty state exposed.\n */\nexport function useModel<M extends Model<any>>(\n factory: () => M\n): ModelHandle<StateOf<M>, M> {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n if (!modelRef.current || modelRef.current.disposed) {\n modelRef.current = factory();\n }\n\n const modelSubscribe = useCallback(\n (onStoreChange: () => void) => modelRef.current!.subscribe(onStoreChange),\n []\n );\n const modelSnapshot = useCallback(\n () => modelRef.current!.state,\n []\n );\n useSyncExternalStore(modelSubscribe, modelSnapshot, modelSnapshot);\n\n useEffect(() => {\n mountedRef.current = true;\n if (isInitializable(modelRef.current)) {\n modelRef.current.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n const model = modelRef.current;\n\n return {\n state: model.state,\n errors: model.errors,\n valid: model.valid,\n dirty: model.dirty,\n model,\n };\n}\n\n/**\n * Create a component-scoped Model with lifecycle management (init + dispose)\n * but NO state subscription. The parent component never re-renders from\n * model state changes.\n *\n * Designed for the per-field isolation pattern: parent creates the model\n * via `useModelRef`, children subscribe to individual fields via `useField`.\n */\nexport function useModelRef<M extends Model<any>>(factory: () => M): M {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n if (!modelRef.current || modelRef.current.disposed) {\n modelRef.current = factory();\n }\n\n useEffect(() => {\n mountedRef.current = true;\n if (isInitializable(modelRef.current)) {\n modelRef.current.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n return modelRef.current;\n}\n\n/** Return type of `useField`, providing a single field's value, error, and setter. */\nexport interface FieldHandle<V> {\n value: V;\n error: string | undefined;\n set: (value: V) => void;\n}\n\n/**\n * Bind to a single Model field with surgical re-renders.\n */\nexport function useField<S extends object, K extends keyof S>(\n model: Model<S>,\n field: K\n): FieldHandle<S[K]> {\n // Track the field value and error for comparison\n const getSnapshot = useCallback(() => {\n return {\n value: model.state[field],\n error: model.errors[field],\n };\n }, [model, field]);\n\n // Use object comparison for subscription\n const cachedRef = useRef(getSnapshot());\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n return model.subscribe(() => {\n const next = getSnapshot();\n const current = cachedRef.current;\n\n // Only trigger re-render if field value or error changed\n if (next.value !== current.value || next.error !== current.error) {\n cachedRef.current = next;\n onStoreChange();\n }\n });\n },\n [model, getSnapshot]\n );\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => cachedRef.current,\n () => cachedRef.current\n );\n\n const set = useCallback(\n (value: S[K]) => {\n // Access the protected set method through type assertion\n // The Model subclass should expose a setter method\n const partial: Partial<S> = { [field]: value } as unknown as Partial<S>;\n (model as unknown as { set: (partial: Partial<S>) => void }).set(partial);\n },\n [model, field]\n );\n\n return {\n value: snapshot.value,\n error: snapshot.error,\n set,\n };\n}\n"],"mappings":";;;;;;AAkBA,SAAgB,SACd,SAC4B;CAC5B,MAAM,YAAA,GAAA,MAAA,QAA4B,KAAK;CACvC,MAAM,cAAA,GAAA,MAAA,QAAoB,MAAM;AAEhC,KAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,SACxC,UAAS,UAAU,SAAS;CAG9B,MAAM,kBAAA,GAAA,MAAA,cACH,kBAA8B,SAAS,QAAS,UAAU,cAAc,EACzE,EAAE,CACH;CACD,MAAM,iBAAA,GAAA,MAAA,mBACE,SAAS,QAAS,OACxB,EAAE,CACH;AACD,EAAA,GAAA,MAAA,sBAAqB,gBAAgB,eAAe,cAAc;AAElE,EAAA,GAAA,MAAA,iBAAgB;AACd,aAAW,UAAU;AACrB,MAAI,eAAA,gBAAgB,SAAS,QAAQ,CACnC,UAAS,QAAQ,MAAM;AAEzB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS,SAAS;MAE5B,EAAE;;IAEN,EAAE,CAAC;CAEN,MAAM,QAAQ,SAAS;AAEvB,QAAO;EACL,OAAO,MAAM;EACb,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,OAAO,MAAM;EACb;EACD;;;;;;;;;;AAWH,SAAgB,YAAkC,SAAqB;CACrE,MAAM,YAAA,GAAA,MAAA,QAA4B,KAAK;CACvC,MAAM,cAAA,GAAA,MAAA,QAAoB,MAAM;AAEhC,KAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,SACxC,UAAS,UAAU,SAAS;AAG9B,EAAA,GAAA,MAAA,iBAAgB;AACd,aAAW,UAAU;AACrB,MAAI,eAAA,gBAAgB,SAAS,QAAQ,CACnC,UAAS,QAAQ,MAAM;AAEzB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS,SAAS;MAE5B,EAAE;;IAEN,EAAE,CAAC;AAEN,QAAO,SAAS;;;;;AAalB,SAAgB,SACd,OACA,OACmB;CAEnB,MAAM,eAAA,GAAA,MAAA,mBAAgC;AACpC,SAAO;GACL,OAAO,MAAM,MAAM;GACnB,OAAO,MAAM,OAAO;GACrB;IACA,CAAC,OAAO,MAAM,CAAC;CAGlB,MAAM,aAAA,GAAA,MAAA,QAAmB,aAAa,CAAC;CAkBvC,MAAM,YAAA,GAAA,MAAA,uBAAA,GAAA,MAAA,cAfH,kBAA8B;AAC7B,SAAO,MAAM,gBAAgB;GAC3B,MAAM,OAAO,aAAa;GAC1B,MAAM,UAAU,UAAU;AAG1B,OAAI,KAAK,UAAU,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO;AAChE,cAAU,UAAU;AACpB,mBAAe;;IAEjB;IAEJ,CAAC,OAAO,YAAY,CACrB,QAIO,UAAU,eACV,UAAU,QACjB;CAED,MAAM,OAAA,GAAA,MAAA,cACH,UAAgB;EAGf,MAAM,UAAsB,GAAG,QAAQ,OAAO;AAC7C,QAA4D,IAAI,QAAQ;IAE3E,CAAC,OAAO,MAAM,CACf;AAED,QAAO;EACL,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB;EACD"}
1
+ {"version":3,"file":"use-model.cjs","names":[],"sources":["../../src/react/use-model.ts"],"sourcesContent":["import { useRef, useEffect, useSyncExternalStore, useCallback } from 'react';\nimport type { Model } from '../Model';\nimport type { ValidationErrors } from '../types';\nimport type { StateOf } from './types';\nimport { isInitializable } from './guards';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nfunction assertModelExists(\n model: unknown,\n hookName: 'useModel' | 'useModelRef' | 'useField',\n): asserts model {\n if (model) return;\n throw new Error(\n `${hookName}: received an undefined/null model. ` +\n 'This usually means the component rendered before the ViewModel finished creating the model. ' +\n 'Either (a) create the model in the ViewModel constructor when the initial data is available ' +\n '(so it exists from the first render), or (b) if the model is created in onInit() after an async ' +\n 'fetch, guard the component render with `if (!vm.model) return <Spinner />` before calling this hook. ' +\n 'Do not use `public model!: FormModel` — the `!` lies to TypeScript about runtime state.',\n );\n}\n\n/** Return type of `useModel`, providing state, validation, and model access. */\nexport interface ModelHandle<S extends object, M extends Model<S>> {\n state: S;\n errors: ValidationErrors<S>;\n valid: boolean;\n dirty: boolean;\n model: M;\n}\n\n/**\n * Bind to a component-scoped Model with validation and dirty state exposed.\n */\nexport function useModel<M extends Model<any>>(\n factory: () => M\n): ModelHandle<StateOf<M>, M> {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n let model = modelRef.current;\n if (!model || model.disposed) {\n model = factory();\n if (__DEV__) assertModelExists(model, 'useModel');\n modelRef.current = model;\n }\n\n const modelSubscribe = useCallback(\n (onStoreChange: () => void) => {\n const m = modelRef.current;\n return m ? m.subscribe(onStoreChange) : () => {};\n },\n []\n );\n const modelSnapshot = useCallback(\n () => {\n const m = modelRef.current;\n if (!m) throw new Error('useModel: model accessed before creation');\n return m.state;\n },\n []\n );\n useSyncExternalStore(modelSubscribe, modelSnapshot, modelSnapshot);\n\n useEffect(() => {\n const m = modelRef.current;\n if (!m) return;\n mountedRef.current = true;\n if (isInitializable(m)) m.init();\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n return {\n state: model.state,\n errors: model.errors,\n valid: model.valid,\n dirty: model.dirty,\n model,\n };\n}\n\n/**\n * Create a component-scoped Model with lifecycle management (init + dispose)\n * but NO state subscription. The parent component never re-renders from\n * model state changes.\n *\n * Designed for the per-field isolation pattern: parent creates the model\n * via `useModelRef`, children subscribe to individual fields via `useField`.\n */\nexport function useModelRef<M extends Model<any>>(factory: () => M): M {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n let model = modelRef.current;\n if (!model || model.disposed) {\n model = factory();\n if (__DEV__) assertModelExists(model, 'useModelRef');\n modelRef.current = model;\n }\n\n useEffect(() => {\n const m = modelRef.current;\n if (!m) return;\n mountedRef.current = true;\n if (isInitializable(m)) m.init();\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n return model;\n}\n\n/** Return type of `useField`, providing a single field's value, error, and setter. */\nexport interface FieldHandle<V> {\n value: V;\n error: string | undefined;\n set: (value: V) => void;\n}\n\n/**\n * Bind to a single Model field with surgical re-renders.\n */\nexport function useField<S extends object, K extends keyof S>(\n model: Model<S>,\n field: K\n): FieldHandle<S[K]> {\n if (__DEV__) assertModelExists(model, 'useField');\n // Track the field value and error for comparison\n const getSnapshot = useCallback(() => {\n return {\n value: model.state[field],\n error: model.errors[field],\n };\n }, [model, field]);\n\n // Use object comparison for subscription\n const cachedRef = useRef(getSnapshot());\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n return model.subscribe(() => {\n const next = getSnapshot();\n const current = cachedRef.current;\n\n // Only trigger re-render if field value or error changed\n if (next.value !== current.value || next.error !== current.error) {\n cachedRef.current = next;\n onStoreChange();\n }\n });\n },\n [model, getSnapshot]\n );\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => cachedRef.current,\n () => cachedRef.current\n );\n\n const set = useCallback(\n (value: S[K]) => {\n // Access the protected set method through type assertion\n // The Model subclass should expose a setter method\n const partial: Partial<S> = { [field]: value } as unknown as Partial<S>;\n (model as unknown as { set: (partial: Partial<S>) => void }).set(partial);\n },\n [model, field]\n );\n\n return {\n value: snapshot.value,\n error: snapshot.error,\n set,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-model.d.ts","sourceRoot":"","sources":["../../src/react/use-model.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGvC,gFAAgF;AAChF,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC;IAC/D,KAAK,EAAE,CAAC,CAAC;IACT,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,CAAC,CAAC;CACV;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,EAC3C,OAAO,EAAE,MAAM,CAAC,GACf,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CA0C5B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAwBrE;AAED,sFAAsF;AACtF,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,KAAK,EAAE,CAAC,CAAC;IACT,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAC1D,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EACf,KAAK,EAAE,CAAC,GACP,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAiDnB"}
1
+ {"version":3,"file":"use-model.d.ts","sourceRoot":"","sources":["../../src/react/use-model.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAoBvC,gFAAgF;AAChF,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC;IAC/D,KAAK,EAAE,CAAC,CAAC;IACT,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC5B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,CAAC,CAAC;CACV;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,EAC3C,OAAO,EAAE,MAAM,CAAC,GACf,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAkD5B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CA2BrE;AAED,sFAAsF;AACtF,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,KAAK,EAAE,CAAC,CAAC;IACT,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAC1D,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EACf,KAAK,EAAE,CAAC,GACP,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAkDnB"}
@@ -1,19 +1,38 @@
1
1
  import { isInitializable } from "./guards.js";
2
2
  import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
3
3
  //#region src/react/use-model.ts
4
+ var __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
5
+ function assertModelExists(model, hookName) {
6
+ if (model) return;
7
+ throw new Error(`${hookName}: received an undefined/null model. This usually means the component rendered before the ViewModel finished creating the model. Either (a) create the model in the ViewModel constructor when the initial data is available (so it exists from the first render), or (b) if the model is created in onInit() after an async fetch, guard the component render with \`if (!vm.model) return <Spinner />\` before calling this hook. Do not use \`public model!: FormModel\` — the \`!\` lies to TypeScript about runtime state.`);
8
+ }
4
9
  /**
5
10
  * Bind to a component-scoped Model with validation and dirty state exposed.
6
11
  */
7
12
  function useModel(factory) {
8
13
  const modelRef = useRef(null);
9
14
  const mountedRef = useRef(false);
10
- if (!modelRef.current || modelRef.current.disposed) modelRef.current = factory();
11
- const modelSubscribe = useCallback((onStoreChange) => modelRef.current.subscribe(onStoreChange), []);
12
- const modelSnapshot = useCallback(() => modelRef.current.state, []);
15
+ let model = modelRef.current;
16
+ if (!model || model.disposed) {
17
+ model = factory();
18
+ if (__DEV__) assertModelExists(model, "useModel");
19
+ modelRef.current = model;
20
+ }
21
+ const modelSubscribe = useCallback((onStoreChange) => {
22
+ const m = modelRef.current;
23
+ return m ? m.subscribe(onStoreChange) : () => {};
24
+ }, []);
25
+ const modelSnapshot = useCallback(() => {
26
+ const m = modelRef.current;
27
+ if (!m) throw new Error("useModel: model accessed before creation");
28
+ return m.state;
29
+ }, []);
13
30
  useSyncExternalStore(modelSubscribe, modelSnapshot, modelSnapshot);
14
31
  useEffect(() => {
32
+ const m = modelRef.current;
33
+ if (!m) return;
15
34
  mountedRef.current = true;
16
- if (isInitializable(modelRef.current)) modelRef.current.init();
35
+ if (isInitializable(m)) m.init();
17
36
  return () => {
18
37
  mountedRef.current = false;
19
38
  setTimeout(() => {
@@ -21,7 +40,6 @@ function useModel(factory) {
21
40
  }, 0);
22
41
  };
23
42
  }, []);
24
- const model = modelRef.current;
25
43
  return {
26
44
  state: model.state,
27
45
  errors: model.errors,
@@ -41,10 +59,17 @@ function useModel(factory) {
41
59
  function useModelRef(factory) {
42
60
  const modelRef = useRef(null);
43
61
  const mountedRef = useRef(false);
44
- if (!modelRef.current || modelRef.current.disposed) modelRef.current = factory();
62
+ let model = modelRef.current;
63
+ if (!model || model.disposed) {
64
+ model = factory();
65
+ if (__DEV__) assertModelExists(model, "useModelRef");
66
+ modelRef.current = model;
67
+ }
45
68
  useEffect(() => {
69
+ const m = modelRef.current;
70
+ if (!m) return;
46
71
  mountedRef.current = true;
47
- if (isInitializable(modelRef.current)) modelRef.current.init();
72
+ if (isInitializable(m)) m.init();
48
73
  return () => {
49
74
  mountedRef.current = false;
50
75
  setTimeout(() => {
@@ -52,12 +77,13 @@ function useModelRef(factory) {
52
77
  }, 0);
53
78
  };
54
79
  }, []);
55
- return modelRef.current;
80
+ return model;
56
81
  }
57
82
  /**
58
83
  * Bind to a single Model field with surgical re-renders.
59
84
  */
60
85
  function useField(model, field) {
86
+ if (__DEV__) assertModelExists(model, "useField");
61
87
  const getSnapshot = useCallback(() => {
62
88
  return {
63
89
  value: model.state[field],
@@ -1 +1 @@
1
- {"version":3,"file":"use-model.js","names":[],"sources":["../../src/react/use-model.ts"],"sourcesContent":["import { useRef, useEffect, useSyncExternalStore, useCallback } from 'react';\nimport type { Model } from '../Model';\nimport type { ValidationErrors } from '../types';\nimport type { StateOf } from './types';\nimport { isInitializable } from './guards';\n\n/** Return type of `useModel`, providing state, validation, and model access. */\nexport interface ModelHandle<S extends object, M extends Model<S>> {\n state: S;\n errors: ValidationErrors<S>;\n valid: boolean;\n dirty: boolean;\n model: M;\n}\n\n/**\n * Bind to a component-scoped Model with validation and dirty state exposed.\n */\nexport function useModel<M extends Model<any>>(\n factory: () => M\n): ModelHandle<StateOf<M>, M> {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n if (!modelRef.current || modelRef.current.disposed) {\n modelRef.current = factory();\n }\n\n const modelSubscribe = useCallback(\n (onStoreChange: () => void) => modelRef.current!.subscribe(onStoreChange),\n []\n );\n const modelSnapshot = useCallback(\n () => modelRef.current!.state,\n []\n );\n useSyncExternalStore(modelSubscribe, modelSnapshot, modelSnapshot);\n\n useEffect(() => {\n mountedRef.current = true;\n if (isInitializable(modelRef.current)) {\n modelRef.current.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n const model = modelRef.current;\n\n return {\n state: model.state,\n errors: model.errors,\n valid: model.valid,\n dirty: model.dirty,\n model,\n };\n}\n\n/**\n * Create a component-scoped Model with lifecycle management (init + dispose)\n * but NO state subscription. The parent component never re-renders from\n * model state changes.\n *\n * Designed for the per-field isolation pattern: parent creates the model\n * via `useModelRef`, children subscribe to individual fields via `useField`.\n */\nexport function useModelRef<M extends Model<any>>(factory: () => M): M {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n if (!modelRef.current || modelRef.current.disposed) {\n modelRef.current = factory();\n }\n\n useEffect(() => {\n mountedRef.current = true;\n if (isInitializable(modelRef.current)) {\n modelRef.current.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n return modelRef.current;\n}\n\n/** Return type of `useField`, providing a single field's value, error, and setter. */\nexport interface FieldHandle<V> {\n value: V;\n error: string | undefined;\n set: (value: V) => void;\n}\n\n/**\n * Bind to a single Model field with surgical re-renders.\n */\nexport function useField<S extends object, K extends keyof S>(\n model: Model<S>,\n field: K\n): FieldHandle<S[K]> {\n // Track the field value and error for comparison\n const getSnapshot = useCallback(() => {\n return {\n value: model.state[field],\n error: model.errors[field],\n };\n }, [model, field]);\n\n // Use object comparison for subscription\n const cachedRef = useRef(getSnapshot());\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n return model.subscribe(() => {\n const next = getSnapshot();\n const current = cachedRef.current;\n\n // Only trigger re-render if field value or error changed\n if (next.value !== current.value || next.error !== current.error) {\n cachedRef.current = next;\n onStoreChange();\n }\n });\n },\n [model, getSnapshot]\n );\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => cachedRef.current,\n () => cachedRef.current\n );\n\n const set = useCallback(\n (value: S[K]) => {\n // Access the protected set method through type assertion\n // The Model subclass should expose a setter method\n const partial: Partial<S> = { [field]: value } as unknown as Partial<S>;\n (model as unknown as { set: (partial: Partial<S>) => void }).set(partial);\n },\n [model, field]\n );\n\n return {\n value: snapshot.value,\n error: snapshot.error,\n set,\n };\n}\n"],"mappings":";;;;;;AAkBA,SAAgB,SACd,SAC4B;CAC5B,MAAM,WAAW,OAAiB,KAAK;CACvC,MAAM,aAAa,OAAO,MAAM;AAEhC,KAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,SACxC,UAAS,UAAU,SAAS;CAG9B,MAAM,iBAAiB,aACpB,kBAA8B,SAAS,QAAS,UAAU,cAAc,EACzE,EAAE,CACH;CACD,MAAM,gBAAgB,kBACd,SAAS,QAAS,OACxB,EAAE,CACH;AACD,sBAAqB,gBAAgB,eAAe,cAAc;AAElE,iBAAgB;AACd,aAAW,UAAU;AACrB,MAAI,gBAAgB,SAAS,QAAQ,CACnC,UAAS,QAAQ,MAAM;AAEzB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS,SAAS;MAE5B,EAAE;;IAEN,EAAE,CAAC;CAEN,MAAM,QAAQ,SAAS;AAEvB,QAAO;EACL,OAAO,MAAM;EACb,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,OAAO,MAAM;EACb;EACD;;;;;;;;;;AAWH,SAAgB,YAAkC,SAAqB;CACrE,MAAM,WAAW,OAAiB,KAAK;CACvC,MAAM,aAAa,OAAO,MAAM;AAEhC,KAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,SACxC,UAAS,UAAU,SAAS;AAG9B,iBAAgB;AACd,aAAW,UAAU;AACrB,MAAI,gBAAgB,SAAS,QAAQ,CACnC,UAAS,QAAQ,MAAM;AAEzB,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS,SAAS;MAE5B,EAAE;;IAEN,EAAE,CAAC;AAEN,QAAO,SAAS;;;;;AAalB,SAAgB,SACd,OACA,OACmB;CAEnB,MAAM,cAAc,kBAAkB;AACpC,SAAO;GACL,OAAO,MAAM,MAAM;GACnB,OAAO,MAAM,OAAO;GACrB;IACA,CAAC,OAAO,MAAM,CAAC;CAGlB,MAAM,YAAY,OAAO,aAAa,CAAC;CAkBvC,MAAM,WAAW,qBAhBC,aACf,kBAA8B;AAC7B,SAAO,MAAM,gBAAgB;GAC3B,MAAM,OAAO,aAAa;GAC1B,MAAM,UAAU,UAAU;AAG1B,OAAI,KAAK,UAAU,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO;AAChE,cAAU,UAAU;AACpB,mBAAe;;IAEjB;IAEJ,CAAC,OAAO,YAAY,CACrB,QAIO,UAAU,eACV,UAAU,QACjB;CAED,MAAM,MAAM,aACT,UAAgB;EAGf,MAAM,UAAsB,GAAG,QAAQ,OAAO;AAC7C,QAA4D,IAAI,QAAQ;IAE3E,CAAC,OAAO,MAAM,CACf;AAED,QAAO;EACL,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB;EACD"}
1
+ {"version":3,"file":"use-model.js","names":[],"sources":["../../src/react/use-model.ts"],"sourcesContent":["import { useRef, useEffect, useSyncExternalStore, useCallback } from 'react';\nimport type { Model } from '../Model';\nimport type { ValidationErrors } from '../types';\nimport type { StateOf } from './types';\nimport { isInitializable } from './guards';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nfunction assertModelExists(\n model: unknown,\n hookName: 'useModel' | 'useModelRef' | 'useField',\n): asserts model {\n if (model) return;\n throw new Error(\n `${hookName}: received an undefined/null model. ` +\n 'This usually means the component rendered before the ViewModel finished creating the model. ' +\n 'Either (a) create the model in the ViewModel constructor when the initial data is available ' +\n '(so it exists from the first render), or (b) if the model is created in onInit() after an async ' +\n 'fetch, guard the component render with `if (!vm.model) return <Spinner />` before calling this hook. ' +\n 'Do not use `public model!: FormModel` — the `!` lies to TypeScript about runtime state.',\n );\n}\n\n/** Return type of `useModel`, providing state, validation, and model access. */\nexport interface ModelHandle<S extends object, M extends Model<S>> {\n state: S;\n errors: ValidationErrors<S>;\n valid: boolean;\n dirty: boolean;\n model: M;\n}\n\n/**\n * Bind to a component-scoped Model with validation and dirty state exposed.\n */\nexport function useModel<M extends Model<any>>(\n factory: () => M\n): ModelHandle<StateOf<M>, M> {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n let model = modelRef.current;\n if (!model || model.disposed) {\n model = factory();\n if (__DEV__) assertModelExists(model, 'useModel');\n modelRef.current = model;\n }\n\n const modelSubscribe = useCallback(\n (onStoreChange: () => void) => {\n const m = modelRef.current;\n return m ? m.subscribe(onStoreChange) : () => {};\n },\n []\n );\n const modelSnapshot = useCallback(\n () => {\n const m = modelRef.current;\n if (!m) throw new Error('useModel: model accessed before creation');\n return m.state;\n },\n []\n );\n useSyncExternalStore(modelSubscribe, modelSnapshot, modelSnapshot);\n\n useEffect(() => {\n const m = modelRef.current;\n if (!m) return;\n mountedRef.current = true;\n if (isInitializable(m)) m.init();\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n return {\n state: model.state,\n errors: model.errors,\n valid: model.valid,\n dirty: model.dirty,\n model,\n };\n}\n\n/**\n * Create a component-scoped Model with lifecycle management (init + dispose)\n * but NO state subscription. The parent component never re-renders from\n * model state changes.\n *\n * Designed for the per-field isolation pattern: parent creates the model\n * via `useModelRef`, children subscribe to individual fields via `useField`.\n */\nexport function useModelRef<M extends Model<any>>(factory: () => M): M {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n let model = modelRef.current;\n if (!model || model.disposed) {\n model = factory();\n if (__DEV__) assertModelExists(model, 'useModelRef');\n modelRef.current = model;\n }\n\n useEffect(() => {\n const m = modelRef.current;\n if (!m) return;\n mountedRef.current = true;\n if (isInitializable(m)) m.init();\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n return model;\n}\n\n/** Return type of `useField`, providing a single field's value, error, and setter. */\nexport interface FieldHandle<V> {\n value: V;\n error: string | undefined;\n set: (value: V) => void;\n}\n\n/**\n * Bind to a single Model field with surgical re-renders.\n */\nexport function useField<S extends object, K extends keyof S>(\n model: Model<S>,\n field: K\n): FieldHandle<S[K]> {\n if (__DEV__) assertModelExists(model, 'useField');\n // Track the field value and error for comparison\n const getSnapshot = useCallback(() => {\n return {\n value: model.state[field],\n error: model.errors[field],\n };\n }, [model, field]);\n\n // Use object comparison for subscription\n const cachedRef = useRef(getSnapshot());\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n return model.subscribe(() => {\n const next = getSnapshot();\n const current = cachedRef.current;\n\n // Only trigger re-render if field value or error changed\n if (next.value !== current.value || next.error !== current.error) {\n cachedRef.current = next;\n onStoreChange();\n }\n });\n },\n [model, getSnapshot]\n );\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => cachedRef.current,\n () => cachedRef.current\n );\n\n const set = useCallback(\n (value: S[K]) => {\n // Access the protected set method through type assertion\n // The Model subclass should expose a setter method\n const partial: Partial<S> = { [field]: value } as unknown as Partial<S>;\n (model as unknown as { set: (partial: Partial<S>) => void }).set(partial);\n },\n [model, field]\n );\n\n return {\n value: snapshot.value,\n error: snapshot.error,\n set,\n };\n}\n"],"mappings":";;;AAMA,IAAM,UAAU,OAAO,oBAAoB,eAAe;AAE1D,SAAS,kBACP,OACA,UACe;AACf,KAAI,MAAO;AACX,OAAM,IAAI,MACR,GAAG,SAAS,ggBAMb;;;;;AAeH,SAAgB,SACd,SAC4B;CAC5B,MAAM,WAAW,OAAiB,KAAK;CACvC,MAAM,aAAa,OAAO,MAAM;CAEhC,IAAI,QAAQ,SAAS;AACrB,KAAI,CAAC,SAAS,MAAM,UAAU;AAC5B,UAAQ,SAAS;AACjB,MAAI,QAAS,mBAAkB,OAAO,WAAW;AACjD,WAAS,UAAU;;CAGrB,MAAM,iBAAiB,aACpB,kBAA8B;EAC7B,MAAM,IAAI,SAAS;AACnB,SAAO,IAAI,EAAE,UAAU,cAAc,SAAS;IAEhD,EAAE,CACH;CACD,MAAM,gBAAgB,kBACd;EACJ,MAAM,IAAI,SAAS;AACnB,MAAI,CAAC,EAAG,OAAM,IAAI,MAAM,2CAA2C;AACnE,SAAO,EAAE;IAEX,EAAE,CACH;AACD,sBAAqB,gBAAgB,eAAe,cAAc;AAElE,iBAAgB;EACd,MAAM,IAAI,SAAS;AACnB,MAAI,CAAC,EAAG;AACR,aAAW,UAAU;AACrB,MAAI,gBAAgB,EAAE,CAAE,GAAE,MAAM;AAChC,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS,SAAS;MAE5B,EAAE;;IAEN,EAAE,CAAC;AAEN,QAAO;EACL,OAAO,MAAM;EACb,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,OAAO,MAAM;EACb;EACD;;;;;;;;;;AAWH,SAAgB,YAAkC,SAAqB;CACrE,MAAM,WAAW,OAAiB,KAAK;CACvC,MAAM,aAAa,OAAO,MAAM;CAEhC,IAAI,QAAQ,SAAS;AACrB,KAAI,CAAC,SAAS,MAAM,UAAU;AAC5B,UAAQ,SAAS;AACjB,MAAI,QAAS,mBAAkB,OAAO,cAAc;AACpD,WAAS,UAAU;;AAGrB,iBAAgB;EACd,MAAM,IAAI,SAAS;AACnB,MAAI,CAAC,EAAG;AACR,aAAW,UAAU;AACrB,MAAI,gBAAgB,EAAE,CAAE,GAAE,MAAM;AAChC,eAAa;AACX,cAAW,UAAU;AACrB,oBAAiB;AACf,QAAI,CAAC,WAAW,QACd,UAAS,SAAS,SAAS;MAE5B,EAAE;;IAEN,EAAE,CAAC;AAEN,QAAO;;;;;AAaT,SAAgB,SACd,OACA,OACmB;AACnB,KAAI,QAAS,mBAAkB,OAAO,WAAW;CAEjD,MAAM,cAAc,kBAAkB;AACpC,SAAO;GACL,OAAO,MAAM,MAAM;GACnB,OAAO,MAAM,OAAO;GACrB;IACA,CAAC,OAAO,MAAM,CAAC;CAGlB,MAAM,YAAY,OAAO,aAAa,CAAC;CAkBvC,MAAM,WAAW,qBAhBC,aACf,kBAA8B;AAC7B,SAAO,MAAM,gBAAgB;GAC3B,MAAM,OAAO,aAAa;GAC1B,MAAM,UAAU,UAAU;AAG1B,OAAI,KAAK,UAAU,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO;AAChE,cAAU,UAAU;AACpB,mBAAe;;IAEjB;IAEJ,CAAC,OAAO,YAAY,CACrB,QAIO,UAAU,eACV,UAAU,QACjB;CAED,MAAM,MAAM,aACT,UAAgB;EAGf,MAAM,UAAsB,GAAG,QAAQ,OAAO;AAC7C,QAA4D,IAAI,QAAQ;IAE3E,CAAC,OAAO,MAAM,CACf;AAED,QAAO;EACL,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB;EACD"}
@@ -10,18 +10,19 @@ function useSubscribeOnly(target) {
10
10
  const ref = (0, react.useRef)(null);
11
11
  if (!ref.current || ref.current.target !== target) {
12
12
  const version = { current: ref.current?.version ?? 0 };
13
- ref.current = {
13
+ const entry = {
14
14
  target,
15
15
  version: version.current,
16
16
  subscribe: (onStoreChange) => {
17
17
  return target.subscribe(() => {
18
18
  version.current++;
19
- ref.current.version = version.current;
19
+ entry.version = version.current;
20
20
  onStoreChange();
21
21
  });
22
22
  },
23
23
  getSnapshot: () => version.current
24
24
  };
25
+ ref.current = entry;
25
26
  }
26
27
  (0, react.useSyncExternalStore)(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);
27
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"use-subscribe-only.cjs","names":[],"sources":["../../src/react/use-subscribe-only.ts"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\n\ninterface SubscribeOnlyRef {\n target: { subscribe(cb: () => void): () => void };\n version: number;\n subscribe: (onStoreChange: () => void) => () => void;\n getSnapshot: () => number;\n}\n\nconst SERVER_SNAPSHOT = () => 0;\n\n/**\n * Subscribe to a notification-only object (has subscribe() but no state).\n * Triggers React re-renders via version counter when the target notifies.\n * @internal\n */\nexport function useSubscribeOnly(\n target: { subscribe(cb: () => void): () => void },\n): void {\n const ref = useRef<SubscribeOnlyRef | null>(null);\n\n if (!ref.current || ref.current.target !== target) {\n const version = { current: ref.current?.version ?? 0 };\n ref.current = {\n target,\n version: version.current,\n subscribe: (onStoreChange: () => void) => {\n return target.subscribe(() => {\n version.current++;\n ref.current!.version = version.current;\n onStoreChange();\n });\n },\n getSnapshot: () => version.current,\n };\n }\n\n useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);\n}\n"],"mappings":";;AASA,IAAM,wBAAwB;;;;;;AAO9B,SAAgB,iBACd,QACM;CACN,MAAM,OAAA,GAAA,MAAA,QAAsC,KAAK;AAEjD,KAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,WAAW,QAAQ;EACjD,MAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,GAAG;AACtD,MAAI,UAAU;GACZ;GACA,SAAS,QAAQ;GACjB,YAAY,kBAA8B;AACxC,WAAO,OAAO,gBAAgB;AAC5B,aAAQ;AACR,SAAI,QAAS,UAAU,QAAQ;AAC/B,oBAAe;MACf;;GAEJ,mBAAmB,QAAQ;GAC5B;;AAGH,EAAA,GAAA,MAAA,sBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,gBAAgB"}
1
+ {"version":3,"file":"use-subscribe-only.cjs","names":[],"sources":["../../src/react/use-subscribe-only.ts"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\n\ninterface SubscribeOnlyRef {\n target: { subscribe(cb: () => void): () => void };\n version: number;\n subscribe: (onStoreChange: () => void) => () => void;\n getSnapshot: () => number;\n}\n\nconst SERVER_SNAPSHOT = () => 0;\n\n/**\n * Subscribe to a notification-only object (has subscribe() but no state).\n * Triggers React re-renders via version counter when the target notifies.\n * @internal\n */\nexport function useSubscribeOnly(\n target: { subscribe(cb: () => void): () => void },\n): void {\n const ref = useRef<SubscribeOnlyRef | null>(null);\n\n if (!ref.current || ref.current.target !== target) {\n const version = { current: ref.current?.version ?? 0 };\n const entry: SubscribeOnlyRef = {\n target,\n version: version.current,\n subscribe: (onStoreChange: () => void) => {\n return target.subscribe(() => {\n version.current++;\n entry.version = version.current;\n onStoreChange();\n });\n },\n getSnapshot: () => version.current,\n };\n ref.current = entry;\n }\n\n useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);\n}\n"],"mappings":";;AASA,IAAM,wBAAwB;;;;;;AAO9B,SAAgB,iBACd,QACM;CACN,MAAM,OAAA,GAAA,MAAA,QAAsC,KAAK;AAEjD,KAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,WAAW,QAAQ;EACjD,MAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,GAAG;EACtD,MAAM,QAA0B;GAC9B;GACA,SAAS,QAAQ;GACjB,YAAY,kBAA8B;AACxC,WAAO,OAAO,gBAAgB;AAC5B,aAAQ;AACR,WAAM,UAAU,QAAQ;AACxB,oBAAe;MACf;;GAEJ,mBAAmB,QAAQ;GAC5B;AACD,MAAI,UAAU;;AAGhB,EAAA,GAAA,MAAA,sBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,gBAAgB"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-subscribe-only.d.ts","sourceRoot":"","sources":["../../src/react/use-subscribe-only.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE;IAAE,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAAE,GAChD,IAAI,CAoBN"}
1
+ {"version":3,"file":"use-subscribe-only.d.ts","sourceRoot":"","sources":["../../src/react/use-subscribe-only.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE;IAAE,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;CAAE,GAChD,IAAI,CAqBN"}
@@ -10,18 +10,19 @@ function useSubscribeOnly(target) {
10
10
  const ref = useRef(null);
11
11
  if (!ref.current || ref.current.target !== target) {
12
12
  const version = { current: ref.current?.version ?? 0 };
13
- ref.current = {
13
+ const entry = {
14
14
  target,
15
15
  version: version.current,
16
16
  subscribe: (onStoreChange) => {
17
17
  return target.subscribe(() => {
18
18
  version.current++;
19
- ref.current.version = version.current;
19
+ entry.version = version.current;
20
20
  onStoreChange();
21
21
  });
22
22
  },
23
23
  getSnapshot: () => version.current
24
24
  };
25
+ ref.current = entry;
25
26
  }
26
27
  useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);
27
28
  }