mvc-kit 2.11.1 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +4 -0
  2. package/agent-config/claude-code/skills/guide/anti-patterns.md +41 -0
  3. package/agent-config/claude-code/skills/guide/api-reference.md +43 -1
  4. package/agent-config/claude-code/skills/guide/patterns.md +52 -0
  5. package/agent-config/copilot/copilot-instructions.md +9 -5
  6. package/agent-config/cursor/cursorrules +9 -5
  7. package/dist/Feed.cjs +10 -22
  8. package/dist/Feed.cjs.map +1 -1
  9. package/dist/Feed.d.ts +2 -5
  10. package/dist/Feed.d.ts.map +1 -1
  11. package/dist/Feed.js +10 -22
  12. package/dist/Feed.js.map +1 -1
  13. package/dist/Pagination.cjs +8 -20
  14. package/dist/Pagination.cjs.map +1 -1
  15. package/dist/Pagination.d.ts +2 -5
  16. package/dist/Pagination.d.ts.map +1 -1
  17. package/dist/Pagination.js +8 -20
  18. package/dist/Pagination.js.map +1 -1
  19. package/dist/Pending.cjs +26 -39
  20. package/dist/Pending.cjs.map +1 -1
  21. package/dist/Pending.d.ts +5 -9
  22. package/dist/Pending.d.ts.map +1 -1
  23. package/dist/Pending.js +26 -39
  24. package/dist/Pending.js.map +1 -1
  25. package/dist/Selection.cjs +5 -13
  26. package/dist/Selection.cjs.map +1 -1
  27. package/dist/Selection.d.ts +2 -4
  28. package/dist/Selection.d.ts.map +1 -1
  29. package/dist/Selection.js +5 -13
  30. package/dist/Selection.js.map +1 -1
  31. package/dist/Sorting.cjs +7 -19
  32. package/dist/Sorting.cjs.map +1 -1
  33. package/dist/Sorting.d.ts +2 -5
  34. package/dist/Sorting.d.ts.map +1 -1
  35. package/dist/Sorting.js +7 -19
  36. package/dist/Sorting.js.map +1 -1
  37. package/dist/Trackable.cjs +81 -0
  38. package/dist/Trackable.cjs.map +1 -0
  39. package/dist/Trackable.d.ts +82 -0
  40. package/dist/Trackable.d.ts.map +1 -0
  41. package/dist/Trackable.js +81 -0
  42. package/dist/Trackable.js.map +1 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/mvc-kit.cjs +4 -0
  46. package/dist/mvc-kit.cjs.map +1 -1
  47. package/dist/mvc-kit.js +4 -0
  48. package/dist/mvc-kit.js.map +1 -1
  49. package/dist/react/guards.cjs +2 -0
  50. package/dist/react/guards.cjs.map +1 -1
  51. package/dist/react/guards.d.ts +4 -0
  52. package/dist/react/guards.d.ts.map +1 -1
  53. package/dist/react/guards.js +3 -1
  54. package/dist/react/guards.js.map +1 -1
  55. package/dist/react/use-local.cjs +5 -0
  56. package/dist/react/use-local.cjs.map +1 -1
  57. package/dist/react/use-local.d.ts.map +1 -1
  58. package/dist/react/use-local.js +6 -1
  59. package/dist/react/use-local.js.map +1 -1
  60. package/dist/react/use-singleton.cjs +5 -0
  61. package/dist/react/use-singleton.cjs.map +1 -1
  62. package/dist/react/use-singleton.d.ts.map +1 -1
  63. package/dist/react/use-singleton.js +6 -1
  64. package/dist/react/use-singleton.js.map +1 -1
  65. package/dist/react/use-subscribe-only.cjs +25 -0
  66. package/dist/react/use-subscribe-only.cjs.map +1 -0
  67. package/dist/react/use-subscribe-only.d.ts +9 -0
  68. package/dist/react/use-subscribe-only.d.ts.map +1 -0
  69. package/dist/react/use-subscribe-only.js +25 -0
  70. package/dist/react/use-subscribe-only.js.map +1 -0
  71. package/package.json +1 -1
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 |
@@ -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 } 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';
@@ -285,9 +286,50 @@ No state, no getters, no async tracking.
285
286
 
286
287
  ---
287
288
 
289
+ ## Trackable
290
+
291
+ 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).
292
+
293
+ Use Trackable when integrating third-party SDKs, custom query objects, or any reactive state that doesn't fit ViewModel's state/getter model.
294
+
295
+ ### Subscribe & Notify
296
+ - `subscribe(cb: () => void): () => void` — Subscribe to change notifications. Duck-typed contract recognized by ViewModel auto-tracking.
297
+ - `notify(): void` — Protected. Notify all subscribers that state changed.
298
+
299
+ ### Lifecycle & Cleanup
300
+ - `disposed: boolean` — Whether this instance has been disposed.
301
+ - `disposeSignal: AbortSignal` — Lazily created, auto-aborted on `dispose()`.
302
+ - `addCleanup(fn: () => void): void` — Protected. Register teardown callback.
303
+ - `onDispose(): void` — Protected lifecycle hook called at the end of `dispose()`.
304
+ - `dispose(): void` — Idempotent. Aborts signal, runs cleanups, clears subscribers, calls `onDispose`.
305
+
306
+ ### Auto-Binding
307
+ Public methods are auto-bound in the constructor via `bindPublicMethods()`, so they can be passed point-free as callbacks.
308
+
309
+ ---
310
+
311
+ ## bindPublicMethods(instance, stopPrototype?, exclude?)
312
+
313
+ 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.
314
+
315
+ ```typescript
316
+ import { bindPublicMethods } from 'mvc-kit';
317
+
318
+ class MyClass {
319
+ constructor() {
320
+ bindPublicMethods(this);
321
+ }
322
+ greet() { return 'hello'; }
323
+ }
324
+ const { greet } = new MyClass();
325
+ greet(); // 'hello' — no lost `this`
326
+ ```
327
+
328
+ ---
329
+
288
330
  ## Composable Helpers
289
331
 
290
- Plain classes with `subscribe()` — auto-tracked when declared as ViewModel instance properties. Each has an `apply()` method that transforms arrays. Helpers manage state; ViewModels compose them in getters.
332
+ 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
333
 
292
334
  ### Sorting\<T\>
293
335
 
@@ -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
- | `Sorting<T>` | Multi-column sort state with 3-click toggle cycle and `apply()` pipeline | ViewModel property |
19
- | `Pagination` | Page/pageSize state with `apply()` slicing | ViewModel property |
20
- | `Selection<K>` | Key-based selection set with toggle/select-all semantics | ViewModel property |
21
- | `Feed<T>` | Cursor + hasMore + item accumulation for server-side pagination | ViewModel property |
22
- | `Pending<K, Meta?>` | Per-item operation queue with retry + status tracking + optional typed metadata | Resource property |
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
- | `Sorting<T>` | Multi-column sort state with 3-click toggle cycle and `apply()` pipeline | ViewModel property |
19
- | `Pagination` | Page/pageSize state with `apply()` slicing | ViewModel property |
20
- | `Selection<K>` | Key-based selection set with toggle/select-all semantics | ViewModel property |
21
- | `Feed<T>` | Cursor + hasMore + item accumulation for server-side pagination | ViewModel property |
22
- | `Pending<K, Meta?>` | Per-item operation queue with retry + status tracking + optional typed metadata | Resource property |
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)
package/dist/Feed.cjs CHANGED
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const bindPublicMethods = require("./bindPublicMethods.cjs");
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
- bindPublicMethods.bindPublicMethods(this);
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._notify();
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._notify();
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._notify();
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._notify();
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._notify();
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._notify();
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._notify();
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 { bindPublicMethods } from './bindPublicMethods';\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> {\n private _cursor: string | null = null;\n private _hasMore: boolean = true;\n private _items: readonly T[] = Object.freeze([] as T[]);\n private _listeners = new Set<() => void>();\n\n constructor() {\n bindPublicMethods(this);\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 // ── Subscribable interface ──\n\n /** Subscribe to feed state changes. Returns an unsubscribe function. */\n subscribe(cb: () => void): () => void {\n this._listeners.add(cb);\n return () => { this._listeners.delete(cb); };\n }\n\n private _notify(): void {\n for (const cb of this._listeners) cb();\n }\n}\n"],"names":["bindPublicMethods"],"mappings":";;;AAcO,MAAM,KAAkB;AAAA,EACrB,UAAyB;AAAA,EACzB,WAAoB;AAAA,EACpB,SAAuB,OAAO,OAAO,EAAS;AAAA,EAC9C,iCAAiB,IAAA;AAAA,EAEzB,cAAc;AACZA,sBAAAA,kBAAkB,IAAI;AAAA,EACxB;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,QAAA;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,QAAA;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,QAAA;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,QAAA;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,QAAA;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,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,UAAU;AACf,SAAK,WAAW;AAChB,SAAK,SAAS,OAAO,OAAO,CAAA,CAAS;AACrC,SAAK,QAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,UAAU,IAA4B;AACpC,SAAK,WAAW,IAAI,EAAE;AACtB,WAAO,MAAM;AAAE,WAAK,WAAW,OAAO,EAAE;AAAA,IAAG;AAAA,EAC7C;AAAA,EAEQ,UAAgB;AACtB,eAAW,MAAM,KAAK,WAAY,IAAA;AAAA,EACpC;AACF;;"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"Feed.d.ts","sourceRoot":"","sources":["../src/Feed.ts"],"names":[],"mappings":"AAEA,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;IAC3B,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,UAAU,CAAyB;;IAQ3C,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;IASb,wEAAwE;IACxE,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKrC,OAAO,CAAC,OAAO;CAGhB"}
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"}
package/dist/Feed.js CHANGED
@@ -1,11 +1,10 @@
1
- import { bindPublicMethods } from "./bindPublicMethods.js";
2
- class Feed {
1
+ import { Trackable } from "./Trackable.js";
2
+ class Feed extends Trackable {
3
3
  _cursor = null;
4
4
  _hasMore = true;
5
5
  _items = Object.freeze([]);
6
- _listeners = /* @__PURE__ */ new Set();
7
6
  constructor() {
8
- bindPublicMethods(this);
7
+ super();
9
8
  }
10
9
  // ── Readable state ──
11
10
  /** Current cursor position for the next page fetch, or null if at the beginning. */
@@ -29,59 +28,48 @@ class Feed {
29
28
  setResult(result) {
30
29
  this._hasMore = result.hasMore;
31
30
  this._cursor = result.cursor ?? null;
32
- this._notify();
31
+ this.notify();
33
32
  }
34
33
  /** Append page items and update cursor/hasMore. */
35
34
  appendPage(page) {
36
35
  this._items = Object.freeze([...this._items, ...page.items]);
37
36
  this._hasMore = page.hasMore;
38
37
  this._cursor = page.cursor ?? null;
39
- this._notify();
38
+ this.notify();
40
39
  }
41
40
  /** Prepend page items and update cursor/hasMore. */
42
41
  prependPage(page) {
43
42
  this._items = Object.freeze([...page.items, ...this._items]);
44
43
  this._hasMore = page.hasMore;
45
44
  this._cursor = page.cursor ?? null;
46
- this._notify();
45
+ this.notify();
47
46
  }
48
47
  /** Add items without affecting cursor/hasMore. */
49
48
  push(...items) {
50
49
  if (items.length === 0) return;
51
50
  this._items = Object.freeze([...this._items, ...items]);
52
- this._notify();
51
+ this.notify();
53
52
  }
54
53
  /** Remove items that don't match the predicate. No-op if nothing is filtered out. */
55
54
  filter(predicate) {
56
55
  const filtered = this._items.filter(predicate);
57
56
  if (filtered.length === this._items.length) return;
58
57
  this._items = Object.freeze(filtered);
59
- this._notify();
58
+ this.notify();
60
59
  }
61
60
  /** Replace all items and update cursor/hasMore atomically. Ideal for pull-to-refresh. */
62
61
  replacePage(page) {
63
62
  this._items = Object.freeze([...page.items]);
64
63
  this._hasMore = page.hasMore;
65
64
  this._cursor = page.cursor ?? null;
66
- this._notify();
65
+ this.notify();
67
66
  }
68
67
  /** Reset to initial empty state with hasMore=true. */
69
68
  reset() {
70
69
  this._cursor = null;
71
70
  this._hasMore = true;
72
71
  this._items = Object.freeze([]);
73
- this._notify();
74
- }
75
- // ── Subscribable interface ──
76
- /** Subscribe to feed state changes. Returns an unsubscribe function. */
77
- subscribe(cb) {
78
- this._listeners.add(cb);
79
- return () => {
80
- this._listeners.delete(cb);
81
- };
82
- }
83
- _notify() {
84
- for (const cb of this._listeners) cb();
72
+ this.notify();
85
73
  }
86
74
  }
87
75
  export {
package/dist/Feed.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Feed.js","sources":["../src/Feed.ts"],"sourcesContent":["import { bindPublicMethods } from './bindPublicMethods';\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> {\n private _cursor: string | null = null;\n private _hasMore: boolean = true;\n private _items: readonly T[] = Object.freeze([] as T[]);\n private _listeners = new Set<() => void>();\n\n constructor() {\n bindPublicMethods(this);\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 // ── Subscribable interface ──\n\n /** Subscribe to feed state changes. Returns an unsubscribe function. */\n subscribe(cb: () => void): () => void {\n this._listeners.add(cb);\n return () => { this._listeners.delete(cb); };\n }\n\n private _notify(): void {\n for (const cb of this._listeners) cb();\n }\n}\n"],"names":[],"mappings":";AAcO,MAAM,KAAkB;AAAA,EACrB,UAAyB;AAAA,EACzB,WAAoB;AAAA,EACpB,SAAuB,OAAO,OAAO,EAAS;AAAA,EAC9C,iCAAiB,IAAA;AAAA,EAEzB,cAAc;AACZ,sBAAkB,IAAI;AAAA,EACxB;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,QAAA;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,QAAA;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,QAAA;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,QAAA;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,QAAA;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,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,QAAc;AACZ,SAAK,UAAU;AACf,SAAK,WAAW;AAChB,SAAK,SAAS,OAAO,OAAO,CAAA,CAAS;AACrC,SAAK,QAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,UAAU,IAA4B;AACpC,SAAK,WAAW,IAAI,EAAE;AACtB,WAAO,MAAM;AAAE,WAAK,WAAW,OAAO,EAAE;AAAA,IAAG;AAAA,EAC7C;AAAA,EAEQ,UAAgB;AACtB,eAAW,MAAM,KAAK,WAAY,IAAA;AAAA,EACpC;AACF;"}
1
+ {"version":3,"file":"Feed.js","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":[],"mappings":";AAcO,MAAM,aAA0B,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;"}
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const bindPublicMethods = require("./bindPublicMethods.cjs");
4
- class Pagination {
3
+ const Trackable = require("./Trackable.cjs");
4
+ class Pagination extends Trackable.Trackable {
5
5
  _page = 1;
6
6
  _pageSize;
7
- _listeners = /* @__PURE__ */ new Set();
8
7
  constructor(options) {
8
+ super();
9
9
  this._pageSize = options?.pageSize ?? 10;
10
- bindPublicMethods.bindPublicMethods(this);
11
10
  }
12
11
  // ── Readable state ──
13
12
  /** Current page number (1-based). */
@@ -37,32 +36,32 @@ class Pagination {
37
36
  const clamped = Math.max(1, Math.floor(page));
38
37
  if (clamped === this._page) return;
39
38
  this._page = clamped;
40
- this._notify();
39
+ this.notify();
41
40
  }
42
41
  /** Change the page size and reset to page 1. */
43
42
  setPageSize(size) {
44
43
  if (size < 1) return;
45
44
  this._pageSize = size;
46
45
  this._page = 1;
47
- this._notify();
46
+ this.notify();
48
47
  }
49
48
  /** Advance to the next page. */
50
49
  nextPage() {
51
50
  this._page++;
52
- this._notify();
51
+ this.notify();
53
52
  }
54
53
  /** Go back to the previous page. No-op if already on page 1. */
55
54
  prevPage() {
56
55
  if (this._page > 1) {
57
56
  this._page--;
58
- this._notify();
57
+ this.notify();
59
58
  }
60
59
  }
61
60
  /** Reset to page 1. */
62
61
  reset() {
63
62
  if (this._page === 1) return;
64
63
  this._page = 1;
65
- this._notify();
64
+ this.notify();
66
65
  }
67
66
  // ── Pipeline ──
68
67
  /** Slice an array to the current page window. Returns the page subset. */
@@ -70,17 +69,6 @@ class Pagination {
70
69
  const start = (this._page - 1) * this._pageSize;
71
70
  return items.slice(start, start + this._pageSize);
72
71
  }
73
- // ── Subscribable interface ──
74
- /** Subscribe to pagination state changes. Returns an unsubscribe function. */
75
- subscribe(cb) {
76
- this._listeners.add(cb);
77
- return () => {
78
- this._listeners.delete(cb);
79
- };
80
- }
81
- _notify() {
82
- for (const cb of this._listeners) cb();
83
- }
84
72
  }
85
73
  exports.Pagination = Pagination;
86
74
  //# sourceMappingURL=Pagination.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["import { bindPublicMethods } from './bindPublicMethods';\n\n/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination {\n private _page: number = 1;\n private _pageSize: number;\n private _listeners = new Set<() => void>();\n\n constructor(options?: { pageSize?: number }) {\n this._pageSize = options?.pageSize ?? 10;\n bindPublicMethods(this);\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this._notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this._notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this._notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this._notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this._notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n\n // ── Subscribable interface ──\n\n /** Subscribe to pagination state changes. Returns an unsubscribe function. */\n subscribe(cb: () => void): () => void {\n this._listeners.add(cb);\n return () => { this._listeners.delete(cb); };\n }\n\n private _notify(): void {\n for (const cb of this._listeners) cb();\n }\n}\n"],"names":["bindPublicMethods"],"mappings":";;;AAOO,MAAM,WAAW;AAAA,EACd,QAAgB;AAAA,EAChB;AAAA,EACA,iCAAiB,IAAA;AAAA,EAEzB,YAAY,SAAiC;AAC3C,SAAK,YAAY,SAAS,YAAY;AACtCA,sBAAAA,kBAAkB,IAAI;AAAA,EACxB;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAKA,UAAU,OAAuB;AAC/B,WAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,SAAS,CAAC;AAAA,EACtD;AAAA;AAAA,EAGA,QAAQ,OAAwB;AAC9B,WAAO,KAAK,QAAQ,KAAK,UAAU,KAAK;AAAA,EAC1C;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA,EAKA,QAAQ,MAAoB;AAC1B,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAI,YAAY,KAAK,MAAO;AAC5B,SAAK,QAAQ;AACb,SAAK,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,QAAI,OAAO,EAAG;AACd,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK;AACL,SAAK,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,QAAI,KAAK,QAAQ,GAAG;AAClB,WAAK;AACL,WAAK,QAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,EAAG;AACtB,SAAK,QAAQ;AACb,SAAK,QAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,MAAS,OAAiB;AACxB,UAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;AACtC,WAAO,MAAM,MAAM,OAAO,QAAQ,KAAK,SAAS;AAAA,EAClD;AAAA;AAAA;AAAA,EAKA,UAAU,IAA4B;AACpC,SAAK,WAAW,IAAI,EAAE;AACtB,WAAO,MAAM;AAAE,WAAK,WAAW,OAAO,EAAE;AAAA,IAAG;AAAA,EAC7C;AAAA,EAEQ,UAAgB;AACtB,eAAW,MAAM,KAAK,WAAY,IAAA;AAAA,EACpC;AACF;;"}
1
+ {"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination extends Trackable {\n private _page: number = 1;\n private _pageSize: number;\n\n constructor(options?: { pageSize?: number }) {\n super();\n this._pageSize = options?.pageSize ?? 10;\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this.notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this.notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this.notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this.notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n}\n"],"names":["Trackable"],"mappings":";;;AAOO,MAAM,mBAAmBA,UAAAA,UAAU;AAAA,EAChC,QAAgB;AAAA,EAChB;AAAA,EAER,YAAY,SAAiC;AAC3C,UAAA;AACA,SAAK,YAAY,SAAS,YAAY;AAAA,EACxC;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAKA,UAAU,OAAuB;AAC/B,WAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,SAAS,CAAC;AAAA,EACtD;AAAA;AAAA,EAGA,QAAQ,OAAwB;AAC9B,WAAO,KAAK,QAAQ,KAAK,UAAU,KAAK;AAAA,EAC1C;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA,EAKA,QAAQ,MAAoB;AAC1B,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAI,YAAY,KAAK,MAAO;AAC5B,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,QAAI,OAAO,EAAG;AACd,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK;AACL,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,QAAI,KAAK,QAAQ,GAAG;AAClB,WAAK;AACL,WAAK,OAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,EAAG;AACtB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,MAAS,OAAiB;AACxB,UAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;AACtC,WAAO,MAAM,MAAM,OAAO,QAAQ,KAAK,SAAS;AAAA,EAClD;AACF;;"}
@@ -1,12 +1,12 @@
1
+ import { Trackable } from './Trackable';
1
2
  /**
2
3
  * Page-based pagination state manager with array slicing pipeline.
3
4
  * Tracks current page and page size, provides navigation helpers.
4
5
  * Subscribable — auto-tracked when used as a ViewModel property.
5
6
  */
6
- export declare class Pagination {
7
+ export declare class Pagination extends Trackable {
7
8
  private _page;
8
9
  private _pageSize;
9
- private _listeners;
10
10
  constructor(options?: {
11
11
  pageSize?: number;
12
12
  });
@@ -32,8 +32,5 @@ export declare class Pagination {
32
32
  reset(): void;
33
33
  /** Slice an array to the current page window. Returns the page subset. */
34
34
  apply<T>(items: T[]): T[];
35
- /** Subscribe to pagination state changes. Returns an unsubscribe function. */
36
- subscribe(cb: () => void): () => void;
37
- private _notify;
38
35
  }
39
36
  //# sourceMappingURL=Pagination.d.ts.map