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.
- package/README.md +18 -1
- package/agent-config/claude-code/skills/guide/SKILL.md +1 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +8 -1
- package/agent-config/claude-code/skills/scaffold/templates/model.md +38 -1
- package/agent-config/copilot/copilot-instructions.md +2 -1
- package/agent-config/cursor/cursorrules +2 -1
- package/dist/Collection.cjs +31 -17
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +31 -17
- package/dist/Collection.js.map +1 -1
- package/dist/Model.cjs +22 -4
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.d.ts +2 -0
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +22 -4
- package/dist/Model.js.map +1 -1
- package/dist/PersistentCollection.cjs +8 -10
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.d.ts +1 -0
- package/dist/PersistentCollection.d.ts.map +1 -1
- package/dist/PersistentCollection.js +8 -10
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +21 -157
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.d.ts +1 -3
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +21 -157
- package/dist/Resource.js.map +1 -1
- package/dist/ViewModel.cjs +178 -228
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.d.ts +10 -13
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +178 -228
- package/dist/ViewModel.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/use-instance.cjs +31 -21
- package/dist/react/use-instance.cjs.map +1 -1
- package/dist/react/use-instance.d.ts +1 -1
- package/dist/react/use-instance.d.ts.map +1 -1
- package/dist/react/use-instance.js +32 -22
- package/dist/react/use-instance.js.map +1 -1
- package/dist/react/use-model.cjs +29 -2
- package/dist/react/use-model.cjs.map +1 -1
- package/dist/react/use-model.d.ts +9 -0
- package/dist/react/use-model.d.ts.map +1 -1
- package/dist/react/use-model.js +30 -3
- package/dist/react/use-model.js.map +1 -1
- package/dist/react.cjs +1 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +2 -1
- package/dist/walkPrototypeChain.cjs.map +1 -1
- package/dist/walkPrototypeChain.d.ts +2 -2
- package/dist/walkPrototypeChain.js.map +1 -1
- package/dist/wrapAsyncMethods.cjs +179 -0
- package/dist/wrapAsyncMethods.cjs.map +1 -0
- package/dist/wrapAsyncMethods.d.ts +35 -0
- package/dist/wrapAsyncMethods.d.ts.map +1 -0
- package/dist/wrapAsyncMethods.js +179 -0
- package/dist/wrapAsyncMethods.js.map +1 -0
- package/package.json +1 -1
package/dist/Collection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collection.js","sources":["../src/Collection.ts"],"sourcesContent":["import type { Listener, Subscribable } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\ntype CollectionState<T> = T[];\ntype CollectionListener<T> = Listener<CollectionState<T>>;\n\n/**\n * Reactive typed array with CRUD and query methods.\n */\nexport class Collection<T extends { id: string | number }> implements Subscribable<CollectionState<T>> {\n /** Maximum number of items before FIFO eviction. 0 = unlimited. */\n static MAX_SIZE = 0;\n /** Time-to-live in milliseconds. 0 = no expiry. */\n static TTL = 0;\n\n private _items: readonly T[] = [];\n private _disposed = false;\n private _listeners = new Set<CollectionListener<T>>();\n private _index = new Map<T['id'], T>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _timestamps: Map<T['id'], number> | null = null;\n private _evictionTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n let result = [...initialItems];\n\n if (this._ttl > 0) {\n this._timestamps = new Map();\n const now = Date.now();\n for (const item of result) {\n this._timestamps.set(item.id, now);\n }\n }\n\n if (this._maxSize > 0 && result.length > this._maxSize) {\n // FIFO: trim from the front (oldest items)\n const excess = result.length - this._maxSize;\n const evicted = result.slice(0, excess);\n result = result.slice(excess);\n for (const item of evicted) {\n this._timestamps?.delete(item.id);\n }\n }\n\n this._items = Object.freeze(result);\n this.rebuildIndex();\n this._scheduleEvictionTimer();\n }\n\n /**\n * Alias for Subscribable compatibility.\n */\n get state(): T[] {\n return this._items as T[];\n }\n\n /** The raw array of items. */\n get items(): T[] {\n return this._items as T[];\n }\n\n /** Number of items in the collection. */\n get length(): number {\n return this._items.length;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\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 // ── Config Accessors ──\n\n private get _maxSize(): number {\n return (this.constructor as typeof Collection).MAX_SIZE;\n }\n\n private get _ttl(): number {\n return (this.constructor as typeof Collection).TTL;\n }\n\n // ── CRUD Methods (notify listeners) ──\n\n /**\n * Add one or more items. Items with existing IDs are silently skipped.\n */\n add(...items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot add to disposed Collection');\n }\n\n if (items.length === 0) {\n return;\n }\n\n const seen = new Set<T['id']>();\n const newItems: T[] = [];\n for (const item of items) {\n if (!this._index.has(item.id) && !seen.has(item.id)) {\n newItems.push(item);\n seen.add(item.id);\n }\n }\n if (newItems.length === 0) return;\n\n const prev = this._items;\n let result = [...prev, ...newItems];\n\n for (const item of newItems) {\n this._index.set(item.id, item);\n }\n\n // Record timestamps for TTL\n if (this._timestamps) {\n const now = Date.now();\n for (const item of newItems) {\n this._timestamps.set(item.id, now);\n }\n }\n\n // Enforce capacity before freeze/notify\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = Object.freeze(result);\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Add or replace items by ID. Existing items are replaced in-place\n * (preserving array position); new items are appended. Deduplicates\n * input — last occurrence wins. No-op if nothing changed (reference\n * comparison).\n */\n upsert(...items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot upsert on disposed Collection');\n }\n if (items.length === 0) return;\n\n // Deduplicate input — last occurrence wins\n const incoming = new Map<T['id'], T>();\n for (const item of items) {\n incoming.set(item.id, item);\n }\n\n const prev = this._items;\n let changed = false;\n const replaced = new Set<T['id']>();\n const newArray: T[] = [];\n\n // Replace existing items in-place\n for (const existing of prev) {\n if (incoming.has(existing.id)) {\n const replacement = incoming.get(existing.id)!;\n if (replacement !== existing) changed = true;\n newArray.push(replacement);\n replaced.add(existing.id);\n } else {\n newArray.push(existing);\n }\n }\n\n // Append genuinely new items\n for (const [id, item] of incoming) {\n if (!replaced.has(id)) {\n newArray.push(item);\n changed = true;\n }\n }\n\n if (!changed) return;\n\n // Record/refresh timestamps for TTL (upsert refreshes existing)\n if (this._timestamps) {\n const now = Date.now();\n for (const [id] of incoming) {\n this._timestamps.set(id, now);\n }\n }\n\n for (const [id, item] of incoming) {\n this._index.set(id, item);\n }\n\n // Enforce capacity before freeze/notify\n let result = newArray;\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = Object.freeze(result);\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Remove items by id(s).\n */\n remove(...ids: T['id'][]): void {\n if (this._disposed) {\n throw new Error('Cannot remove from disposed Collection');\n }\n\n if (ids.length === 0) {\n return;\n }\n\n const idSet = new Set(ids);\n const filtered = this._items.filter(item => !idSet.has(item.id));\n\n if (filtered.length === this._items.length) {\n return; // No items removed\n }\n\n const prev = this._items;\n this._items = Object.freeze(filtered);\n\n for (const id of ids) {\n this._index.delete(id);\n this._timestamps?.delete(id);\n }\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Update an item by id with partial changes.\n */\n update(id: T['id'], changes: Partial<T>): void {\n if (this._disposed) {\n throw new Error('Cannot update disposed Collection');\n }\n\n const idx = this._items.findIndex(item => item.id === id);\n if (idx === -1) {\n return;\n }\n\n const existing = this._items[idx];\n const updated = { ...existing, ...changes, id }; // Ensure id is preserved\n\n // Check if anything actually changed\n const keys = Object.keys(changes) as (keyof T)[];\n const hasChanges = keys.some(key => changes[key] !== existing[key]);\n if (!hasChanges) {\n return;\n }\n\n const prev = this._items;\n const newItems = [...prev];\n newItems[idx] = updated;\n this._items = Object.freeze(newItems);\n this._index.set(id, updated);\n\n this.notify(prev);\n }\n\n /**\n * Replace all items.\n */\n reset(items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot reset disposed Collection');\n }\n\n const prev = this._items;\n\n // Record timestamps for TTL\n if (this._timestamps) {\n this._timestamps.clear();\n const now = Date.now();\n for (const item of items) {\n this._timestamps.set(item.id, now);\n }\n }\n\n let result = [...items];\n\n // Enforce capacity before freeze/notify\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = Object.freeze(result);\n this.rebuildIndex();\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Remove all items.\n */\n clear(): void {\n if (this._disposed) {\n throw new Error('Cannot clear disposed Collection');\n }\n\n if (this._items.length === 0) {\n return;\n }\n\n const prev = this._items;\n this._items = Object.freeze([]);\n this._index.clear();\n this._timestamps?.clear();\n this._clearEvictionTimer();\n\n this.notify(prev);\n }\n\n /**\n * Snapshot current state, apply callback mutations, and return a rollback function.\n * Rollback restores items to pre-callback state regardless of later mutations.\n */\n optimistic(callback: () => void): () => void {\n if (this._disposed) {\n throw new Error('Cannot perform optimistic update on disposed Collection');\n }\n\n const snapshot = this._items;\n const timestampSnapshot = this._timestamps ? new Map(this._timestamps) : null;\n callback();\n\n let rolledBack = false;\n return () => {\n if (rolledBack || this._disposed) return;\n rolledBack = true;\n\n const prev = this._items;\n this._items = snapshot;\n if (timestampSnapshot) {\n this._timestamps = timestampSnapshot;\n }\n this.rebuildIndex();\n this.notify(prev);\n this._scheduleEvictionTimer();\n };\n }\n\n // ── Query Methods (pure, no notification) ──\n\n /**\n * Get item by id.\n */\n get(id: T['id']): T | undefined {\n return this._index.get(id);\n }\n\n /**\n * Check if item exists by id.\n */\n has(id: T['id']): boolean {\n return this._index.has(id);\n }\n\n /**\n * Find first item matching predicate.\n */\n find(predicate: (item: T) => boolean): T | undefined {\n return this._items.find(predicate);\n }\n\n /**\n * Filter items matching predicate.\n */\n filter(predicate: (item: T) => boolean): T[] {\n return this._items.filter(predicate) as T[];\n }\n\n /**\n * Return sorted copy.\n */\n sorted(compareFn: (a: T, b: T) => number): T[] {\n return [...this._items].sort(compareFn);\n }\n\n /**\n * Map items to new array.\n */\n map<U>(fn: (item: T) => U): U[] {\n return this._items.map(fn);\n }\n\n // ── Subscribable interface ──\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: CollectionListener<T>): () => 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 this._clearEvictionTimer();\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n this._index.clear();\n this._timestamps?.clear();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n /**\n * Called before items are auto-evicted by capacity or TTL.\n * Override to filter which items get evicted, or veto entirely.\n *\n * @param items - Candidates for eviction\n * @param reason - Why eviction is happening\n * @returns void to proceed with all, false to veto, or T[] subset to evict only those\n */\n protected onEvict?(items: T[], reason: 'capacity' | 'ttl'): T[] | false | void;\n\n private notify(prev: readonly T[]): void {\n for (const listener of this._listeners) {\n listener(this._items as T[], prev as T[]);\n }\n }\n\n private rebuildIndex(): void {\n this._index.clear();\n for (const item of this._items) {\n this._index.set(item.id, item);\n }\n }\n\n // ── Eviction Internals ──\n\n private _evictForCapacity(items: T[]): T[] {\n const excess = items.length - this._maxSize;\n if (excess <= 0) return items;\n\n const candidates = items.slice(0, excess);\n const toEvict = this._applyOnEvict(candidates, 'capacity');\n\n if (toEvict === false) return items; // veto\n\n if (toEvict.length === 0) return items; // nothing to evict\n\n const evictIds = new Set(toEvict.map(item => item.id));\n const result = items.filter(item => !evictIds.has(item.id));\n\n // Clean up index and timestamps for evicted items\n for (const item of toEvict) {\n this._index.delete(item.id);\n this._timestamps?.delete(item.id);\n }\n\n return result;\n }\n\n private _applyOnEvict(candidates: T[], reason: 'capacity' | 'ttl'): T[] | false {\n if (!this.onEvict) return candidates;\n\n const result = this.onEvict(candidates, reason);\n if (result === false) {\n // DEV warning when veto causes collection to exceed 2x MAX_SIZE\n if (__DEV__ && reason === 'capacity' && this._maxSize > 0) {\n const currentSize = this._items.length + candidates.length;\n if (currentSize > this._maxSize * 2) {\n console.warn(\n `[mvc-kit] Collection exceeded 2x MAX_SIZE (${currentSize}/${this._maxSize}). ` +\n `onEvict is vetoing eviction — this may cause unbounded growth.`\n );\n }\n }\n return false;\n }\n if (Array.isArray(result)) {\n // Only include items that are actually in the current items\n const candidateIds = new Set(candidates.map(c => c.id));\n return result.filter(item => candidateIds.has(item.id));\n }\n return candidates; // void = proceed with all\n }\n\n private _sweepExpired(): void {\n if (this._disposed || !this._timestamps || this._ttl <= 0) return;\n\n const now = Date.now();\n const ttl = this._ttl;\n const expired: T[] = [];\n\n for (const item of this._items) {\n const ts = this._timestamps.get(item.id);\n if (ts !== undefined && (now - ts) >= ttl) {\n expired.push(item);\n }\n }\n\n if (expired.length === 0) {\n this._scheduleEvictionTimer();\n return;\n }\n\n const toEvict = this._applyOnEvict(expired, 'ttl');\n\n if (toEvict === false) {\n this._scheduleEvictionTimer();\n return;\n }\n\n if (toEvict.length === 0) {\n this._scheduleEvictionTimer();\n return;\n }\n\n const evictIds = new Set(toEvict.map(item => item.id));\n const prev = this._items;\n this._items = Object.freeze(\n (prev as T[]).filter((item: T) => !evictIds.has(item.id))\n );\n\n for (const item of toEvict) {\n this._index.delete(item.id);\n this._timestamps.delete(item.id);\n }\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n private _scheduleEvictionTimer(): void {\n this._clearEvictionTimer();\n\n if (this._disposed || !this._timestamps || this._ttl <= 0 || this._timestamps.size === 0) return;\n\n const now = Date.now();\n const ttl = this._ttl;\n let earliest = Infinity;\n\n for (const ts of this._timestamps.values()) {\n if (ts < earliest) earliest = ts;\n }\n\n const delay = Math.max(0, (earliest + ttl) - now);\n this._evictionTimer = setTimeout(() => this._sweepExpired(), delay);\n }\n\n private _clearEvictionTimer(): void {\n if (this._evictionTimer !== null) {\n clearTimeout(this._evictionTimer);\n this._evictionTimer = null;\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Collection.js","sources":["../src/Collection.ts"],"sourcesContent":["import type { Listener, Subscribable } from './types';\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\ntype CollectionState<T> = T[];\ntype CollectionListener<T> = Listener<CollectionState<T>>;\n\n/**\n * Reactive typed array with CRUD and query methods.\n */\nexport class Collection<T extends { id: string | number }> implements Subscribable<CollectionState<T>> {\n /** Maximum number of items before FIFO eviction. 0 = unlimited. */\n static MAX_SIZE = 0;\n /** Time-to-live in milliseconds. 0 = no expiry. */\n static TTL = 0;\n\n private _items: readonly T[] = [];\n private _disposed = false;\n private _listeners = new Set<CollectionListener<T>>();\n private _index = new Map<T['id'], T>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _timestamps: Map<T['id'], number> | null = null;\n private _evictionTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n let result = [...initialItems];\n\n if (this._ttl > 0) {\n this._timestamps = new Map();\n const now = Date.now();\n for (const item of result) {\n this._timestamps.set(item.id, now);\n }\n }\n\n if (this._maxSize > 0 && result.length > this._maxSize) {\n // FIFO: trim from the front (oldest items)\n const excess = result.length - this._maxSize;\n const evicted = result.slice(0, excess);\n result = result.slice(excess);\n for (const item of evicted) {\n this._timestamps?.delete(item.id);\n }\n }\n\n this._items = freeze(result);\n this.rebuildIndex();\n this._scheduleEvictionTimer();\n }\n\n /**\n * Alias for Subscribable compatibility.\n */\n get state(): T[] {\n return this._items as T[];\n }\n\n /** The raw array of items. */\n get items(): T[] {\n return this._items as T[];\n }\n\n /** Number of items in the collection. */\n get length(): number {\n return this._items.length;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\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 // ── Config Accessors ──\n\n private get _maxSize(): number {\n return (this.constructor as typeof Collection).MAX_SIZE;\n }\n\n private get _ttl(): number {\n return (this.constructor as typeof Collection).TTL;\n }\n\n // ── CRUD Methods (notify listeners) ──\n\n /**\n * Add one or more items. Items with existing IDs are silently skipped.\n */\n add(...items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot add to disposed Collection');\n }\n\n if (items.length === 0) {\n return;\n }\n\n // Fast path: single item (most common case)\n if (items.length === 1) {\n const item = items[0];\n if (this._index.has(item.id)) return;\n const prev = this._items;\n let result = [...prev, item];\n this._index.set(item.id, item);\n if (this._timestamps) this._timestamps.set(item.id, Date.now());\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n this._items = freeze(result);\n this.notify(prev);\n this._scheduleEvictionTimer();\n return;\n }\n\n // Multi-item path\n const seen = new Set<T['id']>();\n const newItems: T[] = [];\n for (const item of items) {\n if (!this._index.has(item.id) && !seen.has(item.id)) {\n newItems.push(item);\n seen.add(item.id);\n }\n }\n if (newItems.length === 0) return;\n\n const prev = this._items;\n let result = [...prev, ...newItems];\n\n for (const item of newItems) {\n this._index.set(item.id, item);\n }\n\n // Record timestamps for TTL\n if (this._timestamps) {\n const now = Date.now();\n for (const item of newItems) {\n this._timestamps.set(item.id, now);\n }\n }\n\n // Enforce capacity before freeze/notify\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = freeze(result);\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Add or replace items by ID. Existing items are replaced in-place\n * (preserving array position); new items are appended. Deduplicates\n * input — last occurrence wins. No-op if nothing changed (reference\n * comparison).\n */\n upsert(...items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot upsert on disposed Collection');\n }\n if (items.length === 0) return;\n\n // Deduplicate input — last occurrence wins\n const incoming = new Map<T['id'], T>();\n for (const item of items) {\n incoming.set(item.id, item);\n }\n\n const prev = this._items;\n let changed = false;\n const replaced = new Set<T['id']>();\n const newArray: T[] = [];\n\n // Replace existing items in-place\n for (const existing of prev) {\n if (incoming.has(existing.id)) {\n const replacement = incoming.get(existing.id)!;\n if (replacement !== existing) changed = true;\n newArray.push(replacement);\n replaced.add(existing.id);\n } else {\n newArray.push(existing);\n }\n }\n\n // Append genuinely new items\n for (const [id, item] of incoming) {\n if (!replaced.has(id)) {\n newArray.push(item);\n changed = true;\n }\n }\n\n if (!changed) return;\n\n // Record/refresh timestamps for TTL (upsert refreshes existing)\n if (this._timestamps) {\n const now = Date.now();\n for (const [id] of incoming) {\n this._timestamps.set(id, now);\n }\n }\n\n for (const [id, item] of incoming) {\n this._index.set(id, item);\n }\n\n // Enforce capacity before freeze/notify\n let result = newArray;\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = freeze(result);\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Remove items by id(s).\n */\n remove(...ids: T['id'][]): void {\n if (this._disposed) {\n throw new Error('Cannot remove from disposed Collection');\n }\n\n if (ids.length === 0) {\n return;\n }\n\n const idSet = new Set(ids);\n const filtered = this._items.filter(item => !idSet.has(item.id));\n\n if (filtered.length === this._items.length) {\n return; // No items removed\n }\n\n const prev = this._items;\n this._items = freeze(filtered);\n\n for (const id of ids) {\n this._index.delete(id);\n this._timestamps?.delete(id);\n }\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Update an item by id with partial changes.\n */\n update(id: T['id'], changes: Partial<T>): void {\n if (this._disposed) {\n throw new Error('Cannot update disposed Collection');\n }\n\n // O(1) existence check via index Map\n const existing = this._index.get(id);\n if (!existing) return;\n\n // Check if anything actually changed (before any array work)\n const keys = Object.keys(changes) as (keyof T)[];\n const hasChanges = keys.some(key => changes[key] !== existing[key]);\n if (!hasChanges) return;\n\n const updated = { ...existing, ...changes, id };\n const prev = this._items;\n const idx = this._items.indexOf(existing);\n const newItems = [...prev];\n newItems[idx] = updated;\n this._items = freeze(newItems);\n this._index.set(id, updated);\n\n this.notify(prev);\n }\n\n /**\n * Replace all items.\n */\n reset(items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot reset disposed Collection');\n }\n\n const prev = this._items;\n\n // Record timestamps for TTL\n if (this._timestamps) {\n this._timestamps.clear();\n const now = Date.now();\n for (const item of items) {\n this._timestamps.set(item.id, now);\n }\n }\n\n let result = [...items];\n\n // Enforce capacity before freeze/notify\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = freeze(result);\n this.rebuildIndex();\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Remove all items.\n */\n clear(): void {\n if (this._disposed) {\n throw new Error('Cannot clear disposed Collection');\n }\n\n if (this._items.length === 0) {\n return;\n }\n\n const prev = this._items;\n this._items = freeze([] as T[]);\n this._index.clear();\n this._timestamps?.clear();\n this._clearEvictionTimer();\n\n this.notify(prev);\n }\n\n /**\n * Snapshot current state, apply callback mutations, and return a rollback function.\n * Rollback restores items to pre-callback state regardless of later mutations.\n */\n optimistic(callback: () => void): () => void {\n if (this._disposed) {\n throw new Error('Cannot perform optimistic update on disposed Collection');\n }\n\n const snapshot = this._items;\n const timestampSnapshot = this._timestamps ? new Map(this._timestamps) : null;\n callback();\n\n let rolledBack = false;\n return () => {\n if (rolledBack || this._disposed) return;\n rolledBack = true;\n\n const prev = this._items;\n this._items = snapshot;\n if (timestampSnapshot) {\n this._timestamps = timestampSnapshot;\n }\n this.rebuildIndex();\n this.notify(prev);\n this._scheduleEvictionTimer();\n };\n }\n\n // ── Query Methods (pure, no notification) ──\n\n /**\n * Get item by id.\n */\n get(id: T['id']): T | undefined {\n return this._index.get(id);\n }\n\n /**\n * Check if item exists by id.\n */\n has(id: T['id']): boolean {\n return this._index.has(id);\n }\n\n /**\n * Find first item matching predicate.\n */\n find(predicate: (item: T) => boolean): T | undefined {\n return this._items.find(predicate);\n }\n\n /**\n * Filter items matching predicate.\n */\n filter(predicate: (item: T) => boolean): T[] {\n return this._items.filter(predicate) as T[];\n }\n\n /**\n * Return sorted copy.\n */\n sorted(compareFn: (a: T, b: T) => number): T[] {\n return [...this._items].sort(compareFn);\n }\n\n /**\n * Map items to new array.\n */\n map<U>(fn: (item: T) => U): U[] {\n return this._items.map(fn);\n }\n\n // ── Subscribable interface ──\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: CollectionListener<T>): () => 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 this._clearEvictionTimer();\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n this._index.clear();\n this._timestamps?.clear();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n /**\n * Called before items are auto-evicted by capacity or TTL.\n * Override to filter which items get evicted, or veto entirely.\n *\n * @param items - Candidates for eviction\n * @param reason - Why eviction is happening\n * @returns void to proceed with all, false to veto, or T[] subset to evict only those\n */\n protected onEvict?(items: T[], reason: 'capacity' | 'ttl'): T[] | false | void;\n\n private notify(prev: readonly T[]): void {\n for (const listener of this._listeners) {\n listener(this._items as T[], prev as T[]);\n }\n }\n\n private rebuildIndex(): void {\n this._index.clear();\n for (const item of this._items) {\n this._index.set(item.id, item);\n }\n }\n\n // ── Eviction Internals ──\n\n private _evictForCapacity(items: T[]): T[] {\n const excess = items.length - this._maxSize;\n if (excess <= 0) return items;\n\n const candidates = items.slice(0, excess);\n const toEvict = this._applyOnEvict(candidates, 'capacity');\n\n if (toEvict === false) return items; // veto\n\n if (toEvict.length === 0) return items; // nothing to evict\n\n const evictIds = new Set(toEvict.map(item => item.id));\n const result = items.filter(item => !evictIds.has(item.id));\n\n // Clean up index and timestamps for evicted items\n for (const item of toEvict) {\n this._index.delete(item.id);\n this._timestamps?.delete(item.id);\n }\n\n return result;\n }\n\n private _applyOnEvict(candidates: T[], reason: 'capacity' | 'ttl'): T[] | false {\n if (!this.onEvict) return candidates;\n\n const result = this.onEvict(candidates, reason);\n if (result === false) {\n // DEV warning when veto causes collection to exceed 2x MAX_SIZE\n if (__DEV__ && reason === 'capacity' && this._maxSize > 0) {\n const currentSize = this._items.length + candidates.length;\n if (currentSize > this._maxSize * 2) {\n console.warn(\n `[mvc-kit] Collection exceeded 2x MAX_SIZE (${currentSize}/${this._maxSize}). ` +\n `onEvict is vetoing eviction — this may cause unbounded growth.`\n );\n }\n }\n return false;\n }\n if (Array.isArray(result)) {\n // Only include items that are actually in the current items\n const candidateIds = new Set(candidates.map(c => c.id));\n return result.filter(item => candidateIds.has(item.id));\n }\n return candidates; // void = proceed with all\n }\n\n private _sweepExpired(): void {\n if (this._disposed || !this._timestamps || this._ttl <= 0) return;\n\n const now = Date.now();\n const ttl = this._ttl;\n const expired: T[] = [];\n\n for (const item of this._items) {\n const ts = this._timestamps.get(item.id);\n if (ts !== undefined && (now - ts) >= ttl) {\n expired.push(item);\n }\n }\n\n if (expired.length === 0) {\n this._scheduleEvictionTimer();\n return;\n }\n\n const toEvict = this._applyOnEvict(expired, 'ttl');\n\n if (toEvict === false) {\n this._scheduleEvictionTimer();\n return;\n }\n\n if (toEvict.length === 0) {\n this._scheduleEvictionTimer();\n return;\n }\n\n const evictIds = new Set(toEvict.map(item => item.id));\n const prev = this._items;\n this._items = freeze(\n (prev as T[]).filter((item: T) => !evictIds.has(item.id))\n );\n\n for (const item of toEvict) {\n this._index.delete(item.id);\n this._timestamps.delete(item.id);\n }\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n private _scheduleEvictionTimer(): void {\n this._clearEvictionTimer();\n\n if (this._disposed || !this._timestamps || this._ttl <= 0 || this._timestamps.size === 0) return;\n\n const now = Date.now();\n const ttl = this._ttl;\n let earliest = Infinity;\n\n for (const ts of this._timestamps.values()) {\n if (ts < earliest) earliest = ts;\n }\n\n const delay = Math.max(0, (earliest + ttl) - now);\n this._evictionTimer = setTimeout(() => this._sweepExpired(), delay);\n }\n\n private _clearEvictionTimer(): void {\n if (this._evictionTimer !== null) {\n clearTimeout(this._evictionTimer);\n this._evictionTimer = null;\n }\n }\n}\n"],"names":["prev","result"],"mappings":"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"}
|
package/dist/Model.cjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
|
|
4
|
+
function freeze(obj) {
|
|
5
|
+
return __DEV__ ? Object.freeze(obj) : obj;
|
|
6
|
+
}
|
|
3
7
|
class Model {
|
|
4
8
|
_state;
|
|
5
9
|
_committed;
|
|
@@ -8,8 +12,10 @@ class Model {
|
|
|
8
12
|
_listeners = /* @__PURE__ */ new Set();
|
|
9
13
|
_abortController = null;
|
|
10
14
|
_cleanups = null;
|
|
15
|
+
_cachedDirty = null;
|
|
16
|
+
_cachedErrors = null;
|
|
11
17
|
constructor(initialState) {
|
|
12
|
-
const frozen =
|
|
18
|
+
const frozen = freeze({ ...initialState });
|
|
13
19
|
this._state = frozen;
|
|
14
20
|
this._committed = frozen;
|
|
15
21
|
}
|
|
@@ -27,13 +33,19 @@ class Model {
|
|
|
27
33
|
* True if current state differs from committed state.
|
|
28
34
|
*/
|
|
29
35
|
get dirty() {
|
|
30
|
-
|
|
36
|
+
if (this._cachedDirty === null) {
|
|
37
|
+
this._cachedDirty = !this.shallowEqual(this._state, this._committed);
|
|
38
|
+
}
|
|
39
|
+
return this._cachedDirty;
|
|
31
40
|
}
|
|
32
41
|
/**
|
|
33
42
|
* Validation errors for the current state.
|
|
34
43
|
*/
|
|
35
44
|
get errors() {
|
|
36
|
-
|
|
45
|
+
if (this._cachedErrors === null) {
|
|
46
|
+
this._cachedErrors = this.validate(this._state);
|
|
47
|
+
}
|
|
48
|
+
return this._cachedErrors;
|
|
37
49
|
}
|
|
38
50
|
/**
|
|
39
51
|
* True if there are no validation errors.
|
|
@@ -79,8 +91,10 @@ class Model {
|
|
|
79
91
|
return;
|
|
80
92
|
}
|
|
81
93
|
const prev = this._state;
|
|
82
|
-
const next =
|
|
94
|
+
const next = freeze({ ...prev, ...partial });
|
|
83
95
|
this._state = next;
|
|
96
|
+
this._cachedDirty = null;
|
|
97
|
+
this._cachedErrors = null;
|
|
84
98
|
this.onSet?.(prev, next);
|
|
85
99
|
for (const listener of this._listeners) {
|
|
86
100
|
listener(next, prev);
|
|
@@ -94,6 +108,8 @@ class Model {
|
|
|
94
108
|
throw new Error("Cannot commit on disposed Model");
|
|
95
109
|
}
|
|
96
110
|
this._committed = this._state;
|
|
111
|
+
this._cachedDirty = null;
|
|
112
|
+
this._cachedErrors = null;
|
|
97
113
|
}
|
|
98
114
|
/**
|
|
99
115
|
* Revert state to committed baseline.
|
|
@@ -107,6 +123,8 @@ class Model {
|
|
|
107
123
|
}
|
|
108
124
|
const prev = this._state;
|
|
109
125
|
this._state = this._committed;
|
|
126
|
+
this._cachedDirty = null;
|
|
127
|
+
this._cachedErrors = null;
|
|
110
128
|
this.onSet?.(prev, this._state);
|
|
111
129
|
for (const listener of this._listeners) {
|
|
112
130
|
listener(this._state, prev);
|
package/dist/Model.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.cjs","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: 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\n constructor(initialState: S) {\n const frozen = Object.freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n return !this.shallowEqual(this._state, this._committed);\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n return this.validate(this._state);\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\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 /** 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 return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\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\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\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 this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\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 this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @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 this.addCleanup(unsubscribe);\n return unsubscribe;\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 private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Model.cjs","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\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/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: 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 _cachedDirty: boolean | null = null;\n private _cachedErrors: ValidationErrors<S> | null = null;\n\n constructor(initialState: S) {\n const frozen = freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n if (this._cachedDirty === null) {\n this._cachedDirty = !this.shallowEqual(this._state, this._committed);\n }\n return this._cachedDirty;\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n if (this._cachedErrors === null) {\n this._cachedErrors = this.validate(this._state);\n }\n return this._cachedErrors;\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\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 /** 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 return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\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 = freeze({ ...prev, ...partial });\n this._state = next;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n this._cachedDirty = null;\n this._cachedErrors = null;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\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 this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\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 this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @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 this.addCleanup(unsubscribe);\n return unsubscribe;\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 private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/Model.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare abstract class Model<S extends object> implements Subscribable<S>
|
|
|
10
10
|
private _listeners;
|
|
11
11
|
private _abortController;
|
|
12
12
|
private _cleanups;
|
|
13
|
+
private _cachedDirty;
|
|
14
|
+
private _cachedErrors;
|
|
13
15
|
constructor(initialState: S);
|
|
14
16
|
/** Current frozen state object. */
|
|
15
17
|
get state(): S;
|
package/dist/Model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../src/Model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../src/Model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAQ/F;;GAEG;AACH,8BAAsB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAE,YAAW,YAAY,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,aAAa,CAAoC;gBAE7C,YAAY,EAAE,CAAC;IAM3B,mCAAmC;IACnC,IAAI,KAAK,IAAI,CAAC,CAEb;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,CAAC,CAEjB;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,OAAO,CAKnB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAKhC;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,+CAA+C;IAC/C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,sCAAsC;IACtC,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,6EAA6E;IAC7E,IAAI,aAAa,IAAI,WAAW,CAK/B;IAED,iFAAiF;IACjF,IAAI,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5B;;;OAGG;IACH,SAAS,CAAC,GAAG,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAiC9D;;OAEG;IACH,MAAM,IAAI,IAAI;IASd;;OAEG;IACH,QAAQ,IAAI,IAAI;IAqBhB,oEAAoE;IACpE,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAY5C,0EAA0E;IAC1E,OAAO,IAAI,IAAI;IAef;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC;IAIlD,uEAAuE;IACvE,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAO1C,2FAA2F;IAC3F,SAAS,CAAC,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAMpF,yGAAyG;IACzG,SAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS;QAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;KAAE,EAC1G,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,GAC7C,MAAM,IAAI;IAMb,kFAAkF;IAClF,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI;IACxC,4FAA4F;IAC5F,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACzC,uFAAuF;IACvF,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI;IAE5B,OAAO,CAAC,YAAY;CAgBrB"}
|
package/dist/Model.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
|
|
2
|
+
function freeze(obj) {
|
|
3
|
+
return __DEV__ ? Object.freeze(obj) : obj;
|
|
4
|
+
}
|
|
1
5
|
class Model {
|
|
2
6
|
_state;
|
|
3
7
|
_committed;
|
|
@@ -6,8 +10,10 @@ class Model {
|
|
|
6
10
|
_listeners = /* @__PURE__ */ new Set();
|
|
7
11
|
_abortController = null;
|
|
8
12
|
_cleanups = null;
|
|
13
|
+
_cachedDirty = null;
|
|
14
|
+
_cachedErrors = null;
|
|
9
15
|
constructor(initialState) {
|
|
10
|
-
const frozen =
|
|
16
|
+
const frozen = freeze({ ...initialState });
|
|
11
17
|
this._state = frozen;
|
|
12
18
|
this._committed = frozen;
|
|
13
19
|
}
|
|
@@ -25,13 +31,19 @@ class Model {
|
|
|
25
31
|
* True if current state differs from committed state.
|
|
26
32
|
*/
|
|
27
33
|
get dirty() {
|
|
28
|
-
|
|
34
|
+
if (this._cachedDirty === null) {
|
|
35
|
+
this._cachedDirty = !this.shallowEqual(this._state, this._committed);
|
|
36
|
+
}
|
|
37
|
+
return this._cachedDirty;
|
|
29
38
|
}
|
|
30
39
|
/**
|
|
31
40
|
* Validation errors for the current state.
|
|
32
41
|
*/
|
|
33
42
|
get errors() {
|
|
34
|
-
|
|
43
|
+
if (this._cachedErrors === null) {
|
|
44
|
+
this._cachedErrors = this.validate(this._state);
|
|
45
|
+
}
|
|
46
|
+
return this._cachedErrors;
|
|
35
47
|
}
|
|
36
48
|
/**
|
|
37
49
|
* True if there are no validation errors.
|
|
@@ -77,8 +89,10 @@ class Model {
|
|
|
77
89
|
return;
|
|
78
90
|
}
|
|
79
91
|
const prev = this._state;
|
|
80
|
-
const next =
|
|
92
|
+
const next = freeze({ ...prev, ...partial });
|
|
81
93
|
this._state = next;
|
|
94
|
+
this._cachedDirty = null;
|
|
95
|
+
this._cachedErrors = null;
|
|
82
96
|
this.onSet?.(prev, next);
|
|
83
97
|
for (const listener of this._listeners) {
|
|
84
98
|
listener(next, prev);
|
|
@@ -92,6 +106,8 @@ class Model {
|
|
|
92
106
|
throw new Error("Cannot commit on disposed Model");
|
|
93
107
|
}
|
|
94
108
|
this._committed = this._state;
|
|
109
|
+
this._cachedDirty = null;
|
|
110
|
+
this._cachedErrors = null;
|
|
95
111
|
}
|
|
96
112
|
/**
|
|
97
113
|
* Revert state to committed baseline.
|
|
@@ -105,6 +121,8 @@ class Model {
|
|
|
105
121
|
}
|
|
106
122
|
const prev = this._state;
|
|
107
123
|
this._state = this._committed;
|
|
124
|
+
this._cachedDirty = null;
|
|
125
|
+
this._cachedErrors = null;
|
|
108
126
|
this.onSet?.(prev, this._state);
|
|
109
127
|
for (const listener of this._listeners) {
|
|
110
128
|
listener(this._state, prev);
|
package/dist/Model.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.js","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: 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\n constructor(initialState: S) {\n const frozen = Object.freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n return !this.shallowEqual(this._state, this._committed);\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n return this.validate(this._state);\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\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 /** 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 return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\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\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\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 this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\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 this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @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 this.addCleanup(unsubscribe);\n return unsubscribe;\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 private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Model.js","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\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/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: 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 _cachedDirty: boolean | null = null;\n private _cachedErrors: ValidationErrors<S> | null = null;\n\n constructor(initialState: S) {\n const frozen = freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n if (this._cachedDirty === null) {\n this._cachedDirty = !this.shallowEqual(this._state, this._committed);\n }\n return this._cachedDirty;\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n if (this._cachedErrors === null) {\n this._cachedErrors = this.validate(this._state);\n }\n return this._cachedErrors;\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\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 /** 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 return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\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 = freeze({ ...prev, ...partial });\n this._state = next;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n this._cachedDirty = null;\n this._cachedErrors = null;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\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 this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\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 this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @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 this.addCleanup(unsubscribe);\n return unsubscribe;\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 private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,7 @@ class PersistentCollection extends Collection.Collection {
|
|
|
22
22
|
_preHydrationWarned = false;
|
|
23
23
|
_pendingWrites = /* @__PURE__ */ new Map();
|
|
24
24
|
_pendingRemoves = /* @__PURE__ */ new Set();
|
|
25
|
+
_diffMap = /* @__PURE__ */ new Map();
|
|
25
26
|
_pendingClear = false;
|
|
26
27
|
_flushTimer = null;
|
|
27
28
|
constructor(initialItems = []) {
|
|
@@ -187,27 +188,24 @@ class PersistentCollection extends Collection.Collection {
|
|
|
187
188
|
}
|
|
188
189
|
// ── Private: delta tracking ──
|
|
189
190
|
_diffAndQueue(current, prev) {
|
|
190
|
-
const prevMap =
|
|
191
|
+
const prevMap = this._diffMap;
|
|
192
|
+
prevMap.clear();
|
|
191
193
|
for (const item of prev) {
|
|
192
194
|
prevMap.set(item.id, item);
|
|
193
195
|
}
|
|
194
|
-
const currentMap = /* @__PURE__ */ new Map();
|
|
195
|
-
for (const item of current) {
|
|
196
|
-
currentMap.set(item.id, item);
|
|
197
|
-
}
|
|
198
196
|
for (const item of current) {
|
|
199
197
|
const prevItem = prevMap.get(item.id);
|
|
200
198
|
if (!prevItem || prevItem !== item) {
|
|
201
199
|
this._pendingWrites.set(item.id, item);
|
|
202
200
|
this._pendingRemoves.delete(item.id);
|
|
203
201
|
}
|
|
202
|
+
prevMap.delete(item.id);
|
|
204
203
|
}
|
|
205
|
-
for (const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this._pendingWrites.delete(item.id);
|
|
209
|
-
}
|
|
204
|
+
for (const [id] of prevMap) {
|
|
205
|
+
this._pendingRemoves.add(id);
|
|
206
|
+
this._pendingWrites.delete(id);
|
|
210
207
|
}
|
|
208
|
+
prevMap.clear();
|
|
211
209
|
}
|
|
212
210
|
_hasPending() {
|
|
213
211
|
return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersistentCollection.cjs","sources":["../src/PersistentCollection.ts"],"sourcesContent":["import { Collection } from './Collection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// Track storageKey uniqueness in DEV\nconst _registeredKeys: Map<string, string> | null = __DEV__ ? new Map() : null;\n\n/**\n * Abstract base for Collections that persist to external storage.\n * Tracks deltas per mutation and flushes via debounced writes.\n * Subclasses implement the storage-specific `persist*` methods.\n */\nexport abstract class PersistentCollection<\n T extends { id: string | number },\n> extends Collection<T> {\n /** Debounce delay in ms for storage writes. 0 = immediate. */\n static WRITE_DELAY = 100;\n\n /** Unique key identifying this collection in storage. */\n protected abstract readonly storageKey: string;\n\n // ── Abstract persistence methods ──\n\n protected abstract persistGet(id: T['id']): T | null | Promise<T | null>;\n protected abstract persistGetAll(): T[] | Promise<T[]>;\n /** Upsert semantics — insert or replace the given items. */\n protected abstract persistSet(items: T[]): void | Promise<void>;\n protected abstract persistRemove(ids: T['id'][]): void | Promise<void>;\n protected abstract persistClear(): void | Promise<void>;\n\n // ── Serialization hooks ──\n\n /** Serialize items to a string. Used by string-based adapters (WebStorage, NativeCollection). */\n protected serialize(items: T[]): string {\n return JSON.stringify(items);\n }\n\n /** Deserialize a string back to items. Used by string-based adapters. */\n protected deserialize(raw: string): T[] {\n return JSON.parse(raw);\n }\n\n // ── Error hook ──\n\n /** Called when a storage operation fails. Override for custom error handling. */\n protected onPersistError?(error: unknown): void;\n\n // ── Internal state ──\n\n private _hydrated = false;\n private _hydrating = false;\n private _persistenceReady = false;\n private _preHydrationWarned = false;\n private _pendingWrites = new Map<T['id'], T>();\n private _pendingRemoves = new Set<T['id']>();\n private _pendingClear = false;\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n super(initialItems);\n\n // Self-subscribe to detect mutations via diff.\n // storageKey may not be available yet (class field initializers run after super()),\n // but that's fine — the subscriber only fires on mutations, not during construction.\n const unsub = this.subscribe((current, prev) => {\n if (this._hydrating) return;\n this._ensurePersistenceReady();\n this._diffAndQueue(current, prev);\n this._scheduleSave();\n });\n this.addCleanup(unsub);\n }\n\n /**\n * DEV check for duplicate storageKey. Called lazily since storageKey is an abstract\n * field that isn't available during the parent constructor chain.\n */\n private _ensurePersistenceReady(): void {\n if (this._persistenceReady) return;\n this._persistenceReady = true;\n\n if (__DEV__ && _registeredKeys) {\n const className = this.constructor.name;\n const existing = _registeredKeys.get(this.storageKey);\n if (existing && existing !== className) {\n console.warn(\n `[mvc-kit] Duplicate storageKey \"${this.storageKey}\" used by \"${className}\" ` +\n `and \"${existing}\". Each PersistentCollection should have a unique storageKey.`,\n );\n }\n _registeredKeys.set(this.storageKey, className);\n }\n }\n\n // ── Public API ──\n\n /** Whether storage data has been loaded. */\n get hydrated(): boolean {\n return this._hydrated;\n }\n\n /**\n * Load data from storage into the collection. Idempotent — subsequent calls return current items.\n * Returns the items after hydration.\n */\n async hydrate(): Promise<T[]> {\n if (this._hydrated) return this.items;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = await this.persistGetAll();\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n return this.items;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n return this.items;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Synchronous hydration for sync adapters (e.g., WebStorage).\n * Call from the **leaf class** constructor (after field initializers have run).\n */\n protected _hydrateSync(): void {\n if (this._hydrated) return;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = this.persistGetAll();\n if (stored instanceof Promise) {\n throw new Error('[mvc-kit] _hydrateSync called with async persistGetAll');\n }\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Clear all data from storage AND from the in-memory collection.\n */\n clearStorage(): void | Promise<void> {\n this._ensurePersistenceReady();\n\n // Clear pending queues — we're wiping everything\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._pendingClear = false;\n this._cancelSave();\n\n // Clear in-memory\n if (this.length > 0) {\n super.clear();\n }\n\n try {\n const result = this.persistClear();\n if (result instanceof Promise) {\n return result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // ── Overrides for clear/reset tracking ──\n\n reset(items: T[]): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n for (const item of items) {\n this._pendingWrites.set(item.id, item);\n }\n this._hydrating = true; // Re-use flag to skip subscriber\n try {\n super.reset(items);\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n clear(): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._hydrating = true;\n try {\n super.clear();\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n // ── Override items getter for DEV pre-hydration warning ──\n\n get items(): T[] {\n if (__DEV__ && !this._hydrated && !this._hydrating && !this._preHydrationWarned) {\n this._preHydrationWarned = true;\n console.warn(\n `[mvc-kit] Accessing items on \"${this.constructor.name}\" before hydrate() has been called. ` +\n `Data may be incomplete. Call hydrate() first.`,\n );\n }\n return super.items;\n }\n\n get state(): T[] {\n return this.items;\n }\n\n // ── Dispose ──\n\n dispose(): void {\n if (this.disposed) return;\n\n // Flush any pending saves before disposing\n this._cancelSave();\n if (this._hasPending()) {\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // DEV: unregister storageKey\n if (__DEV__ && _registeredKeys && this._persistenceReady) {\n _registeredKeys.delete(this.storageKey);\n }\n\n super.dispose();\n }\n\n // ── Private: delta tracking ──\n\n private _diffAndQueue(current: readonly T[], prev: readonly T[]): void {\n const prevMap = new Map<T['id'], T>();\n for (const item of prev) {\n prevMap.set(item.id, item);\n }\n\n const currentMap = new Map<T['id'], T>();\n for (const item of current) {\n currentMap.set(item.id, item);\n }\n\n // Added or updated: in current but not in prev, or different reference\n for (const item of current) {\n const prevItem = prevMap.get(item.id);\n if (!prevItem || prevItem !== item) {\n this._pendingWrites.set(item.id, item);\n this._pendingRemoves.delete(item.id);\n }\n }\n\n // Removed: in prev but not in current\n for (const item of prev) {\n if (!currentMap.has(item.id)) {\n this._pendingRemoves.add(item.id);\n this._pendingWrites.delete(item.id);\n }\n }\n }\n\n private _hasPending(): boolean {\n return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;\n }\n\n // ── Private: debounce + flush ──\n\n private _scheduleSave(): void {\n this._cancelSave();\n const delay = (this.constructor as typeof PersistentCollection).WRITE_DELAY;\n if (delay <= 0) {\n this._doFlush();\n return;\n }\n this._flushTimer = setTimeout(() => this._doFlush(), delay);\n }\n\n private _cancelSave(): void {\n if (this._flushTimer !== null) {\n clearTimeout(this._flushTimer);\n this._flushTimer = null;\n }\n }\n\n private _doFlush(): void {\n if (!this._hasPending()) return;\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n private _flush(): void | Promise<void> {\n const doClear = this._pendingClear;\n const writes = this._pendingWrites.size > 0 ? [...this._pendingWrites.values()] : null;\n const removes = this._pendingRemoves.size > 0 ? [...this._pendingRemoves] : null;\n\n // Clear queues\n this._pendingClear = false;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n\n if (doClear) {\n const clearResult = this.persistClear();\n if (clearResult instanceof Promise) {\n return clearResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n if (writes) {\n return this.persistSet(writes);\n }\n return;\n }\n\n // Non-clear: removes then writes\n if (removes) {\n const removeResult = this.persistRemove(removes);\n if (removeResult instanceof Promise) {\n return removeResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n }\n if (writes) {\n return this.persistSet(writes);\n }\n }\n\n // ── Private: error handling ──\n\n private _handlePersistError(err: unknown): void {\n if (this.onPersistError) {\n this.onPersistError(err);\n return;\n }\n if (__DEV__) {\n console.warn('[mvc-kit] Storage error:', err);\n }\n }\n}\n"],"names":["Collection"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PersistentCollection.cjs","sources":["../src/PersistentCollection.ts"],"sourcesContent":["import { Collection } from './Collection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// Track storageKey uniqueness in DEV\nconst _registeredKeys: Map<string, string> | null = __DEV__ ? new Map() : null;\n\n/**\n * Abstract base for Collections that persist to external storage.\n * Tracks deltas per mutation and flushes via debounced writes.\n * Subclasses implement the storage-specific `persist*` methods.\n */\nexport abstract class PersistentCollection<\n T extends { id: string | number },\n> extends Collection<T> {\n /** Debounce delay in ms for storage writes. 0 = immediate. */\n static WRITE_DELAY = 100;\n\n /** Unique key identifying this collection in storage. */\n protected abstract readonly storageKey: string;\n\n // ── Abstract persistence methods ──\n\n protected abstract persistGet(id: T['id']): T | null | Promise<T | null>;\n protected abstract persistGetAll(): T[] | Promise<T[]>;\n /** Upsert semantics — insert or replace the given items. */\n protected abstract persistSet(items: T[]): void | Promise<void>;\n protected abstract persistRemove(ids: T['id'][]): void | Promise<void>;\n protected abstract persistClear(): void | Promise<void>;\n\n // ── Serialization hooks ──\n\n /** Serialize items to a string. Used by string-based adapters (WebStorage, NativeCollection). */\n protected serialize(items: T[]): string {\n return JSON.stringify(items);\n }\n\n /** Deserialize a string back to items. Used by string-based adapters. */\n protected deserialize(raw: string): T[] {\n return JSON.parse(raw);\n }\n\n // ── Error hook ──\n\n /** Called when a storage operation fails. Override for custom error handling. */\n protected onPersistError?(error: unknown): void;\n\n // ── Internal state ──\n\n private _hydrated = false;\n private _hydrating = false;\n private _persistenceReady = false;\n private _preHydrationWarned = false;\n private _pendingWrites = new Map<T['id'], T>();\n private _pendingRemoves = new Set<T['id']>();\n private _diffMap = new Map<T['id'], T>();\n private _pendingClear = false;\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n super(initialItems);\n\n // Self-subscribe to detect mutations via diff.\n // storageKey may not be available yet (class field initializers run after super()),\n // but that's fine — the subscriber only fires on mutations, not during construction.\n const unsub = this.subscribe((current, prev) => {\n if (this._hydrating) return;\n this._ensurePersistenceReady();\n this._diffAndQueue(current, prev);\n this._scheduleSave();\n });\n this.addCleanup(unsub);\n }\n\n /**\n * DEV check for duplicate storageKey. Called lazily since storageKey is an abstract\n * field that isn't available during the parent constructor chain.\n */\n private _ensurePersistenceReady(): void {\n if (this._persistenceReady) return;\n this._persistenceReady = true;\n\n if (__DEV__ && _registeredKeys) {\n const className = this.constructor.name;\n const existing = _registeredKeys.get(this.storageKey);\n if (existing && existing !== className) {\n console.warn(\n `[mvc-kit] Duplicate storageKey \"${this.storageKey}\" used by \"${className}\" ` +\n `and \"${existing}\". Each PersistentCollection should have a unique storageKey.`,\n );\n }\n _registeredKeys.set(this.storageKey, className);\n }\n }\n\n // ── Public API ──\n\n /** Whether storage data has been loaded. */\n get hydrated(): boolean {\n return this._hydrated;\n }\n\n /**\n * Load data from storage into the collection. Idempotent — subsequent calls return current items.\n * Returns the items after hydration.\n */\n async hydrate(): Promise<T[]> {\n if (this._hydrated) return this.items;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = await this.persistGetAll();\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n return this.items;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n return this.items;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Synchronous hydration for sync adapters (e.g., WebStorage).\n * Call from the **leaf class** constructor (after field initializers have run).\n */\n protected _hydrateSync(): void {\n if (this._hydrated) return;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = this.persistGetAll();\n if (stored instanceof Promise) {\n throw new Error('[mvc-kit] _hydrateSync called with async persistGetAll');\n }\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Clear all data from storage AND from the in-memory collection.\n */\n clearStorage(): void | Promise<void> {\n this._ensurePersistenceReady();\n\n // Clear pending queues — we're wiping everything\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._pendingClear = false;\n this._cancelSave();\n\n // Clear in-memory\n if (this.length > 0) {\n super.clear();\n }\n\n try {\n const result = this.persistClear();\n if (result instanceof Promise) {\n return result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // ── Overrides for clear/reset tracking ──\n\n reset(items: T[]): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n for (const item of items) {\n this._pendingWrites.set(item.id, item);\n }\n this._hydrating = true; // Re-use flag to skip subscriber\n try {\n super.reset(items);\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n clear(): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._hydrating = true;\n try {\n super.clear();\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n // ── Override items getter for DEV pre-hydration warning ──\n\n get items(): T[] {\n if (__DEV__ && !this._hydrated && !this._hydrating && !this._preHydrationWarned) {\n this._preHydrationWarned = true;\n console.warn(\n `[mvc-kit] Accessing items on \"${this.constructor.name}\" before hydrate() has been called. ` +\n `Data may be incomplete. Call hydrate() first.`,\n );\n }\n return super.items;\n }\n\n get state(): T[] {\n return this.items;\n }\n\n // ── Dispose ──\n\n dispose(): void {\n if (this.disposed) return;\n\n // Flush any pending saves before disposing\n this._cancelSave();\n if (this._hasPending()) {\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // DEV: unregister storageKey\n if (__DEV__ && _registeredKeys && this._persistenceReady) {\n _registeredKeys.delete(this.storageKey);\n }\n\n super.dispose();\n }\n\n // ── Private: delta tracking ──\n\n private _diffAndQueue(current: readonly T[], prev: readonly T[]): void {\n const prevMap = this._diffMap;\n prevMap.clear();\n for (const item of prev) {\n prevMap.set(item.id, item);\n }\n\n // Added or updated: in current but different reference in prev\n for (const item of current) {\n const prevItem = prevMap.get(item.id);\n if (!prevItem || prevItem !== item) {\n this._pendingWrites.set(item.id, item);\n this._pendingRemoves.delete(item.id);\n }\n prevMap.delete(item.id); // consume matched items\n }\n\n // Remaining in prevMap = removed (in prev but not in current)\n for (const [id] of prevMap) {\n this._pendingRemoves.add(id);\n this._pendingWrites.delete(id);\n }\n\n prevMap.clear();\n }\n\n private _hasPending(): boolean {\n return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;\n }\n\n // ── Private: debounce + flush ──\n\n private _scheduleSave(): void {\n this._cancelSave();\n const delay = (this.constructor as typeof PersistentCollection).WRITE_DELAY;\n if (delay <= 0) {\n this._doFlush();\n return;\n }\n this._flushTimer = setTimeout(() => this._doFlush(), delay);\n }\n\n private _cancelSave(): void {\n if (this._flushTimer !== null) {\n clearTimeout(this._flushTimer);\n this._flushTimer = null;\n }\n }\n\n private _doFlush(): void {\n if (!this._hasPending()) return;\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n private _flush(): void | Promise<void> {\n const doClear = this._pendingClear;\n const writes = this._pendingWrites.size > 0 ? [...this._pendingWrites.values()] : null;\n const removes = this._pendingRemoves.size > 0 ? [...this._pendingRemoves] : null;\n\n // Clear queues\n this._pendingClear = false;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n\n if (doClear) {\n const clearResult = this.persistClear();\n if (clearResult instanceof Promise) {\n return clearResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n if (writes) {\n return this.persistSet(writes);\n }\n return;\n }\n\n // Non-clear: removes then writes\n if (removes) {\n const removeResult = this.persistRemove(removes);\n if (removeResult instanceof Promise) {\n return removeResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n }\n if (writes) {\n return this.persistSet(writes);\n }\n }\n\n // ── Private: error handling ──\n\n private _handlePersistError(err: unknown): void {\n if (this.onPersistError) {\n this.onPersistError(err);\n return;\n }\n if (__DEV__) {\n console.warn('[mvc-kit] Storage error:', err);\n }\n }\n}\n"],"names":["Collection"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersistentCollection.d.ts","sourceRoot":"","sources":["../src/PersistentCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C;;;;GAIG;AACH,8BAAsB,oBAAoB,CACxC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CACjC,SAAQ,UAAU,CAAC,CAAC,CAAC;IACrB,8DAA8D;IAC9D,MAAM,CAAC,WAAW,SAAO;IAEzB,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAI/C,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IACxE,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IACtD,4DAA4D;IAC5D,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC/D,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACtE,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD,iGAAiG;IACjG,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,MAAM;IAIvC,yEAAyE;IACzE,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE;IAMvC,iFAAiF;IACjF,SAAS,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI/C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,eAAe,CAAsB;IAC7C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAA8C;gBAErD,YAAY,GAAE,CAAC,EAAO;IAelC;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAmB/B,4CAA4C;IAC5C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;IAqB7B;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAsB9B;;OAEG;IACH,YAAY,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BpC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAgBvB,KAAK,IAAI,IAAI;IAeb,IAAI,KAAK,IAAI,CAAC,EAAE,CASf;IAED,IAAI,KAAK,IAAI,CAAC,EAAE,CAEf;IAID,OAAO,IAAI,IAAI;IA0Bf,OAAO,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"PersistentCollection.d.ts","sourceRoot":"","sources":["../src/PersistentCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C;;;;GAIG;AACH,8BAAsB,oBAAoB,CACxC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CACjC,SAAQ,UAAU,CAAC,CAAC,CAAC;IACrB,8DAA8D;IAC9D,MAAM,CAAC,WAAW,SAAO;IAEzB,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAI/C,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IACxE,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IACtD,4DAA4D;IAC5D,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC/D,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACtE,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD,iGAAiG;IACjG,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,MAAM;IAIvC,yEAAyE;IACzE,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE;IAMvC,iFAAiF;IACjF,SAAS,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI/C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,eAAe,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAA8C;gBAErD,YAAY,GAAE,CAAC,EAAO;IAelC;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAmB/B,4CAA4C;IAC5C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;IAqB7B;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAsB9B;;OAEG;IACH,YAAY,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BpC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAgBvB,KAAK,IAAI,IAAI;IAeb,IAAI,KAAK,IAAI,CAAC,EAAE,CASf;IAED,IAAI,KAAK,IAAI,CAAC,EAAE,CAEf;IAID,OAAO,IAAI,IAAI;IA0Bf,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,MAAM;IAuCd,OAAO,CAAC,mBAAmB;CAS5B"}
|
|
@@ -20,6 +20,7 @@ class PersistentCollection extends Collection {
|
|
|
20
20
|
_preHydrationWarned = false;
|
|
21
21
|
_pendingWrites = /* @__PURE__ */ new Map();
|
|
22
22
|
_pendingRemoves = /* @__PURE__ */ new Set();
|
|
23
|
+
_diffMap = /* @__PURE__ */ new Map();
|
|
23
24
|
_pendingClear = false;
|
|
24
25
|
_flushTimer = null;
|
|
25
26
|
constructor(initialItems = []) {
|
|
@@ -185,27 +186,24 @@ class PersistentCollection extends Collection {
|
|
|
185
186
|
}
|
|
186
187
|
// ── Private: delta tracking ──
|
|
187
188
|
_diffAndQueue(current, prev) {
|
|
188
|
-
const prevMap =
|
|
189
|
+
const prevMap = this._diffMap;
|
|
190
|
+
prevMap.clear();
|
|
189
191
|
for (const item of prev) {
|
|
190
192
|
prevMap.set(item.id, item);
|
|
191
193
|
}
|
|
192
|
-
const currentMap = /* @__PURE__ */ new Map();
|
|
193
|
-
for (const item of current) {
|
|
194
|
-
currentMap.set(item.id, item);
|
|
195
|
-
}
|
|
196
194
|
for (const item of current) {
|
|
197
195
|
const prevItem = prevMap.get(item.id);
|
|
198
196
|
if (!prevItem || prevItem !== item) {
|
|
199
197
|
this._pendingWrites.set(item.id, item);
|
|
200
198
|
this._pendingRemoves.delete(item.id);
|
|
201
199
|
}
|
|
200
|
+
prevMap.delete(item.id);
|
|
202
201
|
}
|
|
203
|
-
for (const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
this._pendingWrites.delete(item.id);
|
|
207
|
-
}
|
|
202
|
+
for (const [id] of prevMap) {
|
|
203
|
+
this._pendingRemoves.add(id);
|
|
204
|
+
this._pendingWrites.delete(id);
|
|
208
205
|
}
|
|
206
|
+
prevMap.clear();
|
|
209
207
|
}
|
|
210
208
|
_hasPending() {
|
|
211
209
|
return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;
|