@adriangalilea/utils 2.15.0 → 2.16.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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Message-reaction arbitration: ONE owner for the bot's reaction on each
3
+ * message. Telegram's setMessageReaction REPLACES the bot's whole reaction
4
+ * set, so independent features reacting to the same message (an "on it" ack,
5
+ * an outcome, a side-feature's shrug) silently clobber each other — last
6
+ * writer wins, and the client shows flickering swaps.
7
+ *
8
+ * The fix is semantic states with ranks, arbitrated per message:
9
+ *
10
+ * const status = reactionPolicy({
11
+ * working: { emoji: "🫡", rank: 1 },
12
+ * offTopic: { emoji: "🤷", rank: 2 },
13
+ * failed: { emoji: "👎", rank: 3 },
14
+ * done: { emoji: "🫡", rank: 4 }, // same emoji as working: no API call, but locks out offTopic
15
+ * });
16
+ * const r = status.for(ctx);
17
+ * await r.set("working"); // applied
18
+ * await r.set("done"); // rank bookkeeping only (same emoji) — zero API calls, zero flicker
19
+ * await r.set("offTopic"); // REJECTED: outranked by done — the shrug never lands
20
+ *
21
+ * Guarantees:
22
+ * - a state only applies when its rank is >= the current state's rank
23
+ * (equal rank may refine; strictly lower is rejected — returns false);
24
+ * - setting the current state again is a no-op (idempotent, no API call);
25
+ * - a transition between states sharing an emoji makes NO API call
26
+ * (rank bookkeeping without flicker);
27
+ * - react() failures are swallowed — reactions are decoration, they must
28
+ * never break the flow that set them;
29
+ * - state is keyed by chat:message, so one policy instance serves every
30
+ * message concurrently, and is bounded (reactions are transient
31
+ * request-lifetime coordination, not durable state).
32
+ */
33
+ export interface ReactionStateSpec {
34
+ /** The Telegram reaction emoji this state renders as. */
35
+ emoji: string;
36
+ /** Arbitration rank: a state only applies over an equal-or-lower rank. */
37
+ rank: number;
38
+ }
39
+ /** The context surface needed: the message identity plus gramio's react(). */
40
+ export interface ReactionCtx {
41
+ id?: number;
42
+ chatId?: number;
43
+ chat?: {
44
+ id?: number;
45
+ };
46
+ react(emoji: string): Promise<unknown>;
47
+ }
48
+ export interface ReactionHandle<S extends string> {
49
+ /** The current state on this message, or null if none was set. */
50
+ state(): S | null;
51
+ /**
52
+ * Request a state. Applies (and reacts, if the emoji changes) only when
53
+ * not outranked by the current state. Returns whether the state holds
54
+ * after the call (true also for an idempotent re-set).
55
+ */
56
+ set(next: S): Promise<boolean>;
57
+ }
58
+ /** Declare the reaction vocabulary once; get per-message arbitrated handles. */
59
+ export declare function reactionPolicy<S extends Record<string, ReactionStateSpec>>(states: S): {
60
+ /** The arbitrated handle for THIS ctx's message (keyed chat:message). */
61
+ for(ctx: ReactionCtx): ReactionHandle<keyof S & string>;
62
+ };
63
+ //# sourceMappingURL=reactions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.d.ts","sourceRoot":"","sources":["../../src/bot/reactions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,iBAAiB;IACjC,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;CACb;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM;IAC/C,kEAAkE;IAClE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAClB;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/B;AAMD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAInF,yEAAyE;aAChE,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;EAqBxD"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Message-reaction arbitration: ONE owner for the bot's reaction on each
3
+ * message. Telegram's setMessageReaction REPLACES the bot's whole reaction
4
+ * set, so independent features reacting to the same message (an "on it" ack,
5
+ * an outcome, a side-feature's shrug) silently clobber each other — last
6
+ * writer wins, and the client shows flickering swaps.
7
+ *
8
+ * The fix is semantic states with ranks, arbitrated per message:
9
+ *
10
+ * const status = reactionPolicy({
11
+ * working: { emoji: "🫡", rank: 1 },
12
+ * offTopic: { emoji: "🤷", rank: 2 },
13
+ * failed: { emoji: "👎", rank: 3 },
14
+ * done: { emoji: "🫡", rank: 4 }, // same emoji as working: no API call, but locks out offTopic
15
+ * });
16
+ * const r = status.for(ctx);
17
+ * await r.set("working"); // applied
18
+ * await r.set("done"); // rank bookkeeping only (same emoji) — zero API calls, zero flicker
19
+ * await r.set("offTopic"); // REJECTED: outranked by done — the shrug never lands
20
+ *
21
+ * Guarantees:
22
+ * - a state only applies when its rank is >= the current state's rank
23
+ * (equal rank may refine; strictly lower is rejected — returns false);
24
+ * - setting the current state again is a no-op (idempotent, no API call);
25
+ * - a transition between states sharing an emoji makes NO API call
26
+ * (rank bookkeeping without flicker);
27
+ * - react() failures are swallowed — reactions are decoration, they must
28
+ * never break the flow that set them;
29
+ * - state is keyed by chat:message, so one policy instance serves every
30
+ * message concurrently, and is bounded (reactions are transient
31
+ * request-lifetime coordination, not durable state).
32
+ */
33
+ // Transient per-isolate coordination, not durable state: bound it so a
34
+ // long-lived isolate can't grow the map without limit.
35
+ const MAX_TRACKED_MESSAGES = 512;
36
+ /** Declare the reaction vocabulary once; get per-message arbitrated handles. */
37
+ export function reactionPolicy(states) {
38
+ const current = new Map();
39
+ return {
40
+ /** The arbitrated handle for THIS ctx's message (keyed chat:message). */
41
+ for(ctx) {
42
+ const chat = ctx.chatId ?? ctx.chat?.id;
43
+ const key = `${chat}:${ctx.id}`;
44
+ return {
45
+ state: () => current.get(key) ?? null,
46
+ async set(next) {
47
+ const spec = states[next];
48
+ if (!spec)
49
+ throw new Error(`unknown reaction state: ${String(next)}`);
50
+ const prev = current.get(key);
51
+ if (prev === next)
52
+ return true; // idempotent
53
+ if (prev !== undefined && spec.rank < states[prev].rank)
54
+ return false; // outranked
55
+ if (!current.has(key) && current.size >= MAX_TRACKED_MESSAGES)
56
+ current.clear();
57
+ current.set(key, next);
58
+ // Same emoji under a new state: bookkeeping only — no API call, no flicker.
59
+ if (prev !== undefined && states[prev].emoji === spec.emoji)
60
+ return true;
61
+ await ctx.react(spec.emoji).catch(() => { });
62
+ return true;
63
+ },
64
+ };
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=reactions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.js","sourceRoot":"","sources":["../../src/bot/reactions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA4BH,uEAAuE;AACvE,uDAAuD;AACvD,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAEjC,gFAAgF;AAChF,MAAM,UAAU,cAAc,CAA8C,MAAS;IACpF,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IAEpD,OAAO;QACN,yEAAyE;QACzE,GAAG,CAAC,GAAgB;YACnB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;YACxC,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;YAChC,OAAO;gBACN,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI;gBACrC,KAAK,CAAC,GAAG,CAAC,IAAI;oBACb,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;oBAC1B,IAAI,CAAC,IAAI;wBAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACtE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;oBAC9B,IAAI,IAAI,KAAK,IAAI;wBAAE,OAAO,IAAI,CAAC,CAAC,aAAa;oBAC7C,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI;wBAAE,OAAO,KAAK,CAAC,CAAC,YAAY;oBACnF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,IAAI,oBAAoB;wBAAE,OAAO,CAAC,KAAK,EAAE,CAAC;oBAC/E,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;oBACvB,4EAA4E;oBAC5E,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK;wBAAE,OAAO,IAAI,CAAC;oBACzE,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;oBAC5C,OAAO,IAAI,CAAC;gBACb,CAAC;aACD,CAAC;QACH,CAAC;KACD,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adriangalilea/utils",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "TypeScript utilities - logger, currency, formatter, GramIO bot plugins, and more",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -156,6 +156,10 @@
156
156
  "types": "./dist/bot/urls.d.ts",
157
157
  "default": "./dist/bot/urls.js"
158
158
  },
159
+ "./bot/reactions": {
160
+ "types": "./dist/bot/reactions.d.ts",
161
+ "default": "./dist/bot/reactions.js"
162
+ },
159
163
  "./llm": {
160
164
  "types": "./dist/llm/index.d.ts",
161
165
  "default": "./dist/llm/index.js"
@@ -200,7 +204,8 @@
200
204
  "test:tg-md": "tsx tests/tg-md-test.ts",
201
205
  "test:self-echo": "tsx tests/self-echo-test.ts",
202
206
  "test:url": "tsx tests/url-test.ts",
203
- "test:bot-urls": "tsx tests/bot-urls-test.ts"
207
+ "test:bot-urls": "tsx tests/bot-urls-test.ts",
208
+ "test:reactions": "tsx tests/reactions-test.ts"
204
209
  },
205
210
  "keywords": [
206
211
  "typescript",