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 +48 -2
- package/agent-config/claude-code/agents/mvc-kit-architect.md +7 -5
- package/agent-config/claude-code/skills/guide/anti-patterns.md +40 -1
- package/agent-config/claude-code/skills/guide/api-reference.md +35 -2
- package/agent-config/claude-code/skills/guide/patterns.md +26 -0
- package/agent-config/copilot/copilot-instructions.md +2 -1
- package/agent-config/cursor/cursorrules +2 -1
- package/dist/Resource.d.ts +60 -0
- package/dist/Resource.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +1 -1
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +398 -183
- package/dist/mvc-kit.js.map +1 -1
- package/package.json +1 -1
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.
|
|
28
|
-
5.
|
|
29
|
-
6.
|
|
30
|
-
7.
|
|
31
|
-
8.
|
|
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.
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|