@adriangalilea/utils 2.15.0 → 2.16.1

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,66 @@
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 read off the ctx: the message identity plus gramio's react().
40
+ * `for()` takes `unknown` and narrows structurally (gramio's react() param is an
41
+ * emoji-literal union, which no portable signature satisfies in strict variance) —
42
+ * a ctx without a callable react() screams instead of silently not reacting. */
43
+ export interface ReactionCtx {
44
+ id?: number;
45
+ chatId?: number;
46
+ chat?: {
47
+ id?: number;
48
+ };
49
+ react(emoji: string): Promise<unknown>;
50
+ }
51
+ export interface ReactionHandle<S extends string> {
52
+ /** The current state on this message, or null if none was set. */
53
+ state(): S | null;
54
+ /**
55
+ * Request a state. Applies (and reacts, if the emoji changes) only when
56
+ * not outranked by the current state. Returns whether the state holds
57
+ * after the call (true also for an idempotent re-set).
58
+ */
59
+ set(next: S): Promise<boolean>;
60
+ }
61
+ /** Declare the reaction vocabulary once; get per-message arbitrated handles. */
62
+ export declare function reactionPolicy<S extends Record<string, ReactionStateSpec>>(states: S): {
63
+ /** The arbitrated handle for THIS ctx's message (keyed chat:message). */
64
+ for(context: unknown): ReactionHandle<keyof S & string>;
65
+ };
66
+ //# 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;;;iFAGiF;AACjF,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;iBAC5D,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;EAuBxD"}
@@ -0,0 +1,71 @@
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(context) {
42
+ const ctx = context;
43
+ if (typeof ctx.react !== "function")
44
+ throw new Error("reactionPolicy.for: ctx has no react()");
45
+ const chat = ctx.chatId ?? ctx.chat?.id;
46
+ const key = `${chat}:${ctx.id}`;
47
+ return {
48
+ state: () => current.get(key) ?? null,
49
+ async set(next) {
50
+ const spec = states[next];
51
+ if (!spec)
52
+ throw new Error(`unknown reaction state: ${String(next)}`);
53
+ const prev = current.get(key);
54
+ if (prev === next)
55
+ return true; // idempotent
56
+ if (prev !== undefined && spec.rank < states[prev].rank)
57
+ return false; // outranked
58
+ if (!current.has(key) && current.size >= MAX_TRACKED_MESSAGES)
59
+ current.clear();
60
+ current.set(key, next);
61
+ // Same emoji under a new state: bookkeeping only — no API call, no flicker.
62
+ if (prev !== undefined && states[prev].emoji === spec.emoji)
63
+ return true;
64
+ await ctx.react(spec.emoji).catch(() => { });
65
+ return true;
66
+ },
67
+ };
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=reactions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactions.js","sourceRoot":"","sources":["../../src/bot/reactions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA+BH,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,OAAgB;YACnB,MAAM,GAAG,GAAG,OAAsB,CAAC;YACnC,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,UAAU;gBAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;YAC/F,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.1",
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",