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.
Files changed (2) hide show
  1. package/README.md +81 -1000
  2. 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
  [![npm version](https://badge.fury.io/js/synapse-storage.svg)](https://badge.fury.io/js/synapse-storage)
4
8
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/synapse-storage)](https://bundlephobia.com/package/synapse-storage)
5
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/)
10
+ [![RxJS Version](https://img.shields.io/badge/RxJS-%5E7.8.2-red?logo=reactivex)](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
- Synapse — это набор инструментов для управления состоянием + API-клиент.
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
- # Для React интеграции
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
- // Эффект для отслеживания изменений ID
482
- export const watchIdEffect = createEffect<
483
- PokemonState,
484
- PokemonDispatcherType,
485
- PokemonApiType,
486
- AppConfig,
487
- any //ExternalStorages
488
- >((action$, state$, externalStorages, { pokemonDispatcher }) =>
489
- action$.pipe(
490
- ofType(pokemonDispatcher.watchers.watchCurrentId),
491
- withLatestFrom(
492
- selectorMap(state$,
493
- //... selectors
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
- return from(
534
- // Использую waitWithCallbacks чтобы иметь доступ к методу loading
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
- setActiveChange: createAction<void, void>({
628
- type: 'setActiveChange',
629
- // meta: ,
630
- // action: async () => {...}),
631
- })
632
- // Другие диспетчеры ...
633
- })).use(loggerMiddleware)
634
- }
635
-
636
- export type UserInfoDispatcher = ReturnType<typeof createUserInfoDispatcher>
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synapse-storage",
3
- "version": "3.0.9",
3
+ "version": "3.0.11",
4
4
  "description": "Набор инструментов для управления состоянием и апи-запросами",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",