@rilong/grammyjs-conversations-esm 2.0.2
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/LICENSE +21 -0
- package/README.md +59 -0
- package/out/conversation.d.ts +885 -0
- package/out/conversation.js +832 -0
- package/out/deps.node.d.ts +2 -0
- package/out/deps.node.js +1 -0
- package/out/engine.d.ts +212 -0
- package/out/engine.js +238 -0
- package/out/form.d.ts +530 -0
- package/out/form.js +598 -0
- package/out/menu.d.ts +593 -0
- package/out/menu.js +698 -0
- package/out/mod.d.ts +8 -0
- package/out/mod.js +8 -0
- package/out/nope.d.ts +16 -0
- package/out/nope.js +35 -0
- package/out/plugin.d.ts +678 -0
- package/out/plugin.js +578 -0
- package/out/resolve.d.ts +43 -0
- package/out/resolve.js +21 -0
- package/out/state.d.ts +147 -0
- package/out/state.js +125 -0
- package/out/storage.d.ts +169 -0
- package/out/storage.js +105 -0
- package/package.json +43 -0
package/out/plugin.js
ADDED
@@ -0,0 +1,578 @@
|
|
1
|
+
import { Conversation } from "./conversation.js";
|
2
|
+
import { Api, Composer, Context, HttpError, } from "./deps.node.js";
|
3
|
+
import { ReplayEngine, } from "./engine.js";
|
4
|
+
import { youTouchYouDie } from "./nope.js";
|
5
|
+
import { uniformStorage } from "./storage.js";
|
6
|
+
const internalRecursionDetection = Symbol("conversations.recursion");
|
7
|
+
const internalState = Symbol("conversations.state");
|
8
|
+
const internalCompletenessMarker = Symbol("conversations.completeness");
|
9
|
+
function controls(getData, isParallel, enter, exit, canSave) {
|
10
|
+
async function fireExit(events) {
|
11
|
+
if (exit === undefined)
|
12
|
+
return;
|
13
|
+
const len = events.length;
|
14
|
+
for (let i = 0; i < len; i++) {
|
15
|
+
await exit(events[i]);
|
16
|
+
}
|
17
|
+
}
|
18
|
+
return {
|
19
|
+
async enter(name, ...args) {
|
20
|
+
var _a, _b;
|
21
|
+
if (!canSave()) {
|
22
|
+
throw new Error("The middleware has already completed so it is \
|
23
|
+
no longer possible to enter a conversation");
|
24
|
+
}
|
25
|
+
const data = getData();
|
26
|
+
if (Object.keys(data).length > 0 && !isParallel(name)) {
|
27
|
+
throw new Error(`A conversation was already entered and '${name}' \
|
28
|
+
is not a parallel conversation. Make sure to exit all active conversations \
|
29
|
+
before entering a new one, or specify { parallel: true } for '${name}' \
|
30
|
+
if you want it to run in parallel.`);
|
31
|
+
}
|
32
|
+
(_a = data[name]) !== null && _a !== void 0 ? _a : (data[name] = []);
|
33
|
+
const result = await enter(name, ...args);
|
34
|
+
if (!canSave()) {
|
35
|
+
throw new Error("The middleware has completed before conversation was fully \
|
36
|
+
entered so the conversations plugin cannot persist data anymore, did you forget \
|
37
|
+
to use `await`?");
|
38
|
+
}
|
39
|
+
switch (result.status) {
|
40
|
+
case "complete":
|
41
|
+
return;
|
42
|
+
case "error":
|
43
|
+
throw result.error;
|
44
|
+
case "handled":
|
45
|
+
case "skipped": {
|
46
|
+
const args = result.args === undefined
|
47
|
+
? {}
|
48
|
+
: { args: result.args };
|
49
|
+
const state = {
|
50
|
+
...args,
|
51
|
+
interrupts: result.interrupts,
|
52
|
+
replay: result.replay,
|
53
|
+
};
|
54
|
+
(_b = data[name]) === null || _b === void 0 ? void 0 : _b.push(state);
|
55
|
+
return;
|
56
|
+
}
|
57
|
+
}
|
58
|
+
},
|
59
|
+
async exitAll() {
|
60
|
+
if (!canSave()) {
|
61
|
+
throw new Error("The middleware has already completed so it is no longer possible to exit all conversations");
|
62
|
+
}
|
63
|
+
const data = getData();
|
64
|
+
const keys = Object.keys(data);
|
65
|
+
const events = keys.flatMap((key) => Array(data[key].length).fill(key));
|
66
|
+
keys.forEach((key) => delete data[key]);
|
67
|
+
await fireExit(events);
|
68
|
+
},
|
69
|
+
async exit(name) {
|
70
|
+
if (!canSave()) {
|
71
|
+
throw new Error(`The middleware has already completed so it is no longer possible to exit any conversations named '${name}'`);
|
72
|
+
}
|
73
|
+
const data = getData();
|
74
|
+
if (data[name] === undefined)
|
75
|
+
return;
|
76
|
+
const events = Array(data[name].length).fill(name);
|
77
|
+
delete data[name];
|
78
|
+
await fireExit(events);
|
79
|
+
},
|
80
|
+
async exitOne(name, index) {
|
81
|
+
if (!canSave()) {
|
82
|
+
throw new Error(`The middleware has already completed so it is no longer possible to exit the conversation '${name}'`);
|
83
|
+
}
|
84
|
+
const data = getData();
|
85
|
+
if (data[name] === undefined ||
|
86
|
+
index < 0 || data[name].length <= index)
|
87
|
+
return;
|
88
|
+
data[name].splice(index, 1);
|
89
|
+
await fireExit([name]);
|
90
|
+
},
|
91
|
+
// deno-lint-ignore no-explicit-any
|
92
|
+
active(name) {
|
93
|
+
var _a, _b;
|
94
|
+
const data = getData();
|
95
|
+
return name === undefined
|
96
|
+
? Object.fromEntries(Object.entries(data)
|
97
|
+
.map(([name, states]) => [name, states.length]))
|
98
|
+
: (_b = (_a = data[name]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
|
99
|
+
},
|
100
|
+
};
|
101
|
+
}
|
102
|
+
/**
|
103
|
+
* Middleware for the conversations plugin.
|
104
|
+
*
|
105
|
+
* This is the main thing you have to install in order to use this plugin. It
|
106
|
+
* performs various setup tasks for each context object, and it reads and writes
|
107
|
+
* to the data storage if provided. This middleware has to be installed before
|
108
|
+
* you can install `createConversation` with your conversation builder function.
|
109
|
+
*
|
110
|
+
* You can pass {@link ConversationOptions | an options object} to the plugin.
|
111
|
+
* The most important option is called `storage`. It can be used to persist
|
112
|
+
* conversations durably in any storage backend of your choice. That way, the
|
113
|
+
* conversations can survive restarts of your server.
|
114
|
+
*
|
115
|
+
* ```ts
|
116
|
+
* conversations({
|
117
|
+
* storage: {
|
118
|
+
* type: "key",
|
119
|
+
* version: 0, // change the version when you change your code
|
120
|
+
* adapter: new FileAdapter("/home/bot/data"),
|
121
|
+
* },
|
122
|
+
* });
|
123
|
+
* ```
|
124
|
+
*
|
125
|
+
* A list of known storage adapters can be found
|
126
|
+
* [here](https://github.com/grammyjs/storages/tree/main/packages#grammy-storages).
|
127
|
+
*
|
128
|
+
* It is advisable to version your data when you persist it. Every time you
|
129
|
+
* change your conversation function, you can increment the version. That way,
|
130
|
+
* the conversations plugin can make sure to avoid any data corruption caused by
|
131
|
+
* mismatches between state and implementation.
|
132
|
+
*
|
133
|
+
* Note that the plugin takes two different type parameters. The first type
|
134
|
+
* parameter should corresopnd with the context type of the outside middleware
|
135
|
+
* tree. The second type parameter should correspond with the custom context
|
136
|
+
* type used inside all conversations. If you may want to use different context
|
137
|
+
* types for different conversations, you can simply use `Context` here, and
|
138
|
+
* adjust the type for each conversation individually.
|
139
|
+
*
|
140
|
+
* Be sure to read [the documentation about the conversations
|
141
|
+
* plugin](https://grammy.dev/plugins/conversations) to learn more about how to
|
142
|
+
* use it.
|
143
|
+
*
|
144
|
+
* @param options Optional options for the conversations plugin
|
145
|
+
* @typeParam OC Custom context type of the outside middleware
|
146
|
+
* @typeParam C Custom context type used inside conversations
|
147
|
+
*/
|
148
|
+
export function conversations(options = {}) {
|
149
|
+
const createStorage = uniformStorage(options.storage);
|
150
|
+
return async (ctx, next) => {
|
151
|
+
var _a, _b;
|
152
|
+
if (internalRecursionDetection in ctx) {
|
153
|
+
throw new Error("Cannot install the conversations plugin on context objects created by the conversations plugin!");
|
154
|
+
}
|
155
|
+
if (internalState in ctx) {
|
156
|
+
throw new Error("Cannot install conversations plugin twice!");
|
157
|
+
}
|
158
|
+
const storage = createStorage(ctx);
|
159
|
+
let read = false;
|
160
|
+
const state = (_a = await storage.read()) !== null && _a !== void 0 ? _a : {};
|
161
|
+
const empty = Object.keys(state).length === 0;
|
162
|
+
function getData() {
|
163
|
+
read = true;
|
164
|
+
return state; // will be mutated by conversations
|
165
|
+
}
|
166
|
+
const index = new Map();
|
167
|
+
async function enter(id, ...args) {
|
168
|
+
var _a;
|
169
|
+
const entry = index.get(id);
|
170
|
+
if (entry === undefined) {
|
171
|
+
const known = Array.from(index.keys())
|
172
|
+
.map((id) => `'${id}'`)
|
173
|
+
.join(", ");
|
174
|
+
throw new Error(`The conversation '${id}' has not been registered! Known conversations are: ${known}`);
|
175
|
+
}
|
176
|
+
const { builder, plugins, maxMillisecondsToWait } = entry;
|
177
|
+
await ((_a = options.onEnter) === null || _a === void 0 ? void 0 : _a.call(options, id, ctx));
|
178
|
+
const base = {
|
179
|
+
update: ctx.update,
|
180
|
+
api: ctx.api,
|
181
|
+
me: ctx.me,
|
182
|
+
};
|
183
|
+
return await enterConversation(builder, base, {
|
184
|
+
args,
|
185
|
+
ctx,
|
186
|
+
plugins,
|
187
|
+
maxMillisecondsToWait,
|
188
|
+
});
|
189
|
+
}
|
190
|
+
const exit = options.onExit !== undefined
|
191
|
+
? async (name) => {
|
192
|
+
var _a;
|
193
|
+
await ((_a = options.onExit) === null || _a === void 0 ? void 0 : _a.call(options, name, ctx));
|
194
|
+
}
|
195
|
+
: undefined;
|
196
|
+
function isParallel(name) {
|
197
|
+
var _a, _b;
|
198
|
+
return (_b = (_a = index.get(name)) === null || _a === void 0 ? void 0 : _a.parallel) !== null && _b !== void 0 ? _b : true;
|
199
|
+
}
|
200
|
+
function canSave() {
|
201
|
+
return !(internalCompletenessMarker in ctx);
|
202
|
+
}
|
203
|
+
const internal = {
|
204
|
+
getMutableData: getData,
|
205
|
+
index,
|
206
|
+
defaultPlugins: (_b = options.plugins) !== null && _b !== void 0 ? _b : [],
|
207
|
+
exitHandler: exit,
|
208
|
+
};
|
209
|
+
Object.defineProperty(ctx, internalState, { value: internal });
|
210
|
+
ctx.conversation = controls(getData, isParallel, enter, exit, canSave);
|
211
|
+
try {
|
212
|
+
await next();
|
213
|
+
}
|
214
|
+
finally {
|
215
|
+
Object.defineProperty(ctx, internalCompletenessMarker, {
|
216
|
+
value: true,
|
217
|
+
});
|
218
|
+
if (read) {
|
219
|
+
// In case of bad usage of async/await, it is possible that
|
220
|
+
// `next` resolves while an enter call is still running. It then
|
221
|
+
// may not have cleaned up its data, leaving behind empty arrays
|
222
|
+
// on the state. Instead of delegating the cleanup
|
223
|
+
// responsibility to enter calls which are unable to do this
|
224
|
+
// reliably, we purge empty arrays ourselves before persisting
|
225
|
+
// the state. That way, we don't store useless data even when
|
226
|
+
// bot developers mess up.
|
227
|
+
const keys = Object.keys(state);
|
228
|
+
const len = keys.length;
|
229
|
+
let del = 0;
|
230
|
+
for (let i = 0; i < len; i++) {
|
231
|
+
const key = keys[i];
|
232
|
+
if (state[key].length === 0) {
|
233
|
+
delete state[key];
|
234
|
+
del++;
|
235
|
+
}
|
236
|
+
}
|
237
|
+
if (len !== del) { // len - del > 0
|
238
|
+
await storage.write(state);
|
239
|
+
}
|
240
|
+
else if (!empty) {
|
241
|
+
await storage.delete();
|
242
|
+
}
|
243
|
+
}
|
244
|
+
}
|
245
|
+
};
|
246
|
+
}
|
247
|
+
/**
|
248
|
+
* Takes a {@link ConversationBuilder | conversation builder function}, and
|
249
|
+
* turns it into middleware that can be installed on your bot. This middleware
|
250
|
+
* registers the conversation on the context object. Downstream handlers can
|
251
|
+
* then enter the conversation using `ctx.conversation.enter`.
|
252
|
+
*
|
253
|
+
* When an update reaches this middleware and the given conversation is
|
254
|
+
* currently active, then it will receive the update and process it. This
|
255
|
+
* advances the conversation.
|
256
|
+
*
|
257
|
+
* If the conversation is marked as parallel, downstream middleware will be
|
258
|
+
* called if this conversation decides to skip the update.
|
259
|
+
*
|
260
|
+
* You can pass a second parameter of type string to this function in order to
|
261
|
+
* give a different identifier to the conversation. By default, [the name of the
|
262
|
+
* function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name)
|
263
|
+
* is used.
|
264
|
+
*
|
265
|
+
* ```ts
|
266
|
+
* bot.use(createConversation(example, "new-name"))
|
267
|
+
* ```
|
268
|
+
*
|
269
|
+
* Optionally, instead of passing an identifier string as a second argument, you
|
270
|
+
* can pass an options object. It lets you configure the conversation. For example, this is how you can mark a conversation as parallel.
|
271
|
+
*
|
272
|
+
* ```ts
|
273
|
+
* bot.use(createConversation(example, {
|
274
|
+
* id: "new-name",
|
275
|
+
* parallel: true,
|
276
|
+
* }))
|
277
|
+
* ```
|
278
|
+
*
|
279
|
+
* Note that this function takes two different type parameters. The first type
|
280
|
+
* parameter should corresopnd with the context type of the outside middleware
|
281
|
+
* tree. The second type parameter should correspond with the custom context
|
282
|
+
* type used inside the given conversation. These two custom context types can
|
283
|
+
* never be identical because the outside middleware must have
|
284
|
+
* {@link ConversationFlavor} installed, but the custom context type used in the
|
285
|
+
* conversation must never have this type installed.
|
286
|
+
*
|
287
|
+
* @param builder A conversation builder function
|
288
|
+
* @param options A different name for the conversation, or an options object
|
289
|
+
* @typeParam OC Custom context type of the outside middleware
|
290
|
+
* @typeParam C Custom context type used inside this conversation
|
291
|
+
*/
|
292
|
+
export function createConversation(builder, options) {
|
293
|
+
const { id = builder.name, plugins = [], maxMillisecondsToWait = undefined, parallel = false, } = typeof options === "string" ? { id: options } : options !== null && options !== void 0 ? options : {};
|
294
|
+
if (!id) {
|
295
|
+
throw new Error("Cannot register a conversation without a name!");
|
296
|
+
}
|
297
|
+
return async (ctx, next) => {
|
298
|
+
if (!(internalState in ctx)) {
|
299
|
+
throw new Error("Cannot register a conversation without installing the conversations plugin first!");
|
300
|
+
}
|
301
|
+
const { index, defaultPlugins, getMutableData, exitHandler } = ctx[internalState];
|
302
|
+
if (index.has(id)) {
|
303
|
+
throw new Error(`Duplicate conversation identifier '${id}'!`);
|
304
|
+
}
|
305
|
+
const combinedPlugins = [...defaultPlugins, ...plugins];
|
306
|
+
index.set(id, {
|
307
|
+
builder,
|
308
|
+
plugins: combinedPlugins,
|
309
|
+
maxMillisecondsToWait,
|
310
|
+
parallel,
|
311
|
+
});
|
312
|
+
const onHalt = async () => {
|
313
|
+
await (exitHandler === null || exitHandler === void 0 ? void 0 : exitHandler(id));
|
314
|
+
};
|
315
|
+
const mutableData = getMutableData();
|
316
|
+
const base = {
|
317
|
+
update: ctx.update,
|
318
|
+
api: ctx.api,
|
319
|
+
me: ctx.me,
|
320
|
+
};
|
321
|
+
const options = {
|
322
|
+
ctx,
|
323
|
+
plugins: combinedPlugins,
|
324
|
+
onHalt,
|
325
|
+
maxMillisecondsToWait,
|
326
|
+
parallel,
|
327
|
+
};
|
328
|
+
const result = await runParallelConversations(builder, base, id, mutableData, // will be mutated on ctx
|
329
|
+
options);
|
330
|
+
switch (result.status) {
|
331
|
+
case "complete":
|
332
|
+
case "skipped":
|
333
|
+
if (result.next)
|
334
|
+
await next();
|
335
|
+
return;
|
336
|
+
case "error":
|
337
|
+
throw result.error;
|
338
|
+
case "handled":
|
339
|
+
return;
|
340
|
+
}
|
341
|
+
};
|
342
|
+
}
|
343
|
+
/**
|
344
|
+
* Takes a conversation builder function and some state and runs all parallel
|
345
|
+
* instances of it until a conversation result was produced.
|
346
|
+
*
|
347
|
+
* This is used internally to run a conversation, but bots typically don't have
|
348
|
+
* to call this method.
|
349
|
+
*
|
350
|
+
* @param builder A conversation builder function
|
351
|
+
* @param base Context base data containing the incoming update
|
352
|
+
* @param id The identifier of the conversation
|
353
|
+
* @param data The state of execution of all parallel conversations
|
354
|
+
* @param options Additional configuration options
|
355
|
+
* @typeParam OC Custom context type of the outside middleware
|
356
|
+
* @typeParam C Custom context type used inside this conversation
|
357
|
+
*/
|
358
|
+
export async function runParallelConversations(builder, base, id, data, options) {
|
359
|
+
if (!(id in data))
|
360
|
+
return { status: "skipped", next: true };
|
361
|
+
const states = data[id];
|
362
|
+
const len = states.length;
|
363
|
+
for (let i = 0; i < len; i++) {
|
364
|
+
const state = states[i];
|
365
|
+
const result = await resumeConversation(builder, base, state, options);
|
366
|
+
switch (result.status) {
|
367
|
+
case "skipped":
|
368
|
+
if (result.next)
|
369
|
+
continue;
|
370
|
+
else
|
371
|
+
return { status: "skipped", next: false };
|
372
|
+
case "handled":
|
373
|
+
states[i].replay = result.replay;
|
374
|
+
states[i].interrupts = result.interrupts;
|
375
|
+
return result;
|
376
|
+
case "complete":
|
377
|
+
states.splice(i, 1);
|
378
|
+
if (states.length === 0)
|
379
|
+
delete data[id];
|
380
|
+
if (result.next)
|
381
|
+
continue;
|
382
|
+
else
|
383
|
+
return result;
|
384
|
+
case "error":
|
385
|
+
states.splice(i, 1);
|
386
|
+
if (states.length === 0)
|
387
|
+
delete data[id];
|
388
|
+
return result;
|
389
|
+
}
|
390
|
+
}
|
391
|
+
return { status: "skipped", next: true };
|
392
|
+
}
|
393
|
+
/**
|
394
|
+
* Begins a new execution of a conversation builder function from scratch until
|
395
|
+
* a result was produced.
|
396
|
+
*
|
397
|
+
* This is used internally to enter a conversation, but bots typically don't have
|
398
|
+
* to call this method.
|
399
|
+
*
|
400
|
+
* @param conversation A conversation builder function
|
401
|
+
* @param base Context base data containing the incoming update
|
402
|
+
* @param options Additional configuration options
|
403
|
+
* @typeParam OC Custom context type of the outside middleware
|
404
|
+
* @typeParam C Custom context type used inside this conversation
|
405
|
+
*/
|
406
|
+
export async function enterConversation(conversation, base, options) {
|
407
|
+
const { args = [], ...opts } = options !== null && options !== void 0 ? options : {};
|
408
|
+
const [initialState, int] = ReplayEngine.open("wait");
|
409
|
+
const packedArgs = args.length === 0 ? {} : { args: JSON.stringify(args) };
|
410
|
+
const state = {
|
411
|
+
...packedArgs,
|
412
|
+
replay: initialState,
|
413
|
+
interrupts: [int],
|
414
|
+
};
|
415
|
+
const result = await resumeConversation(conversation, base, state, opts);
|
416
|
+
switch (result.status) {
|
417
|
+
case "complete":
|
418
|
+
case "error":
|
419
|
+
return result;
|
420
|
+
case "handled":
|
421
|
+
return { ...packedArgs, ...result };
|
422
|
+
case "skipped":
|
423
|
+
return {
|
424
|
+
...packedArgs,
|
425
|
+
replay: initialState,
|
426
|
+
interrupts: state.interrupts,
|
427
|
+
...result,
|
428
|
+
};
|
429
|
+
}
|
430
|
+
}
|
431
|
+
/**
|
432
|
+
* Resumes an execution of a conversation builder function until a result was
|
433
|
+
* produced.
|
434
|
+
*
|
435
|
+
* This is used internally to resume a conversation, but bots typically don't
|
436
|
+
* have to call this method.
|
437
|
+
*
|
438
|
+
* @param conversation A conversation builder function
|
439
|
+
* @param base Context base data containing the incoming update
|
440
|
+
* @param state Previous state of the conversation
|
441
|
+
* @param options Additional configuration options
|
442
|
+
* @typeParam OC Custom context type of the outside middleware
|
443
|
+
* @typeParam C Custom context type used inside this conversation
|
444
|
+
*/
|
445
|
+
export async function resumeConversation(conversation, base, state, options) {
|
446
|
+
const { update, api, me } = base;
|
447
|
+
const args = state.args === undefined ? [] : JSON.parse(state.args);
|
448
|
+
const { ctx = youTouchYouDie("The conversation was advanced from an event so there is no access to an outside context object"), plugins = [], onHalt, maxMillisecondsToWait, parallel, } = options !== null && options !== void 0 ? options : {};
|
449
|
+
const middleware = new Composer(...plugins).middleware();
|
450
|
+
// deno-lint-ignore no-explicit-any
|
451
|
+
const escape = (fn) => fn(ctx);
|
452
|
+
const engine = new ReplayEngine(async (controls) => {
|
453
|
+
const hydrate = hydrateContext(controls, api, me);
|
454
|
+
const convo = new Conversation(controls, hydrate, escape, middleware, {
|
455
|
+
onHalt,
|
456
|
+
maxMillisecondsToWait,
|
457
|
+
parallel,
|
458
|
+
});
|
459
|
+
const ctx = await convo.wait({ maxMilliseconds: undefined });
|
460
|
+
await conversation(convo, ctx, ...args);
|
461
|
+
});
|
462
|
+
const replayState = state.replay;
|
463
|
+
// The last execution may have completed with a number of interrupts
|
464
|
+
// (parallel wait calls, floating promises basically). We replay the
|
465
|
+
// conversation once for each of these interrupts until one of them does not
|
466
|
+
// skip the update (actually handles it in a meaningful way).
|
467
|
+
const ints = state.interrupts;
|
468
|
+
const len = ints.length;
|
469
|
+
let next = true;
|
470
|
+
INTERRUPTS: for (let i = 0; i < len; i++) {
|
471
|
+
const int = ints[i];
|
472
|
+
const checkpoint = ReplayEngine.supply(replayState, int, update);
|
473
|
+
let rewind;
|
474
|
+
do {
|
475
|
+
rewind = false;
|
476
|
+
const result = await engine.replay(replayState);
|
477
|
+
switch (result.type) {
|
478
|
+
case "returned":
|
479
|
+
// tell caller that we are done, all good
|
480
|
+
return { status: "complete", next: false };
|
481
|
+
case "thrown":
|
482
|
+
// tell caller that an error was thrown, it should leave the
|
483
|
+
// conversation and rethrow the error
|
484
|
+
return { status: "error", error: result.error };
|
485
|
+
case "interrupted":
|
486
|
+
// tell caller that we handled the update and updated the
|
487
|
+
// state accordingly
|
488
|
+
return {
|
489
|
+
status: "handled",
|
490
|
+
replay: result.state,
|
491
|
+
interrupts: result.interrupts,
|
492
|
+
};
|
493
|
+
// TODO: disable lint until the following issue is fixed:
|
494
|
+
// https://github.com/denoland/deno_lint/issues/1331
|
495
|
+
// deno-lint-ignore no-fallthrough
|
496
|
+
case "canceled":
|
497
|
+
// check the type of interrupt by inspecting its message
|
498
|
+
if (Array.isArray(result.message)) {
|
499
|
+
const c = result.message;
|
500
|
+
ReplayEngine.reset(replayState, c);
|
501
|
+
rewind = true;
|
502
|
+
break;
|
503
|
+
}
|
504
|
+
switch (result.message) {
|
505
|
+
case "skip":
|
506
|
+
// current interrupt was skipped, replay again with
|
507
|
+
// the next interrupt from the list
|
508
|
+
ReplayEngine.reset(replayState, checkpoint);
|
509
|
+
next = true;
|
510
|
+
continue INTERRUPTS;
|
511
|
+
case "drop":
|
512
|
+
// current interrupt was skipped, replay again with
|
513
|
+
// the next and if this was the last iteration of
|
514
|
+
// the loop, then tell the caller that downstream
|
515
|
+
// middleware must be called
|
516
|
+
ReplayEngine.reset(replayState, checkpoint);
|
517
|
+
next = false;
|
518
|
+
continue INTERRUPTS;
|
519
|
+
case "halt":
|
520
|
+
// tell caller that we are done, all good
|
521
|
+
return { status: "complete", next: false };
|
522
|
+
case "kill":
|
523
|
+
// tell the called that we are done and that
|
524
|
+
// downstream middleware must be called
|
525
|
+
return { status: "complete", next: true };
|
526
|
+
default:
|
527
|
+
throw new Error("invalid cancel message received"); // cannot happen
|
528
|
+
}
|
529
|
+
default:
|
530
|
+
// cannot happen
|
531
|
+
throw new Error("engine returned invalid replay result type");
|
532
|
+
}
|
533
|
+
} while (rewind);
|
534
|
+
}
|
535
|
+
// tell caller that we want to skip the update and did not modify the state
|
536
|
+
return { status: "skipped", next };
|
537
|
+
}
|
538
|
+
function hydrateContext(controls, protoApi, me) {
|
539
|
+
return (update) => {
|
540
|
+
const api = new Api(protoApi.token, protoApi.options);
|
541
|
+
api.config.use(async (prev, method, payload, signal) => {
|
542
|
+
// Prepare values before storing them
|
543
|
+
async function action() {
|
544
|
+
try {
|
545
|
+
const res = await prev(method, payload, signal);
|
546
|
+
return { ok: true, res }; // directly return successful responses
|
547
|
+
}
|
548
|
+
catch (e) {
|
549
|
+
if (e instanceof HttpError) { // dismantle HttpError instances
|
550
|
+
return {
|
551
|
+
ok: false,
|
552
|
+
err: {
|
553
|
+
message: e.message,
|
554
|
+
error: JSON.stringify(e.error),
|
555
|
+
},
|
556
|
+
};
|
557
|
+
}
|
558
|
+
else {
|
559
|
+
throw new Error(`Unknown error thrown in conversation while calling '${method}'`,
|
560
|
+
// @ts-ignore not available on old Node versions
|
561
|
+
{ cause: e });
|
562
|
+
}
|
563
|
+
}
|
564
|
+
}
|
565
|
+
const ret = await controls.action(action, method);
|
566
|
+
// Recover values after loading them
|
567
|
+
if (ret.ok) {
|
568
|
+
return ret.res;
|
569
|
+
}
|
570
|
+
else {
|
571
|
+
throw new HttpError("Error inside conversation: " + ret.err.message, new Error(JSON.parse(ret.err.error)));
|
572
|
+
}
|
573
|
+
});
|
574
|
+
const ctx = new Context(update, api, me);
|
575
|
+
Object.defineProperty(ctx, internalRecursionDetection, { value: true });
|
576
|
+
return ctx;
|
577
|
+
};
|
578
|
+
}
|
package/out/resolve.d.ts
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
/**
|
2
|
+
* A resolver wraps a promise so that it can be resolved by an outside event. It
|
3
|
+
* is a container for two things:
|
4
|
+
*
|
5
|
+
* - a promise which you can `await`
|
6
|
+
* - a function `resolve` which you can call
|
7
|
+
*
|
8
|
+
* Once you call `resolve`, the contained promise will resolve.
|
9
|
+
*
|
10
|
+
* The status flag `isResolved` indicates if `resolve` has been called or not.
|
11
|
+
*
|
12
|
+
* @typeParam T The type of value to which the promise resolves
|
13
|
+
*/
|
14
|
+
export interface Resolver<T> {
|
15
|
+
/** The promise which can be resolved by calling `resolve` */
|
16
|
+
promise: Promise<T>;
|
17
|
+
/** Value of the promise, if is it resolved, and undefined otherwise */
|
18
|
+
value?: T;
|
19
|
+
/**
|
20
|
+
* Resolves the promise of this resolver.
|
21
|
+
*
|
22
|
+
* Does nothing if called repeatedly.
|
23
|
+
*/
|
24
|
+
resolve(t?: T): void;
|
25
|
+
/**
|
26
|
+
* A flag indicating whether `resolve` has been called, i.e. whether the
|
27
|
+
* promise has been resolved. Returns `false` until `resolve` is called, and
|
28
|
+
* returns `true` afterwards.
|
29
|
+
*/
|
30
|
+
isResolved(): this is {
|
31
|
+
value: T;
|
32
|
+
};
|
33
|
+
}
|
34
|
+
/**
|
35
|
+
* Creates a new resolver.
|
36
|
+
*
|
37
|
+
* Optionally accepts a value to which the promise will resolve when `resolve`
|
38
|
+
* is called without arguments. Note that the value will be discarded if
|
39
|
+
* `resolve` is called with an argument.
|
40
|
+
*
|
41
|
+
* @param value An optional default to resolve to when calling `resolve`
|
42
|
+
*/
|
43
|
+
export declare function resolver<T>(value?: T): Resolver<T>;
|
package/out/resolve.js
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
/**
|
2
|
+
* Creates a new resolver.
|
3
|
+
*
|
4
|
+
* Optionally accepts a value to which the promise will resolve when `resolve`
|
5
|
+
* is called without arguments. Note that the value will be discarded if
|
6
|
+
* `resolve` is called with an argument.
|
7
|
+
*
|
8
|
+
* @param value An optional default to resolve to when calling `resolve`
|
9
|
+
*/
|
10
|
+
export function resolver(value) {
|
11
|
+
const rsr = { value, isResolved: () => false };
|
12
|
+
rsr.promise = new Promise((resolve) => {
|
13
|
+
rsr.resolve = (t = value) => {
|
14
|
+
rsr.isResolved = () => true;
|
15
|
+
rsr.value = t;
|
16
|
+
resolve(t); // cast to handle void
|
17
|
+
rsr.resolve = () => { }; // double resolve is no-op
|
18
|
+
};
|
19
|
+
});
|
20
|
+
return rsr;
|
21
|
+
}
|