@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 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
  *
@@ -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":"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"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './hooks';
2
2
  export * from './rxdb-react';
3
+ export * from './useInfiniteScroll';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -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 m } from "@aiao/utils";
2
- import { useState as i, useRef as h, useMemo as F, useEffect as A, createContext as B, useContext as E } from "react";
3
- import { jsx as O } from "react/jsx-runtime";
4
- const s = (r, e, o, c) => {
5
- const [D, R] = i(o), [b, d] = i(void 0), [g, f] = i(!0), [p, l] = i(void 0), [y, v] = i(!1), t = h(void 0), u = h(!0), x = F(() => m(c) ? c() : c, [c]);
6
- return A(() => {
7
- u.current = !0, t.current && (t.current.unsubscribe(), t.current = void 0);
8
- const a = r[e];
9
- if (!a || typeof a != "function") {
10
- const n = new Error(`Method "${String(e)}" not found on EntityType`);
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
- u.current && (d(n), f(!1));
12
+ i.current && (l(t), d(!1));
13
13
  });
14
14
  return;
15
15
  }
16
16
  try {
17
- t.current = a(x).subscribe({
18
- next: (n) => {
19
- u.current && (f(!1), v(!0), d(void 0), Array.isArray(n) ? l(n.length === 0) : l(n == null), R(n));
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: (n) => {
22
- u.current && (f(!1), v(!1), d(n), console.error(`RxDB query error in ${String(e)}:`, n));
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 (n) {
26
- const P = n instanceof Error ? n : new Error(String(n));
25
+ } catch (t) {
26
+ const x = t instanceof Error ? t : new Error(String(t));
27
27
  Promise.resolve().then(() => {
28
- u.current && (f(!1), d(P));
28
+ i.current && (d(!1), l(x));
29
29
  });
30
30
  }
31
31
  return () => {
32
- u.current = !1, t.current && (t.current.unsubscribe(), t.current = void 0);
32
+ i.current = !1, o.current && (o.current.unsubscribe(), o.current = void 0);
33
33
  };
34
- }, [r, e, x]), {
34
+ }, [r, e, M]), {
35
35
  value: D,
36
- error: b,
37
- isLoading: g,
38
- isEmpty: p,
39
- hasValue: y
36
+ error: y,
37
+ isLoading: B,
38
+ isEmpty: u,
39
+ hasValue: I
40
40
  };
41
- }, S = (r, e) => s(r, "get", void 0, e), G = (r, e) => s(r, "findOne", void 0, e), V = (r, e) => s(r, "findOneOrFail", void 0, e), q = (r, e) => s(r, "find", [], e), I = (r, e) => s(r, "findAll", [], e), L = (r, e) => s(r, "count", 0, e), $ = (r, e) => s(r, "findDescendants", [], e), j = (r, e) => s(r, "countDescendants", 0, e), k = (r, e) => s(r, "findAncestors", [], e), H = (r, e) => s(r, "countAncestors", 0, e), Q = (r, e) => s(r, "findNeighbors", [], e), z = (r, e) => s(r, "countNeighbors", 0, e), J = (r, e) => s(r, "findPaths", [], e);
42
- function C() {
43
- const r = B(void 0);
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 o = E(r);
46
+ const c = U(r);
47
47
  if (e !== void 0) return e;
48
- if (!o) throw new Error("No RxDB instance found, use RxDBProvider to provide one");
49
- return o;
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: o }) => /* @__PURE__ */ O(r.Provider, { value: o, children: e })
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
- K as RxDBProvider,
57
- C as makeRxDBProvider,
58
- L as useCount,
59
- H as useCountAncestors,
60
- j as useCountDescendants,
61
- z as useCountNeighbors,
62
- q as useFind,
63
- I as useFindAll,
64
- k as useFindAncestors,
65
- $ as useFindDescendants,
66
- G as useFindOne,
67
- V as useFindOneOrFail,
68
- S as useGet,
69
- Q as useGraphNeighbors,
70
- J as useGraphPaths,
71
- T as useRxDB
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.9",
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.9",
19
- "@aiao/utils": "0.0.9"
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 { useFind, useFindOne, useGet } from './hooks';
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
@@ -1,2 +1,3 @@
1
1
  export * from './hooks';
2
2
  export * from './rxdb-react';
3
+ export * from './useInfiniteScroll';
@@ -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
  });