muya 2.4.4 → 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,21 +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
9
  /**
10
10
  * Load the next page of results and return if isDone to show more results.
11
11
  * @returns isDone: boolean
12
12
  */
13
- readonly next: () => Promise<boolean>
13
+ readonly nextPage: () => Promise<boolean>
14
14
  /**
15
15
  * Reset the pagination and load the first page of results.
16
16
  * @returns void
17
17
  */
18
18
  readonly reset: () => Promise<void>
19
+ readonly keysIndex: Map<Key, number>
19
20
  }
20
21
 
21
22
  export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
@@ -26,102 +27,160 @@ export interface UseSearchOptions<Document extends DocType, Selected = Document>
26
27
  }
27
28
 
28
29
  /**
29
- * Generate a cache key based on the search options to uniquely identify the query
30
- * @param options The search options to generate the key from
31
- * @returns A string representing the unique cache key for the given search options
32
- */
33
- function generateCacheKey(options: UseSearchOptions<DocType, unknown>): string {
34
- const { limit, offset, order, sortBy, where, stepSize, select } = options
35
- let key = ''
36
- if (limit !== undefined) {
37
- key += `l${limit}`
38
- }
39
- if (offset !== undefined) {
40
- key += `o${offset}`
41
- }
42
- if (order !== undefined) {
43
- key += `r${order}`
44
- }
45
- if (sortBy !== undefined) {
46
- key += `s${sortBy}`
47
- }
48
- if (where !== undefined) {
49
- key += `w${JSON.stringify(where)}`
50
- }
51
- if (stepSize !== undefined) {
52
- key += `t${stepSize}`
53
- }
54
- if (select !== undefined) {
55
- key += `f${select.toString()}`
56
- }
57
- return key
58
- }
59
-
60
- /**
61
- * React hook to subscribe to a SyncTable and get its current snapshot, with optional search options and selector for derived state
62
- * @param state The SyncTable to subscribe to
63
- * @param options Optional search options to filter and sort the documents
64
- * @param deps Dependency list to control when to update the search options
65
- * @returns A tuple containing the current array of documents (or selected documents) and an object with actions to interact with the SyncTable
66
- * @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.
67
36
  */
68
37
  export function useSqliteValue<Document extends DocType, Selected = Document>(
69
38
  state: SyncTable<Document>,
70
39
  options: UseSearchOptions<Document, Selected> = {},
71
40
  deps: DependencyList = [],
72
- ): [undefined extends Selected ? Document[] : Selected[], SqLiteActions] {
73
- const { select } = options
41
+ ): [(undefined extends Selected ? Document[] : Selected[]) | undefined, SqLiteActions] {
42
+ const { select, pageSize = DEFAULT_PAGE_SIZE } = options
74
43
 
75
- const searchId = useMemo(() => generateCacheKey({ ...options, select: undefined }), [options])
76
- 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 } }>>()
77
48
 
78
- useLayoutEffect(() => {
79
- 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 })
80
53
  // eslint-disable-next-line react-hooks/exhaustive-deps
81
- }, deps)
54
+ }, [state, ...deps])
82
55
 
83
- useEffect(() => {
84
- // state.load(searchId)
85
- return () => {
86
- 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
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)
87
89
  }
90
+ return isDone
88
91
  // eslint-disable-next-line react-hooks/exhaustive-deps
89
92
  }, [])
90
93
 
91
- const selector = useCallback(
92
- (documents: Document[]) => {
93
- // eslint-disable-next-line unicorn/no-array-callback-reference
94
- return select ? documents.map(select) : (documents as unknown as Selected[])
95
- },
96
- [select],
97
- )
98
-
99
- const subscribe = useCallback(
100
- (onStorageChange: () => void) => {
101
- return state.subscribe(searchId, componentId, onStorageChange)
102
- },
103
- [state, searchId, componentId],
104
- )
105
-
106
- const getSnapshot = useCallback(() => {
107
- return state.getSnapshot(searchId)
108
- }, [state, searchId])
109
-
110
- const value = useSyncExternalStoreWithSelector<Document[], Selected[]>(subscribe, getSnapshot, getSnapshot, selector)
111
-
112
- useDebugValue(value)
113
- if (isPromise(value)) {
114
- throw value
115
- }
116
- if (isError(value)) {
117
- throw value
118
- }
119
-
120
- const actions = useMemo((): SqLiteActions => {
121
- return {
122
- next: () => state.next(searchId),
123
- 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()
124
165
  }
125
- }, [searchId, state])
126
- 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
+ ]
127
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,11 +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 load: (searchId: SearchId) => void;
27
- readonly select: <Params extends unknown[]>(compute: (...args: Params) => SearchOptions<Document>) => CreateState<Document, Params>;
22
+ readonly clear: () => void;
28
23
  }
29
24
  /**
30
25
  * Create a SyncTable that wraps a Table and provides reactive capabilities
@@ -32,4 +27,3 @@ export interface SyncTable<Document extends DocType> {
32
27
  * @returns A SyncTable instance with methods to interact with the underlying Table and manage reactive searches
33
28
  */
34
29
  export declare function createSqliteState<Document extends DocType>(options: CreateSqliteOptions<Document>): SyncTable<Document>;
35
- 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,18 +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
5
  /**
7
6
  * Load the next page of results and return if isDone to show more results.
8
7
  * @returns isDone: boolean
9
8
  */
10
- readonly next: () => Promise<boolean>;
9
+ readonly nextPage: () => Promise<boolean>;
11
10
  /**
12
11
  * Reset the pagination and load the first page of results.
13
12
  * @returns void
14
13
  */
15
14
  readonly reset: () => Promise<void>;
15
+ readonly keysIndex: Map<Key, number>;
16
16
  }
17
17
  export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
18
18
  /**
@@ -21,11 +21,11 @@ export interface UseSearchOptions<Document extends DocType, Selected = Document>
21
21
  readonly select?: (document: Document) => Selected;
22
22
  }
23
23
  /**
24
- * React hook to subscribe to a SyncTable and get its current snapshot, with optional search options and selector for derived state
25
- * @param state The SyncTable to subscribe to
26
- * @param options Optional search options to filter and sort the documents
27
- * @param deps Dependency list to control when to update the search options
28
- * @returns A tuple containing the current array of documents (or selected documents) and an object with actions to interact with the SyncTable
29
- * @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.
30
30
  */
31
- 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];