muya 2.5.0 → 2.5.2
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/README.md +409 -155
- package/cjs/index.js +1 -1
- package/esm/index.js +1 -1
- package/esm/sqlite/use-sqlite.js +1 -1
- package/esm/use-value-loadable.js +1 -0
- package/package.json +1 -1
- package/src/__tests__/use-value-loadable.test.tsx +135 -0
- package/src/index.ts +1 -0
- package/src/sqlite/__tests__/use-sqlite.more.test.tsx +6 -6
- package/src/sqlite/__tests__/use-sqlite.test.tsx +510 -25
- package/src/sqlite/use-sqlite.ts +40 -5
- package/src/use-value-loadable.ts +39 -0
- package/types/index.d.ts +1 -0
- package/types/sqlite/use-sqlite.d.ts +8 -1
- package/types/use-value-loadable.d.ts +14 -0
- package/esm/sqlite/select-sql.js +0 -1
- package/src/sqlite/select-sql.ts +0 -65
- package/types/sqlite/select-sql.d.ts +0 -20
package/src/sqlite/use-sqlite.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
/* eslint-disable sonarjs/cognitive-complexity */
|
|
2
|
-
import { useCallback, useLayoutEffect, useReducer, useRef, type DependencyList } from 'react'
|
|
2
|
+
import { useCallback, useLayoutEffect, useReducer, useRef, useState, type DependencyList } from 'react'
|
|
3
3
|
import type { SyncTable } from './create-sqlite'
|
|
4
4
|
import type { DocType, Key, SqlSeachOptions } from './table/table.types'
|
|
5
5
|
import { DEFAULT_PAGE_SIZE } from './table'
|
|
6
6
|
import { shallow } from '../utils/shallow'
|
|
7
7
|
const MAX_ITERATIONS = 10_000
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Shallow compare two dependency arrays
|
|
11
|
+
* @param previousDeps Previous deps array
|
|
12
|
+
* @param nextDeps Next deps array
|
|
13
|
+
* @returns True if arrays have same length and all items are strictly equal
|
|
14
|
+
*/
|
|
15
|
+
function shallowEqualDeps(previousDeps: DependencyList, nextDeps: DependencyList): boolean {
|
|
16
|
+
if (previousDeps.length !== nextDeps.length) return false
|
|
17
|
+
for (const [index, previousDep] of previousDeps.entries()) {
|
|
18
|
+
if (!Object.is(previousDep, nextDeps[index])) return false
|
|
19
|
+
}
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export interface SqLiteActions {
|
|
10
24
|
/**
|
|
11
25
|
* Load the next page of results and return if isDone to show more results.
|
|
@@ -17,8 +31,15 @@ export interface SqLiteActions {
|
|
|
17
31
|
* @returns void
|
|
18
32
|
*/
|
|
19
33
|
readonly reset: () => Promise<void>
|
|
34
|
+
/**
|
|
35
|
+
* Map of document keys to their index in the results array.
|
|
36
|
+
*/
|
|
20
37
|
readonly keysIndex: Map<Key, number>
|
|
21
|
-
|
|
38
|
+
/**
|
|
39
|
+
* True when deps changed but fresh data hasn't loaded yet.
|
|
40
|
+
* Use this to show stale/dimmed UI while new results are loading.
|
|
41
|
+
*/
|
|
42
|
+
readonly isStale: boolean
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
@@ -48,6 +69,11 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
48
69
|
const keysIndex = useRef(new Map<Key, number>())
|
|
49
70
|
const iteratorRef = useRef<AsyncIterableIterator<{ doc: Document; meta: { key: Key } }> | null>(null)
|
|
50
71
|
|
|
72
|
+
// Track "settled" deps - the deps value when data last finished loading
|
|
73
|
+
// isStale is derived: true when current deps differ from settled deps
|
|
74
|
+
const [settledDeps, setSettledDeps] = useState<DependencyList | null>(null)
|
|
75
|
+
const isStale = settledDeps === null || !shallowEqualDeps(settledDeps, deps)
|
|
76
|
+
|
|
51
77
|
const updateIterator = useCallback(() => {
|
|
52
78
|
// eslint-disable-next-line sonarjs/no-unused-vars
|
|
53
79
|
const { select: _ignore, ...resetOptions } = options
|
|
@@ -198,17 +224,26 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
198
224
|
}, [state])
|
|
199
225
|
|
|
200
226
|
useLayoutEffect(() => {
|
|
227
|
+
// Capture current deps for this effect invocation
|
|
228
|
+
const currentDeps = deps
|
|
201
229
|
resetDataAndUpdateIterator()
|
|
202
|
-
nextPage()
|
|
230
|
+
nextPage().then(() => {
|
|
231
|
+
// Mark these deps as settled when data finishes loading
|
|
232
|
+
setSettledDeps(currentDeps)
|
|
233
|
+
})
|
|
203
234
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
235
|
}, deps)
|
|
205
236
|
|
|
206
237
|
const resetCb = useCallback(async () => {
|
|
238
|
+
// Set settledDeps to null to make isStale=true during reset
|
|
239
|
+
setSettledDeps(null)
|
|
207
240
|
resetDataAndUpdateIterator()
|
|
208
241
|
await nextPage()
|
|
209
|
-
|
|
242
|
+
// After data loads, mark current deps as settled
|
|
243
|
+
setSettledDeps(deps)
|
|
244
|
+
}, [nextPage, resetDataAndUpdateIterator, deps])
|
|
210
245
|
|
|
211
|
-
return [itemsRef.current, { nextPage, reset: resetCb, keysIndex: keysIndex.current }] as [
|
|
246
|
+
return [itemsRef.current, { nextPage, reset: resetCb, keysIndex: keysIndex.current, isStale }] as [
|
|
212
247
|
(undefined extends Selected ? Document[] : Selected[]) | null,
|
|
213
248
|
SqLiteActions,
|
|
214
249
|
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useDebugValue, useSyncExternalStore } from 'react'
|
|
2
|
+
import { EMPTY_SELECTOR, type GetState } from './types'
|
|
3
|
+
import { isError, isPromise } from './utils/is'
|
|
4
|
+
|
|
5
|
+
type LoadableLoading = [undefined, true, false, undefined]
|
|
6
|
+
type LoadableSuccess<T> = [T, false, false, undefined]
|
|
7
|
+
type LoadableError = [undefined, false, true, Error]
|
|
8
|
+
|
|
9
|
+
export type LoadableResult<T> = LoadableLoading | LoadableSuccess<T> | LoadableError
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* React hook to subscribe to a state and get its value without throwing to Suspense.
|
|
13
|
+
* Returns a tuple of [value, isLoading, isError, error] for handling async states.
|
|
14
|
+
* @param state The state to subscribe to
|
|
15
|
+
* @param selector Optional function to derive a value from the state
|
|
16
|
+
* @returns Tuple of [value, isLoading, isError, error] with discriminated union types
|
|
17
|
+
*/
|
|
18
|
+
export function useValueLoadable<T, S = undefined>(
|
|
19
|
+
state: GetState<T>,
|
|
20
|
+
selector: (stateValue: Awaited<T>) => S = EMPTY_SELECTOR,
|
|
21
|
+
): LoadableResult<undefined extends S ? Awaited<T> : S> {
|
|
22
|
+
const { emitter } = state
|
|
23
|
+
|
|
24
|
+
const rawValue = useSyncExternalStore(emitter.subscribe, emitter.getSnapshot, emitter.getInitialSnapshot)
|
|
25
|
+
|
|
26
|
+
useDebugValue(rawValue)
|
|
27
|
+
|
|
28
|
+
if (isPromise(rawValue)) {
|
|
29
|
+
return [undefined, true, false, undefined] as LoadableResult<undefined extends S ? Awaited<T> : S>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isError(rawValue)) {
|
|
33
|
+
return [undefined, false, true, rawValue] as LoadableResult<undefined extends S ? Awaited<T> : S>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selectedValue = selector(rawValue as Awaited<T>)
|
|
37
|
+
|
|
38
|
+
return [selectedValue, false, false, undefined] as LoadableResult<undefined extends S ? Awaited<T> : S>
|
|
39
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -12,8 +12,15 @@ export interface SqLiteActions {
|
|
|
12
12
|
* @returns void
|
|
13
13
|
*/
|
|
14
14
|
readonly reset: () => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Map of document keys to their index in the results array.
|
|
17
|
+
*/
|
|
15
18
|
readonly keysIndex: Map<Key, number>;
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* True when deps changed but fresh data hasn't loaded yet.
|
|
21
|
+
* Use this to show stale/dimmed UI while new results are loading.
|
|
22
|
+
*/
|
|
23
|
+
readonly isStale: boolean;
|
|
17
24
|
}
|
|
18
25
|
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
19
26
|
/**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type GetState } from './types';
|
|
2
|
+
type LoadableLoading = [undefined, true, false, undefined];
|
|
3
|
+
type LoadableSuccess<T> = [T, false, false, undefined];
|
|
4
|
+
type LoadableError = [undefined, false, true, Error];
|
|
5
|
+
export type LoadableResult<T> = LoadableLoading | LoadableSuccess<T> | LoadableError;
|
|
6
|
+
/**
|
|
7
|
+
* React hook to subscribe to a state and get its value without throwing to Suspense.
|
|
8
|
+
* Returns a tuple of [value, isLoading, isError, error] for handling async states.
|
|
9
|
+
* @param state The state to subscribe to
|
|
10
|
+
* @param selector Optional function to derive a value from the state
|
|
11
|
+
* @returns Tuple of [value, isLoading, isError, error] with discriminated union types
|
|
12
|
+
*/
|
|
13
|
+
export declare function useValueLoadable<T, S = undefined>(state: GetState<T>, selector?: (stateValue: Awaited<T>) => S): LoadableResult<undefined extends S ? Awaited<T> : S>;
|
|
14
|
+
export {};
|
package/esm/sqlite/select-sql.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createState as l}from"../create-state";let n=0;function o(){return n++,`${n.toString(36)}-sql`}function S(r,a){const{subscribe:c,updateSearchOptions:s}=r,m=o();return(...u)=>{const e=o(),p=c(e,m,()=>{t.emitter.emit()}),i=a(...u),t=l({destroy(){p(),t.emitter.clear(),t.cache.current=void 0},get(){return r.getSnapshot(e)},getSnapshot(){return r.getSnapshot(e)}});return s(e,i),t}}export{S as selectSql};
|
package/src/sqlite/select-sql.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { createState } from '../create-state'
|
|
2
|
-
import type { GetState } from '../types'
|
|
3
|
-
import type { SyncTable } from './create-sqlite'
|
|
4
|
-
import type { DocType, DotPath } from './table/table.types'
|
|
5
|
-
import type { Where } from './table/where'
|
|
6
|
-
|
|
7
|
-
export type CreateState<Document, Params extends unknown[]> = (...params: Params) => GetState<Document[]>
|
|
8
|
-
|
|
9
|
-
export interface SqlSeachOptions<Document extends DocType> {
|
|
10
|
-
readonly sortBy?: DotPath<Document>
|
|
11
|
-
readonly order?: 'asc' | 'desc'
|
|
12
|
-
readonly limit?: number
|
|
13
|
-
readonly offset?: number
|
|
14
|
-
readonly where?: Where<Document>
|
|
15
|
-
readonly stepSize?: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let stateId = 0
|
|
19
|
-
/**
|
|
20
|
-
* Generate a unique state ID
|
|
21
|
-
* @returns A unique state ID
|
|
22
|
-
*/
|
|
23
|
-
function getStateId() {
|
|
24
|
-
stateId++
|
|
25
|
-
return `${stateId.toString(36)}-sql`
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Create a state that derives its value from a SyncTable using a compute function
|
|
30
|
-
* @param state The SyncTable to derive from
|
|
31
|
-
* @param compute A function that takes parameters and returns SqlSeachOptions to filter the SyncTable
|
|
32
|
-
* @returns A function that takes parameters and returns a GetState of the derived documents
|
|
33
|
-
*/
|
|
34
|
-
export function selectSql<Document extends DocType, Params extends unknown[] = []>(
|
|
35
|
-
state: SyncTable<Document>,
|
|
36
|
-
compute: (...args: Params) => SqlSeachOptions<Document>,
|
|
37
|
-
): CreateState<Document, Params> {
|
|
38
|
-
const { subscribe, updateSearchOptions } = state
|
|
39
|
-
const componentId = getStateId()
|
|
40
|
-
const result: CreateState<Document, Params> = (...params) => {
|
|
41
|
-
const searchId = getStateId()
|
|
42
|
-
const destroy = subscribe(searchId, componentId, () => {
|
|
43
|
-
getState.emitter.emit()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const options = compute(...params)
|
|
47
|
-
const getState = createState<Document[]>({
|
|
48
|
-
destroy() {
|
|
49
|
-
destroy()
|
|
50
|
-
getState.emitter.clear()
|
|
51
|
-
getState.cache.current = undefined
|
|
52
|
-
},
|
|
53
|
-
get() {
|
|
54
|
-
return state.getSnapshot(searchId)
|
|
55
|
-
},
|
|
56
|
-
getSnapshot() {
|
|
57
|
-
return state.getSnapshot(searchId)
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
updateSearchOptions<Document>(searchId, options)
|
|
61
|
-
|
|
62
|
-
return getState
|
|
63
|
-
}
|
|
64
|
-
return result
|
|
65
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { GetState } from '../types';
|
|
2
|
-
import type { SyncTable } from './create-sqlite';
|
|
3
|
-
import type { DocType, DotPath } from './table/table.types';
|
|
4
|
-
import type { Where } from './table/where';
|
|
5
|
-
export type CreateState<Document, Params extends unknown[]> = (...params: Params) => GetState<Document[]>;
|
|
6
|
-
export interface SqlSeachOptions<Document extends DocType> {
|
|
7
|
-
readonly sortBy?: DotPath<Document>;
|
|
8
|
-
readonly order?: 'asc' | 'desc';
|
|
9
|
-
readonly limit?: number;
|
|
10
|
-
readonly offset?: number;
|
|
11
|
-
readonly where?: Where<Document>;
|
|
12
|
-
readonly stepSize?: number;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Create a state that derives its value from a SyncTable using a compute function
|
|
16
|
-
* @param state The SyncTable to derive from
|
|
17
|
-
* @param compute A function that takes parameters and returns SqlSeachOptions to filter the SyncTable
|
|
18
|
-
* @returns A function that takes parameters and returns a GetState of the derived documents
|
|
19
|
-
*/
|
|
20
|
-
export declare function selectSql<Document extends DocType, Params extends unknown[] = []>(state: SyncTable<Document>, compute: (...args: Params) => SqlSeachOptions<Document>): CreateState<Document, Params>;
|