muya 2.1.2 → 2.2.0

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.
@@ -0,0 +1 @@
1
+ import{createSqliteState as o}from"../create-sqlite";import{bunMemoryBackend as s}from"../table/bun-backend";const n=s();describe("create-sqlite-state",()=>{it("should batchSet and update multiple documents",async()=>{const e=o({backend:n,tableName:"State2",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25}]);const a=[];for await(const i of e.search())a.push(i);expect(a).toHaveLength(2),await e.batchSet([{id:"1",name:"Alice2",age:31},{id:"2",name:"Bob2",age:26}]);const t=[];for await(const i of e.search())t.push(i);expect(t).toEqual([{id:"1",name:"Alice2",age:31},{id:"2",name:"Bob2",age:26}])}),it("should deleteBy condition",async()=>{const e=o({backend:n,tableName:"State3",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);const a=await e.deleteBy({age:{gt:30}});expect(a.length).toBe(1);const t=[];for await(const i of e.search())t.push(i);expect(t.map(i=>i.id)).toEqual(["1","2"])}),it("should get by key and with selector",async()=>{const e=o({backend:n,tableName:"State4",key:"id"});await e.set({id:"1",name:"Alice",age:30});const a=await e.get("1");expect(a).toEqual({id:"1",name:"Alice",age:30});const t=await e.get("1",c=>c.name);expect(t).toBe("Alice");const i=await e.get("999");expect(i).toBeUndefined()}),it("should count documents with and without where",async()=>{const e=o({backend:n,tableName:"State5",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]),expect(await e.count()).toBe(3),expect(await e.count({where:{age:{gt:30}}})).toBe(1)}),it("should support search with options",async()=>{const e=o({backend:n,tableName:"State6",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);const a=[];for await(const t of e.search({where:{age:{lt:35}}}))a.push(t);expect(a.map(t=>t.id)).toEqual(["1","2"])})});
@@ -0,0 +1 @@
1
+ import{act as s,renderHook as c}from"@testing-library/react-hooks";import{createSqliteState as i}from"../create-sqlite";import{useSqliteValue as l}from"../use-sqlite";import{waitFor as o}from"@testing-library/react";import{bunMemoryBackend as g}from"../table/bun-backend";import{useState as x}from"react";import{DEFAULT_STEP_SIZE as m}from"../table/table";const u=g();describe("use-sqlite-state",()=>{it("should get basic value states",async()=>{const a=i({backend:u,tableName:"State1",key:"id"});let t=0;const{result:n}=c(()=>(t++,l(a,{},[])));expect(t).toBe(1),s(()=>{a.set({id:"1",name:"Alice",age:30})}),await o(()=>{expect(n.current[0]).toEqual([{id:"1",name:"Alice",age:30}]),expect(t).toBe(3)}),s(()=>{a.set({id:"1",name:"Alice2",age:30})}),await o(()=>{expect(n.current[0]).toEqual([{id:"1",name:"Alice2",age:30}]),expect(t).toBe(4)}),s(()=>{a.delete("1")}),await o(()=>{expect(n.current[0]).toEqual([]),expect(t).toBe(5)}),s(()=>{a.set({id:"1",name:"Alice",age:30}),a.set({id:"2",name:"Bob",age:25})}),await o(()=>{expect(n.current[0].length).toBe(2),expect(t).toBe(6)})}),it("should use where clause changed via state",async()=>{const a=i({backend:u,tableName:"State2",key:"id"});await a.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);let t=0;const{result:n}=c(()=>{t++;const[r,e]=x(20);return[l(a,{where:{age:{gt:r}},sorBy:"age"},[r]),e]});await o(()=>{expect(n.current[0][0].map(r=>r.name)).toEqual(["Bob","Alice","Carol"]),expect(t).toBe(2)}),s(()=>{n.current[1](29)}),await o(()=>{expect(n.current[0][0].map(r=>r.name)).toEqual(["Alice","Carol"]),expect(t).toBe(4)})}),it("should support like in where clause and update results",async()=>{const a=i({backend:u,tableName:"State3",key:"id"});await a.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Alicia",age:25},{id:"3",name:"Bob",age:40}]);let t=0;const{result:n,rerender:r}=c(({like:e})=>(t++,l(a,{where:{name:{like:e}}},[e])),{initialProps:{like:"%Ali%"}});await o(()=>{expect(n.current[0].map(e=>e.name)).toEqual(["Alice","Alicia"])}),s(()=>{r({like:"%Bob%"})}),await o(()=>{expect(n.current[0].map(e=>e.name)).toEqual(["Bob"])}),expect(t).toBeGreaterThanOrEqual(2)}),it("should update results when changing order and limit options",async()=>{const a=i({backend:u,tableName:"State4",key:"id"});await a.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);const{result:t,rerender:n}=c(({order:r,limit:e})=>l(a,{sorBy:"age",order:r,limit:e},[r,e]),{initialProps:{order:"asc",limit:2}});await o(()=>{expect(t.current[0].map(r=>r.name)).toEqual(["Bob","Alice"])}),s(()=>{n({order:"desc",limit:2})}),await o(()=>{expect(t.current[0].map(r=>r.name)).toEqual(["Carol","Alice"])}),s(()=>{n({order:"desc",limit:1})}),await o(()=>{expect(t.current[0].map(r=>r.name)).toEqual(["Carol"])})}),it("should support actions.next and actions.refresh",async()=>{const a=i({backend:u,tableName:"State5",key:"id"});await a.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25}]);const{result:t}=c(()=>l(a,{},[]));await o(()=>{expect(typeof t.current[1].next).toBe("function"),expect(typeof t.current[1].reset).toBe("function"),expect(t.current[1].reset()).resolves.toBeUndefined(),expect(t.current[1].next()).resolves.toBeFalsy()})}),it("should handle thousands of records",async()=>{const a=i({backend:u,tableName:"State6",key:"id"}),t=[],n=1e3;for(let e=1;e<=n;e++)t.push({id:e.toString(),name:`Person${e}`,age:20+e%50});await a.batchSet(t);const{result:r}=c(()=>l(a,{},[]));await o(()=>{expect(r.current[0].length).toBe(m)});for(let e=0;e<n/m;e++)s(()=>{r.current[1].next()}),await o(()=>{expect(r.current[0].length).toBe(Math.min(m*(e+2),n))});s(()=>{r.current[1].reset()}),await o(()=>{expect(r.current[0].length).toBe(m)})}),it("should handle thousands of records with single update",async()=>{const a=i({backend:u,tableName:"State6",key:"id"}),t=[],n=1e4,r=5e3;for(let d=1;d<=n;d++)t.push({id:d.toString(),name:`Person${d}`,age:20+d%50});await a.batchSet(t);let e=0;const{result:p}=c(()=>(e++,l(a,{stepSize:r},[])));await o(()=>{expect(e).toBe(2),expect(p.current[0].length).toBe(r)}),s(()=>{for(let d=0;d<n/r;d++)p.current[1].next()}),await o(()=>{expect(e).toBe(4),expect(p.current[0].length).toBe(n)}),s(()=>{p.current[1].reset()}),await o(()=>{expect(e).toBe(5),expect(p.current[0].length).toBe(r)})}),it("should change ordering",async()=>{const a=i({backend:u,tableName:"State7",key:"id",indexes:["age"]}),t=[];for(let e=1;e<=100;e++)t.push({id:e.toString(),name:`Person${e}`,age:20+e%50});await a.batchSet(t);const{result:n,rerender:r}=c(({order:e})=>l(a,{sorBy:"age",order:e},[e]),{initialProps:{order:"asc"}});await o(()=>{expect(n.current[0][0].age).toBe(20)}),s(()=>{r({order:"desc"})}),await o(()=>{expect(n.current[0][0].age).toBe(69)})}),it("should support selector in options",async()=>{const a=i({backend:u,tableName:"State8",key:"id"});await a.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);const{result:t}=c(()=>l(a,{sorBy:"age",select:n=>n.name},[]));await o(()=>{expect(t.current[0]).toEqual(["Bob","Alice","Carol"])})})});
@@ -0,0 +1 @@
1
+ import{createScheduler as P}from"../scheduler";import{shallow as R}from"../utils/shallow";import{selectSql as M}from"./select-sql";import{createTable as O,DEFAULT_STEP_SIZE as v}from"./table/table";const f=P();let C=0;function E(){return C++}function N(g){const T=E();function l(e){return`state-${T}-search-${e}`}let m;async function o(){return m||(m=await O(g)),m}const c=new Map,i=new Map,h=new Map;async function p(e,n){const t=h.get(e),{options:a={}}=n,{stepSize:s=v}=a;if(!t)return!1;const r=[];for(let d=0;d<s;d++){const y=await t.next();if(y.done){h.delete(e);break}r.push(y.value.document),n.keys.add(String(y.value.rowId))}return r.length===0||R(n.items,r)?!1:(n.items=[...n.items,...r],!0)}function b(e){const n=i.get(e);n&&n()}async function k(e){const n=await o(),t=c.get(e);if(!t)return;const{options:a}=t,s=n.search({...a,select:(r,{rowId:d})=>({document:r,rowId:d})});h.set(e,s),t.keys=new Set,t.items=[],await p(e,t)}async function S(e){await k(e),b(e)}function x(e){const{key:n,op:t}=e,a=new Set;for(const[s,{keys:r}]of c)switch(t){case"delete":case"update":{r.has(String(n))&&a.add(s);break}case"insert":{a.add(s);break}}return a}async function u(e){const n=new Set;for(const t of e){const a=x(t);for(const s of a)n.add(s)}for(const t of n){const a=l(t);f.schedule(a,{searchId:t})}}const w=new Set;function D(e,n){c.has(e)||(c.set(e,{items:[],options:n,keys:new Set}),n&&S(e));const t=c.get(e);return n&&(t.options=n),t}const I={async set(e){const t=await(await o()).set(e);return await u([t]),t},async batchSet(e){const t=await(await o()).batchSet(e);return await u(t),t},async delete(e){const t=await(await o()).delete(e);return t&&await u([t]),t},async deleteBy(e){const t=await(await o()).deleteBy(e);return await u(t),t},async get(e,n){return(await o()).get(e,n)},async*search(e={}){const n=await o();for await(const t of n.search(e))yield t},async count(e){return await(await o()).count(e)},updateSearchOptions(e,n){const t=D(e,n);t.options=n;const a=l(e);f.schedule(a,{searchId:e})},subscribe(e,n){const t=l(e),a=f.add(t,{onScheduleDone(){S(e)}});return w.add(a),i.has(e)||i.set(e,n),()=>{i.delete(e),a(),c.delete(e)}},getSnapshot(e){return D(e).items},refresh:S,destroy(){for(const e of w)e();c.clear(),i.clear()},async next(e){const n=c.get(e);if(n){const t=await p(e,n);return t&&b(e),t}return!1},select(e){return M(I,e)}};return I}export{N as createSqliteState};
@@ -0,0 +1 @@
1
+ export*from"./create-sqlite";export*from"./table";export*from"./select-sql";export*from"./use-sqlite";
@@ -0,0 +1 @@
1
+ import{createState as p}from"../create-state";let n=0;function i(){return n++,`${n.toString(36)}-sql`}function S(r,o){const{subscribe:a,updateSearchOptions:s}=r;return(...c)=>{const e=i(),m=a(e,()=>{t.emitter.emit()}),u=o(...c),t=p({destroy(){m(),t.emitter.clear(),t.cache.current=void 0},get(){return r.getSnapshot(e)},getSnapshot(){return r.getSnapshot(e)}});return s(e,u),t}}export{S as selectSql};
@@ -0,0 +1 @@
1
+ export*from"./backend";export*from"./table.types";export*from"./where";export*from"./table";export*from"./map-deque";
@@ -0,0 +1 @@
1
+ import{useCallback as r,useDebugValue as S,useId as p,useLayoutEffect as a,useMemo as D}from"react";import{isError as f,isPromise as y}from"../utils/is";import{useSyncExternalStoreWithSelector as x}from"use-sync-external-store/shim/with-selector";function L(e,s={},u=[]){const{select:c}=s,t=p();a(()=>{e.updateSearchOptions(t,{...s,select:void 0})},u);const l=r(o=>c?o.map(c):o,[c]),d=r(o=>e.subscribe(t,o),[e,t]),i=r(()=>e.getSnapshot(t),[e,t]),n=x(d,i,i,l);if(S(n),y(n)||f(n))throw n;const m=D(()=>({next:()=>e.next(t),reset:()=>e.refresh(t)}),[t,e]);return[n,m]}export{L as useSqliteValue};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muya",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "author": "samuel.gjabel@gmail.com",
5
5
  "repository": "https://github.com/samuelgjabel/muya",
6
6
  "main": "cjs/index.js",
@@ -1,4 +1,4 @@
1
- import { createSqliteState } from '../create-sqlite-state'
1
+ import { createSqliteState } from '../create-sqlite'
2
2
  import { bunMemoryBackend } from '../table/bun-backend'
3
3
 
4
4
  const backend = bunMemoryBackend()
@@ -10,7 +10,7 @@ interface Person {
10
10
 
11
11
  describe('create-sqlite-state', () => {
12
12
  it('should batchSet and update multiple documents', async () => {
13
- const sql = await createSqliteState<Person>({ backend, tableName: 'State2', key: 'id' })
13
+ const sql = createSqliteState<Person>({ backend, tableName: 'State2', key: 'id' })
14
14
  await sql.batchSet([
15
15
  { id: '1', name: 'Alice', age: 30 },
16
16
  { id: '2', name: 'Bob', age: 25 },
@@ -32,7 +32,7 @@ describe('create-sqlite-state', () => {
32
32
  })
33
33
 
34
34
  it('should deleteBy condition', async () => {
35
- const sql = await createSqliteState<Person>({ backend, tableName: 'State3', key: 'id' })
35
+ const sql = createSqliteState<Person>({ backend, tableName: 'State3', key: 'id' })
36
36
  await sql.batchSet([
37
37
  { id: '1', name: 'Alice', age: 30 },
38
38
  { id: '2', name: 'Bob', age: 25 },
@@ -46,7 +46,7 @@ describe('create-sqlite-state', () => {
46
46
  })
47
47
 
48
48
  it('should get by key and with selector', async () => {
49
- const sql = await createSqliteState<Person>({ backend, tableName: 'State4', key: 'id' })
49
+ const sql = createSqliteState<Person>({ backend, tableName: 'State4', key: 'id' })
50
50
  await sql.set({ id: '1', name: 'Alice', age: 30 })
51
51
  const doc = await sql.get('1')
52
52
  expect(doc).toEqual({ id: '1', name: 'Alice', age: 30 })
@@ -57,7 +57,7 @@ describe('create-sqlite-state', () => {
57
57
  })
58
58
 
59
59
  it('should count documents with and without where', async () => {
60
- const sql = await createSqliteState<Person>({ backend, tableName: 'State5', key: 'id' })
60
+ const sql = createSqliteState<Person>({ backend, tableName: 'State5', key: 'id' })
61
61
  await sql.batchSet([
62
62
  { id: '1', name: 'Alice', age: 30 },
63
63
  { id: '2', name: 'Bob', age: 25 },
@@ -68,7 +68,7 @@ describe('create-sqlite-state', () => {
68
68
  })
69
69
 
70
70
  it('should support search with options', async () => {
71
- const sql = await createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
71
+ const sql = createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
72
72
  await sql.batchSet([
73
73
  { id: '1', name: 'Alice', age: 30 },
74
74
  { id: '2', name: 'Bob', age: 25 },
@@ -1,6 +1,6 @@
1
1
  import { act, renderHook } from '@testing-library/react-hooks'
2
- import { createSqliteState } from '../create-sqlite-state'
3
- import { useSqliteValue } from '../use-sqlite-value'
2
+ import { createSqliteState } from '../create-sqlite'
3
+ import { useSqliteValue } from '../use-sqlite'
4
4
  import { waitFor } from '@testing-library/react'
5
5
  import { bunMemoryBackend } from '../table/bun-backend'
6
6
  import { useState } from 'react'
@@ -15,7 +15,7 @@ interface Person {
15
15
 
16
16
  describe('use-sqlite-state', () => {
17
17
  it('should get basic value states', async () => {
18
- const sql = await createSqliteState<Person>({ backend, tableName: 'State1', key: 'id' })
18
+ const sql = createSqliteState<Person>({ backend, tableName: 'State1', key: 'id' })
19
19
  let reRenders = 0
20
20
  const { result } = renderHook(() => {
21
21
  reRenders++
@@ -62,7 +62,7 @@ describe('use-sqlite-state', () => {
62
62
  })
63
63
 
64
64
  it('should use where clause changed via state', async () => {
65
- const sql = await createSqliteState<Person>({ backend, tableName: 'State2', key: 'id' })
65
+ const sql = createSqliteState<Person>({ backend, tableName: 'State2', key: 'id' })
66
66
  await sql.batchSet([
67
67
  { id: '1', name: 'Alice', age: 30 },
68
68
  { id: '2', name: 'Bob', age: 25 },
@@ -91,7 +91,7 @@ describe('use-sqlite-state', () => {
91
91
  })
92
92
 
93
93
  it('should support like in where clause and update results', async () => {
94
- const sql = await createSqliteState<Person>({ backend, tableName: 'State3', key: 'id' })
94
+ const sql = createSqliteState<Person>({ backend, tableName: 'State3', key: 'id' })
95
95
  await sql.batchSet([
96
96
  { id: '1', name: 'Alice', age: 30 },
97
97
  { id: '2', name: 'Alicia', age: 25 },
@@ -118,7 +118,7 @@ describe('use-sqlite-state', () => {
118
118
  })
119
119
 
120
120
  it('should update results when changing order and limit options', async () => {
121
- const sql = await createSqliteState<Person>({ backend, tableName: 'State4', key: 'id' })
121
+ const sql = createSqliteState<Person>({ backend, tableName: 'State4', key: 'id' })
122
122
  await sql.batchSet([
123
123
  { id: '1', name: 'Alice', age: 30 },
124
124
  { id: '2', name: 'Bob', age: 25 },
@@ -146,7 +146,7 @@ describe('use-sqlite-state', () => {
146
146
  })
147
147
 
148
148
  it('should support actions.next and actions.refresh', async () => {
149
- const sql = await createSqliteState<Person>({ backend, tableName: 'State5', key: 'id' })
149
+ const sql = createSqliteState<Person>({ backend, tableName: 'State5', key: 'id' })
150
150
  await sql.batchSet([
151
151
  { id: '1', name: 'Alice', age: 30 },
152
152
  { id: '2', name: 'Bob', age: 25 },
@@ -161,7 +161,7 @@ describe('use-sqlite-state', () => {
161
161
  })
162
162
  })
163
163
  it('should handle thousands of records', async () => {
164
- const sql = await createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
164
+ const sql = createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
165
165
  const people: Person[] = []
166
166
  const ITEMS_COUNT = 1000
167
167
  for (let index = 1; index <= ITEMS_COUNT; index++) {
@@ -190,8 +190,47 @@ describe('use-sqlite-state', () => {
190
190
  expect(result.current[0].length).toBe(DEFAULT_STEP_SIZE)
191
191
  })
192
192
  })
193
+
194
+ it('should handle thousands of records with single update', async () => {
195
+ const sql = createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
196
+ const people: Person[] = []
197
+ const ITEMS_COUNT = 10_000
198
+ const stepSize = 5000
199
+ for (let index = 1; index <= ITEMS_COUNT; index++) {
200
+ people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
201
+ }
202
+ await sql.batchSet(people)
203
+ let reRenders = 0
204
+ const { result } = renderHook(() => {
205
+ reRenders++
206
+ return useSqliteValue(sql, { stepSize }, [])
207
+ })
208
+ await waitFor(() => {
209
+ expect(reRenders).toBe(2)
210
+ expect(result.current[0].length).toBe(stepSize)
211
+ })
212
+
213
+ act(() => {
214
+ for (let index = 0; index < ITEMS_COUNT / stepSize; index++) {
215
+ result.current[1].next()
216
+ }
217
+ })
218
+
219
+ await waitFor(() => {
220
+ expect(reRenders).toBe(4)
221
+ expect(result.current[0].length).toBe(ITEMS_COUNT)
222
+ })
223
+
224
+ act(() => {
225
+ result.current[1].reset()
226
+ })
227
+ await waitFor(() => {
228
+ expect(reRenders).toBe(5)
229
+ expect(result.current[0].length).toBe(stepSize)
230
+ })
231
+ })
193
232
  it('should change ordering', async () => {
194
- const sql = await createSqliteState<Person>({ backend, tableName: 'State7', key: 'id' })
233
+ const sql = createSqliteState<Person>({ backend, tableName: 'State7', key: 'id', indexes: ['age'] })
195
234
  const people: Person[] = []
196
235
  for (let index = 1; index <= 100; index++) {
197
236
  people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
@@ -210,4 +249,26 @@ describe('use-sqlite-state', () => {
210
249
  expect(result.current[0][0].age).toBe(69)
211
250
  })
212
251
  })
252
+
253
+ it('should support selector in options', async () => {
254
+ const sql = createSqliteState<Person>({ backend, tableName: 'State8', key: 'id' })
255
+ await sql.batchSet([
256
+ { id: '1', name: 'Alice', age: 30 },
257
+ { id: '2', name: 'Bob', age: 25 },
258
+ { id: '3', name: 'Carol', age: 40 },
259
+ ])
260
+ const { result } = renderHook(() =>
261
+ useSqliteValue(
262
+ sql,
263
+ {
264
+ sorBy: 'age',
265
+ select: (d) => d.name,
266
+ },
267
+ [],
268
+ ),
269
+ )
270
+ await waitFor(() => {
271
+ expect(result.current[0]).toEqual(['Bob', 'Alice', 'Carol'])
272
+ })
273
+ })
213
274
  })
@@ -3,6 +3,7 @@
3
3
  /* eslint-disable no-shadow */
4
4
  import { createScheduler } from '../scheduler'
5
5
  import { shallow } from '../utils/shallow'
6
+ import { selectSql, type CreateState } from './select-sql'
6
7
  import { createTable, DEFAULT_STEP_SIZE } from './table/table'
7
8
  import type { DbOptions, DocType, Key, MutationResult, SearchOptions, Table } from './table/table.types'
8
9
  import type { Where } from './table/where'
@@ -14,6 +15,7 @@ let stateId = 0
14
15
  function getStateId() {
15
16
  return stateId++
16
17
  }
18
+
17
19
  export interface SyncTable<Document extends DocType> {
18
20
  // readonly registerSearch: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => () => void
19
21
  readonly updateSearchOptions: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => void
@@ -31,6 +33,10 @@ export interface SyncTable<Document extends DocType> {
31
33
  readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>
32
34
  readonly destroy: () => void
33
35
  readonly next: (searchId: SearchId) => Promise<boolean>
36
+
37
+ readonly select: <Params extends unknown[]>(
38
+ compute: (...args: Params) => SearchOptions<Document>,
39
+ ) => CreateState<Document, Params>
34
40
  }
35
41
 
36
42
  interface DataItems<Document extends DocType> {
@@ -39,7 +45,7 @@ interface DataItems<Document extends DocType> {
39
45
  options?: SearchOptions<Document, unknown>
40
46
  }
41
47
 
42
- export async function createSqliteState<Document extends DocType>(options: DbOptions<Document>): Promise<SyncTable<Document>> {
48
+ export function createSqliteState<Document extends DocType>(options: DbOptions<Document>): SyncTable<Document> {
43
49
  // const table = await createTable<Document>(options)
44
50
 
45
51
  const id = getStateId()
@@ -66,7 +72,7 @@ export async function createSqliteState<Document extends DocType>(options: DbOpt
66
72
 
67
73
  async function next(searchId: SearchId, data: DataItems<Document>): Promise<boolean> {
68
74
  const iterator = iterators.get(searchId)
69
- const { items, options = {} } = data
75
+ const { options = {} } = data
70
76
  const { stepSize = DEFAULT_STEP_SIZE } = options
71
77
  if (!iterator) return false
72
78
  const newItems: Document[] = []
@@ -81,9 +87,10 @@ export async function createSqliteState<Document extends DocType>(options: DbOpt
81
87
  data.keys.add(String(result.value.rowId))
82
88
  }
83
89
 
90
+ if (newItems.length === 0) return false
84
91
  if (shallow(data.items, newItems)) return false
85
- data.items = [...items, ...newItems]
86
- return newItems.length > 0
92
+ data.items = [...data.items, ...newItems]
93
+ return true
87
94
  }
88
95
 
89
96
  function notifyListeners(searchId: SearchId) {
@@ -164,7 +171,7 @@ export async function createSqliteState<Document extends DocType>(options: DbOpt
164
171
  return data
165
172
  }
166
173
 
167
- return {
174
+ const state: SyncTable<Document> = {
168
175
  async set(document) {
169
176
  const table = await getTable()
170
177
  const changes = await table.set(document)
@@ -252,5 +259,11 @@ export async function createSqliteState<Document extends DocType>(options: DbOpt
252
259
  }
253
260
  return false
254
261
  },
262
+
263
+ select(compute) {
264
+ return selectSql(state, compute)
265
+ },
255
266
  }
267
+
268
+ return state
256
269
  }
@@ -0,0 +1,4 @@
1
+ export * from './create-sqlite'
2
+ export * from './table'
3
+ export * from './select-sql'
4
+ export * from './use-sqlite'
@@ -0,0 +1,55 @@
1
+ import { createState } from '../create-state'
2
+ import type { GetState } from '../types'
3
+ import type { SyncTable } from './create-sqlite'
4
+ import type { DocType } from './table/table.types'
5
+ import type { Where } from './table/where'
6
+
7
+ export type CreateState<Document, Params extends unknown[]> = (...params: Params) => GetState<Document[]>
8
+
9
+ export interface SqlSeachOptions<Document extends DocType> {
10
+ readonly sorBy?: keyof Document
11
+ readonly order?: 'asc' | 'desc'
12
+ readonly limit?: number
13
+ readonly offset?: number
14
+ readonly where?: Where<Document>
15
+ readonly stepSize?: number
16
+ }
17
+
18
+ let stateId = 0
19
+ function getStateId() {
20
+ stateId++
21
+ return `${stateId.toString(36)}-sql`
22
+ }
23
+
24
+ export function selectSql<Document extends DocType, Params extends unknown[] = []>(
25
+ state: SyncTable<Document>,
26
+ compute: (...args: Params) => SqlSeachOptions<Document>,
27
+ ): CreateState<Document, Params> {
28
+ const { subscribe, updateSearchOptions } = state
29
+
30
+ const result: CreateState<Document, Params> = (...params) => {
31
+ const searchId = getStateId()
32
+ const destroy = subscribe(searchId, () => {
33
+ getState.emitter.emit()
34
+ })
35
+
36
+ const options = compute(...params)
37
+ const getState = createState<Document[]>({
38
+ destroy() {
39
+ destroy()
40
+ getState.emitter.clear()
41
+ getState.cache.current = undefined
42
+ },
43
+ get() {
44
+ return state.getSnapshot(searchId)
45
+ },
46
+ getSnapshot() {
47
+ return state.getSnapshot(searchId)
48
+ },
49
+ })
50
+ updateSearchOptions<Document>(searchId, options)
51
+
52
+ return getState
53
+ }
54
+ return result
55
+ }
@@ -0,0 +1,5 @@
1
+ export * from './backend'
2
+ export * from './table.types'
3
+ export * from './where'
4
+ export * from './table'
5
+ export * from './map-deque'
@@ -1,22 +1,16 @@
1
1
  import { useCallback, useDebugValue, useId, useLayoutEffect, useMemo, type DependencyList } from 'react'
2
- import type { SyncTable } from './create-sqlite-state'
2
+ import type { SyncTable } from './create-sqlite'
3
3
  import type { DocType } from './table/table.types'
4
4
  import { isError, isPromise } from '../utils/is'
5
5
  import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
6
- import type { Where } from './table/where'
6
+ import type { SqlSeachOptions } from './select-sql'
7
7
 
8
8
  export interface SqLiteActions {
9
9
  readonly next: () => Promise<boolean>
10
10
  readonly reset: () => Promise<void>
11
11
  }
12
12
 
13
- export interface UseSearchOptions<Document extends DocType, Selected = Document> {
14
- readonly sorBy?: keyof Document
15
- readonly order?: 'asc' | 'desc'
16
- readonly limit?: number
17
- readonly offset?: number
18
- readonly where?: Where<Document>
19
- readonly stepSize?: number
13
+ export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
20
14
  /**
21
15
  * Naive projection. Prefer specialized queries for heavy fan-out graphs.
22
16
  */
@@ -1,3 +1,4 @@
1
+ import { type CreateState } from './select-sql';
1
2
  import type { DbOptions, DocType, Key, MutationResult, SearchOptions } from './table/table.types';
2
3
  import type { Where } from './table/where';
3
4
  type SearchId = string;
@@ -17,6 +18,7 @@ export interface SyncTable<Document extends DocType> {
17
18
  readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>;
18
19
  readonly destroy: () => void;
19
20
  readonly next: (searchId: SearchId) => Promise<boolean>;
21
+ readonly select: <Params extends unknown[]>(compute: (...args: Params) => SearchOptions<Document>) => CreateState<Document, Params>;
20
22
  }
21
- export declare function createSqliteState<Document extends DocType>(options: DbOptions<Document>): Promise<SyncTable<Document>>;
23
+ export declare function createSqliteState<Document extends DocType>(options: DbOptions<Document>): SyncTable<Document>;
22
24
  export {};
@@ -0,0 +1,4 @@
1
+ export * from './create-sqlite';
2
+ export * from './table';
3
+ export * from './select-sql';
4
+ export * from './use-sqlite';
@@ -0,0 +1,14 @@
1
+ import type { GetState } from '../types';
2
+ import type { SyncTable } from './create-sqlite';
3
+ import type { DocType } from './table/table.types';
4
+ import type { Where } from './table/where';
5
+ export type CreateState<Document, Params extends unknown[]> = (...params: Params) => GetState<Document[]>;
6
+ export interface SqlSeachOptions<Document extends DocType> {
7
+ readonly sorBy?: keyof Document;
8
+ readonly order?: 'asc' | 'desc';
9
+ readonly limit?: number;
10
+ readonly offset?: number;
11
+ readonly where?: Where<Document>;
12
+ readonly stepSize?: number;
13
+ }
14
+ export declare function selectSql<Document extends DocType, Params extends unknown[] = []>(state: SyncTable<Document>, compute: (...args: Params) => SqlSeachOptions<Document>): CreateState<Document, Params>;
@@ -0,0 +1,5 @@
1
+ export * from './backend';
2
+ export * from './table.types';
3
+ export * from './where';
4
+ export * from './table';
5
+ export * from './map-deque';
@@ -1,18 +1,12 @@
1
1
  import { type DependencyList } from 'react';
2
- import type { SyncTable } from './create-sqlite-state';
2
+ import type { SyncTable } from './create-sqlite';
3
3
  import type { DocType } from './table/table.types';
4
- import type { Where } from './table/where';
4
+ import type { SqlSeachOptions } from './select-sql';
5
5
  export interface SqLiteActions {
6
6
  readonly next: () => Promise<boolean>;
7
7
  readonly reset: () => Promise<void>;
8
8
  }
9
- export interface UseSearchOptions<Document extends DocType, Selected = Document> {
10
- readonly sorBy?: keyof Document;
11
- readonly order?: 'asc' | 'desc';
12
- readonly limit?: number;
13
- readonly offset?: number;
14
- readonly where?: Where<Document>;
15
- readonly stepSize?: number;
9
+ export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
16
10
  /**
17
11
  * Naive projection. Prefer specialized queries for heavy fan-out graphs.
18
12
  */
@@ -1 +0,0 @@
1
- import{createSqliteState as o}from"../create-sqlite-state";import{bunMemoryBackend as s}from"../table/bun-backend";const n=s();describe("create-sqlite-state",()=>{it("should batchSet and update multiple documents",async()=>{const e=await o({backend:n,tableName:"State2",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25}]);const t=[];for await(const i of e.search())t.push(i);expect(t).toHaveLength(2),await e.batchSet([{id:"1",name:"Alice2",age:31},{id:"2",name:"Bob2",age:26}]);const a=[];for await(const i of e.search())a.push(i);expect(a).toEqual([{id:"1",name:"Alice2",age:31},{id:"2",name:"Bob2",age:26}])}),it("should deleteBy condition",async()=>{const e=await o({backend:n,tableName:"State3",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);const t=await e.deleteBy({age:{gt:30}});expect(t.length).toBe(1);const a=[];for await(const i of e.search())a.push(i);expect(a.map(i=>i.id)).toEqual(["1","2"])}),it("should get by key and with selector",async()=>{const e=await o({backend:n,tableName:"State4",key:"id"});await e.set({id:"1",name:"Alice",age:30});const t=await e.get("1");expect(t).toEqual({id:"1",name:"Alice",age:30});const a=await e.get("1",c=>c.name);expect(a).toBe("Alice");const i=await e.get("999");expect(i).toBeUndefined()}),it("should count documents with and without where",async()=>{const e=await o({backend:n,tableName:"State5",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]),expect(await e.count()).toBe(3),expect(await e.count({where:{age:{gt:30}}})).toBe(1)}),it("should support search with options",async()=>{const e=await o({backend:n,tableName:"State6",key:"id"});await e.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);const t=[];for await(const a of e.search({where:{age:{lt:35}}}))t.push(a);expect(t.map(a=>a.id)).toEqual(["1","2"])})});
@@ -1 +0,0 @@
1
- import{act as o,renderHook as c}from"@testing-library/react-hooks";import{createSqliteState as s}from"../create-sqlite-state";import{useSqliteValue as l}from"../use-sqlite-value";import{waitFor as i}from"@testing-library/react";import{bunMemoryBackend as m}from"../table/bun-backend";import{useState as p}from"react";import{DEFAULT_STEP_SIZE as d}from"../table/table";const u=m();describe("use-sqlite-state",()=>{it("should get basic value states",async()=>{const r=await s({backend:u,tableName:"State1",key:"id"});let t=0;const{result:n}=c(()=>(t++,l(r,{},[])));expect(t).toBe(1),o(()=>{r.set({id:"1",name:"Alice",age:30})}),await i(()=>{expect(n.current[0]).toEqual([{id:"1",name:"Alice",age:30}]),expect(t).toBe(3)}),o(()=>{r.set({id:"1",name:"Alice2",age:30})}),await i(()=>{expect(n.current[0]).toEqual([{id:"1",name:"Alice2",age:30}]),expect(t).toBe(4)}),o(()=>{r.delete("1")}),await i(()=>{expect(n.current[0]).toEqual([]),expect(t).toBe(5)}),o(()=>{r.set({id:"1",name:"Alice",age:30}),r.set({id:"2",name:"Bob",age:25})}),await i(()=>{expect(n.current[0].length).toBe(2),expect(t).toBe(6)})}),it("should use where clause changed via state",async()=>{const r=await s({backend:u,tableName:"State2",key:"id"});await r.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);let t=0;const{result:n}=c(()=>{t++;const[a,e]=p(20);return[l(r,{where:{age:{gt:a}},sorBy:"age"},[a]),e]});await i(()=>{expect(n.current[0][0].map(a=>a.name)).toEqual(["Bob","Alice","Carol"]),expect(t).toBe(2)}),o(()=>{n.current[1](29)}),await i(()=>{expect(n.current[0][0].map(a=>a.name)).toEqual(["Alice","Carol"]),expect(t).toBe(4)})}),it("should support like in where clause and update results",async()=>{const r=await s({backend:u,tableName:"State3",key:"id"});await r.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Alicia",age:25},{id:"3",name:"Bob",age:40}]);let t=0;const{result:n,rerender:a}=c(({like:e})=>(t++,l(r,{where:{name:{like:e}}},[e])),{initialProps:{like:"%Ali%"}});await i(()=>{expect(n.current[0].map(e=>e.name)).toEqual(["Alice","Alicia"])}),o(()=>{a({like:"%Bob%"})}),await i(()=>{expect(n.current[0].map(e=>e.name)).toEqual(["Bob"])}),expect(t).toBeGreaterThanOrEqual(2)}),it("should update results when changing order and limit options",async()=>{const r=await s({backend:u,tableName:"State4",key:"id"});await r.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25},{id:"3",name:"Carol",age:40}]);const{result:t,rerender:n}=c(({order:a,limit:e})=>l(r,{sorBy:"age",order:a,limit:e},[a,e]),{initialProps:{order:"asc",limit:2}});await i(()=>{expect(t.current[0].map(a=>a.name)).toEqual(["Bob","Alice"])}),o(()=>{n({order:"desc",limit:2})}),await i(()=>{expect(t.current[0].map(a=>a.name)).toEqual(["Carol","Alice"])}),o(()=>{n({order:"desc",limit:1})}),await i(()=>{expect(t.current[0].map(a=>a.name)).toEqual(["Carol"])})}),it("should support actions.next and actions.refresh",async()=>{const r=await s({backend:u,tableName:"State5",key:"id"});await r.batchSet([{id:"1",name:"Alice",age:30},{id:"2",name:"Bob",age:25}]);const{result:t}=c(()=>l(r,{},[]));await i(()=>{expect(typeof t.current[1].next).toBe("function"),expect(typeof t.current[1].reset).toBe("function"),expect(t.current[1].reset()).resolves.toBeUndefined(),expect(t.current[1].next()).resolves.toBeFalsy()})}),it("should handle thousands of records",async()=>{const r=await s({backend:u,tableName:"State6",key:"id"}),t=[],n=1e3;for(let e=1;e<=n;e++)t.push({id:e.toString(),name:`Person${e}`,age:20+e%50});await r.batchSet(t);const{result:a}=c(()=>l(r,{},[]));await i(()=>{expect(a.current[0].length).toBe(d)});for(let e=0;e<n/d;e++)o(()=>{a.current[1].next()}),await i(()=>{expect(a.current[0].length).toBe(Math.min(d*(e+2),n))});o(()=>{a.current[1].reset()}),await i(()=>{expect(a.current[0].length).toBe(d)})}),it("should change ordering",async()=>{const r=await s({backend:u,tableName:"State7",key:"id"}),t=[];for(let e=1;e<=100;e++)t.push({id:e.toString(),name:`Person${e}`,age:20+e%50});await r.batchSet(t);const{result:n,rerender:a}=c(({order:e})=>l(r,{sorBy:"age",order:e},[e]),{initialProps:{order:"asc"}});await i(()=>{expect(n.current[0][0].age).toBe(20)}),o(()=>{a({order:"desc"})}),await i(()=>{expect(n.current[0][0].age).toBe(69)})})});
@@ -1 +0,0 @@
1
- import{createScheduler as P}from"../scheduler";import{shallow as R}from"../utils/shallow";import{createTable as M,DEFAULT_STEP_SIZE as v}from"./table/table";const f=P();let O=0;function E(){return O++}async function L(g){const T=E();function l(e){return`state-${T}-search-${e}`}let h;async function o(){return h||(h=await M(g)),h}const c=new Map,i=new Map,m=new Map;async function b(e,n){const t=m.get(e),{items:a,options:s={}}=n,{stepSize:d=v}=s;if(!t)return!1;const r=[];for(let I=0;I<d;I++){const y=await t.next();if(y.done){m.delete(e);break}r.push(y.value.document),n.keys.add(String(y.value.rowId))}return R(n.items,r)?!1:(n.items=[...a,...r],r.length>0)}function w(e){const n=i.get(e);n&&n()}async function k(e){const n=await o(),t=c.get(e);if(!t)return;const{options:a}=t,s=n.search({...a,select:(d,{rowId:r})=>({document:d,rowId:r})});m.set(e,s),t.keys=new Set,t.items=[],await b(e,t)}async function S(e){await k(e),w(e)}function x(e){const{key:n,op:t}=e,a=new Set;for(const[s,{keys:d}]of c)switch(t){case"delete":case"update":{d.has(String(n))&&a.add(s);break}case"insert":{a.add(s);break}}return a}async function u(e){const n=new Set;for(const t of e){const a=x(t);for(const s of a)n.add(s)}for(const t of n){const a=l(t);f.schedule(a,{searchId:t})}}const p=new Set;function D(e,n){c.has(e)||(c.set(e,{items:[],options:n,keys:new Set}),n&&S(e));const t=c.get(e);return n&&(t.options=n),t}return{async set(e){const t=await(await o()).set(e);return await u([t]),t},async batchSet(e){const t=await(await o()).batchSet(e);return await u(t),t},async delete(e){const t=await(await o()).delete(e);return t&&await u([t]),t},async deleteBy(e){const t=await(await o()).deleteBy(e);return await u(t),t},async get(e,n){return(await o()).get(e,n)},async*search(e={}){const n=await o();for await(const t of n.search(e))yield t},async count(e){return await(await o()).count(e)},updateSearchOptions(e,n){const t=D(e,n);t.options=n;const a=l(e);f.schedule(a,{searchId:e})},subscribe(e,n){const t=l(e),a=f.add(t,{onScheduleDone(){S(e)}});return p.add(a),i.has(e)||i.set(e,n),()=>{i.delete(e),a(),c.delete(e)}},getSnapshot(e){return D(e).items},refresh:S,destroy(){for(const e of p)e();c.clear(),i.clear()},async next(e){const n=c.get(e);if(n){const t=await b(e,n);return t&&w(e),t}return!1}}}export{L as createSqliteState};
@@ -1 +0,0 @@
1
- import{useCallback as c,useDebugValue as a,useId as p,useLayoutEffect as y,useMemo as S}from"react";import{isError as f,isPromise as D}from"../utils/is";import{useSyncExternalStoreWithSelector as b}from"use-sync-external-store/shim/with-selector";function q(e,s={},u=[]){const{select:r}=s,t=p();y(()=>{e.updateSearchOptions(t,{...s,select:void 0})},u);const l=c(n=>r?n.map(r):n,[r]),d=c(n=>e.subscribe(t,n),[e,t]),i=c(()=>e.getSnapshot(t),[e,t]),o=b(d,i,i,l);if(a(o),D(o)||f(o))throw o;const m=S(()=>({next:()=>e.next(t),reset:()=>e.refresh(t)}),[t,e]);return[o,m]}export{q as useSqliteValue};