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
JavaScript
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