muya 2.1.1 → 2.1.3
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/create-state.js +1 -1
- package/esm/create.js +1 -1
- package/esm/scheduler.js +1 -1
- package/esm/select.js +1 -1
- package/esm/sqlite/__tests__/create-sqlite.test.js +1 -0
- package/esm/sqlite/__tests__/map-deque.test.js +1 -0
- package/esm/sqlite/__tests__/table.test.js +1 -0
- package/esm/sqlite/__tests__/use-sqlite.test.js +1 -0
- package/esm/sqlite/create-sqlite.js +1 -0
- package/esm/sqlite/select-sql.js +1 -0
- package/esm/sqlite/table/backend.js +1 -0
- package/esm/sqlite/table/bun-backend.js +1 -0
- package/esm/sqlite/table/map-deque.js +1 -0
- package/esm/sqlite/table/table.js +10 -0
- package/esm/sqlite/table/table.types.js +0 -0
- package/esm/sqlite/table/where.js +1 -0
- package/esm/sqlite/use-sqlite.js +1 -0
- package/esm/utils/common.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/scheduler.test.tsx +2 -2
- package/src/create-state.ts +3 -2
- package/src/create.ts +22 -24
- package/src/scheduler.ts +15 -7
- package/src/select.ts +15 -17
- package/src/sqlite/__tests__/create-sqlite.test.ts +81 -0
- package/src/sqlite/__tests__/map-deque.test.ts +61 -0
- package/src/sqlite/__tests__/table.test.ts +142 -0
- package/src/sqlite/__tests__/use-sqlite.test.ts +274 -0
- package/src/sqlite/create-sqlite.ts +273 -0
- package/src/sqlite/select-sql.ts +55 -0
- package/src/sqlite/table/backend.ts +21 -0
- package/src/sqlite/table/bun-backend.ts +38 -0
- package/src/sqlite/table/map-deque.ts +29 -0
- package/src/sqlite/table/table.ts +200 -0
- package/src/sqlite/table/table.types.ts +55 -0
- package/src/sqlite/table/where.ts +267 -0
- package/src/sqlite/use-sqlite.ts +70 -0
- package/src/types.ts +1 -0
- package/src/utils/common.ts +6 -2
- package/types/create.d.ts +3 -3
- package/types/scheduler.d.ts +12 -3
- package/types/sqlite/create-sqlite.d.ts +28 -0
- package/types/sqlite/select-sql.d.ts +14 -0
- package/types/sqlite/table/backend.d.ts +20 -0
- package/types/sqlite/table/bun-backend.d.ts +2 -0
- package/types/sqlite/table/map-deque.d.ts +5 -0
- package/types/sqlite/table/table.d.ts +3 -0
- package/types/sqlite/table/table.types.d.ts +52 -0
- package/types/sqlite/table/where.d.ts +32 -0
- package/types/sqlite/use-sqlite.d.ts +15 -0
- package/types/types.d.ts +1 -0
- package/types/utils/common.d.ts +2 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/* eslint-disable sonarjs/no-nested-conditional */
|
|
2
|
+
/* eslint-disable sonarjs/cognitive-complexity */
|
|
3
|
+
// -------------------------------------------------------------
|
|
4
|
+
// Simplified `Where` type: each field may be a `Condition`
|
|
5
|
+
// *or* directly a `Document[K]`/`Document[K][]`, shorthand for "is"/"in".
|
|
6
|
+
// We also allow the special literal "KEY" to filter by the primary‐key column.
|
|
7
|
+
// -------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export interface Field {
|
|
10
|
+
readonly table: string
|
|
11
|
+
readonly field: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Condition<Document extends Record<string, unknown>, K extends keyof Document = keyof Document> {
|
|
15
|
+
readonly is?: Document[K] | Array<Document[K]>
|
|
16
|
+
readonly isNot?: Document[K] | Array<Document[K]>
|
|
17
|
+
readonly gt?: Document[K] | Array<Document[K]>
|
|
18
|
+
readonly gte?: Document[K] | Array<Document[K]>
|
|
19
|
+
readonly lt?: Document[K] | Array<Document[K]>
|
|
20
|
+
readonly lte?: Document[K] | Array<Document[K]>
|
|
21
|
+
readonly in?: Document[K] | Array<Document[K]>
|
|
22
|
+
readonly notIn?: Document[K] | Array<Document[K]>
|
|
23
|
+
readonly like?: Document[K] | Array<Document[K]>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* We extend `keyof Document` by the special literal "KEY".
|
|
28
|
+
* That means users can write `{ KEY: ... }` in addition to `{ someField: ... }`.
|
|
29
|
+
*
|
|
30
|
+
* - If K extends keyof Document, then primitive values must match Document[K].
|
|
31
|
+
* - If K === "KEY", then primitive values are treated as strings/Array<string>.
|
|
32
|
+
*/
|
|
33
|
+
export type Where<Document extends Record<string, unknown>> =
|
|
34
|
+
| {
|
|
35
|
+
[K in keyof Document | 'KEY']?:
|
|
36
|
+
| Condition<Document, K extends keyof Document ? K : keyof Document>
|
|
37
|
+
| (K extends keyof Document ? Document[K] : string)
|
|
38
|
+
| (K extends keyof Document ? Array<Document[K]> : string[])
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
readonly AND?: Array<Where<Document>>
|
|
42
|
+
readonly OR?: Array<Where<Document>>
|
|
43
|
+
readonly NOT?: Where<Document>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// -------------------------------------------------------------
|
|
47
|
+
// A tiny helper to escape/inline a single primitive into SQL.
|
|
48
|
+
// -------------------------------------------------------------
|
|
49
|
+
function inlineValue(value: unknown): string {
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
return `'${(value as string).split("'").join("''")}'`
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === 'number') {
|
|
54
|
+
return (value as number).toString()
|
|
55
|
+
}
|
|
56
|
+
if (typeof value === 'boolean') {
|
|
57
|
+
return (value as boolean) ? '1' : '0'
|
|
58
|
+
}
|
|
59
|
+
return `'${String(value).split("'").join("''")}'`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// -------------------------------------------------------------
|
|
63
|
+
// Build the expression for a given field.
|
|
64
|
+
// If field === "KEY", refer directly to the primary‐key column (`key`).
|
|
65
|
+
// Otherwise, extract from JSON `data`.
|
|
66
|
+
// -------------------------------------------------------------
|
|
67
|
+
function getFieldExpr(field: string, value: unknown, tableAlias?: string): string {
|
|
68
|
+
const prefix = tableAlias ? `${tableAlias}.` : ''
|
|
69
|
+
if (field === 'KEY') {
|
|
70
|
+
// Use double‐quotes around key to avoid conflicts with reserved words
|
|
71
|
+
return `"${prefix}key"`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Otherwise, treat as JSON field under "data"
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
return `CAST(json_extract(${prefix}data, '$.${field}') AS TEXT)`
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'boolean') {
|
|
79
|
+
return `CAST(json_extract(${prefix}data, '$.${field}') AS INTEGER)`
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === 'number') {
|
|
82
|
+
return `CAST(json_extract(${prefix}data, '$.${field}') AS NUMERIC)`
|
|
83
|
+
}
|
|
84
|
+
return `json_extract(${prefix}data, '$.${field}')`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------
|
|
88
|
+
// Valid operators set (for quick membership checks).
|
|
89
|
+
// -------------------------------------------------------------
|
|
90
|
+
const OPS_SET: ReadonlySet<string> = new Set(['is', 'isNot', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'like'])
|
|
91
|
+
|
|
92
|
+
function isUndefined(value: unknown): value is undefined {
|
|
93
|
+
return value === undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// -------------------------------------------------------------
|
|
97
|
+
// Main recursive parser: turn a `Where<Document>` into a SQL clause
|
|
98
|
+
// (without the leading "WHERE").
|
|
99
|
+
// -------------------------------------------------------------
|
|
100
|
+
export function getWhere<Document extends Record<string, unknown>>(where: Where<Document>, tableAlias?: string): string {
|
|
101
|
+
if (!where || typeof where !== 'object') {
|
|
102
|
+
return ''
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ----- Logical branches: AND / OR / NOT -----
|
|
106
|
+
if (!isUndefined(where.AND)) {
|
|
107
|
+
const array = where.AND as Array<Where<Document>>
|
|
108
|
+
if (Array.isArray(array) && array.length > 0) {
|
|
109
|
+
let combined = ''
|
|
110
|
+
let firstAdded = false
|
|
111
|
+
for (const sub of array) {
|
|
112
|
+
const clause = getWhere(sub, tableAlias)
|
|
113
|
+
if (!clause) continue
|
|
114
|
+
if (firstAdded) combined += ' AND '
|
|
115
|
+
combined += clause
|
|
116
|
+
firstAdded = true
|
|
117
|
+
}
|
|
118
|
+
return firstAdded ? `(${combined})` : ''
|
|
119
|
+
}
|
|
120
|
+
return ''
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!isUndefined(where.OR)) {
|
|
124
|
+
const array = where.OR as Array<Where<Document>>
|
|
125
|
+
if (Array.isArray(array) && array.length > 0) {
|
|
126
|
+
let combined = ''
|
|
127
|
+
let firstAdded = false
|
|
128
|
+
for (const sub of array) {
|
|
129
|
+
const clause = getWhere(sub, tableAlias)
|
|
130
|
+
if (!clause) continue
|
|
131
|
+
if (firstAdded) combined += ' OR '
|
|
132
|
+
combined += clause
|
|
133
|
+
firstAdded = true
|
|
134
|
+
}
|
|
135
|
+
return firstAdded ? `(${combined})` : ''
|
|
136
|
+
}
|
|
137
|
+
return ''
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!isUndefined(where.NOT)) {
|
|
141
|
+
const sub = where.NOT as Where<Document>
|
|
142
|
+
if (sub && typeof sub === 'object') {
|
|
143
|
+
const clause = getWhere(sub, tableAlias)
|
|
144
|
+
return clause ? `(NOT ${clause})` : ''
|
|
145
|
+
}
|
|
146
|
+
return ''
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ----- Field‐based conditions: default is AND across fields -----
|
|
150
|
+
let fieldClauses = ''
|
|
151
|
+
let anyFieldClause = false
|
|
152
|
+
|
|
153
|
+
for (const key in where as Record<string, unknown>) {
|
|
154
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') continue
|
|
155
|
+
|
|
156
|
+
const rawValue = (where as Record<string, unknown>)[key]
|
|
157
|
+
if (rawValue == null) continue
|
|
158
|
+
|
|
159
|
+
// If the user provided a primitive or an array, coerce it to a Condition:
|
|
160
|
+
// - single primitive → { is: rawVal }
|
|
161
|
+
// - array → { in: rawVal }
|
|
162
|
+
let cond: Condition<Document, typeof key>
|
|
163
|
+
if (typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
164
|
+
cond = Array.isArray(rawValue) ? { in: rawValue } : ({ is: rawValue } as Condition<Document, typeof key>)
|
|
165
|
+
} else {
|
|
166
|
+
cond = rawValue as Condition<Document, typeof key>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Iterate only over real operator keys that exist on this `cond`
|
|
170
|
+
for (const opKey of Object.keys(cond) as Array<keyof typeof cond>) {
|
|
171
|
+
if (!OPS_SET.has(opKey as string)) continue
|
|
172
|
+
const rawOpValue = cond[opKey]
|
|
173
|
+
if (rawOpValue == null) continue
|
|
174
|
+
|
|
175
|
+
// Always treat it as an array for uniformity:
|
|
176
|
+
const array = Array.isArray(rawOpValue) ? (rawOpValue as unknown[]) : [rawOpValue]
|
|
177
|
+
if (array.length === 0) continue
|
|
178
|
+
|
|
179
|
+
// Handle `is` / `isNot` / `in` / `notIn`
|
|
180
|
+
if (opKey === 'is' || opKey === 'isNot' || opKey === 'in' || opKey === 'notIn') {
|
|
181
|
+
const [firstValue] = array
|
|
182
|
+
const fieldExpr = getFieldExpr(key, firstValue, tableAlias)
|
|
183
|
+
|
|
184
|
+
// Build comma‐separated list without using `.map()`
|
|
185
|
+
let inList = ''
|
|
186
|
+
if (array.length > 1) {
|
|
187
|
+
for (const [index, elt] of array.entries()) {
|
|
188
|
+
if (index > 0) inList += ','
|
|
189
|
+
inList += inlineValue(elt)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
switch (opKey) {
|
|
194
|
+
case 'is': {
|
|
195
|
+
fieldClauses +=
|
|
196
|
+
array.length > 1
|
|
197
|
+
? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} IN (${inList})`
|
|
198
|
+
: (anyFieldClause ? ' AND ' : '') + `${fieldExpr} = ${inlineValue(array[0])}`
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
case 'isNot': {
|
|
202
|
+
fieldClauses +=
|
|
203
|
+
array.length > 1
|
|
204
|
+
? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} NOT IN (${inList})`
|
|
205
|
+
: (anyFieldClause ? ' AND ' : '') + `${fieldExpr} <> ${inlineValue(array[0])}`
|
|
206
|
+
break
|
|
207
|
+
}
|
|
208
|
+
case 'in': {
|
|
209
|
+
fieldClauses +=
|
|
210
|
+
array.length > 1
|
|
211
|
+
? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} IN (${inList})`
|
|
212
|
+
: (anyFieldClause ? ' AND ' : '') + `${fieldExpr} IN (${inlineValue(array[0])})`
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
case 'notIn': {
|
|
216
|
+
fieldClauses +=
|
|
217
|
+
array.length > 1
|
|
218
|
+
? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} NOT IN (${inList})`
|
|
219
|
+
: (anyFieldClause ? ' AND ' : '') + `${fieldExpr} NOT IN (${inlineValue(array[0])})`
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
anyFieldClause = true
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle comparisons: gt, gte, lt, lte, like
|
|
229
|
+
for (const v of array) {
|
|
230
|
+
const fieldExpr = getFieldExpr(key, v, tableAlias)
|
|
231
|
+
switch (opKey) {
|
|
232
|
+
case 'gt': {
|
|
233
|
+
fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} > ${inlineValue(v)}`
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
case 'gte': {
|
|
237
|
+
fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} >= ${inlineValue(v)}`
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
case 'lt': {
|
|
241
|
+
fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} < ${inlineValue(v)}`
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
case 'lte': {
|
|
245
|
+
fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} <= ${inlineValue(v)}`
|
|
246
|
+
break
|
|
247
|
+
}
|
|
248
|
+
case 'like': {
|
|
249
|
+
fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} LIKE ${inlineValue(v)}`
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
anyFieldClause = true
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return anyFieldClause ? `(${fieldClauses})` : ''
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// -------------------------------------------------------------
|
|
262
|
+
// Wrap `parse(...)` in "WHERE (…)". If empty, return "".
|
|
263
|
+
// -------------------------------------------------------------
|
|
264
|
+
export function getWhereQuery<Document extends Record<string, unknown>>(where: Where<Document>): string {
|
|
265
|
+
const clause = getWhere(where)
|
|
266
|
+
return clause ? `WHERE ${clause}` : ''
|
|
267
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useDebugValue, useId, useLayoutEffect, useMemo, type DependencyList } from 'react'
|
|
2
|
+
import type { SyncTable } from './create-sqlite'
|
|
3
|
+
import type { DocType } from './table/table.types'
|
|
4
|
+
import { isError, isPromise } from '../utils/is'
|
|
5
|
+
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
|
|
6
|
+
import type { SqlSeachOptions } from './select-sql'
|
|
7
|
+
|
|
8
|
+
export interface SqLiteActions {
|
|
9
|
+
readonly next: () => Promise<boolean>
|
|
10
|
+
readonly reset: () => Promise<void>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
14
|
+
/**
|
|
15
|
+
* Naive projection. Prefer specialized queries for heavy fan-out graphs.
|
|
16
|
+
*/
|
|
17
|
+
readonly select?: (document: Document) => Selected
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
21
|
+
state: SyncTable<Document>,
|
|
22
|
+
options: UseSearchOptions<Document, Selected> = {},
|
|
23
|
+
deps: DependencyList = [],
|
|
24
|
+
): [undefined extends Selected ? Document[] : Selected[], SqLiteActions] {
|
|
25
|
+
const { select } = options
|
|
26
|
+
|
|
27
|
+
const id = useId()
|
|
28
|
+
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
state.updateSearchOptions(id, { ...options, select: undefined })
|
|
31
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
+
}, deps)
|
|
33
|
+
|
|
34
|
+
const selector = useCallback(
|
|
35
|
+
(documents: Document[]) => {
|
|
36
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
37
|
+
return select ? documents.map(select) : (documents as unknown as Selected[])
|
|
38
|
+
},
|
|
39
|
+
[select],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const subscribe = useCallback(
|
|
43
|
+
(onStorageChange: () => void) => {
|
|
44
|
+
return state.subscribe(id, onStorageChange)
|
|
45
|
+
},
|
|
46
|
+
[state, id],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const getSnapshot = useCallback(() => {
|
|
50
|
+
return state.getSnapshot(id)
|
|
51
|
+
}, [state, id])
|
|
52
|
+
|
|
53
|
+
const value = useSyncExternalStoreWithSelector<Document[], Selected[]>(subscribe, getSnapshot, getSnapshot, selector)
|
|
54
|
+
|
|
55
|
+
useDebugValue(value)
|
|
56
|
+
if (isPromise(value)) {
|
|
57
|
+
throw value
|
|
58
|
+
}
|
|
59
|
+
if (isError(value)) {
|
|
60
|
+
throw value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const actions = useMemo((): SqLiteActions => {
|
|
64
|
+
return {
|
|
65
|
+
next: () => state.next(id),
|
|
66
|
+
reset: () => state.refresh(id),
|
|
67
|
+
}
|
|
68
|
+
}, [id, state])
|
|
69
|
+
return [value as undefined extends Selected ? Document[] : Selected[], actions]
|
|
70
|
+
}
|
package/src/types.ts
CHANGED
package/src/utils/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Cache, IsEqual } from '../types'
|
|
1
|
+
import type { Cache, IsEqual, State } from '../types'
|
|
2
2
|
import { isAbortError, isEqualBase, isPromise, isUndefined } from './is'
|
|
3
3
|
|
|
4
4
|
export interface CancelablePromise<T> {
|
|
@@ -46,7 +46,11 @@ export function canUpdate<T>(cache: Cache<T>, isEqual: IsEqual<T> = isEqualBase)
|
|
|
46
46
|
/**
|
|
47
47
|
* Handle async updates for `create` and `select`
|
|
48
48
|
*/
|
|
49
|
-
export function handleAsyncUpdate<T>(
|
|
49
|
+
export function handleAsyncUpdate<T>(state: State<T>, value: T): T {
|
|
50
|
+
const {
|
|
51
|
+
cache,
|
|
52
|
+
emitter: { emit },
|
|
53
|
+
} = state
|
|
50
54
|
if (!isPromise(value)) {
|
|
51
55
|
return value
|
|
52
56
|
}
|
package/types/create.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DefaultValue, IsEqual, State } from './types';
|
|
2
|
-
export declare const
|
|
3
|
-
add<T>(id: number, option: import("./scheduler").SchedulerOptions<T>): () => void;
|
|
4
|
-
schedule<T>(id: number, value: T): void;
|
|
2
|
+
export declare const STATE_SCHEDULER: {
|
|
3
|
+
add<T>(id: string | number | symbol, option: import("./scheduler").SchedulerOptions<T>): () => void;
|
|
4
|
+
schedule<T>(id: string | number | symbol, value: T): void;
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
7
|
* Create state from a default value.
|
package/types/scheduler.d.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
export declare const THRESHOLD = 0.2;
|
|
2
2
|
export declare const THRESHOLD_ITEMS = 10;
|
|
3
3
|
export declare const RESCHEDULE_COUNT = 0;
|
|
4
|
+
type ScheduleId = string | number | symbol;
|
|
4
5
|
export interface SchedulerOptions<T> {
|
|
5
6
|
readonly onResolveItem?: (item: T) => void;
|
|
6
|
-
readonly
|
|
7
|
+
readonly onScheduleDone: () => void | Promise<void>;
|
|
7
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* A simple scheduler to batch updates and avoid blocking the main thread
|
|
11
|
+
* It uses a combination of time-based and count-based strategies to determine when to flush the queue.
|
|
12
|
+
* - Time-based: If the time taken to process the current batch is less than a threshold (THRESHOLD), it continues processing.
|
|
13
|
+
* - Count-based: If the ScheduleId of items in the batch exceeds a certain limit (THRESHOLD_ITEMS), it defers processing to the next microtask.
|
|
14
|
+
* @returns An object with methods to add listeners and schedule tasks.
|
|
15
|
+
*/
|
|
8
16
|
export declare function createScheduler(): {
|
|
9
|
-
add<T>(id:
|
|
10
|
-
schedule<T>(id:
|
|
17
|
+
add<T>(id: ScheduleId, option: SchedulerOptions<T>): () => void;
|
|
18
|
+
schedule<T>(id: ScheduleId, value: T): void;
|
|
11
19
|
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CreateState } from './select-sql';
|
|
2
|
+
import type { DbOptions, DocType, Key, MutationResult, SearchOptions } from './table/table.types';
|
|
3
|
+
import type { Where } from './table/where';
|
|
4
|
+
type SearchId = string;
|
|
5
|
+
export interface SqLiteActions {
|
|
6
|
+
readonly next: () => Promise<boolean>;
|
|
7
|
+
readonly reset: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface SyncTable<Document extends DocType> {
|
|
10
|
+
readonly updateSearchOptions: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => void;
|
|
11
|
+
readonly subscribe: (searchId: SearchId, listener: () => void) => () => void;
|
|
12
|
+
readonly getSnapshot: (searchId: SearchId) => Document[];
|
|
13
|
+
readonly refresh: (searchId: SearchId) => Promise<void>;
|
|
14
|
+
readonly set: (document: Document) => Promise<MutationResult>;
|
|
15
|
+
readonly batchSet: (documents: Document[]) => Promise<MutationResult[]>;
|
|
16
|
+
readonly get: <Selected = Document>(key: Key, selector?: (document: Document) => Selected) => Promise<Selected | undefined>;
|
|
17
|
+
readonly delete: (key: Key) => Promise<MutationResult | undefined>;
|
|
18
|
+
readonly search: <Selected = Document>(options?: SearchOptions<Document, Selected>) => AsyncIterableIterator<Selected>;
|
|
19
|
+
readonly count: (options?: {
|
|
20
|
+
where?: Where<Document>;
|
|
21
|
+
}) => Promise<number>;
|
|
22
|
+
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>;
|
|
23
|
+
readonly destroy: () => void;
|
|
24
|
+
readonly next: (searchId: SearchId) => Promise<boolean>;
|
|
25
|
+
readonly select: <Params extends unknown[]>(compute: (...args: Params) => SearchOptions<Document>) => CreateState<Document, Params>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createSqliteState<Document extends DocType>(options: DbOptions<Document>): SyncTable<Document>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GetState } from '../types';
|
|
2
|
+
import type { SyncTable } from './create-sqlite';
|
|
3
|
+
import type { DocType } 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 sorBy?: keyof 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
|
+
export declare function selectSql<Document extends DocType, Params extends unknown[] = []>(state: SyncTable<Document>, compute: (...args: Params) => SqlSeachOptions<Document>): CreateState<Document, Params>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const IN_MEMORY_DB = ":memory:";
|
|
2
|
+
export interface QueryResult {
|
|
3
|
+
/** The number of rows affected by the query. */
|
|
4
|
+
rowsAffected: number;
|
|
5
|
+
/**
|
|
6
|
+
* The last inserted `id`.
|
|
7
|
+
*
|
|
8
|
+
* This value is not set for Postgres databases. If the
|
|
9
|
+
* last inserted id is required on Postgres, the `select` function
|
|
10
|
+
* must be used, with a `RETURNING` clause
|
|
11
|
+
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
|
|
12
|
+
*/
|
|
13
|
+
lastInsertId?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Backend {
|
|
16
|
+
execute: (query: string, bindValues?: unknown[]) => Promise<QueryResult>;
|
|
17
|
+
select: <T>(query: string, bindValues?: unknown[]) => Promise<T>;
|
|
18
|
+
transaction: (callback: (tx: Backend) => Promise<void>) => Promise<void>;
|
|
19
|
+
path: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Backend } from './backend';
|
|
2
|
+
import type { Where } from './where';
|
|
3
|
+
export type DocType = {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
};
|
|
6
|
+
export type KeyTypeAvailable = 'string' | 'number';
|
|
7
|
+
export interface DbOptions<Document extends DocType> {
|
|
8
|
+
readonly sorBy?: keyof Document;
|
|
9
|
+
readonly order?: 'asc' | 'desc';
|
|
10
|
+
readonly tableName: string;
|
|
11
|
+
readonly indexes?: Array<keyof Document>;
|
|
12
|
+
readonly backend: Backend;
|
|
13
|
+
/**
|
|
14
|
+
* Optional key. If omitted, the table uses implicit SQLite ROWID as the key.
|
|
15
|
+
*/
|
|
16
|
+
readonly key?: keyof Document;
|
|
17
|
+
}
|
|
18
|
+
interface DbNotGeneric {
|
|
19
|
+
readonly backend: Backend;
|
|
20
|
+
}
|
|
21
|
+
export interface SearchOptions<Document extends DocType, Selected = Document> {
|
|
22
|
+
readonly sorBy?: keyof Document;
|
|
23
|
+
readonly order?: 'asc' | 'desc';
|
|
24
|
+
readonly limit?: number;
|
|
25
|
+
readonly offset?: number;
|
|
26
|
+
readonly where?: Where<Document>;
|
|
27
|
+
readonly stepSize?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Naive projection. Prefer specialized queries for heavy fan-out graphs.
|
|
30
|
+
*/
|
|
31
|
+
readonly select?: (document: Document, meta: {
|
|
32
|
+
rowId: number;
|
|
33
|
+
}) => Selected;
|
|
34
|
+
}
|
|
35
|
+
export type Key = string | number;
|
|
36
|
+
export type MutationOp = 'insert' | 'update' | 'delete';
|
|
37
|
+
export interface MutationResult {
|
|
38
|
+
key: Key;
|
|
39
|
+
op: MutationOp;
|
|
40
|
+
}
|
|
41
|
+
export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
42
|
+
readonly set: (document: Document, backendOverride?: Backend) => Promise<MutationResult>;
|
|
43
|
+
readonly batchSet: (documents: Document[]) => Promise<MutationResult[]>;
|
|
44
|
+
readonly get: <Selected = Document>(key: Key, selector?: (document: Document) => Selected) => Promise<Selected | undefined>;
|
|
45
|
+
readonly delete: (key: Key) => Promise<MutationResult | undefined>;
|
|
46
|
+
readonly search: <Selected = Document>(options?: SearchOptions<Document, Selected>) => AsyncIterableIterator<Selected>;
|
|
47
|
+
readonly count: (options?: {
|
|
48
|
+
where?: Where<Document>;
|
|
49
|
+
}) => Promise<number>;
|
|
50
|
+
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>;
|
|
51
|
+
}
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface Field {
|
|
2
|
+
readonly table: string;
|
|
3
|
+
readonly field: string;
|
|
4
|
+
}
|
|
5
|
+
interface Condition<Document extends Record<string, unknown>, K extends keyof Document = keyof Document> {
|
|
6
|
+
readonly is?: Document[K] | Array<Document[K]>;
|
|
7
|
+
readonly isNot?: Document[K] | Array<Document[K]>;
|
|
8
|
+
readonly gt?: Document[K] | Array<Document[K]>;
|
|
9
|
+
readonly gte?: Document[K] | Array<Document[K]>;
|
|
10
|
+
readonly lt?: Document[K] | Array<Document[K]>;
|
|
11
|
+
readonly lte?: Document[K] | Array<Document[K]>;
|
|
12
|
+
readonly in?: Document[K] | Array<Document[K]>;
|
|
13
|
+
readonly notIn?: Document[K] | Array<Document[K]>;
|
|
14
|
+
readonly like?: Document[K] | Array<Document[K]>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* We extend `keyof Document` by the special literal "KEY".
|
|
18
|
+
* That means users can write `{ KEY: ... }` in addition to `{ someField: ... }`.
|
|
19
|
+
*
|
|
20
|
+
* - If K extends keyof Document, then primitive values must match Document[K].
|
|
21
|
+
* - If K === "KEY", then primitive values are treated as strings/Array<string>.
|
|
22
|
+
*/
|
|
23
|
+
export type Where<Document extends Record<string, unknown>> = {
|
|
24
|
+
[K in keyof Document | 'KEY']?: Condition<Document, K extends keyof Document ? K : keyof Document> | (K extends keyof Document ? Document[K] : string) | (K extends keyof Document ? Array<Document[K]> : string[]);
|
|
25
|
+
} | {
|
|
26
|
+
readonly AND?: Array<Where<Document>>;
|
|
27
|
+
readonly OR?: Array<Where<Document>>;
|
|
28
|
+
readonly NOT?: Where<Document>;
|
|
29
|
+
};
|
|
30
|
+
export declare function getWhere<Document extends Record<string, unknown>>(where: Where<Document>, tableAlias?: string): string;
|
|
31
|
+
export declare function getWhereQuery<Document extends Record<string, unknown>>(where: Where<Document>): string;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DependencyList } from 'react';
|
|
2
|
+
import type { SyncTable } from './create-sqlite';
|
|
3
|
+
import type { DocType } from './table/table.types';
|
|
4
|
+
import type { SqlSeachOptions } from './select-sql';
|
|
5
|
+
export interface SqLiteActions {
|
|
6
|
+
readonly next: () => Promise<boolean>;
|
|
7
|
+
readonly reset: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
10
|
+
/**
|
|
11
|
+
* Naive projection. Prefer specialized queries for heavy fan-out graphs.
|
|
12
|
+
*/
|
|
13
|
+
readonly select?: (document: Document) => Selected;
|
|
14
|
+
}
|
|
15
|
+
export declare function useSqliteValue<Document extends DocType, Selected = Document>(state: SyncTable<Document>, options?: UseSearchOptions<Document, Selected>, deps?: DependencyList): [undefined extends Selected ? Document[] : Selected[], SqLiteActions];
|
package/types/types.d.ts
CHANGED
package/types/utils/common.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Cache, IsEqual } from '../types';
|
|
1
|
+
import type { Cache, IsEqual, State } from '../types';
|
|
2
2
|
export interface CancelablePromise<T> {
|
|
3
3
|
promise: Promise<T>;
|
|
4
4
|
controller?: AbortController;
|
|
@@ -17,4 +17,4 @@ export declare function canUpdate<T>(cache: Cache<T>, isEqual?: IsEqual<T>): boo
|
|
|
17
17
|
/**
|
|
18
18
|
* Handle async updates for `create` and `select`
|
|
19
19
|
*/
|
|
20
|
-
export declare function handleAsyncUpdate<T>(
|
|
20
|
+
export declare function handleAsyncUpdate<T>(state: State<T>, value: T): T;
|