-
Notifications
You must be signed in to change notification settings - Fork 400
/
conversation-store.ts
75 lines (70 loc) · 2.96 KB
/
conversation-store.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { Middleware, AnyMiddlewareArgs } from './types';
import { getTypeAndConversation } from './helpers';
/**
* Storage backend used by the conversation context middleware
*/
export interface ConversationStore<ConversationState = any> {
// NOTE: expiresAt is in milliseconds
set(conversationId: string, value: ConversationState, expiresAt?: number): Promise<unknown>;
get(conversationId: string): Promise<ConversationState>;
}
/**
* Default implementation of ConversationStore, which stores data in memory.
*
* This should not be used in situations where there is more than once instance of the app running because state will
* not be shared amongst the processes.
*/
export class MemoryStore<ConversationState = any> implements ConversationStore<ConversationState> {
private state: Map<string, { value: ConversationState; expiresAt?: number }> = new Map();
public set(conversationId: string, value: ConversationState, expiresAt?: number): Promise<void> {
return new Promise((resolve) => {
this.state.set(conversationId, { value, expiresAt });
resolve();
});
}
public get(conversationId: string): Promise<ConversationState> {
return new Promise((resolve, reject) => {
const entry = this.state.get(conversationId);
if (entry !== undefined) {
if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) {
// release the memory
this.state.delete(conversationId);
reject(new Error('Conversation expired'));
}
resolve(entry.value);
}
reject(new Error('Conversation not found'));
});
}
}
/**
* Conversation context global middleware.
*
* This middleware allows listeners (and other middleware) to store state related to the conversationId of an incoming
* event using the `context.updateConversation()` function. That state will be made available in future events that
* take place in the same conversation by reading from `context.conversation`.
*
* @param store storage backend used to store and retrieve all conversation state
* @param logger a logger
*/
export function conversationContext<ConversationState = any>(
store: ConversationStore<ConversationState>,
): Middleware<AnyMiddlewareArgs> {
return async ({ body, context, next, logger }) => {
const { conversationId } = getTypeAndConversation(body);
if (conversationId !== undefined) {
// TODO: expiresAt is not passed through to store.set
context.updateConversation = (conversation: ConversationState) => store.set(conversationId, conversation);
try {
context.conversation = await store.get(conversationId);
logger.debug(`Conversation context loaded for ID ${conversationId}`);
} catch (error) {
logger.debug(`Conversation context failed loading for ID: ${conversationId}, error: ${error.message}`);
}
} else {
logger.debug('No conversation ID for incoming event');
}
// TODO: remove the non-null assertion operator
await next!();
};
}