mvc-kit 2.7.0 → 2.8.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 (62) hide show
  1. package/README.md +18 -1
  2. package/agent-config/claude-code/skills/guide/SKILL.md +1 -0
  3. package/agent-config/claude-code/skills/guide/api-reference.md +8 -1
  4. package/agent-config/claude-code/skills/scaffold/templates/model.md +38 -1
  5. package/agent-config/copilot/copilot-instructions.md +2 -1
  6. package/agent-config/cursor/cursorrules +2 -1
  7. package/dist/Collection.cjs +31 -17
  8. package/dist/Collection.cjs.map +1 -1
  9. package/dist/Collection.d.ts.map +1 -1
  10. package/dist/Collection.js +31 -17
  11. package/dist/Collection.js.map +1 -1
  12. package/dist/Model.cjs +22 -4
  13. package/dist/Model.cjs.map +1 -1
  14. package/dist/Model.d.ts +2 -0
  15. package/dist/Model.d.ts.map +1 -1
  16. package/dist/Model.js +22 -4
  17. package/dist/Model.js.map +1 -1
  18. package/dist/PersistentCollection.cjs +8 -10
  19. package/dist/PersistentCollection.cjs.map +1 -1
  20. package/dist/PersistentCollection.d.ts +1 -0
  21. package/dist/PersistentCollection.d.ts.map +1 -1
  22. package/dist/PersistentCollection.js +8 -10
  23. package/dist/PersistentCollection.js.map +1 -1
  24. package/dist/Resource.cjs +21 -157
  25. package/dist/Resource.cjs.map +1 -1
  26. package/dist/Resource.d.ts +1 -3
  27. package/dist/Resource.d.ts.map +1 -1
  28. package/dist/Resource.js +21 -157
  29. package/dist/Resource.js.map +1 -1
  30. package/dist/ViewModel.cjs +178 -228
  31. package/dist/ViewModel.cjs.map +1 -1
  32. package/dist/ViewModel.d.ts +10 -13
  33. package/dist/ViewModel.d.ts.map +1 -1
  34. package/dist/ViewModel.js +178 -228
  35. package/dist/ViewModel.js.map +1 -1
  36. package/dist/react/index.d.ts +1 -1
  37. package/dist/react/index.d.ts.map +1 -1
  38. package/dist/react/use-instance.cjs +31 -21
  39. package/dist/react/use-instance.cjs.map +1 -1
  40. package/dist/react/use-instance.d.ts +1 -1
  41. package/dist/react/use-instance.d.ts.map +1 -1
  42. package/dist/react/use-instance.js +32 -22
  43. package/dist/react/use-instance.js.map +1 -1
  44. package/dist/react/use-model.cjs +29 -2
  45. package/dist/react/use-model.cjs.map +1 -1
  46. package/dist/react/use-model.d.ts +9 -0
  47. package/dist/react/use-model.d.ts.map +1 -1
  48. package/dist/react/use-model.js +30 -3
  49. package/dist/react/use-model.js.map +1 -1
  50. package/dist/react.cjs +1 -0
  51. package/dist/react.cjs.map +1 -1
  52. package/dist/react.js +2 -1
  53. package/dist/walkPrototypeChain.cjs.map +1 -1
  54. package/dist/walkPrototypeChain.d.ts +2 -2
  55. package/dist/walkPrototypeChain.js.map +1 -1
  56. package/dist/wrapAsyncMethods.cjs +179 -0
  57. package/dist/wrapAsyncMethods.cjs.map +1 -0
  58. package/dist/wrapAsyncMethods.d.ts +35 -0
  59. package/dist/wrapAsyncMethods.d.ts.map +1 -0
  60. package/dist/wrapAsyncMethods.js +179 -0
  61. package/dist/wrapAsyncMethods.js.map +1 -0
  62. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ViewModel.js","sources":["../src/ViewModel.ts"],"sourcesContent":["import { EventBus } from './EventBus';\nimport { isAbortError, classifyError } from './errors';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport type { Listener, Updater, Subscribable, TaskState, EventPayload } from './types';\n\n// Re-export for backwards compatibility\nexport type { Listener, Updater } from './types';\nexport { walkPrototypeChain } from './walkPrototypeChain';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Auto-tracking types ──────────────────────────────────────────\n\ninterface TrackedSource {\n source: { subscribe(cb: () => void): () => void };\n revision: number;\n unsubscribe: () => void;\n}\n\n// ── Auto-tracking utilities ──────────────────────────────────────\n\nfunction isAutoTrackable(value: unknown): boolean {\n return (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as any).subscribe === 'function'\n );\n}\n\n// ── Async tracking types ─────────────────────────────────────────\n\nconst DEFAULT_TASK_STATE: TaskState = Object.freeze({ loading: false, error: null, errorCode: null });\n\nexport type AsyncMethodKeys<T, Base = ViewModel<any, any>> = {\n [K in Exclude<keyof T, keyof Base>]: T[K] extends (...args: any[]) => Promise<any> ? K : never;\n}[Exclude<keyof T, keyof Base>];\n\ntype AsyncMap<T> = {\n readonly [K in AsyncMethodKeys<T>]: TaskState;\n};\n\ninterface InternalTaskState {\n loading: boolean;\n error: string | null;\n errorCode: TaskState['errorCode'];\n count: number;\n}\n\nconst RESERVED_ASYNC_KEYS = ['async', 'subscribeAsync'] as const;\nconst LIFECYCLE_HOOKS = new Set(['onInit', 'onSet', 'onDispose']);\n\n// ── ViewModel ────────────────────────────────────────────────────\n\ntype EmptyViewModelState = { __brand: 'EmptyViewModelState'}\n\n/**\n * Reactive state container with computed getters, automatic async tracking, and typed events.\n * Subclass and define state shape, getters, and action methods. Use with `useLocal` in React.\n */\nexport abstract class ViewModel<S extends object = EmptyViewModelState, E extends Record<string, any> = {}> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _initialState: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _subscriptionCleanups: (() => void)[] | null = null;\n private _eventBus: EventBus<E> | null = null;\n\n // ── Reactive derived state (RFC 1) ─────────────────────────────\n private _revision = 0;\n private _stateTracking: Set<string> | null = null;\n private _sourceTracking: Map<string, TrackedSource> | null = null;\n private _trackedSources = new Map<string, TrackedSource>();\n\n // ── Async tracking (RFC 2) ──────────────────────────────────────\n private _asyncStates = new Map<string, InternalTaskState>();\n private _asyncSnapshots = new Map<string, TaskState>();\n private _asyncListeners = new Set<() => void>();\n private _asyncProxy: AsyncMap<this> | null = null;\n private _activeOps: Map<string, number> | null = null;\n\n /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */\n static GHOST_TIMEOUT = 3000;\n\n constructor(...args: EmptyViewModelState extends S ? [] | [initialState: S] : [initialState: S]) {\n const initialState = (args[0] ?? {} as S);\n this._state = Object.freeze({ ...initialState });\n this._initialState = this._state;\n this._guardReservedKeys();\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Lazily-created typed EventBus for emitting and subscribing to events. */\n get events(): EventBus<E> {\n if (!this._eventBus) {\n this._eventBus = new EventBus<E>();\n }\n return this._eventBus;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n this._trackSubscribables();\n this._installStateProxy();\n this._memoizeGetters();\n this._wrapMethods();\n return this.onInit?.();\n }\n\n /**\n * Merges partial state into current state. If no values actually\n * changed by reference, this is a no-op.\n *\n * Triggers React re-render via listener notification. Called when:\n * - User code calls set() to update source state\n *\n * NOT called for subscribable member notifications — those use\n * a separate notification path (see _trackSubscribables).\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) return;\n\n // __DEV__ guard: set() inside a getter would cause infinite loops.\n // After init(), getters are auto-memoized; set() → notify → recompute → set() → ∞\n if (__DEV__ && this._stateTracking) {\n console.error(\n '[mvc-kit] set() called inside a getter. ' +\n 'Getters must be pure — they read state and return a value. ' +\n 'They must never call set(), which would cause an infinite ' +\n 'render loop. Move this logic to an action method.'\n );\n return;\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality)\n const keys = Object.keys(partial) as (keyof S)[];\n const hasChanges = keys.some(\n (key) => partial[key] !== this._state[key]\n );\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = Object.freeze({ ...prev, ...partial });\n this._state = next;\n this._revision++;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Emits a typed event via the internal EventBus.\n * Safe to call during dispose cleanup callbacks.\n * @protected\n */\n protected emit<K extends keyof E>(event: K, payload: E[K]): void {\n // During dispose sequence: _disposed is true but eventBus not yet disposed.\n // Cleanup callbacks can still emit. After eventBus.dispose(), this is a no-op.\n // If eventBus was never created, fall back to _disposed check.\n if (this._eventBus?.disposed ?? this._disposed) return;\n this.events.emit(event, payload);\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n this._listeners.add(listener);\n\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n\n this._teardownSubscriptions();\n\n // Async tracking cleanup — handled by addCleanup registered in _wrapMethods()\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this._eventBus?.dispose();\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Resets state to initial values (or provided state), aborts in-flight work,\n * clears async tracking, and re-runs onInit().\n */\n reset(newState?: S): void | Promise<void> {\n if (this._disposed) return;\n\n // 1. Abort in-flight, lazy-recreate on next disposeSignal access\n this._abortController?.abort();\n this._abortController = null;\n\n this._teardownSubscriptions();\n\n // 2. Reset state\n this._state = newState ? Object.freeze({ ...newState }) : this._initialState;\n this._revision++;\n\n // 3. Clear async tracking (preserve listeners — React still subscribed)\n this._asyncStates.clear();\n this._asyncSnapshots.clear();\n this._notifyAsync();\n\n // 4. Re-track subscribable members (fresh subscriptions)\n this._trackSubscribables();\n\n // 5. Notify state listeners (React re-renders with new state)\n for (const listener of this._listeners) {\n listener(this._state, this._state);\n }\n\n // 6. Re-run onInit\n return this.onInit?.();\n }\n\n /**\n * Registers a cleanup function to be called on dispose. Used internally for things like method wrapper\n * cleanup and event bus disposal, but can also be used by subclasses for custom cleanup logic.\n * @param fn\n * @protected\n */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n if (!this._subscriptionCleanups) this._subscriptionCleanups = [];\n this._subscriptionCleanups.push(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose and reset. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n if (!this._subscriptionCleanups) this._subscriptionCleanups = [];\n this._subscriptionCleanups.push(unsubscribe);\n return unsubscribe;\n }\n\n /** Pipes a Channel event into a Collection via upsert. Calls channel.init() and registers auto-cleanup on dispose and reset. @protected */\n protected pipeChannel<\n K extends string,\n C extends { init(): void | Promise<void>; on(event: K, handler: (payload: any) => void): () => void },\n >(\n channel: C,\n type: K,\n target: { upsert(item: EventPayload<C, K>): void },\n ): () => void {\n channel.init();\n return this.listenTo(channel, type, (payload) => {\n target.upsert(payload);\n });\n }\n\n /** Lifecycle hook called after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n // ── Async tracking API ──────────────────────────────────────────\n\n /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */\n get async(): AsyncMap<this> {\n if (!this._asyncProxy) {\n const self = this;\n this._asyncProxy = new Proxy({} as AsyncMap<this>, {\n get(_, prop: string) {\n return self._asyncSnapshots.get(prop) ?? DEFAULT_TASK_STATE;\n },\n has(_, prop: string) {\n return self._asyncSnapshots.has(prop);\n },\n ownKeys() {\n return Array.from(self._asyncSnapshots.keys());\n },\n getOwnPropertyDescriptor(_, prop: string) {\n if (self._asyncSnapshots.has(prop)) {\n return { configurable: true, enumerable: true, value: self._asyncSnapshots.get(prop) };\n }\n return undefined;\n },\n });\n }\n return this._asyncProxy;\n }\n\n /** Subscribes to async state changes. Used by `useAsync` for React integration. */\n subscribeAsync(listener: () => void): () => void {\n if (this._disposed) return () => {};\n this._asyncListeners.add(listener);\n return () => { this._asyncListeners.delete(listener); };\n }\n\n private _notifyAsync(): void {\n for (const listener of this._asyncListeners) {\n listener();\n }\n }\n\n // ── Async tracking internals ────────────────────────────────────\n\n private _teardownSubscriptions(): void {\n for (const tracked of this._trackedSources.values()) tracked.unsubscribe();\n this._trackedSources.clear();\n\n if (this._subscriptionCleanups) {\n for (const fn of this._subscriptionCleanups) fn();\n this._subscriptionCleanups = null;\n }\n }\n\n private _guardReservedKeys(): void {\n // Prototype check (runs in constructor — catches method/getter definitions)\n walkPrototypeChain(this, ViewModel.prototype, (key) => {\n if (RESERVED_ASYNC_KEYS.includes(key as any)) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ViewModel and cannot be overridden.`\n );\n }\n });\n }\n\n private _wrapMethods(): void {\n // Instance property check (class fields assigned after super())\n for (const key of RESERVED_ASYNC_KEYS) {\n if (Object.getOwnPropertyDescriptor(this, key)?.value !== undefined) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ViewModel and cannot be overridden.`\n );\n }\n }\n\n const self = this;\n const processed = new Set<string>();\n const wrappedKeys: string[] = [];\n\n if (__DEV__) {\n this._activeOps = new Map();\n }\n\n walkPrototypeChain(this, ViewModel.prototype, (key, desc) => {\n // Skip getters/setters (owned by RFC 1 memoization)\n if (desc.get || desc.set) return;\n // Skip non-functions\n if (typeof desc.value !== 'function') return;\n // Skip _-prefixed (private convention)\n if (key.startsWith('_')) return;\n // Skip lifecycle hooks (managed by framework)\n if (LIFECYCLE_HOOKS.has(key)) return;\n // Most-derived wins\n if (processed.has(key)) return;\n processed.add(key);\n\n const original = desc.value as (...args: unknown[]) => unknown;\n let pruned = false;\n\n const wrapper = function (this: any, ...args: unknown[]) {\n // Disposed guard\n if (self._disposed) {\n if (__DEV__) {\n console.warn(`[mvc-kit] \"${key}\" called after dispose — ignored.`);\n }\n return undefined;\n }\n\n // Pre-init guard (DEV only — method still executes)\n if (__DEV__ && !self._initialized) {\n console.warn(\n `[mvc-kit] \"${key}\" called before init(). ` +\n `Async tracking is active only after init().`\n );\n }\n\n let result: unknown;\n try {\n result = original.apply(self, args);\n } catch (e) {\n // Sync throw — not tracked as async\n throw e;\n }\n\n // Sync detection: if not thenable, prune from async tracking\n if (!result || typeof (result as any).then !== 'function') {\n if (!pruned) {\n pruned = true;\n // Remove from async maps\n self._asyncStates.delete(key);\n self._asyncSnapshots.delete(key);\n // Replace wrapper with bound original for zero overhead\n (self as any)[key] = original.bind(self);\n }\n return result;\n }\n\n // ── Async tracking ──────────────────────────────────────\n let internal = self._asyncStates.get(key);\n if (!internal) {\n internal = { loading: false, error: null, errorCode: null, count: 0 };\n self._asyncStates.set(key, internal);\n }\n\n internal.count++;\n internal.loading = true;\n internal.error = null;\n internal.errorCode = null;\n self._asyncSnapshots.set(key, Object.freeze({ loading: true, error: null, errorCode: null }));\n self._notifyAsync();\n\n if (__DEV__ && self._activeOps) {\n self._activeOps.set(key, (self._activeOps.get(key) ?? 0) + 1);\n }\n\n return (result as Promise<unknown>).then(\n (value) => {\n if (self._disposed) return value;\n\n internal!.count--;\n internal!.loading = internal!.count > 0;\n self._asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n self._notifyAsync();\n\n if (__DEV__ && self._activeOps) {\n const c = (self._activeOps.get(key) ?? 1) - 1;\n if (c <= 0) self._activeOps.delete(key);\n else self._activeOps.set(key, c);\n }\n\n return value;\n },\n (error) => {\n // AbortError — silently swallow\n if (isAbortError(error)) {\n if (!self._disposed) {\n internal!.count--;\n internal!.loading = internal!.count > 0;\n self._asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n self._notifyAsync();\n }\n\n if (__DEV__ && self._activeOps) {\n const c = (self._activeOps.get(key) ?? 1) - 1;\n if (c <= 0) self._activeOps.delete(key);\n else self._activeOps.set(key, c);\n }\n\n return undefined;\n }\n\n // Disposed — fizzle silently\n if (self._disposed) return undefined;\n\n internal!.count--;\n internal!.loading = internal!.count > 0;\n const classified = classifyError(error);\n internal!.error = classified.message;\n internal!.errorCode = classified.code;\n self._asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: classified.message, errorCode: classified.code }),\n );\n self._notifyAsync();\n\n if (__DEV__ && self._activeOps) {\n const c = (self._activeOps.get(key) ?? 1) - 1;\n if (c <= 0) self._activeOps.delete(key);\n else self._activeOps.set(key, c);\n }\n\n // Re-throw to preserve standard Promise rejection\n throw error;\n },\n );\n };\n\n wrappedKeys.push(key);\n (self as any)[key] = wrapper;\n });\n\n // Register cleanup for disposal\n if (wrappedKeys.length > 0) {\n this.addCleanup(() => {\n // Snapshot active ops for ghost check before clearing\n const opsSnapshot = __DEV__ && self._activeOps ? new Map(self._activeOps) : null;\n\n // Swap all wrapped methods to no-ops (with DEV warning)\n for (const k of wrappedKeys) {\n if (__DEV__) {\n (self as any)[k] = () => {\n console.warn(`[mvc-kit] \"${k}\" called after dispose — ignored.`);\n return undefined;\n };\n } else {\n (self as any)[k] = () => undefined;\n }\n }\n\n // Clear async state\n self._asyncListeners.clear();\n self._asyncStates.clear();\n self._asyncSnapshots.clear();\n\n // DEV: schedule ghost check\n if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {\n self._scheduleGhostCheck(opsSnapshot);\n }\n });\n }\n }\n\n private _scheduleGhostCheck(opsSnapshot: Map<string, number>): void {\n if (!__DEV__) return;\n setTimeout(() => {\n for (const [key, count] of opsSnapshot) {\n console.warn(\n `[mvc-kit] Ghost async operation detected: \"${key}\" had ${count} ` +\n `pending call(s) when the ViewModel was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, (this.constructor as typeof ViewModel).GHOST_TIMEOUT);\n }\n\n // ── Auto-tracking internals ────────────────────────────────────\n\n /**\n * Installs a context-sensitive state getter on the instance.\n *\n * During getter tracking (_stateTracking is active): returns a Proxy\n * that records which state properties are accessed.\n *\n * Otherwise: returns the frozen state object directly. This is critical\n * for React's useSyncExternalStore — it needs a changing reference to\n * detect state updates and trigger re-renders.\n */\n private _installStateProxy(): void {\n const stateProxy = new Proxy({} as S, {\n get: (_, prop: string) => {\n this._stateTracking?.add(prop);\n return (this._state as any)[prop];\n },\n ownKeys: () => Reflect.ownKeys(this._state as object),\n getOwnPropertyDescriptor: (_, prop) =>\n Reflect.getOwnPropertyDescriptor(this._state as object, prop),\n set: () => {\n throw new Error('Cannot mutate state directly. Use set() instead.');\n },\n has: (_, prop) => prop in (this._state as object),\n });\n\n Object.defineProperty(this, 'state', {\n get: () => {\n if (this._stateTracking) return stateProxy;\n return this._state;\n },\n configurable: true,\n enumerable: true,\n });\n }\n\n /**\n * Scans own instance properties for Subscribable objects and sets up\n * automatic dependency tracking for each one found.\n *\n * For each subscribable member:\n * 1. Subscribe to it. On notification: bump its tracked revision\n * AND the VM's global revision, then force a new state reference\n * and notify listeners so React re-renders.\n * 2. Replace the instance property with a getter that participates\n * in dependency tracking.\n * 3. Register unsubscribe in the dispose chain.\n *\n * Called during init(), AFTER all subclass property initializers\n * have run (they execute during the constructor, before init()).\n */\n private _trackSubscribables(): void {\n for (const key of Object.getOwnPropertyNames(this)) {\n const value = (this as any)[key];\n if (!isAutoTrackable(value)) continue;\n\n let tracked: TrackedSource;\n\n const onSourceNotify = () => {\n if (this._disposed) return;\n\n // Source notified — bump revisions for getter invalidation\n tracked.revision++;\n this._revision++;\n\n // Force a new state reference so React's useSyncExternalStore\n // detects the change and triggers a re-render. State values\n // are unchanged, but getters may return different results.\n this._state = Object.freeze({ ...this._state });\n\n for (const listener of this._listeners) {\n listener(this._state, this._state);\n }\n };\n\n const unsubState = value.subscribe(onSourceNotify);\n const unsubAsync =\n typeof value.subscribeAsync === 'function'\n ? value.subscribeAsync(onSourceNotify)\n : undefined;\n\n tracked = {\n source: value,\n revision: 0,\n unsubscribe: unsubAsync\n ? () => { unsubState(); unsubAsync(); }\n : unsubState,\n };\n\n this._trackedSources.set(key, tracked);\n\n // Replace the instance property with a tracking getter.\n // The original value is captured in the closure.\n Object.defineProperty(this, key, {\n get: () => {\n this._sourceTracking?.set(key, tracked);\n return value;\n },\n configurable: true,\n enumerable: false,\n });\n }\n }\n\n /**\n * Walks the prototype chain from the subclass up to (but not including)\n * ViewModel.prototype. For every getter found, replaces it on the\n * instance with a memoized version that tracks dependencies and caches.\n *\n * Processing order: most-derived class first. If a subclass overrides\n * a parent getter, only the subclass version is memoized.\n */\n private _memoizeGetters(): void {\n const processed = new Set<string>();\n walkPrototypeChain(this, ViewModel.prototype, (key, desc) => {\n if (!desc.get || processed.has(key)) return;\n processed.add(key);\n this._wrapGetter(key, desc.get);\n });\n }\n\n /**\n * Replaces a single prototype getter with a memoized version on this\n * instance. The memoized getter tracks both state dependencies and\n * subscribable member dependencies, caching its result and\n * revalidating through a three-tier strategy:\n *\n * Tier 1 (fast): revision unchanged → return cached (1 int compare)\n * Tier 2 (medium): revision changed but this getter's deps didn't → return cached\n * Tier 3 (slow): at least one dep changed → full recompute with tracking\n */\n private _wrapGetter(key: string, original: () => unknown): void {\n // Per-getter cache state, private to this getter on this instance.\n let cached: unknown;\n let validatedAtRevision = -1;\n let stateDeps: Set<string> | undefined;\n let stateSnapshot: Map<string, unknown> | undefined;\n let sourceDeps: Map<string, number> | undefined;\n\n Object.defineProperty(this, key, {\n get: () => {\n // After dispose, return last cached value without re-executing\n if (this._disposed) return cached;\n\n // ── Tier 1: Fast path ───────────────────────────────────\n if (validatedAtRevision === this._revision) {\n return cached;\n }\n\n // ── Tier 2: Medium path ─────────────────────────────────\n if (stateDeps && stateSnapshot) {\n let fresh = true;\n\n // Check state deps by reference\n for (const [k, v] of stateSnapshot) {\n if ((this._state as any)[k] !== v) {\n fresh = false;\n break;\n }\n }\n\n // Check subscribable deps by revision\n if (fresh && sourceDeps) {\n for (const [memberKey, rev] of sourceDeps) {\n const ts = this._trackedSources.get(memberKey);\n if (ts && ts.revision !== rev) {\n fresh = false;\n break;\n }\n }\n }\n\n if (fresh) {\n validatedAtRevision = this._revision;\n return cached;\n }\n }\n\n // ── Tier 3: Slow path — full recompute ─────────────────\n // Save parent tracking context for nested getter composition\n const parentStateTracking = this._stateTracking;\n const parentSourceTracking = this._sourceTracking;\n\n this._stateTracking = new Set();\n this._sourceTracking = new Map();\n\n try {\n cached = original.call(this);\n } catch (e) {\n // Don't cache failed computations\n this._stateTracking = parentStateTracking;\n this._sourceTracking = parentSourceTracking;\n throw e;\n }\n\n stateDeps = this._stateTracking;\n const capturedSourceDeps = this._sourceTracking;\n\n // Restore parent tracking context\n this._stateTracking = parentStateTracking;\n this._sourceTracking = parentSourceTracking;\n\n // Bubble deps up to parent getter if nested\n if (parentStateTracking) {\n for (const d of stateDeps) parentStateTracking.add(d);\n }\n if (parentSourceTracking) {\n for (const [k, v] of capturedSourceDeps) {\n parentSourceTracking.set(k, v);\n }\n }\n\n // Snapshot state dep values for future medium-path comparison\n stateSnapshot = new Map();\n for (const d of stateDeps) {\n stateSnapshot.set(d, (this._state as any)[d]);\n }\n\n // Snapshot subscribable revisions\n sourceDeps = new Map();\n for (const [memberKey, tracked] of capturedSourceDeps) {\n sourceDeps.set(memberKey, tracked.revision);\n }\n\n validatedAtRevision = this._revision;\n\n return cached;\n },\n configurable: true,\n enumerable: true,\n });\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ViewModel.js","sources":["../src/ViewModel.ts"],"sourcesContent":["import { EventBus } from './EventBus';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport { wrapAsyncMethods } from './wrapAsyncMethods';\nimport type { InternalTaskState } from './wrapAsyncMethods';\nimport type { Listener, Updater, Subscribable, TaskState, EventPayload } from './types';\n\n// Re-export for backwards compatibility\nexport type { Listener, Updater } from './types';\nexport { walkPrototypeChain } from './walkPrototypeChain';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nfunction freeze<T>(obj: T): T {\n return __DEV__ ? Object.freeze(obj) as T : obj;\n}\n\n// ── Class metadata cache ─────────────────────────────────────────\n// Caches prototype walk results per class to avoid repeated Object.getOwnPropertyDescriptors\n// on every init(). Single walk extracts getters, methods, and reserved key violations.\n\ninterface ClassMemberInfo {\n getters: Array<{ key: string; get: () => unknown }>;\n methods: Array<{ key: string; fn: Function }>;\n reservedKeys: string[];\n}\n\nconst classMembers = new WeakMap<Function, ClassMemberInfo>();\n\nfunction getClassMemberInfo(\n instance: object,\n stopPrototype: object,\n reservedKeys: readonly string[],\n lifecycleHooks: Set<string>,\n): ClassMemberInfo {\n const ctor = instance.constructor;\n let info = classMembers.get(ctor);\n if (info) return info;\n\n const getters: ClassMemberInfo['getters'] = [];\n const methods: ClassMemberInfo['methods'] = [];\n const found: string[] = [];\n const processedGetters = new Set<string>();\n const processedMethods = new Set<string>();\n\n walkPrototypeChain(instance, stopPrototype, (key, desc) => {\n // Check reserved keys\n if (reservedKeys.includes(key as any)) {\n found.push(key);\n }\n\n // Collect getters (most-derived wins)\n if (desc.get && !processedGetters.has(key)) {\n processedGetters.add(key);\n getters.push({ key, get: desc.get });\n }\n\n // Collect wrappable methods (most-derived wins)\n if (!desc.get && !desc.set && typeof desc.value === 'function' &&\n !key.startsWith('_') && !lifecycleHooks.has(key) && !processedMethods.has(key)) {\n processedMethods.add(key);\n methods.push({ key, fn: desc.value });\n }\n });\n\n info = { getters, methods, reservedKeys: found };\n classMembers.set(ctor, info);\n return info;\n}\n\n// ── Auto-tracking types ──────────────────────────────────────────\n\ninterface TrackedSource {\n source: { subscribe(cb: () => void): () => void };\n revision: number;\n unsubscribe: () => void;\n}\n\n// ── Auto-tracking utilities ──────────────────────────────────────\n\nfunction isAutoTrackable(value: unknown): boolean {\n return (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as any).subscribe === 'function'\n );\n}\n\n// ── Async tracking types ─────────────────────────────────────────\n\nconst DEFAULT_TASK_STATE: TaskState = Object.freeze({ loading: false, error: null, errorCode: null });\n\nexport type AsyncMethodKeys<T, Base = ViewModel<any, any>> = {\n [K in Exclude<keyof T, keyof Base>]: T[K] extends (...args: any[]) => Promise<any> ? K : never;\n}[Exclude<keyof T, keyof Base>];\n\ntype AsyncMap<T> = {\n readonly [K in AsyncMethodKeys<T>]: TaskState;\n};\n\nconst RESERVED_ASYNC_KEYS = ['async', 'subscribeAsync'] as const;\nconst LIFECYCLE_HOOKS = new Set(['onInit', 'onSet', 'onDispose']);\n\n// ── ViewModel ────────────────────────────────────────────────────\n\ntype EmptyViewModelState = { __brand: 'EmptyViewModelState'}\n\n/**\n * Reactive state container with computed getters, automatic async tracking, and typed events.\n * Subclass and define state shape, getters, and action methods. Use with `useLocal` in React.\n */\nexport abstract class ViewModel<S extends object = EmptyViewModelState, E extends Record<string, any> = {}> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _initialState: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _subscriptionCleanups: (() => void)[] | null = null;\n private _eventBus: EventBus<E> | null = null;\n\n // ── Reactive derived state (RFC 1) ─────────────────────────────\n private _revision = 0;\n private _stateTracking: Set<string> | null = null;\n private _sourceTracking: Map<string, TrackedSource> | null = null;\n private _trackedSources = new Map<string, TrackedSource>();\n\n // ── Async tracking (RFC 2) ──────────────────────────────────────\n // Lazily allocated on first async method wrap to keep construction fast.\n private _asyncStates: Map<string, InternalTaskState> | null = null;\n private _asyncSnapshots: Map<string, TaskState> | null = null;\n private _asyncListeners: Set<() => void> | null = null;\n private _asyncProxy: AsyncMap<this> | null = null;\n private _activeOps: Map<string, number> | null = null;\n\n /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */\n static GHOST_TIMEOUT = 3000;\n\n constructor(...args: EmptyViewModelState extends S ? [] | [initialState: S] : [initialState: S]) {\n const initialState = (args[0] ?? {} as S);\n this._state = freeze({ ...initialState });\n this._initialState = this._state;\n this._guardReservedKeys();\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Lazily-created typed EventBus for emitting and subscribing to events. */\n get events(): EventBus<E> {\n if (!this._eventBus) {\n this._eventBus = new EventBus<E>();\n }\n return this._eventBus;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n this._trackSubscribables();\n this._installStateProxy();\n this._processMembers();\n return this.onInit?.();\n }\n\n /**\n * Merges partial state into current state. If no values actually\n * changed by reference, this is a no-op.\n *\n * Triggers React re-render via listener notification. Called when:\n * - User code calls set() to update source state\n *\n * NOT called for subscribable member notifications — those use\n * a separate notification path (see _trackSubscribables).\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) return;\n\n // __DEV__ guard: set() inside a getter would cause infinite loops.\n // After init(), getters are auto-memoized; set() → notify → recompute → set() → ∞\n if (__DEV__ && this._stateTracking) {\n console.error(\n '[mvc-kit] set() called inside a getter. ' +\n 'Getters must be pure — they read state and return a value. ' +\n 'They must never call set(), which would cause an infinite ' +\n 'render loop. Move this logic to an action method.'\n );\n return;\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality).\n // Uses for..in to avoid Object.keys() array allocation.\n let hasChanges = false;\n const current = this._state;\n for (const key in partial) {\n if ((partial as any)[key] !== (current as any)[key]) {\n hasChanges = true;\n break;\n }\n }\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = freeze({ ...prev, ...partial });\n this._state = next;\n this._revision++;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Emits a typed event via the internal EventBus.\n * Safe to call during dispose cleanup callbacks.\n * @protected\n */\n protected emit<K extends keyof E>(event: K, payload: E[K]): void {\n // During dispose sequence: _disposed is true but eventBus not yet disposed.\n // Cleanup callbacks can still emit. After eventBus.dispose(), this is a no-op.\n // If eventBus was never created, fall back to _disposed check.\n if (this._eventBus?.disposed ?? this._disposed) return;\n this.events.emit(event, payload);\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n this._listeners.add(listener);\n\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n\n this._teardownSubscriptions();\n\n // Async tracking cleanup — handled by addCleanup registered in _processMembers()\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this._eventBus?.dispose();\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Resets state to initial values (or provided state), aborts in-flight work,\n * clears async tracking, and re-runs onInit().\n */\n reset(newState?: S): void | Promise<void> {\n if (this._disposed) return;\n\n // 1. Abort in-flight, lazy-recreate on next disposeSignal access\n this._abortController?.abort();\n this._abortController = null;\n\n this._teardownSubscriptions();\n\n // 2. Reset state\n this._state = newState ? freeze({ ...newState }) : this._initialState;\n this._revision++;\n\n // 3. Clear async tracking (preserve listeners — React still subscribed)\n this._asyncStates?.clear();\n this._asyncSnapshots?.clear();\n this._notifyAsync();\n\n // 4. Re-track subscribable members (fresh subscriptions)\n this._trackSubscribables();\n\n // 5. Notify state listeners (React re-renders with new state)\n for (const listener of this._listeners) {\n listener(this._state, this._state);\n }\n\n // 6. Re-run onInit\n return this.onInit?.();\n }\n\n /**\n * Registers a cleanup function to be called on dispose. Used internally for things like method wrapper\n * cleanup and event bus disposal, but can also be used by subclasses for custom cleanup logic.\n * @param fn\n * @protected\n */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n if (!this._subscriptionCleanups) this._subscriptionCleanups = [];\n this._subscriptionCleanups.push(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose and reset. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n if (!this._subscriptionCleanups) this._subscriptionCleanups = [];\n this._subscriptionCleanups.push(unsubscribe);\n return unsubscribe;\n }\n\n /** Pipes a Channel event into a Collection via upsert. Calls channel.init() and registers auto-cleanup on dispose and reset. @protected */\n protected pipeChannel<\n K extends string,\n C extends { init(): void | Promise<void>; on(event: K, handler: (payload: any) => void): () => void },\n >(\n channel: C,\n type: K,\n target: { upsert(item: EventPayload<C, K>): void },\n ): () => void {\n channel.init();\n return this.listenTo(channel, type, (payload) => {\n target.upsert(payload);\n });\n }\n\n /** Lifecycle hook called after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n // ── Async tracking API ──────────────────────────────────────────\n\n /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */\n get async(): AsyncMap<this> {\n if (!this._asyncProxy) {\n const self = this;\n this._asyncProxy = new Proxy({} as AsyncMap<this>, {\n get(_, prop: string) {\n return self._asyncSnapshots?.get(prop) ?? DEFAULT_TASK_STATE;\n },\n has(_, prop: string) {\n return self._asyncSnapshots?.has(prop) ?? false;\n },\n ownKeys() {\n return self._asyncSnapshots ? Array.from(self._asyncSnapshots.keys()) : [];\n },\n getOwnPropertyDescriptor(_, prop: string) {\n if (self._asyncSnapshots?.has(prop)) {\n return { configurable: true, enumerable: true, value: self._asyncSnapshots.get(prop) };\n }\n return undefined;\n },\n });\n }\n return this._asyncProxy;\n }\n\n /** Subscribes to async state changes. Used for React integration. */\n subscribeAsync(listener: () => void): () => void {\n if (this._disposed) return () => {};\n if (!this._asyncListeners) this._asyncListeners = new Set();\n this._asyncListeners.add(listener);\n return () => { this._asyncListeners!.delete(listener); };\n }\n\n private _notifyAsync(): void {\n if (!this._asyncListeners) return;\n for (const listener of this._asyncListeners) {\n listener();\n }\n }\n\n // ── Async tracking internals ────────────────────────────────────\n\n private _teardownSubscriptions(): void {\n for (const tracked of this._trackedSources.values()) tracked.unsubscribe();\n this._trackedSources.clear();\n\n if (this._subscriptionCleanups) {\n for (const fn of this._subscriptionCleanups) fn();\n this._subscriptionCleanups = null;\n }\n }\n\n private _guardReservedKeys(): void {\n // Prototype check (runs in constructor — catches method/getter definitions)\n const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);\n if (info.reservedKeys.length > 0) {\n throw new Error(\n `[mvc-kit] \"${info.reservedKeys[0]}\" is a reserved property on ViewModel and cannot be overridden.`\n );\n }\n }\n\n // ── Member processing (merged getter memoization + async method wrapping) ──\n\n /**\n * Uses cached class metadata to memoize getters (RFC 1) and delegates\n * async method wrapping to the shared wrapAsyncMethods helper (RFC 2).\n * Class metadata is computed once per class via getClassMemberInfo() and reused\n * across all instances — avoids repeated prototype walks.\n */\n private _processMembers(): void {\n const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);\n\n // Memoize getters\n for (let i = 0; i < info.getters.length; i++) {\n this._wrapGetter(info.getters[i].key, info.getters[i].get);\n }\n\n // DEV: Instance property reserved key check (must run even if no methods to wrap)\n if (__DEV__) {\n for (const key of RESERVED_ASYNC_KEYS) {\n if (Object.getOwnPropertyDescriptor(this, key)?.value !== undefined) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ViewModel and cannot be overridden.`\n );\n }\n }\n }\n\n // Skip async wrapping if no methods to wrap\n if (info.methods.length === 0) return;\n\n // Lazily allocate async tracking collections\n if (!this._asyncStates) this._asyncStates = new Map();\n if (!this._asyncSnapshots) this._asyncSnapshots = new Map();\n if (!this._asyncListeners) this._asyncListeners = new Set();\n\n // Initialize DEV-only active ops tracking\n if (__DEV__) {\n this._activeOps = new Map();\n }\n\n // Wrap async methods (shared with Resource)\n wrapAsyncMethods({\n instance: this,\n stopPrototype: ViewModel.prototype,\n reservedKeys: RESERVED_ASYNC_KEYS,\n lifecycleHooks: LIFECYCLE_HOOKS,\n isDisposed: () => this._disposed,\n isInitialized: () => this._initialized,\n asyncStates: this._asyncStates,\n asyncSnapshots: this._asyncSnapshots,\n asyncListeners: this._asyncListeners,\n notifyAsync: () => this._notifyAsync(),\n addCleanup: (fn) => this.addCleanup(fn),\n ghostTimeout: (this.constructor as typeof ViewModel).GHOST_TIMEOUT,\n className: 'ViewModel',\n activeOps: this._activeOps,\n methods: info.methods,\n });\n }\n\n // ── Auto-tracking internals ────────────────────────────────────\n\n /**\n * Installs a context-sensitive state getter on the instance.\n *\n * During getter tracking (_stateTracking is active): returns a Proxy\n * that records which state properties are accessed. The Proxy is created\n * lazily on first tracking access to keep init() fast.\n *\n * Otherwise: returns the frozen state object directly. This is critical\n * for React's useSyncExternalStore — it needs a changing reference to\n * detect state updates and trigger re-renders.\n */\n private _installStateProxy(): void {\n let stateProxy: S | null = null;\n\n Object.defineProperty(this, 'state', {\n get: () => {\n if (this._stateTracking) {\n if (!stateProxy) {\n stateProxy = new Proxy({} as S, {\n get: (_, prop: string) => {\n this._stateTracking?.add(prop);\n return (this._state as any)[prop];\n },\n ownKeys: () => Reflect.ownKeys(this._state as object),\n getOwnPropertyDescriptor: (_, prop) =>\n Reflect.getOwnPropertyDescriptor(this._state as object, prop),\n set: () => {\n throw new Error('Cannot mutate state directly. Use set() instead.');\n },\n has: (_, prop) => prop in (this._state as object),\n });\n }\n return stateProxy;\n }\n return this._state;\n },\n configurable: true,\n enumerable: true,\n });\n }\n\n /**\n * Scans own instance properties for Subscribable objects and sets up\n * automatic dependency tracking for each one found.\n *\n * For each subscribable member:\n * 1. Subscribe to it. On notification: bump its tracked revision\n * AND the VM's global revision, then force a new state reference\n * and notify listeners so React re-renders.\n * 2. Replace the instance property with a getter that participates\n * in dependency tracking.\n * 3. Register unsubscribe in the dispose chain.\n *\n * Called during init(), AFTER all subclass property initializers\n * have run (they execute during the constructor, before init()).\n */\n private _trackSubscribables(): void {\n for (const key of Object.getOwnPropertyNames(this)) {\n const value = (this as any)[key];\n if (!isAutoTrackable(value)) continue;\n\n let tracked: TrackedSource;\n\n const onSourceNotify = () => {\n if (this._disposed) return;\n\n // Source notified — bump revisions for getter invalidation\n tracked.revision++;\n this._revision++;\n\n for (const listener of this._listeners) {\n listener(this._state, this._state);\n }\n };\n\n const unsubState = value.subscribe(onSourceNotify);\n const unsubAsync =\n typeof value.subscribeAsync === 'function'\n ? value.subscribeAsync(onSourceNotify)\n : undefined;\n\n tracked = {\n source: value,\n revision: 0,\n unsubscribe: unsubAsync\n ? () => { unsubState(); unsubAsync(); }\n : unsubState,\n };\n\n this._trackedSources.set(key, tracked);\n\n // Replace the instance property with a tracking getter.\n // The original value is captured in the closure.\n Object.defineProperty(this, key, {\n get: () => {\n this._sourceTracking?.set(key, tracked);\n return value;\n },\n configurable: true,\n enumerable: false,\n });\n }\n }\n\n /**\n * Replaces a single prototype getter with a memoized version on this\n * instance. The memoized getter tracks both state dependencies and\n * subscribable member dependencies, caching its result and\n * revalidating through a three-tier strategy:\n *\n * Tier 1 (fast): revision unchanged → return cached (1 int compare)\n * Tier 2 (medium): revision changed but this getter's deps didn't → return cached\n * Tier 3 (slow): at least one dep changed → full recompute with tracking\n */\n private _wrapGetter(key: string, original: () => unknown): void {\n // Per-getter cache state, private to this getter on this instance.\n let cached: unknown;\n let validatedAtRevision = -1;\n\n // Array-based dep tracking — avoids Map iterator allocation in Tier 2\n let stateDepKeys: string[] | undefined;\n let stateDepValues: unknown[] | undefined;\n let sourceDepKeys: string[] | undefined;\n let sourceDepRevisions: number[] | undefined;\n\n // Reusable tracking containers — allocated on first Tier 3, reused via clear()\n let trackingSet: Set<string> | undefined;\n let trackingMap: Map<string, TrackedSource> | undefined;\n\n Object.defineProperty(this, key, {\n get: () => {\n // ── Tier 1: Fast path (1 integer compare) ───────────────\n if (validatedAtRevision === this._revision) {\n return cached;\n }\n\n // After dispose, revision never changes so Tier 1 hits if\n // getter was ever called. Guard the uncalled-before-dispose edge case.\n if (this._disposed) return cached;\n\n // ── Tier 2: Medium path — array-based dep check ─────────\n if (stateDepKeys !== undefined) {\n let fresh = true;\n\n // Check state deps by reference (array iteration, no iterator alloc)\n const state = this._state as any;\n for (let i = 0; i < stateDepKeys.length; i++) {\n if (state[stateDepKeys[i]] !== stateDepValues![i]) {\n fresh = false;\n break;\n }\n }\n\n // Check subscribable deps by revision\n if (fresh && sourceDepKeys !== undefined && sourceDepKeys.length > 0) {\n for (let i = 0; i < sourceDepKeys.length; i++) {\n const ts = this._trackedSources.get(sourceDepKeys[i]);\n if (ts && ts.revision !== sourceDepRevisions![i]) {\n fresh = false;\n break;\n }\n }\n }\n\n if (fresh) {\n validatedAtRevision = this._revision;\n return cached;\n }\n }\n\n // ── Tier 3: Slow path — full recompute ─────────────────\n // Save parent tracking context for nested getter composition\n const parentStateTracking = this._stateTracking;\n const parentSourceTracking = this._sourceTracking;\n\n // Reuse tracking containers (clear instead of allocate)\n if (trackingSet) {\n trackingSet.clear();\n } else {\n trackingSet = new Set();\n }\n if (trackingMap) {\n trackingMap.clear();\n } else {\n trackingMap = new Map();\n }\n\n this._stateTracking = trackingSet;\n this._sourceTracking = trackingMap;\n\n try {\n cached = original.call(this);\n } catch (e) {\n // Don't cache failed computations\n this._stateTracking = parentStateTracking;\n this._sourceTracking = parentSourceTracking;\n throw e;\n }\n\n // Restore parent tracking context\n this._stateTracking = parentStateTracking;\n this._sourceTracking = parentSourceTracking;\n\n // Bubble deps up to parent getter if nested\n if (parentStateTracking) {\n for (const d of trackingSet) parentStateTracking.add(d);\n }\n if (parentSourceTracking) {\n for (const [k, v] of trackingMap) {\n parentSourceTracking.set(k, v);\n }\n }\n\n // Snapshot state dep values into arrays for Tier 2\n const depCount = trackingSet.size;\n if (!stateDepKeys || stateDepKeys.length !== depCount) {\n stateDepKeys = new Array(depCount);\n stateDepValues = new Array(depCount);\n }\n {\n let i = 0;\n const state = this._state as any;\n for (const d of trackingSet) {\n stateDepKeys[i] = d;\n stateDepValues![i] = state[d];\n i++;\n }\n }\n\n // Snapshot subscribable revisions into arrays\n const sourceCount = trackingMap.size;\n if (sourceCount > 0) {\n if (!sourceDepKeys || sourceDepKeys.length !== sourceCount) {\n sourceDepKeys = new Array(sourceCount);\n sourceDepRevisions = new Array(sourceCount);\n }\n let i = 0;\n for (const [memberKey, tracked] of trackingMap) {\n sourceDepKeys[i] = memberKey;\n sourceDepRevisions![i] = tracked.revision;\n i++;\n }\n } else {\n sourceDepKeys = undefined;\n sourceDepRevisions = undefined;\n }\n\n validatedAtRevision = this._revision;\n\n return cached;\n },\n configurable: true,\n enumerable: true,\n });\n }\n}\n"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ export type { StateOf, ItemOf, SingletonClass, ProviderRegistry } from './types'
2
2
  export { useInstance } from './use-instance';
3
3
  export { useLocal } from './use-local';
4
4
  export { useSingleton } from './use-singleton';
5
- export { useModel, useField } from './use-model';
5
+ export { useModel, useModelRef, useField } from './use-model';
6
6
  export type { ModelHandle, FieldHandle } from './use-model';
7
7
  export { useEvent, useEmit } from './use-event-bus';
8
8
  export { useTeardown } from './use-teardown';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAGjF,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAGjF,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAGpD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
@@ -4,28 +4,38 @@ const react = require("react");
4
4
  function hasAsyncSubscription(obj) {
5
5
  return obj !== null && typeof obj === "object" && typeof obj.subscribeAsync === "function";
6
6
  }
7
+ const SERVER_SNAPSHOT = () => 0;
7
8
  function useInstance(subscribable) {
8
- const state = react.useSyncExternalStore(
9
- (onStoreChange) => subscribable.subscribe(onStoreChange),
10
- () => subscribable.state,
11
- () => subscribable.state
12
- // SSR snapshot
13
- );
14
- const versionRef = react.useRef(0);
15
- react.useSyncExternalStore(
16
- (onStoreChange) => {
17
- if (!hasAsyncSubscription(subscribable)) return () => {
18
- };
19
- return subscribable.subscribeAsync(() => {
20
- versionRef.current++;
21
- onStoreChange();
22
- });
23
- },
24
- () => versionRef.current,
25
- () => 0
26
- // SSR: no async ops server-side
27
- );
28
- return state;
9
+ const ref = react.useRef(null);
10
+ if (!ref.current || ref.current.subscribable !== subscribable) {
11
+ const version = { current: ref.current?.version ?? 0 };
12
+ ref.current = {
13
+ version: version.current,
14
+ subscribable,
15
+ subscribe: (onStoreChange) => {
16
+ const unsub1 = subscribable.subscribe(() => {
17
+ version.current++;
18
+ ref.current.version = version.current;
19
+ onStoreChange();
20
+ });
21
+ let unsub2;
22
+ if (hasAsyncSubscription(subscribable)) {
23
+ unsub2 = subscribable.subscribeAsync(() => {
24
+ version.current++;
25
+ ref.current.version = version.current;
26
+ onStoreChange();
27
+ });
28
+ }
29
+ return () => {
30
+ unsub1();
31
+ unsub2?.();
32
+ };
33
+ },
34
+ getSnapshot: () => version.current
35
+ };
36
+ }
37
+ react.useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);
38
+ return subscribable.state;
29
39
  }
30
40
  exports.useInstance = useInstance;
31
41
  //# sourceMappingURL=use-instance.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-instance.cjs","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\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 second subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): S {\n const state = useSyncExternalStore(\n (onStoreChange) => subscribable.subscribe(onStoreChange),\n () => subscribable.state,\n () => subscribable.state // SSR snapshot\n );\n\n // Async subscription forces re-render when any async status changes.\n // Duck-typed: safe for Collection/Model (they don't have subscribeAsync).\n const versionRef = useRef(0);\n useSyncExternalStore(\n (onStoreChange) => {\n if (!hasAsyncSubscription(subscribable)) return () => {};\n return subscribable.subscribeAsync(() => {\n versionRef.current++;\n onStoreChange();\n });\n },\n () => versionRef.current,\n () => 0 // SSR: no async ops server-side\n );\n\n return state;\n}\n"],"names":["useSyncExternalStore","useRef"],"mappings":";;;AAGA,SAAS,qBAAqB,KAAqE;AACjG,SACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;AAE3C;AAUO,SAAS,YAAe,cAAkC;AAC/D,QAAM,QAAQA,MAAAA;AAAAA,IACZ,CAAC,kBAAkB,aAAa,UAAU,aAAa;AAAA,IACvD,MAAM,aAAa;AAAA,IACnB,MAAM,aAAa;AAAA;AAAA,EAAA;AAKrB,QAAM,aAAaC,MAAAA,OAAO,CAAC;AAC3BD,QAAAA;AAAAA,IACE,CAAC,kBAAkB;AACjB,UAAI,CAAC,qBAAqB,YAAY,UAAU,MAAM;AAAA,MAAC;AACvD,aAAO,aAAa,eAAe,MAAM;AACvC,mBAAW;AACX,sBAAA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,MAAM,WAAW;AAAA,IACjB,MAAM;AAAA;AAAA,EAAA;AAGR,SAAO;AACT;;"}
1
+ {"version":3,"file":"use-instance.cjs","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"],"names":["useRef","useSyncExternalStore"],"mappings":";;;AAGA,SAAS,qBAAqB,KAAqE;AACjG,SACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;AAE3C;AAEA,MAAM,kBAAkB,MAAM;AAiBvB,SAAS,YAAe,cAAkC;AAC/D,QAAM,MAAMA,MAAAA,OAA8B,IAAI;AAE9C,MAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,iBAAiB,cAAc;AAC7D,UAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,EAAA;AACnD,QAAI,UAAU;AAAA,MACZ,SAAS,QAAQ;AAAA,MACjB;AAAA,MACA,WAAW,CAAC,kBAA8B;AACxC,cAAM,SAAS,aAAa,UAAU,MAAM;AAC1C,kBAAQ;AACR,cAAI,QAAS,UAAU,QAAQ;AAC/B,wBAAA;AAAA,QACF,CAAC;AACD,YAAI;AACJ,YAAI,qBAAqB,YAAY,GAAG;AACtC,mBAAS,aAAa,eAAe,MAAM;AACzC,oBAAQ;AACR,gBAAI,QAAS,UAAU,QAAQ;AAC/B,0BAAA;AAAA,UACF,CAAC;AAAA,QACH;AACA,eAAO,MAAM;AAAE,iBAAA;AAAU,mBAAA;AAAA,QAAY;AAAA,MACvC;AAAA,MACA,aAAa,MAAM,QAAQ;AAAA,IAAA;AAAA,EAE/B;AAEAC,QAAAA,qBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,eAAe;AAEpF,SAAO,aAAa;AACtB;;"}
@@ -4,7 +4,7 @@ import type { Subscribable } from '../types';
4
4
  * No ownership - caller manages the instance lifecycle.
5
5
  *
6
6
  * If the instance has a `subscribeAsync` method (duck-typed),
7
- * a second subscription ensures async state changes also
7
+ * a combined subscription ensures async state changes also
8
8
  * trigger React re-renders.
9
9
  */
10
10
  export declare function useInstance<S>(subscribable: Subscribable<S>): S;
@@ -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;AAU7C;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAuB/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;AAmB7C;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CA+B/D"}
@@ -1,29 +1,39 @@
1
- import { useSyncExternalStore, useRef } from "react";
1
+ import { useRef, useSyncExternalStore } from "react";
2
2
  function hasAsyncSubscription(obj) {
3
3
  return obj !== null && typeof obj === "object" && typeof obj.subscribeAsync === "function";
4
4
  }
5
+ const SERVER_SNAPSHOT = () => 0;
5
6
  function useInstance(subscribable) {
6
- const state = useSyncExternalStore(
7
- (onStoreChange) => subscribable.subscribe(onStoreChange),
8
- () => subscribable.state,
9
- () => subscribable.state
10
- // SSR snapshot
11
- );
12
- const versionRef = useRef(0);
13
- useSyncExternalStore(
14
- (onStoreChange) => {
15
- if (!hasAsyncSubscription(subscribable)) return () => {
16
- };
17
- return subscribable.subscribeAsync(() => {
18
- versionRef.current++;
19
- onStoreChange();
20
- });
21
- },
22
- () => versionRef.current,
23
- () => 0
24
- // SSR: no async ops server-side
25
- );
26
- return state;
7
+ const ref = useRef(null);
8
+ if (!ref.current || ref.current.subscribable !== subscribable) {
9
+ const version = { current: ref.current?.version ?? 0 };
10
+ ref.current = {
11
+ version: version.current,
12
+ subscribable,
13
+ subscribe: (onStoreChange) => {
14
+ const unsub1 = subscribable.subscribe(() => {
15
+ version.current++;
16
+ ref.current.version = version.current;
17
+ onStoreChange();
18
+ });
19
+ let unsub2;
20
+ if (hasAsyncSubscription(subscribable)) {
21
+ unsub2 = subscribable.subscribeAsync(() => {
22
+ version.current++;
23
+ ref.current.version = version.current;
24
+ onStoreChange();
25
+ });
26
+ }
27
+ return () => {
28
+ unsub1();
29
+ unsub2?.();
30
+ };
31
+ },
32
+ getSnapshot: () => version.current
33
+ };
34
+ }
35
+ useSyncExternalStore(ref.current.subscribe, ref.current.getSnapshot, SERVER_SNAPSHOT);
36
+ return subscribable.state;
27
37
  }
28
38
  export {
29
39
  useInstance
@@ -1 +1 @@
1
- {"version":3,"file":"use-instance.js","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\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 second subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): S {\n const state = useSyncExternalStore(\n (onStoreChange) => subscribable.subscribe(onStoreChange),\n () => subscribable.state,\n () => subscribable.state // SSR snapshot\n );\n\n // Async subscription forces re-render when any async status changes.\n // Duck-typed: safe for Collection/Model (they don't have subscribeAsync).\n const versionRef = useRef(0);\n useSyncExternalStore(\n (onStoreChange) => {\n if (!hasAsyncSubscription(subscribable)) return () => {};\n return subscribable.subscribeAsync(() => {\n versionRef.current++;\n onStoreChange();\n });\n },\n () => versionRef.current,\n () => 0 // SSR: no async ops server-side\n );\n\n return state;\n}\n"],"names":[],"mappings":";AAGA,SAAS,qBAAqB,KAAqE;AACjG,SACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;AAE3C;AAUO,SAAS,YAAe,cAAkC;AAC/D,QAAM,QAAQ;AAAA,IACZ,CAAC,kBAAkB,aAAa,UAAU,aAAa;AAAA,IACvD,MAAM,aAAa;AAAA,IACnB,MAAM,aAAa;AAAA;AAAA,EAAA;AAKrB,QAAM,aAAa,OAAO,CAAC;AAC3B;AAAA,IACE,CAAC,kBAAkB;AACjB,UAAI,CAAC,qBAAqB,YAAY,UAAU,MAAM;AAAA,MAAC;AACvD,aAAO,aAAa,eAAe,MAAM;AACvC,mBAAW;AACX,sBAAA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,MAAM,WAAW;AAAA,IACjB,MAAM;AAAA;AAAA,EAAA;AAGR,SAAO;AACT;"}
1
+ {"version":3,"file":"use-instance.js","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"],"names":[],"mappings":";AAGA,SAAS,qBAAqB,KAAqE;AACjG,SACE,QAAQ,QACR,OAAO,QAAQ,YACf,OAAQ,IAAY,mBAAmB;AAE3C;AAEA,MAAM,kBAAkB,MAAM;AAiBvB,SAAS,YAAe,cAAkC;AAC/D,QAAM,MAAM,OAA8B,IAAI;AAE9C,MAAI,CAAC,IAAI,WAAW,IAAI,QAAQ,iBAAiB,cAAc;AAC7D,UAAM,UAAU,EAAE,SAAS,IAAI,SAAS,WAAW,EAAA;AACnD,QAAI,UAAU;AAAA,MACZ,SAAS,QAAQ;AAAA,MACjB;AAAA,MACA,WAAW,CAAC,kBAA8B;AACxC,cAAM,SAAS,aAAa,UAAU,MAAM;AAC1C,kBAAQ;AACR,cAAI,QAAS,UAAU,QAAQ;AAC/B,wBAAA;AAAA,QACF,CAAC;AACD,YAAI;AACJ,YAAI,qBAAqB,YAAY,GAAG;AACtC,mBAAS,aAAa,eAAe,MAAM;AACzC,oBAAQ;AACR,gBAAI,QAAS,UAAU,QAAQ;AAC/B,0BAAA;AAAA,UACF,CAAC;AAAA,QACH;AACA,eAAO,MAAM;AAAE,iBAAA;AAAU,mBAAA;AAAA,QAAY;AAAA,MACvC;AAAA,MACA,aAAa,MAAM,QAAQ;AAAA,IAAA;AAAA,EAE/B;AAEA,uBAAqB,IAAI,QAAQ,WAAW,IAAI,QAAQ,aAAa,eAAe;AAEpF,SAAO,aAAa;AACtB;"}
@@ -8,11 +8,15 @@ function useModel(factory) {
8
8
  if (!modelRef.current || modelRef.current.disposed) {
9
9
  modelRef.current = factory();
10
10
  }
11
- react.useSyncExternalStore(
11
+ const modelSubscribe = react.useCallback(
12
12
  (onStoreChange) => modelRef.current.subscribe(onStoreChange),
13
+ []
14
+ );
15
+ const modelSnapshot = react.useCallback(
13
16
  () => modelRef.current.state,
14
- () => modelRef.current.state
17
+ []
15
18
  );
19
+ react.useSyncExternalStore(modelSubscribe, modelSnapshot, modelSnapshot);
16
20
  react.useEffect(() => {
17
21
  mountedRef.current = true;
18
22
  if (guards.isInitializable(modelRef.current)) {
@@ -36,6 +40,28 @@ function useModel(factory) {
36
40
  model
37
41
  };
38
42
  }
43
+ function useModelRef(factory) {
44
+ const modelRef = react.useRef(null);
45
+ const mountedRef = react.useRef(false);
46
+ if (!modelRef.current || modelRef.current.disposed) {
47
+ modelRef.current = factory();
48
+ }
49
+ react.useEffect(() => {
50
+ mountedRef.current = true;
51
+ if (guards.isInitializable(modelRef.current)) {
52
+ modelRef.current.init();
53
+ }
54
+ return () => {
55
+ mountedRef.current = false;
56
+ setTimeout(() => {
57
+ if (!mountedRef.current) {
58
+ modelRef.current?.dispose();
59
+ }
60
+ }, 0);
61
+ };
62
+ }, []);
63
+ return modelRef.current;
64
+ }
39
65
  function useField(model, field) {
40
66
  const getSnapshot = react.useCallback(() => {
41
67
  return {
@@ -77,4 +103,5 @@ function useField(model, field) {
77
103
  }
78
104
  exports.useField = useField;
79
105
  exports.useModel = useModel;
106
+ exports.useModelRef = useModelRef;
80
107
  //# sourceMappingURL=use-model.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-model.cjs","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 useSyncExternalStore(\n (onStoreChange) => modelRef.current!.subscribe(onStoreChange),\n () => modelRef.current!.state,\n () => modelRef.current!.state,\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 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/** 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"],"names":["useRef","useSyncExternalStore","useEffect","isInitializable","useCallback"],"mappings":";;;;AAkBO,SAAS,SACd,SAC4B;AAC5B,QAAM,WAAWA,MAAAA,OAAiB,IAAI;AACtC,QAAM,aAAaA,MAAAA,OAAO,KAAK;AAE/B,MAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,UAAU;AAClD,aAAS,UAAU,QAAA;AAAA,EACrB;AAEAC,QAAAA;AAAAA,IACE,CAAC,kBAAkB,SAAS,QAAS,UAAU,aAAa;AAAA,IAC5D,MAAM,SAAS,QAAS;AAAA,IACxB,MAAM,SAAS,QAAS;AAAA,EAAA;AAG1BC,QAAAA,UAAU,MAAM;AACd,eAAW,UAAU;AACrB,QAAIC,OAAAA,gBAAgB,SAAS,OAAO,GAAG;AACrC,eAAS,QAAQ,KAAA;AAAA,IACnB;AACA,WAAO,MAAM;AACX,iBAAW,UAAU;AACrB,iBAAW,MAAM;AACf,YAAI,CAAC,WAAW,SAAS;AACvB,mBAAS,SAAS,QAAA;AAAA,QACpB;AAAA,MACF,GAAG,CAAC;AAAA,IACN;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAQ,SAAS;AAEvB,SAAO;AAAA,IACL,OAAO,MAAM;AAAA,IACb,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,OAAO,MAAM;AAAA,IACb;AAAA,EAAA;AAEJ;AAYO,SAAS,SACd,OACA,OACmB;AAEnB,QAAM,cAAcC,MAAAA,YAAY,MAAM;AACpC,WAAO;AAAA,MACL,OAAO,MAAM,MAAM,KAAK;AAAA,MACxB,OAAO,MAAM,OAAO,KAAK;AAAA,IAAA;AAAA,EAE7B,GAAG,CAAC,OAAO,KAAK,CAAC;AAGjB,QAAM,YAAYJ,aAAO,aAAa;AAEtC,QAAM,YAAYI,MAAAA;AAAAA,IAChB,CAAC,kBAA8B;AAC7B,aAAO,MAAM,UAAU,MAAM;AAC3B,cAAM,OAAO,YAAA;AACb,cAAM,UAAU,UAAU;AAG1B,YAAI,KAAK,UAAU,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO;AAChE,oBAAU,UAAU;AACpB,wBAAA;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,CAAC,OAAO,WAAW;AAAA,EAAA;AAGrB,QAAM,WAAWH,MAAAA;AAAAA,IACf;AAAA,IACA,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,EAAA;AAGlB,QAAM,MAAMG,MAAAA;AAAAA,IACV,CAAC,UAAgB;AAGf,YAAM,UAAsB,EAAE,CAAC,KAAK,GAAG,MAAA;AACtC,YAA4D,IAAI,OAAO;AAAA,IAC1E;AAAA,IACA,CAAC,OAAO,KAAK;AAAA,EAAA;AAGf,SAAO;AAAA,IACL,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB;AAAA,EAAA;AAEJ;;;"}
1
+ {"version":3,"file":"use-model.cjs","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"],"names":["useRef","useCallback","useSyncExternalStore","useEffect","isInitializable"],"mappings":"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"}
@@ -13,6 +13,15 @@ export interface ModelHandle<S extends object, M extends Model<S>> {
13
13
  * Bind to a component-scoped Model with validation and dirty state exposed.
14
14
  */
15
15
  export declare function useModel<M extends Model<any>>(factory: () => M): ModelHandle<StateOf<M>, M>;
16
+ /**
17
+ * Create a component-scoped Model with lifecycle management (init + dispose)
18
+ * but NO state subscription. The parent component never re-renders from
19
+ * model state changes.
20
+ *
21
+ * Designed for the per-field isolation pattern: parent creates the model
22
+ * via `useModelRef`, children subscribe to individual fields via `useField`.
23
+ */
24
+ export declare function useModelRef<M extends Model<any>>(factory: () => M): M;
16
25
  /** Return type of `useField`, providing a single field's value, error, and setter. */
17
26
  export interface FieldHandle<V> {
18
27
  value: V;
@@ -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,CAsC5B;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;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"}
@@ -6,11 +6,15 @@ function useModel(factory) {
6
6
  if (!modelRef.current || modelRef.current.disposed) {
7
7
  modelRef.current = factory();
8
8
  }
9
- useSyncExternalStore(
9
+ const modelSubscribe = useCallback(
10
10
  (onStoreChange) => modelRef.current.subscribe(onStoreChange),
11
+ []
12
+ );
13
+ const modelSnapshot = useCallback(
11
14
  () => modelRef.current.state,
12
- () => modelRef.current.state
15
+ []
13
16
  );
17
+ useSyncExternalStore(modelSubscribe, modelSnapshot, modelSnapshot);
14
18
  useEffect(() => {
15
19
  mountedRef.current = true;
16
20
  if (isInitializable(modelRef.current)) {
@@ -34,6 +38,28 @@ function useModel(factory) {
34
38
  model
35
39
  };
36
40
  }
41
+ function useModelRef(factory) {
42
+ const modelRef = useRef(null);
43
+ const mountedRef = useRef(false);
44
+ if (!modelRef.current || modelRef.current.disposed) {
45
+ modelRef.current = factory();
46
+ }
47
+ useEffect(() => {
48
+ mountedRef.current = true;
49
+ if (isInitializable(modelRef.current)) {
50
+ modelRef.current.init();
51
+ }
52
+ return () => {
53
+ mountedRef.current = false;
54
+ setTimeout(() => {
55
+ if (!mountedRef.current) {
56
+ modelRef.current?.dispose();
57
+ }
58
+ }, 0);
59
+ };
60
+ }, []);
61
+ return modelRef.current;
62
+ }
37
63
  function useField(model, field) {
38
64
  const getSnapshot = useCallback(() => {
39
65
  return {
@@ -75,6 +101,7 @@ function useField(model, field) {
75
101
  }
76
102
  export {
77
103
  useField,
78
- useModel
104
+ useModel,
105
+ useModelRef
79
106
  };
80
107
  //# sourceMappingURL=use-model.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-model.js","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 useSyncExternalStore(\n (onStoreChange) => modelRef.current!.subscribe(onStoreChange),\n () => modelRef.current!.state,\n () => modelRef.current!.state,\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 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/** 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"],"names":[],"mappings":";;AAkBO,SAAS,SACd,SAC4B;AAC5B,QAAM,WAAW,OAAiB,IAAI;AACtC,QAAM,aAAa,OAAO,KAAK;AAE/B,MAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,UAAU;AAClD,aAAS,UAAU,QAAA;AAAA,EACrB;AAEA;AAAA,IACE,CAAC,kBAAkB,SAAS,QAAS,UAAU,aAAa;AAAA,IAC5D,MAAM,SAAS,QAAS;AAAA,IACxB,MAAM,SAAS,QAAS;AAAA,EAAA;AAG1B,YAAU,MAAM;AACd,eAAW,UAAU;AACrB,QAAI,gBAAgB,SAAS,OAAO,GAAG;AACrC,eAAS,QAAQ,KAAA;AAAA,IACnB;AACA,WAAO,MAAM;AACX,iBAAW,UAAU;AACrB,iBAAW,MAAM;AACf,YAAI,CAAC,WAAW,SAAS;AACvB,mBAAS,SAAS,QAAA;AAAA,QACpB;AAAA,MACF,GAAG,CAAC;AAAA,IACN;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAQ,SAAS;AAEvB,SAAO;AAAA,IACL,OAAO,MAAM;AAAA,IACb,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,OAAO,MAAM;AAAA,IACb;AAAA,EAAA;AAEJ;AAYO,SAAS,SACd,OACA,OACmB;AAEnB,QAAM,cAAc,YAAY,MAAM;AACpC,WAAO;AAAA,MACL,OAAO,MAAM,MAAM,KAAK;AAAA,MACxB,OAAO,MAAM,OAAO,KAAK;AAAA,IAAA;AAAA,EAE7B,GAAG,CAAC,OAAO,KAAK,CAAC;AAGjB,QAAM,YAAY,OAAO,aAAa;AAEtC,QAAM,YAAY;AAAA,IAChB,CAAC,kBAA8B;AAC7B,aAAO,MAAM,UAAU,MAAM;AAC3B,cAAM,OAAO,YAAA;AACb,cAAM,UAAU,UAAU;AAG1B,YAAI,KAAK,UAAU,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO;AAChE,oBAAU,UAAU;AACpB,wBAAA;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,CAAC,OAAO,WAAW;AAAA,EAAA;AAGrB,QAAM,WAAW;AAAA,IACf;AAAA,IACA,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,EAAA;AAGlB,QAAM,MAAM;AAAA,IACV,CAAC,UAAgB;AAGf,YAAM,UAAsB,EAAE,CAAC,KAAK,GAAG,MAAA;AACtC,YAA4D,IAAI,OAAO;AAAA,IAC1E;AAAA,IACA,CAAC,OAAO,KAAK;AAAA,EAAA;AAGf,SAAO;AAAA,IACL,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB;AAAA,EAAA;AAEJ;"}
1
+ {"version":3,"file":"use-model.js","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"],"names":[],"mappings":";;AAkBO,SAAS,SACd,SAC4B;AAC5B,QAAM,WAAW,OAAiB,IAAI;AACtC,QAAM,aAAa,OAAO,KAAK;AAE/B,MAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,UAAU;AAClD,aAAS,UAAU,QAAA;AAAA,EACrB;AAEA,QAAM,iBAAiB;AAAA,IACrB,CAAC,kBAA8B,SAAS,QAAS,UAAU,aAAa;AAAA,IACxE,CAAA;AAAA,EAAC;AAEH,QAAM,gBAAgB;AAAA,IACpB,MAAM,SAAS,QAAS;AAAA,IACxB,CAAA;AAAA,EAAC;AAEH,uBAAqB,gBAAgB,eAAe,aAAa;AAEjE,YAAU,MAAM;AACd,eAAW,UAAU;AACrB,QAAI,gBAAgB,SAAS,OAAO,GAAG;AACrC,eAAS,QAAQ,KAAA;AAAA,IACnB;AACA,WAAO,MAAM;AACX,iBAAW,UAAU;AACrB,iBAAW,MAAM;AACf,YAAI,CAAC,WAAW,SAAS;AACvB,mBAAS,SAAS,QAAA;AAAA,QACpB;AAAA,MACF,GAAG,CAAC;AAAA,IACN;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAQ,SAAS;AAEvB,SAAO;AAAA,IACL,OAAO,MAAM;AAAA,IACb,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,OAAO,MAAM;AAAA,IACb;AAAA,EAAA;AAEJ;AAUO,SAAS,YAAkC,SAAqB;AACrE,QAAM,WAAW,OAAiB,IAAI;AACtC,QAAM,aAAa,OAAO,KAAK;AAE/B,MAAI,CAAC,SAAS,WAAW,SAAS,QAAQ,UAAU;AAClD,aAAS,UAAU,QAAA;AAAA,EACrB;AAEA,YAAU,MAAM;AACd,eAAW,UAAU;AACrB,QAAI,gBAAgB,SAAS,OAAO,GAAG;AACrC,eAAS,QAAQ,KAAA;AAAA,IACnB;AACA,WAAO,MAAM;AACX,iBAAW,UAAU;AACrB,iBAAW,MAAM;AACf,YAAI,CAAC,WAAW,SAAS;AACvB,mBAAS,SAAS,QAAA;AAAA,QACpB;AAAA,MACF,GAAG,CAAC;AAAA,IACN;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO,SAAS;AAClB;AAYO,SAAS,SACd,OACA,OACmB;AAEnB,QAAM,cAAc,YAAY,MAAM;AACpC,WAAO;AAAA,MACL,OAAO,MAAM,MAAM,KAAK;AAAA,MACxB,OAAO,MAAM,OAAO,KAAK;AAAA,IAAA;AAAA,EAE7B,GAAG,CAAC,OAAO,KAAK,CAAC;AAGjB,QAAM,YAAY,OAAO,aAAa;AAEtC,QAAM,YAAY;AAAA,IAChB,CAAC,kBAA8B;AAC7B,aAAO,MAAM,UAAU,MAAM;AAC3B,cAAM,OAAO,YAAA;AACb,cAAM,UAAU,UAAU;AAG1B,YAAI,KAAK,UAAU,QAAQ,SAAS,KAAK,UAAU,QAAQ,OAAO;AAChE,oBAAU,UAAU;AACpB,wBAAA;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,CAAC,OAAO,WAAW;AAAA,EAAA;AAGrB,QAAM,WAAW;AAAA,IACf;AAAA,IACA,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,EAAA;AAGlB,QAAM,MAAM;AAAA,IACV,CAAC,UAAgB;AAGf,YAAM,UAAsB,EAAE,CAAC,KAAK,GAAG,MAAA;AACtC,YAA4D,IAAI,OAAO;AAAA,IAC1E;AAAA,IACA,CAAC,OAAO,KAAK;AAAA,EAAA;AAGf,SAAO;AAAA,IACL,OAAO,SAAS;AAAA,IAChB,OAAO,SAAS;AAAA,IAChB;AAAA,EAAA;AAEJ;"}
package/dist/react.cjs CHANGED
@@ -12,6 +12,7 @@ exports.useLocal = useLocal.useLocal;
12
12
  exports.useSingleton = useSingleton.useSingleton;
13
13
  exports.useField = useModel.useField;
14
14
  exports.useModel = useModel.useModel;
15
+ exports.useModelRef = useModel.useModelRef;
15
16
  exports.useEmit = useEventBus.useEmit;
16
17
  exports.useEvent = useEventBus.useEvent;
17
18
  exports.useTeardown = useTeardown.useTeardown;
@@ -1 +1 @@
1
- {"version":3,"file":"react.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"react.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;"}
package/dist/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useInstance } from "./react/use-instance.js";
2
2
  import { useLocal } from "./react/use-local.js";
3
3
  import { useSingleton } from "./react/use-singleton.js";
4
- import { useField, useModel } from "./react/use-model.js";
4
+ import { useField, useModel, useModelRef } from "./react/use-model.js";
5
5
  import { useEmit, useEvent } from "./react/use-event-bus.js";
6
6
  import { useTeardown } from "./react/use-teardown.js";
7
7
  import { Provider, useResolve } from "./react/provider.js";
@@ -13,6 +13,7 @@ export {
13
13
  useInstance,
14
14
  useLocal,
15
15
  useModel,
16
+ useModelRef,
16
17
  useResolve,
17
18
  useSingleton,
18
19
  useTeardown