@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 CHANGED
@@ -1,10 +1,13 @@
1
- import { Context, Router, Session, milky } from "@fraqjs/fraq";
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
- open<R>(session: Session, handler: (conversationContext: ConversationContext<R>) => void | Promise<void>, options?: ConversationOptions): Promise<R | null>;
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: ConversationServiceOptions], import("@fraqjs/fraq").Injection | undefined, import("@fraqjs/fraq").Injection | undefined>;
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
- async open(session, handler, options) {
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) return;
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
- matched = await active.router.dispatch(session, raw);
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.1.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.4.0"
20
+ "@fraqjs/fraq": "^0.5.1"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@fraqjs/mock": "^0.1.0"