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.
- package/cjs/index.js +1 -1
- package/esm/scheduler.js +1 -1
- package/esm/sqlite/create-sqlite.js +1 -1
- package/esm/sqlite/index.js +1 -1
- package/esm/sqlite/table/table.js +2 -2
- package/esm/sqlite/use-sqlite-count.js +1 -0
- package/esm/sqlite/use-sqlite.js +1 -1
- package/package.json +1 -1
- package/src/scheduler.ts +8 -2
- package/src/sqlite/__tests__/use-slite-count.test.tsx +96 -0
- package/src/sqlite/__tests__/use-sqlite.test.tsx +124 -41
- package/src/sqlite/create-sqlite.ts +44 -235
- package/src/sqlite/index.ts +1 -1
- package/src/sqlite/table/table.ts +3 -3
- package/src/sqlite/table/table.types.ts +9 -1
- package/src/sqlite/use-sqlite-count.ts +69 -0
- package/src/sqlite/use-sqlite.ts +157 -89
- package/types/scheduler.d.ts +1 -1
- package/types/sqlite/create-sqlite.d.ts +6 -11
- package/types/sqlite/index.d.ts +1 -1
- package/types/sqlite/table/table.d.ts +1 -1
- package/types/sqlite/table/table.types.d.ts +8 -1
- package/types/sqlite/use-sqlite-count.d.ts +17 -0
- package/types/sqlite/use-sqlite.d.ts +18 -10
package/src/sqlite/use-sqlite.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
|
|
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 {
|
|
5
|
-
|
|
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
|
+
* 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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
68
|
-
const
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
}
|
package/types/scheduler.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {};
|
package/types/sqlite/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
17
|
-
*
|
|
18
|
-
* @param
|
|
19
|
-
* @param
|
|
20
|
-
* @
|
|
21
|
-
* @
|
|
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];
|