synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
366 lines (359 loc) • 16.5 kB
JavaScript
import { handleCallbackError, logError } from "../../../_utils/error-handling.util.js";
import { StorageEvents } from "../storage.interface.js";
// ─── Async ───────────────────────────────────────────────────────────────────
// Обработка сообщений для MemoryStorage
async function handleMemoryStorageMessage(api, type, payload) {
switch(type){
case 'set':
if (payload?.key !== undefined && payload?.value !== undefined) {
await api.storage.doSet(payload.key, payload.value);
api.storage.notifySubscribers(payload.key, payload.value);
}
break;
case 'update':
if (Array.isArray(payload?.value)) {
await api.storage.doUpdate(payload.value);
//@ts-ignore
payload.value.forEach(({ key, value })=>{
//@ts-ignore
api.storage.notifySubscribers(key, value);
});
}
break;
case 'delete':
if (payload?.key !== undefined) {
await api.storage.doDelete(payload.key);
api.storage.notifySubscribers(payload.key, undefined);
}
break;
case 'clear':
await api.storage.doClear();
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
value: {},
source: 'broadcast'
});
break;
}
// Уведомляем глобальных подписчиков
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
key: payload?.key,
value: payload?.value,
source: 'broadcast'
});
}
// Обработка сообщений для LocalStorage и IndexedDB
async function handlePersistentStorageMessage(api, type, payload) {
// Для LocalStorage и IndexedDB данные уже синхронизированы браузером
// Нужно только получить актуальные данные и уведомить подписчиков
switch(type){
case 'set':
if (payload?.key !== undefined) {
// Получаем актуальное значение из хранилища
const currentValue = await api.storage.doGet(payload.key);
api.storage.notifySubscribers(payload.key, currentValue);
}
break;
case 'update':
if (Array.isArray(payload?.value)) {
// Получаем актуальные значения для каждого обновленного ключа
for (const { key } of payload.value){
const currentValue = await api.storage.doGet(key);
api.storage.notifySubscribers(key, currentValue);
}
// Уведомляем об обновлении с актуальными данными
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
//@ts-ignore
key: payload.value.map(({ key })=>key),
value: payload.value,
source: 'broadcast'
});
}
break;
case 'delete':
if (payload?.key !== undefined) {
api.storage.notifySubscribers(payload.key, undefined);
}
break;
case 'clear':
// При очистке уведомляем всех подписчиков
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
value: {},
source: 'broadcast'
});
break;
}
// Общее уведомление для глобальных подписчиков
if (type !== 'update') {
// Для update уже отправили выше
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
key: payload?.key,
value: type === 'delete' ? undefined : payload?.value,
source: 'broadcast'
});
}
}
/**
* Фабрика async кросс-табной мидлвары. Возвращает `(props) => Middleware` — ту же
* сигнатуру, что у публичных `broadcastMiddleware`/`sharedWorkerMiddleware`.
*/ function createSharedStateMiddleware(config) {
const { name, label, createChannel } = config;
return (props)=>{
const { storageName, storageType } = props;
const channelName = `${storageType}-${storageName}`;
const channel = createChannel(channelName);
return {
name,
setup: (api)=>{
// Настройка синхронизации для MemoryStorage
if (storageType === 'memory') {
channel.setSyncHandler(async ()=>{
const state = await api.getState();
const updates = Object.entries(state).map(([key, value])=>({
key,
value
}));
const action = {
type: 'update',
key: '*',
value: updates,
metadata: {
batchUpdate: true,
timestamp: Date.now()
}
};
return action;
});
// Запрашиваем начальную синхронизацию для MemoryStorage
channel.requestSync().then(async (action)=>{
if (action?.type === 'update' && Array.isArray(action.value)) {
try {
const validUpdates = action.value.every((update)=>update && typeof update === 'object' && 'key' in update && 'value' in update);
if (!validUpdates) {
logError(`${label}: invalid sync response updates structure`, action.value);
return;
}
await api.storage.doUpdate(action.value);
// Уведомляем подписчиков о каждом обновленном значении
action.value.forEach(({ key, value })=>{
api.storage.notifySubscribers(key, value);
});
// Уведомляем глобальных подписчиков
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
value: action.value,
source: 'broadcast'
});
} catch (error) {
handleCallbackError(`${label}: error applying sync updates`, error);
}
}
}).catch((error)=>{
logError(`${label}: initial sync failed`, error, null, 'warn');
});
}
// Подписка на сообщения от других вкладок
return channel.subscribe(async (message)=>{
const { type, payload } = message;
if (storageType === 'memory') {
// Для MemoryStorage обновляем данные и уведомляем подписчиков
await handleMemoryStorageMessage(api, type, payload);
} else {
// Для LocalStorage и IndexedDB только уведомляем подписчиков
await handlePersistentStorageMessage(api, type, payload);
}
});
},
reducer: (_api)=>(next)=>async (action)=>{
const result = await next(action);
// Отправляем сообщение другим вкладкам для всех типов операций
// Текущая вкладка уже получила уведомления через обычный flow BaseStorage
if ([
'set',
'delete',
'clear',
'update'
].includes(action.type)) {
channel.broadcast(action.type, action);
}
return result;
},
cleanup: ()=>{
channel.close();
}
};
};
}
function handleSyncStorageMessage(api, type, payload) {
switch(type){
case 'set':
if (payload?.key !== undefined && payload?.value !== undefined) {
api.storage.doSet(payload.key, payload.value);
api.storage.notifySubscribers(payload.key, payload.value);
}
break;
case 'update':
if (Array.isArray(payload?.value)) {
api.storage.doUpdate(payload.value);
payload.value.forEach(({ key, value })=>{
api.storage.notifySubscribers(key, value);
});
}
break;
case 'delete':
if (payload?.key !== undefined) {
api.storage.doRemove(payload.key);
api.storage.notifySubscribers(payload.key, undefined);
}
break;
case 'clear':
api.storage.doClear();
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
value: {},
source: 'broadcast'
});
break;
}
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
key: payload?.key,
value: payload?.value,
source: 'broadcast'
});
}
function handlePersistentSyncStorageMessage(api, type, payload) {
switch(type){
case 'set':
if (payload?.key !== undefined) {
const currentValue = api.storage.doGet(payload.key);
api.storage.notifySubscribers(payload.key, currentValue);
}
break;
case 'update':
if (Array.isArray(payload?.value)) {
for (const { key } of payload.value){
const currentValue = api.storage.doGet(key);
api.storage.notifySubscribers(key, currentValue);
}
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
key: payload.value.map(({ key })=>key),
value: payload.value,
source: 'broadcast'
});
}
break;
case 'delete':
if (payload?.key !== undefined) {
api.storage.notifySubscribers(payload.key, undefined);
}
break;
case 'clear':
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
value: {},
source: 'broadcast'
});
break;
}
if (type !== 'update') {
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
key: payload?.key,
value: type === 'delete' ? undefined : payload?.value,
source: 'broadcast'
});
}
}
/**
* Фабрика sync кросс-табной мидлвары. Возвращает `(props) => SyncMiddleware` — ту же
* сигнатуру, что у публичных `syncBroadcastMiddleware`/`syncSharedWorkerMiddleware`.
*/ function createSyncSharedStateMiddleware(config) {
const { name, label, createChannel } = config;
return (props)=>{
const { storageName, storageType } = props;
const channelName = `${storageType}-${storageName}`;
const channel = createChannel(channelName);
return {
name,
setup: (api)=>{
if (storageType === 'memory') {
channel.setSyncHandler(async ()=>{
const state = api.getState();
const updates = Object.entries(state).map(([key, value])=>({
key,
value
}));
const action = {
type: 'update',
key: '*',
value: updates,
metadata: {
batchUpdate: true,
timestamp: Date.now()
}
};
return action;
});
// Начальная синхронизация (fire-and-forget)
channel.requestSync().then((action)=>{
if (action?.type === 'update' && Array.isArray(action.value)) {
try {
const validUpdates = action.value.every((update)=>update && typeof update === 'object' && 'key' in update && 'value' in update);
if (!validUpdates) {
logError(`${label}: invalid sync response updates structure`, action.value);
return;
}
api.storage.doUpdate(action.value);
action.value.forEach(({ key, value })=>{
api.storage.notifySubscribers(key, value);
});
api.storage.notifySubscribers('*', {
type: StorageEvents.STORAGE_UPDATE,
value: action.value,
source: 'broadcast'
});
} catch (error) {
handleCallbackError(`${label}: error applying sync updates`, error);
}
}
}).catch((error)=>{
logError(`${label}: initial sync failed`, error, null, 'warn');
});
}
// Подписка на сообщения (BroadcastChannel API всегда async)
return channel.subscribe((message)=>{
const { type, payload } = message;
if (storageType === 'memory') {
handleSyncStorageMessage(api, type, payload);
} else {
handlePersistentSyncStorageMessage(api, type, payload);
}
});
},
reducer: (_api)=>(next)=>(action)=>{
const result = next(action);
// Fire-and-forget broadcast to other tabs
if ([
'set',
'delete',
'clear',
'update'
].includes(action.type)) {
channel.broadcast(action.type, action);
}
return result;
},
cleanup: ()=>{
channel.close();
}
};
};
}
export { createSharedStateMiddleware, createSyncSharedStateMiddleware };
//# sourceMappingURL=shared-state.factory.js.map