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.
@@ -45,7 +45,7 @@ var InMemoryStore = class {
45
45
 
46
46
  // src/server/handler.ts
47
47
  async function handleMessage(body, options) {
48
- const { slack, slackChannel, botName, botIcon, onMessage, store } = options;
48
+ const { slack, slackChannel, botName, botIcon, onMessage, store, emitter } = options;
49
49
  if (!body.message || typeof body.message !== "string") {
50
50
  return {
51
51
  data: { success: false, error: "Missing required field: message" },
@@ -108,6 +108,9 @@ ${body.message}`,
108
108
  user: body.user,
109
109
  context: body.context
110
110
  });
111
+ if (emitter) {
112
+ emitter.emitThreadCreated(body.sessionId, threadTs);
113
+ }
111
114
  return {
112
115
  data: { success: true, threadId: threadTs },
113
116
  status: 200
@@ -122,7 +125,7 @@ ${body.message}`,
122
125
  }
123
126
  }
124
127
  function createSupportHandler(options) {
125
- const { slackBotToken, slackChannel, botName, botIcon, onMessage, store } = options;
128
+ const { slackBotToken, slackChannel, botName, botIcon, onMessage, store, emitter } = options;
126
129
  const slack = new webApi.WebClient(slackBotToken);
127
130
  const resolvedStore = store ?? new InMemoryStore();
128
131
  return async (request) => {
@@ -147,13 +150,14 @@ function createSupportHandler(options) {
147
150
  botName,
148
151
  botIcon,
149
152
  onMessage,
150
- store: resolvedStore
153
+ store: resolvedStore,
154
+ emitter
151
155
  });
152
156
  return jsonResponse(result.data, result.status);
153
157
  };
154
158
  }
155
159
  function createExpressHandler(options) {
156
- const { slackBotToken, slackChannel, botName, botIcon, onMessage, store } = options;
160
+ const { slackBotToken, slackChannel, botName, botIcon, onMessage, store, emitter } = options;
157
161
  const slack = new webApi.WebClient(slackBotToken);
158
162
  const resolvedStore = store ?? new InMemoryStore();
159
163
  return async (req, res) => {
@@ -164,7 +168,8 @@ function createExpressHandler(options) {
164
168
  botName,
165
169
  botIcon,
166
170
  onMessage,
167
- store: resolvedStore
171
+ store: resolvedStore,
172
+ emitter
168
173
  });
169
174
  res.status(result.status).json(result.data);
170
175
  };
@@ -2136,7 +2141,7 @@ function verifySlackSignature(signingSecret, signature, timestamp, rawBody) {
2136
2141
  }
2137
2142
  }
2138
2143
  function createWebhookHandler(options) {
2139
- const { store, signingSecret } = options;
2144
+ const { store, signingSecret, emitter } = options;
2140
2145
  return async (request) => {
2141
2146
  let rawBody;
2142
2147
  try {
@@ -2187,13 +2192,16 @@ function createWebhookHandler(options) {
2187
2192
  threadTs: event.thread_ts
2188
2193
  };
2189
2194
  await store.saveReply(threadRecord.sessionId, reply);
2195
+ if (emitter) {
2196
+ emitter.emit(threadRecord.sessionId, reply);
2197
+ }
2190
2198
  return jsonResponse2({ ok: true }, 200);
2191
2199
  }
2192
2200
  return jsonResponse2({ ok: true }, 200);
2193
2201
  };
2194
2202
  }
2195
2203
  function createExpressWebhookHandler(options) {
2196
- const { store, signingSecret } = options;
2204
+ const { store, signingSecret, emitter } = options;
2197
2205
  return async (req, res) => {
2198
2206
  const rawBody = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf-8") : JSON.stringify(req.body);
2199
2207
  const signature = req.headers?.["x-slack-signature"] ?? "";
@@ -2244,6 +2252,9 @@ function createExpressWebhookHandler(options) {
2244
2252
  threadTs: event.thread_ts
2245
2253
  };
2246
2254
  await store.saveReply(threadRecord.sessionId, reply);
2255
+ if (emitter) {
2256
+ emitter.emit(threadRecord.sessionId, reply);
2257
+ }
2247
2258
  res.status(200).json({ ok: true });
2248
2259
  return;
2249
2260
  }
@@ -2306,11 +2317,196 @@ function jsonResponse3(data, status = 200) {
2306
2317
  });
2307
2318
  }
2308
2319
 
2320
+ // src/server/emitter.ts
2321
+ function createReplyEmitter() {
2322
+ const subscribers = /* @__PURE__ */ new Map();
2323
+ return {
2324
+ emit(sessionId, reply) {
2325
+ const callbacks = subscribers.get(sessionId);
2326
+ if (!callbacks) return;
2327
+ const event = { type: "reply", sessionId, reply };
2328
+ for (const cb of callbacks) {
2329
+ try {
2330
+ cb(event);
2331
+ } catch {
2332
+ }
2333
+ }
2334
+ },
2335
+ emitThreadCreated(sessionId, threadTs) {
2336
+ const callbacks = subscribers.get(sessionId);
2337
+ if (!callbacks) return;
2338
+ const event = {
2339
+ type: "thread_created",
2340
+ sessionId,
2341
+ threadTs
2342
+ };
2343
+ for (const cb of callbacks) {
2344
+ try {
2345
+ cb(event);
2346
+ } catch {
2347
+ }
2348
+ }
2349
+ },
2350
+ subscribe(sessionId, callback) {
2351
+ let callbacks = subscribers.get(sessionId);
2352
+ if (!callbacks) {
2353
+ callbacks = /* @__PURE__ */ new Set();
2354
+ subscribers.set(sessionId, callbacks);
2355
+ }
2356
+ callbacks.add(callback);
2357
+ },
2358
+ unsubscribe(sessionId, callback) {
2359
+ const callbacks = subscribers.get(sessionId);
2360
+ if (!callbacks) return;
2361
+ callbacks.delete(callback);
2362
+ if (callbacks.size === 0) {
2363
+ subscribers.delete(sessionId);
2364
+ }
2365
+ }
2366
+ };
2367
+ }
2368
+
2369
+ // src/server/sse.ts
2370
+ function createSSEHandler(options) {
2371
+ const { emitter, heartbeatInterval = 3e4 } = options;
2372
+ return (request) => {
2373
+ const url = new URL(request.url);
2374
+ const sessionId = url.searchParams.get("sessionId");
2375
+ if (!sessionId) {
2376
+ return new Response(
2377
+ JSON.stringify({ error: "Missing required query parameter: sessionId" }),
2378
+ {
2379
+ status: 400,
2380
+ headers: { "Content-Type": "application/json" }
2381
+ }
2382
+ );
2383
+ }
2384
+ const sid = sessionId;
2385
+ let heartbeatTimer = null;
2386
+ let unsubscribed = false;
2387
+ const stream = new ReadableStream({
2388
+ start(controller) {
2389
+ const callback = (event) => {
2390
+ if (unsubscribed) return;
2391
+ try {
2392
+ if (event.type === "reply") {
2393
+ const data = JSON.stringify({ reply: event.reply });
2394
+ controller.enqueue(formatSSE("reply", data));
2395
+ } else if (event.type === "thread_created") {
2396
+ const data = JSON.stringify({ threadTs: event.threadTs });
2397
+ controller.enqueue(formatSSE("thread_created", data));
2398
+ }
2399
+ } catch {
2400
+ cleanup();
2401
+ }
2402
+ };
2403
+ emitter.subscribe(sid, callback);
2404
+ heartbeatTimer = setInterval(() => {
2405
+ try {
2406
+ controller.enqueue(formatSSE("heartbeat", "{}"));
2407
+ } catch {
2408
+ cleanup();
2409
+ }
2410
+ }, heartbeatInterval);
2411
+ function cleanup() {
2412
+ if (unsubscribed) return;
2413
+ unsubscribed = true;
2414
+ emitter.unsubscribe(sid, callback);
2415
+ if (heartbeatTimer !== null) {
2416
+ clearInterval(heartbeatTimer);
2417
+ heartbeatTimer = null;
2418
+ }
2419
+ }
2420
+ if (request.signal) {
2421
+ request.signal.addEventListener("abort", () => {
2422
+ cleanup();
2423
+ try {
2424
+ controller.close();
2425
+ } catch {
2426
+ }
2427
+ });
2428
+ }
2429
+ },
2430
+ cancel() {
2431
+ unsubscribed = true;
2432
+ if (heartbeatTimer !== null) {
2433
+ clearInterval(heartbeatTimer);
2434
+ heartbeatTimer = null;
2435
+ }
2436
+ }
2437
+ });
2438
+ return new Response(stream, {
2439
+ status: 200,
2440
+ headers: {
2441
+ "Content-Type": "text/event-stream",
2442
+ "Cache-Control": "no-cache",
2443
+ Connection: "keep-alive"
2444
+ }
2445
+ });
2446
+ };
2447
+ }
2448
+ function createExpressSSEHandler(options) {
2449
+ const { emitter, heartbeatInterval = 3e4 } = options;
2450
+ return (req, res) => {
2451
+ const sessionId = req.query?.sessionId;
2452
+ if (!sessionId || typeof sessionId !== "string") {
2453
+ res.writeHead(400, { "Content-Type": "application/json" });
2454
+ res.write(JSON.stringify({ error: "Missing required query parameter: sessionId" }));
2455
+ res.end();
2456
+ return;
2457
+ }
2458
+ res.writeHead(200, {
2459
+ "Content-Type": "text/event-stream",
2460
+ "Cache-Control": "no-cache",
2461
+ Connection: "keep-alive"
2462
+ });
2463
+ let cleaned = false;
2464
+ const callback = (event) => {
2465
+ if (cleaned) return;
2466
+ if (event.type === "reply") {
2467
+ const data = JSON.stringify({ reply: event.reply });
2468
+ res.write(formatSSEString("reply", data));
2469
+ } else if (event.type === "thread_created") {
2470
+ const data = JSON.stringify({ threadTs: event.threadTs });
2471
+ res.write(formatSSEString("thread_created", data));
2472
+ }
2473
+ };
2474
+ emitter.subscribe(sessionId, callback);
2475
+ const heartbeatTimer = setInterval(() => {
2476
+ if (cleaned) return;
2477
+ res.write(formatSSEString("heartbeat", "{}"));
2478
+ }, heartbeatInterval);
2479
+ function cleanup() {
2480
+ if (cleaned) return;
2481
+ cleaned = true;
2482
+ emitter.unsubscribe(sessionId, callback);
2483
+ clearInterval(heartbeatTimer);
2484
+ }
2485
+ res.on("close", cleanup);
2486
+ };
2487
+ }
2488
+ function formatSSE(event, data) {
2489
+ const text = `event: ${event}
2490
+ data: ${data}
2491
+
2492
+ `;
2493
+ return new TextEncoder().encode(text);
2494
+ }
2495
+ function formatSSEString(event, data) {
2496
+ return `event: ${event}
2497
+ data: ${data}
2498
+
2499
+ `;
2500
+ }
2501
+
2309
2502
  exports.InMemoryStore = InMemoryStore;
2310
2503
  exports.createExpressHandler = createExpressHandler;
2311
2504
  exports.createExpressRepliesHandler = createExpressRepliesHandler;
2505
+ exports.createExpressSSEHandler = createExpressSSEHandler;
2312
2506
  exports.createExpressWebhookHandler = createExpressWebhookHandler;
2313
2507
  exports.createRepliesHandler = createRepliesHandler;
2508
+ exports.createReplyEmitter = createReplyEmitter;
2509
+ exports.createSSEHandler = createSSEHandler;
2314
2510
  exports.createSupportHandler = createSupportHandler;
2315
2511
  exports.createWebhookHandler = createWebhookHandler;
2316
2512
  exports.emojify = emojify;