@fal-ai/client
Version:
The fal.ai client for JavaScript and TypeScript
331 lines • 15.5 kB
JavaScript
"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());
});
};
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = {}, verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FalStream = void 0;
exports.createStreamingClient = createStreamingClient;
const eventsource_parser_1 = require("eventsource-parser");
const auth_1 = require("./auth");
const request_1 = require("./request");
const response_1 = require("./response");
const utils_1 = require("./utils");
const CONTENT_TYPE_EVENT_STREAM = "text/event-stream";
const EVENT_STREAM_TIMEOUT = 15 * 1000;
/**
* The class representing a streaming response. With t
*/
class FalStream {
constructor(endpointId, config, options) {
var _a;
// support for event listeners
this.listeners = new Map();
this.buffer = [];
// local state
this.currentData = undefined;
this.lastEventTimestamp = 0;
this.streamClosed = false;
this._requestId = null;
this.abortController = new AbortController();
this.start = () => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const { endpointId, options } = this;
const { input, method = "post", connectionMode = "server", tokenProvider, } = options;
try {
if (connectionMode === "client") {
// if we are in the browser, we need to get a temporary token
// to authenticate the request
const appId = (0, utils_1.ensureEndpointIdFormat)(endpointId);
const resolvedPath = (_a = (0, utils_1.resolveEndpointPath)(endpointId, undefined, "/stream")) !== null && _a !== void 0 ? _a : "";
const fetchToken = tokenProvider
? () => tokenProvider(`${appId}${resolvedPath}`)
: () => {
console.warn("[fal.stream] Using the default token provider is deprecated. " +
'Please provide a `tokenProvider` function when using `connectionMode: "client"`. ' +
"See https://docs.fal.ai/fal-client/authentication for more information.");
return (0, auth_1.getTemporaryAuthToken)(endpointId, this.config);
};
const token = yield fetchToken();
const { fetch } = this.config;
const parsedUrl = new URL(this.url);
parsedUrl.searchParams.set("fal_jwt_token", token);
const response = yield fetch(parsedUrl.toString(), {
method: method.toUpperCase(),
headers: {
accept: (_b = options.accept) !== null && _b !== void 0 ? _b : CONTENT_TYPE_EVENT_STREAM,
"content-type": "application/json",
},
body: input && method !== "get" ? JSON.stringify(input) : undefined,
signal: this.abortController.signal,
});
this._requestId = response.headers.get("x-fal-request-id");
return yield this.handleResponse(response);
}
return yield (0, request_1.dispatchRequest)({
method: method.toUpperCase(),
targetUrl: this.url,
input,
config: this.config,
options: {
headers: {
accept: (_c = options.accept) !== null && _c !== void 0 ? _c : CONTENT_TYPE_EVENT_STREAM,
},
responseHandler: (response) => __awaiter(this, void 0, void 0, function* () {
this._requestId = response.headers.get("x-fal-request-id");
return yield this.handleResponse(response);
}),
signal: this.abortController.signal,
},
});
}
catch (error) {
this.handleError(error);
}
});
this.handleResponse = (response) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!response.ok) {
try {
// we know the response failed, call the response handler
// so the exception gets converted to ApiError correctly
yield (0, response_1.defaultResponseHandler)(response);
}
catch (error) {
this.emit("error", error);
}
return;
}
const body = response.body;
if (!body) {
this.emit("error", new response_1.ApiError({
message: "Response body is empty.",
status: 400,
body: undefined,
requestId: this._requestId || undefined,
}));
return;
}
const isEventStream = ((_a = response.headers.get("content-type")) !== null && _a !== void 0 ? _a : "").startsWith(CONTENT_TYPE_EVENT_STREAM);
// any response that is not a text/event-stream will be handled as a binary stream
if (!isEventStream) {
const reader = body.getReader();
const emitRawChunk = () => {
reader.read().then(({ done, value }) => {
if (done) {
this.emit("done", this.currentData);
return;
}
this.buffer.push(value);
this.currentData = value;
this.emit("data", value);
emitRawChunk();
});
};
emitRawChunk();
return;
}
const decoder = new TextDecoder("utf-8");
const reader = response.body.getReader();
const parser = (0, eventsource_parser_1.createParser)((event) => {
if (event.type === "event") {
const data = event.data;
try {
const parsedData = JSON.parse(data);
this.buffer.push(parsedData);
this.currentData = parsedData;
this.emit("data", parsedData);
// also emit 'message'for backwards compatibility
this.emit("message", parsedData);
}
catch (e) {
this.emit("error", e);
}
}
});
const timeout = (_b = this.options.timeout) !== null && _b !== void 0 ? _b : EVENT_STREAM_TIMEOUT;
const readPartialResponse = () => __awaiter(this, void 0, void 0, function* () {
const { value, done } = yield reader.read();
this.lastEventTimestamp = Date.now();
parser.feed(decoder.decode(value));
if (Date.now() - this.lastEventTimestamp > timeout) {
this.emit("error", new response_1.ApiError({
message: `Event stream timed out after ${(timeout / 1000).toFixed(0)} seconds with no messages.`,
status: 408,
requestId: this._requestId || undefined,
}));
}
if (!done) {
readPartialResponse().catch(this.handleError);
}
else {
this.emit("done", this.currentData);
}
});
readPartialResponse().catch(this.handleError);
return;
});
this.handleError = (error) => {
var _a;
// In case AbortError is thrown but the signal is marked as aborted
// it means the user called abort() and we should not emit an error
// as it's expected behavior
// See note on: https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
if (error.name === "AbortError" || this.signal.aborted) {
return;
}
const apiError = error instanceof response_1.ApiError
? error
: new response_1.ApiError({
message: (_a = error.message) !== null && _a !== void 0 ? _a : "An unknown error occurred",
status: 500,
requestId: this._requestId || undefined,
});
this.emit("error", apiError);
return;
};
this.on = (type, listener) => {
var _a;
if (!this.listeners.has(type)) {
this.listeners.set(type, []);
}
(_a = this.listeners.get(type)) === null || _a === void 0 ? void 0 : _a.push(listener);
};
this.emit = (type, event) => {
const listeners = this.listeners.get(type) || [];
for (const listener of listeners) {
listener(event);
}
};
/**
* Gets a reference to the `Promise` that indicates whether the streaming
* is done or not. Developers should always call this in their apps to ensure
* the request is over.
*
* An alternative to this, is to use `on('done')` in case your application
* architecture works best with event listeners.
*
* @returns the promise that resolves when the request is done.
*/
this.done = () => __awaiter(this, void 0, void 0, function* () { return this.donePromise; });
/**
* Aborts the streaming request.
*
* **Note:** This method is noop in case the request is already done.
*
* @param reason optional cause for aborting the request.
*/
this.abort = (reason) => {
if (!this.streamClosed) {
this.abortController.abort(reason);
}
};
this.endpointId = endpointId;
this.config = config;
this.url =
(_a = options.url) !== null && _a !== void 0 ? _a : (0, request_1.buildUrl)(endpointId, {
path: (0, utils_1.resolveEndpointPath)(endpointId, undefined, "/stream"),
query: options.queryParams,
});
this.options = options;
this.donePromise = new Promise((resolve, reject) => {
if (this.streamClosed) {
reject(new response_1.ApiError({
message: "Streaming connection is already closed.",
status: 400,
body: undefined,
requestId: this._requestId || undefined,
}));
}
this.signal.addEventListener("abort", () => {
var _a;
resolve((_a = this.currentData) !== null && _a !== void 0 ? _a : {});
});
this.on("done", (data) => {
this.streamClosed = true;
resolve(data);
});
this.on("error", (error) => {
this.streamClosed = true;
reject(error);
});
});
// if a abort signal was passed, sync it with the internal one
if (options.signal) {
options.signal.addEventListener("abort", () => {
this.abortController.abort();
});
}
// start the streaming request
this.start().catch(this.handleError);
}
[Symbol.asyncIterator]() {
return __asyncGenerator(this, arguments, function* _a() {
let running = true;
const stopAsyncIterator = () => (running = false);
this.on("error", stopAsyncIterator);
this.on("done", stopAsyncIterator);
while (running || this.buffer.length > 0) {
const data = this.buffer.shift();
if (data) {
yield yield __await(data);
}
// the short timeout ensures the while loop doesn't block other
// frames getting executed concurrently
yield __await(new Promise((resolve) => setTimeout(resolve, 16)));
}
});
}
/**
* Gets the `AbortSignal` instance that can be used to listen for abort events.
*
* **Note:** this signal is internal to the `FalStream` instance. If you pass your
* own abort signal, the `FalStream` will listen to it and abort it appropriately.
*
* @returns the `AbortSignal` instance.
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
*/
get signal() {
return this.abortController.signal;
}
/**
* Gets the request id of the streaming request.
*
* @returns the request id.
*/
get requestId() {
return this._requestId;
}
}
exports.FalStream = FalStream;
function createStreamingClient({ config, storage, }) {
return {
stream(endpointId, options) {
return __awaiter(this, void 0, void 0, function* () {
const input = options.input
? yield storage.transformInput(options.input)
: undefined;
return new FalStream(endpointId, config, Object.assign(Object.assign({}, options), { input: input }));
});
},
};
}
//# sourceMappingURL=streaming.js.map