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,26 +1,21 @@
1
- /* eslint-disable sonarjs/redundant-type-aliases */
2
1
  import { STATE_SCHEDULER } from '../create'
3
2
  import { getId } from '../utils/id'
4
- import { shallow } from '../utils/shallow'
5
- import { selectSql, type CreateState } from './select-sql'
6
3
  import type { Backend } from './table'
7
- import { createTable, DEFAULT_STEP_SIZE } from './table/table'
4
+ import { createTable } from './table/table'
8
5
  import type { DbOptions, DocType, Key, MutationResult, SearchOptions, Table } from './table/table.types'
9
6
  import type { Where } from './table/where'
10
7
 
11
- type SearchId = string
12
-
13
8
  export interface CreateSqliteOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
14
9
  readonly backend: Backend | Promise<Backend>
15
10
  }
16
11
 
17
- export interface SyncTable<Document extends DocType> {
18
- // readonly registerSearch: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => () => void
19
- readonly updateSearchOptions: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => void
20
- readonly subscribe: (searchId: SearchId, componentId: string, listener: () => void) => () => void
21
- readonly getSnapshot: (searchId: SearchId) => Document[]
22
- readonly refresh: (searchId: SearchId) => Promise<void>
12
+ export interface MutationItems {
13
+ mutations?: MutationResult[]
14
+ removedAll?: boolean
15
+ }
23
16
 
17
+ export interface SyncTable<Document extends DocType> {
18
+ readonly subscribe: (listener: (mutation: MutationItems) => void) => () => void
24
19
  readonly set: (document: Document) => Promise<MutationResult>
25
20
  readonly batchSet: (documents: Document[]) => Promise<MutationResult[]>
26
21
  readonly get: <Selected = Document>(key: Key, selector?: (document: Document) => Selected) => Promise<Selected | undefined>
@@ -29,19 +24,7 @@ export interface SyncTable<Document extends DocType> {
29
24
  readonly search: <Selected = Document>(options?: SearchOptions<Document, Selected>) => AsyncIterableIterator<Selected>
30
25
  readonly count: (options?: { where?: Where<Document> }) => Promise<number>
31
26
  readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>
32
- readonly destroy: () => void
33
- readonly next: (searchId: SearchId) => Promise<boolean>
34
- readonly clear: (searchId: SearchId) => void
35
-
36
- readonly select: <Params extends unknown[]>(
37
- compute: (...args: Params) => SearchOptions<Document>,
38
- ) => CreateState<Document, Params>
39
- }
40
-
41
- interface DataItems<Document extends DocType> {
42
- items: Document[]
43
- keys: Set<Key>
44
- options?: SearchOptions<Document, unknown>
27
+ readonly clear: () => void
45
28
  }
46
29
 
47
30
  /**
@@ -50,17 +33,6 @@ interface DataItems<Document extends DocType> {
50
33
  * @returns A SyncTable instance with methods to interact with the underlying Table and manage reactive searches
51
34
  */
52
35
  export function createSqliteState<Document extends DocType>(options: CreateSqliteOptions<Document>): SyncTable<Document> {
53
- const id = getId()
54
-
55
- /**
56
- * Get a unique schedule ID for a search ID
57
- * @param searchId The search ID
58
- * @returns The unique schedule ID
59
- */
60
- function getScheduleId(searchId: SearchId) {
61
- return `state-${id}-search-${searchId}`
62
- }
63
-
64
36
  let cachedTable: Table<Document> | undefined
65
37
  /**
66
38
  * Get or create the underlying table
@@ -75,182 +47,74 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
75
47
  return cachedTable
76
48
  }
77
49
 
78
- interface NextResult {
79
- document: Document
80
- key: Key
81
- }
82
- // const emitter = createEmitter<Table<Document>>()
83
- const cachedData = new Map<SearchId, DataItems<Document>>()
84
- const listeners = new Map<SearchId, Map<string, () => void>>()
85
- const iterators = new Map<SearchId, AsyncIterableIterator<NextResult>>()
86
-
87
- /**
88
- * Next step in the iterator
89
- * @param searchId The search ID
90
- * @param data The data items to process
91
- * @returns boolean indicating if new items were added
92
- */
93
- async function next(searchId: SearchId, data: DataItems<Document>): Promise<boolean> {
94
- const iterator = iterators.get(searchId)
95
- const { options: nextOptions = {} } = data
96
- const { stepSize = DEFAULT_STEP_SIZE } = nextOptions
97
- if (!iterator) return false
98
- const newItems: Document[] = []
99
-
100
- for (let index = 0; index < stepSize; index++) {
101
- const result = await iterator.next()
102
- if (result.done) {
103
- iterators.delete(searchId)
104
- break
105
- }
106
-
107
- if (!data.keys.has(String(result.value.key))) {
108
- newItems.push(result.value.document)
109
- data.keys.add(String(result.value.key))
110
- }
111
- }
112
-
113
- if (newItems.length === 0) return false
114
- if (shallow(data.items, newItems)) return false
115
- data.items = [...data.items, ...newItems]
116
- return true
117
- }
118
-
119
- /**
120
- * Notify listeners of up dates
121
- * @param searchId The search ID to notify
122
- */
123
- function notifyListeners(searchId: SearchId) {
124
- const searchListeners = listeners.get(searchId)
125
- if (searchListeners) {
126
- for (const [, listener] of searchListeners) {
127
- listener()
50
+ const id = getId()
51
+ STATE_SCHEDULER.add(id, {
52
+ onScheduleDone(unknownItems) {
53
+ if (!unknownItems) {
54
+ return
128
55
  }
129
- }
130
- }
131
-
132
- /**
133
- * Refresh the cache for a search ID
134
- * @param searchId The search ID to refresh
135
- */
136
- async function refreshCache(searchId: SearchId) {
137
- const table = await getTable()
138
- const data = cachedData.get(searchId)
139
- if (!data) return
140
- const { options: refreshOptions } = data
141
- const iterator = table.search({ ...refreshOptions, select: (document, { rowId, key }) => ({ document, rowId, key }) })
142
- iterators.set(searchId, iterator)
143
- data.keys = new Set()
144
- data.items = []
145
- await next(searchId, data)
146
- }
147
- /**
148
- * Refresh the data and notify listeners
149
- * @param searchId The search ID to refresh
150
- */
151
- async function refresh(searchId: SearchId) {
152
- await refreshCache(searchId)
153
- notifyListeners(searchId)
154
- }
155
-
156
- /**
157
- * Handle changes to the data
158
- * @param mutationResult The mutation result
159
- * @returns A set of search IDs that need to be updated
160
- */
161
- function handleChange(mutationResult: MutationResult) {
162
- const { key, op } = mutationResult
163
- // find all cached data with key
164
- const searchIds = new Set<SearchId>()
165
- for (const [searchId, { keys }] of cachedData) {
166
- switch (op) {
167
- case 'delete':
168
- case 'update': {
169
- if (keys.has(String(key))) {
170
- searchIds.add(searchId)
171
- }
172
- break
56
+ const items = unknownItems as MutationItems[]
57
+ const merged: MutationItems = {}
58
+ for (const item of items) {
59
+ if (item.removedAll) {
60
+ merged.removedAll = true
173
61
  }
174
- case 'insert': {
175
- // we do not know about the key
176
- searchIds.add(searchId)
177
- break
62
+ if (item.mutations) {
63
+ if (!merged.mutations) {
64
+ merged.mutations = []
65
+ }
66
+ merged.mutations.push(...item.mutations)
178
67
  }
179
68
  }
180
- }
181
- return searchIds
182
- }
183
-
184
- /**
185
- * Handle multiple changes
186
- * @param mutationResults The array of mutation results
187
- */
188
- async function handleChanges(mutationResults: MutationResult[]) {
189
- const updateSearchIds = new Set<SearchId>()
190
- for (const mutationResult of mutationResults) {
191
- const searchIds = handleChange(mutationResult)
192
- for (const searchId of searchIds) {
193
- updateSearchIds.add(searchId)
69
+ for (const listener of listeners) {
70
+ listener(merged)
194
71
  }
195
- }
196
-
197
- // const promises = []
198
- for (const searchId of updateSearchIds) {
199
- const scheduleId = getScheduleId(searchId)
200
- STATE_SCHEDULER.schedule(scheduleId, { searchId })
201
- }
202
- }
203
-
204
- const clearSchedulers = new Set<() => void>()
72
+ },
73
+ })
205
74
 
206
75
  /**
207
- * Register data for a search ID
208
- * @param searchId The search ID
209
- * @param registerDataOptions Optional search options
210
- * @returns The data items for the search ID
76
+ * Notify all subscribers of changes
77
+ * @param item The mutation items to notify subscribers about
211
78
  */
212
- function registerData(searchId: SearchId, registerDataOptions?: SearchOptions<Document, unknown>) {
213
- if (!cachedData.has(searchId)) {
214
- cachedData.set(searchId, { items: [], options: registerDataOptions, keys: new Set() })
215
- if (registerDataOptions) {
216
- refresh(searchId)
217
- }
218
- }
219
- const data = cachedData.get(searchId)!
220
- if (registerDataOptions) {
221
- data.options = registerDataOptions
222
- }
223
- return data
79
+ function handleChanges(item: MutationItems) {
80
+ STATE_SCHEDULER.schedule(id, item)
224
81
  }
225
82
 
83
+ const listeners = new Set<(mutation: MutationItems) => void>()
84
+
226
85
  const state: SyncTable<Document> = {
227
- clear(searchId: SearchId) {
228
- cachedData.delete(searchId)
86
+ subscribe(listener) {
87
+ listeners.add(listener)
88
+ return () => listeners.delete(listener)
89
+ },
90
+ clear() {
91
+ cachedTable?.clear()
92
+ handleChanges({ removedAll: true })
229
93
  },
230
94
  async set(document) {
231
95
  const table = await getTable()
232
96
  const changes = await table.set(document)
233
- await handleChanges([changes])
97
+ handleChanges({ mutations: [changes] })
234
98
  return changes
235
99
  },
236
100
  async batchSet(documents) {
237
101
  const table = await getTable()
238
102
  const changes = await table.batchSet(documents)
239
- await handleChanges(changes)
103
+ handleChanges({ mutations: changes })
240
104
  return changes
241
105
  },
242
106
  async delete(key) {
243
107
  const table = await getTable()
244
108
  const changes = await table.delete(key)
245
109
  if (changes) {
246
- await handleChanges([changes])
110
+ handleChanges({ mutations: [changes] })
247
111
  }
248
112
  return changes
249
113
  },
250
114
  async deleteBy(where) {
251
115
  const table = await getTable()
252
116
  const changes = await table.deleteBy(where)
253
- await handleChanges(changes)
117
+ handleChanges({ mutations: changes })
254
118
  return changes
255
119
  },
256
120
  async get(key, selector) {
@@ -267,61 +131,6 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
267
131
  const table = await getTable()
268
132
  return await table.count(countOptions)
269
133
  },
270
-
271
- updateSearchOptions(searchId, updateSearchOptions) {
272
- const data = registerData(searchId, updateSearchOptions)
273
- data.options = updateSearchOptions
274
- const scheduleId = getScheduleId(searchId)
275
- STATE_SCHEDULER.schedule(scheduleId, { searchId })
276
- },
277
-
278
- subscribe(searchId, componentId, listener) {
279
- const scheduleId = getScheduleId(searchId)
280
- const clearScheduler = STATE_SCHEDULER.add(scheduleId, {
281
- onScheduleDone() {
282
- refresh(searchId)
283
- },
284
- })
285
- clearSchedulers.add(clearScheduler)
286
-
287
- if (!listeners.has(searchId)) {
288
- listeners.set(searchId, new Map())
289
- }
290
- const searchListeners = listeners.get(searchId)!
291
- searchListeners.set(componentId, listener)
292
- return () => {
293
- searchListeners.delete(componentId)
294
- if (searchListeners.size === 0) {
295
- listeners.delete(searchId)
296
- }
297
- clearScheduler()
298
- }
299
- },
300
- getSnapshot(searchId) {
301
- const data = registerData(searchId)
302
- return data.items
303
- },
304
- refresh,
305
- destroy() {
306
- for (const clear of clearSchedulers) clear()
307
- cachedData.clear()
308
- listeners.clear()
309
- },
310
- async next(searchId) {
311
- const data = cachedData.get(searchId)
312
- if (data) {
313
- const hasNext = await next(searchId, data)
314
- if (hasNext) {
315
- notifyListeners(searchId)
316
- }
317
- return hasNext
318
- }
319
- return false
320
- },
321
-
322
- select(compute) {
323
- return selectSql(state, compute)
324
- },
325
134
  }
326
135
 
327
136
  return state
@@ -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'
@@ -9,7 +9,7 @@ import type { Where } from './where'
9
9
  import { getWhereQuery } from './where'
10
10
 
11
11
  const DELETE_IN_CHUNK = 500
12
- export const DEFAULT_STEP_SIZE = 100
12
+ export const DEFAULT_PAGE_SIZE = 100
13
13
 
14
14
  /**
15
15
  * Convert a dot-separated path to a JSON path
@@ -239,7 +239,7 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
239
239
  offset = 0,
240
240
  where,
241
241
  select = (document) => document as unknown as Selected,
242
- stepSize = DEFAULT_STEP_SIZE,
242
+ pageSize = DEFAULT_PAGE_SIZE,
243
243
  } = options
244
244
 
245
245
  const whereSql = getWhereQuery<Document>(where, tableName)
@@ -256,7 +256,7 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
256
256
  query += hasUserKey ? ` ORDER BY key COLLATE NOCASE ${order.toUpperCase()}` : ` ORDER BY rowid ${order.toUpperCase()}`
257
257
  }
258
258
 
259
- const batchLimit = limit ? Math.min(stepSize, limit - yielded) : stepSize
259
+ const batchLimit = limit ? Math.min(pageSize, limit - yielded) : pageSize
260
260
  query += ` LIMIT ${batchLimit} OFFSET ${currentOffset}`
261
261
 
262
262
  const results = await backend.select<Array<{ rowid: number; data: string }>>(query)
@@ -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'
@@ -7,6 +6,15 @@ import type { Where } from './where'
7
6
  export type DocType = { [key: string]: any }
8
7
  export type KeyTypeAvailable = 'string' | 'number'
9
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 pageSize?: number
16
+ }
17
+
10
18
  // Expand all nested keys into dot-paths
11
19
  export type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`
12
20
 
@@ -0,0 +1,69 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useReducer, useRef, 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
+ /**
7
+ * A React hook to count the number of items in a SyncTable reactively.
8
+ * It updates the count when items are inserted or deleted, but ignores updates.
9
+ * Supports filtering the count using a `where` clause.
10
+ * @param state The SyncTable instance to observe.
11
+ * @param options Optional filtering options.
12
+ * @param options.where A `where` clause to filter the count.
13
+ * @param deps Dependency list to control when to re-run the effect.
14
+ * @returns The current count of items in the table.
15
+ */
16
+ export function useSqliteCount<Document extends DocType>(
17
+ state: SyncTable<Document>,
18
+ options: { where?: Where<Document> } = {},
19
+ deps: DependencyList = [],
20
+ ): number {
21
+ const countRef = useRef(0)
22
+ const [, rerender] = useReducer((c: number) => c + 1, 0)
23
+
24
+ const updateCount = useCallback(async () => {
25
+ const newCount = await state.count(options)
26
+ countRef.current = newCount
27
+ rerender()
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ }, deps)
30
+
31
+ useEffect(() => {
32
+ updateCount()
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ }, deps)
35
+
36
+ useLayoutEffect(() => {
37
+ const unsubscribe = state.subscribe((item) => {
38
+ const { mutations, removedAll } = item
39
+ if (removedAll) {
40
+ countRef.current = 0
41
+ rerender()
42
+ return
43
+ }
44
+ if (!mutations) {
45
+ return
46
+ }
47
+
48
+ let shouldUpdate = false
49
+ for (const mutation of mutations) {
50
+ const { op } = mutation
51
+ if (op === 'insert' || op === 'delete') {
52
+ shouldUpdate = true
53
+ break
54
+ }
55
+ }
56
+
57
+ if (shouldUpdate) {
58
+ updateCount()
59
+ }
60
+ })
61
+
62
+ return () => {
63
+ unsubscribe()
64
+ }
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [state])
67
+
68
+ return countRef.current
69
+ }