muya 2.4.5 → 2.4.7
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/esm/sqlite/create-sqlite.js +1 -1
- package/esm/sqlite/table/table.js +10 -10
- package/esm/sqlite/use-sqlite.js +1 -1
- package/package.json +1 -1
- package/src/sqlite/__tests__/use-sqlite.more.test.tsx +431 -0
- package/src/sqlite/create-sqlite.ts +4 -3
- package/src/sqlite/table/table.ts +5 -17
- package/src/sqlite/use-sqlite.ts +37 -13
- package/types/sqlite/create-sqlite.d.ts +1 -1
- package/types/sqlite/use-sqlite.d.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{STATE_SCHEDULER as l}from"../create";import{getId as m}from"../utils/id";import{createTable as d}from"./table/table";function D(u){let s;async function a(){if(!s){const{backend:e,...n}=u,t=e instanceof Promise?await e:e;s=await d({backend:t,...n})}return s}const i=m();l.add(i,{onScheduleDone(e){if(!e)return;const n=e,t={};for(const c of n)c.removedAll&&(t.removedAll=!0),c.mutations&&(t.mutations||(t.mutations=[]),t.mutations.push(...c.mutations));for(const c of r)c(t)}});function o(e){l.schedule(i,e)}const r=new Set;return{subscribe(e){return r.add(e),()=>r.delete(e)},clear(){
|
|
1
|
+
import{STATE_SCHEDULER as l}from"../create";import{getId as m}from"../utils/id";import{createTable as d}from"./table/table";function D(u){let s;async function a(){if(!s){const{backend:e,...n}=u,t=e instanceof Promise?await e:e;s=await d({backend:t,...n})}return s}const i=m();l.add(i,{onScheduleDone(e){if(!e)return;const n=e,t={};for(const c of n)c.removedAll&&(t.removedAll=!0),c.mutations&&(t.mutations||(t.mutations=[]),t.mutations.push(...c.mutations));for(const c of r)c(t)}});function o(e){l.schedule(i,e)}const r=new Set;return{subscribe(e){return r.add(e),()=>r.delete(e)},async clear(){const e=await a();return o({removedAll:!0}),e.clear()},async set(e){const t=await(await a()).set(e);return o({mutations:[t]}),t},async batchSet(e){const t=await(await a()).batchSet(e);return o({mutations:t}),t},async delete(e){const t=await(await a()).delete(e);return t&&o({mutations:[t]}),t},async deleteBy(e){const t=await(await a()).deleteBy(e);return o({mutations:t}),t},async get(e,n){return(await a()).get(e,n)},async*search(e={}){const n=await a();for await(const t of n.search(e))yield t},async count(e){return await(await a()).count(e)}}}export{D as createSqliteState};
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import{unicodeTokenizer as
|
|
1
|
+
import{unicodeTokenizer as _}from"./tokenizer";import{getWhereQuery as g}from"./where";const x=500,C=100;function m(l){return"$."+l}function M(l,s){if(!(!l||!s))return s.split(".").reduce((t,y)=>{if(typeof t=="object"&&t!==null&&y in t)return t[y]},l)}async function G(l){const{backend:s,tableName:t,indexes:y,key:$,disablePragmaOptimization:L}=l,u=$!==void 0;L||(await s.execute("PRAGMA journal_mode=WAL;"),await s.execute("PRAGMA synchronous=NORMAL;"),await s.execute("PRAGMA temp_store=MEMORY;"),await s.execute("PRAGMA cache_size=-20000;")),u?await s.execute(`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS ${t} (
|
|
3
3
|
key TEXT PRIMARY KEY,
|
|
4
4
|
data TEXT NOT NULL
|
|
5
5
|
);
|
|
6
|
-
`):await
|
|
6
|
+
`):await s.execute(`
|
|
7
7
|
CREATE TABLE IF NOT EXISTS ${t} (
|
|
8
8
|
data TEXT NOT NULL
|
|
9
9
|
);
|
|
10
|
-
`);let d;const T=[],f={};for(const e of y??[])if(typeof e=="string"&&e.startsWith("fts:")){const n=e.slice(4),
|
|
11
|
-
ON ${t} (json_extract(data, '${
|
|
10
|
+
`);let d;const T=[],f={};for(const e of y??[])if(typeof e=="string"&&e.startsWith("fts:")){const n=e.slice(4),o=n.replaceAll(".","_");T.push(n),f[n]=o}else if(typeof e=="object"&&e.type==="fts"){const n=e.path,o=n.replaceAll(".","_");if(T.push(n),f[n]=o,e.tokenizer){if(!d)d=e.tokenizer;else if(d!==e.tokenizer)throw new Error(`Conflicting FTS tokenizers: already using "${d}", got "${e.tokenizer}"`)}}else{const n=String(e);await s.execute(`CREATE INDEX IF NOT EXISTS idx_${t}_${n.replaceAll(/\W/g,"_")}
|
|
11
|
+
ON ${t} (json_extract(data, '${m(n)}'));`)}if(T.length>0){let e;typeof d=="object"?e=_(d):d===void 0?e='"unicode61", "remove_diacritics=1"':e=d;const n=T.map(r=>f[r]).join(", "),o=`
|
|
12
12
|
CREATE VIRTUAL TABLE IF NOT EXISTS ${t}_fts
|
|
13
13
|
USING fts5(${n}, tokenize=${e});
|
|
14
|
-
`;await
|
|
14
|
+
`;await s.execute(o),await s.execute(`
|
|
15
15
|
CREATE TRIGGER IF NOT EXISTS ${t}_ai
|
|
16
16
|
AFTER INSERT ON ${t}
|
|
17
17
|
BEGIN
|
|
18
18
|
INSERT INTO ${t}_fts(rowid, ${n})
|
|
19
19
|
VALUES (
|
|
20
20
|
new.rowid,
|
|
21
|
-
${T.map(
|
|
21
|
+
${T.map(r=>`json_extract(new.data, '${m(r)}')`).join(", ")}
|
|
22
22
|
);
|
|
23
23
|
END;
|
|
24
|
-
`),await
|
|
24
|
+
`),await s.execute(`
|
|
25
25
|
CREATE TRIGGER IF NOT EXISTS ${t}_ad
|
|
26
26
|
AFTER DELETE ON ${t}
|
|
27
27
|
BEGIN
|
|
28
28
|
DELETE FROM ${t}_fts WHERE rowid = old.rowid;
|
|
29
29
|
END;
|
|
30
|
-
`),await
|
|
30
|
+
`),await s.execute(`
|
|
31
31
|
CREATE TRIGGER IF NOT EXISTS ${t}_au
|
|
32
32
|
AFTER UPDATE ON ${t}
|
|
33
33
|
BEGIN
|
|
34
34
|
UPDATE ${t}_fts
|
|
35
|
-
SET ${T.map(
|
|
35
|
+
SET ${T.map(r=>`${f[r]}=json_extract(new.data, '${m(r)}')`).join(", ")}
|
|
36
36
|
WHERE rowid = old.rowid;
|
|
37
37
|
END;
|
|
38
|
-
`)}function
|
|
38
|
+
`)}function A(e){if(u)return M(e,String($))}const k={backend:s,async set(e,n){const o=n??s,r=JSON.stringify(e);if(u){const a=A(e);if(a==null)throw new Error(`Document is missing the configured key "${String($)}".`);return(await o.select(`SELECT key FROM ${t} WHERE key = ?`,[a])).length>0?(await o.execute(`UPDATE ${t} SET data = ? WHERE key = ?`,[r,a]),{key:a,op:"update"}):(await o.execute(`INSERT INTO ${t} (key, data) VALUES (?, ?)`,[a,r]),{key:a,op:"insert"})}await o.execute(`INSERT INTO ${t} (data) VALUES (?)`,[r]);const c=(await o.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof c!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:c,op:"insert"}},async get(e,n=o=>o){const o=u?"key = ?":"rowid = ?",r=await s.select(`SELECT rowid, data FROM ${t} WHERE ${o}`,[e]);if(r.length===0)return;const{data:E,rowid:c}=r[0],a=JSON.parse(E),i=u?A(a)??c:c;return n(a,{rowId:c,key:i})},async delete(e){const n=u?"key = ?":"rowid = ?";if(await s.execute(`DELETE FROM ${t} WHERE ${n}`,[e]),((await s.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:e,op:"delete"}},async*search(e={}){const{sortBy:n,order:o="asc",limit:r,offset:E=0,where:c,select:a=w=>w,pageSize:i=C}=e,S=g(c,t),h=`SELECT rowid, data FROM ${t} ${S}`;let p=0,I=E;for(;;){let w=h;n?w+=` ORDER BY json_extract(data, '${m(String(n))}') COLLATE NOCASE ${o.toUpperCase()}`:w+=u?` ORDER BY key COLLATE NOCASE ${o.toUpperCase()}`:` ORDER BY rowid ${o.toUpperCase()}`;const N=r?Math.min(i,r-p):i;w+=` LIMIT ${N} OFFSET ${I}`;const R=await s.select(w);if(R.length===0)break;for(const{rowid:O,data:F}of R){if(r&&p>=r)return;const D=JSON.parse(F),b=u?A(D)??O:O;yield a(D,{rowId:O,key:b}),p++}if(R.length<N||r&&p>=r)break;I+=R.length}},async count(e={}){const n=g(e.where,t),o=`SELECT COUNT(*) as count FROM ${t} ${n}`;return(await s.select(o))[0]?.count??0},async deleteBy(e){const n=g(e,t),o=u?"key":"rowid",r=[];return await s.transaction(async E=>{const c=await E.select(`SELECT ${o} AS k FROM ${t} ${n}`);if(c.length===0)return;const a=c.map(i=>i.k);for(let i=0;i<a.length;i+=x){const S=a.slice(i,i+x),h=S.map(()=>"?").join(",");await E.execute(`DELETE FROM ${t} WHERE ${o} IN (${h})`,S)}for(const i of a)r.push({key:i,op:"delete"})}),r},async clear(){await s.execute(`DELETE FROM ${t}`)},async batchSet(e){const n=[];return await s.transaction(async o=>{for(const r of e){const E=await k.set(r,o);n.push(E)}}),n}};return k}export{C as DEFAULT_PAGE_SIZE,G as createTable,M as getByPath,m as toJsonPath};
|
package/esm/sqlite/use-sqlite.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{useCallback as
|
|
1
|
+
import{useCallback as f,useLayoutEffect as P,useReducer as R,useRef as D}from"react";import{DEFAULT_PAGE_SIZE as O}from"./table";const T=1e4;function K(i,w={},k=[]){const{select:m,pageSize:q=O}=w,e=D(),[,I]=R(c=>c+1,0),n=D(new Map),x=D(),L=f(()=>{const{select:c,...d}=w;x.current=i.search({select:(u,a)=>({doc:u,meta:a}),...d})},[i,...k]),l=f(()=>{e.current=[],n.current.clear(),L()},[L]),y=f(async c=>{e.current===void 0&&(e.current=[]),c===!0&&l();const{current:d}=x;if(!d)return!0;let u=!1;for(let a=0;a<q;a++){const s=await d.next();if(s.done){x.current=void 0,u=!0;break}if(n.current.has(s.value.meta.key)){a+=-1;continue}e.current.push(m?m(s.value.doc):s.value.doc),n.current.set(s.value.meta.key,e.current.length-1)}return e.current=[...e.current],u},[]),p=f(async()=>{const c=await y(!1);return I(),c},[y]);P(()=>{const c=i.subscribe(async d=>{const{mutations:u,removedAll:a}=d;if(a&&l(),!u)return;const s=e.current?.length??0;let g=s,h=!1;const S=new Set;for(const o of u){const{key:r,op:b}=o;switch(b){case"insert":{g+=1;break}case"delete":{if(e.current&&e.current.length>0&&n.current.has(r)){const t=n.current.get(r);if(t===void 0)break;S.add(t),h=!0}break}case"update":{if(n.current.has(r)){const t=n.current.get(r);t!==void 0&&e.current&&(e.current[t]=await i.get(r,m),e.current=[...e.current],h=!0)}else{const t=await i.get(r,m);t&&(e.current=[...e.current??[],t],n.current.set(r,e.current.length-1),h=!0)}break}}}if(S.size>0&&e.current&&e.current.length>0){const o=new Map;e.current=e.current?.filter((b,t)=>!S.has(t));let r=0;for(const[b,t]of n.current)S.has(t)||(o.set(b,r),r++);n.current=o}const A=s!==g;if(A||h){if(A){await y(!0);let o=0;for(;(e.current?.length??0)<g&&o<T;)await y(!1),o++;o===T&&console.warn("Reached maximum iterations in fillNextPage loop. Possible duplicate or data issue.")}I()}});return()=>{c()}},[i]),P(()=>{l(),p()},k);const v=f(async()=>{l(),await p()},[p,l]);return[e.current,{nextPage:p,reset:v,keysIndex:n.current}]}export{K as useSqliteValue};
|
package/package.json
CHANGED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-hooks'
|
|
2
|
+
import { createSqliteState } from '../create-sqlite'
|
|
3
|
+
import { useSqliteValue } from '../use-sqlite'
|
|
4
|
+
import { waitFor } from '@testing-library/react'
|
|
5
|
+
import { bunMemoryBackend } from '../table/bun-backend'
|
|
6
|
+
import { StrictMode, Suspense } from 'react'
|
|
7
|
+
|
|
8
|
+
const backend = bunMemoryBackend()
|
|
9
|
+
|
|
10
|
+
interface Person {
|
|
11
|
+
id: string
|
|
12
|
+
name: string
|
|
13
|
+
age: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wrapper component to provide necessary React context for testing.
|
|
18
|
+
* @param props - The props object containing children.
|
|
19
|
+
* @param props.children - The children to render inside the wrapper.
|
|
20
|
+
* @returns The wrapped children with React context.
|
|
21
|
+
*/
|
|
22
|
+
function Wrapper({ children }: Readonly<{ children: React.ReactNode }>) {
|
|
23
|
+
return (
|
|
24
|
+
<StrictMode>
|
|
25
|
+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
|
|
26
|
+
</StrictMode>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('use-sqlite edge cases', () => {
|
|
31
|
+
it('should remove an item and verify it is removed', async () => {
|
|
32
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'RemoveTest', key: 'id' })
|
|
33
|
+
await sql.batchSet([
|
|
34
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
35
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
39
|
+
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(result.current[0]).toEqual([
|
|
42
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
43
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
44
|
+
])
|
|
45
|
+
expect(result.current[1].keysIndex).toEqual(
|
|
46
|
+
new Map([
|
|
47
|
+
['1', 0],
|
|
48
|
+
['2', 1],
|
|
49
|
+
]),
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
act(() => {
|
|
54
|
+
sql.delete('1')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(result.current[0]).toEqual([{ id: '2', name: 'Bob', age: 25 }])
|
|
59
|
+
expect(result.current[1].keysIndex).toEqual(new Map([['2', 0]]))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
act(() => {
|
|
63
|
+
sql.delete('2')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(result.current[0]).toEqual([])
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
it('should handle deleting a non-existent item gracefully', async () => {
|
|
71
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'EdgeCaseTest', key: 'id' })
|
|
72
|
+
await sql.batchSet([
|
|
73
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
74
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
78
|
+
|
|
79
|
+
await waitFor(() => {
|
|
80
|
+
expect(result.current[0]).toEqual([
|
|
81
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
82
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
83
|
+
])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
act(() => {
|
|
87
|
+
sql.delete('3') // Attempt to delete a non-existent item
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(result.current[0]).toEqual([
|
|
92
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
93
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
94
|
+
]) // State should remain unchanged
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should handle deleting all items', async () => {
|
|
99
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'DeleteAllTest', key: 'id' })
|
|
100
|
+
await sql.batchSet([
|
|
101
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
102
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
106
|
+
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(result.current[0]).toEqual([
|
|
109
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
110
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
111
|
+
])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
act(() => {
|
|
115
|
+
sql.delete('1')
|
|
116
|
+
sql.delete('2')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(result.current[0]).toEqual([]) // All items should be removed
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should handle concurrent operations', async () => {
|
|
125
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'ConcurrentOpsTest', key: 'id' })
|
|
126
|
+
await sql.batchSet([
|
|
127
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
128
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
132
|
+
|
|
133
|
+
await waitFor(() => {
|
|
134
|
+
expect(result.current[0]).toEqual([
|
|
135
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
136
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
137
|
+
])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
act(() => {
|
|
141
|
+
sql.delete('1')
|
|
142
|
+
sql.set({ id: '3', name: 'Carol', age: 40 })
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
await waitFor(() => {
|
|
146
|
+
expect(result.current[0]).toEqual([
|
|
147
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
148
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
149
|
+
]) // State should reflect both operations
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should handle repeated updates, removals, and insertions in a loop', async () => {
|
|
154
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'LoopTest', key: 'id' })
|
|
155
|
+
sql.clear()
|
|
156
|
+
// Initial batch set
|
|
157
|
+
await sql.batchSet([
|
|
158
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
159
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
163
|
+
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(result.current[0]).toEqual([
|
|
166
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
167
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
168
|
+
])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Perform updates, removals, and insertions in a loop
|
|
172
|
+
act(() => {
|
|
173
|
+
for (let index = 0; index < 10; index++) {
|
|
174
|
+
sql.set({ id: `new-${index}`, name: `Person ${index}`, age: 20 + index }) // Insert new item
|
|
175
|
+
sql.delete('1') // Remove an existing item
|
|
176
|
+
sql.set({ id: '2', name: `Updated Bob ${index}`, age: 25 + index }) // Update an existing item
|
|
177
|
+
}
|
|
178
|
+
// fetch next
|
|
179
|
+
result.current[1].nextPage()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(result.current[0]).toEqual([
|
|
184
|
+
{ id: '2', name: 'Updated Bob 9', age: 34 },
|
|
185
|
+
{ id: 'new-0', name: 'Person 0', age: 20 },
|
|
186
|
+
{ id: 'new-1', name: 'Person 1', age: 21 },
|
|
187
|
+
{ id: 'new-2', name: 'Person 2', age: 22 },
|
|
188
|
+
{ id: 'new-3', name: 'Person 3', age: 23 },
|
|
189
|
+
{ id: 'new-4', name: 'Person 4', age: 24 },
|
|
190
|
+
{ id: 'new-5', name: 'Person 5', age: 25 },
|
|
191
|
+
{ id: 'new-6', name: 'Person 6', age: 26 },
|
|
192
|
+
{ id: 'new-7', name: 'Person 7', age: 27 },
|
|
193
|
+
{ id: 'new-8', name: 'Person 8', age: 28 },
|
|
194
|
+
{ id: 'new-9', name: 'Person 9', age: 29 },
|
|
195
|
+
])
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
it('should handle concurrent insertions and deletions', async () => {
|
|
199
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'ConcurrentInsertDeleteTest', key: 'id' })
|
|
200
|
+
await sql.batchSet([
|
|
201
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
202
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(result.current[0]).toEqual([
|
|
209
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
210
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
211
|
+
])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
act(() => {
|
|
215
|
+
sql.set({ id: '3', name: 'Carol', age: 40 })
|
|
216
|
+
sql.delete('1')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
await waitFor(() => {
|
|
220
|
+
expect(result.current[0]).toEqual([
|
|
221
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
222
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
223
|
+
])
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should handle pagination with empty pages', async () => {
|
|
228
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'EmptyPaginationTest', key: 'id' })
|
|
229
|
+
sql.clear()
|
|
230
|
+
|
|
231
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
232
|
+
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(result.current[0]).toEqual([])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
act(() => {
|
|
238
|
+
result.current[1].nextPage()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(result.current[0]).toEqual([]) // Still empty
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should handle duplicate key insertions gracefully', async () => {
|
|
247
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'DuplicateKeyTest', key: 'id' })
|
|
248
|
+
await sql.batchSet([{ id: '1', name: 'Alice', age: 30 }])
|
|
249
|
+
|
|
250
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
251
|
+
|
|
252
|
+
await waitFor(() => {
|
|
253
|
+
expect(result.current[0]).toEqual([{ id: '1', name: 'Alice', age: 30 }])
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
act(() => {
|
|
257
|
+
sql.set({ id: '1', name: 'Updated Alice', age: 35 })
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
await waitFor(() => {
|
|
261
|
+
expect(result.current[0]).toEqual([{ id: '1', name: 'Updated Alice', age: 35 }])
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should handle reset during pagination', async () => {
|
|
266
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'ResetDuringPaginationTest', key: 'id' })
|
|
267
|
+
await sql.batchSet([
|
|
268
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
269
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
273
|
+
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(result.current[0]).toEqual([
|
|
276
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
277
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
278
|
+
])
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
act(() => {
|
|
282
|
+
result.current[1].reset()
|
|
283
|
+
result.current[1].nextPage()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
await waitFor(() => {
|
|
287
|
+
expect(result.current[0]).toEqual([
|
|
288
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
289
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
290
|
+
])
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should handle invalid key deletion gracefully', async () => {
|
|
295
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'InvalidKeyDeletionTest', key: 'id' })
|
|
296
|
+
await sql.batchSet([
|
|
297
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
298
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
299
|
+
])
|
|
300
|
+
|
|
301
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
302
|
+
|
|
303
|
+
await waitFor(() => {
|
|
304
|
+
expect(result.current[0]).toEqual([
|
|
305
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
306
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
307
|
+
])
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
act(() => {
|
|
311
|
+
sql.delete('non-existent-key')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
await waitFor(() => {
|
|
315
|
+
expect(result.current[0]).toEqual([
|
|
316
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
317
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
318
|
+
])
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
it('should update a visible document', async () => {
|
|
322
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'UpdateVisibleTest', key: 'id' })
|
|
323
|
+
await sql.batchSet([
|
|
324
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
325
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
326
|
+
])
|
|
327
|
+
|
|
328
|
+
const { result } = renderHook(() => useSqliteValue(sql, {}, []), { wrapper: Wrapper })
|
|
329
|
+
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
expect(result.current[0]).toEqual([
|
|
332
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
333
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
334
|
+
])
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
act(() => {
|
|
338
|
+
sql.set({ id: '1', name: 'Updated Alice', age: 35 })
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
await waitFor(() => {
|
|
342
|
+
expect(result.current[0]).toEqual([
|
|
343
|
+
{ id: '1', name: 'Updated Alice', age: 35 },
|
|
344
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
345
|
+
])
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should update a non-visible document', async () => {
|
|
350
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'UpdateNonVisibleTest', key: 'id' })
|
|
351
|
+
await sql.batchSet([
|
|
352
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
353
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
354
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
355
|
+
])
|
|
356
|
+
|
|
357
|
+
const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 2 }, []), { wrapper: Wrapper })
|
|
358
|
+
|
|
359
|
+
await waitFor(() => {
|
|
360
|
+
expect(result.current[0]).toEqual([
|
|
361
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
362
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
363
|
+
])
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
act(() => {
|
|
367
|
+
sql.set({ id: '3', name: 'Updated Carol', age: 45 })
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(result.current[0]).toEqual([
|
|
372
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
373
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
374
|
+
]) // No change in visible items
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
act(() => {
|
|
378
|
+
result.current[1].nextPage()
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
await waitFor(() => {
|
|
382
|
+
expect(result.current[0]).toEqual([
|
|
383
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
384
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
385
|
+
{ id: '3', name: 'Updated Carol', age: 45 },
|
|
386
|
+
])
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('should handle updates during pagination', async () => {
|
|
391
|
+
const sql = createSqliteState<Person>({ backend, tableName: 'UpdateDuringPaginationTest', key: 'id' })
|
|
392
|
+
await sql.batchSet([
|
|
393
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
394
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
395
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
396
|
+
])
|
|
397
|
+
|
|
398
|
+
const { result } = renderHook(() => useSqliteValue(sql, { pageSize: 2 }, []), { wrapper: Wrapper })
|
|
399
|
+
|
|
400
|
+
await waitFor(() => {
|
|
401
|
+
expect(result.current[0]).toEqual([
|
|
402
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
403
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
404
|
+
])
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
act(() => {
|
|
408
|
+
result.current[1].nextPage()
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
await waitFor(() => {
|
|
412
|
+
expect(result.current[0]).toEqual([
|
|
413
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
414
|
+
{ id: '2', name: 'Bob', age: 25 },
|
|
415
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
416
|
+
])
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
act(() => {
|
|
420
|
+
sql.set({ id: '2', name: 'Updated Bob', age: 35 })
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
await waitFor(() => {
|
|
424
|
+
expect(result.current[0]).toEqual([
|
|
425
|
+
{ id: '1', name: 'Alice', age: 30 },
|
|
426
|
+
{ id: '2', name: 'Updated Bob', age: 35 },
|
|
427
|
+
{ id: '3', name: 'Carol', age: 40 },
|
|
428
|
+
])
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
})
|
|
@@ -24,7 +24,7 @@ export interface SyncTable<Document extends DocType> {
|
|
|
24
24
|
readonly search: <Selected = Document>(options?: SearchOptions<Document, Selected>) => AsyncIterableIterator<Selected>
|
|
25
25
|
readonly count: (options?: { where?: Where<Document> }) => Promise<number>
|
|
26
26
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>
|
|
27
|
-
readonly clear: () => void
|
|
27
|
+
readonly clear: () => Promise<void>
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -87,9 +87,10 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
|
|
|
87
87
|
listeners.add(listener)
|
|
88
88
|
return () => listeners.delete(listener)
|
|
89
89
|
},
|
|
90
|
-
clear() {
|
|
91
|
-
|
|
90
|
+
async clear() {
|
|
91
|
+
const table = await getTable()
|
|
92
92
|
handleChanges({ removedAll: true })
|
|
93
|
+
return table.clear()
|
|
93
94
|
},
|
|
94
95
|
async set(document) {
|
|
95
96
|
const table = await getTable()
|
|
@@ -164,16 +164,6 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
164
164
|
return getByPath(document, String(key)) as Key | undefined
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/**
|
|
168
|
-
* Get the number of rows changed by the last operation on the given connection
|
|
169
|
-
* @param conn The database connection to check for changes
|
|
170
|
-
* @returns A promise that resolves to the number of changed rows
|
|
171
|
-
*/
|
|
172
|
-
async function getChanges(conn: typeof backend): Promise<number> {
|
|
173
|
-
const r = await conn.select<Array<{ c: number }>>(`SELECT changes() AS c`)
|
|
174
|
-
return r[0]?.c ?? 0
|
|
175
|
-
}
|
|
176
|
-
|
|
177
167
|
const table: Table<Document> = {
|
|
178
168
|
backend,
|
|
179
169
|
|
|
@@ -187,16 +177,14 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
187
177
|
throw new Error(`Document is missing the configured key "${String(key)}".`)
|
|
188
178
|
}
|
|
189
179
|
|
|
190
|
-
await db.
|
|
191
|
-
const updated = await getChanges(db)
|
|
192
|
-
if (updated === 1) return { key: id, op: 'update' }
|
|
180
|
+
const existing = await db.select<Array<{ key: string }>>(`SELECT key FROM ${tableName} WHERE key = ?`, [id])
|
|
193
181
|
|
|
194
|
-
|
|
195
|
-
await db.execute(`INSERT INTO ${tableName} (key, data) VALUES (?, ?)`, [id, json])
|
|
196
|
-
return { key: id, op: 'insert' }
|
|
197
|
-
} catch {
|
|
182
|
+
if (existing.length > 0) {
|
|
198
183
|
await db.execute(`UPDATE ${tableName} SET data = ? WHERE key = ?`, [json, id])
|
|
199
184
|
return { key: id, op: 'update' }
|
|
185
|
+
} else {
|
|
186
|
+
await db.execute(`INSERT INTO ${tableName} (key, data) VALUES (?, ?)`, [id, json])
|
|
187
|
+
return { key: id, op: 'insert' }
|
|
200
188
|
}
|
|
201
189
|
}
|
|
202
190
|
|
package/src/sqlite/use-sqlite.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface SqLiteActions {
|
|
|
17
17
|
*/
|
|
18
18
|
readonly reset: () => Promise<void>
|
|
19
19
|
readonly keysIndex: Map<Key, number>
|
|
20
|
+
readonly isResetting?: boolean
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
@@ -72,6 +73,7 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
72
73
|
return true
|
|
73
74
|
}
|
|
74
75
|
let isDone = false
|
|
76
|
+
|
|
75
77
|
for (let index = 0; index < pageSize; index++) {
|
|
76
78
|
const result = await iterator.next()
|
|
77
79
|
if (result.done) {
|
|
@@ -87,6 +89,7 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
87
89
|
itemsRef.current.push(select ? select(result.value.doc) : (result.value.doc as unknown as Selected))
|
|
88
90
|
keysIndex.current.set(result.value.meta.key, itemsRef.current.length - 1)
|
|
89
91
|
}
|
|
92
|
+
itemsRef.current = [...itemsRef.current]
|
|
90
93
|
return isDone
|
|
91
94
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
95
|
}, [])
|
|
@@ -110,6 +113,7 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
110
113
|
const oldLength = itemsRef.current?.length ?? 0
|
|
111
114
|
let newLength = oldLength
|
|
112
115
|
let hasUpdate = false
|
|
116
|
+
const removeIndexes = new Set<number>()
|
|
113
117
|
for (const mutation of mutations) {
|
|
114
118
|
const { key, op } = mutation
|
|
115
119
|
switch (op) {
|
|
@@ -118,35 +122,57 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
118
122
|
break
|
|
119
123
|
}
|
|
120
124
|
case 'delete': {
|
|
121
|
-
if (itemsRef.current && keysIndex.current.has(key)) {
|
|
125
|
+
if (itemsRef.current && itemsRef.current.length > 0 && keysIndex.current.has(key)) {
|
|
122
126
|
const index = keysIndex.current.get(key)
|
|
123
127
|
if (index === undefined) break
|
|
124
|
-
|
|
125
|
-
keysIndex.current.delete(key)
|
|
128
|
+
removeIndexes.add(index)
|
|
126
129
|
hasUpdate = true
|
|
127
130
|
}
|
|
128
131
|
break
|
|
129
132
|
}
|
|
130
133
|
case 'update': {
|
|
131
|
-
if (
|
|
134
|
+
if (keysIndex.current.has(key)) {
|
|
132
135
|
const index = keysIndex.current.get(key)
|
|
133
|
-
if (index
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
if (index !== undefined && itemsRef.current) {
|
|
137
|
+
itemsRef.current[index] = (await state.get(key, select)) as Selected
|
|
138
|
+
itemsRef.current = [...itemsRef.current]
|
|
139
|
+
hasUpdate = true
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Handle updates to non-visible items
|
|
143
|
+
const updatedItem = await state.get(key, select)
|
|
144
|
+
if (updatedItem) {
|
|
145
|
+
itemsRef.current = [...(itemsRef.current ?? []), updatedItem]
|
|
146
|
+
keysIndex.current.set(key, itemsRef.current.length - 1)
|
|
147
|
+
hasUpdate = true
|
|
148
|
+
}
|
|
136
149
|
}
|
|
137
150
|
break
|
|
138
151
|
}
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
|
|
155
|
+
if (removeIndexes.size > 0 && itemsRef.current && itemsRef.current.length > 0) {
|
|
156
|
+
const newIndex = new Map<Key, number>()
|
|
157
|
+
itemsRef.current = itemsRef.current?.filter((_, index) => {
|
|
158
|
+
return !removeIndexes.has(index)
|
|
159
|
+
})
|
|
160
|
+
let newIdx = 0
|
|
161
|
+
for (const [key, index] of keysIndex.current) {
|
|
162
|
+
if (removeIndexes.has(index)) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
newIndex.set(key, newIdx)
|
|
166
|
+
newIdx++
|
|
167
|
+
}
|
|
168
|
+
keysIndex.current = newIndex
|
|
169
|
+
}
|
|
170
|
+
|
|
142
171
|
const isLengthChanged = oldLength !== newLength
|
|
143
172
|
const isChanged = isLengthChanged || hasUpdate
|
|
144
173
|
if (!isChanged) return
|
|
145
174
|
if (isLengthChanged) {
|
|
146
175
|
await fillNextPage(true)
|
|
147
|
-
|
|
148
|
-
// here we ensure that if the length changed, we fill the next page
|
|
149
|
-
|
|
150
176
|
let iterations = 0
|
|
151
177
|
while ((itemsRef.current?.length ?? 0) < newLength && iterations < MAX_ITERATIONS) {
|
|
152
178
|
await fillNextPage(false)
|
|
@@ -167,9 +193,7 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
167
193
|
}, [state])
|
|
168
194
|
|
|
169
195
|
useLayoutEffect(() => {
|
|
170
|
-
|
|
171
|
-
itemsRef.current = undefined
|
|
172
|
-
keysIndex.current.clear()
|
|
196
|
+
reset()
|
|
173
197
|
nextPage()
|
|
174
198
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
199
|
}, deps)
|
|
@@ -19,7 +19,7 @@ export interface SyncTable<Document extends DocType> {
|
|
|
19
19
|
where?: Where<Document>;
|
|
20
20
|
}) => Promise<number>;
|
|
21
21
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>;
|
|
22
|
-
readonly clear: () => void
|
|
22
|
+
readonly clear: () => Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
25
|
* Create a SyncTable that wraps a Table and provides reactive capabilities
|
|
@@ -13,6 +13,7 @@ export interface SqLiteActions {
|
|
|
13
13
|
*/
|
|
14
14
|
readonly reset: () => Promise<void>;
|
|
15
15
|
readonly keysIndex: Map<Key, number>;
|
|
16
|
+
readonly isResetting?: boolean;
|
|
16
17
|
}
|
|
17
18
|
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
18
19
|
/**
|