@fal-ai/serverless-client
Version:
Deprecation note: this library has been deprecated in favor of @fal-ai/client
254 lines • 12.7 kB
JavaScript
;
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