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.
- package/README.md +197 -3
- package/dist/client/index.cjs +205 -103
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +86 -22
- package/dist/client/index.d.ts +86 -22
- package/dist/client/index.js +205 -104
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.cjs +203 -7
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +120 -1
- package/dist/server/index.d.ts +120 -1
- package/dist/server/index.js +201 -8
- package/dist/server/index.js.map +1 -1
- package/package.json +87 -87
package/dist/server/index.d.cts
CHANGED
|
@@ -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 };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|