@aiao/rxdb-plugin-search-react 0.0.20 → 0.0.21
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/REVIEW.md +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -16
- package/dist/use-search.d.ts +1 -1
- package/dist/use-search.d.ts.map +1 -1
- package/out-tsc/vitest/src/use-search.spec.d.ts +2 -0
- package/out-tsc/vitest/src/use-search.spec.d.ts.map +1 -0
- package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -0
- package/out-tsc/vitest/vite.config.d.mts +3 -0
- package/out-tsc/vitest/vite.config.d.mts.map +1 -0
- package/package.json +4 -4
- package/src/index.spec.ts +18 -0
- package/src/index.ts +7 -0
- package/src/use-search.spec.ts +73 -0
- package/src/use-search.ts +48 -34
- package/tsconfig.json +3 -0
- package/tsconfig.spec.json +2 -1
- package/vite.config.mts +8 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite.config.d.mts","sourceRoot":"","sources":["../../vite.config.mts"],"names":[],"mappings":";AAOA,wBA+CI"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiao/rxdb-plugin-search-react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@aiao/rxdb-plugin-search": "0.0.
|
|
18
|
+
"@aiao/rxdb-plugin-search": "0.0.21"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@aiao/rxdb": "*",
|
|
22
|
-
"react": "
|
|
22
|
+
"react": "^19.2.7",
|
|
23
23
|
"rxjs": "^7.8.2",
|
|
24
|
-
"@aiao/rxdb-plugin-search": "0.0.
|
|
24
|
+
"@aiao/rxdb-plugin-search": "0.0.21"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { SearchExecutionError, SearchHandle, SearchOptions, SearchResult, SearchState } from './index.js';
|
|
4
|
+
|
|
5
|
+
type PublicSearchApi = {
|
|
6
|
+
error: SearchExecutionError | undefined;
|
|
7
|
+
handle: SearchHandle;
|
|
8
|
+
options: SearchOptions;
|
|
9
|
+
result: SearchResult;
|
|
10
|
+
state: SearchState;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('React public search API', () => {
|
|
14
|
+
it('exposes the core search types from the package entrypoint', () => {
|
|
15
|
+
const contract: PublicSearchApi | undefined = undefined;
|
|
16
|
+
expect(contract).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -8,5 +8,12 @@
|
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
export type {
|
|
12
|
+
SearchExecutionError,
|
|
13
|
+
SearchHandle,
|
|
14
|
+
SearchOptions,
|
|
15
|
+
SearchResult,
|
|
16
|
+
SearchState
|
|
17
|
+
} from '@aiao/rxdb-plugin-search';
|
|
11
18
|
export { useSearch } from './use-search.js';
|
|
12
19
|
export type { SearchSourceLike, UseSearchReturn } from './use-search.js';
|
package/src/use-search.spec.ts
CHANGED
|
@@ -148,4 +148,77 @@ describe('useSearch (React binding, T039)', () => {
|
|
|
148
148
|
renderHook(() => useSearch(src, { initialQuery: 'hi' }));
|
|
149
149
|
expect(src.searchSpy).toHaveBeenCalledWith('hi', { initialQuery: 'hi' });
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it('rebuilds the handle when source changes', () => {
|
|
153
|
+
vi.useFakeTimers();
|
|
154
|
+
const first = makeHandle();
|
|
155
|
+
const second = makeHandle();
|
|
156
|
+
const firstSource = makeSource(first.handle);
|
|
157
|
+
const secondSource = makeSource(second.handle);
|
|
158
|
+
const { result, rerender } = renderHook(({ source }) => useSearch(source), {
|
|
159
|
+
initialProps: { source: firstSource as SearchSourceLike }
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
rerender({ source: secondSource });
|
|
163
|
+
act(() => vi.runAllTimers());
|
|
164
|
+
act(() => result.current.setQuery('next'));
|
|
165
|
+
|
|
166
|
+
expect(first.destroyed.value).toBe(true);
|
|
167
|
+
expect(first.setQuery).not.toHaveBeenCalled();
|
|
168
|
+
expect(secondSource.searchSpy).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(second.setQuery).toHaveBeenCalledWith('next');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('does not rebuild for a semantically identical inline options object', () => {
|
|
173
|
+
vi.useFakeTimers();
|
|
174
|
+
const h = makeHandle();
|
|
175
|
+
const src = makeSource(h.handle);
|
|
176
|
+
const { rerender } = renderHook(() => useSearch(src, { debounce: 10, collections: ['Todo'] }));
|
|
177
|
+
|
|
178
|
+
rerender();
|
|
179
|
+
act(() => vi.runAllTimers());
|
|
180
|
+
|
|
181
|
+
expect(src.searchSpy).toHaveBeenCalledTimes(1);
|
|
182
|
+
expect(h.destroyed.value).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('routes calls to the latest handle even via a callback captured before rebuild', () => {
|
|
186
|
+
vi.useFakeTimers();
|
|
187
|
+
const first = makeHandle();
|
|
188
|
+
const second = makeHandle();
|
|
189
|
+
const firstSource = makeSource(first.handle);
|
|
190
|
+
const secondSource = makeSource(second.handle);
|
|
191
|
+
const { result, rerender } = renderHook(({ source }) => useSearch(source), {
|
|
192
|
+
initialProps: { source: firstSource as SearchSourceLike }
|
|
193
|
+
});
|
|
194
|
+
const staleSetQuery = result.current.setQuery;
|
|
195
|
+
|
|
196
|
+
rerender({ source: secondSource });
|
|
197
|
+
act(() => vi.runAllTimers());
|
|
198
|
+
|
|
199
|
+
act(() => staleSetQuery('after-rebuild'));
|
|
200
|
+
|
|
201
|
+
expect(second.setQuery).toHaveBeenCalledWith('after-rebuild');
|
|
202
|
+
expect(first.setQuery).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('keeps stable setQuery/clear/retry/loadMore identities across handle rebuilds', () => {
|
|
206
|
+
vi.useFakeTimers();
|
|
207
|
+
const first = makeHandle();
|
|
208
|
+
const second = makeHandle();
|
|
209
|
+
const firstSource = makeSource(first.handle);
|
|
210
|
+
const secondSource = makeSource(second.handle);
|
|
211
|
+
const { result, rerender } = renderHook(({ source }) => useSearch(source), {
|
|
212
|
+
initialProps: { source: firstSource as SearchSourceLike }
|
|
213
|
+
});
|
|
214
|
+
const { setQuery, clear, retry, loadMore } = result.current;
|
|
215
|
+
|
|
216
|
+
rerender({ source: secondSource });
|
|
217
|
+
act(() => vi.runAllTimers());
|
|
218
|
+
|
|
219
|
+
expect(result.current.setQuery).toBe(setQuery);
|
|
220
|
+
expect(result.current.clear).toBe(clear);
|
|
221
|
+
expect(result.current.retry).toBe(retry);
|
|
222
|
+
expect(result.current.loadMore).toBe(loadMore);
|
|
223
|
+
});
|
|
151
224
|
});
|
package/src/use-search.ts
CHANGED
|
@@ -49,6 +49,30 @@ export interface UseSearchReturn {
|
|
|
49
49
|
retry: () => void;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function collectionsEqual(left: readonly string[] | undefined, right: readonly string[] | undefined): boolean {
|
|
53
|
+
if (left === right) return true;
|
|
54
|
+
if (left === undefined || right === undefined || left.length !== right.length) return false;
|
|
55
|
+
return left.every((value, index) => value === right[index]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function searchOptionsEqual(left: SearchOptions | undefined, right: SearchOptions | undefined): boolean {
|
|
59
|
+
if (left === right) return true;
|
|
60
|
+
if (left === undefined || right === undefined) return false;
|
|
61
|
+
return (
|
|
62
|
+
left.debounce === right.debounce &&
|
|
63
|
+
left.pageSize === right.pageSize &&
|
|
64
|
+
left.snippetLength === right.snippetLength &&
|
|
65
|
+
left.initialQuery === right.initialQuery &&
|
|
66
|
+
collectionsEqual(left.collections, right.collections)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function useStableSearchOptions(options: SearchOptions | undefined): SearchOptions | undefined {
|
|
71
|
+
const stableOptions = useRef(options);
|
|
72
|
+
if (!searchOptionsEqual(stableOptions.current, options)) stableOptions.current = options;
|
|
73
|
+
return stableOptions.current;
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
/**
|
|
53
77
|
* 将 `SearchHandle` 的响应式输出适配为 React hook。
|
|
54
78
|
*
|
|
@@ -63,15 +87,10 @@ export interface UseSearchReturn {
|
|
|
63
87
|
* @public
|
|
64
88
|
*/
|
|
65
89
|
export function useSearch(source: SearchSourceLike, options?: SearchOptions): UseSearchReturn {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const handleRef = useRef<SearchHandle | null>(null);
|
|
69
|
-
const destroyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
70
|
-
if (handleRef.current === null) {
|
|
71
|
-
handleRef.current = source.search(initial, options);
|
|
72
|
-
}
|
|
73
|
-
const handle = handleRef.current;
|
|
90
|
+
const stableOptions = useStableSearchOptions(options);
|
|
91
|
+
const initial = stableOptions?.initialQuery ?? '';
|
|
74
92
|
|
|
93
|
+
const handleRef = useRef<SearchHandle | null>(null);
|
|
75
94
|
const [query, setQueryState] = useState<string>(initial);
|
|
76
95
|
const [results, setResults] = useState<readonly SearchResult[]>([]);
|
|
77
96
|
const [state, setState] = useState<SearchState>('idle');
|
|
@@ -79,42 +98,37 @@ export function useSearch(source: SearchSourceLike, options?: SearchOptions): Us
|
|
|
79
98
|
const [hasMore, setHasMore] = useState<boolean>(false);
|
|
80
99
|
|
|
81
100
|
useEffect(() => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
// handle 是命令式控制器,不参与渲染,存入 ref 而非 state:
|
|
102
|
+
// 避免 handle 变化触发多余的重渲染,并让 setQuery/clear/retry/loadMore
|
|
103
|
+
// 始终读取最新 handle,不会在 rebuild 后仍调用到已销毁的旧实例。
|
|
104
|
+
const newHandle = source.search(initial, stableOptions);
|
|
105
|
+
handleRef.current = newHandle;
|
|
106
|
+
setQueryState(initial);
|
|
86
107
|
|
|
87
108
|
const subs = new Subscription();
|
|
88
|
-
subs.add(
|
|
89
|
-
subs.add(
|
|
90
|
-
subs.add(
|
|
91
|
-
subs.add(
|
|
109
|
+
subs.add(newHandle.results$.subscribe(r => setResults(r)));
|
|
110
|
+
subs.add(newHandle.state$.subscribe(s => setState(s)));
|
|
111
|
+
subs.add(newHandle.error$.subscribe(e => setError(e)));
|
|
112
|
+
subs.add(newHandle.hasMore$.subscribe(h => setHasMore(h)));
|
|
92
113
|
return () => {
|
|
93
114
|
subs.unsubscribe();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
handleRef.current = null;
|
|
97
|
-
destroyTimerRef.current = null;
|
|
98
|
-
}, 0);
|
|
115
|
+
newHandle.destroy();
|
|
116
|
+
if (handleRef.current === newHandle) handleRef.current = null;
|
|
99
117
|
};
|
|
100
|
-
|
|
101
|
-
}, []);
|
|
118
|
+
}, [source, stableOptions, initial]);
|
|
102
119
|
|
|
103
|
-
const setQuery = useCallback(
|
|
104
|
-
(q
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
},
|
|
108
|
-
[handle]
|
|
109
|
-
);
|
|
120
|
+
const setQuery = useCallback((q: string) => {
|
|
121
|
+
setQueryState(q);
|
|
122
|
+
handleRef.current?.setQuery(q);
|
|
123
|
+
}, []);
|
|
110
124
|
|
|
111
125
|
const clear = useCallback(() => {
|
|
112
126
|
setQueryState('');
|
|
113
|
-
|
|
114
|
-
}, [
|
|
127
|
+
handleRef.current?.clear();
|
|
128
|
+
}, []);
|
|
115
129
|
|
|
116
|
-
const retry = useCallback(() =>
|
|
117
|
-
const loadMore = useCallback(() =>
|
|
130
|
+
const retry = useCallback(() => handleRef.current?.retry(), []);
|
|
131
|
+
const loadMore = useCallback(() => handleRef.current?.loadMore() ?? Promise.resolve(), []);
|
|
118
132
|
|
|
119
133
|
return useMemo(
|
|
120
134
|
() => ({ query, setQuery, results, state, error, hasMore, loadMore, clear, retry }),
|
package/tsconfig.json
CHANGED
package/tsconfig.spec.json
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"],
|
|
5
5
|
"jsx": "react-jsx",
|
|
6
6
|
"module": "esnext",
|
|
7
|
-
"moduleResolution": "bundler"
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"rootDir": "."
|
|
8
9
|
},
|
|
9
10
|
"extends": "../../tsconfig.base.json",
|
|
10
11
|
"include": [
|
package/vite.config.mts
CHANGED
|
@@ -10,7 +10,11 @@ export default defineConfig(() => ({
|
|
|
10
10
|
cacheDir: '../../node_modules/.vite/packages/rxdb-plugin-search-react',
|
|
11
11
|
plugins: [
|
|
12
12
|
react(),
|
|
13
|
-
dts({
|
|
13
|
+
dts({
|
|
14
|
+
entryRoot: 'src',
|
|
15
|
+
pathsToAliases: false,
|
|
16
|
+
tsconfigPath: path.join(import.meta.dirname, 'tsconfig.lib.json')
|
|
17
|
+
}),
|
|
14
18
|
...(process.env.CI === 'true' && process.env.CODECOV_TOKEN ?
|
|
15
19
|
[
|
|
16
20
|
codecovVitePlugin({
|
|
@@ -35,7 +39,9 @@ export default defineConfig(() => ({
|
|
|
35
39
|
fileName: 'index',
|
|
36
40
|
formats: ['es' as const]
|
|
37
41
|
},
|
|
38
|
-
|
|
42
|
+
rolldownOptions: {
|
|
43
|
+
// dts 插件生成声明文件天然比 Rolldown 原生链接阶段慢,抑制误报的 PLUGIN_TIMINGS 警告
|
|
44
|
+
checks: { pluginTimings: false },
|
|
39
45
|
external: [/^@aiao\//, 'react', 'react-dom', 'react/jsx-runtime', 'rxjs']
|
|
40
46
|
}
|
|
41
47
|
},
|
|
@@ -45,7 +51,6 @@ export default defineConfig(() => ({
|
|
|
45
51
|
globals: true,
|
|
46
52
|
environment: 'jsdom',
|
|
47
53
|
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
48
|
-
passWithNoTests: true,
|
|
49
54
|
reporters: ['default'],
|
|
50
55
|
coverage: {
|
|
51
56
|
reportsDirectory: './test-output/vitest/coverage',
|