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 +176 -173
- package/esm/sqlite/table/table.js +2 -2
- package/package.json +1 -1
- package/src/__tests__/bench.test.tsx +7 -0
- package/src/__tests__/test-utils.ts +5 -0
- package/src/create-state.ts +7 -0
- package/src/create.ts +17 -1
- package/src/debug/development-tools.ts +15 -0
- package/src/scheduler.ts +6 -0
- package/src/select.ts +22 -2
- package/src/sqlite/__tests__/use-sqlite.test.tsx +1 -0
- package/src/sqlite/create-sqlite.ts +52 -0
- package/src/sqlite/select-sql.ts +10 -0
- package/src/sqlite/table/bun-backend.ts +9 -0
- package/src/sqlite/table/table.ts +27 -2
- package/src/sqlite/table/where.ts +29 -12
- package/src/sqlite/use-sqlite.ts +8 -0
- package/src/use-value.ts +7 -0
- package/src/utils/common.ts +10 -0
- package/src/utils/create-emitter.ts +3 -2
- package/src/utils/is.ts +3 -0
- package/src/utils/shallow.ts +7 -0
- package/types/__tests__/test-utils.d.ts +5 -0
- package/types/create-state.d.ts +2 -0
- package/types/create.d.ts +4 -1
- package/types/debug/development-tools.d.ts +5 -0
- package/types/select.d.ts +5 -2
- package/types/sqlite/create-sqlite.d.ts +5 -0
- package/types/sqlite/select-sql.d.ts +6 -0
- package/types/sqlite/table/bun-backend.d.ts +4 -0
- package/types/sqlite/table/table.d.ts +17 -0
- package/types/sqlite/table/where.d.ts +11 -0
- package/types/sqlite/use-sqlite.d.ts +8 -0
- package/types/use-value.d.ts +7 -0
- package/types/utils/common.d.ts +10 -0
- package/types/utils/create-emitter.d.ts +3 -2
- package/types/utils/shallow.d.ts +7 -0
package/README.md
CHANGED
|
@@ -1,268 +1,271 @@
|
|
|
1
|
-
|
|
2
1
|
# **Muya 🌀**
|
|
3
2
|
|
|
4
|
-
|
|
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
|
[](https://github.com/samuelgja/muya/actions/workflows/build.yml)
|
|
9
|
-
[](https://github.com/samuelgja/muya/actions/workflows/code-check.yml)
|
|
13
|
+
[](https://bundlephobia.com/result?p=muya)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
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
|
-
## 📦
|
|
21
|
-
|
|
22
|
-
Install with your favorite package manager:
|
|
23
|
+
## 📦 Install
|
|
23
24
|
```bash
|
|
24
25
|
bun add muya@latest
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
##
|
|
34
|
+
## 🏃 Quick Start
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
```tsx
|
|
37
|
+
import { create } from 'muya'
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// 1) Make a state
|
|
40
|
+
const counter = create(0)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
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={() =>
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
Create derived states from one or many sources.
|
|
80
59
|
|
|
81
|
-
```
|
|
60
|
+
```ts
|
|
82
61
|
import { create, select } from 'muya'
|
|
83
62
|
|
|
84
|
-
const
|
|
85
|
-
const
|
|
63
|
+
const a = create(1)
|
|
64
|
+
const b = create(2)
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
|
|
66
|
+
// Single source
|
|
67
|
+
const doubleA = a.select(n => n * 2)
|
|
89
68
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
69
|
+
// Multiple sources
|
|
70
|
+
const sum = select([a, b], (x, y) => x + y)
|
|
71
|
+
```
|
|
93
72
|
|
|
94
|
-
|
|
95
|
-
const state = create({ a: 1, b: 2 }, (prev, next) => prev.b === next.b);
|
|
73
|
+
**Equality checks** (to avoid re-emits):
|
|
96
74
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
80
|
+
You can also add an equality function on a `select`:
|
|
102
81
|
|
|
103
|
-
```
|
|
104
|
-
const
|
|
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
|
-
|
|
90
|
+
Muya states are callable hooks. Prefer that.
|
|
91
|
+
If you want a hook wrapper or slicing, use `useValue`.
|
|
111
92
|
|
|
112
|
-
|
|
93
|
+
```tsx
|
|
94
|
+
import { create, useValue } from 'muya'
|
|
113
95
|
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
##
|
|
113
|
+
## ⚡ Async & Lazy (the 2-minute mental model)
|
|
148
114
|
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
143
|
+
> Tip: Prefer keeping selectors **sync** and performing async work **before** calling `set`. It keeps render trees predictable.
|
|
166
144
|
|
|
167
|
-
|
|
145
|
+
---
|
|
168
146
|
|
|
169
|
-
|
|
170
|
-
const derived = select([state1, state2], (s1, s2) => s1 + s2);
|
|
147
|
+
## 🧪 API (short and sweet)
|
|
171
148
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
###
|
|
158
|
+
### `select([states], derive, isEqual?) => State<R>`
|
|
159
|
+
Derive a state from one or multiple states.
|
|
181
160
|
|
|
182
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
## 🧭 Examples
|
|
209
186
|
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
225
|
-
|
|
226
|
-
|
|
209
|
+
```ts
|
|
210
|
+
import { createSqliteState } from 'muya/sqlite'
|
|
211
|
+
import { useSqliteValue } from 'muya/sqlite'
|
|
227
212
|
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
239
|
-
const state = create(initialLoad)
|
|
240
|
-
// or
|
|
241
|
-
const state = create(Promise.resolve(0))
|
|
233
|
+
```
|
|
242
234
|
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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(() => {
|
package/src/create-state.ts
CHANGED
|
@@ -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
|
|
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
|