UNPKG

@fal-ai/client

Version:

The fal.ai client for JavaScript and TypeScript

366 lines 18.6 kB
"use strict"; 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