mvc-kit 2.7.1 → 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 +20 -156
  25. package/dist/Resource.cjs.map +1 -1
  26. package/dist/Resource.d.ts +0 -2
  27. package/dist/Resource.d.ts.map +1 -1
  28. package/dist/Resource.js +20 -156
  29. package/dist/Resource.js.map +1 -1
  30. package/dist/ViewModel.cjs +177 -227
  31. package/dist/ViewModel.cjs.map +1 -1
  32. package/dist/ViewModel.d.ts +9 -12
  33. package/dist/ViewModel.d.ts.map +1 -1
  34. package/dist/ViewModel.js +177 -227
  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":"Resource.js","sources":["../src/Resource.ts"],"sourcesContent":["import { Collection } from './Collection';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport { isAbortError, classifyError } from './errors';\nimport type { Listener, TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Async tracking types ────────────────────────────────────────\n\nconst DEFAULT_TASK_STATE: TaskState = Object.freeze({ loading: false, error: null, errorCode: null });\n\nexport type ResourceAsyncMethodKeys<T> = {\n [K in Exclude<keyof T, keyof Resource<any>>]: T[K] extends (...args: any[]) => Promise<any> ? K : never;\n}[Exclude<keyof T, keyof Resource<any>>];\n\ntype ResourceAsyncMap<T> = {\n readonly [K in ResourceAsyncMethodKeys<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', 'onDispose']);\n\n// ── Resource ────────────────────────────────────────────────────\n\n/**\n * Collection + async tracking toolkit. Extends Collection with lifecycle\n * (init/dispose) and automatic async method tracking. Optionally delegates\n * to an external Collection for shared data scenarios.\n */\nexport class Resource<T extends { id: string | number }> extends Collection<T> {\n private _external: Collection<T> | null = null;\n private _initialized = false;\n\n // ── Async tracking fields ──\n private _asyncStates = new Map<string, InternalTaskState>();\n private _asyncSnapshots = new Map<string, TaskState>();\n private _asyncListeners = new Set<() => void>();\n private _asyncProxy: ResourceAsyncMap<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(collectionOrItems?: Collection<T> | T[]) {\n const isExternal = collectionOrItems != null && !Array.isArray(collectionOrItems);\n super(isExternal ? [] : (collectionOrItems as T[]) ?? []);\n\n if (isExternal) {\n this._external = collectionOrItems as Collection<T>;\n\n if (__DEV__) {\n const Ctor = this.constructor as typeof Resource;\n if (Ctor.MAX_SIZE > 0 || Ctor.TTL > 0) {\n console.warn(\n `[mvc-kit] Resource \"${Ctor.name}\" has MAX_SIZE or TTL set but uses an ` +\n `injected Collection. Configure these on the Collection instead.`\n );\n }\n }\n }\n\n this._guardReservedKeys();\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\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._wrapMethods();\n return this.onInit?.();\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n\n // ── Collection delegation ─────────────────────────────────────\n\n get state(): T[] {\n return this._external ? this._external.state : super.state;\n }\n\n get items(): T[] {\n return this._external ? this._external.items : super.items;\n }\n\n get length(): number {\n return this._external ? this._external.length : super.length;\n }\n\n add(...items: T[]): void {\n this._external ? this._external.add(...items) : super.add(...items);\n }\n\n upsert(...items: T[]): void {\n this._external ? this._external.upsert(...items) : super.upsert(...items);\n }\n\n update(id: T['id'], changes: Partial<T>): void {\n this._external ? this._external.update(id, changes) : super.update(id, changes);\n }\n\n remove(...ids: T['id'][]): void {\n this._external ? this._external.remove(...ids) : super.remove(...ids);\n }\n\n reset(items: T[]): void {\n this._external ? this._external.reset(items) : super.reset(items);\n }\n\n clear(): void {\n this._external ? this._external.clear() : super.clear();\n }\n\n optimistic(callback: () => void): () => void {\n return this._external ? this._external.optimistic(callback) : super.optimistic(callback);\n }\n\n get(id: T['id']): T | undefined {\n return this._external ? this._external.get(id) : super.get(id);\n }\n\n has(id: T['id']): boolean {\n return this._external ? this._external.has(id) : super.has(id);\n }\n\n find(predicate: (item: T) => boolean): T | undefined {\n return this._external ? this._external.find(predicate) : super.find(predicate);\n }\n\n filter(predicate: (item: T) => boolean): T[] {\n return this._external ? this._external.filter(predicate) : super.filter(predicate);\n }\n\n sorted(compareFn: (a: T, b: T) => number): T[] {\n return this._external ? this._external.sorted(compareFn) : super.sorted(compareFn);\n }\n\n map<U>(fn: (item: T) => U): U[] {\n return this._external ? this._external.map(fn) : super.map(fn);\n }\n\n subscribe(listener: Listener<T[]>): () => void {\n if (this.disposed) return () => {};\n return this._external ? this._external.subscribe(listener) : super.subscribe(listener);\n }\n\n // ── Async tracking API ────────────────────────────────────────\n\n /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */\n get async(): ResourceAsyncMap<this> {\n if (!this._asyncProxy) {\n const self = this;\n this._asyncProxy = new Proxy({} as ResourceAsyncMap<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 `useInstance` 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: async tracking internals ─────────────────────────\n\n private _notifyAsync(): void {\n for (const listener of this._asyncListeners) {\n listener();\n }\n }\n\n private _guardReservedKeys(): void {\n walkPrototypeChain(this, Resource.prototype, (key) => {\n if (RESERVED_ASYNC_KEYS.includes(key as any)) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on Resource 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 Resource 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, Resource.prototype, (key, desc) => {\n // Skip getters/setters\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 Resource was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, (this.constructor as typeof Resource).GHOST_TIMEOUT);\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Resource.js","sources":["../src/Resource.ts"],"sourcesContent":["import { Collection } from './Collection';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport { wrapAsyncMethods } from './wrapAsyncMethods';\nimport type { InternalTaskState } from './wrapAsyncMethods';\nimport type { Listener, TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Async tracking types ────────────────────────────────────────\n\nconst DEFAULT_TASK_STATE: TaskState = Object.freeze({ loading: false, error: null, errorCode: null });\n\nexport type ResourceAsyncMethodKeys<T> = {\n [K in Exclude<keyof T, keyof Resource<any>>]: T[K] extends (...args: any[]) => Promise<any> ? K : never;\n}[Exclude<keyof T, keyof Resource<any>>];\n\ntype ResourceAsyncMap<T> = {\n readonly [K in ResourceAsyncMethodKeys<T>]: TaskState;\n};\n\nconst RESERVED_ASYNC_KEYS = ['async', 'subscribeAsync'] as const;\nconst LIFECYCLE_HOOKS = new Set(['onInit', 'onDispose']);\n\n// ── Resource ────────────────────────────────────────────────────\n\n/**\n * Collection + async tracking toolkit. Extends Collection with lifecycle\n * (init/dispose) and automatic async method tracking. Optionally delegates\n * to an external Collection for shared data scenarios.\n */\nexport class Resource<T extends { id: string | number }> extends Collection<T> {\n private _external: Collection<T> | null = null;\n private _initialized = false;\n\n // ── Async tracking fields ──\n private _asyncStates = new Map<string, InternalTaskState>();\n private _asyncSnapshots = new Map<string, TaskState>();\n private _asyncListeners = new Set<() => void>();\n private _asyncProxy: ResourceAsyncMap<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(collectionOrItems?: Collection<T> | T[]) {\n const isExternal = collectionOrItems != null && !Array.isArray(collectionOrItems);\n super(isExternal ? [] : (collectionOrItems as T[]) ?? []);\n\n if (isExternal) {\n this._external = collectionOrItems as Collection<T>;\n\n if (__DEV__) {\n const Ctor = this.constructor as typeof Resource;\n if (Ctor.MAX_SIZE > 0 || Ctor.TTL > 0) {\n console.warn(\n `[mvc-kit] Resource \"${Ctor.name}\" has MAX_SIZE or TTL set but uses an ` +\n `injected Collection. Configure these on the Collection instead.`\n );\n }\n }\n }\n\n this._guardReservedKeys();\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\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\n if (__DEV__) {\n this._activeOps = new Map();\n }\n\n wrapAsyncMethods({\n instance: this,\n stopPrototype: Resource.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 Resource).GHOST_TIMEOUT,\n className: 'Resource',\n activeOps: this._activeOps,\n });\n\n return this.onInit?.();\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n\n // ── Collection delegation ─────────────────────────────────────\n\n get state(): T[] {\n return this._external ? this._external.state : super.state;\n }\n\n get items(): T[] {\n return this._external ? this._external.items : super.items;\n }\n\n get length(): number {\n return this._external ? this._external.length : super.length;\n }\n\n add(...items: T[]): void {\n this._external ? this._external.add(...items) : super.add(...items);\n }\n\n upsert(...items: T[]): void {\n this._external ? this._external.upsert(...items) : super.upsert(...items);\n }\n\n update(id: T['id'], changes: Partial<T>): void {\n this._external ? this._external.update(id, changes) : super.update(id, changes);\n }\n\n remove(...ids: T['id'][]): void {\n this._external ? this._external.remove(...ids) : super.remove(...ids);\n }\n\n reset(items: T[]): void {\n this._external ? this._external.reset(items) : super.reset(items);\n }\n\n clear(): void {\n this._external ? this._external.clear() : super.clear();\n }\n\n optimistic(callback: () => void): () => void {\n return this._external ? this._external.optimistic(callback) : super.optimistic(callback);\n }\n\n get(id: T['id']): T | undefined {\n return this._external ? this._external.get(id) : super.get(id);\n }\n\n has(id: T['id']): boolean {\n return this._external ? this._external.has(id) : super.has(id);\n }\n\n find(predicate: (item: T) => boolean): T | undefined {\n return this._external ? this._external.find(predicate) : super.find(predicate);\n }\n\n filter(predicate: (item: T) => boolean): T[] {\n return this._external ? this._external.filter(predicate) : super.filter(predicate);\n }\n\n sorted(compareFn: (a: T, b: T) => number): T[] {\n return this._external ? this._external.sorted(compareFn) : super.sorted(compareFn);\n }\n\n map<U>(fn: (item: T) => U): U[] {\n return this._external ? this._external.map(fn) : super.map(fn);\n }\n\n subscribe(listener: Listener<T[]>): () => void {\n if (this.disposed) return () => {};\n return this._external ? this._external.subscribe(listener) : super.subscribe(listener);\n }\n\n // ── Async tracking API ────────────────────────────────────────\n\n /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */\n get async(): ResourceAsyncMap<this> {\n if (!this._asyncProxy) {\n const self = this;\n this._asyncProxy = new Proxy({} as ResourceAsyncMap<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 `useInstance` 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: async tracking internals ─────────────────────────\n\n private _notifyAsync(): void {\n for (const listener of this._asyncListeners) {\n listener();\n }\n }\n\n private _guardReservedKeys(): void {\n walkPrototypeChain(this, Resource.prototype, (key) => {\n if (RESERVED_ASYNC_KEYS.includes(key as any)) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on Resource and cannot be overridden.`\n );\n }\n });\n }\n\n}\n"],"names":[],"mappings":"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"}
@@ -1,9 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const EventBus = require("./EventBus.cjs");
4
- const errors = require("./errors.cjs");
5
4
  const walkPrototypeChain = require("./walkPrototypeChain.cjs");
5
+ const wrapAsyncMethods = require("./wrapAsyncMethods.cjs");
6
6
  const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
7
+ function freeze(obj) {
8
+ return __DEV__ ? Object.freeze(obj) : obj;
9
+ }
10
+ const classMembers = /* @__PURE__ */ new WeakMap();
11
+ function getClassMemberInfo(instance, stopPrototype, reservedKeys, lifecycleHooks) {
12
+ const ctor = instance.constructor;
13
+ let info = classMembers.get(ctor);
14
+ if (info) return info;
15
+ const getters = [];
16
+ const methods = [];
17
+ const found = [];
18
+ const processedGetters = /* @__PURE__ */ new Set();
19
+ const processedMethods = /* @__PURE__ */ new Set();
20
+ walkPrototypeChain.walkPrototypeChain(instance, stopPrototype, (key, desc) => {
21
+ if (reservedKeys.includes(key)) {
22
+ found.push(key);
23
+ }
24
+ if (desc.get && !processedGetters.has(key)) {
25
+ processedGetters.add(key);
26
+ getters.push({ key, get: desc.get });
27
+ }
28
+ if (!desc.get && !desc.set && typeof desc.value === "function" && !key.startsWith("_") && !lifecycleHooks.has(key) && !processedMethods.has(key)) {
29
+ processedMethods.add(key);
30
+ methods.push({ key, fn: desc.value });
31
+ }
32
+ });
33
+ info = { getters, methods, reservedKeys: found };
34
+ classMembers.set(ctor, info);
35
+ return info;
36
+ }
7
37
  function isAutoTrackable(value) {
8
38
  return value !== null && typeof value === "object" && typeof value.subscribe === "function";
9
39
  }
@@ -26,16 +56,17 @@ class ViewModel {
26
56
  _sourceTracking = null;
27
57
  _trackedSources = /* @__PURE__ */ new Map();
28
58
  // ── Async tracking (RFC 2) ──────────────────────────────────────
29
- _asyncStates = /* @__PURE__ */ new Map();
30
- _asyncSnapshots = /* @__PURE__ */ new Map();
31
- _asyncListeners = /* @__PURE__ */ new Set();
59
+ // Lazily allocated on first async method wrap to keep construction fast.
60
+ _asyncStates = null;
61
+ _asyncSnapshots = null;
62
+ _asyncListeners = null;
32
63
  _asyncProxy = null;
33
64
  _activeOps = null;
34
65
  /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */
35
66
  static GHOST_TIMEOUT = 3e3;
36
67
  constructor(...args) {
37
68
  const initialState = args[0] ?? {};
38
- this._state = Object.freeze({ ...initialState });
69
+ this._state = freeze({ ...initialState });
39
70
  this._initialState = this._state;
40
71
  this._guardReservedKeys();
41
72
  }
@@ -71,8 +102,7 @@ class ViewModel {
71
102
  this._initialized = true;
72
103
  this._trackSubscribables();
73
104
  this._installStateProxy();
74
- this._memoizeGetters();
75
- this._wrapMethods();
105
+ this._processMembers();
76
106
  return this.onInit?.();
77
107
  }
78
108
  /**
@@ -94,15 +124,19 @@ class ViewModel {
94
124
  return;
95
125
  }
96
126
  const partial = typeof partialOrUpdater === "function" ? partialOrUpdater(this._state) : partialOrUpdater;
97
- const keys = Object.keys(partial);
98
- const hasChanges = keys.some(
99
- (key) => partial[key] !== this._state[key]
100
- );
127
+ let hasChanges = false;
128
+ const current = this._state;
129
+ for (const key in partial) {
130
+ if (partial[key] !== current[key]) {
131
+ hasChanges = true;
132
+ break;
133
+ }
134
+ }
101
135
  if (!hasChanges) {
102
136
  return;
103
137
  }
104
138
  const prev = this._state;
105
- const next = Object.freeze({ ...prev, ...partial });
139
+ const next = freeze({ ...prev, ...partial });
106
140
  this._state = next;
107
141
  this._revision++;
108
142
  this.onSet?.(prev, next);
@@ -155,10 +189,10 @@ class ViewModel {
155
189
  this._abortController?.abort();
156
190
  this._abortController = null;
157
191
  this._teardownSubscriptions();
158
- this._state = newState ? Object.freeze({ ...newState }) : this._initialState;
192
+ this._state = newState ? freeze({ ...newState }) : this._initialState;
159
193
  this._revision++;
160
- this._asyncStates.clear();
161
- this._asyncSnapshots.clear();
194
+ this._asyncStates?.clear();
195
+ this._asyncSnapshots?.clear();
162
196
  this._notifyAsync();
163
197
  this._trackSubscribables();
164
198
  for (const listener of this._listeners) {
@@ -206,16 +240,16 @@ class ViewModel {
206
240
  const self = this;
207
241
  this._asyncProxy = new Proxy({}, {
208
242
  get(_, prop) {
209
- return self._asyncSnapshots.get(prop) ?? DEFAULT_TASK_STATE;
243
+ return self._asyncSnapshots?.get(prop) ?? DEFAULT_TASK_STATE;
210
244
  },
211
245
  has(_, prop) {
212
- return self._asyncSnapshots.has(prop);
246
+ return self._asyncSnapshots?.has(prop) ?? false;
213
247
  },
214
248
  ownKeys() {
215
- return Array.from(self._asyncSnapshots.keys());
249
+ return self._asyncSnapshots ? Array.from(self._asyncSnapshots.keys()) : [];
216
250
  },
217
251
  getOwnPropertyDescriptor(_, prop) {
218
- if (self._asyncSnapshots.has(prop)) {
252
+ if (self._asyncSnapshots?.has(prop)) {
219
253
  return { configurable: true, enumerable: true, value: self._asyncSnapshots.get(prop) };
220
254
  }
221
255
  return void 0;
@@ -228,12 +262,14 @@ class ViewModel {
228
262
  subscribeAsync(listener) {
229
263
  if (this._disposed) return () => {
230
264
  };
265
+ if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
231
266
  this._asyncListeners.add(listener);
232
267
  return () => {
233
268
  this._asyncListeners.delete(listener);
234
269
  };
235
270
  }
236
271
  _notifyAsync() {
272
+ if (!this._asyncListeners) return;
237
273
  for (const listener of this._asyncListeners) {
238
274
  listener();
239
275
  }
@@ -248,195 +284,92 @@ class ViewModel {
248
284
  }
249
285
  }
250
286
  _guardReservedKeys() {
251
- walkPrototypeChain.walkPrototypeChain(this, ViewModel.prototype, (key) => {
252
- if (RESERVED_ASYNC_KEYS.includes(key)) {
253
- throw new Error(
254
- `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
255
- );
256
- }
257
- });
287
+ const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
288
+ if (info.reservedKeys.length > 0) {
289
+ throw new Error(
290
+ `[mvc-kit] "${info.reservedKeys[0]}" is a reserved property on ViewModel and cannot be overridden.`
291
+ );
292
+ }
258
293
  }
259
- _wrapMethods() {
260
- for (const key of RESERVED_ASYNC_KEYS) {
261
- if (Object.getOwnPropertyDescriptor(this, key)?.value !== void 0) {
262
- throw new Error(
263
- `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
264
- );
294
+ // ── Member processing (merged getter memoization + async method wrapping) ──
295
+ /**
296
+ * Uses cached class metadata to memoize getters (RFC 1) and delegates
297
+ * async method wrapping to the shared wrapAsyncMethods helper (RFC 2).
298
+ * Class metadata is computed once per class via getClassMemberInfo() and reused
299
+ * across all instances — avoids repeated prototype walks.
300
+ */
301
+ _processMembers() {
302
+ const info = getClassMemberInfo(this, ViewModel.prototype, RESERVED_ASYNC_KEYS, LIFECYCLE_HOOKS);
303
+ for (let i = 0; i < info.getters.length; i++) {
304
+ this._wrapGetter(info.getters[i].key, info.getters[i].get);
305
+ }
306
+ if (__DEV__) {
307
+ for (const key of RESERVED_ASYNC_KEYS) {
308
+ if (Object.getOwnPropertyDescriptor(this, key)?.value !== void 0) {
309
+ throw new Error(
310
+ `[mvc-kit] "${key}" is a reserved property on ViewModel and cannot be overridden.`
311
+ );
312
+ }
265
313
  }
266
314
  }
267
- const self = this;
268
- const processed = /* @__PURE__ */ new Set();
269
- const wrappedKeys = [];
315
+ if (info.methods.length === 0) return;
316
+ if (!this._asyncStates) this._asyncStates = /* @__PURE__ */ new Map();
317
+ if (!this._asyncSnapshots) this._asyncSnapshots = /* @__PURE__ */ new Map();
318
+ if (!this._asyncListeners) this._asyncListeners = /* @__PURE__ */ new Set();
270
319
  if (__DEV__) {
271
320
  this._activeOps = /* @__PURE__ */ new Map();
272
321
  }
273
- walkPrototypeChain.walkPrototypeChain(this, ViewModel.prototype, (key, desc) => {
274
- if (desc.get || desc.set) return;
275
- if (typeof desc.value !== "function") return;
276
- if (key.startsWith("_")) return;
277
- if (LIFECYCLE_HOOKS.has(key)) return;
278
- if (processed.has(key)) return;
279
- processed.add(key);
280
- const original = desc.value;
281
- let pruned = false;
282
- const wrapper = function(...args) {
283
- if (self._disposed) {
284
- if (__DEV__) {
285
- console.warn(`[mvc-kit] "${key}" called after dispose — ignored.`);
286
- }
287
- return void 0;
288
- }
289
- if (__DEV__ && !self._initialized) {
290
- console.warn(
291
- `[mvc-kit] "${key}" called before init(). Async tracking is active only after init().`
292
- );
293
- }
294
- let result;
295
- try {
296
- result = original.apply(self, args);
297
- } catch (e) {
298
- throw e;
299
- }
300
- if (!result || typeof result.then !== "function") {
301
- if (!pruned) {
302
- pruned = true;
303
- self._asyncStates.delete(key);
304
- self._asyncSnapshots.delete(key);
305
- self[key] = original.bind(self);
306
- }
307
- return result;
308
- }
309
- let internal = self._asyncStates.get(key);
310
- if (!internal) {
311
- internal = { loading: false, error: null, errorCode: null, count: 0 };
312
- self._asyncStates.set(key, internal);
313
- }
314
- internal.count++;
315
- internal.loading = true;
316
- internal.error = null;
317
- internal.errorCode = null;
318
- self._asyncSnapshots.set(key, Object.freeze({ loading: true, error: null, errorCode: null }));
319
- self._notifyAsync();
320
- if (__DEV__ && self._activeOps) {
321
- self._activeOps.set(key, (self._activeOps.get(key) ?? 0) + 1);
322
- }
323
- return result.then(
324
- (value) => {
325
- if (self._disposed) return value;
326
- internal.count--;
327
- internal.loading = internal.count > 0;
328
- self._asyncSnapshots.set(
329
- key,
330
- Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
331
- );
332
- self._notifyAsync();
333
- if (__DEV__ && self._activeOps) {
334
- const c = (self._activeOps.get(key) ?? 1) - 1;
335
- if (c <= 0) self._activeOps.delete(key);
336
- else self._activeOps.set(key, c);
337
- }
338
- return value;
339
- },
340
- (error) => {
341
- if (errors.isAbortError(error)) {
342
- if (!self._disposed) {
343
- internal.count--;
344
- internal.loading = internal.count > 0;
345
- self._asyncSnapshots.set(
346
- key,
347
- Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
348
- );
349
- self._notifyAsync();
350
- }
351
- if (__DEV__ && self._activeOps) {
352
- const c = (self._activeOps.get(key) ?? 1) - 1;
353
- if (c <= 0) self._activeOps.delete(key);
354
- else self._activeOps.set(key, c);
355
- }
356
- return void 0;
357
- }
358
- if (self._disposed) return void 0;
359
- internal.count--;
360
- internal.loading = internal.count > 0;
361
- const classified = errors.classifyError(error);
362
- internal.error = classified.message;
363
- internal.errorCode = classified.code;
364
- self._asyncSnapshots.set(
365
- key,
366
- Object.freeze({ loading: internal.loading, error: classified.message, errorCode: classified.code })
367
- );
368
- self._notifyAsync();
369
- if (__DEV__ && self._activeOps) {
370
- const c = (self._activeOps.get(key) ?? 1) - 1;
371
- if (c <= 0) self._activeOps.delete(key);
372
- else self._activeOps.set(key, c);
373
- }
374
- throw error;
375
- }
376
- );
377
- };
378
- wrappedKeys.push(key);
379
- self[key] = wrapper;
322
+ wrapAsyncMethods.wrapAsyncMethods({
323
+ instance: this,
324
+ stopPrototype: ViewModel.prototype,
325
+ reservedKeys: RESERVED_ASYNC_KEYS,
326
+ lifecycleHooks: LIFECYCLE_HOOKS,
327
+ isDisposed: () => this._disposed,
328
+ isInitialized: () => this._initialized,
329
+ asyncStates: this._asyncStates,
330
+ asyncSnapshots: this._asyncSnapshots,
331
+ asyncListeners: this._asyncListeners,
332
+ notifyAsync: () => this._notifyAsync(),
333
+ addCleanup: (fn) => this.addCleanup(fn),
334
+ ghostTimeout: this.constructor.GHOST_TIMEOUT,
335
+ className: "ViewModel",
336
+ activeOps: this._activeOps,
337
+ methods: info.methods
380
338
  });
381
- if (wrappedKeys.length > 0) {
382
- this.addCleanup(() => {
383
- const opsSnapshot = __DEV__ && self._activeOps ? new Map(self._activeOps) : null;
384
- for (const k of wrappedKeys) {
385
- if (__DEV__) {
386
- self[k] = () => {
387
- console.warn(`[mvc-kit] "${k}" called after dispose — ignored.`);
388
- return void 0;
389
- };
390
- } else {
391
- self[k] = () => void 0;
392
- }
393
- }
394
- self._asyncListeners.clear();
395
- self._asyncStates.clear();
396
- self._asyncSnapshots.clear();
397
- if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {
398
- self._scheduleGhostCheck(opsSnapshot);
399
- }
400
- });
401
- }
402
- }
403
- _scheduleGhostCheck(opsSnapshot) {
404
- if (!__DEV__) return;
405
- setTimeout(() => {
406
- for (const [key, count] of opsSnapshot) {
407
- console.warn(
408
- `[mvc-kit] Ghost async operation detected: "${key}" had ${count} pending call(s) when the ViewModel was disposed. Consider using disposeSignal to cancel in-flight work.`
409
- );
410
- }
411
- }, this.constructor.GHOST_TIMEOUT);
412
339
  }
413
340
  // ── Auto-tracking internals ────────────────────────────────────
414
341
  /**
415
342
  * Installs a context-sensitive state getter on the instance.
416
343
  *
417
344
  * During getter tracking (_stateTracking is active): returns a Proxy
418
- * that records which state properties are accessed.
345
+ * that records which state properties are accessed. The Proxy is created
346
+ * lazily on first tracking access to keep init() fast.
419
347
  *
420
348
  * Otherwise: returns the frozen state object directly. This is critical
421
349
  * for React's useSyncExternalStore — it needs a changing reference to
422
350
  * detect state updates and trigger re-renders.
423
351
  */
424
352
  _installStateProxy() {
425
- const stateProxy = new Proxy({}, {
426
- get: (_, prop) => {
427
- this._stateTracking?.add(prop);
428
- return this._state[prop];
429
- },
430
- ownKeys: () => Reflect.ownKeys(this._state),
431
- getOwnPropertyDescriptor: (_, prop) => Reflect.getOwnPropertyDescriptor(this._state, prop),
432
- set: () => {
433
- throw new Error("Cannot mutate state directly. Use set() instead.");
434
- },
435
- has: (_, prop) => prop in this._state
436
- });
353
+ let stateProxy = null;
437
354
  Object.defineProperty(this, "state", {
438
355
  get: () => {
439
- if (this._stateTracking) return stateProxy;
356
+ if (this._stateTracking) {
357
+ if (!stateProxy) {
358
+ stateProxy = new Proxy({}, {
359
+ get: (_, prop) => {
360
+ this._stateTracking?.add(prop);
361
+ return this._state[prop];
362
+ },
363
+ ownKeys: () => Reflect.ownKeys(this._state),
364
+ getOwnPropertyDescriptor: (_, prop) => Reflect.getOwnPropertyDescriptor(this._state, prop),
365
+ set: () => {
366
+ throw new Error("Cannot mutate state directly. Use set() instead.");
367
+ },
368
+ has: (_, prop) => prop in this._state
369
+ });
370
+ }
371
+ return stateProxy;
372
+ }
440
373
  return this._state;
441
374
  },
442
375
  configurable: true,
@@ -467,7 +400,6 @@ class ViewModel {
467
400
  if (this._disposed) return;
468
401
  tracked.revision++;
469
402
  this._revision++;
470
- this._state = Object.freeze({ ...this._state });
471
403
  for (const listener of this._listeners) {
472
404
  listener(this._state, this._state);
473
405
  }
@@ -493,22 +425,6 @@ class ViewModel {
493
425
  });
494
426
  }
495
427
  }
496
- /**
497
- * Walks the prototype chain from the subclass up to (but not including)
498
- * ViewModel.prototype. For every getter found, replaces it on the
499
- * instance with a memoized version that tracks dependencies and caches.
500
- *
501
- * Processing order: most-derived class first. If a subclass overrides
502
- * a parent getter, only the subclass version is memoized.
503
- */
504
- _memoizeGetters() {
505
- const processed = /* @__PURE__ */ new Set();
506
- walkPrototypeChain.walkPrototypeChain(this, ViewModel.prototype, (key, desc) => {
507
- if (!desc.get || processed.has(key)) return;
508
- processed.add(key);
509
- this._wrapGetter(key, desc.get);
510
- });
511
- }
512
428
  /**
513
429
  * Replaces a single prototype getter with a memoized version on this
514
430
  * instance. The memoized getter tracks both state dependencies and
@@ -522,27 +438,31 @@ class ViewModel {
522
438
  _wrapGetter(key, original) {
523
439
  let cached;
524
440
  let validatedAtRevision = -1;
525
- let stateDeps;
526
- let stateSnapshot;
527
- let sourceDeps;
441
+ let stateDepKeys;
442
+ let stateDepValues;
443
+ let sourceDepKeys;
444
+ let sourceDepRevisions;
445
+ let trackingSet;
446
+ let trackingMap;
528
447
  Object.defineProperty(this, key, {
529
448
  get: () => {
530
- if (this._disposed) return cached;
531
449
  if (validatedAtRevision === this._revision) {
532
450
  return cached;
533
451
  }
534
- if (stateDeps && stateSnapshot) {
452
+ if (this._disposed) return cached;
453
+ if (stateDepKeys !== void 0) {
535
454
  let fresh = true;
536
- for (const [k, v] of stateSnapshot) {
537
- if (this._state[k] !== v) {
455
+ const state = this._state;
456
+ for (let i = 0; i < stateDepKeys.length; i++) {
457
+ if (state[stateDepKeys[i]] !== stateDepValues[i]) {
538
458
  fresh = false;
539
459
  break;
540
460
  }
541
461
  }
542
- if (fresh && sourceDeps) {
543
- for (const [memberKey, rev] of sourceDeps) {
544
- const ts = this._trackedSources.get(memberKey);
545
- if (ts && ts.revision !== rev) {
462
+ if (fresh && sourceDepKeys !== void 0 && sourceDepKeys.length > 0) {
463
+ for (let i = 0; i < sourceDepKeys.length; i++) {
464
+ const ts = this._trackedSources.get(sourceDepKeys[i]);
465
+ if (ts && ts.revision !== sourceDepRevisions[i]) {
546
466
  fresh = false;
547
467
  break;
548
468
  }
@@ -555,8 +475,18 @@ class ViewModel {
555
475
  }
556
476
  const parentStateTracking = this._stateTracking;
557
477
  const parentSourceTracking = this._sourceTracking;
558
- this._stateTracking = /* @__PURE__ */ new Set();
559
- this._sourceTracking = /* @__PURE__ */ new Map();
478
+ if (trackingSet) {
479
+ trackingSet.clear();
480
+ } else {
481
+ trackingSet = /* @__PURE__ */ new Set();
482
+ }
483
+ if (trackingMap) {
484
+ trackingMap.clear();
485
+ } else {
486
+ trackingMap = /* @__PURE__ */ new Map();
487
+ }
488
+ this._stateTracking = trackingSet;
489
+ this._sourceTracking = trackingMap;
560
490
  try {
561
491
  cached = original.call(this);
562
492
  } catch (e) {
@@ -564,25 +494,45 @@ class ViewModel {
564
494
  this._sourceTracking = parentSourceTracking;
565
495
  throw e;
566
496
  }
567
- stateDeps = this._stateTracking;
568
- const capturedSourceDeps = this._sourceTracking;
569
497
  this._stateTracking = parentStateTracking;
570
498
  this._sourceTracking = parentSourceTracking;
571
499
  if (parentStateTracking) {
572
- for (const d of stateDeps) parentStateTracking.add(d);
500
+ for (const d of trackingSet) parentStateTracking.add(d);
573
501
  }
574
502
  if (parentSourceTracking) {
575
- for (const [k, v] of capturedSourceDeps) {
503
+ for (const [k, v] of trackingMap) {
576
504
  parentSourceTracking.set(k, v);
577
505
  }
578
506
  }
579
- stateSnapshot = /* @__PURE__ */ new Map();
580
- for (const d of stateDeps) {
581
- stateSnapshot.set(d, this._state[d]);
507
+ const depCount = trackingSet.size;
508
+ if (!stateDepKeys || stateDepKeys.length !== depCount) {
509
+ stateDepKeys = new Array(depCount);
510
+ stateDepValues = new Array(depCount);
582
511
  }
583
- sourceDeps = /* @__PURE__ */ new Map();
584
- for (const [memberKey, tracked] of capturedSourceDeps) {
585
- sourceDeps.set(memberKey, tracked.revision);
512
+ {
513
+ let i = 0;
514
+ const state = this._state;
515
+ for (const d of trackingSet) {
516
+ stateDepKeys[i] = d;
517
+ stateDepValues[i] = state[d];
518
+ i++;
519
+ }
520
+ }
521
+ const sourceCount = trackingMap.size;
522
+ if (sourceCount > 0) {
523
+ if (!sourceDepKeys || sourceDepKeys.length !== sourceCount) {
524
+ sourceDepKeys = new Array(sourceCount);
525
+ sourceDepRevisions = new Array(sourceCount);
526
+ }
527
+ let i = 0;
528
+ for (const [memberKey, tracked] of trackingMap) {
529
+ sourceDepKeys[i] = memberKey;
530
+ sourceDepRevisions[i] = tracked.revision;
531
+ i++;
532
+ }
533
+ } else {
534
+ sourceDepKeys = void 0;
535
+ sourceDepRevisions = void 0;
586
536
  }
587
537
  validatedAtRevision = this._revision;
588
538
  return cached;