UNPKG

inngest

Version:

Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.

330 lines (328 loc) • 9.92 kB
import { getClientSubscriptionToken, subscribe } from "./components/realtime/subscribe/index.js"; import { useEffect, useRef, useState } from "react"; //#region src/react.ts const terminalRunStatuses = new Set([ "completed", "failed", "cancelled" ]); const clampMessages = (prev, next, limit) => { const merged = [...prev, ...next]; if (limit === null) return merged; if (limit <= 0) return []; return merged.length > limit ? merged.slice(-limit) : merged; }; const getReconnectDelay = (attempt, minMs, maxMs) => { const jitter = Math.floor(Math.random() * minMs); return Math.min(maxMs, minMs * 2 ** attempt + jitter); }; const toError = (err) => { return err instanceof Error ? err : new Error(String(err)); }; const isSubscriptionToken = (value) => { return "channel" in value && "topics" in value; }; const normalizeRunStatus = (value) => { if (typeof value !== "string") return; switch (value.toLowerCase()) { case "running": case "in_progress": return "running"; case "completed": case "complete": case "succeeded": case "success": return "completed"; case "failed": case "error": return "failed"; case "cancelled": case "canceled": return "cancelled"; default: return; } }; const inferRunLifecycleUpdate = (message) => { if (message.kind !== "run") return {}; const data = message.data; if (!data || typeof data !== "object") return {}; const obj = data; const runStatus = normalizeRunStatus(obj.runStatus) || normalizeRunStatus(obj.status) || normalizeRunStatus(obj.state); if ("result" in obj) return { runStatus, result: obj.result }; if (runStatus === "completed" && "output" in obj) return { runStatus, result: obj.output }; return { runStatus }; }; const hasDocument = () => typeof document !== "undefined"; const isDocumentVisible = () => { if (!hasDocument()) return true; return document.visibilityState !== "hidden"; }; const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); const useRealtime = ({ channel, topics, token: tokenInput, key, enabled = true, bufferInterval = 0, validate = true, apiBaseUrl, historyLimit = 100, reconnect = true, reconnectMinMs = 250, reconnectMaxMs = 5e3, pauseOnHidden = true, autoCloseOnTerminal = true }) => { const channelKey = typeof channel === "string" ? channel : channel?.name ?? void 0; const topicsKey = topics ? JSON.stringify([...topics]) : ""; const [allMessages, setAllMessages] = useState([]); const [messageDelta, setMessageDelta] = useState([]); const [messagesByTopic, setMessagesByTopic] = useState({}); const [lastMessage, setLastMessage] = useState(null); const [error, setError] = useState(null); const [connectionStatus, setConnectionStatus] = useState("idle"); const [pauseReason, setPauseReason] = useState(null); const [runStatus, setRunStatus] = useState("unknown"); const [result, setResult] = useState(void 0); const [isVisible, setIsVisible] = useState(() => isDocumentVisible()); const subscriptionRef = useRef(null); const readerRef = useRef(null); const messageBufferRef = useRef([]); const bufferIntervalRef = useRef(bufferInterval); const messageLimitRef = useRef(historyLimit); const runStatusRef = useRef(runStatus); useEffect(() => { runStatusRef.current = runStatus; }, [runStatus]); useEffect(() => { bufferIntervalRef.current = bufferInterval; }, [bufferInterval]); useEffect(() => { messageLimitRef.current = historyLimit; }, [historyLimit]); useEffect(() => { messageBufferRef.current = []; setAllMessages([]); setMessageDelta([]); setMessagesByTopic({}); setLastMessage(null); setResult(void 0); setError(null); runStatusRef.current = "unknown"; setRunStatus("unknown"); }, [ channelKey, topicsKey, key ]); useEffect(() => { if (!pauseOnHidden || !hasDocument()) return; const onVisibilityChange = () => { setIsVisible(isDocumentVisible()); }; document.addEventListener("visibilitychange", onVisibilityChange); return () => { document.removeEventListener("visibilitychange", onVisibilityChange); }; }, [pauseOnHidden]); const reset = () => { messageBufferRef.current = []; setAllMessages([]); setMessageDelta([]); setMessagesByTopic({}); setLastMessage(null); setResult(void 0); setError(null); runStatusRef.current = "unknown"; setRunStatus("unknown"); }; const flushBufferedMessages = () => { if (messageBufferRef.current.length === 0) return; const buffered = [...messageBufferRef.current]; messageBufferRef.current = []; setMessageDelta(buffered); setAllMessages((prev) => clampMessages(prev, buffered, messageLimitRef.current)); setLastMessage(buffered[buffered.length - 1] ?? null); }; useEffect(() => { let interval = null; if (bufferInterval <= 0) flushBufferedMessages(); else interval = setInterval(() => { flushBufferedMessages(); }, bufferInterval); return () => { if (interval) clearInterval(interval); }; }, [bufferInterval]); useEffect(() => { if (!(enabled && (!pauseOnHidden || isVisible))) { const nextPauseReason = !enabled ? "disabled" : pauseOnHidden && !isVisible ? "hidden" : null; if (nextPauseReason) { setPauseReason(nextPauseReason); setConnectionStatus("paused"); } else { setPauseReason(null); setConnectionStatus("idle"); } return; } setPauseReason(null); let cancelled = false; const cleanupConnection = async (reason = "useRealtime cleanup") => { flushBufferedMessages(); const reader = readerRef.current; const sub = subscriptionRef.current; readerRef.current = null; subscriptionRef.current = null; try { await reader?.cancel(); } catch {} try { reader?.releaseLock(); } catch {} try { sub?.unsubscribe(reason); } catch {} }; const resolveToken = async () => { if (tokenInput && typeof tokenInput !== "function") return tokenInput; if (typeof tokenInput === "function") { const next = await tokenInput(); if (typeof next === "string") { if (!channel || !topics) throw new Error("useRealtime token() returned a string but channel/topics were not provided"); return { channel, topics, key: next }; } if (isSubscriptionToken(next)) return next; if (!channel || !topics) throw new Error("useRealtime token() returned a key object but channel/topics were not provided"); return { channel, topics, key: next.key, apiBaseUrl: next.apiBaseUrl }; } throw new Error("No token provided and no token() handler."); }; const applyMessage = (message) => { if (message.kind === "run") { const lifecycle = inferRunLifecycleUpdate(message); if (lifecycle.runStatus) { runStatusRef.current = lifecycle.runStatus; setRunStatus(lifecycle.runStatus); } if ("result" in lifecycle) setResult(lifecycle.result); return; } if (runStatusRef.current === "unknown") { runStatusRef.current = "running"; setRunStatus("running"); } if (message.topic) setMessagesByTopic((prev) => ({ ...prev, [message.topic]: message })); if (bufferIntervalRef.current === 0) { setMessageDelta([message]); setAllMessages((prev) => clampMessages(prev, [message], messageLimitRef.current)); setLastMessage(message); return; } messageBufferRef.current.push(message); }; const run = async () => { let reconnectAttempt = 0; while (!cancelled) { try { setError(null); setConnectionStatus("connecting"); const token = await resolveToken(); if (cancelled) break; const stream = await subscribe({ ...token, validate, apiBaseUrl: apiBaseUrl ?? token.apiBaseUrl }); if (cancelled) { stream.unsubscribe("useRealtime cancelled before start"); break; } reconnectAttempt = 0; subscriptionRef.current = stream; setConnectionStatus("open"); if (runStatusRef.current === "unknown") { runStatusRef.current = "running"; setRunStatus("running"); } const reader = stream.getReader(); readerRef.current = reader; try { while (!cancelled) { const { done, value } = await reader.read(); if (done || cancelled) break; applyMessage(value); if (autoCloseOnTerminal && terminalRunStatuses.has(runStatusRef.current)) { stream.unsubscribe("Run reached terminal status"); break; } } } finally { try { reader.releaseLock(); } catch {} if (readerRef.current === reader) readerRef.current = null; } if (cancelled) break; setConnectionStatus("closed"); if (autoCloseOnTerminal && terminalRunStatuses.has(runStatusRef.current)) break; if (!reconnect) break; } catch (err) { if (cancelled) break; setError(toError(err)); setConnectionStatus("error"); if (!reconnect) break; } finally { await cleanupConnection("reconnect cycle cleanup"); } if (cancelled || !reconnect) break; await sleep(getReconnectDelay(reconnectAttempt++, reconnectMinMs, reconnectMaxMs)); } }; run().catch((err) => { if (!cancelled) { setError(toError(err)); setConnectionStatus("error"); } }); return () => { cancelled = true; cleanupConnection("useRealtime unmount"); setConnectionStatus((prev) => prev === "open" ? "closed" : prev); }; }, [ apiBaseUrl, autoCloseOnTerminal, channelKey, enabled, isVisible, key, pauseOnHidden, reconnect, reconnectMaxMs, reconnectMinMs, tokenInput, topicsKey, validate ]); return { connectionStatus, runStatus, isPaused: connectionStatus === "paused", pauseReason, messages: { byTopic: messagesByTopic, all: allMessages, last: lastMessage, delta: messageDelta }, result, error, reset }; }; //#endregion export { getClientSubscriptionToken, useRealtime }; //# sourceMappingURL=react.js.map