synapse-storage 3.0.10 → 3.0.11
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 +75 -1054
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,75 +1,71 @@
|
|
|
1
1
|
# Synapse Storage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **🇺🇸 English** | [🇷🇺 Русский](./docs/ru/README.md)
|
|
4
|
+
|
|
5
|
+
State management toolkit + API client
|
|
4
6
|
|
|
5
7
|
[](https://badge.fury.io/js/synapse-storage)
|
|
6
8
|
[](https://bundlephobia.com/package/synapse-storage)
|
|
7
9
|
[](https://www.typescriptlang.org/)
|
|
8
10
|
[](https://rxjs.dev/)
|
|
9
11
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
- **Не привязан к конкретному фреймворку**: Вы можете использовать Synapse в контексте любого фреймворка или независимо от него
|
|
13
|
-
- **Разнообразные адаптеры хранилищ**: Выбирайте между Memory, LocalStorage или IndexedDB в зависимости от ваших потребностей
|
|
14
|
-
- **Различный способ получения данных**: Создавайте и комбинируйте селекторы для вычисляемых значений на основе состояния в стиле Redux или просто подписывайтесь на конкретное свойство в хранилище
|
|
15
|
-
- Возможность создания вычисляемых селекторов в стиле Redux
|
|
16
|
-
- Возможность прямой подписки на конкретное свойство в хранилище
|
|
17
|
-
- Возможность подписки на реактивное состояние
|
|
18
|
-
- **Надежный API-клиент**: Создайте удобный API-клиент для вашего приложения (похож на RTK Query)
|
|
19
|
-
- **Поддержка middleware**: Расширяйте функциональность с помощью пользовательских middleware
|
|
20
|
-
- **Система плагинов**: Используйте готовые или создавайте собственные плагины для расширения функциональности
|
|
21
|
-
- **Отдельные возможности для реактивного подхода**: Возможность гибкой работы с api-запросами в стиле Redux-Observable и RxJS
|
|
12
|
+
## ✨ Key Features
|
|
22
13
|
|
|
23
|
-
|
|
14
|
+
- 🚀 **Framework Agnostic** - You can use Synapse with any framework or independently
|
|
15
|
+
- 💾 **Various Storage Adapters** - Memory, LocalStorage, IndexedDB
|
|
16
|
+
- 🧮 **Different Ways to Access Data** - Computed values with memoization
|
|
17
|
+
- Ability to create Redux-style computed selectors
|
|
18
|
+
- Ability to directly subscribe to specific properties in storage
|
|
19
|
+
- Ability to subscribe to reactive state
|
|
20
|
+
- 🌐 **API Client Creation** - HTTP client with caching capabilities (Similar to RTK Query)
|
|
21
|
+
- ⚛️ **React** - Several convenient hooks for React
|
|
22
|
+
- ⚡ **RxJS** - Ability to create Redux-Observable style effects
|
|
23
|
+
- ⚙️ **Custom Middleware Support** - Ability to extend storage functionality with custom middlewares
|
|
24
|
+
- 🔌 **Custom Plugin Support** - Ability to extend storage functionality with custom plugins
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
---
|
|
27
|
+
## Author
|
|
26
28
|
|
|
29
|
+
**Vladislav** — Senior Frontend Developer (React, TypeScript)
|
|
27
30
|
|
|
28
|
-
> ### 🔎
|
|
31
|
+
> ### 🔎 Currently looking for new career opportunities!
|
|
29
32
|
>
|
|
30
33
|
> [GitHub](https://github.com/Vlad92msk/) | [LinkedIn](https://www.linkedin.com/in/vlad-firsov/)
|
|
31
34
|
|
|
32
|
-
> ### Подробные примеры:
|
|
33
|
-
>[GitHub](https://github.com/Vlad92msk/synapse-examples)
|
|
34
|
-
> |
|
|
35
|
-
>[YouTube](https://www.youtube.com/channel/UCGENI_i4qmBkPp98P2HvvGw)
|
|
36
|
-
|
|
37
35
|
---
|
|
38
|
-
*PS:
|
|
39
|
-
|
|
40
|
-
|
|
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*
|
|
41
39
|
---
|
|
42
40
|
|
|
43
|
-
## 📦
|
|
41
|
+
## 📦 Installation
|
|
44
42
|
|
|
45
43
|
```bash
|
|
46
44
|
npm install synapse-storage
|
|
47
45
|
```
|
|
48
46
|
|
|
49
47
|
```bash
|
|
50
|
-
#
|
|
48
|
+
# For reactive capabilities
|
|
51
49
|
npm install rxjs
|
|
52
50
|
|
|
53
|
-
#
|
|
51
|
+
# For React integration
|
|
54
52
|
npm install react react-dom
|
|
55
53
|
|
|
56
|
-
#
|
|
54
|
+
# All at once for full functionality
|
|
57
55
|
npm install synapse-storage rxjs react react-dom
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
|
63
|
-
|
|
64
|
-
| `synapse-storage/
|
|
65
|
-
| `synapse-storage/
|
|
66
|
-
| `synapse-storage/
|
|
67
|
-
| `synapse-storage/api` | HTTP клиент | Нет |
|
|
68
|
-
| `synapse-storage/utils` | Утилиты | Нет |
|
|
58
|
+
| Module | Description | Dependencies |
|
|
59
|
+
|--------|-------------|--------------|
|
|
60
|
+
| `synapse-storage/core` | base | - |
|
|
61
|
+
| `synapse-storage/react` | React | React 18+ |
|
|
62
|
+
| `synapse-storage/reactive` | RxJS | RxJS 7.8.2+ |
|
|
63
|
+
| `synapse-storage/api` | HTTP client | - |
|
|
64
|
+
| `synapse-storage/utils` | Utils | - |
|
|
69
65
|
|
|
70
|
-
> **💡
|
|
66
|
+
> **💡 Tip:** Import only the modules you need for optimal bundle size
|
|
71
67
|
|
|
72
|
-
###
|
|
68
|
+
### tsconfig.json:
|
|
73
69
|
```json
|
|
74
70
|
{
|
|
75
71
|
"compilerOptions": {
|
|
@@ -79,1028 +75,53 @@ npm install synapse-storage rxjs react react-dom
|
|
|
79
75
|
}
|
|
80
76
|
}
|
|
81
77
|
```
|
|
82
|
-
|
|
83
|
-
Импорты:
|
|
84
|
-
```typescript
|
|
85
|
-
// Инструменты создания и управления хранилищем
|
|
86
|
-
import {
|
|
87
|
-
// Хранилища
|
|
88
|
-
MemoryStorage,
|
|
89
|
-
IndexedDBStorage,
|
|
90
|
-
LocalStorage,
|
|
91
|
-
|
|
92
|
-
// Интерфейсы для хранилищ
|
|
93
|
-
IStorage,
|
|
94
|
-
|
|
95
|
-
// middleware для хранилища
|
|
96
|
-
broadcastMiddleware,
|
|
97
|
-
|
|
98
|
-
// Для создания кастомных плагинов хранилища
|
|
99
|
-
StoragePluginModule,
|
|
100
|
-
IStoragePlugin,
|
|
101
|
-
PluginContext,
|
|
102
|
-
StorageKeyType,
|
|
103
|
-
|
|
104
|
-
// Для создания кастомных middlewares хранилища
|
|
105
|
-
Middleware,
|
|
106
|
-
MiddlewareAPI,
|
|
107
|
-
NextFunction,
|
|
108
|
-
|
|
109
|
-
// Модуль создания вычисляемых селекторов в Redux стиле
|
|
110
|
-
SelectorModule,
|
|
111
|
-
ISelectorModule
|
|
112
|
-
} from 'synapse-storage/core'
|
|
113
|
-
|
|
114
|
-
// Инструменты для использования реактивного подхода (немного похоже на Redux-Observable)
|
|
115
|
-
import {
|
|
116
|
-
// Инструменты для создания Dispatcher
|
|
117
|
-
createDispatcher,
|
|
118
|
-
loggerDispatcherMiddleware,
|
|
119
|
-
|
|
120
|
-
// Инструменты для создания Effects (напоминает Redux-Observable)
|
|
121
|
-
EffectsModule,
|
|
122
|
-
combineEffects,
|
|
123
|
-
createEffect,
|
|
124
|
-
ofType,
|
|
125
|
-
ofTypes,
|
|
126
|
-
selectorMap,
|
|
127
|
-
validateMap
|
|
128
|
-
} from 'synapse-storage/reactive';
|
|
129
|
-
|
|
130
|
-
// Инструменты для работы с api
|
|
131
|
-
import { ApiClient, ResponseFormat } from 'synapse-storage/api'
|
|
132
|
-
|
|
133
|
-
// Несколько инструментов для удобного использования в React
|
|
134
|
-
import { useStorageSubscribe, useSelector, createSynapseCtx } from 'synapse-storage/react'
|
|
135
|
-
|
|
136
|
-
import { createSynapse } from 'synapse-storage/utils'
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
## Базовое использование
|
|
141
|
-
|
|
142
|
-
### Создание зранилищ
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
const counter1 = await new MemoryStorage<Counter>({
|
|
146
|
-
name: 'counter1',
|
|
147
|
-
initialState: {
|
|
148
|
-
value: 100,
|
|
149
|
-
},
|
|
150
|
-
}).initialize()
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
const counter2 = await new LocalStorage<Counter>({
|
|
156
|
-
name: 'counter2',
|
|
157
|
-
initialState: { value: 100 },
|
|
158
|
-
}).initialize()
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
const { counter3 } = await IndexedDBStorage.createStorages<{ counter3: Counter }>(
|
|
164
|
-
'example1', // Название базы данных в indexDB
|
|
165
|
-
// Таблицы:
|
|
166
|
-
{
|
|
167
|
-
counter3: {
|
|
168
|
-
name: 'counter3',
|
|
169
|
-
initialState: { value: 99 },
|
|
170
|
-
// eventEmitter: ,
|
|
171
|
-
// initialState: ,
|
|
172
|
-
// middlewares: ,
|
|
173
|
-
// pluginExecutor: ,
|
|
174
|
-
},
|
|
175
|
-
// Другие объекты (хранилища в текущей базе данных)
|
|
176
|
-
}
|
|
177
|
-
)
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Способы изменения значений (основные)
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
const updateCounter1 = async () => {
|
|
184
|
-
await counter1.update((state) => {
|
|
185
|
-
state.value = state.value + 1
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const updateCounter2 = async () => {
|
|
190
|
-
await counter2.set('value', counter2ValueSelectorValue! + 1)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const updateCounter3 = async () => {
|
|
194
|
-
counter3.set('value', counter3ValueSelectorValue! + 1)
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
### Создание подписок
|
|
200
|
-
|
|
201
|
-
> **💡 Совет:**
|
|
202
|
-
При создании подписок с помощью subscribe или subscribeToAll лучше не забывать вызывать функцию отписки
|
|
203
|
-
>
|
|
204
|
-
```jsx
|
|
205
|
-
const [counter1Value, setCounter1Value] = useState(0)
|
|
206
|
-
const [counter2Value, setCounter2Value] = useState(0)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
// Подписка через колбэк
|
|
211
|
-
counter1.subscribe((state) => state.value, (value) => {
|
|
212
|
-
setCounter1Value(value)
|
|
213
|
-
})
|
|
214
|
-
// Подписка через путь (может быть типа 'user.settings.theme')
|
|
215
|
-
counter2.subscribe('value', (value) => {
|
|
216
|
-
setCounter2Value(value)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
// Подписка на все события
|
|
220
|
-
counter1.subscribeToAll((event) => {
|
|
221
|
-
console.log('event', event)
|
|
222
|
-
// Здесь мы получим объект:
|
|
223
|
-
// changedPaths:['value'] // все пути по которым были вызваны изменения (['prop1.prop2', 'prop44.prop.555.prop.666'])
|
|
224
|
-
// key:['value'] // Корневые ключи в хранилище которые вкоторых были изменения
|
|
225
|
-
// type:"storage:update" // Тип операции
|
|
226
|
-
// value: {value: 101} // Новый state
|
|
227
|
-
})
|
|
228
|
-
}, [])
|
|
229
|
-
// Для React через специальный селектор
|
|
230
|
-
const counter3Value = useStorageSubscribe(counter3, (state) => state.value)
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### Создание вычисляемых подписок в стиле Redux
|
|
234
|
-
```typescript
|
|
235
|
-
const counter1Selector = new SelectorModule(counter1)
|
|
236
|
-
const counter2Selector = new SelectorModule(counter2)
|
|
237
|
-
const counter3Selector = new SelectorModule(counter3)
|
|
238
|
-
|
|
239
|
-
const counter1ValueSelector = counter1Selector.createSelector((s) => s.value)
|
|
240
|
-
const counter2ValueSelector = counter2Selector.createSelector((s) => s.value)
|
|
241
|
-
const counter3ValueSelector = counter3Selector.createSelector((s) => s.value)
|
|
242
|
-
|
|
243
|
-
const sum = counter3Selector.createSelector(
|
|
244
|
-
[counter1ValueSelector, counter2ValueSelector, counter3ValueSelector],
|
|
245
|
-
(a,b,c) => a + b + c,
|
|
246
|
-
// Опционально:
|
|
247
|
-
// {
|
|
248
|
-
// equals: , // Функция сравнения
|
|
249
|
-
// name: 'doubledCountSelector' // Имя селектора
|
|
250
|
-
// }
|
|
251
|
-
)
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Получение значений из вычисляемых селекторов
|
|
255
|
-
```jsx
|
|
256
|
-
// Нативный способ
|
|
257
78
|
|
|
258
|
-
|
|
259
|
-
const sumValueSelector = sum.select().then(value => value)
|
|
79
|
+
## 📚 Documentation
|
|
260
80
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Для React через специальный селектор
|
|
275
|
-
const counter1ValueSelectorValue = useSelector(counter1ValueSelector)
|
|
276
|
-
const counter2ValueSelectorValue = useSelector(counter2ValueSelector)
|
|
277
|
-
const counter3ValueSelectorValue = useSelector(counter3ValueSelector,
|
|
278
|
-
// Можно указать доп опции
|
|
279
|
-
{
|
|
280
|
-
initialValue: 99,
|
|
281
|
-
withLoading: true,
|
|
282
|
-
equals: (a, b) => a !== b
|
|
283
|
-
})
|
|
284
|
-
// Тогда получать значение так
|
|
285
|
-
counter3ValueSelectorValue.data
|
|
286
|
-
counter3ValueSelectorValue.isLoading
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
### Middlewares
|
|
291
|
-
|
|
292
|
-
> **💡 Важно:**
|
|
293
|
-
Порядок имеет значение!<br/>
|
|
294
|
-
Action → BroadcastMiddleware → ShallowCompare → Batching → Base Operation
|
|
295
|
-
>
|
|
296
|
-
```typescript
|
|
297
|
-
const counter1 = await new MemoryStorage<Counter>({
|
|
298
|
-
name: 'counter1',
|
|
299
|
-
initialState: {
|
|
300
|
-
value: 100,
|
|
301
|
-
},
|
|
302
|
-
middlewares: () => {
|
|
303
|
-
const broadcast = broadcastMiddleware({
|
|
304
|
-
storageType: 'memory', // <-- Важно правильно указывать тип хранилища
|
|
305
|
-
storageName: 'counter1' // <-- Желательно правильно указывать имя хранилища
|
|
306
|
-
})
|
|
307
|
-
return [broadcast]
|
|
308
|
-
}
|
|
309
|
-
}).initialize()
|
|
310
|
-
|
|
311
|
-
const counter2 = await new LocalStorage<Counter>({
|
|
312
|
-
name: 'counter2',
|
|
313
|
-
initialState: { value: 100 },
|
|
314
|
-
middlewares: (getDefaultMiddleware) => {
|
|
315
|
-
const { shallowCompare } = getDefaultMiddleware()
|
|
316
|
-
|
|
317
|
-
const broadcast = broadcastMiddleware({
|
|
318
|
-
storageType: 'localStorage',
|
|
319
|
-
storageName: 'counter2'
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
return [broadcast, shallowCompare()]
|
|
323
|
-
}
|
|
324
|
-
}).initialize()
|
|
325
|
-
|
|
326
|
-
const { counter3 } = await IndexedDBStorage.createStorages<{ counter3: Counter }>(
|
|
327
|
-
'example1', {
|
|
328
|
-
counter3: {
|
|
329
|
-
name: 'counter3',
|
|
330
|
-
initialState: { value: 99 },
|
|
331
|
-
middlewares: (getDefaultMiddleware) => {
|
|
332
|
-
const { batching } = getDefaultMiddleware()
|
|
333
|
-
|
|
334
|
-
const broadcast = broadcastMiddleware({
|
|
335
|
-
storageType: 'indexedDB',
|
|
336
|
-
storageName: 'counter3'
|
|
337
|
-
})
|
|
338
|
-
return [
|
|
339
|
-
broadcast,
|
|
340
|
-
batching({
|
|
341
|
-
batchSize: 20,
|
|
342
|
-
batchDelay: 200
|
|
343
|
-
})
|
|
344
|
-
]
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
)
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
// Поверхностное сравнение
|
|
353
|
-
const updateCounter2 = async () => {
|
|
354
|
-
await counter2.set('value', counter2ValueSelectorValue! + 1) // Это будет применено
|
|
355
|
-
await counter2.set('value', counter2ValueSelectorValue! + 1) // |
|
|
356
|
-
await counter2.set('value', counter2ValueSelectorValue! + 1) // | Не будут вызваны так как payload не изменился
|
|
357
|
-
await counter2.set('value', counter2ValueSelectorValue! + 1) // |
|
|
358
|
-
await counter2.set('value', counter2ValueSelectorValue! + 1) // |
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Батчинг
|
|
362
|
-
// !! работает только для методов без await
|
|
363
|
-
const updateCounter3 = async () => {
|
|
364
|
-
counter3.set('value', counter3ValueSelectorValue! + 1) // | игнорируется
|
|
365
|
-
counter3.set('value', counter3ValueSelectorValue! + 1) // | игнорируется
|
|
366
|
-
counter3.set('value', counter3ValueSelectorValue! + 1) // | игнорируется
|
|
367
|
-
counter3.set('value', counter3ValueSelectorValue! + 1) // | игнорируется
|
|
368
|
-
counter3.set('value', counter3ValueSelectorValue! + 10)// | < --- будет применено только это
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### Иное
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
const counter1 = await new MemoryStorage<Counter>({
|
|
376
|
-
name: 'counter1',
|
|
377
|
-
initialState: {
|
|
378
|
-
value: 100,
|
|
379
|
-
},
|
|
380
|
-
middlewares: () => {
|
|
381
|
-
const broadcast = broadcastMiddleware({
|
|
382
|
-
storageType: 'memory',
|
|
383
|
-
storageName: 'counter1'
|
|
384
|
-
})
|
|
385
|
-
return [broadcast]
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
undefined, // Менеджер плагинов имплементирующий IPluginExecutor
|
|
389
|
-
{
|
|
390
|
-
emit: async (event: StorageEvent) => { // любой EventEmitter имплементирующий IEventEmitter
|
|
391
|
-
console.log('event', event)
|
|
392
|
-
// event будет содержать следующую инф:
|
|
393
|
-
// type: "storage:update"
|
|
394
|
-
// metadata:
|
|
395
|
-
// storageName:"counter1"
|
|
396
|
-
// timestamp: 1748581282102
|
|
397
|
-
// payload:
|
|
398
|
-
// changedPaths:['value']
|
|
399
|
-
// key: ['value']
|
|
400
|
-
// state: {value: 101}
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
console // любой логгер имплементирующий ILogger
|
|
404
|
-
).initialize()
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
## API-клиент
|
|
408
|
-
|
|
409
|
-
Synapse включает в себя API-клиент с поддержкой кеширования:
|
|
410
|
-
|
|
411
|
-
```typescript
|
|
412
|
-
const api = new ApiClient({
|
|
413
|
-
// Настройка кеширования запросов
|
|
414
|
-
cacheableHeaderKeys: ['X-Auth-Token'],
|
|
415
|
-
storage: API, // Передаем готовое экземпляр готового хранилища
|
|
416
|
-
// Настройки кеша
|
|
417
|
-
cache: {
|
|
418
|
-
ttl: 5 * 60 * 1000, // Время жизни кеша: 5 минут
|
|
419
|
-
invalidateOnError: true, // Инвалидация кеша при ошибке
|
|
420
|
-
cleanup: {
|
|
421
|
-
enabled: true, // Периодическая очистка кеша
|
|
422
|
-
interval: 10 * 60 * 1000, // Интервал очистки: 10 минут
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
// Базовые настройки запроса
|
|
426
|
-
baseQuery: {
|
|
427
|
-
baseUrl: 'https://api.example.com',
|
|
428
|
-
timeout: 10000, // 10 секунд
|
|
429
|
-
prepareHeaders: async (headers, context) => {
|
|
430
|
-
// Установка заголовков
|
|
431
|
-
headers.set('X-Auth-Token', 'some-token');
|
|
432
|
-
// Получение данных из хранилища или cookies
|
|
433
|
-
const token = context.getCookie('token');
|
|
434
|
-
if (token) {
|
|
435
|
-
headers.set('Authorization', `Bearer ${token}`);
|
|
436
|
-
}
|
|
437
|
-
return headers;
|
|
438
|
-
},
|
|
439
|
-
credentials: 'same-origin',
|
|
440
|
-
},
|
|
441
|
-
// Определение эндпоинтов
|
|
442
|
-
endpoints: async (create) => ({
|
|
443
|
-
getData: create({
|
|
444
|
-
request: (params) => ({
|
|
445
|
-
path: '/data',
|
|
446
|
-
method: 'GET',
|
|
447
|
-
query: params,
|
|
448
|
-
}),
|
|
449
|
-
// Можно указать специфичные настройки кеша для эндпоинта
|
|
450
|
-
cache: {
|
|
451
|
-
ttl: 60 * 1000, // 1 минута для этого эндпоинта
|
|
452
|
-
},
|
|
453
|
-
}),
|
|
454
|
-
}),
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// Инициализация
|
|
458
|
-
const myApi = await api.init();
|
|
459
|
-
|
|
460
|
-
// Использование с подпиской на состояние запроса
|
|
461
|
-
const request = myApi.getEndpoints().getData.request({ id: 1 });
|
|
462
|
-
|
|
463
|
-
// Вариант 1: Подписка на изменения состояния запроса
|
|
464
|
-
request.subscribe((state) => {
|
|
465
|
-
switch (state.status) {
|
|
466
|
-
case 'idle':
|
|
467
|
-
console.log('Запрос неактивен');
|
|
468
|
-
break;
|
|
469
|
-
case 'loading':
|
|
470
|
-
console.log('Загрузка данных...');
|
|
471
|
-
break;
|
|
472
|
-
case 'success':
|
|
473
|
-
console.log('Данные получены:', state.data);
|
|
474
|
-
break;
|
|
475
|
-
case 'error':
|
|
476
|
-
console.log('Ошибка:', state.error);
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// Вариант 2: Ожидание результата запроса
|
|
482
|
-
const response = await request.wait();
|
|
483
|
-
|
|
484
|
-
// Вариант 3: Ожидание с колбеками для разных состояний
|
|
485
|
-
request.waitWithCallbacks({
|
|
486
|
-
loading: () => console.log('Загрузка...'),
|
|
487
|
-
success: (data) => console.log('Данные:', data),
|
|
488
|
-
error: (error) => console.error('Ошибка:', error),
|
|
489
|
-
});
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
## Реактивный подход
|
|
493
|
-
Synapse предоставляет инструменты для использования реактивного подхода, напоминающий Redux-Observable.
|
|
494
|
-
|
|
495
|
-
Пример создания Диспетчера:
|
|
496
|
-
```typescript
|
|
497
|
-
import { createDispatcher, loggerDispatcherMiddleware } from 'synapse-storage/reactive'
|
|
498
|
-
import { PokemonStorage } from '../storages/pokemon.storage'
|
|
499
|
-
import { createPokemonAlertMiddleware } from '../middlewares/pokenon.middlewares'
|
|
500
|
-
import { Pokemon } from '../types'
|
|
501
|
-
|
|
502
|
-
// const myWorker = new Worker('path-to-my-worker')
|
|
503
|
-
|
|
504
|
-
export interface AlertPayload {
|
|
505
|
-
message: string
|
|
506
|
-
type: 'info' | 'warning' | 'error' | 'success'
|
|
507
|
-
duration?: number // Длительность показа в миллисекундах
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Функция для создания диспетчера
|
|
511
|
-
export function createPokemonDispatcher(storage: PokemonStorage) {
|
|
512
|
-
// Создаем middleware: логгер
|
|
513
|
-
const loggerMiddleware = loggerDispatcherMiddleware({
|
|
514
|
-
collapsed: true, // Сворачиваем группы в консоли для компактности
|
|
515
|
-
colors: {
|
|
516
|
-
title: '#3498db', // Кастомный синий цвет для заголовка
|
|
517
|
-
},
|
|
518
|
-
duration: true,
|
|
519
|
-
diff: true,
|
|
520
|
-
showFullState: true,
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
// Создаем middleware: alertM (просто для примера)
|
|
524
|
-
const alertM = createPokemonAlertMiddleware()
|
|
525
|
-
|
|
526
|
-
return createDispatcher({
|
|
527
|
-
storage,
|
|
528
|
-
middlewares: [loggerMiddleware, alertM],
|
|
529
|
-
}, (storage, { createWatcher, createAction }) => ({
|
|
530
|
-
// watcher`s
|
|
531
|
-
watchCurrentId: createWatcher({...}),
|
|
532
|
-
// сСобытия
|
|
533
|
-
loadPokemon: createAction<number, { id: number }>({...}),
|
|
534
|
-
loadPokemonRequest: createAction<number, { id: number }>({...}),
|
|
535
|
-
// Успешное получение данных
|
|
536
|
-
success: createAction<{ data?: Pokemon}, { data?: Pokemon }>({...}, {
|
|
537
|
-
// Функция мемоизации (пока не тестировал)
|
|
538
|
-
// memoize: (currentArgs: any[], previousArgs: any[], previousResult: any) => true,
|
|
539
|
-
// Веб-воркер для выполнения действия (пока не тестировал)
|
|
540
|
-
// worker: myWorker,
|
|
541
|
-
}),
|
|
542
|
-
failure: createAction<Error, { err: Error }>({...}),
|
|
543
|
-
next: createAction<void, { id: number }>({...}),
|
|
544
|
-
prev: createAction<void, { id: number }>({...}),
|
|
545
|
-
showAlert: createAction<AlertPayload, void>({...}),
|
|
546
|
-
}))
|
|
547
|
-
// Альтернативный вариант добавления:
|
|
548
|
-
// .use(logger)
|
|
549
|
-
// .use(alertM)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Экспортируем тип диспетчера
|
|
553
|
-
export type PokemonDispatcher = ReturnType<typeof createPokemonDispatcher>
|
|
554
|
-
```
|
|
81
|
+
- [📖 Main](./docs/ru/README.md)
|
|
82
|
+
- [🚀 Basic Usage](./docs/ru/basic-usage.md)
|
|
83
|
+
- [🧮 Redux-style Computed Selectors](./docs/ru/redux-selectors.md)
|
|
84
|
+
- [⚙️ Middlewares](./docs/ru/middlewares.md)
|
|
85
|
+
- [🌐 API Client](./docs/ru/api-client.md)
|
|
86
|
+
- ⚡ Reactive Approach
|
|
87
|
+
- [⚡ Creating Dispatcher](./docs/ru/create-dispatcher.md)
|
|
88
|
+
- [⚡ Creating Effects](./docs/ru/create-effects.md)
|
|
89
|
+
- [⚡ Creating Effects Module](./docs/ru/create-effects-module.md)
|
|
90
|
+
- [🛠️ createSynapse Utility](./docs/ru/create-synapse.md)
|
|
91
|
+
- [🔌 Creating Custom Plugins](./docs/ru/custom-plugins.md)
|
|
92
|
+
- [⚙️ Creating Custom Middlewares](./docs/ru/custom-middlewares.md)
|
|
93
|
+
- [📋 Additional](./docs/ru/additional.md)
|
|
555
94
|
|
|
556
|
-
|
|
557
|
-
```typescript
|
|
558
|
-
import { EMPTY, from, mapTo, of, tap } from 'rxjs'
|
|
559
|
-
import { catchError, map, switchMap } from 'rxjs/operators'
|
|
95
|
+
## 🎯 Examples
|
|
560
96
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
ofTypes, // Слушает несколько событий
|
|
564
|
-
createEffect, // Функция создания эффекта
|
|
565
|
-
combineEffects, // Объединяет несколько эффектов в один
|
|
566
|
-
selectorMap, // Выбор частей состояния с помощью селекторов (возвращает массив)
|
|
567
|
-
selectorObject, // Выбор частей состояния с помощью селекторов (возвращает объект)
|
|
568
|
-
validateMap // Оператор для удобной работы с запросом
|
|
569
|
-
} from 'synapse-storage/reactive'
|
|
570
|
-
import { pokemonEndpoints } from '../api.md'
|
|
571
|
-
import { AppConfig } from '../app.config'
|
|
572
|
-
import { PokemonDispatcher } from '../pokemon.dispatcher'
|
|
573
|
-
import { Pokemon, PokemonState } from '../types'
|
|
97
|
+
- [GitHub](https://github.com/Vlad92msk/synapse-examples)
|
|
98
|
+
- [YouTube](https://www.youtube.com/channel/UCGENI_i4qmBkPp98P2HvvGw)
|
|
574
99
|
|
|
575
|
-
// Определяем типы для наших эффектов
|
|
576
|
-
type DispatcherType = {
|
|
577
|
-
pokemonDispatcher: PokemonDispatcher
|
|
578
|
-
}
|
|
579
|
-
type ApiType = {
|
|
580
|
-
pokemonApi: typeof pokemonEndpoints
|
|
581
|
-
}
|
|
582
|
-
type ExternalStorages = {
|
|
583
|
-
core$: typeof coreSynapseIDB.state$
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
type Effect = ReturnType<typeof createEffect<
|
|
587
|
-
AboutUserUserInfo, // Тип текущего хранилища
|
|
588
|
-
DispatcherType, // Типы диспетчеров
|
|
589
|
-
ApiType, // Типы api
|
|
590
|
-
Record<string, void>, // Тип конфигурации
|
|
591
|
-
ExternalStorages // Типы внешних хранилищ потоков
|
|
592
|
-
>>
|
|
593
|
-
|
|
594
|
-
// Эффект для навигации
|
|
595
|
-
export const navigationEffect: Effect = createEffect((action$, state$, externalStorages, { pokemonDispatcher }, _, config) =>
|
|
596
|
-
action$.pipe(
|
|
597
|
-
ofTypes([pokemonDispatcher.dispatch.next, pokemonDispatcher.dispatch.prev]),
|
|
598
|
-
switchMap((action) => {
|
|
599
|
-
const { id } = action.payload
|
|
600
|
-
return of(() => pokemonDispatcher.dispatch.loadPokemon(id))
|
|
601
|
-
}),
|
|
602
|
-
),
|
|
603
|
-
)
|
|
604
|
-
|
|
605
|
-
// Эффект для отслеживания изменений ID
|
|
606
|
-
export const watchIdEffect: Effect = createEffect((action$, state$, externalStorages, { pokemonDispatcher }) =>
|
|
607
|
-
action$.pipe(
|
|
608
|
-
ofType(pokemonDispatcher.watchers.watchCurrentId),
|
|
609
|
-
withLatestFrom(
|
|
610
|
-
selectorMap(state$,
|
|
611
|
-
(state) => state.value
|
|
612
|
-
//... selectors
|
|
613
|
-
),
|
|
614
|
-
),
|
|
615
|
-
mapTo(null),
|
|
616
|
-
),
|
|
617
|
-
)
|
|
618
|
-
|
|
619
|
-
// Эффект для загрузки данных покемона
|
|
620
|
-
export const loadPokemonEffect: Effect = createEffect((
|
|
621
|
-
action$, // Поток событий
|
|
622
|
-
state$, // Поток состояния
|
|
623
|
-
externalStorages, // Потоки внешних хранилищ
|
|
624
|
-
{ pokemonDispatcher }, // Диспетчеры которые мы передали
|
|
625
|
-
{ pokemonApi }, // различные API которые мы передали
|
|
626
|
-
config // Конфигурация, которую мы передали
|
|
627
|
-
) =>
|
|
628
|
-
action$.pipe(
|
|
629
|
-
// Я использую отдельный action loadPokemon который уведомляет о намерении сделать запрос
|
|
630
|
-
// Для того, чтобы не устанавливать loading сразу
|
|
631
|
-
ofType(pokemonDispatcher.dispatch.loadPokemon),
|
|
632
|
-
withLatestFrom(
|
|
633
|
-
selectorMap(state$, (s) => s.currentId, (s) => s.currentId), // |
|
|
634
|
-
selectorMap(pokemon1State$, (s) => s.currentId, (s) => s.currentId), // | получает поток и селекторы, возвращает массив с результатами
|
|
635
|
-
selectorMap(pokemon1State$, (s) => s.currentId), // |
|
|
636
|
-
selectorObject(state$, { // |
|
|
637
|
-
currentId: (s) => s.currentId, // | получает поток и возвращает объект с результатами (для каждого свойства вызывается функция с состояниеме этого потого потока)
|
|
638
|
-
name: (s) => s.currentPokemon?.sprites, // |
|
|
639
|
-
}),
|
|
640
|
-
),
|
|
641
|
-
validateMap({
|
|
642
|
-
apiCall: ([action, [currentId], [externalId, externalId2], [external2Id], externalData]) => {
|
|
643
|
-
const { id } = action.payload
|
|
644
|
-
|
|
645
|
-
return from(
|
|
646
|
-
// Использую waitWithCallbacks чтобы иметь доступ к методу loading
|
|
647
|
-
pokemonApi.fetchPokemonById.request({ id }).waitWithCallbacks({
|
|
648
|
-
// Вызывается только тогда, когда запрос реально отправляется, а не берется из кэша
|
|
649
|
-
loading: (request) => {
|
|
650
|
-
// Именно в в этот момент установится loading и другая необходимая логика
|
|
651
|
-
pokemonDispatcher.dispatch.loadPokemonRequest(id)
|
|
652
|
-
},
|
|
653
|
-
// Можно использовать так:
|
|
654
|
-
// success: (data, request) => {
|
|
655
|
-
// console.log('SUCCESS', request)
|
|
656
|
-
// pokemonDispatcher.dispatch.success({ data })
|
|
657
|
-
// },
|
|
658
|
-
// error: (error, request) => {
|
|
659
|
-
// console.log('ERROR', error, request)
|
|
660
|
-
// pokemonDispatcher.dispatch.failure(error!)
|
|
661
|
-
// },
|
|
662
|
-
}),
|
|
663
|
-
// Можно более стандартным способом:
|
|
664
|
-
).pipe(
|
|
665
|
-
switchMap(({ data }) => {
|
|
666
|
-
return of(pokemonDispatcher.dispatch.success({ data }))
|
|
667
|
-
}),
|
|
668
|
-
catchError((err) => of(pokemonDispatcher.dispatch.failure(err))),
|
|
669
|
-
)
|
|
670
|
-
},
|
|
671
|
-
}),
|
|
672
|
-
),
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
// Объединяем все эффекты в один и экспортируем
|
|
676
|
-
export const pokemonEffects = combineEffects(
|
|
677
|
-
navigationEffect,
|
|
678
|
-
watchIdEffect,
|
|
679
|
-
loadPokemonEffect
|
|
680
|
-
)
|
|
681
|
-
```
|
|
682
100
|
---
|
|
683
|
-
## Пример организации кода и использования утилиты createSynapse
|
|
684
|
-
|
|
685
|
-
Предлагаемая структура файлов
|
|
686
|
-
|
|
687
|
-
```md
|
|
688
|
-
📦some-directory
|
|
689
|
-
└── 📂synapses
|
|
690
|
-
│ └── 📂core
|
|
691
|
-
│ │ ├── 📄core.dispatcher.ts
|
|
692
|
-
│ │ ├── 📄core.synapse.ts
|
|
693
|
-
│ │ └── ...
|
|
694
|
-
│ └── 📂user-info
|
|
695
|
-
│ │ ├── 📄user-info.context.tsx
|
|
696
|
-
│ │ ├── 📄user-info.dispatcher.ts
|
|
697
|
-
│ │ ├── 📄user-info.effects.ts
|
|
698
|
-
│ │ ├── 📄user-info.selectors.ts
|
|
699
|
-
│ │ ├── 📄user-info.store.ts
|
|
700
|
-
│ │ └── 📄user-info.synapse.ts
|
|
701
|
-
│ └──...
|
|
702
|
-
│
|
|
703
|
-
└── 📄indexdb.config.ts
|
|
704
|
-
```
|
|
705
|
-
|
|
706
|
-
```typescript
|
|
707
|
-
// user-info.store.ts
|
|
708
|
-
// === СОЗДАНИЕ ХРАНИЛИЩА НУЖНОГОТИПА ===
|
|
709
|
-
export async function createUserInfoStorage() {
|
|
710
|
-
return new MemoryStorage<AboutUserUserInfo>({
|
|
711
|
-
name: 'user-info',
|
|
712
|
-
initialState: {
|
|
713
|
-
userInfoInit: undefined,
|
|
714
|
-
isChangeActive: false,
|
|
715
|
-
fieldsInit: {},
|
|
716
|
-
fields: {},
|
|
717
|
-
},
|
|
718
|
-
}).initialize()
|
|
719
|
-
}
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
```typescript
|
|
723
|
-
// user-info.dispatcher.ts
|
|
724
|
-
// === СОЗДАНИЕ ДИСПЕТЧЕРА ===
|
|
725
|
-
|
|
726
|
-
import { IStorage } from 'synapse-storage/core'
|
|
727
|
-
import { createDispatcher, loggerDispatcherMiddleware } from 'synapse-storage/reactive'
|
|
728
|
-
|
|
729
|
-
export function createUserInfoDispatcher(store: IStorage<AboutUserUserInfo>) {
|
|
730
|
-
const loggerMiddleware = loggerDispatcherMiddleware({...})
|
|
731
|
-
|
|
732
|
-
return createDispatcher({ storage: store }, (storage, { createAction, createWatcher }) => ({
|
|
733
|
-
setCurrentUserProfile: createAction<UserProfileInfo, UserProfileInfo>({
|
|
734
|
-
type: 'setCurrentUserProfile',
|
|
735
|
-
// meta: ,
|
|
736
|
-
// action: async () => {...}),
|
|
737
|
-
}),
|
|
738
|
-
|
|
739
|
-
setActiveChange: createAction<void, void>({
|
|
740
|
-
type: 'setActiveChange',
|
|
741
|
-
// meta: ,
|
|
742
|
-
// action: async () => {...}),
|
|
743
|
-
})
|
|
744
|
-
// Другие диспетчеры ...
|
|
745
|
-
})).use(loggerMiddleware)
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
export type UserInfoDispatcher = ReturnType<typeof createUserInfoDispatcher>
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
```typescript
|
|
752
|
-
// user-info.dispatcher.ts
|
|
753
|
-
// === СОЗДАНИЕ СЕЛЕКТОРОВ ===
|
|
754
|
-
import { ISelectorModule } from 'synapse-storage/core'
|
|
755
|
-
|
|
756
|
-
export const createUserInfoSelectors = (selectorModule: ISelectorModule<AboutUserUserInfo>) => {
|
|
757
|
-
const currentUserProfile = selectorModule.createSelector((s) => s.userInfoInit)
|
|
758
|
-
const fieldsInit = selectorModule.createSelector((s) => s.fieldsInit)
|
|
759
|
-
|
|
760
|
-
const isChangeActive = selectorModule.createSelector((s) => s.isChangeActive)
|
|
761
|
-
|
|
762
|
-
const fields = selectorModule.createSelector((s) => s.fields)
|
|
763
|
-
// Для React
|
|
764
|
-
// Комопнент будет ререндериться всегда, когда меняется возвращаемое селектором значение
|
|
765
|
-
// Для уменьшения ререндеров советую создавать точечные селекторы
|
|
766
|
-
// Если для отображения information у вас отдельный компонент - лучше создать отдельный для него селектор
|
|
767
|
-
const fieldInformation = selectorModule.createSelector((s) => s.fields.information)
|
|
768
|
-
const fieldPosition = selectorModule.createSelector((s) => s.fields.position)
|
|
769
|
-
//...
|
|
770
|
-
|
|
771
|
-
return ({
|
|
772
|
-
currentUserProfile,
|
|
773
|
-
isChangeActive,
|
|
774
|
-
//...
|
|
775
|
-
})
|
|
776
|
-
}
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
```typescript
|
|
780
|
-
// user-info.effects.ts
|
|
781
|
-
// === СОЗДАНИЕ ЭФФЕКТОВ ===
|
|
782
|
-
import { EMPTY, from, of } from 'rxjs'
|
|
783
|
-
import { catchError, map } from 'rxjs/operators'
|
|
784
|
-
import { combineEffects, createEffect, ofType, validateMap } from 'synapse-storage/reactive'
|
|
785
|
-
|
|
786
|
-
type CurrentDispatchers = {
|
|
787
|
-
userInfoDispatcher: UserInfoDispatcher
|
|
788
|
-
coreIdbDispatcher: CoreDispatcher
|
|
789
|
-
};
|
|
790
|
-
type CurrentApis = {
|
|
791
|
-
userInfoAPi: typeof userInfoEndpoints
|
|
792
|
-
};
|
|
793
|
-
type ExternalStorages = {
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
type Effect = ReturnType<typeof createEffect<
|
|
797
|
-
AboutUserUserInfo, // Тип текущего хранилища
|
|
798
|
-
CurrentDispatchers, // Типы диспетчеров
|
|
799
|
-
CurrentApis, // Типы api
|
|
800
|
-
Record<string, void>, // Тип конфигурации
|
|
801
|
-
ExternalStorages // Типы внешних хранилищ потоков
|
|
802
|
-
>>
|
|
803
|
-
|
|
804
|
-
/**
|
|
805
|
-
* Добавляем полученный профиль пользователя в текущий СТор
|
|
806
|
-
*/
|
|
807
|
-
const loadUserInfoById: Effect = createEffect((action$, state$, { userInfoDispatcher, coreIdbDispatcher }) => action$.pipe(
|
|
808
|
-
// Подписываемся на изменения в стороннем Synapse
|
|
809
|
-
ofType(coreIdbDispatcher.watchers.watchCurrentUserProfile),
|
|
810
|
-
map((s) => {
|
|
811
|
-
if (!s.payload) return EMPTY
|
|
812
|
-
// Берем данные из стороннего Synapse и кладем в текущий
|
|
813
|
-
return userInfoDispatcher.dispatch.setCurrentUserProfile(s.payload)
|
|
814
|
-
}),
|
|
815
|
-
))
|
|
816
|
-
|
|
817
|
-
const updateUserProfile: Effect = createEffect((action$, state$, { userInfoDispatcher }, { userInfoAPi }) => action$.pipe(
|
|
818
|
-
ofType(userInfoDispatcher.dispatch.submit),
|
|
819
|
-
validateMap({
|
|
820
|
-
// Валидация перед запросом
|
|
821
|
-
validator: (action) => ({
|
|
822
|
-
skipAction: userInfoDispatcher.dispatch.reset(),
|
|
823
|
-
conditions: [Boolean(action.payload)]
|
|
824
|
-
}),
|
|
825
|
-
apiCall: (action) => {
|
|
826
|
-
return from(
|
|
827
|
-
userInfoAPi.getUserById.request({ user_id: 1 }).waitWithCallbacks({
|
|
828
|
-
// Вызывается только тогда, когда запрос реально отправляется, а не берется из кэша
|
|
829
|
-
loading: (request) => {
|
|
830
|
-
// Именно в в этот момент установится loading и другая необходимая логика
|
|
831
|
-
// userInfoDispatcher.dispatch.request(id)
|
|
832
|
-
},
|
|
833
|
-
// Можно использовать так:
|
|
834
|
-
success: (data, request) => {
|
|
835
|
-
// userInfoDispatcher.dispatch.success({ data })
|
|
836
|
-
},
|
|
837
|
-
error: (error, request) => {
|
|
838
|
-
// userInfoDispatcher.dispatch.failure(error!)
|
|
839
|
-
},
|
|
840
|
-
}),
|
|
841
|
-
)
|
|
842
|
-
},
|
|
843
|
-
}),
|
|
844
|
-
))
|
|
845
|
-
|
|
846
|
-
export const userInfoEffects = combineEffects(
|
|
847
|
-
loadUserInfoById,
|
|
848
|
-
updateUserProfile,
|
|
849
|
-
)
|
|
850
|
-
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
```typescript
|
|
854
|
-
// user-info.synapse.ts
|
|
855
|
-
// === СОЗДАНИЕ Synapse ===
|
|
856
|
-
import { createSynapse } from 'synapse-storage/utils'
|
|
857
|
-
import { createUserInfoDispatcher } from './user-info.dispatcher'
|
|
858
|
-
import { userInfoEffects } from './user-info.effects'
|
|
859
|
-
import { createUserInfoSelectors } from './user-info.selectors'
|
|
860
|
-
import { createUserInfoStorage } from './user-info.store'
|
|
861
|
-
import { userInfoEndpoints } from '../../api/user-info.api'
|
|
862
|
-
import { coreSynapseIDB } from '../core/core.synapse'
|
|
863
|
-
|
|
864
|
-
export const userInfoSynapse = await createSynapse({
|
|
865
|
-
// Передаем хранилище
|
|
866
|
-
// Это может быть
|
|
867
|
-
// 1 - Функция, которая фозвращает готовое ранилище
|
|
868
|
-
createStorageFn: createUserInfoStorage,
|
|
869
|
-
// 2 - Класс для создания хранилища (initialize() убдет вызван внутри)
|
|
870
|
-
// storage: new MemoryStorage<AboutUserUserInfo>({
|
|
871
|
-
// name: 'user-info',
|
|
872
|
-
// initialState: {
|
|
873
|
-
// userInfoInit: undefined,
|
|
874
|
-
// isChangeActive: false,
|
|
875
|
-
// fieldsInit: {},
|
|
876
|
-
// fields: {},
|
|
877
|
-
// },
|
|
878
|
-
// }),
|
|
879
|
-
// Функция создания диспетчеров (Опционально)
|
|
880
|
-
createDispatcherFn: createUserInfoDispatcher,
|
|
881
|
-
// Функция создания селекторов (Опционально)
|
|
882
|
-
createSelectorsFn: createUserInfoSelectors,
|
|
883
|
-
// Внешние селекторы (Опционально)
|
|
884
|
-
externalSelectors: {
|
|
885
|
-
// externalSelectors1: ...
|
|
886
|
-
},
|
|
887
|
-
// Конфигурация для эффектов (Опционально)
|
|
888
|
-
createEffectConfig: (userInfoDispatcher) => ({
|
|
889
|
-
// Диспетчеры для эффектов
|
|
890
|
-
dispatchers: {
|
|
891
|
-
userInfoDispatcher, // Текущий, для управления соственных хранилищем
|
|
892
|
-
coreIdbDispatcher: coreSynapseIDB.dispatcher, // Внешний, для взаиможействия с внешними хранилищами
|
|
893
|
-
//...
|
|
894
|
-
},
|
|
895
|
-
// Дополнительное АПИ по вашему усмотрения (у меня это API Clients)
|
|
896
|
-
api: {
|
|
897
|
-
userInfoAPi: userInfoEndpoints,
|
|
898
|
-
},
|
|
899
|
-
// Внешние состояния ввиде потоков, которые хотим использовать в эффектах
|
|
900
|
-
externalStates: {
|
|
901
|
-
pokemonState$: pokemon1State$,
|
|
902
|
-
core$: coreSynapseIDB.state$,
|
|
903
|
-
},
|
|
904
|
-
}),
|
|
905
|
-
// Эффекты которые будут запущены для этого synapse
|
|
906
|
-
effects: [userInfoEffects],
|
|
907
|
-
})
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
```tsx
|
|
911
|
-
// user-info.context.tsx
|
|
912
|
-
// === СОЗДАНИЕ React Context ===
|
|
913
|
-
import { createSynapseCtx } from 'synapse-storage/react'
|
|
914
|
-
import { userInfoSynapse } from './user-info.synapse'
|
|
915
101
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
102
|
+
## 📁 Documentation Structure
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
docs/
|
|
106
|
+
├── ru/ # 🇷🇺 Russian documentation
|
|
107
|
+
│ └── ...
|
|
108
|
+
│
|
|
109
|
+
└── en/ # 🇺🇸 English documentation
|
|
110
|
+
├── README.md # Main page
|
|
111
|
+
├── basic-usage.md # Basic Usage
|
|
112
|
+
├── storage-creation.md # Storage Creation
|
|
113
|
+
├── value-updates.md # Value Updates
|
|
114
|
+
├── subscriptions.md # Subscriptions
|
|
115
|
+
├── redux-selectors.md # Redux-style Selectors
|
|
116
|
+
├── middlewares.md # Middlewares
|
|
117
|
+
├── api-client.md # API Client
|
|
118
|
+
├── reactive.md # Reactive Approach
|
|
119
|
+
├── create-dispatcher.md # Create Dispatcher
|
|
120
|
+
├── create-effects.md # Create Effects
|
|
121
|
+
├── create-synapse.md # createSynapse Utility
|
|
122
|
+
├── custom-plugins.md # Custom Plugins
|
|
123
|
+
├── custom-middlewares.md # Custom Middlewares
|
|
124
|
+
└── additional.md # Additional
|
|
932
125
|
```
|
|
933
126
|
|
|
934
|
-
Таким образом вы можете резделить функционал на слои
|
|
935
|
-
|
|
936
|
-
|
|
937
127
|
---
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
## Создание пользовательского middleware
|
|
942
|
-
|
|
943
|
-
Synapse предоставляет две системы расширения функциональности: middleware и плагины. Они выполняют разные роли и имеют разную область применения.
|
|
944
|
-
|
|
945
|
-
Middleware в Synapse работают по принципу "цепочки обработчиков" и позволяют перехватывать любые операции хранилища. Каждое middleware может модифицировать действия до и после их обработки базовым хранилищем.
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
### Порядок выполнения middleware
|
|
949
|
-
|
|
950
|
-
Middleware выполняются в порядке их объявления в массиве:
|
|
951
|
-
1. Действие проходит через все middleware сверху вниз
|
|
952
|
-
2. Затем выполняется базовая операция хранилища
|
|
953
|
-
3. Результат проходит через middleware снизу вверх
|
|
954
|
-
|
|
955
|
-
```
|
|
956
|
-
Action → BroadcastMiddleware → ShallowCompare → Batching → Base Operation
|
|
957
|
-
Result ← BroadcastMiddleware ← ShallowCompare ← Batching ← Base Operation
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
#### Создание пользовательского middleware
|
|
961
|
-
|
|
962
|
-
```typescript
|
|
963
|
-
import { Middleware } from 'synapse-storage/core';
|
|
964
|
-
|
|
965
|
-
const loggingMiddleware = (): Middleware => ({
|
|
966
|
-
// Уникальное имя middleware
|
|
967
|
-
name: 'logging',
|
|
968
|
-
|
|
969
|
-
// Инициализация при добавлении middleware к хранилищу
|
|
970
|
-
setup: (api) => {
|
|
971
|
-
console.log('Logging middleware initialized');
|
|
972
|
-
},
|
|
973
|
-
|
|
974
|
-
// Основная логика перехвата и обработки действий
|
|
975
|
-
reducer: (api) => (next) => async (action) => {
|
|
976
|
-
console.log('Before action:', action);
|
|
977
|
-
|
|
978
|
-
try {
|
|
979
|
-
// Вызов следующего middleware в цепочке
|
|
980
|
-
const result = await next(action);
|
|
981
|
-
|
|
982
|
-
console.log('After action:', {
|
|
983
|
-
action,
|
|
984
|
-
result,
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
return result;
|
|
988
|
-
} catch (error) {
|
|
989
|
-
console.error('Action error:', error);
|
|
990
|
-
throw error;
|
|
991
|
-
}
|
|
992
|
-
},
|
|
993
|
-
|
|
994
|
-
// Очистка ресурсов при уничтожении хранилища
|
|
995
|
-
cleanup: () => {
|
|
996
|
-
console.log('Logging middleware cleanup');
|
|
997
|
-
}
|
|
998
|
-
});
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
### Плагины
|
|
1002
|
-
|
|
1003
|
-
Плагины в Synapse представляют собой систему обработчиков событий хранилища с определенным жизненным циклом. В отличие от middleware, они не формируют цепочку, а работают как независимые "наблюдатели" за операциями хранилища.
|
|
1004
|
-
|
|
1005
|
-
```typescript
|
|
1006
|
-
import { IStoragePlugin, StoragePluginModule } from 'synapse-storage/core';
|
|
1007
|
-
|
|
1008
|
-
// Создаем модуль плагинов
|
|
1009
|
-
const plugins = new StoragePluginModule(
|
|
1010
|
-
undefined, // Родительский модуль плагинов (опционально)
|
|
1011
|
-
console, // Логгер
|
|
1012
|
-
'appStorage' // Имя хранилища
|
|
1013
|
-
);
|
|
1014
|
-
|
|
1015
|
-
// Пример плагина валидации
|
|
1016
|
-
class ValidationPlugin implements IStoragePlugin {
|
|
1017
|
-
name = 'validation';
|
|
1018
|
-
private validators = new Map();
|
|
1019
|
-
private options: any;
|
|
1020
|
-
|
|
1021
|
-
constructor(options = {}) {
|
|
1022
|
-
this.options = options;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// Добавление правила валидации для ключа
|
|
1026
|
-
addValidator(key, validator) {
|
|
1027
|
-
this.validators.set(key, validator);
|
|
1028
|
-
return this;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
// Вызывается перед сохранением значения
|
|
1032
|
-
async onBeforeSet(value, context) {
|
|
1033
|
-
const { key } = context.metadata || {};
|
|
1034
|
-
|
|
1035
|
-
if (key && this.validators.has(key)) {
|
|
1036
|
-
const validator = this.validators.get(key);
|
|
1037
|
-
const result = validator(value);
|
|
1038
|
-
|
|
1039
|
-
if (!result.valid) {
|
|
1040
|
-
if (this.options.throwOnInvalid) {
|
|
1041
|
-
throw new Error(`Validation failed for ${key}: ${result.message}`);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
this.options.onValidationError?.(key, value, result.message);
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
return value;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Инициализация плагина
|
|
1052
|
-
async initialize() {
|
|
1053
|
-
console.log('Validation plugin initialized');
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// Очистка ресурсов
|
|
1057
|
-
async destroy() {
|
|
1058
|
-
this.validators.clear();
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Добавление плагинов в модуль
|
|
1063
|
-
await plugins.add(new ValidationPlugin({
|
|
1064
|
-
throwOnInvalid: true,
|
|
1065
|
-
onValidationError: (key, value, message) => {
|
|
1066
|
-
console.error(`Validation error: ${message}`);
|
|
1067
|
-
}
|
|
1068
|
-
}));
|
|
1069
|
-
|
|
1070
|
-
// Создание хранилища с плагинами
|
|
1071
|
-
const storage = await new MemoryStorage(
|
|
1072
|
-
{ name: 'app-storage' },
|
|
1073
|
-
plugins // Передаем модуль плагинов
|
|
1074
|
-
).initialize();
|
|
1075
|
-
```
|
|
1076
|
-
|
|
1077
|
-
#### Жизненный цикл плагинов
|
|
1078
|
-
|
|
1079
|
-
Плагины имеют следующие методы жизненного цикла:
|
|
1080
|
-
|
|
1081
|
-
1. **Инициализация**: `initialize()` - вызывается при добавлении плагина в хранилище
|
|
1082
|
-
2. **Операции хранилища**:
|
|
1083
|
-
- `onBeforeSet` / `onAfterSet` - до/после сохранения значения
|
|
1084
|
-
- `onBeforeGet` / `onAfterGet` - до/после получения значения
|
|
1085
|
-
- `onBeforeDelete` / `onAfterDelete` - до/после удаления значения
|
|
1086
|
-
- `onClear` - при очистке хранилища
|
|
1087
|
-
3. **Уничтожение**: `destroy()` - вызывается при удалении плагина или уничтожении хранилища
|
|
1088
|
-
|
|
1089
|
-
#### Когда использовать middleware, а когда плагины?
|
|
1090
|
-
|
|
1091
|
-
- **Middleware** лучше использовать для:
|
|
1092
|
-
- Перехвата всех операций хранилища в одном месте
|
|
1093
|
-
- Изменения поведения базовых операций хранилища
|
|
1094
|
-
- Оптимизации (батчинг, дедупликация)
|
|
1095
|
-
- Синхронизации между хранилищами/вкладками
|
|
1096
|
-
|
|
1097
|
-
- **Плагины** лучше использовать для:
|
|
1098
|
-
- Обработки конкретных событий хранилища
|
|
1099
|
-
- Валидации данных
|
|
1100
|
-
- Логирования операций
|
|
1101
|
-
- Реализации бизнес-логики, связанной с хранением данных
|
|
1102
|
-
- Интеграции с внешними сервисами
|
|
1103
|
-
|
|
1104
|
-
## Лицензия
|
|
1105
|
-
|
|
1106
|
-
MIT
|