shared-state-bridge 1.0.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 ADDED
@@ -0,0 +1,1968 @@
1
+ # shared-state-bridge
2
+
3
+ Lightweight shared state bridge for monorepo apps. Sync state across packages in Turborepo / Nx with TypeScript-first API, React hooks, optional persistence, and real-time WebSocket sync across apps.
4
+
5
+ - **Zero-config cross-package sharing** — named bridges resolve via global registry
6
+ - **React 18+ hooks** — `useSyncExternalStore` with selector-based re-render optimization
7
+ - **Optional persistence** — localStorage, AsyncStorage, or custom adapters
8
+ - **Real-time sync** — WebSocket-based state sync across different apps (Next.js + React Native)
9
+ - **Tiny** — core ~1.2KB gzipped, no dependencies
10
+ - **TypeScript-first** — full type inference and safety
11
+ - **Plugin system** — extend with persistence, sync, logging, or your own plugins
12
+
13
+ ---
14
+
15
+ ## Table of Contents
16
+
17
+ - [Install](#install)
18
+ - [Quick Start](#quick-start)
19
+ - [Core Concepts](#core-concepts)
20
+ - [Creating a Bridge](#creating-a-bridge)
21
+ - [Reading and Updating State](#reading-and-updating-state)
22
+ - [Subscribing to Changes](#subscribing-to-changes)
23
+ - [Destroying a Bridge](#destroying-a-bridge)
24
+ - [Cross-Package Sharing](#cross-package-sharing)
25
+ - [How It Works (Architecture)](#how-it-works-architecture)
26
+ - [React Integration](#react-integration)
27
+ - [BridgeProvider](#bridgeprovider)
28
+ - [useBridgeState](#usebridgestate)
29
+ - [useBridge](#usebridge)
30
+ - [Preventing Unnecessary Re-renders](#preventing-unnecessary-re-renders)
31
+ - [Server-Side Rendering (Next.js)](#server-side-rendering-nextjs)
32
+ - [Persistence](#persistence)
33
+ - [Web — localStorage](#web--localstorage)
34
+ - [React Native — AsyncStorage](#react-native--asyncstorage)
35
+ - [Custom Adapter](#custom-adapter)
36
+ - [Persistence Options Reference](#persistence-options-reference)
37
+ - [Schema Migrations](#schema-migrations)
38
+ - [Testing with memoryAdapter](#testing-with-memoryadapter)
39
+ - [Real-Time Sync (WebSocket)](#real-time-sync-websocket)
40
+ - [Basic Usage](#basic-usage)
41
+ - [How It Works](#how-it-works)
42
+ - [Selective Sync (pick / omit)](#selective-sync-pick--omit)
43
+ - [Custom Conflict Resolution](#custom-conflict-resolution)
44
+ - [Reconnection](#reconnection)
45
+ - [Callbacks](#callbacks)
46
+ - [Sync Options Reference](#sync-options-reference)
47
+ - [Wire Protocol](#wire-protocol)
48
+ - [Example WebSocket Server (Node.js)](#example-websocket-server-nodejs)
49
+ - [Plugins](#plugins)
50
+ - [Plugin Lifecycle](#plugin-lifecycle)
51
+ - [Writing a Custom Plugin](#writing-a-custom-plugin)
52
+ - [TypeScript](#typescript)
53
+ - [Monorepo Setup (Turborepo / Nx)](#monorepo-setup-turborepo--nx)
54
+ - [API Reference](#api-reference)
55
+ - [Core API](#core-api)
56
+ - [BridgeApi Instance Methods](#bridgeapi-instance-methods)
57
+ - [React API](#react-api)
58
+ - [Persist API](#persist-api)
59
+ - [Sync API](#sync-api)
60
+ - [Types](#types)
61
+ - [Architecture Deep Dive](#architecture-deep-dive)
62
+ - [State Update Flow](#state-update-flow)
63
+ - [Selector Re-render Optimization](#selector-re-render-optimization)
64
+ - [Global Registry Internals](#global-registry-internals)
65
+ - [Persistence Flow](#persistence-flow)
66
+ - [Sync Flow](#sync-flow)
67
+ - [Gotchas and Common Pitfalls](#gotchas-and-common-pitfalls)
68
+ - [FAQ](#faq)
69
+ - [Contributing](#contributing)
70
+ - [Support](#support)
71
+ - [License](#license)
72
+
73
+ ---
74
+
75
+ ## Install
76
+
77
+ ```bash
78
+ # npm
79
+ npm install shared-state-bridge
80
+
81
+ # yarn
82
+ yarn add shared-state-bridge
83
+
84
+ # pnpm
85
+ pnpm add shared-state-bridge
86
+ ```
87
+
88
+ > **Note:** React is an optional peer dependency. If you only use the core API (no hooks), React is not required.
89
+
90
+ ---
91
+
92
+ ## Quick Start
93
+
94
+ ```typescript
95
+ import { createBridge } from 'shared-state-bridge'
96
+
97
+ const bridge = createBridge({
98
+ name: 'app',
99
+ initialState: { count: 0, theme: 'light' },
100
+ })
101
+
102
+ // Read state
103
+ bridge.getState() // { count: 0, theme: 'light' }
104
+
105
+ // Update state (partial merge)
106
+ bridge.setState({ count: 1 })
107
+
108
+ // Update with updater function
109
+ bridge.setState(prev => ({ count: prev.count + 1 }))
110
+
111
+ // Subscribe to changes
112
+ const unsub = bridge.subscribe((state, prev) => {
113
+ console.log('Changed:', state)
114
+ })
115
+
116
+ // Unsubscribe when done
117
+ unsub()
118
+ ```
119
+
120
+ ### With React
121
+
122
+ ```tsx
123
+ import { BridgeProvider, useBridgeState, useBridge } from 'shared-state-bridge/react'
124
+
125
+ function App() {
126
+ return (
127
+ <BridgeProvider bridge={bridge}>
128
+ <Counter />
129
+ </BridgeProvider>
130
+ )
131
+ }
132
+
133
+ function Counter() {
134
+ const count = useBridgeState<AppState, number>(s => s.count)
135
+ const bridge = useBridge<AppState>()
136
+
137
+ return (
138
+ <button onClick={() => bridge.setState(s => ({ count: s.count + 1 }))}>
139
+ Count: {count}
140
+ </button>
141
+ )
142
+ }
143
+ ```
144
+
145
+ ### With Persistence
146
+
147
+ ```typescript
148
+ import { createBridge } from 'shared-state-bridge'
149
+ import { persist, localStorageAdapter } from 'shared-state-bridge/persist'
150
+
151
+ const bridge = createBridge({
152
+ name: 'app',
153
+ initialState: { theme: 'light', count: 0 },
154
+ plugins: [
155
+ persist({
156
+ adapter: localStorageAdapter,
157
+ key: 'app-state',
158
+ pick: ['theme'], // only persist theme
159
+ }),
160
+ ],
161
+ })
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Core Concepts
167
+
168
+ ### Creating a Bridge
169
+
170
+ Every bridge has a unique **name** that registers it in a global registry. This name is how other packages in your monorepo can access the same bridge instance.
171
+
172
+ ```typescript
173
+ import { createBridge } from 'shared-state-bridge'
174
+
175
+ interface AppState {
176
+ user: { name: string; email: string } | null
177
+ theme: 'light' | 'dark'
178
+ notifications: number
179
+ }
180
+
181
+ const appBridge = createBridge<AppState>({
182
+ name: 'app', // unique identifier
183
+ initialState: { // required initial state
184
+ user: null,
185
+ theme: 'light',
186
+ notifications: 0,
187
+ },
188
+ plugins: [], // optional plugins array
189
+ })
190
+ ```
191
+
192
+ **Key rules:**
193
+ - The `name` must be unique across your entire application. Attempting to create two bridges with the same name throws an error.
194
+ - `initialState` must be a plain object (not an array, not a primitive).
195
+ - The bridge is immediately registered in the global registry upon creation.
196
+
197
+ ### Reading and Updating State
198
+
199
+ #### `getState()`
200
+
201
+ Returns the current state snapshot. This is a synchronous read.
202
+
203
+ ```typescript
204
+ const state = appBridge.getState()
205
+ console.log(state.theme) // 'light'
206
+ ```
207
+
208
+ #### `setState(partial)` — Partial Merge
209
+
210
+ By default, `setState` performs a **shallow merge** with the current state (like React's `useState` with objects). Only the keys you provide are updated; other keys are preserved.
211
+
212
+ ```typescript
213
+ appBridge.setState({ theme: 'dark' })
214
+ // State is now: { user: null, theme: 'dark', notifications: 0 }
215
+ // ^^^^^ changed
216
+ // rest preserved
217
+ ```
218
+
219
+ #### `setState(updater)` — Updater Function
220
+
221
+ Pass a function to compute the next state based on the current state. Useful when the new value depends on the old one.
222
+
223
+ ```typescript
224
+ appBridge.setState(prev => ({
225
+ notifications: prev.notifications + 1,
226
+ }))
227
+ ```
228
+
229
+ #### `setState(state, true)` — Full Replacement
230
+
231
+ Pass `true` as the second argument to **replace** the entire state instead of merging.
232
+
233
+ ```typescript
234
+ appBridge.setState(
235
+ { user: null, theme: 'light', notifications: 0 },
236
+ true // replaces entirely — no merge
237
+ )
238
+ ```
239
+
240
+ > **Important:** After replacement, any keys not included in the new state are gone. Use this carefully.
241
+
242
+ #### `getInitialState()`
243
+
244
+ Returns the original initial state that was passed to `createBridge`. This never changes, even after `setState` calls.
245
+
246
+ ```typescript
247
+ appBridge.setState({ notifications: 99 })
248
+ appBridge.getInitialState() // { user: null, theme: 'light', notifications: 0 }
249
+ appBridge.getState() // { user: null, theme: 'light', notifications: 99 }
250
+ ```
251
+
252
+ ### Subscribing to Changes
253
+
254
+ #### Full State Subscription
255
+
256
+ Listen to every state change:
257
+
258
+ ```typescript
259
+ const unsub = appBridge.subscribe((state, previousState) => {
260
+ console.log('State changed:', state)
261
+ console.log('Previous:', previousState)
262
+ })
263
+
264
+ // Later: stop listening
265
+ unsub()
266
+ ```
267
+
268
+ **Behavior:**
269
+ - The listener is called **synchronously** after each `setState` that produces a new state reference.
270
+ - If `setState` is called but the state reference doesn't change (e.g., replacing with the same object via `setState(sameRef, true)`), listeners are **not** called.
271
+ - Multiple listeners fire in the order they were subscribed.
272
+
273
+ #### Selector-Based Subscription
274
+
275
+ Listen only when a specific slice of state changes:
276
+
277
+ ```typescript
278
+ const unsub = appBridge.subscribe(
279
+ state => state.theme, // selector
280
+ (theme, previousTheme) => { // only called when theme changes
281
+ console.log(`Theme: ${previousTheme} -> ${theme}`)
282
+ }
283
+ )
284
+
285
+ appBridge.setState({ notifications: 5 }) // listener NOT called (theme unchanged)
286
+ appBridge.setState({ theme: 'dark' }) // listener called: 'light' -> 'dark'
287
+ ```
288
+
289
+ #### Subscription Options
290
+
291
+ ```typescript
292
+ appBridge.subscribe(
293
+ state => state.notifications,
294
+ (count, prevCount) => updateBadge(count),
295
+ {
296
+ // Fire the listener immediately with the current value
297
+ fireImmediately: true,
298
+
299
+ // Custom equality function (default: Object.is)
300
+ equalityFn: (a, b) => Math.abs(a - b) < 5,
301
+ }
302
+ )
303
+ ```
304
+
305
+ | Option | Type | Default | Description |
306
+ |---|---|---|---|
307
+ | `fireImmediately` | `boolean` | `false` | Call the listener once immediately with the current value |
308
+ | `equalityFn` | `(a, b) => boolean` | `Object.is` | Custom comparison to determine if the slice changed |
309
+
310
+ ### Destroying a Bridge
311
+
312
+ ```typescript
313
+ appBridge.destroy()
314
+ ```
315
+
316
+ **What `destroy()` does:**
317
+ 1. Removes the bridge from the global registry (it can no longer be found via `getBridge`)
318
+ 2. Calls `onDestroy` on all plugins
319
+ 3. Clears all listeners (no more notifications)
320
+ 4. Marks the bridge as destroyed — subsequent `setState`/`subscribe` calls are silent no-ops
321
+
322
+ **When to use it:**
323
+ - In tests, to clean up between test cases
324
+ - When unmounting a micro-frontend
325
+ - Before re-creating a bridge with the same name
326
+
327
+ ```typescript
328
+ // Re-creating a bridge after destroy
329
+ appBridge.destroy()
330
+ const newBridge = createBridge({ name: 'app', initialState: { ... } }) // OK
331
+ ```
332
+
333
+ ---
334
+
335
+ ## Cross-Package Sharing
336
+
337
+ This is the **key feature** of `shared-state-bridge`. In a monorepo, all packages are bundled into the same application and share the same JavaScript runtime. Bridges leverage this by storing instances in a global registry that any package can access.
338
+
339
+ ### Package A — Creates the Bridge
340
+
341
+ ```typescript
342
+ // packages/shared/src/bridges.ts
343
+ import { createBridge } from 'shared-state-bridge'
344
+
345
+ export interface AuthState {
346
+ user: { id: string; name: string } | null
347
+ token: string | null
348
+ }
349
+
350
+ export const authBridge = createBridge<AuthState>({
351
+ name: 'auth',
352
+ initialState: { user: null, token: null },
353
+ })
354
+ ```
355
+
356
+ ### Package B — Accesses the Bridge by Name
357
+
358
+ ```typescript
359
+ // packages/dashboard/src/auth.ts
360
+ import { getBridge } from 'shared-state-bridge'
361
+ import type { AuthState } from '@myorg/shared'
362
+
363
+ // Returns the EXACT same instance created in Package A
364
+ const authBridge = getBridge<AuthState>('auth')
365
+
366
+ authBridge.subscribe(
367
+ state => state.user,
368
+ (user) => {
369
+ if (!user) redirectToLogin()
370
+ }
371
+ )
372
+ ```
373
+
374
+ ### Registry Utilities
375
+
376
+ ```typescript
377
+ import { getBridge, hasBridge, listBridges } from 'shared-state-bridge'
378
+
379
+ // Check existence without throwing
380
+ if (hasBridge('auth')) {
381
+ const auth = getBridge('auth')
382
+ }
383
+
384
+ // List all registered bridges (useful for debugging)
385
+ console.log(listBridges()) // ['auth', 'app', 'ui']
386
+ ```
387
+
388
+ ### How It Works (Architecture)
389
+
390
+ ```
391
+ ┌─────────────────────────────────────────────────────────────┐
392
+ │ JavaScript Runtime │
393
+ │ │
394
+ │ globalThis[Symbol.for('shared-state-bridge.registry')] │
395
+ │ ┌───────────────────────────────────────────────────┐ │
396
+ │ │ Map<string, BridgeApi> │ │
397
+ │ │ │ │
398
+ │ │ 'auth' -> BridgeApi { state, listeners, ... } │ │
399
+ │ │ 'app' -> BridgeApi { state, listeners, ... } │ │
400
+ │ │ 'ui' -> BridgeApi { state, listeners, ... } │ │
401
+ │ └───────────────────────────────────────────────────┘ │
402
+ │ ^ ^ │
403
+ │ | | │
404
+ │ ┌──────────────┴──┐ ┌──────────┴───────────┐ │
405
+ │ │ Package A │ │ Package B │ │
406
+ │ │ │ │ │ │
407
+ │ │ createBridge() │ │ getBridge('auth') │ │
408
+ │ │ registers here │ │ reads from here │ │
409
+ │ └─────────────────┘ └──────────────────────┘ │
410
+ └─────────────────────────────────────────────────────────────┘
411
+ ```
412
+
413
+ The registry uses `Symbol.for('shared-state-bridge.registry')` as the key on `globalThis`. Why `Symbol.for()` instead of a plain string?
414
+
415
+ - `Symbol.for('x')` returns the **same symbol** across all modules, all files, and even across multiple copies of the package
416
+ - Even if your monorepo accidentally has two versions of `shared-state-bridge` in `node_modules` (a common pitfall), both versions use the same `Symbol.for()` key and access the same `Map`
417
+ - A plain string key like `globalThis.__shared_state_bridge__` could collide with other libraries; symbols cannot
418
+
419
+ ---
420
+
421
+ ## React Integration
422
+
423
+ The React bindings are imported separately from `shared-state-bridge/react` and have React 18+ as an optional peer dependency.
424
+
425
+ ### BridgeProvider
426
+
427
+ Provides a bridge instance to the React component tree via context.
428
+
429
+ ```tsx
430
+ import { createBridge } from 'shared-state-bridge'
431
+ import { BridgeProvider } from 'shared-state-bridge/react'
432
+
433
+ const bridge = createBridge({
434
+ name: 'app',
435
+ initialState: { count: 0, theme: 'light' },
436
+ })
437
+
438
+ function App() {
439
+ return (
440
+ <BridgeProvider bridge={bridge}>
441
+ <YourApp />
442
+ </BridgeProvider>
443
+ )
444
+ }
445
+ ```
446
+
447
+ **Props:**
448
+
449
+ | Prop | Type | Description |
450
+ |---|---|---|
451
+ | `bridge` | `BridgeApi<T>` | The bridge instance to provide |
452
+ | `children` | `React.ReactNode` | Child components |
453
+
454
+ > **Note:** `BridgeProvider` is optional. You can pass bridges directly to hooks if you prefer.
455
+
456
+ ### useBridgeState
457
+
458
+ The primary hook for reading bridge state in React components. Uses `useSyncExternalStore` internally for tear-free, concurrent-safe reads.
459
+
460
+ #### Signature 1: From Context
461
+
462
+ ```tsx
463
+ function useBridgeState<T extends State, U>(
464
+ selector: (state: T) => U,
465
+ options?: { shallow?: boolean }
466
+ ): U
467
+ ```
468
+
469
+ Requires a `BridgeProvider` ancestor:
470
+
471
+ ```tsx
472
+ function ThemeToggle() {
473
+ const theme = useBridgeState<AppState, string>(s => s.theme)
474
+ // Only re-renders when theme changes
475
+ }
476
+ ```
477
+
478
+ #### Signature 2: Direct Bridge Reference
479
+
480
+ ```tsx
481
+ function useBridgeState<T extends State, U>(
482
+ bridge: BridgeApi<T>,
483
+ selector: (state: T) => U,
484
+ options?: { shallow?: boolean }
485
+ ): U
486
+ ```
487
+
488
+ No provider needed:
489
+
490
+ ```tsx
491
+ function Counter() {
492
+ const count = useBridgeState(bridge, s => s.count)
493
+ // Only re-renders when count changes
494
+ }
495
+ ```
496
+
497
+ #### Signature 3: Full State (No Selector)
498
+
499
+ ```tsx
500
+ function useBridgeState<T extends State>(
501
+ bridge: BridgeApi<T>
502
+ ): T
503
+ ```
504
+
505
+ Returns the entire state object:
506
+
507
+ ```tsx
508
+ function Debug() {
509
+ const state = useBridgeState(bridge)
510
+ // Re-renders on EVERY state change
511
+ return <pre>{JSON.stringify(state, null, 2)}</pre>
512
+ }
513
+ ```
514
+
515
+ ### useBridge
516
+
517
+ Returns the bridge instance from the nearest `BridgeProvider`. Use this for imperative operations like `setState`.
518
+
519
+ ```tsx
520
+ import { useBridge } from 'shared-state-bridge/react'
521
+
522
+ function LogoutButton() {
523
+ const bridge = useBridge<AuthState>()
524
+
525
+ return (
526
+ <button onClick={() => bridge.setState({ user: null, token: null })}>
527
+ Log Out
528
+ </button>
529
+ )
530
+ }
531
+ ```
532
+
533
+ Throws if used outside a `BridgeProvider`.
534
+
535
+ ### Preventing Unnecessary Re-renders
536
+
537
+ #### Primitive Selectors (No Problem)
538
+
539
+ Selectors that return primitives (strings, numbers, booleans) work perfectly with the default `Object.is` comparison:
540
+
541
+ ```tsx
542
+ const count = useBridgeState(bridge, s => s.count) // number
543
+ const theme = useBridgeState(bridge, s => s.theme) // string
544
+ // These only re-render when the VALUE actually changes
545
+ ```
546
+
547
+ #### Object Selectors (Use `{ shallow: true }`)
548
+
549
+ Selectors that return **new objects** create a new reference on every call, causing unnecessary re-renders:
550
+
551
+ ```tsx
552
+ // BAD: Creates a new object every render -> re-renders on EVERY state change
553
+ const user = useBridgeState(bridge, s => ({
554
+ name: s.user?.name,
555
+ email: s.user?.email,
556
+ }))
557
+
558
+ // GOOD: Shallow comparison prevents re-render when values haven't changed
559
+ const user = useBridgeState(
560
+ bridge,
561
+ s => ({ name: s.user?.name, email: s.user?.email }),
562
+ { shallow: true }
563
+ )
564
+ ```
565
+
566
+ **How `{ shallow: true }` works internally:**
567
+
568
+ 1. The hook computes the new selector result
569
+ 2. It compares each key/value with the previous result using `Object.is`
570
+ 3. If all keys and values match, it returns the **previous object reference**
571
+ 4. React's `useSyncExternalStore` sees the same reference and skips re-rendering
572
+
573
+ #### Alternative: Select Primitives Individually
574
+
575
+ If you only need a few values, separate selectors can be simpler:
576
+
577
+ ```tsx
578
+ // Each hook only re-renders when its specific value changes
579
+ const name = useBridgeState(bridge, s => s.user?.name)
580
+ const email = useBridgeState(bridge, s => s.user?.email)
581
+ ```
582
+
583
+ ### Server-Side Rendering (Next.js)
584
+
585
+ `useBridgeState` is SSR-safe. During server rendering, it uses `getInitialState()` as the server snapshot, preventing hydration mismatches.
586
+
587
+ ```tsx
588
+ // This works in Next.js App Router and Pages Router
589
+ function ThemeSwitcher() {
590
+ const theme = useBridgeState(bridge, s => s.theme)
591
+ // Server: returns initial state ('light')
592
+ // Client: returns current state (may differ after hydration)
593
+ }
594
+ ```
595
+
596
+ **Caveat:** On the server in development mode, `globalThis` persists across requests due to hot module reload. For production this is not an issue since each request gets a fresh runtime. If you encounter stale state in dev, ensure bridges are created in module scope or call `destroy()` in cleanup.
597
+
598
+ ---
599
+
600
+ ## Persistence
601
+
602
+ Persistence is opt-in via the `persist` plugin from `shared-state-bridge/persist`. State is serialized and written to a storage adapter on every change (throttled). On initialization, persisted state is hydrated back into the bridge.
603
+
604
+ ### Web — localStorage
605
+
606
+ ```typescript
607
+ import { createBridge } from 'shared-state-bridge'
608
+ import { persist, localStorageAdapter } from 'shared-state-bridge/persist'
609
+
610
+ const bridge = createBridge({
611
+ name: 'app',
612
+ initialState: { theme: 'light', count: 0 },
613
+ plugins: [
614
+ persist({
615
+ adapter: localStorageAdapter,
616
+ key: 'app-state',
617
+ pick: ['theme'], // only persist theme, not count
618
+ }),
619
+ ],
620
+ })
621
+ ```
622
+
623
+ The `localStorageAdapter` is safe to use in SSR environments — it silently returns `null` when `localStorage` is unavailable.
624
+
625
+ ### React Native — AsyncStorage
626
+
627
+ The `asyncStorageAdapter` is a **factory function** that accepts an AsyncStorage instance. This avoids a hard dependency on `@react-native-async-storage/async-storage`.
628
+
629
+ ```typescript
630
+ import AsyncStorage from '@react-native-async-storage/async-storage'
631
+ import { createBridge } from 'shared-state-bridge'
632
+ import { persist, asyncStorageAdapter } from 'shared-state-bridge/persist'
633
+
634
+ const bridge = createBridge({
635
+ name: 'app',
636
+ initialState: { theme: 'light' },
637
+ plugins: [
638
+ persist({
639
+ adapter: asyncStorageAdapter(AsyncStorage),
640
+ key: 'app-state',
641
+ }),
642
+ ],
643
+ })
644
+ ```
645
+
646
+ ### Custom Adapter
647
+
648
+ Implement the `PersistAdapter` interface with 3 methods. Each method can return a value synchronously or a `Promise`:
649
+
650
+ ```typescript
651
+ import type { PersistAdapter } from 'shared-state-bridge'
652
+
653
+ const customAdapter: PersistAdapter = {
654
+ getItem: (key: string) => string | null | Promise<string | null>
655
+ setItem: (key: string, value: string) => void | Promise<void>
656
+ removeItem: (key: string) => void | Promise<void>
657
+ }
658
+ ```
659
+
660
+ **Example — IndexedDB adapter:**
661
+
662
+ ```typescript
663
+ const idbAdapter: PersistAdapter = {
664
+ getItem: async (key) => {
665
+ const db = await openDB()
666
+ return db.get('state-store', key)
667
+ },
668
+ setItem: async (key, value) => {
669
+ const db = await openDB()
670
+ await db.put('state-store', value, key)
671
+ },
672
+ removeItem: async (key) => {
673
+ const db = await openDB()
674
+ await db.delete('state-store', key)
675
+ },
676
+ }
677
+ ```
678
+
679
+ ### Persistence Options Reference
680
+
681
+ ```typescript
682
+ persist({
683
+ // REQUIRED
684
+ adapter: PersistAdapter, // Storage backend
685
+ key: string, // Storage key name
686
+
687
+ // FILTERING (pick one or neither)
688
+ pick?: (keyof T)[], // Only persist these keys
689
+ omit?: (keyof T)[], // Exclude these keys
690
+
691
+ // PERFORMANCE
692
+ throttleMs?: number, // Throttle writes (default: 100ms)
693
+
694
+ // SERIALIZATION
695
+ serialize?: (state) => string, // Custom serializer (default: JSON.stringify)
696
+ deserialize?: (raw) => unknown, // Custom deserializer (default: JSON.parse)
697
+
698
+ // VERSIONING
699
+ version?: number, // Schema version (default: 0)
700
+ migrate?: (persisted, oldVersion) => Partial<T>, // Migration function
701
+ })
702
+ ```
703
+
704
+ | Option | Type | Default | Description |
705
+ |---|---|---|---|
706
+ | `adapter` | `PersistAdapter` | *required* | The storage backend to use |
707
+ | `key` | `string` | *required* | Key under which state is stored |
708
+ | `pick` | `(keyof T)[]` | `undefined` | Whitelist: only persist these state keys |
709
+ | `omit` | `(keyof T)[]` | `undefined` | Blacklist: exclude these state keys from persistence |
710
+ | `throttleMs` | `number` | `100` | Minimum interval between writes (ms). Prevents excessive writes during rapid state changes. Trailing writes are guaranteed. |
711
+ | `serialize` | `(state) => string` | `JSON.stringify` | Custom serialization function |
712
+ | `deserialize` | `(raw) => unknown` | `JSON.parse` | Custom deserialization function |
713
+ | `version` | `number` | `0` | Schema version number, stored alongside persisted state |
714
+ | `migrate` | `(persisted, oldVersion) => Partial<T>` | `undefined` | Called when persisted version differs from current version |
715
+
716
+ ### Schema Migrations
717
+
718
+ When your state shape changes between app versions, use `version` and `migrate` to handle the transition:
719
+
720
+ ```typescript
721
+ // Version 1 state: { theme: 'light' }
722
+ // Version 2 state: { theme: 'light', locale: 'en' }
723
+
724
+ const bridge = createBridge({
725
+ name: 'app',
726
+ initialState: { theme: 'light', locale: 'en' },
727
+ plugins: [
728
+ persist({
729
+ adapter: localStorageAdapter,
730
+ key: 'app-state',
731
+ version: 2,
732
+ migrate: (persisted, oldVersion) => {
733
+ if (oldVersion === 1) {
734
+ // Add the new 'locale' field with a default value
735
+ return { ...(persisted as object), locale: 'en' } as Partial<AppState>
736
+ }
737
+ return persisted as Partial<AppState>
738
+ },
739
+ }),
740
+ ],
741
+ })
742
+ ```
743
+
744
+ **Migration behavior:**
745
+ - If persisted version matches current version: hydrate normally
746
+ - If versions differ AND `migrate` is provided: call `migrate(persisted, oldVersion)` and use the result
747
+ - If versions differ AND `migrate` is NOT provided: discard persisted data entirely and start fresh
748
+
749
+ ### Storage Envelope Format
750
+
751
+ The persist plugin stores data in this format:
752
+
753
+ ```json
754
+ {
755
+ "state": { "theme": "dark", "locale": "en" },
756
+ "version": 2
757
+ }
758
+ ```
759
+
760
+ ### Testing with memoryAdapter
761
+
762
+ The `memoryAdapter()` factory creates an in-memory storage backend. Perfect for tests and SSR:
763
+
764
+ ```typescript
765
+ import { memoryAdapter } from 'shared-state-bridge/persist'
766
+
767
+ // Each call creates a fresh, isolated store
768
+ const adapter = memoryAdapter()
769
+
770
+ const bridge = createBridge({
771
+ name: 'test',
772
+ initialState: { count: 0 },
773
+ plugins: [persist({ adapter, key: 'test' })],
774
+ })
775
+ ```
776
+
777
+ ---
778
+
779
+ ## Real-Time Sync (WebSocket)
780
+
781
+ The `sync` plugin enables real-time state synchronization between **different apps** over WebSocket. Connect your Next.js web app, React Native mobile app, and any other client to the same channel — state changes propagate instantly.
782
+
783
+ > **Important:** This is a client-side plugin only. You provide your own WebSocket server URL. A minimal example server is included below.
784
+
785
+ ### Basic Usage
786
+
787
+ ```typescript
788
+ import { createBridge } from 'shared-state-bridge'
789
+ import { sync } from 'shared-state-bridge/sync'
790
+
791
+ const bridge = createBridge({
792
+ name: 'app',
793
+ initialState: { theme: 'light', count: 0, localDraft: '' },
794
+ plugins: [
795
+ sync({
796
+ url: 'wss://your-server.com/sync',
797
+ channel: 'room-123',
798
+ }),
799
+ ],
800
+ })
801
+
802
+ // State changes are now synced across all connected clients
803
+ bridge.setState({ theme: 'dark' }) // -> sent to all other clients in room-123
804
+ ```
805
+
806
+ ### How It Works
807
+
808
+ ```
809
+ App A (Next.js) WebSocket Server App B (React Native)
810
+ ───────────────── ────────────────── ─────────────────────
811
+ bridge.setState()
812
+
813
+ ├─ onStateChange()
814
+ │ ├─ isApplyingRemote? skip (echo guard)
815
+ │ └─ filterState() ──► send({ type: "state" }) ──► broadcast to channel
816
+ │ │ │
817
+ │ │ ◄──────┘
818
+ │ │ onMessage()
819
+ │ │ ├─ same clientId? skip
820
+ │ │ ├─ resolve(local, remote)
821
+ │ │ └─ bridge.setState(merged)
822
+ │ │ └─ isApplyingRemote = true
823
+ │ │ (prevents re-broadcast)
824
+ ```
825
+
826
+ Each client gets a unique `clientId`. Messages are tagged with this ID so clients ignore their own echoes. An `isApplyingRemote` flag prevents re-broadcasting state updates received from the server.
827
+
828
+ ### Selective Sync (pick / omit)
829
+
830
+ Same as persistence — only sync the keys you need:
831
+
832
+ ```typescript
833
+ sync({
834
+ url: 'wss://your-server.com/sync',
835
+ channel: 'room-1',
836
+ pick: ['theme', 'count'], // only sync these keys
837
+ })
838
+
839
+ // OR
840
+
841
+ sync({
842
+ url: 'wss://your-server.com/sync',
843
+ channel: 'room-1',
844
+ omit: ['localDraft'], // sync everything except these
845
+ })
846
+ ```
847
+
848
+ ### Custom Conflict Resolution
849
+
850
+ By default, incoming remote state is merged directly (last-write-wins). You can provide a custom `resolve` function:
851
+
852
+ ```typescript
853
+ sync({
854
+ url: 'wss://your-server.com/sync',
855
+ channel: 'room-1',
856
+ resolve: (localState, remoteState) => {
857
+ // Custom logic: take the higher count, but always accept remote theme
858
+ return {
859
+ count: Math.max(localState.count, remoteState.count ?? 0),
860
+ theme: remoteState.theme ?? localState.theme,
861
+ }
862
+ },
863
+ })
864
+ ```
865
+
866
+ The `resolve` function receives the full local state and the incoming remote partial state, and should return the partial state to apply.
867
+
868
+ ### Reconnection
869
+
870
+ Auto-reconnect is enabled by default with exponential backoff:
871
+
872
+ ```typescript
873
+ sync({
874
+ url: 'wss://your-server.com/sync',
875
+ channel: 'room-1',
876
+ reconnect: true, // default: true
877
+ reconnectInterval: 1000, // base interval in ms (default: 1000)
878
+ maxReconnectInterval: 30000, // cap in ms (default: 30000)
879
+ maxReconnectAttempts: Infinity, // default: Infinity
880
+ })
881
+ ```
882
+
883
+ The backoff sequence is: 1s, 2s, 4s, 8s, 16s, 30s, 30s, 30s... The counter resets on every successful connection. Messages sent while disconnected are buffered and flushed on reconnect.
884
+
885
+ ### Callbacks
886
+
887
+ ```typescript
888
+ sync({
889
+ url: 'wss://your-server.com/sync',
890
+ channel: 'room-1',
891
+ onConnect: () => console.log('Connected to sync server'),
892
+ onDisconnect: () => console.log('Disconnected from sync server'),
893
+ onError: (error) => console.error('Sync error:', error),
894
+ })
895
+ ```
896
+
897
+ ### Sync Options Reference
898
+
899
+ | Option | Type | Default | Description |
900
+ |---|---|---|---|
901
+ | `url` | `string` | *required* | WebSocket server URL (`wss://...`) |
902
+ | `channel` | `string` | *required* | Channel/room name to join |
903
+ | `pick` | `(keyof T)[]` | — | Only sync these keys |
904
+ | `omit` | `(keyof T)[]` | — | Exclude these keys from sync |
905
+ | `throttleMs` | `number` | `50` | Throttle outbound messages (ms) |
906
+ | `reconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
907
+ | `reconnectInterval` | `number` | `1000` | Base reconnect interval (ms) |
908
+ | `maxReconnectInterval` | `number` | `30000` | Max reconnect interval cap (ms) |
909
+ | `maxReconnectAttempts` | `number` | `Infinity` | Max reconnect attempts |
910
+ | `onConnect` | `() => void` | — | Called on successful connection |
911
+ | `onDisconnect` | `() => void` | — | Called on disconnection |
912
+ | `onError` | `(error) => void` | — | Called on WebSocket error |
913
+ | `resolve` | `(local, remote) => Partial<T>` | — | Custom conflict resolver |
914
+
915
+ ### Wire Protocol
916
+
917
+ All messages are JSON over WebSocket. The protocol is simple and easy to implement on any server.
918
+
919
+ **Client -> Server:**
920
+
921
+ ```typescript
922
+ // Join a channel
923
+ { type: "join", channel: "room-1", clientId: "abc123" }
924
+
925
+ // Send state update
926
+ { type: "state", channel: "room-1", clientId: "abc123", state: { count: 5 }, timestamp: 1700000000000 }
927
+ ```
928
+
929
+ **Server -> Client:**
930
+
931
+ ```typescript
932
+ // Relay state from another client
933
+ { type: "state", channel: "room-1", clientId: "other456", state: { count: 5 }, timestamp: 1700000000000 }
934
+
935
+ // Send full state (e.g., on initial join for late-joiners)
936
+ { type: "full_state", channel: "room-1", state: { count: 5, theme: "dark" }, timestamp: 1700000000000 }
937
+ ```
938
+
939
+ ### Example WebSocket Server (Node.js)
940
+
941
+ A minimal relay server that broadcasts state to all clients in a channel:
942
+
943
+ ```javascript
944
+ import { WebSocketServer } from 'ws'
945
+
946
+ const wss = new WebSocketServer({ port: 8080 })
947
+
948
+ // channel -> Set<WebSocket>
949
+ const channels = new Map()
950
+
951
+ wss.on('connection', (ws) => {
952
+ let clientChannel = null
953
+
954
+ ws.on('message', (raw) => {
955
+ const msg = JSON.parse(raw)
956
+
957
+ if (msg.type === 'join') {
958
+ clientChannel = msg.channel
959
+ if (!channels.has(clientChannel)) {
960
+ channels.set(clientChannel, new Set())
961
+ }
962
+ channels.get(clientChannel).add(ws)
963
+ return
964
+ }
965
+
966
+ if (msg.type === 'state' && clientChannel) {
967
+ // Broadcast to all OTHER clients in the same channel
968
+ for (const client of channels.get(clientChannel) || []) {
969
+ if (client !== ws && client.readyState === 1) {
970
+ client.send(JSON.stringify(msg))
971
+ }
972
+ }
973
+ }
974
+ })
975
+
976
+ ws.on('close', () => {
977
+ if (clientChannel && channels.has(clientChannel)) {
978
+ channels.get(clientChannel).delete(ws)
979
+ if (channels.get(clientChannel).size === 0) {
980
+ channels.delete(clientChannel)
981
+ }
982
+ }
983
+ })
984
+ })
985
+
986
+ console.log('Sync server running on ws://localhost:8080')
987
+ ```
988
+
989
+ > **Production tip:** For production, consider using a library like `ws` with `uWebSockets.js` for better performance, adding authentication (verify tokens in the `connection` event), and using Redis pub/sub if you need to scale across multiple server instances.
990
+
991
+ ---
992
+
993
+ ## Plugins
994
+
995
+ Bridges support a plugin system via lifecycle hooks. The `persist` and `sync` plugins are the built-in examples, but you can write your own.
996
+
997
+ ### Plugin Lifecycle
998
+
999
+ ```
1000
+ createBridge() called
1001
+
1002
+ ├── 1. State initialized from initialState
1003
+ ├── 2. Bridge registered in global registry
1004
+ ├── 3. plugin.onInit(bridgeApi) <-- Bridge is fully constructed
1005
+
1006
+
1007
+ bridge.setState() called
1008
+
1009
+ ├── 4. State updated (merge or replace)
1010
+ ├── 5. Listeners notified
1011
+ ├── 6. plugin.onStateChange(state, prev) <-- After listeners
1012
+
1013
+
1014
+ bridge.destroy() called
1015
+
1016
+ ├── 7. Bridge removed from registry
1017
+ ├── 8. plugin.onDestroy() <-- Cleanup
1018
+ └── 9. All listeners cleared
1019
+ ```
1020
+
1021
+ ### Plugin Interface
1022
+
1023
+ ```typescript
1024
+ interface BridgePlugin<T extends State> {
1025
+ /** Unique plugin name (for debugging) */
1026
+ name: string
1027
+
1028
+ /** Called once after the bridge is fully initialized and registered */
1029
+ onInit?: (bridge: BridgeApi<T>) => void
1030
+
1031
+ /** Called after every state change, after listeners are notified */
1032
+ onStateChange?: (state: T, previousState: T) => void
1033
+
1034
+ /** Called when bridge.destroy() is invoked, before listeners are cleared */
1035
+ onDestroy?: () => void
1036
+ }
1037
+ ```
1038
+
1039
+ ### Writing a Custom Plugin
1040
+
1041
+ **Example — Logger Plugin:**
1042
+
1043
+ ```typescript
1044
+ import type { BridgePlugin } from 'shared-state-bridge'
1045
+
1046
+ function createLoggerPlugin<T extends State>(options?: {
1047
+ collapsed?: boolean
1048
+ }): BridgePlugin<T> {
1049
+ return {
1050
+ name: 'logger',
1051
+ onInit: (bridge) => {
1052
+ console.log(`[logger] Bridge "${bridge.getName()}" initialized with:`, bridge.getState())
1053
+ },
1054
+ onStateChange: (state, previousState) => {
1055
+ const method = options?.collapsed ? console.groupCollapsed : console.group
1056
+ method('[logger] State change')
1057
+ console.log('Previous:', previousState)
1058
+ console.log('Current:', state)
1059
+ console.groupEnd()
1060
+ },
1061
+ onDestroy: () => {
1062
+ console.log('[logger] Bridge destroyed')
1063
+ },
1064
+ }
1065
+ }
1066
+
1067
+ // Usage
1068
+ const bridge = createBridge({
1069
+ name: 'app',
1070
+ initialState: { count: 0 },
1071
+ plugins: [createLoggerPlugin({ collapsed: true })],
1072
+ })
1073
+ ```
1074
+
1075
+ **Example — Validation Plugin:**
1076
+
1077
+ ```typescript
1078
+ function createValidationPlugin<T extends State>(
1079
+ validate: (state: T) => boolean,
1080
+ errorMessage?: string
1081
+ ): BridgePlugin<T> {
1082
+ return {
1083
+ name: 'validation',
1084
+ onStateChange: (state) => {
1085
+ if (!validate(state)) {
1086
+ console.error(errorMessage ?? '[validation] Invalid state:', state)
1087
+ }
1088
+ },
1089
+ }
1090
+ }
1091
+
1092
+ // Usage
1093
+ const bridge = createBridge({
1094
+ name: 'counter',
1095
+ initialState: { count: 0 },
1096
+ plugins: [
1097
+ createValidationPlugin(
1098
+ (s) => s.count >= 0,
1099
+ 'Count cannot be negative!'
1100
+ ),
1101
+ ],
1102
+ })
1103
+ ```
1104
+
1105
+ **Combining multiple plugins:**
1106
+
1107
+ ```typescript
1108
+ const bridge = createBridge({
1109
+ name: 'app',
1110
+ initialState: { theme: 'light', count: 0 },
1111
+ plugins: [
1112
+ persist({ adapter: localStorageAdapter, key: 'app', pick: ['theme'] }),
1113
+ createLoggerPlugin(),
1114
+ createValidationPlugin((s) => typeof s.count === 'number'),
1115
+ ],
1116
+ })
1117
+ ```
1118
+
1119
+ Plugins execute in array order — `onInit` and `onStateChange` are called on plugin 1, then plugin 2, then plugin 3.
1120
+
1121
+ ---
1122
+
1123
+ ## TypeScript
1124
+
1125
+ Full type inference throughout the API:
1126
+
1127
+ ```typescript
1128
+ interface AppState {
1129
+ count: number
1130
+ theme: 'light' | 'dark'
1131
+ user: { name: string } | null
1132
+ }
1133
+
1134
+ // createBridge infers T from initialState (or use explicit generic)
1135
+ const bridge = createBridge<AppState>({
1136
+ name: 'app',
1137
+ initialState: { count: 0, theme: 'light', user: null },
1138
+ })
1139
+
1140
+ // setState is fully typed
1141
+ bridge.setState({ count: 1 }) // OK
1142
+ bridge.setState({ count: 'string' }) // Type error: string is not number
1143
+ bridge.setState({ unknown: true }) // Type error: unknown key
1144
+
1145
+ // Updater function receives correctly typed state
1146
+ bridge.setState(prev => ({
1147
+ count: prev.count + 1, // prev is AppState
1148
+ }))
1149
+
1150
+ // Selectors infer return type
1151
+ const count = useBridgeState(bridge, s => s.count)
1152
+ // ^? number
1153
+
1154
+ const theme = useBridgeState(bridge, s => s.theme)
1155
+ // ^? 'light' | 'dark'
1156
+
1157
+ // getBridge with type parameter
1158
+ const b = getBridge<AppState>('app')
1159
+ b.getState().theme // 'light' | 'dark'
1160
+ b.getState().unknown // Type error
1161
+
1162
+ // Subscribe selector is typed
1163
+ bridge.subscribe(
1164
+ s => s.user, // selector returns { name: string } | null
1165
+ (user, prevUser) => { // user and prevUser are { name: string } | null
1166
+ console.log(user?.name)
1167
+ }
1168
+ )
1169
+ ```
1170
+
1171
+ ### Type Exports
1172
+
1173
+ All types are exported for use in your own code:
1174
+
1175
+ ```typescript
1176
+ import type {
1177
+ State, // Record<string, unknown>
1178
+ BridgeApi, // Bridge instance type
1179
+ BridgeConfig, // createBridge config
1180
+ BridgePlugin, // Plugin interface
1181
+ PersistAdapter, // Storage adapter interface
1182
+ PersistOptions, // persist() config
1183
+ SetState, // setState signature
1184
+ Subscribe, // subscribe signature
1185
+ Listener, // Full-state listener type
1186
+ SelectorListener, // Selector listener type
1187
+ Selector, // Selector function type
1188
+ EqualityFn, // Equality function type
1189
+ } from 'shared-state-bridge'
1190
+ ```
1191
+
1192
+ ---
1193
+
1194
+ ## Monorepo Setup (Turborepo / Nx)
1195
+
1196
+ ### 1. Add as a Shared Dependency
1197
+
1198
+ Add `shared-state-bridge` to a shared/common package in your monorepo:
1199
+
1200
+ ```jsonc
1201
+ // packages/shared/package.json
1202
+ {
1203
+ "name": "@myorg/shared",
1204
+ "dependencies": {
1205
+ "shared-state-bridge": "^1.0.0"
1206
+ }
1207
+ }
1208
+ ```
1209
+
1210
+ ### 2. Define Bridges in the Shared Package
1211
+
1212
+ ```typescript
1213
+ // packages/shared/src/bridges.ts
1214
+ import { createBridge } from 'shared-state-bridge'
1215
+
1216
+ // --- Auth Bridge ---
1217
+ export interface AuthState {
1218
+ user: { id: string; name: string; email: string } | null
1219
+ token: string | null
1220
+ isLoading: boolean
1221
+ }
1222
+
1223
+ export const authBridge = createBridge<AuthState>({
1224
+ name: 'auth',
1225
+ initialState: { user: null, token: null, isLoading: false },
1226
+ })
1227
+
1228
+ // --- UI Bridge ---
1229
+ export interface UIState {
1230
+ sidebarOpen: boolean
1231
+ modal: string | null
1232
+ toasts: Array<{ id: string; message: string }>
1233
+ }
1234
+
1235
+ export const uiBridge = createBridge<UIState>({
1236
+ name: 'ui',
1237
+ initialState: { sidebarOpen: false, modal: null, toasts: [] },
1238
+ })
1239
+ ```
1240
+
1241
+ ### 3. Use from Any Package
1242
+
1243
+ **Web (Next.js):**
1244
+
1245
+ ```tsx
1246
+ // packages/web/src/components/Header.tsx
1247
+ import { useBridgeState } from 'shared-state-bridge/react'
1248
+ import { getBridge } from 'shared-state-bridge'
1249
+ import type { AuthState } from '@myorg/shared'
1250
+
1251
+ const auth = getBridge<AuthState>('auth')
1252
+
1253
+ function Header() {
1254
+ const user = useBridgeState(auth, s => s.user)
1255
+
1256
+ return (
1257
+ <header>
1258
+ <span>{user?.name ?? 'Guest'}</span>
1259
+ </header>
1260
+ )
1261
+ }
1262
+ ```
1263
+
1264
+ **Mobile (React Native):**
1265
+
1266
+ ```tsx
1267
+ // packages/mobile/src/screens/Profile.tsx
1268
+ import { useBridgeState } from 'shared-state-bridge/react'
1269
+ import { getBridge } from 'shared-state-bridge'
1270
+ import type { AuthState } from '@myorg/shared'
1271
+
1272
+ const auth = getBridge<AuthState>('auth')
1273
+
1274
+ function ProfileScreen() {
1275
+ const user = useBridgeState(auth, s => s.user)
1276
+
1277
+ return (
1278
+ <View>
1279
+ <Text>{user?.name}</Text>
1280
+ <Text>{user?.email}</Text>
1281
+ </View>
1282
+ )
1283
+ }
1284
+ ```
1285
+
1286
+ **Service Package (No React):**
1287
+
1288
+ ```typescript
1289
+ // packages/analytics/src/tracker.ts
1290
+ import { getBridge } from 'shared-state-bridge'
1291
+ import type { AuthState } from '@myorg/shared'
1292
+
1293
+ const auth = getBridge<AuthState>('auth')
1294
+
1295
+ auth.subscribe(
1296
+ s => s.user,
1297
+ (user) => {
1298
+ if (user) {
1299
+ analytics.identify(user.id, { name: user.name })
1300
+ }
1301
+ }
1302
+ )
1303
+ ```
1304
+
1305
+ ### Project Structure Example
1306
+
1307
+ ```
1308
+ my-monorepo/
1309
+ ├── packages/
1310
+ │ ├── shared/ # Bridge definitions + types
1311
+ │ │ └── src/bridges.ts
1312
+ │ ├── web/ # Next.js app
1313
+ │ │ └── src/components/Header.tsx
1314
+ │ ├── mobile/ # React Native app
1315
+ │ │ └── src/screens/Profile.tsx
1316
+ │ └── analytics/ # Service package (no React)
1317
+ │ └── src/tracker.ts
1318
+ ├── turbo.json / nx.json
1319
+ └── package.json
1320
+ ```
1321
+
1322
+ ---
1323
+
1324
+ ## API Reference
1325
+
1326
+ ### Core API
1327
+
1328
+ #### `createBridge<T>(config): BridgeApi<T>`
1329
+
1330
+ Creates and registers a new bridge store.
1331
+
1332
+ | Parameter | Type | Description |
1333
+ |---|---|---|
1334
+ | `config.name` | `string` | Unique name for the global registry |
1335
+ | `config.initialState` | `T` | Initial state object |
1336
+ | `config.plugins` | `BridgePlugin<T>[]` | Optional plugins array |
1337
+
1338
+ **Returns:** `BridgeApi<T>` instance
1339
+
1340
+ **Throws:** If a bridge with the same name already exists
1341
+
1342
+ ---
1343
+
1344
+ #### `getBridge<T>(name): BridgeApi<T>`
1345
+
1346
+ Retrieves an existing bridge from the global registry.
1347
+
1348
+ | Parameter | Type | Description |
1349
+ |---|---|---|
1350
+ | `name` | `string` | The bridge name |
1351
+
1352
+ **Returns:** `BridgeApi<T>` — the exact same instance that was created
1353
+
1354
+ **Throws:** If no bridge with that name exists
1355
+
1356
+ ---
1357
+
1358
+ #### `hasBridge(name): boolean`
1359
+
1360
+ Checks if a bridge exists in the registry without throwing.
1361
+
1362
+ ---
1363
+
1364
+ #### `listBridges(): string[]`
1365
+
1366
+ Returns an array of all registered bridge names.
1367
+
1368
+ ---
1369
+
1370
+ ### BridgeApi Instance Methods
1371
+
1372
+ These are the methods available on the object returned by `createBridge()`.
1373
+
1374
+ #### `bridge.getState(): T`
1375
+
1376
+ Returns the current state snapshot. Synchronous.
1377
+
1378
+ ---
1379
+
1380
+ #### `bridge.setState(partial): void`
1381
+
1382
+ Updates state by shallow-merging `partial` into current state.
1383
+
1384
+ ```typescript
1385
+ // Object form
1386
+ bridge.setState({ count: 1 })
1387
+
1388
+ // Updater function form
1389
+ bridge.setState(prev => ({ count: prev.count + 1 }))
1390
+ ```
1391
+
1392
+ ---
1393
+
1394
+ #### `bridge.setState(state, true): void`
1395
+
1396
+ Replaces the entire state (no merge).
1397
+
1398
+ ```typescript
1399
+ bridge.setState({ count: 0, theme: 'light' }, true)
1400
+ ```
1401
+
1402
+ ---
1403
+
1404
+ #### `bridge.subscribe(listener): () => void`
1405
+
1406
+ Subscribes to all state changes. Returns an unsubscribe function.
1407
+
1408
+ ```typescript
1409
+ const unsub = bridge.subscribe((state, previousState) => { ... })
1410
+ unsub() // stop listening
1411
+ ```
1412
+
1413
+ ---
1414
+
1415
+ #### `bridge.subscribe(selector, listener, options?): () => void`
1416
+
1417
+ Subscribes with a selector. Listener only fires when the selected value changes.
1418
+
1419
+ ```typescript
1420
+ const unsub = bridge.subscribe(
1421
+ s => s.count,
1422
+ (count, prevCount) => { ... },
1423
+ { equalityFn: Object.is, fireImmediately: false }
1424
+ )
1425
+ ```
1426
+
1427
+ ---
1428
+
1429
+ #### `bridge.getInitialState(): T`
1430
+
1431
+ Returns the original initial state (never changes).
1432
+
1433
+ ---
1434
+
1435
+ #### `bridge.getName(): string`
1436
+
1437
+ Returns the bridge's registered name.
1438
+
1439
+ ---
1440
+
1441
+ #### `bridge.destroy(): void`
1442
+
1443
+ Removes from registry, calls plugin `onDestroy`, clears listeners. Idempotent.
1444
+
1445
+ ---
1446
+
1447
+ ### React API
1448
+
1449
+ Import from `shared-state-bridge/react`.
1450
+
1451
+ #### `<BridgeProvider bridge={bridge}>`
1452
+
1453
+ Context provider. Makes `bridge` available to `useBridge()` and `useBridgeState(selector)`.
1454
+
1455
+ ---
1456
+
1457
+ #### `useBridge<T>(): BridgeApi<T>`
1458
+
1459
+ Returns bridge from context. Throws outside `BridgeProvider`.
1460
+
1461
+ ---
1462
+
1463
+ #### `useBridgeState(selector, options?): U`
1464
+
1465
+ Subscribes to state from context bridge.
1466
+
1467
+ #### `useBridgeState(bridge, selector, options?): U`
1468
+
1469
+ Subscribes to state from a direct bridge reference.
1470
+
1471
+ #### `useBridgeState(bridge): T`
1472
+
1473
+ Subscribes to the full state.
1474
+
1475
+ **Options:**
1476
+
1477
+ | Option | Type | Default | Description |
1478
+ |---|---|---|---|
1479
+ | `shallow` | `boolean` | `false` | Use shallow equality to prevent re-renders from object selectors |
1480
+
1481
+ ---
1482
+
1483
+ ### Persist API
1484
+
1485
+ Import from `shared-state-bridge/persist`.
1486
+
1487
+ #### `persist<T>(options): BridgePlugin<T>`
1488
+
1489
+ Creates a persistence plugin. See [Persistence Options Reference](#persistence-options-reference) for full options.
1490
+
1491
+ ---
1492
+
1493
+ #### `localStorageAdapter: PersistAdapter`
1494
+
1495
+ Pre-built adapter for `window.localStorage`. Safe in SSR (returns `null`).
1496
+
1497
+ ---
1498
+
1499
+ #### `asyncStorageAdapter(storage): PersistAdapter`
1500
+
1501
+ Factory that wraps a React Native `AsyncStorage` instance.
1502
+
1503
+ ```typescript
1504
+ import AsyncStorage from '@react-native-async-storage/async-storage'
1505
+ const adapter = asyncStorageAdapter(AsyncStorage)
1506
+ ```
1507
+
1508
+ ---
1509
+
1510
+ #### `memoryAdapter(): PersistAdapter`
1511
+
1512
+ Creates an in-memory storage backend. Each call returns a fresh, isolated store.
1513
+
1514
+ ---
1515
+
1516
+ ### Sync API
1517
+
1518
+ #### `sync<T>(options: SyncOptions<T>): BridgePlugin<T>`
1519
+
1520
+ Creates a WebSocket sync plugin. Pass it in the `plugins` array of `createBridge()`.
1521
+
1522
+ ```typescript
1523
+ import { sync } from 'shared-state-bridge/sync'
1524
+
1525
+ sync({
1526
+ url: 'wss://your-server.com/sync',
1527
+ channel: 'room-1',
1528
+ pick: ['theme'],
1529
+ throttleMs: 50,
1530
+ onConnect: () => console.log('connected'),
1531
+ })
1532
+ ```
1533
+
1534
+ See [Sync Options Reference](#sync-options-reference) for all available options.
1535
+
1536
+ #### `SyncConnection`
1537
+
1538
+ Low-level WebSocket connection manager with auto-reconnect, message buffering, and exponential backoff. Used internally by the `sync` plugin, but exported for advanced use cases.
1539
+
1540
+ ```typescript
1541
+ import { SyncConnection } from 'shared-state-bridge/sync'
1542
+
1543
+ const conn = new SyncConnection({
1544
+ url: 'wss://your-server.com/sync',
1545
+ reconnect: true,
1546
+ maxReconnectAttempts: 10,
1547
+ reconnectInterval: 1000,
1548
+ maxReconnectInterval: 30000,
1549
+ onConnect: () => {},
1550
+ onDisconnect: () => {},
1551
+ onMessage: (msg) => {},
1552
+ onError: (err) => {},
1553
+ })
1554
+
1555
+ conn.connect()
1556
+ conn.send({ type: 'state', channel: 'room-1', clientId: 'abc', state: {}, timestamp: Date.now() })
1557
+ conn.destroy()
1558
+ ```
1559
+
1560
+ ---
1561
+
1562
+ ### Types
1563
+
1564
+ All types are exported from the main entry point:
1565
+
1566
+ | Type | Description |
1567
+ |---|---|
1568
+ | `State` | Base state constraint: `Record<string, unknown>` |
1569
+ | `BridgeApi<T>` | Bridge instance interface with all methods |
1570
+ | `BridgeConfig<T>` | Configuration object for `createBridge` |
1571
+ | `BridgePlugin<T>` | Plugin interface with lifecycle hooks |
1572
+ | `PersistAdapter` | Storage adapter interface (3 methods) |
1573
+ | `PersistOptions<T>` | Full configuration for the `persist` plugin |
1574
+ | `SetState<T>` | Type signature for `bridge.setState` |
1575
+ | `Subscribe<T>` | Type signature for `bridge.subscribe` |
1576
+ | `Listener<T>` | `(state: T, previousState: T) => void` |
1577
+ | `SelectorListener<T, U>` | `(slice: U, previousSlice: U) => void` |
1578
+ | `SubscribeOptions<U>` | Options for selector-based subscribe |
1579
+ | `Selector<T, U>` | `(state: T) => U` |
1580
+ | `EqualityFn<U>` | `(a: U, b: U) => boolean` |
1581
+ | `SyncOptions<T>` | Configuration for the `sync` plugin |
1582
+ | `JoinMessage` | `{ type: "join", channel, clientId }` |
1583
+ | `StateMessage` | `{ type: "state", channel, clientId, state, timestamp }` |
1584
+ | `FullStateMessage` | `{ type: "full_state", channel, state, timestamp }` |
1585
+ | `OutboundMessage` | `JoinMessage \| StateMessage` |
1586
+ | `InboundMessage` | `StateMessage \| FullStateMessage` |
1587
+
1588
+ ---
1589
+
1590
+ ## Architecture Deep Dive
1591
+
1592
+ ### State Update Flow
1593
+
1594
+ ```
1595
+ bridge.setState({ count: 1 })
1596
+
1597
+ ├── 1. Resolve partial: if function, call with current state
1598
+ │ nextPartial = isFunction(partial) ? partial(state) : partial
1599
+
1600
+ ├── 2. Apply update:
1601
+ │ if (replace) state = nextPartial
1602
+ │ else state = Object.assign({}, state, nextPartial) // new reference
1603
+
1604
+ ├── 3. Check: Object.is(state, previousState)?
1605
+ │ YES → return (no notifications)
1606
+ │ NO → continue
1607
+
1608
+ ├── 4. Notify listeners (Set, insertion order):
1609
+ │ listeners.forEach(fn => fn(state, previousState))
1610
+
1611
+ └── 5. Notify plugins:
1612
+ plugins.forEach(p => p.onStateChange?.(state, previousState))
1613
+ ```
1614
+
1615
+ Key detail: `Object.assign({}, state, nextPartial)` always creates a **new object reference**. This means even `setState({})` with an empty object will create a new reference and trigger listeners. However, `setState(sameRef, true)` with the exact same reference will be caught by the `Object.is` check and skip notifications.
1616
+
1617
+ ### Selector Re-render Optimization
1618
+
1619
+ ```
1620
+ State update: { count: 1, theme: 'dark' } -> { count: 2, theme: 'dark' }
1621
+
1622
+ Component A: useBridgeState(bridge, s => s.count)
1623
+ -> getSnapshot() returns 2 (was 1)
1624
+ -> Object.is(1, 2) === false
1625
+ -> RE-RENDERS (correct: count changed)
1626
+
1627
+ Component B: useBridgeState(bridge, s => s.theme)
1628
+ -> getSnapshot() returns 'dark' (was 'dark')
1629
+ -> Object.is('dark', 'dark') === true
1630
+ -> SKIPS RE-RENDER (correct: theme unchanged)
1631
+
1632
+ Component C: useBridgeState(bridge, s => ({ count: s.count, theme: s.theme }))
1633
+ -> getSnapshot() returns NEW object { count: 2, theme: 'dark' }
1634
+ -> Object.is(prevObj, newObj) === false (different references!)
1635
+ -> RE-RENDERS (unnecessary: theme didn't change)
1636
+
1637
+ Component D: useBridgeState(bridge, s => ({ count: s.count, theme: s.theme }), { shallow: true })
1638
+ -> getSnapshot() returns { count: 2, theme: 'dark' }
1639
+ -> shallowEqual check: count changed (1 !== 2)
1640
+ -> Returns NEW reference
1641
+ -> RE-RENDERS (correct: count changed)
1642
+
1643
+ Component E (same selector, theme unchanged):
1644
+ -> getSnapshot() returns { count: 2, theme: 'dark' }
1645
+ -> shallowEqual check: all values same
1646
+ -> Returns PREVIOUS reference (same object)
1647
+ -> Object.is(prevRef, prevRef) === true
1648
+ -> SKIPS RE-RENDER (correct!)
1649
+ ```
1650
+
1651
+ ### Global Registry Internals
1652
+
1653
+ ```typescript
1654
+ // The registry is a Map stored on globalThis with a Symbol.for key:
1655
+
1656
+ const REGISTRY_KEY = Symbol.for('shared-state-bridge.registry')
1657
+
1658
+ // globalThis[REGISTRY_KEY] = Map {
1659
+ // 'auth' => BridgeApi { ... },
1660
+ // 'app' => BridgeApi { ... },
1661
+ // 'ui' => BridgeApi { ... },
1662
+ // }
1663
+
1664
+ // Why Symbol.for() is critical:
1665
+ //
1666
+ // Module A: Symbol.for('shared-state-bridge.registry') → Symbol(123)
1667
+ // Module B: Symbol.for('shared-state-bridge.registry') → Symbol(123) (SAME!)
1668
+ // Duplicate package: Symbol.for('shared-state-bridge.registry') → Symbol(123) (SAME!)
1669
+ //
1670
+ // Plain string would also work, but Symbols avoid collisions with other
1671
+ // libraries that might use globalThis.
1672
+ ```
1673
+
1674
+ ### Persistence Flow
1675
+
1676
+ ```
1677
+ createBridge({ plugins: [persist({ adapter, key, version: 2 })] })
1678
+
1679
+ ├── Bridge initialized with initialState
1680
+ ├── Bridge registered in global registry
1681
+
1682
+ └── persist.onInit(bridge):
1683
+
1684
+ ├── Set up throttled writer function
1685
+
1686
+ └── Hydrate (async):
1687
+
1688
+ ├── raw = await adapter.getItem(key)
1689
+
1690
+ ├── if null → skip (first run, nothing persisted)
1691
+
1692
+ ├── envelope = deserialize(raw) // { state, version }
1693
+
1694
+ ├── if envelope.version === current version:
1695
+ │ └── bridge.setState(envelope.state) // merge persisted state
1696
+
1697
+ ├── if version mismatch + migrate function:
1698
+ │ ├── migrated = migrate(envelope.state, envelope.version)
1699
+ │ └── bridge.setState(migrated)
1700
+
1701
+ └── if version mismatch + NO migrate:
1702
+ └── adapter.removeItem(key) // discard stale data
1703
+
1704
+
1705
+ On every bridge.setState():
1706
+
1707
+ └── persist.onStateChange(state):
1708
+
1709
+ ├── filtered = filterState(state) // apply pick/omit
1710
+ ├── envelope = { state: filtered, version }
1711
+ ├── serialized = serialize(envelope)
1712
+ └── adapter.setItem(key, serialized) // throttled
1713
+ ```
1714
+
1715
+ ### Sync Flow
1716
+
1717
+ ```
1718
+ bridge.setState({ count: 5 }) // local update
1719
+
1720
+ ├── plugins.forEach(p => p.onStateChange(state, prev))
1721
+ │ │
1722
+ │ └── sync.onStateChange(state)
1723
+ │ ├── isApplyingRemote? return (echo guard)
1724
+ │ └── sendState(state) // throttled
1725
+ │ ├── filtered = filterState(state) // apply pick/omit
1726
+ │ └── connection.send({ type: "state", channel, clientId, state: filtered })
1727
+ │ ├── ws.readyState === OPEN? ws.send(data)
1728
+ │ └── else: buffer.push(data) // flush on reconnect
1729
+
1730
+ ▼ (incoming from server)
1731
+ ws.onmessage(data)
1732
+
1733
+ ├── parse JSON
1734
+ ├── message.clientId === ownClientId? skip (echo prevention)
1735
+ ├── resolve? stateToApply = resolve(local, remote)
1736
+ │ else: stateToApply = remoteState (last-write-wins)
1737
+ ├── isApplyingRemote = true // prevent re-broadcast
1738
+ ├── bridge.setState(stateToApply)
1739
+ └── isApplyingRemote = false
1740
+ ```
1741
+
1742
+ ---
1743
+
1744
+ ## Gotchas and Common Pitfalls
1745
+
1746
+ ### 1. Bridge Name Collisions
1747
+
1748
+ ```typescript
1749
+ createBridge({ name: 'app', initialState: {} })
1750
+ createBridge({ name: 'app', initialState: {} })
1751
+ // Error: A bridge named "app" already exists.
1752
+ ```
1753
+
1754
+ **Fix:** Use unique names, or check with `hasBridge('app')` first, or `destroy()` the old bridge.
1755
+
1756
+ ### 2. getBridge Before createBridge
1757
+
1758
+ ```typescript
1759
+ const bridge = getBridge('auth')
1760
+ // Error: No bridge named "auth" found.
1761
+ ```
1762
+
1763
+ **Fix:** Ensure the package that calls `createBridge` is imported/executed before any `getBridge` calls. In a monorepo, the shared package with bridge definitions should be imported at app startup.
1764
+
1765
+ ### 3. Object Selectors Without Shallow
1766
+
1767
+ ```tsx
1768
+ // This causes re-renders on EVERY state change
1769
+ const data = useBridgeState(bridge, s => ({
1770
+ a: s.a,
1771
+ b: s.b,
1772
+ }))
1773
+ ```
1774
+
1775
+ **Fix:** Add `{ shallow: true }` or use separate primitive selectors.
1776
+
1777
+ ### 4. Stale Closures in Updaters
1778
+
1779
+ ```typescript
1780
+ // WRONG: count is captured once
1781
+ const count = bridge.getState().count
1782
+ bridge.setState({ count: count + 1 })
1783
+ bridge.setState({ count: count + 1 }) // Both set count to the same value!
1784
+
1785
+ // CORRECT: use updater function for sequential updates
1786
+ bridge.setState(s => ({ count: s.count + 1 }))
1787
+ bridge.setState(s => ({ count: s.count + 1 })) // Correctly increments twice
1788
+ ```
1789
+
1790
+ ### 5. Async Hydration Timing
1791
+
1792
+ Hydration from async adapters (AsyncStorage) happens asynchronously. State may briefly hold `initialState` before the persisted state loads:
1793
+
1794
+ ```typescript
1795
+ const bridge = createBridge({
1796
+ name: 'app',
1797
+ initialState: { theme: 'light' },
1798
+ plugins: [persist({ adapter: asyncStorageAdapter(AsyncStorage), key: 'app' })],
1799
+ })
1800
+
1801
+ bridge.getState() // { theme: 'light' } (initial, not yet hydrated)
1802
+
1803
+ // After microtask:
1804
+ // bridge.getState() // { theme: 'dark' } (hydrated from storage)
1805
+ ```
1806
+
1807
+ **Fix:** Design your UI to handle the initial state gracefully, or use a loading flag.
1808
+
1809
+ ### 6. Mutating State Directly
1810
+
1811
+ ```typescript
1812
+ // WRONG: mutation won't trigger listeners
1813
+ const state = bridge.getState()
1814
+ state.count = 5
1815
+
1816
+ // CORRECT: use setState
1817
+ bridge.setState({ count: 5 })
1818
+ ```
1819
+
1820
+ ### 7. Subscribing in useEffect Without Cleanup
1821
+
1822
+ ```tsx
1823
+ // WRONG: leaks a listener on every render
1824
+ useEffect(() => {
1825
+ bridge.subscribe(listener)
1826
+ })
1827
+
1828
+ // CORRECT: return the unsubscribe function
1829
+ useEffect(() => {
1830
+ return bridge.subscribe(listener)
1831
+ }, [])
1832
+
1833
+ // BEST: use useBridgeState hook instead
1834
+ const value = useBridgeState(bridge, selector)
1835
+ ```
1836
+
1837
+ ---
1838
+
1839
+ ## FAQ
1840
+
1841
+ **How does cross-package sharing work?**
1842
+ Bridges are stored in a `Map` on `globalThis` using `Symbol.for()` as the key. Since `Symbol.for()` returns the same symbol across all modules (even duplicated packages), all packages in your monorepo access the same registry. See [Architecture Deep Dive](#architecture-deep-dive) for details.
1843
+
1844
+ **Can I use this without React?**
1845
+ Yes. The core (`shared-state-bridge`) has zero dependencies and works in any JS environment — Node.js, browsers, React Native, Deno, Bun. The React hooks are a separate, optional entry point (`shared-state-bridge/react`).
1846
+
1847
+ **Does it work with SSR / Next.js?**
1848
+ Yes. `useBridgeState` uses `useSyncExternalStore` with a `getServerSnapshot` that returns `getInitialState()`, which is SSR-safe. See [Server-Side Rendering](#server-side-rendering-nextjs).
1849
+
1850
+ **How does this compare to Zustand?**
1851
+ Zustand is a general-purpose state manager. `shared-state-bridge` is specifically designed for cross-package state sharing in monorepos. The core API is similar (event-emitter store + `useSyncExternalStore`), but the global registry and named bridges are unique to this library. If you only need state within a single package, Zustand is great. If you need to share state *across* packages in a monorepo, `shared-state-bridge` is purpose-built for that.
1852
+
1853
+ **What happens if two packages create a bridge with the same name?**
1854
+ An error is thrown: `A bridge named "x" already exists`. This prevents silent overwrites. Use `getBridge()` to access existing bridges, or call `destroy()` first to remove the old one.
1855
+
1856
+ **Does it support React Native?**
1857
+ Yes. The core and React hooks work identically in React Native. For persistence, use `asyncStorageAdapter()` with `@react-native-async-storage/async-storage`.
1858
+
1859
+ **What's the bundle size?**
1860
+ - Core: ~1.2 KB gzipped
1861
+ - React hooks: ~1.1 KB gzipped
1862
+ - Persist plugin: ~1.0 KB gzipped
1863
+ - Sync plugin: ~1.8 KB gzipped
1864
+ - All four combined: ~5.1 KB gzipped
1865
+
1866
+ Each entry point is independently tree-shakeable. If you only use the core, React, persist, and sync code is never included.
1867
+
1868
+ **Can I have multiple BridgeProviders?**
1869
+ Yes. Each `BridgeProvider` provides its own bridge to its subtree. Components use the nearest provider. You can nest providers for different bridges.
1870
+
1871
+ **Is it concurrent-safe (React 18)?**
1872
+ Yes. `useBridgeState` uses `useSyncExternalStore`, which is React's official API for integrating external stores with concurrent features like `useTransition` and `Suspense`.
1873
+
1874
+ **Can I use this in a non-monorepo project?**
1875
+ Absolutely. The core API works anywhere. The cross-package registry feature just happens to shine in monorepos, but a single-package app can use `createBridge` + React hooks perfectly fine as a lightweight state manager.
1876
+
1877
+ **Does the sync plugin actually sync between different apps (Next.js + React Native)?**
1878
+ Yes! The `sync` plugin connects to a WebSocket server and broadcasts state changes to all clients in the same channel. Unlike the core bridge (which shares within the same JS runtime), the sync plugin enables true cross-app real-time state synchronization. You need to provide your own WebSocket server — a minimal example is included in the docs.
1879
+
1880
+ **Can I use both persist and sync together?**
1881
+ Yes. They are independent plugins that compose naturally:
1882
+
1883
+ ```typescript
1884
+ createBridge({
1885
+ name: 'app',
1886
+ initialState: { theme: 'light', count: 0 },
1887
+ plugins: [
1888
+ persist({ adapter: localStorageAdapter, key: 'app' }),
1889
+ sync({ url: 'wss://server.com/sync', channel: 'room-1' }),
1890
+ ],
1891
+ })
1892
+ ```
1893
+
1894
+ State persists locally AND syncs in real-time with other connected apps.
1895
+
1896
+ ---
1897
+
1898
+ ## Contributing
1899
+
1900
+ Contributions are welcome! Here's how to get started:
1901
+
1902
+ ```bash
1903
+ # Clone the repo
1904
+ git clone https://github.com/your-username/shared-state-bridge.git
1905
+ cd shared-state-bridge
1906
+
1907
+ # Install dependencies
1908
+ npm install
1909
+
1910
+ # Run tests
1911
+ npm test
1912
+
1913
+ # Run tests in watch mode
1914
+ npm run test:watch
1915
+
1916
+ # Type check
1917
+ npm run typecheck
1918
+
1919
+ # Build
1920
+ npm run build
1921
+ ```
1922
+
1923
+ ### Project Structure
1924
+
1925
+ ```
1926
+ src/
1927
+ ├── index.ts # Core entry point
1928
+ ├── core/
1929
+ │ ├── types.ts # All TypeScript type definitions
1930
+ │ ├── utils.ts # shallowEqual, throttle, isFunction
1931
+ │ ├── registry.ts # Global bridge registry
1932
+ │ └── bridge.ts # createBridge + Bridge store logic
1933
+ ├── react/
1934
+ │ ├── index.ts # React entry point
1935
+ │ ├── context.ts # React context
1936
+ │ ├── provider.tsx # BridgeProvider component
1937
+ │ └── hooks.ts # useBridgeState, useBridge
1938
+ ├── persist/
1939
+ │ ├── index.ts # Persist entry point
1940
+ │ ├── plugin.ts # Persistence plugin
1941
+ │ └── adapters.ts # Storage adapters
1942
+ └── sync/
1943
+ ├── index.ts # Sync entry point
1944
+ ├── types.ts # SyncOptions, wire protocol types
1945
+ ├── connection.ts # WebSocket manager (reconnect, buffer)
1946
+ └── plugin.ts # sync() plugin factory
1947
+ ```
1948
+
1949
+ ### Guidelines
1950
+
1951
+ - Keep the core dependency-free
1952
+ - Maintain 100% TypeScript strict mode compliance
1953
+ - Write tests for all new features
1954
+ - Keep bundle sizes minimal — every byte counts
1955
+
1956
+ ---
1957
+
1958
+ ## Support
1959
+
1960
+ If you find this package useful, consider buying me a coffee!
1961
+
1962
+ [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/aemadeldin)
1963
+
1964
+ ---
1965
+
1966
+ ## License
1967
+
1968
+ MIT