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.
@@ -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
- readonly isResetting?: boolean
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
- }, [nextPage, resetDataAndUpdateIterator])
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
@@ -3,4 +3,5 @@ export * from './types';
3
3
  export { create } from './create';
4
4
  export { select } from './select';
5
5
  export { useValue } from './use-value';
6
+ export { useValueLoadable, type LoadableResult } from './use-value-loadable';
6
7
  export { shallow } from './utils/shallow';
@@ -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
- readonly isResetting?: boolean;
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 {};
@@ -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};
@@ -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>;