@fraqjs/plugin-conversation 0.1.0 → 0.2.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/dist/index.d.mts +12 -5
- package/dist/index.mjs +70 -9
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { Context, Router, Session
|
|
1
|
+
import { Context, ParamsOf, Pattern, Router, Session } from "@fraqjs/fraq";
|
|
2
2
|
|
|
3
3
|
//#region src/service.d.ts
|
|
4
4
|
interface ConversationServiceOptions {
|
|
5
5
|
defaultTimeout?: number;
|
|
6
6
|
onCollision?: 'reject-incoming' | 'abort-existing';
|
|
7
7
|
}
|
|
8
|
+
interface ConversationCommandScope {
|
|
9
|
+
open<R>(handler: (conversationContext: ConversationContext<R>) => void | Promise<void>, options?: ConversationOptions): Promise<R | null>;
|
|
10
|
+
}
|
|
8
11
|
interface ConversationContext<R> {
|
|
9
12
|
session: Session;
|
|
10
13
|
router: Router;
|
|
@@ -13,15 +16,19 @@ interface ConversationContext<R> {
|
|
|
13
16
|
}
|
|
14
17
|
interface ConversationOptions {
|
|
15
18
|
timeout?: number;
|
|
16
|
-
onUnmatched?: (raw: milky.IncomingMessage) => void | Promise<void>;
|
|
17
19
|
}
|
|
18
20
|
declare class ConversationService {
|
|
21
|
+
private readonly ctx;
|
|
19
22
|
readonly defaultTimeout: number;
|
|
20
23
|
readonly onCollision: 'reject-incoming' | 'abort-existing';
|
|
24
|
+
private readonly router;
|
|
21
25
|
private readonly activeConversations;
|
|
22
26
|
constructor(ctx: Context, options?: ConversationServiceOptions);
|
|
23
|
-
|
|
27
|
+
command<P extends Pattern>(name: string, pattern: P, handler: (session: Session, params: ParamsOf<P>, scope: ConversationCommandScope) => void | Promise<void>): this;
|
|
28
|
+
private openFromCommand;
|
|
24
29
|
private accept;
|
|
30
|
+
private dispatchCommand;
|
|
31
|
+
private dispatchActive;
|
|
25
32
|
private resolve;
|
|
26
33
|
private reject;
|
|
27
34
|
private abort;
|
|
@@ -37,6 +44,6 @@ declare class ConversationAbortionError extends Error {
|
|
|
37
44
|
}
|
|
38
45
|
//#endregion
|
|
39
46
|
//#region src/index.d.ts
|
|
40
|
-
declare const ConversationPlugin: import("@fraqjs/fraq").Plugin<[options
|
|
47
|
+
declare const ConversationPlugin: import("@fraqjs/fraq").Plugin<[options?: ConversationServiceOptions | undefined], import("@fraqjs/fraq").Injection | undefined, import("@fraqjs/fraq").Injection | undefined>;
|
|
41
48
|
//#endregion
|
|
42
|
-
export { ConversationAbortionError, ConversationContext, ConversationOptions, ConversationPlugin, ConversationRejectionError, ConversationService, ConversationServiceOptions };
|
|
49
|
+
export { ConversationAbortionError, ConversationCommandScope, ConversationContext, ConversationOptions, ConversationPlugin, ConversationRejectionError, ConversationService, ConversationServiceOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -23,11 +23,39 @@ function assertPositiveTimeout(timeout, name) {
|
|
|
23
23
|
function isSame(a, b) {
|
|
24
24
|
return a.message_scene === b.message_scene && a.peer_id === b.peer_id && a.sender_id === b.sender_id && a.message_seq === b.message_seq;
|
|
25
25
|
}
|
|
26
|
+
function branchFromMatch(match) {
|
|
27
|
+
switch (match.type) {
|
|
28
|
+
case "command": return {
|
|
29
|
+
type: "command",
|
|
30
|
+
path: match.path,
|
|
31
|
+
command: match.command
|
|
32
|
+
};
|
|
33
|
+
case "rawPattern": return {
|
|
34
|
+
type: "rawPattern",
|
|
35
|
+
path: match.path,
|
|
36
|
+
rawPattern: match.rawPattern
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function isSameBranch(a, b) {
|
|
41
|
+
if (a.path.length !== b.path.length || !a.path.every((part, index) => part === b.path[index])) return false;
|
|
42
|
+
switch (a.type) {
|
|
43
|
+
case "command":
|
|
44
|
+
if (b.type !== "command") return false;
|
|
45
|
+
return a.command === b.command;
|
|
46
|
+
case "rawPattern":
|
|
47
|
+
if (b.type !== "rawPattern") return false;
|
|
48
|
+
return a.rawPattern === b.rawPattern;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
26
51
|
var ConversationService = class {
|
|
52
|
+
ctx;
|
|
27
53
|
defaultTimeout;
|
|
28
54
|
onCollision;
|
|
55
|
+
router = new Router();
|
|
29
56
|
activeConversations = /* @__PURE__ */ new Map();
|
|
30
57
|
constructor(ctx, options) {
|
|
58
|
+
this.ctx = ctx;
|
|
31
59
|
this.defaultTimeout = options?.defaultTimeout ?? 3e4;
|
|
32
60
|
assertPositiveTimeout(this.defaultTimeout, "defaultTimeout");
|
|
33
61
|
this.onCollision = options?.onCollision ?? "reject-incoming";
|
|
@@ -35,7 +63,23 @@ var ConversationService = class {
|
|
|
35
63
|
this.accept(data);
|
|
36
64
|
});
|
|
37
65
|
}
|
|
38
|
-
|
|
66
|
+
command(name, pattern, handler) {
|
|
67
|
+
let branch;
|
|
68
|
+
this.router.command(name, pattern, (session, params) => {
|
|
69
|
+
const registeredBranch = branch;
|
|
70
|
+
if (!registeredBranch) throw new Error(`Conversation command "${name}" was not registered correctly.`);
|
|
71
|
+
return handler(session, params, { open: (conversationHandler, options) => this.openFromCommand(session, registeredBranch, conversationHandler, options) });
|
|
72
|
+
});
|
|
73
|
+
const entry = this.router.routes().at(-1);
|
|
74
|
+
if (entry?.type !== "command") throw new Error(`Conversation command "${name}" was not registered correctly.`);
|
|
75
|
+
branch = {
|
|
76
|
+
type: "command",
|
|
77
|
+
path: [],
|
|
78
|
+
command: entry.command
|
|
79
|
+
};
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
async openFromCommand(session, branch, handler, options) {
|
|
39
83
|
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
40
84
|
assertPositiveTimeout(timeout, "timeout");
|
|
41
85
|
const key = conversationKey(session.raw);
|
|
@@ -48,6 +92,7 @@ var ConversationService = class {
|
|
|
48
92
|
const active = {
|
|
49
93
|
key,
|
|
50
94
|
session,
|
|
95
|
+
branch,
|
|
51
96
|
router: new Router(),
|
|
52
97
|
options,
|
|
53
98
|
resolve,
|
|
@@ -69,8 +114,30 @@ var ConversationService = class {
|
|
|
69
114
|
});
|
|
70
115
|
}
|
|
71
116
|
async accept(raw) {
|
|
117
|
+
const session = this.ctx.createSession(raw);
|
|
118
|
+
const commandMatch = this.router.match(session, raw);
|
|
119
|
+
const commandBranch = commandMatch ? branchFromMatch(commandMatch) : void 0;
|
|
72
120
|
const active = this.activeConversations.get(conversationKey(raw));
|
|
73
|
-
if (!active || active.settled)
|
|
121
|
+
if (!active || active.settled) {
|
|
122
|
+
await this.dispatchCommand(commandMatch, session);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (commandBranch && isSameBranch(active.branch, commandBranch) && this.onCollision === "abort-existing" && !isSame(active.session.raw, raw)) {
|
|
126
|
+
this.abort(active, "aborted due to new conversation");
|
|
127
|
+
await this.dispatchCommand(commandMatch, session);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await this.dispatchActive(active, raw);
|
|
131
|
+
}
|
|
132
|
+
async dispatchCommand(match, session) {
|
|
133
|
+
if (match?.type !== "command") return;
|
|
134
|
+
try {
|
|
135
|
+
await match.command.handler(session, match.params);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
this.ctx.logger.error(`Error routing conversation command (scene=${session.raw.message_scene} peer=${session.raw.peer_id} sender=${session.raw.sender_id} seq=${session.raw.message_seq})`, error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async dispatchActive(active, raw) {
|
|
74
141
|
if (isSame(active.session.raw, raw)) return;
|
|
75
142
|
await active.ready;
|
|
76
143
|
if (this.activeConversations.get(active.key) !== active || active.settled) return;
|
|
@@ -78,18 +145,12 @@ var ConversationService = class {
|
|
|
78
145
|
...active.session,
|
|
79
146
|
raw
|
|
80
147
|
};
|
|
81
|
-
let matched;
|
|
82
148
|
try {
|
|
83
|
-
|
|
149
|
+
await active.router.dispatch(session, raw);
|
|
84
150
|
} catch (error) {
|
|
85
151
|
this.reject(active, error);
|
|
86
152
|
return;
|
|
87
153
|
}
|
|
88
|
-
if (!matched && this.activeConversations.get(active.key) === active && !active.settled) try {
|
|
89
|
-
await active.options?.onUnmatched?.(raw);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
this.reject(active, error);
|
|
92
|
-
}
|
|
93
154
|
}
|
|
94
155
|
resolve(active, result) {
|
|
95
156
|
this.settle(active, () => active.resolve(result));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fraqjs/plugin-conversation",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Conversation utility plugin for Fraq",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"homepage": "https://fraq.ntqqrev.org/",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@fraqjs/fraq": "^0.
|
|
20
|
+
"@fraqjs/fraq": "^0.5.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@fraqjs/mock": "^0.1.0"
|