muya 2.2.8 → 2.3.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.
package/README.md CHANGED
@@ -1,268 +1,271 @@
1
-
2
1
  # **Muya 🌀**
3
2
 
4
- Muya is simple and lightweight react state management library.
3
+ *A tiny, type-safe state manager for React with a dead-simple mental model:*
4
+ - **Create a state**
5
+ - **Read it in components**
6
+ - **Update it**
7
+ - **Derive more states when you need to**
5
8
 
6
9
  ---
7
10
 
8
11
  [![Build](https://github.com/samuelgja/muya/actions/workflows/build.yml/badge.svg)](https://github.com/samuelgja/muya/actions/workflows/build.yml)
9
- [![Code Quality Check](https://github.com/samuelgja/muya/actions/workflows/code-check.yml/badge.svg)](https://github.com/samuelgja/muya/actions/workflows/code-check.yml)
10
- [![Build Size](https://img.shields.io/bundlephobia/minzip/muya?label=Bundle%20size)](https://bundlephobia.com/result?p=muya)
11
-
12
+ [![Code Quality](https://github.com/samuelgja/muya/actions/workflows/code-check.yml/badge.svg)](https://github.com/samuelgja/muya/actions/workflows/code-check.yml)
13
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/muya?label=Bundle%20size)](https://bundlephobia.com/result?p=muya)
12
14
 
13
- - **Simplified API**: Only `create` and `select`.
14
- - **Batch Updates**: Built-in batching ensures efficient `muya` state updates internally.
15
- - **TypeScript Support**: Type first.
16
- - **Lightweight**: Minimal bundle size.
15
+ ## Highlights
16
+ - **2-call API**: `create` and `select` (plus a tiny hook `useValue`)
17
+ - **React-friendly**: internal batching; opt-in equality checks to skip renders
18
+ - **Type-first**: full TypeScript support
19
+ - **Lightweight**: built for small mental & bundle footprint
17
20
 
18
21
  ---
19
22
 
20
- ## 📦 **Installation**
21
-
22
- Install with your favorite package manager:
23
+ ## 📦 Install
23
24
  ```bash
24
25
  bun add muya@latest
25
- ```
26
- ```bash
27
- npm install muya@latest
28
- ```
29
- ```bash
26
+ # or
27
+ npm i muya@latest
28
+ # or
30
29
  yarn add muya@latest
31
30
  ```
32
31
 
33
32
  ---
34
33
 
35
- ## 📝 **Quick Start**
34
+ ## 🏃 Quick Start
36
35
 
37
- ### **Create and Use State**
36
+ ```tsx
37
+ import { create } from 'muya'
38
38
 
39
- ```typescript
40
- import { create } from 'muya';
39
+ // 1) Make a state
40
+ const counter = create(0)
41
41
 
42
- const useCounter = create(0);
43
-
44
- // Access in a React component
45
- function Counter() {
46
- const count = useCounter(); // Call state directly
42
+ // 2) Read it inside React
43
+ export function Counter() {
44
+ const count = counter() // state is a hook
47
45
  return (
48
46
  <div>
49
- <button onClick={() => useCounter.set((prev) => prev + 1)}>Increment</button>
47
+ <button onClick={() => counter.set(n => n + 1)}>+1</button>
50
48
  <p>Count: {count}</p>
51
49
  </div>
52
- );
50
+ )
53
51
  }
54
52
  ```
55
53
 
56
54
  ---
57
55
 
58
- ### **Select and Slice State**
59
-
60
- Use `select` to derive a slice of the state:
61
-
62
- ```typescript
63
- const state = create({ count: 0, value: 42 });
64
-
65
- const countSlice = state.select((s) => s.count);
66
-
67
- // Also async is possible, but Muya do not recommend
68
- // It can lead to spaghetti re-renders code which is hard to maintain and debug
69
- const asyncCountSlice = state.select(async (s) => {
70
- const data = await fetchData();
71
- return data.count;
72
- });
73
- ```
74
-
75
- ---
76
-
77
- ### **Combine Multiple States**
56
+ ## 🧩 Selecting / Deriving
78
57
 
79
- Combine multiple states into a derived state via `select` method:
58
+ Create derived states from one or many sources.
80
59
 
81
- ```typescript
60
+ ```ts
82
61
  import { create, select } from 'muya'
83
62
 
84
- const state1 = create(1);
85
- const state2 = create(2);
63
+ const a = create(1)
64
+ const b = create(2)
86
65
 
87
- const sum = select([state1, state2], (s1, s2) => s1 + s2);
88
- ```
66
+ // Single source
67
+ const doubleA = a.select(n => n * 2)
89
68
 
90
- ### **Equality Check**
91
-
92
- Customize equality checks to prevent unnecessary updates:
69
+ // Multiple sources
70
+ const sum = select([a, b], (x, y) => x + y)
71
+ ```
93
72
 
94
- ```typescript
95
- const state = create({ a: 1, b: 2 }, (prev, next) => prev.b === next.b);
73
+ **Equality checks** (to avoid re-emits):
96
74
 
97
- // Updates only when `b` changes
98
- state.set((prev) => ({ ...prev, a: prev.a + 1 }));
75
+ ```ts
76
+ const obj = create({ a: 1, b: 2 }, (prev, next) => prev.b === next.b)
77
+ obj.set(p => ({ ...p, a: p.a + 1 })) // does not notify (b unchanged)
99
78
  ```
100
79
 
101
- Or in select methods:
80
+ You can also add an equality function on a `select`:
102
81
 
103
- ```typescript
104
- const derived = select([state1, state2], (s1, s2) => s1 + s2, (prev, next) => prev === next);
82
+ ```ts
83
+ const stable = select([a, b], (x, y) => x + y, (prev, next) => prev === next)
105
84
  ```
106
85
 
107
86
  ---
108
87
 
88
+ ## 🎣 Using in Components
109
89
 
110
- ## 🖥️ **Using State in Components**
90
+ Muya states are callable hooks. Prefer that.
91
+ If you want a hook wrapper or slicing, use `useValue`.
111
92
 
112
- Access state directly or through `useValue` hook:
93
+ ```tsx
94
+ import { create, useValue } from 'muya'
113
95
 
114
- ### **Option 1: Access State Directly**
115
- Each state can be called as the hook directly
116
- ```typescript
117
- const userState = create(0);
96
+ const user = create({ id: 'u1', name: 'Ada', admin: false })
118
97
 
119
- function App() {
120
- const user = userState(); // Directly call state
121
- return <p>User: {user}</p>;
98
+ // Option 1: call the state directly
99
+ function Profile() {
100
+ const u = user()
101
+ return <div>{u.name}</div>
122
102
  }
123
- ```
124
103
 
125
- ### **Option 2: Use the Hook**
126
- Or for convenience, there is `useValue` method
127
- ```typescript
128
- import { useValue } from 'muya';
129
-
130
- function App() {
131
- const user = useValue(userState); // Access state via hook
132
- return <p>User: {user}</p>;
133
- }
134
- ```
135
-
136
- ### **Option 3: Slice with Hook**
137
- For efficient re-renders, `useValue` provides a slicing method.
138
- ```typescript
139
- function App() {
140
- const count = useValue(state, (s) => s.count); // Use selector in hook
141
- return <p>Count: {count}</p>;
104
+ // Option 2: useValue for a selector
105
+ function OnlyName() {
106
+ const name = useValue(user, u => u.name)
107
+ return <div>{name}</div>
142
108
  }
143
109
  ```
144
110
 
145
111
  ---
146
112
 
147
- ## 📖 **API Overview**
113
+ ## Async & Lazy (the 2-minute mental model)
148
114
 
149
- ### **`create`**
115
+ **Immediate vs Lazy**
116
+ ```ts
117
+ create(0) // immediate: value exists now
118
+ create(() => 0) // lazy: computed on first read (.get() / component render)
119
+ ```
150
120
 
151
- Create a new state:
121
+ **Async sources**
122
+ ```ts
123
+ async function fetchInitial() { return 0 }
124
+ create(fetchInitial) // lazy + async
125
+ create(Promise.resolve(0)) // immediate + async
126
+ ```
152
127
 
153
- ```typescript
154
- const state = create(initialValue, isEqual?);
128
+ **Setting with async state**
129
+ - `state.set(2)` **overrides immediately** (cancels previous pending promise)
130
+ - `state.set(prev => prev + 1)` **waits for the current promise to resolve** so `prev` is always the resolved value
155
131
 
156
- // Methods:
157
- state.get(); // Get current value
158
- state.set(value); // Update value
159
- state.listen(listener); // Subscribe to changes
160
- state.select(selector, isEqual?); // Create derived state
161
- state.destroy(); // Unsubscribe from changes, useful for dynamic state creation in components
162
- state.withName(name); // Add a name for debugging, otherwise it will be auto generated number
132
+ **Async selectors**
133
+ ```ts
134
+ const base = create(0)
135
+ const plusOne = base.select(async n => {
136
+ await doWork()
137
+ return n + 1
138
+ })
163
139
  ```
140
+ - Async selects **suspend** the first time (and when their upstream async value requires it)
141
+ - A sync selector reading an async parent will **suspend once** on initial load
164
142
 
165
- ### **`select`**
143
+ > Tip: Prefer keeping selectors **sync** and performing async work **before** calling `set`. It keeps render trees predictable.
166
144
 
167
- Combine or derive new states:
145
+ ---
168
146
 
169
- ```typescript
170
- const derived = select([state1, state2], (s1, s2) => s1 + s2);
147
+ ## 🧪 API (short and sweet)
171
148
 
172
- // Methods:
173
- derived.get(); // Get current value
174
- derived.listen(listener); // Subscribe to changes
175
- derived.select(selector, isEqual?); // Create nested derived state
176
- derived.destroy(); // Unsubscribe from changes, useful for dynamic state creation in components
177
- derived.withName(name); // Add a name for debugging, otherwise it will be auto generated number
178
- ```
149
+ ### `create<T>(initial, isEqual?) => State<T>`
150
+ **State methods**
151
+ - `get(): T` read current value (resolves lazy/async when needed)
152
+ - `set(value | (prev) => next)` update value (batched)
153
+ - `listen(fn)` subscribe, returns `unsubscribe`
154
+ - `select(selector, isEqual?)` derive new state from this state
155
+ - `destroy()` — clear listeners and dispose
156
+ - `withName(name)` — add a debug label (DevTools)
179
157
 
180
- ### **`useValue`**
158
+ ### `select([states], derive, isEqual?) => State<R>`
159
+ Derive a state from one or multiple states.
181
160
 
182
- React hook to access state:
183
-
184
- ```typescript
185
- const value = useValue(state, (s) => s.slice); // Optional selector
186
- ```
161
+ ### `useValue(state, selector?)`
162
+ React hook to read a state or a slice.
187
163
 
188
164
  ---
189
165
 
190
- ## ⚠️ **Notes**
166
+ ## 🪵 DevTools
167
+ In **dev**, Muya auto-connects to Redux DevTools if present and reports state updates by name.
168
+ Use `state.withName('CartItems')` to make timelines readable.
191
169
 
192
- - **Equality Check**: Prevent unnecessary updates by passing a custom equality function to `create` or `select`.
193
- - **Batch Updates**: Muya batches internal updates for better performance, reducing communication overhead similarly how react do.
194
- - **Async Selectors / Derives**: Muya has support for async selectors / derives, but do not recommend to use as it can lead to spaghetti re-renders code which is hard to maintain and debug, if you want so, you can or maybe you should consider using other alternatives like `Jotai`.
170
+ ---
195
171
 
172
+ ## 🤝 Patterns & Anti-patterns
196
173
 
174
+ **✅ Good**
175
+ - Keep selectors pure and fast
176
+ - Use equality checks to avoid unnecessary updates
177
+ - Do async outside, then `set` synchronously
178
+
179
+ **⚠️ Be cautious**
180
+ - Deep async chains of selectors → harder to debug and may re-suspend often
181
+ - Long-running async work inside selectors → push it out of render path
197
182
 
198
- `Muya` encourage use async updates withing sync state like this:
199
- ```typescript
200
- const state = create({ data: null });
201
- async function update() {
202
- const data = await fetchData();
203
- state.set({ data });
204
- }
205
- ```
206
183
  ---
207
184
 
208
- But of course you can do
185
+ ## 🧭 Examples
209
186
 
210
- Note: Handling async updates for the state (`set`) will cancel the previous pending promise.
211
- ```typescript
212
- const state = create(0)
213
- const asyncState = state.select(async (s) => {
214
- await longPromise(100)
215
- return s + 1
216
- })
187
+ **Boolean flags derived from async state (suspends only once):**
188
+ ```tsx
189
+ const user = create(fetchUser) // async lazy
190
+ const isAdmin = user.select(u => u.role === 'admin')
191
+ // First mount suspends while user loads, subsequent updates are instant
192
+ ```
193
+
194
+ **Batching inside events:**
195
+ ```ts
196
+ function onCheckout() {
197
+ cart.set(c => applyDiscount(c))
198
+ total.set(t => t - 10)
199
+ // Muya batches internally; React sees one commit
200
+ }
217
201
  ```
202
+
218
203
  ---
219
204
 
220
- ### Lazy resolution
221
- `Muya` can be used in `immediate` mode or in `lazy` mode. When create a state with just plain data, it will be in immediate mode, but if you create a state with a function, it will be in lazy mode. This is useful when you want to create a state that is executed only when it is accessed for the first time.
205
+ ## 🗃️ (Optional) Muya + SQLite Companion
222
206
 
207
+ If you’re using the companion `muya/sqlite` package, you can manage large, queryable lists with React-friendly pagination:
223
208
 
224
- ```typescript
225
- // immediate mode, so no matter what, this value is already stored in memory
226
- const state = create(0)
209
+ ```ts
210
+ import { createSqliteState } from 'muya/sqlite'
211
+ import { useSqliteValue } from 'muya/sqlite'
227
212
 
228
- // lazy mode, value is not stored in memory until it is accessed for the first time via get or component render
229
- const state = create(() => 0)
230
- ```
213
+ type Person = { id: string; name: string; age: number }
214
+
215
+ const people = createSqliteState<Person>({
216
+ backend, // e.g. expo-sqlite / bunMemoryBackend
217
+ tableName: 'People',
218
+ key: 'id',
219
+ indexes: ['age'], // optional
220
+ })
231
221
 
232
- And in async:
233
- ```typescript
234
- // we can create some initial functions like this
235
- async function initialLoad() {
236
- return 0
222
+ // In React: stepwise fetching + where/order/limit
223
+ function PeopleList() {
224
+ const [rows, actions] = useSqliteValue(people, { sorBy: 'age', order: 'asc', limit: 50 }, [])
225
+ return (
226
+ <>
227
+ <ul>{rows.map(p => <li key={p.id}>{p.name}</li>)}</ul>
228
+ <button onClick={() => actions.next()}>Load more</button>
229
+ <button onClick={() => actions.reset()}>Reset</button>
230
+ </>
231
+ )
237
232
  }
238
- // immediate mode, so no matter what, this value is already stored in memory
239
- const state = create(initialLoad)
240
- // or
241
- const state = create(Promise.resolve(0))
233
+ ```
242
234
 
243
- // lazy mode, value is not stored in memory until it is accessed for the first time via get or component render
244
- const state = create(() => Promise.resolve(0))
235
+ **Quick ops**
236
+ ```ts
237
+ await people.batchSet([{ id:'1', name:'Alice', age:30 }])
238
+ await people.set({ id:'2', name:'Bob', age:25 })
239
+ await people.delete('1')
240
+ const alice = await people.get('2', p => p.name) // 'Bob'
241
+ const count = await people.count({ where: { age: { gt: 20 } } })
242
+ for await (const p of people.search({ where: { name: { like: '%Ali%' }}})) { /* … */ }
245
243
  ```
246
244
 
247
- And when setting state when initial value is promise, set is always sync.
248
- But as in react there are two methods how to set a state. Directly `.set(2)` or with a function `.set((prev) => prev + 1)`.
245
+ ---
246
+
247
+ ## ❓ FAQ
248
+
249
+ **Is Muya a replacement for Redux/Zustand/Jotai?**
250
+ No—Muya is intentionally tiny. If you need complex middleware, effects, or ecosystem plugins, those tools are great choices.
249
251
 
250
- So how `set` state will behave with async initial value?
251
- 1. Directly call `.set(2)` will be sync, and will set the value to 2 (it will cancel the initial promise)
252
- 2. Call `.set((prev) => prev + 1)` will wait until previous promise is resolved, so previous value in set callback is always resolved.
252
+ **Can I use Suspense?**
253
+ Yes. Async states/selectors will suspend on first read (and when upstream requires it).
253
254
 
254
- ### Async selectors knowledge base
255
- 1. Values in selectors derived from another async selectors will be always sync **(not promise)**
256
- 2. If the selector is async (`state.select(async (s) => s + 1)`) it will automatically use suspense mode, so on each change it firstly resolved promise (hit suspense) and then update the value.
257
- 3. If the selector is sync, but it's derived from async selector, it will also be in suspense mode - but it depends what the parent is, if the parent is async state, it will hit the suspense only once, on initial load, but if the parent is another async selector, it will hit suspense on each change.
255
+ **How do I avoid re-renders?**
256
+ Provide an `isEqual(prev, next)` to `create` or `select`, or select a smaller slice in `useValue`.
258
257
 
259
- ### Debugging
260
- `Muya` in dev mode automatically connects to the `redux` devtools extension if it is installed in the browser. For now devtool api is simple - state updates.
258
+ ---
261
259
 
262
- ## 🙏 **Acknowledgments**
260
+ ## 🧪 Testing Tips
261
+ - State reads/writes are synchronous, but **async sources/selectors** resolve over time. In tests, use `await waitFor(...)` around expectations that depend on async resolution.
263
262
 
264
- This library is a fun, experimental project and not a replacement for robust state management tools. For more advanced use cases, consider libraries like `Zustand`, `Jotai`, or `Redux`.
265
- If you enjoy `Muya`, please give it a ⭐️! :)
263
+ ---
266
264
 
265
+ ## 📜 License
266
+ MIT — if you like Muya, a ⭐️ is always appreciated!
267
267
 
268
+ ---
268
269
 
270
+ ### Changelog / Contributing
271
+ See repo issues and PRs. Keep changes small and measured—Muya’s value is simplicity.
@@ -1,4 +1,4 @@
1
- import{getWhereQuery as h}from"./where";const O=500,N=100;function D(E){return"$."+E}function I(E,n){if(!(!E||!n))return n.split(".").reduce((r,y)=>{if(typeof r=="object"&&r!==null&&y in r)return r[y]},E)}async function M(E){const{backend:n,tableName:r,indexes:y,key:S,disablePragmaOptimization:$}=E,d=S!==void 0;$||(await n.execute("PRAGMA journal_mode=WAL;"),await n.execute("PRAGMA synchronous=NORMAL;"),await n.execute("PRAGMA temp_store=MEMORY;"),await n.execute("PRAGMA cache_size=-20000;")),d?await n.execute(`
1
+ import{getWhereQuery as p}from"./where";const O=500,N=100;function D(E){return"$."+E}function I(E,n){if(!(!E||!n))return n.split(".").reduce((r,y)=>{if(typeof r=="object"&&r!==null&&y in r)return r[y]},E)}async function M(E){const{backend:n,tableName:r,indexes:y,key:S,disablePragmaOptimization:$}=E,d=S!==void 0;$||(await n.execute("PRAGMA journal_mode=WAL;"),await n.execute("PRAGMA synchronous=NORMAL;"),await n.execute("PRAGMA temp_store=MEMORY;"),await n.execute("PRAGMA cache_size=-20000;")),d?await n.execute(`
2
2
  CREATE TABLE IF NOT EXISTS ${r} (
3
3
  key TEXT PRIMARY KEY,
4
4
  data TEXT NOT NULL
@@ -8,4 +8,4 @@ import{getWhereQuery as h}from"./where";const O=500,N=100;function D(E){return"$
8
8
  data TEXT NOT NULL
9
9
  );
10
10
  `);for(const t of y??[]){const o=String(t);await n.execute(`CREATE INDEX IF NOT EXISTS idx_${r}_${o.replaceAll(/\W/g,"_")}
11
- ON ${r} (json_extract(data, '${D(o)}'));`)}function k(t){if(d)return I(t,String(S))}async function g(t){return(await t.select("SELECT changes() AS c"))[0]?.c??0}const p={backend:n,async set(t,o){const e=o??n,a=JSON.stringify(t);if(d){const s=k(t);if(s==null)throw new Error(`Document is missing the configured key "${String(S)}". Provide it or create the table without "key".`);if(await e.execute(`UPDATE ${r} SET data = ? WHERE key = ?`,[a,s]),await g(e)===1)return{key:s,op:"update"};try{return await e.execute(`INSERT INTO ${r} (key, data) VALUES (?, ?)`,[s,a]),{key:s,op:"insert"}}catch{return await e.execute(`UPDATE ${r} SET data = ? WHERE key = ?`,[a,s]),{key:s,op:"update"}}}await e.execute(`INSERT INTO ${r} (data) VALUES (?)`,[a]);const u=(await e.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof u!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:u,op:"insert"}},async get(t,o=e=>e){const e=d?"key = ?":"rowid = ?",a=await n.select(`SELECT rowid, data FROM ${r} WHERE ${e}`,[t]);if(a.length===0)return;const{data:i,rowid:u}=a[0],s=JSON.parse(i);return o(s,{rowId:u})},async delete(t){const o=d?"key = ?":"rowid = ?";if(await n.execute(`DELETE FROM ${r} WHERE ${o}`,[t]),((await n.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:t,op:"delete"}},async*search(t={}){const{sortBy:o,order:e="asc",limit:a,offset:i=0,where:u,select:s=l=>l,stepSize:c=N}=t,w=h(u),f=`SELECT rowid, data FROM ${r} ${w}`;let T=0,A=i;for(;;){let l=f;o?l+=` ORDER BY json_extract(data, '${D(String(o))}') COLLATE NOCASE ${e.toUpperCase()}`:l+=d?` ORDER BY key COLLATE NOCASE ${e.toUpperCase()}`:` ORDER BY rowid ${e.toUpperCase()}`;const R=a?Math.min(c,a-T):c;l+=` LIMIT ${R} OFFSET ${A}`;const m=await n.select(l);if(m.length===0)break;for(const{rowid:b,data:L}of m){if(a&&T>=a)return;const x=JSON.parse(L);yield s(x,{rowId:b}),T++}if(m.length<R||a&&T>=a)break;A+=m.length}},async count(t={}){const o=h(t.where),e=`SELECT COUNT(*) as count FROM ${r} ${o}`;return(await n.select(e))[0]?.count??0},async deleteBy(t){const o=h(t),e=d?"key":"rowid",a=[];return await n.transaction(async i=>{const u=await i.select(`SELECT ${e} AS k FROM ${r} ${o}`);if(u.length===0)return;const s=u.map(c=>c.k);for(let c=0;c<s.length;c+=O){const w=s.slice(c,c+O),f=w.map(()=>"?").join(",");await i.execute(`DELETE FROM ${r} WHERE ${e} IN (${f})`,w)}for(const c of s)a.push({key:c,op:"delete"})}),a},async clear(){await n.execute(`DELETE FROM ${r}`)},async batchSet(t){const o=[];return await n.transaction(async e=>{for(const a of t){const i=await p.set(a,e);o.push(i)}}),o}};return p}export{N as DEFAULT_STEP_SIZE,M as createTable,I as getByPath};
11
+ ON ${r} (json_extract(data, '${D(o)}'));`)}function k(t){if(d)return I(t,String(S))}async function g(t){return(await t.select("SELECT changes() AS c"))[0]?.c??0}const h={backend:n,async set(t,o){const e=o??n,a=JSON.stringify(t);if(d){const s=k(t);if(s==null)throw new Error(`Document is missing the configured key "${String(S)}". Provide it or create the table without "key".`);if(await e.execute(`UPDATE ${r} SET data = ? WHERE key = ?`,[a,s]),await g(e)===1)return{key:s,op:"update"};try{return await e.execute(`INSERT INTO ${r} (key, data) VALUES (?, ?)`,[s,a]),{key:s,op:"insert"}}catch{return await e.execute(`UPDATE ${r} SET data = ? WHERE key = ?`,[a,s]),{key:s,op:"update"}}}await e.execute(`INSERT INTO ${r} (data) VALUES (?)`,[a]);const u=(await e.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof u!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:u,op:"insert"}},async get(t,o=e=>e){const e=d?"key = ?":"rowid = ?",a=await n.select(`SELECT rowid, data FROM ${r} WHERE ${e}`,[t]);if(a.length===0)return;const{data:i,rowid:u}=a[0],s=JSON.parse(i);return o(s,{rowId:u})},async delete(t){const o=d?"key = ?":"rowid = ?";if(await n.execute(`DELETE FROM ${r} WHERE ${o}`,[t]),((await n.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:t,op:"delete"}},async*search(t={}){const{sortBy:o,order:e="asc",limit:a,offset:i=0,where:u,select:s=l=>l,stepSize:c=N}=t,w=p(u),f=`SELECT rowid, data FROM ${r} ${w}`;let T=0,A=i;for(;;){let l=f;o?l+=` ORDER BY json_extract(data, '${D(String(o))}') COLLATE NOCASE ${e.toUpperCase()}`:l+=d?` ORDER BY key COLLATE NOCASE ${e.toUpperCase()}`:` ORDER BY rowid ${e.toUpperCase()}`;const R=a?Math.min(c,a-T):c;l+=` LIMIT ${R} OFFSET ${A}`;const m=await n.select(l);if(m.length===0)break;for(const{rowid:b,data:L}of m){if(a&&T>=a)return;const x=JSON.parse(L);yield s(x,{rowId:b}),T++}if(m.length<R||a&&T>=a)break;A+=m.length}},async count(t={}){const o=p(t.where),e=`SELECT COUNT(*) as count FROM ${r} ${o}`;return(await n.select(e))[0]?.count??0},async deleteBy(t){const o=p(t),e=d?"key":"rowid",a=[];return await n.transaction(async i=>{const u=await i.select(`SELECT ${e} AS k FROM ${r} ${o}`);if(u.length===0)return;const s=u.map(c=>c.k);for(let c=0;c<s.length;c+=O){const w=s.slice(c,c+O),f=w.map(()=>"?").join(",");await i.execute(`DELETE FROM ${r} WHERE ${e} IN (${f})`,w)}for(const c of s)a.push({key:c,op:"delete"})}),a},async clear(){await n.execute(`DELETE FROM ${r}`)},async batchSet(t){const o=[];return await n.transaction(async e=>{for(const a of t){const i=await h.set(a,e);o.push(i)}}),o}};return h}export{N as DEFAULT_STEP_SIZE,M as createTable,I as getByPath,D as toJsonPath};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muya",
3
- "version": "2.2.8",
3
+ "version": "2.3.0",
4
4
  "author": "samuel.gjabel@gmail.com",
5
5
  "repository": "https://github.com/samuelgjabel/muya",
6
6
  "main": "cjs/index.js",
@@ -12,6 +12,13 @@ import { useValue } from '../use-value'
12
12
  import { atom, useAtom } from 'jotai'
13
13
  import { create } from '../create'
14
14
 
15
+ /**
16
+ * Utility to render a hook and measure the time it takes to reach a certain state
17
+ * @param hook The hook to render
18
+ * @param getValue Function to extract the value to monitor from the hook's return data
19
+ * @param toBe The target value to wait for
20
+ * @returns An object containing the hook's result, a waitFor function, and a promise that resolves with the time taken
21
+ */
15
22
  function renderPerfHook<T>(hook: () => T, getValue: (data: T) => number, toBe: number) {
16
23
  let onResolve = (_value: number) => {}
17
24
  const resolvePromise = new Promise<number>((resolve) => {
@@ -1,6 +1,11 @@
1
1
  /* eslint-disable unicorn/prevent-abbreviations */
2
2
  import { Component } from 'react'
3
3
 
4
+ /**
5
+ * Create a promise that resolves after a specified time
6
+ * @param time Time in ms to wait
7
+ * @returns A promise that resolves after the specified time
8
+ */
4
9
  export function longPromise(time = 200): Promise<number> {
5
10
  return new Promise((resolve) => {
6
11
  setTimeout(() => {
@@ -12,6 +12,11 @@ interface GetStateOptions<T> {
12
12
  }
13
13
 
14
14
  let stateId = 0
15
+
16
+ /**
17
+ * Generate a unique state ID
18
+ * @returns A unique state ID
19
+ */
15
20
  function getStateId() {
16
21
  return stateId++
17
22
  }
@@ -19,6 +24,8 @@ function getStateId() {
19
24
  type FullState<T> = GetStateOptions<T>['set'] extends undefined ? GetState<T> : State<T>
20
25
  /**
21
26
  * This is just utility function to create state base data
27
+ * @param options Options to create state
28
+ * @returns FullState<T>
22
29
  */
23
30
  export function createState<T>(options: GetStateOptions<T>): FullState<T> {
24
31
  const { get, destroy, set, getSnapshot } = options
package/src/create.ts CHANGED
@@ -8,9 +8,16 @@ import { createState } from './create-state'
8
8
  export const STATE_SCHEDULER = createScheduler()
9
9
 
10
10
  /**
11
- * Create state from a default value.
11
+ * Create a new state with an initial value and optional equality check
12
+ * @param initialValue The initial value or a function that returns the initial value
13
+ * @param isEqual Optional custom equality check function
14
+ * @returns A State<T> object
12
15
  */
13
16
  export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = isEqualBase): State<T> {
17
+ /**
18
+ * Get the current value of the state, initializing it if necessary.
19
+ * @returns The current value of the state
20
+ */
14
21
  function getValue(): T {
15
22
  try {
16
23
  if (isUndefined(state.cache.current)) {
@@ -28,6 +35,11 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
28
35
  return state.cache.current
29
36
  }
30
37
 
38
+ /**
39
+ * Handle async set value when previous value is promise and new value is function
40
+ * @param previousPromise Previous promise
41
+ * @param value New value function
42
+ */
31
43
  async function handleAsyncSetValue(previousPromise: Promise<T>, value: SetStateCb<T>) {
32
44
  await previousPromise
33
45
  const newValue = value(state.cache.current as Awaited<T>)
@@ -35,6 +47,10 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
35
47
  state.cache.current = resolvedValue
36
48
  }
37
49
 
50
+ /**
51
+ * Set value to state
52
+ * @param value Value to set
53
+ */
38
54
  function setValue(value: SetValue<T>) {
39
55
  const previous = getValue()
40
56
  const isFunctionValue = isSetValueFunction(value)
@@ -20,6 +20,10 @@ interface SendOptions {
20
20
  value: unknown
21
21
  name: string
22
22
  }
23
+ /**
24
+ * Send state information to Redux DevTools if available
25
+ * @param options Options containing message, type, value, and name
26
+ */
23
27
  function sendToDevelopmentTools(options: SendOptions) {
24
28
  if (!reduxDevelopmentTools) {
25
29
  return
@@ -31,12 +35,23 @@ function sendToDevelopmentTools(options: SendOptions) {
31
35
  reduxDevelopmentTools.send(name, { value, type, message }, type)
32
36
  }
33
37
 
38
+ /**
39
+ * Create a listener function for development tools that sends state updates
40
+ * @param name The name of the state
41
+ * @param type The type of the state ('state' or 'derived')
42
+ * @returns A listener function that sends updates to development tools
43
+ */
34
44
  function developmentToolsListener(name: string, type: StateType) {
35
45
  return (value: unknown) => {
36
46
  sendToDevelopmentTools({ name, type, value, message: 'update' })
37
47
  }
38
48
  }
39
49
 
50
+ /**
51
+ * Subscribe a state to development tools if available
52
+ * @param state The state to subscribe
53
+ * @returns A function to unsubscribe from development tools
54
+ */
40
55
  export function subscribeToDevelopmentTools<T>(state: State<T> | GetState<T>) {
41
56
  if (process.env.NODE_ENV === 'production') {
42
57
  return
package/src/scheduler.ts CHANGED
@@ -27,6 +27,9 @@ export function createScheduler() {
27
27
  let frame = performance.now()
28
28
  let scheduled = false
29
29
 
30
+ /**
31
+ * Schedule the next flush based on time and item count thresholds
32
+ */
30
33
  function schedule() {
31
34
  const startFrame = performance.now()
32
35
  const frameSizeDiffIn = startFrame - frame
@@ -47,6 +50,9 @@ export function createScheduler() {
47
50
  }
48
51
  }
49
52
 
53
+ /**
54
+ * Flush the current batch of scheduled items
55
+ */
50
56
  function flush() {
51
57
  if (batches.size === 0) {
52
58
  return