@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/out/state.d.ts ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * A replay state.
3
+ *
4
+ * A replay state consists of two logs of operations.
5
+ *
6
+ * 1. A send log which records send operations in the shape of {@link SendOp}
7
+ * 2. A receive log which records receive operations in the shape of
8
+ * {@link ReceiveOp}
9
+ *
10
+ * Note that each receive op links to a specific send op. A valid replay state
11
+ * should only contain receive ops that point to send ops contained in the same
12
+ * replay state.
13
+ *
14
+ * A replay state can be created using {@link create}.
15
+ */
16
+ export interface ReplayState {
17
+ /** The send log of the replay state */
18
+ send: SendOp[];
19
+ /** The receive log of the replay state */
20
+ receive: ReceiveOp[];
21
+ }
22
+ /** A send operation */
23
+ export interface SendOp {
24
+ /** Any string payload for the send operation */
25
+ payload: string;
26
+ }
27
+ /** A receive operation */
28
+ export interface ReceiveOp {
29
+ /** The identifier (index in a send log) of a send op */
30
+ send: number;
31
+ /** The received value */
32
+ returnValue: unknown;
33
+ }
34
+ /** A checkpoint in a replay log */
35
+ export type Checkpoint = [number, number];
36
+ /**
37
+ * Creates and returns an empty {@link ReplayState} object.
38
+ *
39
+ * The returned replay state can be inspected via {@link inspect}, mutated via
40
+ * {@link mutate}, and replayed via {@link cursor}.
41
+ */
42
+ export declare function create(): ReplayState;
43
+ /**
44
+ * Holds a number of tools that can be used to inspect a replay state.
45
+ *
46
+ * This object is typically created via {@link inspect}.
47
+ */
48
+ export interface InspectTools {
49
+ /** Gets the number of send ops */
50
+ opCount(): number;
51
+ /** Gets the number of receive ops */
52
+ doneCount(): number;
53
+ /** Looks up the payload of a send op */
54
+ payload(op: number): string;
55
+ /** Creates a checkpoint for the current replay state */
56
+ checkpoint(): Checkpoint;
57
+ }
58
+ /**
59
+ * Provides inspections tools for a given replay state.
60
+ *
61
+ * @param state The replay state to inspect
62
+ */
63
+ export declare function inspect(state: ReplayState): InspectTools;
64
+ /**
65
+ * Holds a number of tools that can be used to mutate a replay state.
66
+ *
67
+ * This object is typically created via {@link mutate}.
68
+ */
69
+ export interface MutateTools {
70
+ /**
71
+ * Begins an op by recording a send op. Returns the send op identifier.
72
+ *
73
+ * @param payload A payload to send
74
+ */
75
+ op(payload: string): number;
76
+ /**
77
+ * Completes an op by recording a receive op for a given send op.
78
+ *
79
+ * @param op The identifier of the send op to complete.
80
+ * @param result The result of the op
81
+ */
82
+ done(op: number, result: unknown): void;
83
+ /**
84
+ * Resets the replay state to a given checkpoint that was obtained
85
+ * previously through {@link inspect}ion of the replay state.
86
+ *
87
+ * @param checkpoint The known checkpoint
88
+ */
89
+ reset([op, done]: Checkpoint): void;
90
+ }
91
+ /**
92
+ * Provides tools to mutate a given replay state.
93
+ *
94
+ * @param state The replay state to mutate
95
+ */
96
+ export declare function mutate(state: ReplayState): MutateTools;
97
+ /**
98
+ * Can be used to iterate a given replay state.
99
+ *
100
+ * This object is typically created via {@link cursor}.
101
+ *
102
+ * Note that this object holds state outside of the replay state itself, namely
103
+ * the current position of the cursor.
104
+ */
105
+ export interface ReplayCursor {
106
+ /**
107
+ * Performs an action at the current position of the replay cursor, records
108
+ * its result in the replay state, and advances the cursor.
109
+ *
110
+ * Note that if the cursor has not reached the end of the replay state yet,
111
+ * the action will be replayed from the log.
112
+ *
113
+ * @param action The action to perform, receiving a send op identifer
114
+ * @param payload The payload to assign to this action
115
+ */
116
+ perform(action: (op: number) => unknown | Promise<unknown>, payload: string): Promise<unknown>;
117
+ /**
118
+ * Begins a new op at the current position of the replay cursor, and
119
+ * advances the cursor.
120
+ *
121
+ * Note that if the cursor has not reached the end of the replay state yet,
122
+ * the op will be taken from the log.
123
+ *
124
+ * @param payload The payload to assign to this op
125
+ */
126
+ op(payload: string): number;
127
+ /**
128
+ * Completes a given op with the result obtained from a callback function,
129
+ * and advances the cursor.
130
+ *
131
+ * Note that if the cursor has not reached the end of the replay state yet,
132
+ * the callback function will not be invoked. Instead, the result will be
133
+ * replayed from the log.
134
+ *
135
+ * @param op The op to complete
136
+ * @param result The result to record
137
+ */
138
+ done(op: number, result: () => unknown | Promise<unknown>): Promise<unknown>;
139
+ /** Creates a checkpoint at the current state of the cursor */
140
+ checkpoint(): Checkpoint;
141
+ }
142
+ /**
143
+ * Provides tools to iterate a given replay state.
144
+ *
145
+ * @param state The replay state to iterate
146
+ */
147
+ export declare function cursor(state: ReplayState): ReplayCursor;
package/out/state.js ADDED
@@ -0,0 +1,125 @@
1
+ import { resolver } from "./resolve.js";
2
+ /**
3
+ * Creates and returns an empty {@link ReplayState} object.
4
+ *
5
+ * The returned replay state can be inspected via {@link inspect}, mutated via
6
+ * {@link mutate}, and replayed via {@link cursor}.
7
+ */
8
+ export function create() {
9
+ return { send: [], receive: [] };
10
+ }
11
+ /**
12
+ * Provides inspections tools for a given replay state.
13
+ *
14
+ * @param state The replay state to inspect
15
+ */
16
+ export function inspect(state) {
17
+ function opCount() {
18
+ return state.send.length;
19
+ }
20
+ function doneCount() {
21
+ return state.receive.length;
22
+ }
23
+ function payload(op) {
24
+ if (op < 0)
25
+ throw new Error(`Op ${op} is invalid`);
26
+ if (op >= state.send.length)
27
+ throw new Error(`No op ${op} in state`);
28
+ return state.send[op].payload;
29
+ }
30
+ function checkpoint() {
31
+ return [opCount(), doneCount()];
32
+ }
33
+ return { opCount, doneCount, payload, checkpoint };
34
+ }
35
+ /**
36
+ * Provides tools to mutate a given replay state.
37
+ *
38
+ * @param state The replay state to mutate
39
+ */
40
+ export function mutate(state) {
41
+ function op(payload) {
42
+ const index = state.send.length;
43
+ state.send.push({ payload });
44
+ return index;
45
+ }
46
+ function done(op, result) {
47
+ if (op < 0)
48
+ throw new Error(`Op ${op} is invalid`);
49
+ if (op >= state.send.length)
50
+ throw new Error(`No op ${op} in state`);
51
+ state.receive.push({ send: op, returnValue: result });
52
+ }
53
+ function reset([op, done]) {
54
+ if (op < 0 || done < 0)
55
+ throw new Error("Invalid checkpoint");
56
+ state.send.splice(op);
57
+ state.receive.splice(done);
58
+ }
59
+ return { op, done, reset };
60
+ }
61
+ /**
62
+ * Provides tools to iterate a given replay state.
63
+ *
64
+ * @param state The replay state to iterate
65
+ */
66
+ export function cursor(state) {
67
+ let changes = resolver();
68
+ function notify() {
69
+ changes.resolve();
70
+ changes = resolver();
71
+ }
72
+ let send = 0; // 0 <= send <= state.send.length
73
+ let receive = 0; // 0 <= receive <= state.receive.length
74
+ function op(payload) {
75
+ if (send < state.send.length) {
76
+ // replay existing data (do nothing)
77
+ const expected = state.send[send].payload;
78
+ if (expected !== payload) {
79
+ throw new Error(`Bad replay, expected op '${expected}'`);
80
+ }
81
+ }
82
+ else { // send === state.send.length
83
+ // log new data
84
+ state.send.push({ payload });
85
+ }
86
+ const index = send++;
87
+ notify();
88
+ return index;
89
+ }
90
+ async function done(op, result) {
91
+ if (op < 0)
92
+ throw new Error(`Op ${op} is invalid`);
93
+ if (op >= state.send.length)
94
+ throw new Error(`No op ${op} in state`);
95
+ let data;
96
+ if (receive < state.receive.length) {
97
+ // replay existing data (do nothing)
98
+ while (state.receive[receive].send !== op) {
99
+ // make sure we resolve only when it is our turn
100
+ await changes.promise;
101
+ if (receive === state.receive.length) {
102
+ // It will never be our turn, because the replay completed
103
+ // and we are still here. We will have to call `result`.
104
+ return await done(op, result);
105
+ }
106
+ } // state.receive[receive].send === op
107
+ data = state.receive[receive].returnValue;
108
+ }
109
+ else { // receive === state.receive.length
110
+ data = await result();
111
+ state.receive.push({ send: op, returnValue: data });
112
+ }
113
+ receive++;
114
+ notify();
115
+ return data;
116
+ }
117
+ async function perform(action, payload) {
118
+ const index = op(payload);
119
+ return await done(index, () => action(index));
120
+ }
121
+ function checkpoint() {
122
+ return [send, receive];
123
+ }
124
+ return { perform, op, done, checkpoint };
125
+ }
@@ -0,0 +1,169 @@
1
+ import type { Context } from "./deps.node.js";
2
+ /** Current data version of this plugin */
3
+ export declare const PLUGIN_DATA_VERSION = 0;
4
+ /**
5
+ * A value with a version.
6
+ *
7
+ * The version consists of two pieces.
8
+ *
9
+ * The first piece is a number that is defined by the plugin internally and
10
+ * cannot be changed. When the plugin is updated and it changes its internal
11
+ * data format, then it can use this part of the version to detect and
12
+ * automatically migrate the versioned state as necessary.
13
+ *
14
+ * The second piece is a number or a string and can be set by the developer. It
15
+ * should be changed whenever the application code changes in a way that
16
+ * invalidates the state. The plugin can then discard and re-create the state as
17
+ * necesarry.
18
+ *
19
+ * Versioned states are typically created via the {@link pinVersion} function.
20
+ *
21
+ * @typeParam S The type of the state to be versioned
22
+ */
23
+ export interface VersionedState<S> {
24
+ /** The version of the state */
25
+ version: [typeof PLUGIN_DATA_VERSION, string | number];
26
+ /** The state to be versioned */
27
+ state: S;
28
+ }
29
+ /**
30
+ * A container for two functions that are pinned to a specific version. The two
31
+ * functions can be used to add the bound version to data, and to unpack the
32
+ * data again. This container is typically created using {@link pinVersion}.
33
+ */
34
+ export interface PinnedVersion {
35
+ /**
36
+ * Adds a version to some data.
37
+ *
38
+ * @param state Some data
39
+ */
40
+ versionify<S>(state: S): VersionedState<S>;
41
+ /**
42
+ * Unpacks some versioned data. Returns the original data if the data is
43
+ * correct, and `undefined` otherwise. If `undefined` is passed, then
44
+ * `undefined` will be returned.
45
+ *
46
+ * @param data Some versioned data or `undefined`
47
+ */
48
+ unpack<S>(data?: VersionedState<S>): S | undefined;
49
+ }
50
+ /**
51
+ * Takes a version number and state management functions that are pinned to this
52
+ * version.
53
+ *
54
+ * The two functions it returns are `versionify` and `unpack`. The former can be
55
+ * used to add a version to some data. The latter can be used to unpack the data
56
+ * again, validating the version on the fly.
57
+ *
58
+ * ```ts
59
+ * import { assert } from "jsr:@std/assert";
60
+ *
61
+ * const { versionify, unpack } = pinVersion(42);
62
+ *
63
+ * const data = { prop: "pizza" };
64
+ * const versioned = versionify(data);
65
+ * const unpacked = unpack(versioned);
66
+ * assert(data === unpacked);
67
+ * ```
68
+ *
69
+ * @param version the version to use for pinning
70
+ */
71
+ export declare function pinVersion(version: string | number): PinnedVersion;
72
+ /**
73
+ * A value or a promise of a value.
74
+ *
75
+ * @typeParam T The type of value
76
+ */
77
+ export type MaybePromise<T> = T | Promise<T>;
78
+ /**
79
+ * A storage for versioned state.
80
+ *
81
+ * Specify this to define how to persist data.
82
+ *
83
+ * This type is a union of three types, each representing a different way to
84
+ * store data.
85
+ *
86
+ * 1. A {@link VersionedStateStorage} directly provides definitions for reading,
87
+ * writing, and deleting data based on `ctx.chatId`. No versions can be
88
+ * specified and the storage key function cannot be changed.
89
+ * 2. A {@link ConversationKeyStorage}, disambiguated via `{ type: "key" }`, is
90
+ * more general. It supports versioning the data and changing the storage key
91
+ * function.
92
+ * 3. A {@link ConversationContextStorage}, disambiguated via `{ type: "context"
93
+ * }`, is even more general. It no longer needs a storage key function.
94
+ * Instead, it provides read, write, and delete operations for data based on
95
+ * the context object directly. It also supports versioning data.
96
+ *
97
+ * @typeParam C A custom context type
98
+ * @typeParam S A type for the state to version and store
99
+ */
100
+ export type ConversationStorage<C extends Context, S> = {
101
+ type?: never;
102
+ version?: never;
103
+ } & VersionedStateStorage<string, S> | ConversationContextStorage<C, S> | ConversationKeyStorage<C, S>;
104
+ /**
105
+ * An object that defines how to read, write, and delete versioned data based on
106
+ * a key.
107
+ *
108
+ * @typeParam K The type of key to use
109
+ * @typeParam S The type of data to store
110
+ */
111
+ export interface VersionedStateStorage<K, S> {
112
+ /**
113
+ * Reads the data for a given key.
114
+ *
115
+ * @param key A key to identify the data
116
+ */
117
+ read(key: K): MaybePromise<VersionedState<S> | undefined>;
118
+ /**
119
+ * Writes some data to the storage for a given key.
120
+ *
121
+ * @param key A key to identify the data
122
+ * @param state The data to write
123
+ */
124
+ write(key: K, state: VersionedState<S>): MaybePromise<void>;
125
+ /**
126
+ * Deletes some data from the storage for a given key.
127
+ *
128
+ * @param key A key to identify the data
129
+ */
130
+ delete(key: K): MaybePromise<void>;
131
+ }
132
+ /**
133
+ * An object that defines how to read, write, or delete versioned data based on
134
+ * a context object.
135
+ */
136
+ export interface ConversationContextStorage<C extends Context, S> {
137
+ /** The type of storage, always `"context"` */
138
+ type: "context";
139
+ /** An optional version for the data, defaults to `0` */
140
+ version?: string | number;
141
+ /** The underlying storage that defines how to read and write raw data */
142
+ adapter: VersionedStateStorage<C, S>;
143
+ }
144
+ export interface ConversationKeyStorage<C extends Context, S> {
145
+ /** The type of storage, always `"key"` */
146
+ type: "key";
147
+ /** An optional version for the data, defaults to `0` */
148
+ version?: string | number;
149
+ /** An optional prefix to prepend to the storage key */
150
+ prefix?: string;
151
+ /** An optional storage key function, defaults to `ctx.chatId` */
152
+ getStorageKey?(ctx: C): string | undefined;
153
+ /** The underlying storage that defines how to read and write raw data */
154
+ adapter: VersionedStateStorage<string, S>;
155
+ }
156
+ /**
157
+ * Coerces different storages to a single uniform abstraction.
158
+ *
159
+ * This function takes a {@link ConversationStorage} object and unifies its
160
+ * union members behind a common abstraction that simply exposes a read, write,
161
+ * and delete method for a given context object.
162
+ *
163
+ * @param storage An object defining how to store data
164
+ */
165
+ export declare function uniformStorage<C extends Context, S>(storage?: ConversationStorage<C, S>): (ctx: C) => {
166
+ read: () => MaybePromise<S | undefined>;
167
+ write: (state: S) => MaybePromise<void>;
168
+ delete: () => MaybePromise<void>;
169
+ };
package/out/storage.js ADDED
@@ -0,0 +1,105 @@
1
+ /** Current data version of this plugin */
2
+ export const PLUGIN_DATA_VERSION = 0;
3
+ /**
4
+ * Takes a version number and state management functions that are pinned to this
5
+ * version.
6
+ *
7
+ * The two functions it returns are `versionify` and `unpack`. The former can be
8
+ * used to add a version to some data. The latter can be used to unpack the data
9
+ * again, validating the version on the fly.
10
+ *
11
+ * ```ts
12
+ * import { assert } from "jsr:@std/assert";
13
+ *
14
+ * const { versionify, unpack } = pinVersion(42);
15
+ *
16
+ * const data = { prop: "pizza" };
17
+ * const versioned = versionify(data);
18
+ * const unpacked = unpack(versioned);
19
+ * assert(data === unpacked);
20
+ * ```
21
+ *
22
+ * @param version the version to use for pinning
23
+ */
24
+ export function pinVersion(version) {
25
+ function versionify(state) {
26
+ return { version: [PLUGIN_DATA_VERSION, version], state };
27
+ }
28
+ function unpack(data) {
29
+ if (data === undefined)
30
+ return undefined;
31
+ if (!Array.isArray(data.version)) {
32
+ throw new Error("Unknown data format, cannot parse version");
33
+ }
34
+ const [pluginVersion, dataVersion] = data.version;
35
+ if (dataVersion !== version)
36
+ return undefined;
37
+ if (pluginVersion !== PLUGIN_DATA_VERSION) {
38
+ // In the future, we might want to migrate the data from an old
39
+ // plugin version to a new one here.
40
+ return undefined;
41
+ }
42
+ return data.state;
43
+ }
44
+ return { versionify, unpack };
45
+ }
46
+ function defaultStorageKey(ctx) {
47
+ var _a;
48
+ return (_a = ctx.chatId) === null || _a === void 0 ? void 0 : _a.toString();
49
+ }
50
+ function defaultStorage() {
51
+ const store = new Map();
52
+ return {
53
+ type: "key",
54
+ adapter: {
55
+ read: (key) => store.get(key),
56
+ write: (key, state) => void store.set(key, state),
57
+ delete: (key) => void store.delete(key),
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * Coerces different storages to a single uniform abstraction.
63
+ *
64
+ * This function takes a {@link ConversationStorage} object and unifies its
65
+ * union members behind a common abstraction that simply exposes a read, write,
66
+ * and delete method for a given context object.
67
+ *
68
+ * @param storage An object defining how to store data
69
+ */
70
+ export function uniformStorage(storage) {
71
+ var _a;
72
+ storage !== null && storage !== void 0 ? storage : (storage = defaultStorage());
73
+ if (storage.type === undefined) {
74
+ return uniformStorage({ type: "key", adapter: storage });
75
+ }
76
+ const version = (_a = storage.version) !== null && _a !== void 0 ? _a : 0;
77
+ const { versionify, unpack } = pinVersion(version);
78
+ if (storage.type === "key") {
79
+ const { getStorageKey = defaultStorageKey, prefix = "", adapter } = storage;
80
+ return (ctx) => {
81
+ const key = prefix + getStorageKey(ctx);
82
+ return key === undefined
83
+ ? {
84
+ read: () => undefined,
85
+ write: () => undefined,
86
+ delete: () => undefined,
87
+ }
88
+ : {
89
+ read: async () => unpack(await adapter.read(key)),
90
+ write: (state) => adapter.write(key, versionify(state)),
91
+ delete: () => adapter.delete(key),
92
+ };
93
+ };
94
+ }
95
+ else {
96
+ const { adapter } = storage;
97
+ return (ctx) => {
98
+ return {
99
+ read: async () => unpack(await adapter.read(ctx)),
100
+ write: (state) => adapter.write(ctx, versionify(state)),
101
+ delete: () => adapter.delete(ctx),
102
+ };
103
+ };
104
+ }
105
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@rilong/grammyjs-conversations-esm",
3
+ "description": "Conversational interfaces for grammY",
4
+ "version": "2.0.2",
5
+ "author": "KnorpelSenf",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": "^12.20.0 || >=14.13.1"
9
+ },
10
+ "homepage": "https://grammy.dev/",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/grammyjs/grammY"
14
+ },
15
+ "scripts": {
16
+ "prepare": "npm run backport",
17
+ "backport": "deno2node tsconfig.json"
18
+ },
19
+ "peerDependencies": {
20
+ "grammy": "^1.20.1"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^12.20.55",
24
+ "deno2node": "^1.14.0"
25
+ },
26
+ "files": [
27
+ "out/"
28
+ ],
29
+ "main": "./out/mod.js",
30
+ "types": "./out/mod.d.ts",
31
+ "keywords": [
32
+ "telegram",
33
+ "bot",
34
+ "api",
35
+ "client",
36
+ "framework",
37
+ "library",
38
+ "grammy",
39
+ "conversations",
40
+ "scenes",
41
+ "wizards"
42
+ ]
43
+ }