muya 2.5.0 → 2.5.1
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 +409 -155
- package/cjs/index.js +1 -1
- package/esm/index.js +1 -1
- package/esm/use-value-loadable.js +1 -0
- package/package.json +1 -1
- package/src/__tests__/use-value-loadable.test.tsx +135 -0
- package/src/index.ts +1 -0
- package/src/use-value-loadable.ts +39 -0
- package/types/index.d.ts +1 -0
- package/types/use-value-loadable.d.ts +14 -0
- package/esm/sqlite/select-sql.js +0 -1
- package/src/sqlite/select-sql.ts +0 -65
- package/types/sqlite/select-sql.d.ts +0 -20
package/README.md
CHANGED
|
@@ -1,271 +1,525 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Muya
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- **Create a state**
|
|
5
|
-
- **Read it in components**
|
|
6
|
-
- **Update it**
|
|
7
|
-
- **Derive more states when you need to**
|
|
8
|
-
|
|
9
|
-
---
|
|
3
|
+
A tiny, type-safe state manager for React.
|
|
10
4
|
|
|
11
5
|
[](https://github.com/samuelgja/muya/actions/workflows/build.yml)
|
|
12
6
|
[](https://github.com/samuelgja/muya/actions/workflows/code-check.yml)
|
|
13
|
-
[](https://bundlephobia.com/result?p=muya)
|
|
8
|
+
[](https://www.npmjs.com/package/muya)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why Muya?
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
| Feature | useState + Context | Zustand | Jotai | Muya |
|
|
15
|
+
| -------------- | ------------------ | ------- | --------- | --------------- |
|
|
16
|
+
| Bundle size | 0kb (built-in) | ~2.9kb | ~2.4kb | **~1.5kb** |
|
|
17
|
+
| Boilerplate | High | Low | Low | **Minimal** |
|
|
18
|
+
| TypeScript | Manual | Good | Good | **First-class** |
|
|
19
|
+
| Async support | Manual | Manual | Built-in | **Built-in** |
|
|
20
|
+
| Derived state | Manual | Manual | Built-in | **Built-in** |
|
|
21
|
+
| React Suspense | No | No | Yes | **Yes** |
|
|
22
|
+
| Batching | React handles | Manual | Automatic | **Automatic** |
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
23
|
-
##
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
24
28
|
```bash
|
|
25
|
-
|
|
29
|
+
npm install muya
|
|
26
30
|
# or
|
|
27
|
-
|
|
31
|
+
bun add muya
|
|
28
32
|
# or
|
|
29
|
-
yarn add muya
|
|
33
|
+
yarn add muya
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
---
|
|
33
37
|
|
|
34
|
-
##
|
|
38
|
+
## Quick Start
|
|
35
39
|
|
|
36
40
|
```tsx
|
|
37
41
|
import { create } from 'muya'
|
|
38
42
|
|
|
39
|
-
// 1) Make a state
|
|
40
43
|
const counter = create(0)
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
export function Counter() {
|
|
45
|
+
function Counter() {
|
|
44
46
|
const count = counter() // state is a hook
|
|
45
|
-
return (
|
|
46
|
-
<div>
|
|
47
|
-
<button onClick={() => counter.set(n => n + 1)}>+1</button>
|
|
48
|
-
<p>Count: {count}</p>
|
|
49
|
-
</div>
|
|
50
|
-
)
|
|
47
|
+
return <button onClick={() => counter.set((n) => n + 1)}>Count: {count}</button>
|
|
51
48
|
}
|
|
52
49
|
```
|
|
53
50
|
|
|
51
|
+
That's it. No providers, no setup, no boilerplate.
|
|
52
|
+
|
|
54
53
|
---
|
|
55
54
|
|
|
56
|
-
##
|
|
55
|
+
## Comparison
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
### vs useState + useContext
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
**Before** (React Context):
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// 1. Create context
|
|
63
|
+
const CountContext = createContext(null)
|
|
64
|
+
|
|
65
|
+
// 2. Create provider component
|
|
66
|
+
function CountProvider({ children }) {
|
|
67
|
+
const [count, setCount] = useState(0)
|
|
68
|
+
return <CountContext.Provider value={{ count, setCount }}>{children}</CountContext.Provider>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Create custom hook
|
|
72
|
+
function useCount() {
|
|
73
|
+
const context = useContext(CountContext)
|
|
74
|
+
if (!context) throw new Error('Must be in provider')
|
|
75
|
+
return context
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 4. Wrap your app
|
|
79
|
+
function App() {
|
|
80
|
+
return (
|
|
81
|
+
<CountProvider>
|
|
82
|
+
<Counter />
|
|
83
|
+
</CountProvider>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
// 5. Finally use it
|
|
88
|
+
function Counter() {
|
|
89
|
+
const { count, setCount } = useCount()
|
|
90
|
+
return <button onClick={() => setCount((n) => n + 1)}>{count}</button>
|
|
91
|
+
}
|
|
92
|
+
```
|
|
65
93
|
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
**After** (Muya):
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
const counter = create(0)
|
|
68
98
|
|
|
69
|
-
|
|
70
|
-
const
|
|
99
|
+
function Counter() {
|
|
100
|
+
const count = counter()
|
|
101
|
+
return <button onClick={() => counter.set((n) => n + 1)}>{count}</button>
|
|
102
|
+
}
|
|
71
103
|
```
|
|
72
104
|
|
|
73
|
-
|
|
105
|
+
### vs Zustand
|
|
106
|
+
|
|
107
|
+
**Zustand**:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { create } from 'zustand'
|
|
74
111
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
const useStore = create((set) => ({
|
|
113
|
+
count: 0,
|
|
114
|
+
increment: () => set((state) => ({ count: state.count + 1 })),
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
function Counter() {
|
|
118
|
+
const count = useStore((state) => state.count)
|
|
119
|
+
const increment = useStore((state) => state.increment)
|
|
120
|
+
return <button onClick={increment}>{count}</button>
|
|
121
|
+
}
|
|
78
122
|
```
|
|
79
123
|
|
|
80
|
-
|
|
124
|
+
**Muya**:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { create } from 'muya'
|
|
81
128
|
|
|
82
|
-
|
|
83
|
-
|
|
129
|
+
const counter = create(0)
|
|
130
|
+
|
|
131
|
+
function Counter() {
|
|
132
|
+
const count = counter()
|
|
133
|
+
return <button onClick={() => counter.set((n) => n + 1)}>{count}</button>
|
|
134
|
+
}
|
|
84
135
|
```
|
|
85
136
|
|
|
86
137
|
---
|
|
87
138
|
|
|
88
|
-
##
|
|
139
|
+
## Core API
|
|
140
|
+
|
|
141
|
+
### `create(initial, isEqual?)`
|
|
89
142
|
|
|
90
|
-
|
|
91
|
-
|
|
143
|
+
Create a state. The state itself is a hook.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// Simple value
|
|
147
|
+
const name = create('Ada')
|
|
148
|
+
|
|
149
|
+
// Object
|
|
150
|
+
const user = create({ id: 1, name: 'Ada', role: 'admin' })
|
|
151
|
+
|
|
152
|
+
// Lazy (computed on first read)
|
|
153
|
+
const expensive = create(() => computeExpensiveValue())
|
|
154
|
+
|
|
155
|
+
// Async
|
|
156
|
+
const data = create(fetch('/api/data').then((r) => r.json()))
|
|
157
|
+
const lazyData = create(() => fetch('/api/data').then((r) => r.json()))
|
|
158
|
+
|
|
159
|
+
// With equality check (skip updates when equal)
|
|
160
|
+
const position = create({ x: 0, y: 0 }, (prev, next) => prev.x === next.x && prev.y === next.y)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### State Methods
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const counter = create(0)
|
|
167
|
+
|
|
168
|
+
// Read (outside React)
|
|
169
|
+
counter.get() // 0
|
|
170
|
+
|
|
171
|
+
// Update
|
|
172
|
+
counter.set(5)
|
|
173
|
+
counter.set((prev) => prev + 1)
|
|
174
|
+
|
|
175
|
+
// Subscribe (outside React)
|
|
176
|
+
const unsubscribe = counter.listen((value) => console.log(value))
|
|
177
|
+
|
|
178
|
+
// Derive new state
|
|
179
|
+
const doubled = counter.select((n) => n * 2)
|
|
180
|
+
|
|
181
|
+
// Debug name (for DevTools)
|
|
182
|
+
counter.withName('counter')
|
|
183
|
+
|
|
184
|
+
// Cleanup
|
|
185
|
+
counter.destroy()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `select([states], derive, isEqual?)`
|
|
189
|
+
|
|
190
|
+
Derive state from multiple sources.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { create, select } from 'muya'
|
|
194
|
+
|
|
195
|
+
const firstName = create('Ada')
|
|
196
|
+
const lastName = create('Lovelace')
|
|
197
|
+
|
|
198
|
+
const fullName = select([firstName, lastName], (first, last) => `${first} ${last}`)
|
|
199
|
+
|
|
200
|
+
function Greeting() {
|
|
201
|
+
const name = fullName() // 'Ada Lovelace'
|
|
202
|
+
return <h1>Hello, {name}</h1>
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `useValue(state, selector?)`
|
|
207
|
+
|
|
208
|
+
Hook for reading state with optional selector.
|
|
92
209
|
|
|
93
210
|
```tsx
|
|
94
211
|
import { create, useValue } from 'muya'
|
|
95
212
|
|
|
96
|
-
const user = create({ id:
|
|
213
|
+
const user = create({ id: 1, name: 'Ada', role: 'admin' })
|
|
97
214
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
return <
|
|
215
|
+
function UserName() {
|
|
216
|
+
// Only re-renders when name changes
|
|
217
|
+
const name = useValue(user, (u) => u.name)
|
|
218
|
+
return <span>{name}</span>
|
|
102
219
|
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `useValueLoadable(state, selector?)`
|
|
223
|
+
|
|
224
|
+
Hook for async states without Suspense. Returns `[value, isLoading, isError, error]`.
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { create, useValueLoadable } from 'muya'
|
|
228
|
+
|
|
229
|
+
const data = create(() => fetch('/api/data').then((r) => r.json()))
|
|
103
230
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return <
|
|
231
|
+
function DataView() {
|
|
232
|
+
const [value, isLoading, isError, error] = useValueLoadable(data)
|
|
233
|
+
|
|
234
|
+
if (isLoading) return <Spinner />
|
|
235
|
+
if (isError) return <Error message={error.message} />
|
|
236
|
+
return <Display data={value} />
|
|
108
237
|
}
|
|
109
238
|
```
|
|
110
239
|
|
|
111
240
|
---
|
|
112
241
|
|
|
113
|
-
##
|
|
242
|
+
## Async States
|
|
243
|
+
|
|
244
|
+
### Async Initialization
|
|
114
245
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
create(
|
|
118
|
-
|
|
246
|
+
```tsx
|
|
247
|
+
// Promise (loads immediately)
|
|
248
|
+
const user = create(fetch('/api/user').then((r) => r.json()))
|
|
249
|
+
|
|
250
|
+
// Lazy async (loads on first read)
|
|
251
|
+
const user = create(() => fetch('/api/user').then((r) => r.json()))
|
|
119
252
|
```
|
|
120
253
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
create(
|
|
125
|
-
|
|
254
|
+
### Async Updates
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
const user = create(() => fetchUser())
|
|
258
|
+
|
|
259
|
+
// Override immediately (cancels pending)
|
|
260
|
+
user.set({ id: 1, name: 'New User' })
|
|
261
|
+
|
|
262
|
+
// Wait for current value, then update
|
|
263
|
+
user.set((prev) => ({ ...prev, name: 'Updated' }))
|
|
126
264
|
```
|
|
127
265
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
266
|
+
### Async Selectors
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
const userId = create(1)
|
|
131
270
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const plusOne = base.select(async n => {
|
|
136
|
-
await doWork()
|
|
137
|
-
return n + 1
|
|
271
|
+
const userDetails = userId.select(async (id) => {
|
|
272
|
+
const response = await fetch(`/api/users/${id}`)
|
|
273
|
+
return response.json()
|
|
138
274
|
})
|
|
275
|
+
|
|
276
|
+
// Suspends on first read
|
|
277
|
+
function UserProfile() {
|
|
278
|
+
const details = userDetails()
|
|
279
|
+
return <Profile {...details} />
|
|
280
|
+
}
|
|
139
281
|
```
|
|
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
|
|
142
282
|
|
|
143
|
-
|
|
283
|
+
### With Suspense
|
|
144
284
|
|
|
145
|
-
|
|
285
|
+
```tsx
|
|
286
|
+
const data = create(() => fetchData())
|
|
146
287
|
|
|
147
|
-
|
|
288
|
+
function App() {
|
|
289
|
+
return (
|
|
290
|
+
<Suspense fallback={<Loading />}>
|
|
291
|
+
<DataView />
|
|
292
|
+
</Suspense>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
148
295
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
- `select(selector, isEqual?)` — derive new state from this state
|
|
155
|
-
- `destroy()` — clear listeners and dispose
|
|
156
|
-
- `withName(name)` — add a debug label (DevTools)
|
|
296
|
+
function DataView() {
|
|
297
|
+
const value = data() // suspends until resolved
|
|
298
|
+
return <div>{value}</div>
|
|
299
|
+
}
|
|
300
|
+
```
|
|
157
301
|
|
|
158
|
-
###
|
|
159
|
-
Derive a state from one or multiple states.
|
|
302
|
+
### Without Suspense
|
|
160
303
|
|
|
161
|
-
|
|
162
|
-
|
|
304
|
+
```tsx
|
|
305
|
+
const data = create(() => fetchData())
|
|
163
306
|
|
|
164
|
-
|
|
307
|
+
function DataView() {
|
|
308
|
+
const [value, isLoading, isError, error] = useValueLoadable(data)
|
|
165
309
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
310
|
+
if (isLoading) return <Loading />
|
|
311
|
+
if (isError) return <Error error={error} />
|
|
312
|
+
return <div>{value}</div>
|
|
313
|
+
}
|
|
314
|
+
```
|
|
169
315
|
|
|
170
316
|
---
|
|
171
317
|
|
|
172
|
-
##
|
|
318
|
+
## Patterns
|
|
173
319
|
|
|
174
|
-
|
|
175
|
-
- Keep selectors pure and fast
|
|
176
|
-
- Use equality checks to avoid unnecessary updates
|
|
177
|
-
- Do async outside, then `set` synchronously
|
|
320
|
+
### Computed Values
|
|
178
321
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
322
|
+
```tsx
|
|
323
|
+
const items = create([
|
|
324
|
+
{ id: 1, name: 'Apple', price: 1.5, quantity: 2 },
|
|
325
|
+
{ id: 2, name: 'Banana', price: 0.5, quantity: 5 },
|
|
326
|
+
])
|
|
182
327
|
|
|
183
|
-
|
|
328
|
+
const total = items.select((list) => list.reduce((sum, item) => sum + item.price * item.quantity, 0))
|
|
329
|
+
|
|
330
|
+
const count = items.select((list) => list.length)
|
|
331
|
+
```
|
|
184
332
|
|
|
185
|
-
|
|
333
|
+
### Actions
|
|
186
334
|
|
|
187
|
-
**Boolean flags derived from async state (suspends only once):**
|
|
188
335
|
```tsx
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
336
|
+
const cart = create({ items: [], discount: 0 })
|
|
337
|
+
|
|
338
|
+
const cartActions = {
|
|
339
|
+
addItem: (item) =>
|
|
340
|
+
cart.set((state) => ({
|
|
341
|
+
...state,
|
|
342
|
+
items: [...state.items, item],
|
|
343
|
+
})),
|
|
344
|
+
|
|
345
|
+
applyDiscount: (percent) =>
|
|
346
|
+
cart.set((state) => ({
|
|
347
|
+
...state,
|
|
348
|
+
discount: percent,
|
|
349
|
+
})),
|
|
350
|
+
|
|
351
|
+
clear: () => cart.set({ items: [], discount: 0 }),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Usage
|
|
355
|
+
cartActions.addItem({ id: 1, name: 'Book', price: 20 })
|
|
192
356
|
```
|
|
193
357
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
358
|
+
### Shallow Equality
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
import { create, shallow } from 'muya'
|
|
362
|
+
|
|
363
|
+
const list = create(
|
|
364
|
+
[1, 2, 3],
|
|
365
|
+
shallow, // built-in shallow comparison
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
// Won't notify if array contents are the same
|
|
369
|
+
list.set([1, 2, 3])
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Batching
|
|
373
|
+
|
|
374
|
+
Multiple updates in the same event are batched automatically:
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
function checkout() {
|
|
378
|
+
cart.set((c) => applyDiscount(c))
|
|
379
|
+
total.set((t) => t - 10)
|
|
380
|
+
inventory.set((i) => decrementStock(i))
|
|
381
|
+
// React sees one render
|
|
200
382
|
}
|
|
201
383
|
```
|
|
202
384
|
|
|
203
385
|
---
|
|
204
386
|
|
|
205
|
-
##
|
|
387
|
+
## DevTools
|
|
388
|
+
|
|
389
|
+
Muya auto-connects to Redux DevTools in development.
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
const counter = create(0).withName('counter')
|
|
393
|
+
const user = create({ name: 'Ada' }).withName('user')
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## SQLite Companion
|
|
206
399
|
|
|
207
|
-
|
|
400
|
+
For large, queryable lists with pagination. Works with expo-sqlite, better-sqlite3, or in-memory.
|
|
208
401
|
|
|
209
|
-
```
|
|
210
|
-
import { createSqliteState } from 'muya/sqlite'
|
|
211
|
-
import { useSqliteValue } from 'muya/sqlite'
|
|
402
|
+
```tsx
|
|
403
|
+
import { createSqliteState, useSqliteValue } from 'muya/sqlite'
|
|
212
404
|
|
|
213
|
-
type
|
|
405
|
+
type Task = { id: string; title: string; done: boolean; priority: number }
|
|
214
406
|
|
|
215
|
-
const
|
|
216
|
-
backend,
|
|
217
|
-
tableName: '
|
|
407
|
+
const tasks = createSqliteState<Task>({
|
|
408
|
+
backend,
|
|
409
|
+
tableName: 'tasks',
|
|
218
410
|
key: 'id',
|
|
219
|
-
indexes: ['
|
|
411
|
+
indexes: ['priority', 'done'],
|
|
220
412
|
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### React Hook with Pagination
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
function TaskList() {
|
|
419
|
+
const [rows, actions] = useSqliteValue(tasks, { sortBy: 'priority', order: 'desc', limit: 20 }, [])
|
|
221
420
|
|
|
222
|
-
// In React: stepwise fetching + where/order/limit
|
|
223
|
-
function PeopleList() {
|
|
224
|
-
const [rows, actions] = useSqliteValue(people, { sorBy: 'age', order: 'asc', limit: 50 }, [])
|
|
225
421
|
return (
|
|
226
422
|
<>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
423
|
+
{rows.map((task) => (
|
|
424
|
+
<TaskItem key={task.id} task={task} />
|
|
425
|
+
))}
|
|
426
|
+
<button onClick={actions.next}>Load more</button>
|
|
427
|
+
<button onClick={actions.reset}>Reset</button>
|
|
230
428
|
</>
|
|
231
429
|
)
|
|
232
430
|
}
|
|
233
431
|
```
|
|
234
432
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
await
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
433
|
+
### CRUD Operations
|
|
434
|
+
|
|
435
|
+
```tsx
|
|
436
|
+
// Create
|
|
437
|
+
await tasks.set({ id: '1', title: 'Buy milk', done: false, priority: 1 })
|
|
438
|
+
|
|
439
|
+
// Batch create
|
|
440
|
+
await tasks.batchSet([
|
|
441
|
+
{ id: '2', title: 'Walk dog', done: false, priority: 2 },
|
|
442
|
+
{ id: '3', title: 'Read book', done: true, priority: 3 },
|
|
443
|
+
])
|
|
444
|
+
|
|
445
|
+
// Read
|
|
446
|
+
const task = await tasks.get('1')
|
|
447
|
+
const title = await tasks.get('1', (t) => t.title)
|
|
448
|
+
|
|
449
|
+
// Update
|
|
450
|
+
await tasks.set({ id: '1', title: 'Buy milk', done: true, priority: 1 })
|
|
451
|
+
|
|
452
|
+
// Delete
|
|
453
|
+
await tasks.delete('1')
|
|
454
|
+
|
|
455
|
+
// Batch delete
|
|
456
|
+
await tasks.batchDelete(['2', '3'])
|
|
457
|
+
|
|
458
|
+
// Count
|
|
459
|
+
const total = await tasks.count()
|
|
460
|
+
const pending = await tasks.count({ where: { done: false } })
|
|
243
461
|
```
|
|
244
462
|
|
|
245
|
-
|
|
463
|
+
### Querying
|
|
246
464
|
|
|
247
|
-
|
|
465
|
+
```tsx
|
|
466
|
+
// Search with where clause
|
|
467
|
+
for await (const task of tasks.search({
|
|
468
|
+
where: { done: false, priority: { gt: 1 } },
|
|
469
|
+
orderBy: 'priority',
|
|
470
|
+
order: 'desc',
|
|
471
|
+
})) {
|
|
472
|
+
console.log(task.title)
|
|
473
|
+
}
|
|
248
474
|
|
|
249
|
-
|
|
250
|
-
No—Muya is intentionally tiny. If you need complex middleware, effects, or ecosystem plugins, those tools are great choices.
|
|
475
|
+
```
|
|
251
476
|
|
|
252
|
-
**
|
|
253
|
-
Yes. Async states/selectors will suspend on first read (and when upstream requires it).
|
|
477
|
+
**Where clause operators:**
|
|
254
478
|
|
|
255
|
-
|
|
256
|
-
|
|
479
|
+
| Operator | Example | Description |
|
|
480
|
+
| -------- | ------------------------------- | ---------------------- |
|
|
481
|
+
| equals | `{ done: false }` | Exact match |
|
|
482
|
+
| gt | `{ priority: { gt: 5 } }` | Greater than |
|
|
483
|
+
| gte | `{ priority: { gte: 5 } }` | Greater than or equal |
|
|
484
|
+
| lt | `{ priority: { lt: 5 } }` | Less than |
|
|
485
|
+
| lte | `{ priority: { lte: 5 } }` | Less than or equal |
|
|
486
|
+
| like | `{ title: { like: '%milk%' } }` | SQL LIKE pattern match |
|
|
257
487
|
|
|
258
488
|
---
|
|
259
489
|
|
|
260
|
-
##
|
|
261
|
-
|
|
490
|
+
## TypeScript
|
|
491
|
+
|
|
492
|
+
Full type inference out of the box:
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
const user = create({ id: 1, name: 'Ada', role: 'admin' as const })
|
|
496
|
+
// Type: State<{ id: number; name: string; role: 'admin' }>
|
|
497
|
+
|
|
498
|
+
const role = user.select((u) => u.role)
|
|
499
|
+
// Type: State<'admin'>
|
|
500
|
+
|
|
501
|
+
const name = useValue(user, (u) => u.name)
|
|
502
|
+
// Type: string
|
|
503
|
+
```
|
|
262
504
|
|
|
263
505
|
---
|
|
264
506
|
|
|
265
|
-
##
|
|
266
|
-
|
|
507
|
+
## FAQ
|
|
508
|
+
|
|
509
|
+
**Is Muya a replacement for Redux/Zustand/Jotai?**
|
|
510
|
+
Muya is intentionally minimal. If you need middleware, devtools plugins, or large ecosystem, consider those alternatives.
|
|
511
|
+
|
|
512
|
+
**How do I avoid re-renders?**
|
|
513
|
+
Use `isEqual` function with `create`/`select`, or use a selector with `useValue` to subscribe to a slice.
|
|
514
|
+
|
|
515
|
+
**Can I use Suspense?**
|
|
516
|
+
Yes. Async states suspend on first read. Use `useValueLoadable` if you prefer loading states over Suspense.
|
|
517
|
+
|
|
518
|
+
**Does it work with React Native?**
|
|
519
|
+
Yes, Muya has no DOM dependencies.
|
|
267
520
|
|
|
268
521
|
---
|
|
269
522
|
|
|
270
|
-
|
|
271
|
-
|
|
523
|
+
## License
|
|
524
|
+
|
|
525
|
+
MIT
|
package/cjs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var O=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var W=Object.getOwnPropertyNames;var J=Object.prototype.hasOwnProperty;var Q=(e,t)=>{for(var n in t)O(e,n,{get:t[n],enumerable:!0})},X=(e,t,n,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of W(t))!J.call(e,s)&&s!==n&&O(e,s,{get:()=>t[s],enumerable:!(a=Y(t,s))||a.enumerable});return e};var Z=e=>X(O({},"__esModule",{value:!0}),e);var ee={};Q(ee,{EMPTY_SELECTOR:()=>w,create:()=>F,isAbortError:()=>v,isArray:()=>k,isEqualBase:()=>S,isError:()=>b,isFunction:()=>h,isMap:()=>g,isPromise:()=>l,isSet:()=>P,isSetValueFunction:()=>D,isState:()=>B,isUndefined:()=>f,select:()=>V,shallow:()=>j,useValue:()=>C,useValueLoadable:()=>_});module.exports=Z(ee);var T=class extends Error{static Error="AbortError"};function $(e,t){t&&t.abort();let n=new AbortController,{signal:a}=n;return{promise:new Promise((o,c)=>{a.addEventListener("abort",()=>{c(new T)}),e.then(o).catch(c)}),controller:n}}function E(e,t=S){if(!f(e.current)){if(!f(e.previous)&&t(e.current,e.previous))return!1;e.previous=e.current}return!0}function p(e,t){let{cache:n,emitter:{emit:a}}=e;if(!l(t))return t;n.abortController&&n.abortController.abort();let{promise:s,controller:o}=$(t,n.abortController);return n.abortController=o,s.then(c=>{n.current=c,a()}).catch(c=>{v(c)||(n.current=c,a())})}function l(e){return e instanceof Promise}function h(e){return typeof e=="function"}function g(e){return e instanceof Map}function P(e){return e instanceof Set}function k(e){return Array.isArray(e)}function S(e,t){return e===t?!0:!!Object.is(e,t)}function D(e){return typeof e=="function"}function v(e){return e instanceof T}function b(e){return e instanceof Error}function f(e){return e===void 0}function B(e){return h(e)&&"get"in e&&"set"in e&&"isSet"in e&&e.isSet===!0}var w=e=>e;function G(){let e=new Map,t=new Set,n=performance.now(),a=!1;function s(){let c=performance.now(),r=c-n,{size:i}=t;if(r<.2&&i>0&&i<10){n=c,o();return}a||(a=!0,Promise.resolve().then(()=>{a=!1,n=performance.now(),o()}))}function o(){if(t.size===0)return;let c=new Set,r=new Map;for(let i of t){if(e.has(i.id)){c.add(i.id);let{onResolveItem:u}=e.get(i.id);u&&u(i.value),r.has(i.id)||r.set(i.id,[]),r.get(i.id).push(i.value)}t.delete(i)}if(t.size>0){s();return}for(let i of c){let u=r.get(i);e.get(i)?.onScheduleDone(u)}}return{add(c,r){return e.set(c,r),()=>{e.delete(c)}},schedule(c,r){t.add({value:r,id:c}),s()}}}function V(e,t,n){function a(){let u=!1,d=e.map(L=>{let x=L.get();return l(x)&&(u=!0),x});return u?new Promise((L,x)=>{Promise.all(d).then(R=>{if(R.some(K=>f(K)))return x(new T);let N=t(...R);L(N)})}):t(...d)}function s(){if(f(r.cache.current)){let u=a();r.cache.current=p(r,u)}return r.cache.current}function o(){if(f(r.cache.current)){let d=a();r.cache.current=p(r,d)}let{current:u}=r.cache;return l(u)?new Promise(d=>{u.then(m=>{if(f(m)){d(o());return}d(m)})}):r.cache.current}let c=[];for(let u of e){let d=u.emitter.subscribe(()=>{y.schedule(r.id,null)});c.push(d)}let r=A({destroy(){for(let u of c)u();i(),r.emitter.clear(),r.cache.current=void 0},get:o,getSnapshot:s}),i=y.add(r.id,{onScheduleDone(){let u=a();r.cache.current=p(r,u),E(r.cache,n)&&r.emitter.emit()}});return r}var U=require("react");var M=require("use-sync-external-store/shim/with-selector");function C(e,t=w){let{emitter:n}=e,a=(0,M.useSyncExternalStoreWithSelector)(e.emitter.subscribe,n.getSnapshot,n.getInitialSnapshot,t);if((0,U.useDebugValue)(a),l(a)||b(a))throw a;return a}function q(e,t){let n=new Set,a=[];return{clear:()=>{for(let s of a)s();n.clear()},subscribe:s=>(n.add(s),()=>{n.delete(s)}),emit:(...s)=>{for(let o of n)o(...s)},contains:s=>n.has(s),getSnapshot:e,getInitialSnapshot:t,getSize:()=>n.size,subscribeToOtherEmitter(s){let o=s.subscribe(()=>{this.emit()});a.push(o)}}}var H=0;function z(){return H++,H.toString(36)}function A(e){let{get:t,destroy:n,set:a,getSnapshot:s}=e,o=!!a,c={},r=function(i){return C(r,i)};return r.isSet=o,r.id=z(),r.emitter=q(s),r.destroy=n,r.listen=function(i){return this.emitter.subscribe(()=>{let u=t();l(u)||i(t())})},r.withName=function(i){return this.stateName=i,this},r.select=function(i,u=S){return V([r],i,u)},r.get=t,r.set=a,r.cache=c,r}var y=G();function F(e,t=S){function n(){try{if(f(o.cache.current)){let r=h(e)?e():e,i=p(o,r);return o.cache.current=i,o.cache.current}return o.cache.current}catch(r){o.cache.current=r}return o.cache.current}async function a(r,i){await r;let u=i(o.cache.current),d=p(o,u);o.cache.current=d}function s(r){let i=n(),u=D(r);if(u&&l(i)){a(i,r);return}o.cache.abortController&&o.cache.abortController.abort();let d=u?r(i):r,m=p(o,d);o.cache.current=m}let o=A({get:n,destroy(){n(),c(),o.emitter.clear(),o.cache.current=void 0},set(r){y.schedule(o.id,r)},getSnapshot:n}),c=y.add(o.id,{onScheduleDone(){o.cache.current=n(),E(o.cache,t)&&o.emitter.emit()},onResolveItem:s});return h(e)||n(),o}var I=require("react");function _(e,t=w){let{emitter:n}=e,a=(0,I.useSyncExternalStore)(n.subscribe,n.getSnapshot,n.getInitialSnapshot);return(0,I.useDebugValue)(a),l(a)?[void 0,!0,!1,void 0]:b(a)?[void 0,!1,!0,a]:[t(a),!1,!1,void 0]}function j(e,t){if(e==t)return!0;if(typeof e!="object"||e==null||typeof t!="object"||t==null)return!1;if(g(e)&&g(t)){if(e.size!==t.size)return!1;for(let[s,o]of e)if(!Object.is(o,t.get(s)))return!1;return!0}if(P(e)&&P(t)){if(e.size!==t.size)return!1;for(let s of e)if(!t.has(s))return!1;return!0}if(k(e)&&k(t)){if(e.length!==t.length)return!1;for(let[s,o]of e.entries())if(!Object.is(o,t[s]))return!1;return!0}let n=Object.keys(e),a=Object.keys(t);if(n.length!==a.length)return!1;for(let s of n)if(!Object.prototype.hasOwnProperty.call(t,s)||!Object.is(e[s],t[s]))return!1;return!0}
|
package/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export*from"./utils/is";export*from"./types";import{create as
|
|
1
|
+
export*from"./utils/is";export*from"./types";import{create as a}from"./create";import{select as p}from"./select";import{useValue as m}from"./use-value";import{useValueLoadable as s}from"./use-value-loadable";import{shallow as b}from"./utils/shallow";export{a as create,p as select,b as shallow,m as useValue,s as useValueLoadable};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useDebugValue as n,useSyncExternalStore as s}from"react";import{EMPTY_SELECTOR as u}from"./types";import{isError as r,isPromise as l}from"./utils/is";function b(t,d=u){const{emitter:a}=t,e=s(a.subscribe,a.getSnapshot,a.getInitialSnapshot);return n(e),l(e)?[void 0,!0,!1,void 0]:r(e)?[void 0,!1,!0,e]:[d(e),!1,!1,void 0]}export{b as useValueLoadable};
|
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react-hooks'
|
|
2
|
+
import { waitFor } from '@testing-library/react'
|
|
3
|
+
import { create } from '../create'
|
|
4
|
+
import { useValueLoadable } from '../use-value-loadable'
|
|
5
|
+
import { longPromise } from './test-utils'
|
|
6
|
+
|
|
7
|
+
describe('useValueLoadable', () => {
|
|
8
|
+
it('should return value immediately for sync state', () => {
|
|
9
|
+
const state = create(42)
|
|
10
|
+
const { result } = renderHook(() => useValueLoadable(state))
|
|
11
|
+
|
|
12
|
+
const [value, isLoading, isError, error] = result.current
|
|
13
|
+
expect(value).toBe(42)
|
|
14
|
+
expect(isLoading).toBe(false)
|
|
15
|
+
expect(isError).toBe(false)
|
|
16
|
+
expect(error).toBeUndefined()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return loading state for async state', async () => {
|
|
20
|
+
const state = create(longPromise(50))
|
|
21
|
+
const { result } = renderHook(() => useValueLoadable(state))
|
|
22
|
+
|
|
23
|
+
// Initially loading
|
|
24
|
+
expect(result.current[0]).toBeUndefined()
|
|
25
|
+
expect(result.current[1]).toBe(true)
|
|
26
|
+
expect(result.current[2]).toBe(false)
|
|
27
|
+
expect(result.current[3]).toBeUndefined()
|
|
28
|
+
|
|
29
|
+
// After resolution
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
expect(result.current[0]).toBe(0)
|
|
32
|
+
expect(result.current[1]).toBe(false)
|
|
33
|
+
expect(result.current[2]).toBe(false)
|
|
34
|
+
expect(result.current[3]).toBeUndefined()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should return error state when state throws', () => {
|
|
39
|
+
const testError = new Error('Test error')
|
|
40
|
+
const state = create(() => {
|
|
41
|
+
throw testError
|
|
42
|
+
})
|
|
43
|
+
const { result } = renderHook(() => useValueLoadable(state))
|
|
44
|
+
|
|
45
|
+
const [value, isLoading, isError, error] = result.current
|
|
46
|
+
expect(value).toBeUndefined()
|
|
47
|
+
expect(isLoading).toBe(false)
|
|
48
|
+
expect(isError).toBe(true)
|
|
49
|
+
expect(error).toBe(testError)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should return error state when async state rejects', async () => {
|
|
53
|
+
const testError = new Error('Async error')
|
|
54
|
+
const state = create(Promise.reject(testError))
|
|
55
|
+
const { result } = renderHook(() => useValueLoadable(state))
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(result.current[0]).toBeUndefined()
|
|
59
|
+
expect(result.current[1]).toBe(false)
|
|
60
|
+
expect(result.current[2]).toBe(true)
|
|
61
|
+
expect(result.current[3]).toBe(testError)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should update when sync state changes', async () => {
|
|
66
|
+
const state = create(1)
|
|
67
|
+
const { result } = renderHook(() => useValueLoadable(state))
|
|
68
|
+
|
|
69
|
+
expect(result.current[0]).toBe(1)
|
|
70
|
+
|
|
71
|
+
act(() => {
|
|
72
|
+
state.set(2)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
await waitFor(() => {
|
|
76
|
+
expect(result.current[0]).toBe(2)
|
|
77
|
+
expect(result.current[1]).toBe(false)
|
|
78
|
+
expect(result.current[2]).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should work with selector', () => {
|
|
83
|
+
const state = create({ count: 10, name: 'test' })
|
|
84
|
+
const { result } = renderHook(() => useValueLoadable(state, (s) => s.count))
|
|
85
|
+
|
|
86
|
+
const [value, isLoading, isError] = result.current
|
|
87
|
+
expect(value).toBe(10)
|
|
88
|
+
expect(isLoading).toBe(false)
|
|
89
|
+
expect(isError).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should work with selector on async state', async () => {
|
|
93
|
+
const state = create(Promise.resolve({ count: 5, name: 'async' }))
|
|
94
|
+
const { result } = renderHook(() => useValueLoadable(state, (s) => s.count))
|
|
95
|
+
|
|
96
|
+
// Initially loading
|
|
97
|
+
expect(result.current[1]).toBe(true)
|
|
98
|
+
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(result.current[0]).toBe(5)
|
|
101
|
+
expect(result.current[1]).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should not throw to suspense boundary', async () => {
|
|
106
|
+
const state = create(longPromise(50))
|
|
107
|
+
const renderCount = jest.fn()
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => {
|
|
110
|
+
renderCount()
|
|
111
|
+
return useValueLoadable(state)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Should render without throwing
|
|
115
|
+
expect(renderCount).toHaveBeenCalled()
|
|
116
|
+
expect(result.current[1]).toBe(true)
|
|
117
|
+
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(result.current[1]).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should provide type narrowing when isLoading is false', () => {
|
|
124
|
+
const state = create(42)
|
|
125
|
+
const { result } = renderHook(() => useValueLoadable(state))
|
|
126
|
+
|
|
127
|
+
const [value, isLoading, isError] = result.current
|
|
128
|
+
|
|
129
|
+
if (!isLoading && !isError) {
|
|
130
|
+
// TypeScript should know value is number here
|
|
131
|
+
const number_: number = value
|
|
132
|
+
expect(number_).toBe(42)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useDebugValue, useSyncExternalStore } from 'react'
|
|
2
|
+
import { EMPTY_SELECTOR, type GetState } from './types'
|
|
3
|
+
import { isError, isPromise } from './utils/is'
|
|
4
|
+
|
|
5
|
+
type LoadableLoading = [undefined, true, false, undefined]
|
|
6
|
+
type LoadableSuccess<T> = [T, false, false, undefined]
|
|
7
|
+
type LoadableError = [undefined, false, true, Error]
|
|
8
|
+
|
|
9
|
+
export type LoadableResult<T> = LoadableLoading | LoadableSuccess<T> | LoadableError
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* React hook to subscribe to a state and get its value without throwing to Suspense.
|
|
13
|
+
* Returns a tuple of [value, isLoading, isError, error] for handling async states.
|
|
14
|
+
* @param state The state to subscribe to
|
|
15
|
+
* @param selector Optional function to derive a value from the state
|
|
16
|
+
* @returns Tuple of [value, isLoading, isError, error] with discriminated union types
|
|
17
|
+
*/
|
|
18
|
+
export function useValueLoadable<T, S = undefined>(
|
|
19
|
+
state: GetState<T>,
|
|
20
|
+
selector: (stateValue: Awaited<T>) => S = EMPTY_SELECTOR,
|
|
21
|
+
): LoadableResult<undefined extends S ? Awaited<T> : S> {
|
|
22
|
+
const { emitter } = state
|
|
23
|
+
|
|
24
|
+
const rawValue = useSyncExternalStore(emitter.subscribe, emitter.getSnapshot, emitter.getInitialSnapshot)
|
|
25
|
+
|
|
26
|
+
useDebugValue(rawValue)
|
|
27
|
+
|
|
28
|
+
if (isPromise(rawValue)) {
|
|
29
|
+
return [undefined, true, false, undefined] as LoadableResult<undefined extends S ? Awaited<T> : S>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isError(rawValue)) {
|
|
33
|
+
return [undefined, false, true, rawValue] as LoadableResult<undefined extends S ? Awaited<T> : S>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selectedValue = selector(rawValue as Awaited<T>)
|
|
37
|
+
|
|
38
|
+
return [selectedValue, false, false, undefined] as LoadableResult<undefined extends S ? Awaited<T> : S>
|
|
39
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type GetState } from './types';
|
|
2
|
+
type LoadableLoading = [undefined, true, false, undefined];
|
|
3
|
+
type LoadableSuccess<T> = [T, false, false, undefined];
|
|
4
|
+
type LoadableError = [undefined, false, true, Error];
|
|
5
|
+
export type LoadableResult<T> = LoadableLoading | LoadableSuccess<T> | LoadableError;
|
|
6
|
+
/**
|
|
7
|
+
* React hook to subscribe to a state and get its value without throwing to Suspense.
|
|
8
|
+
* Returns a tuple of [value, isLoading, isError, error] for handling async states.
|
|
9
|
+
* @param state The state to subscribe to
|
|
10
|
+
* @param selector Optional function to derive a value from the state
|
|
11
|
+
* @returns Tuple of [value, isLoading, isError, error] with discriminated union types
|
|
12
|
+
*/
|
|
13
|
+
export declare function useValueLoadable<T, S = undefined>(state: GetState<T>, selector?: (stateValue: Awaited<T>) => S): LoadableResult<undefined extends S ? Awaited<T> : S>;
|
|
14
|
+
export {};
|
package/esm/sqlite/select-sql.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{createState as l}from"../create-state";let n=0;function o(){return n++,`${n.toString(36)}-sql`}function S(r,a){const{subscribe:c,updateSearchOptions:s}=r,m=o();return(...u)=>{const e=o(),p=c(e,m,()=>{t.emitter.emit()}),i=a(...u),t=l({destroy(){p(),t.emitter.clear(),t.cache.current=void 0},get(){return r.getSnapshot(e)},getSnapshot(){return r.getSnapshot(e)}});return s(e,i),t}}export{S as selectSql};
|
package/src/sqlite/select-sql.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { createState } from '../create-state'
|
|
2
|
-
import type { GetState } from '../types'
|
|
3
|
-
import type { SyncTable } from './create-sqlite'
|
|
4
|
-
import type { DocType, DotPath } 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 sortBy?: DotPath<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
|
-
/**
|
|
20
|
-
* Generate a unique state ID
|
|
21
|
-
* @returns A unique state ID
|
|
22
|
-
*/
|
|
23
|
-
function getStateId() {
|
|
24
|
-
stateId++
|
|
25
|
-
return `${stateId.toString(36)}-sql`
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Create a state that derives its value from a SyncTable using a compute function
|
|
30
|
-
* @param state The SyncTable to derive from
|
|
31
|
-
* @param compute A function that takes parameters and returns SqlSeachOptions to filter the SyncTable
|
|
32
|
-
* @returns A function that takes parameters and returns a GetState of the derived documents
|
|
33
|
-
*/
|
|
34
|
-
export function selectSql<Document extends DocType, Params extends unknown[] = []>(
|
|
35
|
-
state: SyncTable<Document>,
|
|
36
|
-
compute: (...args: Params) => SqlSeachOptions<Document>,
|
|
37
|
-
): CreateState<Document, Params> {
|
|
38
|
-
const { subscribe, updateSearchOptions } = state
|
|
39
|
-
const componentId = getStateId()
|
|
40
|
-
const result: CreateState<Document, Params> = (...params) => {
|
|
41
|
-
const searchId = getStateId()
|
|
42
|
-
const destroy = subscribe(searchId, componentId, () => {
|
|
43
|
-
getState.emitter.emit()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const options = compute(...params)
|
|
47
|
-
const getState = createState<Document[]>({
|
|
48
|
-
destroy() {
|
|
49
|
-
destroy()
|
|
50
|
-
getState.emitter.clear()
|
|
51
|
-
getState.cache.current = undefined
|
|
52
|
-
},
|
|
53
|
-
get() {
|
|
54
|
-
return state.getSnapshot(searchId)
|
|
55
|
-
},
|
|
56
|
-
getSnapshot() {
|
|
57
|
-
return state.getSnapshot(searchId)
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
updateSearchOptions<Document>(searchId, options)
|
|
61
|
-
|
|
62
|
-
return getState
|
|
63
|
-
}
|
|
64
|
-
return result
|
|
65
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { GetState } from '../types';
|
|
2
|
-
import type { SyncTable } from './create-sqlite';
|
|
3
|
-
import type { DocType, DotPath } 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 sortBy?: DotPath<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
|
-
/**
|
|
15
|
-
* Create a state that derives its value from a SyncTable using a compute function
|
|
16
|
-
* @param state The SyncTable to derive from
|
|
17
|
-
* @param compute A function that takes parameters and returns SqlSeachOptions to filter the SyncTable
|
|
18
|
-
* @returns A function that takes parameters and returns a GetState of the derived documents
|
|
19
|
-
*/
|
|
20
|
-
export declare function selectSql<Document extends DocType, Params extends unknown[] = []>(state: SyncTable<Document>, compute: (...args: Params) => SqlSeachOptions<Document>): CreateState<Document, Params>;
|