mvc-kit 2.12.0 → 2.12.1
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/agent-config/bin/postinstall.mjs +5 -3
- package/agent-config/bin/setup.mjs +3 -4
- package/agent-config/claude-code/agents/mvc-kit-architect.md +14 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +24 -2
- package/agent-config/lib/install-claude.mjs +10 -33
- package/dist/Model.cjs +9 -1
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.d.ts +1 -1
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +9 -1
- package/dist/Model.js.map +1 -1
- package/dist/ViewModel.cjs +9 -1
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.d.ts +1 -1
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +9 -1
- package/dist/ViewModel.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +3 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +3 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/produceDraft.cjs +105 -0
- package/dist/produceDraft.cjs.map +1 -0
- package/dist/produceDraft.d.ts +19 -0
- package/dist/produceDraft.d.ts.map +1 -0
- package/dist/produceDraft.js +105 -0
- package/dist/produceDraft.js.map +1 -0
- package/package.json +4 -2
- package/src/Channel.md +408 -0
- package/src/Channel.test.ts +957 -0
- package/src/Channel.ts +429 -0
- package/src/Collection.md +533 -0
- package/src/Collection.test.ts +1559 -0
- package/src/Collection.ts +653 -0
- package/src/Controller.md +306 -0
- package/src/Controller.test.ts +380 -0
- package/src/Controller.ts +90 -0
- package/src/EventBus.md +308 -0
- package/src/EventBus.test.ts +295 -0
- package/src/EventBus.ts +110 -0
- package/src/Feed.md +218 -0
- package/src/Feed.test.ts +442 -0
- package/src/Feed.ts +101 -0
- package/src/Model.md +524 -0
- package/src/Model.test.ts +642 -0
- package/src/Model.ts +260 -0
- package/src/Pagination.md +168 -0
- package/src/Pagination.test.ts +244 -0
- package/src/Pagination.ts +92 -0
- package/src/Pending.md +380 -0
- package/src/Pending.test.ts +1719 -0
- package/src/Pending.ts +390 -0
- package/src/PersistentCollection.md +183 -0
- package/src/PersistentCollection.test.ts +649 -0
- package/src/PersistentCollection.ts +375 -0
- package/src/Resource.ViewModel.test.ts +503 -0
- package/src/Resource.md +239 -0
- package/src/Resource.test.ts +786 -0
- package/src/Resource.ts +231 -0
- package/src/Selection.md +155 -0
- package/src/Selection.test.ts +326 -0
- package/src/Selection.ts +117 -0
- package/src/Service.md +440 -0
- package/src/Service.test.ts +241 -0
- package/src/Service.ts +72 -0
- package/src/Sorting.md +170 -0
- package/src/Sorting.test.ts +334 -0
- package/src/Sorting.ts +135 -0
- package/src/Trackable.md +166 -0
- package/src/Trackable.test.ts +236 -0
- package/src/Trackable.ts +129 -0
- package/src/ViewModel.async.test.ts +813 -0
- package/src/ViewModel.derived.test.ts +1583 -0
- package/src/ViewModel.md +1111 -0
- package/src/ViewModel.test.ts +1236 -0
- package/src/ViewModel.ts +800 -0
- package/src/bindPublicMethods.test.ts +126 -0
- package/src/bindPublicMethods.ts +48 -0
- package/src/env.d.ts +5 -0
- package/src/errors.test.ts +155 -0
- package/src/errors.ts +133 -0
- package/src/index.ts +49 -0
- package/src/produceDraft.md +90 -0
- package/src/produceDraft.test.ts +394 -0
- package/src/produceDraft.ts +168 -0
- package/src/react/components/CardList.md +97 -0
- package/src/react/components/CardList.test.tsx +142 -0
- package/src/react/components/CardList.tsx +68 -0
- package/src/react/components/DataTable.md +179 -0
- package/src/react/components/DataTable.test.tsx +599 -0
- package/src/react/components/DataTable.tsx +267 -0
- package/src/react/components/InfiniteScroll.md +116 -0
- package/src/react/components/InfiniteScroll.test.tsx +218 -0
- package/src/react/components/InfiniteScroll.tsx +70 -0
- package/src/react/components/types.ts +90 -0
- package/src/react/derived.test.tsx +261 -0
- package/src/react/guards.ts +24 -0
- package/src/react/index.ts +40 -0
- package/src/react/provider.test.tsx +143 -0
- package/src/react/provider.tsx +55 -0
- package/src/react/strict-mode.test.tsx +266 -0
- package/src/react/types.ts +25 -0
- package/src/react/use-event-bus.md +214 -0
- package/src/react/use-event-bus.test.tsx +168 -0
- package/src/react/use-event-bus.ts +40 -0
- package/src/react/use-instance.md +204 -0
- package/src/react/use-instance.test.tsx +350 -0
- package/src/react/use-instance.ts +60 -0
- package/src/react/use-local.md +457 -0
- package/src/react/use-local.rapid-remount.test.tsx +503 -0
- package/src/react/use-local.test.tsx +692 -0
- package/src/react/use-local.ts +165 -0
- package/src/react/use-model.md +364 -0
- package/src/react/use-model.test.tsx +394 -0
- package/src/react/use-model.ts +161 -0
- package/src/react/use-singleton.md +415 -0
- package/src/react/use-singleton.test.tsx +296 -0
- package/src/react/use-singleton.ts +69 -0
- package/src/react/use-subscribe-only.ts +39 -0
- package/src/react/use-teardown.md +169 -0
- package/src/react/use-teardown.test.tsx +86 -0
- package/src/react/use-teardown.ts +27 -0
- package/src/react-native/NativeCollection.test.ts +250 -0
- package/src/react-native/NativeCollection.ts +138 -0
- package/src/react-native/index.ts +1 -0
- package/src/singleton.md +310 -0
- package/src/singleton.test.ts +204 -0
- package/src/singleton.ts +70 -0
- package/src/types.ts +70 -0
- package/src/walkPrototypeChain.ts +22 -0
- package/src/web/IndexedDBCollection.test.ts +235 -0
- package/src/web/IndexedDBCollection.ts +66 -0
- package/src/web/WebStorageCollection.test.ts +214 -0
- package/src/web/WebStorageCollection.ts +116 -0
- package/src/web/idb.ts +184 -0
- package/src/web/index.ts +2 -0
- package/src/wrapAsyncMethods.ts +249 -0
package/src/Resource.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Resource
|
|
2
|
+
|
|
3
|
+
A **Collection + async tracking toolkit**. Resource extends Collection with lifecycle management (`init`/`dispose`) and automatic async method tracking (`resource.async.methodName.loading/error`). Use it when you need a data cache with built-in loading state for your API calls.
|
|
4
|
+
|
|
5
|
+
## When to Use Resource vs Collection
|
|
6
|
+
|
|
7
|
+
| Use Case | Class |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Shared data cache, no async loading | `Collection` |
|
|
10
|
+
| Shared data cache + API loading + loading/error state | `Resource` |
|
|
11
|
+
| Multiple data sources feeding one cache (Resource + Channel) | `Resource` with external `Collection` |
|
|
12
|
+
|
|
13
|
+
## Creating a Resource
|
|
14
|
+
|
|
15
|
+
Define your own async methods. Use inherited Collection mutations (`reset`, `add`, `upsert`, etc.) to manage data. Resource automatically tracks loading/error state per method.
|
|
16
|
+
|
|
17
|
+
If you already have a typed API client (RPC client, tRPC, GraphQL codegen, OpenAPI generated client), call it directly — no Service wrapper needed:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { Resource } from 'mvc-kit';
|
|
21
|
+
import { apiClient } from '@/api';
|
|
22
|
+
|
|
23
|
+
class UsersResource extends Resource<User> {
|
|
24
|
+
async loadAll() {
|
|
25
|
+
const users = await apiClient.users.list({ signal: this.disposeSignal });
|
|
26
|
+
this.reset(users);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async loadById(id: number) {
|
|
30
|
+
const user = await apiClient.users.get(id, { signal: this.disposeSignal });
|
|
31
|
+
this.upsert(user);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async create(data: CreateUserInput) {
|
|
35
|
+
const user = await apiClient.users.create(data, { signal: this.disposeSignal });
|
|
36
|
+
this.add(user);
|
|
37
|
+
return user;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async remove(id: number) {
|
|
41
|
+
await apiClient.users.delete(id, { signal: this.disposeSignal });
|
|
42
|
+
super.remove(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When using raw `fetch()`, wrap it in a Service to handle `HttpError` and response parsing:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { Resource, singleton, Service, HttpError } from 'mvc-kit';
|
|
51
|
+
|
|
52
|
+
class UserService extends Service {
|
|
53
|
+
async getAll(signal?: AbortSignal): Promise<User[]> {
|
|
54
|
+
const res = await fetch('/api/users', { signal });
|
|
55
|
+
if (!res.ok) throw new HttpError(res.status, res.statusText);
|
|
56
|
+
return res.json();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class UsersResource extends Resource<User> {
|
|
61
|
+
private api = singleton(UserService);
|
|
62
|
+
|
|
63
|
+
async loadAll() {
|
|
64
|
+
const data = await this.api.getAll(this.disposeSignal);
|
|
65
|
+
this.reset(data);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
> **When does a Service earn its place?** When it wraps raw `fetch()` with `HttpError`, transforms responses, composes multiple HTTP calls, handles retries, or manages authentication headers. If your API client already does this, the Service is a pass-through — skip it.
|
|
71
|
+
|
|
72
|
+
## External Collection Injection
|
|
73
|
+
|
|
74
|
+
When multiple sources feed the same data store (e.g., a Resource for REST + a Channel for WebSocket), inject a shared Collection:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
class SharedUsersCollection extends Collection<User> {}
|
|
78
|
+
|
|
79
|
+
class UsersResource extends Resource<User> {
|
|
80
|
+
private api = singleton(UserService);
|
|
81
|
+
|
|
82
|
+
constructor() {
|
|
83
|
+
super(singleton(SharedUsersCollection)); // Inject external collection
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async loadAll() {
|
|
87
|
+
const data = await this.api.getAll(this.disposeSignal);
|
|
88
|
+
this.reset(data); // Mutates the shared collection
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class UsersChannel extends Channel<UserMessages> {
|
|
93
|
+
private users = singleton(SharedUsersCollection);
|
|
94
|
+
|
|
95
|
+
protected onMessage(type: string, data: any) {
|
|
96
|
+
if (type === 'user:updated') {
|
|
97
|
+
this.users.upsert(data); // Same shared collection
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
All Collection methods transparently delegate to the external collection. `resource.items`, `resource.get(id)`, `resource.subscribe()` all operate on the shared data. Resource disposal does **not** dispose the shared collection.
|
|
104
|
+
|
|
105
|
+
> **Note:** `MAX_SIZE` and `TTL` statics on a Resource with external injection are inert — configure these on the shared Collection instead.
|
|
106
|
+
|
|
107
|
+
## Async Tracking
|
|
108
|
+
|
|
109
|
+
After `init()`, all subclass methods are automatically tracked:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const users = singleton(UsersResource);
|
|
113
|
+
await users.init();
|
|
114
|
+
|
|
115
|
+
users.loadAll();
|
|
116
|
+
users.async.loadAll.loading; // true while loading
|
|
117
|
+
users.async.loadAll.error; // error message string, or null
|
|
118
|
+
users.async.loadAll.errorCode; // 'not_found', 'network', etc., or null
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- **Concurrent calls**: `loading` stays `true` until all concurrent calls resolve
|
|
122
|
+
- **AbortError**: silently swallowed (no error state)
|
|
123
|
+
- **Sync methods**: auto-pruned on first call (zero overhead after)
|
|
124
|
+
- **Pre-init calls**: methods work but have no tracking (DEV warning)
|
|
125
|
+
|
|
126
|
+
## Lifecycle
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
class UsersResource extends Resource<User> {
|
|
130
|
+
// Called after init() — load initial data
|
|
131
|
+
onInit() {
|
|
132
|
+
if (this.length === 0) this.loadAll();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Called during dispose() — custom teardown
|
|
136
|
+
onDispose() {
|
|
137
|
+
// cleanup logic
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `init()` → wraps methods for async tracking → calls `onInit()`
|
|
143
|
+
- `dispose()` → inherited from Collection → aborts `disposeSignal`, runs cleanups, calls `onDispose()`
|
|
144
|
+
- `initialized` — `false` before `init()`, `true` after
|
|
145
|
+
- `disposeSignal` — AbortSignal that fires on dispose (pass to fetch calls)
|
|
146
|
+
|
|
147
|
+
## React Integration
|
|
148
|
+
|
|
149
|
+
Resource works with `useSingleton` (shared) or `useLocal` (component-scoped):
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { useSingleton } from 'mvc-kit/react';
|
|
153
|
+
|
|
154
|
+
function UserList() {
|
|
155
|
+
const [items, users] = useSingleton(UsersResource);
|
|
156
|
+
const { loading, error } = users.async.loadAll;
|
|
157
|
+
|
|
158
|
+
if (loading) return <Spinner />;
|
|
159
|
+
if (error) return <Error message={error} />;
|
|
160
|
+
|
|
161
|
+
return <ul>{items.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `useSingleton` auto-calls `init()` (via `isInitializable` duck-typing)
|
|
166
|
+
- `subscribeAsync` duck-typing enables `useInstance` to re-render on async changes
|
|
167
|
+
- ViewModel getters that read `resource.items` auto-track via `subscribe` delegation
|
|
168
|
+
|
|
169
|
+
## ViewModel Integration
|
|
170
|
+
|
|
171
|
+
ViewModels access Resources via `singleton()`. Getter auto-tracking handles reactivity:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
class UsersViewModel extends ViewModel<{ search: string }> {
|
|
175
|
+
private users = singleton(UsersResource);
|
|
176
|
+
|
|
177
|
+
// Auto-tracked: re-evaluates when users.items changes
|
|
178
|
+
get filtered() {
|
|
179
|
+
return this.users.filter(u =>
|
|
180
|
+
u.name.includes(this.state.search)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
onInit() {
|
|
185
|
+
if (this.users.length === 0) this.users.loadAll();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Properties & Methods
|
|
191
|
+
|
|
192
|
+
### Inherited from Collection
|
|
193
|
+
|
|
194
|
+
| Member | Type | Description |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `items` | `T[]` | The raw array |
|
|
197
|
+
| `state` | `T[]` | Alias for `items` (Subscribable compatibility) |
|
|
198
|
+
| `length` | `number` | Item count |
|
|
199
|
+
| `disposed` | `boolean` | Whether disposed |
|
|
200
|
+
| `disposeSignal` | `AbortSignal` | Fires on dispose |
|
|
201
|
+
| `add(...items)` | `void` | Add items (skip duplicates) |
|
|
202
|
+
| `upsert(...items)` | `void` | Add or replace by ID |
|
|
203
|
+
| `update(id, changes)` | `void` | Partial update by ID |
|
|
204
|
+
| `remove(...ids)` | `void` | Remove by ID(s) |
|
|
205
|
+
| `reset(items)` | `void` | Replace all items |
|
|
206
|
+
| `clear()` | `void` | Remove all items |
|
|
207
|
+
| `optimistic(cb)` | `() => void` | Snapshot → mutate → rollback fn |
|
|
208
|
+
| `get(id)` | `T \| undefined` | Lookup by ID |
|
|
209
|
+
| `has(id)` | `boolean` | Check existence |
|
|
210
|
+
| `find(pred)` | `T \| undefined` | First match |
|
|
211
|
+
| `filter(pred)` | `T[]` | All matches |
|
|
212
|
+
| `sorted(cmp)` | `T[]` | Sorted copy |
|
|
213
|
+
| `map(fn)` | `U[]` | Mapped array |
|
|
214
|
+
| `subscribe(listener)` | `() => void` | Subscribe to changes |
|
|
215
|
+
| `dispose()` | `void` | Tear down |
|
|
216
|
+
| `static MAX_SIZE` | `number` | Max items (FIFO eviction), 0 = unlimited |
|
|
217
|
+
| `static TTL` | `number` | Time-to-live (ms), 0 = no expiry |
|
|
218
|
+
|
|
219
|
+
### Resource-Specific
|
|
220
|
+
|
|
221
|
+
| Member | Type | Description |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `initialized` | `boolean` | Whether `init()` has been called |
|
|
224
|
+
| `init()` | `void \| Promise<void>` | Initialize (wraps methods, calls `onInit`) |
|
|
225
|
+
| `onInit()` | `void \| Promise<void>` | Lifecycle hook for initial data loading |
|
|
226
|
+
| `onDispose()` | `void` | Lifecycle hook for custom teardown |
|
|
227
|
+
| `async` | `{ [method]: TaskState }` | Async tracking proxy |
|
|
228
|
+
| `subscribeAsync(cb)` | `() => void` | Subscribe to async state changes |
|
|
229
|
+
| `addCleanup(fn)` | `void` | Register cleanup for dispose |
|
|
230
|
+
| `static GHOST_TIMEOUT` | `number` | DEV-only ghost detection delay (ms) |
|
|
231
|
+
|
|
232
|
+
## Method Binding
|
|
233
|
+
|
|
234
|
+
All public methods are auto-bound in the constructor. You can pass them point-free as callbacks without losing `this` context:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
const { add, upsert } = resource;
|
|
238
|
+
channel.on("update", upsert); // point-free works
|
|
239
|
+
```
|