mvc-kit 2.2.2 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +5 -4
  2. package/agent-config/claude-code/agents/mvc-kit-architect.md +2 -3
  3. package/agent-config/claude-code/skills/guide/SKILL.md +1 -1
  4. package/agent-config/claude-code/skills/guide/anti-patterns.md +42 -3
  5. package/agent-config/claude-code/skills/guide/api-reference.md +4 -3
  6. package/agent-config/claude-code/skills/guide/patterns.md +18 -13
  7. package/agent-config/claude-code/skills/review/checklist.md +1 -1
  8. package/agent-config/claude-code/skills/scaffold/SKILL.md +1 -1
  9. package/agent-config/claude-code/skills/scaffold/templates/collection.md +7 -14
  10. package/agent-config/claude-code/skills/scaffold/templates/viewmodel.md +13 -42
  11. package/agent-config/copilot/copilot-instructions.md +14 -16
  12. package/agent-config/cursor/cursorrules +14 -16
  13. package/dist/Channel.d.ts +29 -0
  14. package/dist/Channel.d.ts.map +1 -1
  15. package/dist/Collection.d.ts +16 -1
  16. package/dist/Collection.d.ts.map +1 -1
  17. package/dist/Controller.d.ts +9 -0
  18. package/dist/Controller.d.ts.map +1 -1
  19. package/dist/EventBus.d.ts +5 -0
  20. package/dist/EventBus.d.ts.map +1 -1
  21. package/dist/Model.d.ts +16 -0
  22. package/dist/Model.d.ts.map +1 -1
  23. package/dist/Service.d.ts +8 -0
  24. package/dist/Service.d.ts.map +1 -1
  25. package/dist/ViewModel.d.ts +35 -1
  26. package/dist/ViewModel.d.ts.map +1 -1
  27. package/dist/mvc-kit.cjs +1 -1
  28. package/dist/mvc-kit.cjs.map +1 -1
  29. package/dist/mvc-kit.js +226 -111
  30. package/dist/mvc-kit.js.map +1 -1
  31. package/dist/react/provider.d.ts +1 -0
  32. package/dist/react/provider.d.ts.map +1 -1
  33. package/dist/react/use-model.d.ts +2 -0
  34. package/dist/react/use-model.d.ts.map +1 -1
  35. package/dist/react.cjs.map +1 -1
  36. package/dist/react.js +1 -1
  37. package/dist/react.js.map +1 -1
  38. package/dist/{singleton-C8_FRbA7.js → singleton-CaEXSbYg.js} +5 -1
  39. package/dist/singleton-CaEXSbYg.js.map +1 -0
  40. package/dist/singleton-L-u2W_lX.cjs.map +1 -1
  41. package/dist/singleton.d.ts +10 -0
  42. package/dist/singleton.d.ts.map +1 -1
  43. package/mvc-kit-logo.jpg +0 -0
  44. package/package.json +2 -1
  45. package/dist/singleton-C8_FRbA7.js.map +0 -1
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sources":["../src/react/use-instance.ts","../src/react/guards.ts","../src/react/use-local.ts","../src/react/use-singleton.ts","../src/react/use-model.ts","../src/react/use-event-bus.ts","../src/react/use-teardown.ts","../src/react/provider.tsx"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\nimport type { Subscribable } from '../types';\n\nfunction hasAsyncSubscription(obj: unknown): obj is { subscribeAsync(cb: () => void): () => void } {\n return (\n obj !== null &&\n typeof obj === 'object' &&\n typeof (obj as any).subscribeAsync === 'function'\n );\n}\n\n/**\n * Subscribe to an existing Subscribable instance.\n * No ownership - caller manages the instance lifecycle.\n *\n * If the instance has a `subscribeAsync` method (duck-typed),\n * a second subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): Readonly<S> {\n const state = useSyncExternalStore(\n (onStoreChange) => subscribable.subscribe(onStoreChange),\n () => subscribable.state,\n () => subscribable.state // SSR snapshot\n );\n\n // Async subscription — forces re-render when any async status changes.\n // Duck-typed: safe for Collection/Model (they don't have subscribeAsync).\n const versionRef = useRef(0);\n useSyncExternalStore(\n (onStoreChange) => {\n if (!hasAsyncSubscription(subscribable)) return () => {};\n return subscribable.subscribeAsync(() => {\n versionRef.current++;\n onStoreChange();\n });\n },\n () => versionRef.current,\n () => 0 // SSR: no async ops server-side\n );\n\n return state;\n}\n","import type { Subscribable } from '../types';\n\n/** @internal Type guard for Subscribable */\nexport const isSubscribable = (obj: unknown): obj is Subscribable<unknown> =>\n obj !== null &&\n typeof obj === 'object' &&\n 'state' in obj &&\n 'subscribe' in obj &&\n typeof (obj as Subscribable<unknown>).subscribe === 'function';\n\n/** @internal Type guard for Initializable */\nexport const isInitializable = (obj: unknown): obj is { init(): void | Promise<void> } =>\n obj !== null &&\n typeof obj === 'object' &&\n 'init' in obj &&\n typeof (obj as any).init === 'function';\n","import { useRef, useEffect } from 'react';\nimport type { DependencyList } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { isSubscribable, isInitializable } from './guards';\nimport { useInstance } from './use-instance';\nimport type { StateOf } from './types';\n\nfunction depsChanged(prev: DependencyList | undefined, next: DependencyList): boolean {\n if (prev === undefined) return false;\n if (prev.length !== next.length) return true;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return true;\n }\n return false;\n}\n\n// ── With deps (class + initialState + deps) ────────────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<any> & Disposable>(\n Class: new (initialState: StateOf<T>) => T,\n initialState: StateOf<T>,\n deps: DependencyList,\n): [Readonly<StateOf<T>>, T];\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T,\n deps: DependencyList,\n): [Readonly<S>, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(\n factory: () => T,\n deps: DependencyList,\n): T;\n\n// ── Without deps (existing overloads, unchanged) ───────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [Readonly<S>, T];\n\n/**\n * Create component-scoped Disposable instance (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T\n): [Readonly<S>, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(factory: () => T): T;\n\n// ── Implementation ─────────────────────────────────────────────────\n\nexport function useLocal<T extends Disposable, S = StateOf<T>>(\n classOrFactory: (new (...args: unknown[]) => T) | (() => T),\n ...rest: unknown[]\n): [Readonly<S>, T] | T {\n // ── Detect deps: last arg is an array → treat as deps ──\n let args: unknown[];\n let deps: DependencyList | undefined;\n\n if (rest.length > 0 && Array.isArray(rest[rest.length - 1])) {\n deps = rest[rest.length - 1] as DependencyList;\n args = rest.slice(0, -1);\n } else {\n args = rest;\n deps = undefined;\n }\n\n const instanceRef = useRef<T | null>(null);\n const mountedRef = useRef(false);\n const prevDepsRef = useRef<DependencyList | undefined>(undefined);\n\n // ── Render phase: dep-change detection ──\n if (deps !== undefined && depsChanged(prevDepsRef.current, deps)) {\n instanceRef.current?.dispose();\n instanceRef.current = null;\n }\n if (deps !== undefined) {\n prevDepsRef.current = deps;\n }\n\n // ── Create instance if needed ──\n if (!instanceRef.current || instanceRef.current.disposed) {\n const isClass =\n typeof classOrFactory === 'function' &&\n classOrFactory.prototype &&\n classOrFactory.prototype.constructor === classOrFactory;\n\n if (isClass) {\n instanceRef.current = new (classOrFactory as new (...a: unknown[]) => T)(...args);\n } else {\n instanceRef.current = (classOrFactory as () => T)();\n }\n }\n\n // ── Effect: init + deferred cleanup ──\n useEffect(() => {\n const instance = instanceRef.current!; // capture for cleanup closure\n mountedRef.current = true;\n if (isInitializable(instance)) {\n instance.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n instance.dispose();\n }\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps ?? []);\n\n // ── Subscribe to state if Subscribable ──\n if (isSubscribable(instanceRef.current)) {\n const state = useInstance(instanceRef.current as unknown as Subscribable<S>);\n return [state, instanceRef.current];\n }\n\n return instanceRef.current;\n}\n","import { useEffect } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { singleton } from '../singleton';\nimport { useInstance } from './use-instance';\nimport { isSubscribable, isInitializable } from './guards';\nimport type { StateOf } from './types';\n\n/**\n * Get singleton Subscribable instance and subscribe to its state.\n * Returns [state, instance] tuple.\n */\nexport function useSingleton<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [Readonly<S>, T];\n\n/**\n * Get singleton Disposable instance (non-Subscribable).\n * Returns the instance directly.\n */\nexport function useSingleton<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n// Implementation\nexport function useSingleton<T extends Disposable, S = StateOf<T>, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [Readonly<S>, T] | T {\n const instance = singleton(Class, ...args);\n\n useEffect(() => {\n if (isInitializable(instance)) {\n instance.init();\n }\n }, [instance]);\n\n if (isSubscribable(instance)) {\n const state = useInstance(instance) as Readonly<S>;\n return [state, instance];\n }\n\n return instance;\n}\n","import { useRef, useEffect, useSyncExternalStore, useCallback } from 'react';\nimport type { Model } from '../Model';\nimport type { ValidationErrors } from '../types';\nimport type { StateOf } from './types';\nimport { isInitializable } from './guards';\n\nexport interface ModelHandle<S extends object, M extends Model<S>> {\n state: Readonly<S>;\n errors: ValidationErrors<S>;\n valid: boolean;\n dirty: boolean;\n model: M;\n}\n\n/**\n * Bind to a component-scoped Model with validation and dirty state exposed.\n */\nexport function useModel<M extends Model<any>>(\n factory: () => M\n): ModelHandle<StateOf<M>, M> {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n if (!modelRef.current || modelRef.current.disposed) {\n modelRef.current = factory();\n }\n\n useSyncExternalStore(\n (onStoreChange) => modelRef.current!.subscribe(onStoreChange),\n () => modelRef.current!.state,\n () => modelRef.current!.state,\n );\n\n useEffect(() => {\n mountedRef.current = true;\n if (isInitializable(modelRef.current)) {\n modelRef.current.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n const model = modelRef.current;\n\n return {\n state: model.state,\n errors: model.errors,\n valid: model.valid,\n dirty: model.dirty,\n model,\n };\n}\n\nexport interface FieldHandle<V> {\n value: V;\n error: string | undefined;\n set: (value: V) => void;\n}\n\n/**\n * Bind to a single Model field with surgical re-renders.\n */\nexport function useField<S extends object, K extends keyof S>(\n model: Model<S>,\n field: K\n): FieldHandle<S[K]> {\n // Track the field value and error for comparison\n const getSnapshot = useCallback(() => {\n return {\n value: model.state[field],\n error: model.errors[field],\n };\n }, [model, field]);\n\n // Use object comparison for subscription\n const cachedRef = useRef(getSnapshot());\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n return model.subscribe(() => {\n const next = getSnapshot();\n const current = cachedRef.current;\n\n // Only trigger re-render if field value or error changed\n if (next.value !== current.value || next.error !== current.error) {\n cachedRef.current = next;\n onStoreChange();\n }\n });\n },\n [model, getSnapshot]\n );\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => cachedRef.current,\n () => cachedRef.current\n );\n\n const set = useCallback(\n (value: S[K]) => {\n // Access the protected set method through type assertion\n // The Model subclass should expose a setter method\n const partial: Partial<S> = { [field]: value } as unknown as Partial<S>;\n (model as unknown as { set: (partial: Partial<S>) => void }).set(partial);\n },\n [model, field]\n );\n\n return {\n value: snapshot.value,\n error: snapshot.error,\n set,\n };\n}\n","import { useEffect, useCallback, useRef } from 'react';\nimport { EventBus } from '../EventBus';\n\n/**\n * Subscribe to a typed event, auto-unsubscribes on unmount.\n * Accepts an EventBus directly or any object with an `events` property (e.g. a ViewModel).\n */\nexport function useEvent<E extends Record<string, any>, K extends keyof E>(\n source: EventBus<E> | { events: EventBus<E> },\n event: K,\n handler: (payload: E[K]) => void\n): void {\n const bus = source instanceof EventBus ? source : source.events;\n\n // Use ref to keep handler stable across re-renders\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n\n useEffect(() => {\n const unsubscribe = bus.on(event, (payload) => {\n handlerRef.current(payload);\n });\n\n return unsubscribe;\n }, [bus, event]);\n}\n\n/**\n * Get a stable emit function for an EventBus.\n */\nexport function useEmit<E extends Record<string, any>>(\n bus: EventBus<E>\n): <K extends keyof E>(event: K, payload: E[K]) => void {\n return useCallback(\n <K extends keyof E>(event: K, payload: E[K]) => {\n bus.emit(event, payload);\n },\n [bus]\n );\n}\n","import { useEffect, useRef } from 'react';\nimport type { Disposable } from '../types';\nimport { teardown } from '../singleton';\n\n/**\n * Teardown singleton class(es) on unmount.\n * Uses deferred disposal to handle StrictMode's double-mount cycle.\n */\nexport function useTeardown(\n ...Classes: Array<new (...args: unknown[]) => Disposable>\n): void {\n const mountedRef = useRef(false);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n for (const Class of Classes) {\n teardown(Class);\n }\n }\n }, 0);\n };\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n}\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { singleton } from '../singleton';\nimport type { Disposable } from '../types';\nimport type { ProviderRegistry } from './types';\n\nconst ProviderContext = createContext<ProviderRegistry | null>(null);\n\nexport interface ProviderProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n provide: Array<[new (...args: any[]) => any, any]>;\n children: ReactNode;\n}\n\n/**\n * DI container for testing and Storybook.\n */\nexport function Provider({ provide, children }: ProviderProps): ReactNode {\n const registry = useMemo(() => {\n const map: ProviderRegistry = new Map();\n for (const [Class, instance] of provide) {\n map.set(Class, instance);\n }\n return map;\n }, [provide]);\n\n return (\n <ProviderContext.Provider value={registry}>\n {children}\n </ProviderContext.Provider>\n );\n}\n\n/**\n * Resolve from Provider context or fallback to singleton().\n */\nexport function useResolve<T, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T {\n const registry = useContext(ProviderContext);\n\n if (registry?.has(Class)) {\n return registry.get(Class) as T;\n }\n return singleton(Class as new (...args: Args) => T & Disposable, ...args);\n}\n"],"names":["hasAsyncSubscription","obj","useInstance","subscribable","state","useSyncExternalStore","onStoreChange","versionRef","useRef","isSubscribable","isInitializable","depsChanged","prev","next","i","useLocal","classOrFactory","rest","args","deps","instanceRef","mountedRef","prevDepsRef","useEffect","instance","useSingleton","Class","singleton","useModel","factory","modelRef","model","useField","field","getSnapshot","useCallback","cachedRef","subscribe","current","snapshot","set","value","partial","useEvent","source","event","handler","bus","EventBus","handlerRef","payload","useEmit","useTeardown","Classes","teardown","ProviderContext","createContext","Provider","provide","children","registry","useMemo","map","useResolve","useContext"],"mappings":"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"}
1
+ {"version":3,"file":"react.js","sources":["../src/react/use-instance.ts","../src/react/guards.ts","../src/react/use-local.ts","../src/react/use-singleton.ts","../src/react/use-model.ts","../src/react/use-event-bus.ts","../src/react/use-teardown.ts","../src/react/provider.tsx"],"sourcesContent":["import { useSyncExternalStore, useRef } from 'react';\nimport type { Subscribable } from '../types';\n\nfunction hasAsyncSubscription(obj: unknown): obj is { subscribeAsync(cb: () => void): () => void } {\n return (\n obj !== null &&\n typeof obj === 'object' &&\n typeof (obj as any).subscribeAsync === 'function'\n );\n}\n\n/**\n * Subscribe to an existing Subscribable instance.\n * No ownership - caller manages the instance lifecycle.\n *\n * If the instance has a `subscribeAsync` method (duck-typed),\n * a second subscription ensures async state changes also\n * trigger React re-renders.\n */\nexport function useInstance<S>(subscribable: Subscribable<S>): Readonly<S> {\n const state = useSyncExternalStore(\n (onStoreChange) => subscribable.subscribe(onStoreChange),\n () => subscribable.state,\n () => subscribable.state // SSR snapshot\n );\n\n // Async subscription — forces re-render when any async status changes.\n // Duck-typed: safe for Collection/Model (they don't have subscribeAsync).\n const versionRef = useRef(0);\n useSyncExternalStore(\n (onStoreChange) => {\n if (!hasAsyncSubscription(subscribable)) return () => {};\n return subscribable.subscribeAsync(() => {\n versionRef.current++;\n onStoreChange();\n });\n },\n () => versionRef.current,\n () => 0 // SSR: no async ops server-side\n );\n\n return state;\n}\n","import type { Subscribable } from '../types';\n\n/** @internal Type guard for Subscribable */\nexport const isSubscribable = (obj: unknown): obj is Subscribable<unknown> =>\n obj !== null &&\n typeof obj === 'object' &&\n 'state' in obj &&\n 'subscribe' in obj &&\n typeof (obj as Subscribable<unknown>).subscribe === 'function';\n\n/** @internal Type guard for Initializable */\nexport const isInitializable = (obj: unknown): obj is { init(): void | Promise<void> } =>\n obj !== null &&\n typeof obj === 'object' &&\n 'init' in obj &&\n typeof (obj as any).init === 'function';\n","import { useRef, useEffect } from 'react';\nimport type { DependencyList } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { isSubscribable, isInitializable } from './guards';\nimport { useInstance } from './use-instance';\nimport type { StateOf } from './types';\n\nfunction depsChanged(prev: DependencyList | undefined, next: DependencyList): boolean {\n if (prev === undefined) return false;\n if (prev.length !== next.length) return true;\n for (let i = 0; i < prev.length; i++) {\n if (!Object.is(prev[i], next[i])) return true;\n }\n return false;\n}\n\n// ── With deps (class + initialState + deps) ────────────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<any> & Disposable>(\n Class: new (initialState: StateOf<T>) => T,\n initialState: StateOf<T>,\n deps: DependencyList,\n): [Readonly<StateOf<T>>, T];\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T,\n deps: DependencyList,\n): [Readonly<S>, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Disposes and recreates when deps change.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(\n factory: () => T,\n deps: DependencyList,\n): T;\n\n// ── Without deps (existing overloads, unchanged) ───────────────────\n\n/**\n * Create component-scoped Subscribable instance, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [Readonly<S>, T];\n\n/**\n * Create component-scoped Disposable instance (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n/**\n * Create component-scoped Subscribable instance via factory, auto-disposed on unmount.\n * Returns [state, instance] tuple.\n */\nexport function useLocal<T extends Subscribable<S> & Disposable, S = StateOf<T>>(\n factory: () => T\n): [Readonly<S>, T];\n\n/**\n * Create component-scoped Disposable instance via factory (non-Subscribable), auto-disposed on unmount.\n * Returns the instance directly.\n */\nexport function useLocal<T extends Disposable>(factory: () => T): T;\n\n// ── Implementation ─────────────────────────────────────────────────\n\nexport function useLocal<T extends Disposable, S = StateOf<T>>(\n classOrFactory: (new (...args: unknown[]) => T) | (() => T),\n ...rest: unknown[]\n): [Readonly<S>, T] | T {\n // ── Detect deps: last arg is an array → treat as deps ──\n let args: unknown[];\n let deps: DependencyList | undefined;\n\n if (rest.length > 0 && Array.isArray(rest[rest.length - 1])) {\n deps = rest[rest.length - 1] as DependencyList;\n args = rest.slice(0, -1);\n } else {\n args = rest;\n deps = undefined;\n }\n\n const instanceRef = useRef<T | null>(null);\n const mountedRef = useRef(false);\n const prevDepsRef = useRef<DependencyList | undefined>(undefined);\n\n // ── Render phase: dep-change detection ──\n if (deps !== undefined && depsChanged(prevDepsRef.current, deps)) {\n instanceRef.current?.dispose();\n instanceRef.current = null;\n }\n if (deps !== undefined) {\n prevDepsRef.current = deps;\n }\n\n // ── Create instance if needed ──\n if (!instanceRef.current || instanceRef.current.disposed) {\n const isClass =\n typeof classOrFactory === 'function' &&\n classOrFactory.prototype &&\n classOrFactory.prototype.constructor === classOrFactory;\n\n if (isClass) {\n instanceRef.current = new (classOrFactory as new (...a: unknown[]) => T)(...args);\n } else {\n instanceRef.current = (classOrFactory as () => T)();\n }\n }\n\n // ── Effect: init + deferred cleanup ──\n useEffect(() => {\n const instance = instanceRef.current!; // capture for cleanup closure\n mountedRef.current = true;\n if (isInitializable(instance)) {\n instance.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n instance.dispose();\n }\n }, 0);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps ?? []);\n\n // ── Subscribe to state if Subscribable ──\n if (isSubscribable(instanceRef.current)) {\n const state = useInstance(instanceRef.current as unknown as Subscribable<S>);\n return [state, instanceRef.current];\n }\n\n return instanceRef.current;\n}\n","import { useEffect } from 'react';\nimport type { Subscribable, Disposable } from '../types';\nimport { singleton } from '../singleton';\nimport { useInstance } from './use-instance';\nimport { isSubscribable, isInitializable } from './guards';\nimport type { StateOf } from './types';\n\n/**\n * Get singleton Subscribable instance and subscribe to its state.\n * Returns [state, instance] tuple.\n */\nexport function useSingleton<\n T extends Subscribable<S> & Disposable,\n S = StateOf<T>,\n Args extends unknown[] = unknown[]\n>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [Readonly<S>, T];\n\n/**\n * Get singleton Disposable instance (non-Subscribable).\n * Returns the instance directly.\n */\nexport function useSingleton<T extends Disposable, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T;\n\n// Implementation\nexport function useSingleton<T extends Disposable, S = StateOf<T>, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): [Readonly<S>, T] | T {\n const instance = singleton(Class, ...args);\n\n useEffect(() => {\n if (isInitializable(instance)) {\n instance.init();\n }\n }, [instance]);\n\n if (isSubscribable(instance)) {\n const state = useInstance(instance) as Readonly<S>;\n return [state, instance];\n }\n\n return instance;\n}\n","import { useRef, useEffect, useSyncExternalStore, useCallback } from 'react';\nimport type { Model } from '../Model';\nimport type { ValidationErrors } from '../types';\nimport type { StateOf } from './types';\nimport { isInitializable } from './guards';\n\n/** Return type of `useModel`, providing state, validation, and model access. */\nexport interface ModelHandle<S extends object, M extends Model<S>> {\n state: Readonly<S>;\n errors: ValidationErrors<S>;\n valid: boolean;\n dirty: boolean;\n model: M;\n}\n\n/**\n * Bind to a component-scoped Model with validation and dirty state exposed.\n */\nexport function useModel<M extends Model<any>>(\n factory: () => M\n): ModelHandle<StateOf<M>, M> {\n const modelRef = useRef<M | null>(null);\n const mountedRef = useRef(false);\n\n if (!modelRef.current || modelRef.current.disposed) {\n modelRef.current = factory();\n }\n\n useSyncExternalStore(\n (onStoreChange) => modelRef.current!.subscribe(onStoreChange),\n () => modelRef.current!.state,\n () => modelRef.current!.state,\n );\n\n useEffect(() => {\n mountedRef.current = true;\n if (isInitializable(modelRef.current)) {\n modelRef.current.init();\n }\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n modelRef.current?.dispose();\n }\n }, 0);\n };\n }, []);\n\n const model = modelRef.current;\n\n return {\n state: model.state,\n errors: model.errors,\n valid: model.valid,\n dirty: model.dirty,\n model,\n };\n}\n\n/** Return type of `useField`, providing a single field's value, error, and setter. */\nexport interface FieldHandle<V> {\n value: V;\n error: string | undefined;\n set: (value: V) => void;\n}\n\n/**\n * Bind to a single Model field with surgical re-renders.\n */\nexport function useField<S extends object, K extends keyof S>(\n model: Model<S>,\n field: K\n): FieldHandle<S[K]> {\n // Track the field value and error for comparison\n const getSnapshot = useCallback(() => {\n return {\n value: model.state[field],\n error: model.errors[field],\n };\n }, [model, field]);\n\n // Use object comparison for subscription\n const cachedRef = useRef(getSnapshot());\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n return model.subscribe(() => {\n const next = getSnapshot();\n const current = cachedRef.current;\n\n // Only trigger re-render if field value or error changed\n if (next.value !== current.value || next.error !== current.error) {\n cachedRef.current = next;\n onStoreChange();\n }\n });\n },\n [model, getSnapshot]\n );\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => cachedRef.current,\n () => cachedRef.current\n );\n\n const set = useCallback(\n (value: S[K]) => {\n // Access the protected set method through type assertion\n // The Model subclass should expose a setter method\n const partial: Partial<S> = { [field]: value } as unknown as Partial<S>;\n (model as unknown as { set: (partial: Partial<S>) => void }).set(partial);\n },\n [model, field]\n );\n\n return {\n value: snapshot.value,\n error: snapshot.error,\n set,\n };\n}\n","import { useEffect, useCallback, useRef } from 'react';\nimport { EventBus } from '../EventBus';\n\n/**\n * Subscribe to a typed event, auto-unsubscribes on unmount.\n * Accepts an EventBus directly or any object with an `events` property (e.g. a ViewModel).\n */\nexport function useEvent<E extends Record<string, any>, K extends keyof E>(\n source: EventBus<E> | { events: EventBus<E> },\n event: K,\n handler: (payload: E[K]) => void\n): void {\n const bus = source instanceof EventBus ? source : source.events;\n\n // Use ref to keep handler stable across re-renders\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n\n useEffect(() => {\n const unsubscribe = bus.on(event, (payload) => {\n handlerRef.current(payload);\n });\n\n return unsubscribe;\n }, [bus, event]);\n}\n\n/**\n * Get a stable emit function for an EventBus.\n */\nexport function useEmit<E extends Record<string, any>>(\n bus: EventBus<E>\n): <K extends keyof E>(event: K, payload: E[K]) => void {\n return useCallback(\n <K extends keyof E>(event: K, payload: E[K]) => {\n bus.emit(event, payload);\n },\n [bus]\n );\n}\n","import { useEffect, useRef } from 'react';\nimport type { Disposable } from '../types';\nimport { teardown } from '../singleton';\n\n/**\n * Teardown singleton class(es) on unmount.\n * Uses deferred disposal to handle StrictMode's double-mount cycle.\n */\nexport function useTeardown(\n ...Classes: Array<new (...args: unknown[]) => Disposable>\n): void {\n const mountedRef = useRef(false);\n\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n setTimeout(() => {\n if (!mountedRef.current) {\n for (const Class of Classes) {\n teardown(Class);\n }\n }\n }, 0);\n };\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n}\n","import { createContext, useContext, useMemo, type ReactNode } from 'react';\nimport { singleton } from '../singleton';\nimport type { Disposable } from '../types';\nimport type { ProviderRegistry } from './types';\n\nconst ProviderContext = createContext<ProviderRegistry | null>(null);\n\n/** Props for the `Provider` component used to inject test/Storybook dependencies. */\nexport interface ProviderProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n provide: Array<[new (...args: any[]) => any, any]>;\n children: ReactNode;\n}\n\n/**\n * DI container for testing and Storybook.\n */\nexport function Provider({ provide, children }: ProviderProps): ReactNode {\n const registry = useMemo(() => {\n const map: ProviderRegistry = new Map();\n for (const [Class, instance] of provide) {\n map.set(Class, instance);\n }\n return map;\n }, [provide]);\n\n return (\n <ProviderContext.Provider value={registry}>\n {children}\n </ProviderContext.Provider>\n );\n}\n\n/**\n * Resolve from Provider context or fallback to singleton().\n */\nexport function useResolve<T, Args extends unknown[] = unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T {\n const registry = useContext(ProviderContext);\n\n if (registry?.has(Class)) {\n return registry.get(Class) as T;\n }\n return singleton(Class as new (...args: Args) => T & Disposable, ...args);\n}\n"],"names":["hasAsyncSubscription","obj","useInstance","subscribable","state","useSyncExternalStore","onStoreChange","versionRef","useRef","isSubscribable","isInitializable","depsChanged","prev","next","i","useLocal","classOrFactory","rest","args","deps","instanceRef","mountedRef","prevDepsRef","useEffect","instance","useSingleton","Class","singleton","useModel","factory","modelRef","model","useField","field","getSnapshot","useCallback","cachedRef","subscribe","current","snapshot","set","value","partial","useEvent","source","event","handler","bus","EventBus","handlerRef","payload","useEmit","useTeardown","Classes","teardown","ProviderContext","createContext","Provider","provide","children","registry","useMemo","map","useResolve","useContext"],"mappings":";;;AAGA,SAASA,EAAqBC,GAAqE;AACjG,SACEA,MAAQ,QACR,OAAOA,KAAQ,YACf,OAAQA,EAAY,kBAAmB;AAE3C;AAUO,SAASC,EAAeC,GAA4C;AACzE,QAAMC,IAAQC;AAAA,IACZ,CAACC,MAAkBH,EAAa,UAAUG,CAAa;AAAA,IACvD,MAAMH,EAAa;AAAA,IACnB,MAAMA,EAAa;AAAA;AAAA,EAAA,GAKfI,IAAaC,EAAO,CAAC;AAC3B,SAAAH;AAAA,IACE,CAACC,MACMN,EAAqBG,CAAY,IAC/BA,EAAa,eAAe,MAAM;AACvC,MAAAI,EAAW,WACXD,EAAA;AAAA,IACF,CAAC,IAJ+C,MAAM;AAAA,IAAC;AAAA,IAMzD,MAAMC,EAAW;AAAA,IACjB,MAAM;AAAA;AAAA,EAAA,GAGDH;AACT;ACvCO,MAAMK,IAAiB,CAACR,MAC7BA,MAAQ,QACR,OAAOA,KAAQ,YACf,WAAWA,KACX,eAAeA,KACf,OAAQA,EAA8B,aAAc,YAGzCS,IAAkB,CAACT,MAC9BA,MAAQ,QACR,OAAOA,KAAQ,YACf,UAAUA,KACV,OAAQA,EAAY,QAAS;ACR/B,SAASU,EAAYC,GAAkCC,GAA+B;AACpF,MAAID,MAAS,OAAW,QAAO;AAC/B,MAAIA,EAAK,WAAWC,EAAK,OAAQ,QAAO;AACxC,WAASC,IAAI,GAAGA,IAAIF,EAAK,QAAQE;AAC/B,QAAI,CAAC,OAAO,GAAGF,EAAKE,CAAC,GAAGD,EAAKC,CAAC,CAAC,EAAG,QAAO;AAE3C,SAAO;AACT;AA2EO,SAASC,EACdC,MACGC,GACmB;AAEtB,MAAIC,GACAC;AAEJ,EAAIF,EAAK,SAAS,KAAK,MAAM,QAAQA,EAAKA,EAAK,SAAS,CAAC,CAAC,KACxDE,IAAOF,EAAKA,EAAK,SAAS,CAAC,GAC3BC,IAAOD,EAAK,MAAM,GAAG,EAAE,MAEvBC,IAAOD,GACPE,IAAO;AAGT,QAAMC,IAAcZ,EAAiB,IAAI,GACnCa,IAAab,EAAO,EAAK,GACzBc,IAAcd,EAAmC,MAAS;AA4ChE,SAzCIW,MAAS,UAAaR,EAAYW,EAAY,SAASH,CAAI,MAC7DC,EAAY,SAAS,QAAA,GACrBA,EAAY,UAAU,OAEpBD,MAAS,WACXG,EAAY,UAAUH,KAIpB,CAACC,EAAY,WAAWA,EAAY,QAAQ,cAE5C,OAAOJ,KAAmB,cAC1BA,EAAe,aACfA,EAAe,UAAU,gBAAgBA,IAGzCI,EAAY,UAAU,IAAKJ,EAA8C,GAAGE,CAAI,IAEhFE,EAAY,UAAWJ,EAAA,IAK3BO,EAAU,MAAM;AACd,UAAMC,IAAWJ,EAAY;AAC7B,WAAAC,EAAW,UAAU,IACjBX,EAAgBc,CAAQ,KAC1BA,EAAS,KAAA,GAEJ,MAAM;AACX,MAAAH,EAAW,UAAU,IACrB,WAAW,MAAM;AACf,QAAKA,EAAW,WACdG,EAAS,QAAA;AAAA,MAEb,GAAG,CAAC;AAAA,IACN;AAAA,EAEF,GAAGL,KAAQ,EAAE,GAGTV,EAAeW,EAAY,OAAO,IAE7B,CADOlB,EAAYkB,EAAY,OAAqC,GAC5DA,EAAY,OAAO,IAG7BA,EAAY;AACrB;AC/HO,SAASK,EACdC,MACGR,GACmB;AACtB,QAAMM,IAAWG,EAAUD,GAAO,GAAGR,CAAI;AAQzC,SANAK,EAAU,MAAM;AACd,IAAIb,EAAgBc,CAAQ,KAC1BA,EAAS,KAAA;AAAA,EAEb,GAAG,CAACA,CAAQ,CAAC,GAETf,EAAee,CAAQ,IAElB,CADOtB,EAAYsB,CAAQ,GACnBA,CAAQ,IAGlBA;AACT;AC9BO,SAASI,EACdC,GAC4B;AAC5B,QAAMC,IAAWtB,EAAiB,IAAI,GAChCa,IAAab,EAAO,EAAK;AAE/B,GAAI,CAACsB,EAAS,WAAWA,EAAS,QAAQ,cACxCA,EAAS,UAAUD,EAAA,IAGrBxB;AAAA,IACE,CAACC,MAAkBwB,EAAS,QAAS,UAAUxB,CAAa;AAAA,IAC5D,MAAMwB,EAAS,QAAS;AAAA,IACxB,MAAMA,EAAS,QAAS;AAAA,EAAA,GAG1BP,EAAU,OACRF,EAAW,UAAU,IACjBX,EAAgBoB,EAAS,OAAO,KAClCA,EAAS,QAAQ,KAAA,GAEZ,MAAM;AACX,IAAAT,EAAW,UAAU,IACrB,WAAW,MAAM;AACf,MAAKA,EAAW,WACdS,EAAS,SAAS,QAAA;AAAA,IAEtB,GAAG,CAAC;AAAA,EACN,IACC,CAAA,CAAE;AAEL,QAAMC,IAAQD,EAAS;AAEvB,SAAO;AAAA,IACL,OAAOC,EAAM;AAAA,IACb,QAAQA,EAAM;AAAA,IACd,OAAOA,EAAM;AAAA,IACb,OAAOA,EAAM;AAAA,IACb,OAAAA;AAAA,EAAA;AAEJ;AAYO,SAASC,EACdD,GACAE,GACmB;AAEnB,QAAMC,IAAcC,EAAY,OACvB;AAAA,IACL,OAAOJ,EAAM,MAAME,CAAK;AAAA,IACxB,OAAOF,EAAM,OAAOE,CAAK;AAAA,EAAA,IAE1B,CAACF,GAAOE,CAAK,CAAC,GAGXG,IAAY5B,EAAO0B,GAAa,GAEhCG,IAAYF;AAAA,IAChB,CAAC7B,MACQyB,EAAM,UAAU,MAAM;AAC3B,YAAMlB,IAAOqB,EAAA,GACPI,IAAUF,EAAU;AAG1B,OAAIvB,EAAK,UAAUyB,EAAQ,SAASzB,EAAK,UAAUyB,EAAQ,WACzDF,EAAU,UAAUvB,GACpBP,EAAA;AAAA,IAEJ,CAAC;AAAA,IAEH,CAACyB,GAAOG,CAAW;AAAA,EAAA,GAGfK,IAAWlC;AAAA,IACfgC;AAAA,IACA,MAAMD,EAAU;AAAA,IAChB,MAAMA,EAAU;AAAA,EAAA,GAGZI,IAAML;AAAA,IACV,CAACM,MAAgB;AAGf,YAAMC,IAAsB,EAAE,CAACT,CAAK,GAAGQ,EAAA;AACtC,MAAAV,EAA4D,IAAIW,CAAO;AAAA,IAC1E;AAAA,IACA,CAACX,GAAOE,CAAK;AAAA,EAAA;AAGf,SAAO;AAAA,IACL,OAAOM,EAAS;AAAA,IAChB,OAAOA,EAAS;AAAA,IAChB,KAAAC;AAAA,EAAA;AAEJ;ACnHO,SAASG,EACdC,GACAC,GACAC,GACM;AACN,QAAMC,IAAMH,aAAkBI,IAAWJ,IAASA,EAAO,QAGnDK,IAAazC,EAAOsC,CAAO;AACjC,EAAAG,EAAW,UAAUH,GAErBvB,EAAU,MACYwB,EAAI,GAAGF,GAAO,CAACK,MAAY;AAC7C,IAAAD,EAAW,QAAQC,CAAO;AAAA,EAC5B,CAAC,GAGA,CAACH,GAAKF,CAAK,CAAC;AACjB;AAKO,SAASM,EACdJ,GACsD;AACtD,SAAOZ;AAAA,IACL,CAAoBU,GAAUK,MAAkB;AAC9C,MAAAH,EAAI,KAAKF,GAAOK,CAAO;AAAA,IACzB;AAAA,IACA,CAACH,CAAG;AAAA,EAAA;AAER;AC/BO,SAASK,KACXC,GACG;AACN,QAAMhC,IAAab,EAAO,EAAK;AAE/B,EAAAe,EAAU,OACRF,EAAW,UAAU,IACd,MAAM;AACX,IAAAA,EAAW,UAAU,IACrB,WAAW,MAAM;AACf,UAAI,CAACA,EAAW;AACd,mBAAWK,KAAS2B;AAClB,UAAAC,EAAS5B,CAAK;AAAA,IAGpB,GAAG,CAAC;AAAA,EACN,IACC,CAAA,CAAE;AACP;ACrBA,MAAM6B,IAAkBC,EAAuC,IAAI;AAY5D,SAASC,EAAS,EAAE,SAAAC,GAAS,UAAAC,KAAsC;AACxE,QAAMC,IAAWC,EAAQ,MAAM;AAC7B,UAAMC,wBAA4B,IAAA;AAClC,eAAW,CAACpC,GAAOF,CAAQ,KAAKkC;AAC9B,MAAAI,EAAI,IAAIpC,GAAOF,CAAQ;AAEzB,WAAOsC;AAAA,EACT,GAAG,CAACJ,CAAO,CAAC;AAEZ,2BACGH,EAAgB,UAAhB,EAAyB,OAAOK,GAC9B,UAAAD,GACH;AAEJ;AAKO,SAASI,EACdrC,MACGR,GACA;AACH,QAAM0C,IAAWI,EAAWT,CAAe;AAE3C,SAAIK,GAAU,IAAIlC,CAAK,IACdkC,EAAS,IAAIlC,CAAK,IAEpBC,EAAUD,GAAgD,GAAGR,CAAI;AAC1E;"}
@@ -3,9 +3,11 @@ class r {
3
3
  _handlers = /* @__PURE__ */ new Map();
4
4
  _abortController = null;
5
5
  _cleanups = null;
6
+ /** Whether this instance has been disposed. */
6
7
  get disposed() {
7
8
  return this._disposed;
8
9
  }
10
+ /** AbortSignal that fires when this instance is disposed. Lazily created. */
9
11
  get disposeSignal() {
10
12
  return this._abortController || (this._abortController = new AbortController()), this._abortController.signal;
11
13
  }
@@ -41,6 +43,7 @@ class r {
41
43
  });
42
44
  return e;
43
45
  }
46
+ /** Tears down the instance, releasing all subscriptions and resources. */
44
47
  dispose() {
45
48
  if (!this._disposed) {
46
49
  if (this._disposed = !0, this._abortController?.abort(), this._cleanups) {
@@ -50,6 +53,7 @@ class r {
50
53
  this.onDispose?.(), this._handlers.clear();
51
54
  }
52
55
  }
56
+ /** Registers a cleanup function to be called on dispose. @protected */
53
57
  addCleanup(s) {
54
58
  this._cleanups || (this._cleanups = []), this._cleanups.push(s);
55
59
  }
@@ -82,4 +86,4 @@ export {
82
86
  l as s,
83
87
  d as t
84
88
  };
85
- //# sourceMappingURL=singleton-C8_FRbA7.js.map
89
+ //# sourceMappingURL=singleton-CaEXSbYg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton-CaEXSbYg.js","sources":["../src/EventBus.ts","../src/singleton.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\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 /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\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._handlers.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","import type { Disposable } from './types';\n\n// Using 'any' for registry types to avoid variance issues with generics\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyDisposableClass = new (...args: any[]) => Disposable;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<AnyDisposableClass, Disposable>();\n\n/**\n * Get-or-create a singleton instance for the given class.\n * Returns the existing instance if one exists and is not disposed; otherwise creates a new one.\n */\nexport function singleton<T extends Disposable, Args extends unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T {\n const existing = registry.get(Class as AnyDisposableClass);\n\n if (existing && !existing.disposed) {\n return existing as T;\n }\n\n const instance = new Class(...args);\n registry.set(Class as AnyDisposableClass, instance);\n return instance;\n}\n\n/**\n * Check if a singleton instance exists for a class.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function hasSingleton(Class: new (...args: any[]) => Disposable): boolean {\n const existing = registry.get(Class);\n return existing !== undefined && !existing.disposed;\n}\n\n/**\n * Disposes and removes the singleton instance for the given class.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function teardown(Class: new (...args: any[]) => Disposable): void {\n const instance = registry.get(Class);\n if (instance) {\n instance.dispose();\n registry.delete(Class);\n }\n}\n\n/**\n * Disposes all singletons and clears the registry. Typically used in test cleanup.\n */\nexport function teardownAll(): void {\n for (const instance of registry.values()) {\n instance.dispose();\n }\n registry.clear();\n}\n"],"names":["EventBus","event","payload","handlers","handler","unsubscribe","fn","registry","singleton","Class","args","existing","instance","hasSingleton","teardown","teardownAll"],"mappings":"AAOO,MAAMA,EAA8D;AAAA,EAIjE,YAAY;AAAA,EACZ,gCAAgB,IAAA;AAAA,EAChB,mBAA2C;AAAA,EAC3C,YAAmC;AAAA;AAAA,EAG3C,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,WAAK,KAAK,qBACR,KAAK,mBAAmB,IAAI,gBAAA,IAEvB,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwBC,GAAUC,GAAqB;AACrD,QAAI,KAAK;AACP,YAAM,IAAI,MAAM,kCAAkC;AAGpD,UAAMC,IAAW,KAAK,UAAU,IAAIF,CAAK;AACzC,QAAIE;AACF,iBAAWC,KAAWD;AACpB,QAAAC,EAAQF,CAAO;AAAA,EAGrB;AAAA;AAAA;AAAA;AAAA,EAKA,GAAsBD,GAAUG,GAAoC;AAClE,QAAI,KAAK;AACP,aAAO,MAAM;AAAA,MAAC;AAGhB,QAAID,IAAW,KAAK,UAAU,IAAIF,CAAK;AACvC,WAAKE,MACHA,wBAAe,IAAA,GACf,KAAK,UAAU,IAAIF,GAAOE,CAAQ,IAGpCA,EAAS,IAAIC,CAA2B,GAEjC,MAAM;AACX,MAAAD,EAAU,OAAOC,CAA2B;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwBH,GAAUG,GAAoC;AACpE,UAAMC,IAAc,KAAK,GAAGJ,GAAO,CAACC,MAAY;AAC9C,MAAAG,EAAA,GACAD,EAAQF,CAAO;AAAA,IACjB,CAAC;AACD,WAAOG;AAAA,EACT;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,MAAK,WAMT;AAAA,UAFA,KAAK,YAAY,IACjB,KAAK,kBAAkB,MAAA,GACnB,KAAK,WAAW;AAClB,mBAAWC,KAAM,KAAK,UAAW,CAAAA,EAAA;AACjC,aAAK,YAAY;AAAA,MACnB;AACA,WAAK,YAAA,GACL,KAAK,UAAU,MAAA;AAAA;AAAA,EACjB;AAAA;AAAA,EAGU,WAAWA,GAAsB;AACzC,IAAK,KAAK,cACR,KAAK,YAAY,CAAA,IAEnB,KAAK,UAAU,KAAKA,CAAE;AAAA,EACxB;AAIF;ACjGA,MAAMC,wBAAe,IAAA;AAMd,SAASC,EACdC,MACGC,GACA;AACH,QAAMC,IAAWJ,EAAS,IAAIE,CAA2B;AAEzD,MAAIE,KAAY,CAACA,EAAS;AACxB,WAAOA;AAGT,QAAMC,IAAW,IAAIH,EAAM,GAAGC,CAAI;AAClC,SAAAH,EAAS,IAAIE,GAA6BG,CAAQ,GAC3CA;AACT;AAMO,SAASC,EAAaJ,GAAoD;AAC/E,QAAME,IAAWJ,EAAS,IAAIE,CAAK;AACnC,SAAOE,MAAa,UAAa,CAACA,EAAS;AAC7C;AAMO,SAASG,EAASL,GAAiD;AACxE,QAAMG,IAAWL,EAAS,IAAIE,CAAK;AACnC,EAAIG,MACFA,EAAS,QAAA,GACTL,EAAS,OAAOE,CAAK;AAEzB;AAKO,SAASM,IAAoB;AAClC,aAAWH,KAAYL,EAAS;AAC9B,IAAAK,EAAS,QAAA;AAEX,EAAAL,EAAS,MAAA;AACX;"}
@@ -1 +1 @@
1
- {"version":3,"file":"singleton-L-u2W_lX.cjs","sources":["../src/EventBus.ts","../src/singleton.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n get disposed(): boolean {\n return this._disposed;\n }\n\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\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._handlers.clear();\n }\n\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n protected onDispose?(): void;\n}\n","import type { Disposable } from './types';\n\n// Using 'any' for registry types to avoid variance issues with generics\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyDisposableClass = new (...args: any[]) => Disposable;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<AnyDisposableClass, Disposable>();\n\nexport function singleton<T extends Disposable, Args extends unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T {\n const existing = registry.get(Class as AnyDisposableClass);\n\n if (existing && !existing.disposed) {\n return existing as T;\n }\n\n const instance = new Class(...args);\n registry.set(Class as AnyDisposableClass, instance);\n return instance;\n}\n\n/**\n * Check if a singleton instance exists for a class.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function hasSingleton(Class: new (...args: any[]) => Disposable): boolean {\n const existing = registry.get(Class);\n return existing !== undefined && !existing.disposed;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function teardown(Class: new (...args: any[]) => Disposable): void {\n const instance = registry.get(Class);\n if (instance) {\n instance.dispose();\n registry.delete(Class);\n }\n}\n\nexport function teardownAll(): void {\n for (const instance of registry.values()) {\n instance.dispose();\n }\n registry.clear();\n}\n"],"names":["EventBus","event","payload","handlers","handler","unsubscribe","fn","registry","singleton","Class","args","existing","instance","hasSingleton","teardown","teardownAll"],"mappings":"aAOO,MAAMA,CAA8D,CAIjE,UAAY,GACZ,cAAgB,IAChB,iBAA2C,KAC3C,UAAmC,KAE3C,IAAI,UAAoB,CACtB,OAAO,KAAK,SACd,CAEA,IAAI,eAA6B,CAC/B,OAAK,KAAK,mBACR,KAAK,iBAAmB,IAAI,iBAEvB,KAAK,iBAAiB,MAC/B,CAKA,KAAwBC,EAAUC,EAAqB,CACrD,GAAI,KAAK,UACP,MAAM,IAAI,MAAM,kCAAkC,EAGpD,MAAMC,EAAW,KAAK,UAAU,IAAIF,CAAK,EACzC,GAAIE,EACF,UAAWC,KAAWD,EACpBC,EAAQF,CAAO,CAGrB,CAKA,GAAsBD,EAAUG,EAAoC,CAClE,GAAI,KAAK,UACP,MAAO,IAAM,CAAC,EAGhB,IAAID,EAAW,KAAK,UAAU,IAAIF,CAAK,EACvC,OAAKE,IACHA,MAAe,IACf,KAAK,UAAU,IAAIF,EAAOE,CAAQ,GAGpCA,EAAS,IAAIC,CAA2B,EAEjC,IAAM,CACXD,EAAU,OAAOC,CAA2B,CAC9C,CACF,CAKA,KAAwBH,EAAUG,EAAoC,CACpE,MAAMC,EAAc,KAAK,GAAGJ,EAAQC,GAAY,CAC9CG,EAAA,EACAD,EAAQF,CAAO,CACjB,CAAC,EACD,OAAOG,CACT,CAEA,SAAgB,CACd,GAAI,MAAK,UAMT,IAFA,KAAK,UAAY,GACjB,KAAK,kBAAkB,MAAA,EACnB,KAAK,UAAW,CAClB,UAAWC,KAAM,KAAK,UAAWA,EAAA,EACjC,KAAK,UAAY,IACnB,CACA,KAAK,YAAA,EACL,KAAK,UAAU,MAAA,EACjB,CAEU,WAAWA,EAAsB,CACpC,KAAK,YACR,KAAK,UAAY,CAAA,GAEnB,KAAK,UAAU,KAAKA,CAAE,CACxB,CAGF,CC5FA,MAAMC,MAAe,IAEd,SAASC,EACdC,KACGC,EACA,CACH,MAAMC,EAAWJ,EAAS,IAAIE,CAA2B,EAEzD,GAAIE,GAAY,CAACA,EAAS,SACxB,OAAOA,EAGT,MAAMC,EAAW,IAAIH,EAAM,GAAGC,CAAI,EAClC,OAAAH,EAAS,IAAIE,EAA6BG,CAAQ,EAC3CA,CACT,CAMO,SAASC,EAAaJ,EAAoD,CAC/E,MAAME,EAAWJ,EAAS,IAAIE,CAAK,EACnC,OAAOE,IAAa,QAAa,CAACA,EAAS,QAC7C,CAGO,SAASG,EAASL,EAAiD,CACxE,MAAMG,EAAWL,EAAS,IAAIE,CAAK,EAC/BG,IACFA,EAAS,QAAA,EACTL,EAAS,OAAOE,CAAK,EAEzB,CAEO,SAASM,GAAoB,CAClC,UAAWH,KAAYL,EAAS,SAC9BK,EAAS,QAAA,EAEXL,EAAS,MAAA,CACX"}
1
+ {"version":3,"file":"singleton-L-u2W_lX.cjs","sources":["../src/EventBus.ts","../src/singleton.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\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 /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\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._handlers.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","import type { Disposable } from './types';\n\n// Using 'any' for registry types to avoid variance issues with generics\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyDisposableClass = new (...args: any[]) => Disposable;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<AnyDisposableClass, Disposable>();\n\n/**\n * Get-or-create a singleton instance for the given class.\n * Returns the existing instance if one exists and is not disposed; otherwise creates a new one.\n */\nexport function singleton<T extends Disposable, Args extends unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T {\n const existing = registry.get(Class as AnyDisposableClass);\n\n if (existing && !existing.disposed) {\n return existing as T;\n }\n\n const instance = new Class(...args);\n registry.set(Class as AnyDisposableClass, instance);\n return instance;\n}\n\n/**\n * Check if a singleton instance exists for a class.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function hasSingleton(Class: new (...args: any[]) => Disposable): boolean {\n const existing = registry.get(Class);\n return existing !== undefined && !existing.disposed;\n}\n\n/**\n * Disposes and removes the singleton instance for the given class.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function teardown(Class: new (...args: any[]) => Disposable): void {\n const instance = registry.get(Class);\n if (instance) {\n instance.dispose();\n registry.delete(Class);\n }\n}\n\n/**\n * Disposes all singletons and clears the registry. Typically used in test cleanup.\n */\nexport function teardownAll(): void {\n for (const instance of registry.values()) {\n instance.dispose();\n }\n registry.clear();\n}\n"],"names":["EventBus","event","payload","handlers","handler","unsubscribe","fn","registry","singleton","Class","args","existing","instance","hasSingleton","teardown","teardownAll"],"mappings":"aAOO,MAAMA,CAA8D,CAIjE,UAAY,GACZ,cAAgB,IAChB,iBAA2C,KAC3C,UAAmC,KAG3C,IAAI,UAAoB,CACtB,OAAO,KAAK,SACd,CAGA,IAAI,eAA6B,CAC/B,OAAK,KAAK,mBACR,KAAK,iBAAmB,IAAI,iBAEvB,KAAK,iBAAiB,MAC/B,CAKA,KAAwBC,EAAUC,EAAqB,CACrD,GAAI,KAAK,UACP,MAAM,IAAI,MAAM,kCAAkC,EAGpD,MAAMC,EAAW,KAAK,UAAU,IAAIF,CAAK,EACzC,GAAIE,EACF,UAAWC,KAAWD,EACpBC,EAAQF,CAAO,CAGrB,CAKA,GAAsBD,EAAUG,EAAoC,CAClE,GAAI,KAAK,UACP,MAAO,IAAM,CAAC,EAGhB,IAAID,EAAW,KAAK,UAAU,IAAIF,CAAK,EACvC,OAAKE,IACHA,MAAe,IACf,KAAK,UAAU,IAAIF,EAAOE,CAAQ,GAGpCA,EAAS,IAAIC,CAA2B,EAEjC,IAAM,CACXD,EAAU,OAAOC,CAA2B,CAC9C,CACF,CAKA,KAAwBH,EAAUG,EAAoC,CACpE,MAAMC,EAAc,KAAK,GAAGJ,EAAQC,GAAY,CAC9CG,EAAA,EACAD,EAAQF,CAAO,CACjB,CAAC,EACD,OAAOG,CACT,CAGA,SAAgB,CACd,GAAI,MAAK,UAMT,IAFA,KAAK,UAAY,GACjB,KAAK,kBAAkB,MAAA,EACnB,KAAK,UAAW,CAClB,UAAWC,KAAM,KAAK,UAAWA,EAAA,EACjC,KAAK,UAAY,IACnB,CACA,KAAK,YAAA,EACL,KAAK,UAAU,MAAA,EACjB,CAGU,WAAWA,EAAsB,CACpC,KAAK,YACR,KAAK,UAAY,CAAA,GAEnB,KAAK,UAAU,KAAKA,CAAE,CACxB,CAIF,CCjGA,MAAMC,MAAe,IAMd,SAASC,EACdC,KACGC,EACA,CACH,MAAMC,EAAWJ,EAAS,IAAIE,CAA2B,EAEzD,GAAIE,GAAY,CAACA,EAAS,SACxB,OAAOA,EAGT,MAAMC,EAAW,IAAIH,EAAM,GAAGC,CAAI,EAClC,OAAAH,EAAS,IAAIE,EAA6BG,CAAQ,EAC3CA,CACT,CAMO,SAASC,EAAaJ,EAAoD,CAC/E,MAAME,EAAWJ,EAAS,IAAIE,CAAK,EACnC,OAAOE,IAAa,QAAa,CAACA,EAAS,QAC7C,CAMO,SAASG,EAASL,EAAiD,CACxE,MAAMG,EAAWL,EAAS,IAAIE,CAAK,EAC/BG,IACFA,EAAS,QAAA,EACTL,EAAS,OAAOE,CAAK,EAEzB,CAKO,SAASM,GAAoB,CAClC,UAAWH,KAAYL,EAAS,SAC9BK,EAAS,QAAA,EAEXL,EAAS,MAAA,CACX"}
@@ -1,9 +1,19 @@
1
1
  import type { Disposable } from './types';
2
+ /**
3
+ * Get-or-create a singleton instance for the given class.
4
+ * Returns the existing instance if one exists and is not disposed; otherwise creates a new one.
5
+ */
2
6
  export declare function singleton<T extends Disposable, Args extends unknown[]>(Class: new (...args: Args) => T, ...args: Args): T;
3
7
  /**
4
8
  * Check if a singleton instance exists for a class.
5
9
  */
6
10
  export declare function hasSingleton(Class: new (...args: any[]) => Disposable): boolean;
11
+ /**
12
+ * Disposes and removes the singleton instance for the given class.
13
+ */
7
14
  export declare function teardown(Class: new (...args: any[]) => Disposable): void;
15
+ /**
16
+ * Disposes all singletons and clears the registry. Typically used in test cleanup.
17
+ */
8
18
  export declare function teardownAll(): void;
9
19
  //# sourceMappingURL=singleton.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../src/singleton.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAQ1C,wBAAgB,SAAS,CAAC,CAAC,SAAS,UAAU,EAAE,IAAI,SAAS,OAAO,EAAE,EACpE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,EAC/B,GAAG,IAAI,EAAE,IAAI,GACZ,CAAC,CAUH;AAED;;GAEG;AAEH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,GAAG,OAAO,CAG/E;AAGD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,GAAG,IAAI,CAMxE;AAED,wBAAgB,WAAW,IAAI,IAAI,CAKlC"}
1
+ {"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../src/singleton.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAQ1C;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,UAAU,EAAE,IAAI,SAAS,OAAO,EAAE,EACpE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,EAC/B,GAAG,IAAI,EAAE,IAAI,GACZ,CAAC,CAUH;AAED;;GAEG;AAEH,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,GAAG,OAAO,CAG/E;AAED;;GAEG;AAEH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,GAAG,IAAI,CAMxE;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAKlC"}
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mvc-kit",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "Zero-magic, class-based reactive ViewModel library",
5
5
  "type": "module",
6
6
  "main": "./dist/mvc-kit.cjs",
@@ -29,6 +29,7 @@
29
29
  }
30
30
  },
31
31
  "files": [
32
+ "./mvc-kit-logo.jpg",
32
33
  "dist",
33
34
  "agent-config"
34
35
  ],
@@ -1 +0,0 @@
1
- {"version":3,"file":"singleton-C8_FRbA7.js","sources":["../src/EventBus.ts","../src/singleton.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n get disposed(): boolean {\n return this._disposed;\n }\n\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\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._handlers.clear();\n }\n\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n protected onDispose?(): void;\n}\n","import type { Disposable } from './types';\n\n// Using 'any' for registry types to avoid variance issues with generics\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyDisposableClass = new (...args: any[]) => Disposable;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst registry = new Map<AnyDisposableClass, Disposable>();\n\nexport function singleton<T extends Disposable, Args extends unknown[]>(\n Class: new (...args: Args) => T,\n ...args: Args\n): T {\n const existing = registry.get(Class as AnyDisposableClass);\n\n if (existing && !existing.disposed) {\n return existing as T;\n }\n\n const instance = new Class(...args);\n registry.set(Class as AnyDisposableClass, instance);\n return instance;\n}\n\n/**\n * Check if a singleton instance exists for a class.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function hasSingleton(Class: new (...args: any[]) => Disposable): boolean {\n const existing = registry.get(Class);\n return existing !== undefined && !existing.disposed;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function teardown(Class: new (...args: any[]) => Disposable): void {\n const instance = registry.get(Class);\n if (instance) {\n instance.dispose();\n registry.delete(Class);\n }\n}\n\nexport function teardownAll(): void {\n for (const instance of registry.values()) {\n instance.dispose();\n }\n registry.clear();\n}\n"],"names":["EventBus","event","payload","handlers","handler","unsubscribe","fn","registry","singleton","Class","args","existing","instance","hasSingleton","teardown","teardownAll"],"mappings":"AAOO,MAAMA,EAA8D;AAAA,EAIjE,YAAY;AAAA,EACZ,gCAAgB,IAAA;AAAA,EAChB,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EAE3C,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,gBAA6B;AAC/B,WAAK,KAAK,qBACR,KAAK,mBAAmB,IAAI,gBAAA,IAEvB,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwBC,GAAUC,GAAqB;AACrD,QAAI,KAAK;AACP,YAAM,IAAI,MAAM,kCAAkC;AAGpD,UAAMC,IAAW,KAAK,UAAU,IAAIF,CAAK;AACzC,QAAIE;AACF,iBAAWC,KAAWD;AACpB,QAAAC,EAAQF,CAAO;AAAA,EAGrB;AAAA;AAAA;AAAA;AAAA,EAKA,GAAsBD,GAAUG,GAAoC;AAClE,QAAI,KAAK;AACP,aAAO,MAAM;AAAA,MAAC;AAGhB,QAAID,IAAW,KAAK,UAAU,IAAIF,CAAK;AACvC,WAAKE,MACHA,wBAAe,IAAA,GACf,KAAK,UAAU,IAAIF,GAAOE,CAAQ,IAGpCA,EAAS,IAAIC,CAA2B,GAEjC,MAAM;AACX,MAAAD,EAAU,OAAOC,CAA2B;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwBH,GAAUG,GAAoC;AACpE,UAAMC,IAAc,KAAK,GAAGJ,GAAO,CAACC,MAAY;AAC9C,MAAAG,EAAA,GACAD,EAAQF,CAAO;AAAA,IACjB,CAAC;AACD,WAAOG;AAAA,EACT;AAAA,EAEA,UAAgB;AACd,QAAI,MAAK,WAMT;AAAA,UAFA,KAAK,YAAY,IACjB,KAAK,kBAAkB,MAAA,GACnB,KAAK,WAAW;AAClB,mBAAWC,KAAM,KAAK,UAAW,CAAAA,EAAA;AACjC,aAAK,YAAY;AAAA,MACnB;AACA,WAAK,YAAA,GACL,KAAK,UAAU,MAAA;AAAA;AAAA,EACjB;AAAA,EAEU,WAAWA,GAAsB;AACzC,IAAK,KAAK,cACR,KAAK,YAAY,CAAA,IAEnB,KAAK,UAAU,KAAKA,CAAE;AAAA,EACxB;AAGF;AC5FA,MAAMC,wBAAe,IAAA;AAEd,SAASC,EACdC,MACGC,GACA;AACH,QAAMC,IAAWJ,EAAS,IAAIE,CAA2B;AAEzD,MAAIE,KAAY,CAACA,EAAS;AACxB,WAAOA;AAGT,QAAMC,IAAW,IAAIH,EAAM,GAAGC,CAAI;AAClC,SAAAH,EAAS,IAAIE,GAA6BG,CAAQ,GAC3CA;AACT;AAMO,SAASC,EAAaJ,GAAoD;AAC/E,QAAME,IAAWJ,EAAS,IAAIE,CAAK;AACnC,SAAOE,MAAa,UAAa,CAACA,EAAS;AAC7C;AAGO,SAASG,EAASL,GAAiD;AACxE,QAAMG,IAAWL,EAAS,IAAIE,CAAK;AACnC,EAAIG,MACFA,EAAS,QAAA,GACTL,EAAS,OAAOE,CAAK;AAEzB;AAEO,SAASM,IAAoB;AAClC,aAAWH,KAAYL,EAAS;AAC9B,IAAAK,EAAS,QAAA;AAEX,EAAAL,EAAS,MAAA;AACX;"}