mvc-kit 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +4 -0
  2. package/agent-config/claude-code/skills/guide/anti-patterns.md +41 -0
  3. package/agent-config/claude-code/skills/guide/api-reference.md +43 -1
  4. package/agent-config/claude-code/skills/guide/patterns.md +64 -0
  5. package/agent-config/copilot/copilot-instructions.md +10 -6
  6. package/agent-config/cursor/cursorrules +10 -6
  7. package/dist/Feed.cjs +10 -22
  8. package/dist/Feed.cjs.map +1 -1
  9. package/dist/Feed.d.ts +2 -5
  10. package/dist/Feed.d.ts.map +1 -1
  11. package/dist/Feed.js +10 -22
  12. package/dist/Feed.js.map +1 -1
  13. package/dist/Pagination.cjs +8 -20
  14. package/dist/Pagination.cjs.map +1 -1
  15. package/dist/Pagination.d.ts +2 -5
  16. package/dist/Pagination.d.ts.map +1 -1
  17. package/dist/Pagination.js +8 -20
  18. package/dist/Pagination.js.map +1 -1
  19. package/dist/Pending.cjs +26 -39
  20. package/dist/Pending.cjs.map +1 -1
  21. package/dist/Pending.d.ts +5 -9
  22. package/dist/Pending.d.ts.map +1 -1
  23. package/dist/Pending.js +26 -39
  24. package/dist/Pending.js.map +1 -1
  25. package/dist/Selection.cjs +5 -13
  26. package/dist/Selection.cjs.map +1 -1
  27. package/dist/Selection.d.ts +2 -4
  28. package/dist/Selection.d.ts.map +1 -1
  29. package/dist/Selection.js +5 -13
  30. package/dist/Selection.js.map +1 -1
  31. package/dist/Sorting.cjs +7 -19
  32. package/dist/Sorting.cjs.map +1 -1
  33. package/dist/Sorting.d.ts +2 -5
  34. package/dist/Sorting.d.ts.map +1 -1
  35. package/dist/Sorting.js +7 -19
  36. package/dist/Sorting.js.map +1 -1
  37. package/dist/Trackable.cjs +81 -0
  38. package/dist/Trackable.cjs.map +1 -0
  39. package/dist/Trackable.d.ts +82 -0
  40. package/dist/Trackable.d.ts.map +1 -0
  41. package/dist/Trackable.js +81 -0
  42. package/dist/Trackable.js.map +1 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/mvc-kit.cjs +4 -0
  46. package/dist/mvc-kit.cjs.map +1 -1
  47. package/dist/mvc-kit.js +4 -0
  48. package/dist/mvc-kit.js.map +1 -1
  49. package/dist/react/guards.cjs +2 -0
  50. package/dist/react/guards.cjs.map +1 -1
  51. package/dist/react/guards.d.ts +4 -0
  52. package/dist/react/guards.d.ts.map +1 -1
  53. package/dist/react/guards.js +3 -1
  54. package/dist/react/guards.js.map +1 -1
  55. package/dist/react/use-local.cjs +5 -0
  56. package/dist/react/use-local.cjs.map +1 -1
  57. package/dist/react/use-local.d.ts.map +1 -1
  58. package/dist/react/use-local.js +6 -1
  59. package/dist/react/use-local.js.map +1 -1
  60. package/dist/react/use-singleton.cjs +5 -0
  61. package/dist/react/use-singleton.cjs.map +1 -1
  62. package/dist/react/use-singleton.d.ts.map +1 -1
  63. package/dist/react/use-singleton.js +6 -1
  64. package/dist/react/use-singleton.js.map +1 -1
  65. package/dist/react/use-subscribe-only.cjs +25 -0
  66. package/dist/react/use-subscribe-only.cjs.map +1 -0
  67. package/dist/react/use-subscribe-only.d.ts +9 -0
  68. package/dist/react/use-subscribe-only.d.ts.map +1 -0
  69. package/dist/react/use-subscribe-only.js +25 -0
  70. package/dist/react/use-subscribe-only.js.map +1 -0
  71. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["import { bindPublicMethods } from './bindPublicMethods';\n\n/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination {\n private _page: number = 1;\n private _pageSize: number;\n private _listeners = new Set<() => void>();\n\n constructor(options?: { pageSize?: number }) {\n this._pageSize = options?.pageSize ?? 10;\n bindPublicMethods(this);\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this._notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this._notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this._notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this._notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this._notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n\n // ── Subscribable interface ──\n\n /** Subscribe to pagination state changes. Returns an unsubscribe function. */\n subscribe(cb: () => void): () => void {\n this._listeners.add(cb);\n return () => { this._listeners.delete(cb); };\n }\n\n private _notify(): void {\n for (const cb of this._listeners) cb();\n }\n}\n"],"names":["bindPublicMethods"],"mappings":";;;AAOO,MAAM,WAAW;AAAA,EACd,QAAgB;AAAA,EAChB;AAAA,EACA,iCAAiB,IAAA;AAAA,EAEzB,YAAY,SAAiC;AAC3C,SAAK,YAAY,SAAS,YAAY;AACtCA,sBAAAA,kBAAkB,IAAI;AAAA,EACxB;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAKA,UAAU,OAAuB;AAC/B,WAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,SAAS,CAAC;AAAA,EACtD;AAAA;AAAA,EAGA,QAAQ,OAAwB;AAC9B,WAAO,KAAK,QAAQ,KAAK,UAAU,KAAK;AAAA,EAC1C;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA,EAKA,QAAQ,MAAoB;AAC1B,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAI,YAAY,KAAK,MAAO;AAC5B,SAAK,QAAQ;AACb,SAAK,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,QAAI,OAAO,EAAG;AACd,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK;AACL,SAAK,QAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,QAAI,KAAK,QAAQ,GAAG;AAClB,WAAK;AACL,WAAK,QAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,EAAG;AACtB,SAAK,QAAQ;AACb,SAAK,QAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,MAAS,OAAiB;AACxB,UAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;AACtC,WAAO,MAAM,MAAM,OAAO,QAAQ,KAAK,SAAS;AAAA,EAClD;AAAA;AAAA;AAAA,EAKA,UAAU,IAA4B;AACpC,SAAK,WAAW,IAAI,EAAE;AACtB,WAAO,MAAM;AAAE,WAAK,WAAW,OAAO,EAAE;AAAA,IAAG;AAAA,EAC7C;AAAA,EAEQ,UAAgB;AACtB,eAAW,MAAM,KAAK,WAAY,IAAA;AAAA,EACpC;AACF;;"}
1
+ {"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination extends Trackable {\n private _page: number = 1;\n private _pageSize: number;\n\n constructor(options?: { pageSize?: number }) {\n super();\n this._pageSize = options?.pageSize ?? 10;\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this.notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this.notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this.notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this.notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n}\n"],"names":["Trackable"],"mappings":";;;AAOO,MAAM,mBAAmBA,UAAAA,UAAU;AAAA,EAChC,QAAgB;AAAA,EAChB;AAAA,EAER,YAAY,SAAiC;AAC3C,UAAA;AACA,SAAK,YAAY,SAAS,YAAY;AAAA,EACxC;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAKA,UAAU,OAAuB;AAC/B,WAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,SAAS,CAAC;AAAA,EACtD;AAAA;AAAA,EAGA,QAAQ,OAAwB;AAC9B,WAAO,KAAK,QAAQ,KAAK,UAAU,KAAK;AAAA,EAC1C;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA,EAKA,QAAQ,MAAoB;AAC1B,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAI,YAAY,KAAK,MAAO;AAC5B,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,QAAI,OAAO,EAAG;AACd,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK;AACL,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,QAAI,KAAK,QAAQ,GAAG;AAClB,WAAK;AACL,WAAK,OAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,EAAG;AACtB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,MAAS,OAAiB;AACxB,UAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;AACtC,WAAO,MAAM,MAAM,OAAO,QAAQ,KAAK,SAAS;AAAA,EAClD;AACF;;"}
@@ -1,12 +1,12 @@
1
+ import { Trackable } from './Trackable';
1
2
  /**
2
3
  * Page-based pagination state manager with array slicing pipeline.
3
4
  * Tracks current page and page size, provides navigation helpers.
4
5
  * Subscribable — auto-tracked when used as a ViewModel property.
5
6
  */
6
- export declare class Pagination {
7
+ export declare class Pagination extends Trackable {
7
8
  private _page;
8
9
  private _pageSize;
9
- private _listeners;
10
10
  constructor(options?: {
11
11
  pageSize?: number;
12
12
  });
@@ -32,8 +32,5 @@ export declare class Pagination {
32
32
  reset(): void;
33
33
  /** Slice an array to the current page window. Returns the page subset. */
34
34
  apply<T>(items: T[]): T[];
35
- /** Subscribe to pagination state changes. Returns an unsubscribe function. */
36
- subscribe(cb: () => void): () => void;
37
- private _notify;
38
35
  }
39
36
  //# sourceMappingURL=Pagination.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"Pagination.d.ts","sourceRoot":"","sources":["../src/Pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,SAAS;IACvC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,SAAS,CAAS;gBAEd,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;CAI1B"}
@@ -1,11 +1,10 @@
1
- import { bindPublicMethods } from "./bindPublicMethods.js";
2
- class Pagination {
1
+ import { Trackable } from "./Trackable.js";
2
+ class Pagination extends Trackable {
3
3
  _page = 1;
4
4
  _pageSize;
5
- _listeners = /* @__PURE__ */ new Set();
6
5
  constructor(options) {
6
+ super();
7
7
  this._pageSize = options?.pageSize ?? 10;
8
- bindPublicMethods(this);
9
8
  }
10
9
  // ── Readable state ──
11
10
  /** Current page number (1-based). */
@@ -35,32 +34,32 @@ class Pagination {
35
34
  const clamped = Math.max(1, Math.floor(page));
36
35
  if (clamped === this._page) return;
37
36
  this._page = clamped;
38
- this._notify();
37
+ this.notify();
39
38
  }
40
39
  /** Change the page size and reset to page 1. */
41
40
  setPageSize(size) {
42
41
  if (size < 1) return;
43
42
  this._pageSize = size;
44
43
  this._page = 1;
45
- this._notify();
44
+ this.notify();
46
45
  }
47
46
  /** Advance to the next page. */
48
47
  nextPage() {
49
48
  this._page++;
50
- this._notify();
49
+ this.notify();
51
50
  }
52
51
  /** Go back to the previous page. No-op if already on page 1. */
53
52
  prevPage() {
54
53
  if (this._page > 1) {
55
54
  this._page--;
56
- this._notify();
55
+ this.notify();
57
56
  }
58
57
  }
59
58
  /** Reset to page 1. */
60
59
  reset() {
61
60
  if (this._page === 1) return;
62
61
  this._page = 1;
63
- this._notify();
62
+ this.notify();
64
63
  }
65
64
  // ── Pipeline ──
66
65
  /** Slice an array to the current page window. Returns the page subset. */
@@ -68,17 +67,6 @@ class Pagination {
68
67
  const start = (this._page - 1) * this._pageSize;
69
68
  return items.slice(start, start + this._pageSize);
70
69
  }
71
- // ── Subscribable interface ──
72
- /** Subscribe to pagination state changes. Returns an unsubscribe function. */
73
- subscribe(cb) {
74
- this._listeners.add(cb);
75
- return () => {
76
- this._listeners.delete(cb);
77
- };
78
- }
79
- _notify() {
80
- for (const cb of this._listeners) cb();
81
- }
82
70
  }
83
71
  export {
84
72
  Pagination
@@ -1 +1 @@
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;"}
1
+ {"version":3,"file":"Pagination.js","sources":["../src/Pagination.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination extends Trackable {\n private _page: number = 1;\n private _pageSize: number;\n\n constructor(options?: { pageSize?: number }) {\n super();\n this._pageSize = options?.pageSize ?? 10;\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this.notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this.notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this.notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this.notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n}\n"],"names":[],"mappings":";AAOO,MAAM,mBAAmB,UAAU;AAAA,EAChC,QAAgB;AAAA,EAChB;AAAA,EAER,YAAY,SAAiC;AAC3C,UAAA;AACA,SAAK,YAAY,SAAS,YAAY;AAAA,EACxC;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAKA,UAAU,OAAuB;AAC/B,WAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,SAAS,CAAC;AAAA,EACtD;AAAA;AAAA,EAGA,QAAQ,OAAwB;AAC9B,WAAO,KAAK,QAAQ,KAAK,UAAU,KAAK;AAAA,EAC1C;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA,EAKA,QAAQ,MAAoB;AAC1B,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAI,YAAY,KAAK,MAAO;AAC5B,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,QAAI,OAAO,EAAG;AACd,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK;AACL,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,QAAI,KAAK,QAAQ,GAAG;AAClB,WAAK;AACL,WAAK,OAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,EAAG;AACtB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,MAAS,OAAiB;AACxB,UAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;AACtC,WAAO,MAAM,MAAM,OAAO,QAAQ,KAAK,SAAS;AAAA,EAClD;AACF;"}
package/dist/Pending.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const errors = require("./errors.cjs");
4
- const bindPublicMethods = require("./bindPublicMethods.cjs");
4
+ const Trackable = require("./Trackable.cjs");
5
5
  const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
6
- class Pending {
6
+ class Pending extends Trackable.Trackable {
7
7
  // ── Static config (Channel pattern — override via subclass) ──
8
8
  /** Maximum number of retry attempts before marking as failed. */
9
9
  static MAX_RETRIES = 5;
@@ -16,11 +16,9 @@ class Pending {
16
16
  // ── Private state ──
17
17
  _operations = /* @__PURE__ */ new Map();
18
18
  _snapshots = /* @__PURE__ */ new Map();
19
- _listeners = /* @__PURE__ */ new Set();
20
- _disposed = false;
21
19
  _entriesCache = null;
22
20
  constructor() {
23
- bindPublicMethods.bindPublicMethods(this);
21
+ super();
24
22
  }
25
23
  // ── Readable state (reactive — auto-tracked by ViewModel getters) ──
26
24
  /** Get the frozen status snapshot for an operation by ID, or null if not found. */
@@ -74,7 +72,7 @@ class Pending {
74
72
  * If the same ID already has a pending operation, it is superseded (aborted).
75
73
  */
76
74
  enqueue(id, operation, execute, meta) {
77
- if (this._disposed) {
75
+ if (this.disposed) {
78
76
  if (__DEV__) {
79
77
  console.warn("[mvc-kit] Pending.enqueue() called after dispose — ignored.");
80
78
  }
@@ -103,13 +101,13 @@ class Pending {
103
101
  };
104
102
  this._operations.set(id, op);
105
103
  this._snapshot(op);
106
- this._notify();
104
+ this.notify();
107
105
  queueMicrotask(() => this._process(id));
108
106
  }
109
107
  // ── Controls ──
110
108
  /** Retry a failed operation. No-op if the operation is not in 'failed' status. */
111
109
  retry(id) {
112
- if (this._disposed) {
110
+ if (this.disposed) {
113
111
  if (__DEV__) {
114
112
  console.warn("[mvc-kit] Pending.retry() called after dispose — ignored.");
115
113
  }
@@ -126,7 +124,7 @@ class Pending {
126
124
  }
127
125
  /** Retry all failed operations. */
128
126
  retryAll() {
129
- if (this._disposed) {
127
+ if (this.disposed) {
130
128
  if (__DEV__) {
131
129
  console.warn("[mvc-kit] Pending.retryAll() called after dispose — ignored.");
132
130
  }
@@ -150,7 +148,7 @@ class Pending {
150
148
  }
151
149
  this._operations.delete(id);
152
150
  this._snapshots.delete(id);
153
- this._notify();
151
+ this.notify();
154
152
  }
155
153
  /** Cancel all operations. */
156
154
  cancelAll() {
@@ -162,7 +160,7 @@ class Pending {
162
160
  }
163
161
  this._operations.clear();
164
162
  this._snapshots.clear();
165
- if (this._listeners.size > 0) this._notify();
163
+ this.notify();
166
164
  }
167
165
  /** Remove a failed operation without retrying. No-op if the operation is not in 'failed' status. */
168
166
  dismiss(id) {
@@ -170,7 +168,7 @@ class Pending {
170
168
  if (!op || op.status !== "failed") return;
171
169
  this._operations.delete(id);
172
170
  this._snapshots.delete(id);
173
- this._notify();
171
+ this.notify();
174
172
  }
175
173
  /** Remove all failed operations without retrying. */
176
174
  dismissAll() {
@@ -183,7 +181,7 @@ class Pending {
183
181
  this._operations.delete(id);
184
182
  this._snapshots.delete(id);
185
183
  }
186
- this._notify();
184
+ this.notify();
187
185
  }
188
186
  // ── Hooks (overridable in subclass) ──
189
187
  /**
@@ -195,25 +193,18 @@ class Pending {
195
193
  const code = errors.classifyError(error).code;
196
194
  return code === "network" || code === "timeout" || code === "server_error";
197
195
  }
198
- // ── Subscribable interface (duck-typed — auto-tracked by ViewModel) ──
199
- /** Subscribe to state changes. Returns an unsubscribe function. */
200
- subscribe(cb) {
201
- this._listeners.add(cb);
202
- return () => {
203
- this._listeners.delete(cb);
204
- };
205
- }
206
196
  // ── Lifecycle ──
207
- /** Whether this instance has been disposed. */
208
- get disposed() {
209
- return this._disposed;
210
- }
211
- /** Dispose: cancels all operations, clears listeners. */
197
+ /** Dispose: cancels all operations, then runs Trackable cleanup. */
212
198
  dispose() {
213
- if (this._disposed) return;
214
- this._disposed = true;
199
+ if (this.disposed) return;
215
200
  this.cancelAll();
216
- this._listeners.clear();
201
+ super.dispose();
202
+ }
203
+ // ── Notification override ──
204
+ /** @internal Invalidates entries cache before notifying subscribers. */
205
+ notify() {
206
+ this._entriesCache = null;
207
+ super.notify();
217
208
  }
218
209
  // ── Internals ──
219
210
  _snapshot(op) {
@@ -230,27 +221,23 @@ class Pending {
230
221
  meta: op.meta
231
222
  }));
232
223
  }
233
- _notify() {
234
- this._entriesCache = null;
235
- for (const cb of this._listeners) cb();
236
- }
237
224
  _process(id) {
238
225
  const op = this._operations.get(id);
239
- if (!op || this._disposed) return;
226
+ if (!op || this.disposed) return;
240
227
  op.status = "active";
241
228
  op.attempts++;
242
229
  op.error = null;
243
230
  op.errorCode = null;
244
231
  op.nextRetryAt = null;
245
232
  this._snapshot(op);
246
- this._notify();
233
+ this.notify();
247
234
  op.execute(op.abortController.signal).then(
248
235
  () => {
249
236
  if (this._operations.get(id) !== op) return;
250
237
  const operation = op.operation;
251
238
  this._operations.delete(id);
252
239
  this._snapshots.delete(id);
253
- this._notify();
240
+ this.notify();
254
241
  this.onConfirmed?.(id, operation);
255
242
  },
256
243
  (error) => {
@@ -258,7 +245,7 @@ class Pending {
258
245
  if (errors.isAbortError(error)) {
259
246
  this._operations.delete(id);
260
247
  this._snapshots.delete(id);
261
- this._notify();
248
+ this.notify();
262
249
  return;
263
250
  }
264
251
  const ctor = this.constructor;
@@ -270,7 +257,7 @@ class Pending {
270
257
  op.error = classified.message;
271
258
  op.errorCode = classified.code;
272
259
  this._snapshot(op);
273
- this._notify();
260
+ this.notify();
274
261
  op.retryTimer = setTimeout(() => {
275
262
  op.retryTimer = null;
276
263
  this._process(id);
@@ -281,7 +268,7 @@ class Pending {
281
268
  op.errorCode = classified.code;
282
269
  op.nextRetryAt = null;
283
270
  this._snapshot(op);
284
- this._notify();
271
+ this.notify();
285
272
  this.onFailed?.(id, op.operation, error);
286
273
  }
287
274
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Pending.cjs","sources":["../src/Pending.ts"],"sourcesContent":["import { classifyError, isAbortError } from './errors';\nimport type { AppError } from './errors';\nimport { bindPublicMethods } from './bindPublicMethods';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Types ─────────────────────────────────────────────────────────\n\n/** Frozen snapshot of a single pending operation's state. */\nexport interface PendingOperation<Meta = unknown> {\n readonly status: 'active' | 'retrying' | 'failed';\n readonly operation: string;\n readonly attempts: number;\n readonly maxRetries: number;\n readonly error: string | null;\n readonly errorCode: AppError['code'] | null;\n readonly nextRetryAt: number | null;\n readonly createdAt: number;\n readonly meta: Meta | null;\n}\n\n/** A PendingOperation snapshot paired with its key, for iteration. */\nexport interface PendingEntry<K extends string | number, Meta = unknown>\n extends PendingOperation<Meta> {\n readonly id: K;\n}\n\n/** Mutable internal state for a pending operation. */\ninterface InternalOp<K, Meta> {\n id: K;\n operation: string;\n execute: (signal: AbortSignal) => Promise<void>;\n status: PendingOperation['status'];\n attempts: number;\n error: string | null;\n errorCode: AppError['code'] | null;\n nextRetryAt: number | null;\n createdAt: number;\n abortController: AbortController;\n retryTimer: ReturnType<typeof setTimeout> | null;\n meta: Meta | null;\n}\n\n// ── Pending ───────────────────────────────────────────────────────\n\n/**\n * Per-item operation queue with retry and status tracking.\n * Tracks operations by key with exponential backoff retry on transient errors.\n * Subscribable — auto-tracked when used as a ViewModel/Resource property.\n */\nexport class Pending<K extends string | number = string | number, Meta = unknown> {\n // ── Static config (Channel pattern — override via subclass) ──\n\n /** Maximum number of retry attempts before marking as failed. */\n static MAX_RETRIES = 5;\n /** Base delay (ms) for retry backoff. */\n static RETRY_BASE = 1000;\n /** Maximum delay cap (ms) for retry backoff. */\n static RETRY_MAX = 30000;\n /** Exponential backoff multiplier for retry delay. */\n static RETRY_FACTOR = 2;\n\n // ── Private state ──\n\n private _operations = new Map<K, InternalOp<K, Meta>>();\n private _snapshots = new Map<K, PendingOperation<Meta>>();\n private _listeners = new Set<() => void>();\n private _disposed = false;\n private _entriesCache: readonly PendingEntry<K, Meta>[] | null = null;\n\n constructor() {\n bindPublicMethods(this);\n }\n\n // ── Readable state (reactive — auto-tracked by ViewModel getters) ──\n\n /** Get the frozen status snapshot for an operation by ID, or null if not found. */\n getStatus(id: K): PendingOperation<Meta> | null {\n return this._snapshots.get(id) ?? null;\n }\n\n /** Whether an operation exists for the given ID. */\n has(id: K): boolean {\n return this._operations.has(id);\n }\n\n /** Number of operations (all statuses). */\n get count(): number {\n return this._operations.size;\n }\n\n /** Whether any operations are in-flight (active or retrying). */\n get hasPending(): boolean {\n for (const op of this._operations.values()) {\n if (op.status !== 'failed') return true;\n }\n return false;\n }\n\n /** Whether any operations are in a failed state. */\n get hasFailed(): boolean {\n for (const op of this._operations.values()) {\n if (op.status === 'failed') return true;\n }\n return false;\n }\n\n /** Number of operations in a failed state. */\n get failedCount(): number {\n let n = 0;\n for (const op of this._operations.values()) {\n if (op.status === 'failed') n++;\n }\n return n;\n }\n\n /** All operations as a frozen array of entries (id + snapshot). Cached until next mutation. */\n get entries(): readonly PendingEntry<K, Meta>[] {\n if (this._entriesCache === null) {\n const result: PendingEntry<K, Meta>[] = [];\n for (const [id, snapshot] of this._snapshots) {\n result.push(Object.freeze({ ...snapshot, id }));\n }\n this._entriesCache = Object.freeze(result) as readonly PendingEntry<K, Meta>[];\n }\n return this._entriesCache;\n }\n\n // ── Core API ──\n\n /**\n * Enqueue an operation for the given ID. Fire-and-forget (synchronous return).\n * If the same ID already has a pending operation, it is superseded (aborted).\n */\n enqueue(id: K, operation: string, execute: (signal: AbortSignal) => Promise<void>, meta?: Meta): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.enqueue() called after dispose — ignored.');\n }\n return;\n }\n\n // Supersede existing operation for this ID\n const existing = this._operations.get(id);\n if (existing) {\n existing.abortController.abort();\n if (existing.retryTimer !== null) {\n clearTimeout(existing.retryTimer);\n }\n }\n\n const op: InternalOp<K, Meta> = {\n id,\n operation,\n execute,\n status: 'active',\n attempts: 0,\n error: null,\n errorCode: null,\n nextRetryAt: null,\n createdAt: Date.now(),\n abortController: new AbortController(),\n retryTimer: null,\n meta: meta ?? null,\n };\n\n this._operations.set(id, op);\n this._snapshot(op);\n this._notify();\n\n // Schedule processing via microtask (allows batching multiple enqueues)\n queueMicrotask(() => this._process(id));\n }\n\n // ── Controls ──\n\n /** Retry a failed operation. No-op if the operation is not in 'failed' status. */\n retry(id: K): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retry() called after dispose — ignored.');\n }\n return;\n }\n\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n op.attempts = 0;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n op.abortController = new AbortController();\n this._process(id);\n }\n\n /** Retry all failed operations. */\n retryAll(): void {\n if (this._disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retryAll() called after dispose — ignored.');\n }\n return;\n }\n\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n for (const id of failedIds) {\n this.retry(id);\n }\n }\n\n /** Cancel an in-flight operation by ID. Aborts the signal, clears timers, and removes it. */\n cancel(id: K): void {\n const op = this._operations.get(id);\n if (!op) return;\n\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n }\n\n /** Cancel all operations. */\n cancelAll(): void {\n for (const op of this._operations.values()) {\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n }\n this._operations.clear();\n this._snapshots.clear();\n if (this._listeners.size > 0) this._notify();\n }\n\n /** Remove a failed operation without retrying. No-op if the operation is not in 'failed' status. */\n dismiss(id: K): void {\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n }\n\n /** Remove all failed operations without retrying. */\n dismissAll(): void {\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n if (failedIds.length === 0) return;\n for (const id of failedIds) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n }\n this._notify();\n }\n\n // ── Hooks (overridable in subclass) ──\n\n /**\n * Determines whether an error is retryable. Override in a subclass to customize.\n * Default: retries on network, timeout, and server_error codes.\n * @protected\n */\n protected isRetryable(error: unknown): boolean {\n const code = classifyError(error).code;\n return code === 'network' || code === 'timeout' || code === 'server_error';\n }\n\n /** Called when an operation succeeds. Override in a subclass for side effects. @protected */\n protected onConfirmed?(id: K, operation: string): void;\n /** Called when an operation fails permanently. Override in a subclass for side effects. @protected */\n protected onFailed?(id: K, operation: string, error: unknown): void;\n\n // ── Subscribable interface (duck-typed — auto-tracked by ViewModel) ──\n\n /** Subscribe to 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 // ── Lifecycle ──\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Dispose: cancels all operations, clears listeners. */\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this.cancelAll();\n this._listeners.clear();\n }\n\n // ── Internals ──\n\n private _snapshot(op: InternalOp<K, Meta>): void {\n const ctor = this.constructor as typeof Pending;\n this._snapshots.set(op.id, Object.freeze({\n status: op.status,\n operation: op.operation,\n attempts: op.attempts,\n maxRetries: ctor.MAX_RETRIES,\n error: op.error,\n errorCode: op.errorCode,\n nextRetryAt: op.nextRetryAt,\n createdAt: op.createdAt,\n meta: op.meta,\n }));\n }\n\n private _notify(): void {\n this._entriesCache = null;\n for (const cb of this._listeners) cb();\n }\n\n private _process(id: K): void {\n const op = this._operations.get(id);\n if (!op || this._disposed) return;\n\n // Set active status\n op.status = 'active';\n op.attempts++;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n this._snapshot(op);\n this._notify();\n\n op.execute(op.abortController.signal).then(\n () => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n const operation = op.operation;\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n this.onConfirmed?.(id, operation);\n },\n (error: unknown) => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n\n // AbortError — remove silently (cancel or supersede)\n if (isAbortError(error)) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n this._notify();\n return;\n }\n\n const ctor = this.constructor as typeof Pending;\n const classified = classifyError(error);\n\n // Check if retryable and under max retries\n if (this.isRetryable(error) && op.attempts < ctor.MAX_RETRIES) {\n op.status = 'retrying';\n const delay = this._calculateDelay(op.attempts - 1);\n op.nextRetryAt = Date.now() + delay;\n op.error = classified.message;\n op.errorCode = classified.code;\n this._snapshot(op);\n this._notify();\n\n op.retryTimer = setTimeout(() => {\n op.retryTimer = null;\n this._process(id);\n }, delay);\n } else {\n // Non-retryable or max retries exceeded\n op.status = 'failed';\n op.error = classified.message;\n op.errorCode = classified.code;\n op.nextRetryAt = null;\n this._snapshot(op);\n this._notify();\n this.onFailed?.(id, op.operation, error);\n }\n },\n );\n }\n\n /** Computes retry backoff delay with jitter (Channel formula). @private */\n private _calculateDelay(attempt: number): number {\n const ctor = this.constructor as typeof Pending;\n const capped = Math.min(\n ctor.RETRY_BASE * Math.pow(ctor.RETRY_FACTOR, attempt),\n ctor.RETRY_MAX,\n );\n return Math.random() * capped;\n }\n}\n"],"names":["bindPublicMethods","classifyError","isAbortError"],"mappings":"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"}
1
+ {"version":3,"file":"Pending.cjs","sources":["../src/Pending.ts"],"sourcesContent":["import { classifyError, isAbortError } from './errors';\nimport type { AppError } from './errors';\nimport { Trackable } from './Trackable';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Types ─────────────────────────────────────────────────────────\n\n/** Frozen snapshot of a single pending operation's state. */\nexport interface PendingOperation<Meta = unknown> {\n readonly status: 'active' | 'retrying' | 'failed';\n readonly operation: string;\n readonly attempts: number;\n readonly maxRetries: number;\n readonly error: string | null;\n readonly errorCode: AppError['code'] | null;\n readonly nextRetryAt: number | null;\n readonly createdAt: number;\n readonly meta: Meta | null;\n}\n\n/** A PendingOperation snapshot paired with its key, for iteration. */\nexport interface PendingEntry<K extends string | number, Meta = unknown>\n extends PendingOperation<Meta> {\n readonly id: K;\n}\n\n/** Mutable internal state for a pending operation. */\ninterface InternalOp<K, Meta> {\n id: K;\n operation: string;\n execute: (signal: AbortSignal) => Promise<void>;\n status: PendingOperation['status'];\n attempts: number;\n error: string | null;\n errorCode: AppError['code'] | null;\n nextRetryAt: number | null;\n createdAt: number;\n abortController: AbortController;\n retryTimer: ReturnType<typeof setTimeout> | null;\n meta: Meta | null;\n}\n\n// ── Pending ───────────────────────────────────────────────────────\n\n/**\n * Per-item operation queue with retry and status tracking.\n * Tracks operations by key with exponential backoff retry on transient errors.\n * Subscribable — auto-tracked when used as a ViewModel/Resource property.\n */\nexport class Pending<K extends string | number = string | number, Meta = unknown> extends Trackable {\n // ── Static config (Channel pattern — override via subclass) ──\n\n /** Maximum number of retry attempts before marking as failed. */\n static MAX_RETRIES = 5;\n /** Base delay (ms) for retry backoff. */\n static RETRY_BASE = 1000;\n /** Maximum delay cap (ms) for retry backoff. */\n static RETRY_MAX = 30000;\n /** Exponential backoff multiplier for retry delay. */\n static RETRY_FACTOR = 2;\n\n // ── Private state ──\n\n private _operations = new Map<K, InternalOp<K, Meta>>();\n private _snapshots = new Map<K, PendingOperation<Meta>>();\n private _entriesCache: readonly PendingEntry<K, Meta>[] | null = null;\n\n constructor() {\n super();\n }\n\n // ── Readable state (reactive — auto-tracked by ViewModel getters) ──\n\n /** Get the frozen status snapshot for an operation by ID, or null if not found. */\n getStatus(id: K): PendingOperation<Meta> | null {\n return this._snapshots.get(id) ?? null;\n }\n\n /** Whether an operation exists for the given ID. */\n has(id: K): boolean {\n return this._operations.has(id);\n }\n\n /** Number of operations (all statuses). */\n get count(): number {\n return this._operations.size;\n }\n\n /** Whether any operations are in-flight (active or retrying). */\n get hasPending(): boolean {\n for (const op of this._operations.values()) {\n if (op.status !== 'failed') return true;\n }\n return false;\n }\n\n /** Whether any operations are in a failed state. */\n get hasFailed(): boolean {\n for (const op of this._operations.values()) {\n if (op.status === 'failed') return true;\n }\n return false;\n }\n\n /** Number of operations in a failed state. */\n get failedCount(): number {\n let n = 0;\n for (const op of this._operations.values()) {\n if (op.status === 'failed') n++;\n }\n return n;\n }\n\n /** All operations as a frozen array of entries (id + snapshot). Cached until next mutation. */\n get entries(): readonly PendingEntry<K, Meta>[] {\n if (this._entriesCache === null) {\n const result: PendingEntry<K, Meta>[] = [];\n for (const [id, snapshot] of this._snapshots) {\n result.push(Object.freeze({ ...snapshot, id }));\n }\n this._entriesCache = Object.freeze(result) as readonly PendingEntry<K, Meta>[];\n }\n return this._entriesCache;\n }\n\n // ── Core API ──\n\n /**\n * Enqueue an operation for the given ID. Fire-and-forget (synchronous return).\n * If the same ID already has a pending operation, it is superseded (aborted).\n */\n enqueue(id: K, operation: string, execute: (signal: AbortSignal) => Promise<void>, meta?: Meta): void {\n if (this.disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.enqueue() called after dispose — ignored.');\n }\n return;\n }\n\n // Supersede existing operation for this ID\n const existing = this._operations.get(id);\n if (existing) {\n existing.abortController.abort();\n if (existing.retryTimer !== null) {\n clearTimeout(existing.retryTimer);\n }\n }\n\n const op: InternalOp<K, Meta> = {\n id,\n operation,\n execute,\n status: 'active',\n attempts: 0,\n error: null,\n errorCode: null,\n nextRetryAt: null,\n createdAt: Date.now(),\n abortController: new AbortController(),\n retryTimer: null,\n meta: meta ?? null,\n };\n\n this._operations.set(id, op);\n this._snapshot(op);\n this.notify();\n\n // Schedule processing via microtask (allows batching multiple enqueues)\n queueMicrotask(() => this._process(id));\n }\n\n // ── Controls ──\n\n /** Retry a failed operation. No-op if the operation is not in 'failed' status. */\n retry(id: K): void {\n if (this.disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retry() called after dispose — ignored.');\n }\n return;\n }\n\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n op.attempts = 0;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n op.abortController = new AbortController();\n this._process(id);\n }\n\n /** Retry all failed operations. */\n retryAll(): void {\n if (this.disposed) {\n if (__DEV__) {\n console.warn('[mvc-kit] Pending.retryAll() called after dispose — ignored.');\n }\n return;\n }\n\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n for (const id of failedIds) {\n this.retry(id);\n }\n }\n\n /** Cancel an in-flight operation by ID. Aborts the signal, clears timers, and removes it. */\n cancel(id: K): void {\n const op = this._operations.get(id);\n if (!op) return;\n\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n }\n\n /** Cancel all operations. */\n cancelAll(): void {\n for (const op of this._operations.values()) {\n op.abortController.abort();\n if (op.retryTimer !== null) {\n clearTimeout(op.retryTimer);\n }\n }\n this._operations.clear();\n this._snapshots.clear();\n this.notify();\n }\n\n /** Remove a failed operation without retrying. No-op if the operation is not in 'failed' status. */\n dismiss(id: K): void {\n const op = this._operations.get(id);\n if (!op || op.status !== 'failed') return;\n\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n }\n\n /** Remove all failed operations without retrying. */\n dismissAll(): void {\n const failedIds: K[] = [];\n for (const op of this._operations.values()) {\n if (op.status === 'failed') failedIds.push(op.id);\n }\n if (failedIds.length === 0) return;\n for (const id of failedIds) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n }\n this.notify();\n }\n\n // ── Hooks (overridable in subclass) ──\n\n /**\n * Determines whether an error is retryable. Override in a subclass to customize.\n * Default: retries on network, timeout, and server_error codes.\n * @protected\n */\n protected isRetryable(error: unknown): boolean {\n const code = classifyError(error).code;\n return code === 'network' || code === 'timeout' || code === 'server_error';\n }\n\n /** Called when an operation succeeds. Override in a subclass for side effects. @protected */\n protected onConfirmed?(id: K, operation: string): void;\n /** Called when an operation fails permanently. Override in a subclass for side effects. @protected */\n protected onFailed?(id: K, operation: string, error: unknown): void;\n\n // ── Lifecycle ──\n\n /** Dispose: cancels all operations, then runs Trackable cleanup. */\n dispose(): void {\n if (this.disposed) return;\n this.cancelAll();\n super.dispose();\n }\n\n // ── Notification override ──\n\n /** @internal Invalidates entries cache before notifying subscribers. */\n protected notify(): void {\n this._entriesCache = null;\n super.notify();\n }\n\n // ── Internals ──\n\n private _snapshot(op: InternalOp<K, Meta>): void {\n const ctor = this.constructor as typeof Pending;\n this._snapshots.set(op.id, Object.freeze({\n status: op.status,\n operation: op.operation,\n attempts: op.attempts,\n maxRetries: ctor.MAX_RETRIES,\n error: op.error,\n errorCode: op.errorCode,\n nextRetryAt: op.nextRetryAt,\n createdAt: op.createdAt,\n meta: op.meta,\n }));\n }\n\n private _process(id: K): void {\n const op = this._operations.get(id);\n if (!op || this.disposed) return;\n\n // Set active status\n op.status = 'active';\n op.attempts++;\n op.error = null;\n op.errorCode = null;\n op.nextRetryAt = null;\n this._snapshot(op);\n this.notify();\n\n op.execute(op.abortController.signal).then(\n () => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n const operation = op.operation;\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n this.onConfirmed?.(id, operation);\n },\n (error: unknown) => {\n // Identity check: ignore if this op was superseded or cancelled\n if (this._operations.get(id) !== op) return;\n\n // AbortError — remove silently (cancel or supersede)\n if (isAbortError(error)) {\n this._operations.delete(id);\n this._snapshots.delete(id);\n this.notify();\n return;\n }\n\n const ctor = this.constructor as typeof Pending;\n const classified = classifyError(error);\n\n // Check if retryable and under max retries\n if (this.isRetryable(error) && op.attempts < ctor.MAX_RETRIES) {\n op.status = 'retrying';\n const delay = this._calculateDelay(op.attempts - 1);\n op.nextRetryAt = Date.now() + delay;\n op.error = classified.message;\n op.errorCode = classified.code;\n this._snapshot(op);\n this.notify();\n\n op.retryTimer = setTimeout(() => {\n op.retryTimer = null;\n this._process(id);\n }, delay);\n } else {\n // Non-retryable or max retries exceeded\n op.status = 'failed';\n op.error = classified.message;\n op.errorCode = classified.code;\n op.nextRetryAt = null;\n this._snapshot(op);\n this.notify();\n this.onFailed?.(id, op.operation, error);\n }\n },\n );\n }\n\n /** Computes retry backoff delay with jitter (Channel formula). @private */\n private _calculateDelay(attempt: number): number {\n const ctor = this.constructor as typeof Pending;\n const capped = Math.min(\n ctor.RETRY_BASE * Math.pow(ctor.RETRY_FACTOR, attempt),\n ctor.RETRY_MAX,\n );\n return Math.random() * capped;\n }\n}\n"],"names":["Trackable","classifyError","isAbortError"],"mappings":"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"}
package/dist/Pending.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AppError } from './errors';
2
+ import { Trackable } from './Trackable';
2
3
  /** Frozen snapshot of a single pending operation's state. */
3
4
  export interface PendingOperation<Meta = unknown> {
4
5
  readonly status: 'active' | 'retrying' | 'failed';
@@ -20,7 +21,7 @@ export interface PendingEntry<K extends string | number, Meta = unknown> extends
20
21
  * Tracks operations by key with exponential backoff retry on transient errors.
21
22
  * Subscribable — auto-tracked when used as a ViewModel/Resource property.
22
23
  */
23
- export declare class Pending<K extends string | number = string | number, Meta = unknown> {
24
+ export declare class Pending<K extends string | number = string | number, Meta = unknown> extends Trackable {
24
25
  /** Maximum number of retry attempts before marking as failed. */
25
26
  static MAX_RETRIES: number;
26
27
  /** Base delay (ms) for retry backoff. */
@@ -31,8 +32,6 @@ export declare class Pending<K extends string | number = string | number, Meta =
31
32
  static RETRY_FACTOR: number;
32
33
  private _operations;
33
34
  private _snapshots;
34
- private _listeners;
35
- private _disposed;
36
35
  private _entriesCache;
37
36
  constructor();
38
37
  /** Get the frozen status snapshot for an operation by ID, or null if not found. */
@@ -76,14 +75,11 @@ export declare class Pending<K extends string | number = string | number, Meta =
76
75
  protected onConfirmed?(id: K, operation: string): void;
77
76
  /** Called when an operation fails permanently. Override in a subclass for side effects. @protected */
78
77
  protected onFailed?(id: K, operation: string, error: unknown): void;
79
- /** Subscribe to state changes. Returns an unsubscribe function. */
80
- subscribe(cb: () => void): () => void;
81
- /** Whether this instance has been disposed. */
82
- get disposed(): boolean;
83
- /** Dispose: cancels all operations, clears listeners. */
78
+ /** Dispose: cancels all operations, then runs Trackable cleanup. */
84
79
  dispose(): void;
80
+ /** @internal Invalidates entries cache before notifying subscribers. */
81
+ protected notify(): void;
85
82
  private _snapshot;
86
- private _notify;
87
83
  private _process;
88
84
  /** Computes retry backoff delay with jitter (Channel formula). @private */
89
85
  private _calculateDelay;
@@ -1 +1 @@
1
- {"version":3,"file":"Pending.d.ts","sourceRoot":"","sources":["../src/Pending.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Pending.d.ts","sourceRoot":"","sources":["../src/Pending.ts"],"names":[],"mappings":"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"}
package/dist/Pending.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { classifyError, isAbortError } from "./errors.js";
2
- import { bindPublicMethods } from "./bindPublicMethods.js";
2
+ import { Trackable } from "./Trackable.js";
3
3
  const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
4
- class Pending {
4
+ class Pending extends Trackable {
5
5
  // ── Static config (Channel pattern — override via subclass) ──
6
6
  /** Maximum number of retry attempts before marking as failed. */
7
7
  static MAX_RETRIES = 5;
@@ -14,11 +14,9 @@ class Pending {
14
14
  // ── Private state ──
15
15
  _operations = /* @__PURE__ */ new Map();
16
16
  _snapshots = /* @__PURE__ */ new Map();
17
- _listeners = /* @__PURE__ */ new Set();
18
- _disposed = false;
19
17
  _entriesCache = null;
20
18
  constructor() {
21
- bindPublicMethods(this);
19
+ super();
22
20
  }
23
21
  // ── Readable state (reactive — auto-tracked by ViewModel getters) ──
24
22
  /** Get the frozen status snapshot for an operation by ID, or null if not found. */
@@ -72,7 +70,7 @@ class Pending {
72
70
  * If the same ID already has a pending operation, it is superseded (aborted).
73
71
  */
74
72
  enqueue(id, operation, execute, meta) {
75
- if (this._disposed) {
73
+ if (this.disposed) {
76
74
  if (__DEV__) {
77
75
  console.warn("[mvc-kit] Pending.enqueue() called after dispose — ignored.");
78
76
  }
@@ -101,13 +99,13 @@ class Pending {
101
99
  };
102
100
  this._operations.set(id, op);
103
101
  this._snapshot(op);
104
- this._notify();
102
+ this.notify();
105
103
  queueMicrotask(() => this._process(id));
106
104
  }
107
105
  // ── Controls ──
108
106
  /** Retry a failed operation. No-op if the operation is not in 'failed' status. */
109
107
  retry(id) {
110
- if (this._disposed) {
108
+ if (this.disposed) {
111
109
  if (__DEV__) {
112
110
  console.warn("[mvc-kit] Pending.retry() called after dispose — ignored.");
113
111
  }
@@ -124,7 +122,7 @@ class Pending {
124
122
  }
125
123
  /** Retry all failed operations. */
126
124
  retryAll() {
127
- if (this._disposed) {
125
+ if (this.disposed) {
128
126
  if (__DEV__) {
129
127
  console.warn("[mvc-kit] Pending.retryAll() called after dispose — ignored.");
130
128
  }
@@ -148,7 +146,7 @@ class Pending {
148
146
  }
149
147
  this._operations.delete(id);
150
148
  this._snapshots.delete(id);
151
- this._notify();
149
+ this.notify();
152
150
  }
153
151
  /** Cancel all operations. */
154
152
  cancelAll() {
@@ -160,7 +158,7 @@ class Pending {
160
158
  }
161
159
  this._operations.clear();
162
160
  this._snapshots.clear();
163
- if (this._listeners.size > 0) this._notify();
161
+ this.notify();
164
162
  }
165
163
  /** Remove a failed operation without retrying. No-op if the operation is not in 'failed' status. */
166
164
  dismiss(id) {
@@ -168,7 +166,7 @@ class Pending {
168
166
  if (!op || op.status !== "failed") return;
169
167
  this._operations.delete(id);
170
168
  this._snapshots.delete(id);
171
- this._notify();
169
+ this.notify();
172
170
  }
173
171
  /** Remove all failed operations without retrying. */
174
172
  dismissAll() {
@@ -181,7 +179,7 @@ class Pending {
181
179
  this._operations.delete(id);
182
180
  this._snapshots.delete(id);
183
181
  }
184
- this._notify();
182
+ this.notify();
185
183
  }
186
184
  // ── Hooks (overridable in subclass) ──
187
185
  /**
@@ -193,25 +191,18 @@ class Pending {
193
191
  const code = classifyError(error).code;
194
192
  return code === "network" || code === "timeout" || code === "server_error";
195
193
  }
196
- // ── Subscribable interface (duck-typed — auto-tracked by ViewModel) ──
197
- /** Subscribe to state changes. Returns an unsubscribe function. */
198
- subscribe(cb) {
199
- this._listeners.add(cb);
200
- return () => {
201
- this._listeners.delete(cb);
202
- };
203
- }
204
194
  // ── Lifecycle ──
205
- /** Whether this instance has been disposed. */
206
- get disposed() {
207
- return this._disposed;
208
- }
209
- /** Dispose: cancels all operations, clears listeners. */
195
+ /** Dispose: cancels all operations, then runs Trackable cleanup. */
210
196
  dispose() {
211
- if (this._disposed) return;
212
- this._disposed = true;
197
+ if (this.disposed) return;
213
198
  this.cancelAll();
214
- this._listeners.clear();
199
+ super.dispose();
200
+ }
201
+ // ── Notification override ──
202
+ /** @internal Invalidates entries cache before notifying subscribers. */
203
+ notify() {
204
+ this._entriesCache = null;
205
+ super.notify();
215
206
  }
216
207
  // ── Internals ──
217
208
  _snapshot(op) {
@@ -228,27 +219,23 @@ class Pending {
228
219
  meta: op.meta
229
220
  }));
230
221
  }
231
- _notify() {
232
- this._entriesCache = null;
233
- for (const cb of this._listeners) cb();
234
- }
235
222
  _process(id) {
236
223
  const op = this._operations.get(id);
237
- if (!op || this._disposed) return;
224
+ if (!op || this.disposed) return;
238
225
  op.status = "active";
239
226
  op.attempts++;
240
227
  op.error = null;
241
228
  op.errorCode = null;
242
229
  op.nextRetryAt = null;
243
230
  this._snapshot(op);
244
- this._notify();
231
+ this.notify();
245
232
  op.execute(op.abortController.signal).then(
246
233
  () => {
247
234
  if (this._operations.get(id) !== op) return;
248
235
  const operation = op.operation;
249
236
  this._operations.delete(id);
250
237
  this._snapshots.delete(id);
251
- this._notify();
238
+ this.notify();
252
239
  this.onConfirmed?.(id, operation);
253
240
  },
254
241
  (error) => {
@@ -256,7 +243,7 @@ class Pending {
256
243
  if (isAbortError(error)) {
257
244
  this._operations.delete(id);
258
245
  this._snapshots.delete(id);
259
- this._notify();
246
+ this.notify();
260
247
  return;
261
248
  }
262
249
  const ctor = this.constructor;
@@ -268,7 +255,7 @@ class Pending {
268
255
  op.error = classified.message;
269
256
  op.errorCode = classified.code;
270
257
  this._snapshot(op);
271
- this._notify();
258
+ this.notify();
272
259
  op.retryTimer = setTimeout(() => {
273
260
  op.retryTimer = null;
274
261
  this._process(id);
@@ -279,7 +266,7 @@ class Pending {
279
266
  op.errorCode = classified.code;
280
267
  op.nextRetryAt = null;
281
268
  this._snapshot(op);
282
- this._notify();
269
+ this.notify();
283
270
  this.onFailed?.(id, op.operation, error);
284
271
  }
285
272
  }