UNPKG

@fal-ai/serverless-client

Version:

Deprecation note: this library has been deprecated in favor of @fal-ai/client

254 lines 12.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.realtimeImpl = void 0; /* 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 { 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)("expireToken", "idle", (0, robot3_1.reduce)(expireToken)), (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)("unauthorized", "idle", (0, robot3_1.reduce)(expireToken)), (0, robot3_1.transition)("connectionClosed", "idle", (0, robot3_1.reduce)(closeConnection)), (0, robot3_1.transition)("close", "idle", (0, robot3_1.reduce)(closeConnection))), failed: (0, robot3_1.state)((0, robot3_1.transition)("send", "failed"), (0, robot3_1.transition)("close", "idle", (0, robot3_1.reduce)(closeConnection))), }, initialState); function buildRealtimeUrl(app, { token, maxBuffering }) { 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.ensureAppIdFormat)(app); return `wss://fal.run/${appId}/realtime?${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 machine = (0, robot3_1.interpret)(connectionStateMachine, onChange); connectionCache.set(key, Object.assign(Object.assign({}, machine), { throttledSend: throttleInterval > 0 ? (0, utils_1.throttle)(machine.send, throttleInterval, true) : machine.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"; } /** * The default implementation of the realtime client. */ exports.realtimeImpl = { 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, throttleInterval = DEFAULT_THROTTLE_INTERVAL, } = handler; if (clientOnly && !(0, runtime_1.isBrowser)()) { return NoOpConnection; } let previousState; // 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, { onError: handler.onError, onResult: handler.onResult, }); const getCallbacks = () => connectionCallbacks.get(connectionKey); const stateMachine = reuseInterpreter(connectionKey, throttleInterval, ({ context, machine, send }) => { const { enqueuedMessage, token } = context; if (machine.current === "active" && enqueuedMessage) { send({ type: "send", message: enqueuedMessage }); } if (machine.current === "authRequired" && token === undefined && previousState !== machine.current) { send({ type: "initiateAuth" }); (0, auth_1.getTemporaryAuthToken)(app) .then((token) => { send({ type: "authenticated", token }); const tokenExpirationTimeout = Math.round(auth_1.TOKEN_EXPIRATION_SECONDS * 0.9 * 1000); setTimeout(() => { send({ type: "expireToken" }); }, tokenExpirationTimeout); }) .catch((error) => { send({ type: "unauthorized", error }); }); } if (machine.current === "connecting" && previousState !== machine.current && token !== undefined) { const ws = new WebSocket(buildRealtimeUrl(app, { token, maxBuffering })); ws.onopen = () => { send({ type: "connected", websocket: ws }); }; 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 { onResult } = getCallbacks(); // Handle binary messages as msgpack messages if (event.data instanceof ArrayBuffer) { const result = (0, msgpack_1.decode)(new Uint8Array(event.data)); onResult(result); return; } if (event.data instanceof Uint8Array) { const result = (0, msgpack_1.decode)(event.data); onResult(result); return; } if (event.data instanceof Blob) { event.data.arrayBuffer().then((buffer) => { const result = (0, msgpack_1.decode)(new Uint8Array(buffer)); onResult(result); }); return; } // Otherwise handle strings as plain JSON messages const data = JSON.parse(event.data); // 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(data)) { send({ type: "unauthorized", error: new Error("Unauthorized") }); return; } if (isSuccessfulResult(data)) { onResult(data); return; } if (isFalErrorResult(data)) { if (data.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; } const { onError = noop } = getCallbacks(); onError(new response_1.ApiError({ message: `${data.error}: ${data.reason}`, // TODO better error status code status: 400, body: data, })); return; } }; } previousState = machine.current; }); const send = (input) => { // Use throttled send to avoid sending too many messages var _a; const message = input instanceof Uint8Array ? input : Object.assign(Object.assign({}, input), { request_id: (_a = input["request_id"]) !== null && _a !== void 0 ? _a : crypto.randomUUID() }); stateMachine.throttledSend({ type: "send", message, }); }; const close = () => { stateMachine.send({ type: "close" }); }; return { send, close, }; }, }; //# sourceMappingURL=realtime.js.map