@vladstudio/kstate 0.1.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 +488 -0
- package/dist/adapters/api.d.ts +18 -0
- package/dist/adapters/api.d.ts.map +1 -0
- package/dist/adapters/api.js +31 -0
- package/dist/adapters/api.js.map +1 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +5 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/local.d.ts +20 -0
- package/dist/adapters/local.d.ts.map +1 -0
- package/dist/adapters/local.js +48 -0
- package/dist/adapters/local.js.map +1 -0
- package/dist/adapters/queuedApi.d.ts +18 -0
- package/dist/adapters/queuedApi.d.ts.map +1 -0
- package/dist/adapters/queuedApi.js +41 -0
- package/dist/adapters/queuedApi.js.map +1 -0
- package/dist/adapters/sse.d.ts +15 -0
- package/dist/adapters/sse.d.ts.map +1 -0
- package/dist/adapters/sse.js +50 -0
- package/dist/adapters/sse.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -0
- package/dist/core/cache.d.ts +8 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +27 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/network.d.ts +16 -0
- package/dist/core/network.d.ts.map +1 -0
- package/dist/core/network.js +51 -0
- package/dist/core/network.js.map +1 -0
- package/dist/core/proxy.d.ts +11 -0
- package/dist/core/proxy.d.ts.map +1 -0
- package/dist/core/proxy.js +146 -0
- package/dist/core/proxy.js.map +1 -0
- package/dist/core/subscribers.d.ts +3 -0
- package/dist/core/subscribers.d.ts.map +1 -0
- package/dist/core/subscribers.js +50 -0
- package/dist/core/subscribers.js.map +1 -0
- package/dist/hooks/useStore.d.ts +2 -0
- package/dist/hooks/useStore.d.ts.map +1 -0
- package/dist/hooks/useStore.js +64 -0
- package/dist/hooks/useStore.js.map +1 -0
- package/dist/hooks/useStoreStatus.d.ts +8 -0
- package/dist/hooks/useStoreStatus.d.ts.map +1 -0
- package/dist/hooks/useStoreStatus.js +13 -0
- package/dist/hooks/useStoreStatus.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/stores/computed.d.ts +14 -0
- package/dist/stores/computed.d.ts.map +1 -0
- package/dist/stores/computed.js +39 -0
- package/dist/stores/computed.js.map +1 -0
- package/dist/stores/createSetStore.d.ts +5 -0
- package/dist/stores/createSetStore.d.ts.map +1 -0
- package/dist/stores/createSetStore.js +130 -0
- package/dist/stores/createSetStore.js.map +1 -0
- package/dist/stores/createStore.d.ts +3 -0
- package/dist/stores/createStore.d.ts.map +1 -0
- package/dist/stores/createStore.js +98 -0
- package/dist/stores/createStore.js.map +1 -0
- package/dist/sync/api.d.ts +16 -0
- package/dist/sync/api.d.ts.map +1 -0
- package/dist/sync/api.js +106 -0
- package/dist/sync/api.js.map +1 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# KState
|
|
2
|
+
|
|
3
|
+
Minimal, type-safe state management for React SPAs with fine-grained reactivity, optimistic updates, and flexible adapters.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Tiny bundle** — No external dependencies except React
|
|
8
|
+
- **Fine-grained reactivity** — Components re-render only when subscribed values change
|
|
9
|
+
- **Optimistic updates** — UI updates instantly, auto-rollback on failure
|
|
10
|
+
- **Flexible adapters** — Mix REST API, localStorage, and SSE in any combination
|
|
11
|
+
- **TypeScript-native** — Full type inference, zero manual annotations
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add kstate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { configureKState } from 'kstate'
|
|
21
|
+
|
|
22
|
+
configureKState({
|
|
23
|
+
baseUrl: 'https://api.example.com',
|
|
24
|
+
getHeaders: async () => {
|
|
25
|
+
const token = localStorage.getItem('token')
|
|
26
|
+
return token ? { Authorization: `Bearer ${token}` } : {}
|
|
27
|
+
},
|
|
28
|
+
onError: (error, operation, meta) => {
|
|
29
|
+
console.error(`[KState] ${operation} failed:`, error.message)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Stores
|
|
37
|
+
|
|
38
|
+
### `createSetStore` — Array of items
|
|
39
|
+
|
|
40
|
+
For managing arrays of objects with `id`:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { createSetStore, api } from 'kstate'
|
|
44
|
+
|
|
45
|
+
interface User {
|
|
46
|
+
id: string
|
|
47
|
+
name: string
|
|
48
|
+
email: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const users = createSetStore<User>({
|
|
52
|
+
...api({ list: '/users', item: '/users/:id' }),
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Operations:**
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
await users.get() // Fetch all
|
|
60
|
+
await users.get({ role: 'admin' }) // With query params
|
|
61
|
+
await users.getOne({ id: '123' }) // Fetch one
|
|
62
|
+
await users.create({ name: 'Jane' }) // Create (returns new item)
|
|
63
|
+
await users.patch({ id: '123', name: 'Jo' }) // Partial update (optimistic)
|
|
64
|
+
await users.delete({ id: '123' }) // Delete (optimistic)
|
|
65
|
+
users.clear() // Clear local state
|
|
66
|
+
users.dispose() // Cleanup listeners
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `createStore` — Single value
|
|
70
|
+
|
|
71
|
+
For managing a single object:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { createStore, local } from 'kstate'
|
|
75
|
+
|
|
76
|
+
interface Settings {
|
|
77
|
+
theme: 'light' | 'dark'
|
|
78
|
+
language: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const settings = createStore<Settings>(local('settings', { theme: 'light', language: 'en' }))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Operations:**
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
await profile.get() // Fetch
|
|
88
|
+
await profile.set({ ... }) // Full replace (optimistic)
|
|
89
|
+
await profile.patch({ name: 'Jane' }) // Partial update (optimistic)
|
|
90
|
+
profile.clear() // Clear
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Adapters
|
|
96
|
+
|
|
97
|
+
Adapters provide sync logic. Mix them per-operation:
|
|
98
|
+
|
|
99
|
+
### `api({ list, item?, dataKey?, requestKey? })` — REST API
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { api } from 'kstate'
|
|
103
|
+
|
|
104
|
+
// RESTful API - item defaults to list + '/:id'
|
|
105
|
+
const users = createSetStore<User>({
|
|
106
|
+
...api({ list: '/users' }), // item: '/users/:id' inferred
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Explicit item endpoint
|
|
110
|
+
const posts = createSetStore<Post>({
|
|
111
|
+
...api({ list: '/posts', item: '/posts/:id' }),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Query-based API
|
|
115
|
+
const products = createSetStore<Product>({
|
|
116
|
+
...api({ list: '/products', item: '/products?id=:id' }),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Nested resources
|
|
120
|
+
const comments = createSetStore<Comment>({
|
|
121
|
+
...api({ list: '/posts/:postId/comments', item: '/comments/:id' }),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Response/request wrappers
|
|
125
|
+
const items = createSetStore<Item>({
|
|
126
|
+
...api({ list: '/items', dataKey: 'data', requestKey: 'item' }),
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Config:**
|
|
131
|
+
- `list` — Collection endpoint (GET all, POST create)
|
|
132
|
+
- `item` — Single item endpoint (GET one, PUT, PATCH, DELETE). Defaults to `list + '/:id'`
|
|
133
|
+
- `dataKey` — Extract data from response wrapper
|
|
134
|
+
- `requestKey` — Wrap request body
|
|
135
|
+
|
|
136
|
+
### `queuedApi({ list, item?, dataKey?, requestKey? })` — Sequential REST API
|
|
137
|
+
|
|
138
|
+
Like `api`, but requests execute one at a time. Use for low-priority batch operations:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { queuedApi } from 'kstate'
|
|
142
|
+
|
|
143
|
+
const logs = createSetStore<Log>({
|
|
144
|
+
...queuedApi({ list: '/logs' }),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// 50 calls → executed sequentially, continues on errors
|
|
148
|
+
for (const log of items) {
|
|
149
|
+
logs.patch({ id: log.id, synced: true })
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Note:** All `queuedApi` instances share a single global queue. This ensures low-priority operations across your app don't compete with each other.
|
|
154
|
+
|
|
155
|
+
### `local(key, defaultValue?)` — localStorage
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { local } from 'kstate'
|
|
159
|
+
|
|
160
|
+
// Shorthand for local-only store
|
|
161
|
+
const favorites = createSetStore<Favorite>(local('favorites'))
|
|
162
|
+
|
|
163
|
+
// Add persistence to API store
|
|
164
|
+
const users = createSetStore<User>({
|
|
165
|
+
...api({ list: '/users' }),
|
|
166
|
+
persist: local('users-cache').persist,
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `sse(url, opts?)` — Server-Sent Events
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { sse } from 'kstate'
|
|
174
|
+
|
|
175
|
+
const jobs = createSetStore<Job>({
|
|
176
|
+
...api({ list: '/jobs' }),
|
|
177
|
+
subscribe: sse('/jobs/stream', {
|
|
178
|
+
mode: 'upsert', // 'replace' | 'append' | 'upsert'
|
|
179
|
+
dataKey: 'items',
|
|
180
|
+
maxItems: 100, // For 'append' mode
|
|
181
|
+
}),
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Custom Adapter
|
|
186
|
+
|
|
187
|
+
Pass custom functions directly for any data source:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// Inline functions
|
|
191
|
+
const users = createSetStore<User>({
|
|
192
|
+
get: async () => mySource.getUsers(),
|
|
193
|
+
create: async (data) => mySource.createUser(data),
|
|
194
|
+
patch: async (data) => mySource.updateUser(data.id, data),
|
|
195
|
+
delete: async ({ id }) => mySource.deleteUser(id),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Reusable adapter factory
|
|
199
|
+
function myAdapter<T extends { id: string }>(source: MySource<T>) {
|
|
200
|
+
return {
|
|
201
|
+
get: async () => source.list(),
|
|
202
|
+
getOne: async ({ id }) => source.find(id),
|
|
203
|
+
create: async (data) => source.create(data),
|
|
204
|
+
patch: async (data) => source.update(data.id, data),
|
|
205
|
+
delete: async ({ id }) => source.remove(id),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const posts = createSetStore<Post>({
|
|
210
|
+
...myAdapter(postsSource),
|
|
211
|
+
persist: local('posts-cache').persist, // Mix with other adapters
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Hybrid Example
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
const jobs = createSetStore<Job>({
|
|
219
|
+
...api({ list: '/jobs' }),
|
|
220
|
+
subscribe: sse('/jobs/stream', { mode: 'upsert' }),
|
|
221
|
+
persist: local('jobs-cache').persist,
|
|
222
|
+
ttl: 60_000, // Cache for 1 minute
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## React Hooks
|
|
229
|
+
|
|
230
|
+
### `useStore(store)`
|
|
231
|
+
|
|
232
|
+
Subscribe to store data:
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import { useStore } from 'kstate'
|
|
236
|
+
|
|
237
|
+
function UserList() {
|
|
238
|
+
const items = useStore(users)
|
|
239
|
+
return items.map(u => <div key={u.id}>{u.name}</div>)
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `useStoreStatus(store)`
|
|
244
|
+
|
|
245
|
+
Subscribe to loading/error state:
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
import { useStoreStatus } from 'kstate'
|
|
249
|
+
|
|
250
|
+
function UserList() {
|
|
251
|
+
const items = useStore(users)
|
|
252
|
+
const { isLoading, isRevalidating, error, isOffline } = useStoreStatus(users)
|
|
253
|
+
|
|
254
|
+
if (isLoading) return <Spinner />
|
|
255
|
+
if (error) return <Error message={error.message} />
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<>
|
|
259
|
+
{isRevalidating && <RefreshIndicator />}
|
|
260
|
+
{items.map(u => <div key={u.id}>{u.name}</div>)}
|
|
261
|
+
</>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Fine-Grained Reactivity
|
|
269
|
+
|
|
270
|
+
Subscribe to specific paths — components only re-render when that exact value changes:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
function UserName({ index }: { index: number }) {
|
|
274
|
+
const name = useStore(users[index].name) // Only re-renders on name change
|
|
275
|
+
return <span>{name}</span>
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function UserEmail({ index }: { index: number }) {
|
|
279
|
+
const email = useStore(users[index].email) // Only re-renders on email change
|
|
280
|
+
return <span>{email}</span>
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**How paths work:**
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
useStore(users) // Path: [] - All changes
|
|
288
|
+
useStore(users[0]) // Path: [0] - First item changes
|
|
289
|
+
useStore(users[0].name) // Path: [0, 'name'] - First item's name only
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
When `users.patch({ id: '1', name: 'New' })` is called:
|
|
293
|
+
- `useStore(users)` — re-renders (ancestor)
|
|
294
|
+
- `useStore(users[0])` — re-renders (ancestor)
|
|
295
|
+
- `useStore(users[0].name)` — re-renders (exact match)
|
|
296
|
+
- `useStore(users[0].email)` — **does NOT** re-render (sibling)
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Optimistic Updates
|
|
301
|
+
|
|
302
|
+
`patch` and `delete` update local state immediately, then sync with server. On error, auto-rollback:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
const handleNameChange = (name: string) => {
|
|
306
|
+
users.patch({ id: userId, name }) // UI updates instantly
|
|
307
|
+
// If API fails, rolls back automatically
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const handleDelete = async () => {
|
|
311
|
+
try {
|
|
312
|
+
await users.delete({ id: userId }) // UI removes instantly
|
|
313
|
+
navigate('/users')
|
|
314
|
+
} catch (error) {
|
|
315
|
+
// Item restored, show error
|
|
316
|
+
toast.error('Failed to delete')
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Note:** `create` is NOT optimistic — it waits for server response to get the real ID.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Computed Stores
|
|
326
|
+
|
|
327
|
+
Derived stores that auto-update when sources change:
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
import { computed } from 'kstate'
|
|
331
|
+
|
|
332
|
+
// Filter
|
|
333
|
+
const activeUsers = computed(users, items => items.filter(u => u.isActive))
|
|
334
|
+
|
|
335
|
+
// Transform
|
|
336
|
+
const userStats = computed(users, items => ({
|
|
337
|
+
total: items.length,
|
|
338
|
+
active: items.filter(u => u.isActive).length,
|
|
339
|
+
}))
|
|
340
|
+
|
|
341
|
+
// Multiple sources
|
|
342
|
+
const dashboard = computed(
|
|
343
|
+
[users, orders],
|
|
344
|
+
([userList, orderList]) => ({
|
|
345
|
+
userCount: userList.length,
|
|
346
|
+
orderCount: orderList.length,
|
|
347
|
+
})
|
|
348
|
+
)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Fine-grained reactivity works with computed too:
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
const total = useStore(userStats.total) // Only re-renders on total change
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Complete Examples
|
|
360
|
+
|
|
361
|
+
### User Management
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
// stores/users.ts
|
|
365
|
+
import { createSetStore, api, computed } from 'kstate'
|
|
366
|
+
|
|
367
|
+
interface User {
|
|
368
|
+
id: string
|
|
369
|
+
name: string
|
|
370
|
+
email: string
|
|
371
|
+
role: 'admin' | 'user'
|
|
372
|
+
isActive: boolean
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export const users = createSetStore<User>({
|
|
376
|
+
...api({ list: '/users', dataKey: 'items' }),
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
export const activeUsers = computed(users, items => items.filter(u => u.isActive))
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
// components/UserList.tsx
|
|
384
|
+
import { useStore, useStoreStatus } from 'kstate'
|
|
385
|
+
import { users, activeUsers } from '../stores/users'
|
|
386
|
+
|
|
387
|
+
export function UserList() {
|
|
388
|
+
const items = useStore(activeUsers)
|
|
389
|
+
const { isLoading, error } = useStoreStatus(users)
|
|
390
|
+
|
|
391
|
+
useEffect(() => { users.get() }, [])
|
|
392
|
+
|
|
393
|
+
if (isLoading) return <Spinner />
|
|
394
|
+
if (error) return <Error message={error.message} />
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<ul>
|
|
398
|
+
{items.map(user => (
|
|
399
|
+
<li key={user.id}>
|
|
400
|
+
{user.name}
|
|
401
|
+
<button onClick={() => users.delete({ id: user.id })}>Delete</button>
|
|
402
|
+
</li>
|
|
403
|
+
))}
|
|
404
|
+
</ul>
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Shopping Cart (localStorage)
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
// stores/cart.ts
|
|
413
|
+
import { createSetStore, local, computed } from 'kstate'
|
|
414
|
+
|
|
415
|
+
interface CartItem {
|
|
416
|
+
id: string
|
|
417
|
+
productId: string
|
|
418
|
+
name: string
|
|
419
|
+
price: number
|
|
420
|
+
quantity: number
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export const cart = createSetStore<CartItem>(local('cart'))
|
|
424
|
+
|
|
425
|
+
export const cartTotal = computed(cart, items =>
|
|
426
|
+
items.reduce((sum, i) => sum + i.price * i.quantity, 0)
|
|
427
|
+
)
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
// components/Cart.tsx
|
|
432
|
+
function CartSummary() {
|
|
433
|
+
const total = useStore(cartTotal)
|
|
434
|
+
return <span>Total: ${total.toFixed(2)}</span>
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function AddToCart({ product }: { product: Product }) {
|
|
438
|
+
const handleAdd = () => {
|
|
439
|
+
const existing = cart.value.find(i => i.productId === product.id)
|
|
440
|
+
if (existing) {
|
|
441
|
+
cart.patch({ id: existing.id, quantity: existing.quantity + 1 })
|
|
442
|
+
} else {
|
|
443
|
+
cart.create({
|
|
444
|
+
id: crypto.randomUUID(),
|
|
445
|
+
productId: product.id,
|
|
446
|
+
name: product.name,
|
|
447
|
+
price: product.price,
|
|
448
|
+
quantity: 1,
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return <button onClick={handleAdd}>Add to Cart</button>
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Realtime Jobs (SSE + API)
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
// stores/jobs.ts
|
|
460
|
+
import { createSetStore, api, sse, local } from 'kstate'
|
|
461
|
+
|
|
462
|
+
interface Job {
|
|
463
|
+
id: string
|
|
464
|
+
status: 'pending' | 'running' | 'complete'
|
|
465
|
+
url: string
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export const jobs = createSetStore<Job>({
|
|
469
|
+
...api({ list: '/jobs', dataKey: 'items' }),
|
|
470
|
+
subscribe: sse('/jobs/stream', { mode: 'upsert' }),
|
|
471
|
+
persist: local('jobs-cache').persist,
|
|
472
|
+
})
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
```tsx
|
|
476
|
+
// components/Jobs.tsx
|
|
477
|
+
function JobList() {
|
|
478
|
+
const items = useStore(jobs)
|
|
479
|
+
|
|
480
|
+
useEffect(() => { jobs.get() }, [])
|
|
481
|
+
|
|
482
|
+
return items.map(job => (
|
|
483
|
+
<div key={job.id}>
|
|
484
|
+
{job.status}: {job.url}
|
|
485
|
+
</div>
|
|
486
|
+
))
|
|
487
|
+
}
|
|
488
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApiAdapterConfig } from '../types';
|
|
2
|
+
export declare function api<T extends {
|
|
3
|
+
id: string;
|
|
4
|
+
}>(config: ApiAdapterConfig): {
|
|
5
|
+
get: (params?: Record<string, unknown>) => Promise<T[]>;
|
|
6
|
+
getOne: (params: {
|
|
7
|
+
id: string;
|
|
8
|
+
} & Record<string, unknown>) => Promise<T>;
|
|
9
|
+
create: (body: Omit<T, "id"> | T) => Promise<T>;
|
|
10
|
+
set: (body: T) => Promise<T>;
|
|
11
|
+
patch: (body: Partial<T> & {
|
|
12
|
+
id: string;
|
|
13
|
+
}) => Promise<T>;
|
|
14
|
+
delete: (params: {
|
|
15
|
+
id: string;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/adapters/api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAIhD,wBAAgB,GAAG,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,gBAAgB;mBAK7C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;qBAIrB;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;mBAI1C,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC;gBAIpB,CAAC;kBAIC,OAAO,CAAC,CAAC,CAAC,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE;qBAIxB;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE;EAIxC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { apiFetch } from '../sync/api';
|
|
2
|
+
export function api(config) {
|
|
3
|
+
const { list, item = `${list}/:id`, dataKey, requestKey } = config;
|
|
4
|
+
const toParams = (p) => p;
|
|
5
|
+
return {
|
|
6
|
+
get: async (params) => {
|
|
7
|
+
const { data } = await apiFetch({ method: 'GET', endpoint: list, params: toParams(params), dataKey });
|
|
8
|
+
return data;
|
|
9
|
+
},
|
|
10
|
+
getOne: async (params) => {
|
|
11
|
+
const { data } = await apiFetch({ method: 'GET', endpoint: item, params: params, dataKey });
|
|
12
|
+
return data;
|
|
13
|
+
},
|
|
14
|
+
create: async (body) => {
|
|
15
|
+
const { data } = await apiFetch({ method: 'POST', endpoint: list, body, dataKey, requestKey });
|
|
16
|
+
return data;
|
|
17
|
+
},
|
|
18
|
+
set: async (body) => {
|
|
19
|
+
const { data } = await apiFetch({ method: 'PUT', endpoint: item, params: { id: body.id }, body, dataKey, requestKey });
|
|
20
|
+
return data;
|
|
21
|
+
},
|
|
22
|
+
patch: async (body) => {
|
|
23
|
+
const { data } = await apiFetch({ method: 'PATCH', endpoint: item, params: { id: body.id }, body, dataKey, requestKey });
|
|
24
|
+
return data;
|
|
25
|
+
},
|
|
26
|
+
delete: async (params) => {
|
|
27
|
+
await apiFetch({ method: 'DELETE', endpoint: item, params: params });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/adapters/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAKtC,MAAM,UAAU,GAAG,CAA2B,MAAwB;IACpE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,IAAI,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAAA;IAClE,MAAM,QAAQ,GAAG,CAAC,CAA2B,EAAE,EAAE,CAAC,CAAuB,CAAA;IAEzE,OAAO;QACL,GAAG,EAAE,KAAK,EAAE,MAAgC,EAAE,EAAE;YAC9C,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,QAAQ,CAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;YAC1G,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,EAAE,KAAK,EAAE,MAAgD,EAAE,EAAE;YACjE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,QAAQ,CAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAgB,EAAE,OAAO,EAAE,CAAC,CAAA;YACxG,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,EAAE,KAAK,EAAE,IAAuB,EAAE,EAAE;YACxC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,QAAQ,CAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAA;YACjG,OAAO,IAAI,CAAA;QACb,CAAC;QACD,GAAG,EAAE,KAAK,EAAE,IAAO,EAAE,EAAE;YACrB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,QAAQ,CAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAA;YACzH,OAAO,IAAI,CAAA;QACb,CAAC;QACD,KAAK,EAAE,KAAK,EAAE,IAAiC,EAAE,EAAE;YACjD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,QAAQ,CAAI,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAA;YAC3H,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,EAAE,KAAK,EAAE,MAAsB,EAAE,EAAE;YACvC,MAAM,QAAQ,CAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAgB,EAAE,CAAC,CAAA;QACtF,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function local<T extends {
|
|
2
|
+
id: string;
|
|
3
|
+
}>(key: string, defaultValue?: T[]): {
|
|
4
|
+
get: () => T[];
|
|
5
|
+
getOne: (params: {
|
|
6
|
+
id: string;
|
|
7
|
+
}) => T;
|
|
8
|
+
create: (data: Omit<T, "id"> | T) => T;
|
|
9
|
+
patch: (data: Partial<T> & {
|
|
10
|
+
id: string;
|
|
11
|
+
}) => T;
|
|
12
|
+
delete: (params: {
|
|
13
|
+
id: string;
|
|
14
|
+
}) => void;
|
|
15
|
+
persist: {
|
|
16
|
+
load: () => T[];
|
|
17
|
+
save: (data: T[]) => void;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/adapters/local.ts"],"names":[],"mappings":"AAEA,wBAAgB,KAAK,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC,EAAE;;qBAkB1D;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE;mBAChB,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC;kBAOlB,OAAO,CAAC,CAAC,CAAC,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE;qBAOxB;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE;;oBAhChB,CAAC,EAAE;qBAUA,CAAC,EAAE;;EA2BxB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const getStorage = () => typeof localStorage !== 'undefined' ? localStorage : globalThis.localStorage;
|
|
2
|
+
export function local(key, defaultValue) {
|
|
3
|
+
const load = () => {
|
|
4
|
+
try {
|
|
5
|
+
const storage = getStorage();
|
|
6
|
+
if (!storage)
|
|
7
|
+
return defaultValue ?? [];
|
|
8
|
+
const raw = storage.getItem(key);
|
|
9
|
+
if (!raw)
|
|
10
|
+
return defaultValue ?? [];
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
return Array.isArray(parsed) ? parsed : (defaultValue ?? []);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return defaultValue ?? [];
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const save = (data) => {
|
|
19
|
+
const storage = getStorage();
|
|
20
|
+
if (storage)
|
|
21
|
+
storage.setItem(key, JSON.stringify(data));
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
get: () => load(),
|
|
25
|
+
getOne: (params) => load().find(i => i.id === params.id) ?? (() => { throw new Error(`Item ${params.id} not found`); })(),
|
|
26
|
+
create: (data) => {
|
|
27
|
+
const items = load();
|
|
28
|
+
const item = { ...data, id: data.id ?? crypto.randomUUID() };
|
|
29
|
+
items.push(item);
|
|
30
|
+
save(items);
|
|
31
|
+
return item;
|
|
32
|
+
},
|
|
33
|
+
patch: (data) => {
|
|
34
|
+
const items = load();
|
|
35
|
+
const idx = items.findIndex(i => i.id === data.id);
|
|
36
|
+
if (idx < 0)
|
|
37
|
+
throw new Error(`Item ${data.id} not found`);
|
|
38
|
+
items[idx] = { ...items[idx], ...data };
|
|
39
|
+
save(items);
|
|
40
|
+
return items[idx];
|
|
41
|
+
},
|
|
42
|
+
delete: (params) => {
|
|
43
|
+
save(load().filter(i => i.id !== params.id));
|
|
44
|
+
},
|
|
45
|
+
persist: { load, save },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/adapters/local.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApiAdapterConfig } from '../types';
|
|
2
|
+
export declare function queuedApi<T extends {
|
|
3
|
+
id: string;
|
|
4
|
+
}>(config: ApiAdapterConfig): {
|
|
5
|
+
get: (params?: Record<string, unknown>) => Promise<T[]>;
|
|
6
|
+
getOne: (params: {
|
|
7
|
+
id: string;
|
|
8
|
+
} & Record<string, unknown>) => Promise<T>;
|
|
9
|
+
create: (body: Omit<T, "id"> | T) => Promise<T>;
|
|
10
|
+
set: (body: T) => Promise<T>;
|
|
11
|
+
patch: (body: Partial<T> & {
|
|
12
|
+
id: string;
|
|
13
|
+
}) => Promise<T>;
|
|
14
|
+
delete: (params: {
|
|
15
|
+
id: string;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=queuedApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queuedApi.d.ts","sourceRoot":"","sources":["../../src/adapters/queuedApi.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AA0BhD,wBAAgB,SAAS,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,gBAAgB;mBAGzD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;qBACrB;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;mBAC1C,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC;gBACpB,CAAC;kBACC,OAAO,CAAC,CAAC,CAAC,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE;qBACxB;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE;EAElC"}
|