export type PacerConfig = { allowedRequests: number; intervalMs: number; }; export type PacerEntry = PacerConfig & { lastScheduledAtMs: number }; export type Pacer = { _tag: "pacer"; name: string; config: PacerConfig; wait(): Promise; }; const REGISTRY_KEY = Symbol.for("@notionhq/apps/pacer-registry"); const STATE_KEY = Symbol.for("@notionhq/apps/pacer-state"); function registry(): Map { const holder = globalThis as { [REGISTRY_KEY]?: Map }; holder[REGISTRY_KEY] ??= new Map(); return holder[REGISTRY_KEY]; } function state(): Map { const holder = globalThis as { [STATE_KEY]?: Map }; holder[STATE_KEY] ??= new Map(); return holder[STATE_KEY]; } export function createPacer(name: string, config: PacerConfig): Pacer { if (typeof name !== "string" || name.length === 0) { throw new Error("Pacer name must be a non-empty string"); } if (!Number.isFinite(config.allowedRequests) || config.allowedRequests <= 0) { throw new Error("Pacer allowedRequests must be greater than zero"); } if (!Number.isFinite(config.intervalMs) || config.intervalMs <= 0) { throw new Error("Pacer intervalMs must be greater than zero"); } if (registry().has(name)) { throw new Error(`Two distinct pacers are named "${name}"`); } const pacer: Pacer = { _tag: "pacer", name, config, async wait() { const entry = state().get(name) ?? { ...config, lastScheduledAtMs: 0 }; const stepMs = Math.ceil(entry.intervalMs / entry.allowedRequests); const now = Date.now(); const scheduledAtMs = Math.max(entry.lastScheduledAtMs + stepMs, now); state().set(name, { ...entry, lastScheduledAtMs: scheduledAtMs }); if (scheduledAtMs > now) { await new Promise((resolve) => setTimeout(resolve, scheduledAtMs - now)); } }, }; registry().set(name, pacer); return pacer; } /** @internal */ export function initPacerState(entries: Record | undefined): void { state().clear(); for (const [name, entry] of Object.entries(entries ?? {})) state().set(name, entry); } /** @internal */ export function pacerStates(): Record { return Object.fromEntries(state()); } /** @internal */ export function registeredPacers(): Pacer[] { return [...registry().values()]; } /** @internal */ export function clearRegisteredPacers(): void { registry().clear(); }