synapse-storage 3.0.10 → 3.0.12

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 CHANGED
@@ -1,75 +1,71 @@
1
1
  # Synapse Storage
2
2
 
3
- Набор инструментов для управления состоянием + API-клиент
3
+ > **🇺🇸 English** | [🇷🇺 Русский](./docs/ru/README.md)
4
+
5
+ State management toolkit + API client
4
6
 
5
7
  [![npm version](https://badge.fury.io/js/synapse-storage.svg)](https://badge.fury.io/js/synapse-storage)
6
8
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/synapse-storage)](https://bundlephobia.com/package/synapse-storage)
7
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/)
8
10
  [![RxJS Version](https://img.shields.io/badge/RxJS-%5E7.8.2-red?logo=reactivex)](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
- **Владислав** — Senior Frontend Developer (React, TypeScript)
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: Пока не рекоммендую использовать в production т.к разработкой занимаюсь в свободное время.
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
- # Для React интеграции
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/core` | Хранилища состояния | Нет |
65
- | `synapse-storage/react` | Инструменты для React | React 18+ |
66
- | `synapse-storage/reactive` | RxJS интеграция | RxJS 7.8.2+ |
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
- ### Минимальный tsconfig.json:
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
- counter2ValueSelector.subscribe({
263
- notify: (value) => {
264
- console.log('counter2ValueSelector', value)
265
- }
266
- })
267
-
268
- counter3ValueSelector.subscribe({
269
- notify: (value) => {
270
- console.log('counter3ValueSelector', value)
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
- import {
562
- ofType, // Слушает 1 событие
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
- export const {
918
- contextSynapse: useUserInfoContextSynapse,
919
- useSynapseActions: useUserInfoSynapseActions,
920
- useSynapseSelectors: useUserInfoSynapseSelectors,
921
- useSynapseState$: useUserInfoSynapseState$,
922
- useSynapseStorage: useUserInfoSynapseStorage,
923
- cleanupSynapse: useUserInfoCleanupSynapse,
924
- } = createSynapseCtx(
925
- // Передаем сам Synapse
926
- userInfoSynapse,
927
- {
928
- loadingComponent: <div>loading</div>, // Передаем компонент, который будет отображаться пока идет загрузка инициализация
929
- // mergeFn: // Функция слияния переданных параметров в initialState (по умолчанию выполняется глубокая копия)
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