@llblab/pi-telegram 0.11.2 → 0.12.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.
@@ -1,166 +0,0 @@
1
- /**
2
- * External Telegram handler registry
3
- * Zones: telegram transport, layered extension interop
4
- * Lets other pi extensions hook into the polling loop without owning their own getUpdates connection
5
- */
6
-
7
- /**
8
- * Verdict returned by an interceptor.
9
- *
10
- * - `"consume"` — the interceptor handled this update; pi-telegram skips default routing.
11
- * - `"pass"` (or `void`/`undefined`) — pi-telegram routes the update normally.
12
- */
13
- export type TelegramExternalHandlerVerdict = "consume" | "pass";
14
-
15
- export type TelegramExternalHandler = (
16
- update: unknown,
17
- ) =>
18
- | TelegramExternalHandlerVerdict
19
- | void
20
- | Promise<TelegramExternalHandlerVerdict | void>;
21
-
22
- export interface TelegramExternalHandlerRegistry {
23
- /** Schema version of this registry shape. */
24
- readonly version: 1;
25
- /**
26
- * Register an interceptor. Returns a disposer that removes it.
27
- *
28
- * Interceptors are invoked in registration order on every Telegram update,
29
- * before pi-telegram's own routing. The first interceptor that returns
30
- * `"consume"` wins and stops the chain for that update.
31
- */
32
- add: (handler: TelegramExternalHandler) => () => void;
33
- /**
34
- * Run all registered interceptors against an update.
35
- *
36
- * Used by pi-telegram's polling runtime; layered extensions should call
37
- * {@link onTelegramExternalUpdate} or `add` instead of dispatching directly.
38
- */
39
- dispatch: (update: unknown) => Promise<TelegramExternalHandlerVerdict>;
40
- }
41
-
42
- const REGISTRY_KEY = "__piTelegramExternalHandlerRegistry__";
43
-
44
- /**
45
- * Validate that a value on `globalThis` matches the full v1 registry contract.
46
- *
47
- * pi-telegram's polling runtime invokes `dispatch`, so a partial object that
48
- * only carries `version` and `add` (which an early draft of the zero-coupling
49
- * docs showed) would silently break the first update. We treat any object
50
- * tagged `version === 1` but missing required methods as malformed and
51
- * replace it with a fresh, fully-formed registry. Layered extensions that
52
- * follow the full documented shape are unaffected; ones that don't lose any
53
- * handlers they registered against the malformed object, which is the
54
- * desired fail-loud-during-development behavior.
55
- */
56
- function isValidV1Registry(
57
- candidate: unknown,
58
- ): candidate is TelegramExternalHandlerRegistry {
59
- if (!candidate || typeof candidate !== "object") return false;
60
- const r = candidate as Partial<TelegramExternalHandlerRegistry>;
61
- return (
62
- r.version === 1 &&
63
- typeof r.add === "function" &&
64
- typeof r.dispatch === "function"
65
- );
66
- }
67
-
68
- function getOrCreateRegistry(): TelegramExternalHandlerRegistry {
69
- const g = globalThis as Record<string, unknown>;
70
- const existing = g[REGISTRY_KEY];
71
- if (isValidV1Registry(existing)) return existing;
72
- const handlers = new Set<TelegramExternalHandler>();
73
- const registry: TelegramExternalHandlerRegistry = {
74
- version: 1,
75
- add(handler) {
76
- handlers.add(handler);
77
- return () => handlers.delete(handler);
78
- },
79
- async dispatch(update) {
80
- for (const handler of handlers) {
81
- try {
82
- const result = await handler(update);
83
- if (result === "consume") return "consume";
84
- } catch {
85
- // External handler errors must not break polling.
86
- }
87
- }
88
- return "pass";
89
- },
90
- };
91
- g[REGISTRY_KEY] = registry;
92
- return registry;
93
- }
94
-
95
- /**
96
- * Called by pi-telegram's own runtime to obtain the registry it dispatches
97
- * through. Layered extensions should not call this; use
98
- * {@link onTelegramExternalUpdate} instead.
99
- */
100
- export function getTelegramExternalHandlerRegistry(): TelegramExternalHandlerRegistry {
101
- return getOrCreateRegistry();
102
- }
103
-
104
- export interface TelegramExternalHandlerWrapDeps<TUpdate, TContext> {
105
- defaultHandle: (update: TUpdate, ctx: TContext) => Promise<void>;
106
- registry?: TelegramExternalHandlerRegistry;
107
- }
108
- export type TelegramExternalInterceptorWrapDeps<TUpdate, TContext> =
109
- TelegramExternalHandlerWrapDeps<TUpdate, TContext>;
110
-
111
- /**
112
- * Wrap a default polling `handleUpdate` with the external interceptor registry.
113
- *
114
- * Returned function dispatches `update` through registered interceptors first;
115
- * if any returns `"consume"`, default routing is skipped for that update.
116
- *
117
- * Composition-root callers (pi-telegram's `index.ts`) should use this builder
118
- * instead of writing the lifting logic inline.
119
- */
120
- export function createTelegramExternalHandleUpdate<TUpdate, TContext>(
121
- deps: TelegramExternalHandlerWrapDeps<TUpdate, TContext>,
122
- ): (update: TUpdate, ctx: TContext) => Promise<void> {
123
- const registry = deps.registry ?? getOrCreateRegistry();
124
- const { defaultHandle } = deps;
125
- return async function handleInterceptedUpdate(update, ctx) {
126
- const verdict = await registry.dispatch(update);
127
- if (verdict === "consume") return;
128
- await defaultHandle(update, ctx);
129
- };
130
- }
131
-
132
- /**
133
- * Register an interceptor that runs before pi-telegram routes a Telegram
134
- * update through its built-in handlers (commands, app menu, queue menu,
135
- * model menu, default prompt routing).
136
- *
137
- * This is the recommended public surface for layered extensions that share
138
- * the same bot and pi process with pi-telegram (single bot ↔ single
139
- * `getUpdates` poller).
140
- *
141
- * Returns a disposer that removes the interceptor.
142
- *
143
- * @example
144
- * ```ts
145
- * import { onTelegramExternalUpdate } from "@llblab/pi-telegram/lib/external-handlers.ts";
146
- *
147
- * const off = onTelegramExternalUpdate(async (update) => {
148
- * const cb = (update as { callback_query?: { data?: string } }).callback_query;
149
- * if (!cb?.data?.startsWith("myext:")) return "pass";
150
- * await handleMyCallback(cb);
151
- * return "consume"; // skip pi-telegram's default routing for this update
152
- * });
153
- *
154
- * // later, e.g. on session shutdown:
155
- * off();
156
- * ```
157
- *
158
- * Extensions that prefer zero coupling can also reach the versioned registry
159
- * directly on `globalThis`. This avoids importing `@llblab/pi-telegram` and
160
- * tolerates either install order.
161
- */
162
- export function onTelegramExternalUpdate(
163
- handler: TelegramExternalHandler,
164
- ): () => void {
165
- return getOrCreateRegistry().add(handler);
166
- }