@tanstack/offline-transactions
Version:
Offline-first transaction capabilities for TanStack DB
147 lines (146 loc) • 3.98 kB
JavaScript
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const LeaderElection = require("./LeaderElection.cjs");
class BroadcastChannelLeader extends LeaderElection.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;
}
}
}
exports.BroadcastChannelLeader = BroadcastChannelLeader;
//# sourceMappingURL=BroadcastChannelLeader.cjs.map