mvc-kit 2.8.0 → 2.9.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 +29 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +3 -3
- package/agent-config/claude-code/skills/guide/api-reference.md +138 -1
- package/agent-config/claude-code/skills/guide/patterns.md +120 -0
- package/agent-config/copilot/copilot-instructions.md +52 -0
- package/agent-config/cursor/cursorrules +52 -0
- package/dist/Collection.cjs +38 -0
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +38 -0
- package/dist/Collection.js.map +1 -1
- package/dist/Feed.cjs +86 -0
- package/dist/Feed.cjs.map +1 -0
- package/dist/Feed.d.ts +46 -0
- package/dist/Feed.d.ts.map +1 -0
- package/dist/Feed.js +86 -0
- package/dist/Feed.js.map +1 -0
- package/dist/Pagination.cjs +84 -0
- package/dist/Pagination.cjs.map +1 -0
- package/dist/Pagination.d.ts +39 -0
- package/dist/Pagination.d.ts.map +1 -0
- package/dist/Pagination.js +84 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/PersistentCollection.cjs +8 -5
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.d.ts +6 -1
- package/dist/PersistentCollection.d.ts.map +1 -1
- package/dist/PersistentCollection.js +8 -5
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +3 -0
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.d.ts +3 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +3 -0
- package/dist/Resource.js.map +1 -1
- package/dist/Selection.cjs +99 -0
- package/dist/Selection.cjs.map +1 -0
- package/dist/Selection.d.ts +36 -0
- package/dist/Selection.d.ts.map +1 -0
- package/dist/Selection.js +99 -0
- package/dist/Selection.js.map +1 -0
- package/dist/Sorting.cjs +114 -0
- package/dist/Sorting.cjs.map +1 -0
- package/dist/Sorting.d.ts +43 -0
- package/dist/Sorting.d.ts.map +1 -0
- package/dist/Sorting.js +114 -0
- package/dist/Sorting.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +8 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +8 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/components/CardList.cjs +42 -0
- package/dist/react/components/CardList.cjs.map +1 -0
- package/dist/react/components/CardList.d.ts +22 -0
- package/dist/react/components/CardList.d.ts.map +1 -0
- package/dist/react/components/CardList.js +42 -0
- package/dist/react/components/CardList.js.map +1 -0
- package/dist/react/components/DataTable.cjs +179 -0
- package/dist/react/components/DataTable.cjs.map +1 -0
- package/dist/react/components/DataTable.d.ts +30 -0
- package/dist/react/components/DataTable.d.ts.map +1 -0
- package/dist/react/components/DataTable.js +179 -0
- package/dist/react/components/DataTable.js.map +1 -0
- package/dist/react/components/InfiniteScroll.cjs +44 -0
- package/dist/react/components/InfiniteScroll.cjs.map +1 -0
- package/dist/react/components/InfiniteScroll.d.ts +21 -0
- package/dist/react/components/InfiniteScroll.d.ts.map +1 -0
- package/dist/react/components/InfiniteScroll.js +44 -0
- package/dist/react/components/InfiniteScroll.js.map +1 -0
- package/dist/react/components/types.cjs +15 -0
- package/dist/react/components/types.cjs.map +1 -0
- package/dist/react/components/types.d.ts +71 -0
- package/dist/react/components/types.d.ts.map +1 -0
- package/dist/react/components/types.js +15 -0
- package/dist/react/components/types.js.map +1 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react-native/NativeCollection.cjs +3 -0
- package/dist/react-native/NativeCollection.cjs.map +1 -1
- package/dist/react-native/NativeCollection.d.ts +3 -0
- package/dist/react-native/NativeCollection.d.ts.map +1 -1
- package/dist/react-native/NativeCollection.js +3 -0
- package/dist/react-native/NativeCollection.js.map +1 -1
- package/dist/react.cjs +6 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +6 -0
- package/dist/react.js.map +1 -1
- package/dist/web/idb.cjs.map +1 -1
- package/dist/web/idb.d.ts +18 -0
- package/dist/web/idb.d.ts.map +1 -1
- package/dist/web/idb.js.map +1 -1
- package/dist/wrapAsyncMethods.cjs +21 -41
- package/dist/wrapAsyncMethods.cjs.map +1 -1
- package/dist/wrapAsyncMethods.d.ts +2 -0
- package/dist/wrapAsyncMethods.d.ts.map +1 -1
- package/dist/wrapAsyncMethods.js +21 -41
- package/dist/wrapAsyncMethods.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeCollection.js","sources":["../../src/react-native/NativeCollection.ts"],"sourcesContent":["import { PersistentCollection } from '../PersistentCollection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\ninterface StorageAdapter {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\nlet _adapter: StorageAdapter | null = null;\n\n/**\n * PersistentCollection for React Native, backed by any async key-value store.\n * Uses blob strategy (full state as a single JSON string under `storageKey`).\n *\n * **Requires manual `hydrate()` call** (async storage).\n *\n * ## Setup (once at app startup)\n *\n * ```ts\n * import { NativeCollection } from 'mvc-kit/react-native';\n * import AsyncStorage from '@react-native-async-storage/async-storage';\n *\n * NativeCollection.configure({\n * getItem: (key) => AsyncStorage.getItem(key),\n * setItem: (key, value) => AsyncStorage.setItem(key, value),\n * removeItem: (key) => AsyncStorage.removeItem(key),\n * });\n * ```\n *\n * ## Usage\n *\n * ```ts\n * class TodosCollection extends NativeCollection<Todo> {\n * protected readonly storageKey = 'todos';\n * }\n * ```\n *\n * ## Per-class override (edge cases)\n *\n * ```ts\n * class SecureCollection extends NativeCollection<Secret> {\n * protected readonly storageKey = 'secrets';\n * protected async getItem(key: string) { return SecureStore.getItem(key); }\n * protected async setItem(key: string, value: string) { await SecureStore.setItem(key, value); }\n * protected async removeItem(key: string) { await SecureStore.removeItem(key); }\n * }\n * ```\n */\nexport abstract class NativeCollection<\n T extends { id: string | number },\n> extends PersistentCollection<T> {\n /**\n * Configure the default storage adapter for all NativeCollection subclasses.\n * Call once at app startup. Per-class method overrides take priority.\n */\n static configure(adapter: StorageAdapter): void {\n _adapter = adapter;\n }\n\n /** Reset the configured adapter (for testing). */\n static resetAdapter(): void {\n _adapter = null;\n }\n\n // ── Per-class override points ──\n\n protected getItem(key: string): Promise<string | null> {\n if (_adapter) return _adapter.getItem(key);\n if (__DEV__) {\n throw new Error(\n `[mvc-kit] No storage adapter configured for \"${this.constructor.name}\". ` +\n `Call NativeCollection.configure() at app startup, or override getItem/setItem/removeItem.`,\n );\n }\n throw new Error('[mvc-kit] No storage adapter configured.');\n }\n\n protected setItem(key: string, value: string): Promise<void> {\n if (_adapter) return _adapter.setItem(key, value);\n if (__DEV__) {\n throw new Error(\n `[mvc-kit] No storage adapter configured for \"${this.constructor.name}\". ` +\n `Call NativeCollection.configure() at app startup, or override getItem/setItem/removeItem.`,\n );\n }\n throw new Error('[mvc-kit] No storage adapter configured.');\n }\n\n protected removeItem(key: string): Promise<void> {\n if (_adapter) return _adapter.removeItem(key);\n if (__DEV__) {\n throw new Error(\n `[mvc-kit] No storage adapter configured for \"${this.constructor.name}\". ` +\n `Call NativeCollection.configure() at app startup, or override getItem/setItem/removeItem.`,\n );\n }\n throw new Error('[mvc-kit] No storage adapter configured.');\n }\n\n // ── Persist interface (blob strategy) ──\n\n protected async persistGetAll(): Promise<T[]> {\n const raw = await this.getItem(this.storageKey);\n if (!raw) return [];\n try {\n return this.deserialize(raw);\n } catch {\n if (__DEV__) {\n console.warn(\n `[mvc-kit] Corrupted data in storage key \"${this.storageKey}\". Ignoring stored data.`,\n );\n }\n return [];\n }\n }\n\n protected async persistGet(id: T['id']): Promise<T | null> {\n const all = await this.persistGetAll();\n return all.find((i) => i.id === id) ?? null;\n }\n\n protected async persistSet(_items: T[]): Promise<void> {\n await this.setItem(this.storageKey, this.serialize([...this.items]));\n }\n\n protected async persistRemove(_ids: T['id'][]): Promise<void> {\n await this.setItem(this.storageKey, this.serialize([...this.items]));\n }\n\n protected async persistClear(): Promise<void> {\n await this.removeItem(this.storageKey);\n }\n}\n"],"names":[],"mappings":";AAEA,MAAM,UAAU,OAAO,oBAAoB,eAAe;AAQ1D,IAAI,WAAkC;AAwC/B,MAAe,yBAEZ,qBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKhC,OAAO,UAAU,SAA+B;AAC9C,eAAW;AAAA,EACb;AAAA;AAAA,EAGA,OAAO,eAAqB;AAC1B,eAAW;AAAA,EACb;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"NativeCollection.js","sources":["../../src/react-native/NativeCollection.ts"],"sourcesContent":["import { PersistentCollection } from '../PersistentCollection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\ninterface StorageAdapter {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n removeItem(key: string): Promise<void>;\n}\n\nlet _adapter: StorageAdapter | null = null;\n\n/**\n * PersistentCollection for React Native, backed by any async key-value store.\n * Uses blob strategy (full state as a single JSON string under `storageKey`).\n *\n * **Requires manual `hydrate()` call** (async storage).\n *\n * ## Setup (once at app startup)\n *\n * ```ts\n * import { NativeCollection } from 'mvc-kit/react-native';\n * import AsyncStorage from '@react-native-async-storage/async-storage';\n *\n * NativeCollection.configure({\n * getItem: (key) => AsyncStorage.getItem(key),\n * setItem: (key, value) => AsyncStorage.setItem(key, value),\n * removeItem: (key) => AsyncStorage.removeItem(key),\n * });\n * ```\n *\n * ## Usage\n *\n * ```ts\n * class TodosCollection extends NativeCollection<Todo> {\n * protected readonly storageKey = 'todos';\n * }\n * ```\n *\n * ## Per-class override (edge cases)\n *\n * ```ts\n * class SecureCollection extends NativeCollection<Secret> {\n * protected readonly storageKey = 'secrets';\n * protected async getItem(key: string) { return SecureStore.getItem(key); }\n * protected async setItem(key: string, value: string) { await SecureStore.setItem(key, value); }\n * protected async removeItem(key: string) { await SecureStore.removeItem(key); }\n * }\n * ```\n */\nexport abstract class NativeCollection<\n T extends { id: string | number },\n> extends PersistentCollection<T> {\n /**\n * Configure the default storage adapter for all NativeCollection subclasses.\n * Call once at app startup. Per-class method overrides take priority.\n */\n static configure(adapter: StorageAdapter): void {\n _adapter = adapter;\n }\n\n /** Reset the configured adapter (for testing). */\n static resetAdapter(): void {\n _adapter = null;\n }\n\n // ── Per-class override points ──\n\n /** Read a value from the storage adapter. Override for custom storage backends. @protected */\n protected getItem(key: string): Promise<string | null> {\n if (_adapter) return _adapter.getItem(key);\n if (__DEV__) {\n throw new Error(\n `[mvc-kit] No storage adapter configured for \"${this.constructor.name}\". ` +\n `Call NativeCollection.configure() at app startup, or override getItem/setItem/removeItem.`,\n );\n }\n throw new Error('[mvc-kit] No storage adapter configured.');\n }\n\n /** Write a value to the storage adapter. Override for custom storage backends. @protected */\n protected setItem(key: string, value: string): Promise<void> {\n if (_adapter) return _adapter.setItem(key, value);\n if (__DEV__) {\n throw new Error(\n `[mvc-kit] No storage adapter configured for \"${this.constructor.name}\". ` +\n `Call NativeCollection.configure() at app startup, or override getItem/setItem/removeItem.`,\n );\n }\n throw new Error('[mvc-kit] No storage adapter configured.');\n }\n\n /** Remove a value from the storage adapter. Override for custom storage backends. @protected */\n protected removeItem(key: string): Promise<void> {\n if (_adapter) return _adapter.removeItem(key);\n if (__DEV__) {\n throw new Error(\n `[mvc-kit] No storage adapter configured for \"${this.constructor.name}\". ` +\n `Call NativeCollection.configure() at app startup, or override getItem/setItem/removeItem.`,\n );\n }\n throw new Error('[mvc-kit] No storage adapter configured.');\n }\n\n // ── Persist interface (blob strategy) ──\n\n protected async persistGetAll(): Promise<T[]> {\n const raw = await this.getItem(this.storageKey);\n if (!raw) return [];\n try {\n return this.deserialize(raw);\n } catch {\n if (__DEV__) {\n console.warn(\n `[mvc-kit] Corrupted data in storage key \"${this.storageKey}\". Ignoring stored data.`,\n );\n }\n return [];\n }\n }\n\n protected async persistGet(id: T['id']): Promise<T | null> {\n const all = await this.persistGetAll();\n return all.find((i) => i.id === id) ?? null;\n }\n\n protected async persistSet(_items: T[]): Promise<void> {\n await this.setItem(this.storageKey, this.serialize([...this.items]));\n }\n\n protected async persistRemove(_ids: T['id'][]): Promise<void> {\n await this.setItem(this.storageKey, this.serialize([...this.items]));\n }\n\n protected async persistClear(): Promise<void> {\n await this.removeItem(this.storageKey);\n }\n}\n"],"names":[],"mappings":";AAEA,MAAM,UAAU,OAAO,oBAAoB,eAAe;AAQ1D,IAAI,WAAkC;AAwC/B,MAAe,yBAEZ,qBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKhC,OAAO,UAAU,SAA+B;AAC9C,eAAW;AAAA,EACb;AAAA;AAAA,EAGA,OAAO,eAAqB;AAC1B,eAAW;AAAA,EACb;AAAA;AAAA;AAAA,EAKU,QAAQ,KAAqC;AACrD,QAAI,SAAU,QAAO,SAAS,QAAQ,GAAG;AACzC,QAAI,SAAS;AACX,YAAM,IAAI;AAAA,QACR,gDAAgD,KAAK,YAAY,IAAI;AAAA,MAAA;AAAA,IAGzE;AACA,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AAAA;AAAA,EAGU,QAAQ,KAAa,OAA8B;AAC3D,QAAI,SAAU,QAAO,SAAS,QAAQ,KAAK,KAAK;AAChD,QAAI,SAAS;AACX,YAAM,IAAI;AAAA,QACR,gDAAgD,KAAK,YAAY,IAAI;AAAA,MAAA;AAAA,IAGzE;AACA,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AAAA;AAAA,EAGU,WAAW,KAA4B;AAC/C,QAAI,SAAU,QAAO,SAAS,WAAW,GAAG;AAC5C,QAAI,SAAS;AACX,YAAM,IAAI;AAAA,QACR,gDAAgD,KAAK,YAAY,IAAI;AAAA,MAAA;AAAA,IAGzE;AACA,UAAM,IAAI,MAAM,0CAA0C;AAAA,EAC5D;AAAA;AAAA,EAIA,MAAgB,gBAA8B;AAC5C,UAAM,MAAM,MAAM,KAAK,QAAQ,KAAK,UAAU;AAC9C,QAAI,CAAC,IAAK,QAAO,CAAA;AACjB,QAAI;AACF,aAAO,KAAK,YAAY,GAAG;AAAA,IAC7B,QAAQ;AACN,UAAI,SAAS;AACX,gBAAQ;AAAA,UACN,4CAA4C,KAAK,UAAU;AAAA,QAAA;AAAA,MAE/D;AACA,aAAO,CAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAgB,WAAW,IAAgC;AACzD,UAAM,MAAM,MAAM,KAAK,cAAA;AACvB,WAAO,IAAI,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK;AAAA,EACzC;AAAA,EAEA,MAAgB,WAAW,QAA4B;AACrD,UAAM,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC;AAAA,EACrE;AAAA,EAEA,MAAgB,cAAc,MAAgC;AAC5D,UAAM,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC;AAAA,EACrE;AAAA,EAEA,MAAgB,eAA8B;AAC5C,UAAM,KAAK,WAAW,KAAK,UAAU;AAAA,EACvC;AACF;"}
|
package/dist/react.cjs
CHANGED
|
@@ -7,6 +7,9 @@ const useModel = require("./react/use-model.cjs");
|
|
|
7
7
|
const useEventBus = require("./react/use-event-bus.cjs");
|
|
8
8
|
const useTeardown = require("./react/use-teardown.cjs");
|
|
9
9
|
const provider = require("./react/provider.cjs");
|
|
10
|
+
const DataTable = require("./react/components/DataTable.cjs");
|
|
11
|
+
const CardList = require("./react/components/CardList.cjs");
|
|
12
|
+
const InfiniteScroll = require("./react/components/InfiniteScroll.cjs");
|
|
10
13
|
exports.useInstance = useInstance.useInstance;
|
|
11
14
|
exports.useLocal = useLocal.useLocal;
|
|
12
15
|
exports.useSingleton = useSingleton.useSingleton;
|
|
@@ -18,4 +21,7 @@ exports.useEvent = useEventBus.useEvent;
|
|
|
18
21
|
exports.useTeardown = useTeardown.useTeardown;
|
|
19
22
|
exports.Provider = provider.Provider;
|
|
20
23
|
exports.useResolve = provider.useResolve;
|
|
24
|
+
exports.DataTable = DataTable.DataTable;
|
|
25
|
+
exports.CardList = CardList.CardList;
|
|
26
|
+
exports.InfiniteScroll = InfiniteScroll.InfiniteScroll;
|
|
21
27
|
//# sourceMappingURL=react.cjs.map
|
package/dist/react.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"react.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/react.js
CHANGED
|
@@ -5,7 +5,13 @@ import { useField, useModel, useModelRef } from "./react/use-model.js";
|
|
|
5
5
|
import { useEmit, useEvent } from "./react/use-event-bus.js";
|
|
6
6
|
import { useTeardown } from "./react/use-teardown.js";
|
|
7
7
|
import { Provider, useResolve } from "./react/provider.js";
|
|
8
|
+
import { DataTable } from "./react/components/DataTable.js";
|
|
9
|
+
import { CardList } from "./react/components/CardList.js";
|
|
10
|
+
import { InfiniteScroll } from "./react/components/InfiniteScroll.js";
|
|
8
11
|
export {
|
|
12
|
+
CardList,
|
|
13
|
+
DataTable,
|
|
14
|
+
InfiniteScroll,
|
|
9
15
|
Provider,
|
|
10
16
|
useEmit,
|
|
11
17
|
useEvent,
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"react.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
|
package/dist/web/idb.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idb.cjs","sources":["../../src/web/idb.ts"],"sourcesContent":["/**\n * Shared IndexedDB connection manager.\n * Deduplicates indexedDB.open() calls and handles dynamic object store creation\n * by bumping the DB version when a new storageKey is encountered.\n */\n\nconst _connections = new Map<string, IDBDatabase>();\nconst _stores = new Map<string, Set<string>>(); // dbName → known store names\nlet _openQueue: Promise<void> = Promise.resolve(); // Sequential open queue\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n // If we already have a connection with the required store, reuse it\n const existing = _connections.get(dbName);\n if (existing) {\n if (existing.objectStoreNames.contains(storeName)) {\n return Promise.resolve(existing);\n }\n // Need to add a new store — close and reopen with bumped version\n existing.close();\n _connections.delete(dbName);\n }\n\n // Track known stores\n let stores = _stores.get(dbName);\n if (!stores) {\n stores = new Set();\n _stores.set(dbName, stores);\n }\n stores.add(storeName);\n\n // Serialize opens to prevent version conflicts\n const result = _openQueue.then(() => doOpen(dbName, stores!));\n _openQueue = result.then(() => {}, () => {}); // Absorb errors for the queue\n return result;\n}\n\nfunction doOpen(dbName: string, stores: Set<string>): Promise<IDBDatabase> {\n // Close existing cached connection if any (may have been opened by queued op)\n const existingDb = _connections.get(dbName);\n existingDb?.close();\n _connections.delete(dbName);\n\n // Probe current DB version (version-less open never triggers upgrade)\n return new Promise<IDBDatabase>((resolve, reject) => {\n const probe = indexedDB.open(dbName);\n probe.onsuccess = () => {\n const db = probe.result;\n const version = db.version;\n\n // Check if all required stores already exist\n let needsUpgrade = false;\n for (const name of stores) {\n if (!db.objectStoreNames.contains(name)) {\n needsUpgrade = true;\n break;\n }\n }\n\n if (!needsUpgrade) {\n _connections.set(dbName, db);\n resolve(db);\n return;\n }\n\n // Need new stores — close and reopen with bumped version\n db.close();\n const upgrade = indexedDB.open(dbName, version + 1);\n upgrade.onupgradeneeded = () => {\n const udb = upgrade.result;\n for (const name of stores) {\n if (!udb.objectStoreNames.contains(name)) {\n udb.createObjectStore(name, { keyPath: 'id' });\n }\n }\n };\n upgrade.onsuccess = () => {\n _connections.set(dbName, upgrade.result);\n resolve(upgrade.result);\n };\n upgrade.onerror = () => reject(upgrade.error);\n };\n probe.onerror = () => reject(probe.error);\n });\n}\n\nexport function getStore(\n dbName: string,\n storeName: string,\n mode: IDBTransactionMode,\n): Promise<IDBObjectStore> {\n return openDB(dbName, storeName).then((db) => {\n const tx = db.transaction(storeName, mode);\n return tx.objectStore(storeName);\n });\n}\n\nexport function idbGetAll<T>(store: IDBObjectStore): Promise<T[]> {\n return new Promise((resolve, reject) => {\n const request = store.getAll();\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nexport function idbGet<T>(store: IDBObjectStore, id: IDBValidKey): Promise<T | null> {\n return new Promise((resolve, reject) => {\n const request = store.get(id);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n}\n\nexport function idbPut<T>(store: IDBObjectStore, items: T[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const item of items) {\n store.put(item);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport function idbDelete(store: IDBObjectStore, ids: IDBValidKey[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const id of ids) {\n store.delete(id);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport function idbClear(store: IDBObjectStore): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n store.clear();\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/** Close all cached connections and delete all known databases. Used in test cleanup. */\nexport function closeAllConnections(): void {\n for (const db of _connections.values()) {\n db.close();\n }\n _connections.clear();\n _stores.clear();\n _openQueue = Promise.resolve();\n}\n\n/** Delete a database by name. Returns a promise that resolves when deleted. */\nexport function deleteDatabase(dbName: string): Promise<void> {\n const existing = _connections.get(dbName);\n existing?.close();\n _connections.delete(dbName);\n _stores.delete(dbName);\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.deleteDatabase(dbName);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n}\n"],"names":[],"mappings":";;AAMA,MAAM,mCAAmB,IAAA;AACzB,MAAM,8BAAc,IAAA;AACpB,IAAI,aAA4B,QAAQ,QAAA;AAExC,SAAS,OAAO,QAAgB,WAAyC;AAEvE,QAAM,WAAW,aAAa,IAAI,MAAM;AACxC,MAAI,UAAU;AACZ,QAAI,SAAS,iBAAiB,SAAS,SAAS,GAAG;AACjD,aAAO,QAAQ,QAAQ,QAAQ;AAAA,IACjC;AAEA,aAAS,MAAA;AACT,iBAAa,OAAO,MAAM;AAAA,EAC5B;AAGA,MAAI,SAAS,QAAQ,IAAI,MAAM;AAC/B,MAAI,CAAC,QAAQ;AACX,iCAAa,IAAA;AACb,YAAQ,IAAI,QAAQ,MAAM;AAAA,EAC5B;AACA,SAAO,IAAI,SAAS;AAGpB,QAAM,SAAS,WAAW,KAAK,MAAM,OAAO,QAAQ,MAAO,CAAC;AAC5D,eAAa,OAAO,KAAK,MAAM;AAAA,EAAC,GAAG,MAAM;AAAA,EAAC,CAAC;AAC3C,SAAO;AACT;AAEA,SAAS,OAAO,QAAgB,QAA2C;AAEzE,QAAM,aAAa,aAAa,IAAI,MAAM;AAC1C,cAAY,MAAA;AACZ,eAAa,OAAO,MAAM;AAG1B,SAAO,IAAI,QAAqB,CAAC,SAAS,WAAW;AACnD,UAAM,QAAQ,UAAU,KAAK,MAAM;AACnC,UAAM,YAAY,MAAM;AACtB,YAAM,KAAK,MAAM;AACjB,YAAM,UAAU,GAAG;AAGnB,UAAI,eAAe;AACnB,iBAAW,QAAQ,QAAQ;AACzB,YAAI,CAAC,GAAG,iBAAiB,SAAS,IAAI,GAAG;AACvC,yBAAe;AACf;AAAA,QACF;AAAA,MACF;AAEA,UAAI,CAAC,cAAc;AACjB,qBAAa,IAAI,QAAQ,EAAE;AAC3B,gBAAQ,EAAE;AACV;AAAA,MACF;AAGA,SAAG,MAAA;AACH,YAAM,UAAU,UAAU,KAAK,QAAQ,UAAU,CAAC;AAClD,cAAQ,kBAAkB,MAAM;AAC9B,cAAM,MAAM,QAAQ;AACpB,mBAAW,QAAQ,QAAQ;AACzB,cAAI,CAAC,IAAI,iBAAiB,SAAS,IAAI,GAAG;AACxC,gBAAI,kBAAkB,MAAM,EAAE,SAAS,MAAM;AAAA,UAC/C;AAAA,QACF;AAAA,MACF;AACA,cAAQ,YAAY,MAAM;AACxB,qBAAa,IAAI,QAAQ,QAAQ,MAAM;AACvC,gBAAQ,QAAQ,MAAM;AAAA,MACxB;AACA,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,IAC9C;AACA,UAAM,UAAU,MAAM,OAAO,MAAM,KAAK;AAAA,EAC1C,CAAC;AACH;
|
|
1
|
+
{"version":3,"file":"idb.cjs","sources":["../../src/web/idb.ts"],"sourcesContent":["/**\n * Shared IndexedDB connection manager.\n * Deduplicates indexedDB.open() calls and handles dynamic object store creation\n * by bumping the DB version when a new storageKey is encountered.\n */\n\nconst _connections = new Map<string, IDBDatabase>();\nconst _stores = new Map<string, Set<string>>(); // dbName → known store names\nlet _openQueue: Promise<void> = Promise.resolve(); // Sequential open queue\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n // If we already have a connection with the required store, reuse it\n const existing = _connections.get(dbName);\n if (existing) {\n if (existing.objectStoreNames.contains(storeName)) {\n return Promise.resolve(existing);\n }\n // Need to add a new store — close and reopen with bumped version\n existing.close();\n _connections.delete(dbName);\n }\n\n // Track known stores\n let stores = _stores.get(dbName);\n if (!stores) {\n stores = new Set();\n _stores.set(dbName, stores);\n }\n stores.add(storeName);\n\n // Serialize opens to prevent version conflicts\n const result = _openQueue.then(() => doOpen(dbName, stores!));\n _openQueue = result.then(() => {}, () => {}); // Absorb errors for the queue\n return result;\n}\n\nfunction doOpen(dbName: string, stores: Set<string>): Promise<IDBDatabase> {\n // Close existing cached connection if any (may have been opened by queued op)\n const existingDb = _connections.get(dbName);\n existingDb?.close();\n _connections.delete(dbName);\n\n // Probe current DB version (version-less open never triggers upgrade)\n return new Promise<IDBDatabase>((resolve, reject) => {\n const probe = indexedDB.open(dbName);\n probe.onsuccess = () => {\n const db = probe.result;\n const version = db.version;\n\n // Check if all required stores already exist\n let needsUpgrade = false;\n for (const name of stores) {\n if (!db.objectStoreNames.contains(name)) {\n needsUpgrade = true;\n break;\n }\n }\n\n if (!needsUpgrade) {\n _connections.set(dbName, db);\n resolve(db);\n return;\n }\n\n // Need new stores — close and reopen with bumped version\n db.close();\n const upgrade = indexedDB.open(dbName, version + 1);\n upgrade.onupgradeneeded = () => {\n const udb = upgrade.result;\n for (const name of stores) {\n if (!udb.objectStoreNames.contains(name)) {\n udb.createObjectStore(name, { keyPath: 'id' });\n }\n }\n };\n upgrade.onsuccess = () => {\n _connections.set(dbName, upgrade.result);\n resolve(upgrade.result);\n };\n upgrade.onerror = () => reject(upgrade.error);\n };\n probe.onerror = () => reject(probe.error);\n });\n}\n\n/**\n * Open a transaction and return the object store for the given database and store name.\n */\nexport function getStore(\n dbName: string,\n storeName: string,\n mode: IDBTransactionMode,\n): Promise<IDBObjectStore> {\n return openDB(dbName, storeName).then((db) => {\n const tx = db.transaction(storeName, mode);\n return tx.objectStore(storeName);\n });\n}\n\n/**\n * Retrieve all items from an IndexedDB object store.\n */\nexport function idbGetAll<T>(store: IDBObjectStore): Promise<T[]> {\n return new Promise((resolve, reject) => {\n const request = store.getAll();\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\n/**\n * Retrieve a single item by key from an IndexedDB object store.\n */\nexport function idbGet<T>(store: IDBObjectStore, id: IDBValidKey): Promise<T | null> {\n return new Promise((resolve, reject) => {\n const request = store.get(id);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n}\n\n/**\n * Write (put) multiple items into an IndexedDB object store.\n */\nexport function idbPut<T>(store: IDBObjectStore, items: T[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const item of items) {\n store.put(item);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/**\n * Delete multiple items by key from an IndexedDB object store.\n */\nexport function idbDelete(store: IDBObjectStore, ids: IDBValidKey[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const id of ids) {\n store.delete(id);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/**\n * Clear all items from an IndexedDB object store.\n */\nexport function idbClear(store: IDBObjectStore): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n store.clear();\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/** Close all cached connections and delete all known databases. Used in test cleanup. */\nexport function closeAllConnections(): void {\n for (const db of _connections.values()) {\n db.close();\n }\n _connections.clear();\n _stores.clear();\n _openQueue = Promise.resolve();\n}\n\n/** Delete a database by name. Returns a promise that resolves when deleted. */\nexport function deleteDatabase(dbName: string): Promise<void> {\n const existing = _connections.get(dbName);\n existing?.close();\n _connections.delete(dbName);\n _stores.delete(dbName);\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.deleteDatabase(dbName);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n}\n"],"names":[],"mappings":"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"}
|
package/dist/web/idb.d.ts
CHANGED
|
@@ -3,11 +3,29 @@
|
|
|
3
3
|
* Deduplicates indexedDB.open() calls and handles dynamic object store creation
|
|
4
4
|
* by bumping the DB version when a new storageKey is encountered.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Open a transaction and return the object store for the given database and store name.
|
|
8
|
+
*/
|
|
6
9
|
export declare function getStore(dbName: string, storeName: string, mode: IDBTransactionMode): Promise<IDBObjectStore>;
|
|
10
|
+
/**
|
|
11
|
+
* Retrieve all items from an IndexedDB object store.
|
|
12
|
+
*/
|
|
7
13
|
export declare function idbGetAll<T>(store: IDBObjectStore): Promise<T[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve a single item by key from an IndexedDB object store.
|
|
16
|
+
*/
|
|
8
17
|
export declare function idbGet<T>(store: IDBObjectStore, id: IDBValidKey): Promise<T | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Write (put) multiple items into an IndexedDB object store.
|
|
20
|
+
*/
|
|
9
21
|
export declare function idbPut<T>(store: IDBObjectStore, items: T[]): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Delete multiple items by key from an IndexedDB object store.
|
|
24
|
+
*/
|
|
10
25
|
export declare function idbDelete(store: IDBObjectStore, ids: IDBValidKey[]): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Clear all items from an IndexedDB object store.
|
|
28
|
+
*/
|
|
11
29
|
export declare function idbClear(store: IDBObjectStore): Promise<void>;
|
|
12
30
|
/** Close all cached connections and delete all known databases. Used in test cleanup. */
|
|
13
31
|
export declare function closeAllConnections(): void;
|
package/dist/web/idb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idb.d.ts","sourceRoot":"","sources":["../../src/web/idb.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiFH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CAKzB;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAMhE;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAMnF;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CASlF;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7D;AAED,yFAAyF;AACzF,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5D"}
|
|
1
|
+
{"version":3,"file":"idb.d.ts","sourceRoot":"","sources":["../../src/web/idb.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAiFH;;GAEG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CAKzB;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAMhE;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAMnF;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1E;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CASlF;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAO7D;AAED,yFAAyF;AACzF,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5D"}
|
package/dist/web/idb.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idb.js","sources":["../../src/web/idb.ts"],"sourcesContent":["/**\n * Shared IndexedDB connection manager.\n * Deduplicates indexedDB.open() calls and handles dynamic object store creation\n * by bumping the DB version when a new storageKey is encountered.\n */\n\nconst _connections = new Map<string, IDBDatabase>();\nconst _stores = new Map<string, Set<string>>(); // dbName → known store names\nlet _openQueue: Promise<void> = Promise.resolve(); // Sequential open queue\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n // If we already have a connection with the required store, reuse it\n const existing = _connections.get(dbName);\n if (existing) {\n if (existing.objectStoreNames.contains(storeName)) {\n return Promise.resolve(existing);\n }\n // Need to add a new store — close and reopen with bumped version\n existing.close();\n _connections.delete(dbName);\n }\n\n // Track known stores\n let stores = _stores.get(dbName);\n if (!stores) {\n stores = new Set();\n _stores.set(dbName, stores);\n }\n stores.add(storeName);\n\n // Serialize opens to prevent version conflicts\n const result = _openQueue.then(() => doOpen(dbName, stores!));\n _openQueue = result.then(() => {}, () => {}); // Absorb errors for the queue\n return result;\n}\n\nfunction doOpen(dbName: string, stores: Set<string>): Promise<IDBDatabase> {\n // Close existing cached connection if any (may have been opened by queued op)\n const existingDb = _connections.get(dbName);\n existingDb?.close();\n _connections.delete(dbName);\n\n // Probe current DB version (version-less open never triggers upgrade)\n return new Promise<IDBDatabase>((resolve, reject) => {\n const probe = indexedDB.open(dbName);\n probe.onsuccess = () => {\n const db = probe.result;\n const version = db.version;\n\n // Check if all required stores already exist\n let needsUpgrade = false;\n for (const name of stores) {\n if (!db.objectStoreNames.contains(name)) {\n needsUpgrade = true;\n break;\n }\n }\n\n if (!needsUpgrade) {\n _connections.set(dbName, db);\n resolve(db);\n return;\n }\n\n // Need new stores — close and reopen with bumped version\n db.close();\n const upgrade = indexedDB.open(dbName, version + 1);\n upgrade.onupgradeneeded = () => {\n const udb = upgrade.result;\n for (const name of stores) {\n if (!udb.objectStoreNames.contains(name)) {\n udb.createObjectStore(name, { keyPath: 'id' });\n }\n }\n };\n upgrade.onsuccess = () => {\n _connections.set(dbName, upgrade.result);\n resolve(upgrade.result);\n };\n upgrade.onerror = () => reject(upgrade.error);\n };\n probe.onerror = () => reject(probe.error);\n });\n}\n\nexport function getStore(\n dbName: string,\n storeName: string,\n mode: IDBTransactionMode,\n): Promise<IDBObjectStore> {\n return openDB(dbName, storeName).then((db) => {\n const tx = db.transaction(storeName, mode);\n return tx.objectStore(storeName);\n });\n}\n\nexport function idbGetAll<T>(store: IDBObjectStore): Promise<T[]> {\n return new Promise((resolve, reject) => {\n const request = store.getAll();\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nexport function idbGet<T>(store: IDBObjectStore, id: IDBValidKey): Promise<T | null> {\n return new Promise((resolve, reject) => {\n const request = store.get(id);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n}\n\nexport function idbPut<T>(store: IDBObjectStore, items: T[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const item of items) {\n store.put(item);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport function idbDelete(store: IDBObjectStore, ids: IDBValidKey[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const id of ids) {\n store.delete(id);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\nexport function idbClear(store: IDBObjectStore): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n store.clear();\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/** Close all cached connections and delete all known databases. Used in test cleanup. */\nexport function closeAllConnections(): void {\n for (const db of _connections.values()) {\n db.close();\n }\n _connections.clear();\n _stores.clear();\n _openQueue = Promise.resolve();\n}\n\n/** Delete a database by name. Returns a promise that resolves when deleted. */\nexport function deleteDatabase(dbName: string): Promise<void> {\n const existing = _connections.get(dbName);\n existing?.close();\n _connections.delete(dbName);\n _stores.delete(dbName);\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.deleteDatabase(dbName);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n}\n"],"names":[],"mappings":"AAMA,MAAM,mCAAmB,IAAA;AACzB,MAAM,8BAAc,IAAA;AACpB,IAAI,aAA4B,QAAQ,QAAA;AAExC,SAAS,OAAO,QAAgB,WAAyC;AAEvE,QAAM,WAAW,aAAa,IAAI,MAAM;AACxC,MAAI,UAAU;AACZ,QAAI,SAAS,iBAAiB,SAAS,SAAS,GAAG;AACjD,aAAO,QAAQ,QAAQ,QAAQ;AAAA,IACjC;AAEA,aAAS,MAAA;AACT,iBAAa,OAAO,MAAM;AAAA,EAC5B;AAGA,MAAI,SAAS,QAAQ,IAAI,MAAM;AAC/B,MAAI,CAAC,QAAQ;AACX,iCAAa,IAAA;AACb,YAAQ,IAAI,QAAQ,MAAM;AAAA,EAC5B;AACA,SAAO,IAAI,SAAS;AAGpB,QAAM,SAAS,WAAW,KAAK,MAAM,OAAO,QAAQ,MAAO,CAAC;AAC5D,eAAa,OAAO,KAAK,MAAM;AAAA,EAAC,GAAG,MAAM;AAAA,EAAC,CAAC;AAC3C,SAAO;AACT;AAEA,SAAS,OAAO,QAAgB,QAA2C;AAEzE,QAAM,aAAa,aAAa,IAAI,MAAM;AAC1C,cAAY,MAAA;AACZ,eAAa,OAAO,MAAM;AAG1B,SAAO,IAAI,QAAqB,CAAC,SAAS,WAAW;AACnD,UAAM,QAAQ,UAAU,KAAK,MAAM;AACnC,UAAM,YAAY,MAAM;AACtB,YAAM,KAAK,MAAM;AACjB,YAAM,UAAU,GAAG;AAGnB,UAAI,eAAe;AACnB,iBAAW,QAAQ,QAAQ;AACzB,YAAI,CAAC,GAAG,iBAAiB,SAAS,IAAI,GAAG;AACvC,yBAAe;AACf;AAAA,QACF;AAAA,MACF;AAEA,UAAI,CAAC,cAAc;AACjB,qBAAa,IAAI,QAAQ,EAAE;AAC3B,gBAAQ,EAAE;AACV;AAAA,MACF;AAGA,SAAG,MAAA;AACH,YAAM,UAAU,UAAU,KAAK,QAAQ,UAAU,CAAC;AAClD,cAAQ,kBAAkB,MAAM;AAC9B,cAAM,MAAM,QAAQ;AACpB,mBAAW,QAAQ,QAAQ;AACzB,cAAI,CAAC,IAAI,iBAAiB,SAAS,IAAI,GAAG;AACxC,gBAAI,kBAAkB,MAAM,EAAE,SAAS,MAAM;AAAA,UAC/C;AAAA,QACF;AAAA,MACF;AACA,cAAQ,YAAY,MAAM;AACxB,qBAAa,IAAI,QAAQ,QAAQ,MAAM;AACvC,gBAAQ,QAAQ,MAAM;AAAA,MACxB;AACA,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,IAC9C;AACA,UAAM,UAAU,MAAM,OAAO,MAAM,KAAK;AAAA,EAC1C,CAAC;AACH;
|
|
1
|
+
{"version":3,"file":"idb.js","sources":["../../src/web/idb.ts"],"sourcesContent":["/**\n * Shared IndexedDB connection manager.\n * Deduplicates indexedDB.open() calls and handles dynamic object store creation\n * by bumping the DB version when a new storageKey is encountered.\n */\n\nconst _connections = new Map<string, IDBDatabase>();\nconst _stores = new Map<string, Set<string>>(); // dbName → known store names\nlet _openQueue: Promise<void> = Promise.resolve(); // Sequential open queue\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n // If we already have a connection with the required store, reuse it\n const existing = _connections.get(dbName);\n if (existing) {\n if (existing.objectStoreNames.contains(storeName)) {\n return Promise.resolve(existing);\n }\n // Need to add a new store — close and reopen with bumped version\n existing.close();\n _connections.delete(dbName);\n }\n\n // Track known stores\n let stores = _stores.get(dbName);\n if (!stores) {\n stores = new Set();\n _stores.set(dbName, stores);\n }\n stores.add(storeName);\n\n // Serialize opens to prevent version conflicts\n const result = _openQueue.then(() => doOpen(dbName, stores!));\n _openQueue = result.then(() => {}, () => {}); // Absorb errors for the queue\n return result;\n}\n\nfunction doOpen(dbName: string, stores: Set<string>): Promise<IDBDatabase> {\n // Close existing cached connection if any (may have been opened by queued op)\n const existingDb = _connections.get(dbName);\n existingDb?.close();\n _connections.delete(dbName);\n\n // Probe current DB version (version-less open never triggers upgrade)\n return new Promise<IDBDatabase>((resolve, reject) => {\n const probe = indexedDB.open(dbName);\n probe.onsuccess = () => {\n const db = probe.result;\n const version = db.version;\n\n // Check if all required stores already exist\n let needsUpgrade = false;\n for (const name of stores) {\n if (!db.objectStoreNames.contains(name)) {\n needsUpgrade = true;\n break;\n }\n }\n\n if (!needsUpgrade) {\n _connections.set(dbName, db);\n resolve(db);\n return;\n }\n\n // Need new stores — close and reopen with bumped version\n db.close();\n const upgrade = indexedDB.open(dbName, version + 1);\n upgrade.onupgradeneeded = () => {\n const udb = upgrade.result;\n for (const name of stores) {\n if (!udb.objectStoreNames.contains(name)) {\n udb.createObjectStore(name, { keyPath: 'id' });\n }\n }\n };\n upgrade.onsuccess = () => {\n _connections.set(dbName, upgrade.result);\n resolve(upgrade.result);\n };\n upgrade.onerror = () => reject(upgrade.error);\n };\n probe.onerror = () => reject(probe.error);\n });\n}\n\n/**\n * Open a transaction and return the object store for the given database and store name.\n */\nexport function getStore(\n dbName: string,\n storeName: string,\n mode: IDBTransactionMode,\n): Promise<IDBObjectStore> {\n return openDB(dbName, storeName).then((db) => {\n const tx = db.transaction(storeName, mode);\n return tx.objectStore(storeName);\n });\n}\n\n/**\n * Retrieve all items from an IndexedDB object store.\n */\nexport function idbGetAll<T>(store: IDBObjectStore): Promise<T[]> {\n return new Promise((resolve, reject) => {\n const request = store.getAll();\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\n/**\n * Retrieve a single item by key from an IndexedDB object store.\n */\nexport function idbGet<T>(store: IDBObjectStore, id: IDBValidKey): Promise<T | null> {\n return new Promise((resolve, reject) => {\n const request = store.get(id);\n request.onsuccess = () => resolve(request.result ?? null);\n request.onerror = () => reject(request.error);\n });\n}\n\n/**\n * Write (put) multiple items into an IndexedDB object store.\n */\nexport function idbPut<T>(store: IDBObjectStore, items: T[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const item of items) {\n store.put(item);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/**\n * Delete multiple items by key from an IndexedDB object store.\n */\nexport function idbDelete(store: IDBObjectStore, ids: IDBValidKey[]): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n for (const id of ids) {\n store.delete(id);\n }\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/**\n * Clear all items from an IndexedDB object store.\n */\nexport function idbClear(store: IDBObjectStore): Promise<void> {\n return new Promise((resolve, reject) => {\n const tx = store.transaction;\n store.clear();\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n}\n\n/** Close all cached connections and delete all known databases. Used in test cleanup. */\nexport function closeAllConnections(): void {\n for (const db of _connections.values()) {\n db.close();\n }\n _connections.clear();\n _stores.clear();\n _openQueue = Promise.resolve();\n}\n\n/** Delete a database by name. Returns a promise that resolves when deleted. */\nexport function deleteDatabase(dbName: string): Promise<void> {\n const existing = _connections.get(dbName);\n existing?.close();\n _connections.delete(dbName);\n _stores.delete(dbName);\n\n return new Promise((resolve, reject) => {\n const request = indexedDB.deleteDatabase(dbName);\n request.onsuccess = () => resolve();\n request.onerror = () => reject(request.error);\n });\n}\n"],"names":[],"mappings":"AAMA,MAAM,mCAAmB,IAAA;AACzB,MAAM,8BAAc,IAAA;AACpB,IAAI,aAA4B,QAAQ,QAAA;AAExC,SAAS,OAAO,QAAgB,WAAyC;AAEvE,QAAM,WAAW,aAAa,IAAI,MAAM;AACxC,MAAI,UAAU;AACZ,QAAI,SAAS,iBAAiB,SAAS,SAAS,GAAG;AACjD,aAAO,QAAQ,QAAQ,QAAQ;AAAA,IACjC;AAEA,aAAS,MAAA;AACT,iBAAa,OAAO,MAAM;AAAA,EAC5B;AAGA,MAAI,SAAS,QAAQ,IAAI,MAAM;AAC/B,MAAI,CAAC,QAAQ;AACX,iCAAa,IAAA;AACb,YAAQ,IAAI,QAAQ,MAAM;AAAA,EAC5B;AACA,SAAO,IAAI,SAAS;AAGpB,QAAM,SAAS,WAAW,KAAK,MAAM,OAAO,QAAQ,MAAO,CAAC;AAC5D,eAAa,OAAO,KAAK,MAAM;AAAA,EAAC,GAAG,MAAM;AAAA,EAAC,CAAC;AAC3C,SAAO;AACT;AAEA,SAAS,OAAO,QAAgB,QAA2C;AAEzE,QAAM,aAAa,aAAa,IAAI,MAAM;AAC1C,cAAY,MAAA;AACZ,eAAa,OAAO,MAAM;AAG1B,SAAO,IAAI,QAAqB,CAAC,SAAS,WAAW;AACnD,UAAM,QAAQ,UAAU,KAAK,MAAM;AACnC,UAAM,YAAY,MAAM;AACtB,YAAM,KAAK,MAAM;AACjB,YAAM,UAAU,GAAG;AAGnB,UAAI,eAAe;AACnB,iBAAW,QAAQ,QAAQ;AACzB,YAAI,CAAC,GAAG,iBAAiB,SAAS,IAAI,GAAG;AACvC,yBAAe;AACf;AAAA,QACF;AAAA,MACF;AAEA,UAAI,CAAC,cAAc;AACjB,qBAAa,IAAI,QAAQ,EAAE;AAC3B,gBAAQ,EAAE;AACV;AAAA,MACF;AAGA,SAAG,MAAA;AACH,YAAM,UAAU,UAAU,KAAK,QAAQ,UAAU,CAAC;AAClD,cAAQ,kBAAkB,MAAM;AAC9B,cAAM,MAAM,QAAQ;AACpB,mBAAW,QAAQ,QAAQ;AACzB,cAAI,CAAC,IAAI,iBAAiB,SAAS,IAAI,GAAG;AACxC,gBAAI,kBAAkB,MAAM,EAAE,SAAS,MAAM;AAAA,UAC/C;AAAA,QACF;AAAA,MACF;AACA,cAAQ,YAAY,MAAM;AACxB,qBAAa,IAAI,QAAQ,QAAQ,MAAM;AACvC,gBAAQ,QAAQ,MAAM;AAAA,MACxB;AACA,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,IAC9C;AACA,UAAM,UAAU,MAAM,OAAO,MAAM,KAAK;AAAA,EAC1C,CAAC;AACH;AAKO,SAAS,SACd,QACA,WACA,MACyB;AACzB,SAAO,OAAO,QAAQ,SAAS,EAAE,KAAK,CAAC,OAAO;AAC5C,UAAM,KAAK,GAAG,YAAY,WAAW,IAAI;AACzC,WAAO,GAAG,YAAY,SAAS;AAAA,EACjC,CAAC;AACH;AAKO,SAAS,UAAa,OAAqC;AAChE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,UAAU,MAAM,OAAA;AACtB,YAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,YAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,EAC9C,CAAC;AACH;AAKO,SAAS,OAAU,OAAuB,IAAoC;AACnF,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,UAAU,MAAM,IAAI,EAAE;AAC5B,YAAQ,YAAY,MAAM,QAAQ,QAAQ,UAAU,IAAI;AACxD,YAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,EAC9C,CAAC;AACH;AAKO,SAAS,OAAU,OAAuB,OAA2B;AAC1E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,KAAK,MAAM;AACjB,eAAW,QAAQ,OAAO;AACxB,YAAM,IAAI,IAAI;AAAA,IAChB;AACA,OAAG,aAAa,MAAM,QAAA;AACtB,OAAG,UAAU,MAAM,OAAO,GAAG,KAAK;AAAA,EACpC,CAAC;AACH;AAKO,SAAS,UAAU,OAAuB,KAAmC;AAClF,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,KAAK,MAAM;AACjB,eAAW,MAAM,KAAK;AACpB,YAAM,OAAO,EAAE;AAAA,IACjB;AACA,OAAG,aAAa,MAAM,QAAA;AACtB,OAAG,UAAU,MAAM,OAAO,GAAG,KAAK;AAAA,EACpC,CAAC;AACH;AAKO,SAAS,SAAS,OAAsC;AAC7D,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,KAAK,MAAM;AACjB,UAAM,MAAA;AACN,OAAG,aAAa,MAAM,QAAA;AACtB,OAAG,UAAU,MAAM,OAAO,GAAG,KAAK;AAAA,EACpC,CAAC;AACH;"}
|
|
@@ -88,57 +88,37 @@ function wrapAsyncMethods(ctx) {
|
|
|
88
88
|
if (__DEV__ && activeOps) {
|
|
89
89
|
activeOps.set(key, (activeOps.get(key) ?? 0) + 1);
|
|
90
90
|
}
|
|
91
|
+
const finalizeOp = (errorMsg, errorCode) => {
|
|
92
|
+
internal.count--;
|
|
93
|
+
internal.loading = internal.count > 0;
|
|
94
|
+
if (errorMsg !== void 0) {
|
|
95
|
+
internal.error = errorMsg;
|
|
96
|
+
internal.errorCode = errorCode ?? null;
|
|
97
|
+
}
|
|
98
|
+
asyncSnapshots.set(
|
|
99
|
+
key,
|
|
100
|
+
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
101
|
+
);
|
|
102
|
+
notifyAsync();
|
|
103
|
+
if (__DEV__ && activeOps) {
|
|
104
|
+
const c = (activeOps.get(key) ?? 1) - 1;
|
|
105
|
+
if (c <= 0) activeOps.delete(key);
|
|
106
|
+
else activeOps.set(key, c);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
91
109
|
return result.then(
|
|
92
110
|
(value) => {
|
|
93
|
-
if (isDisposed())
|
|
94
|
-
internal.count--;
|
|
95
|
-
internal.loading = internal.count > 0;
|
|
96
|
-
asyncSnapshots.set(
|
|
97
|
-
key,
|
|
98
|
-
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
99
|
-
);
|
|
100
|
-
notifyAsync();
|
|
101
|
-
if (__DEV__ && activeOps) {
|
|
102
|
-
const c = (activeOps.get(key) ?? 1) - 1;
|
|
103
|
-
if (c <= 0) activeOps.delete(key);
|
|
104
|
-
else activeOps.set(key, c);
|
|
105
|
-
}
|
|
111
|
+
if (!isDisposed()) finalizeOp();
|
|
106
112
|
return value;
|
|
107
113
|
},
|
|
108
114
|
(error) => {
|
|
109
115
|
if (errors.isAbortError(error)) {
|
|
110
|
-
if (!isDisposed())
|
|
111
|
-
internal.count--;
|
|
112
|
-
internal.loading = internal.count > 0;
|
|
113
|
-
asyncSnapshots.set(
|
|
114
|
-
key,
|
|
115
|
-
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
116
|
-
);
|
|
117
|
-
notifyAsync();
|
|
118
|
-
}
|
|
119
|
-
if (__DEV__ && activeOps) {
|
|
120
|
-
const c = (activeOps.get(key) ?? 1) - 1;
|
|
121
|
-
if (c <= 0) activeOps.delete(key);
|
|
122
|
-
else activeOps.set(key, c);
|
|
123
|
-
}
|
|
116
|
+
if (!isDisposed()) finalizeOp();
|
|
124
117
|
return void 0;
|
|
125
118
|
}
|
|
126
119
|
if (isDisposed()) return void 0;
|
|
127
|
-
internal.count--;
|
|
128
|
-
internal.loading = internal.count > 0;
|
|
129
120
|
const classified = errors.classifyError(error);
|
|
130
|
-
|
|
131
|
-
internal.errorCode = classified.code;
|
|
132
|
-
asyncSnapshots.set(
|
|
133
|
-
key,
|
|
134
|
-
Object.freeze({ loading: internal.loading, error: classified.message, errorCode: classified.code })
|
|
135
|
-
);
|
|
136
|
-
notifyAsync();
|
|
137
|
-
if (__DEV__ && activeOps) {
|
|
138
|
-
const c = (activeOps.get(key) ?? 1) - 1;
|
|
139
|
-
if (c <= 0) activeOps.delete(key);
|
|
140
|
-
else activeOps.set(key, c);
|
|
141
|
-
}
|
|
121
|
+
finalizeOp(classified.message, classified.code);
|
|
142
122
|
throw error;
|
|
143
123
|
}
|
|
144
124
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapAsyncMethods.cjs","sources":["../src/wrapAsyncMethods.ts"],"sourcesContent":["import { isAbortError, classifyError } from './errors';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport type { TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nconst LOADING_TASK_STATE: TaskState = Object.freeze({ loading: true, error: null, errorCode: null });\n\nexport interface InternalTaskState {\n loading: boolean;\n error: string | null;\n errorCode: TaskState['errorCode'];\n count: number;\n}\n\nexport interface AsyncTrackingContext {\n instance: object;\n stopPrototype: object;\n reservedKeys: readonly string[];\n lifecycleHooks: Set<string>;\n isDisposed: () => boolean;\n isInitialized: () => boolean;\n asyncStates: Map<string, InternalTaskState>;\n asyncSnapshots: Map<string, TaskState>;\n asyncListeners: Set<() => void>;\n notifyAsync: () => void;\n addCleanup: (fn: () => void) => void;\n ghostTimeout: number;\n className: string;\n activeOps: Map<string, number> | null;\n /** Pre-scanned methods from class metadata cache. When provided, skips prototype walk. */\n methods?: Array<{ key: string; fn: Function }>;\n}\n\n/**\n * Shared async method wrapping logic used by both ViewModel and Resource.\n * Walks the prototype chain, wraps methods with async tracking, and registers cleanup.\n * Returns the list of wrapped method keys.\n */\nexport function wrapAsyncMethods(ctx: AsyncTrackingContext): string[] {\n const {\n instance,\n stopPrototype,\n reservedKeys,\n lifecycleHooks,\n isDisposed,\n isInitialized,\n asyncStates,\n asyncSnapshots,\n asyncListeners,\n notifyAsync,\n addCleanup,\n ghostTimeout,\n className,\n activeOps,\n } = ctx;\n\n // Instance property reserved key check (DEV-only — prototype check in constructor catches most cases)\n if (__DEV__) {\n for (const key of reservedKeys) {\n if (Object.getOwnPropertyDescriptor(instance, key)?.value !== undefined) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ${className} and cannot be overridden.`\n );\n }\n }\n }\n\n // Use pre-scanned methods from class metadata cache, or walk prototype chain\n const methodEntries: Array<{ key: string; fn: Function }> = ctx.methods ?? (() => {\n const result: Array<{ key: string; fn: Function }> = [];\n const processed = new Set<string>();\n walkPrototypeChain(instance, stopPrototype, (key, desc) => {\n if (desc.get || desc.set) return;\n if (typeof desc.value !== 'function') return;\n if (key.startsWith('_')) return;\n if (lifecycleHooks.has(key)) return;\n if (processed.has(key)) return;\n processed.add(key);\n result.push({ key, fn: desc.value });\n });\n return result;\n })();\n\n const wrappedKeys: string[] = [];\n\n for (const { key, fn: original } of methodEntries) {\n let pruned = false;\n\n const wrapper = function (this: any, ...args: unknown[]) {\n // Disposed guard\n if (isDisposed()) {\n if (__DEV__) {\n console.warn(`[mvc-kit] \"${key}\" called after dispose — ignored.`);\n }\n return undefined;\n }\n\n // Pre-init guard (DEV only — method still executes)\n if (__DEV__ && !isInitialized()) {\n console.warn(\n `[mvc-kit] \"${key}\" called before init(). ` +\n `Async tracking is active only after init().`\n );\n }\n\n let result: unknown;\n try {\n result = original.apply(instance, args);\n } catch (e) {\n // Sync throw — not tracked as async\n throw e;\n }\n\n // Sync detection: if not thenable, prune from async tracking\n if (!result || typeof (result as any).then !== 'function') {\n if (!pruned) {\n pruned = true;\n // Remove from async maps\n asyncStates.delete(key);\n asyncSnapshots.delete(key);\n // Replace wrapper with bound original for zero overhead\n (instance as any)[key] = original.bind(instance);\n }\n return result;\n }\n\n // ── Async tracking ──────────────────────────────────────\n let internal = asyncStates.get(key);\n if (!internal) {\n internal = { loading: false, error: null, errorCode: null, count: 0 };\n asyncStates.set(key, internal);\n }\n\n internal.count++;\n internal.loading = true;\n internal.error = null;\n internal.errorCode = null;\n asyncSnapshots.set(key, LOADING_TASK_STATE);\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n activeOps.set(key, (activeOps.get(key) ?? 0) + 1);\n }\n\n return (result as Promise<unknown>).then(\n (value) => {\n if (isDisposed()) return value;\n\n internal!.count--;\n internal!.loading = internal!.count > 0;\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n\n return value;\n },\n (error) => {\n // AbortError — silently swallow\n if (isAbortError(error)) {\n if (!isDisposed()) {\n internal!.count--;\n internal!.loading = internal!.count > 0;\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n }\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n\n return undefined;\n }\n\n // Disposed — fizzle silently\n if (isDisposed()) return undefined;\n\n internal!.count--;\n internal!.loading = internal!.count > 0;\n const classified = classifyError(error);\n internal!.error = classified.message;\n internal!.errorCode = classified.code;\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: classified.message, errorCode: classified.code }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n\n // Re-throw to preserve standard Promise rejection\n throw error;\n },\n );\n };\n\n wrappedKeys.push(key);\n (instance as any)[key] = wrapper;\n }\n\n // Register cleanup for disposal\n if (wrappedKeys.length > 0) {\n addCleanup(() => {\n // Snapshot active ops for ghost check before clearing\n const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;\n\n // Swap all wrapped methods to no-ops (with DEV warning)\n for (const k of wrappedKeys) {\n if (__DEV__) {\n (instance as any)[k] = () => {\n console.warn(`[mvc-kit] \"${k}\" called after dispose — ignored.`);\n return undefined;\n };\n } else {\n (instance as any)[k] = () => undefined;\n }\n }\n\n // Clear async state\n asyncListeners.clear();\n asyncStates.clear();\n asyncSnapshots.clear();\n\n // DEV: schedule ghost check\n if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {\n setTimeout(() => {\n for (const [key, count] of opsSnapshot) {\n console.warn(\n `[mvc-kit] Ghost async operation detected: \"${key}\" had ${count} ` +\n `pending call(s) when the ${className} was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, ghostTimeout);\n }\n });\n }\n\n return wrappedKeys;\n}\n"],"names":["walkPrototypeChain","isAbortError","classifyError"],"mappings":";;;;AAIA,MAAM,UAAU,OAAO,oBAAoB,eAAe;AAE1D,MAAM,qBAAgC,OAAO,OAAO,EAAE,SAAS,MAAM,OAAO,MAAM,WAAW,MAAM;AAiC5F,SAAS,iBAAiB,KAAqC;AACpE,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE;AAGJ,MAAI,SAAS;AACX,eAAW,OAAO,cAAc;AAC9B,UAAI,OAAO,yBAAyB,UAAU,GAAG,GAAG,UAAU,QAAW;AACvE,cAAM,IAAI;AAAA,UACR,cAAc,GAAG,+BAA+B,SAAS;AAAA,QAAA;AAAA,MAE7D;AAAA,IACF;AAAA,EACF;AAGA,QAAM,gBAAsD,IAAI,YAAY,MAAM;AAChF,UAAM,SAA+C,CAAA;AACrD,UAAM,gCAAgB,IAAA;AACtBA,uBAAAA,mBAAmB,UAAU,eAAe,CAAC,KAAK,SAAS;AACzD,UAAI,KAAK,OAAO,KAAK,IAAK;AAC1B,UAAI,OAAO,KAAK,UAAU,WAAY;AACtC,UAAI,IAAI,WAAW,GAAG,EAAG;AACzB,UAAI,eAAe,IAAI,GAAG,EAAG;AAC7B,UAAI,UAAU,IAAI,GAAG,EAAG;AACxB,gBAAU,IAAI,GAAG;AACjB,aAAO,KAAK,EAAE,KAAK,IAAI,KAAK,OAAO;AAAA,IACrC,CAAC;AACD,WAAO;AAAA,EACT,GAAA;AAEA,QAAM,cAAwB,CAAA;AAE9B,aAAW,EAAE,KAAK,IAAI,SAAA,KAAc,eAAe;AACjD,QAAI,SAAS;AAEb,UAAM,UAAU,YAAwB,MAAiB;AAEvD,UAAI,cAAc;AAChB,YAAI,SAAS;AACX,kBAAQ,KAAK,cAAc,GAAG,mCAAmC;AAAA,QACnE;AACA,eAAO;AAAA,MACT;AAGA,UAAI,WAAW,CAAC,iBAAiB;AAC/B,gBAAQ;AAAA,UACN,cAAc,GAAG;AAAA,QAAA;AAAA,MAGrB;AAEA,UAAI;AACJ,UAAI;AACF,iBAAS,SAAS,MAAM,UAAU,IAAI;AAAA,MACxC,SAAS,GAAG;AAEV,cAAM;AAAA,MACR;AAGA,UAAI,CAAC,UAAU,OAAQ,OAAe,SAAS,YAAY;AACzD,YAAI,CAAC,QAAQ;AACX,mBAAS;AAET,sBAAY,OAAO,GAAG;AACtB,yBAAe,OAAO,GAAG;AAExB,mBAAiB,GAAG,IAAI,SAAS,KAAK,QAAQ;AAAA,QACjD;AACA,eAAO;AAAA,MACT;AAGA,UAAI,WAAW,YAAY,IAAI,GAAG;AAClC,UAAI,CAAC,UAAU;AACb,mBAAW,EAAE,SAAS,OAAO,OAAO,MAAM,WAAW,MAAM,OAAO,EAAA;AAClE,oBAAY,IAAI,KAAK,QAAQ;AAAA,MAC/B;AAEA,eAAS;AACT,eAAS,UAAU;AACnB,eAAS,QAAQ;AACjB,eAAS,YAAY;AACrB,qBAAe,IAAI,KAAK,kBAAkB;AAC1C,kBAAA;AAEA,UAAI,WAAW,WAAW;AACxB,kBAAU,IAAI,MAAM,UAAU,IAAI,GAAG,KAAK,KAAK,CAAC;AAAA,MAClD;AAEA,aAAQ,OAA4B;AAAA,QAClC,CAAC,UAAU;AACT,cAAI,WAAA,EAAc,QAAO;AAEzB,mBAAU;AACV,mBAAU,UAAU,SAAU,QAAQ;AACtC,yBAAe;AAAA,YACb;AAAA,YACA,OAAO,OAAO,EAAE,SAAS,SAAU,SAAS,OAAO,SAAU,OAAO,WAAW,SAAU,UAAA,CAAW;AAAA,UAAA;AAEtG,sBAAA;AAEA,cAAI,WAAW,WAAW;AACxB,kBAAM,KAAK,UAAU,IAAI,GAAG,KAAK,KAAK;AACtC,gBAAI,KAAK,EAAG,WAAU,OAAO,GAAG;AAAA,gBAC3B,WAAU,IAAI,KAAK,CAAC;AAAA,UAC3B;AAEA,iBAAO;AAAA,QACT;AAAA,QACA,CAAC,UAAU;AAET,cAAIC,OAAAA,aAAa,KAAK,GAAG;AACvB,gBAAI,CAAC,cAAc;AACjB,uBAAU;AACV,uBAAU,UAAU,SAAU,QAAQ;AACtC,6BAAe;AAAA,gBACb;AAAA,gBACA,OAAO,OAAO,EAAE,SAAS,SAAU,SAAS,OAAO,SAAU,OAAO,WAAW,SAAU,UAAA,CAAW;AAAA,cAAA;AAEtG,0BAAA;AAAA,YACF;AAEA,gBAAI,WAAW,WAAW;AACxB,oBAAM,KAAK,UAAU,IAAI,GAAG,KAAK,KAAK;AACtC,kBAAI,KAAK,EAAG,WAAU,OAAO,GAAG;AAAA,kBAC3B,WAAU,IAAI,KAAK,CAAC;AAAA,YAC3B;AAEA,mBAAO;AAAA,UACT;AAGA,cAAI,WAAA,EAAc,QAAO;AAEzB,mBAAU;AACV,mBAAU,UAAU,SAAU,QAAQ;AACtC,gBAAM,aAAaC,OAAAA,cAAc,KAAK;AACtC,mBAAU,QAAQ,WAAW;AAC7B,mBAAU,YAAY,WAAW;AACjC,yBAAe;AAAA,YACb;AAAA,YACA,OAAO,OAAO,EAAE,SAAS,SAAU,SAAS,OAAO,WAAW,SAAS,WAAW,WAAW,KAAA,CAAM;AAAA,UAAA;AAErG,sBAAA;AAEA,cAAI,WAAW,WAAW;AACxB,kBAAM,KAAK,UAAU,IAAI,GAAG,KAAK,KAAK;AACtC,gBAAI,KAAK,EAAG,WAAU,OAAO,GAAG;AAAA,gBAC3B,WAAU,IAAI,KAAK,CAAC;AAAA,UAC3B;AAGA,gBAAM;AAAA,QACR;AAAA,MAAA;AAAA,IAEJ;AAEA,gBAAY,KAAK,GAAG;AACnB,aAAiB,GAAG,IAAI;AAAA,EAC3B;AAGA,MAAI,YAAY,SAAS,GAAG;AAC1B,eAAW,MAAM;AAEf,YAAM,cAAc,WAAW,YAAY,IAAI,IAAI,SAAS,IAAI;AAGhE,iBAAW,KAAK,aAAa;AAC3B,YAAI,SAAS;AACV,mBAAiB,CAAC,IAAI,MAAM;AAC3B,oBAAQ,KAAK,cAAc,CAAC,mCAAmC;AAC/D,mBAAO;AAAA,UACT;AAAA,QACF,OAAO;AACJ,mBAAiB,CAAC,IAAI,MAAM;AAAA,QAC/B;AAAA,MACF;AAGA,qBAAe,MAAA;AACf,kBAAY,MAAA;AACZ,qBAAe,MAAA;AAGf,UAAI,WAAW,eAAe,YAAY,OAAO,GAAG;AAClD,mBAAW,MAAM;AACf,qBAAW,CAAC,KAAK,KAAK,KAAK,aAAa;AACtC,oBAAQ;AAAA,cACN,8CAA8C,GAAG,SAAS,KAAK,6BACnC,SAAS;AAAA,YAAA;AAAA,UAGzC;AAAA,QACF,GAAG,YAAY;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;"}
|
|
1
|
+
{"version":3,"file":"wrapAsyncMethods.cjs","sources":["../src/wrapAsyncMethods.ts"],"sourcesContent":["import { isAbortError, classifyError } from './errors';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport type { TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nconst LOADING_TASK_STATE: TaskState = Object.freeze({ loading: true, error: null, errorCode: null });\n\n/** @internal Mutable internal async tracking state per method. */\nexport interface InternalTaskState {\n loading: boolean;\n error: string | null;\n errorCode: TaskState['errorCode'];\n count: number;\n}\n\n/** @internal Configuration for the shared async method wrapping logic. */\nexport interface AsyncTrackingContext {\n instance: object;\n stopPrototype: object;\n reservedKeys: readonly string[];\n lifecycleHooks: Set<string>;\n isDisposed: () => boolean;\n isInitialized: () => boolean;\n asyncStates: Map<string, InternalTaskState>;\n asyncSnapshots: Map<string, TaskState>;\n asyncListeners: Set<() => void>;\n notifyAsync: () => void;\n addCleanup: (fn: () => void) => void;\n ghostTimeout: number;\n className: string;\n activeOps: Map<string, number> | null;\n /** Pre-scanned methods from class metadata cache. When provided, skips prototype walk. */\n methods?: Array<{ key: string; fn: Function }>;\n}\n\n/**\n * Shared async method wrapping logic used by both ViewModel and Resource.\n * Walks the prototype chain, wraps methods with async tracking, and registers cleanup.\n * Returns the list of wrapped method keys.\n */\nexport function wrapAsyncMethods(ctx: AsyncTrackingContext): string[] {\n const {\n instance,\n stopPrototype,\n reservedKeys,\n lifecycleHooks,\n isDisposed,\n isInitialized,\n asyncStates,\n asyncSnapshots,\n asyncListeners,\n notifyAsync,\n addCleanup,\n ghostTimeout,\n className,\n activeOps,\n } = ctx;\n\n // Instance property reserved key check (DEV-only — prototype check in constructor catches most cases)\n if (__DEV__) {\n for (const key of reservedKeys) {\n if (Object.getOwnPropertyDescriptor(instance, key)?.value !== undefined) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ${className} and cannot be overridden.`\n );\n }\n }\n }\n\n // Use pre-scanned methods from class metadata cache, or walk prototype chain\n const methodEntries: Array<{ key: string; fn: Function }> = ctx.methods ?? (() => {\n const result: Array<{ key: string; fn: Function }> = [];\n const processed = new Set<string>();\n walkPrototypeChain(instance, stopPrototype, (key, desc) => {\n if (desc.get || desc.set) return;\n if (typeof desc.value !== 'function') return;\n if (key.startsWith('_')) return;\n if (lifecycleHooks.has(key)) return;\n if (processed.has(key)) return;\n processed.add(key);\n result.push({ key, fn: desc.value });\n });\n return result;\n })();\n\n const wrappedKeys: string[] = [];\n\n for (const { key, fn: original } of methodEntries) {\n let pruned = false;\n\n const wrapper = function (this: any, ...args: unknown[]) {\n // Disposed guard\n if (isDisposed()) {\n if (__DEV__) {\n console.warn(`[mvc-kit] \"${key}\" called after dispose — ignored.`);\n }\n return undefined;\n }\n\n // Pre-init guard (DEV only — method still executes)\n if (__DEV__ && !isInitialized()) {\n console.warn(\n `[mvc-kit] \"${key}\" called before init(). ` +\n `Async tracking is active only after init().`\n );\n }\n\n let result: unknown;\n try {\n result = original.apply(instance, args);\n } catch (e) {\n // Sync throw — not tracked as async\n throw e;\n }\n\n // Sync detection: if not thenable, prune from async tracking\n if (!result || typeof (result as any).then !== 'function') {\n if (!pruned) {\n pruned = true;\n // Remove from async maps\n asyncStates.delete(key);\n asyncSnapshots.delete(key);\n // Replace wrapper with bound original for zero overhead\n (instance as any)[key] = original.bind(instance);\n }\n return result;\n }\n\n // ── Async tracking ──────────────────────────────────────\n let internal = asyncStates.get(key);\n if (!internal) {\n internal = { loading: false, error: null, errorCode: null, count: 0 };\n asyncStates.set(key, internal);\n }\n\n internal.count++;\n internal.loading = true;\n internal.error = null;\n internal.errorCode = null;\n asyncSnapshots.set(key, LOADING_TASK_STATE);\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n activeOps.set(key, (activeOps.get(key) ?? 0) + 1);\n }\n\n // Shared bookkeeping: decrement count, snapshot state, update DEV active ops\n const finalizeOp = (errorMsg?: string | null, errorCode?: TaskState['errorCode']) => {\n internal!.count--;\n internal!.loading = internal!.count > 0;\n if (errorMsg !== undefined) {\n internal!.error = errorMsg;\n internal!.errorCode = errorCode ?? null;\n }\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n };\n\n return (result as Promise<unknown>).then(\n (value) => {\n if (!isDisposed()) finalizeOp();\n return value;\n },\n (error) => {\n // AbortError — silently swallow\n if (isAbortError(error)) {\n if (!isDisposed()) finalizeOp();\n return undefined;\n }\n\n // Disposed — fizzle silently\n if (isDisposed()) return undefined;\n\n const classified = classifyError(error);\n finalizeOp(classified.message, classified.code);\n\n // Re-throw to preserve standard Promise rejection\n throw error;\n },\n );\n };\n\n wrappedKeys.push(key);\n (instance as any)[key] = wrapper;\n }\n\n // Register cleanup for disposal\n if (wrappedKeys.length > 0) {\n addCleanup(() => {\n // Snapshot active ops for ghost check before clearing\n const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;\n\n // Swap all wrapped methods to no-ops (with DEV warning)\n for (const k of wrappedKeys) {\n if (__DEV__) {\n (instance as any)[k] = () => {\n console.warn(`[mvc-kit] \"${k}\" called after dispose — ignored.`);\n return undefined;\n };\n } else {\n (instance as any)[k] = () => undefined;\n }\n }\n\n // Clear async state\n asyncListeners.clear();\n asyncStates.clear();\n asyncSnapshots.clear();\n\n // DEV: schedule ghost check\n if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {\n setTimeout(() => {\n for (const [key, count] of opsSnapshot) {\n console.warn(\n `[mvc-kit] Ghost async operation detected: \"${key}\" had ${count} ` +\n `pending call(s) when the ${className} was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, ghostTimeout);\n }\n });\n }\n\n return wrappedKeys;\n}\n"],"names":["walkPrototypeChain","isAbortError","classifyError"],"mappings":"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"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { TaskState } from './types';
|
|
2
|
+
/** @internal Mutable internal async tracking state per method. */
|
|
2
3
|
export interface InternalTaskState {
|
|
3
4
|
loading: boolean;
|
|
4
5
|
error: string | null;
|
|
5
6
|
errorCode: TaskState['errorCode'];
|
|
6
7
|
count: number;
|
|
7
8
|
}
|
|
9
|
+
/** @internal Configuration for the shared async method wrapping logic. */
|
|
8
10
|
export interface AsyncTrackingContext {
|
|
9
11
|
instance: object;
|
|
10
12
|
stopPrototype: object;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapAsyncMethods.d.ts","sourceRoot":"","sources":["../src/wrapAsyncMethods.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAMzC,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC5C,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACvC,cAAc,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACtC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CAChD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,oBAAoB,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"wrapAsyncMethods.d.ts","sourceRoot":"","sources":["../src/wrapAsyncMethods.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAMzC,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC5C,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACvC,cAAc,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACtC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CAChD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAkMpE"}
|
package/dist/wrapAsyncMethods.js
CHANGED
|
@@ -86,57 +86,37 @@ function wrapAsyncMethods(ctx) {
|
|
|
86
86
|
if (__DEV__ && activeOps) {
|
|
87
87
|
activeOps.set(key, (activeOps.get(key) ?? 0) + 1);
|
|
88
88
|
}
|
|
89
|
+
const finalizeOp = (errorMsg, errorCode) => {
|
|
90
|
+
internal.count--;
|
|
91
|
+
internal.loading = internal.count > 0;
|
|
92
|
+
if (errorMsg !== void 0) {
|
|
93
|
+
internal.error = errorMsg;
|
|
94
|
+
internal.errorCode = errorCode ?? null;
|
|
95
|
+
}
|
|
96
|
+
asyncSnapshots.set(
|
|
97
|
+
key,
|
|
98
|
+
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
99
|
+
);
|
|
100
|
+
notifyAsync();
|
|
101
|
+
if (__DEV__ && activeOps) {
|
|
102
|
+
const c = (activeOps.get(key) ?? 1) - 1;
|
|
103
|
+
if (c <= 0) activeOps.delete(key);
|
|
104
|
+
else activeOps.set(key, c);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
89
107
|
return result.then(
|
|
90
108
|
(value) => {
|
|
91
|
-
if (isDisposed())
|
|
92
|
-
internal.count--;
|
|
93
|
-
internal.loading = internal.count > 0;
|
|
94
|
-
asyncSnapshots.set(
|
|
95
|
-
key,
|
|
96
|
-
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
97
|
-
);
|
|
98
|
-
notifyAsync();
|
|
99
|
-
if (__DEV__ && activeOps) {
|
|
100
|
-
const c = (activeOps.get(key) ?? 1) - 1;
|
|
101
|
-
if (c <= 0) activeOps.delete(key);
|
|
102
|
-
else activeOps.set(key, c);
|
|
103
|
-
}
|
|
109
|
+
if (!isDisposed()) finalizeOp();
|
|
104
110
|
return value;
|
|
105
111
|
},
|
|
106
112
|
(error) => {
|
|
107
113
|
if (isAbortError(error)) {
|
|
108
|
-
if (!isDisposed())
|
|
109
|
-
internal.count--;
|
|
110
|
-
internal.loading = internal.count > 0;
|
|
111
|
-
asyncSnapshots.set(
|
|
112
|
-
key,
|
|
113
|
-
Object.freeze({ loading: internal.loading, error: internal.error, errorCode: internal.errorCode })
|
|
114
|
-
);
|
|
115
|
-
notifyAsync();
|
|
116
|
-
}
|
|
117
|
-
if (__DEV__ && activeOps) {
|
|
118
|
-
const c = (activeOps.get(key) ?? 1) - 1;
|
|
119
|
-
if (c <= 0) activeOps.delete(key);
|
|
120
|
-
else activeOps.set(key, c);
|
|
121
|
-
}
|
|
114
|
+
if (!isDisposed()) finalizeOp();
|
|
122
115
|
return void 0;
|
|
123
116
|
}
|
|
124
117
|
if (isDisposed()) return void 0;
|
|
125
|
-
internal.count--;
|
|
126
|
-
internal.loading = internal.count > 0;
|
|
127
118
|
const classified = classifyError(error);
|
|
128
|
-
|
|
129
|
-
internal.errorCode = classified.code;
|
|
130
|
-
asyncSnapshots.set(
|
|
131
|
-
key,
|
|
132
|
-
Object.freeze({ loading: internal.loading, error: classified.message, errorCode: classified.code })
|
|
133
|
-
);
|
|
134
|
-
notifyAsync();
|
|
135
|
-
if (__DEV__ && activeOps) {
|
|
136
|
-
const c = (activeOps.get(key) ?? 1) - 1;
|
|
137
|
-
if (c <= 0) activeOps.delete(key);
|
|
138
|
-
else activeOps.set(key, c);
|
|
139
|
-
}
|
|
119
|
+
finalizeOp(classified.message, classified.code);
|
|
140
120
|
throw error;
|
|
141
121
|
}
|
|
142
122
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapAsyncMethods.js","sources":["../src/wrapAsyncMethods.ts"],"sourcesContent":["import { isAbortError, classifyError } from './errors';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport type { TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nconst LOADING_TASK_STATE: TaskState = Object.freeze({ loading: true, error: null, errorCode: null });\n\nexport interface InternalTaskState {\n loading: boolean;\n error: string | null;\n errorCode: TaskState['errorCode'];\n count: number;\n}\n\nexport interface AsyncTrackingContext {\n instance: object;\n stopPrototype: object;\n reservedKeys: readonly string[];\n lifecycleHooks: Set<string>;\n isDisposed: () => boolean;\n isInitialized: () => boolean;\n asyncStates: Map<string, InternalTaskState>;\n asyncSnapshots: Map<string, TaskState>;\n asyncListeners: Set<() => void>;\n notifyAsync: () => void;\n addCleanup: (fn: () => void) => void;\n ghostTimeout: number;\n className: string;\n activeOps: Map<string, number> | null;\n /** Pre-scanned methods from class metadata cache. When provided, skips prototype walk. */\n methods?: Array<{ key: string; fn: Function }>;\n}\n\n/**\n * Shared async method wrapping logic used by both ViewModel and Resource.\n * Walks the prototype chain, wraps methods with async tracking, and registers cleanup.\n * Returns the list of wrapped method keys.\n */\nexport function wrapAsyncMethods(ctx: AsyncTrackingContext): string[] {\n const {\n instance,\n stopPrototype,\n reservedKeys,\n lifecycleHooks,\n isDisposed,\n isInitialized,\n asyncStates,\n asyncSnapshots,\n asyncListeners,\n notifyAsync,\n addCleanup,\n ghostTimeout,\n className,\n activeOps,\n } = ctx;\n\n // Instance property reserved key check (DEV-only — prototype check in constructor catches most cases)\n if (__DEV__) {\n for (const key of reservedKeys) {\n if (Object.getOwnPropertyDescriptor(instance, key)?.value !== undefined) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ${className} and cannot be overridden.`\n );\n }\n }\n }\n\n // Use pre-scanned methods from class metadata cache, or walk prototype chain\n const methodEntries: Array<{ key: string; fn: Function }> = ctx.methods ?? (() => {\n const result: Array<{ key: string; fn: Function }> = [];\n const processed = new Set<string>();\n walkPrototypeChain(instance, stopPrototype, (key, desc) => {\n if (desc.get || desc.set) return;\n if (typeof desc.value !== 'function') return;\n if (key.startsWith('_')) return;\n if (lifecycleHooks.has(key)) return;\n if (processed.has(key)) return;\n processed.add(key);\n result.push({ key, fn: desc.value });\n });\n return result;\n })();\n\n const wrappedKeys: string[] = [];\n\n for (const { key, fn: original } of methodEntries) {\n let pruned = false;\n\n const wrapper = function (this: any, ...args: unknown[]) {\n // Disposed guard\n if (isDisposed()) {\n if (__DEV__) {\n console.warn(`[mvc-kit] \"${key}\" called after dispose — ignored.`);\n }\n return undefined;\n }\n\n // Pre-init guard (DEV only — method still executes)\n if (__DEV__ && !isInitialized()) {\n console.warn(\n `[mvc-kit] \"${key}\" called before init(). ` +\n `Async tracking is active only after init().`\n );\n }\n\n let result: unknown;\n try {\n result = original.apply(instance, args);\n } catch (e) {\n // Sync throw — not tracked as async\n throw e;\n }\n\n // Sync detection: if not thenable, prune from async tracking\n if (!result || typeof (result as any).then !== 'function') {\n if (!pruned) {\n pruned = true;\n // Remove from async maps\n asyncStates.delete(key);\n asyncSnapshots.delete(key);\n // Replace wrapper with bound original for zero overhead\n (instance as any)[key] = original.bind(instance);\n }\n return result;\n }\n\n // ── Async tracking ──────────────────────────────────────\n let internal = asyncStates.get(key);\n if (!internal) {\n internal = { loading: false, error: null, errorCode: null, count: 0 };\n asyncStates.set(key, internal);\n }\n\n internal.count++;\n internal.loading = true;\n internal.error = null;\n internal.errorCode = null;\n asyncSnapshots.set(key, LOADING_TASK_STATE);\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n activeOps.set(key, (activeOps.get(key) ?? 0) + 1);\n }\n\n return (result as Promise<unknown>).then(\n (value) => {\n if (isDisposed()) return value;\n\n internal!.count--;\n internal!.loading = internal!.count > 0;\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n\n return value;\n },\n (error) => {\n // AbortError — silently swallow\n if (isAbortError(error)) {\n if (!isDisposed()) {\n internal!.count--;\n internal!.loading = internal!.count > 0;\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n }\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n\n return undefined;\n }\n\n // Disposed — fizzle silently\n if (isDisposed()) return undefined;\n\n internal!.count--;\n internal!.loading = internal!.count > 0;\n const classified = classifyError(error);\n internal!.error = classified.message;\n internal!.errorCode = classified.code;\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: classified.message, errorCode: classified.code }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n\n // Re-throw to preserve standard Promise rejection\n throw error;\n },\n );\n };\n\n wrappedKeys.push(key);\n (instance as any)[key] = wrapper;\n }\n\n // Register cleanup for disposal\n if (wrappedKeys.length > 0) {\n addCleanup(() => {\n // Snapshot active ops for ghost check before clearing\n const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;\n\n // Swap all wrapped methods to no-ops (with DEV warning)\n for (const k of wrappedKeys) {\n if (__DEV__) {\n (instance as any)[k] = () => {\n console.warn(`[mvc-kit] \"${k}\" called after dispose — ignored.`);\n return undefined;\n };\n } else {\n (instance as any)[k] = () => undefined;\n }\n }\n\n // Clear async state\n asyncListeners.clear();\n asyncStates.clear();\n asyncSnapshots.clear();\n\n // DEV: schedule ghost check\n if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {\n setTimeout(() => {\n for (const [key, count] of opsSnapshot) {\n console.warn(\n `[mvc-kit] Ghost async operation detected: \"${key}\" had ${count} ` +\n `pending call(s) when the ${className} was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, ghostTimeout);\n }\n });\n }\n\n return wrappedKeys;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"wrapAsyncMethods.js","sources":["../src/wrapAsyncMethods.ts"],"sourcesContent":["import { isAbortError, classifyError } from './errors';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport type { TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nconst LOADING_TASK_STATE: TaskState = Object.freeze({ loading: true, error: null, errorCode: null });\n\n/** @internal Mutable internal async tracking state per method. */\nexport interface InternalTaskState {\n loading: boolean;\n error: string | null;\n errorCode: TaskState['errorCode'];\n count: number;\n}\n\n/** @internal Configuration for the shared async method wrapping logic. */\nexport interface AsyncTrackingContext {\n instance: object;\n stopPrototype: object;\n reservedKeys: readonly string[];\n lifecycleHooks: Set<string>;\n isDisposed: () => boolean;\n isInitialized: () => boolean;\n asyncStates: Map<string, InternalTaskState>;\n asyncSnapshots: Map<string, TaskState>;\n asyncListeners: Set<() => void>;\n notifyAsync: () => void;\n addCleanup: (fn: () => void) => void;\n ghostTimeout: number;\n className: string;\n activeOps: Map<string, number> | null;\n /** Pre-scanned methods from class metadata cache. When provided, skips prototype walk. */\n methods?: Array<{ key: string; fn: Function }>;\n}\n\n/**\n * Shared async method wrapping logic used by both ViewModel and Resource.\n * Walks the prototype chain, wraps methods with async tracking, and registers cleanup.\n * Returns the list of wrapped method keys.\n */\nexport function wrapAsyncMethods(ctx: AsyncTrackingContext): string[] {\n const {\n instance,\n stopPrototype,\n reservedKeys,\n lifecycleHooks,\n isDisposed,\n isInitialized,\n asyncStates,\n asyncSnapshots,\n asyncListeners,\n notifyAsync,\n addCleanup,\n ghostTimeout,\n className,\n activeOps,\n } = ctx;\n\n // Instance property reserved key check (DEV-only — prototype check in constructor catches most cases)\n if (__DEV__) {\n for (const key of reservedKeys) {\n if (Object.getOwnPropertyDescriptor(instance, key)?.value !== undefined) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on ${className} and cannot be overridden.`\n );\n }\n }\n }\n\n // Use pre-scanned methods from class metadata cache, or walk prototype chain\n const methodEntries: Array<{ key: string; fn: Function }> = ctx.methods ?? (() => {\n const result: Array<{ key: string; fn: Function }> = [];\n const processed = new Set<string>();\n walkPrototypeChain(instance, stopPrototype, (key, desc) => {\n if (desc.get || desc.set) return;\n if (typeof desc.value !== 'function') return;\n if (key.startsWith('_')) return;\n if (lifecycleHooks.has(key)) return;\n if (processed.has(key)) return;\n processed.add(key);\n result.push({ key, fn: desc.value });\n });\n return result;\n })();\n\n const wrappedKeys: string[] = [];\n\n for (const { key, fn: original } of methodEntries) {\n let pruned = false;\n\n const wrapper = function (this: any, ...args: unknown[]) {\n // Disposed guard\n if (isDisposed()) {\n if (__DEV__) {\n console.warn(`[mvc-kit] \"${key}\" called after dispose — ignored.`);\n }\n return undefined;\n }\n\n // Pre-init guard (DEV only — method still executes)\n if (__DEV__ && !isInitialized()) {\n console.warn(\n `[mvc-kit] \"${key}\" called before init(). ` +\n `Async tracking is active only after init().`\n );\n }\n\n let result: unknown;\n try {\n result = original.apply(instance, args);\n } catch (e) {\n // Sync throw — not tracked as async\n throw e;\n }\n\n // Sync detection: if not thenable, prune from async tracking\n if (!result || typeof (result as any).then !== 'function') {\n if (!pruned) {\n pruned = true;\n // Remove from async maps\n asyncStates.delete(key);\n asyncSnapshots.delete(key);\n // Replace wrapper with bound original for zero overhead\n (instance as any)[key] = original.bind(instance);\n }\n return result;\n }\n\n // ── Async tracking ──────────────────────────────────────\n let internal = asyncStates.get(key);\n if (!internal) {\n internal = { loading: false, error: null, errorCode: null, count: 0 };\n asyncStates.set(key, internal);\n }\n\n internal.count++;\n internal.loading = true;\n internal.error = null;\n internal.errorCode = null;\n asyncSnapshots.set(key, LOADING_TASK_STATE);\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n activeOps.set(key, (activeOps.get(key) ?? 0) + 1);\n }\n\n // Shared bookkeeping: decrement count, snapshot state, update DEV active ops\n const finalizeOp = (errorMsg?: string | null, errorCode?: TaskState['errorCode']) => {\n internal!.count--;\n internal!.loading = internal!.count > 0;\n if (errorMsg !== undefined) {\n internal!.error = errorMsg;\n internal!.errorCode = errorCode ?? null;\n }\n asyncSnapshots.set(\n key,\n Object.freeze({ loading: internal!.loading, error: internal!.error, errorCode: internal!.errorCode }),\n );\n notifyAsync();\n\n if (__DEV__ && activeOps) {\n const c = (activeOps.get(key) ?? 1) - 1;\n if (c <= 0) activeOps.delete(key);\n else activeOps.set(key, c);\n }\n };\n\n return (result as Promise<unknown>).then(\n (value) => {\n if (!isDisposed()) finalizeOp();\n return value;\n },\n (error) => {\n // AbortError — silently swallow\n if (isAbortError(error)) {\n if (!isDisposed()) finalizeOp();\n return undefined;\n }\n\n // Disposed — fizzle silently\n if (isDisposed()) return undefined;\n\n const classified = classifyError(error);\n finalizeOp(classified.message, classified.code);\n\n // Re-throw to preserve standard Promise rejection\n throw error;\n },\n );\n };\n\n wrappedKeys.push(key);\n (instance as any)[key] = wrapper;\n }\n\n // Register cleanup for disposal\n if (wrappedKeys.length > 0) {\n addCleanup(() => {\n // Snapshot active ops for ghost check before clearing\n const opsSnapshot = __DEV__ && activeOps ? new Map(activeOps) : null;\n\n // Swap all wrapped methods to no-ops (with DEV warning)\n for (const k of wrappedKeys) {\n if (__DEV__) {\n (instance as any)[k] = () => {\n console.warn(`[mvc-kit] \"${k}\" called after dispose — ignored.`);\n return undefined;\n };\n } else {\n (instance as any)[k] = () => undefined;\n }\n }\n\n // Clear async state\n asyncListeners.clear();\n asyncStates.clear();\n asyncSnapshots.clear();\n\n // DEV: schedule ghost check\n if (__DEV__ && opsSnapshot && opsSnapshot.size > 0) {\n setTimeout(() => {\n for (const [key, count] of opsSnapshot) {\n console.warn(\n `[mvc-kit] Ghost async operation detected: \"${key}\" had ${count} ` +\n `pending call(s) when the ${className} was disposed. ` +\n `Consider using disposeSignal to cancel in-flight work.`\n );\n }\n }, ghostTimeout);\n }\n });\n }\n\n return wrappedKeys;\n}\n"],"names":[],"mappings":"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"}
|