mvc-kit 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/agent-config/claude-code/agents/mvc-kit-architect.md +2 -3
- package/agent-config/claude-code/skills/guide/SKILL.md +1 -1
- package/agent-config/claude-code/skills/guide/anti-patterns.md +42 -3
- package/agent-config/claude-code/skills/guide/api-reference.md +4 -3
- package/agent-config/claude-code/skills/guide/patterns.md +18 -13
- package/agent-config/claude-code/skills/review/checklist.md +1 -1
- package/agent-config/claude-code/skills/scaffold/SKILL.md +1 -1
- package/agent-config/claude-code/skills/scaffold/templates/collection.md +7 -14
- package/agent-config/claude-code/skills/scaffold/templates/viewmodel.md +13 -42
- package/agent-config/copilot/copilot-instructions.md +14 -16
- package/agent-config/cursor/cursorrules +14 -16
- package/dist/Channel.d.ts +29 -0
- package/dist/Channel.d.ts.map +1 -1
- package/dist/Collection.d.ts +16 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Controller.d.ts +9 -0
- package/dist/Controller.d.ts.map +1 -1
- package/dist/EventBus.d.ts +5 -0
- package/dist/EventBus.d.ts.map +1 -1
- package/dist/Model.d.ts +16 -0
- package/dist/Model.d.ts.map +1 -1
- package/dist/Service.d.ts +8 -0
- package/dist/Service.d.ts.map +1 -1
- package/dist/ViewModel.d.ts +35 -1
- package/dist/ViewModel.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 +226 -111
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/provider.d.ts +1 -0
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react/use-model.d.ts +2 -0
- package/dist/react/use-model.d.ts.map +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/{singleton-C8_FRbA7.js → singleton-CaEXSbYg.js} +5 -1
- package/dist/singleton-CaEXSbYg.js.map +1 -0
- package/dist/singleton-L-u2W_lX.cjs.map +1 -1
- package/dist/singleton.d.ts +10 -0
- package/dist/singleton.d.ts.map +1 -1
- package/mvc-kit-logo.jpg +0 -0
- package/package.json +2 -1
- package/dist/singleton-C8_FRbA7.js.map +0 -1
package/README.md
CHANGED
|
@@ -131,6 +131,7 @@ const todos = new Collection<Todo>();
|
|
|
131
131
|
|
|
132
132
|
// CRUD (triggers re-renders)
|
|
133
133
|
todos.add({ id: '1', text: 'Learn mvc-kit', done: false });
|
|
134
|
+
todos.upsert({ id: '1', text: 'Updated', done: true }); // Add-or-replace by ID
|
|
134
135
|
todos.update('1', { done: true });
|
|
135
136
|
todos.remove('1');
|
|
136
137
|
todos.reset([...]); // Replace all
|
|
@@ -401,13 +402,13 @@ class ChatViewModel extends ViewModel<{ messages: Message[] }> {
|
|
|
401
402
|
|
|
402
403
|
### `subscribeTo(source, listener)` (protected)
|
|
403
404
|
|
|
404
|
-
Subscribe to a Subscribable and auto-unsubscribe on dispose. Available on ViewModel, Model, and Controller.
|
|
405
|
+
Subscribe to a Subscribable and auto-unsubscribe on dispose. Available on ViewModel, Model, and Controller. Use for **imperative reactions** — for deriving values from collections, use getters instead (auto-tracking handles reactivity).
|
|
405
406
|
|
|
406
407
|
```typescript
|
|
407
|
-
class
|
|
408
|
+
class ChatViewModel extends ViewModel<State> {
|
|
408
409
|
protected onInit() {
|
|
409
|
-
//
|
|
410
|
-
this.subscribeTo(this.
|
|
410
|
+
// Imperative reaction: play sound on new messages
|
|
411
|
+
this.subscribeTo(this.messagesCollection, () => this.playNotificationSound());
|
|
411
412
|
}
|
|
412
413
|
}
|
|
413
414
|
```
|
|
@@ -52,10 +52,9 @@ When asked to plan a feature:
|
|
|
52
52
|
|
|
53
53
|
- State holds only source-of-truth values. Derived values are `get` accessors. Async status comes from `vm.async.methodName`.
|
|
54
54
|
- One ViewModel per component via `useLocal`. No `useEffect` for data loading.
|
|
55
|
-
- Collections are encapsulated by ViewModels. Components never import Collections.
|
|
55
|
+
- Collections are encapsulated by ViewModels. Getters read from collections directly — auto-tracking handles reactivity. Components never import Collections.
|
|
56
56
|
- Services are stateless, accept `AbortSignal`, throw `HttpError`.
|
|
57
|
-
- `onInit()` handles data loading
|
|
58
|
-
- Use `subscribeTo()` for Collection subscriptions (auto-cleanup).
|
|
57
|
+
- `onInit()` handles data loading. Use `subscribeTo()` only for imperative side effects, not for deriving values.
|
|
59
58
|
- ViewModel section order: Private fields → Computed getters → Lifecycle → Actions → Setters.
|
|
60
59
|
- Pass `this.disposeSignal` to every async call.
|
|
61
60
|
|
|
@@ -29,7 +29,7 @@ You are assisting a developer using **mvc-kit**, a zero-dependency TypeScript-fi
|
|
|
29
29
|
|
|
30
30
|
3. **Components are declarative.** Read `state.x` for raw values, `vm.x` for computed, `vm.async.x` for loading/error. Call `vm.method()` for actions.
|
|
31
31
|
|
|
32
|
-
4. **Collections are encapsulated.**
|
|
32
|
+
4. **Collections are encapsulated.** ViewModel getters read from collections directly — auto-tracking handles reactivity. Use `subscribeTo()` only for imperative side effects. Components never import Collections.
|
|
33
33
|
|
|
34
34
|
5. **Services are stateless.** Accept `AbortSignal`, throw `HttpError`, no knowledge of ViewModels or Collections.
|
|
35
35
|
|
|
@@ -9,16 +9,37 @@ Reject these patterns. Each entry shows the bad pattern and the correct alternat
|
|
|
9
9
|
```typescript
|
|
10
10
|
// BAD
|
|
11
11
|
interface State {
|
|
12
|
-
items: Item[];
|
|
13
12
|
loading: boolean;
|
|
14
13
|
error: string | null;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
// GOOD — async tracking handles it
|
|
17
|
+
// Read: vm.async.load.loading, vm.async.load.error
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1b. Mirroring Collection Data into State
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// BAD — subscribeTo + set() to mirror collection data
|
|
18
26
|
interface State {
|
|
19
27
|
items: Item[];
|
|
28
|
+
search: string;
|
|
29
|
+
}
|
|
30
|
+
protected onInit() {
|
|
31
|
+
this.subscribeTo(this.collection, () => {
|
|
32
|
+
this.set({ items: this.collection.items });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// GOOD — getter reads from collection directly, auto-tracked
|
|
37
|
+
interface State {
|
|
38
|
+
search: string;
|
|
39
|
+
}
|
|
40
|
+
get items(): Item[] {
|
|
41
|
+
return this.collection.items as Item[];
|
|
20
42
|
}
|
|
21
|
-
// Read: vm.async.load.loading, vm.async.load.error
|
|
22
43
|
```
|
|
23
44
|
|
|
24
45
|
---
|
|
@@ -302,7 +323,25 @@ function UsersPage() {
|
|
|
302
323
|
|
|
303
324
|
---
|
|
304
325
|
|
|
305
|
-
## 15.
|
|
326
|
+
## 15. Using reset() for Paginated/Incremental Loads
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// BAD — reset() destroys data other ViewModels depend on
|
|
330
|
+
async loadPage(page: number) {
|
|
331
|
+
const data = await this.service.getPage(page, this.disposeSignal);
|
|
332
|
+
this.collection.reset(data); // wipes previous pages!
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// GOOD — upsert() accumulates data, replacing stale items and adding new ones
|
|
336
|
+
async loadPage(page: number) {
|
|
337
|
+
const data = await this.service.getPage(page, this.disposeSignal);
|
|
338
|
+
this.collection.upsert(...data);
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## 16. Missing disposeSignal on Async Calls
|
|
306
345
|
|
|
307
346
|
```typescript
|
|
308
347
|
// BAD — no cancellation on unmount
|
|
@@ -108,10 +108,11 @@ Same as ViewModel: `onInit()`, `onSet()`, `onDispose()`, `disposeSignal`, `subsc
|
|
|
108
108
|
Reactive typed array with CRUD and query methods. Items must have an `id: string` field.
|
|
109
109
|
|
|
110
110
|
### CRUD (triggers notifications)
|
|
111
|
-
- `add(
|
|
111
|
+
- `add(...items: T[]): void` — Append items. Skips existing IDs.
|
|
112
|
+
- `upsert(...items: T[]): void` — Add-or-replace by ID. Existing replaced in-place; new appended. Ideal for paginated loads.
|
|
112
113
|
- `update(id: string, partial: Partial<T>): void`
|
|
113
|
-
- `remove(
|
|
114
|
-
- `reset(items: T[]): void` — Replace all items.
|
|
114
|
+
- `remove(...ids: string[]): void`
|
|
115
|
+
- `reset(items: T[]): void` — Replace all items. Use for full loads; prefer `upsert()` for incremental.
|
|
115
116
|
- `clear(): void`
|
|
116
117
|
|
|
117
118
|
### Query (pure, no notifications)
|
|
@@ -69,28 +69,27 @@ The setter changes state → React re-renders → getters recompute. No manual r
|
|
|
69
69
|
|
|
70
70
|
## Encapsulating Collections
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
Getters read directly from collection members — auto-tracking handles reactivity. Components never see Collections.
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
75
|
class LocationsViewModel extends ViewModel<State> {
|
|
76
|
-
|
|
76
|
+
collection = singleton(LocationsCollection);
|
|
77
77
|
private service = singleton(LocationService);
|
|
78
78
|
|
|
79
|
+
// Getter reads from collection — auto-tracked
|
|
80
|
+
get items(): LocationState[] {
|
|
81
|
+
return this.collection.items as LocationState[];
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
protected onInit() {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Smart init: use cached data or fetch fresh
|
|
85
|
-
if (this.collection.length > 0) {
|
|
86
|
-
this.set({ items: this.collection.items });
|
|
87
|
-
} else {
|
|
88
|
-
this.load();
|
|
89
|
-
}
|
|
85
|
+
// Smart init: skip fetch if data already loaded
|
|
86
|
+
if (this.collection.length === 0) this.load();
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
89
|
```
|
|
93
90
|
|
|
91
|
+
Use `subscribeTo()` only for imperative side effects (e.g. play a sound on new messages), not for deriving values.
|
|
92
|
+
|
|
94
93
|
---
|
|
95
94
|
|
|
96
95
|
## Async Methods — Happy Path Only
|
|
@@ -100,6 +99,12 @@ async load() {
|
|
|
100
99
|
const data = await this.service.getAll(this.disposeSignal);
|
|
101
100
|
this.collection.reset(data);
|
|
102
101
|
}
|
|
102
|
+
|
|
103
|
+
// For paginated/incremental loads, use upsert() to accumulate data:
|
|
104
|
+
async loadPage(page: number) {
|
|
105
|
+
const data = await this.service.getPage(page, this.disposeSignal);
|
|
106
|
+
this.collection.upsert(...data);
|
|
107
|
+
}
|
|
103
108
|
```
|
|
104
109
|
|
|
105
110
|
No try/catch. No `set({ loading: true })`. No AbortError check. The framework handles it.
|
|
@@ -220,7 +225,7 @@ class UserService extends Service {
|
|
|
220
225
|
class UsersCollection extends Collection<UserState> {}
|
|
221
226
|
```
|
|
222
227
|
|
|
223
|
-
Thin subclass for singleton identity. No custom methods — query logic goes in ViewModel getters.
|
|
228
|
+
Thin subclass for singleton identity. No custom methods — query logic goes in ViewModel getters. Use `reset()` for full loads, `upsert()` for paginated/incremental loads, and `add`/`update`/`remove` for granular mutations.
|
|
224
229
|
|
|
225
230
|
---
|
|
226
231
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
6. **Section order** — Must follow: Private fields → Computed getters → Lifecycle → Actions → Setters.
|
|
14
14
|
7. **No two-step setters** — Setters should be one-liners (`this.set({ x })`). No manual refilter/rederive calls after `set()`.
|
|
15
15
|
8. **Smart init pattern** — When subscribing to a Collection, check `collection.length > 0` before fetching.
|
|
16
|
-
9. **
|
|
16
|
+
9. **Getters read from collections** — Collection data should be accessed via getters, not mirrored into state with `subscribeTo()` + `set()`. Use `subscribeTo()` only for imperative side effects.
|
|
17
17
|
10. **Use `collection.optimistic()`** — No manual snapshot/restore for optimistic updates.
|
|
18
18
|
11. **Singleton resolution as property** — Dependencies resolved via `private service = singleton(MyService)`, not in `onInit()`.
|
|
19
19
|
|
|
@@ -47,6 +47,6 @@ Parse `$ARGUMENTS` as `<type> <Name>` (case-insensitive type, PascalCase Name).
|
|
|
47
47
|
- Services are stateless, accept `AbortSignal`, throw `HttpError`.
|
|
48
48
|
- Collections are thin subclasses — no custom methods.
|
|
49
49
|
- Use `singleton()` for dependency resolution in ViewModels.
|
|
50
|
-
- Use `subscribeTo()` for
|
|
50
|
+
- Getters read from collections directly — auto-tracking handles reactivity. Use `subscribeTo()` only for imperative side effects.
|
|
51
51
|
- Pass `this.disposeSignal` to every async call.
|
|
52
52
|
- Test files use `teardownAll()` in `beforeEach`.
|
|
@@ -22,23 +22,16 @@ import { ViewModel, singleton } from 'mvc-kit';
|
|
|
22
22
|
import { {{Name}}Collection } from '../collections/{{Name}}Collection';
|
|
23
23
|
import type { {{Name}}Item } from '../collections/{{Name}}Collection';
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
25
|
+
class {{Name}}ViewModel extends ViewModel<{ search: string }> {
|
|
26
|
+
collection = singleton({{Name}}Collection);
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// Getter reads from collection — auto-tracked
|
|
29
|
+
get items(): {{Name}}Item[] {
|
|
30
|
+
return this.collection.items as {{Name}}Item[];
|
|
31
|
+
}
|
|
31
32
|
|
|
32
33
|
protected onInit() {
|
|
33
|
-
|
|
34
|
-
this.set({ items: this.collection.items });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
if (this.collection.length > 0) {
|
|
38
|
-
this.set({ items: this.collection.items });
|
|
39
|
-
} else {
|
|
40
|
-
this.load();
|
|
41
|
-
}
|
|
34
|
+
if (this.collection.length === 0) this.load();
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
async load() {
|
|
@@ -8,7 +8,6 @@ import { ViewModel, singleton } from 'mvc-kit';
|
|
|
8
8
|
// import { {{Name}}Collection } from '../collections/{{Name}}Collection';
|
|
9
9
|
|
|
10
10
|
interface {{Name}}State {
|
|
11
|
-
items: any[]; // TODO: Replace with your item type
|
|
12
11
|
search: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -20,11 +19,18 @@ interface {{Name}}State {
|
|
|
20
19
|
export class {{Name}}ViewModel extends ViewModel<{{Name}}State> {
|
|
21
20
|
// --- Private fields ---
|
|
22
21
|
// private service = singleton({{Name}}Service);
|
|
23
|
-
//
|
|
22
|
+
// collection = singleton({{Name}}Collection);
|
|
24
23
|
|
|
25
24
|
// --- Computed getters ---
|
|
25
|
+
// Getter reads from collection — auto-tracked
|
|
26
|
+
// get items(): any[] {
|
|
27
|
+
// return this.collection.items;
|
|
28
|
+
// }
|
|
29
|
+
|
|
26
30
|
get filtered(): any[] {
|
|
27
|
-
const {
|
|
31
|
+
const { search } = this.state;
|
|
32
|
+
// TODO: read from this.items (collection getter) instead of hardcoded array
|
|
33
|
+
const items: any[] = [];
|
|
28
34
|
if (!search) return items;
|
|
29
35
|
const q = search.toLowerCase();
|
|
30
36
|
return items.filter(item =>
|
|
@@ -33,7 +39,7 @@ export class {{Name}}ViewModel extends ViewModel<{{Name}}State> {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
get total(): number {
|
|
36
|
-
return this.
|
|
42
|
+
return this.filtered.length;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
get hasResults(): boolean {
|
|
@@ -42,17 +48,8 @@ export class {{Name}}ViewModel extends ViewModel<{{Name}}State> {
|
|
|
42
48
|
|
|
43
49
|
// --- Lifecycle ---
|
|
44
50
|
protected onInit() {
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// this.set({ items: this.collection.items });
|
|
48
|
-
// });
|
|
49
|
-
|
|
50
|
-
// Smart init: use cached data or fetch fresh
|
|
51
|
-
// if (this.collection.length > 0) {
|
|
52
|
-
// this.set({ items: this.collection.items });
|
|
53
|
-
// } else {
|
|
54
|
-
// this.load();
|
|
55
|
-
// }
|
|
51
|
+
// Smart init: skip fetch if data already loaded
|
|
52
|
+
// if (this.collection.length === 0) this.load();
|
|
56
53
|
|
|
57
54
|
this.load();
|
|
58
55
|
}
|
|
@@ -79,9 +76,8 @@ import { {{Name}}ViewModel } from './{{Name}}ViewModel';
|
|
|
79
76
|
beforeEach(() => teardownAll());
|
|
80
77
|
|
|
81
78
|
describe('{{Name}}ViewModel', () => {
|
|
82
|
-
function create(overrides: Partial<{
|
|
79
|
+
function create(overrides: Partial<{ search: string }> = {}) {
|
|
83
80
|
return new {{Name}}ViewModel({
|
|
84
|
-
items: [],
|
|
85
81
|
search: '',
|
|
86
82
|
...overrides,
|
|
87
83
|
});
|
|
@@ -89,7 +85,6 @@ describe('{{Name}}ViewModel', () => {
|
|
|
89
85
|
|
|
90
86
|
test('initializes with default state', () => {
|
|
91
87
|
const vm = create();
|
|
92
|
-
expect(vm.state.items).toEqual([]);
|
|
93
88
|
expect(vm.state.search).toBe('');
|
|
94
89
|
vm.dispose();
|
|
95
90
|
});
|
|
@@ -100,29 +95,5 @@ describe('{{Name}}ViewModel', () => {
|
|
|
100
95
|
expect(vm.state.search).toBe('test');
|
|
101
96
|
vm.dispose();
|
|
102
97
|
});
|
|
103
|
-
|
|
104
|
-
test('filtered getter applies search', () => {
|
|
105
|
-
const vm = create({
|
|
106
|
-
items: [
|
|
107
|
-
{ id: '1', name: 'Alpha' },
|
|
108
|
-
{ id: '2', name: 'Beta' },
|
|
109
|
-
],
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
expect(vm.filtered).toHaveLength(2);
|
|
113
|
-
|
|
114
|
-
vm.setSearch('alpha');
|
|
115
|
-
expect(vm.filtered).toHaveLength(1);
|
|
116
|
-
|
|
117
|
-
vm.dispose();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('total returns item count', () => {
|
|
121
|
-
const vm = create({
|
|
122
|
-
items: [{ id: '1' }, { id: '2' }, { id: '3' }],
|
|
123
|
-
});
|
|
124
|
-
expect(vm.total).toBe(3);
|
|
125
|
-
vm.dispose();
|
|
126
|
-
});
|
|
127
98
|
});
|
|
128
99
|
```
|
|
@@ -30,7 +30,7 @@ import { useLocal, useSingleton, useInstance, useModel, useField, useEvent, useE
|
|
|
30
30
|
4. **One ViewModel per component** via `useLocal`. No `useEffect` for data loading — use `onInit()`.
|
|
31
31
|
5. **No `useState`/`useMemo`/`useCallback`** — the ViewModel is the hook.
|
|
32
32
|
6. **Components are declarative.** Read `state.x` for raw, `vm.x` for computed, `vm.async.x` for loading/error.
|
|
33
|
-
7. **Collections are encapsulated.**
|
|
33
|
+
7. **Collections are encapsulated.** ViewModel getters read from collections directly — auto-tracking handles reactivity. Use `subscribeTo()` only for imperative side effects. Components never import Collections.
|
|
34
34
|
8. **Services are stateless.** Accept `AbortSignal`, throw `HttpError`, no knowledge of ViewModels.
|
|
35
35
|
9. **Pass `this.disposeSignal`** to every async call.
|
|
36
36
|
10. **Lifecycle**: `construct → init() → use → dispose()`. Hooks auto-call `init()`.
|
|
@@ -39,35 +39,31 @@ import { useLocal, useSingleton, useInstance, useModel, useField, useEvent, useE
|
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
41
|
interface ItemState {
|
|
42
|
-
items: Item[];
|
|
43
42
|
search: string;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
class ItemsViewModel extends ViewModel<ItemState> {
|
|
47
46
|
// --- Private fields ---
|
|
48
47
|
private service = singleton(ItemService);
|
|
49
|
-
|
|
48
|
+
collection = singleton(ItemsCollection);
|
|
50
49
|
|
|
51
50
|
// --- Computed getters ---
|
|
51
|
+
get items(): Item[] {
|
|
52
|
+
return this.collection.items as Item[];
|
|
53
|
+
}
|
|
54
|
+
|
|
52
55
|
get filtered(): Item[] {
|
|
53
|
-
const {
|
|
54
|
-
if (!search) return items;
|
|
56
|
+
const { search } = this.state;
|
|
57
|
+
if (!search) return this.items;
|
|
55
58
|
const q = search.toLowerCase();
|
|
56
|
-
return items.filter(i => i.name.toLowerCase().includes(q));
|
|
59
|
+
return this.items.filter(i => i.name.toLowerCase().includes(q));
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
get total(): number { return this.
|
|
62
|
+
get total(): number { return this.items.length; }
|
|
60
63
|
|
|
61
64
|
// --- Lifecycle ---
|
|
62
65
|
protected onInit() {
|
|
63
|
-
|
|
64
|
-
this.set({ items: this.collection.items });
|
|
65
|
-
});
|
|
66
|
-
if (this.collection.length > 0) {
|
|
67
|
-
this.set({ items: this.collection.items });
|
|
68
|
-
} else {
|
|
69
|
-
this.load();
|
|
70
|
-
}
|
|
66
|
+
if (this.collection.length === 0) this.load();
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
// --- Actions ---
|
|
@@ -87,7 +83,7 @@ class ItemsViewModel extends ViewModel<ItemState> {
|
|
|
87
83
|
|
|
88
84
|
```tsx
|
|
89
85
|
function ItemsPage() {
|
|
90
|
-
const [state, vm] = useLocal(ItemsViewModel, {
|
|
86
|
+
const [state, vm] = useLocal(ItemsViewModel, { search: '' });
|
|
91
87
|
const { loading, error } = vm.async.load;
|
|
92
88
|
|
|
93
89
|
return (
|
|
@@ -133,6 +129,7 @@ class UserService extends Service {
|
|
|
133
129
|
```typescript
|
|
134
130
|
class UsersCollection extends Collection<UserState> {}
|
|
135
131
|
// Thin subclass. Query logic in ViewModel getters.
|
|
132
|
+
// Use reset() for full loads, upsert() for paginated/incremental loads.
|
|
136
133
|
```
|
|
137
134
|
|
|
138
135
|
## Model Pattern
|
|
@@ -222,6 +219,7 @@ test('example', () => {
|
|
|
222
219
|
- Manual try/catch for standard loads → async tracking handles it
|
|
223
220
|
- Two-step setters with refilter calls → getters auto-recompute
|
|
224
221
|
- Manual optimistic snapshot/restore → use `collection.optimistic()`
|
|
222
|
+
- `reset()` for paginated/incremental loads → use `upsert()` to accumulate data
|
|
225
223
|
- `useState`/`useMemo`/`useCallback` in connected components → ViewModel handles it
|
|
226
224
|
|
|
227
225
|
## Decision Framework
|
|
@@ -30,7 +30,7 @@ import { useLocal, useSingleton, useInstance, useModel, useField, useEvent, useE
|
|
|
30
30
|
4. **One ViewModel per component** via `useLocal`. No `useEffect` for data loading — use `onInit()`.
|
|
31
31
|
5. **No `useState`/`useMemo`/`useCallback`** — the ViewModel is the hook.
|
|
32
32
|
6. **Components are declarative.** Read `state.x` for raw, `vm.x` for computed, `vm.async.x` for loading/error.
|
|
33
|
-
7. **Collections are encapsulated.**
|
|
33
|
+
7. **Collections are encapsulated.** ViewModel getters read from collections directly — auto-tracking handles reactivity. Use `subscribeTo()` only for imperative side effects. Components never import Collections.
|
|
34
34
|
8. **Services are stateless.** Accept `AbortSignal`, throw `HttpError`, no knowledge of ViewModels.
|
|
35
35
|
9. **Pass `this.disposeSignal`** to every async call.
|
|
36
36
|
10. **Lifecycle**: `construct → init() → use → dispose()`. Hooks auto-call `init()`.
|
|
@@ -39,35 +39,31 @@ import { useLocal, useSingleton, useInstance, useModel, useField, useEvent, useE
|
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
41
|
interface ItemState {
|
|
42
|
-
items: Item[];
|
|
43
42
|
search: string;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
class ItemsViewModel extends ViewModel<ItemState> {
|
|
47
46
|
// --- Private fields ---
|
|
48
47
|
private service = singleton(ItemService);
|
|
49
|
-
|
|
48
|
+
collection = singleton(ItemsCollection);
|
|
50
49
|
|
|
51
50
|
// --- Computed getters ---
|
|
51
|
+
get items(): Item[] {
|
|
52
|
+
return this.collection.items as Item[];
|
|
53
|
+
}
|
|
54
|
+
|
|
52
55
|
get filtered(): Item[] {
|
|
53
|
-
const {
|
|
54
|
-
if (!search) return items;
|
|
56
|
+
const { search } = this.state;
|
|
57
|
+
if (!search) return this.items;
|
|
55
58
|
const q = search.toLowerCase();
|
|
56
|
-
return items.filter(i => i.name.toLowerCase().includes(q));
|
|
59
|
+
return this.items.filter(i => i.name.toLowerCase().includes(q));
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
get total(): number { return this.
|
|
62
|
+
get total(): number { return this.items.length; }
|
|
60
63
|
|
|
61
64
|
// --- Lifecycle ---
|
|
62
65
|
protected onInit() {
|
|
63
|
-
|
|
64
|
-
this.set({ items: this.collection.items });
|
|
65
|
-
});
|
|
66
|
-
if (this.collection.length > 0) {
|
|
67
|
-
this.set({ items: this.collection.items });
|
|
68
|
-
} else {
|
|
69
|
-
this.load();
|
|
70
|
-
}
|
|
66
|
+
if (this.collection.length === 0) this.load();
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
// --- Actions ---
|
|
@@ -87,7 +83,7 @@ class ItemsViewModel extends ViewModel<ItemState> {
|
|
|
87
83
|
|
|
88
84
|
```tsx
|
|
89
85
|
function ItemsPage() {
|
|
90
|
-
const [state, vm] = useLocal(ItemsViewModel, {
|
|
86
|
+
const [state, vm] = useLocal(ItemsViewModel, { search: '' });
|
|
91
87
|
const { loading, error } = vm.async.load;
|
|
92
88
|
|
|
93
89
|
return (
|
|
@@ -133,6 +129,7 @@ class UserService extends Service {
|
|
|
133
129
|
```typescript
|
|
134
130
|
class UsersCollection extends Collection<UserState> {}
|
|
135
131
|
// Thin subclass. Query logic in ViewModel getters.
|
|
132
|
+
// Use reset() for full loads, upsert() for paginated/incremental loads.
|
|
136
133
|
```
|
|
137
134
|
|
|
138
135
|
## Model Pattern
|
|
@@ -222,6 +219,7 @@ test('example', () => {
|
|
|
222
219
|
- Manual try/catch for standard loads → async tracking handles it
|
|
223
220
|
- Two-step setters with refilter calls → getters auto-recompute
|
|
224
221
|
- Manual optimistic snapshot/restore → use `collection.optimistic()`
|
|
222
|
+
- `reset()` for paginated/incremental loads → use `upsert()` to accumulate data
|
|
225
223
|
- `useState`/`useMemo`/`useCallback` in connected components → ViewModel handles it
|
|
226
224
|
|
|
227
225
|
## Decision Framework
|
package/dist/Channel.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Listener, Subscribable, Disposable, Initializable } from './types';
|
|
2
|
+
/** Describes the current connection state of a Channel. */
|
|
2
3
|
export interface ChannelStatus {
|
|
3
4
|
readonly connected: boolean;
|
|
4
5
|
readonly reconnecting: boolean;
|
|
@@ -6,10 +7,18 @@ export interface ChannelStatus {
|
|
|
6
7
|
readonly error: string | null;
|
|
7
8
|
}
|
|
8
9
|
type Handler<T> = (payload: T) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Abstract persistent connection with automatic reconnection and exponential backoff.
|
|
12
|
+
* Subclass to implement WebSocket, SSE, or other transport protocols.
|
|
13
|
+
*/
|
|
9
14
|
export declare abstract class Channel<M extends Record<string, any>> implements Subscribable<ChannelStatus>, Initializable, Disposable {
|
|
15
|
+
/** Base delay (ms) for reconnection backoff. */
|
|
10
16
|
static RECONNECT_BASE: number;
|
|
17
|
+
/** Maximum delay cap (ms) for reconnection backoff. */
|
|
11
18
|
static RECONNECT_MAX: number;
|
|
19
|
+
/** Exponential backoff multiplier for reconnection delay. */
|
|
12
20
|
static RECONNECT_FACTOR: number;
|
|
21
|
+
/** Maximum number of reconnection attempts before giving up. */
|
|
13
22
|
static MAX_ATTEMPTS: number;
|
|
14
23
|
private _status;
|
|
15
24
|
private _connState;
|
|
@@ -21,25 +30,45 @@ export declare abstract class Channel<M extends Record<string, any>> implements
|
|
|
21
30
|
private _connectAbort;
|
|
22
31
|
private _reconnectTimer;
|
|
23
32
|
private _cleanups;
|
|
33
|
+
/** Current connection status. */
|
|
24
34
|
get state(): Readonly<ChannelStatus>;
|
|
35
|
+
/** Subscribes to connection status changes. Returns an unsubscribe function. */
|
|
25
36
|
subscribe(listener: Listener<ChannelStatus>): () => void;
|
|
37
|
+
/** Whether this instance has been disposed. */
|
|
26
38
|
get disposed(): boolean;
|
|
39
|
+
/** Whether init() has been called. */
|
|
27
40
|
get initialized(): boolean;
|
|
41
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
28
42
|
get disposeSignal(): AbortSignal;
|
|
43
|
+
/** Initializes the instance. Called automatically by React hooks after mount. */
|
|
29
44
|
init(): void | Promise<void>;
|
|
45
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
30
46
|
dispose(): void;
|
|
47
|
+
/** Establishes the underlying connection. Called internally by connect(). @protected */
|
|
31
48
|
protected abstract open(signal: AbortSignal): void | Promise<void>;
|
|
49
|
+
/** Tears down the underlying connection. Called internally by disconnect() and dispose(). @protected */
|
|
32
50
|
protected abstract close(): void;
|
|
51
|
+
/** Initiates a connection with automatic reconnection on failure. */
|
|
33
52
|
connect(): void;
|
|
53
|
+
/** Closes the connection and cancels any pending reconnection. */
|
|
34
54
|
disconnect(): void;
|
|
55
|
+
/** Call from subclass when a message arrives from the transport. @protected */
|
|
35
56
|
protected receive<K extends keyof M>(type: K, payload: M[K]): void;
|
|
57
|
+
/** Call from subclass when the transport connection drops unexpectedly. Triggers reconnection. @protected */
|
|
36
58
|
protected disconnected(): void;
|
|
59
|
+
/** Subscribes to a specific message type. Returns an unsubscribe function. */
|
|
37
60
|
on<K extends keyof M>(type: K, handler: Handler<M[K]>): () => void;
|
|
61
|
+
/** Subscribes to a message type, auto-removing the handler after the first invocation. */
|
|
38
62
|
once<K extends keyof M>(type: K, handler: Handler<M[K]>): () => void;
|
|
63
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
39
64
|
protected addCleanup(fn: () => void): void;
|
|
65
|
+
/** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
|
|
40
66
|
protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void;
|
|
67
|
+
/** Lifecycle hook called at the end of init(). Override to load initial data. @protected */
|
|
41
68
|
protected onInit?(): void | Promise<void>;
|
|
69
|
+
/** Lifecycle hook called during dispose(). Override for custom teardown. @protected */
|
|
42
70
|
protected onDispose?(): void;
|
|
71
|
+
/** Computes the reconnect backoff delay with jitter for the given attempt number. @protected */
|
|
43
72
|
protected _calculateDelay(attempt: number): number;
|
|
44
73
|
private _setStatus;
|
|
45
74
|
private _attemptConnect;
|
package/dist/Channel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Channel.d.ts","sourceRoot":"","sources":["../src/Channel.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Channel.d.ts","sourceRoot":"","sources":["../src/Channel.ts"],"names":[],"mappings":"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"}
|
package/dist/Collection.d.ts
CHANGED
|
@@ -18,14 +18,25 @@ export declare class Collection<T extends {
|
|
|
18
18
|
* Alias for Subscribable compatibility.
|
|
19
19
|
*/
|
|
20
20
|
get state(): readonly T[];
|
|
21
|
+
/** The raw readonly array of items. */
|
|
21
22
|
get items(): readonly T[];
|
|
23
|
+
/** Number of items in the collection. */
|
|
22
24
|
get length(): number;
|
|
25
|
+
/** Whether this instance has been disposed. */
|
|
23
26
|
get disposed(): boolean;
|
|
27
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
24
28
|
get disposeSignal(): AbortSignal;
|
|
25
29
|
/**
|
|
26
|
-
* Add one or more items.
|
|
30
|
+
* Add one or more items. Items with existing IDs are silently skipped.
|
|
27
31
|
*/
|
|
28
32
|
add(...items: T[]): void;
|
|
33
|
+
/**
|
|
34
|
+
* Add or replace items by ID. Existing items are replaced in-place
|
|
35
|
+
* (preserving array position); new items are appended. Deduplicates
|
|
36
|
+
* input — last occurrence wins. No-op if nothing changed (reference
|
|
37
|
+
* comparison).
|
|
38
|
+
*/
|
|
39
|
+
upsert(...items: T[]): void;
|
|
29
40
|
/**
|
|
30
41
|
* Remove items by id(s).
|
|
31
42
|
*/
|
|
@@ -71,9 +82,13 @@ export declare class Collection<T extends {
|
|
|
71
82
|
* Map items to new array.
|
|
72
83
|
*/
|
|
73
84
|
map<U>(fn: (item: T) => U): readonly U[];
|
|
85
|
+
/** Subscribes to state changes. Returns an unsubscribe function. */
|
|
74
86
|
subscribe(listener: CollectionListener<T>): () => void;
|
|
87
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
75
88
|
dispose(): void;
|
|
89
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
76
90
|
protected addCleanup(fn: () => void): void;
|
|
91
|
+
/** Lifecycle hook called during dispose(). Override for custom teardown. @protected */
|
|
77
92
|
protected onDispose?(): void;
|
|
78
93
|
private notify;
|
|
79
94
|
private rebuildIndex;
|
package/dist/Collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collection.d.ts","sourceRoot":"","sources":["../src/Collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEtD,KAAK,eAAe,CAAC,CAAC,IAAI,SAAS,CAAC,EAAE,CAAC;AACvC,KAAK,kBAAkB,CAAC,CAAC,IAAI,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1D;;GAEG;AACH,qBAAa,UAAU,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAE,YAAW,YAAY,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACpG,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,SAAS,CAA+B;gBAEpC,YAAY,GAAE,CAAC,EAAO;IAKlC;;OAEG;IACH,IAAI,KAAK,IAAI,SAAS,CAAC,EAAE,CAExB;IAED,IAAI,KAAK,IAAI,SAAS,CAAC,EAAE,CAExB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,IAAI,aAAa,IAAI,WAAW,CAK/B;IAID;;OAEG;IACH,GAAG,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;
|
|
1
|
+
{"version":3,"file":"Collection.d.ts","sourceRoot":"","sources":["../src/Collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEtD,KAAK,eAAe,CAAC,CAAC,IAAI,SAAS,CAAC,EAAE,CAAC;AACvC,KAAK,kBAAkB,CAAC,CAAC,IAAI,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;AAE1D;;GAEG;AACH,qBAAa,UAAU,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAE,YAAW,YAAY,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACpG,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,SAAS,CAA+B;gBAEpC,YAAY,GAAE,CAAC,EAAO;IAKlC;;OAEG;IACH,IAAI,KAAK,IAAI,SAAS,CAAC,EAAE,CAExB;IAED,uCAAuC;IACvC,IAAI,KAAK,IAAI,SAAS,CAAC,EAAE,CAExB;IAED,yCAAyC;IACzC,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,+CAA+C;IAC/C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,6EAA6E;IAC7E,IAAI,aAAa,IAAI,WAAW,CAK/B;IAID;;OAEG;IACH,GAAG,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IA6BxB;;;;;OAKG;IACH,MAAM,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IA8C3B;;OAEG;IACH,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI;IA0B/B;;OAEG;IACH,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IA6B9C;;OAEG;IACH,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAYvB;;OAEG;IACH,KAAK,IAAI,IAAI;IAgBb;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAsB5C;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,SAAS;IAI/B;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO;IAIzB;;OAEG;IACH,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,CAAC,GAAG,SAAS;IAIpD;;OAEG;IACH,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,SAAS,CAAC,EAAE;IAIrD;;OAEG;IACH,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC,EAAE;IAIvD;;OAEG;IACH,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,EAAE;IAMxC,oEAAoE;IACpE,SAAS,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAYtD,0EAA0E;IAC1E,OAAO,IAAI,IAAI;IAgBf,uEAAuE;IACvE,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAO1C,uFAAuF;IACvF,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI;IAE5B,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,YAAY;CAMrB"}
|