muya 2.5.4 → 2.5.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.
- package/{src/__tests__ → __tests__}/bench.test.tsx +4 -4
- package/{src/__tests__ → __tests__}/compare.test.tsx +8 -6
- package/{src/__tests__ → __tests__}/create.test.tsx +2 -2
- package/{src/utils/__tests__ → __tests__}/is.test.ts +3 -3
- package/{src/__tests__ → __tests__}/scheduler.test.tsx +1 -1
- package/{src/__tests__ → __tests__}/select.test.tsx +5 -5
- package/{src/utils/__tests__ → __tests__}/shallow.test.ts +1 -1
- package/{src/__tests__ → __tests__}/use-value-loadable.test.tsx +3 -3
- package/{src/__tests__ → __tests__}/use-value.test.tsx +8 -8
- package/build.ts +67 -0
- package/dist/cjs/index.js +1 -0
- package/dist/esm/create.js +1 -0
- package/dist/esm/debug/development-tools.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/select.js +1 -0
- package/{types → dist/types}/create-state.d.ts +1 -0
- package/dist/types/create-state.d.ts.map +1 -0
- package/{types → dist/types}/create.d.ts +1 -0
- package/dist/types/create.d.ts.map +1 -0
- package/dist/types/debug/development-tools.d.ts +13 -0
- package/dist/types/debug/development-tools.d.ts.map +1 -0
- package/{types → dist/types}/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -0
- package/{types → dist/types}/scheduler.d.ts +1 -0
- package/dist/types/scheduler.d.ts.map +1 -0
- package/{types → dist/types}/select.d.ts +1 -0
- package/dist/types/select.d.ts.map +1 -0
- package/{types → dist/types}/types.d.ts +1 -0
- package/dist/types/types.d.ts.map +1 -0
- package/{types → dist/types}/use-value-loadable.d.ts +1 -0
- package/dist/types/use-value-loadable.d.ts.map +1 -0
- package/{types → dist/types}/use-value.d.ts +2 -1
- package/dist/types/use-value.d.ts.map +1 -0
- package/{types → dist/types}/utils/common.d.ts +1 -0
- package/dist/types/utils/common.d.ts.map +1 -0
- package/{types → dist/types}/utils/create-emitter.d.ts +1 -0
- package/dist/types/utils/create-emitter.d.ts.map +1 -0
- package/{types → dist/types}/utils/id.d.ts +1 -0
- package/dist/types/utils/id.d.ts.map +1 -0
- package/{types → dist/types}/utils/is.d.ts +1 -0
- package/dist/types/utils/is.d.ts.map +1 -0
- package/{types → dist/types}/utils/shallow.d.ts +1 -0
- package/dist/types/utils/shallow.d.ts.map +1 -0
- package/package.json +23 -8
- package/src/create-state.d.ts.map +1 -0
- package/src/create.d.ts.map +1 -0
- package/src/create.ts +7 -2
- package/src/debug/development-tools.d.ts.map +1 -0
- package/src/debug/development-tools.ts +5 -40
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +2 -1
- package/src/scheduler.d.ts.map +1 -0
- package/src/select.d.ts.map +1 -0
- package/src/select.ts +7 -2
- package/src/types.d.ts.map +1 -0
- package/src/use-value-loadable.d.ts.map +1 -0
- package/src/use-value.d.ts.map +1 -0
- package/src/use-value.ts +1 -1
- package/src/utils/common.d.ts.map +1 -0
- package/src/utils/create-emitter.d.ts.map +1 -0
- package/src/utils/id.d.ts.map +1 -0
- package/src/utils/is.d.ts.map +1 -0
- package/src/utils/shallow.d.ts.map +1 -0
- package/tsconfig.build.json +12 -0
- package/cjs/index.js +0 -1
- package/esm/__tests__/test-utils.js +0 -1
- package/esm/create.js +0 -1
- package/esm/debug/development-tools.js +0 -1
- package/esm/index.js +0 -1
- package/esm/select.js +0 -1
- package/esm/sqlite/__tests__/create-sqlite.test.js +0 -1
- package/esm/sqlite/__tests__/map-deque.test.js +0 -1
- package/esm/sqlite/__tests__/table.test.js +0 -1
- package/esm/sqlite/__tests__/tokenizer.test.js +0 -1
- package/esm/sqlite/__tests__/where.test.js +0 -1
- package/esm/sqlite/create-sqlite.js +0 -1
- package/esm/sqlite/index.js +0 -1
- package/esm/sqlite/table/backend.js +0 -1
- package/esm/sqlite/table/bun-backend.js +0 -1
- package/esm/sqlite/table/index.js +0 -1
- package/esm/sqlite/table/map-deque.js +0 -1
- package/esm/sqlite/table/table.js +0 -43
- package/esm/sqlite/table/table.types.js +0 -0
- package/esm/sqlite/table/tokenizer.js +0 -1
- package/esm/sqlite/table/where.js +0 -1
- package/esm/sqlite/use-sqlite-count.js +0 -1
- package/esm/sqlite/use-sqlite.js +0 -1
- package/esm/utils/__tests__/is.test.js +0 -1
- package/esm/utils/__tests__/shallow.test.js +0 -1
- package/src/sqlite/__tests__/create-sqlite.test.ts +0 -264
- package/src/sqlite/__tests__/map-deque.test.ts +0 -61
- package/src/sqlite/__tests__/table.test.ts +0 -351
- package/src/sqlite/__tests__/tokenizer.test.ts +0 -43
- package/src/sqlite/__tests__/use-slite-count.test.tsx +0 -96
- package/src/sqlite/__tests__/use-sqlite.more.test.tsx +0 -637
- package/src/sqlite/__tests__/use-sqlite.test.tsx +0 -1008
- package/src/sqlite/__tests__/where.test.ts +0 -234
- package/src/sqlite/create-sqlite.ts +0 -164
- package/src/sqlite/index.ts +0 -4
- package/src/sqlite/table/backend.ts +0 -21
- package/src/sqlite/table/bun-backend.ts +0 -47
- package/src/sqlite/table/index.ts +0 -6
- package/src/sqlite/table/map-deque.ts +0 -29
- package/src/sqlite/table/table.ts +0 -353
- package/src/sqlite/table/table.types.ts +0 -129
- package/src/sqlite/table/tokenizer.ts +0 -35
- package/src/sqlite/table/where.ts +0 -207
- package/src/sqlite/use-sqlite-count.ts +0 -69
- package/src/sqlite/use-sqlite.ts +0 -250
- package/types/__tests__/test-utils.d.ts +0 -25
- package/types/debug/development-tools.d.ts +0 -8
- package/types/sqlite/create-sqlite.d.ts +0 -31
- package/types/sqlite/index.d.ts +0 -4
- package/types/sqlite/table/backend.d.ts +0 -20
- package/types/sqlite/table/bun-backend.d.ts +0 -6
- package/types/sqlite/table/index.d.ts +0 -6
- package/types/sqlite/table/map-deque.d.ts +0 -5
- package/types/sqlite/table/table.d.ts +0 -21
- package/types/sqlite/table/table.types.d.ts +0 -91
- package/types/sqlite/table/tokenizer.d.ts +0 -11
- package/types/sqlite/table/where.d.ts +0 -37
- package/types/sqlite/use-sqlite-count.d.ts +0 -17
- package/types/sqlite/use-sqlite.d.ts +0 -39
- /package/{src/__tests__ → __tests__}/test-utils.ts +0 -0
- /package/{esm → dist/esm}/create-state.js +0 -0
- /package/{esm → dist/esm}/scheduler.js +0 -0
- /package/{esm → dist/esm}/types.js +0 -0
- /package/{esm → dist/esm}/use-value-loadable.js +0 -0
- /package/{esm → dist/esm}/use-value.js +0 -0
- /package/{esm → dist/esm}/utils/common.js +0 -0
- /package/{esm → dist/esm}/utils/create-emitter.js +0 -0
- /package/{esm → dist/esm}/utils/id.js +0 -0
- /package/{esm → dist/esm}/utils/is.js +0 -0
- /package/{esm → dist/esm}/utils/shallow.js +0 -0
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/* eslint-disable unicorn/no-array-callback-reference */
|
|
2
|
-
/* eslint-disable unicorn/no-nested-ternary */
|
|
3
|
-
/* eslint-disable no-nested-ternary */
|
|
4
|
-
/* eslint-disable sonarjs/no-nested-conditional */
|
|
5
|
-
/* eslint-disable sonarjs/cognitive-complexity */
|
|
6
|
-
|
|
7
|
-
import type { MakeAllFieldAsRequired } from './table.types'
|
|
8
|
-
|
|
9
|
-
// -------------------------------------------------------------
|
|
10
|
-
// Condition operators for each field
|
|
11
|
-
// -------------------------------------------------------------
|
|
12
|
-
interface Condition<T> {
|
|
13
|
-
readonly is?: T | T[]
|
|
14
|
-
readonly isNot?: T | T[]
|
|
15
|
-
readonly gt?: T
|
|
16
|
-
readonly gte?: T
|
|
17
|
-
readonly lt?: T
|
|
18
|
-
readonly lte?: T
|
|
19
|
-
readonly in?: T[]
|
|
20
|
-
readonly notIn?: T[]
|
|
21
|
-
readonly like?: T | T[]
|
|
22
|
-
readonly fts?: string | string[] // 🔥 NEW
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type WhereRaw<T extends Record<string, unknown>> =
|
|
26
|
-
| {
|
|
27
|
-
[K in keyof T]?: T[K] extends Record<string, unknown>
|
|
28
|
-
? WhereRaw<T[K]> // nested object
|
|
29
|
-
: Condition<T[K]> | T[K] | T[K][]
|
|
30
|
-
}
|
|
31
|
-
| {
|
|
32
|
-
readonly AND?: Array<WhereRaw<T>>
|
|
33
|
-
readonly OR?: Array<WhereRaw<T>>
|
|
34
|
-
readonly NOT?: WhereRaw<T>
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// -------------------------------------------------------------
|
|
38
|
-
// Where type: recursive object with operators or nested fields
|
|
39
|
-
// -------------------------------------------------------------
|
|
40
|
-
export type Where<T extends Record<string, unknown>> = WhereRaw<MakeAllFieldAsRequired<T>>
|
|
41
|
-
/**
|
|
42
|
-
* Inline a value for SQL query, with proper escaping for strings
|
|
43
|
-
* @param value The value to inline
|
|
44
|
-
* @returns The inlined value as a string
|
|
45
|
-
*/
|
|
46
|
-
function inlineValue(value: unknown): string {
|
|
47
|
-
if (typeof value === 'string') return `'${value.replaceAll("'", "''")}'`
|
|
48
|
-
if (typeof value === 'number') return value.toString()
|
|
49
|
-
if (typeof value === 'boolean') return value ? '1' : '0'
|
|
50
|
-
return `'${String(value).replaceAll("'", "''")}'`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Get SQL expression for a field, with proper casting based on value type
|
|
55
|
-
* @param field The field name
|
|
56
|
-
* @param value The field value
|
|
57
|
-
* @param tableAlias Optional table alias to prefix the field name
|
|
58
|
-
* @returns The SQL expression for the field
|
|
59
|
-
*/
|
|
60
|
-
function getFieldExpr(field: string, value: unknown, tableAlias?: string): string {
|
|
61
|
-
const prefix = tableAlias ? `${tableAlias}.` : ''
|
|
62
|
-
if (field === 'KEY') return `"${prefix}key"`
|
|
63
|
-
if (typeof value === 'string') return `CAST(json_extract(${prefix}data, '$.${field}') AS TEXT)`
|
|
64
|
-
if (typeof value === 'number') return `CAST(json_extract(${prefix}data, '$.${field}') AS NUMERIC)`
|
|
65
|
-
if (typeof value === 'boolean') return `CAST(json_extract(${prefix}data, '$.${field}') AS INTEGER)`
|
|
66
|
-
return `json_extract(${prefix}data, '$.${field}')`
|
|
67
|
-
}
|
|
68
|
-
const OPS_SET: ReadonlySet<string> = new Set(['is', 'isNot', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'like', 'fts'])
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Flatten a nested Where object into a single-level object with dot-separated keys
|
|
72
|
-
* @param object The nested Where object
|
|
73
|
-
* @param prefix The prefix for the current recursion level (used internally)
|
|
74
|
-
* @returns A flattened object with dot-separated keys
|
|
75
|
-
*/
|
|
76
|
-
function flattenWhere(object: Record<string, unknown>, prefix = ''): Record<string, unknown> {
|
|
77
|
-
const result: Record<string, unknown> = {}
|
|
78
|
-
|
|
79
|
-
for (const [k, v] of Object.entries(object)) {
|
|
80
|
-
if (k === 'AND' || k === 'OR' || k === 'NOT') {
|
|
81
|
-
result[k] = v
|
|
82
|
-
continue
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const path = prefix ? `${prefix}.${k}` : k
|
|
86
|
-
|
|
87
|
-
if (v && typeof v === 'object' && !Array.isArray(v) && !Object.keys(v).some((kk) => OPS_SET.has(kk))) {
|
|
88
|
-
Object.assign(result, flattenWhere(v as Record<string, unknown>, path))
|
|
89
|
-
} else {
|
|
90
|
-
result[path] = v
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return result
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Write SQL WHERE clause from a Where object
|
|
99
|
-
* @param where The Where object defining the conditions
|
|
100
|
-
* @param tableAlias Optional table alias to prefix field names
|
|
101
|
-
* @param tableName Optional table name (required for FTS conditions)
|
|
102
|
-
* @returns The SQL WHERE clause string (without the "WHERE" keyword)
|
|
103
|
-
*/
|
|
104
|
-
export function getWhere<T extends Record<string, unknown>>(where: Where<T>, tableAlias?: string, tableName?: string): string {
|
|
105
|
-
if (!where || typeof where !== 'object') return ''
|
|
106
|
-
|
|
107
|
-
if (where.AND) {
|
|
108
|
-
const clauses = Array.isArray(where.AND) ? where.AND.map((w) => getWhere(w, tableAlias, tableName)).filter(Boolean) : []
|
|
109
|
-
return clauses.length > 0 ? `(${clauses.join(' AND ')})` : ''
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (where.OR) {
|
|
113
|
-
const clauses = Array.isArray(where.OR) ? where.OR.map((w) => getWhere(w, tableAlias, tableName)).filter(Boolean) : []
|
|
114
|
-
return clauses.length > 0 ? `(${clauses.join(' OR ')})` : ''
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (where.NOT) {
|
|
118
|
-
const clause = getWhere(where.NOT, tableAlias, tableName)
|
|
119
|
-
return clause ? `(NOT ${clause})` : ''
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const flat = flattenWhere(where as Record<string, unknown>)
|
|
123
|
-
let fieldClauses = ''
|
|
124
|
-
let anyField = false
|
|
125
|
-
|
|
126
|
-
for (const [key, rawValue] of Object.entries(flat)) {
|
|
127
|
-
if (rawValue == null) continue
|
|
128
|
-
|
|
129
|
-
let cond: Condition<unknown>
|
|
130
|
-
if (typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
131
|
-
cond = Array.isArray(rawValue) ? { in: rawValue } : { is: rawValue }
|
|
132
|
-
} else {
|
|
133
|
-
cond = rawValue as Condition<unknown>
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
for (const opKey of Object.keys(cond)) {
|
|
137
|
-
const opValue = cond[opKey as keyof Condition<unknown>]
|
|
138
|
-
if (opValue == null) continue
|
|
139
|
-
|
|
140
|
-
const values = Array.isArray(opValue) ? opValue : [opValue]
|
|
141
|
-
if (values.length === 0) continue
|
|
142
|
-
|
|
143
|
-
if (opKey === 'fts') {
|
|
144
|
-
if (!tableName) throw new Error('FTS requires tableName for JOIN reference')
|
|
145
|
-
const clause = values
|
|
146
|
-
.map(
|
|
147
|
-
(v) =>
|
|
148
|
-
`EXISTS (SELECT 1 FROM ${tableName}_fts f WHERE f.rowid = ${tableAlias ?? tableName}.rowid AND ${tableName}_fts MATCH ${inlineValue(v)})`,
|
|
149
|
-
)
|
|
150
|
-
.join(' AND ')
|
|
151
|
-
fieldClauses += (anyField ? ' AND ' : '') + clause
|
|
152
|
-
anyField = true
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (opKey === 'is' || opKey === 'isNot' || opKey === 'in' || opKey === 'notIn') {
|
|
157
|
-
const fieldExpr = getFieldExpr(key, values[0], tableAlias)
|
|
158
|
-
const inList = values.map(inlineValue).join(',')
|
|
159
|
-
const clause =
|
|
160
|
-
opKey === 'is'
|
|
161
|
-
? values.length > 1
|
|
162
|
-
? `${fieldExpr} IN (${inList})`
|
|
163
|
-
: `${fieldExpr} = ${inlineValue(values[0])}`
|
|
164
|
-
: opKey === 'isNot'
|
|
165
|
-
? values.length > 1
|
|
166
|
-
? `${fieldExpr} NOT IN (${inList})`
|
|
167
|
-
: `${fieldExpr} <> ${inlineValue(values[0])}`
|
|
168
|
-
: opKey === 'in'
|
|
169
|
-
? `${fieldExpr} IN (${inList})`
|
|
170
|
-
: `${fieldExpr} NOT IN (${inList})`
|
|
171
|
-
fieldClauses += (anyField ? ' AND ' : '') + clause
|
|
172
|
-
anyField = true
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
for (const v of values) {
|
|
177
|
-
const fieldExpr = getFieldExpr(key, v, tableAlias)
|
|
178
|
-
const clause =
|
|
179
|
-
opKey === 'gt'
|
|
180
|
-
? `${fieldExpr} > ${inlineValue(v)}`
|
|
181
|
-
: opKey === 'gte'
|
|
182
|
-
? `${fieldExpr} >= ${inlineValue(v)}`
|
|
183
|
-
: opKey === 'lt'
|
|
184
|
-
? `${fieldExpr} < ${inlineValue(v)}`
|
|
185
|
-
: opKey === 'lte'
|
|
186
|
-
? `${fieldExpr} <= ${inlineValue(v)}`
|
|
187
|
-
: `${fieldExpr} LIKE ${inlineValue(v)}`
|
|
188
|
-
fieldClauses += (anyField ? ' AND ' : '') + clause
|
|
189
|
-
anyField = true
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return anyField ? `(${fieldClauses})` : ''
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Get SQL WHERE clause from a Where object
|
|
199
|
-
* @param where The Where object defining the conditions
|
|
200
|
-
* @param tableName Optional table name (required for FTS conditions)
|
|
201
|
-
* @returns The SQL WHERE clause string (without the "WHERE" keyword)
|
|
202
|
-
*/
|
|
203
|
-
export function getWhereQuery<T extends Record<string, unknown>>(where?: Where<T>, tableName?: string): string {
|
|
204
|
-
if (!where) return ''
|
|
205
|
-
const clause = getWhere(where, undefined, tableName)
|
|
206
|
-
return clause ? `WHERE ${clause}` : ''
|
|
207
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
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
|
-
}
|
package/src/sqlite/use-sqlite.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/* eslint-disable sonarjs/cognitive-complexity */
|
|
2
|
-
import { useCallback, useLayoutEffect, useReducer, useRef, useState, type DependencyList } from 'react'
|
|
3
|
-
import type { SyncTable } from './create-sqlite'
|
|
4
|
-
import type { DocType, Key, SqlSeachOptions } from './table/table.types'
|
|
5
|
-
import { DEFAULT_PAGE_SIZE } from './table'
|
|
6
|
-
import { shallow } from '../utils/shallow'
|
|
7
|
-
const MAX_ITERATIONS = 10_000
|
|
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
|
-
|
|
23
|
-
export interface SqLiteActions {
|
|
24
|
-
/**
|
|
25
|
-
* Load the next page of results and return if isDone to show more results.
|
|
26
|
-
* @returns isDone: boolean
|
|
27
|
-
*/
|
|
28
|
-
readonly nextPage: () => Promise<boolean>
|
|
29
|
-
/**
|
|
30
|
-
* Reset the pagination and load the first page of results.
|
|
31
|
-
* @returns void
|
|
32
|
-
*/
|
|
33
|
-
readonly reset: () => Promise<void>
|
|
34
|
-
/**
|
|
35
|
-
* Map of document keys to their index in the results array.
|
|
36
|
-
*/
|
|
37
|
-
readonly keysIndex: Map<Key, number>
|
|
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
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
46
|
-
/**
|
|
47
|
-
* Naive projection. Prefer specialized queries for heavy fan-out graphs.
|
|
48
|
-
*/
|
|
49
|
-
readonly select?: (document: Document) => Selected
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A React hook to perform paginated searches on a SyncTable and reactively update the results.
|
|
54
|
-
* It supports pagination, resetting the search, and selecting specific fields from the documents.
|
|
55
|
-
* @param state The SyncTable instance to perform searches on.
|
|
56
|
-
* @param options Options to customize the search behavior, including pagination size and selection function.
|
|
57
|
-
* @param deps Dependency list to control when to re-run the search and reset the iterator.
|
|
58
|
-
* @returns A tuple containing the current list of results and an object with actions to manage pagination and resetting.
|
|
59
|
-
*/
|
|
60
|
-
export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
61
|
-
state: SyncTable<Document>,
|
|
62
|
-
options: UseSearchOptions<Document, Selected> = {},
|
|
63
|
-
deps: DependencyList = [],
|
|
64
|
-
): [(undefined extends Selected ? Document[] : Selected[]) | null, SqLiteActions] {
|
|
65
|
-
const { select, pageSize = DEFAULT_PAGE_SIZE } = options
|
|
66
|
-
|
|
67
|
-
const itemsRef = useRef<null | (Document | Selected)[]>(null)
|
|
68
|
-
const [, rerender] = useReducer((c: number) => c + 1, 0)
|
|
69
|
-
const keysIndex = useRef(new Map<Key, number>())
|
|
70
|
-
const iteratorRef = useRef<AsyncIterableIterator<{ doc: Document; meta: { key: Key } }> | null>(null)
|
|
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
|
-
|
|
77
|
-
const updateIterator = useCallback(() => {
|
|
78
|
-
// eslint-disable-next-line sonarjs/no-unused-vars
|
|
79
|
-
const { select: _ignore, ...resetOptions } = options
|
|
80
|
-
iteratorRef.current = state.search({ select: (doc, meta) => ({ doc, meta }), ...resetOptions })
|
|
81
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
-
}, [state, ...deps])
|
|
83
|
-
|
|
84
|
-
const resetDataAndUpdateIterator = useCallback(() => {
|
|
85
|
-
itemsRef.current = []
|
|
86
|
-
keysIndex.current.clear()
|
|
87
|
-
updateIterator()
|
|
88
|
-
}, [updateIterator])
|
|
89
|
-
|
|
90
|
-
const fillNextPage = useCallback(async (shouldReset: boolean) => {
|
|
91
|
-
if (itemsRef.current === null) {
|
|
92
|
-
itemsRef.current = []
|
|
93
|
-
}
|
|
94
|
-
if (shouldReset === true) {
|
|
95
|
-
resetDataAndUpdateIterator()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const { current: iterator } = iteratorRef
|
|
99
|
-
if (!iterator) {
|
|
100
|
-
return true
|
|
101
|
-
}
|
|
102
|
-
let isDone = false
|
|
103
|
-
|
|
104
|
-
for (let index = 0; index < pageSize; index++) {
|
|
105
|
-
const result = await iterator.next()
|
|
106
|
-
if (result.done) {
|
|
107
|
-
iteratorRef.current = null
|
|
108
|
-
isDone = true
|
|
109
|
-
break
|
|
110
|
-
}
|
|
111
|
-
if (keysIndex.current.has(result.value.meta.key)) {
|
|
112
|
-
continue
|
|
113
|
-
}
|
|
114
|
-
itemsRef.current.push(select ? select(result.value.doc) : (result.value.doc as unknown as Selected))
|
|
115
|
-
keysIndex.current.set(result.value.meta.key, itemsRef.current.length - 1)
|
|
116
|
-
}
|
|
117
|
-
itemsRef.current = [...itemsRef.current]
|
|
118
|
-
return isDone
|
|
119
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
120
|
-
}, [])
|
|
121
|
-
|
|
122
|
-
const nextPage = useCallback(async () => {
|
|
123
|
-
const isDone = await fillNextPage(false)
|
|
124
|
-
rerender()
|
|
125
|
-
return isDone
|
|
126
|
-
}, [fillNextPage])
|
|
127
|
-
|
|
128
|
-
useLayoutEffect(() => {
|
|
129
|
-
const unsubscribe = state.subscribe(async (item) => {
|
|
130
|
-
const { mutations, removedAll } = item
|
|
131
|
-
if (removedAll) {
|
|
132
|
-
resetDataAndUpdateIterator()
|
|
133
|
-
}
|
|
134
|
-
if (!mutations) {
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const oldLength = itemsRef.current?.length ?? 0
|
|
139
|
-
let newLength = oldLength
|
|
140
|
-
let hasUpdate = false
|
|
141
|
-
const removeIndexes = new Set<number>()
|
|
142
|
-
for (const mutation of mutations) {
|
|
143
|
-
const { key, op, document } = mutation
|
|
144
|
-
switch (op) {
|
|
145
|
-
case 'insert': {
|
|
146
|
-
newLength += 1
|
|
147
|
-
break
|
|
148
|
-
}
|
|
149
|
-
case 'delete': {
|
|
150
|
-
if (itemsRef.current && itemsRef.current.length > 0 && keysIndex.current.has(key)) {
|
|
151
|
-
const index = keysIndex.current.get(key)
|
|
152
|
-
if (index === undefined) break
|
|
153
|
-
removeIndexes.add(index)
|
|
154
|
-
hasUpdate = true
|
|
155
|
-
}
|
|
156
|
-
break
|
|
157
|
-
}
|
|
158
|
-
case 'update': {
|
|
159
|
-
if (keysIndex.current.has(key)) {
|
|
160
|
-
const index = keysIndex.current.get(key)
|
|
161
|
-
if (index !== undefined && itemsRef.current) {
|
|
162
|
-
const newItem = select ? select(document as Document) : (document as unknown as Selected)
|
|
163
|
-
const previousItem = itemsRef.current[index]
|
|
164
|
-
|
|
165
|
-
// 🆕 Only update & rerender if shallow comparison fails
|
|
166
|
-
if (!shallow(previousItem, newItem)) {
|
|
167
|
-
itemsRef.current[index] = newItem
|
|
168
|
-
itemsRef.current = [...itemsRef.current]
|
|
169
|
-
hasUpdate = true
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
} else {
|
|
173
|
-
// Handle updates to non-visible items
|
|
174
|
-
const updatedItem = await state.get(key, select)
|
|
175
|
-
if (updatedItem) {
|
|
176
|
-
itemsRef.current = [...(itemsRef.current ?? []), updatedItem]
|
|
177
|
-
keysIndex.current.set(key, itemsRef.current.length - 1)
|
|
178
|
-
hasUpdate = true
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
break
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (removeIndexes.size > 0 && itemsRef.current && itemsRef.current.length > 0) {
|
|
187
|
-
const newIndex = new Map<Key, number>()
|
|
188
|
-
itemsRef.current = itemsRef.current?.filter((_, index) => {
|
|
189
|
-
return !removeIndexes.has(index)
|
|
190
|
-
})
|
|
191
|
-
let newIdx = 0
|
|
192
|
-
for (const [key, index] of keysIndex.current) {
|
|
193
|
-
if (removeIndexes.has(index)) {
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
newIndex.set(key, newIdx)
|
|
197
|
-
newIdx++
|
|
198
|
-
}
|
|
199
|
-
keysIndex.current = newIndex
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const isLengthChanged = oldLength !== newLength
|
|
203
|
-
const isChanged = isLengthChanged || hasUpdate
|
|
204
|
-
if (!isChanged) return
|
|
205
|
-
if (isLengthChanged) {
|
|
206
|
-
await fillNextPage(true)
|
|
207
|
-
let iterations = 0
|
|
208
|
-
while ((itemsRef.current?.length ?? 0) < newLength && iterations < MAX_ITERATIONS) {
|
|
209
|
-
await fillNextPage(false)
|
|
210
|
-
iterations++
|
|
211
|
-
}
|
|
212
|
-
if (iterations === MAX_ITERATIONS) {
|
|
213
|
-
// Optionally log a warning to help with debugging
|
|
214
|
-
// eslint-disable-next-line no-console
|
|
215
|
-
console.warn('Reached maximum iterations in fillNextPage loop. Possible duplicate or data issue.')
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
rerender()
|
|
219
|
-
})
|
|
220
|
-
return () => {
|
|
221
|
-
unsubscribe()
|
|
222
|
-
}
|
|
223
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
224
|
-
}, [state])
|
|
225
|
-
|
|
226
|
-
useLayoutEffect(() => {
|
|
227
|
-
// Capture current deps for this effect invocation
|
|
228
|
-
const currentDeps = deps
|
|
229
|
-
resetDataAndUpdateIterator()
|
|
230
|
-
nextPage().then(() => {
|
|
231
|
-
// Mark these deps as settled when data finishes loading
|
|
232
|
-
setSettledDeps(currentDeps)
|
|
233
|
-
})
|
|
234
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
-
}, deps)
|
|
236
|
-
|
|
237
|
-
const resetCb = useCallback(async () => {
|
|
238
|
-
// Set settledDeps to null to make isStale=true during reset
|
|
239
|
-
setSettledDeps(null)
|
|
240
|
-
resetDataAndUpdateIterator()
|
|
241
|
-
await nextPage()
|
|
242
|
-
// After data loads, mark current deps as settled
|
|
243
|
-
setSettledDeps(deps)
|
|
244
|
-
}, [nextPage, resetDataAndUpdateIterator, deps])
|
|
245
|
-
|
|
246
|
-
return [itemsRef.current, { nextPage, reset: resetCb, keysIndex: keysIndex.current, isStale }] as [
|
|
247
|
-
(undefined extends Selected ? Document[] : Selected[]) | null,
|
|
248
|
-
SqLiteActions,
|
|
249
|
-
]
|
|
250
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Component } from 'react';
|
|
2
|
-
/**
|
|
3
|
-
* Create a promise that resolves after a specified time
|
|
4
|
-
* @param time Time in ms to wait
|
|
5
|
-
* @returns A promise that resolves after the specified time
|
|
6
|
-
*/
|
|
7
|
-
export declare function longPromise(time?: number): Promise<number>;
|
|
8
|
-
export declare class ErrorBoundary extends Component<{
|
|
9
|
-
fallback: React.ReactNode;
|
|
10
|
-
children: React.ReactNode;
|
|
11
|
-
}, {
|
|
12
|
-
hasError: boolean;
|
|
13
|
-
error: Error | null;
|
|
14
|
-
}> {
|
|
15
|
-
constructor(props: {
|
|
16
|
-
fallback: React.ReactNode;
|
|
17
|
-
children: React.ReactNode;
|
|
18
|
-
});
|
|
19
|
-
static getDerivedStateFromError(error: Error): {
|
|
20
|
-
hasError: boolean;
|
|
21
|
-
error: Error;
|
|
22
|
-
};
|
|
23
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
|
|
24
|
-
render(): import("react").ReactNode;
|
|
25
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { GetState, State } from '../types';
|
|
2
|
-
export type StateType = 'state' | 'derived';
|
|
3
|
-
/**
|
|
4
|
-
* Subscribe a state to development tools if available
|
|
5
|
-
* @param state The state to subscribe
|
|
6
|
-
* @returns A function to unsubscribe from development tools
|
|
7
|
-
*/
|
|
8
|
-
export declare function subscribeToDevelopmentTools<T>(state: State<T> | GetState<T>): (() => void) | undefined;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { Backend } from './table';
|
|
2
|
-
import type { DbOptions, DocType, DotPath, GetFieldType, GroupByOptions, GroupByResult, Key, MutationResult, SearchOptions } from './table/table.types';
|
|
3
|
-
import type { Where } from './table/where';
|
|
4
|
-
export interface CreateSqliteOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
|
|
5
|
-
readonly backend: Backend | Promise<Backend>;
|
|
6
|
-
}
|
|
7
|
-
export interface MutationItems<Doc> {
|
|
8
|
-
mutations?: MutationResult<Doc>[];
|
|
9
|
-
removedAll?: boolean;
|
|
10
|
-
}
|
|
11
|
-
export interface SyncTable<Document extends DocType> {
|
|
12
|
-
readonly subscribe: (listener: (mutation: MutationItems<Document>) => void) => () => void;
|
|
13
|
-
readonly set: (document: Document) => Promise<MutationResult<Document>>;
|
|
14
|
-
readonly batchSet: (documents: Document[]) => Promise<MutationResult<Document>[]>;
|
|
15
|
-
readonly batchDelete: (keys: Key[]) => Promise<MutationResult<Document>[]>;
|
|
16
|
-
readonly get: <Selected = Document>(key: Key, selector?: (document: Document) => Selected) => Promise<Selected | undefined>;
|
|
17
|
-
readonly delete: (key: Key) => Promise<MutationResult<Document> | 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<Document>[]>;
|
|
23
|
-
readonly clear: () => Promise<void>;
|
|
24
|
-
readonly groupBy: <Field extends DotPath<Document>>(field: Field, options?: GroupByOptions<Document>) => Promise<Array<GroupByResult<GetFieldType<Document, Field>>>>;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Create a SyncTable that wraps a Table and provides reactive capabilities
|
|
28
|
-
* @param options Options to create the SyncTable, including the backend and table name
|
|
29
|
-
* @returns A SyncTable instance with methods to interact with the underlying Table and manage reactive searches
|
|
30
|
-
*/
|
|
31
|
-
export declare function createSqliteState<Document extends DocType>(options: CreateSqliteOptions<Document>): SyncTable<Document>;
|
package/types/sqlite/index.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { Table, DbOptions, DocType } from './table.types';
|
|
2
|
-
export declare const DEFAULT_PAGE_SIZE = 100;
|
|
3
|
-
/**
|
|
4
|
-
* Convert a dot-separated path to a JSON path
|
|
5
|
-
* @param dot The dot-separated path string
|
|
6
|
-
* @returns The JSON path string
|
|
7
|
-
*/
|
|
8
|
-
export declare function toJsonPath(dot: string): string;
|
|
9
|
-
/**
|
|
10
|
-
* Get a nested value from an object using a dot-separated path
|
|
11
|
-
* @param object The object to retrieve the value from
|
|
12
|
-
* @param path The dot-separated path string
|
|
13
|
-
* @returns The value at the specified path, or undefined if not found
|
|
14
|
-
*/
|
|
15
|
-
export declare function getByPath<T extends object>(object: T, path: string): unknown;
|
|
16
|
-
/**
|
|
17
|
-
* Create and initialize a table in the database with the specified options
|
|
18
|
-
* @param options The options for creating the table, including table name, indexes, backend, and key
|
|
19
|
-
* @returns A promise that resolves to the created Table instance
|
|
20
|
-
*/
|
|
21
|
-
export declare function createTable<Document extends DocType>(options: DbOptions<Document>): Promise<Table<Document>>;
|