@aiao/rxdb-react 0.0.9 → 0.0.11
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/dist/hooks.d.ts +8 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -49
- package/dist/useInfiniteScroll.d.ts +29 -0
- package/dist/useInfiniteScroll.d.ts.map +1 -0
- package/package.json +7 -3
- package/src/hooks.spec.ts +302 -2
- package/src/hooks.ts +12 -0
- package/src/index.ts +1 -0
- package/src/rxdb-react.spec.tsx +41 -1
- package/src/useInfiniteScroll.spec.ts +159 -0
- package/src/useInfiniteScroll.ts +189 -0
- package/vite.config.mts +4 -2
package/dist/hooks.d.ts
CHANGED
|
@@ -69,6 +69,14 @@ export declare const useFindOneOrFail: <T extends EntityType>(EntityType: T, opt
|
|
|
69
69
|
* ```
|
|
70
70
|
*/
|
|
71
71
|
export declare const useFind: <T extends EntityType>(EntityType: T, options: UseOptions<EntityStaticType<T, "findOptions">>) => RxDBResource<InstanceType<T>[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Find entities using cursor-based pagination
|
|
74
|
+
*
|
|
75
|
+
* @param EntityType The entity class
|
|
76
|
+
* @param options Cursor pagination options (where, orderBy, limit, after, before)
|
|
77
|
+
* @returns A resource object containing an array of entities
|
|
78
|
+
*/
|
|
79
|
+
export declare const useFindByCursor: <T extends EntityType>(EntityType: T, options: UseOptions<EntityStaticType<T, "findByCursorOptions">>) => RxDBResource<InstanceType<T>[]>;
|
|
72
80
|
/**
|
|
73
81
|
* Find all entities
|
|
74
82
|
*
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK1D,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnC,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IAClC;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IACtC;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAiHD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,UAAU,EACzC,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,KACrD,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CACgD,CAAC;AAE5F;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,UAAU,EAC7C,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,KACzD,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CACoD,CAAC;AAEhG;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,UAAU,EACnD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC,KAC/D,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAC0D,CAAC;AAEtG;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,UAAU,EAC1C,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,KACtD,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAA8E,CAAC;AAEhH;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,UAAU,EAClD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC,KAC9D,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAsF,CAAC;AAExH;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,UAAU,EAC7C,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,KACzD,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAiF,CAAC;AAEnH;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,UAAU,EAC3C,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,KACvD,YAAY,CAAC,MAAM,CAAmE,CAAC;AAM1F;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,UAAU,EACrD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,KAC1D,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CACqD,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,UAAU,EACtD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,KAC1D,YAAY,CAAC,MAAM,CAA8E,CAAC;AAErG;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,UAAU,EACnD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,KAC1D,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CACmD,CAAC;AAErF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,UAAU,EACpD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,KAC1D,YAAY,CAAC,MAAM,CAA4E,CAAC;AAMnG;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,UAAU,EACpD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC,KAC/D,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CACmD,CAAC;AAErF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,UAAU,EACpD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC,KAC/D,YAAY,CAAC,MAAM,CAA4E,CAAC;AAEnG;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,UAAU,EAChD,YAAY,CAAC,EACb,SAAS,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,KAC3D,YAAY,CAAC,GAAG,EAAE,CAAuE,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,72 +1,135 @@
|
|
|
1
|
-
import { isFunction as
|
|
2
|
-
import { useState as
|
|
3
|
-
import { jsx as
|
|
4
|
-
const s = (r, e,
|
|
5
|
-
const [D,
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
10
|
-
const
|
|
1
|
+
import { isFunction as H, cloneDeep as J } from "@aiao/utils";
|
|
2
|
+
import { useState as a, useRef as f, useMemo as P, useEffect as N, createContext as Q, useContext as U, useCallback as m } from "react";
|
|
3
|
+
import { jsx as W } from "react/jsx-runtime";
|
|
4
|
+
const s = (r, e, c, g) => {
|
|
5
|
+
const [D, p] = a(c), [y, l] = a(void 0), [B, d] = a(!0), [u, C] = a(void 0), [I, b] = a(!1), o = f(void 0), i = f(!0), M = P(() => H(g) ? g() : g, [g]);
|
|
6
|
+
return N(() => {
|
|
7
|
+
i.current = !0, o.current && (o.current.unsubscribe(), o.current = void 0);
|
|
8
|
+
const h = r[e];
|
|
9
|
+
if (!h || typeof h != "function") {
|
|
10
|
+
const t = new Error(`Method "${String(e)}" not found on EntityType`);
|
|
11
11
|
Promise.resolve().then(() => {
|
|
12
|
-
|
|
12
|
+
i.current && (l(t), d(!1));
|
|
13
13
|
});
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
try {
|
|
17
|
-
|
|
18
|
-
next: (
|
|
19
|
-
|
|
17
|
+
o.current = h(M).subscribe({
|
|
18
|
+
next: (t) => {
|
|
19
|
+
i.current && (d(!1), b(!0), l(void 0), Array.isArray(t) ? C(t.length === 0) : C(t == null), p(t));
|
|
20
20
|
},
|
|
21
|
-
error: (
|
|
22
|
-
|
|
21
|
+
error: (t) => {
|
|
22
|
+
i.current && (d(!1), b(!1), l(t), console.error(`RxDB query error in ${String(e)}:`, t));
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
|
-
} catch (
|
|
26
|
-
const
|
|
25
|
+
} catch (t) {
|
|
26
|
+
const x = t instanceof Error ? t : new Error(String(t));
|
|
27
27
|
Promise.resolve().then(() => {
|
|
28
|
-
|
|
28
|
+
i.current && (d(!1), l(x));
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
return () => {
|
|
32
|
-
|
|
32
|
+
i.current = !1, o.current && (o.current.unsubscribe(), o.current = void 0);
|
|
33
33
|
};
|
|
34
|
-
}, [r, e,
|
|
34
|
+
}, [r, e, M]), {
|
|
35
35
|
value: D,
|
|
36
|
-
error:
|
|
37
|
-
isLoading:
|
|
38
|
-
isEmpty:
|
|
39
|
-
hasValue:
|
|
36
|
+
error: y,
|
|
37
|
+
isLoading: B,
|
|
38
|
+
isEmpty: u,
|
|
39
|
+
hasValue: I
|
|
40
40
|
};
|
|
41
|
-
},
|
|
42
|
-
function
|
|
43
|
-
const r =
|
|
41
|
+
}, re = (r, e) => s(r, "get", void 0, e), te = (r, e) => s(r, "findOne", void 0, e), ne = (r, e) => s(r, "findOneOrFail", void 0, e), se = (r, e) => s(r, "find", [], e), oe = (r, e) => s(r, "findByCursor", [], e), ce = (r, e) => s(r, "findAll", [], e), ue = (r, e) => s(r, "count", 0, e), ie = (r, e) => s(r, "findDescendants", [], e), fe = (r, e) => s(r, "countDescendants", 0, e), ae = (r, e) => s(r, "findAncestors", [], e), le = (r, e) => s(r, "countAncestors", 0, e), de = (r, e) => s(r, "findNeighbors", [], e), ge = (r, e) => s(r, "countNeighbors", 0, e), he = (r, e) => s(r, "findPaths", [], e);
|
|
42
|
+
function X() {
|
|
43
|
+
const r = Q(void 0);
|
|
44
44
|
return {
|
|
45
45
|
useRxDB: ((e) => {
|
|
46
|
-
const
|
|
46
|
+
const c = U(r);
|
|
47
47
|
if (e !== void 0) return e;
|
|
48
|
-
if (!
|
|
49
|
-
return
|
|
48
|
+
if (!c) throw new Error("No RxDB instance found, use RxDBProvider to provide one");
|
|
49
|
+
return c;
|
|
50
50
|
}),
|
|
51
|
-
RxDBProvider: ({ children: e, db:
|
|
51
|
+
RxDBProvider: ({ children: e, db: c }) => /* @__PURE__ */ W(r.Provider, { value: c, children: e })
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const { RxDBProvider: ve, useRxDB: Y } = X();
|
|
55
|
+
function pe(r, e) {
|
|
56
|
+
const c = Y(), [g, D] = a([]), p = f([]), y = m(
|
|
57
|
+
(n) => {
|
|
58
|
+
const F = typeof n == "function" ? n(p.current) : n;
|
|
59
|
+
p.current = F, D(F);
|
|
60
|
+
},
|
|
61
|
+
[]
|
|
62
|
+
), [l, B] = a(!1), d = f(!1), u = m((n) => {
|
|
63
|
+
d.current = n, B(n);
|
|
64
|
+
}, []), [C, I] = a(!0), b = f(!0), o = m((n) => {
|
|
65
|
+
b.current = n, I(n);
|
|
66
|
+
}, []), [i, M] = a(!1), h = f(!1), t = f(!1), x = f([]), O = P(() => H(e) ? e() : e, [e]), E = P(() => JSON.stringify(O), [O]), A = P(() => g.flat(), [g]), V = P(
|
|
67
|
+
() => A.length === 0 && !l && i,
|
|
68
|
+
[A.length, l, i]
|
|
69
|
+
), R = m(() => {
|
|
70
|
+
x.current.forEach((n) => n.unsubscribe()), x.current = [];
|
|
71
|
+
}, []), v = m(() => {
|
|
72
|
+
if (d.current || !b.current || !t.current) return;
|
|
73
|
+
u(!0);
|
|
74
|
+
const n = p.current, F = n.length, S = n.length > 0 ? n[n.length - 1] : void 0, q = S && S.length > 0 ? S[S.length - 1] : void 0, L = J(O);
|
|
75
|
+
q && (L.after = q);
|
|
76
|
+
const K = c.entityManager.getRepository(r).findByCursor(L).subscribe({
|
|
77
|
+
next: (z) => {
|
|
78
|
+
if (!t.current) return;
|
|
79
|
+
y((j) => {
|
|
80
|
+
const G = [...j];
|
|
81
|
+
return G[F] = z, G;
|
|
82
|
+
});
|
|
83
|
+
const $ = L.limit || 100;
|
|
84
|
+
z.length < $ && o(!1), u(!1);
|
|
85
|
+
},
|
|
86
|
+
error: () => {
|
|
87
|
+
t.current && u(!1);
|
|
88
|
+
},
|
|
89
|
+
complete: () => {
|
|
90
|
+
t.current && u(!1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
x.current.push(K);
|
|
94
|
+
}, [r, O, c, o, u, y]), k = m(() => {
|
|
95
|
+
if (!t.current) {
|
|
96
|
+
console.warn("Cannot refresh: component is unmounted");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
R(), y([]), u(!1), o(!0), v();
|
|
100
|
+
}, [R, v, o, u, y]);
|
|
101
|
+
N(() => (t.current = !0, h.current || (h.current = !0, M(!0), v()), () => {
|
|
102
|
+
t.current = !1, R();
|
|
103
|
+
}), [R, v]);
|
|
104
|
+
const w = f(E);
|
|
105
|
+
return N(() => {
|
|
106
|
+
h.current && w.current !== E && (w.current = E, R(), p.current = [], d.current = !1, b.current = !0, D([]), B(!1), I(!0), v());
|
|
107
|
+
}, [E, R, v]), {
|
|
108
|
+
value: A,
|
|
109
|
+
isEmpty: V,
|
|
110
|
+
isLoading: l,
|
|
111
|
+
hasMore: C,
|
|
112
|
+
loadMore: v,
|
|
113
|
+
refresh: k
|
|
52
114
|
};
|
|
53
115
|
}
|
|
54
|
-
const { RxDBProvider: K, useRxDB: T } = C();
|
|
55
116
|
export {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
ve as RxDBProvider,
|
|
118
|
+
X as makeRxDBProvider,
|
|
119
|
+
ue as useCount,
|
|
120
|
+
le as useCountAncestors,
|
|
121
|
+
fe as useCountDescendants,
|
|
122
|
+
ge as useCountNeighbors,
|
|
123
|
+
se as useFind,
|
|
124
|
+
ce as useFindAll,
|
|
125
|
+
ae as useFindAncestors,
|
|
126
|
+
oe as useFindByCursor,
|
|
127
|
+
ie as useFindDescendants,
|
|
128
|
+
te as useFindOne,
|
|
129
|
+
ne as useFindOneOrFail,
|
|
130
|
+
re as useGet,
|
|
131
|
+
de as useGraphNeighbors,
|
|
132
|
+
he as useGraphPaths,
|
|
133
|
+
pe as useInfiniteScroll,
|
|
134
|
+
Y as useRxDB
|
|
72
135
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EntityStaticType, EntityType } from '../../rxdb/src/index.ts';
|
|
2
|
+
type UseOptions<T> = T | (() => T);
|
|
3
|
+
export interface InfiniteScrollResource<T> {
|
|
4
|
+
value: T[];
|
|
5
|
+
isEmpty: boolean;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
hasMore: boolean;
|
|
8
|
+
loadMore: () => void;
|
|
9
|
+
refresh: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Infinite scroll hook for cursor-based pagination
|
|
13
|
+
*
|
|
14
|
+
* @param EntityType The entity class
|
|
15
|
+
* @param options Query options with cursor pagination
|
|
16
|
+
* @returns A resource object with infinite scroll controls
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const resource = useInfiniteScroll(Todo, {
|
|
21
|
+
* where: { completed: false },
|
|
22
|
+
* orderBy: [{ field: 'createdAt', sort: 'desc' }],
|
|
23
|
+
* limit: 50
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function useInfiniteScroll<T extends EntityType>(EntityType: T, options: UseOptions<EntityStaticType<T, 'findByCursorOptions'>>): InfiniteScrollResource<InstanceType<T>>;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=useInfiniteScroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInfiniteScroll.d.ts","sourceRoot":"","sources":["../src/useInfiniteScroll.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK1D,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnC,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACvC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,UAAU,EACpD,UAAU,EAAE,CAAC,EACb,OAAO,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC,GAC9D,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAyJzC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiao/rxdb-react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@aiao/rxdb": "0.0.
|
|
19
|
-
"@aiao/utils": "0.0.
|
|
18
|
+
"@aiao/rxdb": "0.0.11",
|
|
19
|
+
"@aiao/utils": "0.0.11"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": ">=19.0.0",
|
|
23
|
+
"rxjs": "^7.8.0"
|
|
20
24
|
},
|
|
21
25
|
"nx": {
|
|
22
26
|
"name": "rxdb-react",
|
package/src/hooks.spec.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import { of } from 'rxjs';
|
|
1
|
+
import { of, Subject, throwError } from 'rxjs';
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useCount,
|
|
5
|
+
useCountAncestors,
|
|
6
|
+
useCountDescendants,
|
|
7
|
+
useCountNeighbors,
|
|
8
|
+
useFind,
|
|
9
|
+
useFindAll,
|
|
10
|
+
useFindAncestors,
|
|
11
|
+
useFindByCursor,
|
|
12
|
+
useFindDescendants,
|
|
13
|
+
useFindOne,
|
|
14
|
+
useFindOneOrFail,
|
|
15
|
+
useGet,
|
|
16
|
+
useGraphNeighbors,
|
|
17
|
+
useGraphPaths
|
|
18
|
+
} from './hooks';
|
|
4
19
|
|
|
5
20
|
// Mock React hooks
|
|
6
21
|
let mockState: any[] = [];
|
|
@@ -32,7 +47,18 @@ class MockEntity {
|
|
|
32
47
|
name!: string;
|
|
33
48
|
static get = vi.fn();
|
|
34
49
|
static findOne = vi.fn();
|
|
50
|
+
static findOneOrFail = vi.fn();
|
|
35
51
|
static find = vi.fn();
|
|
52
|
+
static findAll = vi.fn();
|
|
53
|
+
static count = vi.fn();
|
|
54
|
+
static findDescendants = vi.fn();
|
|
55
|
+
static countDescendants = vi.fn();
|
|
56
|
+
static findAncestors = vi.fn();
|
|
57
|
+
static countAncestors = vi.fn();
|
|
58
|
+
static findNeighbors = vi.fn();
|
|
59
|
+
static countNeighbors = vi.fn();
|
|
60
|
+
static findByCursor = vi.fn();
|
|
61
|
+
static findPaths = vi.fn();
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
describe('useGet', () => {
|
|
@@ -109,6 +135,30 @@ describe('useFind', () => {
|
|
|
109
135
|
});
|
|
110
136
|
});
|
|
111
137
|
|
|
138
|
+
describe('useFindByCursor', () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
vi.clearAllMocks();
|
|
141
|
+
mockState = [];
|
|
142
|
+
mockStateIndex = 0;
|
|
143
|
+
mockEffectCleanups = [];
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should call findByCursor method', () => {
|
|
147
|
+
const mockData = [
|
|
148
|
+
{ id: '1', name: 'User1' },
|
|
149
|
+
{ id: '2', name: 'User2' }
|
|
150
|
+
];
|
|
151
|
+
MockEntity.findByCursor.mockReturnValue(of(mockData));
|
|
152
|
+
|
|
153
|
+
useFindByCursor(MockEntity as any, {
|
|
154
|
+
where: {},
|
|
155
|
+
orderBy: [{ field: 'id', sort: 'ASC' }]
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(MockEntity.findByCursor).toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
112
162
|
describe('Hook cleanup', () => {
|
|
113
163
|
beforeEach(() => {
|
|
114
164
|
vi.clearAllMocks();
|
|
@@ -124,4 +174,254 @@ describe('Hook cleanup', () => {
|
|
|
124
174
|
|
|
125
175
|
expect(mockEffectCleanups.length).toBeGreaterThan(0);
|
|
126
176
|
});
|
|
177
|
+
|
|
178
|
+
it('should unsubscribe on cleanup', () => {
|
|
179
|
+
const mockUnsubscribe = vi.fn();
|
|
180
|
+
const mockSubscription = { unsubscribe: mockUnsubscribe };
|
|
181
|
+
const subject = new Subject();
|
|
182
|
+
MockEntity.get.mockReturnValue(subject.asObservable());
|
|
183
|
+
(subject.subscribe as any) = vi.fn(() => mockSubscription);
|
|
184
|
+
|
|
185
|
+
MockEntity.get.mockReturnValue({
|
|
186
|
+
subscribe: vi.fn(() => mockSubscription)
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
useGet(MockEntity as any, '1');
|
|
190
|
+
|
|
191
|
+
// Execute cleanup
|
|
192
|
+
mockEffectCleanups.forEach(cleanup => cleanup());
|
|
193
|
+
|
|
194
|
+
expect(mockUnsubscribe).toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should clean up previous subscription on re-render', () => {
|
|
198
|
+
const mockUnsubscribe = vi.fn();
|
|
199
|
+
const mockSubscription = { unsubscribe: mockUnsubscribe };
|
|
200
|
+
|
|
201
|
+
MockEntity.get.mockReturnValue({
|
|
202
|
+
subscribe: vi.fn(() => mockSubscription)
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// First render
|
|
206
|
+
useGet(MockEntity as any, '1');
|
|
207
|
+
|
|
208
|
+
// Simulate re-render by calling effect again (the effect will clean up previous subscription)
|
|
209
|
+
mockState = [];
|
|
210
|
+
mockStateIndex = 0;
|
|
211
|
+
|
|
212
|
+
// Second render - this should trigger cleanup of previous subscription
|
|
213
|
+
useGet(MockEntity as any, '2');
|
|
214
|
+
|
|
215
|
+
// The cleanup from first render should have been called
|
|
216
|
+
expect(mockEffectCleanups.length).toBeGreaterThan(0);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('Error handling', () => {
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
vi.clearAllMocks();
|
|
223
|
+
mockState = [];
|
|
224
|
+
mockStateIndex = 0;
|
|
225
|
+
mockEffectCleanups = [];
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should handle method not found error', async () => {
|
|
229
|
+
// Use a mock entity without the method
|
|
230
|
+
const NoMethodEntity = {} as any;
|
|
231
|
+
|
|
232
|
+
const result = useGet(NoMethodEntity, '1');
|
|
233
|
+
|
|
234
|
+
// Wait for Promise.resolve to complete
|
|
235
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
236
|
+
|
|
237
|
+
expect(result).toBeDefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should handle subscription error', () => {
|
|
241
|
+
const testError = new Error('Test error');
|
|
242
|
+
MockEntity.get.mockReturnValue(throwError(() => testError));
|
|
243
|
+
|
|
244
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
245
|
+
|
|
246
|
+
const result = useGet(MockEntity as any, '1');
|
|
247
|
+
|
|
248
|
+
expect(result).toBeDefined();
|
|
249
|
+
consoleSpy.mockRestore();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle exception during query execution', () => {
|
|
253
|
+
MockEntity.get.mockImplementation(() => {
|
|
254
|
+
throw new Error('Sync error');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const result = useGet(MockEntity as any, '1');
|
|
258
|
+
|
|
259
|
+
expect(result).toBeDefined();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('isEmpty behavior', () => {
|
|
264
|
+
beforeEach(() => {
|
|
265
|
+
vi.clearAllMocks();
|
|
266
|
+
mockState = [];
|
|
267
|
+
mockStateIndex = 0;
|
|
268
|
+
mockEffectCleanups = [];
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should set isEmpty true for empty array', () => {
|
|
272
|
+
MockEntity.find.mockReturnValue(of([]));
|
|
273
|
+
|
|
274
|
+
const result = useFind(MockEntity as any, {});
|
|
275
|
+
|
|
276
|
+
expect(result).toBeDefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should set isEmpty false for non-empty array', () => {
|
|
280
|
+
MockEntity.find.mockReturnValue(of([{ id: '1' }]));
|
|
281
|
+
|
|
282
|
+
const result = useFind(MockEntity as any, {});
|
|
283
|
+
|
|
284
|
+
expect(result).toBeDefined();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should set isEmpty true for null value', () => {
|
|
288
|
+
MockEntity.findOne.mockReturnValue(of(null));
|
|
289
|
+
|
|
290
|
+
const result = useFindOne(MockEntity as any, {});
|
|
291
|
+
|
|
292
|
+
expect(result).toBeDefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should set isEmpty false for non-null value', () => {
|
|
296
|
+
MockEntity.findOne.mockReturnValue(of({ id: '1' }));
|
|
297
|
+
|
|
298
|
+
const result = useFindOne(MockEntity as any, {});
|
|
299
|
+
|
|
300
|
+
expect(result).toBeDefined();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('useFindOneOrFail', () => {
|
|
305
|
+
beforeEach(() => {
|
|
306
|
+
vi.clearAllMocks();
|
|
307
|
+
mockState = [];
|
|
308
|
+
mockStateIndex = 0;
|
|
309
|
+
mockEffectCleanups = [];
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should call findOneOrFail method', () => {
|
|
313
|
+
MockEntity.findOneOrFail.mockReturnValue(of({ id: '1', name: 'Found' }));
|
|
314
|
+
|
|
315
|
+
useFindOneOrFail(MockEntity as any, { where: { id: '1' } });
|
|
316
|
+
|
|
317
|
+
expect(MockEntity.findOneOrFail).toHaveBeenCalledWith({ where: { id: '1' } });
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('useFindAll', () => {
|
|
322
|
+
beforeEach(() => {
|
|
323
|
+
vi.clearAllMocks();
|
|
324
|
+
mockState = [];
|
|
325
|
+
mockStateIndex = 0;
|
|
326
|
+
mockEffectCleanups = [];
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should call findAll method', () => {
|
|
330
|
+
MockEntity.findAll.mockReturnValue(of([{ id: '1' }, { id: '2' }]));
|
|
331
|
+
|
|
332
|
+
useFindAll(MockEntity as any, {});
|
|
333
|
+
|
|
334
|
+
expect(MockEntity.findAll).toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('useCount', () => {
|
|
339
|
+
beforeEach(() => {
|
|
340
|
+
vi.clearAllMocks();
|
|
341
|
+
mockState = [];
|
|
342
|
+
mockStateIndex = 0;
|
|
343
|
+
mockEffectCleanups = [];
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should call count method', () => {
|
|
347
|
+
MockEntity.count.mockReturnValue(of(5));
|
|
348
|
+
|
|
349
|
+
useCount(MockEntity as any, {});
|
|
350
|
+
|
|
351
|
+
expect(MockEntity.count).toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('Tree Repository Hooks', () => {
|
|
356
|
+
beforeEach(() => {
|
|
357
|
+
vi.clearAllMocks();
|
|
358
|
+
mockState = [];
|
|
359
|
+
mockStateIndex = 0;
|
|
360
|
+
mockEffectCleanups = [];
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('useFindDescendants should call findDescendants', () => {
|
|
364
|
+
MockEntity.findDescendants.mockReturnValue(of([{ id: '2' }]));
|
|
365
|
+
|
|
366
|
+
useFindDescendants(MockEntity as any, { entityId: '1' });
|
|
367
|
+
|
|
368
|
+
expect(MockEntity.findDescendants).toHaveBeenCalledWith({ entityId: '1' });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('useCountDescendants should call countDescendants', () => {
|
|
372
|
+
MockEntity.countDescendants.mockReturnValue(of(3));
|
|
373
|
+
|
|
374
|
+
useCountDescendants(MockEntity as any, { entityId: '1' });
|
|
375
|
+
|
|
376
|
+
expect(MockEntity.countDescendants).toHaveBeenCalledWith({ entityId: '1' });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('useFindAncestors should call findAncestors', () => {
|
|
380
|
+
MockEntity.findAncestors.mockReturnValue(of([{ id: '0' }]));
|
|
381
|
+
|
|
382
|
+
useFindAncestors(MockEntity as any, { entityId: '1' });
|
|
383
|
+
|
|
384
|
+
expect(MockEntity.findAncestors).toHaveBeenCalledWith({ entityId: '1' });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('useCountAncestors should call countAncestors', () => {
|
|
388
|
+
MockEntity.countAncestors.mockReturnValue(of(2));
|
|
389
|
+
|
|
390
|
+
useCountAncestors(MockEntity as any, { entityId: '1' });
|
|
391
|
+
|
|
392
|
+
expect(MockEntity.countAncestors).toHaveBeenCalledWith({ entityId: '1' });
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('Graph Repository Hooks', () => {
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
vi.clearAllMocks();
|
|
399
|
+
mockState = [];
|
|
400
|
+
mockStateIndex = 0;
|
|
401
|
+
mockEffectCleanups = [];
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('useGraphNeighbors should call findNeighbors', () => {
|
|
405
|
+
MockEntity.findNeighbors.mockReturnValue(of([{ id: '2' }]));
|
|
406
|
+
|
|
407
|
+
useGraphNeighbors(MockEntity as any, { entityId: '1', direction: 'out', level: 1 });
|
|
408
|
+
|
|
409
|
+
expect(MockEntity.findNeighbors).toHaveBeenCalledWith({ entityId: '1', direction: 'out', level: 1 });
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('useCountNeighbors should call countNeighbors', () => {
|
|
413
|
+
MockEntity.countNeighbors.mockReturnValue(of(5));
|
|
414
|
+
|
|
415
|
+
useCountNeighbors(MockEntity as any, { entityId: '1', direction: 'out', level: 1 });
|
|
416
|
+
|
|
417
|
+
expect(MockEntity.countNeighbors).toHaveBeenCalledWith({ entityId: '1', direction: 'out', level: 1 });
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('useGraphPaths should call findPaths', () => {
|
|
421
|
+
MockEntity.findPaths.mockReturnValue(of([['1', '2', '3']]));
|
|
422
|
+
|
|
423
|
+
useGraphPaths(MockEntity as any, { fromId: '1', toId: '3', maxDepth: 5 });
|
|
424
|
+
|
|
425
|
+
expect(MockEntity.findPaths).toHaveBeenCalledWith({ fromId: '1', toId: '3', maxDepth: 5 });
|
|
426
|
+
});
|
|
127
427
|
});
|
package/src/hooks.ts
CHANGED
|
@@ -205,6 +205,18 @@ export const useFind = <T extends EntityType>(
|
|
|
205
205
|
options: UseOptions<EntityStaticType<T, 'findOptions'>>
|
|
206
206
|
): RxDBResource<InstanceType<T>[]> => useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'find', [], options);
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Find entities using cursor-based pagination
|
|
210
|
+
*
|
|
211
|
+
* @param EntityType The entity class
|
|
212
|
+
* @param options Cursor pagination options (where, orderBy, limit, after, before)
|
|
213
|
+
* @returns A resource object containing an array of entities
|
|
214
|
+
*/
|
|
215
|
+
export const useFindByCursor = <T extends EntityType>(
|
|
216
|
+
EntityType: T,
|
|
217
|
+
options: UseOptions<EntityStaticType<T, 'findByCursorOptions'>>
|
|
218
|
+
): RxDBResource<InstanceType<T>[]> => useRepositoryQuery<T, InstanceType<T>[]>(EntityType, 'findByCursor', [], options);
|
|
219
|
+
|
|
208
220
|
/**
|
|
209
221
|
* Find all entities
|
|
210
222
|
*
|
package/src/index.ts
CHANGED
package/src/rxdb-react.spec.tsx
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import { makeRxDBProvider, RxDBProvider, useRxDB } from './rxdb-react';
|
|
4
4
|
|
|
5
|
+
// Mock React hooks for testing useRxDB behavior
|
|
6
|
+
let mockContextValue: any = undefined;
|
|
7
|
+
vi.mock('react', async () => {
|
|
8
|
+
const actual = await vi.importActual<typeof React>('react');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
useContext: () => mockContextValue,
|
|
12
|
+
createContext: actual.createContext
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
5
16
|
describe('makeRxDBProvider', () => {
|
|
6
17
|
it('should create RxDBProvider and useRxDB hook', () => {
|
|
7
18
|
const { RxDBProvider, useRxDB } = makeRxDBProvider();
|
|
@@ -39,3 +50,32 @@ describe('Default exports', () => {
|
|
|
39
50
|
expect(typeof makeRxDBProvider).toBe('function');
|
|
40
51
|
});
|
|
41
52
|
});
|
|
53
|
+
|
|
54
|
+
describe('useRxDB behavior', () => {
|
|
55
|
+
it('should return provided db parameter if given', () => {
|
|
56
|
+
const { useRxDB } = makeRxDBProvider();
|
|
57
|
+
const directDB = { name: 'direct-db' } as any;
|
|
58
|
+
mockContextValue = { name: 'context-db' };
|
|
59
|
+
|
|
60
|
+
const result = useRxDB(directDB);
|
|
61
|
+
|
|
62
|
+
expect(result).toBe(directDB);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return context db when no parameter given', () => {
|
|
66
|
+
const { useRxDB } = makeRxDBProvider();
|
|
67
|
+
const contextDB = { name: 'context-db' };
|
|
68
|
+
mockContextValue = contextDB;
|
|
69
|
+
|
|
70
|
+
const result = useRxDB();
|
|
71
|
+
|
|
72
|
+
expect(result).toBe(contextDB);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should throw error when no db in context and no parameter', () => {
|
|
76
|
+
const { useRxDB } = makeRxDBProvider();
|
|
77
|
+
mockContextValue = undefined;
|
|
78
|
+
|
|
79
|
+
expect(() => useRxDB()).toThrow('No RxDB instance found, use RxDBProvider to provide one');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { of, throwError } from 'rxjs';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
// Import after mocks are set up
|
|
5
|
+
import { useInfiniteScroll } from './useInfiniteScroll';
|
|
6
|
+
|
|
7
|
+
// Mock findByCursor + repository
|
|
8
|
+
const mockFindByCursor = vi.fn();
|
|
9
|
+
const mockGetRepository = vi.fn(() => ({ findByCursor: mockFindByCursor }));
|
|
10
|
+
|
|
11
|
+
// Mock useRxDB before importing useInfiniteScroll
|
|
12
|
+
vi.mock('./rxdb-react', () => ({
|
|
13
|
+
useRxDB: () => ({ entityManager: { getRepository: mockGetRepository } })
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock React hooks
|
|
17
|
+
let mockState: any[] = [];
|
|
18
|
+
let mockStateIndex = 0;
|
|
19
|
+
let mockEffectCleanups: Array<() => void> = [];
|
|
20
|
+
|
|
21
|
+
vi.mock('react', () => ({
|
|
22
|
+
useState: (initial: any) => {
|
|
23
|
+
const index = mockStateIndex++;
|
|
24
|
+
if (mockState[index] === undefined) {
|
|
25
|
+
mockState[index] = typeof initial === 'function' ? initial() : initial;
|
|
26
|
+
}
|
|
27
|
+
const setState = (newValue: any) => {
|
|
28
|
+
mockState[index] = typeof newValue === 'function' ? newValue(mockState[index]) : newValue;
|
|
29
|
+
};
|
|
30
|
+
return [mockState[index], setState];
|
|
31
|
+
},
|
|
32
|
+
useEffect: (effect: () => any) => {
|
|
33
|
+
const cleanup = effect();
|
|
34
|
+
if (cleanup) mockEffectCleanups.push(cleanup);
|
|
35
|
+
},
|
|
36
|
+
useRef: (initial: any) => ({ current: initial }),
|
|
37
|
+
useMemo: (fn: () => any) => fn(),
|
|
38
|
+
useCallback: (fn: any) => fn
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
class MockEntity {
|
|
42
|
+
id!: string;
|
|
43
|
+
name!: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const defaultOptions = {
|
|
47
|
+
where: {},
|
|
48
|
+
orderBy: [{ field: 'id', sort: 'ASC' }],
|
|
49
|
+
limit: 10
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
describe('useInfiniteScroll (React)', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
mockState = [];
|
|
56
|
+
mockStateIndex = 0;
|
|
57
|
+
mockEffectCleanups = [];
|
|
58
|
+
mockGetRepository.mockReturnValue({ findByCursor: mockFindByCursor });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
mockEffectCleanups.forEach(cleanup => cleanup());
|
|
63
|
+
mockEffectCleanups = [];
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should export useInfiniteScroll function', () => {
|
|
67
|
+
expect(useInfiniteScroll).toBeDefined();
|
|
68
|
+
expect(typeof useInfiniteScroll).toBe('function');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return InfiniteScrollResource structure', () => {
|
|
72
|
+
mockFindByCursor.mockReturnValue(of([]));
|
|
73
|
+
|
|
74
|
+
const resource = useInfiniteScroll(MockEntity as any, defaultOptions);
|
|
75
|
+
|
|
76
|
+
expect(resource.value).toBeDefined();
|
|
77
|
+
expect(typeof resource.isEmpty).toBe('boolean');
|
|
78
|
+
expect(typeof resource.isLoading).toBe('boolean');
|
|
79
|
+
expect(typeof resource.hasMore).toBe('boolean');
|
|
80
|
+
expect(typeof resource.loadMore).toBe('function');
|
|
81
|
+
expect(typeof resource.refresh).toBe('function');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should call findByCursor on initial load', () => {
|
|
85
|
+
mockFindByCursor.mockReturnValue(of([{ id: '1', name: 'a' }]));
|
|
86
|
+
|
|
87
|
+
useInfiniteScroll(MockEntity as any, defaultOptions);
|
|
88
|
+
|
|
89
|
+
expect(mockGetRepository).toHaveBeenCalledWith(MockEntity);
|
|
90
|
+
expect(mockFindByCursor).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return value from subscription', () => {
|
|
94
|
+
const items = [
|
|
95
|
+
{ id: '1', name: 'a' },
|
|
96
|
+
{ id: '2', name: 'b' }
|
|
97
|
+
];
|
|
98
|
+
mockFindByCursor.mockReturnValue(of(items));
|
|
99
|
+
|
|
100
|
+
const resource = useInfiniteScroll(MockEntity as any, { ...defaultOptions, limit: 10 });
|
|
101
|
+
|
|
102
|
+
expect(resource).toBeDefined();
|
|
103
|
+
expect(mockFindByCursor).toHaveBeenCalledTimes(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should support function options', () => {
|
|
107
|
+
mockFindByCursor.mockReturnValue(of([]));
|
|
108
|
+
|
|
109
|
+
const resource = useInfiniteScroll(MockEntity as any, () => ({
|
|
110
|
+
where: { status: 'active' },
|
|
111
|
+
orderBy: [{ field: 'id', sort: 'ASC' }],
|
|
112
|
+
limit: 20
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
expect(resource).toBeDefined();
|
|
116
|
+
expect(mockFindByCursor).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should clean up subscriptions on unmount', () => {
|
|
120
|
+
const unsubscribeSpy = vi.fn();
|
|
121
|
+
mockFindByCursor.mockReturnValue({
|
|
122
|
+
subscribe: (observer: any) => {
|
|
123
|
+
observer.next([]);
|
|
124
|
+
return { unsubscribe: unsubscribeSpy };
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
useInfiniteScroll(MockEntity as any, defaultOptions);
|
|
129
|
+
|
|
130
|
+
mockEffectCleanups.forEach(cleanup => cleanup());
|
|
131
|
+
mockEffectCleanups = [];
|
|
132
|
+
|
|
133
|
+
expect(unsubscribeSpy).toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle subscription errors gracefully', () => {
|
|
137
|
+
mockFindByCursor.mockReturnValue(throwError(() => new Error('fail')));
|
|
138
|
+
|
|
139
|
+
expect(() => {
|
|
140
|
+
useInfiniteScroll(MockEntity as any, defaultOptions);
|
|
141
|
+
}).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should expose refresh function', () => {
|
|
145
|
+
mockFindByCursor.mockReturnValue(of([]));
|
|
146
|
+
|
|
147
|
+
const resource = useInfiniteScroll(MockEntity as any, defaultOptions);
|
|
148
|
+
|
|
149
|
+
expect(typeof resource.refresh).toBe('function');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should expose loadMore function', () => {
|
|
153
|
+
mockFindByCursor.mockReturnValue(of([]));
|
|
154
|
+
|
|
155
|
+
const resource = useInfiniteScroll(MockEntity as any, defaultOptions);
|
|
156
|
+
|
|
157
|
+
expect(typeof resource.loadMore).toBe('function');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { EntityStaticType, EntityType } from '@aiao/rxdb';
|
|
2
|
+
import { cloneDeep, isFunction } from '@aiao/utils';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useRxDB } from './rxdb-react';
|
|
5
|
+
|
|
6
|
+
type UseOptions<T> = T | (() => T);
|
|
7
|
+
|
|
8
|
+
export interface InfiniteScrollResource<T> {
|
|
9
|
+
value: T[];
|
|
10
|
+
isEmpty: boolean;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
hasMore: boolean;
|
|
13
|
+
loadMore: () => void;
|
|
14
|
+
refresh: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Infinite scroll hook for cursor-based pagination
|
|
19
|
+
*
|
|
20
|
+
* @param EntityType The entity class
|
|
21
|
+
* @param options Query options with cursor pagination
|
|
22
|
+
* @returns A resource object with infinite scroll controls
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const resource = useInfiniteScroll(Todo, {
|
|
27
|
+
* where: { completed: false },
|
|
28
|
+
* orderBy: [{ field: 'createdAt', sort: 'desc' }],
|
|
29
|
+
* limit: 50
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useInfiniteScroll<T extends EntityType>(
|
|
34
|
+
EntityType: T,
|
|
35
|
+
options: UseOptions<EntityStaticType<T, 'findByCursorOptions'>>
|
|
36
|
+
): InfiniteScrollResource<InstanceType<T>> {
|
|
37
|
+
const rxdb = useRxDB();
|
|
38
|
+
|
|
39
|
+
const [pages, setPages] = useState<InstanceType<T>[][]>([]);
|
|
40
|
+
const pagesRef = useRef<InstanceType<T>[][]>([]);
|
|
41
|
+
const setPagesState = useCallback(
|
|
42
|
+
(next: InstanceType<T>[][] | ((prev: InstanceType<T>[][]) => InstanceType<T>[][])) => {
|
|
43
|
+
const resolved =
|
|
44
|
+
typeof next === 'function' ?
|
|
45
|
+
(next as (prev: InstanceType<T>[][]) => InstanceType<T>[][])(pagesRef.current)
|
|
46
|
+
: next;
|
|
47
|
+
pagesRef.current = resolved;
|
|
48
|
+
setPages(resolved);
|
|
49
|
+
},
|
|
50
|
+
[]
|
|
51
|
+
);
|
|
52
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
53
|
+
const isLoadingRef = useRef(false);
|
|
54
|
+
const setIsLoadingState = useCallback((value: boolean) => {
|
|
55
|
+
isLoadingRef.current = value;
|
|
56
|
+
setIsLoading(value);
|
|
57
|
+
}, []);
|
|
58
|
+
const [hasMore, setHasMore] = useState(true);
|
|
59
|
+
const hasMoreRef = useRef(true);
|
|
60
|
+
const setHasMoreState = useCallback((value: boolean) => {
|
|
61
|
+
hasMoreRef.current = value;
|
|
62
|
+
setHasMore(value);
|
|
63
|
+
}, []);
|
|
64
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
65
|
+
const isInitializedRef = useRef(false);
|
|
66
|
+
const isMounted = useRef(false);
|
|
67
|
+
const subscriptions = useRef<{ unsubscribe: () => void }[]>([]);
|
|
68
|
+
|
|
69
|
+
const resolvedOptions = useMemo(() => {
|
|
70
|
+
return isFunction(options) ? options() : options;
|
|
71
|
+
}, [options]);
|
|
72
|
+
|
|
73
|
+
const optionsKey = useMemo(() => JSON.stringify(resolvedOptions), [resolvedOptions]);
|
|
74
|
+
|
|
75
|
+
const allItems = useMemo(() => pages.flat(), [pages]);
|
|
76
|
+
const isEmpty = useMemo(
|
|
77
|
+
() => allItems.length === 0 && !isLoading && isInitialized,
|
|
78
|
+
[allItems.length, isLoading, isInitialized]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const clearSubscriptions = useCallback(() => {
|
|
82
|
+
subscriptions.current.forEach(sub => sub.unsubscribe());
|
|
83
|
+
subscriptions.current = [];
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const loadMore = useCallback(() => {
|
|
87
|
+
if (isLoadingRef.current || !hasMoreRef.current || !isMounted.current) return;
|
|
88
|
+
|
|
89
|
+
setIsLoadingState(true);
|
|
90
|
+
|
|
91
|
+
const currentPages = pagesRef.current;
|
|
92
|
+
const currentIndex = currentPages.length;
|
|
93
|
+
const lastPage = currentPages.length > 0 ? currentPages[currentPages.length - 1] : undefined;
|
|
94
|
+
const lastEntity = lastPage && lastPage.length > 0 ? lastPage[lastPage.length - 1] : undefined;
|
|
95
|
+
|
|
96
|
+
const queryOptions = cloneDeep(resolvedOptions);
|
|
97
|
+
if (lastEntity) {
|
|
98
|
+
queryOptions.after = lastEntity;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const repository = rxdb.entityManager.getRepository(EntityType);
|
|
102
|
+
|
|
103
|
+
const subscription = repository.findByCursor(queryOptions).subscribe({
|
|
104
|
+
next: (result: InstanceType<T>[]) => {
|
|
105
|
+
if (!isMounted.current) return;
|
|
106
|
+
|
|
107
|
+
setPagesState(prev => {
|
|
108
|
+
const updated = [...prev];
|
|
109
|
+
updated[currentIndex] = result;
|
|
110
|
+
return updated;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const limit = queryOptions.limit || 100;
|
|
114
|
+
if (result.length < limit) {
|
|
115
|
+
setHasMoreState(false);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setIsLoadingState(false);
|
|
119
|
+
},
|
|
120
|
+
error: () => {
|
|
121
|
+
if (!isMounted.current) return;
|
|
122
|
+
setIsLoadingState(false);
|
|
123
|
+
},
|
|
124
|
+
complete: () => {
|
|
125
|
+
if (!isMounted.current) return;
|
|
126
|
+
setIsLoadingState(false);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
subscriptions.current.push(subscription);
|
|
131
|
+
}, [EntityType, resolvedOptions, rxdb, setHasMoreState, setIsLoadingState, setPagesState]);
|
|
132
|
+
|
|
133
|
+
const refresh = useCallback(() => {
|
|
134
|
+
if (!isMounted.current) {
|
|
135
|
+
console.warn('Cannot refresh: component is unmounted');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
clearSubscriptions();
|
|
139
|
+
setPagesState([]);
|
|
140
|
+
setIsLoadingState(false);
|
|
141
|
+
setHasMoreState(true);
|
|
142
|
+
loadMore();
|
|
143
|
+
}, [clearSubscriptions, loadMore, setHasMoreState, setIsLoadingState, setPagesState]);
|
|
144
|
+
|
|
145
|
+
// Initial load
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
isMounted.current = true;
|
|
148
|
+
if (!isInitializedRef.current) {
|
|
149
|
+
isInitializedRef.current = true;
|
|
150
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- 初始化标记仅在首次 mount 时设置一次
|
|
151
|
+
setIsInitialized(true);
|
|
152
|
+
|
|
153
|
+
loadMore();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
isMounted.current = false;
|
|
158
|
+
clearSubscriptions();
|
|
159
|
+
};
|
|
160
|
+
}, [clearSubscriptions, loadMore]);
|
|
161
|
+
|
|
162
|
+
// Reload on options change
|
|
163
|
+
const prevOptionsKey = useRef(optionsKey);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!isInitializedRef.current) return;
|
|
166
|
+
|
|
167
|
+
if (prevOptionsKey.current !== optionsKey) {
|
|
168
|
+
prevOptionsKey.current = optionsKey;
|
|
169
|
+
clearSubscriptions();
|
|
170
|
+
pagesRef.current = [];
|
|
171
|
+
isLoadingRef.current = false;
|
|
172
|
+
hasMoreRef.current = true;
|
|
173
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
174
|
+
setPages([]);
|
|
175
|
+
setIsLoading(false);
|
|
176
|
+
setHasMore(true);
|
|
177
|
+
loadMore();
|
|
178
|
+
}
|
|
179
|
+
}, [optionsKey, clearSubscriptions, loadMore]);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
value: allItems,
|
|
183
|
+
isEmpty,
|
|
184
|
+
isLoading,
|
|
185
|
+
hasMore,
|
|
186
|
+
loadMore,
|
|
187
|
+
refresh
|
|
188
|
+
};
|
|
189
|
+
}
|
package/vite.config.mts
CHANGED
|
@@ -72,9 +72,11 @@ export default defineConfig({
|
|
|
72
72
|
coverage: {
|
|
73
73
|
enabled: true,
|
|
74
74
|
reportsDirectory: '../../coverage/packages/rxdb-react',
|
|
75
|
-
provider: 'v8',
|
|
75
|
+
provider: 'v8' as const,
|
|
76
|
+
reporter: ['text', 'json', 'clover', 'lcovonly'],
|
|
76
77
|
include: ['src/**/*'],
|
|
77
|
-
exclude: ['**/index.ts', '**/dist/**']
|
|
78
|
+
exclude: ['**/index.ts', '**/dist/**'],
|
|
79
|
+
all: false
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
});
|