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