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.
@@ -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(){s?.clear(),o({removedAll:!0})},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
+ 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 C}from"./tokenizer";import{getWhereQuery as k}from"./where";const L=500,M=100;function R(l){return"$."+l}function U(l,a){if(!(!l||!a))return a.split(".").reduce((t,y)=>{if(typeof t=="object"&&t!==null&&y in t)return t[y]},l)}async function P(l){const{backend:a,tableName:t,indexes:y,key:A,disablePragmaOptimization:x}=l,u=A!==void 0;x||(await a.execute("PRAGMA journal_mode=WAL;"),await a.execute("PRAGMA synchronous=NORMAL;"),await a.execute("PRAGMA temp_store=MEMORY;"),await a.execute("PRAGMA cache_size=-20000;")),u?await a.execute(`
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 a.execute(`
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),r=n.replaceAll(".","_");T.push(n),f[n]=r}else if(typeof e=="object"&&e.type==="fts"){const n=e.path,r=n.replaceAll(".","_");if(T.push(n),f[n]=r,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 a.execute(`CREATE INDEX IF NOT EXISTS idx_${t}_${n.replaceAll(/\W/g,"_")}
11
- ON ${t} (json_extract(data, '${R(n)}'));`)}if(T.length>0){let e;typeof d=="object"?e=C(d):d===void 0?e='"unicode61", "remove_diacritics=1"':e=d;const n=T.map(o=>f[o]).join(", "),r=`
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 a.execute(r),await a.execute(`
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(o=>`json_extract(new.data, '${R(o)}')`).join(", ")}
21
+ ${T.map(r=>`json_extract(new.data, '${m(r)}')`).join(", ")}
22
22
  );
23
23
  END;
24
- `),await a.execute(`
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 a.execute(`
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(o=>`${f[o]}=json_extract(new.data, '${R(o)}')`).join(", ")}
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 $(e){if(u)return U(e,String(A))}async function b(e){return(await e.select("SELECT changes() AS c"))[0]?.c??0}const g={backend:a,async set(e,n){const r=n??a,o=JSON.stringify(e);if(u){const s=$(e);if(s==null)throw new Error(`Document is missing the configured key "${String(A)}".`);if(await r.execute(`UPDATE ${t} SET data = ? WHERE key = ?`,[o,s]),await b(r)===1)return{key:s,op:"update"};try{return await r.execute(`INSERT INTO ${t} (key, data) VALUES (?, ?)`,[s,o]),{key:s,op:"insert"}}catch{return await r.execute(`UPDATE ${t} SET data = ? WHERE key = ?`,[o,s]),{key:s,op:"update"}}}await r.execute(`INSERT INTO ${t} (data) VALUES (?)`,[o]);const c=(await r.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=r=>r){const r=u?"key = ?":"rowid = ?",o=await a.select(`SELECT rowid, data FROM ${t} WHERE ${r}`,[e]);if(o.length===0)return;const{data:E,rowid:c}=o[0],s=JSON.parse(E),i=u?$(s)??c:c;return n(s,{rowId:c,key:i})},async delete(e){const n=u?"key = ?":"rowid = ?";if(await a.execute(`DELETE FROM ${t} WHERE ${n}`,[e]),((await a.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:e,op:"delete"}},async*search(e={}){const{sortBy:n,order:r="asc",limit:o,offset:E=0,where:c,select:s=w=>w,pageSize:i=M}=e,p=k(c,t),h=`SELECT rowid, data FROM ${t} ${p}`;let S=0,D=E;for(;;){let w=h;n?w+=` ORDER BY json_extract(data, '${R(String(n))}') COLLATE NOCASE ${r.toUpperCase()}`:w+=u?` ORDER BY key COLLATE NOCASE ${r.toUpperCase()}`:` ORDER BY rowid ${r.toUpperCase()}`;const I=o?Math.min(i,o-S):i;w+=` LIMIT ${I} OFFSET ${D}`;const m=await a.select(w);if(m.length===0)break;for(const{rowid:O,data:F}of m){if(o&&S>=o)return;const N=JSON.parse(F),_=u?$(N)??O:O;yield s(N,{rowId:O,key:_}),S++}if(m.length<I||o&&S>=o)break;D+=m.length}},async count(e={}){const n=k(e.where,t),r=`SELECT COUNT(*) as count FROM ${t} ${n}`;return(await a.select(r))[0]?.count??0},async deleteBy(e){const n=k(e,t),r=u?"key":"rowid",o=[];return await a.transaction(async E=>{const c=await E.select(`SELECT ${r} AS k FROM ${t} ${n}`);if(c.length===0)return;const s=c.map(i=>i.k);for(let i=0;i<s.length;i+=L){const p=s.slice(i,i+L),h=p.map(()=>"?").join(",");await E.execute(`DELETE FROM ${t} WHERE ${r} IN (${h})`,p)}for(const i of s)o.push({key:i,op:"delete"})}),o},async clear(){await a.execute(`DELETE FROM ${t}`)},async batchSet(e){const n=[];return await a.transaction(async r=>{for(const o of e){const E=await g.set(o,r);n.push(E)}}),n}};return g}export{M as DEFAULT_PAGE_SIZE,P as createTable,U as getByPath,R as toJsonPath};
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};
@@ -1 +1 @@
1
- import{useCallback as l,useLayoutEffect as A,useReducer as v,useRef as D}from"react";import{DEFAULT_PAGE_SIZE as O}from"./table";const I=1e4;function _(u,g={},k=[]){const{select:p,pageSize:P=O}=g,e=D(),[,w]=v(n=>n+1,0),t=D(new Map),S=D(),b=l(()=>{const{select:n,...o}=g;S.current=u.search({select:(c,s)=>({doc:c,meta:s}),...o})},[u,...k]),f=l(()=>{e.current=[],t.current.clear(),b()},[b]),m=l(async n=>{e.current===void 0&&(e.current=[]),n===!0&&f();const{current:o}=S;if(!o)return!0;let c=!1;for(let s=0;s<P;s++){const r=await o.next();if(r.done){S.current=void 0,c=!0;break}if(t.current.has(r.value.meta.key)){s+=-1;continue}e.current.push(p?p(r.value.doc):r.value.doc),t.current.set(r.value.meta.key,e.current.length-1)}return c},[]),y=l(async()=>{const n=await m(!1);return w(),n},[m]);A(()=>{const n=u.subscribe(async o=>{const{mutations:c,removedAll:s}=o;if(s&&f(),!c)return;const r=e.current?.length??0;let h=r,x=!1;for(const i of c){const{key:a,op:q}=i;switch(q){case"insert":{h+=1;break}case"delete":{if(e.current&&t.current.has(a)){const d=t.current.get(a);if(d===void 0)break;e.current.splice(d,1),t.current.delete(a),x=!0}break}case"update":{if(e.current&&t.current.has(a)){const d=t.current.get(a);if(d===void 0)break;e.current[d]=await u.get(a,p),x=!0}break}}}const L=r!==h;if(L||x){if(L){await m(!0);let i=0;for(;(e.current?.length??0)<h&&i<I;)await m(!1),i++;i===I&&console.warn("Reached maximum iterations in fillNextPage loop. Possible duplicate or data issue.")}w()}});return()=>{n()}},[u]),A(()=>{b(),e.current=void 0,t.current.clear(),y()},k);const T=l(async()=>{f(),await y()},[y,f]);return[e.current,{nextPage:y,reset:T,keysIndex:t.current}]}export{_ as useSqliteValue};
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muya",
3
- "version": "2.4.5",
3
+ "version": "2.4.7",
4
4
  "author": "samuel.gjabel@gmail.com",
5
5
  "repository": "https://github.com/samuelgjabel/muya",
6
6
  "main": "cjs/index.js",
@@ -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
- cachedTable?.clear()
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.execute(`UPDATE ${tableName} SET data = ? WHERE key = ?`, [json, id])
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
- try {
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
 
@@ -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
- itemsRef.current.splice(index, 1)
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 (itemsRef.current && keysIndex.current.has(key)) {
134
+ if (keysIndex.current.has(key)) {
132
135
  const index = keysIndex.current.get(key)
133
- if (index === undefined) break
134
- itemsRef.current[index] = (await state.get(key, select)) as Selected
135
- hasUpdate = true
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
- updateIterator()
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
  /**