@fal-ai/client
Version:
The fal.ai client for JavaScript and TypeScript
366 lines • 18.6 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRealtimeClient = createRealtimeClient;
/* eslint-disable @typescript-eslint/no-explicit-any */
const msgpack_1 = require("@msgpack/msgpack");
const robot3_1 = require("robot3");
const auth_1 = require("./auth");
const response_1 = require("./response");
const runtime_1 = require("./runtime");
const utils_1 = require("./utils");
const initialState = () => ({
enqueuedMessage: undefined,
});
function hasToken(context) {
return context.token !== undefined;
}
function noToken(context) {
return !hasToken(context);
}
function enqueueMessage(context, event) {
return Object.assign(Object.assign({}, context), { enqueuedMessage: event.message });
}
function closeConnection(context) {
if (context.websocket && context.websocket.readyState === WebSocket.OPEN) {
context.websocket.close();
}
return Object.assign(Object.assign({}, context), { websocket: undefined });
}
function sendMessage(context, event) {
if (context.websocket && context.websocket.readyState === WebSocket.OPEN) {
if (event.message instanceof Uint8Array) {
context.websocket.send(event.message);
}
else if (typeof event.message === "string") {
context.websocket.send(event.message);
}
else {
context.websocket.send((0, msgpack_1.encode)(event.message));
}
return Object.assign(Object.assign({}, context), { enqueuedMessage: undefined });
}
return Object.assign(Object.assign({}, context), { enqueuedMessage: event.message });
}
function expireToken(context) {
return Object.assign(Object.assign({}, context), { token: undefined });
}
function setToken(context, event) {
return Object.assign(Object.assign({}, context), { token: event.token });
}
function connectionEstablished(context, event) {
return Object.assign(Object.assign({}, context), { websocket: event.websocket });
}
// State machine
const connectionStateMachine = (0, robot3_1.createMachine)("idle", {
idle: (0, robot3_1.state)((0, robot3_1.transition)("send", "connecting", (0, robot3_1.reduce)(enqueueMessage)), (0, robot3_1.transition)("close", "idle", (0, robot3_1.reduce)(closeConnection))),
connecting: (0, robot3_1.state)((0, robot3_1.transition)("connecting", "connecting"), (0, robot3_1.transition)("connected", "active", (0, robot3_1.reduce)(connectionEstablished)), (0, robot3_1.transition)("connectionClosed", "idle", (0, robot3_1.reduce)(closeConnection)), (0, robot3_1.transition)("send", "connecting", (0, robot3_1.reduce)(enqueueMessage)), (0, robot3_1.transition)("close", "idle", (0, robot3_1.reduce)(closeConnection)), (0, robot3_1.immediate)("authRequired", (0, robot3_1.guard)(noToken))),
authRequired: (0, robot3_1.state)((0, robot3_1.transition)("initiateAuth", "authInProgress"), (0, robot3_1.transition)("send", "authRequired", (0, robot3_1.reduce)(enqueueMessage)), (0, robot3_1.transition)("close", "idle", (0, robot3_1.reduce)(closeConnection))),
authInProgress: (0, robot3_1.state)((0, robot3_1.transition)("authenticated", "connecting", (0, robot3_1.reduce)(setToken)), (0, robot3_1.transition)("unauthorized", "idle", (0, robot3_1.reduce)(expireToken), (0, robot3_1.reduce)(closeConnection)), (0, robot3_1.transition)("send", "authInProgress", (0, robot3_1.reduce)(enqueueMessage)), (0, robot3_1.transition)("close", "idle", (0, robot3_1.reduce)(closeConnection))),
active: (0, robot3_1.state)((0, robot3_1.transition)("send", "active", (0, robot3_1.reduce)(sendMessage)), (0, robot3_1.transition)("authenticated", "active", (0, robot3_1.reduce)(setToken)), (0, robot3_1.transition)("unauthorized", "idle", (0, robot3_1.reduce)(expireToken)), (0, robot3_1.transition)("connectionClosed", "idle", (0, robot3_1.reduce)(expireToken), (0, robot3_1.reduce)(closeConnection)), (0, robot3_1.transition)("close", "idle", (0, robot3_1.reduce)(expireToken), (0, robot3_1.reduce)(closeConnection))),
}, initialState);
function buildRealtimeUrl(app, { token, maxBuffering, path }) {
var _a;
if (maxBuffering !== undefined && (maxBuffering < 1 || maxBuffering > 60)) {
throw new Error("The `maxBuffering` must be between 1 and 60 (inclusive)");
}
const queryParams = new URLSearchParams({
fal_jwt_token: token,
});
if (maxBuffering !== undefined) {
queryParams.set("max_buffering", maxBuffering.toFixed(0));
}
const appId = (0, utils_1.ensureEndpointIdFormat)(app);
const resolvedPath = (_a = (0, utils_1.resolveEndpointPath)(app, path, "/realtime")) !== null && _a !== void 0 ? _a : "";
return `wss://fal.run/${appId}${resolvedPath}?${queryParams.toString()}`;
}
const DEFAULT_THROTTLE_INTERVAL = 128;
function isUnauthorizedError(message) {
// TODO we need better protocol definition with error codes
return message["status"] === "error" && message["error"] === "Unauthorized";
}
/**
* See https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
*/
const WebSocketErrorCodes = {
NORMAL_CLOSURE: 1000,
GOING_AWAY: 1001,
};
const connectionCache = new Map();
const connectionCallbacks = new Map();
function reuseInterpreter(key, throttleInterval, onChange) {
if (!connectionCache.has(key)) {
const service = (0, robot3_1.interpret)(connectionStateMachine, onChange);
connectionCache.set(key, {
service,
throttledSend: throttleInterval > 0
? (0, utils_1.throttle)(service.send, throttleInterval, true)
: service.send,
});
}
return connectionCache.get(key);
}
const noop = () => {
/* No-op */
};
/**
* A no-op connection that does not send any message.
* Useful on the frameworks that reuse code for both ssr and csr (e.g. Next)
* so the call when doing ssr has no side-effects.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const NoOpConnection = {
send: noop,
close: noop,
};
function isSuccessfulResult(data) {
return (data.status !== "error" &&
data.type !== "x-fal-message" &&
!isFalErrorResult(data));
}
function isFalErrorResult(data) {
return data.type === "x-fal-error";
}
function decodeRealtimeMessage(data) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof data === "string") {
return JSON.parse(data);
}
const toUint8Array = (value) => __awaiter(this, void 0, void 0, function* () {
if (value instanceof Uint8Array) {
return value;
}
if (value instanceof Blob) {
return new Uint8Array(yield value.arrayBuffer());
}
return new Uint8Array(value);
});
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
return (0, msgpack_1.decode)(yield toUint8Array(data));
}
if (data instanceof Blob) {
return (0, msgpack_1.decode)(yield toUint8Array(data));
}
return data;
});
}
function encodeRealtimeMessage(input) {
if (input instanceof Uint8Array) {
return input;
}
if (typeof input === "string") {
return (0, msgpack_1.encode)(input);
}
return (0, msgpack_1.encode)(input);
}
function handleRealtimeMessage({ data, decodeMessage, onResult, onError, send, }) {
const handleDecoded = (decoded) => {
// Drop messages that are not related to the actual result.
// In the future, we might want to handle other types of messages.
// TODO: specify the fal ws protocol format
if (isUnauthorizedError(decoded)) {
send({
type: "unauthorized",
error: new Error("Unauthorized"),
});
return;
}
if (isSuccessfulResult(decoded)) {
onResult(decoded);
return;
}
if (isFalErrorResult(decoded)) {
if (decoded.error === "TIMEOUT") {
// Timeout error messages just indicate that the connection hasn't
// received an incoming message for a while. We don't need to
// handle them as errors.
return;
}
onError(new response_1.ApiError({
message: `${decoded.error}: ${decoded.reason}`,
// TODO better error status code
status: 400,
body: decoded,
}));
return;
}
};
Promise.resolve(decodeMessage ? decodeMessage(data) : data)
.then(handleDecoded)
.catch((error) => {
var _a;
onError(new response_1.ApiError({
message: (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : "Failed to decode realtime message",
status: 400,
}));
});
}
function createRealtimeClient({ config, }) {
return {
connect(app, handler) {
const {
// if running on React in the server, set clientOnly to true by default
clientOnly = (0, utils_1.isReact)() && !(0, runtime_1.isBrowser)(), connectionKey = crypto.randomUUID(), maxBuffering, path, throttleInterval = DEFAULT_THROTTLE_INTERVAL, encodeMessage: encodeMessageOverride, decodeMessage: decodeMessageOverride, tokenProvider, tokenExpirationSeconds, } = handler;
if (clientOnly && !(0, runtime_1.isBrowser)()) {
return NoOpConnection;
}
const encodeMessageFn = encodeMessageOverride !== null && encodeMessageOverride !== void 0 ? encodeMessageOverride : ((input) => encodeRealtimeMessage(input));
const decodeMessageFn = decodeMessageOverride !== null && decodeMessageOverride !== void 0 ? decodeMessageOverride : ((data) => decodeRealtimeMessage(data));
let previousState;
let latestEnqueuedMessage;
let tokenRefreshTimer;
let tokenRefreshGeneration = 0;
// Although the state machine is cached so we don't open multiple connections,
// we still need to update the callbacks so we can call the correct references
// when the state machine is reused. This is needed because the callbacks
// are passed as part of the handler object, which can be different across
// different calls to `connect`.
connectionCallbacks.set(connectionKey, {
decodeMessage: decodeMessageFn,
onError: handler.onError,
onResult: handler.onResult,
});
const getCallbacks = () => connectionCallbacks.get(connectionKey);
const stateMachine = reuseInterpreter(connectionKey, throttleInterval, ({ context, machine, send }) => {
var _a;
const { enqueuedMessage, token, websocket } = context;
latestEnqueuedMessage = enqueuedMessage;
if (machine.current === "active" &&
enqueuedMessage &&
(websocket === null || websocket === void 0 ? void 0 : websocket.readyState) === WebSocket.OPEN) {
send({ type: "send", message: enqueuedMessage });
}
if (machine.current === "authRequired" &&
token === undefined &&
previousState !== machine.current) {
send({ type: "initiateAuth" });
tokenRefreshGeneration++;
const generation = tokenRefreshGeneration;
// Use custom tokenProvider if provided, otherwise use default
const appId = (0, utils_1.ensureEndpointIdFormat)(app);
const resolvedPath = (_a = (0, utils_1.resolveEndpointPath)(app, path, "/realtime")) !== null && _a !== void 0 ? _a : "";
const fetchToken = tokenProvider
? () => tokenProvider(`${appId}${resolvedPath}`)
: () => {
console.warn("[fal.realtime] Using the default token provider is deprecated. " +
"Please provide a `tokenProvider` function to `fal.realtime.connect()`. " +
"See https://docs.fal.ai/model-apis/client#client-side-usage-with-token-provider for more information.");
return (0, auth_1.getTemporaryAuthToken)(app, config);
};
const effectiveExpiration = tokenProvider
? tokenExpirationSeconds
: auth_1.TOKEN_EXPIRATION_SECONDS;
const scheduleTokenRefresh = effectiveExpiration !== undefined
? () => {
clearTimeout(tokenRefreshTimer);
const refreshMs = Math.round(effectiveExpiration * 0.9 * 1000);
tokenRefreshTimer = setTimeout(() => {
if (generation !== tokenRefreshGeneration) {
return;
}
fetchToken()
.then((newToken) => {
if (generation !== tokenRefreshGeneration) {
return;
}
queueMicrotask(() => {
send({ type: "authenticated", token: newToken });
});
scheduleTokenRefresh();
})
.catch(() => {
if (generation !== tokenRefreshGeneration) {
return;
}
const retryMs = Math.round(effectiveExpiration * 0.05 * 1000);
tokenRefreshTimer = setTimeout(() => {
scheduleTokenRefresh();
}, retryMs);
});
}, refreshMs);
}
: noop;
fetchToken()
.then((token) => {
queueMicrotask(() => {
send({ type: "authenticated", token });
});
scheduleTokenRefresh();
})
.catch((error) => {
queueMicrotask(() => {
send({ type: "unauthorized", error });
});
});
}
if (machine.current === "connecting" &&
previousState !== machine.current &&
token !== undefined) {
const ws = new WebSocket(buildRealtimeUrl(app, { token, maxBuffering, path }));
ws.onopen = () => {
var _a, _b;
send({ type: "connected", websocket: ws });
const queued = (_b = (_a = stateMachine.service.context) === null || _a === void 0 ? void 0 : _a.enqueuedMessage) !== null && _b !== void 0 ? _b : latestEnqueuedMessage;
if (queued) {
ws.send(encodeMessageFn(queued));
stateMachine.service.context = Object.assign(Object.assign({}, stateMachine.service.context), { enqueuedMessage: undefined });
}
};
ws.onclose = (event) => {
if (event.code !== WebSocketErrorCodes.NORMAL_CLOSURE) {
const { onError = noop } = getCallbacks();
onError(new response_1.ApiError({
message: `Error closing the connection: ${event.reason}`,
status: event.code,
}));
}
send({ type: "connectionClosed", code: event.code });
};
ws.onerror = (event) => {
// TODO specify error protocol for identified errors
const { onError = noop } = getCallbacks();
onError(new response_1.ApiError({ message: "Unknown error", status: 500 }));
};
ws.onmessage = (event) => {
const { decodeMessage = decodeMessageFn, onResult, onError = noop, } = getCallbacks();
handleRealtimeMessage({
data: event.data,
decodeMessage,
onResult,
onError,
send,
});
};
}
if (previousState === "active" && machine.current !== "active") {
clearTimeout(tokenRefreshTimer);
tokenRefreshTimer = undefined;
}
previousState = machine.current;
});
const send = (input) => {
// Use throttled send to avoid sending too many messages
stateMachine.throttledSend({
type: "send",
message: encodeMessageFn(input),
});
};
const close = () => {
stateMachine.service.send({ type: "close" });
};
return {
send,
close,
};
},
};
}
//# sourceMappingURL=realtime.js.map