@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.
@@ -0,0 +1,2 @@
1
+ export { Api, type ApiClientOptions, type CallbackQueryContext, type CommandContext, Composer, Context, type Filter, type FilterQuery, type GameQueryContext, GrammyError, type HearsContext, HttpError, type Middleware, type MiddlewareFn, type ReactionContext, } from "grammy";
2
+ export type { Animation, ApiError, Audio, Contact, CopyTextButton, Dice, Document, File, Game, InlineKeyboardButton, InlineKeyboardMarkup, Location, LoginUrl, MessageEntity, PaidMediaInfo, PhotoSize, Poll, ReactionType, ReactionTypeEmoji, Sticker, Story, SwitchInlineQueryChosenChat, Update, User, UserFromGetMe, Venue, Video, VideoNote, Voice, } from "grammy/types";
@@ -0,0 +1 @@
1
+ export { Api, Composer, Context, GrammyError, HttpError, } from "grammy";
@@ -0,0 +1,212 @@
1
+ import { type Checkpoint, type ReplayState } from "./state.js";
2
+ export { type Checkpoint, type ReplayState } from "./state.js";
3
+ /**
4
+ * Controls for a replay. This is the object that a {@link ReplayEngine} passes
5
+ * to the replay function when executing it.
6
+ */
7
+ export interface ReplayControls {
8
+ /**
9
+ * Interrupts the current replay and record this event in the replay logs.
10
+ * The replay will finish with an {@link Interrupted} result.
11
+ *
12
+ * Use {@link ReplayEngine.supply} to supply the result of this interrupt to
13
+ * the underlying replay state. When replaying the modified state, this call
14
+ * to `interrupt` will resolve with the supplied value.
15
+ *
16
+ * You also need to pass a key that identifies this type of interrupt. It is
17
+ * stored in the replay state and will be collated with the key that is
18
+ * passed to `interrupt` during the repeated call. If the two keys do not
19
+ * match, this means that a bad replay was detected and an error will be
20
+ * thrown. You should discard the replay state and restart the replay from
21
+ * scratch.
22
+ *
23
+ * @param key A key to collate interrupts across replays
24
+ */
25
+ interrupt(key: string): Promise<unknown>;
26
+ /**
27
+ * Cancels the replay. This tells the replay engine that a supplied
28
+ * interrupt value should be rejected. The replay will finish with a
29
+ * {@link Canceled} result.
30
+ *
31
+ * A message object can be passed to `cancel`. This can be used to
32
+ * communicate to the caller why the interrupt value was rejected.
33
+ *
34
+ * @param message A message specifiying the reason for the cancelation
35
+ */
36
+ cancel(message?: unknown): Promise<never>;
37
+ /**
38
+ * Performs an action.
39
+ *
40
+ * Actions are a way to signal to the replay engine that a particular piece
41
+ * of code should not be run repeatedly. The result of the action will be
42
+ * stored in the underlying replay log. During a subsequent replay, the
43
+ * action will not be repeated. Instead, the return is taken from the replay
44
+ * log.
45
+ *
46
+ * You also need to pass a key that identifies this type of action. It is
47
+ * stored in the replay state and will be collated with the key that is
48
+ * passed to `action` during the repeated call. If the two keys do not
49
+ * match, this means that a bad replay was detected and an error will be
50
+ * thrown. You should discard the replay state and restart the replay from
51
+ * scratch.
52
+ *
53
+ * @param fn The action to perform
54
+ * @param key A key to collate actions across replays
55
+ */
56
+ action<R = unknown>(fn: () => R | Promise<R>, key: string): Promise<R>;
57
+ /**
58
+ * Creates a checkpoint at the current position of the replay. This can be
59
+ * passed to {@link ReplayEngine.reset} in order to restart a replay from an
60
+ * arbitrary position.
61
+ */
62
+ checkpoint(): Checkpoint;
63
+ }
64
+ /** A function to be replayed by a {@link ReplayEngine} */
65
+ export type Builder = (controls: ReplayControls) => void | Promise<void>;
66
+ /** The result of a replay performed by a {@link ReplayEngine} */
67
+ export type ReplayResult = Returned | Thrown | Interrupted | Canceled;
68
+ /**
69
+ * This result is returned by a {@link ReplayEngine} when the builder function
70
+ * completes normally by returning.
71
+ */
72
+ export interface Returned {
73
+ /**
74
+ * Type of the replay result, indicates that the replay has completed
75
+ * normally because the builder function has returned.
76
+ */
77
+ type: "returned";
78
+ /** The return value of the builder function */
79
+ returnValue: unknown;
80
+ }
81
+ /**
82
+ * This result is returned by a {@link ReplayEngine} when the builder function
83
+ * throws an error.
84
+ */
85
+ export interface Thrown {
86
+ /**
87
+ * Type of the replay result, indicates that the replay has completed
88
+ * because the builder function has thrown an error.
89
+ */
90
+ type: "thrown";
91
+ /** The error thrown by the builder function */
92
+ error: unknown;
93
+ }
94
+ /**
95
+ * This result is returned by a {@link ReplayEngine} when the builder function
96
+ * interrupts itself by calling {@link ReplayControls.interrupt}.
97
+ */
98
+ export interface Interrupted {
99
+ /**
100
+ * Type of the replay result, indicates that the replay has completed
101
+ * because the builder function has interrupted itself.
102
+ */
103
+ type: "interrupted";
104
+ /** The replay state left behind by the replay engine */
105
+ state: ReplayState;
106
+ /** The list of concurrent interrupts that were performed */
107
+ interrupts: number[];
108
+ }
109
+ /**
110
+ * This result is returned by a {@link ReplayEngine} when the builder function
111
+ * cancels itself by calling {@link ReplayControls.cancel}.
112
+ */
113
+ export interface Canceled {
114
+ /**
115
+ * Type of the replay result, indicates that the replay has completed
116
+ * because the builder function has canceled itself.
117
+ */
118
+ type: "canceled";
119
+ /** The message passed to the last concurrent cancel call */
120
+ message?: unknown;
121
+ }
122
+ /**
123
+ * A replay engine takes control of the event loop of the JavaScript runtime and
124
+ * lets you execute a JavaScript function in abnormal ways. The function
125
+ * execution can be halted, resumed, aborted, and reversed. This lets you run a
126
+ * function partially and persist the state of execution in a database. Later,
127
+ * function execution can be resumed from where it was left off.
128
+ *
129
+ * Replay engines are the fundamental building block of the conversations
130
+ * plugin. In a sense, everything else is just a number of wrapper layers to
131
+ * make working with replay engines more convenient, and to integrate the power
132
+ * of replay engines into your bot's middleware system.
133
+ *
134
+ * Using a standalone replay engine is straightforward.
135
+ *
136
+ * 1. Create an instance of this class and pass a normal JavaScript function to
137
+ * the constructor. The function receives a {@link ReplayControls} object as
138
+ * its only parameter.
139
+ * 2. Call {@link ReplayEngine.play} to begin a new execution. It returns a
140
+ * {@link ReplayResult} object.
141
+ * 3. Use the {@link ReplayState} you obtained inside the result object and
142
+ * resume execution by calling {@link ReplayEngine.replay}.
143
+ *
144
+ * The `ReplayEngine` class furthermore provides you with static helper methods
145
+ * to supply values to interrupts, and to reset the replay state to a previously
146
+ * created checkpoint.
147
+ */
148
+ export declare class ReplayEngine {
149
+ private readonly builder;
150
+ /**
151
+ * Constructs a new replay engine from a builder function. The function
152
+ * receives a single parameter that can be used to control the replay.
153
+ *
154
+ * @param builder A builder function to be executed and replayed
155
+ */
156
+ constructor(builder: Builder);
157
+ /**
158
+ * Begins a new execution of the builder function. This starts based on
159
+ * fresh state. The execution is independent from any previously created
160
+ * executions.
161
+ *
162
+ * A {@link ReplayResult} object is returned to communicate the outcome of
163
+ * the execution.
164
+ */
165
+ play(): Promise<ReplayResult>;
166
+ /**
167
+ * Resumes execution based on a previously created replay state. This is the
168
+ * most important method of this class.
169
+ *
170
+ * A {@link ReplayResult} object is returned to communicate the outcome of
171
+ * the execution.
172
+ *
173
+ * @param state A previously created replay state
174
+ */
175
+ replay(state: ReplayState): Promise<ReplayResult>;
176
+ /**
177
+ * Creates a new replay state with a single unresolved interrupt. This state
178
+ * can be used as a starting point to replay arbitrary builder functions.
179
+ *
180
+ * You need to pass the collation key for the aforementioned first
181
+ * interrupt. This must be the same value that the builder function will
182
+ * pass to its first interrupt.
183
+ *
184
+ * @param key The builder functions first collation key
185
+ */
186
+ static open(key: string): readonly [ReplayState, number];
187
+ /**
188
+ * Mutates a given replay state by supplying a value for a given interrupt.
189
+ * The next time the state is replayed, the targeted interrupt will return
190
+ * this value.
191
+ *
192
+ * The interrupt value has to be one of the interrupts of a previously
193
+ * received {@link Interrupted} result.
194
+ *
195
+ * In addition to mutating the replay state, a checkpoint is created and
196
+ * returned. This checkpoint may be used to reset the replay state to its
197
+ * previous value. This will undo this and all following mutations.
198
+ *
199
+ * @param state A replay state to mutate
200
+ * @param interrupt An interrupt to resolve
201
+ * @param value The value to supply
202
+ */
203
+ static supply(state: ReplayState, interrupt: number, value: unknown): Checkpoint;
204
+ /**
205
+ * Resets a given replay state to a previously received checkpoint by
206
+ * mutating the replay state.
207
+ *
208
+ * @param state The state to mutate
209
+ * @param checkpoint The checkpoint to which to return
210
+ */
211
+ static reset(state: ReplayState, checkpoint: Checkpoint): void;
212
+ }
package/out/engine.js ADDED
@@ -0,0 +1,238 @@
1
+ import { resolver } from "./resolve.js";
2
+ import { create, cursor, inspect, mutate, } from "./state.js";
3
+ /**
4
+ * A replay engine takes control of the event loop of the JavaScript runtime and
5
+ * lets you execute a JavaScript function in abnormal ways. The function
6
+ * execution can be halted, resumed, aborted, and reversed. This lets you run a
7
+ * function partially and persist the state of execution in a database. Later,
8
+ * function execution can be resumed from where it was left off.
9
+ *
10
+ * Replay engines are the fundamental building block of the conversations
11
+ * plugin. In a sense, everything else is just a number of wrapper layers to
12
+ * make working with replay engines more convenient, and to integrate the power
13
+ * of replay engines into your bot's middleware system.
14
+ *
15
+ * Using a standalone replay engine is straightforward.
16
+ *
17
+ * 1. Create an instance of this class and pass a normal JavaScript function to
18
+ * the constructor. The function receives a {@link ReplayControls} object as
19
+ * its only parameter.
20
+ * 2. Call {@link ReplayEngine.play} to begin a new execution. It returns a
21
+ * {@link ReplayResult} object.
22
+ * 3. Use the {@link ReplayState} you obtained inside the result object and
23
+ * resume execution by calling {@link ReplayEngine.replay}.
24
+ *
25
+ * The `ReplayEngine` class furthermore provides you with static helper methods
26
+ * to supply values to interrupts, and to reset the replay state to a previously
27
+ * created checkpoint.
28
+ */
29
+ export class ReplayEngine {
30
+ /**
31
+ * Constructs a new replay engine from a builder function. The function
32
+ * receives a single parameter that can be used to control the replay.
33
+ *
34
+ * @param builder A builder function to be executed and replayed
35
+ */
36
+ constructor(builder) {
37
+ this.builder = builder;
38
+ }
39
+ /**
40
+ * Begins a new execution of the builder function. This starts based on
41
+ * fresh state. The execution is independent from any previously created
42
+ * executions.
43
+ *
44
+ * A {@link ReplayResult} object is returned to communicate the outcome of
45
+ * the execution.
46
+ */
47
+ async play() {
48
+ const state = create();
49
+ return await this.replay(state);
50
+ }
51
+ /**
52
+ * Resumes execution based on a previously created replay state. This is the
53
+ * most important method of this class.
54
+ *
55
+ * A {@link ReplayResult} object is returned to communicate the outcome of
56
+ * the execution.
57
+ *
58
+ * @param state A previously created replay state
59
+ */
60
+ async replay(state) {
61
+ return await replayState(this.builder, state);
62
+ }
63
+ /**
64
+ * Creates a new replay state with a single unresolved interrupt. This state
65
+ * can be used as a starting point to replay arbitrary builder functions.
66
+ *
67
+ * You need to pass the collation key for the aforementioned first
68
+ * interrupt. This must be the same value that the builder function will
69
+ * pass to its first interrupt.
70
+ *
71
+ * @param key The builder functions first collation key
72
+ */
73
+ static open(key) {
74
+ const state = create();
75
+ const mut = mutate(state);
76
+ const int = mut.op(key);
77
+ return [state, int];
78
+ }
79
+ /**
80
+ * Mutates a given replay state by supplying a value for a given interrupt.
81
+ * The next time the state is replayed, the targeted interrupt will return
82
+ * this value.
83
+ *
84
+ * The interrupt value has to be one of the interrupts of a previously
85
+ * received {@link Interrupted} result.
86
+ *
87
+ * In addition to mutating the replay state, a checkpoint is created and
88
+ * returned. This checkpoint may be used to reset the replay state to its
89
+ * previous value. This will undo this and all following mutations.
90
+ *
91
+ * @param state A replay state to mutate
92
+ * @param interrupt An interrupt to resolve
93
+ * @param value The value to supply
94
+ */
95
+ static supply(state, interrupt, value) {
96
+ const get = inspect(state);
97
+ const checkpoint = get.checkpoint();
98
+ const mut = mutate(state);
99
+ mut.done(interrupt, value);
100
+ return checkpoint;
101
+ }
102
+ /**
103
+ * Resets a given replay state to a previously received checkpoint by
104
+ * mutating the replay state.
105
+ *
106
+ * @param state The state to mutate
107
+ * @param checkpoint The checkpoint to which to return
108
+ */
109
+ static reset(state, checkpoint) {
110
+ const mut = mutate(state);
111
+ mut.reset(checkpoint);
112
+ }
113
+ }
114
+ async function replayState(builder, state) {
115
+ const cur = cursor(state);
116
+ // Set up interrupt and action tracking
117
+ let interrupted = false;
118
+ const interrupts = [];
119
+ let boundary = resolver();
120
+ const actions = new Set();
121
+ function updateBoundary() {
122
+ if (interrupted && actions.size === 0) {
123
+ boundary.resolve();
124
+ }
125
+ }
126
+ async function runBoundary() {
127
+ while (!boundary.isResolved()) {
128
+ await boundary.promise;
129
+ // clear microtask queue and check if another action was started
130
+ await new Promise((r) => setTimeout(r, 0));
131
+ }
132
+ }
133
+ // Set up event loop tracking to prevent
134
+ // premature returns with floating promises
135
+ let promises = 0; // counts the number of promises on the event loop
136
+ let dirty = resolver(); // resolves as soon as the event loop is clear
137
+ let complete = false; // locks the engine after the event loop has cleared
138
+ function begin() {
139
+ if (complete) {
140
+ throw new Error("Cannot begin another operation after the conversation has completed, are you missing an `await`?");
141
+ }
142
+ promises++;
143
+ if (boundary.isResolved()) {
144
+ // new action was started after interrupt, reset boundary
145
+ boundary = resolver();
146
+ }
147
+ }
148
+ function end() {
149
+ promises--;
150
+ if (promises === 0) {
151
+ dirty.resolve();
152
+ dirty = resolver();
153
+ }
154
+ }
155
+ // Collect data to return to caller
156
+ let canceled = false;
157
+ let message = undefined;
158
+ let returned = false;
159
+ let returnValue = undefined;
160
+ // Define replay controls
161
+ async function interrupt(key) {
162
+ if (returned || (interrupted && interrupts.length === 0)) {
163
+ // Already returned or canceled, so we must no longer perform an interrupt.
164
+ await boom();
165
+ }
166
+ begin();
167
+ const res = await cur.perform(async (op) => {
168
+ interrupted = true;
169
+ interrupts.push(op);
170
+ updateBoundary();
171
+ await boom();
172
+ }, key);
173
+ end();
174
+ return res;
175
+ }
176
+ async function cancel(key) {
177
+ canceled = true;
178
+ interrupted = true;
179
+ message = key;
180
+ updateBoundary();
181
+ return await boom();
182
+ }
183
+ async function action(fn, key) {
184
+ begin();
185
+ const res = await cur.perform(async (op) => {
186
+ actions.add(op);
187
+ const ret = await fn();
188
+ actions.delete(op);
189
+ updateBoundary();
190
+ return ret;
191
+ }, key);
192
+ end();
193
+ return res;
194
+ }
195
+ function checkpoint() {
196
+ return cur.checkpoint();
197
+ }
198
+ const controls = { interrupt, cancel, action, checkpoint };
199
+ // Perform replay
200
+ async function run() {
201
+ returnValue = await builder(controls);
202
+ returned = true;
203
+ // wait for pending ops to complete
204
+ while (promises > 0) {
205
+ await dirty.promise;
206
+ // clear microtask queue and check again
207
+ await new Promise((r) => setTimeout(r, 0));
208
+ }
209
+ }
210
+ try {
211
+ const boundaryPromise = runBoundary();
212
+ const runPromise = run();
213
+ await Promise.race([boundaryPromise, runPromise]);
214
+ if (returned) {
215
+ return { type: "returned", returnValue };
216
+ }
217
+ else if (boundary.isResolved()) {
218
+ if (canceled) {
219
+ return { type: "canceled", message };
220
+ }
221
+ else {
222
+ return { type: "interrupted", state, interrupts };
223
+ }
224
+ }
225
+ else {
226
+ throw new Error("Neither returned nor interrupted!"); // should never happen
227
+ }
228
+ }
229
+ catch (error) {
230
+ return { type: "thrown", error };
231
+ }
232
+ finally {
233
+ complete = true;
234
+ }
235
+ }
236
+ function boom() {
237
+ return new Promise(() => { });
238
+ }