muya 2.1.1 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-state.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-state.test.js +1 -0
- package/esm/sqlite/create-sqlite-state.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-value.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-state.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-state.test.ts +213 -0
- package/src/sqlite/create-sqlite-state.ts +256 -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-value.ts +76 -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-state.d.ts +22 -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-value.d.ts +21 -0
- package/types/types.d.ts +1 -0
- package/types/utils/common.d.ts +2 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-shadow */
|
|
2
|
+
/* eslint-disable no-shadow */
|
|
3
|
+
/* eslint-disable sonarjs/pseudo-random */
|
|
4
|
+
// /* eslint-disable sonarjs/no-unused-vars */
|
|
5
|
+
// /* eslint-disable unicorn/prevent-abbreviations */
|
|
6
|
+
|
|
7
|
+
import { bunMemoryBackend } from '../table/bun-backend'
|
|
8
|
+
import { createTable } from '../table/table'
|
|
9
|
+
|
|
10
|
+
interface Person {
|
|
11
|
+
name: string
|
|
12
|
+
age: number
|
|
13
|
+
city: string
|
|
14
|
+
}
|
|
15
|
+
describe('table', () => {
|
|
16
|
+
let backend = bunMemoryBackend()
|
|
17
|
+
let table: ReturnType<typeof createTable<Person>> extends Promise<infer T> ? T : never
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
backend = bunMemoryBackend()
|
|
21
|
+
table = await createTable<Person>({
|
|
22
|
+
backend,
|
|
23
|
+
tableName: 'TestTable',
|
|
24
|
+
key: 'name',
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should set and get items', async () => {
|
|
29
|
+
const mutation = await table.set({ name: 'Alice', age: 30, city: 'Paris' })
|
|
30
|
+
expect(mutation.key).toBe('Alice')
|
|
31
|
+
expect(mutation.op).toBe('insert')
|
|
32
|
+
const result = await table.get('Alice')
|
|
33
|
+
expect(result).toEqual({ name: 'Alice', age: 30, city: 'Paris' })
|
|
34
|
+
|
|
35
|
+
const updateMutation = await table.set({ name: 'Alice', age: 31, city: 'Paris' })
|
|
36
|
+
expect(updateMutation.key).toBe('Alice')
|
|
37
|
+
expect(updateMutation.op).toBe('update')
|
|
38
|
+
const updatedResult = await table.get('Alice')
|
|
39
|
+
expect(updatedResult).toEqual({ name: 'Alice', age: 31, city: 'Paris' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should count items and count with where', async () => {
|
|
43
|
+
await table.set({ name: 'Alice', age: 30, city: 'Paris' })
|
|
44
|
+
await table.set({ name: 'Bob', age: 25, city: 'London' })
|
|
45
|
+
expect(await table.count()).toBe(2)
|
|
46
|
+
expect(await table.count({ where: { city: 'Paris' } })).toBe(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should search with ordering, limit and offset', async () => {
|
|
50
|
+
const people: Person[] = [
|
|
51
|
+
{ name: 'Alice', age: 30, city: 'Paris' },
|
|
52
|
+
{ name: 'Bob', age: 25, city: 'London' },
|
|
53
|
+
{ name: 'Carol', age: 35, city: 'Berlin' },
|
|
54
|
+
]
|
|
55
|
+
for (const p of people) {
|
|
56
|
+
await table.set(p)
|
|
57
|
+
}
|
|
58
|
+
// sort by age ascending
|
|
59
|
+
const asc = [] as Person[]
|
|
60
|
+
for await (const p of table.search({ sorBy: 'age', order: 'asc' })) asc.push(p)
|
|
61
|
+
expect(asc.map((p) => p.name)).toEqual(['Bob', 'Alice', 'Carol'])
|
|
62
|
+
// limit and offset
|
|
63
|
+
const limited = [] as Person[]
|
|
64
|
+
for await (const p of table.search({ sorBy: 'age', order: 'asc', limit: 2 })) limited.push(p)
|
|
65
|
+
expect(limited.map((p) => p.name)).toEqual(['Bob', 'Alice'])
|
|
66
|
+
const offsetted = [] as Person[]
|
|
67
|
+
for await (const p of table.search({ sorBy: 'age', order: 'asc', offset: 1, limit: 2 })) offsetted.push(p)
|
|
68
|
+
expect(offsetted.map((p) => p.name)).toEqual(['Alice', 'Carol'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should deleteBy where clause', async () => {
|
|
72
|
+
await table.set({ name: 'Dave', age: 40, city: 'NY' })
|
|
73
|
+
await table.set({ name: 'Eve', age: 45, city: 'NY' })
|
|
74
|
+
await table.set({ name: 'Frank', age: 50, city: 'LA' })
|
|
75
|
+
expect(await table.count()).toBe(3)
|
|
76
|
+
await table.deleteBy({ city: 'NY' })
|
|
77
|
+
expect(await table.count()).toBe(1)
|
|
78
|
+
expect(await table.get('Frank')).toEqual({ name: 'Frank', age: 50, city: 'LA' })
|
|
79
|
+
expect(await table.get('Dave')).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should use selector in get and search', async () => {
|
|
83
|
+
await table.set({ name: 'Gary', age: 60, city: 'SF' })
|
|
84
|
+
// selector in get
|
|
85
|
+
const ageOnly = await table.get('Gary', ({ age }) => age)
|
|
86
|
+
expect(ageOnly).toBe(60)
|
|
87
|
+
// selector in search
|
|
88
|
+
const cities: string[] = []
|
|
89
|
+
for await (const city of table.search({ select: ({ city }) => city })) cities.push(city)
|
|
90
|
+
expect(cities).toEqual(['SF'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should delete items by key', async () => {
|
|
94
|
+
await table.set({ name: 'Helen', age: 28, city: 'Rome' })
|
|
95
|
+
expect(await table.get('Helen')).toBeDefined()
|
|
96
|
+
await table.delete('Helen')
|
|
97
|
+
expect(await table.get('Helen')).toBeUndefined()
|
|
98
|
+
})
|
|
99
|
+
it('should test search with 1000 items', async () => {
|
|
100
|
+
const people: Person[] = []
|
|
101
|
+
for (let index = 0; index < 1000; index++) {
|
|
102
|
+
people.push({
|
|
103
|
+
name: `Person${index}`,
|
|
104
|
+
age: Math.floor(Math.random() * 100),
|
|
105
|
+
city: 'City' + (index % 10),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
for (const p of people) {
|
|
109
|
+
await table.set(p)
|
|
110
|
+
}
|
|
111
|
+
const results: Person[] = []
|
|
112
|
+
for await (const person of table.search({ sorBy: 'age', order: 'asc', limit: 100 })) {
|
|
113
|
+
results.push(person)
|
|
114
|
+
}
|
|
115
|
+
expect(results.length).toBe(100)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should handle operations on an empty table', async () => {
|
|
119
|
+
expect(await table.count()).toBe(0)
|
|
120
|
+
expect(await table.get('NonExistent')).toBeUndefined()
|
|
121
|
+
const results: Person[] = []
|
|
122
|
+
for await (const person of table.search({ sorBy: 'age', order: 'asc' })) {
|
|
123
|
+
results.push(person)
|
|
124
|
+
}
|
|
125
|
+
expect(results.length).toBe(0)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should handle duplicate keys gracefully', async () => {
|
|
129
|
+
await table.set({ name: 'Alice', age: 30, city: 'Paris' })
|
|
130
|
+
await table.set({ name: 'Alice', age: 35, city: 'Berlin' })
|
|
131
|
+
const result = await table.get('Alice')
|
|
132
|
+
expect(result).toEqual({ name: 'Alice', age: 35, city: 'Berlin' })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should handle edge cases in selectors', async () => {
|
|
136
|
+
await table.set({ name: 'Charlie', age: 40, city: 'NY' })
|
|
137
|
+
const nullSelector = await table.get('Charlie', () => null)
|
|
138
|
+
expect(nullSelector).toBeNull()
|
|
139
|
+
const undefinedSelector = await table.get('Charlie', () => void 0)
|
|
140
|
+
expect(undefinedSelector).toBeUndefined()
|
|
141
|
+
})
|
|
142
|
+
})
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-hooks'
|
|
2
|
+
import { createSqliteState } from '../create-sqlite-state'
|
|
3
|
+
import { useSqliteValue } from '../use-sqlite-value'
|
|
4
|
+
import { waitFor } from '@testing-library/react'
|
|
5
|
+
import { bunMemoryBackend } from '../table/bun-backend'
|
|
6
|
+
import { useState } from 'react'
|
|
7
|
+
import { DEFAULT_STEP_SIZE } from '../table/table'
|
|
8
|
+
|
|
9
|
+
const backend = bunMemoryBackend()
|
|
10
|
+
interface Person {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
age: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('use-sqlite-state', () => {
|
|
17
|
+
it('should get basic value states', async () => {
|
|
18
|
+
const sql = await createSqliteState<Person>({ backend, tableName: 'State1', key: 'id' })
|
|
19
|
+
let reRenders = 0
|
|
20
|
+
const { result } = renderHook(() => {
|
|
21
|
+
reRenders++
|
|
22
|
+
return useSqliteValue(sql, {}, [])
|
|
23
|
+
})
|
|
24
|
+
// expect(result.current).toEqual([])
|
|
25
|
+
|
|
26
|
+
expect(reRenders).toBe(1)
|
|
27
|
+
|
|
28
|
+
act(() => {
|
|
29
|
+
sql.set({ id: '1', name: 'Alice', age: 30 })
|
|
30
|
+
})
|
|
31
|
+
await waitFor(() => {
|
|
32
|
+
expect(result.current[0]).toEqual([{ id: '1', name: 'Alice', age: 30 }])
|
|
33
|
+
expect(reRenders).toBe(3)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
act(() => {
|
|
37
|
+
sql.set({ id: '1', name: 'Alice2', age: 30 })
|
|
38
|
+
})
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(result.current[0]).toEqual([{ id: '1', name: 'Alice2', age: 30 }])
|
|
41
|
+
expect(reRenders).toBe(4)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// delete item
|
|
45
|
+
act(() => {
|
|
46
|
+
sql.delete('1')
|
|
47
|
+
})
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(result.current[0]).toEqual([])
|
|
50
|
+
expect(reRenders).toBe(5)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// add two items
|
|
54
|
+
act(() => {
|
|
55
|
+
sql.set({ id: '1', name: 'Alice', age: 30 })
|
|
56
|
+
sql.set({ id: '2', name: 'Bob', age: 25 })
|
|
57
|
+
})
|
|
58
|
+
await waitFor(() => {
|
|
59
|
+
expect(result.current[0].length).toBe(2)
|
|
60
|
+
expect(reRenders).toBe(6)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should use where clause changed via state', async () => {
|
|
65
|
+
const sql = await createSqliteState<Person>({ backend, tableName: 'State2', key: 'id' })
|
|
66
|
+
await sql.batchSet([
|
|
67
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
68
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
69
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
70
|
+
])
|
|
71
|
+
let reRenders = 0
|
|
72
|
+
const { result } = renderHook(() => {
|
|
73
|
+
reRenders++
|
|
74
|
+
const [minAge, setMinAge] = useState(20)
|
|
75
|
+
return [useSqliteValue(sql, { where: { age: { gt: minAge } }, sorBy: 'age' }, [minAge]), setMinAge] as const
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(result.current[0][0].map((p) => p.name)).toEqual(['Bob', 'Alice', 'Carol'])
|
|
80
|
+
expect(reRenders).toBe(2)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// // change minAge to 29
|
|
84
|
+
act(() => {
|
|
85
|
+
result.current[1](29)
|
|
86
|
+
})
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(result.current[0][0].map((p) => p.name)).toEqual(['Alice', 'Carol'])
|
|
89
|
+
expect(reRenders).toBe(4)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should support like in where clause and update results', async () => {
|
|
94
|
+
const sql = await createSqliteState<Person>({ backend, tableName: 'State3', key: 'id' })
|
|
95
|
+
await sql.batchSet([
|
|
96
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
97
|
+
{ id: '2', name: 'Alicia', age: 25 },
|
|
98
|
+
{ id: '3', name: 'Bob', age: 40 },
|
|
99
|
+
])
|
|
100
|
+
let reRenders = 0
|
|
101
|
+
const { result, rerender } = renderHook(
|
|
102
|
+
({ like }) => {
|
|
103
|
+
reRenders++
|
|
104
|
+
return useSqliteValue(sql, { where: { name: { like } } }, [like])
|
|
105
|
+
},
|
|
106
|
+
{ initialProps: { like: '%Ali%' } },
|
|
107
|
+
)
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(result.current[0].map((p) => p.name)).toEqual(['Alice', 'Alicia'])
|
|
110
|
+
})
|
|
111
|
+
act(() => {
|
|
112
|
+
rerender({ like: '%Bob%' })
|
|
113
|
+
})
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(result.current[0].map((p) => p.name)).toEqual(['Bob'])
|
|
116
|
+
})
|
|
117
|
+
expect(reRenders).toBeGreaterThanOrEqual(2)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should update results when changing order and limit options', async () => {
|
|
121
|
+
const sql = await createSqliteState<Person>({ backend, tableName: 'State4', key: 'id' })
|
|
122
|
+
await sql.batchSet([
|
|
123
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
124
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
125
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
126
|
+
])
|
|
127
|
+
const { result, rerender } = renderHook(
|
|
128
|
+
({ order, limit }) => useSqliteValue(sql, { sorBy: 'age', order, limit }, [order, limit]),
|
|
129
|
+
{ initialProps: { order: 'asc' as 'asc' | 'desc', limit: 2 } },
|
|
130
|
+
)
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(result.current[0].map((p) => p.name)).toEqual(['Bob', 'Alice'])
|
|
133
|
+
})
|
|
134
|
+
act(() => {
|
|
135
|
+
rerender({ order: 'desc', limit: 2 })
|
|
136
|
+
})
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(result.current[0].map((p) => p.name)).toEqual(['Carol', 'Alice'])
|
|
139
|
+
})
|
|
140
|
+
act(() => {
|
|
141
|
+
rerender({ order: 'desc', limit: 1 })
|
|
142
|
+
})
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(result.current[0].map((p) => p.name)).toEqual(['Carol'])
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should support actions.next and actions.refresh', async () => {
|
|
149
|
+
const sql = await createSqliteState<Person>({ backend, tableName: 'State5', key: 'id' })
|
|
150
|
+
await sql.batchSet([
|
|
151
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
152
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
153
|
+
])
|
|
154
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []))
|
|
155
|
+
// actions.next and actions.refresh should be functions
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(typeof result.current[1].next).toBe('function')
|
|
158
|
+
expect(typeof result.current[1].reset).toBe('function')
|
|
159
|
+
expect(result.current[1].reset()).resolves.toBeUndefined()
|
|
160
|
+
expect(result.current[1].next()).resolves.toBeFalsy()
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
it('should handle thousands of records', async () => {
|
|
164
|
+
const sql = await createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
|
|
165
|
+
const people: Person[] = []
|
|
166
|
+
const ITEMS_COUNT = 1000
|
|
167
|
+
for (let index = 1; index <= ITEMS_COUNT; index++) {
|
|
168
|
+
people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
|
|
169
|
+
}
|
|
170
|
+
await sql.batchSet(people)
|
|
171
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []))
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(result.current[0].length).toBe(DEFAULT_STEP_SIZE)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// loop until we have all ITEMS_COUNT items
|
|
177
|
+
for (let index = 0; index < ITEMS_COUNT / DEFAULT_STEP_SIZE; index++) {
|
|
178
|
+
act(() => {
|
|
179
|
+
result.current[1].next()
|
|
180
|
+
})
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(result.current[0].length).toBe(Math.min(DEFAULT_STEP_SIZE * (index + 2), ITEMS_COUNT))
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
act(() => {
|
|
187
|
+
result.current[1].reset()
|
|
188
|
+
})
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
expect(result.current[0].length).toBe(DEFAULT_STEP_SIZE)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
it('should change ordering', async () => {
|
|
194
|
+
const sql = await createSqliteState<Person>({ backend, tableName: 'State7', key: 'id' })
|
|
195
|
+
const people: Person[] = []
|
|
196
|
+
for (let index = 1; index <= 100; index++) {
|
|
197
|
+
people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
|
|
198
|
+
}
|
|
199
|
+
await sql.batchSet(people)
|
|
200
|
+
const { result, rerender } = renderHook(({ order }) => useSqliteValue(sql, { sorBy: 'age', order }, [order]), {
|
|
201
|
+
initialProps: { order: 'asc' as 'asc' | 'desc' },
|
|
202
|
+
})
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(result.current[0][0].age).toBe(20)
|
|
205
|
+
})
|
|
206
|
+
act(() => {
|
|
207
|
+
rerender({ order: 'desc' })
|
|
208
|
+
})
|
|
209
|
+
await waitFor(() => {
|
|
210
|
+
expect(result.current[0][0].age).toBe(69)
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/* eslint-disable sonarjs/redundant-type-aliases */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-shadow */
|
|
3
|
+
/* eslint-disable no-shadow */
|
|
4
|
+
import { createScheduler } from '../scheduler'
|
|
5
|
+
import { shallow } from '../utils/shallow'
|
|
6
|
+
import { createTable, DEFAULT_STEP_SIZE } from './table/table'
|
|
7
|
+
import type { DbOptions, DocType, Key, MutationResult, SearchOptions, Table } from './table/table.types'
|
|
8
|
+
import type { Where } from './table/where'
|
|
9
|
+
|
|
10
|
+
type SearchId = string
|
|
11
|
+
const STATE_SCHEDULER = createScheduler()
|
|
12
|
+
|
|
13
|
+
let stateId = 0
|
|
14
|
+
function getStateId() {
|
|
15
|
+
return stateId++
|
|
16
|
+
}
|
|
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, listener: () => void) => () => void
|
|
21
|
+
readonly getSnapshot: (searchId: SearchId) => Document[]
|
|
22
|
+
readonly refresh: (searchId: SearchId) => Promise<void>
|
|
23
|
+
|
|
24
|
+
readonly set: (document: Document) => Promise<MutationResult>
|
|
25
|
+
readonly batchSet: (documents: Document[]) => Promise<MutationResult[]>
|
|
26
|
+
readonly get: <Selected = Document>(key: Key, selector?: (document: Document) => Selected) => Promise<Selected | undefined>
|
|
27
|
+
|
|
28
|
+
readonly delete: (key: Key) => Promise<MutationResult | undefined>
|
|
29
|
+
readonly search: <Selected = Document>(options?: SearchOptions<Document, Selected>) => AsyncIterableIterator<Selected>
|
|
30
|
+
readonly count: (options?: { where?: Where<Document> }) => Promise<number>
|
|
31
|
+
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>
|
|
32
|
+
readonly destroy: () => void
|
|
33
|
+
readonly next: (searchId: SearchId) => Promise<boolean>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DataItems<Document extends DocType> {
|
|
37
|
+
items: Document[]
|
|
38
|
+
keys: Set<Key>
|
|
39
|
+
options?: SearchOptions<Document, unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createSqliteState<Document extends DocType>(options: DbOptions<Document>): Promise<SyncTable<Document>> {
|
|
43
|
+
// const table = await createTable<Document>(options)
|
|
44
|
+
|
|
45
|
+
const id = getStateId()
|
|
46
|
+
function getScheduleId(searchId: SearchId) {
|
|
47
|
+
return `state-${id}-search-${searchId}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let cachedTable: Table<Document> | undefined
|
|
51
|
+
async function getTable() {
|
|
52
|
+
if (!cachedTable) {
|
|
53
|
+
cachedTable = await createTable<Document>(options)
|
|
54
|
+
}
|
|
55
|
+
return cachedTable
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface NextResult {
|
|
59
|
+
document: Document
|
|
60
|
+
rowId: number
|
|
61
|
+
}
|
|
62
|
+
// const emitter = createEmitter<Table<Document>>()
|
|
63
|
+
const cachedData = new Map<SearchId, DataItems<Document>>()
|
|
64
|
+
const listeners = new Map<SearchId, () => void>()
|
|
65
|
+
const iterators = new Map<SearchId, AsyncIterableIterator<NextResult>>()
|
|
66
|
+
|
|
67
|
+
async function next(searchId: SearchId, data: DataItems<Document>): Promise<boolean> {
|
|
68
|
+
const iterator = iterators.get(searchId)
|
|
69
|
+
const { items, options = {} } = data
|
|
70
|
+
const { stepSize = DEFAULT_STEP_SIZE } = options
|
|
71
|
+
if (!iterator) return false
|
|
72
|
+
const newItems: Document[] = []
|
|
73
|
+
|
|
74
|
+
for (let index = 0; index < stepSize; index++) {
|
|
75
|
+
const result = await iterator.next()
|
|
76
|
+
if (result.done) {
|
|
77
|
+
iterators.delete(searchId)
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
newItems.push(result.value.document)
|
|
81
|
+
data.keys.add(String(result.value.rowId))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (shallow(data.items, newItems)) return false
|
|
85
|
+
data.items = [...items, ...newItems]
|
|
86
|
+
return newItems.length > 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function notifyListeners(searchId: SearchId) {
|
|
90
|
+
const searchListeners = listeners.get(searchId)
|
|
91
|
+
if (searchListeners) {
|
|
92
|
+
searchListeners()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function refreshCache(searchId: SearchId) {
|
|
97
|
+
const table = await getTable()
|
|
98
|
+
const data = cachedData.get(searchId)
|
|
99
|
+
if (!data) return
|
|
100
|
+
const { options } = data
|
|
101
|
+
const iterator = table.search({ ...options, select: (document, { rowId }) => ({ document, rowId }) })
|
|
102
|
+
iterators.set(searchId, iterator)
|
|
103
|
+
data.keys = new Set()
|
|
104
|
+
data.items = []
|
|
105
|
+
await next(searchId, data)
|
|
106
|
+
}
|
|
107
|
+
async function refresh(searchId: SearchId) {
|
|
108
|
+
await refreshCache(searchId)
|
|
109
|
+
notifyListeners(searchId)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleChange(mutationResult: MutationResult) {
|
|
113
|
+
const { key, op } = mutationResult
|
|
114
|
+
// find all cached data with key
|
|
115
|
+
const searchIds = new Set<SearchId>()
|
|
116
|
+
for (const [searchId, { keys }] of cachedData) {
|
|
117
|
+
switch (op) {
|
|
118
|
+
case 'delete':
|
|
119
|
+
case 'update': {
|
|
120
|
+
if (keys.has(String(key))) {
|
|
121
|
+
searchIds.add(searchId)
|
|
122
|
+
}
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
case 'insert': {
|
|
126
|
+
// we do not know about the key
|
|
127
|
+
searchIds.add(searchId)
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return searchIds
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function handleChanges(mutationResults: MutationResult[]) {
|
|
136
|
+
const updateSearchIds = new Set<SearchId>()
|
|
137
|
+
for (const mutationResult of mutationResults) {
|
|
138
|
+
const searchIds = handleChange(mutationResult)
|
|
139
|
+
for (const searchId of searchIds) {
|
|
140
|
+
updateSearchIds.add(searchId)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// const promises = []
|
|
145
|
+
for (const searchId of updateSearchIds) {
|
|
146
|
+
const scheduleId = getScheduleId(searchId)
|
|
147
|
+
STATE_SCHEDULER.schedule(scheduleId, { searchId })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const clearSchedulers = new Set<() => void>()
|
|
152
|
+
|
|
153
|
+
function registerData(searchId: SearchId, options?: SearchOptions<Document, unknown>) {
|
|
154
|
+
if (!cachedData.has(searchId)) {
|
|
155
|
+
cachedData.set(searchId, { items: [], options, keys: new Set() })
|
|
156
|
+
if (options) {
|
|
157
|
+
refresh(searchId)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const data = cachedData.get(searchId)!
|
|
161
|
+
if (options) {
|
|
162
|
+
data.options = options
|
|
163
|
+
}
|
|
164
|
+
return data
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
async set(document) {
|
|
169
|
+
const table = await getTable()
|
|
170
|
+
const changes = await table.set(document)
|
|
171
|
+
await handleChanges([changes])
|
|
172
|
+
return changes
|
|
173
|
+
},
|
|
174
|
+
async batchSet(documents) {
|
|
175
|
+
const table = await getTable()
|
|
176
|
+
const changes = await table.batchSet(documents)
|
|
177
|
+
await handleChanges(changes)
|
|
178
|
+
return changes
|
|
179
|
+
},
|
|
180
|
+
async delete(key) {
|
|
181
|
+
const table = await getTable()
|
|
182
|
+
const changes = await table.delete(key)
|
|
183
|
+
if (changes) {
|
|
184
|
+
await handleChanges([changes])
|
|
185
|
+
}
|
|
186
|
+
return changes
|
|
187
|
+
},
|
|
188
|
+
async deleteBy(where) {
|
|
189
|
+
const table = await getTable()
|
|
190
|
+
const changes = await table.deleteBy(where)
|
|
191
|
+
await handleChanges(changes)
|
|
192
|
+
return changes
|
|
193
|
+
},
|
|
194
|
+
async get(key, selector) {
|
|
195
|
+
const table = await getTable()
|
|
196
|
+
return table.get(key, selector)
|
|
197
|
+
},
|
|
198
|
+
async *search(options = {}) {
|
|
199
|
+
const table = await getTable()
|
|
200
|
+
for await (const item of table.search(options)) {
|
|
201
|
+
yield item
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
async count(options) {
|
|
205
|
+
const table = await getTable()
|
|
206
|
+
return await table.count(options)
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
updateSearchOptions(searchId, options) {
|
|
210
|
+
const data = registerData(searchId, options)
|
|
211
|
+
data.options = options
|
|
212
|
+
const scheduleId = getScheduleId(searchId)
|
|
213
|
+
STATE_SCHEDULER.schedule(scheduleId, { searchId })
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
subscribe(searchId, listener) {
|
|
217
|
+
const scheduleId = getScheduleId(searchId)
|
|
218
|
+
const clear = STATE_SCHEDULER.add(scheduleId, {
|
|
219
|
+
onScheduleDone() {
|
|
220
|
+
refresh(searchId)
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
clearSchedulers.add(clear)
|
|
224
|
+
|
|
225
|
+
if (!listeners.has(searchId)) {
|
|
226
|
+
listeners.set(searchId, listener)
|
|
227
|
+
}
|
|
228
|
+
return () => {
|
|
229
|
+
listeners.delete(searchId)
|
|
230
|
+
clear()
|
|
231
|
+
cachedData.delete(searchId)
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
getSnapshot(searchId) {
|
|
235
|
+
const data = registerData(searchId)
|
|
236
|
+
return data.items
|
|
237
|
+
},
|
|
238
|
+
refresh,
|
|
239
|
+
destroy() {
|
|
240
|
+
for (const clear of clearSchedulers) clear()
|
|
241
|
+
cachedData.clear()
|
|
242
|
+
listeners.clear()
|
|
243
|
+
},
|
|
244
|
+
async next(searchId) {
|
|
245
|
+
const data = cachedData.get(searchId)
|
|
246
|
+
if (data) {
|
|
247
|
+
const hasNext = await next(searchId, data)
|
|
248
|
+
if (hasNext) {
|
|
249
|
+
notifyListeners(searchId)
|
|
250
|
+
}
|
|
251
|
+
return hasNext
|
|
252
|
+
}
|
|
253
|
+
return false
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const IN_MEMORY_DB = ':memory:'
|
|
2
|
+
|
|
3
|
+
export interface QueryResult {
|
|
4
|
+
/** The number of rows affected by the query. */
|
|
5
|
+
rowsAffected: number
|
|
6
|
+
/**
|
|
7
|
+
* The last inserted `id`.
|
|
8
|
+
*
|
|
9
|
+
* This value is not set for Postgres databases. If the
|
|
10
|
+
* last inserted id is required on Postgres, the `select` function
|
|
11
|
+
* must be used, with a `RETURNING` clause
|
|
12
|
+
* (`INSERT INTO todos (title) VALUES ($1) RETURNING id`).
|
|
13
|
+
*/
|
|
14
|
+
lastInsertId?: number
|
|
15
|
+
}
|
|
16
|
+
export interface Backend {
|
|
17
|
+
execute: (query: string, bindValues?: unknown[]) => Promise<QueryResult>
|
|
18
|
+
select: <T>(query: string, bindValues?: unknown[]) => Promise<T>
|
|
19
|
+
transaction: (callback: (tx: Backend) => Promise<void>) => Promise<void>
|
|
20
|
+
path: string
|
|
21
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Database, type Statement } from 'bun:sqlite'
|
|
2
|
+
import type { Backend } from './backend'
|
|
3
|
+
import { MapDeque } from './map-deque'
|
|
4
|
+
|
|
5
|
+
export function bunMemoryBackend(): Backend {
|
|
6
|
+
const db = Database.open(':memory:')
|
|
7
|
+
const prepares = new MapDeque<string, Statement>(100)
|
|
8
|
+
function getStatement(query: string): Statement {
|
|
9
|
+
if (prepares.has(query)) {
|
|
10
|
+
return prepares.get(query)!
|
|
11
|
+
}
|
|
12
|
+
const stmt = db.prepare(query)
|
|
13
|
+
prepares.set(query, stmt)
|
|
14
|
+
return stmt
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const backend: Backend = {
|
|
18
|
+
execute: async (query, params = []) => {
|
|
19
|
+
const q = getStatement(query)
|
|
20
|
+
const result = q.run(...(params as never[]))
|
|
21
|
+
return {
|
|
22
|
+
rowsAffected: result.changes,
|
|
23
|
+
changes: result.changes,
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
transaction: async (callback) => {
|
|
27
|
+
return db.transaction(() => callback(backend))()
|
|
28
|
+
},
|
|
29
|
+
path: db.filename,
|
|
30
|
+
select: async (query, params = []) => {
|
|
31
|
+
const q = getStatement(query)
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const result = q.all(...(params as never[])) as Array<Record<string, any>>
|
|
34
|
+
return result as never
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
return backend
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class MapDeque<K, V> extends Map<K, V> {
|
|
2
|
+
constructor(
|
|
3
|
+
private maxSize: number,
|
|
4
|
+
entries?: ReadonlyArray<readonly [K, V]> | null,
|
|
5
|
+
) {
|
|
6
|
+
super(entries)
|
|
7
|
+
if (this.maxSize <= 0) {
|
|
8
|
+
throw new RangeError('maxSize must be greater than 0')
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override set(key: K, value: V): this {
|
|
13
|
+
if (this.has(key)) {
|
|
14
|
+
super.set(key, value)
|
|
15
|
+
return this
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (this.size >= this.maxSize) {
|
|
19
|
+
const firstKey = this.keys().next().value
|
|
20
|
+
if (firstKey !== undefined) {
|
|
21
|
+
this.delete(firstKey)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
super.set(key, value)
|
|
26
|
+
|
|
27
|
+
return this
|
|
28
|
+
}
|
|
29
|
+
}
|