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.
- package/README.md +4 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +41 -0
- package/agent-config/claude-code/skills/guide/api-reference.md +43 -1
- package/agent-config/claude-code/skills/guide/patterns.md +64 -0
- package/agent-config/copilot/copilot-instructions.md +10 -6
- package/agent-config/cursor/cursorrules +10 -6
- package/dist/Feed.cjs +10 -22
- package/dist/Feed.cjs.map +1 -1
- package/dist/Feed.d.ts +2 -5
- package/dist/Feed.d.ts.map +1 -1
- package/dist/Feed.js +10 -22
- package/dist/Feed.js.map +1 -1
- package/dist/Pagination.cjs +8 -20
- package/dist/Pagination.cjs.map +1 -1
- package/dist/Pagination.d.ts +2 -5
- package/dist/Pagination.d.ts.map +1 -1
- package/dist/Pagination.js +8 -20
- package/dist/Pagination.js.map +1 -1
- package/dist/Pending.cjs +26 -39
- package/dist/Pending.cjs.map +1 -1
- package/dist/Pending.d.ts +5 -9
- package/dist/Pending.d.ts.map +1 -1
- package/dist/Pending.js +26 -39
- package/dist/Pending.js.map +1 -1
- package/dist/Selection.cjs +5 -13
- package/dist/Selection.cjs.map +1 -1
- package/dist/Selection.d.ts +2 -4
- package/dist/Selection.d.ts.map +1 -1
- package/dist/Selection.js +5 -13
- package/dist/Selection.js.map +1 -1
- package/dist/Sorting.cjs +7 -19
- package/dist/Sorting.cjs.map +1 -1
- package/dist/Sorting.d.ts +2 -5
- package/dist/Sorting.d.ts.map +1 -1
- package/dist/Sorting.js +7 -19
- package/dist/Sorting.js.map +1 -1
- package/dist/Trackable.cjs +81 -0
- package/dist/Trackable.cjs.map +1 -0
- package/dist/Trackable.d.ts +82 -0
- package/dist/Trackable.d.ts.map +1 -0
- package/dist/Trackable.js +81 -0
- package/dist/Trackable.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +4 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +4 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/guards.cjs +2 -0
- package/dist/react/guards.cjs.map +1 -1
- package/dist/react/guards.d.ts +4 -0
- package/dist/react/guards.d.ts.map +1 -1
- package/dist/react/guards.js +3 -1
- package/dist/react/guards.js.map +1 -1
- package/dist/react/use-local.cjs +5 -0
- package/dist/react/use-local.cjs.map +1 -1
- package/dist/react/use-local.d.ts.map +1 -1
- package/dist/react/use-local.js +6 -1
- package/dist/react/use-local.js.map +1 -1
- package/dist/react/use-singleton.cjs +5 -0
- package/dist/react/use-singleton.cjs.map +1 -1
- package/dist/react/use-singleton.d.ts.map +1 -1
- package/dist/react/use-singleton.js +6 -1
- package/dist/react/use-singleton.js.map +1 -1
- package/dist/react/use-subscribe-only.cjs +25 -0
- package/dist/react/use-subscribe-only.cjs.map +1 -0
- package/dist/react/use-subscribe-only.d.ts +9 -0
- package/dist/react/use-subscribe-only.d.ts.map +1 -0
- package/dist/react/use-subscribe-only.js +25 -0
- package/dist/react/use-subscribe-only.js.map +1 -0
- package/package.json +2 -1
package/dist/Pagination.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["import { Trackable } from './Trackable';\n\n/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination extends Trackable {\n private _page: number = 1;\n private _pageSize: number;\n\n constructor(options?: { pageSize?: number }) {\n super();\n this._pageSize = options?.pageSize ?? 10;\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this.notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this.notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this.notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this.notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this.notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n}\n"],"names":["Trackable"],"mappings":";;;AAOO,MAAM,mBAAmBA,UAAAA,UAAU;AAAA,EAChC,QAAgB;AAAA,EAChB;AAAA,EAER,YAAY,SAAiC;AAC3C,UAAA;AACA,SAAK,YAAY,SAAS,YAAY;AAAA,EACxC;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,WAAmB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAKA,UAAU,OAAuB;AAC/B,WAAO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,KAAK,SAAS,CAAC;AAAA,EACtD;AAAA;AAAA,EAGA,QAAQ,OAAwB;AAC9B,WAAO,KAAK,QAAQ,KAAK,UAAU,KAAK;AAAA,EAC1C;AAAA;AAAA,EAGA,UAAmB;AACjB,WAAO,KAAK,QAAQ;AAAA,EACtB;AAAA;AAAA;AAAA,EAKA,QAAQ,MAAoB;AAC1B,UAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;AAC5C,QAAI,YAAY,KAAK,MAAO;AAC5B,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,YAAY,MAAoB;AAC9B,QAAI,OAAO,EAAG;AACd,SAAK,YAAY;AACjB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,SAAK;AACL,SAAK,OAAA;AAAA,EACP;AAAA;AAAA,EAGA,WAAiB;AACf,QAAI,KAAK,QAAQ,GAAG;AAClB,WAAK;AACL,WAAK,OAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,UAAU,EAAG;AACtB,SAAK,QAAQ;AACb,SAAK,OAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAKA,MAAS,OAAiB;AACxB,UAAM,SAAS,KAAK,QAAQ,KAAK,KAAK;AACtC,WAAO,MAAM,MAAM,OAAO,QAAQ,KAAK,SAAS;AAAA,EAClD;AACF;;"}
|
package/dist/Pagination.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { Trackable } from './Trackable';
|
|
1
2
|
/**
|
|
2
3
|
* Page-based pagination state manager with array slicing pipeline.
|
|
3
4
|
* Tracks current page and page size, provides navigation helpers.
|
|
4
5
|
* Subscribable — auto-tracked when used as a ViewModel property.
|
|
5
6
|
*/
|
|
6
|
-
export declare class Pagination {
|
|
7
|
+
export declare class Pagination extends Trackable {
|
|
7
8
|
private _page;
|
|
8
9
|
private _pageSize;
|
|
9
|
-
private _listeners;
|
|
10
10
|
constructor(options?: {
|
|
11
11
|
pageSize?: number;
|
|
12
12
|
});
|
|
@@ -32,8 +32,5 @@ export declare class Pagination {
|
|
|
32
32
|
reset(): void;
|
|
33
33
|
/** Slice an array to the current page window. Returns the page subset. */
|
|
34
34
|
apply<T>(items: T[]): T[];
|
|
35
|
-
/** Subscribe to pagination state changes. Returns an unsubscribe function. */
|
|
36
|
-
subscribe(cb: () => void): () => void;
|
|
37
|
-
private _notify;
|
|
38
35
|
}
|
|
39
36
|
//# sourceMappingURL=Pagination.d.ts.map
|
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":"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"}
|
package/dist/Pagination.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
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.
|
|
44
|
+
this.notify();
|
|
46
45
|
}
|
|
47
46
|
/** Advance to the next page. */
|
|
48
47
|
nextPage() {
|
|
49
48
|
this._page++;
|
|
50
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
package/dist/Pagination.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pagination.js","sources":["../src/Pagination.ts"],"sourcesContent":["import {
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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.
|
|
214
|
-
this._disposed = true;
|
|
199
|
+
if (this.disposed) return;
|
|
215
200
|
this.cancelAll();
|
|
216
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
271
|
+
this.notify();
|
|
285
272
|
this.onFailed?.(id, op.operation, error);
|
|
286
273
|
}
|
|
287
274
|
}
|
package/dist/Pending.cjs.map
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/Pending.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Pending.d.ts","sourceRoot":"","sources":["../src/Pending.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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.
|
|
212
|
-
this._disposed = true;
|
|
197
|
+
if (this.disposed) return;
|
|
213
198
|
this.cancelAll();
|
|
214
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
269
|
+
this.notify();
|
|
283
270
|
this.onFailed?.(id, op.operation, error);
|
|
284
271
|
}
|
|
285
272
|
}
|