mvc-kit 2.8.0 → 2.9.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 +29 -0
- package/agent-config/claude-code/skills/guide/anti-patterns.md +3 -3
- package/agent-config/claude-code/skills/guide/api-reference.md +138 -1
- package/agent-config/claude-code/skills/guide/patterns.md +120 -0
- package/agent-config/copilot/copilot-instructions.md +52 -0
- package/agent-config/cursor/cursorrules +52 -0
- package/dist/Collection.cjs +38 -0
- package/dist/Collection.cjs.map +1 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +38 -0
- package/dist/Collection.js.map +1 -1
- package/dist/Feed.cjs +86 -0
- package/dist/Feed.cjs.map +1 -0
- package/dist/Feed.d.ts +46 -0
- package/dist/Feed.d.ts.map +1 -0
- package/dist/Feed.js +86 -0
- package/dist/Feed.js.map +1 -0
- package/dist/Pagination.cjs +84 -0
- package/dist/Pagination.cjs.map +1 -0
- package/dist/Pagination.d.ts +39 -0
- package/dist/Pagination.d.ts.map +1 -0
- package/dist/Pagination.js +84 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/PersistentCollection.cjs +8 -5
- package/dist/PersistentCollection.cjs.map +1 -1
- package/dist/PersistentCollection.d.ts +6 -1
- package/dist/PersistentCollection.d.ts.map +1 -1
- package/dist/PersistentCollection.js +8 -5
- package/dist/PersistentCollection.js.map +1 -1
- package/dist/Resource.cjs +3 -0
- package/dist/Resource.cjs.map +1 -1
- package/dist/Resource.d.ts +3 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +3 -0
- package/dist/Resource.js.map +1 -1
- package/dist/Selection.cjs +99 -0
- package/dist/Selection.cjs.map +1 -0
- package/dist/Selection.d.ts +36 -0
- package/dist/Selection.d.ts.map +1 -0
- package/dist/Selection.js +99 -0
- package/dist/Selection.js.map +1 -0
- package/dist/Sorting.cjs +114 -0
- package/dist/Sorting.cjs.map +1 -0
- package/dist/Sorting.d.ts +43 -0
- package/dist/Sorting.d.ts.map +1 -0
- package/dist/Sorting.js +114 -0
- package/dist/Sorting.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/mvc-kit.cjs +8 -0
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +8 -0
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/components/CardList.cjs +42 -0
- package/dist/react/components/CardList.cjs.map +1 -0
- package/dist/react/components/CardList.d.ts +22 -0
- package/dist/react/components/CardList.d.ts.map +1 -0
- package/dist/react/components/CardList.js +42 -0
- package/dist/react/components/CardList.js.map +1 -0
- package/dist/react/components/DataTable.cjs +179 -0
- package/dist/react/components/DataTable.cjs.map +1 -0
- package/dist/react/components/DataTable.d.ts +30 -0
- package/dist/react/components/DataTable.d.ts.map +1 -0
- package/dist/react/components/DataTable.js +179 -0
- package/dist/react/components/DataTable.js.map +1 -0
- package/dist/react/components/InfiniteScroll.cjs +44 -0
- package/dist/react/components/InfiniteScroll.cjs.map +1 -0
- package/dist/react/components/InfiniteScroll.d.ts +21 -0
- package/dist/react/components/InfiniteScroll.d.ts.map +1 -0
- package/dist/react/components/InfiniteScroll.js +44 -0
- package/dist/react/components/InfiniteScroll.js.map +1 -0
- package/dist/react/components/types.cjs +15 -0
- package/dist/react/components/types.cjs.map +1 -0
- package/dist/react/components/types.d.ts +71 -0
- package/dist/react/components/types.d.ts.map +1 -0
- package/dist/react/components/types.js +15 -0
- package/dist/react/components/types.js.map +1 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react-native/NativeCollection.cjs +3 -0
- package/dist/react-native/NativeCollection.cjs.map +1 -1
- package/dist/react-native/NativeCollection.d.ts +3 -0
- package/dist/react-native/NativeCollection.d.ts.map +1 -1
- package/dist/react-native/NativeCollection.js +3 -0
- package/dist/react-native/NativeCollection.js.map +1 -1
- package/dist/react.cjs +6 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +6 -0
- package/dist/react.js.map +1 -1
- package/dist/web/idb.cjs.map +1 -1
- package/dist/web/idb.d.ts +18 -0
- package/dist/web/idb.d.ts.map +1 -1
- package/dist/web/idb.js.map +1 -1
- package/dist/wrapAsyncMethods.cjs +21 -41
- package/dist/wrapAsyncMethods.cjs.map +1 -1
- package/dist/wrapAsyncMethods.d.ts +2 -0
- package/dist/wrapAsyncMethods.d.ts.map +1 -1
- package/dist/wrapAsyncMethods.js +21 -41
- package/dist/wrapAsyncMethods.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
class Pagination {
|
|
4
|
+
_page = 1;
|
|
5
|
+
_pageSize;
|
|
6
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this._pageSize = options?.pageSize ?? 10;
|
|
9
|
+
}
|
|
10
|
+
// ── Readable state ──
|
|
11
|
+
/** Current page number (1-based). */
|
|
12
|
+
get page() {
|
|
13
|
+
return this._page;
|
|
14
|
+
}
|
|
15
|
+
/** Number of items per page. */
|
|
16
|
+
get pageSize() {
|
|
17
|
+
return this._pageSize;
|
|
18
|
+
}
|
|
19
|
+
// ── Derived (require total) ──
|
|
20
|
+
/** Total number of pages for the given item count. */
|
|
21
|
+
pageCount(total) {
|
|
22
|
+
return Math.max(1, Math.ceil(total / this._pageSize));
|
|
23
|
+
}
|
|
24
|
+
/** Whether there is a next page available. */
|
|
25
|
+
hasNext(total) {
|
|
26
|
+
return this._page < this.pageCount(total);
|
|
27
|
+
}
|
|
28
|
+
/** Whether there is a previous page available. */
|
|
29
|
+
hasPrev() {
|
|
30
|
+
return this._page > 1;
|
|
31
|
+
}
|
|
32
|
+
// ── Actions ──
|
|
33
|
+
/** Navigate to a specific page (clamped to >= 1). */
|
|
34
|
+
setPage(page) {
|
|
35
|
+
const clamped = Math.max(1, Math.floor(page));
|
|
36
|
+
if (clamped === this._page) return;
|
|
37
|
+
this._page = clamped;
|
|
38
|
+
this._notify();
|
|
39
|
+
}
|
|
40
|
+
/** Change the page size and reset to page 1. */
|
|
41
|
+
setPageSize(size) {
|
|
42
|
+
if (size < 1) return;
|
|
43
|
+
this._pageSize = size;
|
|
44
|
+
this._page = 1;
|
|
45
|
+
this._notify();
|
|
46
|
+
}
|
|
47
|
+
/** Advance to the next page. */
|
|
48
|
+
nextPage() {
|
|
49
|
+
this._page++;
|
|
50
|
+
this._notify();
|
|
51
|
+
}
|
|
52
|
+
/** Go back to the previous page. No-op if already on page 1. */
|
|
53
|
+
prevPage() {
|
|
54
|
+
if (this._page > 1) {
|
|
55
|
+
this._page--;
|
|
56
|
+
this._notify();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Reset to page 1. */
|
|
60
|
+
reset() {
|
|
61
|
+
if (this._page === 1) return;
|
|
62
|
+
this._page = 1;
|
|
63
|
+
this._notify();
|
|
64
|
+
}
|
|
65
|
+
// ── Pipeline ──
|
|
66
|
+
/** Slice an array to the current page window. Returns the page subset. */
|
|
67
|
+
apply(items) {
|
|
68
|
+
const start = (this._page - 1) * this._pageSize;
|
|
69
|
+
return items.slice(start, start + this._pageSize);
|
|
70
|
+
}
|
|
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
|
+
}
|
|
83
|
+
exports.Pagination = Pagination;
|
|
84
|
+
//# sourceMappingURL=Pagination.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Pagination.cjs","sources":["../src/Pagination.ts"],"sourcesContent":["/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination {\n private _page: number = 1;\n private _pageSize: number;\n private _listeners = new Set<() => void>();\n\n constructor(options?: { pageSize?: number }) {\n this._pageSize = options?.pageSize ?? 10;\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this._notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this._notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this._notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this._notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this._notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n\n // ── Subscribable interface ──\n\n /** Subscribe to pagination state changes. Returns an unsubscribe function. */\n subscribe(cb: () => void): () => void {\n this._listeners.add(cb);\n return () => { this._listeners.delete(cb); };\n }\n\n private _notify(): void {\n for (const cb of this._listeners) cb();\n }\n}\n"],"names":[],"mappings":";;AAKO,MAAM,WAAW;AAAA,EACd,QAAgB;AAAA,EAChB;AAAA,EACA,iCAAiB,IAAA;AAAA,EAEzB,YAAY,SAAiC;AAC3C,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,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;;"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-based pagination state manager with array slicing pipeline.
|
|
3
|
+
* Tracks current page and page size, provides navigation helpers.
|
|
4
|
+
* Subscribable — auto-tracked when used as a ViewModel property.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Pagination {
|
|
7
|
+
private _page;
|
|
8
|
+
private _pageSize;
|
|
9
|
+
private _listeners;
|
|
10
|
+
constructor(options?: {
|
|
11
|
+
pageSize?: number;
|
|
12
|
+
});
|
|
13
|
+
/** Current page number (1-based). */
|
|
14
|
+
get page(): number;
|
|
15
|
+
/** Number of items per page. */
|
|
16
|
+
get pageSize(): number;
|
|
17
|
+
/** Total number of pages for the given item count. */
|
|
18
|
+
pageCount(total: number): number;
|
|
19
|
+
/** Whether there is a next page available. */
|
|
20
|
+
hasNext(total: number): boolean;
|
|
21
|
+
/** Whether there is a previous page available. */
|
|
22
|
+
hasPrev(): boolean;
|
|
23
|
+
/** Navigate to a specific page (clamped to >= 1). */
|
|
24
|
+
setPage(page: number): void;
|
|
25
|
+
/** Change the page size and reset to page 1. */
|
|
26
|
+
setPageSize(size: number): void;
|
|
27
|
+
/** Advance to the next page. */
|
|
28
|
+
nextPage(): void;
|
|
29
|
+
/** Go back to the previous page. No-op if already on page 1. */
|
|
30
|
+
prevPage(): void;
|
|
31
|
+
/** Reset to page 1. */
|
|
32
|
+
reset(): void;
|
|
33
|
+
/** Slice an array to the current page window. Returns the page subset. */
|
|
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
|
+
}
|
|
39
|
+
//# sourceMappingURL=Pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Pagination.d.ts","sourceRoot":"","sources":["../src/Pagination.ts"],"names":[],"mappings":"AAAA;;;;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;IAM3C,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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
class Pagination {
|
|
2
|
+
_page = 1;
|
|
3
|
+
_pageSize;
|
|
4
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this._pageSize = options?.pageSize ?? 10;
|
|
7
|
+
}
|
|
8
|
+
// ── Readable state ──
|
|
9
|
+
/** Current page number (1-based). */
|
|
10
|
+
get page() {
|
|
11
|
+
return this._page;
|
|
12
|
+
}
|
|
13
|
+
/** Number of items per page. */
|
|
14
|
+
get pageSize() {
|
|
15
|
+
return this._pageSize;
|
|
16
|
+
}
|
|
17
|
+
// ── Derived (require total) ──
|
|
18
|
+
/** Total number of pages for the given item count. */
|
|
19
|
+
pageCount(total) {
|
|
20
|
+
return Math.max(1, Math.ceil(total / this._pageSize));
|
|
21
|
+
}
|
|
22
|
+
/** Whether there is a next page available. */
|
|
23
|
+
hasNext(total) {
|
|
24
|
+
return this._page < this.pageCount(total);
|
|
25
|
+
}
|
|
26
|
+
/** Whether there is a previous page available. */
|
|
27
|
+
hasPrev() {
|
|
28
|
+
return this._page > 1;
|
|
29
|
+
}
|
|
30
|
+
// ── Actions ──
|
|
31
|
+
/** Navigate to a specific page (clamped to >= 1). */
|
|
32
|
+
setPage(page) {
|
|
33
|
+
const clamped = Math.max(1, Math.floor(page));
|
|
34
|
+
if (clamped === this._page) return;
|
|
35
|
+
this._page = clamped;
|
|
36
|
+
this._notify();
|
|
37
|
+
}
|
|
38
|
+
/** Change the page size and reset to page 1. */
|
|
39
|
+
setPageSize(size) {
|
|
40
|
+
if (size < 1) return;
|
|
41
|
+
this._pageSize = size;
|
|
42
|
+
this._page = 1;
|
|
43
|
+
this._notify();
|
|
44
|
+
}
|
|
45
|
+
/** Advance to the next page. */
|
|
46
|
+
nextPage() {
|
|
47
|
+
this._page++;
|
|
48
|
+
this._notify();
|
|
49
|
+
}
|
|
50
|
+
/** Go back to the previous page. No-op if already on page 1. */
|
|
51
|
+
prevPage() {
|
|
52
|
+
if (this._page > 1) {
|
|
53
|
+
this._page--;
|
|
54
|
+
this._notify();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Reset to page 1. */
|
|
58
|
+
reset() {
|
|
59
|
+
if (this._page === 1) return;
|
|
60
|
+
this._page = 1;
|
|
61
|
+
this._notify();
|
|
62
|
+
}
|
|
63
|
+
// ── Pipeline ──
|
|
64
|
+
/** Slice an array to the current page window. Returns the page subset. */
|
|
65
|
+
apply(items) {
|
|
66
|
+
const start = (this._page - 1) * this._pageSize;
|
|
67
|
+
return items.slice(start, start + this._pageSize);
|
|
68
|
+
}
|
|
69
|
+
// ── Subscribable interface ──
|
|
70
|
+
/** Subscribe to pagination state changes. Returns an unsubscribe function. */
|
|
71
|
+
subscribe(cb) {
|
|
72
|
+
this._listeners.add(cb);
|
|
73
|
+
return () => {
|
|
74
|
+
this._listeners.delete(cb);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
_notify() {
|
|
78
|
+
for (const cb of this._listeners) cb();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
Pagination
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=Pagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Pagination.js","sources":["../src/Pagination.ts"],"sourcesContent":["/**\n * Page-based pagination state manager with array slicing pipeline.\n * Tracks current page and page size, provides navigation helpers.\n * Subscribable — auto-tracked when used as a ViewModel property.\n */\nexport class Pagination {\n private _page: number = 1;\n private _pageSize: number;\n private _listeners = new Set<() => void>();\n\n constructor(options?: { pageSize?: number }) {\n this._pageSize = options?.pageSize ?? 10;\n }\n\n // ── Readable state ──\n\n /** Current page number (1-based). */\n get page(): number {\n return this._page;\n }\n\n /** Number of items per page. */\n get pageSize(): number {\n return this._pageSize;\n }\n\n // ── Derived (require total) ──\n\n /** Total number of pages for the given item count. */\n pageCount(total: number): number {\n return Math.max(1, Math.ceil(total / this._pageSize));\n }\n\n /** Whether there is a next page available. */\n hasNext(total: number): boolean {\n return this._page < this.pageCount(total);\n }\n\n /** Whether there is a previous page available. */\n hasPrev(): boolean {\n return this._page > 1;\n }\n\n // ── Actions ──\n\n /** Navigate to a specific page (clamped to >= 1). */\n setPage(page: number): void {\n const clamped = Math.max(1, Math.floor(page));\n if (clamped === this._page) return;\n this._page = clamped;\n this._notify();\n }\n\n /** Change the page size and reset to page 1. */\n setPageSize(size: number): void {\n if (size < 1) return;\n this._pageSize = size;\n this._page = 1;\n this._notify();\n }\n\n /** Advance to the next page. */\n nextPage(): void {\n this._page++;\n this._notify();\n }\n\n /** Go back to the previous page. No-op if already on page 1. */\n prevPage(): void {\n if (this._page > 1) {\n this._page--;\n this._notify();\n }\n }\n\n /** Reset to page 1. */\n reset(): void {\n if (this._page === 1) return;\n this._page = 1;\n this._notify();\n }\n\n // ── Pipeline ──\n\n /** Slice an array to the current page window. Returns the page subset. */\n apply<T>(items: T[]): T[] {\n const start = (this._page - 1) * this._pageSize;\n return items.slice(start, start + this._pageSize);\n }\n\n // ── Subscribable interface ──\n\n /** Subscribe to pagination state changes. Returns an unsubscribe function. */\n subscribe(cb: () => void): () => void {\n this._listeners.add(cb);\n return () => { this._listeners.delete(cb); };\n }\n\n private _notify(): void {\n for (const cb of this._listeners) cb();\n }\n}\n"],"names":[],"mappings":"AAKO,MAAM,WAAW;AAAA,EACd,QAAgB;AAAA,EAChB;AAAA,EACA,iCAAiB,IAAA;AAAA,EAEzB,YAAY,SAAiC;AAC3C,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,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;"}
|
|
@@ -18,6 +18,9 @@ class PersistentCollection extends Collection.Collection {
|
|
|
18
18
|
// ── Internal state ──
|
|
19
19
|
_hydrated = false;
|
|
20
20
|
_hydrating = false;
|
|
21
|
+
// Suppresses the self-subscriber during reset/clear overrides,
|
|
22
|
+
// which queue deltas manually instead of relying on diff.
|
|
23
|
+
_suppressSubscriber = false;
|
|
21
24
|
_persistenceReady = false;
|
|
22
25
|
_preHydrationWarned = false;
|
|
23
26
|
_pendingWrites = /* @__PURE__ */ new Map();
|
|
@@ -28,7 +31,7 @@ class PersistentCollection extends Collection.Collection {
|
|
|
28
31
|
constructor(initialItems = []) {
|
|
29
32
|
super(initialItems);
|
|
30
33
|
const unsub = this.subscribe((current, prev) => {
|
|
31
|
-
if (this._hydrating) return;
|
|
34
|
+
if (this._hydrating || this._suppressSubscriber) return;
|
|
32
35
|
this._ensurePersistenceReady();
|
|
33
36
|
this._diffAndQueue(current, prev);
|
|
34
37
|
this._scheduleSave();
|
|
@@ -134,11 +137,11 @@ class PersistentCollection extends Collection.Collection {
|
|
|
134
137
|
for (const item of items) {
|
|
135
138
|
this._pendingWrites.set(item.id, item);
|
|
136
139
|
}
|
|
137
|
-
this.
|
|
140
|
+
this._suppressSubscriber = true;
|
|
138
141
|
try {
|
|
139
142
|
super.reset(items);
|
|
140
143
|
} finally {
|
|
141
|
-
this.
|
|
144
|
+
this._suppressSubscriber = false;
|
|
142
145
|
}
|
|
143
146
|
this._scheduleSave();
|
|
144
147
|
}
|
|
@@ -146,11 +149,11 @@ class PersistentCollection extends Collection.Collection {
|
|
|
146
149
|
this._pendingClear = true;
|
|
147
150
|
this._pendingWrites.clear();
|
|
148
151
|
this._pendingRemoves.clear();
|
|
149
|
-
this.
|
|
152
|
+
this._suppressSubscriber = true;
|
|
150
153
|
try {
|
|
151
154
|
super.clear();
|
|
152
155
|
} finally {
|
|
153
|
-
this.
|
|
156
|
+
this._suppressSubscriber = false;
|
|
154
157
|
}
|
|
155
158
|
this._scheduleSave();
|
|
156
159
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersistentCollection.cjs","sources":["../src/PersistentCollection.ts"],"sourcesContent":["import { Collection } from './Collection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// Track storageKey uniqueness in DEV\nconst _registeredKeys: Map<string, string> | null = __DEV__ ? new Map() : null;\n\n/**\n * Abstract base for Collections that persist to external storage.\n * Tracks deltas per mutation and flushes via debounced writes.\n * Subclasses implement the storage-specific `persist*` methods.\n */\nexport abstract class PersistentCollection<\n T extends { id: string | number },\n> extends Collection<T> {\n /** Debounce delay in ms for storage writes. 0 = immediate. */\n static WRITE_DELAY = 100;\n\n /** Unique key identifying this collection in storage. */\n protected abstract readonly storageKey: string;\n\n // ── Abstract persistence methods ──\n\n protected abstract persistGet(id: T['id']): T | null | Promise<T | null>;\n protected abstract persistGetAll(): T[] | Promise<T[]>;\n /** Upsert semantics — insert or replace the given items. */\n protected abstract persistSet(items: T[]): void | Promise<void>;\n protected abstract persistRemove(ids: T['id'][]): void | Promise<void>;\n protected abstract persistClear(): void | Promise<void>;\n\n // ── Serialization hooks ──\n\n /** Serialize items to a string. Used by string-based adapters (WebStorage, NativeCollection). */\n protected serialize(items: T[]): string {\n return JSON.stringify(items);\n }\n\n /** Deserialize a string back to items. Used by string-based adapters. */\n protected deserialize(raw: string): T[] {\n return JSON.parse(raw);\n }\n\n // ── Error hook ──\n\n /** Called when a storage operation fails. Override for custom error handling. */\n protected onPersistError?(error: unknown): void;\n\n // ── Internal state ──\n\n private _hydrated = false;\n private _hydrating = false;\n private _persistenceReady = false;\n private _preHydrationWarned = false;\n private _pendingWrites = new Map<T['id'], T>();\n private _pendingRemoves = new Set<T['id']>();\n private _diffMap = new Map<T['id'], T>();\n private _pendingClear = false;\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n super(initialItems);\n\n // Self-subscribe to detect mutations via diff.\n // storageKey may not be available yet (class field initializers run after super()),\n // but that's fine — the subscriber only fires on mutations, not during construction.\n const unsub = this.subscribe((current, prev) => {\n if (this._hydrating) return;\n this._ensurePersistenceReady();\n this._diffAndQueue(current, prev);\n this._scheduleSave();\n });\n this.addCleanup(unsub);\n }\n\n /**\n * DEV check for duplicate storageKey. Called lazily since storageKey is an abstract\n * field that isn't available during the parent constructor chain.\n */\n private _ensurePersistenceReady(): void {\n if (this._persistenceReady) return;\n this._persistenceReady = true;\n\n if (__DEV__ && _registeredKeys) {\n const className = this.constructor.name;\n const existing = _registeredKeys.get(this.storageKey);\n if (existing && existing !== className) {\n console.warn(\n `[mvc-kit] Duplicate storageKey \"${this.storageKey}\" used by \"${className}\" ` +\n `and \"${existing}\". Each PersistentCollection should have a unique storageKey.`,\n );\n }\n _registeredKeys.set(this.storageKey, className);\n }\n }\n\n // ── Public API ──\n\n /** Whether storage data has been loaded. */\n get hydrated(): boolean {\n return this._hydrated;\n }\n\n /**\n * Load data from storage into the collection. Idempotent — subsequent calls return current items.\n * Returns the items after hydration.\n */\n async hydrate(): Promise<T[]> {\n if (this._hydrated) return this.items;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = await this.persistGetAll();\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n return this.items;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n return this.items;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Synchronous hydration for sync adapters (e.g., WebStorage).\n * Call from the **leaf class** constructor (after field initializers have run).\n */\n protected _hydrateSync(): void {\n if (this._hydrated) return;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = this.persistGetAll();\n if (stored instanceof Promise) {\n throw new Error('[mvc-kit] _hydrateSync called with async persistGetAll');\n }\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Clear all data from storage AND from the in-memory collection.\n */\n clearStorage(): void | Promise<void> {\n this._ensurePersistenceReady();\n\n // Clear pending queues — we're wiping everything\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._pendingClear = false;\n this._cancelSave();\n\n // Clear in-memory\n if (this.length > 0) {\n super.clear();\n }\n\n try {\n const result = this.persistClear();\n if (result instanceof Promise) {\n return result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // ── Overrides for clear/reset tracking ──\n\n reset(items: T[]): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n for (const item of items) {\n this._pendingWrites.set(item.id, item);\n }\n this._hydrating = true; // Re-use flag to skip subscriber\n try {\n super.reset(items);\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n clear(): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._hydrating = true;\n try {\n super.clear();\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n // ── Override items getter for DEV pre-hydration warning ──\n\n get items(): T[] {\n if (__DEV__ && !this._hydrated && !this._hydrating && !this._preHydrationWarned) {\n this._preHydrationWarned = true;\n console.warn(\n `[mvc-kit] Accessing items on \"${this.constructor.name}\" before hydrate() has been called. ` +\n `Data may be incomplete. Call hydrate() first.`,\n );\n }\n return super.items;\n }\n\n get state(): T[] {\n return this.items;\n }\n\n // ── Dispose ──\n\n dispose(): void {\n if (this.disposed) return;\n\n // Flush any pending saves before disposing\n this._cancelSave();\n if (this._hasPending()) {\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // DEV: unregister storageKey\n if (__DEV__ && _registeredKeys && this._persistenceReady) {\n _registeredKeys.delete(this.storageKey);\n }\n\n super.dispose();\n }\n\n // ── Private: delta tracking ──\n\n private _diffAndQueue(current: readonly T[], prev: readonly T[]): void {\n const prevMap = this._diffMap;\n prevMap.clear();\n for (const item of prev) {\n prevMap.set(item.id, item);\n }\n\n // Added or updated: in current but different reference in prev\n for (const item of current) {\n const prevItem = prevMap.get(item.id);\n if (!prevItem || prevItem !== item) {\n this._pendingWrites.set(item.id, item);\n this._pendingRemoves.delete(item.id);\n }\n prevMap.delete(item.id); // consume matched items\n }\n\n // Remaining in prevMap = removed (in prev but not in current)\n for (const [id] of prevMap) {\n this._pendingRemoves.add(id);\n this._pendingWrites.delete(id);\n }\n\n prevMap.clear();\n }\n\n private _hasPending(): boolean {\n return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;\n }\n\n // ── Private: debounce + flush ──\n\n private _scheduleSave(): void {\n this._cancelSave();\n const delay = (this.constructor as typeof PersistentCollection).WRITE_DELAY;\n if (delay <= 0) {\n this._doFlush();\n return;\n }\n this._flushTimer = setTimeout(() => this._doFlush(), delay);\n }\n\n private _cancelSave(): void {\n if (this._flushTimer !== null) {\n clearTimeout(this._flushTimer);\n this._flushTimer = null;\n }\n }\n\n private _doFlush(): void {\n if (!this._hasPending()) return;\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n private _flush(): void | Promise<void> {\n const doClear = this._pendingClear;\n const writes = this._pendingWrites.size > 0 ? [...this._pendingWrites.values()] : null;\n const removes = this._pendingRemoves.size > 0 ? [...this._pendingRemoves] : null;\n\n // Clear queues\n this._pendingClear = false;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n\n if (doClear) {\n const clearResult = this.persistClear();\n if (clearResult instanceof Promise) {\n return clearResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n if (writes) {\n return this.persistSet(writes);\n }\n return;\n }\n\n // Non-clear: removes then writes\n if (removes) {\n const removeResult = this.persistRemove(removes);\n if (removeResult instanceof Promise) {\n return removeResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n }\n if (writes) {\n return this.persistSet(writes);\n }\n }\n\n // ── Private: error handling ──\n\n private _handlePersistError(err: unknown): void {\n if (this.onPersistError) {\n this.onPersistError(err);\n return;\n }\n if (__DEV__) {\n console.warn('[mvc-kit] Storage error:', err);\n }\n }\n}\n"],"names":["Collection"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PersistentCollection.cjs","sources":["../src/PersistentCollection.ts"],"sourcesContent":["import { Collection } from './Collection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// Track storageKey uniqueness in DEV\nconst _registeredKeys: Map<string, string> | null = __DEV__ ? new Map() : null;\n\n/**\n * Abstract base for Collections that persist to external storage.\n * Tracks deltas per mutation and flushes via debounced writes.\n * Subclasses implement the storage-specific `persist*` methods.\n */\nexport abstract class PersistentCollection<\n T extends { id: string | number },\n> extends Collection<T> {\n /** Debounce delay in ms for storage writes. 0 = immediate. */\n static WRITE_DELAY = 100;\n\n /** Unique key identifying this collection in storage. */\n protected abstract readonly storageKey: string;\n\n // ── Abstract persistence methods ──\n\n /** Retrieve a single item by id from storage. @protected */\n protected abstract persistGet(id: T['id']): T | null | Promise<T | null>;\n /** Retrieve all items from storage. @protected */\n protected abstract persistGetAll(): T[] | Promise<T[]>;\n /** Upsert semantics — insert or replace the given items in storage. @protected */\n protected abstract persistSet(items: T[]): void | Promise<void>;\n /** Remove items by their ids from storage. @protected */\n protected abstract persistRemove(ids: T['id'][]): void | Promise<void>;\n /** Remove all items from storage. @protected */\n protected abstract persistClear(): void | Promise<void>;\n\n // ── Serialization hooks ──\n\n /** Serialize items to a string. Used by string-based adapters (WebStorage, NativeCollection). */\n protected serialize(items: T[]): string {\n return JSON.stringify(items);\n }\n\n /** Deserialize a string back to items. Used by string-based adapters. */\n protected deserialize(raw: string): T[] {\n return JSON.parse(raw);\n }\n\n // ── Error hook ──\n\n /** Called when a storage operation fails. Override for custom error handling. */\n protected onPersistError?(error: unknown): void;\n\n // ── Internal state ──\n\n private _hydrated = false;\n private _hydrating = false;\n // Suppresses the self-subscriber during reset/clear overrides,\n // which queue deltas manually instead of relying on diff.\n private _suppressSubscriber = false;\n private _persistenceReady = false;\n private _preHydrationWarned = false;\n private _pendingWrites = new Map<T['id'], T>();\n private _pendingRemoves = new Set<T['id']>();\n private _diffMap = new Map<T['id'], T>();\n private _pendingClear = false;\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n super(initialItems);\n\n // Self-subscribe to detect mutations via diff.\n // storageKey may not be available yet (class field initializers run after super()),\n // but that's fine — the subscriber only fires on mutations, not during construction.\n const unsub = this.subscribe((current, prev) => {\n if (this._hydrating || this._suppressSubscriber) return;\n this._ensurePersistenceReady();\n this._diffAndQueue(current, prev);\n this._scheduleSave();\n });\n this.addCleanup(unsub);\n }\n\n /**\n * DEV check for duplicate storageKey. Called lazily since storageKey is an abstract\n * field that isn't available during the parent constructor chain.\n */\n private _ensurePersistenceReady(): void {\n if (this._persistenceReady) return;\n this._persistenceReady = true;\n\n if (__DEV__ && _registeredKeys) {\n const className = this.constructor.name;\n const existing = _registeredKeys.get(this.storageKey);\n if (existing && existing !== className) {\n console.warn(\n `[mvc-kit] Duplicate storageKey \"${this.storageKey}\" used by \"${className}\" ` +\n `and \"${existing}\". Each PersistentCollection should have a unique storageKey.`,\n );\n }\n _registeredKeys.set(this.storageKey, className);\n }\n }\n\n // ── Public API ──\n\n /** Whether storage data has been loaded. */\n get hydrated(): boolean {\n return this._hydrated;\n }\n\n /**\n * Load data from storage into the collection. Idempotent — subsequent calls return current items.\n * Returns the items after hydration.\n */\n async hydrate(): Promise<T[]> {\n if (this._hydrated) return this.items;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = await this.persistGetAll();\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n return this.items;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n return this.items;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Synchronous hydration for sync adapters (e.g., WebStorage).\n * Call from the **leaf class** constructor (after field initializers have run).\n */\n protected _hydrateSync(): void {\n if (this._hydrated) return;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = this.persistGetAll();\n if (stored instanceof Promise) {\n throw new Error('[mvc-kit] _hydrateSync called with async persistGetAll');\n }\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Clear all data from storage AND from the in-memory collection.\n */\n clearStorage(): void | Promise<void> {\n this._ensurePersistenceReady();\n\n // Clear pending queues — we're wiping everything\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._pendingClear = false;\n this._cancelSave();\n\n // Clear in-memory\n if (this.length > 0) {\n super.clear();\n }\n\n try {\n const result = this.persistClear();\n if (result instanceof Promise) {\n return result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // ── Overrides for clear/reset tracking ──\n\n reset(items: T[]): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n for (const item of items) {\n this._pendingWrites.set(item.id, item);\n }\n // Suppress the self-subscriber — deltas are queued manually above\n this._suppressSubscriber = true;\n try {\n super.reset(items);\n } finally {\n this._suppressSubscriber = false;\n }\n this._scheduleSave();\n }\n\n clear(): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n // Suppress the self-subscriber — deltas are queued manually above\n this._suppressSubscriber = true;\n try {\n super.clear();\n } finally {\n this._suppressSubscriber = false;\n }\n this._scheduleSave();\n }\n\n // ── Override items getter for DEV pre-hydration warning ──\n\n get items(): T[] {\n if (__DEV__ && !this._hydrated && !this._hydrating && !this._preHydrationWarned) {\n this._preHydrationWarned = true;\n console.warn(\n `[mvc-kit] Accessing items on \"${this.constructor.name}\" before hydrate() has been called. ` +\n `Data may be incomplete. Call hydrate() first.`,\n );\n }\n return super.items;\n }\n\n get state(): T[] {\n return this.items;\n }\n\n // ── Dispose ──\n\n dispose(): void {\n if (this.disposed) return;\n\n // Flush any pending saves before disposing\n this._cancelSave();\n if (this._hasPending()) {\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // DEV: unregister storageKey\n if (__DEV__ && _registeredKeys && this._persistenceReady) {\n _registeredKeys.delete(this.storageKey);\n }\n\n super.dispose();\n }\n\n // ── Private: delta tracking ──\n\n private _diffAndQueue(current: readonly T[], prev: readonly T[]): void {\n const prevMap = this._diffMap;\n prevMap.clear();\n for (const item of prev) {\n prevMap.set(item.id, item);\n }\n\n // Added or updated: in current but different reference in prev\n for (const item of current) {\n const prevItem = prevMap.get(item.id);\n if (!prevItem || prevItem !== item) {\n this._pendingWrites.set(item.id, item);\n this._pendingRemoves.delete(item.id);\n }\n prevMap.delete(item.id); // consume matched items\n }\n\n // Remaining in prevMap = removed (in prev but not in current)\n for (const [id] of prevMap) {\n this._pendingRemoves.add(id);\n this._pendingWrites.delete(id);\n }\n\n prevMap.clear();\n }\n\n private _hasPending(): boolean {\n return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;\n }\n\n // ── Private: debounce + flush ──\n\n private _scheduleSave(): void {\n this._cancelSave();\n const delay = (this.constructor as typeof PersistentCollection).WRITE_DELAY;\n if (delay <= 0) {\n this._doFlush();\n return;\n }\n this._flushTimer = setTimeout(() => this._doFlush(), delay);\n }\n\n private _cancelSave(): void {\n if (this._flushTimer !== null) {\n clearTimeout(this._flushTimer);\n this._flushTimer = null;\n }\n }\n\n private _doFlush(): void {\n if (!this._hasPending()) return;\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n private _flush(): void | Promise<void> {\n const doClear = this._pendingClear;\n const writes = this._pendingWrites.size > 0 ? [...this._pendingWrites.values()] : null;\n const removes = this._pendingRemoves.size > 0 ? [...this._pendingRemoves] : null;\n\n // Clear queues\n this._pendingClear = false;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n\n if (doClear) {\n const clearResult = this.persistClear();\n if (clearResult instanceof Promise) {\n return clearResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n if (writes) {\n return this.persistSet(writes);\n }\n return;\n }\n\n // Non-clear: removes then writes\n if (removes) {\n const removeResult = this.persistRemove(removes);\n if (removeResult instanceof Promise) {\n return removeResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n }\n if (writes) {\n return this.persistSet(writes);\n }\n }\n\n // ── Private: error handling ──\n\n private _handlePersistError(err: unknown): void {\n if (this.onPersistError) {\n this.onPersistError(err);\n return;\n }\n if (__DEV__) {\n console.warn('[mvc-kit] Storage error:', err);\n }\n }\n}\n"],"names":["Collection"],"mappings":";;;AAEA,MAAM,UAAU,OAAO,oBAAoB,eAAe;AAG1D,MAAM,kBAA8C,UAAU,oBAAI,IAAA,IAAQ;AAOnE,MAAe,6BAEZA,WAAAA,WAAc;AAAA;AAAA,EAEtB,OAAO,cAAc;AAAA;AAAA;AAAA,EAqBX,UAAU,OAAoB;AACtC,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AAAA;AAAA,EAGU,YAAY,KAAkB;AACtC,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB;AAAA;AAAA,EASQ,YAAY;AAAA,EACZ,aAAa;AAAA;AAAA;AAAA,EAGb,sBAAsB;AAAA,EACtB,oBAAoB;AAAA,EACpB,sBAAsB;AAAA,EACtB,qCAAqB,IAAA;AAAA,EACrB,sCAAsB,IAAA;AAAA,EACtB,+BAAe,IAAA;AAAA,EACf,gBAAgB;AAAA,EAChB,cAAoD;AAAA,EAE5D,YAAY,eAAoB,IAAI;AAClC,UAAM,YAAY;AAKlB,UAAM,QAAQ,KAAK,UAAU,CAAC,SAAS,SAAS;AAC9C,UAAI,KAAK,cAAc,KAAK,oBAAqB;AACjD,WAAK,wBAAA;AACL,WAAK,cAAc,SAAS,IAAI;AAChC,WAAK,cAAA;AAAA,IACP,CAAC;AACD,SAAK,WAAW,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,0BAAgC;AACtC,QAAI,KAAK,kBAAmB;AAC5B,SAAK,oBAAoB;AAEzB,QAAI,WAAW,iBAAiB;AAC9B,YAAM,YAAY,KAAK,YAAY;AACnC,YAAM,WAAW,gBAAgB,IAAI,KAAK,UAAU;AACpD,UAAI,YAAY,aAAa,WAAW;AACtC,gBAAQ;AAAA,UACN,mCAAmC,KAAK,UAAU,cAAc,SAAS,UAC/D,QAAQ;AAAA,QAAA;AAAA,MAEtB;AACA,sBAAgB,IAAI,KAAK,YAAY,SAAS;AAAA,IAChD;AAAA,EACF;AAAA;AAAA;AAAA,EAKA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAAwB;AAC5B,QAAI,KAAK,UAAW,QAAO,KAAK;AAChC,SAAK,wBAAA;AAEL,SAAK,aAAa;AAClB,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,cAAA;AAC1B,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,MAAM,MAAM;AAAA,MACpB;AACA,WAAK,YAAY;AACjB,aAAO,KAAK;AAAA,IACd,SAAS,KAAK;AACZ,WAAK,oBAAoB,GAAG;AAC5B,WAAK,YAAY;AACjB,aAAO,KAAK;AAAA,IACd,UAAA;AACE,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,eAAqB;AAC7B,QAAI,KAAK,UAAW;AACpB,SAAK,wBAAA;AAEL,SAAK,aAAa;AAClB,QAAI;AACF,YAAM,SAAS,KAAK,cAAA;AACpB,UAAI,kBAAkB,SAAS;AAC7B,cAAM,IAAI,MAAM,wDAAwD;AAAA,MAC1E;AACA,UAAI,OAAO,SAAS,GAAG;AACrB,cAAM,MAAM,MAAM;AAAA,MACpB;AACA,WAAK,YAAY;AAAA,IACnB,SAAS,KAAK;AACZ,WAAK,oBAAoB,GAAG;AAC5B,WAAK,YAAY;AAAA,IACnB,UAAA;AACE,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,eAAqC;AACnC,SAAK,wBAAA;AAGL,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB,MAAA;AACrB,SAAK,gBAAgB;AACrB,SAAK,YAAA;AAGL,QAAI,KAAK,SAAS,GAAG;AACnB,YAAM,MAAA;AAAA,IACR;AAEA,QAAI;AACF,YAAM,SAAS,KAAK,aAAA;AACpB,UAAI,kBAAkB,SAAS;AAC7B,eAAO,OAAO,MAAM,CAAC,QAAQ,KAAK,oBAAoB,GAAG,CAAC;AAAA,MAC5D;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,oBAAoB,GAAG;AAAA,IAC9B;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,OAAkB;AACtB,SAAK,gBAAgB;AACrB,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB,MAAA;AACrB,eAAW,QAAQ,OAAO;AACxB,WAAK,eAAe,IAAI,KAAK,IAAI,IAAI;AAAA,IACvC;AAEA,SAAK,sBAAsB;AAC3B,QAAI;AACF,YAAM,MAAM,KAAK;AAAA,IACnB,UAAA;AACE,WAAK,sBAAsB;AAAA,IAC7B;AACA,SAAK,cAAA;AAAA,EACP;AAAA,EAEA,QAAc;AACZ,SAAK,gBAAgB;AACrB,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB,MAAA;AAErB,SAAK,sBAAsB;AAC3B,QAAI;AACF,YAAM,MAAA;AAAA,IACR,UAAA;AACE,WAAK,sBAAsB;AAAA,IAC7B;AACA,SAAK,cAAA;AAAA,EACP;AAAA;AAAA,EAIA,IAAI,QAAa;AACf,QAAI,WAAW,CAAC,KAAK,aAAa,CAAC,KAAK,cAAc,CAAC,KAAK,qBAAqB;AAC/E,WAAK,sBAAsB;AAC3B,cAAQ;AAAA,QACN,iCAAiC,KAAK,YAAY,IAAI;AAAA,MAAA;AAAA,IAG1D;AACA,WAAO,MAAM;AAAA,EACf;AAAA,EAEA,IAAI,QAAa;AACf,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAIA,UAAgB;AACd,QAAI,KAAK,SAAU;AAGnB,SAAK,YAAA;AACL,QAAI,KAAK,eAAe;AACtB,UAAI;AACF,cAAM,SAAS,KAAK,OAAA;AACpB,YAAI,kBAAkB,SAAS;AAC7B,iBAAO,MAAM,CAAC,QAAQ,KAAK,oBAAoB,GAAG,CAAC;AAAA,QACrD;AAAA,MACF,SAAS,KAAK;AACZ,aAAK,oBAAoB,GAAG;AAAA,MAC9B;AAAA,IACF;AAGA,QAAI,WAAW,mBAAmB,KAAK,mBAAmB;AACxD,sBAAgB,OAAO,KAAK,UAAU;AAAA,IACxC;AAEA,UAAM,QAAA;AAAA,EACR;AAAA;AAAA,EAIQ,cAAc,SAAuB,MAA0B;AACrE,UAAM,UAAU,KAAK;AACrB,YAAQ,MAAA;AACR,eAAW,QAAQ,MAAM;AACvB,cAAQ,IAAI,KAAK,IAAI,IAAI;AAAA,IAC3B;AAGA,eAAW,QAAQ,SAAS;AAC1B,YAAM,WAAW,QAAQ,IAAI,KAAK,EAAE;AACpC,UAAI,CAAC,YAAY,aAAa,MAAM;AAClC,aAAK,eAAe,IAAI,KAAK,IAAI,IAAI;AACrC,aAAK,gBAAgB,OAAO,KAAK,EAAE;AAAA,MACrC;AACA,cAAQ,OAAO,KAAK,EAAE;AAAA,IACxB;AAGA,eAAW,CAAC,EAAE,KAAK,SAAS;AAC1B,WAAK,gBAAgB,IAAI,EAAE;AAC3B,WAAK,eAAe,OAAO,EAAE;AAAA,IAC/B;AAEA,YAAQ,MAAA;AAAA,EACV;AAAA,EAEQ,cAAuB;AAC7B,WAAO,KAAK,iBAAiB,KAAK,eAAe,OAAO,KAAK,KAAK,gBAAgB,OAAO;AAAA,EAC3F;AAAA;AAAA,EAIQ,gBAAsB;AAC5B,SAAK,YAAA;AACL,UAAM,QAAS,KAAK,YAA4C;AAChE,QAAI,SAAS,GAAG;AACd,WAAK,SAAA;AACL;AAAA,IACF;AACA,SAAK,cAAc,WAAW,MAAM,KAAK,SAAA,GAAY,KAAK;AAAA,EAC5D;AAAA,EAEQ,cAAoB;AAC1B,QAAI,KAAK,gBAAgB,MAAM;AAC7B,mBAAa,KAAK,WAAW;AAC7B,WAAK,cAAc;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,WAAiB;AACvB,QAAI,CAAC,KAAK,cAAe;AACzB,QAAI;AACF,YAAM,SAAS,KAAK,OAAA;AACpB,UAAI,kBAAkB,SAAS;AAC7B,eAAO,MAAM,CAAC,QAAQ,KAAK,oBAAoB,GAAG,CAAC;AAAA,MACrD;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,oBAAoB,GAAG;AAAA,IAC9B;AAAA,EACF;AAAA,EAEQ,SAA+B;AACrC,UAAM,UAAU,KAAK;AACrB,UAAM,SAAS,KAAK,eAAe,OAAO,IAAI,CAAC,GAAG,KAAK,eAAe,OAAA,CAAQ,IAAI;AAClF,UAAM,UAAU,KAAK,gBAAgB,OAAO,IAAI,CAAC,GAAG,KAAK,eAAe,IAAI;AAG5E,SAAK,gBAAgB;AACrB,SAAK,eAAe,MAAA;AACpB,SAAK,gBAAgB,MAAA;AAErB,QAAI,SAAS;AACX,YAAM,cAAc,KAAK,aAAA;AACzB,UAAI,uBAAuB,SAAS;AAClC,eAAO,YAAY,KAAK,MAAM;AAC5B,cAAI,OAAQ,QAAO,KAAK,WAAW,MAAM;AAAA,QAC3C,CAAC;AAAA,MACH;AACA,UAAI,QAAQ;AACV,eAAO,KAAK,WAAW,MAAM;AAAA,MAC/B;AACA;AAAA,IACF;AAGA,QAAI,SAAS;AACX,YAAM,eAAe,KAAK,cAAc,OAAO;AAC/C,UAAI,wBAAwB,SAAS;AACnC,eAAO,aAAa,KAAK,MAAM;AAC7B,cAAI,OAAQ,QAAO,KAAK,WAAW,MAAM;AAAA,QAC3C,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,QAAQ;AACV,aAAO,KAAK,WAAW,MAAM;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA,EAIQ,oBAAoB,KAAoB;AAC9C,QAAI,KAAK,gBAAgB;AACvB,WAAK,eAAe,GAAG;AACvB;AAAA,IACF;AACA,QAAI,SAAS;AACX,cAAQ,KAAK,4BAA4B,GAAG;AAAA,IAC9C;AAAA,EACF;AACF;;"}
|
|
@@ -11,11 +11,15 @@ export declare abstract class PersistentCollection<T extends {
|
|
|
11
11
|
static WRITE_DELAY: number;
|
|
12
12
|
/** Unique key identifying this collection in storage. */
|
|
13
13
|
protected abstract readonly storageKey: string;
|
|
14
|
+
/** Retrieve a single item by id from storage. @protected */
|
|
14
15
|
protected abstract persistGet(id: T['id']): T | null | Promise<T | null>;
|
|
16
|
+
/** Retrieve all items from storage. @protected */
|
|
15
17
|
protected abstract persistGetAll(): T[] | Promise<T[]>;
|
|
16
|
-
/** Upsert semantics — insert or replace the given items. */
|
|
18
|
+
/** Upsert semantics — insert or replace the given items in storage. @protected */
|
|
17
19
|
protected abstract persistSet(items: T[]): void | Promise<void>;
|
|
20
|
+
/** Remove items by their ids from storage. @protected */
|
|
18
21
|
protected abstract persistRemove(ids: T['id'][]): void | Promise<void>;
|
|
22
|
+
/** Remove all items from storage. @protected */
|
|
19
23
|
protected abstract persistClear(): void | Promise<void>;
|
|
20
24
|
/** Serialize items to a string. Used by string-based adapters (WebStorage, NativeCollection). */
|
|
21
25
|
protected serialize(items: T[]): string;
|
|
@@ -25,6 +29,7 @@ export declare abstract class PersistentCollection<T extends {
|
|
|
25
29
|
protected onPersistError?(error: unknown): void;
|
|
26
30
|
private _hydrated;
|
|
27
31
|
private _hydrating;
|
|
32
|
+
private _suppressSubscriber;
|
|
28
33
|
private _persistenceReady;
|
|
29
34
|
private _preHydrationWarned;
|
|
30
35
|
private _pendingWrites;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersistentCollection.d.ts","sourceRoot":"","sources":["../src/PersistentCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C;;;;GAIG;AACH,8BAAsB,oBAAoB,CACxC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CACjC,SAAQ,UAAU,CAAC,CAAC,CAAC;IACrB,8DAA8D;IAC9D,MAAM,CAAC,WAAW,SAAO;IAEzB,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAI/C,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IACxE,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IACtD,
|
|
1
|
+
{"version":3,"file":"PersistentCollection.d.ts","sourceRoot":"","sources":["../src/PersistentCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAO1C;;;;GAIG;AACH,8BAAsB,oBAAoB,CACxC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CACjC,SAAQ,UAAU,CAAC,CAAC,CAAC;IACrB,8DAA8D;IAC9D,MAAM,CAAC,WAAW,SAAO;IAEzB,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAI/C,4DAA4D;IAC5D,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IACxE,kDAAkD;IAClD,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IACtD,kFAAkF;IAClF,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC/D,yDAAyD;IACzD,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IACtE,gDAAgD;IAChD,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD,iGAAiG;IACjG,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,MAAM;IAIvC,yEAAyE;IACzE,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE;IAMvC,iFAAiF;IACjF,SAAS,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI/C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;IAG3B,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,eAAe,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAA8C;gBAErD,YAAY,GAAE,CAAC,EAAO;IAelC;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAmB/B,4CAA4C;IAC5C,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;IAqB7B;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAsB9B;;OAEG;IACH,YAAY,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BpC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IAiBvB,KAAK,IAAI,IAAI;IAgBb,IAAI,KAAK,IAAI,CAAC,EAAE,CASf;IAED,IAAI,KAAK,IAAI,CAAC,EAAE,CAEf;IAID,OAAO,IAAI,IAAI;IA0Bf,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,MAAM;IAuCd,OAAO,CAAC,mBAAmB;CAS5B"}
|
|
@@ -16,6 +16,9 @@ class PersistentCollection extends Collection {
|
|
|
16
16
|
// ── Internal state ──
|
|
17
17
|
_hydrated = false;
|
|
18
18
|
_hydrating = false;
|
|
19
|
+
// Suppresses the self-subscriber during reset/clear overrides,
|
|
20
|
+
// which queue deltas manually instead of relying on diff.
|
|
21
|
+
_suppressSubscriber = false;
|
|
19
22
|
_persistenceReady = false;
|
|
20
23
|
_preHydrationWarned = false;
|
|
21
24
|
_pendingWrites = /* @__PURE__ */ new Map();
|
|
@@ -26,7 +29,7 @@ class PersistentCollection extends Collection {
|
|
|
26
29
|
constructor(initialItems = []) {
|
|
27
30
|
super(initialItems);
|
|
28
31
|
const unsub = this.subscribe((current, prev) => {
|
|
29
|
-
if (this._hydrating) return;
|
|
32
|
+
if (this._hydrating || this._suppressSubscriber) return;
|
|
30
33
|
this._ensurePersistenceReady();
|
|
31
34
|
this._diffAndQueue(current, prev);
|
|
32
35
|
this._scheduleSave();
|
|
@@ -132,11 +135,11 @@ class PersistentCollection extends Collection {
|
|
|
132
135
|
for (const item of items) {
|
|
133
136
|
this._pendingWrites.set(item.id, item);
|
|
134
137
|
}
|
|
135
|
-
this.
|
|
138
|
+
this._suppressSubscriber = true;
|
|
136
139
|
try {
|
|
137
140
|
super.reset(items);
|
|
138
141
|
} finally {
|
|
139
|
-
this.
|
|
142
|
+
this._suppressSubscriber = false;
|
|
140
143
|
}
|
|
141
144
|
this._scheduleSave();
|
|
142
145
|
}
|
|
@@ -144,11 +147,11 @@ class PersistentCollection extends Collection {
|
|
|
144
147
|
this._pendingClear = true;
|
|
145
148
|
this._pendingWrites.clear();
|
|
146
149
|
this._pendingRemoves.clear();
|
|
147
|
-
this.
|
|
150
|
+
this._suppressSubscriber = true;
|
|
148
151
|
try {
|
|
149
152
|
super.clear();
|
|
150
153
|
} finally {
|
|
151
|
-
this.
|
|
154
|
+
this._suppressSubscriber = false;
|
|
152
155
|
}
|
|
153
156
|
this._scheduleSave();
|
|
154
157
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PersistentCollection.js","sources":["../src/PersistentCollection.ts"],"sourcesContent":["import { Collection } from './Collection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// Track storageKey uniqueness in DEV\nconst _registeredKeys: Map<string, string> | null = __DEV__ ? new Map() : null;\n\n/**\n * Abstract base for Collections that persist to external storage.\n * Tracks deltas per mutation and flushes via debounced writes.\n * Subclasses implement the storage-specific `persist*` methods.\n */\nexport abstract class PersistentCollection<\n T extends { id: string | number },\n> extends Collection<T> {\n /** Debounce delay in ms for storage writes. 0 = immediate. */\n static WRITE_DELAY = 100;\n\n /** Unique key identifying this collection in storage. */\n protected abstract readonly storageKey: string;\n\n // ── Abstract persistence methods ──\n\n protected abstract persistGet(id: T['id']): T | null | Promise<T | null>;\n protected abstract persistGetAll(): T[] | Promise<T[]>;\n /** Upsert semantics — insert or replace the given items. */\n protected abstract persistSet(items: T[]): void | Promise<void>;\n protected abstract persistRemove(ids: T['id'][]): void | Promise<void>;\n protected abstract persistClear(): void | Promise<void>;\n\n // ── Serialization hooks ──\n\n /** Serialize items to a string. Used by string-based adapters (WebStorage, NativeCollection). */\n protected serialize(items: T[]): string {\n return JSON.stringify(items);\n }\n\n /** Deserialize a string back to items. Used by string-based adapters. */\n protected deserialize(raw: string): T[] {\n return JSON.parse(raw);\n }\n\n // ── Error hook ──\n\n /** Called when a storage operation fails. Override for custom error handling. */\n protected onPersistError?(error: unknown): void;\n\n // ── Internal state ──\n\n private _hydrated = false;\n private _hydrating = false;\n private _persistenceReady = false;\n private _preHydrationWarned = false;\n private _pendingWrites = new Map<T['id'], T>();\n private _pendingRemoves = new Set<T['id']>();\n private _diffMap = new Map<T['id'], T>();\n private _pendingClear = false;\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n super(initialItems);\n\n // Self-subscribe to detect mutations via diff.\n // storageKey may not be available yet (class field initializers run after super()),\n // but that's fine — the subscriber only fires on mutations, not during construction.\n const unsub = this.subscribe((current, prev) => {\n if (this._hydrating) return;\n this._ensurePersistenceReady();\n this._diffAndQueue(current, prev);\n this._scheduleSave();\n });\n this.addCleanup(unsub);\n }\n\n /**\n * DEV check for duplicate storageKey. Called lazily since storageKey is an abstract\n * field that isn't available during the parent constructor chain.\n */\n private _ensurePersistenceReady(): void {\n if (this._persistenceReady) return;\n this._persistenceReady = true;\n\n if (__DEV__ && _registeredKeys) {\n const className = this.constructor.name;\n const existing = _registeredKeys.get(this.storageKey);\n if (existing && existing !== className) {\n console.warn(\n `[mvc-kit] Duplicate storageKey \"${this.storageKey}\" used by \"${className}\" ` +\n `and \"${existing}\". Each PersistentCollection should have a unique storageKey.`,\n );\n }\n _registeredKeys.set(this.storageKey, className);\n }\n }\n\n // ── Public API ──\n\n /** Whether storage data has been loaded. */\n get hydrated(): boolean {\n return this._hydrated;\n }\n\n /**\n * Load data from storage into the collection. Idempotent — subsequent calls return current items.\n * Returns the items after hydration.\n */\n async hydrate(): Promise<T[]> {\n if (this._hydrated) return this.items;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = await this.persistGetAll();\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n return this.items;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n return this.items;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Synchronous hydration for sync adapters (e.g., WebStorage).\n * Call from the **leaf class** constructor (after field initializers have run).\n */\n protected _hydrateSync(): void {\n if (this._hydrated) return;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = this.persistGetAll();\n if (stored instanceof Promise) {\n throw new Error('[mvc-kit] _hydrateSync called with async persistGetAll');\n }\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Clear all data from storage AND from the in-memory collection.\n */\n clearStorage(): void | Promise<void> {\n this._ensurePersistenceReady();\n\n // Clear pending queues — we're wiping everything\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._pendingClear = false;\n this._cancelSave();\n\n // Clear in-memory\n if (this.length > 0) {\n super.clear();\n }\n\n try {\n const result = this.persistClear();\n if (result instanceof Promise) {\n return result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // ── Overrides for clear/reset tracking ──\n\n reset(items: T[]): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n for (const item of items) {\n this._pendingWrites.set(item.id, item);\n }\n this._hydrating = true; // Re-use flag to skip subscriber\n try {\n super.reset(items);\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n clear(): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._hydrating = true;\n try {\n super.clear();\n } finally {\n this._hydrating = false;\n }\n this._scheduleSave();\n }\n\n // ── Override items getter for DEV pre-hydration warning ──\n\n get items(): T[] {\n if (__DEV__ && !this._hydrated && !this._hydrating && !this._preHydrationWarned) {\n this._preHydrationWarned = true;\n console.warn(\n `[mvc-kit] Accessing items on \"${this.constructor.name}\" before hydrate() has been called. ` +\n `Data may be incomplete. Call hydrate() first.`,\n );\n }\n return super.items;\n }\n\n get state(): T[] {\n return this.items;\n }\n\n // ── Dispose ──\n\n dispose(): void {\n if (this.disposed) return;\n\n // Flush any pending saves before disposing\n this._cancelSave();\n if (this._hasPending()) {\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // DEV: unregister storageKey\n if (__DEV__ && _registeredKeys && this._persistenceReady) {\n _registeredKeys.delete(this.storageKey);\n }\n\n super.dispose();\n }\n\n // ── Private: delta tracking ──\n\n private _diffAndQueue(current: readonly T[], prev: readonly T[]): void {\n const prevMap = this._diffMap;\n prevMap.clear();\n for (const item of prev) {\n prevMap.set(item.id, item);\n }\n\n // Added or updated: in current but different reference in prev\n for (const item of current) {\n const prevItem = prevMap.get(item.id);\n if (!prevItem || prevItem !== item) {\n this._pendingWrites.set(item.id, item);\n this._pendingRemoves.delete(item.id);\n }\n prevMap.delete(item.id); // consume matched items\n }\n\n // Remaining in prevMap = removed (in prev but not in current)\n for (const [id] of prevMap) {\n this._pendingRemoves.add(id);\n this._pendingWrites.delete(id);\n }\n\n prevMap.clear();\n }\n\n private _hasPending(): boolean {\n return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;\n }\n\n // ── Private: debounce + flush ──\n\n private _scheduleSave(): void {\n this._cancelSave();\n const delay = (this.constructor as typeof PersistentCollection).WRITE_DELAY;\n if (delay <= 0) {\n this._doFlush();\n return;\n }\n this._flushTimer = setTimeout(() => this._doFlush(), delay);\n }\n\n private _cancelSave(): void {\n if (this._flushTimer !== null) {\n clearTimeout(this._flushTimer);\n this._flushTimer = null;\n }\n }\n\n private _doFlush(): void {\n if (!this._hasPending()) return;\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n private _flush(): void | Promise<void> {\n const doClear = this._pendingClear;\n const writes = this._pendingWrites.size > 0 ? [...this._pendingWrites.values()] : null;\n const removes = this._pendingRemoves.size > 0 ? [...this._pendingRemoves] : null;\n\n // Clear queues\n this._pendingClear = false;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n\n if (doClear) {\n const clearResult = this.persistClear();\n if (clearResult instanceof Promise) {\n return clearResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n if (writes) {\n return this.persistSet(writes);\n }\n return;\n }\n\n // Non-clear: removes then writes\n if (removes) {\n const removeResult = this.persistRemove(removes);\n if (removeResult instanceof Promise) {\n return removeResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n }\n if (writes) {\n return this.persistSet(writes);\n }\n }\n\n // ── Private: error handling ──\n\n private _handlePersistError(err: unknown): void {\n if (this.onPersistError) {\n this.onPersistError(err);\n return;\n }\n if (__DEV__) {\n console.warn('[mvc-kit] Storage error:', err);\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PersistentCollection.js","sources":["../src/PersistentCollection.ts"],"sourcesContent":["import { Collection } from './Collection';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// Track storageKey uniqueness in DEV\nconst _registeredKeys: Map<string, string> | null = __DEV__ ? new Map() : null;\n\n/**\n * Abstract base for Collections that persist to external storage.\n * Tracks deltas per mutation and flushes via debounced writes.\n * Subclasses implement the storage-specific `persist*` methods.\n */\nexport abstract class PersistentCollection<\n T extends { id: string | number },\n> extends Collection<T> {\n /** Debounce delay in ms for storage writes. 0 = immediate. */\n static WRITE_DELAY = 100;\n\n /** Unique key identifying this collection in storage. */\n protected abstract readonly storageKey: string;\n\n // ── Abstract persistence methods ──\n\n /** Retrieve a single item by id from storage. @protected */\n protected abstract persistGet(id: T['id']): T | null | Promise<T | null>;\n /** Retrieve all items from storage. @protected */\n protected abstract persistGetAll(): T[] | Promise<T[]>;\n /** Upsert semantics — insert or replace the given items in storage. @protected */\n protected abstract persistSet(items: T[]): void | Promise<void>;\n /** Remove items by their ids from storage. @protected */\n protected abstract persistRemove(ids: T['id'][]): void | Promise<void>;\n /** Remove all items from storage. @protected */\n protected abstract persistClear(): void | Promise<void>;\n\n // ── Serialization hooks ──\n\n /** Serialize items to a string. Used by string-based adapters (WebStorage, NativeCollection). */\n protected serialize(items: T[]): string {\n return JSON.stringify(items);\n }\n\n /** Deserialize a string back to items. Used by string-based adapters. */\n protected deserialize(raw: string): T[] {\n return JSON.parse(raw);\n }\n\n // ── Error hook ──\n\n /** Called when a storage operation fails. Override for custom error handling. */\n protected onPersistError?(error: unknown): void;\n\n // ── Internal state ──\n\n private _hydrated = false;\n private _hydrating = false;\n // Suppresses the self-subscriber during reset/clear overrides,\n // which queue deltas manually instead of relying on diff.\n private _suppressSubscriber = false;\n private _persistenceReady = false;\n private _preHydrationWarned = false;\n private _pendingWrites = new Map<T['id'], T>();\n private _pendingRemoves = new Set<T['id']>();\n private _diffMap = new Map<T['id'], T>();\n private _pendingClear = false;\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n super(initialItems);\n\n // Self-subscribe to detect mutations via diff.\n // storageKey may not be available yet (class field initializers run after super()),\n // but that's fine — the subscriber only fires on mutations, not during construction.\n const unsub = this.subscribe((current, prev) => {\n if (this._hydrating || this._suppressSubscriber) return;\n this._ensurePersistenceReady();\n this._diffAndQueue(current, prev);\n this._scheduleSave();\n });\n this.addCleanup(unsub);\n }\n\n /**\n * DEV check for duplicate storageKey. Called lazily since storageKey is an abstract\n * field that isn't available during the parent constructor chain.\n */\n private _ensurePersistenceReady(): void {\n if (this._persistenceReady) return;\n this._persistenceReady = true;\n\n if (__DEV__ && _registeredKeys) {\n const className = this.constructor.name;\n const existing = _registeredKeys.get(this.storageKey);\n if (existing && existing !== className) {\n console.warn(\n `[mvc-kit] Duplicate storageKey \"${this.storageKey}\" used by \"${className}\" ` +\n `and \"${existing}\". Each PersistentCollection should have a unique storageKey.`,\n );\n }\n _registeredKeys.set(this.storageKey, className);\n }\n }\n\n // ── Public API ──\n\n /** Whether storage data has been loaded. */\n get hydrated(): boolean {\n return this._hydrated;\n }\n\n /**\n * Load data from storage into the collection. Idempotent — subsequent calls return current items.\n * Returns the items after hydration.\n */\n async hydrate(): Promise<T[]> {\n if (this._hydrated) return this.items;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = await this.persistGetAll();\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n return this.items;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n return this.items;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Synchronous hydration for sync adapters (e.g., WebStorage).\n * Call from the **leaf class** constructor (after field initializers have run).\n */\n protected _hydrateSync(): void {\n if (this._hydrated) return;\n this._ensurePersistenceReady();\n\n this._hydrating = true;\n try {\n const stored = this.persistGetAll();\n if (stored instanceof Promise) {\n throw new Error('[mvc-kit] _hydrateSync called with async persistGetAll');\n }\n if (stored.length > 0) {\n super.reset(stored);\n }\n this._hydrated = true;\n } catch (err) {\n this._handlePersistError(err);\n this._hydrated = true;\n } finally {\n this._hydrating = false;\n }\n }\n\n /**\n * Clear all data from storage AND from the in-memory collection.\n */\n clearStorage(): void | Promise<void> {\n this._ensurePersistenceReady();\n\n // Clear pending queues — we're wiping everything\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n this._pendingClear = false;\n this._cancelSave();\n\n // Clear in-memory\n if (this.length > 0) {\n super.clear();\n }\n\n try {\n const result = this.persistClear();\n if (result instanceof Promise) {\n return result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // ── Overrides for clear/reset tracking ──\n\n reset(items: T[]): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n for (const item of items) {\n this._pendingWrites.set(item.id, item);\n }\n // Suppress the self-subscriber — deltas are queued manually above\n this._suppressSubscriber = true;\n try {\n super.reset(items);\n } finally {\n this._suppressSubscriber = false;\n }\n this._scheduleSave();\n }\n\n clear(): void {\n this._pendingClear = true;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n // Suppress the self-subscriber — deltas are queued manually above\n this._suppressSubscriber = true;\n try {\n super.clear();\n } finally {\n this._suppressSubscriber = false;\n }\n this._scheduleSave();\n }\n\n // ── Override items getter for DEV pre-hydration warning ──\n\n get items(): T[] {\n if (__DEV__ && !this._hydrated && !this._hydrating && !this._preHydrationWarned) {\n this._preHydrationWarned = true;\n console.warn(\n `[mvc-kit] Accessing items on \"${this.constructor.name}\" before hydrate() has been called. ` +\n `Data may be incomplete. Call hydrate() first.`,\n );\n }\n return super.items;\n }\n\n get state(): T[] {\n return this.items;\n }\n\n // ── Dispose ──\n\n dispose(): void {\n if (this.disposed) return;\n\n // Flush any pending saves before disposing\n this._cancelSave();\n if (this._hasPending()) {\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n // DEV: unregister storageKey\n if (__DEV__ && _registeredKeys && this._persistenceReady) {\n _registeredKeys.delete(this.storageKey);\n }\n\n super.dispose();\n }\n\n // ── Private: delta tracking ──\n\n private _diffAndQueue(current: readonly T[], prev: readonly T[]): void {\n const prevMap = this._diffMap;\n prevMap.clear();\n for (const item of prev) {\n prevMap.set(item.id, item);\n }\n\n // Added or updated: in current but different reference in prev\n for (const item of current) {\n const prevItem = prevMap.get(item.id);\n if (!prevItem || prevItem !== item) {\n this._pendingWrites.set(item.id, item);\n this._pendingRemoves.delete(item.id);\n }\n prevMap.delete(item.id); // consume matched items\n }\n\n // Remaining in prevMap = removed (in prev but not in current)\n for (const [id] of prevMap) {\n this._pendingRemoves.add(id);\n this._pendingWrites.delete(id);\n }\n\n prevMap.clear();\n }\n\n private _hasPending(): boolean {\n return this._pendingClear || this._pendingWrites.size > 0 || this._pendingRemoves.size > 0;\n }\n\n // ── Private: debounce + flush ──\n\n private _scheduleSave(): void {\n this._cancelSave();\n const delay = (this.constructor as typeof PersistentCollection).WRITE_DELAY;\n if (delay <= 0) {\n this._doFlush();\n return;\n }\n this._flushTimer = setTimeout(() => this._doFlush(), delay);\n }\n\n private _cancelSave(): void {\n if (this._flushTimer !== null) {\n clearTimeout(this._flushTimer);\n this._flushTimer = null;\n }\n }\n\n private _doFlush(): void {\n if (!this._hasPending()) return;\n try {\n const result = this._flush();\n if (result instanceof Promise) {\n result.catch((err) => this._handlePersistError(err));\n }\n } catch (err) {\n this._handlePersistError(err);\n }\n }\n\n private _flush(): void | Promise<void> {\n const doClear = this._pendingClear;\n const writes = this._pendingWrites.size > 0 ? [...this._pendingWrites.values()] : null;\n const removes = this._pendingRemoves.size > 0 ? [...this._pendingRemoves] : null;\n\n // Clear queues\n this._pendingClear = false;\n this._pendingWrites.clear();\n this._pendingRemoves.clear();\n\n if (doClear) {\n const clearResult = this.persistClear();\n if (clearResult instanceof Promise) {\n return clearResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n if (writes) {\n return this.persistSet(writes);\n }\n return;\n }\n\n // Non-clear: removes then writes\n if (removes) {\n const removeResult = this.persistRemove(removes);\n if (removeResult instanceof Promise) {\n return removeResult.then(() => {\n if (writes) return this.persistSet(writes);\n });\n }\n }\n if (writes) {\n return this.persistSet(writes);\n }\n }\n\n // ── Private: error handling ──\n\n private _handlePersistError(err: unknown): void {\n if (this.onPersistError) {\n this.onPersistError(err);\n return;\n }\n if (__DEV__) {\n console.warn('[mvc-kit] Storage error:', err);\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/Resource.cjs
CHANGED
|
@@ -65,12 +65,15 @@ class Resource extends Collection.Collection {
|
|
|
65
65
|
return this.onInit?.();
|
|
66
66
|
}
|
|
67
67
|
// ── Collection delegation ─────────────────────────────────────
|
|
68
|
+
/** Current items array. Delegates to external Collection when injected. */
|
|
68
69
|
get state() {
|
|
69
70
|
return this._external ? this._external.state : super.state;
|
|
70
71
|
}
|
|
72
|
+
/** The raw array of items. Delegates to external Collection when injected. */
|
|
71
73
|
get items() {
|
|
72
74
|
return this._external ? this._external.items : super.items;
|
|
73
75
|
}
|
|
76
|
+
/** Number of items. Delegates to external Collection when injected. */
|
|
74
77
|
get length() {
|
|
75
78
|
return this._external ? this._external.length : super.length;
|
|
76
79
|
}
|
package/dist/Resource.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Resource.cjs","sources":["../src/Resource.ts"],"sourcesContent":["import { Collection } from './Collection';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport { wrapAsyncMethods } from './wrapAsyncMethods';\nimport type { InternalTaskState } from './wrapAsyncMethods';\nimport type { Listener, TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Async tracking types ────────────────────────────────────────\n\nconst DEFAULT_TASK_STATE: TaskState = Object.freeze({ loading: false, error: null, errorCode: null });\n\nexport type ResourceAsyncMethodKeys<T> = {\n [K in Exclude<keyof T, keyof Resource<any>>]: T[K] extends (...args: any[]) => Promise<any> ? K : never;\n}[Exclude<keyof T, keyof Resource<any>>];\n\ntype ResourceAsyncMap<T> = {\n readonly [K in ResourceAsyncMethodKeys<T>]: TaskState;\n};\n\nconst RESERVED_ASYNC_KEYS = ['async', 'subscribeAsync'] as const;\nconst LIFECYCLE_HOOKS = new Set(['onInit', 'onDispose']);\n\n// ── Resource ────────────────────────────────────────────────────\n\n/**\n * Collection + async tracking toolkit. Extends Collection with lifecycle\n * (init/dispose) and automatic async method tracking. Optionally delegates\n * to an external Collection for shared data scenarios.\n */\nexport class Resource<T extends { id: string | number }> extends Collection<T> {\n private _external: Collection<T> | null = null;\n private _initialized = false;\n\n // ── Async tracking fields ──\n private _asyncStates = new Map<string, InternalTaskState>();\n private _asyncSnapshots = new Map<string, TaskState>();\n private _asyncListeners = new Set<() => void>();\n private _asyncProxy: ResourceAsyncMap<this> | null = null;\n private _activeOps: Map<string, number> | null = null;\n\n /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */\n static GHOST_TIMEOUT = 3000;\n\n constructor(collectionOrItems?: Collection<T> | T[]) {\n const isExternal = collectionOrItems != null && !Array.isArray(collectionOrItems);\n super(isExternal ? [] : (collectionOrItems as T[]) ?? []);\n\n if (isExternal) {\n this._external = collectionOrItems as Collection<T>;\n\n if (__DEV__) {\n const Ctor = this.constructor as typeof Resource;\n if (Ctor.MAX_SIZE > 0 || Ctor.TTL > 0) {\n console.warn(\n `[mvc-kit] Resource \"${Ctor.name}\" has MAX_SIZE or TTL set but uses an ` +\n `injected Collection. Configure these on the Collection instead.`\n );\n }\n }\n }\n\n this._guardReservedKeys();\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this.disposed) return;\n this._initialized = true;\n\n if (__DEV__) {\n this._activeOps = new Map();\n }\n\n wrapAsyncMethods({\n instance: this,\n stopPrototype: Resource.prototype,\n reservedKeys: RESERVED_ASYNC_KEYS,\n lifecycleHooks: LIFECYCLE_HOOKS,\n isDisposed: () => this.disposed,\n isInitialized: () => this._initialized,\n asyncStates: this._asyncStates,\n asyncSnapshots: this._asyncSnapshots,\n asyncListeners: this._asyncListeners,\n notifyAsync: () => this._notifyAsync(),\n addCleanup: (fn) => this.addCleanup(fn),\n ghostTimeout: (this.constructor as typeof Resource).GHOST_TIMEOUT,\n className: 'Resource',\n activeOps: this._activeOps,\n });\n\n return this.onInit?.();\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n\n // ── Collection delegation ─────────────────────────────────────\n\n get state(): T[] {\n return this._external ? this._external.state : super.state;\n }\n\n get items(): T[] {\n return this._external ? this._external.items : super.items;\n }\n\n get length(): number {\n return this._external ? this._external.length : super.length;\n }\n\n add(...items: T[]): void {\n this._external ? this._external.add(...items) : super.add(...items);\n }\n\n upsert(...items: T[]): void {\n this._external ? this._external.upsert(...items) : super.upsert(...items);\n }\n\n update(id: T['id'], changes: Partial<T>): void {\n this._external ? this._external.update(id, changes) : super.update(id, changes);\n }\n\n remove(...ids: T['id'][]): void {\n this._external ? this._external.remove(...ids) : super.remove(...ids);\n }\n\n reset(items: T[]): void {\n this._external ? this._external.reset(items) : super.reset(items);\n }\n\n clear(): void {\n this._external ? this._external.clear() : super.clear();\n }\n\n optimistic(callback: () => void): () => void {\n return this._external ? this._external.optimistic(callback) : super.optimistic(callback);\n }\n\n get(id: T['id']): T | undefined {\n return this._external ? this._external.get(id) : super.get(id);\n }\n\n has(id: T['id']): boolean {\n return this._external ? this._external.has(id) : super.has(id);\n }\n\n find(predicate: (item: T) => boolean): T | undefined {\n return this._external ? this._external.find(predicate) : super.find(predicate);\n }\n\n filter(predicate: (item: T) => boolean): T[] {\n return this._external ? this._external.filter(predicate) : super.filter(predicate);\n }\n\n sorted(compareFn: (a: T, b: T) => number): T[] {\n return this._external ? this._external.sorted(compareFn) : super.sorted(compareFn);\n }\n\n map<U>(fn: (item: T) => U): U[] {\n return this._external ? this._external.map(fn) : super.map(fn);\n }\n\n subscribe(listener: Listener<T[]>): () => void {\n if (this.disposed) return () => {};\n return this._external ? this._external.subscribe(listener) : super.subscribe(listener);\n }\n\n // ── Async tracking API ────────────────────────────────────────\n\n /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */\n get async(): ResourceAsyncMap<this> {\n if (!this._asyncProxy) {\n const self = this;\n this._asyncProxy = new Proxy({} as ResourceAsyncMap<this>, {\n get(_, prop: string) {\n return self._asyncSnapshots.get(prop) ?? DEFAULT_TASK_STATE;\n },\n has(_, prop: string) {\n return self._asyncSnapshots.has(prop);\n },\n ownKeys() {\n return Array.from(self._asyncSnapshots.keys());\n },\n getOwnPropertyDescriptor(_, prop: string) {\n if (self._asyncSnapshots.has(prop)) {\n return { configurable: true, enumerable: true, value: self._asyncSnapshots.get(prop) };\n }\n return undefined;\n },\n });\n }\n return this._asyncProxy;\n }\n\n /** Subscribes to async state changes. Used by `useInstance` for React integration. */\n subscribeAsync(listener: () => void): () => void {\n if (this.disposed) return () => {};\n this._asyncListeners.add(listener);\n return () => { this._asyncListeners.delete(listener); };\n }\n\n // ── Private: async tracking internals ─────────────────────────\n\n private _notifyAsync(): void {\n for (const listener of this._asyncListeners) {\n listener();\n }\n }\n\n private _guardReservedKeys(): void {\n walkPrototypeChain(this, Resource.prototype, (key) => {\n if (RESERVED_ASYNC_KEYS.includes(key as any)) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on Resource and cannot be overridden.`\n );\n }\n });\n }\n\n}\n"],"names":["Collection","wrapAsyncMethods","walkPrototypeChain"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Resource.cjs","sources":["../src/Resource.ts"],"sourcesContent":["import { Collection } from './Collection';\nimport { walkPrototypeChain } from './walkPrototypeChain';\nimport { wrapAsyncMethods } from './wrapAsyncMethods';\nimport type { InternalTaskState } from './wrapAsyncMethods';\nimport type { Listener, TaskState } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\n// ── Async tracking types ────────────────────────────────────────\n\nconst DEFAULT_TASK_STATE: TaskState = Object.freeze({ loading: false, error: null, errorCode: null });\n\nexport type ResourceAsyncMethodKeys<T> = {\n [K in Exclude<keyof T, keyof Resource<any>>]: T[K] extends (...args: any[]) => Promise<any> ? K : never;\n}[Exclude<keyof T, keyof Resource<any>>];\n\ntype ResourceAsyncMap<T> = {\n readonly [K in ResourceAsyncMethodKeys<T>]: TaskState;\n};\n\nconst RESERVED_ASYNC_KEYS = ['async', 'subscribeAsync'] as const;\nconst LIFECYCLE_HOOKS = new Set(['onInit', 'onDispose']);\n\n// ── Resource ────────────────────────────────────────────────────\n\n/**\n * Collection + async tracking toolkit. Extends Collection with lifecycle\n * (init/dispose) and automatic async method tracking. Optionally delegates\n * to an external Collection for shared data scenarios.\n */\nexport class Resource<T extends { id: string | number }> extends Collection<T> {\n private _external: Collection<T> | null = null;\n private _initialized = false;\n\n // ── Async tracking fields ──\n private _asyncStates = new Map<string, InternalTaskState>();\n private _asyncSnapshots = new Map<string, TaskState>();\n private _asyncListeners = new Set<() => void>();\n private _asyncProxy: ResourceAsyncMap<this> | null = null;\n private _activeOps: Map<string, number> | null = null;\n\n /** DEV-only timeout (ms) for detecting ghost async operations after dispose. */\n static GHOST_TIMEOUT = 3000;\n\n constructor(collectionOrItems?: Collection<T> | T[]) {\n const isExternal = collectionOrItems != null && !Array.isArray(collectionOrItems);\n super(isExternal ? [] : (collectionOrItems as T[]) ?? []);\n\n if (isExternal) {\n this._external = collectionOrItems as Collection<T>;\n\n if (__DEV__) {\n const Ctor = this.constructor as typeof Resource;\n if (Ctor.MAX_SIZE > 0 || Ctor.TTL > 0) {\n console.warn(\n `[mvc-kit] Resource \"${Ctor.name}\" has MAX_SIZE or TTL set but uses an ` +\n `injected Collection. Configure these on the Collection instead.`\n );\n }\n }\n }\n\n this._guardReservedKeys();\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this.disposed) return;\n this._initialized = true;\n\n if (__DEV__) {\n this._activeOps = new Map();\n }\n\n wrapAsyncMethods({\n instance: this,\n stopPrototype: Resource.prototype,\n reservedKeys: RESERVED_ASYNC_KEYS,\n lifecycleHooks: LIFECYCLE_HOOKS,\n isDisposed: () => this.disposed,\n isInitialized: () => this._initialized,\n asyncStates: this._asyncStates,\n asyncSnapshots: this._asyncSnapshots,\n asyncListeners: this._asyncListeners,\n notifyAsync: () => this._notifyAsync(),\n addCleanup: (fn) => this.addCleanup(fn),\n ghostTimeout: (this.constructor as typeof Resource).GHOST_TIMEOUT,\n className: 'Resource',\n activeOps: this._activeOps,\n });\n\n return this.onInit?.();\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n\n // ── Collection delegation ─────────────────────────────────────\n\n /** Current items array. Delegates to external Collection when injected. */\n get state(): T[] {\n return this._external ? this._external.state : super.state;\n }\n\n /** The raw array of items. Delegates to external Collection when injected. */\n get items(): T[] {\n return this._external ? this._external.items : super.items;\n }\n\n /** Number of items. Delegates to external Collection when injected. */\n get length(): number {\n return this._external ? this._external.length : super.length;\n }\n\n add(...items: T[]): void {\n this._external ? this._external.add(...items) : super.add(...items);\n }\n\n upsert(...items: T[]): void {\n this._external ? this._external.upsert(...items) : super.upsert(...items);\n }\n\n update(id: T['id'], changes: Partial<T>): void {\n this._external ? this._external.update(id, changes) : super.update(id, changes);\n }\n\n remove(...ids: T['id'][]): void {\n this._external ? this._external.remove(...ids) : super.remove(...ids);\n }\n\n reset(items: T[]): void {\n this._external ? this._external.reset(items) : super.reset(items);\n }\n\n clear(): void {\n this._external ? this._external.clear() : super.clear();\n }\n\n optimistic(callback: () => void): () => void {\n return this._external ? this._external.optimistic(callback) : super.optimistic(callback);\n }\n\n get(id: T['id']): T | undefined {\n return this._external ? this._external.get(id) : super.get(id);\n }\n\n has(id: T['id']): boolean {\n return this._external ? this._external.has(id) : super.has(id);\n }\n\n find(predicate: (item: T) => boolean): T | undefined {\n return this._external ? this._external.find(predicate) : super.find(predicate);\n }\n\n filter(predicate: (item: T) => boolean): T[] {\n return this._external ? this._external.filter(predicate) : super.filter(predicate);\n }\n\n sorted(compareFn: (a: T, b: T) => number): T[] {\n return this._external ? this._external.sorted(compareFn) : super.sorted(compareFn);\n }\n\n map<U>(fn: (item: T) => U): U[] {\n return this._external ? this._external.map(fn) : super.map(fn);\n }\n\n subscribe(listener: Listener<T[]>): () => void {\n if (this.disposed) return () => {};\n return this._external ? this._external.subscribe(listener) : super.subscribe(listener);\n }\n\n // ── Async tracking API ────────────────────────────────────────\n\n /** Proxy providing `TaskState` (loading, error, errorCode) per async method. */\n get async(): ResourceAsyncMap<this> {\n if (!this._asyncProxy) {\n const self = this;\n this._asyncProxy = new Proxy({} as ResourceAsyncMap<this>, {\n get(_, prop: string) {\n return self._asyncSnapshots.get(prop) ?? DEFAULT_TASK_STATE;\n },\n has(_, prop: string) {\n return self._asyncSnapshots.has(prop);\n },\n ownKeys() {\n return Array.from(self._asyncSnapshots.keys());\n },\n getOwnPropertyDescriptor(_, prop: string) {\n if (self._asyncSnapshots.has(prop)) {\n return { configurable: true, enumerable: true, value: self._asyncSnapshots.get(prop) };\n }\n return undefined;\n },\n });\n }\n return this._asyncProxy;\n }\n\n /** Subscribes to async state changes. Used by `useInstance` for React integration. */\n subscribeAsync(listener: () => void): () => void {\n if (this.disposed) return () => {};\n this._asyncListeners.add(listener);\n return () => { this._asyncListeners.delete(listener); };\n }\n\n // ── Private: async tracking internals ─────────────────────────\n\n private _notifyAsync(): void {\n for (const listener of this._asyncListeners) {\n listener();\n }\n }\n\n private _guardReservedKeys(): void {\n walkPrototypeChain(this, Resource.prototype, (key) => {\n if (RESERVED_ASYNC_KEYS.includes(key as any)) {\n throw new Error(\n `[mvc-kit] \"${key}\" is a reserved property on Resource and cannot be overridden.`\n );\n }\n });\n }\n\n}\n"],"names":["Collection","wrapAsyncMethods","walkPrototypeChain"],"mappings":"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"}
|
package/dist/Resource.d.ts
CHANGED
|
@@ -30,8 +30,11 @@ export declare class Resource<T extends {
|
|
|
30
30
|
init(): void | Promise<void>;
|
|
31
31
|
/** Lifecycle hook called at the end of init(). Override to load initial data. @protected */
|
|
32
32
|
protected onInit?(): void | Promise<void>;
|
|
33
|
+
/** Current items array. Delegates to external Collection when injected. */
|
|
33
34
|
get state(): T[];
|
|
35
|
+
/** The raw array of items. Delegates to external Collection when injected. */
|
|
34
36
|
get items(): T[];
|
|
37
|
+
/** Number of items. Delegates to external Collection when injected. */
|
|
35
38
|
get length(): number;
|
|
36
39
|
add(...items: T[]): void;
|
|
37
40
|
upsert(...items: T[]): void;
|