Files
openclaw/extensions/telegram/src/update-offset-persistence.ts
Peter Steinberger b080dd1e76 refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
2026-08-11 23:26:37 -07:00

112 lines
3.2 KiB
TypeScript

// Telegram plugin module owns monotonic update-offset persistence and retry.
import {
computeBackoff,
sleepWithAbort,
type BackoffPolicy,
} from "openclaw/plugin-sdk/runtime-env";
import { asSafeIntegerInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
const OFFSET_PERSIST_RETRY_POLICY: BackoffPolicy = {
initialMs: 250,
maxMs: 5_000,
factor: 2,
jitter: 0.1,
};
type TelegramUpdateOffsetPersistenceOptions = {
initialUpdateId: number | null;
writeUpdateId: (updateId: number) => Promise<void>;
onInvalidUpdateId: (updateId: number) => void;
onRetry: (retry: { attempt: number; delayMs: number; error: unknown; updateId: number }) => void;
abortSignal?: AbortSignal;
};
export function normalizeTelegramUpdateId(value: number | null): number | null {
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
}
export function createTelegramUpdateOffsetPersistence(
options: TelegramUpdateOffsetPersistenceOptions,
) {
const stopController = new AbortController();
const retrySignal = options.abortSignal
? AbortSignal.any([options.abortSignal, stopController.signal])
: stopController.signal;
let acceptedUpdateId = options.initialUpdateId;
let committedUpdateId = options.initialUpdateId;
let pendingUpdateId: number | null = null;
let activeDrain: Promise<void> | undefined;
const drain = async () => {
let attempt = 0;
while (pendingUpdateId !== null) {
if (retrySignal.aborted) {
return;
}
const updateId = pendingUpdateId;
try {
await options.writeUpdateId(updateId);
committedUpdateId = updateId;
if (pendingUpdateId === updateId) {
pendingUpdateId = null;
}
attempt = 0;
} catch (error) {
if (retrySignal.aborted) {
return;
}
attempt += 1;
const delayMs = computeBackoff(OFFSET_PERSIST_RETRY_POLICY, attempt);
options.onRetry({ attempt, delayMs, error, updateId });
await sleepWithAbort(delayMs, retrySignal, { ref: false });
}
}
};
const startDrain = () => {
if (activeDrain) {
return;
}
const run = drain()
.catch(() => undefined)
.finally(() => {
if (activeDrain === run) {
activeDrain = undefined;
if (pendingUpdateId !== null && !retrySignal.aborted) {
startDrain();
}
}
});
activeDrain = run;
};
const persistUpdateId = (updateId: number) => {
if (retrySignal.aborted) {
return;
}
const normalizedUpdateId = normalizeTelegramUpdateId(updateId);
if (normalizedUpdateId === null) {
options.onInvalidUpdateId(updateId);
return;
}
if (acceptedUpdateId !== null && normalizedUpdateId <= acceptedUpdateId) {
return;
}
acceptedUpdateId = normalizedUpdateId;
pendingUpdateId = normalizedUpdateId;
startDrain();
};
const stop = async () => {
stopController.abort(new Error("Telegram update-offset persistence stopped."));
await activeDrain?.catch(() => undefined);
};
return {
getAcceptedUpdateId: () => acceptedUpdateId,
getCommittedUpdateId: () => committedUpdateId,
persistUpdateId,
stop,
};
}