stroid 0.0.4 → 0.1.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/CHANGELOG.md +136 -25
- package/README.md +514 -81
- package/dist/_tsup-dts-rollup.d.cts +2411 -0
- package/dist/_tsup-dts-rollup.d.ts +2411 -0
- package/dist/async.cjs +34 -0
- package/dist/async.cjs.map +1 -0
- package/dist/async.d.cts +9 -0
- package/dist/async.d.ts +9 -30
- package/dist/async.js +34 -1
- package/dist/async.js.map +1 -0
- package/dist/computed.cjs +13 -0
- package/dist/computed.cjs.map +1 -0
- package/dist/computed.d.cts +7 -0
- package/dist/computed.d.ts +7 -0
- package/dist/computed.js +13 -0
- package/dist/computed.js.map +1 -0
- package/dist/core.cjs +24 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +15 -0
- package/dist/core.d.ts +15 -1
- package/dist/core.js +24 -1
- package/dist/core.js.map +1 -0
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.cts +5 -0
- package/dist/devtools.d.ts +5 -0
- package/dist/devtools.js +2 -0
- package/dist/devtools.js.map +1 -0
- package/dist/feature.cjs +2 -0
- package/dist/feature.cjs.map +1 -0
- package/dist/feature.d.cts +14 -0
- package/dist/feature.d.ts +14 -0
- package/dist/feature.js +2 -0
- package/dist/feature.js.map +1 -0
- package/dist/helpers.cjs +24 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.cts +3 -0
- package/dist/helpers.d.ts +3 -0
- package/dist/helpers.js +24 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.cjs +35 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -3
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -0
- package/dist/install.cjs +2 -0
- package/dist/install.cjs.map +1 -0
- package/dist/install.d.cts +4 -0
- package/dist/install.d.ts +4 -0
- package/dist/install.js +2 -0
- package/dist/install.js.map +1 -0
- package/dist/persist.cjs +2 -0
- package/dist/persist.cjs.map +1 -0
- package/dist/persist.d.cts +1 -0
- package/dist/persist.d.ts +1 -0
- package/dist/persist.js +2 -0
- package/dist/persist.js.map +1 -0
- package/dist/react.cjs +36 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +7 -0
- package/dist/react.d.ts +7 -20
- package/dist/react.js +36 -1
- package/dist/react.js.map +1 -0
- package/dist/runtime-admin.cjs +2 -0
- package/dist/runtime-admin.cjs.map +1 -0
- package/dist/runtime-admin.d.cts +2 -0
- package/dist/runtime-admin.d.ts +2 -0
- package/dist/runtime-admin.js +2 -0
- package/dist/runtime-admin.js.map +1 -0
- package/dist/runtime-tools.cjs +4 -0
- package/dist/runtime-tools.cjs.map +1 -0
- package/dist/runtime-tools.d.cts +9 -0
- package/dist/runtime-tools.d.ts +9 -0
- package/dist/runtime-tools.js +4 -0
- package/dist/runtime-tools.js.map +1 -0
- package/dist/selectors.cjs +2 -0
- package/dist/selectors.cjs.map +1 -0
- package/dist/selectors.d.cts +2 -0
- package/dist/selectors.d.ts +2 -0
- package/dist/selectors.js +2 -0
- package/dist/selectors.js.map +1 -0
- package/dist/server.cjs +12 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +1 -0
- package/dist/sync.cjs +2 -0
- package/dist/sync.cjs.map +1 -0
- package/dist/sync.d.cts +1 -0
- package/dist/sync.d.ts +1 -0
- package/dist/sync.js +2 -0
- package/dist/sync.js.map +1 -0
- package/dist/testing.cjs +24 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +4 -0
- package/dist/testing.d.ts +4 -16
- package/dist/testing.js +24 -1
- package/dist/testing.js.map +1 -0
- package/package.json +86 -6
- package/dist/chunk-5F2FD6DX.js +0 -17
- package/dist/chunk-G6JMMJYH.js +0 -5
- package/dist/chunk-JBYLHJKN.js +0 -3
- package/dist/chunk-K6QIWMMW.js +0 -1
- package/dist/core-CKzRwVaY.d.ts +0 -213
package/README.md
CHANGED
|
@@ -1,82 +1,515 @@
|
|
|
1
|
-
# Stroid
|
|
1
|
+
# Stroid
|
|
2
|
+
|
|
3
|
+
[](https://npmjs.com/package/stroid)
|
|
4
|
+
[](https://bundlephobia.com/package/stroid)
|
|
5
|
+
[](https://npmjs.com/package/stroid)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://github.com/Himesh-Bhattarai/stroid/actions)
|
|
8
|
+
|
|
9
|
+
**Named-store state engine for TypeScript and React.**
|
|
10
|
+
Every store has a name. Write to it from anywhere — hooks, utilities, server, tests. Optional layers add persistence, sync, async fetch, SSR isolation, and devtools without touching your core logic.
|
|
11
|
+
|
|
12
|
+
> 🚀 **Power in 4 lines:** Create a store, read/write it, optionally persist, sync, or hydrate for SSR.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
createStore("user", { name: "Ava", role: "admin" }) // define once
|
|
16
|
+
setStore("user", "name", "Kai") // write from anywhere
|
|
17
|
+
const name = useStore("user", s => s.name) // React hook
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Layers
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
┌─────────────────────────────────────────────────────────┐
|
|
26
|
+
│ your app │
|
|
27
|
+
├─────────────────────────────────────────────────────────┤
|
|
28
|
+
│ useStore useSelector useAsyncStore useFormStore │ stroid/react
|
|
29
|
+
├─────────────────────────────────────────────────────────┤
|
|
30
|
+
│ createStore setStore getStore setStoreBatch │ stroid ← core
|
|
31
|
+
│ createComputed createSelector createEntityStore │
|
|
32
|
+
├──────────────┬──────────────┬───────────────────────────┤
|
|
33
|
+
│ stroid/persist│ stroid/sync │ stroid/async │ opt-in features
|
|
34
|
+
│ localStorage │ BroadcastCh │ fetch + cache + retry │
|
|
35
|
+
├──────────────┴──────────────┴───────────────────────────┤
|
|
36
|
+
│ stroid/server createStoreForRequest (AsyncLocalStorage)│ SSR
|
|
37
|
+
├─────────────────────────────────────────────────────────┤
|
|
38
|
+
│ stroid/devtools stroid/testing stroid/runtime-tools │ tooling
|
|
39
|
+
└─────────────────────────────────────────────────────────┘
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Each row is independent. Use only what you need.
|
|
2
43
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
createStore(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
44
|
+
Note: `stroid/core` exports only `createStore`, `setStore`, `getStore`, and `deleteStore`. Import from `stroid` for the full core runtime (batching, reset, hydration, and hooks).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install stroid
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **Note:** `main` is locked between releases. Active development is on the `dev` branch — PRs and forks should target `dev`. Commit messages follow [STATUS.md](./STATUS.md) conventions.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick API Reference
|
|
59
|
+
|
|
60
|
+
| API | Purpose |
|
|
61
|
+
|-----|---------|
|
|
62
|
+
| `createStore(name, state, options?)` | Define a store |
|
|
63
|
+
| `setStore(name, path, value)` | Write a value by path |
|
|
64
|
+
| `setStore(name, draft => { })` | Mutate with a function |
|
|
65
|
+
| `replaceStore(name, value)` | Replace an entire store |
|
|
66
|
+
| `getStore(name, path?)` | Read a store (or a path inside it) |
|
|
67
|
+
| `setStoreBatch(fn)` | Atomic multi-store write, rollback on error |
|
|
68
|
+
| `useStore(name, selector?)` | React hook — subscribes to a store |
|
|
69
|
+
| `useSelector(name, fn)` | React hook — fine-grained derived value |
|
|
70
|
+
| `fetchStore(name, url, options?)` | Async fetch wired to store state |
|
|
71
|
+
| `createComputed(name, deps, fn)` | Reactive derived store |
|
|
72
|
+
| `createStoreForRequest(fn)` | Per-request SSR registry |
|
|
73
|
+
| `hydrateStores(snapshot)` | Rehydrate on client from server state |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
Three levels. Start where you are.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### Level 1 — The Basics
|
|
84
|
+
|
|
85
|
+
**Create a store. Read it. Write to it.**
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { createStore, getStore, setStore } from "stroid"
|
|
89
|
+
|
|
90
|
+
createStore("counter", { count: 0 })
|
|
91
|
+
|
|
92
|
+
setStore("counter", "count", 1)
|
|
93
|
+
console.log(getStore("counter")) // { count: 1 }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Use it in React.**
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { useStore } from "stroid/react"
|
|
100
|
+
|
|
101
|
+
function Counter() {
|
|
102
|
+
const count = useStore("counter", s => s.count)
|
|
103
|
+
return (
|
|
104
|
+
<button onClick={() => setStore("counter", "count", count + 1)}>
|
|
105
|
+
{count}
|
|
106
|
+
</button>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Batch multiple writes — one notification, atomic rollback.**
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { setStoreBatch, setStore } from "stroid"
|
|
115
|
+
|
|
116
|
+
setStoreBatch(() => {
|
|
117
|
+
setStore("cart", { items: [{ id: 1, price: 12 }] })
|
|
118
|
+
setStore("ui", "loading", false)
|
|
119
|
+
setStore("user", "lastSeen", Date.now())
|
|
120
|
+
// if any write throws → all three roll back
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Typed store handle — trade string keys for compile-time safety.**
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { store, createStore, setStore, getStore } from "stroid"
|
|
128
|
+
|
|
129
|
+
const counter = store<"counter", { count: number }>("counter")
|
|
130
|
+
|
|
131
|
+
createStore("counter", { count: 0 })
|
|
132
|
+
setStore(counter, draft => { draft.count += 1 })
|
|
133
|
+
console.log(getStore(counter, "count")) // 1
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Type-safe string store names (module augmentation).**
|
|
137
|
+
|
|
138
|
+
If you prefer `useStore("user")` and `setStore("user", ...)` with compile-time checking,
|
|
139
|
+
augment `StoreStateMap` or `StrictStoreMap` in a `.d.ts` file:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// src/stroid.d.ts
|
|
143
|
+
declare module "stroid" {
|
|
144
|
+
interface StoreStateMap {
|
|
145
|
+
user: {
|
|
146
|
+
name: string
|
|
147
|
+
role: "admin" | "user"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Optional strict opt-in for locked store names:
|
|
153
|
+
// declare module "stroid" { interface StrictStoreMap { user: ... } }
|
|
154
|
+
// If you import from "stroid/core", add the same module augmentation there.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Level 2 — Real Features
|
|
161
|
+
|
|
162
|
+
**Persist to localStorage — survives page reload.**
|
|
163
|
+
|
|
164
|
+
> ⚡ **Tip:** Add `import "stroid/persist"` once at your app entry (e.g. `main.tsx`) to enable persistence globally. Any store with a `persist` option will activate automatically.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { createStore } from "stroid"
|
|
168
|
+
import "stroid/persist"
|
|
169
|
+
|
|
170
|
+
createStore("settings", { theme: "dark", lang: "en" }, {
|
|
171
|
+
persist: {
|
|
172
|
+
key: "app-settings",
|
|
173
|
+
allowPlaintext: true,
|
|
174
|
+
version: 2,
|
|
175
|
+
migrate: (old, v) => v === 1 ? { ...old, lang: "en" } : old,
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Sync across browser tabs — zero wiring.**
|
|
181
|
+
|
|
182
|
+
> ⚡ **Tip:** Add `import "stroid/sync"` once at app entry. Any store with `sync: true` or `sync: { channel }` will start broadcasting automatically.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { createStore } from "stroid"
|
|
186
|
+
import "stroid/sync"
|
|
187
|
+
|
|
188
|
+
createStore("presence", { online: true, cursor: null }, {
|
|
189
|
+
sync: { channel: "presence-sync" }
|
|
190
|
+
// Lamport clock conflict resolution built in.
|
|
191
|
+
// Stale messages from closed tabs auto-rejected.
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Persist + sync together.**
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { createStore } from "stroid"
|
|
199
|
+
import "stroid/persist"
|
|
200
|
+
import "stroid/sync"
|
|
201
|
+
|
|
202
|
+
createStore("settings", { theme: "dark", lang: "en" }, {
|
|
203
|
+
persist: { key: "app-settings", allowPlaintext: true },
|
|
204
|
+
sync: { channel: "settings-sync" },
|
|
205
|
+
})
|
|
206
|
+
// Change in one tab → persisted locally + broadcast to all other tabs.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Async fetch — SWR-style, wired directly to store state.**
|
|
210
|
+
|
|
211
|
+
> ⚡ **Tip:** `fetchStore` manages `loading`, `error`, `data`, and `status` fields automatically. No separate state machine needed — just read `useStore("user")`.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import { createStore } from "stroid"
|
|
215
|
+
import { fetchStore } from "stroid/async"
|
|
216
|
+
import { useStore } from "stroid/react"
|
|
217
|
+
|
|
218
|
+
createStore("user", { data: null, loading: false, error: null, status: "idle" })
|
|
219
|
+
|
|
220
|
+
const controller = new AbortController()
|
|
221
|
+
|
|
222
|
+
fetchStore("user", "/api/user", {
|
|
223
|
+
signal: controller.signal,
|
|
224
|
+
ttl: 30_000, // 30s cache
|
|
225
|
+
staleWhileRevalidate: true, // show stale, revalidate in background
|
|
226
|
+
dedupe: true, // concurrent calls share one request
|
|
227
|
+
retry: 3, // auto-retry on failure
|
|
228
|
+
retryDelay: 400,
|
|
229
|
+
transform: res => res.data, // shape the response
|
|
230
|
+
onSuccess: data => console.log("fetched", data),
|
|
231
|
+
onError: err => Sentry.captureException(err),
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
function UserCard() {
|
|
235
|
+
const user = useStore("user")
|
|
236
|
+
if (user?.loading) return <Spinner />
|
|
237
|
+
if (user?.error) return <Error message={user.error} />
|
|
238
|
+
return <div>{user?.data?.name}</div>
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Computed stores — reactive, cached, cycle-safe.**
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { createStore } from "stroid"
|
|
246
|
+
import { createComputed } from "stroid/computed"
|
|
247
|
+
|
|
248
|
+
createStore("cart", { items: [] })
|
|
249
|
+
createStore("discount", { pct: 10 })
|
|
250
|
+
|
|
251
|
+
createComputed(
|
|
252
|
+
"cartTotal",
|
|
253
|
+
["cart", "discount"],
|
|
254
|
+
(cart, discount) => {
|
|
255
|
+
const raw = cart.items.reduce((sum, i) => sum + i.price, 0)
|
|
256
|
+
return raw * (1 - discount.pct / 100)
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
// cartTotal updates whenever cart or discount changes.
|
|
261
|
+
// Circular dependency detected at definition time.
|
|
262
|
+
// Flush order is topologically sorted — always correct.
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Entity store — built-in CRUD for collections.**
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { createEntityStore } from "stroid/helpers"
|
|
269
|
+
|
|
270
|
+
const users = createEntityStore("users")
|
|
271
|
+
|
|
272
|
+
users.upsert({ id: "1", name: "Ava", role: "admin" })
|
|
273
|
+
users.upsert({ id: "2", name: "Kai", role: "user" })
|
|
274
|
+
|
|
275
|
+
console.log(users.get("1")) // { id: "1", name: "Ava", role: "admin" }
|
|
276
|
+
console.log(users.getAll()) // [{ id: "1" }, { id: "2" }]
|
|
277
|
+
|
|
278
|
+
users.remove("2")
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
### Level 3 — Production Patterns
|
|
284
|
+
|
|
285
|
+
**SSR with per-request isolation — no cross-request leaks.**
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// app/api/render/route.ts (Next.js App Router)
|
|
289
|
+
import { createStoreForRequest } from "stroid/server"
|
|
290
|
+
import { renderToString } from "react-dom/server"
|
|
291
|
+
|
|
292
|
+
export async function GET(req: Request) {
|
|
293
|
+
const session = await getSession(req)
|
|
294
|
+
|
|
295
|
+
// Each request gets a fully isolated registry.
|
|
296
|
+
// AsyncLocalStorage ensures concurrent requests
|
|
297
|
+
// never share store values or subscribers.
|
|
298
|
+
const stores = createStoreForRequest((api) => {
|
|
299
|
+
api.create("user", { name: session.user.name, role: session.user.role })
|
|
300
|
+
api.create("cart", { items: [] })
|
|
301
|
+
api.create("flags", session.featureFlags)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const html = stores.hydrate(() => renderToString(<App />))
|
|
305
|
+
const state = stores.snapshot() // plain JSON → send to client
|
|
306
|
+
|
|
307
|
+
return Response.json({ html, state })
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Client: rehydrate from server snapshot
|
|
311
|
+
hydrateStores(window.__STROID_STATE__)
|
|
312
|
+
|
|
313
|
+
Tip: For typed SSR APIs, either augment `StoreStateMap` or pass a generic:
|
|
314
|
+
`createStoreForRequest<{ user: UserState }>((api) => { ... })`.
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Middleware — intercept, transform, or veto any write.**
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
createStore("cart", { items: [], total: 0 }, {
|
|
321
|
+
middleware: (ctx) => {
|
|
322
|
+
// ctx.action = "set" | "reset" | "hydrate"
|
|
323
|
+
// ctx.prev = previous state
|
|
324
|
+
// ctx.next = incoming state
|
|
325
|
+
// return MIDDLEWARE_ABORT to cancel the write
|
|
326
|
+
if (ctx.action === "set" && ctx.next.items.length > 100) {
|
|
327
|
+
ctx.options.onError?.("Cart limit exceeded")
|
|
328
|
+
return MIDDLEWARE_ABORT
|
|
329
|
+
}
|
|
330
|
+
// log every write to your analytics
|
|
331
|
+
analytics.track("cart.updated", { prev: ctx.prev, next: ctx.next })
|
|
332
|
+
return ctx.next
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Persist with encryption — no plaintext secrets in localStorage.**
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { createStore } from "stroid"
|
|
341
|
+
import "stroid/persist"
|
|
342
|
+
|
|
343
|
+
createStore("vault", { apiKey: "", token: "" }, {
|
|
344
|
+
persist: {
|
|
345
|
+
key: "secure-vault",
|
|
346
|
+
encrypt: (data) => myAES.encrypt(JSON.stringify(data)),
|
|
347
|
+
decrypt: (raw) => JSON.parse(myAES.decrypt(raw)),
|
|
348
|
+
// sensitiveData: true blocks persist entirely if no encrypt is provided
|
|
349
|
+
sensitiveData: true,
|
|
350
|
+
onStorageCleared: ({ name, reason }) => {
|
|
351
|
+
// fires when localStorage is cleared externally (another tab, devtools, etc.)
|
|
352
|
+
console.warn(`${name} storage cleared: ${reason}`)
|
|
353
|
+
redirectToLogin()
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Observability — inspect any store at runtime.**
|
|
360
|
+
|
|
361
|
+
> ⚡ **Tip:** Add `import "stroid/devtools"` at app entry to enable time-travel history and store inspection. Use `getMetrics(name)` in production to track notification performance per store.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { getMetrics, getSubscriberCount, getComputedGraph } from "stroid/runtime-tools"
|
|
365
|
+
|
|
366
|
+
// Per-store performance metrics
|
|
367
|
+
const m = getMetrics("cart")
|
|
368
|
+
// { notifyCount: 42, totalNotifyMs: 8.3, lastNotifyMs: 0.2 }
|
|
369
|
+
|
|
370
|
+
// How many components are subscribed right now
|
|
371
|
+
console.log(getSubscriberCount("cart")) // 3
|
|
372
|
+
|
|
373
|
+
// Full computed dependency graph
|
|
374
|
+
console.log(getComputedGraph())
|
|
375
|
+
// { nodes: ["cartTotal"], edges: [{ from: "cart", to: "cartTotal" }] }
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Global flush configuration — tune for your app's load profile.**
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { configureStroid } from "stroid"
|
|
382
|
+
|
|
383
|
+
configureStroid({
|
|
384
|
+
// Route internal logs to your observability platform
|
|
385
|
+
logSink: {
|
|
386
|
+
warn: msg => Sentry.captureMessage(msg, "warning"),
|
|
387
|
+
critical: msg => Sentry.captureException(new Error(msg)),
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
// Priority stores notify subscribers first
|
|
391
|
+
flush: {
|
|
392
|
+
priorityStores: ["auth", "user"],
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
// Revalidate async stores when tab regains focus
|
|
396
|
+
revalidateOnFocus: {
|
|
397
|
+
debounceMs: 500,
|
|
398
|
+
maxConcurrent: 3,
|
|
399
|
+
staggerMs: 100,
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Large store performance (recommendations).**
|
|
405
|
+
|
|
406
|
+
- Split stores by domain to keep hot updates small.
|
|
407
|
+
- For large lists, prefer `snapshot: "shallow"` per store or `configureStroid({ snapshotStrategy: "shallow" })` globally.
|
|
408
|
+
- Prefer path updates and targeted selectors (`useSelector`, `useStoreField`) over whole-store subscriptions.
|
|
409
|
+
|
|
410
|
+
**Optional structural sharing for mutator updates.**
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import { configureStroid } from "stroid"
|
|
414
|
+
import { produce } from "immer"
|
|
415
|
+
|
|
416
|
+
configureStroid({ mutatorProduce: produce })
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
If you prefer a shorthand, set `globalThis.__STROID_IMMER_PRODUCE__ = produce` once and use `configureStroid({ mutatorProduce: "immer" })`.
|
|
420
|
+
|
|
421
|
+
**Testing — deterministic, isolated, zero globals.**
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { createMockStore, resetAllStoresForTest } from "stroid/testing"
|
|
425
|
+
|
|
426
|
+
beforeEach(() => resetAllStoresForTest())
|
|
427
|
+
|
|
428
|
+
test("cart total updates when item added", () => {
|
|
429
|
+
const cart = createMockStore("cart", { items: [] })
|
|
430
|
+
|
|
431
|
+
setStore("cart", "items", [{ id: 1, price: 50 }])
|
|
432
|
+
|
|
433
|
+
expect(getStore("cart", "items")).toHaveLength(1)
|
|
434
|
+
expect(getStore("cartTotal")).toBe(45) // with 10% discount
|
|
435
|
+
})
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Module Imports
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
// Core
|
|
444
|
+
import { createStore, setStore, getStore, deleteStore,
|
|
445
|
+
resetStore, hasStore, setStoreBatch, hydrateStores } from "stroid"
|
|
446
|
+
|
|
447
|
+
// React
|
|
448
|
+
import { useStore, useSelector, useStoreField,
|
|
449
|
+
useAsyncStore, useFormStore, useAsyncStoreSuspense } from "stroid/react"
|
|
450
|
+
|
|
451
|
+
// Async
|
|
452
|
+
import { fetchStore, refetchStore, enableRevalidateOnFocus } from "stroid/async"
|
|
453
|
+
|
|
454
|
+
// Selectors & Computed
|
|
455
|
+
import { createSelector, subscribeWithSelector } from "stroid/selectors"
|
|
456
|
+
import { createComputed, deleteComputed } from "stroid/computed"
|
|
457
|
+
|
|
458
|
+
// Features (side-effect imports — register once at app entry)
|
|
459
|
+
import "stroid/persist"
|
|
460
|
+
import "stroid/sync"
|
|
461
|
+
import "stroid/devtools"
|
|
462
|
+
|
|
463
|
+
// Server / SSR
|
|
464
|
+
import { createStoreForRequest } from "stroid/server"
|
|
465
|
+
|
|
466
|
+
// Helpers & Testing
|
|
467
|
+
import { createEntityStore, createCounterStore } from "stroid/helpers"
|
|
468
|
+
import { createMockStore, resetAllStoresForTest } from "stroid/testing"
|
|
469
|
+
|
|
470
|
+
// Runtime
|
|
471
|
+
import { listStores, getMetrics, getComputedGraph } from "stroid/runtime-tools"
|
|
472
|
+
import { clearAllStores } from "stroid/runtime-admin"
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Behavior Notes
|
|
478
|
+
|
|
479
|
+
- **Features are explicit.** `persist`, `sync`, and `devtools` require a side-effect import. Nothing loads you didn't ask for.
|
|
480
|
+
- **Snapshot mode defaults to deep clone.** Subscribers and selectors always receive immutable snapshots.
|
|
481
|
+
- **`setStoreBatch` is transactional.** All writes stage first. Commit happens only if the batch completes without error. On failure, all writes roll back.
|
|
482
|
+
- **`setStore(name, data)` merges objects.** It shallow-merges into object stores. Use `replaceStore(name, value)` to replace the whole store.
|
|
483
|
+
- **Typed string store names are opt-in.** If you want `setStore("user", "profile.name", ...)` to be checked, augment `StoreStateMap` or use typed store handles.
|
|
484
|
+
- **SSR stores are request-scoped by default.** Global SSR stores require `{ allowSSRGlobalStore: true }`.
|
|
485
|
+
- **`fetchStore` deduplicates by default.** Concurrent calls with the same store name share one in-flight request.
|
|
486
|
+
- **Computed deps can be store names or handles.** Missing deps yield `null` until the dependency store is created.
|
|
487
|
+
- **Persist defaults to `localStorage`.** Provide a custom `driver` for `sessionStorage`, `IndexedDB`, or any storage adapter.
|
|
488
|
+
- **Sync uses `BroadcastChannel`.** Warns and no-ops gracefully when unavailable (Safari private mode, Node).
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Docs
|
|
493
|
+
|
|
494
|
+
Full documentation, architecture guide, and examples:
|
|
495
|
+
|
|
496
|
+
- [Start Here](./docs/start-here.md)
|
|
497
|
+
- [Core API](./docs/core.md)
|
|
498
|
+
- [React Layer](./docs/react.md)
|
|
499
|
+
- [Async Layer](./docs/async.md)
|
|
500
|
+
- [Persistence](./docs/persist.md)
|
|
501
|
+
- [Cross-tab Sync](./docs/sync.md)
|
|
502
|
+
- [Server & SSR](./docs/server.md)
|
|
503
|
+
- [Computed Stores](./docs/computed.md)
|
|
504
|
+
- [Selectors](./docs/selectors.md)
|
|
505
|
+
- [Testing](./docs/testing.md)
|
|
506
|
+
- [Devtools](./docs/devtools.md)
|
|
507
|
+
- [Runtime Tools](./docs/runtime.md)
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Changelog & License
|
|
512
|
+
|
|
513
|
+
- [CHANGELOG](./CHANGELOG.md)
|
|
514
|
+
- [MIT License](./LICENSE)
|
|
515
|
+
- [Issues](https://github.com/Himesh-Bhattarai/stroid/issues)
|