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.mjs
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
SseProvider,
|
|
4
|
+
useSseStatus,
|
|
5
|
+
useSubscription
|
|
6
|
+
} from "./chunk-GFLW4AMU.mjs";
|
|
7
|
+
|
|
8
|
+
// src/shared/emitter.ts
|
|
9
|
+
import { EventEmitter } from "events";
|
|
10
|
+
var TypedEmitter = class {
|
|
11
|
+
emitter = new EventEmitter();
|
|
12
|
+
on(event, listener) {
|
|
13
|
+
this.emitter.on(event, listener);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
once(event, listener) {
|
|
17
|
+
this.emitter.once(event, listener);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
off(event, listener) {
|
|
21
|
+
this.emitter.off(event, listener);
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
emit(event, payload) {
|
|
25
|
+
return this.emitter.emit(event, payload);
|
|
26
|
+
}
|
|
27
|
+
addListener(event, listener) {
|
|
28
|
+
this.emitter.addListener(event, listener);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
removeListener(event, listener) {
|
|
32
|
+
this.emitter.removeListener(event, listener);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
removeAllListeners(event) {
|
|
36
|
+
this.emitter.removeAllListeners(event);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/server/listener.ts
|
|
42
|
+
import { Client } from "pg";
|
|
43
|
+
var PostgresSseListener = class {
|
|
44
|
+
events = new TypedEmitter();
|
|
45
|
+
client = null;
|
|
46
|
+
isConnected = false;
|
|
47
|
+
isDisconnecting = false;
|
|
48
|
+
retryCount = 0;
|
|
49
|
+
reconnectTimer = null;
|
|
50
|
+
// In-memory registry of active SSE clients
|
|
51
|
+
clients = /* @__PURE__ */ new Map();
|
|
52
|
+
config;
|
|
53
|
+
channels;
|
|
54
|
+
constructor(config, channels = "db_changes") {
|
|
55
|
+
this.config = config;
|
|
56
|
+
this.channels = Array.isArray(channels) ? channels : [channels];
|
|
57
|
+
const channelRegex = /^[a-zA-Z0-9_]+$/;
|
|
58
|
+
for (const channel of this.channels) {
|
|
59
|
+
if (!channelRegex.test(channel)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Invalid channel name: "${channel}". Channel names must be alphanumeric and underscores only.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async connect() {
|
|
67
|
+
if (this.isConnected) return;
|
|
68
|
+
this.isDisconnecting = false;
|
|
69
|
+
try {
|
|
70
|
+
this.client = typeof this.config === "string" ? new Client(this.config) : new Client(this.config);
|
|
71
|
+
this.client.on("error", (err) => {
|
|
72
|
+
this.handleConnectionError(err);
|
|
73
|
+
});
|
|
74
|
+
this.client.on("notification", (msg) => {
|
|
75
|
+
this.handleNotification(msg);
|
|
76
|
+
});
|
|
77
|
+
await this.client.connect();
|
|
78
|
+
for (const channel of this.channels) {
|
|
79
|
+
await this.client.query(`LISTEN ${channel}`);
|
|
80
|
+
}
|
|
81
|
+
this.isConnected = true;
|
|
82
|
+
this.retryCount = 0;
|
|
83
|
+
this.events.emit("connected", void 0);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
this.handleConnectionError(err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async disconnect() {
|
|
89
|
+
this.isDisconnecting = true;
|
|
90
|
+
this.isConnected = false;
|
|
91
|
+
if (this.reconnectTimer) {
|
|
92
|
+
clearTimeout(this.reconnectTimer);
|
|
93
|
+
this.reconnectTimer = null;
|
|
94
|
+
}
|
|
95
|
+
if (this.client) {
|
|
96
|
+
try {
|
|
97
|
+
await this.client.end();
|
|
98
|
+
} catch (err) {
|
|
99
|
+
} finally {
|
|
100
|
+
this.client = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
getActiveConnections() {
|
|
105
|
+
return this.clients.size;
|
|
106
|
+
}
|
|
107
|
+
registerClient(clientId, send) {
|
|
108
|
+
this.clients.set(clientId, send);
|
|
109
|
+
}
|
|
110
|
+
unregisterClient(clientId) {
|
|
111
|
+
this.clients.delete(clientId);
|
|
112
|
+
}
|
|
113
|
+
broadcast(event, data) {
|
|
114
|
+
const message = `event: ${event}
|
|
115
|
+
data: ${JSON.stringify(data)}
|
|
116
|
+
|
|
117
|
+
`;
|
|
118
|
+
for (const send of this.clients.values()) {
|
|
119
|
+
try {
|
|
120
|
+
send(message);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
handleConnectionError(err) {
|
|
126
|
+
if (this.isDisconnecting) return;
|
|
127
|
+
this.isConnected = false;
|
|
128
|
+
this.events.emit("error", err);
|
|
129
|
+
if (this.client) {
|
|
130
|
+
this.client.end().catch(() => {
|
|
131
|
+
});
|
|
132
|
+
this.client = null;
|
|
133
|
+
}
|
|
134
|
+
this.scheduleReconnect();
|
|
135
|
+
}
|
|
136
|
+
scheduleReconnect() {
|
|
137
|
+
if (this.reconnectTimer) return;
|
|
138
|
+
this.retryCount++;
|
|
139
|
+
const delay = Math.min(1e3 * Math.pow(2, this.retryCount - 1), 3e4);
|
|
140
|
+
const jitter = Math.random() * 1e3;
|
|
141
|
+
const finalDelay = delay + jitter;
|
|
142
|
+
this.events.emit("reconnect", this.retryCount);
|
|
143
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
144
|
+
this.reconnectTimer = null;
|
|
145
|
+
try {
|
|
146
|
+
await this.connect();
|
|
147
|
+
} catch (err) {
|
|
148
|
+
}
|
|
149
|
+
}, finalDelay);
|
|
150
|
+
}
|
|
151
|
+
handleNotification(msg) {
|
|
152
|
+
if (!msg.payload) return;
|
|
153
|
+
try {
|
|
154
|
+
let parsedPayload;
|
|
155
|
+
try {
|
|
156
|
+
parsedPayload = JSON.parse(msg.payload);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
parsedPayload = msg.payload;
|
|
159
|
+
}
|
|
160
|
+
const isObjectPayload = typeof parsedPayload === "object" && parsedPayload !== null;
|
|
161
|
+
const payloadObj = isObjectPayload ? parsedPayload : null;
|
|
162
|
+
const eventPayload = payloadObj ? {
|
|
163
|
+
table: payloadObj.table || "unknown",
|
|
164
|
+
action: payloadObj.action || "INSERT",
|
|
165
|
+
id: payloadObj.id !== void 0 ? payloadObj.id : "",
|
|
166
|
+
...payloadObj
|
|
167
|
+
} : {
|
|
168
|
+
table: "unknown",
|
|
169
|
+
action: "INSERT",
|
|
170
|
+
id: "",
|
|
171
|
+
data: parsedPayload
|
|
172
|
+
};
|
|
173
|
+
this.events.emit("notification", eventPayload);
|
|
174
|
+
this.broadcast("notification", eventPayload);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
this.events.emit(
|
|
177
|
+
"error",
|
|
178
|
+
new Error(
|
|
179
|
+
`Failed to process notification payload: ${err.message}`
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/server/handler.ts
|
|
187
|
+
function createSseHandler(listener, req) {
|
|
188
|
+
const clientId = Math.random().toString(36).substring(2, 15);
|
|
189
|
+
const encoder = new TextEncoder();
|
|
190
|
+
const responseStream = new TransformStream();
|
|
191
|
+
const writer = responseStream.writable.getWriter();
|
|
192
|
+
let isCleanedUp = false;
|
|
193
|
+
function cleanup() {
|
|
194
|
+
if (isCleanedUp) return;
|
|
195
|
+
isCleanedUp = true;
|
|
196
|
+
clearInterval(keepAliveInterval);
|
|
197
|
+
listener.unregisterClient(clientId);
|
|
198
|
+
try {
|
|
199
|
+
writer.close();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function send(data) {
|
|
204
|
+
if (isCleanedUp) return;
|
|
205
|
+
writer.write(encoder.encode(data)).catch(() => {
|
|
206
|
+
cleanup();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
listener.registerClient(clientId, send);
|
|
210
|
+
const keepAliveInterval = setInterval(() => {
|
|
211
|
+
if (isCleanedUp) return;
|
|
212
|
+
writer.write(encoder.encode(": keep-alive\n\n")).catch(() => {
|
|
213
|
+
cleanup();
|
|
214
|
+
});
|
|
215
|
+
}, 2e4);
|
|
216
|
+
const handshake = {
|
|
217
|
+
type: "handshake",
|
|
218
|
+
clientId,
|
|
219
|
+
activeConnections: listener.getActiveConnections()
|
|
220
|
+
};
|
|
221
|
+
writer.write(
|
|
222
|
+
encoder.encode(
|
|
223
|
+
`event: handshake
|
|
224
|
+
data: ${JSON.stringify(handshake)}
|
|
225
|
+
|
|
226
|
+
`
|
|
227
|
+
)
|
|
228
|
+
).catch(() => {
|
|
229
|
+
cleanup();
|
|
230
|
+
});
|
|
231
|
+
if (req?.signal) {
|
|
232
|
+
if (req.signal.aborted) {
|
|
233
|
+
cleanup();
|
|
234
|
+
} else {
|
|
235
|
+
req.signal.addEventListener("abort", () => {
|
|
236
|
+
cleanup();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return new Response(responseStream.readable, {
|
|
241
|
+
headers: {
|
|
242
|
+
"Content-Type": "text/event-stream",
|
|
243
|
+
"Cache-Control": "no-cache, no-transform",
|
|
244
|
+
Connection: "keep-alive",
|
|
245
|
+
"X-Accel-Buffering": "no"
|
|
246
|
+
// Turn off buffering in Nginx/Vercel
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
PostgresSseListener,
|
|
252
|
+
SseProvider,
|
|
253
|
+
TypedEmitter,
|
|
254
|
+
createSseHandler,
|
|
255
|
+
useSseStatus,
|
|
256
|
+
useSubscription
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/emitter.ts","../src/server/listener.ts","../src/server/handler.ts"],"sourcesContent":["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"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ClientConfig } from 'pg';
|
|
2
|
+
|
|
3
|
+
declare class TypedEmitter<Events extends object> {
|
|
4
|
+
private emitter;
|
|
5
|
+
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
6
|
+
once<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
7
|
+
off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
8
|
+
emit<K extends keyof Events>(event: K, payload: Events[K]): boolean;
|
|
9
|
+
addListener<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
10
|
+
removeListener<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
11
|
+
removeAllListeners(event?: keyof Events): this;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SseListenerEvents {
|
|
15
|
+
connected: void;
|
|
16
|
+
error: Error;
|
|
17
|
+
reconnect: number;
|
|
18
|
+
notification: {
|
|
19
|
+
table: string;
|
|
20
|
+
action: "INSERT" | "UPDATE" | "DELETE";
|
|
21
|
+
id: string | number;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
interface SseListener {
|
|
26
|
+
connect(): Promise<void>;
|
|
27
|
+
disconnect(): Promise<void>;
|
|
28
|
+
events: TypedEmitter<SseListenerEvents>;
|
|
29
|
+
getActiveConnections(): number;
|
|
30
|
+
registerClient(clientId: string, send: (data: string) => void): void;
|
|
31
|
+
unregisterClient(clientId: string): void;
|
|
32
|
+
broadcast(event: string, data: unknown): void;
|
|
33
|
+
}
|
|
34
|
+
declare class PostgresSseListener implements SseListener {
|
|
35
|
+
events: TypedEmitter<SseListenerEvents>;
|
|
36
|
+
private client;
|
|
37
|
+
private isConnected;
|
|
38
|
+
private isDisconnecting;
|
|
39
|
+
private retryCount;
|
|
40
|
+
private reconnectTimer;
|
|
41
|
+
private clients;
|
|
42
|
+
private readonly config;
|
|
43
|
+
private readonly channels;
|
|
44
|
+
constructor(config: ClientConfig | string, channels?: string | string[]);
|
|
45
|
+
connect(): Promise<void>;
|
|
46
|
+
disconnect(): Promise<void>;
|
|
47
|
+
getActiveConnections(): number;
|
|
48
|
+
registerClient(clientId: string, send: (data: string) => void): void;
|
|
49
|
+
unregisterClient(clientId: string): void;
|
|
50
|
+
broadcast(event: string, data: unknown): void;
|
|
51
|
+
private handleConnectionError;
|
|
52
|
+
private scheduleReconnect;
|
|
53
|
+
private handleNotification;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates an HTTP Response for Server-Sent Events (SSE) compatible with Next.js Route Handlers.
|
|
58
|
+
* Handles client registration, keep-alive pings, and resource cleanup on client disconnect.
|
|
59
|
+
*
|
|
60
|
+
* @param listener The active SseListener instance.
|
|
61
|
+
* @param req Optional incoming Request object to listen for connection aborts.
|
|
62
|
+
* @returns A Response object streaming SSE.
|
|
63
|
+
*/
|
|
64
|
+
declare function createSseHandler(listener: SseListener, req?: Request): Response;
|
|
65
|
+
|
|
66
|
+
export { PostgresSseListener, type SseListener, type SseListenerEvents, createSseHandler };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ClientConfig } from 'pg';
|
|
2
|
+
|
|
3
|
+
declare class TypedEmitter<Events extends object> {
|
|
4
|
+
private emitter;
|
|
5
|
+
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
6
|
+
once<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
7
|
+
off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
8
|
+
emit<K extends keyof Events>(event: K, payload: Events[K]): boolean;
|
|
9
|
+
addListener<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
10
|
+
removeListener<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): this;
|
|
11
|
+
removeAllListeners(event?: keyof Events): this;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SseListenerEvents {
|
|
15
|
+
connected: void;
|
|
16
|
+
error: Error;
|
|
17
|
+
reconnect: number;
|
|
18
|
+
notification: {
|
|
19
|
+
table: string;
|
|
20
|
+
action: "INSERT" | "UPDATE" | "DELETE";
|
|
21
|
+
id: string | number;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
interface SseListener {
|
|
26
|
+
connect(): Promise<void>;
|
|
27
|
+
disconnect(): Promise<void>;
|
|
28
|
+
events: TypedEmitter<SseListenerEvents>;
|
|
29
|
+
getActiveConnections(): number;
|
|
30
|
+
registerClient(clientId: string, send: (data: string) => void): void;
|
|
31
|
+
unregisterClient(clientId: string): void;
|
|
32
|
+
broadcast(event: string, data: unknown): void;
|
|
33
|
+
}
|
|
34
|
+
declare class PostgresSseListener implements SseListener {
|
|
35
|
+
events: TypedEmitter<SseListenerEvents>;
|
|
36
|
+
private client;
|
|
37
|
+
private isConnected;
|
|
38
|
+
private isDisconnecting;
|
|
39
|
+
private retryCount;
|
|
40
|
+
private reconnectTimer;
|
|
41
|
+
private clients;
|
|
42
|
+
private readonly config;
|
|
43
|
+
private readonly channels;
|
|
44
|
+
constructor(config: ClientConfig | string, channels?: string | string[]);
|
|
45
|
+
connect(): Promise<void>;
|
|
46
|
+
disconnect(): Promise<void>;
|
|
47
|
+
getActiveConnections(): number;
|
|
48
|
+
registerClient(clientId: string, send: (data: string) => void): void;
|
|
49
|
+
unregisterClient(clientId: string): void;
|
|
50
|
+
broadcast(event: string, data: unknown): void;
|
|
51
|
+
private handleConnectionError;
|
|
52
|
+
private scheduleReconnect;
|
|
53
|
+
private handleNotification;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates an HTTP Response for Server-Sent Events (SSE) compatible with Next.js Route Handlers.
|
|
58
|
+
* Handles client registration, keep-alive pings, and resource cleanup on client disconnect.
|
|
59
|
+
*
|
|
60
|
+
* @param listener The active SseListener instance.
|
|
61
|
+
* @param req Optional incoming Request object to listen for connection aborts.
|
|
62
|
+
* @returns A Response object streaming SSE.
|
|
63
|
+
*/
|
|
64
|
+
declare function createSseHandler(listener: SseListener, req?: Request): Response;
|
|
65
|
+
|
|
66
|
+
export { PostgresSseListener, type SseListener, type SseListenerEvents, createSseHandler };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/index.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
PostgresSseListener: () => PostgresSseListener,
|
|
24
|
+
createSseHandler: () => createSseHandler
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(server_exports);
|
|
27
|
+
|
|
28
|
+
// src/server/listener.ts
|
|
29
|
+
var import_pg = require("pg");
|
|
30
|
+
|
|
31
|
+
// src/shared/emitter.ts
|
|
32
|
+
var import_events = require("events");
|
|
33
|
+
var TypedEmitter = class {
|
|
34
|
+
emitter = new import_events.EventEmitter();
|
|
35
|
+
on(event, listener) {
|
|
36
|
+
this.emitter.on(event, listener);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
once(event, listener) {
|
|
40
|
+
this.emitter.once(event, listener);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
off(event, listener) {
|
|
44
|
+
this.emitter.off(event, listener);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
emit(event, payload) {
|
|
48
|
+
return this.emitter.emit(event, payload);
|
|
49
|
+
}
|
|
50
|
+
addListener(event, listener) {
|
|
51
|
+
this.emitter.addListener(event, listener);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
removeListener(event, listener) {
|
|
55
|
+
this.emitter.removeListener(event, listener);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
removeAllListeners(event) {
|
|
59
|
+
this.emitter.removeAllListeners(event);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/server/listener.ts
|
|
65
|
+
var PostgresSseListener = class {
|
|
66
|
+
events = new TypedEmitter();
|
|
67
|
+
client = null;
|
|
68
|
+
isConnected = false;
|
|
69
|
+
isDisconnecting = false;
|
|
70
|
+
retryCount = 0;
|
|
71
|
+
reconnectTimer = null;
|
|
72
|
+
// In-memory registry of active SSE clients
|
|
73
|
+
clients = /* @__PURE__ */ new Map();
|
|
74
|
+
config;
|
|
75
|
+
channels;
|
|
76
|
+
constructor(config, channels = "db_changes") {
|
|
77
|
+
this.config = config;
|
|
78
|
+
this.channels = Array.isArray(channels) ? channels : [channels];
|
|
79
|
+
const channelRegex = /^[a-zA-Z0-9_]+$/;
|
|
80
|
+
for (const channel of this.channels) {
|
|
81
|
+
if (!channelRegex.test(channel)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Invalid channel name: "${channel}". Channel names must be alphanumeric and underscores only.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async connect() {
|
|
89
|
+
if (this.isConnected) return;
|
|
90
|
+
this.isDisconnecting = false;
|
|
91
|
+
try {
|
|
92
|
+
this.client = typeof this.config === "string" ? new import_pg.Client(this.config) : new import_pg.Client(this.config);
|
|
93
|
+
this.client.on("error", (err) => {
|
|
94
|
+
this.handleConnectionError(err);
|
|
95
|
+
});
|
|
96
|
+
this.client.on("notification", (msg) => {
|
|
97
|
+
this.handleNotification(msg);
|
|
98
|
+
});
|
|
99
|
+
await this.client.connect();
|
|
100
|
+
for (const channel of this.channels) {
|
|
101
|
+
await this.client.query(`LISTEN ${channel}`);
|
|
102
|
+
}
|
|
103
|
+
this.isConnected = true;
|
|
104
|
+
this.retryCount = 0;
|
|
105
|
+
this.events.emit("connected", void 0);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
this.handleConnectionError(err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async disconnect() {
|
|
111
|
+
this.isDisconnecting = true;
|
|
112
|
+
this.isConnected = false;
|
|
113
|
+
if (this.reconnectTimer) {
|
|
114
|
+
clearTimeout(this.reconnectTimer);
|
|
115
|
+
this.reconnectTimer = null;
|
|
116
|
+
}
|
|
117
|
+
if (this.client) {
|
|
118
|
+
try {
|
|
119
|
+
await this.client.end();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
} finally {
|
|
122
|
+
this.client = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
getActiveConnections() {
|
|
127
|
+
return this.clients.size;
|
|
128
|
+
}
|
|
129
|
+
registerClient(clientId, send) {
|
|
130
|
+
this.clients.set(clientId, send);
|
|
131
|
+
}
|
|
132
|
+
unregisterClient(clientId) {
|
|
133
|
+
this.clients.delete(clientId);
|
|
134
|
+
}
|
|
135
|
+
broadcast(event, data) {
|
|
136
|
+
const message = `event: ${event}
|
|
137
|
+
data: ${JSON.stringify(data)}
|
|
138
|
+
|
|
139
|
+
`;
|
|
140
|
+
for (const send of this.clients.values()) {
|
|
141
|
+
try {
|
|
142
|
+
send(message);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
handleConnectionError(err) {
|
|
148
|
+
if (this.isDisconnecting) return;
|
|
149
|
+
this.isConnected = false;
|
|
150
|
+
this.events.emit("error", err);
|
|
151
|
+
if (this.client) {
|
|
152
|
+
this.client.end().catch(() => {
|
|
153
|
+
});
|
|
154
|
+
this.client = null;
|
|
155
|
+
}
|
|
156
|
+
this.scheduleReconnect();
|
|
157
|
+
}
|
|
158
|
+
scheduleReconnect() {
|
|
159
|
+
if (this.reconnectTimer) return;
|
|
160
|
+
this.retryCount++;
|
|
161
|
+
const delay = Math.min(1e3 * Math.pow(2, this.retryCount - 1), 3e4);
|
|
162
|
+
const jitter = Math.random() * 1e3;
|
|
163
|
+
const finalDelay = delay + jitter;
|
|
164
|
+
this.events.emit("reconnect", this.retryCount);
|
|
165
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
166
|
+
this.reconnectTimer = null;
|
|
167
|
+
try {
|
|
168
|
+
await this.connect();
|
|
169
|
+
} catch (err) {
|
|
170
|
+
}
|
|
171
|
+
}, finalDelay);
|
|
172
|
+
}
|
|
173
|
+
handleNotification(msg) {
|
|
174
|
+
if (!msg.payload) return;
|
|
175
|
+
try {
|
|
176
|
+
let parsedPayload;
|
|
177
|
+
try {
|
|
178
|
+
parsedPayload = JSON.parse(msg.payload);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
parsedPayload = msg.payload;
|
|
181
|
+
}
|
|
182
|
+
const isObjectPayload = typeof parsedPayload === "object" && parsedPayload !== null;
|
|
183
|
+
const payloadObj = isObjectPayload ? parsedPayload : null;
|
|
184
|
+
const eventPayload = payloadObj ? {
|
|
185
|
+
table: payloadObj.table || "unknown",
|
|
186
|
+
action: payloadObj.action || "INSERT",
|
|
187
|
+
id: payloadObj.id !== void 0 ? payloadObj.id : "",
|
|
188
|
+
...payloadObj
|
|
189
|
+
} : {
|
|
190
|
+
table: "unknown",
|
|
191
|
+
action: "INSERT",
|
|
192
|
+
id: "",
|
|
193
|
+
data: parsedPayload
|
|
194
|
+
};
|
|
195
|
+
this.events.emit("notification", eventPayload);
|
|
196
|
+
this.broadcast("notification", eventPayload);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
this.events.emit(
|
|
199
|
+
"error",
|
|
200
|
+
new Error(
|
|
201
|
+
`Failed to process notification payload: ${err.message}`
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/server/handler.ts
|
|
209
|
+
function createSseHandler(listener, req) {
|
|
210
|
+
const clientId = Math.random().toString(36).substring(2, 15);
|
|
211
|
+
const encoder = new TextEncoder();
|
|
212
|
+
const responseStream = new TransformStream();
|
|
213
|
+
const writer = responseStream.writable.getWriter();
|
|
214
|
+
let isCleanedUp = false;
|
|
215
|
+
function cleanup() {
|
|
216
|
+
if (isCleanedUp) return;
|
|
217
|
+
isCleanedUp = true;
|
|
218
|
+
clearInterval(keepAliveInterval);
|
|
219
|
+
listener.unregisterClient(clientId);
|
|
220
|
+
try {
|
|
221
|
+
writer.close();
|
|
222
|
+
} catch (err) {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function send(data) {
|
|
226
|
+
if (isCleanedUp) return;
|
|
227
|
+
writer.write(encoder.encode(data)).catch(() => {
|
|
228
|
+
cleanup();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
listener.registerClient(clientId, send);
|
|
232
|
+
const keepAliveInterval = setInterval(() => {
|
|
233
|
+
if (isCleanedUp) return;
|
|
234
|
+
writer.write(encoder.encode(": keep-alive\n\n")).catch(() => {
|
|
235
|
+
cleanup();
|
|
236
|
+
});
|
|
237
|
+
}, 2e4);
|
|
238
|
+
const handshake = {
|
|
239
|
+
type: "handshake",
|
|
240
|
+
clientId,
|
|
241
|
+
activeConnections: listener.getActiveConnections()
|
|
242
|
+
};
|
|
243
|
+
writer.write(
|
|
244
|
+
encoder.encode(
|
|
245
|
+
`event: handshake
|
|
246
|
+
data: ${JSON.stringify(handshake)}
|
|
247
|
+
|
|
248
|
+
`
|
|
249
|
+
)
|
|
250
|
+
).catch(() => {
|
|
251
|
+
cleanup();
|
|
252
|
+
});
|
|
253
|
+
if (req?.signal) {
|
|
254
|
+
if (req.signal.aborted) {
|
|
255
|
+
cleanup();
|
|
256
|
+
} else {
|
|
257
|
+
req.signal.addEventListener("abort", () => {
|
|
258
|
+
cleanup();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return new Response(responseStream.readable, {
|
|
263
|
+
headers: {
|
|
264
|
+
"Content-Type": "text/event-stream",
|
|
265
|
+
"Cache-Control": "no-cache, no-transform",
|
|
266
|
+
Connection: "keep-alive",
|
|
267
|
+
"X-Accel-Buffering": "no"
|
|
268
|
+
// Turn off buffering in Nginx/Vercel
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
273
|
+
0 && (module.exports = {
|
|
274
|
+
PostgresSseListener,
|
|
275
|
+
createSseHandler
|
|
276
|
+
});
|
|
277
|
+
//# sourceMappingURL=server.js.map
|