mvc-kit 2.9.0 → 2.10.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 +2 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +64 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +33 -2
- package/agent-config/claude-code/skills/guide/patterns.md +38 -0
- package/agent-config/copilot/copilot-instructions.md +5 -1
- package/agent-config/cursor/cursorrules +5 -1
- package/dist/Channel.cjs +5 -0
- package/dist/Channel.cjs.map +1 -1
- package/dist/Channel.d.ts +1 -0
- package/dist/Channel.d.ts.map +1 -1
- package/dist/Channel.js +5 -0
- package/dist/Channel.js.map +1 -1
- package/dist/Collection.cjs +20 -17
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.d.ts +2 -2
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +20 -17
- package/dist/Collection.js.map +1 -1
- package/dist/Controller.cjs +5 -0
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.ts +1 -0
- package/dist/Controller.d.ts.map +1 -1
- package/dist/Controller.js +5 -0
- package/dist/Controller.js.map +1 -1
- package/dist/EventBus.cjs +5 -0
- package/dist/EventBus.cjs.map +1 -1
- package/dist/EventBus.d.ts +1 -0
- package/dist/EventBus.d.ts.map +1 -1
- package/dist/EventBus.js +5 -0
- package/dist/EventBus.js.map +1 -1
- package/dist/Feed.cjs +4 -0
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.d.ts +1 -0
- package/dist/Feed.d.ts.map +1 -1
- package/dist/Feed.js +4 -0
- package/dist/Feed.js.map +1 -1
- package/dist/Model.cjs +6 -3
- package/dist/Model.cjs.map +1 -1
- package/dist/Model.d.ts +1 -1
- package/dist/Model.d.ts.map +1 -1
- package/dist/Model.js +6 -3
- package/dist/Model.js.map +1 -1
- package/dist/Pagination.cjs +2 -0
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.d.ts.map +1 -1
- package/dist/Pagination.js +2 -0
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +301 -0
- package/dist/Pending.cjs.map +1 -0
- package/dist/Pending.d.ts +91 -0
- package/dist/Pending.d.ts.map +1 -0
- package/dist/Pending.js +301 -0
- package/dist/Pending.js.map +1 -0
- package/dist/PersistentCollection.cjs +1 -1
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.d.ts.map +1 -1
- package/dist/PersistentCollection.js +1 -1
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Selection.cjs +4 -0
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.d.ts +1 -0
- package/dist/Selection.d.ts.map +1 -1
- package/dist/Selection.js +4 -0
- package/dist/Selection.js.map +1 -1
- package/dist/Service.cjs +5 -0
- package/dist/Service.cjs.map +1 -1
- package/dist/Service.d.ts +1 -0
- package/dist/Service.d.ts.map +1 -1
- package/dist/Service.js +5 -0
- package/dist/Service.js.map +1 -1
- package/dist/Sorting.cjs +2 -0
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.d.ts.map +1 -1
- package/dist/Sorting.js +2 -0
- package/dist/Sorting.js.map +1 -1
- package/dist/ViewModel.cjs +45 -17
- package/dist/ViewModel.cjs.map +1 -1
- package/dist/ViewModel.d.ts +13 -4
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +45 -17
- package/dist/ViewModel.js.map +1 -1
- package/dist/bindPublicMethods.cjs +27 -0
- package/dist/bindPublicMethods.cjs.map +1 -0
- package/dist/bindPublicMethods.d.ts +18 -0
- package/dist/bindPublicMethods.d.ts.map +1 -0
- package/dist/bindPublicMethods.js +27 -0
- package/dist/bindPublicMethods.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +2 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +2 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/walkPrototypeChain.cjs.map +1 -1
- package/dist/walkPrototypeChain.d.ts +1 -1
- package/dist/walkPrototypeChain.js.map +1 -1
- package/dist/wrapAsyncMethods.cjs +15 -3
- package/dist/wrapAsyncMethods.cjs.map +1 -1
- package/dist/wrapAsyncMethods.d.ts.map +1 -1
- package/dist/wrapAsyncMethods.js +15 -3
- package/dist/wrapAsyncMethods.js.map +1 -1
- package/package.json +2 -1
package/dist/Controller.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Controller.d.ts","sourceRoot":"","sources":["../src/Controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Controller.d.ts","sourceRoot":"","sources":["../src/Controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAKhF;;;GAGG;AACH,8BAAsB,UAAW,YAAW,UAAU;IACpD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,SAAS,CAA+B;;IAMhD,+CAA+C;IAC/C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,sCAAsC;IACtC,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,6EAA6E;IAC7E,IAAI,aAAa,IAAI,WAAW,CAK/B;IAED,iFAAiF;IACjF,IAAI,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5B,0EAA0E;IAC1E,OAAO,IAAI,IAAI;IAcf,uEAAuE;IACvE,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAO1C,2FAA2F;IAC3F,SAAS,CAAC,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAMpF,yGAAyG;IACzG,SAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS;QAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;KAAE,EAC1G,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,GAC7C,MAAM,IAAI;IAMb,4FAA4F;IAC5F,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACzC,uFAAuF;IACvF,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI;CAC7B"}
|
package/dist/Controller.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { bindPublicMethods } from "./bindPublicMethods.js";
|
|
2
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["addCleanup", "subscribeTo", "listenTo"]);
|
|
1
3
|
class Controller {
|
|
2
4
|
_disposed = false;
|
|
3
5
|
_initialized = false;
|
|
4
6
|
_abortController = null;
|
|
5
7
|
_cleanups = null;
|
|
8
|
+
constructor() {
|
|
9
|
+
bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
10
|
+
}
|
|
6
11
|
/** Whether this instance has been disposed. */
|
|
7
12
|
get disposed() {
|
|
8
13
|
return this._disposed;
|
package/dist/Controller.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Controller.js","sources":["../src/Controller.ts"],"sourcesContent":["import type { Disposable, Subscribable, Listener, EventPayload } from './types';\n\n/**\n * Base class for stateless orchestrators.\n * Controllers coordinate between ViewModels, Models, and Services.\n */\nexport abstract class Controller implements Disposable {\n private _disposed = false;\n private _initialized = false;\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Controller.js","sources":["../src/Controller.ts"],"sourcesContent":["import type { Disposable, Subscribable, Listener, EventPayload } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst PROTECTED_KEYS = new Set(['addCleanup', 'subscribeTo', 'listenTo']);\n\n/**\n * Base class for stateless orchestrators.\n * Controllers coordinate between ViewModels, Models, and Services.\n */\nexport abstract class Controller implements Disposable {\n private _disposed = false;\n private _initialized = false;\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":";AAGA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,cAAc,eAAe,UAAU,CAAC;AAMjE,MAAe,WAAiC;AAAA,EAC7C,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EAE3C,cAAc;AACZ,sBAAkB,MAAM,OAAO,WAAW,cAAc;AAAA,EAC1D;AAAA;AAAA,EAGA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA,EAGA,OAA6B;AAC3B,QAAI,KAAK,gBAAgB,KAAK,UAAW;AACzC,SAAK,eAAe;AACpB,WAAO,KAAK,SAAA;AAAA,EACd;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAA;AAAA,EACP;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAAA;AAAA,EAGU,YAAe,QAAyB,UAAmC;AACnF,UAAM,cAAc,OAAO,UAAU,QAAQ;AAC7C,SAAK,WAAW,WAAW;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA,EAGU,SACR,QACA,OACA,SACY;AACZ,UAAM,cAAc,OAAO,GAAG,OAAO,OAAO;AAC5C,SAAK,WAAW,WAAW;AAC3B,WAAO;AAAA,EACT;AAMF;"}
|
package/dist/EventBus.cjs
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const bindPublicMethods = require("./bindPublicMethods.cjs");
|
|
4
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["addCleanup"]);
|
|
3
5
|
class EventBus {
|
|
4
6
|
_disposed = false;
|
|
5
7
|
_handlers = /* @__PURE__ */ new Map();
|
|
6
8
|
_abortController = null;
|
|
7
9
|
_cleanups = null;
|
|
10
|
+
constructor() {
|
|
11
|
+
bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
12
|
+
}
|
|
8
13
|
/** Whether this instance has been disposed. */
|
|
9
14
|
get disposed() {
|
|
10
15
|
return this._disposed;
|
package/dist/EventBus.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventBus.cjs","sources":["../src/EventBus.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._handlers.clear();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EventBus.cjs","sources":["../src/EventBus.ts"],"sourcesContent":["import type { Disposable } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst PROTECTED_KEYS = new Set(['addCleanup']);\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._handlers.clear();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":["bindPublicMethods"],"mappings":";;;AAGA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,YAAY,CAAC;AAMtC,MAAM,SAA8D;AAAA,EAIjE,YAAY;AAAA,EACZ,gCAAgB,IAAA;AAAA,EAChB,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EAE3C,cAAc;AACZA,sBAAAA,kBAAkB,MAAM,OAAO,WAAW,cAAc;AAAA,EAC1D;AAAA;AAAA,EAGA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAqB;AACrD,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAEA,UAAM,WAAW,KAAK,UAAU,IAAI,KAAK;AACzC,QAAI,UAAU;AACZ,iBAAW,WAAW,UAAU;AAC9B,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,GAAsB,OAAU,SAAoC;AAClE,QAAI,KAAK,WAAW;AAClB,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAEA,QAAI,WAAW,KAAK,UAAU,IAAI,KAAK;AACvC,QAAI,CAAC,UAAU;AACb,qCAAe,IAAA;AACf,WAAK,UAAU,IAAI,OAAO,QAAQ;AAAA,IACpC;AAEA,aAAS,IAAI,OAA2B;AAExC,WAAO,MAAM;AACX,eAAU,OAAO,OAA2B;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAoC;AACpE,UAAM,cAAc,KAAK,GAAG,OAAO,CAAC,YAAY;AAC9C,kBAAA;AACA,cAAQ,OAAO;AAAA,IACjB,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAIF;;"}
|
package/dist/EventBus.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class EventBus<E extends Record<string, any>> implements Disposab
|
|
|
10
10
|
private _handlers;
|
|
11
11
|
private _abortController;
|
|
12
12
|
private _cleanups;
|
|
13
|
+
constructor();
|
|
13
14
|
/** Whether this instance has been disposed. */
|
|
14
15
|
get disposed(): boolean;
|
|
15
16
|
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
package/dist/EventBus.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventBus.d.ts","sourceRoot":"","sources":["../src/EventBus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"EventBus.d.ts","sourceRoot":"","sources":["../src/EventBus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAI1C,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;AAEvC;;GAEG;AACH,qBAAa,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAE,YAAW,UAAU;IACxE,8FAA8F;IAC9F,SAAiB,MAAM,EAAE,CAAC,CAAC;IAE3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,SAAS,CAA+B;;IAMhD,+CAA+C;IAC/C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,6EAA6E;IAC7E,IAAI,aAAa,IAAI,WAAW,CAK/B;IAED;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAatD;;OAEG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAkBnE;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAQrE,0EAA0E;IAC1E,OAAO,IAAI,IAAI;IAef,uEAAuE;IACvE,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAO1C,uFAAuF;IACvF,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI;CAC7B"}
|
package/dist/EventBus.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { bindPublicMethods } from "./bindPublicMethods.js";
|
|
2
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["addCleanup"]);
|
|
1
3
|
class EventBus {
|
|
2
4
|
_disposed = false;
|
|
3
5
|
_handlers = /* @__PURE__ */ new Map();
|
|
4
6
|
_abortController = null;
|
|
5
7
|
_cleanups = null;
|
|
8
|
+
constructor() {
|
|
9
|
+
bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
10
|
+
}
|
|
6
11
|
/** Whether this instance has been disposed. */
|
|
7
12
|
get disposed() {
|
|
8
13
|
return this._disposed;
|
package/dist/EventBus.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventBus.js","sources":["../src/EventBus.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._handlers.clear();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EventBus.js","sources":["../src/EventBus.ts"],"sourcesContent":["import type { Disposable } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst PROTECTED_KEYS = new Set(['addCleanup']);\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor() {\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._handlers.clear();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":";AAGA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,YAAY,CAAC;AAMtC,MAAM,SAA8D;AAAA,EAIjE,YAAY;AAAA,EACZ,gCAAgB,IAAA;AAAA,EAChB,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EAE3C,cAAc;AACZ,sBAAkB,MAAM,OAAO,WAAW,cAAc;AAAA,EAC1D;AAAA;AAAA,EAGA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAqB;AACrD,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAEA,UAAM,WAAW,KAAK,UAAU,IAAI,KAAK;AACzC,QAAI,UAAU;AACZ,iBAAW,WAAW,UAAU;AAC9B,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,GAAsB,OAAU,SAAoC;AAClE,QAAI,KAAK,WAAW;AAClB,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAEA,QAAI,WAAW,KAAK,UAAU,IAAI,KAAK;AACvC,QAAI,CAAC,UAAU;AACb,qCAAe,IAAA;AACf,WAAK,UAAU,IAAI,OAAO,QAAQ;AAAA,IACpC;AAEA,aAAS,IAAI,OAA2B;AAExC,WAAO,MAAM;AACX,eAAU,OAAO,OAA2B;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAoC;AACpE,UAAM,cAAc,KAAK,GAAG,OAAO,CAAC,YAAY;AAC9C,kBAAA;AACA,cAAQ,OAAO;AAAA,IACjB,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAA;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAIF;"}
|
package/dist/Feed.cjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const bindPublicMethods = require("./bindPublicMethods.cjs");
|
|
3
4
|
class Feed {
|
|
4
5
|
_cursor = null;
|
|
5
6
|
_hasMore = true;
|
|
6
7
|
_items = Object.freeze([]);
|
|
7
8
|
_listeners = /* @__PURE__ */ new Set();
|
|
9
|
+
constructor() {
|
|
10
|
+
bindPublicMethods.bindPublicMethods(this);
|
|
11
|
+
}
|
|
8
12
|
// ── Readable state ──
|
|
9
13
|
/** Current cursor position for the next page fetch, or null if at the beginning. */
|
|
10
14
|
get cursor() {
|
package/dist/Feed.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Feed.cjs","sources":["../src/Feed.ts"],"sourcesContent":["/** 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 // ── 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":"
|
|
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;;"}
|
package/dist/Feed.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class Feed<T = unknown> {
|
|
|
14
14
|
private _hasMore;
|
|
15
15
|
private _items;
|
|
16
16
|
private _listeners;
|
|
17
|
+
constructor();
|
|
17
18
|
/** Current cursor position for the next page fetch, or null if at the beginning. */
|
|
18
19
|
get cursor(): string | null;
|
|
19
20
|
/** Whether more pages are available from the server. */
|
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":"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"}
|
package/dist/Feed.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { bindPublicMethods } from "./bindPublicMethods.js";
|
|
1
2
|
class Feed {
|
|
2
3
|
_cursor = null;
|
|
3
4
|
_hasMore = true;
|
|
4
5
|
_items = Object.freeze([]);
|
|
5
6
|
_listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
constructor() {
|
|
8
|
+
bindPublicMethods(this);
|
|
9
|
+
}
|
|
6
10
|
// ── Readable state ──
|
|
7
11
|
/** Current cursor position for the next page fetch, or null if at the beginning. */
|
|
8
12
|
get cursor() {
|
package/dist/Feed.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Feed.js","sources":["../src/Feed.ts"],"sourcesContent":["/** 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 // ── 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":"
|
|
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;"}
|
package/dist/Model.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const bindPublicMethods = require("./bindPublicMethods.cjs");
|
|
3
4
|
const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
|
|
5
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["set", "validate", "addCleanup", "subscribeTo", "listenTo"]);
|
|
4
6
|
function freeze(obj) {
|
|
5
7
|
return __DEV__ ? Object.freeze(obj) : obj;
|
|
6
8
|
}
|
|
@@ -18,6 +20,7 @@ class Model {
|
|
|
18
20
|
const frozen = freeze({ ...initialState });
|
|
19
21
|
this._state = frozen;
|
|
20
22
|
this._committed = frozen;
|
|
23
|
+
bindPublicMethods.bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
21
24
|
}
|
|
22
25
|
/** Current frozen state object. */
|
|
23
26
|
get state() {
|
|
@@ -34,7 +37,7 @@ class Model {
|
|
|
34
37
|
*/
|
|
35
38
|
get dirty() {
|
|
36
39
|
if (this._cachedDirty === null) {
|
|
37
|
-
this._cachedDirty = !this.
|
|
40
|
+
this._cachedDirty = !this._shallowEqual(this._state, this._committed);
|
|
38
41
|
}
|
|
39
42
|
return this._cachedDirty;
|
|
40
43
|
}
|
|
@@ -118,7 +121,7 @@ class Model {
|
|
|
118
121
|
if (this._disposed) {
|
|
119
122
|
throw new Error("Cannot rollback on disposed Model");
|
|
120
123
|
}
|
|
121
|
-
if (this.
|
|
124
|
+
if (this._shallowEqual(this._state, this._committed)) {
|
|
122
125
|
return;
|
|
123
126
|
}
|
|
124
127
|
const prev = this._state;
|
|
@@ -181,7 +184,7 @@ class Model {
|
|
|
181
184
|
this.addCleanup(unsubscribe);
|
|
182
185
|
return unsubscribe;
|
|
183
186
|
}
|
|
184
|
-
|
|
187
|
+
_shallowEqual(a, b) {
|
|
185
188
|
const keysA = Object.keys(a);
|
|
186
189
|
const keysB = Object.keys(b);
|
|
187
190
|
if (keysA.length !== keysB.length) {
|
package/dist/Model.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.cjs","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nfunction freeze<T>(obj: T): T {\n return __DEV__ ? Object.freeze(obj) as T : obj;\n}\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _cachedDirty: boolean | null = null;\n private _cachedErrors: ValidationErrors<S> | null = null;\n\n constructor(initialState: S) {\n const frozen = freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n if (this._cachedDirty === null) {\n this._cachedDirty = !this.shallowEqual(this._state, this._committed);\n }\n return this._cachedDirty;\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n if (this._cachedErrors === null) {\n this._cachedErrors = this.validate(this._state);\n }\n return this._cachedErrors;\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality)\n const keys = Object.keys(partial) as (keyof S)[];\n const hasChanges = keys.some(\n (key) => partial[key] !== this._state[key]\n );\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = freeze({ ...prev, ...partial });\n this._state = next;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n this._cachedDirty = null;\n this._cachedErrors = null;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n this._listeners.add(listener);\n\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Model.cjs","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\nconst PROTECTED_KEYS = new Set(['set', 'validate', 'addCleanup', 'subscribeTo', 'listenTo']);\n\nfunction freeze<T>(obj: T): T {\n return __DEV__ ? Object.freeze(obj) as T : obj;\n}\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _cachedDirty: boolean | null = null;\n private _cachedErrors: ValidationErrors<S> | null = null;\n\n constructor(initialState: S) {\n const frozen = freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n if (this._cachedDirty === null) {\n this._cachedDirty = !this._shallowEqual(this._state, this._committed);\n }\n return this._cachedDirty;\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n if (this._cachedErrors === null) {\n this._cachedErrors = this.validate(this._state);\n }\n return this._cachedErrors;\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality)\n const keys = Object.keys(partial) as (keyof S)[];\n const hasChanges = keys.some(\n (key) => partial[key] !== this._state[key]\n );\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = freeze({ ...prev, ...partial });\n this._state = next;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n this._cachedDirty = null;\n this._cachedErrors = null;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this._shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n this._listeners.add(listener);\n\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n private _shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":["bindPublicMethods"],"mappings":"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"}
|
package/dist/Model.d.ts
CHANGED
|
@@ -75,6 +75,6 @@ export declare abstract class Model<S extends object> implements Subscribable<S>
|
|
|
75
75
|
protected onInit?(): void | Promise<void>;
|
|
76
76
|
/** Lifecycle hook called during dispose(). Override for custom teardown. @protected */
|
|
77
77
|
protected onDispose?(): void;
|
|
78
|
-
private
|
|
78
|
+
private _shallowEqual;
|
|
79
79
|
}
|
|
80
80
|
//# sourceMappingURL=Model.d.ts.map
|
package/dist/Model.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../src/Model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../src/Model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAU/F;;GAEG;AACH,8BAAsB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAE,YAAW,YAAY,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,aAAa,CAAoC;gBAE7C,YAAY,EAAE,CAAC;IAO3B,mCAAmC;IACnC,IAAI,KAAK,IAAI,CAAC,CAEb;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,CAAC,CAEjB;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,OAAO,CAKnB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,gBAAgB,CAAC,CAAC,CAAC,CAKhC;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,+CAA+C;IAC/C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,sCAAsC;IACtC,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,6EAA6E;IAC7E,IAAI,aAAa,IAAI,WAAW,CAK/B;IAED,iFAAiF;IACjF,IAAI,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5B;;;OAGG;IACH,SAAS,CAAC,GAAG,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;IAiC9D;;OAEG;IACH,MAAM,IAAI,IAAI;IASd;;OAEG;IACH,QAAQ,IAAI,IAAI;IAqBhB,oEAAoE;IACpE,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAY5C,0EAA0E;IAC1E,OAAO,IAAI,IAAI;IAef;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC;IAIlD,uEAAuE;IACvE,SAAS,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI;IAO1C,2FAA2F;IAC3F,SAAS,CAAC,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAMpF,yGAAyG;IACzG,SAAS,CAAC,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS;QAAE,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;KAAE,EAC1G,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,IAAI,GAC7C,MAAM,IAAI;IAMb,kFAAkF;IAClF,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI;IACxC,4FAA4F;IAC5F,SAAS,CAAC,MAAM,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACzC,uFAAuF;IACvF,SAAS,CAAC,SAAS,CAAC,IAAI,IAAI;IAE5B,OAAO,CAAC,aAAa;CAgBtB"}
|
package/dist/Model.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { bindPublicMethods } from "./bindPublicMethods.js";
|
|
1
2
|
const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
|
|
3
|
+
const PROTECTED_KEYS = /* @__PURE__ */ new Set(["set", "validate", "addCleanup", "subscribeTo", "listenTo"]);
|
|
2
4
|
function freeze(obj) {
|
|
3
5
|
return __DEV__ ? Object.freeze(obj) : obj;
|
|
4
6
|
}
|
|
@@ -16,6 +18,7 @@ class Model {
|
|
|
16
18
|
const frozen = freeze({ ...initialState });
|
|
17
19
|
this._state = frozen;
|
|
18
20
|
this._committed = frozen;
|
|
21
|
+
bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);
|
|
19
22
|
}
|
|
20
23
|
/** Current frozen state object. */
|
|
21
24
|
get state() {
|
|
@@ -32,7 +35,7 @@ class Model {
|
|
|
32
35
|
*/
|
|
33
36
|
get dirty() {
|
|
34
37
|
if (this._cachedDirty === null) {
|
|
35
|
-
this._cachedDirty = !this.
|
|
38
|
+
this._cachedDirty = !this._shallowEqual(this._state, this._committed);
|
|
36
39
|
}
|
|
37
40
|
return this._cachedDirty;
|
|
38
41
|
}
|
|
@@ -116,7 +119,7 @@ class Model {
|
|
|
116
119
|
if (this._disposed) {
|
|
117
120
|
throw new Error("Cannot rollback on disposed Model");
|
|
118
121
|
}
|
|
119
|
-
if (this.
|
|
122
|
+
if (this._shallowEqual(this._state, this._committed)) {
|
|
120
123
|
return;
|
|
121
124
|
}
|
|
122
125
|
const prev = this._state;
|
|
@@ -179,7 +182,7 @@ class Model {
|
|
|
179
182
|
this.addCleanup(unsubscribe);
|
|
180
183
|
return unsubscribe;
|
|
181
184
|
}
|
|
182
|
-
|
|
185
|
+
_shallowEqual(a, b) {
|
|
183
186
|
const keysA = Object.keys(a);
|
|
184
187
|
const keysB = Object.keys(b);
|
|
185
188
|
if (keysA.length !== keysB.length) {
|
package/dist/Model.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Model.js","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\nfunction freeze<T>(obj: T): T {\n return __DEV__ ? Object.freeze(obj) as T : obj;\n}\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _cachedDirty: boolean | null = null;\n private _cachedErrors: ValidationErrors<S> | null = null;\n\n constructor(initialState: S) {\n const frozen = freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n if (this._cachedDirty === null) {\n this._cachedDirty = !this.shallowEqual(this._state, this._committed);\n }\n return this._cachedDirty;\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n if (this._cachedErrors === null) {\n this._cachedErrors = this.validate(this._state);\n }\n return this._cachedErrors;\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality)\n const keys = Object.keys(partial) as (keyof S)[];\n const hasChanges = keys.some(\n (key) => partial[key] !== this._state[key]\n );\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = freeze({ ...prev, ...partial });\n this._state = next;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n this._cachedDirty = null;\n this._cachedErrors = null;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n this._listeners.add(listener);\n\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"AAEA,MAAM,UAAU,OAAO,oBAAoB,eAAe;AAE1D,SAAS,OAAU,KAAW;AAC5B,SAAO,UAAU,OAAO,OAAO,GAAG,IAAS;AAC7C;AAKO,MAAe,MAAmD;AAAA,EAC/D;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,iCAAiB,IAAA;AAAA,EACjB,mBAA2C;AAAA,EAC3C,YAAmC;AAAA,EACnC,eAA+B;AAAA,EAC/B,gBAA4C;AAAA,EAEpD,YAAY,cAAiB;AAC3B,UAAM,SAAS,OAAO,EAAE,GAAG,cAAc;AACzC,SAAK,SAAS;AACd,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA,EAGA,IAAI,QAAW;AACb,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,QAAiB;AACnB,QAAI,KAAK,iBAAiB,MAAM;AAC9B,WAAK,eAAe,CAAC,KAAK,aAAa,KAAK,QAAQ,KAAK,UAAU;AAAA,IACrE;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,SAA8B;AAChC,QAAI,KAAK,kBAAkB,MAAM;AAC/B,WAAK,gBAAgB,KAAK,SAAS,KAAK,MAAM;AAAA,IAChD;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,QAAiB;AACnB,WAAO,OAAO,KAAK,KAAK,MAAM,EAAE,WAAW;AAAA,EAC7C;AAAA;AAAA,EAGA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA,EAGA,OAA6B;AAC3B,QAAI,KAAK,gBAAgB,KAAK,UAAW;AACzC,SAAK,eAAe;AACpB,WAAO,KAAK,SAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,IAAI,kBAAiD;AAC7D,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAEA,UAAM,UACJ,OAAO,qBAAqB,aACxB,iBAAiB,KAAK,MAAM,IAC5B;AAGN,UAAM,OAAO,OAAO,KAAK,OAAO;AAChC,UAAM,aAAa,KAAK;AAAA,MACtB,CAAC,QAAQ,QAAQ,GAAG,MAAM,KAAK,OAAO,GAAG;AAAA,IAAA;AAG3C,QAAI,CAAC,YAAY;AACf;AAAA,IACF;AAEA,UAAM,OAAO,KAAK;AAClB,UAAM,OAAO,OAAO,EAAE,GAAG,MAAM,GAAG,SAAS;AAC3C,SAAK,SAAS;AACd,SAAK,eAAe;AACpB,SAAK,gBAAgB;AAErB,SAAK,QAAQ,MAAM,IAAI;AAEvB,eAAW,YAAY,KAAK,YAAY;AACtC,eAAS,MAAM,IAAI;AAAA,IACrB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,SAAe;AACb,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,SAAK,aAAa,KAAK;AACvB,SAAK,eAAe;AACpB,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,WAAiB;AACf,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AAEA,QAAI,KAAK,aAAa,KAAK,QAAQ,KAAK,UAAU,GAAG;AACnD;AAAA,IACF;AAEA,UAAM,OAAO,KAAK;AAClB,SAAK,SAAS,KAAK;AACnB,SAAK,eAAe;AACpB,SAAK,gBAAgB;AAErB,SAAK,QAAQ,MAAM,KAAK,MAAM;AAE9B,eAAW,YAAY,KAAK,YAAY;AACtC,eAAS,KAAK,QAAQ,IAAI;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA,EAGA,UAAU,UAAmC;AAC3C,QAAI,KAAK,WAAW;AAClB,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAEA,SAAK,WAAW,IAAI,QAAQ;AAE5B,WAAO,MAAM;AACX,WAAK,WAAW,OAAO,QAAQ;AAAA,IACjC;AAAA,EACF;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAA;AACL,SAAK,WAAW,MAAA;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,SAAS,QAAgC;AACjD,WAAO,CAAA;AAAA,EACT;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAAA;AAAA,EAGU,YAAe,QAAyB,UAAmC;AACnF,UAAM,cAAc,OAAO,UAAU,QAAQ;AAC7C,SAAK,WAAW,WAAW;AAC3B,WAAO;AAAA,EACT;AAAA;AAAA,EAGU,SACR,QACA,OACA,SACY;AACZ,UAAM,cAAc,OAAO,GAAG,OAAO,OAAO;AAC5C,SAAK,WAAW,WAAW;AAC3B,WAAO;AAAA,EACT;AAAA,EASQ,aAAa,GAAM,GAAe;AACxC,UAAM,QAAQ,OAAO,KAAK,CAAC;AAC3B,UAAM,QAAQ,OAAO,KAAK,CAAC;AAE3B,QAAI,MAAM,WAAW,MAAM,QAAQ;AACjC,aAAO;AAAA,IACT;AAEA,eAAW,OAAO,OAAO;AACvB,UAAI,EAAE,GAAG,MAAM,EAAE,GAAG,GAAG;AACrB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;"}
|
|
1
|
+
{"version":3,"file":"Model.js","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors, EventPayload } from './types';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\nconst PROTECTED_KEYS = new Set(['set', 'validate', 'addCleanup', 'subscribeTo', 'listenTo']);\n\nfunction freeze<T>(obj: T): T {\n return __DEV__ ? Object.freeze(obj) as T : obj;\n}\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _cachedDirty: boolean | null = null;\n private _cachedErrors: ValidationErrors<S> | null = null;\n\n constructor(initialState: S) {\n const frozen = freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n bindPublicMethods(this, Object.prototype, PROTECTED_KEYS);\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n if (this._cachedDirty === null) {\n this._cachedDirty = !this._shallowEqual(this._state, this._committed);\n }\n return this._cachedDirty;\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n if (this._cachedErrors === null) {\n this._cachedErrors = this.validate(this._state);\n }\n return this._cachedErrors;\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality)\n const keys = Object.keys(partial) as (keyof S)[];\n const hasChanges = keys.some(\n (key) => partial[key] !== this._state[key]\n );\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = freeze({ ...prev, ...partial });\n this._state = next;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n this._cachedDirty = null;\n this._cachedErrors = null;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this._shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n this._cachedDirty = null;\n this._cachedErrors = null;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n this._listeners.add(listener);\n\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Subscribes to a typed event on a Channel or EventBus with automatic cleanup on dispose. @protected */\n protected listenTo<K extends string, S extends { on(event: K, handler: (payload: any) => void): () => void }>(\n source: S,\n event: K,\n handler: (payload: EventPayload<S, K>) => void,\n ): () => void {\n const unsubscribe = source.on(event, handler);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n private _shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/Pagination.cjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const bindPublicMethods = require("./bindPublicMethods.cjs");
|
|
3
4
|
class Pagination {
|
|
4
5
|
_page = 1;
|
|
5
6
|
_pageSize;
|
|
6
7
|
_listeners = /* @__PURE__ */ new Set();
|
|
7
8
|
constructor(options) {
|
|
8
9
|
this._pageSize = options?.pageSize ?? 10;
|
|
10
|
+
bindPublicMethods.bindPublicMethods(this);
|
|
9
11
|
}
|
|
10
12
|
// ── Readable state ──
|
|
11
13
|
/** Current page number (1-based). */
|
package/dist/Pagination.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["/**\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 }\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":[],"mappings":"
|
|
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;;"}
|
package/dist/Pagination.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.d.ts","sourceRoot":"","sources":["../src/Pagination.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Pagination.d.ts","sourceRoot":"","sources":["../src/Pagination.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAyB;gBAE/B,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAO3C,qCAAqC;IACrC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,gCAAgC;IAChC,IAAI,QAAQ,IAAI,MAAM,CAErB;IAID,sDAAsD;IACtD,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAIhC,8CAA8C;IAC9C,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAI/B,kDAAkD;IAClD,OAAO,IAAI,OAAO;IAMlB,qDAAqD;IACrD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO3B,gDAAgD;IAChD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO/B,gCAAgC;IAChC,QAAQ,IAAI,IAAI;IAKhB,gEAAgE;IAChE,QAAQ,IAAI,IAAI;IAOhB,uBAAuB;IACvB,KAAK,IAAI,IAAI;IAQb,0EAA0E;IAC1E,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE;IAOzB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKrC,OAAO,CAAC,OAAO;CAGhB"}
|
package/dist/Pagination.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { bindPublicMethods } from "./bindPublicMethods.js";
|
|
1
2
|
class Pagination {
|
|
2
3
|
_page = 1;
|
|
3
4
|
_pageSize;
|
|
4
5
|
_listeners = /* @__PURE__ */ new Set();
|
|
5
6
|
constructor(options) {
|
|
6
7
|
this._pageSize = options?.pageSize ?? 10;
|
|
8
|
+
bindPublicMethods(this);
|
|
7
9
|
}
|
|
8
10
|
// ── Readable state ──
|
|
9
11
|
/** Current page number (1-based). */
|
package/dist/Pagination.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.js","sources":["../src/Pagination.ts"],"sourcesContent":["/**\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 }\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":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Pagination.js","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":[],"mappings":";AAOO,MAAM,WAAW;AAAA,EACd,QAAgB;AAAA,EAChB;AAAA,EACA,iCAAiB,IAAA;AAAA,EAEzB,YAAY,SAAiC;AAC3C,SAAK,YAAY,SAAS,YAAY;AACtC,sBAAkB,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;"}
|