synapse-storage 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -719
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,747 +1,50 @@
|
|
|
1
1
|
# Synapse Storage
|
|
2
2
|
|
|
3
|
-
> **🇺🇸 English** | [📝 ChangeLog](./CHANGELOG.md)
|
|
4
|
-
|
|
5
|
-
State management toolkit + API client
|
|
6
|
-
|
|
7
3
|
[](https://badge.fury.io/js/synapse-storage)
|
|
8
4
|
[](https://bundlephobia.com/package/synapse-storage)
|
|
9
5
|
[](https://www.typescriptlang.org/)
|
|
10
6
|
[](https://rxjs.dev/)
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- **Framework Agnostic** — works with any framework or standalone
|
|
15
|
-
- **Sync & Async Storage** — Memory/LocalStorage (fully synchronous) and IndexedDB (async) with type-safe separation
|
|
16
|
-
- **Selectors** — memoized computed values with dependency tracking (like Reselect)
|
|
17
|
-
- **Subscriptions** — subscribe to nested paths via selector functions
|
|
18
|
-
- **Immer-like Updates** — mutate state directly inside `update()` callbacks
|
|
19
|
-
- **API Client** — HTTP client with caching, tags, and invalidation (like RTK Query)
|
|
20
|
-
- **React Integration** — hooks built on `useSyncExternalStore` (Concurrent Mode safe)
|
|
21
|
-
- **RxJS Reactive** — Redux-Observable style effects, dispatchers, and watchers
|
|
22
|
-
- **Middleware & Plugins** — separate sync/async systems for extending storage behavior
|
|
23
|
-
- **Singleton Support** — shared storage instances across components with merge strategies
|
|
24
|
-
- **EventBus** — decoupled inter-module communication with wildcards and history
|
|
25
|
-
- **Cross-tab Sync** — BroadcastChannel middleware for multi-tab state synchronization
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Author
|
|
30
|
-
|
|
31
|
-
**Vladislav** — Senior Frontend Developer (React, TypeScript)
|
|
32
|
-
|
|
33
|
-
[GitHub](https://github.com/Vlad92msk/) | [LinkedIn](https://www.linkedin.com/in/vlad-firsov/)
|
|
8
|
+
Framework-agnostic state management toolkit and API client for TypeScript applications.
|
|
9
|
+
Combines reactive storage, memoized selectors, Redux-Observable style effects, and a tag-based HTTP cache — all in one library.
|
|
34
10
|
|
|
35
|
-
|
|
36
|
-
*PS: Not recommended for production use yet as I develop this in my free time.
|
|
37
|
-
The library works in general, but I can provide guarantees only after full integration into my pet project - Social Network.
|
|
38
|
-
This won't happen before changing my current workplace and country of residence*
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Installation
|
|
11
|
+
## Quick Start
|
|
43
12
|
|
|
44
13
|
```bash
|
|
45
14
|
npm install synapse-storage
|
|
46
15
|
```
|
|
47
16
|
|
|
48
|
-
```bash
|
|
49
|
-
# For reactive capabilities
|
|
50
|
-
npm install rxjs
|
|
51
|
-
|
|
52
|
-
# For React integration
|
|
53
|
-
npm install react react-dom
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
| Module | Description | Dependencies |
|
|
57
|
-
|--------|-------------|--------------|
|
|
58
|
-
| `synapse-storage/core` | Storage, selectors, middleware, plugins | — |
|
|
59
|
-
| `synapse-storage/react` | React hooks and context utilities | React 18+ |
|
|
60
|
-
| `synapse-storage/reactive` | Dispatcher, effects, watchers | RxJS 7.8.2+ |
|
|
61
|
-
| `synapse-storage/api` | HTTP client with caching | — |
|
|
62
|
-
| `synapse-storage/utils` | createSynapse, EventBus, awaiter | — |
|
|
63
|
-
|
|
64
|
-
> Import only the modules you need — each works independently.
|
|
65
|
-
|
|
66
|
-
### tsconfig.json
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"compilerOptions": {
|
|
71
|
-
"target": "ES2022",
|
|
72
|
-
"module": "ES2022",
|
|
73
|
-
"moduleResolution": "bundler"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## Quick Start
|
|
81
|
-
|
|
82
17
|
```typescript
|
|
83
18
|
import { MemoryStorage } from 'synapse-storage/core'
|
|
84
|
-
|
|
85
|
-
const storage = new MemoryStorage({
|
|
86
|
-
name: 'counter',
|
|
87
|
-
initialState: { count: 0, user: { name: 'Anonymous' } },
|
|
88
|
-
})
|
|
89
|
-
await storage.initialize()
|
|
90
|
-
|
|
91
|
-
// Read
|
|
92
|
-
storage.getState() // { count: 0, user: { name: 'Anonymous' } }
|
|
93
|
-
storage.get('count') // 0
|
|
94
|
-
|
|
95
|
-
// Write
|
|
96
|
-
storage.set('count', 1)
|
|
97
|
-
|
|
98
|
-
// Immer-like update (multiple mutations = one notification)
|
|
99
|
-
storage.update((state) => {
|
|
100
|
-
state.count += 1
|
|
101
|
-
state.user.name = 'Alice'
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// Subscribe to nested path
|
|
105
|
-
const unsub = storage.subscribe(
|
|
106
|
-
(s) => s.user.name,
|
|
107
|
-
(name) => console.log('Name changed:', name)
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
// Reset to initialState
|
|
111
|
-
storage.reset()
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Storage Types
|
|
117
|
-
|
|
118
|
-
Synapse has two storage categories with **type-safe separation**:
|
|
119
|
-
|
|
120
|
-
### Sync Storage (MemoryStorage, LocalStorage)
|
|
121
|
-
|
|
122
|
-
All operations are synchronous — `get()`, `set()`, `update()`, `getState()` return values directly.
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
import { MemoryStorage, LocalStorage } from 'synapse-storage/core'
|
|
126
|
-
|
|
127
|
-
const memory = new MemoryStorage<State>({ name: 'app', initialState })
|
|
128
|
-
const local = new LocalStorage<State>({ name: 'app', initialState })
|
|
129
|
-
|
|
130
|
-
await memory.initialize()
|
|
131
|
-
const value = memory.get('key') // T — sync
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Async Storage (IndexedDBStorage)
|
|
135
|
-
|
|
136
|
-
Operations return Promises — persistent browser storage.
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
import { IndexedDBStorage } from 'synapse-storage/core'
|
|
140
|
-
|
|
141
|
-
const idb = new IndexedDBStorage<State>({
|
|
142
|
-
name: 'app',
|
|
143
|
-
initialState,
|
|
144
|
-
options: { dbName: 'my_app_db' },
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
await idb.initialize()
|
|
148
|
-
const value = await idb.get('key') // Promise<T>
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### getStateSync()
|
|
152
|
-
|
|
153
|
-
Available on **all** storage types — returns the cached state synchronously, even for IndexedDB:
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
const state = storage.getStateSync() // always sync
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Static Factory Methods
|
|
160
|
-
|
|
161
|
-
Every storage class has a `.create()` static method:
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
const storage = MemoryStorage.create<State>({ name: 'app', initialState })
|
|
165
|
-
const storage = LocalStorage.create<State>({ name: 'app', initialState })
|
|
166
|
-
const storage = IndexedDBStorage.create<State>({ name: 'app', initialState, options: {} })
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### StorageFactory
|
|
170
|
-
|
|
171
|
-
Universal factory with type-safe overloads:
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
import { StorageFactory } from 'synapse-storage/core'
|
|
175
|
-
|
|
176
|
-
// Typed factories
|
|
177
|
-
const mem = StorageFactory.createMemory<S>({ name: 'x', initialState })
|
|
178
|
-
const loc = StorageFactory.createLocal<S>({ name: 'x', initialState })
|
|
179
|
-
const idb = StorageFactory.createIndexedDB<S>({ name: 'x', initialState, options: {} })
|
|
180
|
-
|
|
181
|
-
// Universal — return type depends on `type`
|
|
182
|
-
const storage = StorageFactory.create<S>({
|
|
183
|
-
type: 'memory', // → ISyncStorage<S>
|
|
184
|
-
name: 'x',
|
|
185
|
-
initialState,
|
|
186
|
-
})
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## Reading & Writing Data
|
|
192
|
-
|
|
193
|
-
### Reading
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
storage.get('key') // value by key
|
|
197
|
-
storage.getState() // full state
|
|
198
|
-
storage.getStateSync() // sync cache (all storage types)
|
|
199
|
-
storage.has('key') // boolean
|
|
200
|
-
storage.keys() // string[]
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Writing
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
storage.set('key', value) // set single key
|
|
207
|
-
storage.update((s) => { s.count++ }) // Immer-like mutations
|
|
208
|
-
storage.remove('key') // delete key
|
|
209
|
-
storage.reset() // restore initialState
|
|
210
|
-
storage.clear() // reset to {}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
> For IndexedDB, all write operations return `Promise`.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
## Subscriptions
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
// Subscribe by key
|
|
221
|
-
const unsub = storage.subscribe('count', (newValue) => {
|
|
222
|
-
console.log('count:', newValue)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// Subscribe by selector function (nested paths)
|
|
226
|
-
const unsub = storage.subscribe(
|
|
227
|
-
(state) => state.user.name,
|
|
228
|
-
(name) => console.log('name:', name)
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
// Subscribe to all changes
|
|
232
|
-
const unsub = storage.subscribeToAll((event) => {
|
|
233
|
-
// event.type: 'set' | 'update' | 'remove' | 'clear' | 'reset'
|
|
234
|
-
// event.key, event.changedPaths
|
|
235
|
-
})
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## Selector System
|
|
241
|
-
|
|
242
|
-
Memoized computed values with dependency tracking:
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
import { SelectorModule } from 'synapse-storage/core'
|
|
246
|
-
|
|
247
|
-
const sm = new SelectorModule(storage)
|
|
248
|
-
|
|
249
|
-
// Simple selector
|
|
250
|
-
const count = sm.createSelector((state) => state.count)
|
|
251
|
-
|
|
252
|
-
// With custom equality
|
|
253
|
-
const items = sm.createSelector(
|
|
254
|
-
(state) => state.items,
|
|
255
|
-
{ equals: (a, b) => JSON.stringify(a) === JSON.stringify(b) }
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
// Dependent selector (recalculates only when deps change)
|
|
259
|
-
const filtered = sm.createSelector(
|
|
260
|
-
[items, filter],
|
|
261
|
-
(itemsVal, filterVal) => itemsVal.filter(i => i.type === filterVal)
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
// Usage
|
|
265
|
-
const value = filtered.select()
|
|
266
|
-
const unsub = filtered.subscribe({ notify: (value) => console.log(value) })
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## Middleware System
|
|
272
|
-
|
|
273
|
-
Separate sync and async middleware for each storage type:
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
const storage = new MemoryStorage<State>({
|
|
277
|
-
name: 'store',
|
|
278
|
-
initialState,
|
|
279
|
-
middlewares: (getDefault) => [
|
|
280
|
-
// Batch rapid writes
|
|
281
|
-
getDefault().batching({ batchSize: 5, batchDelay: 100 }),
|
|
282
|
-
|
|
283
|
-
// Skip updates if value unchanged
|
|
284
|
-
getDefault().shallowCompare(),
|
|
285
|
-
],
|
|
286
|
-
})
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Cross-tab Synchronization
|
|
290
|
-
|
|
291
|
-
```typescript
|
|
292
|
-
import { syncBroadcastMiddleware } from 'synapse-storage/core'
|
|
293
|
-
|
|
294
|
-
const storage = new MemoryStorage<State>({
|
|
295
|
-
name: 'store',
|
|
296
|
-
initialState,
|
|
297
|
-
middlewares: () => [
|
|
298
|
-
syncBroadcastMiddleware({ storageName: 'store', storageType: 'memory' }),
|
|
299
|
-
],
|
|
300
|
-
})
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
|
-
## Plugin System
|
|
306
|
-
|
|
307
|
-
Lifecycle hooks for intercepting storage operations:
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
import { ISyncStoragePlugin, SyncStoragePluginModule } from 'synapse-storage/core'
|
|
311
|
-
|
|
312
|
-
class TimestampPlugin implements ISyncStoragePlugin {
|
|
313
|
-
name = 'timestamp'
|
|
314
|
-
|
|
315
|
-
async initialize() {}
|
|
316
|
-
async destroy() {}
|
|
317
|
-
|
|
318
|
-
onBeforeSet<T>(value: T, context): T { return value }
|
|
319
|
-
onAfterSet<T>(key, value: T, ctx): T { return value }
|
|
320
|
-
onBeforeGet(key, ctx) { return key }
|
|
321
|
-
onAfterGet<T>(key, value: T | undefined, ctx) { return value }
|
|
322
|
-
onBeforeDelete(key, ctx): boolean { return true } // false = block
|
|
323
|
-
onAfterDelete(key, ctx) {}
|
|
324
|
-
onClear(ctx) {}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const plugins = new SyncStoragePluginModule(undefined, undefined, 'store')
|
|
328
|
-
await plugins.add(new TimestampPlugin())
|
|
329
|
-
|
|
330
|
-
const storage = new MemoryStorage<State>(
|
|
331
|
-
{ name: 'store', initialState },
|
|
332
|
-
plugins
|
|
333
|
-
)
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
> For IndexedDB, use `IAsyncStoragePlugin` and `AsyncStoragePluginModule`.
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
## React Integration
|
|
341
|
-
|
|
342
|
-
Hooks are built on `useSyncExternalStore` — safe in Concurrent Mode, no tearing.
|
|
343
|
-
|
|
344
|
-
### useCreateStorage
|
|
345
|
-
|
|
346
|
-
Returns a **discriminated union**: when `isReady: true`, `storage` is guaranteed non-null.
|
|
347
|
-
|
|
348
|
-
```tsx
|
|
349
|
-
import { useCreateStorage } from 'synapse-storage/react'
|
|
350
|
-
|
|
351
|
-
function App() {
|
|
352
|
-
const { storage, isReady } = useCreateStorage<State>({
|
|
353
|
-
type: 'memory', // 'memory' | 'localStorage' → ISyncStorage
|
|
354
|
-
name: 'app', // 'indexedDB' → IAsyncStorage
|
|
355
|
-
initialState: { count: 0 },
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
if (!isReady) return <div>Loading...</div>
|
|
359
|
-
// storage is ISyncStorage<State> here (not null)
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### useStorageSubscribe
|
|
364
|
-
|
|
365
|
-
```tsx
|
|
366
|
-
import { useStorageSubscribe } from 'synapse-storage/react'
|
|
367
|
-
|
|
368
|
-
function Counter() {
|
|
369
|
-
const count = useStorageSubscribe(storage, (s) => s.count)
|
|
370
|
-
const summary = useStorageSubscribe(storage, (s) => `Total: ${s.count}`)
|
|
371
|
-
return <div>{count} — {summary}</div>
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
### useSelector
|
|
376
|
-
|
|
377
|
-
```tsx
|
|
378
|
-
import { useSelector } from 'synapse-storage/react'
|
|
379
|
-
|
|
380
|
-
function ItemList() {
|
|
381
|
-
const items = useSelector(filteredItemsSelector)
|
|
382
|
-
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### createSynapseCtx
|
|
387
|
-
|
|
388
|
-
Context-based pattern for sharing synapse across component tree:
|
|
389
|
-
|
|
390
|
-
```tsx
|
|
391
|
-
import { createSynapseCtx, useSelector } from 'synapse-storage/react'
|
|
392
|
-
|
|
393
|
-
const {
|
|
394
|
-
contextSynapse,
|
|
395
|
-
useSynapseStorage,
|
|
396
|
-
useSynapseSelectors,
|
|
397
|
-
useSynapseActions,
|
|
398
|
-
cleanupSynapse,
|
|
399
|
-
} = createSynapseCtx(storePromise, {
|
|
400
|
-
loadingComponent: <div>Loading...</div>,
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
const Page = contextSynapse(() => {
|
|
404
|
-
const selectors = useSynapseSelectors()
|
|
405
|
-
const actions = useSynapseActions()
|
|
406
|
-
const count = useSelector(selectors.count)
|
|
407
|
-
|
|
408
|
-
return <button onClick={() => actions.increment()}>Count: {count}</button>
|
|
409
|
-
})
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### awaitSynapse
|
|
413
|
-
|
|
414
|
-
HOC and hook for waiting on synapse initialization:
|
|
415
|
-
|
|
416
|
-
```tsx
|
|
417
|
-
import { awaitSynapse } from 'synapse-storage/react'
|
|
418
|
-
|
|
419
|
-
const awaiter = awaitSynapse(storePromise, {
|
|
420
|
-
loadingComponent: <div>Loading...</div>,
|
|
421
|
-
errorComponent: (error) => <div>Error: {error.message}</div>,
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
// HOC
|
|
425
|
-
const ReadyComponent = awaiter.withSynapseReady(MyComponent)
|
|
426
|
-
|
|
427
|
-
// Hook
|
|
428
|
-
function Status() {
|
|
429
|
-
const { isReady, isPending, isError, store } = awaiter.useSynapseReady()
|
|
430
|
-
if (isPending) return <div>Loading...</div>
|
|
431
|
-
if (isError) return <div>Error</div>
|
|
432
|
-
return <div>Ready</div>
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Programmatic (also works outside React)
|
|
436
|
-
awaiter.isReady() // boolean
|
|
437
|
-
awaiter.getStatus() // 'pending' | 'ready' | 'error'
|
|
438
|
-
await awaiter.waitForReady() // Promise<Store>
|
|
439
|
-
awaiter.onReady((store) => { /* ... */ })
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
## Reactive Features (RxJS)
|
|
445
|
-
|
|
446
|
-
### Dispatcher — Actions & Watchers
|
|
447
|
-
|
|
448
|
-
```typescript
|
|
449
|
-
import { createDispatcher, createAction, createWatcher } from 'synapse-storage/reactive'
|
|
450
|
-
|
|
451
|
-
const dispatcher = createDispatcher(
|
|
452
|
-
{ storage },
|
|
453
|
-
(_storage, { createAction, createWatcher }) => {
|
|
454
|
-
const increment = createAction({
|
|
455
|
-
type: 'increment',
|
|
456
|
-
action: () => storage.update((s) => { s.count += 1 }),
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
const setName = createAction({
|
|
460
|
-
type: 'setName',
|
|
461
|
-
action: (name: string) => {
|
|
462
|
-
storage.set('name', name)
|
|
463
|
-
return name
|
|
464
|
-
},
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
const watchCount = createWatcher({
|
|
468
|
-
type: 'watchCount',
|
|
469
|
-
selector: (state) => state.count,
|
|
470
|
-
shouldTrigger: (prev, curr) => prev !== curr,
|
|
471
|
-
notifyAfterSubscribe: true,
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
return { increment, setName, watchCount }
|
|
475
|
-
}
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
// Dispatch
|
|
479
|
-
dispatcher.dispatch.increment()
|
|
480
|
-
dispatcher.dispatch.setName('Alice')
|
|
481
|
-
|
|
482
|
-
// Watch (RxJS Observable)
|
|
483
|
-
dispatcher.watchers.watchCount().subscribe((action) => {
|
|
484
|
-
console.log('count:', action.payload)
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
// Action stream
|
|
488
|
-
dispatcher.actions.subscribe((action) => {
|
|
489
|
-
console.log(action.type, action.payload)
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
dispatcher.destroy()
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### Effects
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
import { createEffect, ofType } from 'synapse-storage/reactive'
|
|
499
|
-
import { debounceTime, switchMap, tap } from 'rxjs/operators'
|
|
500
|
-
|
|
501
|
-
createEffect((action$, state$, { dispatcher }) =>
|
|
502
|
-
action$.pipe(
|
|
503
|
-
ofType(dispatcher.dispatch.search),
|
|
504
|
-
debounceTime(400),
|
|
505
|
-
switchMap((action) =>
|
|
506
|
-
fetchResults(action.payload).pipe(
|
|
507
|
-
tap((results) => dispatcher.dispatch.searchSuccess(results))
|
|
508
|
-
)
|
|
509
|
-
)
|
|
510
|
-
)
|
|
511
|
-
)
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
---
|
|
515
|
-
|
|
516
|
-
## createSynapse
|
|
517
|
-
|
|
518
|
-
High-level utility that wires storage + selectors + dispatcher + effects together:
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
19
|
import { createSynapse } from 'synapse-storage/utils'
|
|
20
|
+
import { useSelector } from 'synapse-storage/react'
|
|
522
21
|
|
|
523
|
-
const
|
|
524
|
-
storage: new MemoryStorage
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
count: sm.createSelector((s) => s.count),
|
|
528
|
-
doubled: sm.createSelector(
|
|
529
|
-
[count],
|
|
530
|
-
(c) => c * 2
|
|
531
|
-
),
|
|
22
|
+
const synapse = createSynapse({
|
|
23
|
+
storage: new MemoryStorage({
|
|
24
|
+
name: 'counter',
|
|
25
|
+
initialState: { count: 0 },
|
|
532
26
|
}),
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
createDispatcher({ storage }, (_s, { createAction, createWatcher }) => ({
|
|
536
|
-
increment: createAction({
|
|
537
|
-
type: 'increment',
|
|
538
|
-
action: () => storage.update((s) => { s.count += 1 }),
|
|
539
|
-
}),
|
|
540
|
-
watchCount: createWatcher({
|
|
541
|
-
type: 'watchCount',
|
|
542
|
-
selector: (s) => s.count,
|
|
543
|
-
}),
|
|
544
|
-
})),
|
|
545
|
-
|
|
546
|
-
effects: [
|
|
547
|
-
createEffect((action$, state$, { dispatcher }) =>
|
|
548
|
-
action$.pipe(/* ... */)
|
|
549
|
-
),
|
|
550
|
-
],
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
const store = await storePromise
|
|
554
|
-
|
|
555
|
-
store.storage // ISyncStorage<State>
|
|
556
|
-
store.selectors // { count, doubled }
|
|
557
|
-
store.actions // { increment, ... }
|
|
558
|
-
store.dispatcher // Dispatcher
|
|
559
|
-
store.state$ // Observable<State> (when effects are used)
|
|
560
|
-
store.destroy() // cleanup everything
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Dependencies
|
|
564
|
-
|
|
565
|
-
```typescript
|
|
566
|
-
const authStore = createSynapse({ /* ... */ })
|
|
567
|
-
|
|
568
|
-
const settingsStore = createSynapse({
|
|
569
|
-
dependencies: [authStore],
|
|
570
|
-
dependencyTimeout: 5000,
|
|
571
|
-
|
|
572
|
-
createStorageFn: async () => {
|
|
573
|
-
const auth = await authStore
|
|
574
|
-
const userId = auth.storage.getStateSync().userId
|
|
575
|
-
const storage = new MemoryStorage({ name: 'settings', initialState: { userId } })
|
|
576
|
-
await storage.initialize()
|
|
577
|
-
return storage
|
|
578
|
-
},
|
|
579
|
-
})
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
---
|
|
583
|
-
|
|
584
|
-
## API Client
|
|
585
|
-
|
|
586
|
-
HTTP client with typed endpoints, caching, and tag-based invalidation:
|
|
587
|
-
|
|
588
|
-
```typescript
|
|
589
|
-
import { ApiClient } from 'synapse-storage/api'
|
|
590
|
-
import { MemoryStorage } from 'synapse-storage/core'
|
|
591
|
-
|
|
592
|
-
const cacheStorage = new MemoryStorage<Record<string, any>>({
|
|
593
|
-
name: 'api-cache',
|
|
594
|
-
initialState: {},
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
const api = new ApiClient({
|
|
598
|
-
storage: cacheStorage,
|
|
599
|
-
baseQuery: {
|
|
600
|
-
baseUrl: 'https://api.example.com',
|
|
601
|
-
timeout: 10000,
|
|
602
|
-
prepareHeaders: async (headers, context) => {
|
|
603
|
-
headers.set('Authorization', `Bearer ${token}`)
|
|
604
|
-
return headers
|
|
605
|
-
},
|
|
606
|
-
},
|
|
607
|
-
cache: {
|
|
608
|
-
ttl: 60000,
|
|
609
|
-
cleanup: { enabled: true, interval: 120000 },
|
|
610
|
-
invalidateOnError: true,
|
|
611
|
-
},
|
|
612
|
-
endpoints: async (create) => ({
|
|
613
|
-
getUsers: create<{ limit?: number }, UsersResponse>({
|
|
614
|
-
request: (params) => ({
|
|
615
|
-
path: '/users',
|
|
616
|
-
method: 'GET',
|
|
617
|
-
query: params,
|
|
618
|
-
}),
|
|
619
|
-
cache: { ttl: 120000 },
|
|
620
|
-
tags: ['users'],
|
|
621
|
-
}),
|
|
622
|
-
createUser: create<CreateUserInput, User>({
|
|
623
|
-
request: (params) => ({
|
|
624
|
-
path: '/users',
|
|
625
|
-
method: 'POST',
|
|
626
|
-
body: params,
|
|
627
|
-
}),
|
|
628
|
-
invalidatesTags: ['users'],
|
|
629
|
-
cache: false,
|
|
630
|
-
}),
|
|
27
|
+
createSelectorsFn: (s) => ({
|
|
28
|
+
count: s.createSelector((state) => state.count),
|
|
631
29
|
}),
|
|
632
30
|
})
|
|
633
|
-
|
|
634
|
-
await cacheStorage.initialize()
|
|
635
|
-
await api.init()
|
|
636
|
-
|
|
637
|
-
// Simple request
|
|
638
|
-
const result = await api.request('getUsers', { limit: 10 })
|
|
639
|
-
if (result.ok) console.log(result.data, result.fromCache)
|
|
640
|
-
|
|
641
|
-
// Endpoint-level subscription
|
|
642
|
-
const endpoints = api.getEndpoints()
|
|
643
|
-
const req = endpoints.getUsers.request({ limit: 10 })
|
|
644
|
-
|
|
645
|
-
req.subscribe((state) => {
|
|
646
|
-
// state.status: 'idle' | 'loading' | 'success' | 'error'
|
|
647
|
-
// state.data, state.error, state.fromCache
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
const result = await req.wait()
|
|
651
|
-
req.abort()
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
---
|
|
655
|
-
|
|
656
|
-
## EventBus
|
|
657
|
-
|
|
658
|
-
Decoupled communication between modules:
|
|
659
|
-
|
|
660
|
-
```typescript
|
|
661
|
-
import { createEventBus } from 'synapse-storage/utils'
|
|
662
|
-
|
|
663
|
-
const eventBus = await createEventBus({
|
|
664
|
-
name: 'app-events',
|
|
665
|
-
autoCleanup: true,
|
|
666
|
-
maxEvents: 1000,
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
// Publish
|
|
670
|
-
await eventBus.actions.publish({
|
|
671
|
-
event: 'USER_UPDATED',
|
|
672
|
-
data: { userId: 123 },
|
|
673
|
-
metadata: { priority: 'high', ttl: 60000 },
|
|
674
|
-
})
|
|
675
|
-
|
|
676
|
-
// Subscribe (supports wildcards)
|
|
677
|
-
const { unsubscribe } = await eventBus.actions.subscribe({
|
|
678
|
-
eventPattern: 'USER_*',
|
|
679
|
-
handler: (data, event) => console.log(event.event, data),
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
// History
|
|
683
|
-
const history = await eventBus.actions.getEventHistory({
|
|
684
|
-
eventType: 'USER_UPDATED',
|
|
685
|
-
limit: 10,
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
await eventBus.destroy()
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
---
|
|
692
|
-
|
|
693
|
-
## Singleton Pattern
|
|
694
|
-
|
|
695
|
-
Share storage instances across components:
|
|
696
|
-
|
|
697
|
-
```typescript
|
|
698
|
-
import { MemoryStorage, ConfigMergeStrategy } from 'synapse-storage/core'
|
|
699
|
-
|
|
700
|
-
// Component A
|
|
701
|
-
const storage1 = new MemoryStorage({
|
|
702
|
-
name: 'shared',
|
|
703
|
-
singleton: {
|
|
704
|
-
enabled: true,
|
|
705
|
-
mergeStrategy: ConfigMergeStrategy.FIRST_WINS,
|
|
706
|
-
},
|
|
707
|
-
initialState: { count: 0 },
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
// Component B — gets the same instance
|
|
711
|
-
const storage2 = new MemoryStorage({
|
|
712
|
-
name: 'shared',
|
|
713
|
-
singleton: { enabled: true },
|
|
714
|
-
initialState: { count: 99 }, // ignored (FIRST_WINS)
|
|
715
|
-
})
|
|
716
|
-
|
|
717
|
-
storage1 === storage2 // true
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
Merge strategies: `FIRST_WINS`, `DEEP_MERGE`, `OVERRIDE`, `WARN_AND_USE_FIRST`, `STRICT` (throws).
|
|
721
|
-
|
|
722
|
-
---
|
|
723
|
-
|
|
724
|
-
## Storage Lifecycle
|
|
725
|
-
|
|
726
|
-
```typescript
|
|
727
|
-
await storage.initialize()
|
|
728
|
-
await storage.waitForReady()
|
|
729
|
-
|
|
730
|
-
storage.initStatus // { status: 'ready' | 'loading' | 'error' | 'idle' }
|
|
731
|
-
|
|
732
|
-
const unsub = storage.onStatusChange((status) => console.log(status))
|
|
733
|
-
|
|
734
|
-
await storage.destroy()
|
|
735
31
|
```
|
|
736
32
|
|
|
737
|
-
|
|
33
|
+
## Key Features
|
|
738
34
|
|
|
739
|
-
|
|
35
|
+
- **Sync & Async Storage** — MemoryStorage, LocalStorage (synchronous), IndexedDB (async) with unified API
|
|
36
|
+
- **Selectors** — memoized computed values with dependency tracking
|
|
37
|
+
- **Immer-like Updates** — mutate state directly inside `update()` callbacks
|
|
38
|
+
- **API Client** — HTTP client with tag-based caching and invalidation
|
|
39
|
+
- **React Integration** — hooks on `useSyncExternalStore` (Concurrent Mode safe)
|
|
40
|
+
- **RxJS Effects** — dispatchers, effects, and watchers (Redux-Observable style)
|
|
41
|
+
- **Middleware & Plugins** — extensible sync/async pipelines
|
|
42
|
+
- **EventBus** — decoupled inter-module communication with wildcards
|
|
43
|
+
- **Cross-tab Sync** — BroadcastChannel middleware for multi-tab state
|
|
740
44
|
|
|
741
|
-
|
|
742
|
-
- [YouTube](https://www.youtube.com/channel/UCGENI_i4qmBkPp93P2HvvGw)
|
|
45
|
+
## Documentation
|
|
743
46
|
|
|
744
|
-
|
|
47
|
+
Full documentation, API reference, and examples available on [GitHub](https://github.com/Vlad92msk/synapse).
|
|
745
48
|
|
|
746
49
|
## License
|
|
747
50
|
|