UNPKG

@tanstack/offline-transactions

Version:

Offline-first transaction capabilities for TanStack DB

147 lines (146 loc) 3.85 kB
import { BaseLeaderElection } from "./LeaderElection.js"; class BroadcastChannelLeader extends BaseLeaderElection { constructor(channelName = `offline-executor-leader`) { super(); this.channel = null; this.heartbeatInterval = null; this.electionTimeout = null; this.lastLeaderHeartbeat = 0; this.heartbeatIntervalMs = 5e3; this.electionTimeoutMs = 1e4; this.handleMessage = (event) => { const { type, tabId, timestamp } = event.data; if (tabId === this.tabId) { return; } switch (type) { case `heartbeat`: if (this.isLeaderState && tabId < this.tabId) { this.releaseLeadership(); } else if (!this.isLeaderState) { this.lastLeaderHeartbeat = timestamp; this.cancelElection(); } break; case `election`: if (this.isLeaderState) { this.sendHeartbeat(); } else if (tabId > this.tabId) { this.startElection(); } break; case `leadership-claim`: if (this.isLeaderState && tabId < this.tabId) { this.releaseLeadership(); } break; } }; this.channelName = channelName; this.tabId = crypto.randomUUID(); this.setupChannel(); } setupChannel() { if (!this.isBroadcastChannelSupported()) { return; } this.channel = new BroadcastChannel(this.channelName); this.channel.addEventListener(`message`, this.handleMessage); } async requestLeadership() { if (!this.isBroadcastChannelSupported()) { return false; } if (this.isLeaderState) { return true; } this.startElection(); return new Promise((resolve) => { setTimeout(() => { resolve(this.isLeaderState); }, 1e3); }); } startElection() { if (this.electionTimeout) { return; } this.sendMessage({ type: `election`, tabId: this.tabId, timestamp: Date.now() }); this.electionTimeout = window.setTimeout(() => { const timeSinceLastHeartbeat = Date.now() - this.lastLeaderHeartbeat; if (timeSinceLastHeartbeat > this.electionTimeoutMs) { this.claimLeadership(); } this.electionTimeout = null; }, this.electionTimeoutMs); } cancelElection() { if (this.electionTimeout) { clearTimeout(this.electionTimeout); this.electionTimeout = null; } } claimLeadership() { this.notifyLeadershipChange(true); this.sendMessage({ type: `leadership-claim`, tabId: this.tabId, timestamp: Date.now() }); this.startHeartbeat(); } startHeartbeat() { if (this.heartbeatInterval) { return; } this.sendHeartbeat(); this.heartbeatInterval = window.setInterval(() => { this.sendHeartbeat(); }, this.heartbeatIntervalMs); } stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } sendHeartbeat() { this.sendMessage({ type: `heartbeat`, tabId: this.tabId, timestamp: Date.now() }); } sendMessage(message) { if (this.channel) { this.channel.postMessage(message); } } releaseLeadership() { this.stopHeartbeat(); this.cancelElection(); this.notifyLeadershipChange(false); } isBroadcastChannelSupported() { return typeof BroadcastChannel !== `undefined`; } static isSupported() { return typeof BroadcastChannel !== `undefined`; } dispose() { this.releaseLeadership(); if (this.channel) { this.channel.removeEventListener(`message`, this.handleMessage); this.channel.close(); this.channel = null; } } } export { BroadcastChannelLeader }; //# sourceMappingURL=BroadcastChannelLeader.js.map