pg-sse 0.1.2
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.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/chunk-GFLW4AMU.mjs +323 -0
- package/dist/chunk-GFLW4AMU.mjs.map +1 -0
- package/dist/client.d.mts +22 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +343 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +12 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +592 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +258 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +66 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +277 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +249 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
PostgresSseListener: () => PostgresSseListener,
|
|
25
|
+
SseProvider: () => SseProvider,
|
|
26
|
+
TypedEmitter: () => TypedEmitter,
|
|
27
|
+
createSseHandler: () => createSseHandler,
|
|
28
|
+
useSseStatus: () => useSseStatus,
|
|
29
|
+
useSubscription: () => useSubscription
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(src_exports);
|
|
32
|
+
|
|
33
|
+
// src/shared/emitter.ts
|
|
34
|
+
var import_events = require("events");
|
|
35
|
+
var TypedEmitter = class {
|
|
36
|
+
emitter = new import_events.EventEmitter();
|
|
37
|
+
on(event, listener) {
|
|
38
|
+
this.emitter.on(event, listener);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
once(event, listener) {
|
|
42
|
+
this.emitter.once(event, listener);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
off(event, listener) {
|
|
46
|
+
this.emitter.off(event, listener);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
emit(event, payload) {
|
|
50
|
+
return this.emitter.emit(event, payload);
|
|
51
|
+
}
|
|
52
|
+
addListener(event, listener) {
|
|
53
|
+
this.emitter.addListener(event, listener);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
removeListener(event, listener) {
|
|
57
|
+
this.emitter.removeListener(event, listener);
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
removeAllListeners(event) {
|
|
61
|
+
this.emitter.removeAllListeners(event);
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/server/listener.ts
|
|
67
|
+
var import_pg = require("pg");
|
|
68
|
+
var PostgresSseListener = class {
|
|
69
|
+
events = new TypedEmitter();
|
|
70
|
+
client = null;
|
|
71
|
+
isConnected = false;
|
|
72
|
+
isDisconnecting = false;
|
|
73
|
+
retryCount = 0;
|
|
74
|
+
reconnectTimer = null;
|
|
75
|
+
// In-memory registry of active SSE clients
|
|
76
|
+
clients = /* @__PURE__ */ new Map();
|
|
77
|
+
config;
|
|
78
|
+
channels;
|
|
79
|
+
constructor(config, channels = "db_changes") {
|
|
80
|
+
this.config = config;
|
|
81
|
+
this.channels = Array.isArray(channels) ? channels : [channels];
|
|
82
|
+
const channelRegex = /^[a-zA-Z0-9_]+$/;
|
|
83
|
+
for (const channel of this.channels) {
|
|
84
|
+
if (!channelRegex.test(channel)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Invalid channel name: "${channel}". Channel names must be alphanumeric and underscores only.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async connect() {
|
|
92
|
+
if (this.isConnected) return;
|
|
93
|
+
this.isDisconnecting = false;
|
|
94
|
+
try {
|
|
95
|
+
this.client = typeof this.config === "string" ? new import_pg.Client(this.config) : new import_pg.Client(this.config);
|
|
96
|
+
this.client.on("error", (err) => {
|
|
97
|
+
this.handleConnectionError(err);
|
|
98
|
+
});
|
|
99
|
+
this.client.on("notification", (msg) => {
|
|
100
|
+
this.handleNotification(msg);
|
|
101
|
+
});
|
|
102
|
+
await this.client.connect();
|
|
103
|
+
for (const channel of this.channels) {
|
|
104
|
+
await this.client.query(`LISTEN ${channel}`);
|
|
105
|
+
}
|
|
106
|
+
this.isConnected = true;
|
|
107
|
+
this.retryCount = 0;
|
|
108
|
+
this.events.emit("connected", void 0);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this.handleConnectionError(err);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async disconnect() {
|
|
114
|
+
this.isDisconnecting = true;
|
|
115
|
+
this.isConnected = false;
|
|
116
|
+
if (this.reconnectTimer) {
|
|
117
|
+
clearTimeout(this.reconnectTimer);
|
|
118
|
+
this.reconnectTimer = null;
|
|
119
|
+
}
|
|
120
|
+
if (this.client) {
|
|
121
|
+
try {
|
|
122
|
+
await this.client.end();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
} finally {
|
|
125
|
+
this.client = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
getActiveConnections() {
|
|
130
|
+
return this.clients.size;
|
|
131
|
+
}
|
|
132
|
+
registerClient(clientId, send) {
|
|
133
|
+
this.clients.set(clientId, send);
|
|
134
|
+
}
|
|
135
|
+
unregisterClient(clientId) {
|
|
136
|
+
this.clients.delete(clientId);
|
|
137
|
+
}
|
|
138
|
+
broadcast(event, data) {
|
|
139
|
+
const message = `event: ${event}
|
|
140
|
+
data: ${JSON.stringify(data)}
|
|
141
|
+
|
|
142
|
+
`;
|
|
143
|
+
for (const send of this.clients.values()) {
|
|
144
|
+
try {
|
|
145
|
+
send(message);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
handleConnectionError(err) {
|
|
151
|
+
if (this.isDisconnecting) return;
|
|
152
|
+
this.isConnected = false;
|
|
153
|
+
this.events.emit("error", err);
|
|
154
|
+
if (this.client) {
|
|
155
|
+
this.client.end().catch(() => {
|
|
156
|
+
});
|
|
157
|
+
this.client = null;
|
|
158
|
+
}
|
|
159
|
+
this.scheduleReconnect();
|
|
160
|
+
}
|
|
161
|
+
scheduleReconnect() {
|
|
162
|
+
if (this.reconnectTimer) return;
|
|
163
|
+
this.retryCount++;
|
|
164
|
+
const delay = Math.min(1e3 * Math.pow(2, this.retryCount - 1), 3e4);
|
|
165
|
+
const jitter = Math.random() * 1e3;
|
|
166
|
+
const finalDelay = delay + jitter;
|
|
167
|
+
this.events.emit("reconnect", this.retryCount);
|
|
168
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
169
|
+
this.reconnectTimer = null;
|
|
170
|
+
try {
|
|
171
|
+
await this.connect();
|
|
172
|
+
} catch (err) {
|
|
173
|
+
}
|
|
174
|
+
}, finalDelay);
|
|
175
|
+
}
|
|
176
|
+
handleNotification(msg) {
|
|
177
|
+
if (!msg.payload) return;
|
|
178
|
+
try {
|
|
179
|
+
let parsedPayload;
|
|
180
|
+
try {
|
|
181
|
+
parsedPayload = JSON.parse(msg.payload);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
parsedPayload = msg.payload;
|
|
184
|
+
}
|
|
185
|
+
const isObjectPayload = typeof parsedPayload === "object" && parsedPayload !== null;
|
|
186
|
+
const payloadObj = isObjectPayload ? parsedPayload : null;
|
|
187
|
+
const eventPayload = payloadObj ? {
|
|
188
|
+
table: payloadObj.table || "unknown",
|
|
189
|
+
action: payloadObj.action || "INSERT",
|
|
190
|
+
id: payloadObj.id !== void 0 ? payloadObj.id : "",
|
|
191
|
+
...payloadObj
|
|
192
|
+
} : {
|
|
193
|
+
table: "unknown",
|
|
194
|
+
action: "INSERT",
|
|
195
|
+
id: "",
|
|
196
|
+
data: parsedPayload
|
|
197
|
+
};
|
|
198
|
+
this.events.emit("notification", eventPayload);
|
|
199
|
+
this.broadcast("notification", eventPayload);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
this.events.emit(
|
|
202
|
+
"error",
|
|
203
|
+
new Error(
|
|
204
|
+
`Failed to process notification payload: ${err.message}`
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/server/handler.ts
|
|
212
|
+
function createSseHandler(listener, req) {
|
|
213
|
+
const clientId = Math.random().toString(36).substring(2, 15);
|
|
214
|
+
const encoder = new TextEncoder();
|
|
215
|
+
const responseStream = new TransformStream();
|
|
216
|
+
const writer = responseStream.writable.getWriter();
|
|
217
|
+
let isCleanedUp = false;
|
|
218
|
+
function cleanup() {
|
|
219
|
+
if (isCleanedUp) return;
|
|
220
|
+
isCleanedUp = true;
|
|
221
|
+
clearInterval(keepAliveInterval);
|
|
222
|
+
listener.unregisterClient(clientId);
|
|
223
|
+
try {
|
|
224
|
+
writer.close();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function send(data) {
|
|
229
|
+
if (isCleanedUp) return;
|
|
230
|
+
writer.write(encoder.encode(data)).catch(() => {
|
|
231
|
+
cleanup();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
listener.registerClient(clientId, send);
|
|
235
|
+
const keepAliveInterval = setInterval(() => {
|
|
236
|
+
if (isCleanedUp) return;
|
|
237
|
+
writer.write(encoder.encode(": keep-alive\n\n")).catch(() => {
|
|
238
|
+
cleanup();
|
|
239
|
+
});
|
|
240
|
+
}, 2e4);
|
|
241
|
+
const handshake = {
|
|
242
|
+
type: "handshake",
|
|
243
|
+
clientId,
|
|
244
|
+
activeConnections: listener.getActiveConnections()
|
|
245
|
+
};
|
|
246
|
+
writer.write(
|
|
247
|
+
encoder.encode(
|
|
248
|
+
`event: handshake
|
|
249
|
+
data: ${JSON.stringify(handshake)}
|
|
250
|
+
|
|
251
|
+
`
|
|
252
|
+
)
|
|
253
|
+
).catch(() => {
|
|
254
|
+
cleanup();
|
|
255
|
+
});
|
|
256
|
+
if (req?.signal) {
|
|
257
|
+
if (req.signal.aborted) {
|
|
258
|
+
cleanup();
|
|
259
|
+
} else {
|
|
260
|
+
req.signal.addEventListener("abort", () => {
|
|
261
|
+
cleanup();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return new Response(responseStream.readable, {
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "text/event-stream",
|
|
268
|
+
"Cache-Control": "no-cache, no-transform",
|
|
269
|
+
Connection: "keep-alive",
|
|
270
|
+
"X-Accel-Buffering": "no"
|
|
271
|
+
// Turn off buffering in Nginx/Vercel
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/client/provider.tsx
|
|
277
|
+
var import_react = require("react");
|
|
278
|
+
|
|
279
|
+
// src/client/multiplexer.ts
|
|
280
|
+
var TabMultiplexer = class {
|
|
281
|
+
constructor(endpoint, onEvent, onStatusChange) {
|
|
282
|
+
this.endpoint = endpoint;
|
|
283
|
+
this.onEvent = onEvent;
|
|
284
|
+
this.onStatusChange = onStatusChange;
|
|
285
|
+
this.tabId = Math.random().toString(36).substring(2, 11);
|
|
286
|
+
if (typeof window !== "undefined" && typeof BroadcastChannel !== "undefined") {
|
|
287
|
+
this.channel = new BroadcastChannel("pg-sse-multiplexer-channel");
|
|
288
|
+
this.channel.onmessage = (event) => this.handleChannelMessage(event.data);
|
|
289
|
+
window.addEventListener("beforeunload", () => this.destroy());
|
|
290
|
+
this.resetWatchdog(1500);
|
|
291
|
+
this.broadcast({ type: "query_leader", senderId: this.tabId });
|
|
292
|
+
} else {
|
|
293
|
+
this.startDirectConnection();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
endpoint;
|
|
297
|
+
onEvent;
|
|
298
|
+
onStatusChange;
|
|
299
|
+
channel = null;
|
|
300
|
+
tabId;
|
|
301
|
+
isLeader = false;
|
|
302
|
+
leaderId = null;
|
|
303
|
+
status = "disconnected";
|
|
304
|
+
heartbeatInterval = null;
|
|
305
|
+
watchdogTimeout = null;
|
|
306
|
+
electionTimeout = null;
|
|
307
|
+
eventSource = null;
|
|
308
|
+
claimInProgress = false;
|
|
309
|
+
destroy() {
|
|
310
|
+
const wasLeader = this.isLeader;
|
|
311
|
+
this.isLeader = false;
|
|
312
|
+
if (wasLeader) {
|
|
313
|
+
this.broadcast({ type: "release", senderId: this.tabId });
|
|
314
|
+
}
|
|
315
|
+
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
316
|
+
if (this.watchdogTimeout) clearTimeout(this.watchdogTimeout);
|
|
317
|
+
if (this.electionTimeout) clearTimeout(this.electionTimeout);
|
|
318
|
+
this.stopEventSource();
|
|
319
|
+
if (this.channel) {
|
|
320
|
+
this.channel.close();
|
|
321
|
+
this.channel = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
broadcast(msg) {
|
|
325
|
+
if (this.channel) {
|
|
326
|
+
try {
|
|
327
|
+
this.channel.postMessage(msg);
|
|
328
|
+
} catch (e) {
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
handleChannelMessage(msg) {
|
|
333
|
+
if (msg.senderId === this.tabId) return;
|
|
334
|
+
switch (msg.type) {
|
|
335
|
+
case "query_leader":
|
|
336
|
+
if (this.isLeader) {
|
|
337
|
+
this.sendHeartbeat();
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
case "heartbeat":
|
|
341
|
+
this.claimInProgress = false;
|
|
342
|
+
if (this.electionTimeout) {
|
|
343
|
+
clearTimeout(this.electionTimeout);
|
|
344
|
+
this.electionTimeout = null;
|
|
345
|
+
}
|
|
346
|
+
this.leaderId = msg.senderId;
|
|
347
|
+
this.resetWatchdog(5e3);
|
|
348
|
+
break;
|
|
349
|
+
case "claim":
|
|
350
|
+
if (this.isLeader) {
|
|
351
|
+
this.sendHeartbeat();
|
|
352
|
+
} else if (this.claimInProgress) {
|
|
353
|
+
if (msg.senderId < this.tabId) {
|
|
354
|
+
this.claimInProgress = false;
|
|
355
|
+
if (this.electionTimeout) {
|
|
356
|
+
clearTimeout(this.electionTimeout);
|
|
357
|
+
this.electionTimeout = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
case "release":
|
|
363
|
+
if (this.leaderId === msg.senderId) {
|
|
364
|
+
this.leaderId = null;
|
|
365
|
+
this.attemptLeadershipClaim();
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
case "status":
|
|
369
|
+
if (!this.isLeader && this.leaderId === msg.senderId) {
|
|
370
|
+
const payloadObj = msg.payload;
|
|
371
|
+
this.status = payloadObj.status;
|
|
372
|
+
this.onStatusChange(this.status, payloadObj.activeConnections);
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
case "event":
|
|
376
|
+
if (!this.isLeader && this.leaderId === msg.senderId) {
|
|
377
|
+
const payloadObj = msg.payload;
|
|
378
|
+
this.onEvent(payloadObj.event, payloadObj.data);
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
resetWatchdog(ms) {
|
|
384
|
+
if (this.watchdogTimeout) clearTimeout(this.watchdogTimeout);
|
|
385
|
+
this.watchdogTimeout = setTimeout(() => {
|
|
386
|
+
this.attemptLeadershipClaim();
|
|
387
|
+
}, ms);
|
|
388
|
+
}
|
|
389
|
+
attemptLeadershipClaim() {
|
|
390
|
+
if (this.isLeader || this.claimInProgress) return;
|
|
391
|
+
this.claimInProgress = true;
|
|
392
|
+
this.broadcast({ type: "claim", senderId: this.tabId });
|
|
393
|
+
this.electionTimeout = setTimeout(() => {
|
|
394
|
+
this.electionTimeout = null;
|
|
395
|
+
if (this.claimInProgress) {
|
|
396
|
+
this.claimInProgress = false;
|
|
397
|
+
this.becomeLeader();
|
|
398
|
+
}
|
|
399
|
+
}, 500);
|
|
400
|
+
}
|
|
401
|
+
becomeLeader() {
|
|
402
|
+
this.isLeader = true;
|
|
403
|
+
this.leaderId = this.tabId;
|
|
404
|
+
if (this.watchdogTimeout) {
|
|
405
|
+
clearTimeout(this.watchdogTimeout);
|
|
406
|
+
this.watchdogTimeout = null;
|
|
407
|
+
}
|
|
408
|
+
this.sendHeartbeat();
|
|
409
|
+
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), 2e3);
|
|
410
|
+
this.startEventSource();
|
|
411
|
+
}
|
|
412
|
+
sendHeartbeat() {
|
|
413
|
+
this.broadcast({ type: "heartbeat", senderId: this.tabId });
|
|
414
|
+
}
|
|
415
|
+
updateStatus(newStatus, activeConnections = 0) {
|
|
416
|
+
this.status = newStatus;
|
|
417
|
+
this.onStatusChange(newStatus, activeConnections);
|
|
418
|
+
if (this.isLeader) {
|
|
419
|
+
this.broadcast({
|
|
420
|
+
type: "status",
|
|
421
|
+
senderId: this.tabId,
|
|
422
|
+
payload: { status: newStatus, activeConnections }
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
startEventSource() {
|
|
427
|
+
this.stopEventSource();
|
|
428
|
+
this.updateStatus("connecting");
|
|
429
|
+
try {
|
|
430
|
+
this.eventSource = new EventSource(this.endpoint);
|
|
431
|
+
this.eventSource.addEventListener("handshake", (e) => {
|
|
432
|
+
try {
|
|
433
|
+
const data = JSON.parse(e.data);
|
|
434
|
+
this.updateStatus("connected", data.activeConnections || 1);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
this.updateStatus("connected", 1);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
this.eventSource.addEventListener("notification", (e) => {
|
|
440
|
+
try {
|
|
441
|
+
const data = JSON.parse(e.data);
|
|
442
|
+
this.onEvent("notification", data);
|
|
443
|
+
this.broadcast({
|
|
444
|
+
type: "event",
|
|
445
|
+
senderId: this.tabId,
|
|
446
|
+
payload: { event: "notification", data }
|
|
447
|
+
});
|
|
448
|
+
} catch (err) {
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
this.eventSource.onerror = () => {
|
|
452
|
+
this.updateStatus("connecting");
|
|
453
|
+
};
|
|
454
|
+
} catch (err) {
|
|
455
|
+
this.updateStatus("disconnected");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
startDirectConnection() {
|
|
459
|
+
this.startEventSource();
|
|
460
|
+
}
|
|
461
|
+
stopEventSource() {
|
|
462
|
+
if (this.eventSource) {
|
|
463
|
+
this.eventSource.close();
|
|
464
|
+
this.eventSource = null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// src/client/provider.tsx
|
|
470
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
471
|
+
var SseContext = (0, import_react.createContext)(null);
|
|
472
|
+
var SseProvider = ({ children, endpoint }) => {
|
|
473
|
+
const [status, setStatus] = (0, import_react.useState)("disconnected");
|
|
474
|
+
const [eventCount, setEventCount] = (0, import_react.useState)(0);
|
|
475
|
+
const [activeConnections, setActiveConnections] = (0, import_react.useState)(0);
|
|
476
|
+
const subscriptions = (0, import_react.useRef)(
|
|
477
|
+
/* @__PURE__ */ new Map()
|
|
478
|
+
);
|
|
479
|
+
const handleStatusChange = (0, import_react.useCallback)(
|
|
480
|
+
(newStatus, activeCount) => {
|
|
481
|
+
setActiveConnections(activeCount);
|
|
482
|
+
setStatus((prev) => {
|
|
483
|
+
if (newStatus === "connecting") {
|
|
484
|
+
if (prev === "connected") {
|
|
485
|
+
console.warn(`[pg-sse] Stream connection lost. Reconnecting...`);
|
|
486
|
+
} else if (prev === "disconnected") {
|
|
487
|
+
console.log(`[pg-sse] Connecting to stream...`);
|
|
488
|
+
}
|
|
489
|
+
} else if (newStatus === "connected" && prev !== "connected") {
|
|
490
|
+
console.log(
|
|
491
|
+
`[pg-sse] Stream connected. Active client count: ${activeCount}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return newStatus;
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
[]
|
|
498
|
+
);
|
|
499
|
+
const handleEvent = (0, import_react.useCallback)((event, payload) => {
|
|
500
|
+
if (event === "notification") {
|
|
501
|
+
setEventCount((prev) => prev + 1);
|
|
502
|
+
const table = payload?.table;
|
|
503
|
+
if (table) {
|
|
504
|
+
const callbacks = subscriptions.current.get(table);
|
|
505
|
+
if (callbacks) {
|
|
506
|
+
callbacks.forEach((cb) => {
|
|
507
|
+
try {
|
|
508
|
+
cb(payload);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.error(
|
|
511
|
+
`[pg-sse] Callback for table "${table}" failed:`,
|
|
512
|
+
err
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}, []);
|
|
520
|
+
(0, import_react.useEffect)(() => {
|
|
521
|
+
const multiplexer = new TabMultiplexer(
|
|
522
|
+
endpoint,
|
|
523
|
+
handleEvent,
|
|
524
|
+
handleStatusChange
|
|
525
|
+
);
|
|
526
|
+
return () => {
|
|
527
|
+
multiplexer.destroy();
|
|
528
|
+
};
|
|
529
|
+
}, [endpoint, handleEvent, handleStatusChange]);
|
|
530
|
+
const subscribe = (0, import_react.useCallback)(
|
|
531
|
+
(table, callback) => {
|
|
532
|
+
let callbacks = subscriptions.current.get(table);
|
|
533
|
+
if (!callbacks) {
|
|
534
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
535
|
+
subscriptions.current.set(table, callbacks);
|
|
536
|
+
}
|
|
537
|
+
callbacks.add(callback);
|
|
538
|
+
return () => {
|
|
539
|
+
const callbacks2 = subscriptions.current.get(table);
|
|
540
|
+
if (callbacks2) {
|
|
541
|
+
callbacks2.delete(callback);
|
|
542
|
+
if (callbacks2.size === 0) {
|
|
543
|
+
subscriptions.current.delete(table);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
},
|
|
548
|
+
[]
|
|
549
|
+
);
|
|
550
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
551
|
+
SseContext.Provider,
|
|
552
|
+
{
|
|
553
|
+
value: { subscribe, status, eventCount, activeConnections },
|
|
554
|
+
children
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
};
|
|
558
|
+
function useSubscription(table, callback) {
|
|
559
|
+
const context = (0, import_react.useContext)(SseContext);
|
|
560
|
+
if (!context) {
|
|
561
|
+
throw new Error("useSubscription must be used within an SseProvider");
|
|
562
|
+
}
|
|
563
|
+
const { subscribe } = context;
|
|
564
|
+
const callbackRef = (0, import_react.useRef)(callback);
|
|
565
|
+
(0, import_react.useEffect)(() => {
|
|
566
|
+
callbackRef.current = callback;
|
|
567
|
+
}, [callback]);
|
|
568
|
+
(0, import_react.useEffect)(() => {
|
|
569
|
+
const unsubscribe = subscribe(table, (payload) => {
|
|
570
|
+
callbackRef.current(payload);
|
|
571
|
+
});
|
|
572
|
+
return unsubscribe;
|
|
573
|
+
}, [table, subscribe]);
|
|
574
|
+
}
|
|
575
|
+
function useSseStatus() {
|
|
576
|
+
const context = (0, import_react.useContext)(SseContext);
|
|
577
|
+
if (!context) {
|
|
578
|
+
throw new Error("useSseStatus must be used within an SseProvider");
|
|
579
|
+
}
|
|
580
|
+
const { status, eventCount, activeConnections } = context;
|
|
581
|
+
return { status, eventCount, activeConnections };
|
|
582
|
+
}
|
|
583
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
584
|
+
0 && (module.exports = {
|
|
585
|
+
PostgresSseListener,
|
|
586
|
+
SseProvider,
|
|
587
|
+
TypedEmitter,
|
|
588
|
+
createSseHandler,
|
|
589
|
+
useSseStatus,
|
|
590
|
+
useSubscription
|
|
591
|
+
});
|
|
592
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/shared/emitter.ts","../src/server/listener.ts","../src/server/handler.ts","../src/client/provider.tsx","../src/client/multiplexer.ts"],"sourcesContent":["export * from \"./shared/emitter\";\nexport * from \"./server/index\";\nexport * from \"./client/index\";\n","import { EventEmitter } from \"events\";\n\nexport class TypedEmitter<Events extends object> {\n private emitter = new EventEmitter();\n\n on<K extends keyof Events>(\n event: K,\n listener: (payload: Events[K]) => void,\n ): this {\n this.emitter.on(event as string, listener);\n return this;\n }\n\n once<K extends keyof Events>(\n event: K,\n listener: (payload: Events[K]) => void,\n ): this {\n this.emitter.once(event as string, listener);\n return this;\n }\n\n off<K extends keyof Events>(\n event: K,\n listener: (payload: Events[K]) => void,\n ): this {\n this.emitter.off(event as string, listener);\n return this;\n }\n\n emit<K extends keyof Events>(event: K, payload: Events[K]): boolean {\n return this.emitter.emit(event as string, payload);\n }\n\n addListener<K extends keyof Events>(\n event: K,\n listener: (payload: Events[K]) => void,\n ): this {\n this.emitter.addListener(event as string, listener);\n return this;\n }\n\n removeListener<K extends keyof Events>(\n event: K,\n listener: (payload: Events[K]) => void,\n ): this {\n this.emitter.removeListener(event as string, listener);\n return this;\n }\n\n removeAllListeners(event?: keyof Events): this {\n this.emitter.removeAllListeners(event as string);\n return this;\n }\n}\n","import { Client, ClientConfig } from \"pg\";\nimport { TypedEmitter } from \"../shared/emitter\";\n\nexport interface SseListenerEvents {\n connected: void;\n error: Error;\n reconnect: number;\n notification: {\n table: string;\n action: \"INSERT\" | \"UPDATE\" | \"DELETE\";\n id: string | number;\n [key: string]: unknown;\n };\n}\n\nexport interface SseListener {\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n events: TypedEmitter<SseListenerEvents>;\n getActiveConnections(): number;\n registerClient(clientId: string, send: (data: string) => void): void;\n unregisterClient(clientId: string): void;\n broadcast(event: string, data: unknown): void;\n}\n\nexport class PostgresSseListener implements SseListener {\n public events = new TypedEmitter<SseListenerEvents>();\n private client: Client | null = null;\n private isConnected = false;\n private isDisconnecting = false;\n private retryCount = 0;\n private reconnectTimer: NodeJS.Timeout | null = null;\n\n // In-memory registry of active SSE clients\n private clients = new Map<string, (data: string) => void>();\n\n private readonly config: ClientConfig | string;\n private readonly channels: string[];\n\n constructor(\n config: ClientConfig | string,\n channels: string | string[] = \"db_changes\",\n ) {\n this.config = config;\n this.channels = Array.isArray(channels) ? channels : [channels];\n\n // Security validation of channel names to prevent SQL injection via LISTEN\n const channelRegex = /^[a-zA-Z0-9_]+$/;\n for (const channel of this.channels) {\n if (!channelRegex.test(channel)) {\n throw new Error(\n `Invalid channel name: \"${channel}\". Channel names must be alphanumeric and underscores only.`,\n );\n }\n }\n }\n\n public async connect(): Promise<void> {\n if (this.isConnected) return;\n this.isDisconnecting = false;\n\n try {\n this.client =\n typeof this.config === \"string\"\n ? new Client(this.config)\n : new Client(this.config);\n\n this.client.on(\"error\", (err) => {\n this.handleConnectionError(err);\n });\n\n this.client.on(\"notification\", (msg) => {\n this.handleNotification(msg);\n });\n\n await this.client.connect();\n\n for (const channel of this.channels) {\n // Safe because of strict regex validation in constructor\n await this.client.query(`LISTEN ${channel}`);\n }\n\n this.isConnected = true;\n this.retryCount = 0;\n this.events.emit(\"connected\", undefined);\n } catch (err) {\n this.handleConnectionError(err as Error);\n }\n }\n\n public async disconnect(): Promise<void> {\n this.isDisconnecting = true;\n this.isConnected = false;\n\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n\n if (this.client) {\n try {\n await this.client.end();\n } catch (err) {\n // Ignore errors during clean disconnect\n } finally {\n this.client = null;\n }\n }\n }\n\n public getActiveConnections(): number {\n return this.clients.size;\n }\n\n public registerClient(clientId: string, send: (data: string) => void): void {\n this.clients.set(clientId, send);\n }\n\n public unregisterClient(clientId: string): void {\n this.clients.delete(clientId);\n }\n\n public broadcast(event: string, data: unknown): void {\n const message = `event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`;\n for (const send of this.clients.values()) {\n try {\n send(message);\n } catch (err) {\n // Client stream might be closed/broken\n }\n }\n }\n\n private handleConnectionError(err: Error) {\n if (this.isDisconnecting) return;\n\n this.isConnected = false;\n this.events.emit(\"error\", err);\n\n if (this.client) {\n this.client.end().catch(() => {});\n this.client = null;\n }\n\n this.scheduleReconnect();\n }\n\n private scheduleReconnect() {\n if (this.reconnectTimer) return;\n\n this.retryCount++;\n // Exponential backoff: 1s, 2s, 4s, 8s, 16s, max 30s\n const delay = Math.min(1000 * Math.pow(2, this.retryCount - 1), 30000);\n // Add jitter: up to 1000ms\n const jitter = Math.random() * 1000;\n const finalDelay = delay + jitter;\n\n this.events.emit(\"reconnect\", this.retryCount);\n\n this.reconnectTimer = setTimeout(async () => {\n this.reconnectTimer = null;\n try {\n await this.connect();\n } catch (err) {\n // Error will trigger handleConnectionError which calls scheduleReconnect again\n }\n }, finalDelay);\n }\n\n private handleNotification(msg: { channel: string; payload?: string }) {\n if (!msg.payload) return;\n\n try {\n let parsedPayload: unknown;\n try {\n parsedPayload = JSON.parse(msg.payload);\n } catch (e) {\n parsedPayload = msg.payload; // Fallback to raw string\n }\n\n // Check if the payload matches the expected structure, or wrap it\n const isObjectPayload =\n typeof parsedPayload === \"object\" && parsedPayload !== null;\n const payloadObj = isObjectPayload\n ? (parsedPayload as Record<string, unknown>)\n : null;\n\n const eventPayload = payloadObj\n ? {\n table: (payloadObj.table as string) || \"unknown\",\n action:\n (payloadObj.action as \"INSERT\" | \"UPDATE\" | \"DELETE\") || \"INSERT\",\n id:\n payloadObj.id !== undefined\n ? (payloadObj.id as string | number)\n : \"\",\n ...payloadObj,\n }\n : {\n table: \"unknown\",\n action: \"INSERT\" as const,\n id: \"\",\n data: parsedPayload,\n };\n\n // Emit typed event\n this.events.emit(\"notification\", eventPayload);\n\n // Broadcast to all active client SSE streams\n this.broadcast(\"notification\", eventPayload);\n } catch (err) {\n // Avoid throwing unhandled exception inside event loop\n this.events.emit(\n \"error\",\n new Error(\n `Failed to process notification payload: ${(err as Error).message}`,\n ),\n );\n }\n }\n}\n","import { SseListener } from \"./listener\";\n\n/**\n * Creates an HTTP Response for Server-Sent Events (SSE) compatible with Next.js Route Handlers.\n * Handles client registration, keep-alive pings, and resource cleanup on client disconnect.\n *\n * @param listener The active SseListener instance.\n * @param req Optional incoming Request object to listen for connection aborts.\n * @returns A Response object streaming SSE.\n */\nexport function createSseHandler(\n listener: SseListener,\n req?: Request,\n): Response {\n const clientId = Math.random().toString(36).substring(2, 15);\n const encoder = new TextEncoder();\n const responseStream = new TransformStream();\n const writer = responseStream.writable.getWriter();\n\n let isCleanedUp = false;\n\n function cleanup() {\n if (isCleanedUp) return;\n isCleanedUp = true;\n\n clearInterval(keepAliveInterval);\n listener.unregisterClient(clientId);\n\n try {\n writer.close();\n } catch (err) {\n // Stream is already closed\n }\n }\n\n function send(data: string) {\n if (isCleanedUp) return;\n\n writer.write(encoder.encode(data)).catch(() => {\n // Write failed, connection probably severed\n cleanup();\n });\n }\n\n // Register client to receive broadcasts\n listener.registerClient(clientId, send);\n\n // Keep-alive timer to prevent proxies (Cloud Run, Cloudflare, etc.) from timing out\n const keepAliveInterval = setInterval(() => {\n if (isCleanedUp) return;\n writer.write(encoder.encode(\": keep-alive\\n\\n\")).catch(() => {\n cleanup();\n });\n }, 20000);\n\n // Initial handshake to establish the client ID and current active connections count\n const handshake = {\n type: \"handshake\",\n clientId,\n activeConnections: listener.getActiveConnections(),\n };\n\n writer\n .write(\n encoder.encode(\n `event: handshake\\ndata: ${JSON.stringify(handshake)}\\n\\n`,\n ),\n )\n .catch(() => {\n cleanup();\n });\n\n // Watch for request cancellation/abort\n if (req?.signal) {\n if (req.signal.aborted) {\n cleanup();\n } else {\n req.signal.addEventListener(\"abort\", () => {\n cleanup();\n });\n }\n }\n\n return new Response(responseStream.readable, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n \"X-Accel-Buffering\": \"no\", // Turn off buffering in Nginx/Vercel\n },\n });\n}\n","\"use client\";\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { ConnectionStatus, TabMultiplexer } from \"./multiplexer\";\n\nexport interface SseContextType {\n subscribe: <T = unknown>(\n table: string,\n callback: (payload: T) => void,\n ) => () => void;\n status: ConnectionStatus;\n eventCount: number;\n activeConnections: number;\n}\n\nconst SseContext = createContext<SseContextType | null>(null);\n\nexport const SseProvider: React.FC<{\n children: React.ReactNode;\n endpoint: string;\n}> = ({ children, endpoint }) => {\n const [status, setStatus] = useState<ConnectionStatus>(\"disconnected\");\n const [eventCount, setEventCount] = useState(0);\n const [activeConnections, setActiveConnections] = useState(0);\n\n // Keep subscriptions in a ref to avoid triggering re-renders on subscription updates\n const subscriptions = useRef<Map<string, Set<(payload: unknown) => void>>>(\n new Map(),\n );\n\n // Handle status and active connection count updates\n const handleStatusChange = useCallback(\n (newStatus: ConnectionStatus, activeCount: number) => {\n setActiveConnections(activeCount);\n setStatus((prev) => {\n if (newStatus === \"connecting\") {\n if (prev === \"connected\") {\n console.warn(`[pg-sse] Stream connection lost. Reconnecting...`);\n } else if (prev === \"disconnected\") {\n console.log(`[pg-sse] Connecting to stream...`);\n }\n } else if (newStatus === \"connected\" && prev !== \"connected\") {\n console.log(\n `[pg-sse] Stream connected. Active client count: ${activeCount}`,\n );\n }\n return newStatus;\n });\n },\n [],\n );\n\n // Dispatch events to matching subscribers\n const handleEvent = useCallback((event: string, payload: unknown) => {\n if (event === \"notification\") {\n setEventCount((prev) => prev + 1);\n\n const table = (payload as Record<string, unknown> | null)?.table as\n | string\n | undefined;\n if (table) {\n const callbacks = subscriptions.current.get(table);\n if (callbacks) {\n callbacks.forEach((cb) => {\n try {\n cb(payload);\n } catch (err) {\n console.error(\n `[pg-sse] Callback for table \"${table}\" failed:`,\n err,\n );\n }\n });\n }\n }\n }\n }, []);\n\n useEffect(() => {\n const multiplexer = new TabMultiplexer(\n endpoint,\n handleEvent,\n handleStatusChange,\n );\n\n return () => {\n multiplexer.destroy();\n };\n }, [endpoint, handleEvent, handleStatusChange]);\n\n const subscribe = useCallback(\n <T = unknown,>(table: string, callback: (payload: T) => void) => {\n let callbacks = subscriptions.current.get(table);\n if (!callbacks) {\n callbacks = new Set();\n subscriptions.current.set(table, callbacks);\n }\n callbacks.add(callback as (payload: unknown) => void);\n\n return () => {\n const callbacks = subscriptions.current.get(table);\n if (callbacks) {\n callbacks.delete(callback as (payload: unknown) => void);\n if (callbacks.size === 0) {\n subscriptions.current.delete(table);\n }\n }\n };\n },\n [],\n );\n\n return (\n <SseContext.Provider\n value={{ subscribe, status, eventCount, activeConnections }}\n >\n {children}\n </SseContext.Provider>\n );\n};\n\nexport function useSubscription<T = unknown>(\n table: string,\n callback: (payload: T) => void,\n): void {\n const context = useContext(SseContext);\n if (!context) {\n throw new Error(\"useSubscription must be used within an SseProvider\");\n }\n\n const { subscribe } = context;\n const callbackRef = useRef(callback);\n\n // Keep callback ref updated so subscription doesn't re-subscribe on every callback changes\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n useEffect(() => {\n const unsubscribe = subscribe<T>(table, (payload) => {\n callbackRef.current(payload);\n });\n return unsubscribe;\n }, [table, subscribe]);\n}\n\nexport function useSseStatus(): {\n status: ConnectionStatus;\n eventCount: number;\n activeConnections: number;\n} {\n const context = useContext(SseContext);\n if (!context) {\n throw new Error(\"useSseStatus must be used within an SseProvider\");\n }\n const { status, eventCount, activeConnections } = context;\n return { status, eventCount, activeConnections };\n}\n","export type ConnectionStatus = \"connecting\" | \"connected\" | \"disconnected\";\n\nexport interface MultiplexerMessage {\n type: \"query_leader\" | \"heartbeat\" | \"claim\" | \"release\" | \"status\" | \"event\";\n senderId: string;\n payload?: unknown;\n}\n\nexport class TabMultiplexer {\n private channel: BroadcastChannel | null = null;\n private tabId: string;\n private isLeader = false;\n private leaderId: string | null = null;\n private status: ConnectionStatus = \"disconnected\";\n\n private heartbeatInterval: NodeJS.Timeout | null = null;\n private watchdogTimeout: NodeJS.Timeout | null = null;\n private electionTimeout: NodeJS.Timeout | null = null;\n private eventSource: EventSource | null = null;\n\n private claimInProgress = false;\n\n constructor(\n private endpoint: string,\n private onEvent: (event: string, payload: unknown) => void,\n private onStatusChange: (\n status: ConnectionStatus,\n activeConnections: number,\n ) => void,\n ) {\n this.tabId = Math.random().toString(36).substring(2, 11);\n\n if (\n typeof window !== \"undefined\" &&\n typeof BroadcastChannel !== \"undefined\"\n ) {\n this.channel = new BroadcastChannel(\"pg-sse-multiplexer-channel\");\n this.channel.onmessage = (event) => this.handleChannelMessage(event.data);\n\n // Hook window unload to release leadership if we close\n window.addEventListener(\"beforeunload\", () => this.destroy());\n\n // Start a watchdog to claim leadership if no leader responds in 1.5 seconds\n this.resetWatchdog(1500);\n\n // Query if there is an existing leader\n this.broadcast({ type: \"query_leader\", senderId: this.tabId });\n } else {\n // Fallback for non-browser or environments without BroadcastChannel\n this.startDirectConnection();\n }\n }\n\n public destroy(): void {\n const wasLeader = this.isLeader;\n this.isLeader = false;\n\n if (wasLeader) {\n this.broadcast({ type: \"release\", senderId: this.tabId });\n }\n\n if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);\n if (this.watchdogTimeout) clearTimeout(this.watchdogTimeout);\n if (this.electionTimeout) clearTimeout(this.electionTimeout);\n\n this.stopEventSource();\n\n if (this.channel) {\n this.channel.close();\n this.channel = null;\n }\n }\n\n private broadcast(msg: MultiplexerMessage): void {\n if (this.channel) {\n try {\n this.channel.postMessage(msg);\n } catch (e) {\n // Broadcast channel might be closed\n }\n }\n }\n\n private handleChannelMessage(msg: MultiplexerMessage): void {\n if (msg.senderId === this.tabId) return;\n\n switch (msg.type) {\n case \"query_leader\":\n if (this.isLeader) {\n this.sendHeartbeat();\n }\n break;\n\n case \"heartbeat\":\n this.claimInProgress = false;\n if (this.electionTimeout) {\n clearTimeout(this.electionTimeout);\n this.electionTimeout = null;\n }\n this.leaderId = msg.senderId;\n this.resetWatchdog(5000); // 5s watchdog\n break;\n\n case \"claim\":\n // Another tab wants to claim leadership.\n if (this.isLeader) {\n // Assert dominance immediately\n this.sendHeartbeat();\n } else if (this.claimInProgress) {\n // If we also want to claim, the one with lower lexicographical tabId wins\n if (msg.senderId < this.tabId) {\n // We lose the election, abort our claim\n this.claimInProgress = false;\n if (this.electionTimeout) {\n clearTimeout(this.electionTimeout);\n this.electionTimeout = null;\n }\n }\n }\n break;\n\n case \"release\":\n if (this.leaderId === msg.senderId) {\n this.leaderId = null;\n // Trigger immediate election\n this.attemptLeadershipClaim();\n }\n break;\n\n case \"status\":\n if (!this.isLeader && this.leaderId === msg.senderId) {\n const payloadObj = msg.payload as {\n status: ConnectionStatus;\n activeConnections: number;\n };\n this.status = payloadObj.status;\n this.onStatusChange(this.status, payloadObj.activeConnections);\n }\n break;\n\n case \"event\":\n if (!this.isLeader && this.leaderId === msg.senderId) {\n const payloadObj = msg.payload as { event: string; data: unknown };\n this.onEvent(payloadObj.event, payloadObj.data);\n }\n break;\n }\n }\n\n private resetWatchdog(ms: number): void {\n if (this.watchdogTimeout) clearTimeout(this.watchdogTimeout);\n this.watchdogTimeout = setTimeout(() => {\n this.attemptLeadershipClaim();\n }, ms);\n }\n\n private attemptLeadershipClaim(): void {\n if (this.isLeader || this.claimInProgress) return;\n\n this.claimInProgress = true;\n this.broadcast({ type: \"claim\", senderId: this.tabId });\n\n // Wait 500ms for objection (heartbeat or higher priority claim)\n this.electionTimeout = setTimeout(() => {\n this.electionTimeout = null;\n if (this.claimInProgress) {\n this.claimInProgress = false;\n this.becomeLeader();\n }\n }, 500);\n }\n\n private becomeLeader(): void {\n this.isLeader = true;\n this.leaderId = this.tabId;\n if (this.watchdogTimeout) {\n clearTimeout(this.watchdogTimeout);\n this.watchdogTimeout = null;\n }\n\n // Start heartbeat broadcasts\n this.sendHeartbeat();\n this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), 2000);\n\n // Establish SSE EventSource\n this.startEventSource();\n }\n\n private sendHeartbeat(): void {\n this.broadcast({ type: \"heartbeat\", senderId: this.tabId });\n }\n\n private updateStatus(\n newStatus: ConnectionStatus,\n activeConnections = 0,\n ): void {\n this.status = newStatus;\n this.onStatusChange(newStatus, activeConnections);\n if (this.isLeader) {\n this.broadcast({\n type: \"status\",\n senderId: this.tabId,\n payload: { status: newStatus, activeConnections },\n });\n }\n }\n\n private startEventSource(): void {\n this.stopEventSource();\n this.updateStatus(\"connecting\");\n\n try {\n this.eventSource = new EventSource(this.endpoint);\n\n this.eventSource.addEventListener(\"handshake\", (e) => {\n try {\n const data = JSON.parse(e.data);\n this.updateStatus(\"connected\", data.activeConnections || 1);\n } catch (err) {\n this.updateStatus(\"connected\", 1);\n }\n });\n\n this.eventSource.addEventListener(\"notification\", (e) => {\n try {\n const data = JSON.parse(e.data);\n this.onEvent(\"notification\", data);\n this.broadcast({\n type: \"event\",\n senderId: this.tabId,\n payload: { event: \"notification\", data },\n });\n } catch (err) {\n // Parse error\n }\n });\n\n this.eventSource.onerror = () => {\n this.updateStatus(\"connecting\");\n };\n } catch (err) {\n this.updateStatus(\"disconnected\");\n }\n }\n\n private startDirectConnection(): void {\n // Non-browser fallback or single-tab mode\n this.startEventSource();\n }\n\n private stopEventSource(): void {\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n }\n}\n"],"mappings":"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","names":["callbacks"]}
|