notif-agent 1.0.0
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 +265 -0
- package/dist/index.d.mts +298 -0
- package/dist/index.d.ts +298 -0
- package/dist/index.js +737 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +729 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.d.mts +126 -0
- package/dist/server.d.ts +126 -0
- package/dist/server.js +333 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +331 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +61 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/server/NotifServer.ts
|
|
4
|
+
var NotifServer = class {
|
|
5
|
+
clients = /* @__PURE__ */ new Map();
|
|
6
|
+
heartbeatInterval;
|
|
7
|
+
heartbeatTimer = null;
|
|
8
|
+
generateClientId;
|
|
9
|
+
debug;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.heartbeatInterval = options.heartbeatInterval ?? 2e4;
|
|
12
|
+
this.debug = options.debug ?? false;
|
|
13
|
+
this.generateClientId = options.generateClientId ?? (() => `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`);
|
|
14
|
+
if (this.heartbeatInterval > 0) {
|
|
15
|
+
this.startHeartbeat();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Broadcast an event to one or more channels.
|
|
20
|
+
* Matches Pusher's `pusher.trigger(channel, event, data)` API.
|
|
21
|
+
*
|
|
22
|
+
* Example:
|
|
23
|
+
* ```ts
|
|
24
|
+
* server.trigger('orders', 'order-created', { id: 123, amount: 49.99 });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
trigger(channel, event, data, options) {
|
|
28
|
+
const channels = Array.isArray(channel) ? channel : [channel];
|
|
29
|
+
for (const ch of channels) {
|
|
30
|
+
for (const client of this.clients.values()) {
|
|
31
|
+
if (client.channels.has(ch) || client.channels.has("*")) {
|
|
32
|
+
client.send(event, data, { channel: ch, id: options?.id, sseEvent: options?.sseEvent });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Alias for trigger().
|
|
39
|
+
*/
|
|
40
|
+
broadcast(channel, event, data, options) {
|
|
41
|
+
this.trigger(channel, event, data, options);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Send an event directly to a specific client ID.
|
|
45
|
+
*/
|
|
46
|
+
sendToClient(clientId, event, data, options) {
|
|
47
|
+
const client = this.clients.get(clientId);
|
|
48
|
+
if (client) {
|
|
49
|
+
client.send(event, data, { channel: options?.channel, id: options?.id, sseEvent: options?.sseEvent });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Send an event to all connected clients belonging to a specific user ID.
|
|
56
|
+
*/
|
|
57
|
+
sendToUser(userId, event, data, options) {
|
|
58
|
+
let count = 0;
|
|
59
|
+
for (const client of this.clients.values()) {
|
|
60
|
+
if (client.userId === userId) {
|
|
61
|
+
client.send(event, data, { channel: options?.channel, id: options?.id, sseEvent: options?.sseEvent });
|
|
62
|
+
count++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Subscribe a connected client to a channel.
|
|
69
|
+
*/
|
|
70
|
+
subscribeClient(clientId, channel) {
|
|
71
|
+
const client = this.clients.get(clientId);
|
|
72
|
+
if (client) {
|
|
73
|
+
client.channels.add(channel);
|
|
74
|
+
this.log(`Client ${clientId} subscribed to "${channel}"`);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Unsubscribe a connected client from a channel.
|
|
81
|
+
*/
|
|
82
|
+
unsubscribeClient(clientId, channel) {
|
|
83
|
+
const client = this.clients.get(clientId);
|
|
84
|
+
if (client) {
|
|
85
|
+
const removed = client.channels.delete(channel);
|
|
86
|
+
this.log(`Client ${clientId} unsubscribed from "${channel}"`);
|
|
87
|
+
return removed;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get total connected clients count.
|
|
93
|
+
*/
|
|
94
|
+
getClientCount() {
|
|
95
|
+
return this.clients.size;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get subscribers count for a specific channel.
|
|
99
|
+
*/
|
|
100
|
+
getSubscribersCount(channel) {
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const client of this.clients.values()) {
|
|
103
|
+
if (client.channels.has(channel) || client.channels.has("*")) {
|
|
104
|
+
count++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return count;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Handle incoming HTTP request in Node.js / Express / Fastify environment.
|
|
111
|
+
*
|
|
112
|
+
* Example:
|
|
113
|
+
* ```ts
|
|
114
|
+
* app.get('/sse', (req, res) => {
|
|
115
|
+
* server.handleRequest(req, res);
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
handleRequest(req, res, options) {
|
|
120
|
+
res.writeHead(200, {
|
|
121
|
+
"Content-Type": "text/event-stream",
|
|
122
|
+
"Cache-Control": "no-cache, no-transform",
|
|
123
|
+
Connection: "keep-alive",
|
|
124
|
+
"X-Accel-Buffering": "no",
|
|
125
|
+
// Disable proxy buffering for Nginx
|
|
126
|
+
"Access-Control-Allow-Origin": "*"
|
|
127
|
+
});
|
|
128
|
+
if (typeof res.flushHeaders === "function") {
|
|
129
|
+
res.flushHeaders();
|
|
130
|
+
}
|
|
131
|
+
const clientId = options?.clientId ?? this.generateClientId();
|
|
132
|
+
const subscribedChannels = new Set(options?.channels ?? []);
|
|
133
|
+
if (req.url) {
|
|
134
|
+
try {
|
|
135
|
+
const url = new URL(req.url, "http://localhost");
|
|
136
|
+
const queryChannels = url.searchParams.get("channels");
|
|
137
|
+
if (queryChannels) {
|
|
138
|
+
for (const ch of queryChannels.split(",")) {
|
|
139
|
+
const trimmed = ch.trim();
|
|
140
|
+
if (trimmed) subscribedChannels.add(trimmed);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (subscribedChannels.size === 0 && !options?.channels) {
|
|
147
|
+
subscribedChannels.add("*");
|
|
148
|
+
}
|
|
149
|
+
const client = {
|
|
150
|
+
id: clientId,
|
|
151
|
+
userId: options?.userId,
|
|
152
|
+
channels: subscribedChannels,
|
|
153
|
+
meta: options?.meta ?? {},
|
|
154
|
+
send: (event, data, opt) => {
|
|
155
|
+
try {
|
|
156
|
+
let message = "";
|
|
157
|
+
if (opt?.id) message += `id: ${opt.id}
|
|
158
|
+
`;
|
|
159
|
+
message += `event: ${opt?.sseEvent ?? "message"}
|
|
160
|
+
`;
|
|
161
|
+
const payload = {
|
|
162
|
+
channel: opt?.channel,
|
|
163
|
+
event,
|
|
164
|
+
data
|
|
165
|
+
};
|
|
166
|
+
message += `data: ${JSON.stringify(payload)}
|
|
167
|
+
|
|
168
|
+
`;
|
|
169
|
+
res.write(message);
|
|
170
|
+
if (typeof res.flush === "function") {
|
|
171
|
+
res.flush();
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
this.log(`Error writing to client ${clientId}:`, err);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
sendComment: (comment) => {
|
|
178
|
+
try {
|
|
179
|
+
res.write(`: ${comment}
|
|
180
|
+
|
|
181
|
+
`);
|
|
182
|
+
if (typeof res.flush === "function") {
|
|
183
|
+
res.flush();
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
close: () => {
|
|
189
|
+
try {
|
|
190
|
+
res.end();
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
this.removeClient(clientId);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
this.clients.set(clientId, client);
|
|
197
|
+
this.log(`Client connected: ${clientId}, channels: [${Array.from(subscribedChannels).join(", ")}]`);
|
|
198
|
+
client.send("connected", { clientId });
|
|
199
|
+
req.on("close", () => {
|
|
200
|
+
this.removeClient(clientId);
|
|
201
|
+
});
|
|
202
|
+
res.on("close", () => {
|
|
203
|
+
this.removeClient(clientId);
|
|
204
|
+
});
|
|
205
|
+
return client;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Handle Web standard Request (Next.js App Router, Edge runtime, Hono, Cloudflare Workers).
|
|
209
|
+
* Returns a standard `Response` object with a `ReadableStream`.
|
|
210
|
+
*
|
|
211
|
+
* Example:
|
|
212
|
+
* ```ts
|
|
213
|
+
* export function GET(request: Request) {
|
|
214
|
+
* return server.createWebResponse(request);
|
|
215
|
+
* }
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
createWebResponse(request, options) {
|
|
219
|
+
const clientId = options?.clientId ?? this.generateClientId();
|
|
220
|
+
const subscribedChannels = new Set(options?.channels ?? []);
|
|
221
|
+
if (request?.url) {
|
|
222
|
+
try {
|
|
223
|
+
const url = new URL(request.url);
|
|
224
|
+
const queryChannels = url.searchParams.get("channels");
|
|
225
|
+
if (queryChannels) {
|
|
226
|
+
for (const ch of queryChannels.split(",")) {
|
|
227
|
+
const trimmed = ch.trim();
|
|
228
|
+
if (trimmed) subscribedChannels.add(trimmed);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (subscribedChannels.size === 0 && !options?.channels) {
|
|
235
|
+
subscribedChannels.add("*");
|
|
236
|
+
}
|
|
237
|
+
const encoder = new TextEncoder();
|
|
238
|
+
const stream = new ReadableStream({
|
|
239
|
+
start: (controller) => {
|
|
240
|
+
const client = {
|
|
241
|
+
id: clientId,
|
|
242
|
+
userId: options?.userId,
|
|
243
|
+
channels: subscribedChannels,
|
|
244
|
+
meta: options?.meta ?? {},
|
|
245
|
+
send: (event, data, opt) => {
|
|
246
|
+
try {
|
|
247
|
+
let message = "";
|
|
248
|
+
if (opt?.id) message += `id: ${opt.id}
|
|
249
|
+
`;
|
|
250
|
+
message += `event: ${opt?.sseEvent ?? "message"}
|
|
251
|
+
`;
|
|
252
|
+
const payload = {
|
|
253
|
+
channel: opt?.channel,
|
|
254
|
+
event,
|
|
255
|
+
data
|
|
256
|
+
};
|
|
257
|
+
message += `data: ${JSON.stringify(payload)}
|
|
258
|
+
|
|
259
|
+
`;
|
|
260
|
+
controller.enqueue(encoder.encode(message));
|
|
261
|
+
} catch (err) {
|
|
262
|
+
this.log(`Error enqueueing to client ${clientId}:`, err);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
sendComment: (comment) => {
|
|
266
|
+
try {
|
|
267
|
+
controller.enqueue(encoder.encode(`: ${comment}
|
|
268
|
+
|
|
269
|
+
`));
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
close: () => {
|
|
274
|
+
try {
|
|
275
|
+
controller.close();
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
this.removeClient(clientId);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
this.clients.set(clientId, client);
|
|
282
|
+
this.log(`Web client connected: ${clientId}`);
|
|
283
|
+
client.send("connected", { clientId });
|
|
284
|
+
},
|
|
285
|
+
cancel: () => {
|
|
286
|
+
this.removeClient(clientId);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return new Response(stream, {
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": "text/event-stream",
|
|
292
|
+
"Cache-Control": "no-cache, no-transform",
|
|
293
|
+
Connection: "keep-alive",
|
|
294
|
+
"Access-Control-Allow-Origin": "*"
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Stop the server heartbeat and disconnect all clients.
|
|
300
|
+
*/
|
|
301
|
+
close() {
|
|
302
|
+
if (this.heartbeatTimer) {
|
|
303
|
+
clearInterval(this.heartbeatTimer);
|
|
304
|
+
this.heartbeatTimer = null;
|
|
305
|
+
}
|
|
306
|
+
for (const client of this.clients.values()) {
|
|
307
|
+
client.close();
|
|
308
|
+
}
|
|
309
|
+
this.clients.clear();
|
|
310
|
+
}
|
|
311
|
+
removeClient(clientId) {
|
|
312
|
+
if (this.clients.has(clientId)) {
|
|
313
|
+
this.clients.delete(clientId);
|
|
314
|
+
this.log(`Client disconnected: ${clientId}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
startHeartbeat() {
|
|
318
|
+
this.heartbeatTimer = setInterval(() => {
|
|
319
|
+
for (const client of this.clients.values()) {
|
|
320
|
+
client.sendComment("ping");
|
|
321
|
+
}
|
|
322
|
+
}, this.heartbeatInterval);
|
|
323
|
+
}
|
|
324
|
+
log(...args) {
|
|
325
|
+
if (this.debug) {
|
|
326
|
+
console.log("[notif-agent:server]", ...args);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
exports.NotifServer = NotifServer;
|
|
332
|
+
//# sourceMappingURL=server.js.map
|
|
333
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/NotifServer.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'http';\n\nexport interface NotifClient {\n id: string;\n userId?: string;\n channels: Set<string>;\n send: (event: string, data: any, options?: { channel?: string; id?: string; sseEvent?: string }) => void;\n sendComment: (comment: string) => void;\n close: () => void;\n meta: Record<string, any>;\n}\n\nexport interface NotifServerOptions {\n /**\n * Heartbeat interval in milliseconds to keep SSE connections open through proxies.\n * Defaults to 20000ms (20s). Set to 0 to disable.\n */\n heartbeatInterval?: number;\n\n /**\n * Optional custom client ID generator.\n */\n generateClientId?: () => string;\n\n /**\n * Debug logging.\n */\n debug?: boolean;\n}\n\nexport interface TriggerOptions {\n id?: string;\n sseEvent?: string;\n}\n\nexport class NotifServer {\n private clients: Map<string, NotifClient> = new Map();\n private heartbeatInterval: number;\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private generateClientId: () => string;\n private debug: boolean;\n\n constructor(options: NotifServerOptions = {}) {\n this.heartbeatInterval = options.heartbeatInterval ?? 20000;\n this.debug = options.debug ?? false;\n this.generateClientId =\n options.generateClientId ?? (() => `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`);\n\n if (this.heartbeatInterval > 0) {\n this.startHeartbeat();\n }\n }\n\n /**\n * Broadcast an event to one or more channels.\n * Matches Pusher's `pusher.trigger(channel, event, data)` API.\n *\n * Example:\n * ```ts\n * server.trigger('orders', 'order-created', { id: 123, amount: 49.99 });\n * ```\n */\n public trigger(channel: string | string[], event: string, data: any, options?: TriggerOptions): void {\n const channels = Array.isArray(channel) ? channel : [channel];\n\n for (const ch of channels) {\n for (const client of this.clients.values()) {\n if (client.channels.has(ch) || client.channels.has('*')) {\n client.send(event, data, { channel: ch, id: options?.id, sseEvent: options?.sseEvent });\n }\n }\n }\n }\n\n /**\n * Alias for trigger().\n */\n public broadcast(channel: string | string[], event: string, data: any, options?: TriggerOptions): void {\n this.trigger(channel, event, data, options);\n }\n\n /**\n * Send an event directly to a specific client ID.\n */\n public sendToClient(clientId: string, event: string, data: any, options?: TriggerOptions & { channel?: string }): boolean {\n const client = this.clients.get(clientId);\n if (client) {\n client.send(event, data, { channel: options?.channel, id: options?.id, sseEvent: options?.sseEvent });\n return true;\n }\n return false;\n }\n\n /**\n * Send an event to all connected clients belonging to a specific user ID.\n */\n public sendToUser(userId: string, event: string, data: any, options?: TriggerOptions & { channel?: string }): number {\n let count = 0;\n for (const client of this.clients.values()) {\n if (client.userId === userId) {\n client.send(event, data, { channel: options?.channel, id: options?.id, sseEvent: options?.sseEvent });\n count++;\n }\n }\n return count;\n }\n\n /**\n * Subscribe a connected client to a channel.\n */\n public subscribeClient(clientId: string, channel: string): boolean {\n const client = this.clients.get(clientId);\n if (client) {\n client.channels.add(channel);\n this.log(`Client ${clientId} subscribed to \"${channel}\"`);\n return true;\n }\n return false;\n }\n\n /**\n * Unsubscribe a connected client from a channel.\n */\n public unsubscribeClient(clientId: string, channel: string): boolean {\n const client = this.clients.get(clientId);\n if (client) {\n const removed = client.channels.delete(channel);\n this.log(`Client ${clientId} unsubscribed from \"${channel}\"`);\n return removed;\n }\n return false;\n }\n\n /**\n * Get total connected clients count.\n */\n public getClientCount(): number {\n return this.clients.size;\n }\n\n /**\n * Get subscribers count for a specific channel.\n */\n public getSubscribersCount(channel: string): number {\n let count = 0;\n for (const client of this.clients.values()) {\n if (client.channels.has(channel) || client.channels.has('*')) {\n count++;\n }\n }\n return count;\n }\n\n /**\n * Handle incoming HTTP request in Node.js / Express / Fastify environment.\n *\n * Example:\n * ```ts\n * app.get('/sse', (req, res) => {\n * server.handleRequest(req, res);\n * });\n * ```\n */\n public handleRequest(\n req: IncomingMessage,\n res: ServerResponse,\n options?: {\n clientId?: string;\n userId?: string;\n channels?: string[];\n meta?: Record<string, any>;\n }\n ): NotifClient {\n // Set standard SSE HTTP headers\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no', // Disable proxy buffering for Nginx\n 'Access-Control-Allow-Origin': '*',\n });\n\n if (typeof (res as any).flushHeaders === 'function') {\n (res as any).flushHeaders();\n }\n\n const clientId = options?.clientId ?? this.generateClientId();\n\n // Parse initial channels from query parameters or options\n const subscribedChannels = new Set<string>(options?.channels ?? []);\n if (req.url) {\n try {\n const url = new URL(req.url, 'http://localhost');\n const queryChannels = url.searchParams.get('channels');\n if (queryChannels) {\n for (const ch of queryChannels.split(',')) {\n const trimmed = ch.trim();\n if (trimmed) subscribedChannels.add(trimmed);\n }\n }\n } catch {}\n }\n\n if (subscribedChannels.size === 0 && !options?.channels) {\n subscribedChannels.add('*');\n }\n\n const client: NotifClient = {\n id: clientId,\n userId: options?.userId,\n channels: subscribedChannels,\n meta: options?.meta ?? {},\n send: (event: string, data: any, opt?: { channel?: string; id?: string; sseEvent?: string }) => {\n try {\n let message = '';\n if (opt?.id) message += `id: ${opt.id}\\n`;\n message += `event: ${opt?.sseEvent ?? 'message'}\\n`;\n\n const payload = {\n channel: opt?.channel,\n event,\n data,\n };\n message += `data: ${JSON.stringify(payload)}\\n\\n`;\n\n res.write(message);\n if (typeof (res as any).flush === 'function') {\n (res as any).flush();\n }\n } catch (err) {\n this.log(`Error writing to client ${clientId}:`, err);\n }\n },\n sendComment: (comment: string) => {\n try {\n res.write(`: ${comment}\\n\\n`);\n if (typeof (res as any).flush === 'function') {\n (res as any).flush();\n }\n } catch {}\n },\n close: () => {\n try {\n res.end();\n } catch {}\n this.removeClient(clientId);\n },\n };\n\n this.clients.set(clientId, client);\n this.log(`Client connected: ${clientId}, channels: [${Array.from(subscribedChannels).join(', ')}]`);\n\n // Send connection established event\n client.send('connected', { clientId });\n\n // Handle connection close\n req.on('close', () => {\n this.removeClient(clientId);\n });\n\n res.on('close', () => {\n this.removeClient(clientId);\n });\n\n return client;\n }\n\n /**\n * Handle Web standard Request (Next.js App Router, Edge runtime, Hono, Cloudflare Workers).\n * Returns a standard `Response` object with a `ReadableStream`.\n *\n * Example:\n * ```ts\n * export function GET(request: Request) {\n * return server.createWebResponse(request);\n * }\n * ```\n */\n public createWebResponse(\n request?: Request,\n options?: {\n clientId?: string;\n userId?: string;\n channels?: string[];\n meta?: Record<string, any>;\n }\n ): Response {\n const clientId = options?.clientId ?? this.generateClientId();\n const subscribedChannels = new Set<string>(options?.channels ?? []);\n\n if (request?.url) {\n try {\n const url = new URL(request.url);\n const queryChannels = url.searchParams.get('channels');\n if (queryChannels) {\n for (const ch of queryChannels.split(',')) {\n const trimmed = ch.trim();\n if (trimmed) subscribedChannels.add(trimmed);\n }\n }\n } catch {}\n }\n\n if (subscribedChannels.size === 0 && !options?.channels) {\n subscribedChannels.add('*');\n }\n\n const encoder = new TextEncoder();\n let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;\n\n const stream = new ReadableStream<Uint8Array>({\n start: (controller) => {\n controllerRef = controller;\n\n const client: NotifClient = {\n id: clientId,\n userId: options?.userId,\n channels: subscribedChannels,\n meta: options?.meta ?? {},\n send: (event: string, data: any, opt?: { channel?: string; id?: string; sseEvent?: string }) => {\n try {\n let message = '';\n if (opt?.id) message += `id: ${opt.id}\\n`;\n message += `event: ${opt?.sseEvent ?? 'message'}\\n`;\n\n const payload = {\n channel: opt?.channel,\n event,\n data,\n };\n message += `data: ${JSON.stringify(payload)}\\n\\n`;\n\n controller.enqueue(encoder.encode(message));\n } catch (err) {\n this.log(`Error enqueueing to client ${clientId}:`, err);\n }\n },\n sendComment: (comment: string) => {\n try {\n controller.enqueue(encoder.encode(`: ${comment}\\n\\n`));\n } catch {}\n },\n close: () => {\n try {\n controller.close();\n } catch {}\n this.removeClient(clientId);\n },\n };\n\n this.clients.set(clientId, client);\n this.log(`Web client connected: ${clientId}`);\n\n // Send connection handshake\n client.send('connected', { clientId });\n },\n cancel: () => {\n this.removeClient(clientId);\n },\n });\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'Access-Control-Allow-Origin': '*',\n },\n });\n }\n\n /**\n * Stop the server heartbeat and disconnect all clients.\n */\n public close(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n\n for (const client of this.clients.values()) {\n client.close();\n }\n this.clients.clear();\n }\n\n private removeClient(clientId: string): void {\n if (this.clients.has(clientId)) {\n this.clients.delete(clientId);\n this.log(`Client disconnected: ${clientId}`);\n }\n }\n\n private startHeartbeat(): void {\n this.heartbeatTimer = setInterval(() => {\n for (const client of this.clients.values()) {\n client.sendComment('ping');\n }\n }, this.heartbeatInterval);\n }\n\n private log(...args: any[]): void {\n if (this.debug) {\n console.log('[notif-agent:server]', ...args);\n }\n }\n}\n"]}
|