simple-support-chat 0.3.3 → 0.4.1

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.
@@ -1,3 +1,59 @@
1
+ /** Event types that the reply emitter can broadcast */
2
+ type ReplyEventType = "reply" | "thread_created";
3
+ /** Payload for a reply event */
4
+ interface ReplyEvent {
5
+ type: "reply";
6
+ sessionId: string;
7
+ reply: Reply;
8
+ }
9
+ /** Payload for a thread_created event */
10
+ interface ThreadCreatedEvent {
11
+ type: "thread_created";
12
+ sessionId: string;
13
+ threadTs: string;
14
+ }
15
+ /** Union of all emitter event payloads */
16
+ type EmitterEvent = ReplyEvent | ThreadCreatedEvent;
17
+ /** Callback function signature for emitter subscribers */
18
+ type EmitterCallback = (event: EmitterEvent) => void;
19
+ /** The reply emitter interface */
20
+ interface ReplyEmitter {
21
+ /** Emit a reply event to all subscribers for a given sessionId */
22
+ emit(sessionId: string, reply: Reply): void;
23
+ /** Emit a thread_created event to all subscribers for a given sessionId */
24
+ emitThreadCreated(sessionId: string, threadTs: string): void;
25
+ /** Subscribe to events for a given sessionId */
26
+ subscribe(sessionId: string, callback: EmitterCallback): void;
27
+ /** Unsubscribe a callback from a given sessionId */
28
+ unsubscribe(sessionId: string, callback: EmitterCallback): void;
29
+ }
30
+ /**
31
+ * Creates an in-process event emitter for broadcasting reply events to
32
+ * connected SSE clients.
33
+ *
34
+ * This is intentionally simple — it's an in-process pub/sub mechanism,
35
+ * not a distributed message broker. It works within a single server process,
36
+ * which is the expected use case for this SDK.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { createReplyEmitter } from 'simple-support-chat/server';
41
+ *
42
+ * const emitter = createReplyEmitter();
43
+ *
44
+ * // Subscribe to replies for a session
45
+ * emitter.subscribe('session-123', (event) => {
46
+ * if (event.type === 'reply') {
47
+ * console.log('New reply:', event.reply);
48
+ * }
49
+ * });
50
+ *
51
+ * // Emit a reply (typically called by the webhook handler)
52
+ * emitter.emit('session-123', reply);
53
+ * ```
54
+ */
55
+ declare function createReplyEmitter(): ReplyEmitter;
56
+
1
57
  /** Slack configuration for the support handler */
2
58
  interface SlackConfig {
3
59
  /** Slack Bot User OAuth Token (xoxb-...) */
@@ -15,6 +71,8 @@ interface SupportHandlerOptions extends SlackConfig {
15
71
  onMessage?: (data: IncomingMessage) => void | Promise<void>;
16
72
  /** Optional pluggable storage backend. Defaults to InMemoryStore. */
17
73
  store?: SupportChatStore;
74
+ /** Optional reply emitter for broadcasting events to SSE clients */
75
+ emitter?: ReplyEmitter;
18
76
  }
19
77
  /** User info sent from the client */
20
78
  interface MessageUser {
@@ -162,6 +220,8 @@ interface WebhookHandlerOptions {
162
220
  store: SupportChatStore;
163
221
  /** Slack signing secret for request verification */
164
222
  signingSecret: string;
223
+ /** Optional reply emitter for broadcasting replies to SSE clients */
224
+ emitter?: ReplyEmitter;
165
225
  }
166
226
  /**
167
227
  * Verify Slack request signature.
@@ -273,6 +333,65 @@ declare function createRepliesHandler(options: RepliesHandlerOptions): (request:
273
333
  */
274
334
  declare function createExpressRepliesHandler(options: RepliesHandlerOptions): (req: ExpressRepliesRequest, res: ExpressResponse) => Promise<void>;
275
335
 
336
+ /** Options for createSSEHandler / createExpressSSEHandler */
337
+ interface SSEHandlerOptions {
338
+ /** Reply emitter instance (shared with webhook handler) */
339
+ emitter: ReplyEmitter;
340
+ /** Heartbeat interval in milliseconds. Defaults to 30000 (30 seconds). */
341
+ heartbeatInterval?: number;
342
+ }
343
+ /** Minimal Express-compatible response for SSE streaming */
344
+ interface ExpressSSEResponse {
345
+ writeHead(statusCode: number, headers: Record<string, string>): void;
346
+ write(chunk: string): boolean;
347
+ end(): void;
348
+ on(event: string, listener: () => void): void;
349
+ }
350
+ /** Minimal Express-compatible request for SSE */
351
+ interface ExpressSSERequest {
352
+ query?: Record<string, string | string[] | undefined>;
353
+ method?: string;
354
+ on?(event: string, listener: () => void): void;
355
+ }
356
+ /**
357
+ * Creates an SSE stream handler using the Web API Request/Response standard.
358
+ *
359
+ * Clients connect via GET with `?sessionId={id}`. The handler subscribes to
360
+ * the emitter for that session and streams events as they arrive.
361
+ *
362
+ * Event types streamed:
363
+ * - `reply` — a new reply from the support team: `data: { reply: Reply }`
364
+ * - `thread_created` — a Slack thread was created: `data: { threadTs: string }`
365
+ * - `heartbeat` — keep-alive ping every 30s: `data: {}`
366
+ *
367
+ * Compatible with Next.js App Router, Remix, Hono, Deno, Bun, etc.
368
+ *
369
+ * @example Next.js App Router
370
+ * ```ts
371
+ * // app/api/support/sse/route.ts
372
+ * import { createSSEHandler, createReplyEmitter } from 'simple-support-chat/server';
373
+ *
374
+ * const emitter = createReplyEmitter();
375
+ * export const GET = createSSEHandler({ emitter });
376
+ * ```
377
+ */
378
+ declare function createSSEHandler(options: SSEHandlerOptions): (request: Request) => Response;
379
+ /**
380
+ * Creates an SSE stream handler for Express/Connect frameworks.
381
+ *
382
+ * @example Express
383
+ * ```ts
384
+ * import express from 'express';
385
+ * import { createExpressSSEHandler, createReplyEmitter } from 'simple-support-chat/server';
386
+ *
387
+ * const app = express();
388
+ * const emitter = createReplyEmitter();
389
+ *
390
+ * app.get('/api/support/sse', createExpressSSEHandler({ emitter }));
391
+ * ```
392
+ */
393
+ declare function createExpressSSEHandler(options: SSEHandlerOptions): (req: ExpressSSERequest, res: ExpressSSEResponse) => void;
394
+
276
395
  /**
277
396
  * In-memory implementation of SupportChatStore.
278
397
  *
@@ -302,4 +421,4 @@ declare class InMemoryStore implements SupportChatStore {
302
421
  */
303
422
  declare function emojify(text: string): string;
304
423
 
305
- export { type ErrorResponse, type ExpressRepliesRequest, type ExpressRequest, type ExpressResponse, type ExpressWebhookRequest, type HandlerResponse, InMemoryStore, type IncomingMessage, type MessageContext, type MessageUser, type RepliesErrorResponse, type RepliesHandlerOptions, type RepliesResponse, type Reply, type SlackConfig, type SuccessResponse, type SupportChatStore, type SupportHandlerOptions, type ThreadRecord, type WebhookHandlerOptions, createExpressHandler, createExpressRepliesHandler, createExpressWebhookHandler, createRepliesHandler, createSupportHandler, createWebhookHandler, emojify, validateSlackToken, verifySlackSignature };
424
+ export { type EmitterCallback, type EmitterEvent, type ErrorResponse, type ExpressRepliesRequest, type ExpressRequest, type ExpressResponse, type ExpressSSERequest, type ExpressSSEResponse, type ExpressWebhookRequest, type HandlerResponse, InMemoryStore, type IncomingMessage, type MessageContext, type MessageUser, type RepliesErrorResponse, type RepliesHandlerOptions, type RepliesResponse, type Reply, type ReplyEmitter, type ReplyEvent, type ReplyEventType, type SSEHandlerOptions, type SlackConfig, type SuccessResponse, type SupportChatStore, type SupportHandlerOptions, type ThreadCreatedEvent, type ThreadRecord, type WebhookHandlerOptions, createExpressHandler, createExpressRepliesHandler, createExpressSSEHandler, createExpressWebhookHandler, createRepliesHandler, createReplyEmitter, createSSEHandler, createSupportHandler, createWebhookHandler, emojify, validateSlackToken, verifySlackSignature };
@@ -1,3 +1,59 @@
1
+ /** Event types that the reply emitter can broadcast */
2
+ type ReplyEventType = "reply" | "thread_created";
3
+ /** Payload for a reply event */
4
+ interface ReplyEvent {
5
+ type: "reply";
6
+ sessionId: string;
7
+ reply: Reply;
8
+ }
9
+ /** Payload for a thread_created event */
10
+ interface ThreadCreatedEvent {
11
+ type: "thread_created";
12
+ sessionId: string;
13
+ threadTs: string;
14
+ }
15
+ /** Union of all emitter event payloads */
16
+ type EmitterEvent = ReplyEvent | ThreadCreatedEvent;
17
+ /** Callback function signature for emitter subscribers */
18
+ type EmitterCallback = (event: EmitterEvent) => void;
19
+ /** The reply emitter interface */
20
+ interface ReplyEmitter {
21
+ /** Emit a reply event to all subscribers for a given sessionId */
22
+ emit(sessionId: string, reply: Reply): void;
23
+ /** Emit a thread_created event to all subscribers for a given sessionId */
24
+ emitThreadCreated(sessionId: string, threadTs: string): void;
25
+ /** Subscribe to events for a given sessionId */
26
+ subscribe(sessionId: string, callback: EmitterCallback): void;
27
+ /** Unsubscribe a callback from a given sessionId */
28
+ unsubscribe(sessionId: string, callback: EmitterCallback): void;
29
+ }
30
+ /**
31
+ * Creates an in-process event emitter for broadcasting reply events to
32
+ * connected SSE clients.
33
+ *
34
+ * This is intentionally simple — it's an in-process pub/sub mechanism,
35
+ * not a distributed message broker. It works within a single server process,
36
+ * which is the expected use case for this SDK.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { createReplyEmitter } from 'simple-support-chat/server';
41
+ *
42
+ * const emitter = createReplyEmitter();
43
+ *
44
+ * // Subscribe to replies for a session
45
+ * emitter.subscribe('session-123', (event) => {
46
+ * if (event.type === 'reply') {
47
+ * console.log('New reply:', event.reply);
48
+ * }
49
+ * });
50
+ *
51
+ * // Emit a reply (typically called by the webhook handler)
52
+ * emitter.emit('session-123', reply);
53
+ * ```
54
+ */
55
+ declare function createReplyEmitter(): ReplyEmitter;
56
+
1
57
  /** Slack configuration for the support handler */
2
58
  interface SlackConfig {
3
59
  /** Slack Bot User OAuth Token (xoxb-...) */
@@ -15,6 +71,8 @@ interface SupportHandlerOptions extends SlackConfig {
15
71
  onMessage?: (data: IncomingMessage) => void | Promise<void>;
16
72
  /** Optional pluggable storage backend. Defaults to InMemoryStore. */
17
73
  store?: SupportChatStore;
74
+ /** Optional reply emitter for broadcasting events to SSE clients */
75
+ emitter?: ReplyEmitter;
18
76
  }
19
77
  /** User info sent from the client */
20
78
  interface MessageUser {
@@ -162,6 +220,8 @@ interface WebhookHandlerOptions {
162
220
  store: SupportChatStore;
163
221
  /** Slack signing secret for request verification */
164
222
  signingSecret: string;
223
+ /** Optional reply emitter for broadcasting replies to SSE clients */
224
+ emitter?: ReplyEmitter;
165
225
  }
166
226
  /**
167
227
  * Verify Slack request signature.
@@ -273,6 +333,65 @@ declare function createRepliesHandler(options: RepliesHandlerOptions): (request:
273
333
  */
274
334
  declare function createExpressRepliesHandler(options: RepliesHandlerOptions): (req: ExpressRepliesRequest, res: ExpressResponse) => Promise<void>;
275
335
 
336
+ /** Options for createSSEHandler / createExpressSSEHandler */
337
+ interface SSEHandlerOptions {
338
+ /** Reply emitter instance (shared with webhook handler) */
339
+ emitter: ReplyEmitter;
340
+ /** Heartbeat interval in milliseconds. Defaults to 30000 (30 seconds). */
341
+ heartbeatInterval?: number;
342
+ }
343
+ /** Minimal Express-compatible response for SSE streaming */
344
+ interface ExpressSSEResponse {
345
+ writeHead(statusCode: number, headers: Record<string, string>): void;
346
+ write(chunk: string): boolean;
347
+ end(): void;
348
+ on(event: string, listener: () => void): void;
349
+ }
350
+ /** Minimal Express-compatible request for SSE */
351
+ interface ExpressSSERequest {
352
+ query?: Record<string, string | string[] | undefined>;
353
+ method?: string;
354
+ on?(event: string, listener: () => void): void;
355
+ }
356
+ /**
357
+ * Creates an SSE stream handler using the Web API Request/Response standard.
358
+ *
359
+ * Clients connect via GET with `?sessionId={id}`. The handler subscribes to
360
+ * the emitter for that session and streams events as they arrive.
361
+ *
362
+ * Event types streamed:
363
+ * - `reply` — a new reply from the support team: `data: { reply: Reply }`
364
+ * - `thread_created` — a Slack thread was created: `data: { threadTs: string }`
365
+ * - `heartbeat` — keep-alive ping every 30s: `data: {}`
366
+ *
367
+ * Compatible with Next.js App Router, Remix, Hono, Deno, Bun, etc.
368
+ *
369
+ * @example Next.js App Router
370
+ * ```ts
371
+ * // app/api/support/sse/route.ts
372
+ * import { createSSEHandler, createReplyEmitter } from 'simple-support-chat/server';
373
+ *
374
+ * const emitter = createReplyEmitter();
375
+ * export const GET = createSSEHandler({ emitter });
376
+ * ```
377
+ */
378
+ declare function createSSEHandler(options: SSEHandlerOptions): (request: Request) => Response;
379
+ /**
380
+ * Creates an SSE stream handler for Express/Connect frameworks.
381
+ *
382
+ * @example Express
383
+ * ```ts
384
+ * import express from 'express';
385
+ * import { createExpressSSEHandler, createReplyEmitter } from 'simple-support-chat/server';
386
+ *
387
+ * const app = express();
388
+ * const emitter = createReplyEmitter();
389
+ *
390
+ * app.get('/api/support/sse', createExpressSSEHandler({ emitter }));
391
+ * ```
392
+ */
393
+ declare function createExpressSSEHandler(options: SSEHandlerOptions): (req: ExpressSSERequest, res: ExpressSSEResponse) => void;
394
+
276
395
  /**
277
396
  * In-memory implementation of SupportChatStore.
278
397
  *
@@ -302,4 +421,4 @@ declare class InMemoryStore implements SupportChatStore {
302
421
  */
303
422
  declare function emojify(text: string): string;
304
423
 
305
- export { type ErrorResponse, type ExpressRepliesRequest, type ExpressRequest, type ExpressResponse, type ExpressWebhookRequest, type HandlerResponse, InMemoryStore, type IncomingMessage, type MessageContext, type MessageUser, type RepliesErrorResponse, type RepliesHandlerOptions, type RepliesResponse, type Reply, type SlackConfig, type SuccessResponse, type SupportChatStore, type SupportHandlerOptions, type ThreadRecord, type WebhookHandlerOptions, createExpressHandler, createExpressRepliesHandler, createExpressWebhookHandler, createRepliesHandler, createSupportHandler, createWebhookHandler, emojify, validateSlackToken, verifySlackSignature };
424
+ export { type EmitterCallback, type EmitterEvent, type ErrorResponse, type ExpressRepliesRequest, type ExpressRequest, type ExpressResponse, type ExpressSSERequest, type ExpressSSEResponse, type ExpressWebhookRequest, type HandlerResponse, InMemoryStore, type IncomingMessage, type MessageContext, type MessageUser, type RepliesErrorResponse, type RepliesHandlerOptions, type RepliesResponse, type Reply, type ReplyEmitter, type ReplyEvent, type ReplyEventType, type SSEHandlerOptions, type SlackConfig, type SuccessResponse, type SupportChatStore, type SupportHandlerOptions, type ThreadCreatedEvent, type ThreadRecord, type WebhookHandlerOptions, createExpressHandler, createExpressRepliesHandler, createExpressSSEHandler, createExpressWebhookHandler, createRepliesHandler, createReplyEmitter, createSSEHandler, createSupportHandler, createWebhookHandler, emojify, validateSlackToken, verifySlackSignature };
@@ -39,7 +39,7 @@ var InMemoryStore = class {
39
39
 
40
40
  // src/server/handler.ts
41
41
  async function handleMessage(body, options) {
42
- const { slack, slackChannel, botName, botIcon, onMessage, store } = options;
42
+ const { slack, slackChannel, botName, botIcon, onMessage, store, emitter } = options;
43
43
  if (!body.message || typeof body.message !== "string") {
44
44
  return {
45
45
  data: { success: false, error: "Missing required field: message" },
@@ -102,6 +102,9 @@ ${body.message}`,
102
102
  user: body.user,
103
103
  context: body.context
104
104
  });
105
+ if (emitter) {
106
+ emitter.emitThreadCreated(body.sessionId, threadTs);
107
+ }
105
108
  return {
106
109
  data: { success: true, threadId: threadTs },
107
110
  status: 200
@@ -116,7 +119,7 @@ ${body.message}`,
116
119
  }
117
120
  }
118
121
  function createSupportHandler(options) {
119
- const { slackBotToken, slackChannel, botName, botIcon, onMessage, store } = options;
122
+ const { slackBotToken, slackChannel, botName, botIcon, onMessage, store, emitter } = options;
120
123
  const slack = new WebClient(slackBotToken);
121
124
  const resolvedStore = store ?? new InMemoryStore();
122
125
  return async (request) => {
@@ -141,13 +144,14 @@ function createSupportHandler(options) {
141
144
  botName,
142
145
  botIcon,
143
146
  onMessage,
144
- store: resolvedStore
147
+ store: resolvedStore,
148
+ emitter
145
149
  });
146
150
  return jsonResponse(result.data, result.status);
147
151
  };
148
152
  }
149
153
  function createExpressHandler(options) {
150
- const { slackBotToken, slackChannel, botName, botIcon, onMessage, store } = options;
154
+ const { slackBotToken, slackChannel, botName, botIcon, onMessage, store, emitter } = options;
151
155
  const slack = new WebClient(slackBotToken);
152
156
  const resolvedStore = store ?? new InMemoryStore();
153
157
  return async (req, res) => {
@@ -158,7 +162,8 @@ function createExpressHandler(options) {
158
162
  botName,
159
163
  botIcon,
160
164
  onMessage,
161
- store: resolvedStore
165
+ store: resolvedStore,
166
+ emitter
162
167
  });
163
168
  res.status(result.status).json(result.data);
164
169
  };
@@ -2130,7 +2135,7 @@ function verifySlackSignature(signingSecret, signature, timestamp, rawBody) {
2130
2135
  }
2131
2136
  }
2132
2137
  function createWebhookHandler(options) {
2133
- const { store, signingSecret } = options;
2138
+ const { store, signingSecret, emitter } = options;
2134
2139
  return async (request) => {
2135
2140
  let rawBody;
2136
2141
  try {
@@ -2181,13 +2186,16 @@ function createWebhookHandler(options) {
2181
2186
  threadTs: event.thread_ts
2182
2187
  };
2183
2188
  await store.saveReply(threadRecord.sessionId, reply);
2189
+ if (emitter) {
2190
+ emitter.emit(threadRecord.sessionId, reply);
2191
+ }
2184
2192
  return jsonResponse2({ ok: true }, 200);
2185
2193
  }
2186
2194
  return jsonResponse2({ ok: true }, 200);
2187
2195
  };
2188
2196
  }
2189
2197
  function createExpressWebhookHandler(options) {
2190
- const { store, signingSecret } = options;
2198
+ const { store, signingSecret, emitter } = options;
2191
2199
  return async (req, res) => {
2192
2200
  const rawBody = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf-8") : JSON.stringify(req.body);
2193
2201
  const signature = req.headers?.["x-slack-signature"] ?? "";
@@ -2238,6 +2246,9 @@ function createExpressWebhookHandler(options) {
2238
2246
  threadTs: event.thread_ts
2239
2247
  };
2240
2248
  await store.saveReply(threadRecord.sessionId, reply);
2249
+ if (emitter) {
2250
+ emitter.emit(threadRecord.sessionId, reply);
2251
+ }
2241
2252
  res.status(200).json({ ok: true });
2242
2253
  return;
2243
2254
  }
@@ -2300,6 +2311,188 @@ function jsonResponse3(data, status = 200) {
2300
2311
  });
2301
2312
  }
2302
2313
 
2303
- export { InMemoryStore, createExpressHandler, createExpressRepliesHandler, createExpressWebhookHandler, createRepliesHandler, createSupportHandler, createWebhookHandler, emojify, validateSlackToken, verifySlackSignature };
2314
+ // src/server/emitter.ts
2315
+ function createReplyEmitter() {
2316
+ const subscribers = /* @__PURE__ */ new Map();
2317
+ return {
2318
+ emit(sessionId, reply) {
2319
+ const callbacks = subscribers.get(sessionId);
2320
+ if (!callbacks) return;
2321
+ const event = { type: "reply", sessionId, reply };
2322
+ for (const cb of callbacks) {
2323
+ try {
2324
+ cb(event);
2325
+ } catch {
2326
+ }
2327
+ }
2328
+ },
2329
+ emitThreadCreated(sessionId, threadTs) {
2330
+ const callbacks = subscribers.get(sessionId);
2331
+ if (!callbacks) return;
2332
+ const event = {
2333
+ type: "thread_created",
2334
+ sessionId,
2335
+ threadTs
2336
+ };
2337
+ for (const cb of callbacks) {
2338
+ try {
2339
+ cb(event);
2340
+ } catch {
2341
+ }
2342
+ }
2343
+ },
2344
+ subscribe(sessionId, callback) {
2345
+ let callbacks = subscribers.get(sessionId);
2346
+ if (!callbacks) {
2347
+ callbacks = /* @__PURE__ */ new Set();
2348
+ subscribers.set(sessionId, callbacks);
2349
+ }
2350
+ callbacks.add(callback);
2351
+ },
2352
+ unsubscribe(sessionId, callback) {
2353
+ const callbacks = subscribers.get(sessionId);
2354
+ if (!callbacks) return;
2355
+ callbacks.delete(callback);
2356
+ if (callbacks.size === 0) {
2357
+ subscribers.delete(sessionId);
2358
+ }
2359
+ }
2360
+ };
2361
+ }
2362
+
2363
+ // src/server/sse.ts
2364
+ function createSSEHandler(options) {
2365
+ const { emitter, heartbeatInterval = 3e4 } = options;
2366
+ return (request) => {
2367
+ const url = new URL(request.url);
2368
+ const sessionId = url.searchParams.get("sessionId");
2369
+ if (!sessionId) {
2370
+ return new Response(
2371
+ JSON.stringify({ error: "Missing required query parameter: sessionId" }),
2372
+ {
2373
+ status: 400,
2374
+ headers: { "Content-Type": "application/json" }
2375
+ }
2376
+ );
2377
+ }
2378
+ const sid = sessionId;
2379
+ let heartbeatTimer = null;
2380
+ let unsubscribed = false;
2381
+ const stream = new ReadableStream({
2382
+ start(controller) {
2383
+ const callback = (event) => {
2384
+ if (unsubscribed) return;
2385
+ try {
2386
+ if (event.type === "reply") {
2387
+ const data = JSON.stringify({ reply: event.reply });
2388
+ controller.enqueue(formatSSE("reply", data));
2389
+ } else if (event.type === "thread_created") {
2390
+ const data = JSON.stringify({ threadTs: event.threadTs });
2391
+ controller.enqueue(formatSSE("thread_created", data));
2392
+ }
2393
+ } catch {
2394
+ cleanup();
2395
+ }
2396
+ };
2397
+ emitter.subscribe(sid, callback);
2398
+ heartbeatTimer = setInterval(() => {
2399
+ try {
2400
+ controller.enqueue(formatSSE("heartbeat", "{}"));
2401
+ } catch {
2402
+ cleanup();
2403
+ }
2404
+ }, heartbeatInterval);
2405
+ function cleanup() {
2406
+ if (unsubscribed) return;
2407
+ unsubscribed = true;
2408
+ emitter.unsubscribe(sid, callback);
2409
+ if (heartbeatTimer !== null) {
2410
+ clearInterval(heartbeatTimer);
2411
+ heartbeatTimer = null;
2412
+ }
2413
+ }
2414
+ if (request.signal) {
2415
+ request.signal.addEventListener("abort", () => {
2416
+ cleanup();
2417
+ try {
2418
+ controller.close();
2419
+ } catch {
2420
+ }
2421
+ });
2422
+ }
2423
+ },
2424
+ cancel() {
2425
+ unsubscribed = true;
2426
+ if (heartbeatTimer !== null) {
2427
+ clearInterval(heartbeatTimer);
2428
+ heartbeatTimer = null;
2429
+ }
2430
+ }
2431
+ });
2432
+ return new Response(stream, {
2433
+ status: 200,
2434
+ headers: {
2435
+ "Content-Type": "text/event-stream",
2436
+ "Cache-Control": "no-cache",
2437
+ Connection: "keep-alive"
2438
+ }
2439
+ });
2440
+ };
2441
+ }
2442
+ function createExpressSSEHandler(options) {
2443
+ const { emitter, heartbeatInterval = 3e4 } = options;
2444
+ return (req, res) => {
2445
+ const sessionId = req.query?.sessionId;
2446
+ if (!sessionId || typeof sessionId !== "string") {
2447
+ res.writeHead(400, { "Content-Type": "application/json" });
2448
+ res.write(JSON.stringify({ error: "Missing required query parameter: sessionId" }));
2449
+ res.end();
2450
+ return;
2451
+ }
2452
+ res.writeHead(200, {
2453
+ "Content-Type": "text/event-stream",
2454
+ "Cache-Control": "no-cache",
2455
+ Connection: "keep-alive"
2456
+ });
2457
+ let cleaned = false;
2458
+ const callback = (event) => {
2459
+ if (cleaned) return;
2460
+ if (event.type === "reply") {
2461
+ const data = JSON.stringify({ reply: event.reply });
2462
+ res.write(formatSSEString("reply", data));
2463
+ } else if (event.type === "thread_created") {
2464
+ const data = JSON.stringify({ threadTs: event.threadTs });
2465
+ res.write(formatSSEString("thread_created", data));
2466
+ }
2467
+ };
2468
+ emitter.subscribe(sessionId, callback);
2469
+ const heartbeatTimer = setInterval(() => {
2470
+ if (cleaned) return;
2471
+ res.write(formatSSEString("heartbeat", "{}"));
2472
+ }, heartbeatInterval);
2473
+ function cleanup() {
2474
+ if (cleaned) return;
2475
+ cleaned = true;
2476
+ emitter.unsubscribe(sessionId, callback);
2477
+ clearInterval(heartbeatTimer);
2478
+ }
2479
+ res.on("close", cleanup);
2480
+ };
2481
+ }
2482
+ function formatSSE(event, data) {
2483
+ const text = `event: ${event}
2484
+ data: ${data}
2485
+
2486
+ `;
2487
+ return new TextEncoder().encode(text);
2488
+ }
2489
+ function formatSSEString(event, data) {
2490
+ return `event: ${event}
2491
+ data: ${data}
2492
+
2493
+ `;
2494
+ }
2495
+
2496
+ export { InMemoryStore, createExpressHandler, createExpressRepliesHandler, createExpressSSEHandler, createExpressWebhookHandler, createRepliesHandler, createReplyEmitter, createSSEHandler, createSupportHandler, createWebhookHandler, emojify, validateSlackToken, verifySlackSignature };
2304
2497
  //# sourceMappingURL=index.js.map
2305
2498
  //# sourceMappingURL=index.js.map