muya 2.4.4 → 2.4.6

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,21 +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
- readonly load: (searchId: SearchId) => void
36
-
37
- readonly select: <Params extends unknown[]>(
38
- compute: (...args: Params) => SearchOptions<Document>,
39
- ) => CreateState<Document, Params>
40
- }
41
-
42
- interface CachedItem<Document extends DocType> {
43
- items: Document[]
44
- keys: Set<Key>
45
- options?: SearchOptions<Document, unknown>
46
- wasInitialized: boolean
27
+ readonly clear: () => void
47
28
  }
48
29
 
49
30
  /**
@@ -52,17 +33,6 @@ interface CachedItem<Document extends DocType> {
52
33
  * @returns A SyncTable instance with methods to interact with the underlying Table and manage reactive searches
53
34
  */
54
35
  export function createSqliteState<Document extends DocType>(options: CreateSqliteOptions<Document>): SyncTable<Document> {
55
- const id = getId()
56
-
57
- /**
58
- * Get a unique schedule ID for a search ID
59
- * @param searchId The search ID
60
- * @returns The unique schedule ID
61
- */
62
- function getScheduleId(searchId: SearchId) {
63
- return `state-${id}-search-${searchId}`
64
- }
65
-
66
36
  let cachedTable: Table<Document> | undefined
67
37
  /**
68
38
  * Get or create the underlying table
@@ -77,213 +47,74 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
77
47
  return cachedTable
78
48
  }
79
49
 
80
- interface NextResult {
81
- document: Document
82
- key: Key
83
- }
84
- // const emitter = createEmitter<Table<Document>>()
85
- const cachedData = new Map<SearchId, CachedItem<Document>>()
86
- const listeners = new Map<SearchId, Map<string, () => void>>()
87
- const iterators = new Map<SearchId, AsyncIterableIterator<NextResult>>()
88
-
89
- interface GetNextBase {
90
- readonly newItems?: Document[]
91
- readonly isOk: boolean
92
- readonly isDone?: boolean
93
- }
94
-
95
- interface GetNextTrue extends GetNextBase {
96
- readonly isOk: true
97
- readonly newItems: Document[]
98
- }
99
- interface GetNextFalse extends GetNextBase {
100
- readonly isOk: false
101
- }
102
- type GetNext = GetNextTrue | GetNextFalse
103
- /**
104
- * Next step in the iterator
105
- * @param searchId The search ID
106
- * @param data The data items to process
107
- * @param shouldReset Whether to reset the items
108
- * @returns boolean indicating if new items were added
109
- */
110
- async function getNext(searchId: SearchId, data: CachedItem<Document>, shouldReset: boolean): Promise<GetNext> {
111
- const iterator = iterators.get(searchId)
112
- const { options: nextOptions = {} } = data
113
- const { stepSize = DEFAULT_STEP_SIZE } = nextOptions
114
- if (!iterator) return { isOk: false }
115
- const newItems: Document[] = []
116
- if (shouldReset) {
117
- data.keys.clear()
118
- }
119
-
120
- for (let index = 0; index < stepSize; index++) {
121
- const result = await iterator.next()
122
- if (result.done) {
123
- iterators.delete(searchId)
124
- break
125
- }
126
-
127
- if (!data.keys.has(String(result.value.key))) {
128
- newItems.push(result.value.document)
129
- data.keys.add(String(result.value.key))
130
- }
131
- }
132
-
133
- if (newItems.length === 0) return { isOk: true, newItems: shouldReset ? [] : data.items, isDone: true }
134
- if (shallow(data.items, newItems)) return { isOk: false }
135
- if (shouldReset) return { isOk: true, newItems }
136
- return { newItems: [...data.items, ...newItems], isOk: true }
137
- }
138
-
139
- /**
140
- * Notify listeners of up dates
141
- * @param searchId The search ID to notify
142
- */
143
- function notifyListeners(searchId: SearchId) {
144
- const searchListeners = listeners.get(searchId)
145
- if (searchListeners) {
146
- for (const [, listener] of searchListeners) {
147
- listener()
50
+ const id = getId()
51
+ STATE_SCHEDULER.add(id, {
52
+ onScheduleDone(unknownItems) {
53
+ if (!unknownItems) {
54
+ return
148
55
  }
149
- }
150
- }
151
-
152
- /**
153
- * Refresh the cache for a search ID
154
- * @param searchId The search ID to refresh
155
- */
156
- async function refreshCache(searchId: SearchId) {
157
- const table = await getTable()
158
- const data = cachedData.get(searchId)
159
- if (!data) return
160
- const { options: refreshOptions } = data
161
- const iterator = table.search({ ...refreshOptions, select: (document, { rowId, key }) => ({ document, rowId, key }) })
162
- iterators.set(searchId, iterator)
163
- const { isOk, newItems } = await getNext(searchId, data, true)
164
- if (isOk) {
165
- data.items = newItems
166
- }
167
- }
168
- /**
169
- * Refresh the data and notify listeners
170
- * @param searchId The search ID to refresh
171
- */
172
- async function refresh(searchId: SearchId) {
173
- await refreshCache(searchId)
174
- notifyListeners(searchId)
175
- }
176
-
177
- /**
178
- * Handle changes to the data
179
- * @param mutationResult The mutation result
180
- * @returns A set of search IDs that need to be updated
181
- */
182
- function getChangedKeys(mutationResult: MutationResult) {
183
- const { key, op } = mutationResult
184
- // find all cached data with key
185
- const searchIds = new Set<SearchId>()
186
- for (const [searchId, { keys }] of cachedData) {
187
- switch (op) {
188
- case 'delete':
189
- case 'update': {
190
- if (keys.has(String(key))) {
191
- searchIds.add(searchId)
192
- }
193
- 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
194
61
  }
195
- case 'insert': {
196
- // we do not know about the key
197
- searchIds.add(searchId)
198
- break
62
+ if (item.mutations) {
63
+ if (!merged.mutations) {
64
+ merged.mutations = []
65
+ }
66
+ merged.mutations.push(...item.mutations)
199
67
  }
200
68
  }
201
- }
202
- return searchIds
203
- }
204
-
205
- /**
206
- * Handle multiple changes
207
- * @param mutationResults The array of mutation results
208
- */
209
- async function handleChanges(mutationResults: MutationResult[]) {
210
- const updateSearchIds = new Set<SearchId>()
211
- for (const mutationResult of mutationResults) {
212
- const searchIds = getChangedKeys(mutationResult)
213
- for (const searchId of searchIds) {
214
- updateSearchIds.add(searchId)
69
+ for (const listener of listeners) {
70
+ listener(merged)
215
71
  }
216
- }
217
-
218
- for (const searchId of updateSearchIds) {
219
- const scheduleId = getScheduleId(searchId)
220
- STATE_SCHEDULER.schedule(scheduleId, {})
221
- }
222
- }
223
-
224
- const clearSchedulers = new Set<() => void>()
72
+ },
73
+ })
225
74
 
226
75
  /**
227
- * Register data for a search ID
228
- * @param searchId The search ID
229
- * @param registerDataOptions Optional search options
230
- * @returns The data items for the search ID
76
+ * Notify all subscribers of changes
77
+ * @param item The mutation items to notify subscribers about
231
78
  */
232
- function registerData(searchId: SearchId, registerDataOptions?: SearchOptions<Document, unknown>) {
233
- if (!cachedData.has(searchId)) {
234
- const cachedItem: CachedItem<Document> = {
235
- items: [],
236
- options: registerDataOptions,
237
- keys: new Set(),
238
- wasInitialized: false,
239
- }
240
- cachedData.set(searchId, cachedItem)
241
- }
242
- const cachedItem = cachedData.get(searchId)!
243
-
244
- if (registerDataOptions) {
245
- cachedItem.options = registerDataOptions
246
- }
247
- return cachedItem
79
+ function handleChanges(item: MutationItems) {
80
+ STATE_SCHEDULER.schedule(id, item)
248
81
  }
249
82
 
83
+ const listeners = new Set<(mutation: MutationItems) => void>()
84
+
250
85
  const state: SyncTable<Document> = {
251
- load(searchId: SearchId) {
252
- const cachedItem = cachedData.get(searchId)
253
- if (!cachedItem) return
254
- if (!cachedItem.wasInitialized) {
255
- cachedItem.wasInitialized = true
256
- const scheduleId = getScheduleId(searchId)
257
- STATE_SCHEDULER.schedule(scheduleId, { searchId })
258
- }
86
+ subscribe(listener) {
87
+ listeners.add(listener)
88
+ return () => listeners.delete(listener)
259
89
  },
260
- clear(searchId: SearchId) {
261
- cachedData.delete(searchId)
90
+ clear() {
91
+ cachedTable?.clear()
92
+ handleChanges({ removedAll: true })
262
93
  },
263
94
  async set(document) {
264
95
  const table = await getTable()
265
96
  const changes = await table.set(document)
266
- await handleChanges([changes])
97
+ handleChanges({ mutations: [changes] })
267
98
  return changes
268
99
  },
269
100
  async batchSet(documents) {
270
101
  const table = await getTable()
271
102
  const changes = await table.batchSet(documents)
272
- await handleChanges(changes)
103
+ handleChanges({ mutations: changes })
273
104
  return changes
274
105
  },
275
106
  async delete(key) {
276
107
  const table = await getTable()
277
108
  const changes = await table.delete(key)
278
109
  if (changes) {
279
- await handleChanges([changes])
110
+ handleChanges({ mutations: [changes] })
280
111
  }
281
112
  return changes
282
113
  },
283
114
  async deleteBy(where) {
284
115
  const table = await getTable()
285
116
  const changes = await table.deleteBy(where)
286
- await handleChanges(changes)
117
+ handleChanges({ mutations: changes })
287
118
  return changes
288
119
  },
289
120
  async get(key, selector) {
@@ -300,65 +131,6 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
300
131
  const table = await getTable()
301
132
  return await table.count(countOptions)
302
133
  },
303
-
304
- updateSearchOptions(searchId, updateSearchOptions) {
305
- const data = registerData(searchId, updateSearchOptions)
306
- data.options = updateSearchOptions
307
- const scheduleId = getScheduleId(searchId)
308
- STATE_SCHEDULER.schedule(scheduleId, { searchId })
309
- },
310
-
311
- subscribe(searchId, componentId, listener) {
312
- const scheduleId = getScheduleId(searchId)
313
- const clearScheduler = STATE_SCHEDULER.add(scheduleId, {
314
- onScheduleDone() {
315
- refresh(searchId)
316
- },
317
- })
318
- // console.log('Subscribing to searchId:', searchId)
319
- this.load(searchId)
320
- clearSchedulers.add(clearScheduler)
321
-
322
- if (!listeners.has(searchId)) {
323
- listeners.set(searchId, new Map())
324
- }
325
- const searchListeners = listeners.get(searchId)!
326
- searchListeners.set(componentId, listener)
327
-
328
- return () => {
329
- searchListeners.delete(componentId)
330
- if (searchListeners.size === 0) {
331
- listeners.delete(searchId)
332
- }
333
- clearScheduler()
334
- }
335
- },
336
- getSnapshot(searchId) {
337
- const data = registerData(searchId)
338
- return data.items
339
- },
340
- refresh,
341
- destroy() {
342
- for (const clear of clearSchedulers) clear()
343
- cachedData.clear()
344
- listeners.clear()
345
- },
346
- async next(searchId) {
347
- const data = cachedData.get(searchId)
348
- if (data) {
349
- const hasNext = await getNext(searchId, data, false)
350
- if (hasNext.isOk) {
351
- data.items = hasNext.newItems
352
- notifyListeners(searchId)
353
- }
354
- return hasNext.isDone ?? false
355
- }
356
- return false
357
- },
358
-
359
- select(compute) {
360
- return selectSql(state, compute)
361
- },
362
134
  }
363
135
 
364
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
+ }