fal-js/libs/client/src/function.ts
Daniel Rochetti 9a0497da30
feat: client proxy + nextjs support (#9)
* feat(client): add function alias support

* feat(client): auto config from env vars

* fix(client): username in aliased url

* feat: nextjs proxy handler

* fix: update client tests

* feat(client): built-in queue support

* feat: client + nextjs e2e demo

* chore: update browserslist

* chore: update version info

* chore: update readme badges

* fix: ignore lint error on require
2023-09-11 16:26:06 -07:00

181 lines
5.5 KiB
TypeScript

import { getConfig } from './config';
import { getUserAgent, isBrowser } from './runtime';
import { EnqueueResult, QueueStatus } from './types';
import { isUUIDv4, isValidUrl } from './utils';
/**
* The function input and other configuration when running
* the function, such as the HTTP method to use.
*/
type RunOptions<Input> = {
/**
* The path to the function, if any. Defaults to `/`.
*/
readonly path?: string;
/**
* The function input. It will be submitted either as query params
* or the body payload, depending on the `method`.
*/
readonly input?: Input;
/**
* The HTTP method, defaults to `post`;
*/
readonly method?: 'get' | 'post' | 'put' | 'delete' | string;
};
/**
* Builds the final url to run the function based on its `id` or alias and
* a the options from `RunOptions<Input>`.
*
* @private
* @param id the function id or alias
* @param options the run options
* @returns the final url to run the function
*/
export function buildUrl<Input>(
id: string,
options: RunOptions<Input> = {}
): string {
const { host } = getConfig();
const method = (options.method ?? 'post').toLowerCase();
const path = (options.path ?? '').replace(/^\//, '').replace(/\/{2,}/, '/');
const params =
method === 'get' ? new URLSearchParams(options.input ?? {}) : undefined;
// TODO: change to params.size once it's officially supported
const queryParams = params && params['size'] ? `?${params.toString()}` : '';
const parts = id.split('/');
// if a fal.ai url is passed, just use it
if (isValidUrl(id)) {
const url = id.endsWith('/') ? id : `${id}/`;
return `${url}${path}${queryParams}`;
}
if (parts.length === 2 && isUUIDv4(parts[1])) {
return `https://${host}/trigger/${id}/${path}${queryParams}`;
}
return `https://${id}.${host}/${path}${queryParams}`;
}
/**
* Runs a fal serverless function identified by its `id`.
* TODO: expand documentation and provide examples
*
* @param id the registered function revision id or alias.
* @returns the remote function output
*/
export async function run<Input, Output>(
id: string,
options: RunOptions<Input> = {}
): Promise<Output> {
const { credentials, requestMiddleware, responseHandler } = getConfig();
const method = (options.method ?? 'post').toLowerCase();
const userAgent = isBrowser() ? {} : { 'User-Agent': getUserAgent() };
const { keyId, keySecret } =
typeof credentials === 'function' ? credentials() : credentials;
const { url, headers } = await requestMiddleware({
url: buildUrl(id, options),
});
const authHeader =
keyId && keySecret ? { Authorization: `Key ${keyId}:${keySecret}` } : {};
const requestHeaders = {
...authHeader,
Accept: 'application/json',
'Content-Type': 'application/json',
...userAgent,
...(headers ?? {}),
} as HeadersInit;
const response = await fetch(url, {
method,
headers: requestHeaders,
mode: 'same-origin',
credentials: 'same-origin',
body:
method !== 'get' && options.input
? JSON.stringify(options.input)
: undefined,
});
return await responseHandler(response);
}
type QueueSubscribeOptions = {
pollInterval?: number;
onEnqueue?: (requestId: string) => void;
onQueueUpdate?: (status: QueueStatus) => void;
};
interface Queue {
submit<Input>(id: string, options: RunOptions<Input>): Promise<EnqueueResult>;
status(id: string, requestId: string): Promise<QueueStatus>;
result<Output>(id: string, requestId: string): Promise<Output>;
subscribe<Input, Output>(
id: string,
options: RunOptions<Input> & QueueSubscribeOptions
): Promise<Output>;
}
/**
* The fal run queue module. It allows to submit a function to the queue and get its result
* on a separate call. This is useful for long running functions that can be executed
* asynchronously and not .
*/
export const queue: Queue = {
async submit<Input>(
id: string,
options: RunOptions<Input>
): Promise<EnqueueResult> {
return run(id, { ...options, method: 'post', path: '/fal/queue/submit/' });
},
async status(id: string, requestId: string): Promise<QueueStatus> {
return run(id, {
method: 'get',
path: `/fal/queue/requests/${requestId}/status`,
});
},
async result<Output>(id: string, requestId: string): Promise<Output> {
return run(id, {
method: 'get',
path: `/fal/queue/requests/${requestId}/response`,
});
},
async subscribe<Input, Output>(
id: string,
options: RunOptions<Input> & QueueSubscribeOptions = {}
): Promise<Output> {
const { request_id: requestId } = await queue.submit(id, options);
if (options.onEnqueue) {
options.onEnqueue(requestId);
}
return new Promise<Output>((resolve, reject) => {
let timeoutId: ReturnType<typeof setTimeout>;
const pollInterval = options.pollInterval ?? 1000;
const poll = async () => {
try {
const requestStatus = await queue.status(id, requestId);
if (options.onQueueUpdate) {
options.onQueueUpdate(requestStatus);
}
if (requestStatus.status === 'COMPLETED') {
clearTimeout(timeoutId);
try {
const result = await queue.result<Output>(id, requestId);
resolve(result);
} catch (error) {
reject(error);
}
return;
}
timeoutId = setTimeout(poll, pollInterval);
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
};
timeoutId = setTimeout(poll, pollInterval);
});
},
};