mvc-kit 2.11.1 → 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/README.md +4 -0
- 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/anti-patterns.md +41 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +66 -2
- package/agent-config/claude-code/skills/guide/patterns.md +52 -0
- package/agent-config/copilot/copilot-instructions.md +9 -5
- package/agent-config/cursor/cursorrules +9 -5
- package/agent-config/lib/install-claude.mjs +10 -33
- package/dist/Feed.cjs +10 -22
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.d.ts +2 -5
- package/dist/Feed.d.ts.map +1 -1
- package/dist/Feed.js +10 -22
- package/dist/Feed.js.map +1 -1
- 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/Pagination.cjs +8 -20
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.d.ts +2 -5
- package/dist/Pagination.d.ts.map +1 -1
- package/dist/Pagination.js +8 -20
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +26 -39
- package/dist/Pending.cjs.map +1 -1
- package/dist/Pending.d.ts +5 -9
- package/dist/Pending.d.ts.map +1 -1
- package/dist/Pending.js +26 -39
- package/dist/Pending.js.map +1 -1
- package/dist/Selection.cjs +5 -13
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.d.ts +2 -4
- package/dist/Selection.d.ts.map +1 -1
- package/dist/Selection.js +5 -13
- package/dist/Selection.js.map +1 -1
- package/dist/Sorting.cjs +7 -19
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.d.ts +2 -5
- package/dist/Sorting.d.ts.map +1 -1
- package/dist/Sorting.js +7 -19
- package/dist/Sorting.js.map +1 -1
- package/dist/Trackable.cjs +81 -0
- package/dist/Trackable.cjs.map +1 -0
- package/dist/Trackable.d.ts +82 -0
- package/dist/Trackable.d.ts.map +1 -0
- package/dist/Trackable.js +81 -0
- package/dist/Trackable.js.map +1 -0
- 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 +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +7 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +7 -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/dist/react/guards.cjs +2 -0
- package/dist/react/guards.cjs.map +1 -1
- package/dist/react/guards.d.ts +4 -0
- package/dist/react/guards.d.ts.map +1 -1
- package/dist/react/guards.js +3 -1
- package/dist/react/guards.js.map +1 -1
- package/dist/react/use-local.cjs +5 -0
- package/dist/react/use-local.cjs.map +1 -1
- package/dist/react/use-local.d.ts.map +1 -1
- package/dist/react/use-local.js +6 -1
- package/dist/react/use-local.js.map +1 -1
- package/dist/react/use-singleton.cjs +5 -0
- package/dist/react/use-singleton.cjs.map +1 -1
- package/dist/react/use-singleton.d.ts.map +1 -1
- package/dist/react/use-singleton.js +6 -1
- package/dist/react/use-singleton.js.map +1 -1
- package/dist/react/use-subscribe-only.cjs +25 -0
- package/dist/react/use-subscribe-only.cjs.map +1 -0
- package/dist/react/use-subscribe-only.d.ts +9 -0
- package/dist/react/use-subscribe-only.d.ts.map +1 -0
- package/dist/react/use-subscribe-only.js +25 -0
- package/dist/react/use-subscribe-only.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/README.md
CHANGED
|
@@ -781,9 +781,12 @@ function App() {
|
|
|
781
781
|
| `Controller` | Stateless orchestrator (Disposable) |
|
|
782
782
|
| `Service` | Non-reactive infrastructure service (Disposable) |
|
|
783
783
|
| `EventBus<E>` | Typed pub/sub event bus |
|
|
784
|
+
| `Trackable` | Base class for custom reactive objects (subscribable + disposable + auto-bind) |
|
|
784
785
|
|
|
785
786
|
### Composable Helpers
|
|
786
787
|
|
|
788
|
+
All composable helpers extend `Trackable` — subscribable, disposable, and auto-bound.
|
|
789
|
+
|
|
787
790
|
| Class | Description |
|
|
788
791
|
|-------|-------------|
|
|
789
792
|
| `Sorting<T>` | Multi-column sort state with 3-click toggle cycle and `apply()` pipeline |
|
|
@@ -890,6 +893,7 @@ Each core class and React hook has a dedicated reference doc with full API detai
|
|
|
890
893
|
| [Service](src/Service.md) | Non-reactive infrastructure adapters (HTTP, storage, SDKs) |
|
|
891
894
|
| [EventBus](src/EventBus.md) | Typed pub/sub for cross-cutting event communication |
|
|
892
895
|
| [Channel](src/Channel.md) | Persistent connections (WebSocket, SSE) with auto-reconnect |
|
|
896
|
+
| [Trackable](src/Trackable.md) | Base class for custom reactive objects (subscribable + disposable + auto-bind) |
|
|
893
897
|
| [Singleton Registry](src/singleton.md) | Global instance management: `singleton()`, `teardown()`, `teardownAll()` |
|
|
894
898
|
| [Sorting](src/Sorting.md) | Multi-column sort state with 3-click toggle cycle and apply pipeline |
|
|
895
899
|
| [Pagination](src/Pagination.md) | Page/pageSize state with array slicing |
|
|
@@ -21,14 +21,16 @@ if (resolve(projectRoot) === ownPackageRoot) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// If the developer previously opted in (files exist), auto-update them
|
|
24
|
-
const
|
|
24
|
+
const skillFile = join(projectRoot, '.claude', 'commands', 'mvc-kit.md');
|
|
25
|
+
// Also detect legacy rules file from older versions
|
|
26
|
+
const legacyRulesFile = join(projectRoot, '.claude', 'rules', 'mvc-kit.md');
|
|
25
27
|
|
|
26
|
-
if (existsSync(
|
|
28
|
+
if (existsSync(skillFile) || existsSync(legacyRulesFile)) {
|
|
27
29
|
try {
|
|
28
30
|
const { installClaude } = await import('../lib/install-claude.mjs');
|
|
29
31
|
installClaude(projectRoot);
|
|
30
32
|
console.log('');
|
|
31
|
-
console.log(' mvc-kit — Claude Code
|
|
33
|
+
console.log(' mvc-kit — Claude Code files updated');
|
|
32
34
|
console.log('');
|
|
33
35
|
} catch {
|
|
34
36
|
// Never break npm install
|
|
@@ -23,7 +23,7 @@ function printUsage() {
|
|
|
23
23
|
Usage: npx mvc-kit-setup [targets...] [options]
|
|
24
24
|
|
|
25
25
|
Targets:
|
|
26
|
-
claude Install Claude Code
|
|
26
|
+
claude Install Claude Code skill and agent into .claude/
|
|
27
27
|
cursor Copy mvc-kit rules to .cursorrules
|
|
28
28
|
copilot Copy mvc-kit instructions to .github/copilot-instructions.md
|
|
29
29
|
all Set up all targets (default)
|
|
@@ -87,9 +87,8 @@ function setupClaude() {
|
|
|
87
87
|
console.log(` ${f}`);
|
|
88
88
|
}
|
|
89
89
|
console.log('');
|
|
90
|
-
console.log('
|
|
91
|
-
console.log(' Agent:
|
|
92
|
-
console.log(' Reference: Auto-loaded via .claude/rules/mvc-kit.md\n');
|
|
90
|
+
console.log(' Skill: /project:mvc-kit (framework reference, on-demand)');
|
|
91
|
+
console.log(' Agent: mvc-kit-architect (architecture planning)\n');
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
async function setupCursor(force) {
|
|
@@ -6,6 +6,20 @@ model: sonnet
|
|
|
6
6
|
|
|
7
7
|
You are an architecture planning agent for applications built with **mvc-kit**, a TypeScript-first reactive state management library for React. You help developers plan features by deciding which classes to create, designing state shapes, and selecting sharing patterns.
|
|
8
8
|
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
For detailed, up-to-date documentation on any class or hook, search the `.md` files colocated with source in `node_modules/mvc-kit/src/`. Read these when you need specifics beyond the summary tables below.
|
|
12
|
+
|
|
13
|
+
**Core classes:** `ViewModel.md`, `Model.md`, `Collection.md`, `PersistentCollection.md`, `Resource.md`, `Service.md`, `EventBus.md`, `Channel.md`, `Controller.md`, `Trackable.md`, `singleton.md`
|
|
14
|
+
**Composable helpers:** `Sorting.md`, `Pagination.md`, `Selection.md`, `Feed.md`, `Pending.md`, `produceDraft.md`
|
|
15
|
+
**React hooks:** `react/use-local.md`, `react/use-instance.md`, `react/use-singleton.md`, `react/use-model.md`, `react/use-event-bus.md`, `react/use-teardown.md`
|
|
16
|
+
**Headless components:** `react/components/DataTable.md`, `react/components/CardList.md`, `react/components/InfiniteScroll.md`
|
|
17
|
+
|
|
18
|
+
Additional reference files in `node_modules/mvc-kit/agent-config/claude-code/skills/guide/`:
|
|
19
|
+
- `api-reference.md` — Full API reference for all classes and hooks
|
|
20
|
+
- `patterns.md` — Prescribed patterns with code examples
|
|
21
|
+
- `anti-patterns.md` — Anti-patterns to reject with fixes
|
|
22
|
+
|
|
9
23
|
## Core Classes
|
|
10
24
|
|
|
11
25
|
| Class | Role | Scope |
|
|
@@ -526,6 +526,47 @@ this.pending.enqueue(id, 'delete', async (signal) => {
|
|
|
526
526
|
|
|
527
527
|
---
|
|
528
528
|
|
|
529
|
+
## 25. Manually Reimplementing Subscribe/Notify/Dispose Boilerplate
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
// BAD — hand-rolling what Trackable provides
|
|
533
|
+
class QueryState {
|
|
534
|
+
private listeners = new Set<() => void>();
|
|
535
|
+
private _disposed = false;
|
|
536
|
+
private _data: Data | undefined;
|
|
537
|
+
|
|
538
|
+
subscribe(cb: () => void) {
|
|
539
|
+
this.listeners.add(cb);
|
|
540
|
+
return () => this.listeners.delete(cb);
|
|
541
|
+
}
|
|
542
|
+
private notify() {
|
|
543
|
+
for (const cb of this.listeners) cb();
|
|
544
|
+
}
|
|
545
|
+
dispose() {
|
|
546
|
+
this._disposed = true;
|
|
547
|
+
this.listeners.clear();
|
|
548
|
+
}
|
|
549
|
+
// ... more boilerplate
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// GOOD — extend Trackable for subscribe, notify, dispose, disposeSignal, addCleanup, and auto-binding
|
|
553
|
+
import { Trackable } from 'mvc-kit';
|
|
554
|
+
|
|
555
|
+
class QueryState extends Trackable {
|
|
556
|
+
private _data: Data | undefined;
|
|
557
|
+
get data() { return this._data; }
|
|
558
|
+
|
|
559
|
+
async load() {
|
|
560
|
+
this._data = await fetchData(this.disposeSignal);
|
|
561
|
+
this.notify();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Trackable is the base class for all composable helpers (Sorting, Selection, Feed, Pagination, Pending). Use it whenever you need a subscribable + disposable object.
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
529
570
|
## 24. Using collection.optimistic() Rollback with Pending
|
|
530
571
|
|
|
531
572
|
```typescript
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Core classes and utilities
|
|
7
7
|
import { ViewModel, Model, Collection, PersistentCollection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
|
|
8
8
|
import { Sorting, Pagination, Selection, Feed, Pending } from 'mvc-kit';
|
|
9
|
+
import { Trackable, bindPublicMethods, produceDraft } from 'mvc-kit';
|
|
9
10
|
import { singleton, hasSingleton, teardown, teardownAll } from 'mvc-kit';
|
|
10
11
|
import { HttpError, isAbortError, classifyError } from 'mvc-kit';
|
|
11
12
|
import type { Subscribable, Disposable, Initializable, Listener, Updater, ValidationErrors, TaskState, EventSource, EventPayload, AppError, AsyncMethodKeys, ResourceAsyncMethodKeys, ChannelStatus, SortDescriptor, FeedPage, PendingOperation, PendingEntry } from 'mvc-kit';
|
|
@@ -36,7 +37,8 @@ new MyViewModel(initialState: S)
|
|
|
36
37
|
### State
|
|
37
38
|
- `state: S` — Current frozen state. Read `state.x` for raw values.
|
|
38
39
|
- `set(partial: Partial<S>)` — Merge partial state. Skips if no values change.
|
|
39
|
-
- `set(updater: (prev: S) => Partial<S>)` — Functional update.
|
|
40
|
+
- `set(updater: (prev: S) => Partial<S>)` — Functional update (return a partial).
|
|
41
|
+
- `set(drafter: (draft: S) => void)` — Draft mode: mutate the draft proxy, return nothing. Uses `produceDraft` internally.
|
|
40
42
|
- `reset(newState?: S)` — Tear down lifecycle, re-initialize. Clears async tracking, re-runs `onInit()`.
|
|
41
43
|
|
|
42
44
|
### Computed Getters
|
|
@@ -93,6 +95,7 @@ new MyModel(initialState: S)
|
|
|
93
95
|
### State & Validation
|
|
94
96
|
- `state: S` — Current state.
|
|
95
97
|
- `set(partial: Partial<S>)` — Update state, re-validates.
|
|
98
|
+
- `set(drafter: (draft: S) => void)` — Draft mode: mutate the draft, return nothing.
|
|
96
99
|
- `errors: ValidationErrors<S>` — `Partial<Record<keyof S, string>>`.
|
|
97
100
|
- `valid: boolean` — True when `errors` is empty.
|
|
98
101
|
- `dirty: boolean` — True when state differs from committed state.
|
|
@@ -285,9 +288,70 @@ No state, no getters, no async tracking.
|
|
|
285
288
|
|
|
286
289
|
---
|
|
287
290
|
|
|
291
|
+
## Trackable
|
|
292
|
+
|
|
293
|
+
Base class for custom reactive objects. Provides subscribe/notify, disposal lifecycle (disposeSignal, addCleanup, onDispose), and auto-bound methods — the same building blocks used by Sorting, Selection, Feed, Pagination, and Pending (all extend Trackable).
|
|
294
|
+
|
|
295
|
+
Use Trackable when integrating third-party SDKs, custom query objects, or any reactive state that doesn't fit ViewModel's state/getter model.
|
|
296
|
+
|
|
297
|
+
### Subscribe & Notify
|
|
298
|
+
- `subscribe(cb: () => void): () => void` — Subscribe to change notifications. Duck-typed contract recognized by ViewModel auto-tracking.
|
|
299
|
+
- `notify(): void` — Protected. Notify all subscribers that state changed.
|
|
300
|
+
|
|
301
|
+
### Lifecycle & Cleanup
|
|
302
|
+
- `disposed: boolean` — Whether this instance has been disposed.
|
|
303
|
+
- `disposeSignal: AbortSignal` — Lazily created, auto-aborted on `dispose()`.
|
|
304
|
+
- `addCleanup(fn: () => void): void` — Protected. Register teardown callback.
|
|
305
|
+
- `onDispose(): void` — Protected lifecycle hook called at the end of `dispose()`.
|
|
306
|
+
- `dispose(): void` — Idempotent. Aborts signal, runs cleanups, clears subscribers, calls `onDispose`.
|
|
307
|
+
|
|
308
|
+
### Auto-Binding
|
|
309
|
+
Public methods are auto-bound in the constructor via `bindPublicMethods()`, so they can be passed point-free as callbacks.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## bindPublicMethods(instance, stopPrototype?, exclude?)
|
|
314
|
+
|
|
315
|
+
Utility function that auto-binds all public methods on an instance. Used internally by Trackable, Collection, Service, EventBus, Channel, Controller, and Model. Available as a named export for custom classes that don't extend Trackable.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { bindPublicMethods } from 'mvc-kit';
|
|
319
|
+
|
|
320
|
+
class MyClass {
|
|
321
|
+
constructor() {
|
|
322
|
+
bindPublicMethods(this);
|
|
323
|
+
}
|
|
324
|
+
greet() { return 'hello'; }
|
|
325
|
+
}
|
|
326
|
+
const { greet } = new MyClass();
|
|
327
|
+
greet(); // 'hello' — no lost `this`
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## produceDraft(state, mutator)
|
|
333
|
+
|
|
334
|
+
Copy-on-write draft utility. Creates a proxy of a frozen state object, runs a mutator, and returns only changed top-level keys as a `Partial<S>` (or `null` if nothing changed). Used internally by `ViewModel.set()` and `Model.set()` for draft mode.
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { produceDraft } from 'mvc-kit';
|
|
338
|
+
|
|
339
|
+
const changes = produceDraft(state, draft => {
|
|
340
|
+
draft.user.name = 'Bob';
|
|
341
|
+
});
|
|
342
|
+
// { user: { name: 'Bob', ...unchanged fields } } or null
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
- Nested plain objects use structural sharing (unchanged subtrees keep original references)
|
|
346
|
+
- Same-value assignment is a no-op (returns `null`)
|
|
347
|
+
- Arrays must be replaced via assignment (`draft.items = [...]`), not mutated in place
|
|
348
|
+
- Only POJOs are proxied; class instances, Dates, Maps pass through as-is
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
288
352
|
## Composable Helpers
|
|
289
353
|
|
|
290
|
-
Plain classes
|
|
354
|
+
Plain classes that extend `Trackable` — auto-tracked when declared as ViewModel instance properties. Each has an `apply()` method that transforms arrays. Helpers manage state; ViewModels compose them in getters.
|
|
291
355
|
|
|
292
356
|
### Sorting\<T\>
|
|
293
357
|
|
|
@@ -387,6 +387,58 @@ Components and helpers are independent — use helpers without components, or co
|
|
|
387
387
|
|
|
388
388
|
---
|
|
389
389
|
|
|
390
|
+
## Custom Trackable Pattern
|
|
391
|
+
|
|
392
|
+
Extend `Trackable` when you need a reactive object that integrates with ViewModel auto-tracking but doesn't fit the state/getter model. Call `notify()` after mutating internal state. The object gets `subscribe()`, `dispose()`, `disposeSignal`, `addCleanup()`, and auto-bound methods for free.
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { Trackable } from 'mvc-kit';
|
|
396
|
+
|
|
397
|
+
class RPCQuery<Data> extends Trackable {
|
|
398
|
+
private _data: Data | undefined;
|
|
399
|
+
private _loading = false;
|
|
400
|
+
private _error: string | null = null;
|
|
401
|
+
|
|
402
|
+
get data() { return this._data; }
|
|
403
|
+
get loading() { return this._loading; }
|
|
404
|
+
get error() { return this._error; }
|
|
405
|
+
|
|
406
|
+
async execute(params: Record<string, unknown>): Promise<void> {
|
|
407
|
+
this._loading = true;
|
|
408
|
+
this._error = null;
|
|
409
|
+
this.notify();
|
|
410
|
+
try {
|
|
411
|
+
this._data = await rpcClient.call(params, { signal: this.disposeSignal });
|
|
412
|
+
} catch (e) {
|
|
413
|
+
this._error = (e as Error).message;
|
|
414
|
+
throw e;
|
|
415
|
+
} finally {
|
|
416
|
+
this._loading = false;
|
|
417
|
+
this.notify();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Used as a ViewModel property — auto-tracked like any composable helper:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
class UsersVM extends ViewModel {
|
|
427
|
+
readonly query = new RPCQuery<User[]>();
|
|
428
|
+
|
|
429
|
+
get users() { return this.query.data ?? []; }
|
|
430
|
+
get loading() { return this.query.loading; }
|
|
431
|
+
|
|
432
|
+
protected onInit() { this.query.execute({ limit: 50 }); }
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**When to use Trackable vs ViewModel:**
|
|
437
|
+
- Trackable: custom reactive object used as a ViewModel property (query wrapper, SDK adapter, animation state).
|
|
438
|
+
- ViewModel: component-scoped reactive state with computed getters, async tracking, and lifecycle hooks.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
390
442
|
## Persistence Pattern
|
|
391
443
|
|
|
392
444
|
```typescript
|
|
@@ -15,11 +15,12 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
|
|
|
15
15
|
| `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
|
|
16
16
|
| `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
|
|
17
17
|
| `Controller` | Stateless multi-ViewModel orchestrator (rare) | Component-scoped |
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
18
|
+
| `Trackable` | Base class for custom reactive objects — subscribable + disposable + auto-bind | ViewModel property / `useLocal` |
|
|
19
|
+
| `Sorting<T>` | Multi-column sort state with 3-click toggle cycle and `apply()` pipeline (extends Trackable) | ViewModel property |
|
|
20
|
+
| `Pagination` | Page/pageSize state with `apply()` slicing (extends Trackable) | ViewModel property |
|
|
21
|
+
| `Selection<K>` | Key-based selection set with toggle/select-all semantics (extends Trackable) | ViewModel property |
|
|
22
|
+
| `Feed<T>` | Cursor + hasMore + item accumulation for server-side pagination (extends Trackable) | ViewModel property |
|
|
23
|
+
| `Pending<K, Meta?>` | Per-item operation queue with retry + status tracking + optional typed metadata (extends Trackable) | Resource property |
|
|
23
24
|
|
|
24
25
|
## Headless React Components (`mvc-kit/react`)
|
|
25
26
|
|
|
@@ -34,6 +35,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
|
|
|
34
35
|
```typescript
|
|
35
36
|
import { ViewModel, Model, Collection, PersistentCollection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
|
|
36
37
|
import { Sorting, Pagination, Selection, Feed, Pending } from 'mvc-kit';
|
|
38
|
+
import { Trackable, bindPublicMethods } from 'mvc-kit';
|
|
37
39
|
import { singleton, teardownAll, HttpError, isAbortError, classifyError } from 'mvc-kit';
|
|
38
40
|
import { useLocal, useSingleton, useInstance, useModel, useModelRef, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
|
|
39
41
|
import { DataTable, CardList, InfiniteScroll } from 'mvc-kit/react';
|
|
@@ -288,6 +290,7 @@ test('example', () => {
|
|
|
288
290
|
- Pass-through Service wrapping a typed API client → call the client directly from Resource
|
|
289
291
|
- `addCleanup` for `channel.on()`/`bus.on()` subscriptions → use `listenTo()` (auto-cleanup on dispose and reset)
|
|
290
292
|
- Missing `hydrate()` for async adapters (IndexedDB, NativeCollection) → call `hydrate()` in `onInit()` before accessing data
|
|
293
|
+
- Manually reimplementing subscribe/notify/dispose boilerplate → extend `Trackable`
|
|
291
294
|
|
|
292
295
|
## Resource Pattern
|
|
293
296
|
|
|
@@ -325,6 +328,7 @@ class UsersVM extends ViewModel<{ search: string }> {
|
|
|
325
328
|
- Cross-cutting events → **EventBus**
|
|
326
329
|
- Persistent connection → **Channel**
|
|
327
330
|
- Coordinates multiple ViewModels → **Controller** (rare)
|
|
331
|
+
- Custom reactive object (SDK wrapper, query, animation) → **Trackable** (base class for helpers)
|
|
328
332
|
- Sort/paginate/select on a list → **Sorting/Pagination/Selection** helpers
|
|
329
333
|
- Cursor-based server pagination → **Feed** helper
|
|
330
334
|
- Per-item operation retry with status → **Pending** helper (on Resource, not ViewModel)
|
|
@@ -15,11 +15,12 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
|
|
|
15
15
|
| `EventBus<E>` | Typed pub/sub for cross-cutting events | Singleton |
|
|
16
16
|
| `Channel<M>` | Persistent connection (WebSocket/SSE) with auto-reconnect | Singleton |
|
|
17
17
|
| `Controller` | Stateless multi-ViewModel orchestrator (rare) | Component-scoped |
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
18
|
+
| `Trackable` | Base class for custom reactive objects — subscribable + disposable + auto-bind | ViewModel property / `useLocal` |
|
|
19
|
+
| `Sorting<T>` | Multi-column sort state with 3-click toggle cycle and `apply()` pipeline (extends Trackable) | ViewModel property |
|
|
20
|
+
| `Pagination` | Page/pageSize state with `apply()` slicing (extends Trackable) | ViewModel property |
|
|
21
|
+
| `Selection<K>` | Key-based selection set with toggle/select-all semantics (extends Trackable) | ViewModel property |
|
|
22
|
+
| `Feed<T>` | Cursor + hasMore + item accumulation for server-side pagination (extends Trackable) | ViewModel property |
|
|
23
|
+
| `Pending<K, Meta?>` | Per-item operation queue with retry + status tracking + optional typed metadata (extends Trackable) | Resource property |
|
|
23
24
|
|
|
24
25
|
## Headless React Components (`mvc-kit/react`)
|
|
25
26
|
|
|
@@ -34,6 +35,7 @@ This project uses **mvc-kit**, a zero-dependency TypeScript-first reactive state
|
|
|
34
35
|
```typescript
|
|
35
36
|
import { ViewModel, Model, Collection, PersistentCollection, Resource, Controller, Service, EventBus, Channel } from 'mvc-kit';
|
|
36
37
|
import { Sorting, Pagination, Selection, Feed, Pending } from 'mvc-kit';
|
|
38
|
+
import { Trackable, bindPublicMethods } from 'mvc-kit';
|
|
37
39
|
import { singleton, teardownAll, HttpError, isAbortError, classifyError } from 'mvc-kit';
|
|
38
40
|
import { useLocal, useSingleton, useInstance, useModel, useModelRef, useField, useEvent, useEmit, useResolve, useTeardown, Provider } from 'mvc-kit/react';
|
|
39
41
|
import { DataTable, CardList, InfiniteScroll } from 'mvc-kit/react';
|
|
@@ -288,6 +290,7 @@ test('example', () => {
|
|
|
288
290
|
- Pass-through Service wrapping a typed API client → call the client directly from Resource
|
|
289
291
|
- `addCleanup` for `channel.on()`/`bus.on()` subscriptions → use `listenTo()` (auto-cleanup on dispose and reset)
|
|
290
292
|
- Missing `hydrate()` for async adapters (IndexedDB, NativeCollection) → call `hydrate()` in `onInit()` before accessing data
|
|
293
|
+
- Manually reimplementing subscribe/notify/dispose boilerplate → extend `Trackable`
|
|
291
294
|
|
|
292
295
|
## Resource Pattern
|
|
293
296
|
|
|
@@ -325,6 +328,7 @@ class UsersVM extends ViewModel<{ search: string }> {
|
|
|
325
328
|
- Cross-cutting events → **EventBus**
|
|
326
329
|
- Persistent connection → **Channel**
|
|
327
330
|
- Coordinates multiple ViewModels → **Controller** (rare)
|
|
331
|
+
- Custom reactive object (SDK wrapper, query, animation) → **Trackable** (base class for helpers)
|
|
328
332
|
- Sort/paginate/select on a list → **Sorting/Pagination/Selection** helpers
|
|
329
333
|
- Cursor-based server pagination → **Feed** helper
|
|
330
334
|
- Per-item operation retry with status → **Pending** helper (on Resource, not ViewModel)
|
|
@@ -24,33 +24,29 @@ function ensureDir(dir) {
|
|
|
24
24
|
* Install Claude Code integration files into a project's .claude/ directory.
|
|
25
25
|
*
|
|
26
26
|
* Creates:
|
|
27
|
-
* .claude/
|
|
28
|
-
* .claude/
|
|
29
|
-
* .claude/commands/mvc-kit-review.md — Review command (/project:mvc-kit-review)
|
|
30
|
-
* .claude/agents/mvc-kit-architect.md — Architecture planning agent
|
|
27
|
+
* .claude/commands/mvc-kit.md — Framework reference skill (on-demand via /project:mvc-kit)
|
|
28
|
+
* .claude/agents/mvc-kit-architect.md — Architecture planning agent
|
|
31
29
|
*
|
|
32
30
|
* @param {string} projectRoot — Absolute path to the consuming project's root
|
|
33
31
|
* @returns {{ files: string[] }} — List of created/updated file paths (relative to projectRoot)
|
|
34
32
|
*/
|
|
35
33
|
export function installClaude(projectRoot) {
|
|
36
34
|
const claudeDir = join(projectRoot, '.claude');
|
|
37
|
-
const rulesDir = join(claudeDir, 'rules');
|
|
38
35
|
const commandsDir = join(claudeDir, 'commands');
|
|
39
36
|
const agentsDir = join(claudeDir, 'agents');
|
|
40
37
|
|
|
41
|
-
ensureDir(rulesDir);
|
|
42
38
|
ensureDir(commandsDir);
|
|
43
39
|
ensureDir(agentsDir);
|
|
44
40
|
|
|
45
41
|
const files = [];
|
|
46
42
|
|
|
47
|
-
// 1.
|
|
43
|
+
// 1. Guide skill — framework reference (on-demand, not always-loaded)
|
|
48
44
|
const guideSkill = readFileSync(join(SKILLS_DIR, 'guide', 'SKILL.md'), 'utf-8');
|
|
49
45
|
const guideBody = stripFrontmatter(guideSkill)
|
|
50
46
|
// Remove the "Supporting Files" section — replaced by "Detailed Reference" below
|
|
51
47
|
.replace(/\n## Supporting Files[\s\S]*$/, '');
|
|
52
48
|
|
|
53
|
-
const
|
|
49
|
+
const skillContent = AUTO_HEADER + guideBody + `
|
|
54
50
|
|
|
55
51
|
## Detailed Reference
|
|
56
52
|
|
|
@@ -60,34 +56,15 @@ For complete API details, patterns, and anti-patterns, read the files in:
|
|
|
60
56
|
- \`api-reference.md\` — Full API reference for all classes and hooks
|
|
61
57
|
- \`patterns.md\` — Prescribed patterns with code examples
|
|
62
58
|
- \`anti-patterns.md\` — Anti-patterns to reject with fixes
|
|
63
|
-
`;
|
|
64
|
-
|
|
65
|
-
writeFileSync(join(rulesDir, 'mvc-kit.md'), rulesContent, 'utf-8');
|
|
66
|
-
files.push('.claude/rules/mvc-kit.md');
|
|
67
|
-
|
|
68
|
-
// 2. Scaffold command
|
|
69
|
-
const scaffoldSkill = readFileSync(join(SKILLS_DIR, 'scaffold', 'SKILL.md'), 'utf-8');
|
|
70
|
-
const scaffoldBody = stripFrontmatter(scaffoldSkill)
|
|
71
|
-
.replace(
|
|
72
|
-
'Read the template file from `templates/<type>.md` in this skill directory.',
|
|
73
|
-
'Read the template file from `node_modules/mvc-kit/agent-config/claude-code/skills/scaffold/templates/<type>.md`.'
|
|
74
|
-
);
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// 3. Review command
|
|
80
|
-
const reviewSkill = readFileSync(join(SKILLS_DIR, 'review', 'SKILL.md'), 'utf-8');
|
|
81
|
-
const reviewBody = stripFrontmatter(reviewSkill)
|
|
82
|
-
.replaceAll(
|
|
83
|
-
'`checklist.md`',
|
|
84
|
-
'`node_modules/mvc-kit/agent-config/claude-code/skills/review/checklist.md`'
|
|
85
|
-
);
|
|
60
|
+
For detailed per-class documentation, read the \`.md\` files colocated with source in:
|
|
61
|
+
\`node_modules/mvc-kit/src/\`
|
|
62
|
+
`;
|
|
86
63
|
|
|
87
|
-
writeFileSync(join(commandsDir, 'mvc-kit
|
|
88
|
-
files.push('.claude/commands/mvc-kit
|
|
64
|
+
writeFileSync(join(commandsDir, 'mvc-kit.md'), skillContent, 'utf-8');
|
|
65
|
+
files.push('.claude/commands/mvc-kit.md');
|
|
89
66
|
|
|
90
|
-
//
|
|
67
|
+
// 2. Architect agent
|
|
91
68
|
const architectAgent = readFileSync(join(AGENTS_DIR, 'mvc-kit-architect.md'), 'utf-8');
|
|
92
69
|
writeFileSync(join(agentsDir, 'mvc-kit-architect.md'), AUTO_HEADER + architectAgent, 'utf-8');
|
|
93
70
|
files.push('.claude/agents/mvc-kit-architect.md');
|
package/dist/Feed.cjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
4
|
-
class Feed {
|
|
3
|
+
const Trackable = require("./Trackable.cjs");
|
|
4
|
+
class Feed extends Trackable.Trackable {
|
|
5
5
|
_cursor = null;
|
|
6
6
|
_hasMore = true;
|
|
7
7
|
_items = Object.freeze([]);
|
|
8
|
-
_listeners = /* @__PURE__ */ new Set();
|
|
9
8
|
constructor() {
|
|
10
|
-
|
|
9
|
+
super();
|
|
11
10
|
}
|
|
12
11
|
// ── Readable state ──
|
|
13
12
|
/** Current cursor position for the next page fetch, or null if at the beginning. */
|
|
@@ -31,59 +30,48 @@ class Feed {
|
|
|
31
30
|
setResult(result) {
|
|
32
31
|
this._hasMore = result.hasMore;
|
|
33
32
|
this._cursor = result.cursor ?? null;
|
|
34
|
-
this.
|
|
33
|
+
this.notify();
|
|
35
34
|
}
|
|
36
35
|
/** Append page items and update cursor/hasMore. */
|
|
37
36
|
appendPage(page) {
|
|
38
37
|
this._items = Object.freeze([...this._items, ...page.items]);
|
|
39
38
|
this._hasMore = page.hasMore;
|
|
40
39
|
this._cursor = page.cursor ?? null;
|
|
41
|
-
this.
|
|
40
|
+
this.notify();
|
|
42
41
|
}
|
|
43
42
|
/** Prepend page items and update cursor/hasMore. */
|
|
44
43
|
prependPage(page) {
|
|
45
44
|
this._items = Object.freeze([...page.items, ...this._items]);
|
|
46
45
|
this._hasMore = page.hasMore;
|
|
47
46
|
this._cursor = page.cursor ?? null;
|
|
48
|
-
this.
|
|
47
|
+
this.notify();
|
|
49
48
|
}
|
|
50
49
|
/** Add items without affecting cursor/hasMore. */
|
|
51
50
|
push(...items) {
|
|
52
51
|
if (items.length === 0) return;
|
|
53
52
|
this._items = Object.freeze([...this._items, ...items]);
|
|
54
|
-
this.
|
|
53
|
+
this.notify();
|
|
55
54
|
}
|
|
56
55
|
/** Remove items that don't match the predicate. No-op if nothing is filtered out. */
|
|
57
56
|
filter(predicate) {
|
|
58
57
|
const filtered = this._items.filter(predicate);
|
|
59
58
|
if (filtered.length === this._items.length) return;
|
|
60
59
|
this._items = Object.freeze(filtered);
|
|
61
|
-
this.
|
|
60
|
+
this.notify();
|
|
62
61
|
}
|
|
63
62
|
/** Replace all items and update cursor/hasMore atomically. Ideal for pull-to-refresh. */
|
|
64
63
|
replacePage(page) {
|
|
65
64
|
this._items = Object.freeze([...page.items]);
|
|
66
65
|
this._hasMore = page.hasMore;
|
|
67
66
|
this._cursor = page.cursor ?? null;
|
|
68
|
-
this.
|
|
67
|
+
this.notify();
|
|
69
68
|
}
|
|
70
69
|
/** Reset to initial empty state with hasMore=true. */
|
|
71
70
|
reset() {
|
|
72
71
|
this._cursor = null;
|
|
73
72
|
this._hasMore = true;
|
|
74
73
|
this._items = Object.freeze([]);
|
|
75
|
-
this.
|
|
76
|
-
}
|
|
77
|
-
// ── Subscribable interface ──
|
|
78
|
-
/** Subscribe to feed state changes. Returns an unsubscribe function. */
|
|
79
|
-
subscribe(cb) {
|
|
80
|
-
this._listeners.add(cb);
|
|
81
|
-
return () => {
|
|
82
|
-
this._listeners.delete(cb);
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
_notify() {
|
|
86
|
-
for (const cb of this._listeners) cb();
|
|
74
|
+
this.notify();
|
|
87
75
|
}
|
|
88
76
|
}
|
|
89
77
|
exports.Feed = Feed;
|
package/dist/Feed.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Feed.cjs","sources":["../src/Feed.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Feed.cjs","sources":["../src/Feed.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/** Represents a page of items from a paginated API response. */\nexport interface FeedPage<T> {\n items: T[];\n hasMore: boolean;\n cursor?: string | null;\n}\n\n/**\n * Cursor-based pagination state for server-side paginated feeds.\n * Accumulates items across pages, tracks cursor position and hasMore flag.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Feed<T = unknown> extends Trackable {\n private _cursor: string | null = null;\n private _hasMore: boolean = true;\n private _items: readonly T[] = Object.freeze([] as T[]);\n\n constructor() {\n super();\n }\n\n // ── Readable state ──\n\n /** Current cursor position for the next page fetch, or null if at the beginning. */\n get cursor(): string | null {\n return this._cursor;\n }\n\n /** Whether more pages are available from the server. */\n get hasMore(): boolean {\n return this._hasMore;\n }\n\n /** Accumulated items across all loaded pages. */\n get items(): readonly T[] {\n return this._items;\n }\n\n /** Total number of accumulated items. */\n get count(): number {\n return this._items.length;\n }\n\n // ── Actions ──\n\n /** Update cursor/hasMore only (backward-compatible, does NOT affect items). */\n setResult(result: { hasMore: boolean; cursor?: string | null }): void {\n this._hasMore = result.hasMore;\n this._cursor = result.cursor ?? null;\n this.notify();\n }\n\n /** Append page items and update cursor/hasMore. */\n appendPage(page: FeedPage<T>): void {\n this._items = Object.freeze([...this._items, ...page.items]);\n this._hasMore = page.hasMore;\n this._cursor = page.cursor ?? null;\n this.notify();\n }\n\n /** Prepend page items and update cursor/hasMore. */\n prependPage(page: FeedPage<T>): void {\n this._items = Object.freeze([...page.items, ...this._items]);\n this._hasMore = page.hasMore;\n this._cursor = page.cursor ?? null;\n this.notify();\n }\n\n /** Add items without affecting cursor/hasMore. */\n push(...items: T[]): void {\n if (items.length === 0) return;\n this._items = Object.freeze([...this._items, ...items]);\n this.notify();\n }\n\n /** Remove items that don't match the predicate. No-op if nothing is filtered out. */\n filter(predicate: (item: T) => boolean): void {\n const filtered = this._items.filter(predicate);\n if (filtered.length === this._items.length) return;\n this._items = Object.freeze(filtered);\n this.notify();\n }\n\n /** Replace all items and update cursor/hasMore atomically. Ideal for pull-to-refresh. */\n replacePage(page: FeedPage<T>): void {\n this._items = Object.freeze([...page.items]);\n this._hasMore = page.hasMore;\n this._cursor = page.cursor ?? null;\n this.notify();\n }\n\n /** Reset to initial empty state with hasMore=true. */\n reset(): void {\n this._cursor = null;\n this._hasMore = true;\n this._items = Object.freeze([] as T[]);\n this.notify();\n }\n}\n"],"names":["Trackable"],"mappings":";;;AAcO,MAAM,aAA0BA,UAAAA,UAAU;AAAA,EACvC,UAAyB;AAAA,EACzB,WAAoB;AAAA,EACpB,SAAuB,OAAO,OAAO,EAAS;AAAA,EAEtD,cAAc;AACZ,UAAA;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,IAAI,SAAwB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,UAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,QAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,QAAgB;AAClB,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA;AAAA,EAKA,UAAU,QAA4D;AACpE,SAAK,WAAW,OAAO;AACvB,SAAK,UAAU,OAAO,UAAU;AAChC,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAW,MAAyB;AAClC,SAAK,SAAS,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,GAAG,KAAK,KAAK,CAAC;AAC3D,SAAK,WAAW,KAAK;AACrB,SAAK,UAAU,KAAK,UAAU;AAC9B,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAyB;AACnC,SAAK,SAAS,OAAO,OAAO,CAAC,GAAG,KAAK,OAAO,GAAG,KAAK,MAAM,CAAC;AAC3D,SAAK,WAAW,KAAK;AACrB,SAAK,UAAU,KAAK,UAAU;AAC9B,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAQ,OAAkB;AACxB,QAAI,MAAM,WAAW,EAAG;AACxB,SAAK,SAAS,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,GAAG,KAAK,CAAC;AACtD,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,OAAO,WAAuC;AAC5C,UAAM,WAAW,KAAK,OAAO,OAAO,SAAS;AAC7C,QAAI,SAAS,WAAW,KAAK,OAAO,OAAQ;AAC5C,SAAK,SAAS,OAAO,OAAO,QAAQ;AACpC,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAyB;AACnC,SAAK,SAAS,OAAO,OAAO,CAAC,GAAG,KAAK,KAAK,CAAC;AAC3C,SAAK,WAAW,KAAK;AACrB,SAAK,UAAU,KAAK,UAAU;AAC9B,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,UAAU;AACf,SAAK,WAAW;AAChB,SAAK,SAAS,OAAO,OAAO,CAAA,CAAS;AACrC,SAAK,OAAA;AAAA,EACP;AACF;;"}
|
package/dist/Feed.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Trackable } from './Trackable';
|
|
1
2
|
/** Represents a page of items from a paginated API response. */
|
|
2
3
|
export interface FeedPage<T> {
|
|
3
4
|
items: T[];
|
|
@@ -9,11 +10,10 @@ export interface FeedPage<T> {
|
|
|
9
10
|
* Accumulates items across pages, tracks cursor position and hasMore flag.
|
|
10
11
|
* Subscribable — auto-tracked when used as a ViewModel property.
|
|
11
12
|
*/
|
|
12
|
-
export declare class Feed<T = unknown> {
|
|
13
|
+
export declare class Feed<T = unknown> extends Trackable {
|
|
13
14
|
private _cursor;
|
|
14
15
|
private _hasMore;
|
|
15
16
|
private _items;
|
|
16
|
-
private _listeners;
|
|
17
17
|
constructor();
|
|
18
18
|
/** Current cursor position for the next page fetch, or null if at the beginning. */
|
|
19
19
|
get cursor(): string | null;
|
|
@@ -40,8 +40,5 @@ export declare class Feed<T = unknown> {
|
|
|
40
40
|
replacePage(page: FeedPage<T>): void;
|
|
41
41
|
/** Reset to initial empty state with hasMore=true. */
|
|
42
42
|
reset(): void;
|
|
43
|
-
/** Subscribe to feed state changes. Returns an unsubscribe function. */
|
|
44
|
-
subscribe(cb: () => void): () => void;
|
|
45
|
-
private _notify;
|
|
46
43
|
}
|
|
47
44
|
//# sourceMappingURL=Feed.d.ts.map
|
package/dist/Feed.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Feed.d.ts","sourceRoot":"","sources":["../src/Feed.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Feed.d.ts","sourceRoot":"","sources":["../src/Feed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,gEAAgE;AAChE,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;GAIG;AACH,qBAAa,IAAI,CAAC,CAAC,GAAG,OAAO,CAAE,SAAQ,SAAS;IAC9C,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,MAAM,CAA0C;;IAQxD,oFAAoF;IACpF,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,CAE1B;IAED,wDAAwD;IACxD,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,iDAAiD;IACjD,IAAI,KAAK,IAAI,SAAS,CAAC,EAAE,CAExB;IAED,yCAAyC;IACzC,IAAI,KAAK,IAAI,MAAM,CAElB;IAID,+EAA+E;IAC/E,SAAS,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAMrE,mDAAmD;IACnD,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;IAOnC,oDAAoD;IACpD,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;IAOpC,kDAAkD;IAClD,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAMzB,qFAAqF;IACrF,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,IAAI;IAO7C,yFAAyF;IACzF,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI;IAOpC,sDAAsD;IACtD,KAAK,IAAI,IAAI;CAMd"}
|