@fraqjs/plugin-conversation 0.1.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 +42 -0
- package/dist/index.mjs +124 -0
- package/package.json +29 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Context, Router, Session, milky } from "@fraqjs/fraq";
|
|
2
|
+
|
|
3
|
+
//#region src/service.d.ts
|
|
4
|
+
interface ConversationServiceOptions {
|
|
5
|
+
defaultTimeout?: number;
|
|
6
|
+
onCollision?: 'reject-incoming' | 'abort-existing';
|
|
7
|
+
}
|
|
8
|
+
interface ConversationContext<R> {
|
|
9
|
+
session: Session;
|
|
10
|
+
router: Router;
|
|
11
|
+
done(result: R): void;
|
|
12
|
+
abort(reason?: string): void;
|
|
13
|
+
}
|
|
14
|
+
interface ConversationOptions {
|
|
15
|
+
timeout?: number;
|
|
16
|
+
onUnmatched?: (raw: milky.IncomingMessage) => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
declare class ConversationService {
|
|
19
|
+
readonly defaultTimeout: number;
|
|
20
|
+
readonly onCollision: 'reject-incoming' | 'abort-existing';
|
|
21
|
+
private readonly activeConversations;
|
|
22
|
+
constructor(ctx: Context, options?: ConversationServiceOptions);
|
|
23
|
+
open<R>(session: Session, handler: (conversationContext: ConversationContext<R>) => void | Promise<void>, options?: ConversationOptions): Promise<R | null>;
|
|
24
|
+
private accept;
|
|
25
|
+
private resolve;
|
|
26
|
+
private reject;
|
|
27
|
+
private abort;
|
|
28
|
+
private settle;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/error.d.ts
|
|
32
|
+
declare class ConversationRejectionError extends Error {
|
|
33
|
+
constructor(reason: string);
|
|
34
|
+
}
|
|
35
|
+
declare class ConversationAbortionError extends Error {
|
|
36
|
+
constructor(reason: string);
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/index.d.ts
|
|
40
|
+
declare const ConversationPlugin: import("@fraqjs/fraq").Plugin<[options: ConversationServiceOptions], import("@fraqjs/fraq").Injection | undefined, import("@fraqjs/fraq").Injection | undefined>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { ConversationAbortionError, ConversationContext, ConversationOptions, ConversationPlugin, ConversationRejectionError, ConversationService, ConversationServiceOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Router, definePlugin } from "@fraqjs/fraq";
|
|
2
|
+
//#region src/error.ts
|
|
3
|
+
var ConversationRejectionError = class extends Error {
|
|
4
|
+
constructor(reason) {
|
|
5
|
+
super(`Conversation rejected: ${reason}`);
|
|
6
|
+
this.name = "ConversationRejectionError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var ConversationAbortionError = class extends Error {
|
|
10
|
+
constructor(reason) {
|
|
11
|
+
super(`Conversation aborted: ${reason}`);
|
|
12
|
+
this.name = "ConversationAbortionError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/service.ts
|
|
17
|
+
function conversationKey(raw) {
|
|
18
|
+
return `${raw.sender_id}:${raw.message_scene}:${raw.peer_id}`;
|
|
19
|
+
}
|
|
20
|
+
function assertPositiveTimeout(timeout, name) {
|
|
21
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError(`${name} must be a positive finite number.`);
|
|
22
|
+
}
|
|
23
|
+
function isSame(a, b) {
|
|
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
|
+
}
|
|
26
|
+
var ConversationService = class {
|
|
27
|
+
defaultTimeout;
|
|
28
|
+
onCollision;
|
|
29
|
+
activeConversations = /* @__PURE__ */ new Map();
|
|
30
|
+
constructor(ctx, options) {
|
|
31
|
+
this.defaultTimeout = options?.defaultTimeout ?? 3e4;
|
|
32
|
+
assertPositiveTimeout(this.defaultTimeout, "defaultTimeout");
|
|
33
|
+
this.onCollision = options?.onCollision ?? "reject-incoming";
|
|
34
|
+
ctx.on("message_receive", ({ data }) => {
|
|
35
|
+
this.accept(data);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async open(session, handler, options) {
|
|
39
|
+
const timeout = options?.timeout ?? this.defaultTimeout;
|
|
40
|
+
assertPositiveTimeout(timeout, "timeout");
|
|
41
|
+
const key = conversationKey(session.raw);
|
|
42
|
+
const existing = this.activeConversations.get(key);
|
|
43
|
+
if (existing) {
|
|
44
|
+
if (this.onCollision === "reject-incoming") return Promise.reject(new ConversationRejectionError("conversation already active"));
|
|
45
|
+
this.abort(existing, "aborted due to new conversation");
|
|
46
|
+
}
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const active = {
|
|
49
|
+
key,
|
|
50
|
+
session,
|
|
51
|
+
router: new Router(),
|
|
52
|
+
options,
|
|
53
|
+
resolve,
|
|
54
|
+
reject,
|
|
55
|
+
ready: Promise.resolve().then(() => handler({
|
|
56
|
+
session,
|
|
57
|
+
router: active.router,
|
|
58
|
+
done: (result) => this.resolve(active, result),
|
|
59
|
+
abort: (reason) => this.abort(active, reason)
|
|
60
|
+
})).catch((error) => {
|
|
61
|
+
this.reject(active, error);
|
|
62
|
+
}),
|
|
63
|
+
settled: false
|
|
64
|
+
};
|
|
65
|
+
active.timeoutId = setTimeout(() => {
|
|
66
|
+
this.resolve(active, null);
|
|
67
|
+
}, timeout);
|
|
68
|
+
this.activeConversations.set(key, active);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async accept(raw) {
|
|
72
|
+
const active = this.activeConversations.get(conversationKey(raw));
|
|
73
|
+
if (!active || active.settled) return;
|
|
74
|
+
if (isSame(active.session.raw, raw)) return;
|
|
75
|
+
await active.ready;
|
|
76
|
+
if (this.activeConversations.get(active.key) !== active || active.settled) return;
|
|
77
|
+
const session = {
|
|
78
|
+
...active.session,
|
|
79
|
+
raw
|
|
80
|
+
};
|
|
81
|
+
let matched;
|
|
82
|
+
try {
|
|
83
|
+
matched = await active.router.dispatch(session, raw);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
this.reject(active, error);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
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
|
+
}
|
|
94
|
+
resolve(active, result) {
|
|
95
|
+
this.settle(active, () => active.resolve(result));
|
|
96
|
+
}
|
|
97
|
+
reject(active, error) {
|
|
98
|
+
this.settle(active, () => active.reject(error));
|
|
99
|
+
}
|
|
100
|
+
abort(active, reason) {
|
|
101
|
+
this.reject(active, new ConversationAbortionError(reason ?? "conversation aborted"));
|
|
102
|
+
}
|
|
103
|
+
settle(active, onSettled) {
|
|
104
|
+
if (active.settled) return;
|
|
105
|
+
active.settled = true;
|
|
106
|
+
if (active.timeoutId) {
|
|
107
|
+
clearTimeout(active.timeoutId);
|
|
108
|
+
active.timeoutId = void 0;
|
|
109
|
+
}
|
|
110
|
+
if (this.activeConversations.get(active.key) === active) this.activeConversations.delete(active.key);
|
|
111
|
+
onSettled();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/index.ts
|
|
116
|
+
const ConversationPlugin = definePlugin({
|
|
117
|
+
name: "conversation",
|
|
118
|
+
provides: [ConversationService],
|
|
119
|
+
apply(ctx, options) {
|
|
120
|
+
ctx.provide(ConversationService, new ConversationService(ctx, options));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
//#endregion
|
|
124
|
+
export { ConversationAbortionError, ConversationPlugin, ConversationRejectionError, ConversationService };
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fraqjs/plugin-conversation",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Conversation utility plugin for Fraq",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"main": "dist/index.mjs",
|
|
10
|
+
"typings": "dist/index.d.mts",
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "fraqjs",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/fraqjs/fraq.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://fraq.ntqqrev.org/",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@fraqjs/fraq": "^0.4.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@fraqjs/mock": "^0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"test": "tsx --test test/*.test.ts"
|
|
28
|
+
}
|
|
29
|
+
}
|