muya 2.4.3 → 2.4.5

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.
@@ -1,13 +1,22 @@
1
- import { useCallback, useDebugValue, useEffect, useId, useLayoutEffect, useMemo, type DependencyList } from 'react'
1
+ /* eslint-disable sonarjs/cognitive-complexity */
2
+ import { useCallback, useLayoutEffect, useReducer, useRef, type DependencyList } from 'react'
2
3
  import type { SyncTable } from './create-sqlite'
3
- import type { DocType } from './table/table.types'
4
- import { isError, isPromise } from '../utils/is'
5
- import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
6
- import type { SqlSeachOptions } from './select-sql'
4
+ import type { DocType, Key, SqlSeachOptions } from './table/table.types'
5
+ import { DEFAULT_PAGE_SIZE } from './table'
6
+ const MAX_ITERATIONS = 10_000
7
7
 
8
8
  export interface SqLiteActions {
9
- readonly next: () => Promise<boolean>
9
+ /**
10
+ * Load the next page of results and return if isDone to show more results.
11
+ * @returns isDone: boolean
12
+ */
13
+ readonly nextPage: () => Promise<boolean>
14
+ /**
15
+ * Reset the pagination and load the first page of results.
16
+ * @returns void
17
+ */
10
18
  readonly reset: () => Promise<void>
19
+ readonly keysIndex: Map<Key, number>
11
20
  }
12
21
 
13
22
  export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
@@ -18,101 +27,160 @@ export interface UseSearchOptions<Document extends DocType, Selected = Document>
18
27
  }
19
28
 
20
29
  /**
21
- * Generate a cache key based on the search options to uniquely identify the query
22
- * @param options The search options to generate the key from
23
- * @returns A string representing the unique cache key for the given search options
24
- */
25
- function generateCacheKey(options: UseSearchOptions<DocType, unknown>): string {
26
- const { limit, offset, order, sortBy, where, stepSize, select } = options
27
- let key = ''
28
- if (limit !== undefined) {
29
- key += `l${limit}`
30
- }
31
- if (offset !== undefined) {
32
- key += `o${offset}`
33
- }
34
- if (order !== undefined) {
35
- key += `r${order}`
36
- }
37
- if (sortBy !== undefined) {
38
- key += `s${sortBy}`
39
- }
40
- if (where !== undefined) {
41
- key += `w${JSON.stringify(where)}`
42
- }
43
- if (stepSize !== undefined) {
44
- key += `t${stepSize}`
45
- }
46
- if (select !== undefined) {
47
- key += `f${select.toString()}`
48
- }
49
- return key
50
- }
51
-
52
- /**
53
- * React hook to subscribe to a SyncTable and get its current snapshot, with optional search options and selector for derived state
54
- * @param state The SyncTable to subscribe to
55
- * @param options Optional search options to filter and sort the documents
56
- * @param deps Dependency list to control when to update the search options
57
- * @returns A tuple containing the current array of documents (or selected documents) and an object with actions to interact with the SyncTable
58
- * @throws If the value is a Promise or an Error, it will be thrown to be handled by an error boundary or suspense
30
+ * A React hook to perform paginated searches on a SyncTable and reactively update the results.
31
+ * It supports pagination, resetting the search, and selecting specific fields from the documents.
32
+ * @param state The SyncTable instance to perform searches on.
33
+ * @param options Options to customize the search behavior, including pagination size and selection function.
34
+ * @param deps Dependency list to control when to re-run the search and reset the iterator.
35
+ * @returns A tuple containing the current list of results and an object with actions to manage pagination and resetting.
59
36
  */
60
37
  export function useSqliteValue<Document extends DocType, Selected = Document>(
61
38
  state: SyncTable<Document>,
62
39
  options: UseSearchOptions<Document, Selected> = {},
63
40
  deps: DependencyList = [],
64
- ): [undefined extends Selected ? Document[] : Selected[], SqLiteActions] {
65
- const { select } = options
41
+ ): [(undefined extends Selected ? Document[] : Selected[]) | undefined, SqLiteActions] {
42
+ const { select, pageSize = DEFAULT_PAGE_SIZE } = options
66
43
 
67
- const searchId = useMemo(() => generateCacheKey({ ...options, select: undefined }), [options])
68
- const componentId = useId()
44
+ const itemsRef = useRef<undefined | (Document | Selected)[]>()
45
+ const [, rerender] = useReducer((c: number) => c + 1, 0)
46
+ const keysIndex = useRef(new Map<Key, number>())
47
+ const iteratorRef = useRef<AsyncIterableIterator<{ doc: Document; meta: { key: Key } }>>()
69
48
 
70
- useLayoutEffect(() => {
71
- state.updateSearchOptions(searchId, { ...options, select: undefined })
49
+ const updateIterator = useCallback(() => {
50
+ // eslint-disable-next-line sonarjs/no-unused-vars
51
+ const { select: _ignore, ...resetOptions } = options
52
+ iteratorRef.current = state.search({ select: (doc, meta) => ({ doc, meta }), ...resetOptions })
72
53
  // eslint-disable-next-line react-hooks/exhaustive-deps
73
- }, deps)
54
+ }, [state, ...deps])
74
55
 
75
- useEffect(() => {
76
- return () => {
77
- state.clear(searchId)
56
+ const reset = useCallback(() => {
57
+ itemsRef.current = []
58
+ keysIndex.current.clear()
59
+ updateIterator()
60
+ }, [updateIterator])
61
+
62
+ const fillNextPage = useCallback(async (shouldReset: boolean) => {
63
+ if (itemsRef.current === undefined) {
64
+ itemsRef.current = []
65
+ }
66
+ if (shouldReset === true) {
67
+ reset()
68
+ }
69
+
70
+ const { current: iterator } = iteratorRef
71
+ if (!iterator) {
72
+ return true
78
73
  }
74
+ let isDone = false
75
+ for (let index = 0; index < pageSize; index++) {
76
+ const result = await iterator.next()
77
+ if (result.done) {
78
+ iteratorRef.current = undefined
79
+ isDone = true
80
+ break
81
+ }
82
+ if (keysIndex.current.has(result.value.meta.key)) {
83
+ // eslint-disable-next-line sonarjs/updated-loop-counter
84
+ index += -1
85
+ continue
86
+ }
87
+ itemsRef.current.push(select ? select(result.value.doc) : (result.value.doc as unknown as Selected))
88
+ keysIndex.current.set(result.value.meta.key, itemsRef.current.length - 1)
89
+ }
90
+ return isDone
79
91
  // eslint-disable-next-line react-hooks/exhaustive-deps
80
92
  }, [])
81
93
 
82
- const selector = useCallback(
83
- (documents: Document[]) => {
84
- // eslint-disable-next-line unicorn/no-array-callback-reference
85
- return select ? documents.map(select) : (documents as unknown as Selected[])
86
- },
87
- [select],
88
- )
89
-
90
- const subscribe = useCallback(
91
- (onStorageChange: () => void) => {
92
- return state.subscribe(searchId, componentId, onStorageChange)
93
- },
94
- [state, searchId, componentId],
95
- )
96
-
97
- const getSnapshot = useCallback(() => {
98
- return state.getSnapshot(searchId)
99
- }, [state, searchId])
100
-
101
- const value = useSyncExternalStoreWithSelector<Document[], Selected[]>(subscribe, getSnapshot, getSnapshot, selector)
102
-
103
- useDebugValue(value)
104
- if (isPromise(value)) {
105
- throw value
106
- }
107
- if (isError(value)) {
108
- throw value
109
- }
110
-
111
- const actions = useMemo((): SqLiteActions => {
112
- return {
113
- next: () => state.next(searchId),
114
- reset: () => state.refresh(searchId),
94
+ const nextPage = useCallback(async () => {
95
+ const isDone = await fillNextPage(false)
96
+ rerender()
97
+ return isDone
98
+ }, [fillNextPage])
99
+
100
+ useLayoutEffect(() => {
101
+ const unsubscribe = state.subscribe(async (item) => {
102
+ const { mutations, removedAll } = item
103
+ if (removedAll) {
104
+ reset()
105
+ }
106
+ if (!mutations) {
107
+ return
108
+ }
109
+
110
+ const oldLength = itemsRef.current?.length ?? 0
111
+ let newLength = oldLength
112
+ let hasUpdate = false
113
+ for (const mutation of mutations) {
114
+ const { key, op } = mutation
115
+ switch (op) {
116
+ case 'insert': {
117
+ newLength += 1
118
+ break
119
+ }
120
+ case 'delete': {
121
+ if (itemsRef.current && keysIndex.current.has(key)) {
122
+ const index = keysIndex.current.get(key)
123
+ if (index === undefined) break
124
+ itemsRef.current.splice(index, 1)
125
+ keysIndex.current.delete(key)
126
+ hasUpdate = true
127
+ }
128
+ break
129
+ }
130
+ case 'update': {
131
+ if (itemsRef.current && keysIndex.current.has(key)) {
132
+ const index = keysIndex.current.get(key)
133
+ if (index === undefined) break
134
+ itemsRef.current[index] = (await state.get(key, select)) as Selected
135
+ hasUpdate = true
136
+ }
137
+ break
138
+ }
139
+ }
140
+ }
141
+
142
+ const isLengthChanged = oldLength !== newLength
143
+ const isChanged = isLengthChanged || hasUpdate
144
+ if (!isChanged) return
145
+ if (isLengthChanged) {
146
+ await fillNextPage(true)
147
+
148
+ // here we ensure that if the length changed, we fill the next page
149
+
150
+ let iterations = 0
151
+ while ((itemsRef.current?.length ?? 0) < newLength && iterations < MAX_ITERATIONS) {
152
+ await fillNextPage(false)
153
+ iterations++
154
+ }
155
+ if (iterations === MAX_ITERATIONS) {
156
+ // Optionally log a warning to help with debugging
157
+ // eslint-disable-next-line no-console
158
+ console.warn('Reached maximum iterations in fillNextPage loop. Possible duplicate or data issue.')
159
+ }
160
+ }
161
+ rerender()
162
+ })
163
+ return () => {
164
+ unsubscribe()
115
165
  }
116
- }, [searchId, state])
117
- return [value as undefined extends Selected ? Document[] : Selected[], actions]
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ }, [state])
168
+
169
+ useLayoutEffect(() => {
170
+ updateIterator()
171
+ itemsRef.current = undefined
172
+ keysIndex.current.clear()
173
+ nextPage()
174
+ // eslint-disable-next-line react-hooks/exhaustive-deps
175
+ }, deps)
176
+
177
+ const resetCb = useCallback(async () => {
178
+ reset()
179
+ await nextPage()
180
+ }, [nextPage, reset])
181
+
182
+ return [itemsRef.current, { nextPage, reset: resetCb, keysIndex: keysIndex.current }] as [
183
+ (undefined extends Selected ? Document[] : Selected[]) | undefined,
184
+ SqLiteActions,
185
+ ]
118
186
  }
@@ -4,7 +4,7 @@ export declare const RESCHEDULE_COUNT = 0;
4
4
  type ScheduleId = string | number | symbol;
5
5
  export interface SchedulerOptions<T> {
6
6
  readonly onResolveItem?: (item: T) => void;
7
- readonly onScheduleDone: () => void | Promise<void>;
7
+ readonly onScheduleDone: (values?: unknown[]) => void | Promise<void>;
8
8
  }
9
9
  /**
10
10
  * A simple scheduler to batch updates and avoid blocking the main thread
@@ -1,16 +1,15 @@
1
- import { type CreateState } from './select-sql';
2
1
  import type { Backend } from './table';
3
2
  import type { DbOptions, DocType, Key, MutationResult, SearchOptions } from './table/table.types';
4
3
  import type { Where } from './table/where';
5
- type SearchId = string;
6
4
  export interface CreateSqliteOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
7
5
  readonly backend: Backend | Promise<Backend>;
8
6
  }
7
+ export interface MutationItems {
8
+ mutations?: MutationResult[];
9
+ removedAll?: boolean;
10
+ }
9
11
  export interface SyncTable<Document extends DocType> {
10
- readonly updateSearchOptions: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => void;
11
- readonly subscribe: (searchId: SearchId, componentId: string, listener: () => void) => () => void;
12
- readonly getSnapshot: (searchId: SearchId) => Document[];
13
- readonly refresh: (searchId: SearchId) => Promise<void>;
12
+ readonly subscribe: (listener: (mutation: MutationItems) => void) => () => void;
14
13
  readonly set: (document: Document) => Promise<MutationResult>;
15
14
  readonly batchSet: (documents: Document[]) => Promise<MutationResult[]>;
16
15
  readonly get: <Selected = Document>(key: Key, selector?: (document: Document) => Selected) => Promise<Selected | undefined>;
@@ -20,10 +19,7 @@ export interface SyncTable<Document extends DocType> {
20
19
  where?: Where<Document>;
21
20
  }) => Promise<number>;
22
21
  readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>;
23
- readonly destroy: () => void;
24
- readonly next: (searchId: SearchId) => Promise<boolean>;
25
- readonly clear: (searchId: SearchId) => void;
26
- readonly select: <Params extends unknown[]>(compute: (...args: Params) => SearchOptions<Document>) => CreateState<Document, Params>;
22
+ readonly clear: () => void;
27
23
  }
28
24
  /**
29
25
  * Create a SyncTable that wraps a Table and provides reactive capabilities
@@ -31,4 +27,3 @@ export interface SyncTable<Document extends DocType> {
31
27
  * @returns A SyncTable instance with methods to interact with the underlying Table and manage reactive searches
32
28
  */
33
29
  export declare function createSqliteState<Document extends DocType>(options: CreateSqliteOptions<Document>): SyncTable<Document>;
34
- export {};
@@ -1,4 +1,4 @@
1
1
  export * from './create-sqlite';
2
2
  export * from './table';
3
- export * from './select-sql';
3
+ export * from './use-sqlite-count';
4
4
  export * from './use-sqlite';
@@ -1,5 +1,5 @@
1
1
  import type { Table, DbOptions, DocType } from './table.types';
2
- export declare const DEFAULT_STEP_SIZE = 100;
2
+ export declare const DEFAULT_PAGE_SIZE = 100;
3
3
  /**
4
4
  * Convert a dot-separated path to a JSON path
5
5
  * @param dot The dot-separated path string
@@ -1,4 +1,3 @@
1
- import type { SqlSeachOptions } from '../select-sql';
2
1
  import type { Backend } from './backend';
3
2
  import type { FtsTokenizerOptions } from './tokenizer';
4
3
  import type { Where } from './where';
@@ -6,6 +5,14 @@ export type DocType = {
6
5
  [key: string]: any;
7
6
  };
8
7
  export type KeyTypeAvailable = 'string' | 'number';
8
+ export interface SqlSeachOptions<Document extends DocType> {
9
+ readonly sortBy?: DotPath<Document>;
10
+ readonly order?: 'asc' | 'desc';
11
+ readonly limit?: number;
12
+ readonly offset?: number;
13
+ readonly where?: Where<Document>;
14
+ readonly pageSize?: number;
15
+ }
9
16
  export type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`;
10
17
  type Previous = [never, 0, 1, 2, 3, 4, 5];
11
18
  export type DotPath<T, D extends number = 5> = [D] extends [never] ? never : T extends object ? {
@@ -0,0 +1,17 @@
1
+ import { type DependencyList } from 'react';
2
+ import type { SyncTable } from './create-sqlite';
3
+ import type { DocType } from './table/table.types';
4
+ import type { Where } from './table/where';
5
+ /**
6
+ * A React hook to count the number of items in a SyncTable reactively.
7
+ * It updates the count when items are inserted or deleted, but ignores updates.
8
+ * Supports filtering the count using a `where` clause.
9
+ * @param state The SyncTable instance to observe.
10
+ * @param options Optional filtering options.
11
+ * @param options.where A `where` clause to filter the count.
12
+ * @param deps Dependency list to control when to re-run the effect.
13
+ * @returns The current count of items in the table.
14
+ */
15
+ export declare function useSqliteCount<Document extends DocType>(state: SyncTable<Document>, options?: {
16
+ where?: Where<Document>;
17
+ }, deps?: DependencyList): number;
@@ -1,10 +1,18 @@
1
1
  import { type DependencyList } from 'react';
2
2
  import type { SyncTable } from './create-sqlite';
3
- import type { DocType } from './table/table.types';
4
- import type { SqlSeachOptions } from './select-sql';
3
+ import type { DocType, Key, SqlSeachOptions } from './table/table.types';
5
4
  export interface SqLiteActions {
6
- readonly next: () => Promise<boolean>;
5
+ /**
6
+ * Load the next page of results and return if isDone to show more results.
7
+ * @returns isDone: boolean
8
+ */
9
+ readonly nextPage: () => Promise<boolean>;
10
+ /**
11
+ * Reset the pagination and load the first page of results.
12
+ * @returns void
13
+ */
7
14
  readonly reset: () => Promise<void>;
15
+ readonly keysIndex: Map<Key, number>;
8
16
  }
9
17
  export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
10
18
  /**
@@ -13,11 +21,11 @@ export interface UseSearchOptions<Document extends DocType, Selected = Document>
13
21
  readonly select?: (document: Document) => Selected;
14
22
  }
15
23
  /**
16
- * React hook to subscribe to a SyncTable and get its current snapshot, with optional search options and selector for derived state
17
- * @param state The SyncTable to subscribe to
18
- * @param options Optional search options to filter and sort the documents
19
- * @param deps Dependency list to control when to update the search options
20
- * @returns A tuple containing the current array of documents (or selected documents) and an object with actions to interact with the SyncTable
21
- * @throws If the value is a Promise or an Error, it will be thrown to be handled by an error boundary or suspense
24
+ * A React hook to perform paginated searches on a SyncTable and reactively update the results.
25
+ * It supports pagination, resetting the search, and selecting specific fields from the documents.
26
+ * @param state The SyncTable instance to perform searches on.
27
+ * @param options Options to customize the search behavior, including pagination size and selection function.
28
+ * @param deps Dependency list to control when to re-run the search and reset the iterator.
29
+ * @returns A tuple containing the current list of results and an object with actions to manage pagination and resetting.
22
30
  */
23
- export declare function useSqliteValue<Document extends DocType, Selected = Document>(state: SyncTable<Document>, options?: UseSearchOptions<Document, Selected>, deps?: DependencyList): [undefined extends Selected ? Document[] : Selected[], SqLiteActions];
31
+ export declare function useSqliteValue<Document extends DocType, Selected = Document>(state: SyncTable<Document>, options?: UseSearchOptions<Document, Selected>, deps?: DependencyList): [(undefined extends Selected ? Document[] : Selected[]) | undefined, SqLiteActions];