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.
- package/README.md +4 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +41 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +43 -1
- 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/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/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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +4 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +4 -0
- package/dist/mvc-kit.js.map +1 -1
- 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 +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
|
|
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
|
-
| `
|
|
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)
|
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"}
|
package/dist/Feed.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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;"}
|
package/dist/Pagination.cjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
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.
|
|
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.
|
|
46
|
+
this.notify();
|
|
48
47
|
}
|
|
49
48
|
/** Advance to the next page. */
|
|
50
49
|
nextPage() {
|
|
51
50
|
this._page++;
|
|
52
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
package/dist/Pagination.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["import {
|
|
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;;"}
|
package/dist/Pagination.d.ts
CHANGED
|
@@ -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
|