synapse-storage
Version:
Набор инструментов для управления состоянием и апи-запросами
140 lines (135 loc) • 5.19 kB
JavaScript
import { loggerConsole } from "../../../_utils/logger-console.util.js";
class SyncBroadcastChannel {
channel;
tabId;
messageHandlers;
syncHandler;
syncTimeoutMs = 1000;
closed = false;
pendingSyncRequests;
constructor(channelName){
this.channel = new BroadcastChannel(channelName);
this.tabId = crypto.randomUUID();
this.messageHandlers = new Set();
this.pendingSyncRequests = new Map();
this.channel.onmessage = this.handleMessage.bind(this);
this.channel.onmessageerror = this.handleError.bind(this);
}
error(...args) {
loggerConsole.error(`[SyncBroadcastChannel][${this.tabId}]`, ...args);
}
async handleMessage(event) {
const message = event.data;
// Игнорируем собственные сообщения
if (message.senderId === this.tabId) {
return;
}
// Обработка запроса на синхронизацию
if (message.type === 'SYNC_REQUEST') {
if (this.syncHandler) {
try {
const state = await this.syncHandler();
this.postMessage('SYNC_RESPONSE', state, message.senderId);
} catch (error) {
this.error('Error handling sync request:', error);
}
}
return;
}
// Обработка ответа на запрос синхронизации
if (message.type === 'SYNC_RESPONSE') {
// Фильтруем по targetId — принимаем только ответы, адресованные этой вкладке
if (message.targetId && message.targetId !== this.tabId) {
return;
}
const request = this.pendingSyncRequests.get(this.tabId);
if (request) {
clearTimeout(request.timeout);
this.pendingSyncRequests.delete(this.tabId);
//@ts-ignore
request.resolve(message.payload);
}
return;
}
// Уведомляем всех подписчиков о сообщении
for (const handler of this.messageHandlers){
try {
await handler(message);
} catch (error) {
this.error('Error in message handler:', error);
}
}
}
handleError(event) {
this.error('Channel error:', event);
}
postMessage(type, payload, targetId) {
const message = {
type,
payload,
senderId: this.tabId,
timestamp: Date.now()
};
if (targetId) {
message.targetId = targetId;
}
this.channel.postMessage(message);
}
/**
* Подписка на сообщения канала
*/ subscribe(handler) {
this.messageHandlers.add(handler);
return ()=>this.messageHandlers.delete(handler);
}
/**
* Установка обработчика запросов на синхронизацию
*/ setSyncHandler(handler) {
this.syncHandler = handler;
}
/**
* Отправка сообщения всем подписчикам
*/ broadcast(type, payload) {
//@ts-ignore
this.postMessage(type, payload);
}
/** Закрыт ли канал (dev double-mount / teardown). Потребители могут пропустить initial-sync. */ get isClosed() {
return this.closed;
}
/** Запрос синхронизации данных с других вкладок. */ async requestSync() {
// Канал уже закрыт (dev-teardown между mount'ами) — «нет ответа», без ошибки.
if (this.closed) return null;
return new Promise((resolve, reject)=>{
const timeout = setTimeout(()=>{
this.pendingSyncRequests.delete(this.tabId);
resolve(null);
}, this.syncTimeoutMs);
this.pendingSyncRequests.set(this.tabId, {
resolve,
reject,
timeout
});
this.postMessage('SYNC_REQUEST', {
type: 'sync'
});
});
}
/**
* Закрытие канала
*/ close() {
this.closed = true;
// Pending initial-sync резолвим как «нет ответа» (как таймаут, resolve(null)) — закрытие канала
// не ошибка (типичный dev double-mount в StrictMode/Fast Refresh), не шумим Error-стеком.
for (const [, request] of this.pendingSyncRequests){
clearTimeout(request.timeout);
request.resolve(null);
}
this.pendingSyncRequests.clear();
// Очищаем обработчики
this.messageHandlers.clear();
this.syncHandler = undefined;
// Закрываем канал
this.channel.close();
}
}
export { SyncBroadcastChannel };
//# sourceMappingURL=broadcast.util.js.map