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.
Files changed (106) hide show
  1. package/CHANGELOG.md +136 -25
  2. package/README.md +514 -81
  3. package/dist/_tsup-dts-rollup.d.cts +2411 -0
  4. package/dist/_tsup-dts-rollup.d.ts +2411 -0
  5. package/dist/async.cjs +34 -0
  6. package/dist/async.cjs.map +1 -0
  7. package/dist/async.d.cts +9 -0
  8. package/dist/async.d.ts +9 -30
  9. package/dist/async.js +34 -1
  10. package/dist/async.js.map +1 -0
  11. package/dist/computed.cjs +13 -0
  12. package/dist/computed.cjs.map +1 -0
  13. package/dist/computed.d.cts +7 -0
  14. package/dist/computed.d.ts +7 -0
  15. package/dist/computed.js +13 -0
  16. package/dist/computed.js.map +1 -0
  17. package/dist/core.cjs +24 -0
  18. package/dist/core.cjs.map +1 -0
  19. package/dist/core.d.cts +15 -0
  20. package/dist/core.d.ts +15 -1
  21. package/dist/core.js +24 -1
  22. package/dist/core.js.map +1 -0
  23. package/dist/devtools.cjs +2 -0
  24. package/dist/devtools.cjs.map +1 -0
  25. package/dist/devtools.d.cts +5 -0
  26. package/dist/devtools.d.ts +5 -0
  27. package/dist/devtools.js +2 -0
  28. package/dist/devtools.js.map +1 -0
  29. package/dist/feature.cjs +2 -0
  30. package/dist/feature.cjs.map +1 -0
  31. package/dist/feature.d.cts +14 -0
  32. package/dist/feature.d.ts +14 -0
  33. package/dist/feature.js +2 -0
  34. package/dist/feature.js.map +1 -0
  35. package/dist/helpers.cjs +24 -0
  36. package/dist/helpers.cjs.map +1 -0
  37. package/dist/helpers.d.cts +3 -0
  38. package/dist/helpers.d.ts +3 -0
  39. package/dist/helpers.js +24 -0
  40. package/dist/helpers.js.map +1 -0
  41. package/dist/index.cjs +35 -0
  42. package/dist/index.cjs.map +1 -0
  43. package/dist/index.d.cts +33 -0
  44. package/dist/index.d.ts +33 -3
  45. package/dist/index.js +35 -1
  46. package/dist/index.js.map +1 -0
  47. package/dist/install.cjs +2 -0
  48. package/dist/install.cjs.map +1 -0
  49. package/dist/install.d.cts +4 -0
  50. package/dist/install.d.ts +4 -0
  51. package/dist/install.js +2 -0
  52. package/dist/install.js.map +1 -0
  53. package/dist/persist.cjs +2 -0
  54. package/dist/persist.cjs.map +1 -0
  55. package/dist/persist.d.cts +1 -0
  56. package/dist/persist.d.ts +1 -0
  57. package/dist/persist.js +2 -0
  58. package/dist/persist.js.map +1 -0
  59. package/dist/react.cjs +36 -0
  60. package/dist/react.cjs.map +1 -0
  61. package/dist/react.d.cts +7 -0
  62. package/dist/react.d.ts +7 -20
  63. package/dist/react.js +36 -1
  64. package/dist/react.js.map +1 -0
  65. package/dist/runtime-admin.cjs +2 -0
  66. package/dist/runtime-admin.cjs.map +1 -0
  67. package/dist/runtime-admin.d.cts +2 -0
  68. package/dist/runtime-admin.d.ts +2 -0
  69. package/dist/runtime-admin.js +2 -0
  70. package/dist/runtime-admin.js.map +1 -0
  71. package/dist/runtime-tools.cjs +4 -0
  72. package/dist/runtime-tools.cjs.map +1 -0
  73. package/dist/runtime-tools.d.cts +9 -0
  74. package/dist/runtime-tools.d.ts +9 -0
  75. package/dist/runtime-tools.js +4 -0
  76. package/dist/runtime-tools.js.map +1 -0
  77. package/dist/selectors.cjs +2 -0
  78. package/dist/selectors.cjs.map +1 -0
  79. package/dist/selectors.d.cts +2 -0
  80. package/dist/selectors.d.ts +2 -0
  81. package/dist/selectors.js +2 -0
  82. package/dist/selectors.js.map +1 -0
  83. package/dist/server.cjs +12 -0
  84. package/dist/server.cjs.map +1 -0
  85. package/dist/server.d.cts +2 -0
  86. package/dist/server.d.ts +2 -0
  87. package/dist/server.js +12 -0
  88. package/dist/server.js.map +1 -0
  89. package/dist/sync.cjs +2 -0
  90. package/dist/sync.cjs.map +1 -0
  91. package/dist/sync.d.cts +1 -0
  92. package/dist/sync.d.ts +1 -0
  93. package/dist/sync.js +2 -0
  94. package/dist/sync.js.map +1 -0
  95. package/dist/testing.cjs +24 -0
  96. package/dist/testing.cjs.map +1 -0
  97. package/dist/testing.d.cts +4 -0
  98. package/dist/testing.d.ts +4 -16
  99. package/dist/testing.js +24 -1
  100. package/dist/testing.js.map +1 -0
  101. package/package.json +86 -6
  102. package/dist/chunk-5F2FD6DX.js +0 -17
  103. package/dist/chunk-G6JMMJYH.js +0 -5
  104. package/dist/chunk-JBYLHJKN.js +0 -3
  105. package/dist/chunk-K6QIWMMW.js +0 -1
  106. package/dist/core-CKzRwVaY.d.ts +0 -213
package/README.md CHANGED
@@ -1,82 +1,515 @@
1
- # Stroid
1
+ # Stroid
2
+
3
+ [![npm](https://img.shields.io/npm/v/stroid)](https://npmjs.com/package/stroid)
4
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/stroid)](https://bundlephobia.com/package/stroid)
5
+ [![types](https://img.shields.io/npm/types/stroid)](https://npmjs.com/package/stroid)
6
+ [![license](https://img.shields.io/npm/l/stroid)](./LICENSE)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/Himesh-Bhattarai/stroid/ci.yml)](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
- Compact, batteries-included state management for JavaScript and React.
4
-
5
- Stroid keeps the API small: create a store, update it, read it. Persistence, async caching, cross-tab sync, middleware, schema validation, history, and React hooks are configured per store instead of bolted on later.
6
-
7
- This package is ESM-only, tree-shakeable, side-effect free, and ships with zero runtime dependencies.
8
-
9
- ## Install
10
-
11
- ```bash
12
- npm install stroid
13
- ```
14
-
15
- ## Quick Example
16
-
17
- ```js
18
- import { createStore, setStore } from "stroid/core"
19
- import { useStore } from "stroid/react"
20
-
21
- createStore("user", { name: "Eli", theme: "dark" })
22
-
23
- function Profile() {
24
- const name = useStore("user", "name")
25
- return <h1>Hello, {name}</h1>
26
- }
27
-
28
- setStore("user", "theme", "light")
29
- ```
30
-
31
- The core path API uses `storeName` and `path` separately:
32
-
33
- - `setStore("user", "name", "Jo")`
34
- - `getStore("user", "name")`
35
- - `useStore("user", "name")`
36
-
37
- ## What Ships
38
-
39
- - Core store primitives: `createStore`, `setStore`, `getStore`, `mergeStore`, `resetStore`
40
- - React hooks: `useStore`, `useSelector`, `useStoreStatic`, `useAsyncStore`, `useFormStore`
41
- - Async helpers: `fetchStore`, `refetchStore`, `enableRevalidateOnFocus`
42
- - Per-store features: `persist`, `sync`, middleware, validator/schema, devtools, history
43
- - Utility helpers: selectors, metrics, testing helpers, entity/list/counter presets, SSR hydrate helpers
44
-
45
- ## Highlights
46
-
47
- - Dot-path reads and writes with path validation
48
- - Draft-style mutator updates
49
- - Persistence with custom drivers, migrations, and recovery hooks
50
- - Async caching with TTL, dedupe, retries, and focus/online revalidation
51
- - BroadcastChannel sync with conflict resolution and payload size guardrails
52
- - No Provider required for React usage
53
-
54
- ## Docs
55
-
56
- - [The Book](./docs/README.md)
57
- - [Getting Started](./docs/02-getting-started.md)
58
- - [Core API](./docs/04-createStore.md)
59
- - [React](./docs/12-react.md)
60
- - [Async](./docs/13-async.md)
61
- - [Persistence](./docs/14-persist.md)
62
- - [Sync](./docs/15-sync.md)
63
- - [Testing](./docs/20-testing.md)
64
- - [Roadmap](./docs/24-roadmap.md)
65
-
66
- ## Package Entry Points
67
-
68
- - `stroid` exports the full public API
69
- - `stroid/core` exports the core store APIs
70
- - `stroid/react` exports the React hooks
71
- - `stroid/async` exports async helpers
72
- - `stroid/testing` exports test helpers
73
-
74
- ## Notes
75
-
76
- - React is a peer dependency (`>=18`)
77
- - Node `>=18` is required
78
- - Planned or not-yet-implemented ideas belong in the roadmap, not the API docs
79
-
80
- ## License
81
-
82
- MIT
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)