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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kenrick Tandrian
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# pg-sse
|
|
2
|
+
|
|
3
|
+
Real-Time PostgreSQL Subscriptions for Next.js & React via Server-Sent Events (SSE). Zero external dependencies. Zero vendor lock-in.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero SaaS Dependencies:** Runs entirely in-process on your standard PostgreSQL and Next.js / Node.js backend. No Pusher, Ably, or Supabase lock-in required.
|
|
8
|
+
- **Database Pool Protection:** Multiplexes all incoming client streams into exactly **one** PostgreSQL connection per Node.js process using `LISTEN`/`NOTIFY`.
|
|
9
|
+
- **Browser Tab Multiplexing:** Restricts active event streams to exactly **one** socket connection per browser, sharing events across sibling tabs using `BroadcastChannel` with automated leader election.
|
|
10
|
+
- **Resilient Reconnection:** Native retry mechanism featuring exponential backoff and randomized jitter to prevent thundering herd scenarios when backend instances recycle.
|
|
11
|
+
- **Type-Safe:** Written in 100% type-safe TypeScript.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add pg-sse pg
|
|
17
|
+
# or
|
|
18
|
+
npm install pg-sse pg
|
|
19
|
+
# or
|
|
20
|
+
yarn add pg-sse pg
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Make sure you have standard peer dependencies installed (`pg`, `react`, and `react-dom`).
|
|
24
|
+
|
|
25
|
+
## Step-by-Step Setup Guide
|
|
26
|
+
|
|
27
|
+
### 1. Database Setup: PostgreSQL Triggers
|
|
28
|
+
|
|
29
|
+
Add a trigger function to your PostgreSQL database to dispatch notify payloads whenever records change.
|
|
30
|
+
|
|
31
|
+
```sql
|
|
32
|
+
-- Create trigger function that serializes table actions to JSON
|
|
33
|
+
CREATE OR REPLACE FUNCTION notify_table_update()
|
|
34
|
+
RETURNS trigger AS $$
|
|
35
|
+
BEGIN
|
|
36
|
+
PERFORM pg_notify(
|
|
37
|
+
'db_changes',
|
|
38
|
+
json_build_object(
|
|
39
|
+
'table', TG_TABLE_NAME,
|
|
40
|
+
'action', TG_OP,
|
|
41
|
+
'id', COALESCE(NEW.id, OLD.id)
|
|
42
|
+
)::text
|
|
43
|
+
);
|
|
44
|
+
RETURN NEW;
|
|
45
|
+
END;
|
|
46
|
+
$$ LANGUAGE plpgsql;
|
|
47
|
+
|
|
48
|
+
-- Attach the trigger to your tables (e.g. users)
|
|
49
|
+
CREATE TRIGGER users_update_trigger
|
|
50
|
+
AFTER INSERT OR UPDATE OR DELETE ON users
|
|
51
|
+
FOR EACH ROW EXECUTE FUNCTION notify_table_update();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Server Setup: PostgreSQL Listener
|
|
55
|
+
|
|
56
|
+
Initialize the `PostgresSseListener` instance exactly **once** in your Node.js server to start listening to PostgreSQL events.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// lib/pg-listener.ts
|
|
60
|
+
import { PostgresSseListener } from "pg-sse/server";
|
|
61
|
+
|
|
62
|
+
// Ensure a single persistent instance in Next.js development hot-reloads
|
|
63
|
+
const globalForPgSse = globalThis as unknown as {
|
|
64
|
+
pgListener?: PostgresSseListener;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const pgListener =
|
|
68
|
+
globalForPgSse.pgListener ??
|
|
69
|
+
new PostgresSseListener(
|
|
70
|
+
{
|
|
71
|
+
connectionString: process.env.DATABASE_URL,
|
|
72
|
+
},
|
|
73
|
+
"db_changes",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (process.env.NODE_ENV !== "production") {
|
|
77
|
+
globalForPgSse.pgListener = pgListener;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Start listening (returns promise, handles auto-reconnections internally)
|
|
81
|
+
pgListener.connect();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Server Setup: Next.js API Route Handler
|
|
85
|
+
|
|
86
|
+
Create a Next.js App Router Route Handler to stream events. Pass the `pgListener` and the incoming `Request` to the helper.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// app/api/sse/route.ts
|
|
90
|
+
import { createSseHandler } from "pg-sse/server";
|
|
91
|
+
import { pgListener } from "@/lib/pg-listener";
|
|
92
|
+
|
|
93
|
+
export async function GET(req: Request) {
|
|
94
|
+
// Option: Execute authentication or access validation here before streaming
|
|
95
|
+
|
|
96
|
+
return createSseHandler(pgListener, req);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 4. Client Setup: React Provider
|
|
101
|
+
|
|
102
|
+
Wrap your application in the `SseProvider` to establish connection state.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
// app/layout.tsx
|
|
106
|
+
import { SseProvider } from "pg-sse/client";
|
|
107
|
+
|
|
108
|
+
export default function RootLayout({
|
|
109
|
+
children,
|
|
110
|
+
}: {
|
|
111
|
+
children: React.ReactNode;
|
|
112
|
+
}) {
|
|
113
|
+
return (
|
|
114
|
+
<html lang="en">
|
|
115
|
+
<body>
|
|
116
|
+
<SseProvider endpoint="/api/sse">{children}</SseProvider>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 5. Client Setup: Component Subscriptions
|
|
124
|
+
|
|
125
|
+
Use the `useSubscription` hook in Client Components to bind event handlers to database updates.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// app/components/UserList.tsx
|
|
129
|
+
"use client";
|
|
130
|
+
|
|
131
|
+
import { useState } from "react";
|
|
132
|
+
import { useSubscription } from "pg-sse/client";
|
|
133
|
+
import { useRouter } from "next/navigation";
|
|
134
|
+
|
|
135
|
+
interface UserUpdatePayload {
|
|
136
|
+
table: string;
|
|
137
|
+
action: "INSERT" | "UPDATE" | "DELETE";
|
|
138
|
+
id: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function UserList() {
|
|
142
|
+
const router = useRouter();
|
|
143
|
+
|
|
144
|
+
// Listen to PostgreSQL updates on the "users" table
|
|
145
|
+
useSubscription<UserUpdatePayload>("users", (payload) => {
|
|
146
|
+
console.log(
|
|
147
|
+
`Database updated on table ${payload.table} (ID: ${payload.id})`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Automatically trigger Server Component page data revalidation
|
|
151
|
+
router.refresh();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div>
|
|
156
|
+
<h3>Live Users Directory</h3>
|
|
157
|
+
{/* Directory content rendered server-side */}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 🔒 Security Best Practice: The Thin Event Pattern
|
|
164
|
+
|
|
165
|
+
PostgreSQL notification payloads have a standard size limit of **8000 bytes**. To guarantee security and scalability, you should use the **Thin Event** pattern:
|
|
166
|
+
|
|
167
|
+
1. **Do not** serialize sensitive rows or rich columns (like emails, password hashes, or messages) directly into the PostgreSQL notification trigger.
|
|
168
|
+
2. **Do** only serialize identifiers and actions:
|
|
169
|
+
```json
|
|
170
|
+
{ "table": "users", "action": "UPDATE", "id": 123 }
|
|
171
|
+
```
|
|
172
|
+
3. When the client receives the update notification, query the actual data details through standard, authenticated server endpoints (such as Next.js Server Actions or API routes) where access control lists (ACLs) are strictly enforced.
|
|
173
|
+
|
|
174
|
+
## API Reference
|
|
175
|
+
|
|
176
|
+
### Server Side (`pg-sse/server`)
|
|
177
|
+
|
|
178
|
+
#### `PostgresSseListener(config, channels)`
|
|
179
|
+
|
|
180
|
+
Initializes a new DB event listener.
|
|
181
|
+
|
|
182
|
+
- `config`: A `pg.ClientConfig` object or connection string.
|
|
183
|
+
- `channels` (optional): Channel string or array of channel strings. Defaults to `"db_changes"`.
|
|
184
|
+
|
|
185
|
+
#### `createSseHandler(listener, request)`
|
|
186
|
+
|
|
187
|
+
Generates an SSE stream Response compatible with standard edge and Node.js runtimes.
|
|
188
|
+
|
|
189
|
+
- `listener`: The active `PostgresSseListener` instance.
|
|
190
|
+
- `request` (optional): Incoming Fetch `Request` object. When provided, registers abort listeners to unregister client registry entries instantly on connection loss.
|
|
191
|
+
|
|
192
|
+
### Client Side (`pg-sse/client`)
|
|
193
|
+
|
|
194
|
+
#### `<SseProvider endpoint="...">`
|
|
195
|
+
|
|
196
|
+
Creates the context state and initiates leader election for tab multiplexing.
|
|
197
|
+
|
|
198
|
+
- `endpoint`: The string API route endpoint URL.
|
|
199
|
+
|
|
200
|
+
#### `useSubscription<T>(table, callback)`
|
|
201
|
+
|
|
202
|
+
Registers a component callback for real-time table notifications.
|
|
203
|
+
|
|
204
|
+
- `table`: The string table name to subscribe to.
|
|
205
|
+
- `callback`: `(payload: T) => void` triggered when a matching database notification fires.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/client/provider.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
// src/client/multiplexer.ts
|
|
14
|
+
var TabMultiplexer = class {
|
|
15
|
+
constructor(endpoint, onEvent, onStatusChange) {
|
|
16
|
+
this.endpoint = endpoint;
|
|
17
|
+
this.onEvent = onEvent;
|
|
18
|
+
this.onStatusChange = onStatusChange;
|
|
19
|
+
this.tabId = Math.random().toString(36).substring(2, 11);
|
|
20
|
+
if (typeof window !== "undefined" && typeof BroadcastChannel !== "undefined") {
|
|
21
|
+
this.channel = new BroadcastChannel("pg-sse-multiplexer-channel");
|
|
22
|
+
this.channel.onmessage = (event) => this.handleChannelMessage(event.data);
|
|
23
|
+
window.addEventListener("beforeunload", () => this.destroy());
|
|
24
|
+
this.resetWatchdog(1500);
|
|
25
|
+
this.broadcast({ type: "query_leader", senderId: this.tabId });
|
|
26
|
+
} else {
|
|
27
|
+
this.startDirectConnection();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
endpoint;
|
|
31
|
+
onEvent;
|
|
32
|
+
onStatusChange;
|
|
33
|
+
channel = null;
|
|
34
|
+
tabId;
|
|
35
|
+
isLeader = false;
|
|
36
|
+
leaderId = null;
|
|
37
|
+
status = "disconnected";
|
|
38
|
+
heartbeatInterval = null;
|
|
39
|
+
watchdogTimeout = null;
|
|
40
|
+
electionTimeout = null;
|
|
41
|
+
eventSource = null;
|
|
42
|
+
claimInProgress = false;
|
|
43
|
+
destroy() {
|
|
44
|
+
const wasLeader = this.isLeader;
|
|
45
|
+
this.isLeader = false;
|
|
46
|
+
if (wasLeader) {
|
|
47
|
+
this.broadcast({ type: "release", senderId: this.tabId });
|
|
48
|
+
}
|
|
49
|
+
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
50
|
+
if (this.watchdogTimeout) clearTimeout(this.watchdogTimeout);
|
|
51
|
+
if (this.electionTimeout) clearTimeout(this.electionTimeout);
|
|
52
|
+
this.stopEventSource();
|
|
53
|
+
if (this.channel) {
|
|
54
|
+
this.channel.close();
|
|
55
|
+
this.channel = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
broadcast(msg) {
|
|
59
|
+
if (this.channel) {
|
|
60
|
+
try {
|
|
61
|
+
this.channel.postMessage(msg);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
handleChannelMessage(msg) {
|
|
67
|
+
if (msg.senderId === this.tabId) return;
|
|
68
|
+
switch (msg.type) {
|
|
69
|
+
case "query_leader":
|
|
70
|
+
if (this.isLeader) {
|
|
71
|
+
this.sendHeartbeat();
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
case "heartbeat":
|
|
75
|
+
this.claimInProgress = false;
|
|
76
|
+
if (this.electionTimeout) {
|
|
77
|
+
clearTimeout(this.electionTimeout);
|
|
78
|
+
this.electionTimeout = null;
|
|
79
|
+
}
|
|
80
|
+
this.leaderId = msg.senderId;
|
|
81
|
+
this.resetWatchdog(5e3);
|
|
82
|
+
break;
|
|
83
|
+
case "claim":
|
|
84
|
+
if (this.isLeader) {
|
|
85
|
+
this.sendHeartbeat();
|
|
86
|
+
} else if (this.claimInProgress) {
|
|
87
|
+
if (msg.senderId < this.tabId) {
|
|
88
|
+
this.claimInProgress = false;
|
|
89
|
+
if (this.electionTimeout) {
|
|
90
|
+
clearTimeout(this.electionTimeout);
|
|
91
|
+
this.electionTimeout = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case "release":
|
|
97
|
+
if (this.leaderId === msg.senderId) {
|
|
98
|
+
this.leaderId = null;
|
|
99
|
+
this.attemptLeadershipClaim();
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case "status":
|
|
103
|
+
if (!this.isLeader && this.leaderId === msg.senderId) {
|
|
104
|
+
const payloadObj = msg.payload;
|
|
105
|
+
this.status = payloadObj.status;
|
|
106
|
+
this.onStatusChange(this.status, payloadObj.activeConnections);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case "event":
|
|
110
|
+
if (!this.isLeader && this.leaderId === msg.senderId) {
|
|
111
|
+
const payloadObj = msg.payload;
|
|
112
|
+
this.onEvent(payloadObj.event, payloadObj.data);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
resetWatchdog(ms) {
|
|
118
|
+
if (this.watchdogTimeout) clearTimeout(this.watchdogTimeout);
|
|
119
|
+
this.watchdogTimeout = setTimeout(() => {
|
|
120
|
+
this.attemptLeadershipClaim();
|
|
121
|
+
}, ms);
|
|
122
|
+
}
|
|
123
|
+
attemptLeadershipClaim() {
|
|
124
|
+
if (this.isLeader || this.claimInProgress) return;
|
|
125
|
+
this.claimInProgress = true;
|
|
126
|
+
this.broadcast({ type: "claim", senderId: this.tabId });
|
|
127
|
+
this.electionTimeout = setTimeout(() => {
|
|
128
|
+
this.electionTimeout = null;
|
|
129
|
+
if (this.claimInProgress) {
|
|
130
|
+
this.claimInProgress = false;
|
|
131
|
+
this.becomeLeader();
|
|
132
|
+
}
|
|
133
|
+
}, 500);
|
|
134
|
+
}
|
|
135
|
+
becomeLeader() {
|
|
136
|
+
this.isLeader = true;
|
|
137
|
+
this.leaderId = this.tabId;
|
|
138
|
+
if (this.watchdogTimeout) {
|
|
139
|
+
clearTimeout(this.watchdogTimeout);
|
|
140
|
+
this.watchdogTimeout = null;
|
|
141
|
+
}
|
|
142
|
+
this.sendHeartbeat();
|
|
143
|
+
this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), 2e3);
|
|
144
|
+
this.startEventSource();
|
|
145
|
+
}
|
|
146
|
+
sendHeartbeat() {
|
|
147
|
+
this.broadcast({ type: "heartbeat", senderId: this.tabId });
|
|
148
|
+
}
|
|
149
|
+
updateStatus(newStatus, activeConnections = 0) {
|
|
150
|
+
this.status = newStatus;
|
|
151
|
+
this.onStatusChange(newStatus, activeConnections);
|
|
152
|
+
if (this.isLeader) {
|
|
153
|
+
this.broadcast({
|
|
154
|
+
type: "status",
|
|
155
|
+
senderId: this.tabId,
|
|
156
|
+
payload: { status: newStatus, activeConnections }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
startEventSource() {
|
|
161
|
+
this.stopEventSource();
|
|
162
|
+
this.updateStatus("connecting");
|
|
163
|
+
try {
|
|
164
|
+
this.eventSource = new EventSource(this.endpoint);
|
|
165
|
+
this.eventSource.addEventListener("handshake", (e) => {
|
|
166
|
+
try {
|
|
167
|
+
const data = JSON.parse(e.data);
|
|
168
|
+
this.updateStatus("connected", data.activeConnections || 1);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
this.updateStatus("connected", 1);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
this.eventSource.addEventListener("notification", (e) => {
|
|
174
|
+
try {
|
|
175
|
+
const data = JSON.parse(e.data);
|
|
176
|
+
this.onEvent("notification", data);
|
|
177
|
+
this.broadcast({
|
|
178
|
+
type: "event",
|
|
179
|
+
senderId: this.tabId,
|
|
180
|
+
payload: { event: "notification", data }
|
|
181
|
+
});
|
|
182
|
+
} catch (err) {
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
this.eventSource.onerror = () => {
|
|
186
|
+
this.updateStatus("connecting");
|
|
187
|
+
};
|
|
188
|
+
} catch (err) {
|
|
189
|
+
this.updateStatus("disconnected");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
startDirectConnection() {
|
|
193
|
+
this.startEventSource();
|
|
194
|
+
}
|
|
195
|
+
stopEventSource() {
|
|
196
|
+
if (this.eventSource) {
|
|
197
|
+
this.eventSource.close();
|
|
198
|
+
this.eventSource = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// src/client/provider.tsx
|
|
204
|
+
import { jsx } from "react/jsx-runtime";
|
|
205
|
+
var SseContext = createContext(null);
|
|
206
|
+
var SseProvider = ({ children, endpoint }) => {
|
|
207
|
+
const [status, setStatus] = useState("disconnected");
|
|
208
|
+
const [eventCount, setEventCount] = useState(0);
|
|
209
|
+
const [activeConnections, setActiveConnections] = useState(0);
|
|
210
|
+
const subscriptions = useRef(
|
|
211
|
+
/* @__PURE__ */ new Map()
|
|
212
|
+
);
|
|
213
|
+
const handleStatusChange = useCallback(
|
|
214
|
+
(newStatus, activeCount) => {
|
|
215
|
+
setActiveConnections(activeCount);
|
|
216
|
+
setStatus((prev) => {
|
|
217
|
+
if (newStatus === "connecting") {
|
|
218
|
+
if (prev === "connected") {
|
|
219
|
+
console.warn(`[pg-sse] Stream connection lost. Reconnecting...`);
|
|
220
|
+
} else if (prev === "disconnected") {
|
|
221
|
+
console.log(`[pg-sse] Connecting to stream...`);
|
|
222
|
+
}
|
|
223
|
+
} else if (newStatus === "connected" && prev !== "connected") {
|
|
224
|
+
console.log(
|
|
225
|
+
`[pg-sse] Stream connected. Active client count: ${activeCount}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return newStatus;
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
[]
|
|
232
|
+
);
|
|
233
|
+
const handleEvent = useCallback((event, payload) => {
|
|
234
|
+
if (event === "notification") {
|
|
235
|
+
setEventCount((prev) => prev + 1);
|
|
236
|
+
const table = payload?.table;
|
|
237
|
+
if (table) {
|
|
238
|
+
const callbacks = subscriptions.current.get(table);
|
|
239
|
+
if (callbacks) {
|
|
240
|
+
callbacks.forEach((cb) => {
|
|
241
|
+
try {
|
|
242
|
+
cb(payload);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error(
|
|
245
|
+
`[pg-sse] Callback for table "${table}" failed:`,
|
|
246
|
+
err
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}, []);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const multiplexer = new TabMultiplexer(
|
|
256
|
+
endpoint,
|
|
257
|
+
handleEvent,
|
|
258
|
+
handleStatusChange
|
|
259
|
+
);
|
|
260
|
+
return () => {
|
|
261
|
+
multiplexer.destroy();
|
|
262
|
+
};
|
|
263
|
+
}, [endpoint, handleEvent, handleStatusChange]);
|
|
264
|
+
const subscribe = useCallback(
|
|
265
|
+
(table, callback) => {
|
|
266
|
+
let callbacks = subscriptions.current.get(table);
|
|
267
|
+
if (!callbacks) {
|
|
268
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
269
|
+
subscriptions.current.set(table, callbacks);
|
|
270
|
+
}
|
|
271
|
+
callbacks.add(callback);
|
|
272
|
+
return () => {
|
|
273
|
+
const callbacks2 = subscriptions.current.get(table);
|
|
274
|
+
if (callbacks2) {
|
|
275
|
+
callbacks2.delete(callback);
|
|
276
|
+
if (callbacks2.size === 0) {
|
|
277
|
+
subscriptions.current.delete(table);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
[]
|
|
283
|
+
);
|
|
284
|
+
return /* @__PURE__ */ jsx(
|
|
285
|
+
SseContext.Provider,
|
|
286
|
+
{
|
|
287
|
+
value: { subscribe, status, eventCount, activeConnections },
|
|
288
|
+
children
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
function useSubscription(table, callback) {
|
|
293
|
+
const context = useContext(SseContext);
|
|
294
|
+
if (!context) {
|
|
295
|
+
throw new Error("useSubscription must be used within an SseProvider");
|
|
296
|
+
}
|
|
297
|
+
const { subscribe } = context;
|
|
298
|
+
const callbackRef = useRef(callback);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
callbackRef.current = callback;
|
|
301
|
+
}, [callback]);
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
const unsubscribe = subscribe(table, (payload) => {
|
|
304
|
+
callbackRef.current(payload);
|
|
305
|
+
});
|
|
306
|
+
return unsubscribe;
|
|
307
|
+
}, [table, subscribe]);
|
|
308
|
+
}
|
|
309
|
+
function useSseStatus() {
|
|
310
|
+
const context = useContext(SseContext);
|
|
311
|
+
if (!context) {
|
|
312
|
+
throw new Error("useSseStatus must be used within an SseProvider");
|
|
313
|
+
}
|
|
314
|
+
const { status, eventCount, activeConnections } = context;
|
|
315
|
+
return { status, eventCount, activeConnections };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export {
|
|
319
|
+
SseProvider,
|
|
320
|
+
useSubscription,
|
|
321
|
+
useSseStatus
|
|
322
|
+
};
|
|
323
|
+
//# sourceMappingURL=chunk-GFLW4AMU.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/provider.tsx","../src/client/multiplexer.ts"],"sourcesContent":["\"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"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type ConnectionStatus = "connecting" | "connected" | "disconnected";
|
|
4
|
+
|
|
5
|
+
interface SseContextType {
|
|
6
|
+
subscribe: <T = unknown>(table: string, callback: (payload: T) => void) => () => void;
|
|
7
|
+
status: ConnectionStatus;
|
|
8
|
+
eventCount: number;
|
|
9
|
+
activeConnections: number;
|
|
10
|
+
}
|
|
11
|
+
declare const SseProvider: React.FC<{
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
}>;
|
|
15
|
+
declare function useSubscription<T = unknown>(table: string, callback: (payload: T) => void): void;
|
|
16
|
+
declare function useSseStatus(): {
|
|
17
|
+
status: ConnectionStatus;
|
|
18
|
+
eventCount: number;
|
|
19
|
+
activeConnections: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { type ConnectionStatus, type SseContextType, SseProvider, useSseStatus, useSubscription };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type ConnectionStatus = "connecting" | "connected" | "disconnected";
|
|
4
|
+
|
|
5
|
+
interface SseContextType {
|
|
6
|
+
subscribe: <T = unknown>(table: string, callback: (payload: T) => void) => () => void;
|
|
7
|
+
status: ConnectionStatus;
|
|
8
|
+
eventCount: number;
|
|
9
|
+
activeConnections: number;
|
|
10
|
+
}
|
|
11
|
+
declare const SseProvider: React.FC<{
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
endpoint: string;
|
|
14
|
+
}>;
|
|
15
|
+
declare function useSubscription<T = unknown>(table: string, callback: (payload: T) => void): void;
|
|
16
|
+
declare function useSseStatus(): {
|
|
17
|
+
status: ConnectionStatus;
|
|
18
|
+
eventCount: number;
|
|
19
|
+
activeConnections: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { type ConnectionStatus, type SseContextType, SseProvider, useSseStatus, useSubscription };
|