mvc-kit 2.3.0 → 2.4.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 CHANGED
@@ -162,6 +162,49 @@ todos.sorted((a, b) => a.text.localeCompare(b.text));
162
162
  todos.map(t => t.text); // Map to new array
163
163
  ```
164
164
 
165
+ ### Resource
166
+
167
+ Collection + async tracking toolkit. Extends Collection with lifecycle and automatic async method tracking.
168
+
169
+ ```typescript
170
+ class UsersResource extends Resource<User> {
171
+ private api = singleton(UserService);
172
+
173
+ async loadAll() {
174
+ const data = await this.api.getAll(this.disposeSignal);
175
+ this.reset(data);
176
+ }
177
+
178
+ async loadById(id: number) {
179
+ const user = await this.api.getById(id, this.disposeSignal);
180
+ this.upsert(user);
181
+ }
182
+ }
183
+
184
+ const users = singleton(UsersResource);
185
+ await users.init();
186
+
187
+ users.loadAll();
188
+ users.async.loadAll.loading; // true while loading
189
+ users.async.loadAll.error; // error message, or null
190
+ users.async.loadAll.errorCode; // 'not_found', 'network', etc.
191
+
192
+ // Inherits all Collection methods
193
+ users.items; // readonly User[]
194
+ users.get(1); // User | undefined
195
+ users.filter(u => u.active);
196
+ ```
197
+
198
+ Supports external Collection injection for shared data scenarios:
199
+
200
+ ```typescript
201
+ class UsersResource extends Resource<User> {
202
+ constructor() {
203
+ super(singleton(SharedUsersCollection)); // All mutations go to the shared collection
204
+ }
205
+ }
206
+ ```
207
+
165
208
  ### Controller
166
209
 
167
210
  Stateless orchestrator for complex logic. Component-scoped, auto-disposed.
@@ -664,6 +707,7 @@ function App() {
664
707
  | `ViewModel<S, E?>` | Reactive state container with optional typed events |
665
708
  | `Model<S>` | Reactive entity with validation/dirty tracking |
666
709
  | `Collection<T>` | Reactive typed array with CRUD |
710
+ | `Resource<T>` | Collection + async tracking + external Collection injection |
667
711
  | `Controller` | Stateless orchestrator (Disposable) |
668
712
  | `Service` | Non-reactive infrastructure service (Disposable) |
669
713
  | `EventBus<E>` | Typed pub/sub event bus |
@@ -679,7 +723,8 @@ function App() {
679
723
  | `Updater<S>` | `(state: Readonly<S>) => Partial<S>` |
680
724
  | `ValidationErrors<S>` | `Partial<Record<keyof S, string>>` |
681
725
  | `TaskState` | `{ loading: boolean; error: string \| null }` |
682
- | `AsyncMethodKeys<T>` | Union of method names on `T` that return `Promise` |
726
+ | `AsyncMethodKeys<T>` | Union of method names on `T` that return `Promise` (ViewModel) |
727
+ | `ResourceAsyncMethodKeys<T>` | Union of method names on `T` that return `Promise` (Resource) |
683
728
 
684
729
  ### Singleton Functions
685
730
 
@@ -734,7 +779,7 @@ function App() {
734
779
  - After `init()`, all subclass methods are wrapped for automatic async tracking; `vm.async.methodName` returns `TaskState`
735
780
  - Sync methods are auto-pruned on first call — zero overhead after detection
736
781
  - Async errors are re-thrown (preserves standard Promise rejection); AbortErrors are silently swallowed by the wrapper (but internal catch blocks do receive them — use `isAbortError()` to guard shared-state side effects like Collection rollbacks)
737
- - `async` and `subscribeAsync` are reserved property names on ViewModel
782
+ - `async` and `subscribeAsync` are reserved property names on ViewModel and Resource
738
783
  - React hooks (`useLocal`, `useModel`, `useSingleton`) auto-call `init()` after mount
739
784
  - `singleton()` does **not** auto-call `init()` — call it manually outside React
740
785
  - StrictMode safe: the `_initialized` guard prevents double-init during React's double-mount cycle; `disposeSignal` is not aborted during StrictMode's fake unmount/remount cycle
@@ -751,6 +796,7 @@ Each core class and React hook has a dedicated reference doc with full API detai
751
796
  | [ViewModel](src/ViewModel.md) | State management, computed getters, async tracking, typed events, lifecycle hooks |
752
797
  | [Model](src/Model.md) | Validation, dirty tracking, commit/rollback for entity forms |
753
798
  | [Collection](src/Collection.md) | Reactive typed array, CRUD, optimistic updates, shared data cache |
799
+ | [Resource](src/Resource.md) | Collection + async tracking toolkit with external Collection injection |
754
800
  | [Controller](src/Controller.md) | Stateless orchestrator for multi-ViewModel coordination |
755
801
  | [Service](src/Service.md) | Non-reactive infrastructure adapters (HTTP, storage, SDKs) |
756
802
  | [EventBus](src/EventBus.md) | Typed pub/sub for cross-cutting event communication |
@@ -13,6 +13,7 @@ You are an architecture planning agent for applications built with **mvc-kit**,
13
13
  | `ViewModel<S, E?>` | Reactive state + computed getters + async tracking + typed events | Component-scoped (`useLocal`) |
14
14
  | `Model<S>` | Entity with validation + dirty tracking + commit/rollback | Component-scoped (`useModel`) |
15
15
  | `Collection<T>` | Reactive typed array, shared data cache, optimistic updates | Singleton |
16
+ | `Resource<T>` | Collection + async tracking, transparent Collection injection | Singleton |
16
17
  | `Service` | Stateless infrastructure adapter (HTTP, storage) | Singleton |
17
18
  | `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
18
19
  | `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
@@ -24,11 +25,12 @@ You are an architecture planning agent for applications built with **mvc-kit**,
24
25
  1. Holds UI state for a component? → **ViewModel**
25
26
  2. Single entity with validation? → **Model**
26
27
  3. List of entities with CRUD? → **Collection**
27
- 4. Fetches external data? → **Service**
28
- 5. Broadcasts cross-cutting events? → **EventBus**
29
- 6. Persistent connection? → **Channel**
30
- 7. Coordinates multiple ViewModels? → **Controller** (rare)
31
- 8. None of the above plain utility function
28
+ 4. List of entities with CRUD + API loading + loading/error tracking? → **Resource**
29
+ 5. Fetches external data? → **Service**
30
+ 6. Broadcasts cross-cutting events? → **EventBus**
31
+ 7. Persistent connection? → **Channel**
32
+ 8. Coordinates multiple ViewModels?**Controller** (rare)
33
+ 9. None of the above → plain utility function
32
34
 
33
35
  ### Which sharing pattern?
34
36
  - "Can the parent own one ViewModel and pass props?" → **Pattern A** (default)
@@ -341,7 +341,46 @@ async loadPage(page: number) {
341
341
 
342
342
  ---
343
343
 
344
- ## 16. Missing disposeSignal on Async Calls
344
+ ## 16. Empty Collection + Service When Resource Would Suffice
345
+
346
+ ```typescript
347
+ // BAD — boilerplate: empty Collection subclass + Service + manual cache check
348
+ class UsersCollection extends Collection<User> {}
349
+ class UsersViewModel extends ViewModel<State> {
350
+ private collection = singleton(UsersCollection);
351
+ private service = singleton(UserService);
352
+
353
+ onInit() {
354
+ if (this.collection.length === 0) this.load();
355
+ }
356
+
357
+ async load() {
358
+ const data = await this.service.getAll(this.disposeSignal);
359
+ this.collection.reset(data);
360
+ }
361
+ }
362
+
363
+ // GOOD — Resource combines Collection + async tracking
364
+ class UsersResource extends Resource<User> {
365
+ private api = singleton(UserService);
366
+
367
+ async loadAll() {
368
+ const data = await this.api.getAll(this.disposeSignal);
369
+ this.reset(data);
370
+ }
371
+ }
372
+ class UsersViewModel extends ViewModel<State> {
373
+ private users = singleton(UsersResource);
374
+
375
+ onInit() {
376
+ if (this.users.length === 0) this.users.loadAll();
377
+ }
378
+ }
379
+ ```
380
+
381
+ ---
382
+
383
+ ## 17. Missing disposeSignal on Async Calls
345
384
 
346
385
  ```typescript
347
386
  // BAD — no cancellation on unmount
@@ -4,10 +4,10 @@
4
4
 
5
5
  ```typescript
6
6
  // Core classes and utilities
7
- import { ViewModel, Model, Collection, Controller, Service, EventBus, Channel } from 'mvc-kit';
7
+ import { ViewModel, Model, Collection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
8
8
  import { singleton, hasSingleton, teardown, teardownAll } from 'mvc-kit';
9
9
  import { HttpError, isAbortError, classifyError } from 'mvc-kit';
10
- import type { Subscribable, Disposable, Initializable, Listener, Updater, ValidationErrors, TaskState, AppError, AsyncMethodKeys, ChannelStatus } from 'mvc-kit';
10
+ import type { Subscribable, Disposable, Initializable, Listener, Updater, ValidationErrors, TaskState, AppError, AsyncMethodKeys, ResourceAsyncMethodKeys, ChannelStatus } from 'mvc-kit';
11
11
 
12
12
  // React hooks
13
13
  import { useLocal, useSingleton, useInstance, useModel, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
@@ -144,6 +144,39 @@ Static overrides for auto-eviction (zero-cost when not configured):
144
144
 
145
145
  ---
146
146
 
147
+ ## Resource<T extends { id: string | number }>
148
+
149
+ Collection + async tracking toolkit. Extends Collection with lifecycle and automatic async method tracking.
150
+
151
+ ### Constructor
152
+ ```typescript
153
+ new MyResource() // Default: Resource IS the collection
154
+ new MyResource([item1, item2]) // With initial items
155
+ new MyResource(externalCollection) // Inject external Collection
156
+ ```
157
+
158
+ ### Lifecycle
159
+ - `initialized: boolean` — Whether `init()` has been called.
160
+ - `init(): void | Promise<void>` — Wraps methods for async tracking, calls `onInit()`. Idempotent.
161
+ - `onInit(): void | Promise<void>` — Override for initial data loading.
162
+ - `onDispose(): void` — Override for custom teardown.
163
+
164
+ ### Async Tracking
165
+ After `init()`, every subclass method is wrapped. Async methods get automatic loading/error tracking.
166
+ - `async: Record<string, TaskState>` — `resource.async.methodName` returns `{ loading, error, errorCode }`.
167
+ - `subscribeAsync(listener: () => void): () => void` — Subscribe to async state changes.
168
+
169
+ ### External Collection Injection
170
+ When constructor receives a Collection instance, ALL inherited Collection methods (add, upsert, reset, items, subscribe, etc.) transparently delegate to the external collection. Resource disposal does NOT dispose the shared collection.
171
+
172
+ ### Inherits All Collection API
173
+ CRUD, query, subscribe, optimistic, MAX_SIZE, TTL — all from Collection.
174
+
175
+ ### Static
176
+ - `static GHOST_TIMEOUT = 3000` — DEV-only ghost detection delay.
177
+
178
+ ---
179
+
147
180
  ## Service
148
181
 
149
182
  Stateless infrastructure adapter. Singleton-scoped.
@@ -229,6 +229,32 @@ Thin subclass for singleton identity. No custom methods — query logic goes in
229
229
 
230
230
  ---
231
231
 
232
+ ## Resource Pattern
233
+
234
+ ```typescript
235
+ class UsersResource extends Resource<UserState> {
236
+ private api = singleton(UserService);
237
+
238
+ async loadAll() {
239
+ const data = await this.api.getAll(this.disposeSignal);
240
+ this.reset(data);
241
+ }
242
+ }
243
+ ```
244
+
245
+ Use Resource when you need a Collection with built-in async tracking. Define your own async methods; use inherited Collection mutations. Each method gets independent tracking: `resource.async.loadAll.loading`.
246
+
247
+ For shared data (Resource + Channel feeding same store), inject an external Collection:
248
+ ```typescript
249
+ class UsersResource extends Resource<UserState> {
250
+ constructor() {
251
+ super(singleton(SharedUsersCollection));
252
+ }
253
+ }
254
+ ```
255
+
256
+ ---
257
+
232
258
  ## Optimistic Updates
233
259
 
234
260
  ```typescript
@@ -9,6 +9,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
9
9
  | `ViewModel<S, E?>` | Reactive state + computed getters + async tracking + typed events | Component-scoped (`useLocal`) |
10
10
  | `Model<S>` | Entity with validation + dirty tracking + commit/rollback | Component-scoped (`useModel`) |
11
11
  | `Collection<T>` | Reactive typed array, shared data cache, optimistic updates | Singleton |
12
+ | `Resource<T>` | Collection + async tracking, transparent Collection injection | Singleton |
12
13
  | `Service` | Stateless infrastructure adapter (HTTP, storage) | Singleton |
13
14
  | `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
14
15
  | `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
@@ -17,7 +18,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
17
18
  ## Imports
18
19
 
19
20
  ```typescript
20
- import { ViewModel, Model, Collection, Controller, Service, EventBus, Channel } from 'mvc-kit';
21
+ import { ViewModel, Model, Collection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
21
22
  import { singleton, teardownAll, HttpError, isAbortError, classifyError } from 'mvc-kit';
22
23
  import { useLocal, useSingleton, useInstance, useModel, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
23
24
  ```
@@ -9,6 +9,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
9
9
  | `ViewModel<S, E?>` | Reactive state + computed getters + async tracking + typed events | Component-scoped (`useLocal`) |
10
10
  | `Model<S>` | Entity with validation + dirty tracking + commit/rollback | Component-scoped (`useModel`) |
11
11
  | `Collection<T>` | Reactive typed array, shared data cache, optimistic updates | Singleton |
12
+ | `Resource<T>` | Collection + async tracking, transparent Collection injection | Singleton |
12
13
  | `Service` | Stateless infrastructure adapter (HTTP, storage) | Singleton |
13
14
  | `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
14
15
  | `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
@@ -17,7 +18,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
17
18
  ## Imports
18
19
 
19
20
  ```typescript
20
- import { ViewModel, Model, Collection, Controller, Service, EventBus, Channel } from 'mvc-kit';
21
+ import { ViewModel, Model, Collection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
21
22
  import { singleton, teardownAll, HttpError, isAbortError, classifyError } from 'mvc-kit';
22
23
  import { useLocal, useSingleton, useInstance, useModel, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
23
24
  ```
@@ -0,0 +1,60 @@
1
+ import { Collection } from './Collection';
2
+ import type { Listener, TaskState } from './types';
3
+ export type ResourceAsyncMethodKeys<T> = {
4
+ [K in Exclude<keyof T, keyof Resource<any>>]: T[K] extends (...args: any[]) => Promise<any> ? K : never;
5
+ }[Exclude<keyof T, keyof Resource<any>>];
6
+ type ResourceAsyncMap<T> = {
7
+ readonly [K in ResourceAsyncMethodKeys<T>]: TaskState;
8
+ };
9
+ /**
10
+ * Collection + async tracking toolkit. Extends Collection with lifecycle
11
+ * (init/dispose) and automatic async method tracking. Optionally delegates
12
+ * to an external Collection for shared data scenarios.
13
+ */
14
+ export declare class Resource<T extends {
15
+ id: string | number;
16
+ }> extends Collection<T> {
17
+ private _external;
18
+ private _initialized;
19
+ private _asyncStates;
20
+ private _asyncSnapshots;
21
+ private _asyncListeners;
22
+ private _asyncProxy;
23
+ private _activeOps;
24
+ /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */
25
+ static GHOST_TIMEOUT: number;
26
+ constructor(collectionOrItems?: Collection<T> | T[]);
27
+ /** Whether init() has been called. */
28
+ get initialized(): boolean;
29
+ /** Initializes the instance. Called automatically by React hooks after mount. */
30
+ init(): void | Promise<void>;
31
+ /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */
32
+ protected onInit?(): void | Promise<void>;
33
+ get state(): readonly T[];
34
+ get items(): readonly T[];
35
+ get length(): number;
36
+ add(...items: T[]): void;
37
+ upsert(...items: T[]): void;
38
+ update(id: T['id'], changes: Partial<T>): void;
39
+ remove(...ids: T['id'][]): void;
40
+ reset(items: T[]): void;
41
+ clear(): void;
42
+ optimistic(callback: () => void): () => void;
43
+ get(id: T['id']): T | undefined;
44
+ has(id: T['id']): boolean;
45
+ find(predicate: (item: T) => boolean): T | undefined;
46
+ filter(predicate: (item: T) => boolean): readonly T[];
47
+ sorted(compareFn: (a: T, b: T) => number): readonly T[];
48
+ map<U>(fn: (item: T) => U): readonly U[];
49
+ subscribe(listener: Listener<readonly T[]>): () => void;
50
+ /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */
51
+ get async(): ResourceAsyncMap<this>;
52
+ /** Subscribes to async state changes. Used by `useAsync` and `useInstance` for React integration. */
53
+ subscribeAsync(listener: () => void): () => void;
54
+ private _notifyAsync;
55
+ private _guardReservedKeys;
56
+ private _wrapMethods;
57
+ private _scheduleGhostCheck;
58
+ }
59
+ export {};
60
+ //# sourceMappingURL=Resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Resource.d.ts","sourceRoot":"","sources":["../src/Resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAQnD,MAAM,MAAM,uBAAuB,CAAC,CAAC,IAAI;KACtC,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK;CACxG,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAEzC,KAAK,gBAAgB,CAAC,CAAC,IAAI;IACzB,QAAQ,EAAE,CAAC,IAAI,uBAAuB,CAAC,CAAC,CAAC,GAAG,SAAS;CACtD,CAAC;AAcF;;;;GAIG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAE,SAAQ,UAAU,CAAC,CAAC,CAAC;IAC5E,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,UAAU,CAAoC;IAEtD,gFAAgF;IAChF,MAAM,CAAC,aAAa,SAAQ;gBAEhB,iBAAiB,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE;IAuBnD,sCAAsC;IACtC,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,iFAAiF;IACjF,IAAI,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5B,4FAA4F;IAC5F,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzC,IAAI,KAAK,IAAI,SAAS,CAAC,EAAE,CAExB;IAED,IAAI,KAAK,IAAI,SAAS,CAAC,EAAE,CAExB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,GAAG,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAIxB,MAAM,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAI3B,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAI9C,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI;IAI/B,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAIvB,KAAK,IAAI,IAAI;IAIb,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAI5C,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,SAAS;IAI/B,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO;IAIzB,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,CAAC,GAAG,SAAS;IAIpD,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,SAAS,CAAC,EAAE;IAIrD,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC,EAAE;IAIvD,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,EAAE;IAIxC,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,MAAM,IAAI;IAOvD,gFAAgF;IAChF,IAAI,KAAK,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAsBlC;IAED,qGAAqG;IACrG,cAAc,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAQhD,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,YAAY;IAiMpB,OAAO,CAAC,mBAAmB;CAY5B"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export type { Listener, Updater, Subscribable, Disposable, Initializable, ValidationErrors, TaskState, } from './types';
2
2
  export type { AsyncMethodKeys } from './ViewModel';
3
+ export type { ResourceAsyncMethodKeys } from './Resource';
3
4
  export { ViewModel } from './ViewModel';
4
5
  export { Model } from './Model';
5
6
  export { Collection } from './Collection';
7
+ export { Resource } from './Resource';
6
8
  export { Controller } from './Controller';
7
9
  export { Service } from './Service';
8
10
  export { EventBus } from './EventBus';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,SAAS,GACV,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG/C,YAAY,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGlE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,SAAS,GACV,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAG1D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG/C,YAAY,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGlE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
package/dist/mvc-kit.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("./singleton-L-u2W_lX.cjs");class C extends Error{constructor(t,s){super(s??`HTTP ${t}`),this.status=t,this.name="HttpError"}}function y(r){return r instanceof Error&&r.name==="AbortError"}function v(r){return r===401?"unauthorized":r===403?"forbidden":r===404?"not_found":r===422?"validation":r===429?"rate_limited":r>=500?"server_error":"unknown"}function E(r){return typeof r=="object"&&r!==null&&typeof r.status=="number"&&typeof r.statusText=="string"&&!(r instanceof Error)}function T(r){return r instanceof Error&&r.name==="AbortError"?{code:"abort",message:"Request was aborted",original:r}:r instanceof C?{code:v(r.status),message:r.message,status:r.status,original:r}:E(r)?{code:v(r.status),message:r.statusText||`HTTP ${r.status}`,status:r.status,original:r}:r instanceof TypeError&&r.message.toLowerCase().includes("fetch")?{code:"network",message:r.message,original:r}:r instanceof Error&&r.name==="TimeoutError"?{code:"timeout",message:r.message,original:r}:r instanceof Error?{code:"unknown",message:r.message,original:r}:{code:"unknown",message:String(r),original:r}}const d=typeof __MVC_KIT_DEV__<"u"&&__MVC_KIT_DEV__;function O(r){return r!==null&&typeof r=="object"&&typeof r.subscribe=="function"}function g(r,t,s){let e=Object.getPrototypeOf(r);for(;e&&e!==t;){const i=Object.getOwnPropertyDescriptors(e);for(const[n,a]of Object.entries(i))n!=="constructor"&&s(n,a,e);e=Object.getPrototypeOf(e)}}const z=Object.freeze({loading:!1,error:null,errorCode:null}),w=["async","subscribeAsync"],A=new Set(["onInit","onSet","onDispose"]);class b{_state;_initialState;_disposed=!1;_initialized=!1;_listeners=new Set;_abortController=null;_cleanups=null;_subscriptionCleanups=null;_eventBus=null;_revision=0;_stateTracking=null;_sourceTracking=null;_trackedSources=new Map;_asyncStates=new Map;_asyncSnapshots=new Map;_asyncListeners=new Set;_asyncProxy=null;_activeOps=null;static GHOST_TIMEOUT=3e3;constructor(t){this._state=Object.freeze({...t}),this._initialState=this._state,this._guardReservedKeys()}get state(){return this._state}get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}get events(){return this._eventBus||(this._eventBus=new m.EventBus),this._eventBus}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this._trackSubscribables(),this._installStateProxy(),this._memoizeGetters(),this._wrapMethods(),this.onInit?.()}set(t){if(this._disposed)return;if(d&&this._stateTracking){console.error("[mvc-kit] set() called inside a getter. Getters must be pure — they read state and return a value. They must never call set(), which would cause an infinite render loop. Move this logic to an action method.");return}const s=typeof t=="function"?t(this._state):t;if(!Object.keys(s).some(o=>s[o]!==this._state[o]))return;const n=this._state,a=Object.freeze({...n,...s});this._state=a,this._revision++,this.onSet?.(n,a);for(const o of this._listeners)o(a,n)}emit(t,s){(this._eventBus?.disposed??this._disposed)||this.events.emit(t,s)}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}dispose(){if(!this._disposed){if(this._disposed=!0,this._teardownSubscriptions(),this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this._eventBus?.dispose(),this.onDispose?.(),this._listeners.clear()}}reset(t){if(!this._disposed){this._abortController?.abort(),this._abortController=null,this._teardownSubscriptions(),this._state=t?Object.freeze({...t}):this._initialState,this._revision++,this._asyncStates.clear(),this._asyncSnapshots.clear(),this._notifyAsync(),this._trackSubscribables();for(const s of this._listeners)s(this._state,this._state);return this.onInit?.()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this._subscriptionCleanups||(this._subscriptionCleanups=[]),this._subscriptionCleanups.push(e),e}get async(){if(!this._asyncProxy){const t=this;this._asyncProxy=new Proxy({},{get(s,e){return t._asyncSnapshots.get(e)??z},has(s,e){return t._asyncSnapshots.has(e)},ownKeys(){return Array.from(t._asyncSnapshots.keys())},getOwnPropertyDescriptor(s,e){if(t._asyncSnapshots.has(e))return{configurable:!0,enumerable:!0,value:t._asyncSnapshots.get(e)}}})}return this._asyncProxy}subscribeAsync(t){return this._disposed?()=>{}:(this._asyncListeners.add(t),()=>{this._asyncListeners.delete(t)})}_notifyAsync(){for(const t of this._asyncListeners)t()}_teardownSubscriptions(){for(const t of this._trackedSources.values())t.unsubscribe();if(this._trackedSources.clear(),this._subscriptionCleanups){for(const t of this._subscriptionCleanups)t();this._subscriptionCleanups=null}}_guardReservedKeys(){g(this,b.prototype,t=>{if(w.includes(t))throw new Error(`[mvc-kit] "${t}" is a reserved property on ViewModel and cannot be overridden.`)})}_wrapMethods(){for(const i of w)if(Object.getOwnPropertyDescriptor(this,i)?.value!==void 0)throw new Error(`[mvc-kit] "${i}" is a reserved property on ViewModel and cannot be overridden.`);const t=this,s=new Set,e=[];d&&(this._activeOps=new Map),g(this,b.prototype,(i,n)=>{if(n.get||n.set||typeof n.value!="function"||i.startsWith("_")||A.has(i)||s.has(i))return;s.add(i);const a=n.value;let o=!1;const l=function(...h){if(t._disposed){d&&console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);return}d&&!t._initialized&&console.warn(`[mvc-kit] "${i}" called before init(). Async tracking is active only after init().`);let u;try{u=a.apply(t,h)}catch(_){throw _}if(!u||typeof u.then!="function")return o||(o=!0,t._asyncStates.delete(i),t._asyncSnapshots.delete(i),t[i]=a.bind(t)),u;let c=t._asyncStates.get(i);return c||(c={loading:!1,error:null,errorCode:null,count:0},t._asyncStates.set(i,c)),c.count++,c.loading=!0,c.error=null,c.errorCode=null,t._asyncSnapshots.set(i,Object.freeze({loading:!0,error:null,errorCode:null})),t._notifyAsync(),d&&t._activeOps&&t._activeOps.set(i,(t._activeOps.get(i)??0)+1),u.then(_=>{if(t._disposed)return _;if(c.count--,c.loading=c.count>0,t._asyncSnapshots.set(i,Object.freeze({loading:c.loading,error:c.error,errorCode:c.errorCode})),t._notifyAsync(),d&&t._activeOps){const f=(t._activeOps.get(i)??1)-1;f<=0?t._activeOps.delete(i):t._activeOps.set(i,f)}return _},_=>{if(y(_)){if(t._disposed||(c.count--,c.loading=c.count>0,t._asyncSnapshots.set(i,Object.freeze({loading:c.loading,error:c.error,errorCode:c.errorCode})),t._notifyAsync()),d&&t._activeOps){const p=(t._activeOps.get(i)??1)-1;p<=0?t._activeOps.delete(i):t._activeOps.set(i,p)}return}if(t._disposed)return;c.count--,c.loading=c.count>0;const f=T(_);if(c.error=f.message,c.errorCode=f.code,t._asyncSnapshots.set(i,Object.freeze({loading:c.loading,error:f.message,errorCode:f.code})),t._notifyAsync(),d&&t._activeOps){const p=(t._activeOps.get(i)??1)-1;p<=0?t._activeOps.delete(i):t._activeOps.set(i,p)}throw _})};e.push(i),t[i]=l}),e.length>0&&this.addCleanup(()=>{const i=d&&t._activeOps?new Map(t._activeOps):null;for(const n of e)d?t[n]=()=>{console.warn(`[mvc-kit] "${n}" called after dispose — ignored.`)}:t[n]=()=>{};t._asyncListeners.clear(),t._asyncStates.clear(),t._asyncSnapshots.clear(),d&&i&&i.size>0&&t._scheduleGhostCheck(i)})}_scheduleGhostCheck(t){d&&setTimeout(()=>{for(const[s,e]of t)console.warn(`[mvc-kit] Ghost async operation detected: "${s}" had ${e} pending call(s) when the ViewModel was disposed. Consider using disposeSignal to cancel in-flight work.`)},this.constructor.GHOST_TIMEOUT)}_installStateProxy(){const t=new Proxy({},{get:(s,e)=>(this._stateTracking?.add(e),this._state[e]),ownKeys:()=>Reflect.ownKeys(this._state),getOwnPropertyDescriptor:(s,e)=>Reflect.getOwnPropertyDescriptor(this._state,e),set:()=>{throw new Error("Cannot mutate state directly. Use set() instead.")},has:(s,e)=>e in this._state});Object.defineProperty(this,"state",{get:()=>this._stateTracking?t:this._state,configurable:!0,enumerable:!0})}_trackSubscribables(){for(const t of Object.getOwnPropertyNames(this)){const s=this[t];if(!O(s))continue;const e={source:s,revision:0,unsubscribe:s.subscribe(()=>{if(!this._disposed){e.revision++,this._revision++,this._state=Object.freeze({...this._state});for(const i of this._listeners)i(this._state,this._state)}})};this._trackedSources.set(t,e),Object.defineProperty(this,t,{get:()=>(this._sourceTracking?.set(t,e),s),configurable:!0,enumerable:!1})}}_memoizeGetters(){const t=new Set;g(this,b.prototype,(s,e)=>{!e.get||t.has(s)||(t.add(s),this._wrapGetter(s,e.get))})}_wrapGetter(t,s){let e,i=-1,n,a,o;Object.defineProperty(this,t,{get:()=>{if(this._disposed||i===this._revision)return e;if(n&&a){let c=!0;for(const[_,f]of a)if(this._state[_]!==f){c=!1;break}if(c&&o)for(const[_,f]of o){const p=this._trackedSources.get(_);if(p&&p.revision!==f){c=!1;break}}if(c)return i=this._revision,e}const l=this._stateTracking,h=this._sourceTracking;this._stateTracking=new Set,this._sourceTracking=new Map;try{e=s.call(this)}catch(c){throw this._stateTracking=l,this._sourceTracking=h,c}n=this._stateTracking;const u=this._sourceTracking;if(this._stateTracking=l,this._sourceTracking=h,l)for(const c of n)l.add(c);if(h)for(const[c,_]of u)h.set(c,_);a=new Map;for(const c of n)a.set(c,this._state[c]);o=new Map;for(const[c,_]of u)o.set(c,_.revision);return i=this._revision,e},configurable:!0,enumerable:!0})}}class x{_state;_committed;_disposed=!1;_initialized=!1;_listeners=new Set;_abortController=null;_cleanups=null;constructor(t){const s=Object.freeze({...t});this._state=s,this._committed=s}get state(){return this._state}get committed(){return this._committed}get dirty(){return!this.shallowEqual(this._state,this._committed)}get errors(){return this.validate(this._state)}get valid(){return Object.keys(this.errors).length===0}get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}set(t){if(this._disposed)throw new Error("Cannot set state on disposed Model");const s=typeof t=="function"?t(this._state):t;if(!Object.keys(s).some(o=>s[o]!==this._state[o]))return;const n=this._state,a=Object.freeze({...n,...s});this._state=a,this.onSet?.(n,a);for(const o of this._listeners)o(a,n)}commit(){if(this._disposed)throw new Error("Cannot commit on disposed Model");this._committed=this._state}rollback(){if(this._disposed)throw new Error("Cannot rollback on disposed Model");if(this.shallowEqual(this._state,this._committed))return;const t=this._state;this._state=this._committed,this.onSet?.(t,this._state);for(const s of this._listeners)s(this._state,t)}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}dispose(){if(!this._disposed){if(this._disposed=!0,this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.(),this._listeners.clear()}}validate(t){return{}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this.addCleanup(e),e}shallowEqual(t,s){const e=Object.keys(t),i=Object.keys(s);if(e.length!==i.length)return!1;for(const n of e)if(t[n]!==s[n])return!1;return!0}}const M=typeof __MVC_KIT_DEV__<"u"&&__MVC_KIT_DEV__;class k{static MAX_SIZE=0;static TTL=0;_items=[];_disposed=!1;_listeners=new Set;_index=new Map;_abortController=null;_cleanups=null;_timestamps=null;_evictionTimer=null;constructor(t=[]){let s=[...t];if(this._ttl>0){this._timestamps=new Map;const e=Date.now();for(const i of s)this._timestamps.set(i.id,e)}if(this._maxSize>0&&s.length>this._maxSize){const e=s.length-this._maxSize,i=s.slice(0,e);s=s.slice(e);for(const n of i)this._timestamps?.delete(n.id)}this._items=Object.freeze(s),this.rebuildIndex(),this._scheduleEvictionTimer()}get state(){return this._items}get items(){return this._items}get length(){return this._items.length}get disposed(){return this._disposed}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}get _maxSize(){return this.constructor.MAX_SIZE}get _ttl(){return this.constructor.TTL}add(...t){if(this._disposed)throw new Error("Cannot add to disposed Collection");if(t.length===0)return;const s=new Set,e=[];for(const a of t)!this._index.has(a.id)&&!s.has(a.id)&&(e.push(a),s.add(a.id));if(e.length===0)return;const i=this._items;let n=[...i,...e];for(const a of e)this._index.set(a.id,a);if(this._timestamps){const a=Date.now();for(const o of e)this._timestamps.set(o.id,a)}this._maxSize>0&&n.length>this._maxSize&&(n=this._evictForCapacity(n)),this._items=Object.freeze(n),this.notify(i),this._scheduleEvictionTimer()}upsert(...t){if(this._disposed)throw new Error("Cannot upsert on disposed Collection");if(t.length===0)return;const s=new Map;for(const l of t)s.set(l.id,l);const e=this._items;let i=!1;const n=new Set,a=[];for(const l of e)if(s.has(l.id)){const h=s.get(l.id);h!==l&&(i=!0),a.push(h),n.add(l.id)}else a.push(l);for(const[l,h]of s)n.has(l)||(a.push(h),i=!0);if(!i)return;if(this._timestamps){const l=Date.now();for(const[h]of s)this._timestamps.set(h,l)}for(const[l,h]of s)this._index.set(l,h);let o=a;this._maxSize>0&&o.length>this._maxSize&&(o=this._evictForCapacity(o)),this._items=Object.freeze(o),this.notify(e),this._scheduleEvictionTimer()}remove(...t){if(this._disposed)throw new Error("Cannot remove from disposed Collection");if(t.length===0)return;const s=new Set(t),e=this._items.filter(n=>!s.has(n.id));if(e.length===this._items.length)return;const i=this._items;this._items=Object.freeze(e);for(const n of t)this._index.delete(n),this._timestamps?.delete(n);this.notify(i),this._scheduleEvictionTimer()}update(t,s){if(this._disposed)throw new Error("Cannot update disposed Collection");const e=this._items.findIndex(u=>u.id===t);if(e===-1)return;const i=this._items[e],n={...i,...s,id:t};if(!Object.keys(s).some(u=>s[u]!==i[u]))return;const l=this._items,h=[...l];h[e]=n,this._items=Object.freeze(h),this._index.set(t,n),this.notify(l)}reset(t){if(this._disposed)throw new Error("Cannot reset disposed Collection");const s=this._items;if(this._timestamps){this._timestamps.clear();const i=Date.now();for(const n of t)this._timestamps.set(n.id,i)}let e=[...t];this._maxSize>0&&e.length>this._maxSize&&(e=this._evictForCapacity(e)),this._items=Object.freeze(e),this.rebuildIndex(),this.notify(s),this._scheduleEvictionTimer()}clear(){if(this._disposed)throw new Error("Cannot clear disposed Collection");if(this._items.length===0)return;const t=this._items;this._items=Object.freeze([]),this._index.clear(),this._timestamps?.clear(),this._clearEvictionTimer(),this.notify(t)}optimistic(t){if(this._disposed)throw new Error("Cannot perform optimistic update on disposed Collection");const s=this._items,e=this._timestamps?new Map(this._timestamps):null;t();let i=!1;return()=>{if(i||this._disposed)return;i=!0;const n=this._items;this._items=s,e&&(this._timestamps=e),this.rebuildIndex(),this.notify(n),this._scheduleEvictionTimer()}}get(t){return this._index.get(t)}has(t){return this._index.has(t)}find(t){return this._items.find(t)}filter(t){return this._items.filter(t)}sorted(t){return[...this._items].sort(t)}map(t){return this._items.map(t)}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}dispose(){if(!this._disposed){if(this._disposed=!0,this._clearEvictionTimer(),this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.(),this._listeners.clear(),this._index.clear(),this._timestamps?.clear()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}notify(t){for(const s of this._listeners)s(this._items,t)}rebuildIndex(){this._index.clear();for(const t of this._items)this._index.set(t.id,t)}_evictForCapacity(t){const s=t.length-this._maxSize;if(s<=0)return t;const e=t.slice(0,s),i=this._applyOnEvict(e,"capacity");if(i===!1||i.length===0)return t;const n=new Set(i.map(o=>o.id)),a=t.filter(o=>!n.has(o.id));for(const o of i)this._index.delete(o.id),this._timestamps?.delete(o.id);return a}_applyOnEvict(t,s){if(!this.onEvict)return t;const e=this.onEvict(t,s);if(e===!1){if(M&&s==="capacity"&&this._maxSize>0){const i=this._items.length+t.length;i>this._maxSize*2&&console.warn(`[mvc-kit] Collection exceeded 2x MAX_SIZE (${i}/${this._maxSize}). onEvict is vetoing eviction — this may cause unbounded growth.`)}return!1}if(Array.isArray(e)){const i=new Set(t.map(n=>n.id));return e.filter(n=>i.has(n.id))}return t}_sweepExpired(){if(this._disposed||!this._timestamps||this._ttl<=0)return;const t=Date.now(),s=this._ttl,e=[];for(const o of this._items){const l=this._timestamps.get(o.id);l!==void 0&&t-l>=s&&e.push(o)}if(e.length===0){this._scheduleEvictionTimer();return}const i=this._applyOnEvict(e,"ttl");if(i===!1){this._scheduleEvictionTimer();return}if(i.length===0){this._scheduleEvictionTimer();return}const n=new Set(i.map(o=>o.id)),a=this._items;this._items=Object.freeze(a.filter(o=>!n.has(o.id)));for(const o of i)this._index.delete(o.id),this._timestamps.delete(o.id);this.notify(a),this._scheduleEvictionTimer()}_scheduleEvictionTimer(){if(this._clearEvictionTimer(),this._disposed||!this._timestamps||this._ttl<=0||this._timestamps.size===0)return;const t=Date.now(),s=this._ttl;let e=1/0;for(const n of this._timestamps.values())n<e&&(e=n);const i=Math.max(0,e+s-t);this._evictionTimer=setTimeout(()=>this._sweepExpired(),i)}_clearEvictionTimer(){this._evictionTimer!==null&&(clearTimeout(this._evictionTimer),this._evictionTimer=null)}}class j{_disposed=!1;_initialized=!1;_abortController=null;_cleanups=null;get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}dispose(){if(!this._disposed){if(this._disposed=!0,this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this.addCleanup(e),e}}class D{_disposed=!1;_initialized=!1;_abortController=null;_cleanups=null;get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}dispose(){if(!this._disposed){if(this._disposed=!0,this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}}const S=typeof __MVC_KIT_DEV__<"u"&&__MVC_KIT_DEV__,I=Object.freeze({connected:!1,reconnecting:!1,attempt:0,error:null});class P{static RECONNECT_BASE=1e3;static RECONNECT_MAX=3e4;static RECONNECT_FACTOR=2;static MAX_ATTEMPTS=1/0;_status=I;_connState=0;_disposed=!1;_initialized=!1;_listeners=new Set;_handlers=new Map;_abortController=null;_connectAbort=null;_reconnectTimer=null;_cleanups=null;get state(){return this._status}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}dispose(){if(!this._disposed){this._disposed=!0,this._connState=4,this._reconnectTimer!==null&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._connectAbort?.abort(),this._connectAbort=null,this._abortController?.abort();try{this.close()}catch{}if(this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.(),this._listeners.clear(),this._handlers.clear()}}connect(){if(this._disposed){S&&console.warn("[mvc-kit] connect() called after dispose — ignored.");return}S&&!this._initialized&&console.warn("[mvc-kit] connect() called before init()."),!(this._connState===1||this._connState===2)&&(this._reconnectTimer!==null&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._attemptConnect(0))}disconnect(){if(!this._disposed){if(this._reconnectTimer!==null&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._connectAbort?.abort(),this._connectAbort=null,this._connState===2||this._connState===1){this._connState=0;try{this.close()}catch{}}else this._connState=0;this._setStatus({connected:!1,reconnecting:!1,attempt:0,error:null})}}receive(t,s){if(this._disposed){S&&console.warn(`[mvc-kit] receive("${String(t)}") called after dispose — ignored.`);return}const e=this._handlers.get(t);if(e)for(const i of e)i(s)}disconnected(){this._disposed||this._connState!==2&&this._connState!==1||(this._connectAbort?.abort(),this._connectAbort=null,this._connState=3,this._scheduleReconnect(1))}on(t,s){if(this._disposed)return()=>{};let e=this._handlers.get(t);return e||(e=new Set,this._handlers.set(t,e)),e.add(s),()=>{e.delete(s)}}once(t,s){const e=this.on(t,i=>{e(),s(i)});return e}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this.addCleanup(e),e}_calculateDelay(t){const s=this.constructor,e=Math.min(s.RECONNECT_BASE*Math.pow(s.RECONNECT_FACTOR,t),s.RECONNECT_MAX);return Math.random()*e}_setStatus(t){const s=this._status;if(!(s.connected===t.connected&&s.reconnecting===t.reconnecting&&s.attempt===t.attempt&&s.error===t.error)){this._status=Object.freeze(t);for(const e of this._listeners)e(this._status,s)}}_attemptConnect(t){if(this._disposed)return;this._connState=1,this._connectAbort?.abort(),this._connectAbort=new AbortController;const s=this._abortController?AbortSignal.any([this._abortController.signal,this._connectAbort.signal]):this._connectAbort.signal;this._setStatus({connected:!1,reconnecting:t>0,attempt:t,error:null});let e;try{e=this.open(s)}catch(i){this._onOpenFailed(t,i);return}e&&typeof e.then=="function"?e.then(()=>this._onOpenSucceeded(),i=>this._onOpenFailed(t,i)):this._onOpenSucceeded()}_onOpenSucceeded(){this._disposed||this._connState===1&&(this._connState=2,this._setStatus({connected:!0,reconnecting:!1,attempt:0,error:null}))}_onOpenFailed(t,s){this._disposed||this._connState!==0&&(this._connectAbort?.abort(),this._connectAbort=null,this._connState=3,this._scheduleReconnect(t+1,s))}_scheduleReconnect(t,s){const e=this.constructor;if(t>e.MAX_ATTEMPTS){this._connState=0,this._setStatus({connected:!1,reconnecting:!1,attempt:t,error:s instanceof Error?s.message:"Max reconnection attempts reached"});return}const i=s instanceof Error?s.message:s?String(s):null;this._setStatus({connected:!1,reconnecting:!0,attempt:t,error:i});const n=this._calculateDelay(t-1);this._reconnectTimer=setTimeout(()=>{this._reconnectTimer=null,this._attemptConnect(t)},n)}}exports.EventBus=m.EventBus;exports.hasSingleton=m.hasSingleton;exports.singleton=m.singleton;exports.teardown=m.teardown;exports.teardownAll=m.teardownAll;exports.Channel=P;exports.Collection=k;exports.Controller=j;exports.HttpError=C;exports.Model=x;exports.Service=D;exports.ViewModel=b;exports.classifyError=T;exports.isAbortError=y;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("./singleton-L-u2W_lX.cjs");class x extends Error{constructor(t,s){super(s??`HTTP ${t}`),this.status=t,this.name="HttpError"}}function w(o){return o instanceof Error&&o.name==="AbortError"}function T(o){return o===401?"unauthorized":o===403?"forbidden":o===404?"not_found":o===422?"validation":o===429?"rate_limited":o>=500?"server_error":"unknown"}function A(o){return typeof o=="object"&&o!==null&&typeof o.status=="number"&&typeof o.statusText=="string"&&!(o instanceof Error)}function C(o){return o instanceof Error&&o.name==="AbortError"?{code:"abort",message:"Request was aborted",original:o}:o instanceof x?{code:T(o.status),message:o.message,status:o.status,original:o}:A(o)?{code:T(o.status),message:o.statusText||`HTTP ${o.status}`,status:o.status,original:o}:o instanceof TypeError&&o.message.toLowerCase().includes("fetch")?{code:"network",message:o.message,original:o}:o instanceof Error&&o.name==="TimeoutError"?{code:"timeout",message:o.message,original:o}:o instanceof Error?{code:"unknown",message:o.message,original:o}:{code:"unknown",message:String(o),original:o}}const p=typeof __MVC_KIT_DEV__<"u"&&__MVC_KIT_DEV__;function M(o){return o!==null&&typeof o=="object"&&typeof o.subscribe=="function"}function b(o,t,s){let e=Object.getPrototypeOf(o);for(;e&&e!==t;){const i=Object.getOwnPropertyDescriptors(e);for(const[n,c]of Object.entries(i))n!=="constructor"&&s(n,c,e);e=Object.getPrototypeOf(e)}}const j=Object.freeze({loading:!1,error:null,errorCode:null}),E=["async","subscribeAsync"],D=new Set(["onInit","onSet","onDispose"]);class v{_state;_initialState;_disposed=!1;_initialized=!1;_listeners=new Set;_abortController=null;_cleanups=null;_subscriptionCleanups=null;_eventBus=null;_revision=0;_stateTracking=null;_sourceTracking=null;_trackedSources=new Map;_asyncStates=new Map;_asyncSnapshots=new Map;_asyncListeners=new Set;_asyncProxy=null;_activeOps=null;static GHOST_TIMEOUT=3e3;constructor(t){this._state=Object.freeze({...t}),this._initialState=this._state,this._guardReservedKeys()}get state(){return this._state}get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}get events(){return this._eventBus||(this._eventBus=new g.EventBus),this._eventBus}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this._trackSubscribables(),this._installStateProxy(),this._memoizeGetters(),this._wrapMethods(),this.onInit?.()}set(t){if(this._disposed)return;if(p&&this._stateTracking){console.error("[mvc-kit] set() called inside a getter. Getters must be pure — they read state and return a value. They must never call set(), which would cause an infinite render loop. Move this logic to an action method.");return}const s=typeof t=="function"?t(this._state):t;if(!Object.keys(s).some(a=>s[a]!==this._state[a]))return;const n=this._state,c=Object.freeze({...n,...s});this._state=c,this._revision++,this.onSet?.(n,c);for(const a of this._listeners)a(c,n)}emit(t,s){(this._eventBus?.disposed??this._disposed)||this.events.emit(t,s)}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}dispose(){if(!this._disposed){if(this._disposed=!0,this._teardownSubscriptions(),this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this._eventBus?.dispose(),this.onDispose?.(),this._listeners.clear()}}reset(t){if(!this._disposed){this._abortController?.abort(),this._abortController=null,this._teardownSubscriptions(),this._state=t?Object.freeze({...t}):this._initialState,this._revision++,this._asyncStates.clear(),this._asyncSnapshots.clear(),this._notifyAsync(),this._trackSubscribables();for(const s of this._listeners)s(this._state,this._state);return this.onInit?.()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this._subscriptionCleanups||(this._subscriptionCleanups=[]),this._subscriptionCleanups.push(e),e}get async(){if(!this._asyncProxy){const t=this;this._asyncProxy=new Proxy({},{get(s,e){return t._asyncSnapshots.get(e)??j},has(s,e){return t._asyncSnapshots.has(e)},ownKeys(){return Array.from(t._asyncSnapshots.keys())},getOwnPropertyDescriptor(s,e){if(t._asyncSnapshots.has(e))return{configurable:!0,enumerable:!0,value:t._asyncSnapshots.get(e)}}})}return this._asyncProxy}subscribeAsync(t){return this._disposed?()=>{}:(this._asyncListeners.add(t),()=>{this._asyncListeners.delete(t)})}_notifyAsync(){for(const t of this._asyncListeners)t()}_teardownSubscriptions(){for(const t of this._trackedSources.values())t.unsubscribe();if(this._trackedSources.clear(),this._subscriptionCleanups){for(const t of this._subscriptionCleanups)t();this._subscriptionCleanups=null}}_guardReservedKeys(){b(this,v.prototype,t=>{if(E.includes(t))throw new Error(`[mvc-kit] "${t}" is a reserved property on ViewModel and cannot be overridden.`)})}_wrapMethods(){for(const i of E)if(Object.getOwnPropertyDescriptor(this,i)?.value!==void 0)throw new Error(`[mvc-kit] "${i}" is a reserved property on ViewModel and cannot be overridden.`);const t=this,s=new Set,e=[];p&&(this._activeOps=new Map),b(this,v.prototype,(i,n)=>{if(n.get||n.set||typeof n.value!="function"||i.startsWith("_")||D.has(i)||s.has(i))return;s.add(i);const c=n.value;let a=!1;const l=function(..._){if(t._disposed){p&&console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);return}p&&!t._initialized&&console.warn(`[mvc-kit] "${i}" called before init(). Async tracking is active only after init().`);let u;try{u=c.apply(t,_)}catch(h){throw h}if(!u||typeof u.then!="function")return a||(a=!0,t._asyncStates.delete(i),t._asyncSnapshots.delete(i),t[i]=c.bind(t)),u;let r=t._asyncStates.get(i);return r||(r={loading:!1,error:null,errorCode:null,count:0},t._asyncStates.set(i,r)),r.count++,r.loading=!0,r.error=null,r.errorCode=null,t._asyncSnapshots.set(i,Object.freeze({loading:!0,error:null,errorCode:null})),t._notifyAsync(),p&&t._activeOps&&t._activeOps.set(i,(t._activeOps.get(i)??0)+1),u.then(h=>{if(t._disposed)return h;if(r.count--,r.loading=r.count>0,t._asyncSnapshots.set(i,Object.freeze({loading:r.loading,error:r.error,errorCode:r.errorCode})),t._notifyAsync(),p&&t._activeOps){const d=(t._activeOps.get(i)??1)-1;d<=0?t._activeOps.delete(i):t._activeOps.set(i,d)}return h},h=>{if(w(h)){if(t._disposed||(r.count--,r.loading=r.count>0,t._asyncSnapshots.set(i,Object.freeze({loading:r.loading,error:r.error,errorCode:r.errorCode})),t._notifyAsync()),p&&t._activeOps){const f=(t._activeOps.get(i)??1)-1;f<=0?t._activeOps.delete(i):t._activeOps.set(i,f)}return}if(t._disposed)return;r.count--,r.loading=r.count>0;const d=C(h);if(r.error=d.message,r.errorCode=d.code,t._asyncSnapshots.set(i,Object.freeze({loading:r.loading,error:d.message,errorCode:d.code})),t._notifyAsync(),p&&t._activeOps){const f=(t._activeOps.get(i)??1)-1;f<=0?t._activeOps.delete(i):t._activeOps.set(i,f)}throw h})};e.push(i),t[i]=l}),e.length>0&&this.addCleanup(()=>{const i=p&&t._activeOps?new Map(t._activeOps):null;for(const n of e)p?t[n]=()=>{console.warn(`[mvc-kit] "${n}" called after dispose — ignored.`)}:t[n]=()=>{};t._asyncListeners.clear(),t._asyncStates.clear(),t._asyncSnapshots.clear(),p&&i&&i.size>0&&t._scheduleGhostCheck(i)})}_scheduleGhostCheck(t){p&&setTimeout(()=>{for(const[s,e]of t)console.warn(`[mvc-kit] Ghost async operation detected: "${s}" had ${e} pending call(s) when the ViewModel was disposed. Consider using disposeSignal to cancel in-flight work.`)},this.constructor.GHOST_TIMEOUT)}_installStateProxy(){const t=new Proxy({},{get:(s,e)=>(this._stateTracking?.add(e),this._state[e]),ownKeys:()=>Reflect.ownKeys(this._state),getOwnPropertyDescriptor:(s,e)=>Reflect.getOwnPropertyDescriptor(this._state,e),set:()=>{throw new Error("Cannot mutate state directly. Use set() instead.")},has:(s,e)=>e in this._state});Object.defineProperty(this,"state",{get:()=>this._stateTracking?t:this._state,configurable:!0,enumerable:!0})}_trackSubscribables(){for(const t of Object.getOwnPropertyNames(this)){const s=this[t];if(!M(s))continue;const e={source:s,revision:0,unsubscribe:s.subscribe(()=>{if(!this._disposed){e.revision++,this._revision++,this._state=Object.freeze({...this._state});for(const i of this._listeners)i(this._state,this._state)}})};this._trackedSources.set(t,e),Object.defineProperty(this,t,{get:()=>(this._sourceTracking?.set(t,e),s),configurable:!0,enumerable:!1})}}_memoizeGetters(){const t=new Set;b(this,v.prototype,(s,e)=>{!e.get||t.has(s)||(t.add(s),this._wrapGetter(s,e.get))})}_wrapGetter(t,s){let e,i=-1,n,c,a;Object.defineProperty(this,t,{get:()=>{if(this._disposed||i===this._revision)return e;if(n&&c){let r=!0;for(const[h,d]of c)if(this._state[h]!==d){r=!1;break}if(r&&a)for(const[h,d]of a){const f=this._trackedSources.get(h);if(f&&f.revision!==d){r=!1;break}}if(r)return i=this._revision,e}const l=this._stateTracking,_=this._sourceTracking;this._stateTracking=new Set,this._sourceTracking=new Map;try{e=s.call(this)}catch(r){throw this._stateTracking=l,this._sourceTracking=_,r}n=this._stateTracking;const u=this._sourceTracking;if(this._stateTracking=l,this._sourceTracking=_,l)for(const r of n)l.add(r);if(_)for(const[r,h]of u)_.set(r,h);c=new Map;for(const r of n)c.set(r,this._state[r]);a=new Map;for(const[r,h]of u)a.set(r,h.revision);return i=this._revision,e},configurable:!0,enumerable:!0})}}class I{_state;_committed;_disposed=!1;_initialized=!1;_listeners=new Set;_abortController=null;_cleanups=null;constructor(t){const s=Object.freeze({...t});this._state=s,this._committed=s}get state(){return this._state}get committed(){return this._committed}get dirty(){return!this.shallowEqual(this._state,this._committed)}get errors(){return this.validate(this._state)}get valid(){return Object.keys(this.errors).length===0}get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}set(t){if(this._disposed)throw new Error("Cannot set state on disposed Model");const s=typeof t=="function"?t(this._state):t;if(!Object.keys(s).some(a=>s[a]!==this._state[a]))return;const n=this._state,c=Object.freeze({...n,...s});this._state=c,this.onSet?.(n,c);for(const a of this._listeners)a(c,n)}commit(){if(this._disposed)throw new Error("Cannot commit on disposed Model");this._committed=this._state}rollback(){if(this._disposed)throw new Error("Cannot rollback on disposed Model");if(this.shallowEqual(this._state,this._committed))return;const t=this._state;this._state=this._committed,this.onSet?.(t,this._state);for(const s of this._listeners)s(this._state,t)}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}dispose(){if(!this._disposed){if(this._disposed=!0,this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.(),this._listeners.clear()}}validate(t){return{}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this.addCleanup(e),e}shallowEqual(t,s){const e=Object.keys(t),i=Object.keys(s);if(e.length!==i.length)return!1;for(const n of e)if(t[n]!==s[n])return!1;return!0}}const P=typeof __MVC_KIT_DEV__<"u"&&__MVC_KIT_DEV__;class z{static MAX_SIZE=0;static TTL=0;_items=[];_disposed=!1;_listeners=new Set;_index=new Map;_abortController=null;_cleanups=null;_timestamps=null;_evictionTimer=null;constructor(t=[]){let s=[...t];if(this._ttl>0){this._timestamps=new Map;const e=Date.now();for(const i of s)this._timestamps.set(i.id,e)}if(this._maxSize>0&&s.length>this._maxSize){const e=s.length-this._maxSize,i=s.slice(0,e);s=s.slice(e);for(const n of i)this._timestamps?.delete(n.id)}this._items=Object.freeze(s),this.rebuildIndex(),this._scheduleEvictionTimer()}get state(){return this._items}get items(){return this._items}get length(){return this._items.length}get disposed(){return this._disposed}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}get _maxSize(){return this.constructor.MAX_SIZE}get _ttl(){return this.constructor.TTL}add(...t){if(this._disposed)throw new Error("Cannot add to disposed Collection");if(t.length===0)return;const s=new Set,e=[];for(const c of t)!this._index.has(c.id)&&!s.has(c.id)&&(e.push(c),s.add(c.id));if(e.length===0)return;const i=this._items;let n=[...i,...e];for(const c of e)this._index.set(c.id,c);if(this._timestamps){const c=Date.now();for(const a of e)this._timestamps.set(a.id,c)}this._maxSize>0&&n.length>this._maxSize&&(n=this._evictForCapacity(n)),this._items=Object.freeze(n),this.notify(i),this._scheduleEvictionTimer()}upsert(...t){if(this._disposed)throw new Error("Cannot upsert on disposed Collection");if(t.length===0)return;const s=new Map;for(const l of t)s.set(l.id,l);const e=this._items;let i=!1;const n=new Set,c=[];for(const l of e)if(s.has(l.id)){const _=s.get(l.id);_!==l&&(i=!0),c.push(_),n.add(l.id)}else c.push(l);for(const[l,_]of s)n.has(l)||(c.push(_),i=!0);if(!i)return;if(this._timestamps){const l=Date.now();for(const[_]of s)this._timestamps.set(_,l)}for(const[l,_]of s)this._index.set(l,_);let a=c;this._maxSize>0&&a.length>this._maxSize&&(a=this._evictForCapacity(a)),this._items=Object.freeze(a),this.notify(e),this._scheduleEvictionTimer()}remove(...t){if(this._disposed)throw new Error("Cannot remove from disposed Collection");if(t.length===0)return;const s=new Set(t),e=this._items.filter(n=>!s.has(n.id));if(e.length===this._items.length)return;const i=this._items;this._items=Object.freeze(e);for(const n of t)this._index.delete(n),this._timestamps?.delete(n);this.notify(i),this._scheduleEvictionTimer()}update(t,s){if(this._disposed)throw new Error("Cannot update disposed Collection");const e=this._items.findIndex(u=>u.id===t);if(e===-1)return;const i=this._items[e],n={...i,...s,id:t};if(!Object.keys(s).some(u=>s[u]!==i[u]))return;const l=this._items,_=[...l];_[e]=n,this._items=Object.freeze(_),this._index.set(t,n),this.notify(l)}reset(t){if(this._disposed)throw new Error("Cannot reset disposed Collection");const s=this._items;if(this._timestamps){this._timestamps.clear();const i=Date.now();for(const n of t)this._timestamps.set(n.id,i)}let e=[...t];this._maxSize>0&&e.length>this._maxSize&&(e=this._evictForCapacity(e)),this._items=Object.freeze(e),this.rebuildIndex(),this.notify(s),this._scheduleEvictionTimer()}clear(){if(this._disposed)throw new Error("Cannot clear disposed Collection");if(this._items.length===0)return;const t=this._items;this._items=Object.freeze([]),this._index.clear(),this._timestamps?.clear(),this._clearEvictionTimer(),this.notify(t)}optimistic(t){if(this._disposed)throw new Error("Cannot perform optimistic update on disposed Collection");const s=this._items,e=this._timestamps?new Map(this._timestamps):null;t();let i=!1;return()=>{if(i||this._disposed)return;i=!0;const n=this._items;this._items=s,e&&(this._timestamps=e),this.rebuildIndex(),this.notify(n),this._scheduleEvictionTimer()}}get(t){return this._index.get(t)}has(t){return this._index.has(t)}find(t){return this._items.find(t)}filter(t){return this._items.filter(t)}sorted(t){return[...this._items].sort(t)}map(t){return this._items.map(t)}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}dispose(){if(!this._disposed){if(this._disposed=!0,this._clearEvictionTimer(),this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.(),this._listeners.clear(),this._index.clear(),this._timestamps?.clear()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}notify(t){for(const s of this._listeners)s(this._items,t)}rebuildIndex(){this._index.clear();for(const t of this._items)this._index.set(t.id,t)}_evictForCapacity(t){const s=t.length-this._maxSize;if(s<=0)return t;const e=t.slice(0,s),i=this._applyOnEvict(e,"capacity");if(i===!1||i.length===0)return t;const n=new Set(i.map(a=>a.id)),c=t.filter(a=>!n.has(a.id));for(const a of i)this._index.delete(a.id),this._timestamps?.delete(a.id);return c}_applyOnEvict(t,s){if(!this.onEvict)return t;const e=this.onEvict(t,s);if(e===!1){if(P&&s==="capacity"&&this._maxSize>0){const i=this._items.length+t.length;i>this._maxSize*2&&console.warn(`[mvc-kit] Collection exceeded 2x MAX_SIZE (${i}/${this._maxSize}). onEvict is vetoing eviction — this may cause unbounded growth.`)}return!1}if(Array.isArray(e)){const i=new Set(t.map(n=>n.id));return e.filter(n=>i.has(n.id))}return t}_sweepExpired(){if(this._disposed||!this._timestamps||this._ttl<=0)return;const t=Date.now(),s=this._ttl,e=[];for(const a of this._items){const l=this._timestamps.get(a.id);l!==void 0&&t-l>=s&&e.push(a)}if(e.length===0){this._scheduleEvictionTimer();return}const i=this._applyOnEvict(e,"ttl");if(i===!1){this._scheduleEvictionTimer();return}if(i.length===0){this._scheduleEvictionTimer();return}const n=new Set(i.map(a=>a.id)),c=this._items;this._items=Object.freeze(c.filter(a=>!n.has(a.id)));for(const a of i)this._index.delete(a.id),this._timestamps.delete(a.id);this.notify(c),this._scheduleEvictionTimer()}_scheduleEvictionTimer(){if(this._clearEvictionTimer(),this._disposed||!this._timestamps||this._ttl<=0||this._timestamps.size===0)return;const t=Date.now(),s=this._ttl;let e=1/0;for(const n of this._timestamps.values())n<e&&(e=n);const i=Math.max(0,e+s-t);this._evictionTimer=setTimeout(()=>this._sweepExpired(),i)}_clearEvictionTimer(){this._evictionTimer!==null&&(clearTimeout(this._evictionTimer),this._evictionTimer=null)}}const m=typeof __MVC_KIT_DEV__<"u"&&__MVC_KIT_DEV__,k=Object.freeze({loading:!1,error:null,errorCode:null}),O=["async","subscribeAsync"],R=new Set(["onInit","onDispose"]);class S extends z{_external=null;_initialized=!1;_asyncStates=new Map;_asyncSnapshots=new Map;_asyncListeners=new Set;_asyncProxy=null;_activeOps=null;static GHOST_TIMEOUT=3e3;constructor(t){const s=t!=null&&!Array.isArray(t);if(super(s?[]:t??[]),s&&(this._external=t,m)){const e=this.constructor;(e.MAX_SIZE>0||e.TTL>0)&&console.warn(`[mvc-kit] Resource "${e.name}" has MAX_SIZE or TTL set but uses an injected Collection. Configure these on the Collection instead.`)}this._guardReservedKeys()}get initialized(){return this._initialized}init(){if(!(this._initialized||this.disposed))return this._initialized=!0,this._wrapMethods(),this.onInit?.()}get state(){return this._external?this._external.state:super.state}get items(){return this._external?this._external.items:super.items}get length(){return this._external?this._external.length:super.length}add(...t){this._external?this._external.add(...t):super.add(...t)}upsert(...t){this._external?this._external.upsert(...t):super.upsert(...t)}update(t,s){this._external?this._external.update(t,s):super.update(t,s)}remove(...t){this._external?this._external.remove(...t):super.remove(...t)}reset(t){this._external?this._external.reset(t):super.reset(t)}clear(){this._external?this._external.clear():super.clear()}optimistic(t){return this._external?this._external.optimistic(t):super.optimistic(t)}get(t){return this._external?this._external.get(t):super.get(t)}has(t){return this._external?this._external.has(t):super.has(t)}find(t){return this._external?this._external.find(t):super.find(t)}filter(t){return this._external?this._external.filter(t):super.filter(t)}sorted(t){return this._external?this._external.sorted(t):super.sorted(t)}map(t){return this._external?this._external.map(t):super.map(t)}subscribe(t){return this.disposed?()=>{}:this._external?this._external.subscribe(t):super.subscribe(t)}get async(){if(!this._asyncProxy){const t=this;this._asyncProxy=new Proxy({},{get(s,e){return t._asyncSnapshots.get(e)??k},has(s,e){return t._asyncSnapshots.has(e)},ownKeys(){return Array.from(t._asyncSnapshots.keys())},getOwnPropertyDescriptor(s,e){if(t._asyncSnapshots.has(e))return{configurable:!0,enumerable:!0,value:t._asyncSnapshots.get(e)}}})}return this._asyncProxy}subscribeAsync(t){return this.disposed?()=>{}:(this._asyncListeners.add(t),()=>{this._asyncListeners.delete(t)})}_notifyAsync(){for(const t of this._asyncListeners)t()}_guardReservedKeys(){b(this,S.prototype,t=>{if(O.includes(t))throw new Error(`[mvc-kit] "${t}" is a reserved property on Resource and cannot be overridden.`)})}_wrapMethods(){for(const i of O)if(Object.getOwnPropertyDescriptor(this,i)?.value!==void 0)throw new Error(`[mvc-kit] "${i}" is a reserved property on Resource and cannot be overridden.`);const t=this,s=new Set,e=[];m&&(this._activeOps=new Map),b(this,S.prototype,(i,n)=>{if(n.get||n.set||typeof n.value!="function"||i.startsWith("_")||R.has(i)||s.has(i))return;s.add(i);const c=n.value;let a=!1;const l=function(..._){if(t.disposed){m&&console.warn(`[mvc-kit] "${i}" called after dispose — ignored.`);return}m&&!t._initialized&&console.warn(`[mvc-kit] "${i}" called before init(). Async tracking is active only after init().`);let u;try{u=c.apply(t,_)}catch(h){throw h}if(!u||typeof u.then!="function")return a||(a=!0,t._asyncStates.delete(i),t._asyncSnapshots.delete(i),t[i]=c.bind(t)),u;let r=t._asyncStates.get(i);return r||(r={loading:!1,error:null,errorCode:null,count:0},t._asyncStates.set(i,r)),r.count++,r.loading=!0,r.error=null,r.errorCode=null,t._asyncSnapshots.set(i,Object.freeze({loading:!0,error:null,errorCode:null})),t._notifyAsync(),m&&t._activeOps&&t._activeOps.set(i,(t._activeOps.get(i)??0)+1),u.then(h=>{if(t.disposed)return h;if(r.count--,r.loading=r.count>0,t._asyncSnapshots.set(i,Object.freeze({loading:r.loading,error:r.error,errorCode:r.errorCode})),t._notifyAsync(),m&&t._activeOps){const d=(t._activeOps.get(i)??1)-1;d<=0?t._activeOps.delete(i):t._activeOps.set(i,d)}return h},h=>{if(w(h)){if(t.disposed||(r.count--,r.loading=r.count>0,t._asyncSnapshots.set(i,Object.freeze({loading:r.loading,error:r.error,errorCode:r.errorCode})),t._notifyAsync()),m&&t._activeOps){const f=(t._activeOps.get(i)??1)-1;f<=0?t._activeOps.delete(i):t._activeOps.set(i,f)}return}if(t.disposed)return;r.count--,r.loading=r.count>0;const d=C(h);if(r.error=d.message,r.errorCode=d.code,t._asyncSnapshots.set(i,Object.freeze({loading:r.loading,error:d.message,errorCode:d.code})),t._notifyAsync(),m&&t._activeOps){const f=(t._activeOps.get(i)??1)-1;f<=0?t._activeOps.delete(i):t._activeOps.set(i,f)}throw h})};e.push(i),t[i]=l}),e.length>0&&this.addCleanup(()=>{const i=m&&t._activeOps?new Map(t._activeOps):null;for(const n of e)m?t[n]=()=>{console.warn(`[mvc-kit] "${n}" called after dispose — ignored.`)}:t[n]=()=>{};t._asyncListeners.clear(),t._asyncStates.clear(),t._asyncSnapshots.clear(),m&&i&&i.size>0&&t._scheduleGhostCheck(i)})}_scheduleGhostCheck(t){m&&setTimeout(()=>{for(const[s,e]of t)console.warn(`[mvc-kit] Ghost async operation detected: "${s}" had ${e} pending call(s) when the Resource was disposed. Consider using disposeSignal to cancel in-flight work.`)},this.constructor.GHOST_TIMEOUT)}}class K{_disposed=!1;_initialized=!1;_abortController=null;_cleanups=null;get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}dispose(){if(!this._disposed){if(this._disposed=!0,this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this.addCleanup(e),e}}class ${_disposed=!1;_initialized=!1;_abortController=null;_cleanups=null;get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}dispose(){if(!this._disposed){if(this._disposed=!0,this._abortController?.abort(),this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.()}}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}}const y=typeof __MVC_KIT_DEV__<"u"&&__MVC_KIT_DEV__,V=Object.freeze({connected:!1,reconnecting:!1,attempt:0,error:null});class L{static RECONNECT_BASE=1e3;static RECONNECT_MAX=3e4;static RECONNECT_FACTOR=2;static MAX_ATTEMPTS=1/0;_status=V;_connState=0;_disposed=!1;_initialized=!1;_listeners=new Set;_handlers=new Map;_abortController=null;_connectAbort=null;_reconnectTimer=null;_cleanups=null;get state(){return this._status}subscribe(t){return this._disposed?()=>{}:(this._listeners.add(t),()=>{this._listeners.delete(t)})}get disposed(){return this._disposed}get initialized(){return this._initialized}get disposeSignal(){return this._abortController||(this._abortController=new AbortController),this._abortController.signal}init(){if(!(this._initialized||this._disposed))return this._initialized=!0,this.onInit?.()}dispose(){if(!this._disposed){this._disposed=!0,this._connState=4,this._reconnectTimer!==null&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._connectAbort?.abort(),this._connectAbort=null,this._abortController?.abort();try{this.close()}catch{}if(this._cleanups){for(const t of this._cleanups)t();this._cleanups=null}this.onDispose?.(),this._listeners.clear(),this._handlers.clear()}}connect(){if(this._disposed){y&&console.warn("[mvc-kit] connect() called after dispose — ignored.");return}y&&!this._initialized&&console.warn("[mvc-kit] connect() called before init()."),!(this._connState===1||this._connState===2)&&(this._reconnectTimer!==null&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._attemptConnect(0))}disconnect(){if(!this._disposed){if(this._reconnectTimer!==null&&(clearTimeout(this._reconnectTimer),this._reconnectTimer=null),this._connectAbort?.abort(),this._connectAbort=null,this._connState===2||this._connState===1){this._connState=0;try{this.close()}catch{}}else this._connState=0;this._setStatus({connected:!1,reconnecting:!1,attempt:0,error:null})}}receive(t,s){if(this._disposed){y&&console.warn(`[mvc-kit] receive("${String(t)}") called after dispose — ignored.`);return}const e=this._handlers.get(t);if(e)for(const i of e)i(s)}disconnected(){this._disposed||this._connState!==2&&this._connState!==1||(this._connectAbort?.abort(),this._connectAbort=null,this._connState=3,this._scheduleReconnect(1))}on(t,s){if(this._disposed)return()=>{};let e=this._handlers.get(t);return e||(e=new Set,this._handlers.set(t,e)),e.add(s),()=>{e.delete(s)}}once(t,s){const e=this.on(t,i=>{e(),s(i)});return e}addCleanup(t){this._cleanups||(this._cleanups=[]),this._cleanups.push(t)}subscribeTo(t,s){const e=t.subscribe(s);return this.addCleanup(e),e}_calculateDelay(t){const s=this.constructor,e=Math.min(s.RECONNECT_BASE*Math.pow(s.RECONNECT_FACTOR,t),s.RECONNECT_MAX);return Math.random()*e}_setStatus(t){const s=this._status;if(!(s.connected===t.connected&&s.reconnecting===t.reconnecting&&s.attempt===t.attempt&&s.error===t.error)){this._status=Object.freeze(t);for(const e of this._listeners)e(this._status,s)}}_attemptConnect(t){if(this._disposed)return;this._connState=1,this._connectAbort?.abort(),this._connectAbort=new AbortController;const s=this._abortController?AbortSignal.any([this._abortController.signal,this._connectAbort.signal]):this._connectAbort.signal;this._setStatus({connected:!1,reconnecting:t>0,attempt:t,error:null});let e;try{e=this.open(s)}catch(i){this._onOpenFailed(t,i);return}e&&typeof e.then=="function"?e.then(()=>this._onOpenSucceeded(),i=>this._onOpenFailed(t,i)):this._onOpenSucceeded()}_onOpenSucceeded(){this._disposed||this._connState===1&&(this._connState=2,this._setStatus({connected:!0,reconnecting:!1,attempt:0,error:null}))}_onOpenFailed(t,s){this._disposed||this._connState!==0&&(this._connectAbort?.abort(),this._connectAbort=null,this._connState=3,this._scheduleReconnect(t+1,s))}_scheduleReconnect(t,s){const e=this.constructor;if(t>e.MAX_ATTEMPTS){this._connState=0,this._setStatus({connected:!1,reconnecting:!1,attempt:t,error:s instanceof Error?s.message:"Max reconnection attempts reached"});return}const i=s instanceof Error?s.message:s?String(s):null;this._setStatus({connected:!1,reconnecting:!0,attempt:t,error:i});const n=this._calculateDelay(t-1);this._reconnectTimer=setTimeout(()=>{this._reconnectTimer=null,this._attemptConnect(t)},n)}}exports.EventBus=g.EventBus;exports.hasSingleton=g.hasSingleton;exports.singleton=g.singleton;exports.teardown=g.teardown;exports.teardownAll=g.teardownAll;exports.Channel=L;exports.Collection=z;exports.Controller=K;exports.HttpError=x;exports.Model=I;exports.Resource=S;exports.Service=$;exports.ViewModel=v;exports.classifyError=C;exports.isAbortError=w;
2
2
  //# sourceMappingURL=mvc-kit.cjs.map