diff --git a/README.md b/README.md index 8b384ea..cc40934 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # The fal.ai JS client -![@fal-ai/serverless-client npm package](https://img.shields.io/npm/v/@fal-ai/serverless-client?color=%237527D7&label=client&style=flat-square) -![@fal-ai/serverless-proxy npm package](https://img.shields.io/npm/v/@fal-ai/serverless-proxy?color=%237527D7&label=proxy&style=flat-square) +![@fal-ai/client npm package](https://img.shields.io/npm/v/@fal-ai/client?color=%237527D7&label=client&style=flat-square) +![@fal-ai/server-proxy npm package](https://img.shields.io/npm/v/@fal-ai/server-proxy?color=%237527D7&label=proxy&style=flat-square) ![Build](https://img.shields.io/github/actions/workflow/status/fal-ai/fal-js/build.yml?style=flat-square) ![License](https://img.shields.io/github/license/fal-ai/fal-js?style=flat-square) ## About the Project -The fal serverless JavaScript/TypeScript Client is a robust and user-friendly library designed for seamless integration of fal serverless functions in Web, Node.js, and React Native applications. Developed in TypeScript, it provides developers with type safety right from the start. +The fal JavaScript/TypeScript Client is a robust and user-friendly library designed for seamless integration of fal endpoints in Web, Node.js, and React Native applications. Developed in TypeScript, it provides developers with type safety right from the start. ## Getting Started -The `@fal-ai/serverless-client` library serves as a client for fal serverless Python functions. For guidance on creating your functions, refer to the [quickstart guide](https://fal.ai/docs). +The `@fal-ai/client` library serves as a client for fal apps hosted on fal. For guidance on consuming and creating apps, refer to the [quickstart guide](https://fal.ai/docs). ### Client Library @@ -22,12 +22,12 @@ This client library is crafted as a lightweight layer atop platform standards li 1. Install the client library ```sh - npm install --save @fal-ai/serverless-client + npm install --save @fal-ai/client ``` 2. Start by configuring your credentials: ```ts - import * as fal from "@fal-ai/serverless-client"; + import { fal } from "@fal-ai/client"; fal.config({ // Can also be auto-configured using environment variables: @@ -46,21 +46,21 @@ See the available [model APIs](https://fal.ai/models) for more details. ### The fal client proxy -Although the fal client is designed to work in any JS environment, including directly in your browser, **it is not recommended** to store your credentials in your client source code. The common practice is to use your own server to serve as a proxy to serverless APIs. Luckily fal supports that out-of-the-box with plug-and-play proxy functions for the most common engines/frameworks. +Although the fal client is designed to work in any JS environment, including directly in your browser, **it is not recommended** to store your credentials in your client source code. The common practice is to use your own server to serve as a proxy to fal APIs. Luckily fal supports that out-of-the-box with plug-and-play proxy functions for the most common engines/frameworks. For example, if you are using Next.js, you can: 1. Instal the proxy library ```sh - npm install --save @fal-ai/serverless-proxy + npm install --save @fal-ai/server-proxy ``` 2. Add the proxy as an API endpoint of your app, see an example here in [pages/api/fal/proxy.ts](https://github.com/fal-ai/fal-js/blob/main/apps/demo-nextjs-page-router/pages/api/fal/proxy.ts) ```ts - export { handler as default } from "@fal-ai/serverless-proxy/nextjs"; + export { handler as default } from "@fal-ai/server-proxy/nextjs"; ``` 3. Configure the client to use the proxy: ```ts - import * as fal from "@fal-ai/serverless-client"; + import { fal } from "@fal-ai/client"; fal.config({ proxyUrl: "/api/fal/proxy", }); diff --git a/apps/demo-express-app/src/main.ts b/apps/demo-express-app/src/main.ts index 5b19b5f..5d63e92 100644 --- a/apps/demo-express-app/src/main.ts +++ b/apps/demo-express-app/src/main.ts @@ -3,8 +3,8 @@ * This is only a minimal backend to get started. */ -import * as fal from "@fal-ai/serverless-client"; -import * as falProxy from "@fal-ai/serverless-proxy/express"; +import { fal } from "@fal-ai/client"; +import * as falProxy from "@fal-ai/server-proxy/express"; import cors from "cors"; import { configDotenv } from "dotenv"; import express from "express"; diff --git a/apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts b/apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts index db10d70..e5ea428 100644 --- a/apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts +++ b/apps/demo-nextjs-app-router/app/api/fal/proxy/route.ts @@ -1,3 +1,3 @@ -import { route } from "@fal-ai/serverless-proxy/nextjs"; +import { route } from "@fal-ai/server-proxy/nextjs"; export const { GET, POST, PUT } = route; diff --git a/apps/demo-nextjs-app-router/app/camera-turbo/page.tsx b/apps/demo-nextjs-app-router/app/camera-turbo/page.tsx index ce74a8d..8f61e79 100644 --- a/apps/demo-nextjs-app-router/app/camera-turbo/page.tsx +++ b/apps/demo-nextjs-app-router/app/camera-turbo/page.tsx @@ -1,10 +1,10 @@ /* eslint-disable @next/next/no-img-element */ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { MutableRefObject, useEffect, useRef, useState } from "react"; -fal.config({ +const fal = createFalClient({ proxyUrl: "/api/fal/proxy", }); diff --git a/apps/demo-nextjs-app-router/app/comfy/image-to-image/page.tsx b/apps/demo-nextjs-app-router/app/comfy/image-to-image/page.tsx index c8e9ab6..4be2197 100644 --- a/apps/demo-nextjs-app-router/app/comfy/image-to-image/page.tsx +++ b/apps/demo-nextjs-app-router/app/comfy/image-to-image/page.tsx @@ -1,16 +1,14 @@ +/* eslint-disable @next/next/no-img-element */ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { useMemo, useState } from "react"; -// @snippet:start(client.config) -fal.config({ +const fal = createFalClient({ proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy }); -// @snippet:end -// @snippet:start(client.result.type) type Image = { filename: string; subfolder: string; @@ -18,12 +16,11 @@ type Image = { url: string; }; -type Result = { +type ComfyOutput = { url: string; outputs: Record[]; images: Image[]; }; -// @snippet:end type ErrorProps = { error: any; @@ -54,7 +51,7 @@ export default function ComfyImageToImagePage() { // Result state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [logs, setLogs] = useState([]); const [elapsedTime, setElapsedTime] = useState(0); // @snippet:end @@ -73,7 +70,7 @@ export default function ComfyImageToImagePage() { setElapsedTime(0); }; - const getImageURL = (result: Result) => { + const getImageURL = (result: ComfyOutput) => { return result.outputs[9].images[0]; }; @@ -83,7 +80,7 @@ export default function ComfyImageToImagePage() { setLoading(true); const start = Date.now(); try { - const result: Result = await fal.subscribe( + const { data } = await fal.subscribe( "comfy/fal-ai/image-to-image", { input: { @@ -102,7 +99,7 @@ export default function ComfyImageToImagePage() { }, }, ); - setResult(getImageURL(result)); + setResult(getImageURL(data)); } catch (error: any) { setError(error); } finally { diff --git a/apps/demo-nextjs-app-router/app/comfy/image-to-video/page.tsx b/apps/demo-nextjs-app-router/app/comfy/image-to-video/page.tsx index 11fb365..af75f01 100644 --- a/apps/demo-nextjs-app-router/app/comfy/image-to-video/page.tsx +++ b/apps/demo-nextjs-app-router/app/comfy/image-to-video/page.tsx @@ -1,16 +1,13 @@ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { useMemo, useState } from "react"; -// @snippet:start(client.config) -fal.config({ +const fal = createFalClient({ proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy }); -// @snippet:end -// @snippet:start(client.result.type) type Image = { filename: string; subfolder: string; @@ -18,12 +15,11 @@ type Image = { url: string; }; -type Result = { +type ComfyOutput = { url: string; outputs: Record[]; images: Image[]; }; -// @snippet:end type ErrorProps = { error: any; @@ -50,7 +46,7 @@ export default function ComfyImageToVideoPage() { // Result state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [logs, setLogs] = useState([]); const [elapsedTime, setElapsedTime] = useState(0); // @snippet:end @@ -69,7 +65,7 @@ export default function ComfyImageToVideoPage() { setElapsedTime(0); }; - const getImageURL = (result: Result) => { + const getImageURL = (result: ComfyOutput) => { return result.outputs[10].images[0]; }; @@ -79,7 +75,7 @@ export default function ComfyImageToVideoPage() { setLoading(true); const start = Date.now(); try { - const result: Result = await fal.subscribe( + const { data } = await fal.subscribe( "comfy/fal-ai/image-to-video", { input: { @@ -97,7 +93,7 @@ export default function ComfyImageToVideoPage() { }, }, ); - setResult(getImageURL(result)); + setResult(getImageURL(data)); } catch (error: any) { setError(error); } finally { diff --git a/apps/demo-nextjs-app-router/app/comfy/text-to-image/page.tsx b/apps/demo-nextjs-app-router/app/comfy/text-to-image/page.tsx index 21dd7d4..0545844 100644 --- a/apps/demo-nextjs-app-router/app/comfy/text-to-image/page.tsx +++ b/apps/demo-nextjs-app-router/app/comfy/text-to-image/page.tsx @@ -1,16 +1,13 @@ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { useMemo, useState } from "react"; -// @snippet:start(client.config) -fal.config({ +const fal = createFalClient({ proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy }); -// @snippet:end -// @snippet:start(client.result.type) type Image = { filename: string; subfolder: string; @@ -18,12 +15,11 @@ type Image = { url: string; }; -type Result = { +type ComfyOutput = { url: string; outputs: Record[]; images: Image[]; }; -// @snippet:end type ErrorProps = { error: any; @@ -53,7 +49,7 @@ export default function ComfyTextToImagePage() { // Result state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [logs, setLogs] = useState([]); const [elapsedTime, setElapsedTime] = useState(0); // @snippet:end @@ -72,7 +68,7 @@ export default function ComfyTextToImagePage() { setElapsedTime(0); }; - const getImageURL = (result: Result) => { + const getImageURL = (result: ComfyOutput) => { return result.outputs[9].images[0]; }; @@ -82,22 +78,25 @@ export default function ComfyTextToImagePage() { setLoading(true); const start = Date.now(); try { - const result: Result = await fal.subscribe("comfy/fal-ai/text-to-image", { - input: { - prompt: prompt, + const { data } = await fal.subscribe( + "comfy/fal-ai/text-to-image", + { + input: { + prompt: prompt, + }, + logs: true, + onQueueUpdate(update) { + setElapsedTime(Date.now() - start); + if ( + update.status === "IN_PROGRESS" || + update.status === "COMPLETED" + ) { + setLogs((update.logs || []).map((log) => log.message)); + } + }, }, - logs: true, - onQueueUpdate(update) { - setElapsedTime(Date.now() - start); - if ( - update.status === "IN_PROGRESS" || - update.status === "COMPLETED" - ) { - setLogs((update.logs || []).map((log) => log.message)); - } - }, - }); - setResult(getImageURL(result)); + ); + setResult(getImageURL(data)); } catch (error: any) { setError(error); } finally { diff --git a/apps/demo-nextjs-app-router/app/page.tsx b/apps/demo-nextjs-app-router/app/page.tsx index 2b227f6..9d841e3 100644 --- a/apps/demo-nextjs-app-router/app/page.tsx +++ b/apps/demo-nextjs-app-router/app/page.tsx @@ -1,23 +1,20 @@ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { useMemo, useState } from "react"; -// @snippet:start(client.config) -fal.config({ +const fal = createFalClient({ // credentials: 'FAL_KEY_ID:FAL_KEY_SECRET', proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy }); -// @snippet:end -// @snippet:start(client.result.type) type Image = { url: string; file_name: string; file_size: number; }; -type Result = { +type Output = { image: Image; }; // @snippet:end @@ -51,7 +48,7 @@ export default function Home() { // Result state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [logs, setLogs] = useState([]); const [elapsedTime, setElapsedTime] = useState(0); // @snippet:end @@ -79,7 +76,7 @@ export default function Home() { setLoading(true); const start = Date.now(); try { - const result: Result = await fal.subscribe("fal-ai/illusion-diffusion", { + const result = await fal.subscribe("fal-ai/illusion-diffusion", { input: { prompt, image_url: imageFile, @@ -96,7 +93,7 @@ export default function Home() { } }, }); - setResult(result); + setResult(result.data); } catch (error: any) { setError(error); } finally { diff --git a/apps/demo-nextjs-app-router/app/queue/page.tsx b/apps/demo-nextjs-app-router/app/queue/page.tsx index 68a4513..eb81cd0 100644 --- a/apps/demo-nextjs-app-router/app/queue/page.tsx +++ b/apps/demo-nextjs-app-router/app/queue/page.tsx @@ -1,6 +1,6 @@ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { fal } from "@fal-ai/client"; import { useState } from "react"; fal.config({ @@ -54,7 +54,7 @@ export default function Home() { setLoading(true); const start = Date.now(); try { - const result: any = await fal.subscribe(endpointId, { + const result = await fal.subscribe(endpointId, { input: JSON.parse(input), logs: true, // mode: "streaming", diff --git a/apps/demo-nextjs-app-router/app/realtime/page.tsx b/apps/demo-nextjs-app-router/app/realtime/page.tsx index dc6cda9..58df97e 100644 --- a/apps/demo-nextjs-app-router/app/realtime/page.tsx +++ b/apps/demo-nextjs-app-router/app/realtime/page.tsx @@ -1,11 +1,11 @@ "use client"; /* eslint-disable @next/next/no-img-element */ -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { ChangeEvent, useRef, useState } from "react"; import { DrawingCanvas } from "../../components/drawing"; -fal.config({ +const fal = createFalClient({ proxyUrl: "/api/fal/proxy", }); diff --git a/apps/demo-nextjs-app-router/app/streaming/page.tsx b/apps/demo-nextjs-app-router/app/streaming/page.tsx index 3335b4d..245e7f2 100644 --- a/apps/demo-nextjs-app-router/app/streaming/page.tsx +++ b/apps/demo-nextjs-app-router/app/streaming/page.tsx @@ -1,9 +1,9 @@ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { useState } from "react"; -fal.config({ +const fal = createFalClient({ proxyUrl: "/api/fal/proxy", }); @@ -29,7 +29,7 @@ export default function StreamingDemo() { const [streamStatus, setStreamStatus] = useState("idle"); const runInference = async () => { - const stream = await fal.stream( + const stream = await fal.stream( "fal-ai/llavav15-13b", { input: { diff --git a/apps/demo-nextjs-app-router/app/whisper/page.tsx b/apps/demo-nextjs-app-router/app/whisper/page.tsx index b40afb8..561daa8 100644 --- a/apps/demo-nextjs-app-router/app/whisper/page.tsx +++ b/apps/demo-nextjs-app-router/app/whisper/page.tsx @@ -1,9 +1,9 @@ "use client"; -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { useCallback, useMemo, useState } from "react"; -fal.config({ +const fal = createFalClient({ // credentials: 'FAL_KEY_ID:FAL_KEY_SECRET', proxyUrl: "/api/fal/proxy", }); diff --git a/apps/demo-nextjs-page-router/pages/api/fal/proxy.ts b/apps/demo-nextjs-page-router/pages/api/fal/proxy.ts index ca598dc..4d149bc 100644 --- a/apps/demo-nextjs-page-router/pages/api/fal/proxy.ts +++ b/apps/demo-nextjs-page-router/pages/api/fal/proxy.ts @@ -1,3 +1,3 @@ // @snippet:start("client.proxy.nextjs") -export { handler as default } from "@fal-ai/serverless-proxy/nextjs"; +export { handler as default } from "@fal-ai/server-proxy/nextjs"; // @snippet:end diff --git a/apps/demo-nextjs-page-router/pages/index.tsx b/apps/demo-nextjs-page-router/pages/index.tsx index 896174c..5d277e3 100644 --- a/apps/demo-nextjs-page-router/pages/index.tsx +++ b/apps/demo-nextjs-page-router/pages/index.tsx @@ -1,8 +1,8 @@ -import * as fal from "@fal-ai/serverless-client"; +import { createFalClient } from "@fal-ai/client"; import { useMemo, useState } from "react"; // @snippet:start(client.config) -fal.config({ +const fal = createFalClient({ proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy // proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy }); @@ -14,7 +14,7 @@ type Image = { file_name: string; file_size: number; }; -type Result = { +type Output = { images: Image[]; }; // @snippet:end @@ -47,7 +47,7 @@ export function Index() { // Result state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [logs, setLogs] = useState([]); const [elapsedTime, setElapsedTime] = useState(0); // @snippet:end @@ -72,7 +72,7 @@ export function Index() { setLoading(true); const start = Date.now(); try { - const result: Result = await fal.subscribe("fal-ai/lora", { + const result = await fal.subscribe("fal-ai/lora", { input: { prompt, model_name: "stabilityai/stable-diffusion-xl-base-1.0", @@ -89,7 +89,7 @@ export function Index() { } }, }); - setResult(result); + setResult(result.data); } catch (error: any) { setError(error); } finally { diff --git a/docs/reference/assets/icons.js b/docs/reference/assets/icons.js index c27b1d1..e88e8ca 100644 --- a/docs/reference/assets/icons.js +++ b/docs/reference/assets/icons.js @@ -1,21 +1,18 @@ -(function () { - addIcons(); - function addIcons() { - if (document.readyState === "loading") - return document.addEventListener("DOMContentLoaded", addIcons); - const svg = document.body.appendChild( - document.createElementNS("http://www.w3.org/2000/svg", "svg"), - ); - svg.innerHTML = `""`; - svg.style.display = "none"; - if (location.protocol === "file:") updateUseElements(); - } +(function() { + addIcons(); + function addIcons() { + if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); + const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); + svg.innerHTML = `""`; + svg.style.display = "none"; + if (location.protocol === "file:") updateUseElements(); + } - function updateUseElements() { - document.querySelectorAll("use").forEach((el) => { - if (el.getAttribute("href").includes("#icon-")) { - el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); - } - }); - } -})(); + function updateUseElements() { + document.querySelectorAll("use").forEach(el => { + if (el.getAttribute("href").includes("#icon-")) { + el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); + } + }); + } +})() \ No newline at end of file diff --git a/docs/reference/assets/main.js b/docs/reference/assets/main.js index 22969f9..21a5d74 100644 --- a/docs/reference/assets/main.js +++ b/docs/reference/assets/main.js @@ -1,2162 +1,9 @@ "use strict"; -window.translations = { - copy: "Copy", - copied: "Copied!", - normally_hidden: - "This member is normally hidden due to your filter settings.", -}; -("use strict"); -(() => { - var Pe = Object.create; - var ie = Object.defineProperty; - var Oe = Object.getOwnPropertyDescriptor; - var _e = Object.getOwnPropertyNames; - var Re = Object.getPrototypeOf, - Me = Object.prototype.hasOwnProperty; - var Fe = (t, e) => () => ( - e || t((e = { exports: {} }).exports, e), e.exports - ); - var De = (t, e, n, r) => { - if ((e && typeof e == "object") || typeof e == "function") - for (let i of _e(e)) - !Me.call(t, i) && - i !== n && - ie(t, i, { - get: () => e[i], - enumerable: !(r = Oe(e, i)) || r.enumerable, - }); - return t; - }; - var Ae = (t, e, n) => ( - (n = t != null ? Pe(Re(t)) : {}), - De( - e || !t || !t.__esModule - ? ie(n, "default", { value: t, enumerable: !0 }) - : n, - t, - ) - ); - var ue = Fe((ae, le) => { - (function () { - var t = function (e) { - var n = new t.Builder(); - return ( - n.pipeline.add(t.trimmer, t.stopWordFilter, t.stemmer), - n.searchPipeline.add(t.stemmer), - e.call(n, n), - n.build() - ); - }; - t.version = "2.3.9"; - (t.utils = {}), - (t.utils.warn = (function (e) { - return function (n) { - e.console && console.warn && console.warn(n); - }; - })(this)), - (t.utils.asString = function (e) { - return e == null ? "" : e.toString(); - }), - (t.utils.clone = function (e) { - if (e == null) return e; - for ( - var n = Object.create(null), r = Object.keys(e), i = 0; - i < r.length; - i++ - ) { - var s = r[i], - o = e[s]; - if (Array.isArray(o)) { - n[s] = o.slice(); - continue; - } - if ( - typeof o == "string" || - typeof o == "number" || - typeof o == "boolean" - ) { - n[s] = o; - continue; - } - throw new TypeError( - "clone is not deep and does not support nested objects", - ); - } - return n; - }), - (t.FieldRef = function (e, n, r) { - (this.docRef = e), (this.fieldName = n), (this._stringValue = r); - }), - (t.FieldRef.joiner = "/"), - (t.FieldRef.fromString = function (e) { - var n = e.indexOf(t.FieldRef.joiner); - if (n === -1) throw "malformed field ref string"; - var r = e.slice(0, n), - i = e.slice(n + 1); - return new t.FieldRef(i, r, e); - }), - (t.FieldRef.prototype.toString = function () { - return ( - this._stringValue == null && - (this._stringValue = - this.fieldName + t.FieldRef.joiner + this.docRef), - this._stringValue - ); - }); - (t.Set = function (e) { - if (((this.elements = Object.create(null)), e)) { - this.length = e.length; - for (var n = 0; n < this.length; n++) this.elements[e[n]] = !0; - } else this.length = 0; - }), - (t.Set.complete = { - intersect: function (e) { - return e; - }, - union: function () { - return this; - }, - contains: function () { - return !0; - }, - }), - (t.Set.empty = { - intersect: function () { - return this; - }, - union: function (e) { - return e; - }, - contains: function () { - return !1; - }, - }), - (t.Set.prototype.contains = function (e) { - return !!this.elements[e]; - }), - (t.Set.prototype.intersect = function (e) { - var n, - r, - i, - s = []; - if (e === t.Set.complete) return this; - if (e === t.Set.empty) return e; - this.length < e.length - ? ((n = this), (r = e)) - : ((n = e), (r = this)), - (i = Object.keys(n.elements)); - for (var o = 0; o < i.length; o++) { - var a = i[o]; - a in r.elements && s.push(a); - } - return new t.Set(s); - }), - (t.Set.prototype.union = function (e) { - return e === t.Set.complete - ? t.Set.complete - : e === t.Set.empty - ? this - : new t.Set( - Object.keys(this.elements).concat(Object.keys(e.elements)), - ); - }), - (t.idf = function (e, n) { - var r = 0; - for (var i in e) i != "_index" && (r += Object.keys(e[i]).length); - var s = (n - r + 0.5) / (r + 0.5); - return Math.log(1 + Math.abs(s)); - }), - (t.Token = function (e, n) { - (this.str = e || ""), (this.metadata = n || {}); - }), - (t.Token.prototype.toString = function () { - return this.str; - }), - (t.Token.prototype.update = function (e) { - return (this.str = e(this.str, this.metadata)), this; - }), - (t.Token.prototype.clone = function (e) { - return ( - (e = - e || - function (n) { - return n; - }), - new t.Token(e(this.str, this.metadata), this.metadata) - ); - }); - (t.tokenizer = function (e, n) { - if (e == null || e == null) return []; - if (Array.isArray(e)) - return e.map(function (m) { - return new t.Token( - t.utils.asString(m).toLowerCase(), - t.utils.clone(n), - ); - }); - for ( - var r = e.toString().toLowerCase(), - i = r.length, - s = [], - o = 0, - a = 0; - o <= i; - o++ - ) { - var l = r.charAt(o), - u = o - a; - if (l.match(t.tokenizer.separator) || o == i) { - if (u > 0) { - var d = t.utils.clone(n) || {}; - (d.position = [a, u]), - (d.index = s.length), - s.push(new t.Token(r.slice(a, o), d)); - } - a = o + 1; - } - } - return s; - }), - (t.tokenizer.separator = /[\s\-]+/); - (t.Pipeline = function () { - this._stack = []; - }), - (t.Pipeline.registeredFunctions = Object.create(null)), - (t.Pipeline.registerFunction = function (e, n) { - n in this.registeredFunctions && - t.utils.warn("Overwriting existing registered function: " + n), - (e.label = n), - (t.Pipeline.registeredFunctions[e.label] = e); - }), - (t.Pipeline.warnIfFunctionNotRegistered = function (e) { - var n = e.label && e.label in this.registeredFunctions; - n || - t.utils.warn( - `Function is not registered with pipeline. This may cause problems when serialising the index. -`, - e, - ); - }), - (t.Pipeline.load = function (e) { - var n = new t.Pipeline(); - return ( - e.forEach(function (r) { - var i = t.Pipeline.registeredFunctions[r]; - if (i) n.add(i); - else throw new Error("Cannot load unregistered function: " + r); - }), - n - ); - }), - (t.Pipeline.prototype.add = function () { - var e = Array.prototype.slice.call(arguments); - e.forEach(function (n) { - t.Pipeline.warnIfFunctionNotRegistered(n), this._stack.push(n); - }, this); - }), - (t.Pipeline.prototype.after = function (e, n) { - t.Pipeline.warnIfFunctionNotRegistered(n); - var r = this._stack.indexOf(e); - if (r == -1) throw new Error("Cannot find existingFn"); - (r = r + 1), this._stack.splice(r, 0, n); - }), - (t.Pipeline.prototype.before = function (e, n) { - t.Pipeline.warnIfFunctionNotRegistered(n); - var r = this._stack.indexOf(e); - if (r == -1) throw new Error("Cannot find existingFn"); - this._stack.splice(r, 0, n); - }), - (t.Pipeline.prototype.remove = function (e) { - var n = this._stack.indexOf(e); - n != -1 && this._stack.splice(n, 1); - }), - (t.Pipeline.prototype.run = function (e) { - for (var n = this._stack.length, r = 0; r < n; r++) { - for (var i = this._stack[r], s = [], o = 0; o < e.length; o++) { - var a = i(e[o], o, e); - if (!(a == null || a === "")) - if (Array.isArray(a)) - for (var l = 0; l < a.length; l++) s.push(a[l]); - else s.push(a); - } - e = s; - } - return e; - }), - (t.Pipeline.prototype.runString = function (e, n) { - var r = new t.Token(e, n); - return this.run([r]).map(function (i) { - return i.toString(); - }); - }), - (t.Pipeline.prototype.reset = function () { - this._stack = []; - }), - (t.Pipeline.prototype.toJSON = function () { - return this._stack.map(function (e) { - return t.Pipeline.warnIfFunctionNotRegistered(e), e.label; - }); - }); - (t.Vector = function (e) { - (this._magnitude = 0), (this.elements = e || []); - }), - (t.Vector.prototype.positionForIndex = function (e) { - if (this.elements.length == 0) return 0; - for ( - var n = 0, - r = this.elements.length / 2, - i = r - n, - s = Math.floor(i / 2), - o = this.elements[s * 2]; - i > 1 && (o < e && (n = s), o > e && (r = s), o != e); - - ) - (i = r - n), - (s = n + Math.floor(i / 2)), - (o = this.elements[s * 2]); - if (o == e || o > e) return s * 2; - if (o < e) return (s + 1) * 2; - }), - (t.Vector.prototype.insert = function (e, n) { - this.upsert(e, n, function () { - throw "duplicate index"; - }); - }), - (t.Vector.prototype.upsert = function (e, n, r) { - this._magnitude = 0; - var i = this.positionForIndex(e); - this.elements[i] == e - ? (this.elements[i + 1] = r(this.elements[i + 1], n)) - : this.elements.splice(i, 0, e, n); - }), - (t.Vector.prototype.magnitude = function () { - if (this._magnitude) return this._magnitude; - for (var e = 0, n = this.elements.length, r = 1; r < n; r += 2) { - var i = this.elements[r]; - e += i * i; - } - return (this._magnitude = Math.sqrt(e)); - }), - (t.Vector.prototype.dot = function (e) { - for ( - var n = 0, - r = this.elements, - i = e.elements, - s = r.length, - o = i.length, - a = 0, - l = 0, - u = 0, - d = 0; - u < s && d < o; - - ) - (a = r[u]), - (l = i[d]), - a < l - ? (u += 2) - : a > l - ? (d += 2) - : a == l && ((n += r[u + 1] * i[d + 1]), (u += 2), (d += 2)); - return n; - }), - (t.Vector.prototype.similarity = function (e) { - return this.dot(e) / this.magnitude() || 0; - }), - (t.Vector.prototype.toArray = function () { - for ( - var e = new Array(this.elements.length / 2), n = 1, r = 0; - n < this.elements.length; - n += 2, r++ - ) - e[r] = this.elements[n]; - return e; - }), - (t.Vector.prototype.toJSON = function () { - return this.elements; - }); - (t.stemmer = (function () { - var e = { - ational: "ate", - tional: "tion", - enci: "ence", - anci: "ance", - izer: "ize", - bli: "ble", - alli: "al", - entli: "ent", - eli: "e", - ousli: "ous", - ization: "ize", - ation: "ate", - ator: "ate", - alism: "al", - iveness: "ive", - fulness: "ful", - ousness: "ous", - aliti: "al", - iviti: "ive", - biliti: "ble", - logi: "log", - }, - n = { - icate: "ic", - ative: "", - alize: "al", - iciti: "ic", - ical: "ic", - ful: "", - ness: "", - }, - r = "[^aeiou]", - i = "[aeiouy]", - s = r + "[^aeiouy]*", - o = i + "[aeiou]*", - a = "^(" + s + ")?" + o + s, - l = "^(" + s + ")?" + o + s + "(" + o + ")?$", - u = "^(" + s + ")?" + o + s + o + s, - d = "^(" + s + ")?" + i, - m = new RegExp(a), - p = new RegExp(u), - b = new RegExp(l), - g = new RegExp(d), - L = /^(.+?)(ss|i)es$/, - f = /^(.+?)([^s])s$/, - y = /^(.+?)eed$/, - S = /^(.+?)(ed|ing)$/, - w = /.$/, - k = /(at|bl|iz)$/, - _ = new RegExp("([^aeiouylsz])\\1$"), - B = new RegExp("^" + s + i + "[^aeiouwxy]$"), - A = /^(.+?[^aeiou])y$/, - j = - /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/, - q = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/, - V = - /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/, - $ = /^(.+?)(s|t)(ion)$/, - C = /^(.+?)e$/, - z = /ll$/, - W = new RegExp("^" + s + i + "[^aeiouwxy]$"), - H = function (c) { - var v, P, T, h, x, O, M; - if (c.length < 3) return c; - if ( - ((T = c.substr(0, 1)), - T == "y" && (c = T.toUpperCase() + c.substr(1)), - (h = L), - (x = f), - h.test(c) - ? (c = c.replace(h, "$1$2")) - : x.test(c) && (c = c.replace(x, "$1$2")), - (h = y), - (x = S), - h.test(c)) - ) { - var E = h.exec(c); - (h = m), h.test(E[1]) && ((h = w), (c = c.replace(h, ""))); - } else if (x.test(c)) { - var E = x.exec(c); - (v = E[1]), - (x = g), - x.test(v) && - ((c = v), - (x = k), - (O = _), - (M = B), - x.test(c) - ? (c = c + "e") - : O.test(c) - ? ((h = w), (c = c.replace(h, ""))) - : M.test(c) && (c = c + "e")); - } - if (((h = A), h.test(c))) { - var E = h.exec(c); - (v = E[1]), (c = v + "i"); - } - if (((h = j), h.test(c))) { - var E = h.exec(c); - (v = E[1]), (P = E[2]), (h = m), h.test(v) && (c = v + e[P]); - } - if (((h = q), h.test(c))) { - var E = h.exec(c); - (v = E[1]), (P = E[2]), (h = m), h.test(v) && (c = v + n[P]); - } - if (((h = V), (x = $), h.test(c))) { - var E = h.exec(c); - (v = E[1]), (h = p), h.test(v) && (c = v); - } else if (x.test(c)) { - var E = x.exec(c); - (v = E[1] + E[2]), (x = p), x.test(v) && (c = v); - } - if (((h = C), h.test(c))) { - var E = h.exec(c); - (v = E[1]), - (h = p), - (x = b), - (O = W), - (h.test(v) || (x.test(v) && !O.test(v))) && (c = v); - } - return ( - (h = z), - (x = p), - h.test(c) && x.test(c) && ((h = w), (c = c.replace(h, ""))), - T == "y" && (c = T.toLowerCase() + c.substr(1)), - c - ); - }; - return function (R) { - return R.update(H); - }; - })()), - t.Pipeline.registerFunction(t.stemmer, "stemmer"); - (t.generateStopWordFilter = function (e) { - var n = e.reduce(function (r, i) { - return (r[i] = i), r; - }, {}); - return function (r) { - if (r && n[r.toString()] !== r.toString()) return r; - }; - }), - (t.stopWordFilter = t.generateStopWordFilter([ - "a", - "able", - "about", - "across", - "after", - "all", - "almost", - "also", - "am", - "among", - "an", - "and", - "any", - "are", - "as", - "at", - "be", - "because", - "been", - "but", - "by", - "can", - "cannot", - "could", - "dear", - "did", - "do", - "does", - "either", - "else", - "ever", - "every", - "for", - "from", - "get", - "got", - "had", - "has", - "have", - "he", - "her", - "hers", - "him", - "his", - "how", - "however", - "i", - "if", - "in", - "into", - "is", - "it", - "its", - "just", - "least", - "let", - "like", - "likely", - "may", - "me", - "might", - "most", - "must", - "my", - "neither", - "no", - "nor", - "not", - "of", - "off", - "often", - "on", - "only", - "or", - "other", - "our", - "own", - "rather", - "said", - "say", - "says", - "she", - "should", - "since", - "so", - "some", - "than", - "that", - "the", - "their", - "them", - "then", - "there", - "these", - "they", - "this", - "tis", - "to", - "too", - "twas", - "us", - "wants", - "was", - "we", - "were", - "what", - "when", - "where", - "which", - "while", - "who", - "whom", - "why", - "will", - "with", - "would", - "yet", - "you", - "your", - ])), - t.Pipeline.registerFunction(t.stopWordFilter, "stopWordFilter"); - (t.trimmer = function (e) { - return e.update(function (n) { - return n.replace(/^\W+/, "").replace(/\W+$/, ""); - }); - }), - t.Pipeline.registerFunction(t.trimmer, "trimmer"); - (t.TokenSet = function () { - (this.final = !1), - (this.edges = {}), - (this.id = t.TokenSet._nextId), - (t.TokenSet._nextId += 1); - }), - (t.TokenSet._nextId = 1), - (t.TokenSet.fromArray = function (e) { - for ( - var n = new t.TokenSet.Builder(), r = 0, i = e.length; - r < i; - r++ - ) - n.insert(e[r]); - return n.finish(), n.root; - }), - (t.TokenSet.fromClause = function (e) { - return "editDistance" in e - ? t.TokenSet.fromFuzzyString(e.term, e.editDistance) - : t.TokenSet.fromString(e.term); - }), - (t.TokenSet.fromFuzzyString = function (e, n) { - for ( - var r = new t.TokenSet(), - i = [{ node: r, editsRemaining: n, str: e }]; - i.length; - - ) { - var s = i.pop(); - if (s.str.length > 0) { - var o = s.str.charAt(0), - a; - o in s.node.edges - ? (a = s.node.edges[o]) - : ((a = new t.TokenSet()), (s.node.edges[o] = a)), - s.str.length == 1 && (a.final = !0), - i.push({ - node: a, - editsRemaining: s.editsRemaining, - str: s.str.slice(1), - }); - } - if (s.editsRemaining != 0) { - if ("*" in s.node.edges) var l = s.node.edges["*"]; - else { - var l = new t.TokenSet(); - s.node.edges["*"] = l; - } - if ( - (s.str.length == 0 && (l.final = !0), - i.push({ - node: l, - editsRemaining: s.editsRemaining - 1, - str: s.str, - }), - s.str.length > 1 && - i.push({ - node: s.node, - editsRemaining: s.editsRemaining - 1, - str: s.str.slice(1), - }), - s.str.length == 1 && (s.node.final = !0), - s.str.length >= 1) - ) { - if ("*" in s.node.edges) var u = s.node.edges["*"]; - else { - var u = new t.TokenSet(); - s.node.edges["*"] = u; - } - s.str.length == 1 && (u.final = !0), - i.push({ - node: u, - editsRemaining: s.editsRemaining - 1, - str: s.str.slice(1), - }); - } - if (s.str.length > 1) { - var d = s.str.charAt(0), - m = s.str.charAt(1), - p; - m in s.node.edges - ? (p = s.node.edges[m]) - : ((p = new t.TokenSet()), (s.node.edges[m] = p)), - s.str.length == 1 && (p.final = !0), - i.push({ - node: p, - editsRemaining: s.editsRemaining - 1, - str: d + s.str.slice(2), - }); - } - } - } - return r; - }), - (t.TokenSet.fromString = function (e) { - for ( - var n = new t.TokenSet(), r = n, i = 0, s = e.length; - i < s; - i++ - ) { - var o = e[i], - a = i == s - 1; - if (o == "*") (n.edges[o] = n), (n.final = a); - else { - var l = new t.TokenSet(); - (l.final = a), (n.edges[o] = l), (n = l); - } - } - return r; - }), - (t.TokenSet.prototype.toArray = function () { - for (var e = [], n = [{ prefix: "", node: this }]; n.length; ) { - var r = n.pop(), - i = Object.keys(r.node.edges), - s = i.length; - r.node.final && (r.prefix.charAt(0), e.push(r.prefix)); - for (var o = 0; o < s; o++) { - var a = i[o]; - n.push({ prefix: r.prefix.concat(a), node: r.node.edges[a] }); - } - } - return e; - }), - (t.TokenSet.prototype.toString = function () { - if (this._str) return this._str; - for ( - var e = this.final ? "1" : "0", - n = Object.keys(this.edges).sort(), - r = n.length, - i = 0; - i < r; - i++ - ) { - var s = n[i], - o = this.edges[s]; - e = e + s + o.id; - } - return e; - }), - (t.TokenSet.prototype.intersect = function (e) { - for ( - var n = new t.TokenSet(), - r = void 0, - i = [{ qNode: e, output: n, node: this }]; - i.length; - - ) { - r = i.pop(); - for ( - var s = Object.keys(r.qNode.edges), - o = s.length, - a = Object.keys(r.node.edges), - l = a.length, - u = 0; - u < o; - u++ - ) - for (var d = s[u], m = 0; m < l; m++) { - var p = a[m]; - if (p == d || d == "*") { - var b = r.node.edges[p], - g = r.qNode.edges[d], - L = b.final && g.final, - f = void 0; - p in r.output.edges - ? ((f = r.output.edges[p]), (f.final = f.final || L)) - : ((f = new t.TokenSet()), - (f.final = L), - (r.output.edges[p] = f)), - i.push({ qNode: g, output: f, node: b }); - } - } - } - return n; - }), - (t.TokenSet.Builder = function () { - (this.previousWord = ""), - (this.root = new t.TokenSet()), - (this.uncheckedNodes = []), - (this.minimizedNodes = {}); - }), - (t.TokenSet.Builder.prototype.insert = function (e) { - var n, - r = 0; - if (e < this.previousWord) - throw new Error("Out of order word insertion"); - for ( - var i = 0; - i < e.length && - i < this.previousWord.length && - e[i] == this.previousWord[i]; - i++ - ) - r++; - this.minimize(r), - this.uncheckedNodes.length == 0 - ? (n = this.root) - : (n = this.uncheckedNodes[this.uncheckedNodes.length - 1].child); - for (var i = r; i < e.length; i++) { - var s = new t.TokenSet(), - o = e[i]; - (n.edges[o] = s), - this.uncheckedNodes.push({ parent: n, char: o, child: s }), - (n = s); - } - (n.final = !0), (this.previousWord = e); - }), - (t.TokenSet.Builder.prototype.finish = function () { - this.minimize(0); - }), - (t.TokenSet.Builder.prototype.minimize = function (e) { - for (var n = this.uncheckedNodes.length - 1; n >= e; n--) { - var r = this.uncheckedNodes[n], - i = r.child.toString(); - i in this.minimizedNodes - ? (r.parent.edges[r.char] = this.minimizedNodes[i]) - : ((r.child._str = i), (this.minimizedNodes[i] = r.child)), - this.uncheckedNodes.pop(); - } - }); - (t.Index = function (e) { - (this.invertedIndex = e.invertedIndex), - (this.fieldVectors = e.fieldVectors), - (this.tokenSet = e.tokenSet), - (this.fields = e.fields), - (this.pipeline = e.pipeline); - }), - (t.Index.prototype.search = function (e) { - return this.query(function (n) { - var r = new t.QueryParser(e, n); - r.parse(); - }); - }), - (t.Index.prototype.query = function (e) { - for ( - var n = new t.Query(this.fields), - r = Object.create(null), - i = Object.create(null), - s = Object.create(null), - o = Object.create(null), - a = Object.create(null), - l = 0; - l < this.fields.length; - l++ - ) - i[this.fields[l]] = new t.Vector(); - e.call(n, n); - for (var l = 0; l < n.clauses.length; l++) { - var u = n.clauses[l], - d = null, - m = t.Set.empty; - u.usePipeline - ? (d = this.pipeline.runString(u.term, { fields: u.fields })) - : (d = [u.term]); - for (var p = 0; p < d.length; p++) { - var b = d[p]; - u.term = b; - var g = t.TokenSet.fromClause(u), - L = this.tokenSet.intersect(g).toArray(); - if (L.length === 0 && u.presence === t.Query.presence.REQUIRED) { - for (var f = 0; f < u.fields.length; f++) { - var y = u.fields[f]; - o[y] = t.Set.empty; - } - break; - } - for (var S = 0; S < L.length; S++) - for ( - var w = L[S], k = this.invertedIndex[w], _ = k._index, f = 0; - f < u.fields.length; - f++ - ) { - var y = u.fields[f], - B = k[y], - A = Object.keys(B), - j = w + "/" + y, - q = new t.Set(A); - if ( - (u.presence == t.Query.presence.REQUIRED && - ((m = m.union(q)), - o[y] === void 0 && (o[y] = t.Set.complete)), - u.presence == t.Query.presence.PROHIBITED) - ) { - a[y] === void 0 && (a[y] = t.Set.empty), - (a[y] = a[y].union(q)); - continue; - } - if ( - (i[y].upsert(_, u.boost, function (Ie, Ce) { - return Ie + Ce; - }), - !s[j]) - ) { - for (var V = 0; V < A.length; V++) { - var $ = A[V], - C = new t.FieldRef($, y), - z = B[$], - W; - (W = r[C]) === void 0 - ? (r[C] = new t.MatchData(w, y, z)) - : W.add(w, y, z); - } - s[j] = !0; - } - } - } - if (u.presence === t.Query.presence.REQUIRED) - for (var f = 0; f < u.fields.length; f++) { - var y = u.fields[f]; - o[y] = o[y].intersect(m); - } - } - for ( - var H = t.Set.complete, R = t.Set.empty, l = 0; - l < this.fields.length; - l++ - ) { - var y = this.fields[l]; - o[y] && (H = H.intersect(o[y])), a[y] && (R = R.union(a[y])); - } - var c = Object.keys(r), - v = [], - P = Object.create(null); - if (n.isNegated()) { - c = Object.keys(this.fieldVectors); - for (var l = 0; l < c.length; l++) { - var C = c[l], - T = t.FieldRef.fromString(C); - r[C] = new t.MatchData(); - } - } - for (var l = 0; l < c.length; l++) { - var T = t.FieldRef.fromString(c[l]), - h = T.docRef; - if (H.contains(h) && !R.contains(h)) { - var x = this.fieldVectors[T], - O = i[T.fieldName].similarity(x), - M; - if ((M = P[h]) !== void 0) - (M.score += O), M.matchData.combine(r[T]); - else { - var E = { ref: h, score: O, matchData: r[T] }; - (P[h] = E), v.push(E); - } - } - } - return v.sort(function (ke, Qe) { - return Qe.score - ke.score; - }); - }), - (t.Index.prototype.toJSON = function () { - var e = Object.keys(this.invertedIndex) - .sort() - .map(function (r) { - return [r, this.invertedIndex[r]]; - }, this), - n = Object.keys(this.fieldVectors).map(function (r) { - return [r, this.fieldVectors[r].toJSON()]; - }, this); - return { - version: t.version, - fields: this.fields, - fieldVectors: n, - invertedIndex: e, - pipeline: this.pipeline.toJSON(), - }; - }), - (t.Index.load = function (e) { - var n = {}, - r = {}, - i = e.fieldVectors, - s = Object.create(null), - o = e.invertedIndex, - a = new t.TokenSet.Builder(), - l = t.Pipeline.load(e.pipeline); - e.version != t.version && - t.utils.warn( - "Version mismatch when loading serialised index. Current version of lunr '" + - t.version + - "' does not match serialized index '" + - e.version + - "'", - ); - for (var u = 0; u < i.length; u++) { - var d = i[u], - m = d[0], - p = d[1]; - r[m] = new t.Vector(p); - } - for (var u = 0; u < o.length; u++) { - var d = o[u], - b = d[0], - g = d[1]; - a.insert(b), (s[b] = g); - } - return ( - a.finish(), - (n.fields = e.fields), - (n.fieldVectors = r), - (n.invertedIndex = s), - (n.tokenSet = a.root), - (n.pipeline = l), - new t.Index(n) - ); - }); - (t.Builder = function () { - (this._ref = "id"), - (this._fields = Object.create(null)), - (this._documents = Object.create(null)), - (this.invertedIndex = Object.create(null)), - (this.fieldTermFrequencies = {}), - (this.fieldLengths = {}), - (this.tokenizer = t.tokenizer), - (this.pipeline = new t.Pipeline()), - (this.searchPipeline = new t.Pipeline()), - (this.documentCount = 0), - (this._b = 0.75), - (this._k1 = 1.2), - (this.termIndex = 0), - (this.metadataWhitelist = []); - }), - (t.Builder.prototype.ref = function (e) { - this._ref = e; - }), - (t.Builder.prototype.field = function (e, n) { - if (/\//.test(e)) - throw new RangeError( - "Field '" + e + "' contains illegal character '/'", - ); - this._fields[e] = n || {}; - }), - (t.Builder.prototype.b = function (e) { - e < 0 ? (this._b = 0) : e > 1 ? (this._b = 1) : (this._b = e); - }), - (t.Builder.prototype.k1 = function (e) { - this._k1 = e; - }), - (t.Builder.prototype.add = function (e, n) { - var r = e[this._ref], - i = Object.keys(this._fields); - (this._documents[r] = n || {}), (this.documentCount += 1); - for (var s = 0; s < i.length; s++) { - var o = i[s], - a = this._fields[o].extractor, - l = a ? a(e) : e[o], - u = this.tokenizer(l, { fields: [o] }), - d = this.pipeline.run(u), - m = new t.FieldRef(r, o), - p = Object.create(null); - (this.fieldTermFrequencies[m] = p), - (this.fieldLengths[m] = 0), - (this.fieldLengths[m] += d.length); - for (var b = 0; b < d.length; b++) { - var g = d[b]; - if ( - (p[g] == null && (p[g] = 0), - (p[g] += 1), - this.invertedIndex[g] == null) - ) { - var L = Object.create(null); - (L._index = this.termIndex), (this.termIndex += 1); - for (var f = 0; f < i.length; f++) - L[i[f]] = Object.create(null); - this.invertedIndex[g] = L; - } - this.invertedIndex[g][o][r] == null && - (this.invertedIndex[g][o][r] = Object.create(null)); - for (var y = 0; y < this.metadataWhitelist.length; y++) { - var S = this.metadataWhitelist[y], - w = g.metadata[S]; - this.invertedIndex[g][o][r][S] == null && - (this.invertedIndex[g][o][r][S] = []), - this.invertedIndex[g][o][r][S].push(w); - } - } - } - }), - (t.Builder.prototype.calculateAverageFieldLengths = function () { - for ( - var e = Object.keys(this.fieldLengths), - n = e.length, - r = {}, - i = {}, - s = 0; - s < n; - s++ - ) { - var o = t.FieldRef.fromString(e[s]), - a = o.fieldName; - i[a] || (i[a] = 0), - (i[a] += 1), - r[a] || (r[a] = 0), - (r[a] += this.fieldLengths[o]); - } - for (var l = Object.keys(this._fields), s = 0; s < l.length; s++) { - var u = l[s]; - r[u] = r[u] / i[u]; - } - this.averageFieldLength = r; - }), - (t.Builder.prototype.createFieldVectors = function () { - for ( - var e = {}, - n = Object.keys(this.fieldTermFrequencies), - r = n.length, - i = Object.create(null), - s = 0; - s < r; - s++ - ) { - for ( - var o = t.FieldRef.fromString(n[s]), - a = o.fieldName, - l = this.fieldLengths[o], - u = new t.Vector(), - d = this.fieldTermFrequencies[o], - m = Object.keys(d), - p = m.length, - b = this._fields[a].boost || 1, - g = this._documents[o.docRef].boost || 1, - L = 0; - L < p; - L++ - ) { - var f = m[L], - y = d[f], - S = this.invertedIndex[f]._index, - w, - k, - _; - i[f] === void 0 - ? ((w = t.idf(this.invertedIndex[f], this.documentCount)), - (i[f] = w)) - : (w = i[f]), - (k = - (w * ((this._k1 + 1) * y)) / - (this._k1 * - (1 - this._b + this._b * (l / this.averageFieldLength[a])) + - y)), - (k *= b), - (k *= g), - (_ = Math.round(k * 1e3) / 1e3), - u.insert(S, _); - } - e[o] = u; - } - this.fieldVectors = e; - }), - (t.Builder.prototype.createTokenSet = function () { - this.tokenSet = t.TokenSet.fromArray( - Object.keys(this.invertedIndex).sort(), - ); - }), - (t.Builder.prototype.build = function () { - return ( - this.calculateAverageFieldLengths(), - this.createFieldVectors(), - this.createTokenSet(), - new t.Index({ - invertedIndex: this.invertedIndex, - fieldVectors: this.fieldVectors, - tokenSet: this.tokenSet, - fields: Object.keys(this._fields), - pipeline: this.searchPipeline, - }) - ); - }), - (t.Builder.prototype.use = function (e) { - var n = Array.prototype.slice.call(arguments, 1); - n.unshift(this), e.apply(this, n); - }), - (t.MatchData = function (e, n, r) { - for ( - var i = Object.create(null), s = Object.keys(r || {}), o = 0; - o < s.length; - o++ - ) { - var a = s[o]; - i[a] = r[a].slice(); - } - (this.metadata = Object.create(null)), - e !== void 0 && - ((this.metadata[e] = Object.create(null)), - (this.metadata[e][n] = i)); - }), - (t.MatchData.prototype.combine = function (e) { - for (var n = Object.keys(e.metadata), r = 0; r < n.length; r++) { - var i = n[r], - s = Object.keys(e.metadata[i]); - this.metadata[i] == null && - (this.metadata[i] = Object.create(null)); - for (var o = 0; o < s.length; o++) { - var a = s[o], - l = Object.keys(e.metadata[i][a]); - this.metadata[i][a] == null && - (this.metadata[i][a] = Object.create(null)); - for (var u = 0; u < l.length; u++) { - var d = l[u]; - this.metadata[i][a][d] == null - ? (this.metadata[i][a][d] = e.metadata[i][a][d]) - : (this.metadata[i][a][d] = this.metadata[i][a][d].concat( - e.metadata[i][a][d], - )); - } - } - } - }), - (t.MatchData.prototype.add = function (e, n, r) { - if (!(e in this.metadata)) { - (this.metadata[e] = Object.create(null)), (this.metadata[e][n] = r); - return; - } - if (!(n in this.metadata[e])) { - this.metadata[e][n] = r; - return; - } - for (var i = Object.keys(r), s = 0; s < i.length; s++) { - var o = i[s]; - o in this.metadata[e][n] - ? (this.metadata[e][n][o] = this.metadata[e][n][o].concat(r[o])) - : (this.metadata[e][n][o] = r[o]); - } - }), - (t.Query = function (e) { - (this.clauses = []), (this.allFields = e); - }), - (t.Query.wildcard = new String("*")), - (t.Query.wildcard.NONE = 0), - (t.Query.wildcard.LEADING = 1), - (t.Query.wildcard.TRAILING = 2), - (t.Query.presence = { OPTIONAL: 1, REQUIRED: 2, PROHIBITED: 3 }), - (t.Query.prototype.clause = function (e) { - return ( - "fields" in e || (e.fields = this.allFields), - "boost" in e || (e.boost = 1), - "usePipeline" in e || (e.usePipeline = !0), - "wildcard" in e || (e.wildcard = t.Query.wildcard.NONE), - e.wildcard & t.Query.wildcard.LEADING && - e.term.charAt(0) != t.Query.wildcard && - (e.term = "*" + e.term), - e.wildcard & t.Query.wildcard.TRAILING && - e.term.slice(-1) != t.Query.wildcard && - (e.term = "" + e.term + "*"), - "presence" in e || (e.presence = t.Query.presence.OPTIONAL), - this.clauses.push(e), - this - ); - }), - (t.Query.prototype.isNegated = function () { - for (var e = 0; e < this.clauses.length; e++) - if (this.clauses[e].presence != t.Query.presence.PROHIBITED) - return !1; - return !0; - }), - (t.Query.prototype.term = function (e, n) { - if (Array.isArray(e)) - return ( - e.forEach(function (i) { - this.term(i, t.utils.clone(n)); - }, this), - this - ); - var r = n || {}; - return (r.term = e.toString()), this.clause(r), this; - }), - (t.QueryParseError = function (e, n, r) { - (this.name = "QueryParseError"), - (this.message = e), - (this.start = n), - (this.end = r); - }), - (t.QueryParseError.prototype = new Error()), - (t.QueryLexer = function (e) { - (this.lexemes = []), - (this.str = e), - (this.length = e.length), - (this.pos = 0), - (this.start = 0), - (this.escapeCharPositions = []); - }), - (t.QueryLexer.prototype.run = function () { - for (var e = t.QueryLexer.lexText; e; ) e = e(this); - }), - (t.QueryLexer.prototype.sliceString = function () { - for ( - var e = [], n = this.start, r = this.pos, i = 0; - i < this.escapeCharPositions.length; - i++ - ) - (r = this.escapeCharPositions[i]), - e.push(this.str.slice(n, r)), - (n = r + 1); - return ( - e.push(this.str.slice(n, this.pos)), - (this.escapeCharPositions.length = 0), - e.join("") - ); - }), - (t.QueryLexer.prototype.emit = function (e) { - this.lexemes.push({ - type: e, - str: this.sliceString(), - start: this.start, - end: this.pos, - }), - (this.start = this.pos); - }), - (t.QueryLexer.prototype.escapeCharacter = function () { - this.escapeCharPositions.push(this.pos - 1), (this.pos += 1); - }), - (t.QueryLexer.prototype.next = function () { - if (this.pos >= this.length) return t.QueryLexer.EOS; - var e = this.str.charAt(this.pos); - return (this.pos += 1), e; - }), - (t.QueryLexer.prototype.width = function () { - return this.pos - this.start; - }), - (t.QueryLexer.prototype.ignore = function () { - this.start == this.pos && (this.pos += 1), (this.start = this.pos); - }), - (t.QueryLexer.prototype.backup = function () { - this.pos -= 1; - }), - (t.QueryLexer.prototype.acceptDigitRun = function () { - var e, n; - do (e = this.next()), (n = e.charCodeAt(0)); - while (n > 47 && n < 58); - e != t.QueryLexer.EOS && this.backup(); - }), - (t.QueryLexer.prototype.more = function () { - return this.pos < this.length; - }), - (t.QueryLexer.EOS = "EOS"), - (t.QueryLexer.FIELD = "FIELD"), - (t.QueryLexer.TERM = "TERM"), - (t.QueryLexer.EDIT_DISTANCE = "EDIT_DISTANCE"), - (t.QueryLexer.BOOST = "BOOST"), - (t.QueryLexer.PRESENCE = "PRESENCE"), - (t.QueryLexer.lexField = function (e) { - return ( - e.backup(), - e.emit(t.QueryLexer.FIELD), - e.ignore(), - t.QueryLexer.lexText - ); - }), - (t.QueryLexer.lexTerm = function (e) { - if ( - (e.width() > 1 && (e.backup(), e.emit(t.QueryLexer.TERM)), - e.ignore(), - e.more()) - ) - return t.QueryLexer.lexText; - }), - (t.QueryLexer.lexEditDistance = function (e) { - return ( - e.ignore(), - e.acceptDigitRun(), - e.emit(t.QueryLexer.EDIT_DISTANCE), - t.QueryLexer.lexText - ); - }), - (t.QueryLexer.lexBoost = function (e) { - return ( - e.ignore(), - e.acceptDigitRun(), - e.emit(t.QueryLexer.BOOST), - t.QueryLexer.lexText - ); - }), - (t.QueryLexer.lexEOS = function (e) { - e.width() > 0 && e.emit(t.QueryLexer.TERM); - }), - (t.QueryLexer.termSeparator = t.tokenizer.separator), - (t.QueryLexer.lexText = function (e) { - for (;;) { - var n = e.next(); - if (n == t.QueryLexer.EOS) return t.QueryLexer.lexEOS; - if (n.charCodeAt(0) == 92) { - e.escapeCharacter(); - continue; - } - if (n == ":") return t.QueryLexer.lexField; - if (n == "~") - return ( - e.backup(), - e.width() > 0 && e.emit(t.QueryLexer.TERM), - t.QueryLexer.lexEditDistance - ); - if (n == "^") - return ( - e.backup(), - e.width() > 0 && e.emit(t.QueryLexer.TERM), - t.QueryLexer.lexBoost - ); - if ((n == "+" && e.width() === 1) || (n == "-" && e.width() === 1)) - return e.emit(t.QueryLexer.PRESENCE), t.QueryLexer.lexText; - if (n.match(t.QueryLexer.termSeparator)) - return t.QueryLexer.lexTerm; - } - }), - (t.QueryParser = function (e, n) { - (this.lexer = new t.QueryLexer(e)), - (this.query = n), - (this.currentClause = {}), - (this.lexemeIdx = 0); - }), - (t.QueryParser.prototype.parse = function () { - this.lexer.run(), (this.lexemes = this.lexer.lexemes); - for (var e = t.QueryParser.parseClause; e; ) e = e(this); - return this.query; - }), - (t.QueryParser.prototype.peekLexeme = function () { - return this.lexemes[this.lexemeIdx]; - }), - (t.QueryParser.prototype.consumeLexeme = function () { - var e = this.peekLexeme(); - return (this.lexemeIdx += 1), e; - }), - (t.QueryParser.prototype.nextClause = function () { - var e = this.currentClause; - this.query.clause(e), (this.currentClause = {}); - }), - (t.QueryParser.parseClause = function (e) { - var n = e.peekLexeme(); - if (n != null) - switch (n.type) { - case t.QueryLexer.PRESENCE: - return t.QueryParser.parsePresence; - case t.QueryLexer.FIELD: - return t.QueryParser.parseField; - case t.QueryLexer.TERM: - return t.QueryParser.parseTerm; - default: - var r = "expected either a field or a term, found " + n.type; - throw ( - (n.str.length >= 1 && (r += " with value '" + n.str + "'"), - new t.QueryParseError(r, n.start, n.end)) - ); - } - }), - (t.QueryParser.parsePresence = function (e) { - var n = e.consumeLexeme(); - if (n != null) { - switch (n.str) { - case "-": - e.currentClause.presence = t.Query.presence.PROHIBITED; - break; - case "+": - e.currentClause.presence = t.Query.presence.REQUIRED; - break; - default: - var r = "unrecognised presence operator'" + n.str + "'"; - throw new t.QueryParseError(r, n.start, n.end); - } - var i = e.peekLexeme(); - if (i == null) { - var r = "expecting term or field, found nothing"; - throw new t.QueryParseError(r, n.start, n.end); - } - switch (i.type) { - case t.QueryLexer.FIELD: - return t.QueryParser.parseField; - case t.QueryLexer.TERM: - return t.QueryParser.parseTerm; - default: - var r = "expecting term or field, found '" + i.type + "'"; - throw new t.QueryParseError(r, i.start, i.end); - } - } - }), - (t.QueryParser.parseField = function (e) { - var n = e.consumeLexeme(); - if (n != null) { - if (e.query.allFields.indexOf(n.str) == -1) { - var r = e.query.allFields - .map(function (o) { - return "'" + o + "'"; - }) - .join(", "), - i = "unrecognised field '" + n.str + "', possible fields: " + r; - throw new t.QueryParseError(i, n.start, n.end); - } - e.currentClause.fields = [n.str]; - var s = e.peekLexeme(); - if (s == null) { - var i = "expecting term, found nothing"; - throw new t.QueryParseError(i, n.start, n.end); - } - switch (s.type) { - case t.QueryLexer.TERM: - return t.QueryParser.parseTerm; - default: - var i = "expecting term, found '" + s.type + "'"; - throw new t.QueryParseError(i, s.start, s.end); - } - } - }), - (t.QueryParser.parseTerm = function (e) { - var n = e.consumeLexeme(); - if (n != null) { - (e.currentClause.term = n.str.toLowerCase()), - n.str.indexOf("*") != -1 && (e.currentClause.usePipeline = !1); - var r = e.peekLexeme(); - if (r == null) { - e.nextClause(); - return; - } - switch (r.type) { - case t.QueryLexer.TERM: - return e.nextClause(), t.QueryParser.parseTerm; - case t.QueryLexer.FIELD: - return e.nextClause(), t.QueryParser.parseField; - case t.QueryLexer.EDIT_DISTANCE: - return t.QueryParser.parseEditDistance; - case t.QueryLexer.BOOST: - return t.QueryParser.parseBoost; - case t.QueryLexer.PRESENCE: - return e.nextClause(), t.QueryParser.parsePresence; - default: - var i = "Unexpected lexeme type '" + r.type + "'"; - throw new t.QueryParseError(i, r.start, r.end); - } - } - }), - (t.QueryParser.parseEditDistance = function (e) { - var n = e.consumeLexeme(); - if (n != null) { - var r = parseInt(n.str, 10); - if (isNaN(r)) { - var i = "edit distance must be numeric"; - throw new t.QueryParseError(i, n.start, n.end); - } - e.currentClause.editDistance = r; - var s = e.peekLexeme(); - if (s == null) { - e.nextClause(); - return; - } - switch (s.type) { - case t.QueryLexer.TERM: - return e.nextClause(), t.QueryParser.parseTerm; - case t.QueryLexer.FIELD: - return e.nextClause(), t.QueryParser.parseField; - case t.QueryLexer.EDIT_DISTANCE: - return t.QueryParser.parseEditDistance; - case t.QueryLexer.BOOST: - return t.QueryParser.parseBoost; - case t.QueryLexer.PRESENCE: - return e.nextClause(), t.QueryParser.parsePresence; - default: - var i = "Unexpected lexeme type '" + s.type + "'"; - throw new t.QueryParseError(i, s.start, s.end); - } - } - }), - (t.QueryParser.parseBoost = function (e) { - var n = e.consumeLexeme(); - if (n != null) { - var r = parseInt(n.str, 10); - if (isNaN(r)) { - var i = "boost must be numeric"; - throw new t.QueryParseError(i, n.start, n.end); - } - e.currentClause.boost = r; - var s = e.peekLexeme(); - if (s == null) { - e.nextClause(); - return; - } - switch (s.type) { - case t.QueryLexer.TERM: - return e.nextClause(), t.QueryParser.parseTerm; - case t.QueryLexer.FIELD: - return e.nextClause(), t.QueryParser.parseField; - case t.QueryLexer.EDIT_DISTANCE: - return t.QueryParser.parseEditDistance; - case t.QueryLexer.BOOST: - return t.QueryParser.parseBoost; - case t.QueryLexer.PRESENCE: - return e.nextClause(), t.QueryParser.parsePresence; - default: - var i = "Unexpected lexeme type '" + s.type + "'"; - throw new t.QueryParseError(i, s.start, s.end); - } - } - }), - (function (e, n) { - typeof define == "function" && define.amd - ? define(n) - : typeof ae == "object" - ? (le.exports = n()) - : (e.lunr = n()); - })(this, function () { - return t; - }); - })(); - }); - var se = []; - function G(t, e) { - se.push({ selector: e, constructor: t }); - } - var U = class { - constructor() { - this.alwaysVisibleMember = null; - this.createComponents(document.body), - this.ensureFocusedElementVisible(), - this.listenForCodeCopies(), - window.addEventListener("hashchange", () => - this.ensureFocusedElementVisible(), - ), - document.body.style.display || - (this.ensureFocusedElementVisible(), - this.updateIndexVisibility(), - this.scrollToHash()); - } - createComponents(e) { - se.forEach((n) => { - e.querySelectorAll(n.selector).forEach((r) => { - r.dataset.hasInstance || - (new n.constructor({ el: r, app: this }), - (r.dataset.hasInstance = String(!0))); - }); - }); - } - filterChanged() { - this.ensureFocusedElementVisible(); - } - showPage() { - document.body.style.display && - (document.body.style.removeProperty("display"), - this.ensureFocusedElementVisible(), - this.updateIndexVisibility(), - this.scrollToHash()); - } - scrollToHash() { - if (location.hash) { - let e = document.getElementById(location.hash.substring(1)); - if (!e) return; - e.scrollIntoView({ behavior: "instant", block: "start" }); - } - } - ensureActivePageVisible() { - let e = document.querySelector(".tsd-navigation .current"), - n = e?.parentElement; - for (; n && !n.classList.contains(".tsd-navigation"); ) - n instanceof HTMLDetailsElement && (n.open = !0), (n = n.parentElement); - if (e && !Ve(e)) { - let r = - e.getBoundingClientRect().top - - document.documentElement.clientHeight / 4; - (document.querySelector(".site-menu").scrollTop = r), - (document.querySelector(".col-sidebar").scrollTop = r); - } - } - updateIndexVisibility() { - let e = document.querySelector(".tsd-index-content"), - n = e?.open; - e && (e.open = !0), - document.querySelectorAll(".tsd-index-section").forEach((r) => { - r.style.display = "block"; - let i = Array.from(r.querySelectorAll(".tsd-index-link")).every( - (s) => s.offsetParent == null, - ); - r.style.display = i ? "none" : "block"; - }), - e && (e.open = n); - } - ensureFocusedElementVisible() { - if ( - (this.alwaysVisibleMember && - (this.alwaysVisibleMember.classList.remove("always-visible"), - this.alwaysVisibleMember.firstElementChild.remove(), - (this.alwaysVisibleMember = null)), - !location.hash) - ) - return; - let e = document.getElementById(location.hash.substring(1)); - if (!e) return; - let n = e.parentElement; - for (; n && n.tagName !== "SECTION"; ) n = n.parentElement; - if (!n) return; - let r = n.offsetParent == null, - i = n; - for (; i !== document.body; ) - i instanceof HTMLDetailsElement && (i.open = !0), (i = i.parentElement); - if (n.offsetParent == null) { - (this.alwaysVisibleMember = n), n.classList.add("always-visible"); - let s = document.createElement("p"); - s.classList.add("warning"), - (s.textContent = window.translations.normally_hidden), - n.prepend(s); - } - r && e.scrollIntoView(); - } - listenForCodeCopies() { - document.querySelectorAll("pre > button").forEach((e) => { - let n; - e.addEventListener("click", () => { - e.previousElementSibling instanceof HTMLElement && - navigator.clipboard.writeText( - e.previousElementSibling.innerText.trim(), - ), - (e.textContent = window.translations.copied), - e.classList.add("visible"), - clearTimeout(n), - (n = setTimeout(() => { - e.classList.remove("visible"), - (n = setTimeout(() => { - e.textContent = window.translations.copy; - }, 100)); - }, 1e3)); - }); - }); - } - }; - function Ve(t) { - let e = t.getBoundingClientRect(), - n = Math.max(document.documentElement.clientHeight, window.innerHeight); - return !(e.bottom < 0 || e.top - n >= 0); - } - var oe = (t, e = 100) => { - let n; - return () => { - clearTimeout(n), (n = setTimeout(() => t(), e)); - }; - }; - var pe = Ae(ue()); - async function ce(t, e) { - if (!window.searchData) return; - let n = await fetch(window.searchData), - r = new Blob([await n.arrayBuffer()]) - .stream() - .pipeThrough(new DecompressionStream("gzip")), - i = await new Response(r).json(); - (t.data = i), - (t.index = pe.Index.load(i.index)), - e.classList.remove("loading"), - e.classList.add("ready"); - } - function fe() { - let t = document.getElementById("tsd-search"); - if (!t) return; - let e = { base: t.dataset.base + "/" }, - n = document.getElementById("tsd-search-script"); - t.classList.add("loading"), - n && - (n.addEventListener("error", () => { - t.classList.remove("loading"), t.classList.add("failure"); - }), - n.addEventListener("load", () => { - ce(e, t); - }), - ce(e, t)); - let r = document.querySelector("#tsd-search input"), - i = document.querySelector("#tsd-search .results"); - if (!r || !i) - throw new Error( - "The input field or the result list wrapper was not found", - ); - i.addEventListener("mouseup", () => { - te(t); - }), - r.addEventListener("focus", () => t.classList.add("has-focus")), - He(t, i, r, e); - } - function He(t, e, n, r) { - n.addEventListener( - "input", - oe(() => { - Ne(t, e, n, r); - }, 200), - ), - n.addEventListener("keydown", (i) => { - i.key == "Enter" - ? Be(e, t) - : i.key == "ArrowUp" - ? (de(e, n, -1), i.preventDefault()) - : i.key === "ArrowDown" && (de(e, n, 1), i.preventDefault()); - }), - document.body.addEventListener("keypress", (i) => { - i.altKey || - i.ctrlKey || - i.metaKey || - (!n.matches(":focus") && - i.key === "/" && - (i.preventDefault(), n.focus())); - }), - document.body.addEventListener("keyup", (i) => { - t.classList.contains("has-focus") && - (i.key === "Escape" || - (!e.matches(":focus-within") && !n.matches(":focus"))) && - (n.blur(), te(t)); - }); - } - function te(t) { - t.classList.remove("has-focus"); - } - function Ne(t, e, n, r) { - if (!r.index || !r.data) return; - e.textContent = ""; - let i = n.value.trim(), - s; - if (i) { - let o = i - .split(" ") - .map((a) => (a.length ? `*${a}*` : "")) - .join(" "); - s = r.index.search(o); - } else s = []; - for (let o = 0; o < s.length; o++) { - let a = s[o], - l = r.data.rows[Number(a.ref)], - u = 1; - l.name.toLowerCase().startsWith(i.toLowerCase()) && - (u *= 1 + 1 / (1 + Math.abs(l.name.length - i.length))), - (a.score *= u); - } - if (s.length === 0) { - let o = document.createElement("li"); - o.classList.add("no-results"); - let a = document.createElement("span"); - (a.textContent = "No results found"), o.appendChild(a), e.appendChild(o); - } - s.sort((o, a) => a.score - o.score); - for (let o = 0, a = Math.min(10, s.length); o < a; o++) { - let l = r.data.rows[Number(s[o].ref)], - u = ``, - d = he(l.name, i); - globalThis.DEBUG_SEARCH_WEIGHTS && - (d += ` (score: ${s[o].score.toFixed(2)})`), - l.parent && - (d = ` - ${he(l.parent, i)}.${d}`); - let m = document.createElement("li"); - m.classList.value = l.classes ?? ""; - let p = document.createElement("a"); - (p.href = r.base + l.url), - (p.innerHTML = u + d), - m.append(p), - p.addEventListener("focus", () => { - e.querySelector(".current")?.classList.remove("current"), - m.classList.add("current"); - }), - e.appendChild(m); - } - } - function de(t, e, n) { - let r = t.querySelector(".current"); - if (!r) - (r = t.querySelector(n == 1 ? "li:first-child" : "li:last-child")), - r && r.classList.add("current"); - else { - let i = r; - if (n === 1) - do i = i.nextElementSibling ?? void 0; - while (i instanceof HTMLElement && i.offsetParent == null); - else - do i = i.previousElementSibling ?? void 0; - while (i instanceof HTMLElement && i.offsetParent == null); - i - ? (r.classList.remove("current"), i.classList.add("current")) - : n === -1 && (r.classList.remove("current"), e.focus()); - } - } - function Be(t, e) { - let n = t.querySelector(".current"); - if ((n || (n = t.querySelector("li:first-child")), n)) { - let r = n.querySelector("a"); - r && (window.location.href = r.href), te(e); - } - } - function he(t, e) { - if (e === "") return t; - let n = t.toLocaleLowerCase(), - r = e.toLocaleLowerCase(), - i = [], - s = 0, - o = n.indexOf(r); - for (; o != -1; ) - i.push( - ee(t.substring(s, o)), - `${ee(t.substring(o, o + r.length))}`, - ), - (s = o + r.length), - (o = n.indexOf(r, s)); - return i.push(ee(t.substring(s))), i.join(""); - } - var je = { - "&": "&", - "<": "<", - ">": ">", - "'": "'", - '"': """, - }; - function ee(t) { - return t.replace(/[&<>"'"]/g, (e) => je[e]); - } - var I = class { - constructor(e) { - (this.el = e.el), (this.app = e.app); - } - }; - var F = "mousedown", - ye = "mousemove", - N = "mouseup", - J = { x: 0, y: 0 }, - me = !1, - ne = !1, - qe = !1, - D = !1, - ve = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( - navigator.userAgent, - ); - document.documentElement.classList.add(ve ? "is-mobile" : "not-mobile"); - ve && - "ontouchstart" in document.documentElement && - ((qe = !0), (F = "touchstart"), (ye = "touchmove"), (N = "touchend")); - document.addEventListener(F, (t) => { - (ne = !0), (D = !1); - let e = F == "touchstart" ? t.targetTouches[0] : t; - (J.y = e.pageY || 0), (J.x = e.pageX || 0); - }); - document.addEventListener(ye, (t) => { - if (ne && !D) { - let e = F == "touchstart" ? t.targetTouches[0] : t, - n = J.x - (e.pageX || 0), - r = J.y - (e.pageY || 0); - D = Math.sqrt(n * n + r * r) > 10; - } - }); - document.addEventListener(N, () => { - ne = !1; - }); - document.addEventListener("click", (t) => { - me && (t.preventDefault(), t.stopImmediatePropagation(), (me = !1)); - }); - var X = class extends I { - constructor(e) { - super(e), - (this.className = this.el.dataset.toggle || ""), - this.el.addEventListener(N, (n) => this.onPointerUp(n)), - this.el.addEventListener("click", (n) => n.preventDefault()), - document.addEventListener(F, (n) => this.onDocumentPointerDown(n)), - document.addEventListener(N, (n) => this.onDocumentPointerUp(n)); - } - setActive(e) { - if (this.active == e) return; - (this.active = e), - document.documentElement.classList.toggle("has-" + this.className, e), - this.el.classList.toggle("active", e); - let n = (this.active ? "to-has-" : "from-has-") + this.className; - document.documentElement.classList.add(n), - setTimeout(() => document.documentElement.classList.remove(n), 500); - } - onPointerUp(e) { - D || (this.setActive(!0), e.preventDefault()); - } - onDocumentPointerDown(e) { - if (this.active) { - if (e.target.closest(".col-sidebar, .tsd-filter-group")) return; - this.setActive(!1); - } - } - onDocumentPointerUp(e) { - if (!D && this.active && e.target.closest(".col-sidebar")) { - let n = e.target.closest("a"); - if (n) { - let r = window.location.href; - r.indexOf("#") != -1 && (r = r.substring(0, r.indexOf("#"))), - n.href.substring(0, r.length) == r && - setTimeout(() => this.setActive(!1), 250); - } - } - } - }; - var re; - try { - re = localStorage; - } catch { - re = { - getItem() { - return null; - }, - setItem() {}, - }; - } - var Q = re; - var ge = document.head.appendChild(document.createElement("style")); - ge.dataset.for = "filters"; - var Y = class extends I { - constructor(e) { - super(e), - (this.key = `filter-${this.el.name}`), - (this.value = this.el.checked), - this.el.addEventListener("change", () => { - this.setLocalStorage(this.el.checked); - }), - this.setLocalStorage(this.fromLocalStorage()), - (ge.innerHTML += `html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } -`), - this.app.updateIndexVisibility(); - } - fromLocalStorage() { - let e = Q.getItem(this.key); - return e ? e === "true" : this.el.checked; - } - setLocalStorage(e) { - Q.setItem(this.key, e.toString()), - (this.value = e), - this.handleValueChange(); - } - handleValueChange() { - (this.el.checked = this.value), - document.documentElement.classList.toggle(this.key, this.value), - this.app.filterChanged(), - this.app.updateIndexVisibility(); - } - }; - var Z = class extends I { - constructor(e) { - super(e), - (this.summary = this.el.querySelector(".tsd-accordion-summary")), - (this.icon = this.summary.querySelector("svg")), - (this.key = `tsd-accordion-${this.summary.dataset.key ?? this.summary.textContent.trim().replace(/\s+/g, "-").toLowerCase()}`); - let n = Q.getItem(this.key); - (this.el.open = n ? n === "true" : this.el.open), - this.el.addEventListener("toggle", () => this.update()); - let r = this.summary.querySelector("a"); - r && - r.addEventListener("click", () => { - location.assign(r.href); - }), - this.update(); - } - update() { - (this.icon.style.transform = `rotate(${this.el.open ? 0 : -90}deg)`), - Q.setItem(this.key, this.el.open.toString()); - } - }; - function Ee(t) { - let e = Q.getItem("tsd-theme") || "os"; - (t.value = e), - xe(e), - t.addEventListener("change", () => { - Q.setItem("tsd-theme", t.value), xe(t.value); - }); - } - function xe(t) { - document.documentElement.dataset.theme = t; - } - var K; - function we() { - let t = document.getElementById("tsd-nav-script"); - t && (t.addEventListener("load", Le), Le()); - } - async function Le() { - let t = document.getElementById("tsd-nav-container"); - if (!t || !window.navigationData) return; - let n = await (await fetch(window.navigationData)).arrayBuffer(), - r = new Blob([n]).stream().pipeThrough(new DecompressionStream("gzip")), - i = await new Response(r).json(); - (K = t.dataset.base), K.endsWith("/") || (K += "/"), (t.innerHTML = ""); - for (let s of i) Se(s, t, []); - window.app.createComponents(t), - window.app.showPage(), - window.app.ensureActivePageVisible(); - } - function Se(t, e, n) { - let r = e.appendChild(document.createElement("li")); - if (t.children) { - let i = [...n, t.text], - s = r.appendChild(document.createElement("details")); - s.className = t.class ? `${t.class} tsd-accordion` : "tsd-accordion"; - let o = s.appendChild(document.createElement("summary")); - (o.className = "tsd-accordion-summary"), - (o.dataset.key = i.join("$")), - (o.innerHTML = - ''), - be(t, o); - let a = s.appendChild(document.createElement("div")); - a.className = "tsd-accordion-details"; - let l = a.appendChild(document.createElement("ul")); - l.className = "tsd-nested-navigation"; - for (let u of t.children) Se(u, l, i); - } else be(t, r, t.class); - } - function be(t, e, n) { - if (t.path) { - let r = e.appendChild(document.createElement("a")); - (r.href = K + t.path), - n && (r.className = n), - location.pathname === r.pathname && - !r.href.includes("#") && - r.classList.add("current"), - t.kind && - (r.innerHTML = ``), - (r.appendChild(document.createElement("span")).textContent = t.text); - } else e.appendChild(document.createElement("span")).textContent = t.text; - } - G(X, "a[data-toggle]"); - G(Z, ".tsd-accordion"); - G(Y, ".tsd-filter-item input[type=checkbox]"); - var Te = document.getElementById("tsd-theme"); - Te && Ee(Te); - var $e = new U(); - Object.defineProperty(window, "app", { value: $e }); - fe(); - we(); -})(); +window.translations={"copy":"Copy","copied":"Copied!","normally_hidden":"This member is normally hidden due to your filter settings."}; +"use strict";(()=>{var Pe=Object.create;var ie=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var _e=Object.getOwnPropertyNames;var Re=Object.getPrototypeOf,Me=Object.prototype.hasOwnProperty;var Fe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var De=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of _e(e))!Me.call(t,i)&&i!==n&&ie(t,i,{get:()=>e[i],enumerable:!(r=Oe(e,i))||r.enumerable});return t};var Ae=(t,e,n)=>(n=t!=null?Pe(Re(t)):{},De(e||!t||!t.__esModule?ie(n,"default",{value:t,enumerable:!0}):n,t));var ue=Fe((ae,le)=>{(function(){var t=function(e){var n=new t.Builder;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),n.searchPipeline.add(t.stemmer),e.call(n,n),n.build()};t.version="2.3.9";t.utils={},t.utils.warn=function(e){return function(n){e.console&&console.warn&&console.warn(n)}}(this),t.utils.asString=function(e){return e==null?"":e.toString()},t.utils.clone=function(e){if(e==null)return e;for(var n=Object.create(null),r=Object.keys(e),i=0;i0){var d=t.utils.clone(n)||{};d.position=[a,u],d.index=s.length,s.push(new t.Token(r.slice(a,o),d))}a=o+1}}return s},t.tokenizer.separator=/[\s\-]+/;t.Pipeline=function(){this._stack=[]},t.Pipeline.registeredFunctions=Object.create(null),t.Pipeline.registerFunction=function(e,n){n in this.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[e.label]=e},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn(`Function is not registered with pipeline. This may cause problems when serialising the index. +`,e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(r){var i=t.Pipeline.registeredFunctions[r];if(i)n.add(i);else throw new Error("Cannot load unregistered function: "+r)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(n){t.Pipeline.warnIfFunctionNotRegistered(n),this._stack.push(n)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");r=r+1,this._stack.splice(r,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var r=this._stack.indexOf(e);if(r==-1)throw new Error("Cannot find existingFn");this._stack.splice(r,0,n)},t.Pipeline.prototype.remove=function(e){var n=this._stack.indexOf(e);n!=-1&&this._stack.splice(n,1)},t.Pipeline.prototype.run=function(e){for(var n=this._stack.length,r=0;r1&&(oe&&(r=s),o!=e);)i=r-n,s=n+Math.floor(i/2),o=this.elements[s*2];if(o==e||o>e)return s*2;if(ol?d+=2:a==l&&(n+=r[u+1]*i[d+1],u+=2,d+=2);return n},t.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},t.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),n=1,r=0;n0){var o=s.str.charAt(0),a;o in s.node.edges?a=s.node.edges[o]:(a=new t.TokenSet,s.node.edges[o]=a),s.str.length==1&&(a.final=!0),i.push({node:a,editsRemaining:s.editsRemaining,str:s.str.slice(1)})}if(s.editsRemaining!=0){if("*"in s.node.edges)var l=s.node.edges["*"];else{var l=new t.TokenSet;s.node.edges["*"]=l}if(s.str.length==0&&(l.final=!0),i.push({node:l,editsRemaining:s.editsRemaining-1,str:s.str}),s.str.length>1&&i.push({node:s.node,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)}),s.str.length==1&&(s.node.final=!0),s.str.length>=1){if("*"in s.node.edges)var u=s.node.edges["*"];else{var u=new t.TokenSet;s.node.edges["*"]=u}s.str.length==1&&(u.final=!0),i.push({node:u,editsRemaining:s.editsRemaining-1,str:s.str.slice(1)})}if(s.str.length>1){var d=s.str.charAt(0),m=s.str.charAt(1),p;m in s.node.edges?p=s.node.edges[m]:(p=new t.TokenSet,s.node.edges[m]=p),s.str.length==1&&(p.final=!0),i.push({node:p,editsRemaining:s.editsRemaining-1,str:d+s.str.slice(2)})}}}return r},t.TokenSet.fromString=function(e){for(var n=new t.TokenSet,r=n,i=0,s=e.length;i=e;n--){var r=this.uncheckedNodes[n],i=r.child.toString();i in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[i]:(r.child._str=i,this.minimizedNodes[i]=r.child),this.uncheckedNodes.pop()}};t.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},t.Index.prototype.search=function(e){return this.query(function(n){var r=new t.QueryParser(e,n);r.parse()})},t.Index.prototype.query=function(e){for(var n=new t.Query(this.fields),r=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=Object.create(null),l=0;l1?this._b=1:this._b=e},t.Builder.prototype.k1=function(e){this._k1=e},t.Builder.prototype.add=function(e,n){var r=e[this._ref],i=Object.keys(this._fields);this._documents[r]=n||{},this.documentCount+=1;for(var s=0;s=this.length)return t.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},t.QueryLexer.prototype.width=function(){return this.pos-this.start},t.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},t.QueryLexer.prototype.backup=function(){this.pos-=1},t.QueryLexer.prototype.acceptDigitRun=function(){var e,n;do e=this.next(),n=e.charCodeAt(0);while(n>47&&n<58);e!=t.QueryLexer.EOS&&this.backup()},t.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(t.QueryLexer.TERM)),e.ignore(),e.more())return t.QueryLexer.lexText},t.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.EDIT_DISTANCE),t.QueryLexer.lexText},t.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(t.QueryLexer.BOOST),t.QueryLexer.lexText},t.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(t.QueryLexer.TERM)},t.QueryLexer.termSeparator=t.tokenizer.separator,t.QueryLexer.lexText=function(e){for(;;){var n=e.next();if(n==t.QueryLexer.EOS)return t.QueryLexer.lexEOS;if(n.charCodeAt(0)==92){e.escapeCharacter();continue}if(n==":")return t.QueryLexer.lexField;if(n=="~")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexEditDistance;if(n=="^")return e.backup(),e.width()>0&&e.emit(t.QueryLexer.TERM),t.QueryLexer.lexBoost;if(n=="+"&&e.width()===1||n=="-"&&e.width()===1)return e.emit(t.QueryLexer.PRESENCE),t.QueryLexer.lexText;if(n.match(t.QueryLexer.termSeparator))return t.QueryLexer.lexTerm}},t.QueryParser=function(e,n){this.lexer=new t.QueryLexer(e),this.query=n,this.currentClause={},this.lexemeIdx=0},t.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=t.QueryParser.parseClause;e;)e=e(this);return this.query},t.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},t.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},t.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},t.QueryParser.parseClause=function(e){var n=e.peekLexeme();if(n!=null)switch(n.type){case t.QueryLexer.PRESENCE:return t.QueryParser.parsePresence;case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+n.type;throw n.str.length>=1&&(r+=" with value '"+n.str+"'"),new t.QueryParseError(r,n.start,n.end)}},t.QueryParser.parsePresence=function(e){var n=e.consumeLexeme();if(n!=null){switch(n.str){case"-":e.currentClause.presence=t.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=t.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+n.str+"'";throw new t.QueryParseError(r,n.start,n.end)}var i=e.peekLexeme();if(i==null){var r="expecting term or field, found nothing";throw new t.QueryParseError(r,n.start,n.end)}switch(i.type){case t.QueryLexer.FIELD:return t.QueryParser.parseField;case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var r="expecting term or field, found '"+i.type+"'";throw new t.QueryParseError(r,i.start,i.end)}}},t.QueryParser.parseField=function(e){var n=e.consumeLexeme();if(n!=null){if(e.query.allFields.indexOf(n.str)==-1){var r=e.query.allFields.map(function(o){return"'"+o+"'"}).join(", "),i="unrecognised field '"+n.str+"', possible fields: "+r;throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.fields=[n.str];var s=e.peekLexeme();if(s==null){var i="expecting term, found nothing";throw new t.QueryParseError(i,n.start,n.end)}switch(s.type){case t.QueryLexer.TERM:return t.QueryParser.parseTerm;default:var i="expecting term, found '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseTerm=function(e){var n=e.consumeLexeme();if(n!=null){e.currentClause.term=n.str.toLowerCase(),n.str.indexOf("*")!=-1&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(r==null){e.nextClause();return}switch(r.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+r.type+"'";throw new t.QueryParseError(i,r.start,r.end)}}},t.QueryParser.parseEditDistance=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="edit distance must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.editDistance=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},t.QueryParser.parseBoost=function(e){var n=e.consumeLexeme();if(n!=null){var r=parseInt(n.str,10);if(isNaN(r)){var i="boost must be numeric";throw new t.QueryParseError(i,n.start,n.end)}e.currentClause.boost=r;var s=e.peekLexeme();if(s==null){e.nextClause();return}switch(s.type){case t.QueryLexer.TERM:return e.nextClause(),t.QueryParser.parseTerm;case t.QueryLexer.FIELD:return e.nextClause(),t.QueryParser.parseField;case t.QueryLexer.EDIT_DISTANCE:return t.QueryParser.parseEditDistance;case t.QueryLexer.BOOST:return t.QueryParser.parseBoost;case t.QueryLexer.PRESENCE:return e.nextClause(),t.QueryParser.parsePresence;default:var i="Unexpected lexeme type '"+s.type+"'";throw new t.QueryParseError(i,s.start,s.end)}}},function(e,n){typeof define=="function"&&define.amd?define(n):typeof ae=="object"?le.exports=n():e.lunr=n()}(this,function(){return t})})()});var se=[];function G(t,e){se.push({selector:e,constructor:t})}var U=class{constructor(){this.alwaysVisibleMember=null;this.createComponents(document.body),this.ensureFocusedElementVisible(),this.listenForCodeCopies(),window.addEventListener("hashchange",()=>this.ensureFocusedElementVisible()),document.body.style.display||(this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}createComponents(e){se.forEach(n=>{e.querySelectorAll(n.selector).forEach(r=>{r.dataset.hasInstance||(new n.constructor({el:r,app:this}),r.dataset.hasInstance=String(!0))})})}filterChanged(){this.ensureFocusedElementVisible()}showPage(){document.body.style.display&&(document.body.style.removeProperty("display"),this.ensureFocusedElementVisible(),this.updateIndexVisibility(),this.scrollToHash())}scrollToHash(){if(location.hash){let e=document.getElementById(location.hash.substring(1));if(!e)return;e.scrollIntoView({behavior:"instant",block:"start"})}}ensureActivePageVisible(){let e=document.querySelector(".tsd-navigation .current"),n=e?.parentElement;for(;n&&!n.classList.contains(".tsd-navigation");)n instanceof HTMLDetailsElement&&(n.open=!0),n=n.parentElement;if(e&&!Ve(e)){let r=e.getBoundingClientRect().top-document.documentElement.clientHeight/4;document.querySelector(".site-menu").scrollTop=r,document.querySelector(".col-sidebar").scrollTop=r}}updateIndexVisibility(){let e=document.querySelector(".tsd-index-content"),n=e?.open;e&&(e.open=!0),document.querySelectorAll(".tsd-index-section").forEach(r=>{r.style.display="block";let i=Array.from(r.querySelectorAll(".tsd-index-link")).every(s=>s.offsetParent==null);r.style.display=i?"none":"block"}),e&&(e.open=n)}ensureFocusedElementVisible(){if(this.alwaysVisibleMember&&(this.alwaysVisibleMember.classList.remove("always-visible"),this.alwaysVisibleMember.firstElementChild.remove(),this.alwaysVisibleMember=null),!location.hash)return;let e=document.getElementById(location.hash.substring(1));if(!e)return;let n=e.parentElement;for(;n&&n.tagName!=="SECTION";)n=n.parentElement;if(!n)return;let r=n.offsetParent==null,i=n;for(;i!==document.body;)i instanceof HTMLDetailsElement&&(i.open=!0),i=i.parentElement;if(n.offsetParent==null){this.alwaysVisibleMember=n,n.classList.add("always-visible");let s=document.createElement("p");s.classList.add("warning"),s.textContent=window.translations.normally_hidden,n.prepend(s)}r&&e.scrollIntoView()}listenForCodeCopies(){document.querySelectorAll("pre > button").forEach(e=>{let n;e.addEventListener("click",()=>{e.previousElementSibling instanceof HTMLElement&&navigator.clipboard.writeText(e.previousElementSibling.innerText.trim()),e.textContent=window.translations.copied,e.classList.add("visible"),clearTimeout(n),n=setTimeout(()=>{e.classList.remove("visible"),n=setTimeout(()=>{e.textContent=window.translations.copy},100)},1e3)})})}};function Ve(t){let e=t.getBoundingClientRect(),n=Math.max(document.documentElement.clientHeight,window.innerHeight);return!(e.bottom<0||e.top-n>=0)}var oe=(t,e=100)=>{let n;return()=>{clearTimeout(n),n=setTimeout(()=>t(),e)}};var pe=Ae(ue());async function ce(t,e){if(!window.searchData)return;let n=await fetch(window.searchData),r=new Blob([await n.arrayBuffer()]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();t.data=i,t.index=pe.Index.load(i.index),e.classList.remove("loading"),e.classList.add("ready")}function fe(){let t=document.getElementById("tsd-search");if(!t)return;let e={base:t.dataset.base+"/"},n=document.getElementById("tsd-search-script");t.classList.add("loading"),n&&(n.addEventListener("error",()=>{t.classList.remove("loading"),t.classList.add("failure")}),n.addEventListener("load",()=>{ce(e,t)}),ce(e,t));let r=document.querySelector("#tsd-search input"),i=document.querySelector("#tsd-search .results");if(!r||!i)throw new Error("The input field or the result list wrapper was not found");i.addEventListener("mouseup",()=>{te(t)}),r.addEventListener("focus",()=>t.classList.add("has-focus")),He(t,i,r,e)}function He(t,e,n,r){n.addEventListener("input",oe(()=>{Ne(t,e,n,r)},200)),n.addEventListener("keydown",i=>{i.key=="Enter"?Be(e,t):i.key=="ArrowUp"?(de(e,n,-1),i.preventDefault()):i.key==="ArrowDown"&&(de(e,n,1),i.preventDefault())}),document.body.addEventListener("keypress",i=>{i.altKey||i.ctrlKey||i.metaKey||!n.matches(":focus")&&i.key==="/"&&(i.preventDefault(),n.focus())}),document.body.addEventListener("keyup",i=>{t.classList.contains("has-focus")&&(i.key==="Escape"||!e.matches(":focus-within")&&!n.matches(":focus"))&&(n.blur(),te(t))})}function te(t){t.classList.remove("has-focus")}function Ne(t,e,n,r){if(!r.index||!r.data)return;e.textContent="";let i=n.value.trim(),s;if(i){let o=i.split(" ").map(a=>a.length?`*${a}*`:"").join(" ");s=r.index.search(o)}else s=[];for(let o=0;oa.score-o.score);for(let o=0,a=Math.min(10,s.length);o`,d=he(l.name,i);globalThis.DEBUG_SEARCH_WEIGHTS&&(d+=` (score: ${s[o].score.toFixed(2)})`),l.parent&&(d=` + ${he(l.parent,i)}.${d}`);let m=document.createElement("li");m.classList.value=l.classes??"";let p=document.createElement("a");p.href=r.base+l.url,p.innerHTML=u+d,m.append(p),p.addEventListener("focus",()=>{e.querySelector(".current")?.classList.remove("current"),m.classList.add("current")}),e.appendChild(m)}}function de(t,e,n){let r=t.querySelector(".current");if(!r)r=t.querySelector(n==1?"li:first-child":"li:last-child"),r&&r.classList.add("current");else{let i=r;if(n===1)do i=i.nextElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);else do i=i.previousElementSibling??void 0;while(i instanceof HTMLElement&&i.offsetParent==null);i?(r.classList.remove("current"),i.classList.add("current")):n===-1&&(r.classList.remove("current"),e.focus())}}function Be(t,e){let n=t.querySelector(".current");if(n||(n=t.querySelector("li:first-child")),n){let r=n.querySelector("a");r&&(window.location.href=r.href),te(e)}}function he(t,e){if(e==="")return t;let n=t.toLocaleLowerCase(),r=e.toLocaleLowerCase(),i=[],s=0,o=n.indexOf(r);for(;o!=-1;)i.push(ee(t.substring(s,o)),`${ee(t.substring(o,o+r.length))}`),s=o+r.length,o=n.indexOf(r,s);return i.push(ee(t.substring(s))),i.join("")}var je={"&":"&","<":"<",">":">","'":"'",'"':"""};function ee(t){return t.replace(/[&<>"'"]/g,e=>je[e])}var I=class{constructor(e){this.el=e.el,this.app=e.app}};var F="mousedown",ye="mousemove",N="mouseup",J={x:0,y:0},me=!1,ne=!1,qe=!1,D=!1,ve=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);document.documentElement.classList.add(ve?"is-mobile":"not-mobile");ve&&"ontouchstart"in document.documentElement&&(qe=!0,F="touchstart",ye="touchmove",N="touchend");document.addEventListener(F,t=>{ne=!0,D=!1;let e=F=="touchstart"?t.targetTouches[0]:t;J.y=e.pageY||0,J.x=e.pageX||0});document.addEventListener(ye,t=>{if(ne&&!D){let e=F=="touchstart"?t.targetTouches[0]:t,n=J.x-(e.pageX||0),r=J.y-(e.pageY||0);D=Math.sqrt(n*n+r*r)>10}});document.addEventListener(N,()=>{ne=!1});document.addEventListener("click",t=>{me&&(t.preventDefault(),t.stopImmediatePropagation(),me=!1)});var X=class extends I{constructor(e){super(e),this.className=this.el.dataset.toggle||"",this.el.addEventListener(N,n=>this.onPointerUp(n)),this.el.addEventListener("click",n=>n.preventDefault()),document.addEventListener(F,n=>this.onDocumentPointerDown(n)),document.addEventListener(N,n=>this.onDocumentPointerUp(n))}setActive(e){if(this.active==e)return;this.active=e,document.documentElement.classList.toggle("has-"+this.className,e),this.el.classList.toggle("active",e);let n=(this.active?"to-has-":"from-has-")+this.className;document.documentElement.classList.add(n),setTimeout(()=>document.documentElement.classList.remove(n),500)}onPointerUp(e){D||(this.setActive(!0),e.preventDefault())}onDocumentPointerDown(e){if(this.active){if(e.target.closest(".col-sidebar, .tsd-filter-group"))return;this.setActive(!1)}}onDocumentPointerUp(e){if(!D&&this.active&&e.target.closest(".col-sidebar")){let n=e.target.closest("a");if(n){let r=window.location.href;r.indexOf("#")!=-1&&(r=r.substring(0,r.indexOf("#"))),n.href.substring(0,r.length)==r&&setTimeout(()=>this.setActive(!1),250)}}}};var re;try{re=localStorage}catch{re={getItem(){return null},setItem(){}}}var Q=re;var ge=document.head.appendChild(document.createElement("style"));ge.dataset.for="filters";var Y=class extends I{constructor(e){super(e),this.key=`filter-${this.el.name}`,this.value=this.el.checked,this.el.addEventListener("change",()=>{this.setLocalStorage(this.el.checked)}),this.setLocalStorage(this.fromLocalStorage()),ge.innerHTML+=`html:not(.${this.key}) .tsd-is-${this.el.name} { display: none; } +`,this.app.updateIndexVisibility()}fromLocalStorage(){let e=Q.getItem(this.key);return e?e==="true":this.el.checked}setLocalStorage(e){Q.setItem(this.key,e.toString()),this.value=e,this.handleValueChange()}handleValueChange(){this.el.checked=this.value,document.documentElement.classList.toggle(this.key,this.value),this.app.filterChanged(),this.app.updateIndexVisibility()}};var Z=class extends I{constructor(e){super(e),this.summary=this.el.querySelector(".tsd-accordion-summary"),this.icon=this.summary.querySelector("svg"),this.key=`tsd-accordion-${this.summary.dataset.key??this.summary.textContent.trim().replace(/\s+/g,"-").toLowerCase()}`;let n=Q.getItem(this.key);this.el.open=n?n==="true":this.el.open,this.el.addEventListener("toggle",()=>this.update());let r=this.summary.querySelector("a");r&&r.addEventListener("click",()=>{location.assign(r.href)}),this.update()}update(){this.icon.style.transform=`rotate(${this.el.open?0:-90}deg)`,Q.setItem(this.key,this.el.open.toString())}};function Ee(t){let e=Q.getItem("tsd-theme")||"os";t.value=e,xe(e),t.addEventListener("change",()=>{Q.setItem("tsd-theme",t.value),xe(t.value)})}function xe(t){document.documentElement.dataset.theme=t}var K;function we(){let t=document.getElementById("tsd-nav-script");t&&(t.addEventListener("load",Le),Le())}async function Le(){let t=document.getElementById("tsd-nav-container");if(!t||!window.navigationData)return;let n=await(await fetch(window.navigationData)).arrayBuffer(),r=new Blob([n]).stream().pipeThrough(new DecompressionStream("gzip")),i=await new Response(r).json();K=t.dataset.base,K.endsWith("/")||(K+="/"),t.innerHTML="";for(let s of i)Se(s,t,[]);window.app.createComponents(t),window.app.showPage(),window.app.ensureActivePageVisible()}function Se(t,e,n){let r=e.appendChild(document.createElement("li"));if(t.children){let i=[...n,t.text],s=r.appendChild(document.createElement("details"));s.className=t.class?`${t.class} tsd-accordion`:"tsd-accordion";let o=s.appendChild(document.createElement("summary"));o.className="tsd-accordion-summary",o.dataset.key=i.join("$"),o.innerHTML='',be(t,o);let a=s.appendChild(document.createElement("div"));a.className="tsd-accordion-details";let l=a.appendChild(document.createElement("ul"));l.className="tsd-nested-navigation";for(let u of t.children)Se(u,l,i)}else be(t,r,t.class)}function be(t,e,n){if(t.path){let r=e.appendChild(document.createElement("a"));r.href=K+t.path,n&&(r.className=n),location.pathname===r.pathname&&!r.href.includes("#")&&r.classList.add("current"),t.kind&&(r.innerHTML=``),r.appendChild(document.createElement("span")).textContent=t.text}else e.appendChild(document.createElement("span")).textContent=t.text}G(X,"a[data-toggle]");G(Z,".tsd-accordion");G(Y,".tsd-filter-item input[type=checkbox]");var Te=document.getElementById("tsd-theme");Te&&Ee(Te);var $e=new U;Object.defineProperty(window,"app",{value:$e});fe();we();})(); /*! Bundled license information: lunr/lunr.js: diff --git a/docs/reference/assets/navigation.js b/docs/reference/assets/navigation.js index bb4e05f..374fdac 100644 --- a/docs/reference/assets/navigation.js +++ b/docs/reference/assets/navigation.js @@ -1,2 +1 @@ -window.navigationData = - "data:application/octet-stream;base64,H4sIAAAAAAAAE4WTS2sCMRRG/0vW0qn27U6koItCH9AuSheZmasGYxLvvalK6X8vasd5xcz6fOcQLuTzRzBsWQzFyKlHRIuiJ5zkhRiKTEsioKQgFwteadETS2VyMewP7n97J/tdapVLVtaciTQGsdaLBw9vLNlT2eGdA0oqqF4YXD7c9W8GlcorrD0QP6k817CRCM1Wa9BdJGcNwUSaXAO2ezXcVWscZGpmtlkMTLqqH5BOrF0Wb2kWG7irtt6fu2x8S1Qy1UDJAdTtq6qIIDWrVdAtWEQntijnQfsfReTMmpmal+7Mm2x/Q0qOpK7eXlfUOfD4rH2CkYCTSDBybpqHCiWNJNCbkIveRCRiBLkKeUcSU31KGaoUgnYBI4GN4kXol5WV+qIj9Yx2uztXOcBW4OsPsjwAYcYEAAA="; +window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAE43U0U7CMBQG4HfpNXGCgsqdIRhIJCJGvTBeHLYzaCjtbM8EYnx3Iwrbuq7ldv9/vmynWd++GOGWWJ/dZnyotdKsxTKgJeuzWIAxaKJDcraktWAttuIyYf125/q7dZx+AcETIK5kA2IVfNZArTOBhMljjjk+EVBuCpBLQp1CjCZyFatwp9srwXcgBoKjJKd2TH3EWE61Wmg0JvRyzqaf3hfDrl3zofua56NLuY+ZIQjia59UrfiwJ1IaFj6r0vBTGmHN5cKLVTo+boKkeVzaO+0yNNH/Y2vy/Oaq3e3Yu7ZP7k9oPq2aMsOPHA3dq4WNFMmJxoQnicANaGygikJYNJmSBkcgE4G67lXiE7RckAPJhX0+9dlcPmS/90htyUUSMp61aDCKJGRYF9pYpsrGHJWQ+orzkVKrwz5t0YpDWgqiED5Bc5gLNFEKojp5UR6KNQKh465McxnvVxNZlSrWuyxh3Phv88J0N710UDwNykAbHMokU1zSOHFRVsWDbTgtXf9dYVUbAWqq1XbXpOzDGvD+A5P5Ic/UBwAA" \ No newline at end of file diff --git a/docs/reference/assets/search.js b/docs/reference/assets/search.js index 285a820..51702cb 100644 --- a/docs/reference/assets/search.js +++ b/docs/reference/assets/search.js @@ -1,2 +1 @@ -window.searchData = - "data:application/octet-stream;base64,H4sIAAAAAAAAE62ZXW+jOBSG/4vn1k1zDiEhuRutdjW9WGk/pNkLFFUkuA0qAcZAO6Oo/31lIGDjQ2umc9UK+33PwX58jJ0Lk/lLyXbhhT0lWcx26xVnWXQWbMeOefaQPDLOapmyHXuos2OV5Fl52zYsTtU5ZZwd06gsRcl2jL1ywuZRVL9NOfVtb5p52Jt9q0UteqPnSCbRIRXlbfPcNSNZZ0Quss5cDcr6UB5lchCETd/mavaSVKc/kzhOxUskKUezwxzbv2T+/ceEY9P2phkutxvwh7H/R3yrRVkRuVY/ClHeWu1vp+r73rr3vr9XHk6Gn/q+mi9nRSRFVpFpkiRJEaVVcqZguja9mT9g0Ht9LpLfpcxl79Upbq8Nbzr5gPqaKytZH6v3zD6ZPemh6PPS0l6ixnEVVXX5TqC+08/FOOTxj3ciqC43OCuCNvhfozSJIwU2PQej9l8yFZSn04yMk9VW2xphNQzbQyLSuOljzw8Z3RTMjr5cBXrF/mNueEszNwMXMMnIBJ9VGd8k5U2SnYRMKhH/RAYktmT8rudHo9vVtizyrBRfoixOhbRKo9H68Upr271fZ80EySpbVrmMHqki27U4b7mVFNGZ2m+bhlkb2d/qY+FfE7F2HLSWWY6jab3LHvKRM9Hjo5M2ZfnexFHJTqyCc/k4L+iiVTgHXnS5TsRP8+PM+K3iV8WfP+qLuUNvZzCG6z9x+JLnT9f1Nspn1Oq6nopIluJzUdzFxJoaGim7PWdJFovvbHdhz0KWSZ6xHcOFt9gy3m5c6kTRRuLsmJ/P6u05i/Nj3fy777p9FWqzVJ3b3rdLxsMl95aLLez3PLxqm+fNg6vF8KTRAeMhEDqwdGDokPEQCR1aOjR0HuOhR+g8S+cZuhXj4YrQrSzdytD5jIc+ofMtnW/o1oyHa0K3tnRrQ7dhPNwQuo2l2xi6gPEw4IiLlbcxhIElDAzhlvFwSwTcWrqtOfGKA6CQAZsZGEHTUAMc/UWwRFNMgGOSAwoIQFJs0wMmPqCoAI8U2wiByRAoNICiCGyMwOQI/OlXtlkCEyZQjACFIdg8gQkUKE6AQhFspsCECoLpoba5AhMs2E4PtQ0XmHRhQxe1ENCmC026EKbWAtpw4agsNXAFVFyiMplsYcMWtZbQRgtNtFDRgtRqQhstNNFCBQtSRRhtstAkC9eTY2WThSZZqGBBsojbZKFJFipWkCrkaIOFJlioUEFqGaLNFZpceQoVpFaSZ3PlmVx5ChWkVpJnc9U9anbsZyErEd+1O3cY9t+UF3bfbef9CfDCAra7vHKG0P1dq7+vw2bePO33c9Wm4kZFItoz+GAKy8EVlk427YlKs/A0C68VwdbJ6nqHOZhp6bhlYxzrtaRAS6obJvCdHJtvH9EdlDVHX3N0G/BHUdlvqCfm6jKV0lrz2jiZNR/ggwNqc4eBk0NzhNAcUHNwy6H5fo2KIokNI+1lPLeR6a6bNQ8tF3eH6xWF5qNhiCsnp+HCcrDZDi5u60G2l6Nn7Q53cNsMbm7jLLtDx+l6R6GRo5mh20JrruUHBw0cz0lvjzJo0wXYrVE3CPuLC81Nq4/oNvvXWwvNRJsydHyv4deGwWc12LjhMy72qDmgGzzP/enVrvKaG7glNHJLmjsTLT+tjKFbXX0Rh1OeP8n+oKy5abXVc8NR/UhCrxPNyzGxpDoV7W8xg4tWjRyK/Z6zIilEmmSC7cL96+v/kPat4robAAA="; +window.searchData = "data:application/octet-stream;base64,H4sIAAAAAAAAE72cWY/bRhKA/wvnVZanT1LztggSxMAGm83uZh8EY8CRODNEJFIhKTuG4f++aB5iFatabI6MfZI97Dq6+qvqg8fXqCo/19HD9mv0R17sowclV1GRHrPoIXpOD9EqOleH6CH6lFZ5+nTI6vfP6WH92hzdpd0hreusjh6i6NtqUGD1RcGuytIm+yk9/HDIs6K5KHs+F7smL4v6/aTFVcXS2ItmqjMvmqx6TndZ/T5MnbiXo6d/nrNzNq/rbmgHNK6iU1o5T6BXHjNVlh6a/BhiCTR9o7G6Kav0JcTW2PLNpqosPebFS5CxsW24OXmvkzGM5yIkgm2rN5qoz0/1rsqfgsIH2t4UwODoLTJkjVFj5jw+Nl9OYVA4S+tL+xmD694zthB8zpvXX/L9/pB9TquMqQO4QWh9cVK/VuVfXzwa22vXa8r9JhZmLHm/ZX+es7phfHVRqN+T69ddvRb5awrvZqJO3eTL5D9dufIXSnB5Jko4M455kLq7S1O+F9A7n7Embc51mLGh6Q3GHMH/WmISCbzd8FBA/l0usc5IvdmFKqvPh7BRvTR9s7FdWuyyQ5CxS9NFxkAK/NbPo/4swC3CE2FXFkW2C1V5Nzb35TRyFEwUcrT5t1P+Y1WV1cVor+v9cOGq+0ZI6H3dVOddM6fsDrfknb/45Z3fENa8oZkMnrPxVO6/zFhwTd7JRRZA8H9PD/k+ddMLPwaT699lKDidQSMydRZAbKXQY9ie8+ywb9vQ8WGtY4HF1mEKvWTNT0vNE5mlHoSAyVpm+Gzq/bu8fpcXr1mVN9n+DR6w2LL2+5a3WqdrnvpUFnX2c1rsD1lFFijo6u3rHapufrWDHeQL/b+6TYy/zqMG4VvD8+lQpvswhXeXxnxPsI/Ll+hei/0y/Uptw6K9n54uN1Va1M9ldfxQnM6BsbwjQv+3EGDLczsWrGDitQ+tfst6DS7UZMFy2rvz41TO7f+mnvqzvl05TVabXYqCK4v2TpOK86F4LieamRa31hOfyrmawjnryYZj/bLM6LqTCDY8EOuxfyh3C+13Et/L/vKor5eGnnowheu/2dPPZfnHMBVM/JlcDT0/OKVVnf1Y7E9lXjQfxho/niJMWoQqzmsuvUa16Hq40h/K4+mQNdl+TjvXcOE5CNoOXubu8+F6aQtbAQxaAiZ+54WHy33apLO6132rawZm+K+6wxbAh9cYbLrMIhmAc/GPUzucU6uXCzcPBNY0OxijR55A5Wi1cM3Ium36TgQYmxmcY9a8lmRkeKOXtsttTofnP9WBH57xwq3DM9E0NzzAI99W5/y0L49pXgQZWsPmcyZnBunPc1Z9CbM6NL3R4iltXsMM9i2X2/McHf+9nK4Wxgvf6bB40BR4Suw88mZPXcNbRNfMrMfGc+bmFjTZJ3D6d9Xm0PRGi3V5rnaB3by0vdGmO8urm/R4CjMLmy+3PIXxl6yp8t20OPV/vRVDqGaOwcER75TxnFVZscse0W1Rr501EbhqlgkU2NZ9KNrlEbeYAtsw2ir87MB/+8Kjde4MlHH52h3tx1NZ565+LXGBSN7qStUvyx+dCwscmcjd7kabP485e5jjdwJIhR29XXUJA/hrVb5UWc1uF5BzTMPvg6FP8TyJnO9vJsDrRiAEC5w5lC9LY9GLfJdIXIfwShzewOF1xyCKV7eWwMPFO8tAEL165zhkHX8jhn4ngihc4ooPQr8LVxlcYvo4WRsEWR+FvscwXMuBa4OwOAV4tz6uorzYZ39FD1+jT1lVu2nuIZJrtd5Eq+6GmHskrvN3Fe3K47E79t2Xu3P7z499s98zdxPONe5av7+PVtv7lRZrYdXHj6vtINxeaP8w6Bj/0gqKaLUVnKAgggIJymi1lZygJIISCapotVWcoCKCCgnqaLXVnKAmghoJmmi1NZygIYIGCdpotbWcoCWCFgnG0Wobc4IxEYyRYBKttgknmBDBBAluotV2s1J2fW8MEtwQwQ0GwPEg7lfyfh1vNEaAwiMm9LT48PwwAGGChONCsAwJCpHAFAnHhmA5EhQkgUkS2t9nCpPANAnHiGBBFBQogYkSjhPBwigoVAJTJRwrwq6kXeMxFhQrgbkSjhbBIikoWgKzJRwxgsVSULwE5ku2fLFoSsqXxHxJR4xkC5ukfMlJhWpLFAunZIoU5ks6YiRf4ChfEvMlHTGShVNSviTmSzpipGYDRvmSmC9pPYhISpfEdElHjDSsXcqXxHxJR4xka6WkfEnMl9z4e0z5kpgv5YiRLNmK8qUwX6rliyVbUb4U5ktJT6wVpUtN5kDljbVipkFMl2rp2rBOU7oUpksZb+lTlC6F6VKOGMWmo6J8KcyXcsQoNh0V5UthvlTid5vypTBfyhGj2FxWlC+F+dL++VFTvjTmSztiFFsINOVLY7609M3pmgKmMWC6XWXxqyUKmJ4stBwyip2kNLPWwoBpP2CaAqYxYLoFjC0jmgKmMWC6BYytBJoCpjFg2iGj2EqgKWAaA6ZbwNiM1BQwjQEzDhnNJpWhgBkMmHHIaDapDAXMYMCMY0azeWEoYQYTZpR3XjeUMIMJM/4FmKGEmcly3jGj2aQyzIoeE2YcM5pNDEMJM5gw45jR/FaCEmYwYcZfwgwlzGDCjGNGs4lhKGEGE2ZbwtjEsJQwiwmzLWFsYlhKmMWEWents6WEWUyYdcxoNqssJcxiwqxjxrBZZSlhFhNm2x0jm1WWEmYnm0bHjGGzyjL7RkyYjf0Bo4RZTJh1zBg2MSwlzGLCrGPGsIlhKWEWExY7ZgybGDElLMaExY4Zw7IdU8JiTFjsmDExV4ZiSliMCYuVN9oxJSzGhMUtYWxixJSwGBMWt4SxbMeUsBgTFvsW+THlK54cTDhiLJsWMXM2gfmKHTFWrJRc2w2WpXjFGK/YAWMlJ0vpijFdiePFslwnlK4E05UIT7ASylaC2Uqkr78JRSvBaCUOFsvuaRKKVoLRSrQvWAklK8FkJY4Vy6ZiQslKMFmJj6yEkpVgspLYGywKVjI59Ur8wWIOvjBZycZbARKKVoLR2tz7Ir2hZPV/ao9xP2VVk+0/dMe52+3ljvXX6LE/4xX3w8nz10jcRw9fv60iobtfZfrfpPvV/XXd/9307Ux/3cr+N+5+Y+V+v41nxO1fL8fE7przPD3lWffKyOiWVKNbUgep6V4AACoMUNF7pMI8Gt6rAspAmKQIUzKcrre3jIcbNaNKC/xLTKDK/i0p4JgEjgX2Dr4gAzRpoKkfYbkJ09i+qP6cHnb9I+CAL4BXkK7ukcRRgQYkmLAwta/mjxrA0N2HytOOwDiHaXG3QLL+PRygJx4VqTB/XrLGpywBysLiO31GZFRmgLI4LOfy4tTfRPVBDkYvCett/6AkIMACAsKAzIvWH49TZgM6GoZUXs+lswaAmDBCcl/UNMgaEzas/bNgoJMgnW1Y1Non40cNCpCq40ANL3j4gROJ6oSSJEjV5YE60CUAkw3V0j3VCmILemXDiLzc3gWuAC1xP+0lYVFu338AUQZ0axukoX3uPuufu89x70CxM2G9657xBF0D8Nmw7OifSwU6gBs2rJT0Hw8BgQGlLVwDeDQLYAjcicNIbpUx0xkAWgQHh8txBTTpsA6OXz0BAw60LFLCzHBg4GVYBYMPHYBwg1IYb/rk6NeKmzAoe8UTuEFnTViq9HoOJco5Axy0ixQdwbcvABWAVRE6lvCZGRA7MAhxv65OhgIThu6g+nV4ZRUMMZj5VCgv3csuoLdAiey3BCaw12eUl7CIhsqXw5sFgAuwkjZhiobnmAETQEkcBilNagGKuej3HNJ2v6ofxbj/fyL63zAAL58hAi6Dbi/RQVNfgQKpQv3p3rUclQAs+qTXYVUEfPQIJAJI0WVamO6B/NRh66lOGzPCgFkRtgYB74oAZkG0bFjyDB+MAc4AAkRglMbPMY16wMI/uEudlqZkQgTUibA1EXi+H+ANEIjDRu3ywjLZRChQ8lWgU5OjEgX6pcPiNLzxDpSA+q7CatW5OnBFD3hjw6Lz6fJuKT1vAbGWYX2baMvbN5pBRwGbOmyq+Zw9vZblH9XlNVagDaSLDhs+940rz3QNDyXCQueUnbqPaQE9gCkRUOo+rqJTfsoOeZFFD9uP3779D9KIs1xgUAAA"; \ No newline at end of file diff --git a/docs/reference/classes/ApiError.html b/docs/reference/classes/ApiError.html index 1c13bbb..5902f0e 100644 --- a/docs/reference/classes/ApiError.html +++ b/docs/reference/classes/ApiError.html @@ -1,4 +1,4 @@ -ApiError | @fal-ai/serverless-client - v0.14.2

Type Parameters

  • Body

Hierarchy (view full)

Constructors

constructor +ApiError | @fal-ai/client - v1.0.0

Class ApiError<Body>

Type Parameters

  • Body

Hierarchy (view full)

Constructors

Properties

Constructors

Properties

body: Body
status: number
+

Constructors

Properties

body: Body
status: number
diff --git a/docs/reference/classes/ValidationError.html b/docs/reference/classes/ValidationError.html index a1d0bfd..f935c97 100644 --- a/docs/reference/classes/ValidationError.html +++ b/docs/reference/classes/ValidationError.html @@ -1,6 +1,6 @@ -ValidationError | @fal-ai/serverless-client - v0.14.2

Hierarchy (view full)

  • ApiError<ValidationErrorBody>
    • ValidationError

Constructors

constructor +ValidationError | @fal-ai/client - v1.0.0

Class ValidationError

Hierarchy (view full)

  • ApiError<ValidationErrorBody>
    • ValidationError

Constructors

Properties

Accessors

Methods

Constructors

Properties

body: ValidationErrorBody
status: number

Accessors

Methods

+

Constructors

Properties

body: ValidationErrorBody
status: number

Accessors

Methods

diff --git a/docs/reference/functions/config.html b/docs/reference/functions/config.html deleted file mode 100644 index 60b5af6..0000000 --- a/docs/reference/functions/config.html +++ /dev/null @@ -1,3 +0,0 @@ -config | @fal-ai/serverless-client - v0.14.2
  • Configures the fal serverless client.

    -

    Parameters

    • config: Config

      the new configuration.

      -

    Returns void

diff --git a/docs/reference/functions/createFalClient.html b/docs/reference/functions/createFalClient.html new file mode 100644 index 0000000..c543e63 --- /dev/null +++ b/docs/reference/functions/createFalClient.html @@ -0,0 +1,4 @@ +createFalClient | @fal-ai/client - v1.0.0

Function createFalClient

  • Creates a new reference of the FalClient.

    +

    Parameters

    • userConfig: Config = {}

      Optional configuration to override the default settings.

      +

    Returns FalClient

    a new instance of the FalClient.

    +
diff --git a/docs/reference/functions/getConfig.html b/docs/reference/functions/getConfig.html deleted file mode 100644 index 11f6992..0000000 --- a/docs/reference/functions/getConfig.html +++ /dev/null @@ -1,3 +0,0 @@ -getConfig | @fal-ai/serverless-client - v0.14.2
  • Get the current fal serverless client configuration.

    -

    Returns RequiredConfig

    the current client configuration.

    -
diff --git a/docs/reference/functions/isCompletedQueueStatus.html b/docs/reference/functions/isCompletedQueueStatus.html new file mode 100644 index 0000000..d0ed935 --- /dev/null +++ b/docs/reference/functions/isCompletedQueueStatus.html @@ -0,0 +1 @@ +isCompletedQueueStatus | @fal-ai/client - v1.0.0

Function isCompletedQueueStatus

diff --git a/docs/reference/functions/isQueueStatus.html b/docs/reference/functions/isQueueStatus.html new file mode 100644 index 0000000..9bee16c --- /dev/null +++ b/docs/reference/functions/isQueueStatus.html @@ -0,0 +1 @@ +isQueueStatus | @fal-ai/client - v1.0.0

Function isQueueStatus

diff --git a/docs/reference/functions/parseAppId.html b/docs/reference/functions/parseAppId.html deleted file mode 100644 index d1b7a51..0000000 --- a/docs/reference/functions/parseAppId.html +++ /dev/null @@ -1 +0,0 @@ -parseAppId | @fal-ai/serverless-client - v0.14.2
  • Parameters

    • id: string

    Returns AppId

diff --git a/docs/reference/functions/parseEndpointId.html b/docs/reference/functions/parseEndpointId.html new file mode 100644 index 0000000..642fbc5 --- /dev/null +++ b/docs/reference/functions/parseEndpointId.html @@ -0,0 +1 @@ +parseEndpointId | @fal-ai/client - v1.0.0

Function parseEndpointId

  • Parameters

    • id: string

    Returns EndpointId

diff --git a/docs/reference/functions/run.html b/docs/reference/functions/run.html deleted file mode 100644 index 033d81a..0000000 --- a/docs/reference/functions/run.html +++ /dev/null @@ -1,4 +0,0 @@ -run | @fal-ai/serverless-client - v0.14.2
  • Runs a fal serverless function identified by its id.

    -

    Type Parameters

    • Input
    • Output

    Parameters

    • id: string

      the registered function revision id or alias.

      -
    • options: RunOptions<Input> = {}

    Returns Promise<Output>

    the remote function output

    -
diff --git a/docs/reference/functions/stream.html b/docs/reference/functions/stream.html deleted file mode 100644 index f5620cc..0000000 --- a/docs/reference/functions/stream.html +++ /dev/null @@ -1,7 +0,0 @@ -stream | @fal-ai/serverless-client - v0.14.2
  • Calls a fal app that supports streaming and provides a streaming-capable -object as a result, that can be used to get partial results through either -AsyncIterator or through an event listener.

    -

    Type Parameters

    • Input = Record<string, any>
    • Output = any

    Parameters

    • endpointId: string

      the endpoint id, e.g. fal-ai/llavav15-13b.

      -
    • options: StreamOptions<Input>

      the request options, including the input payload.

      -

    Returns Promise<FalStream<Input, Output>>

    the FalStream instance.

    -
diff --git a/docs/reference/functions/subscribe.html b/docs/reference/functions/subscribe.html deleted file mode 100644 index 1f4f2ef..0000000 --- a/docs/reference/functions/subscribe.html +++ /dev/null @@ -1,5 +0,0 @@ -subscribe | @fal-ai/serverless-client - v0.14.2
  • Subscribes to updates for a specific request in the queue.

    -

    Type Parameters

    • Input
    • Output

    Parameters

    • endpointId: string

      The ID of the function web endpoint.

      -
    • options: RunOptions<Input> & QueueSubscribeOptions = {}

      Options to configure how the request is run and how updates are received.

      -

    Returns Promise<Output>

    A promise that resolves to the result of the request once it's completed.

    -
diff --git a/docs/reference/functions/withMiddleware.html b/docs/reference/functions/withMiddleware.html index 97c1617..953c273 100644 --- a/docs/reference/functions/withMiddleware.html +++ b/docs/reference/functions/withMiddleware.html @@ -1,4 +1,4 @@ -withMiddleware | @fal-ai/serverless-client - v0.14.2
diff --git a/docs/reference/functions/withProxy.html b/docs/reference/functions/withProxy.html index 2323c1c..e1e9722 100644 --- a/docs/reference/functions/withProxy.html +++ b/docs/reference/functions/withProxy.html @@ -1 +1 @@ -withProxy | @fal-ai/serverless-client - v0.14.2
+withProxy | @fal-ai/client - v1.0.0

Function withProxy

diff --git a/docs/reference/hierarchy.html b/docs/reference/hierarchy.html index b918b08..9976d41 100644 --- a/docs/reference/hierarchy.html +++ b/docs/reference/hierarchy.html @@ -1 +1 @@ -@fal-ai/serverless-client - v0.14.2

@fal-ai/serverless-client - v0.14.2

Class Hierarchy

+@fal-ai/client - v1.0.0

@fal-ai/client - v1.0.0

Class Hierarchy

diff --git a/docs/reference/index.html b/docs/reference/index.html index 56eabef..09b9ec8 100644 --- a/docs/reference/index.html +++ b/docs/reference/index.html @@ -1,19 +1,28 @@ -@fal-ai/serverless-client - v0.14.2

@fal-ai/serverless-client - v0.14.2

Index

Classes

ApiError +@fal-ai/client - v1.0.0
+
diff --git a/docs/reference/interfaces/CompletedQueueStatus.html b/docs/reference/interfaces/CompletedQueueStatus.html new file mode 100644 index 0000000..fd95083 --- /dev/null +++ b/docs/reference/interfaces/CompletedQueueStatus.html @@ -0,0 +1,6 @@ +CompletedQueueStatus | @fal-ai/client - v1.0.0

Interface CompletedQueueStatus

interface CompletedQueueStatus {
    logs: RequestLog[];
    metrics?: Metrics;
    request_id: string;
    response_url: string;
    status: "COMPLETED";
}

Hierarchy

  • BaseQueueStatus
    • CompletedQueueStatus

Properties

logs: RequestLog[]
metrics?: Metrics
request_id: string
response_url: string
status: "COMPLETED"
diff --git a/docs/reference/interfaces/FalClient.html b/docs/reference/interfaces/FalClient.html new file mode 100644 index 0000000..9946c50 --- /dev/null +++ b/docs/reference/interfaces/FalClient.html @@ -0,0 +1,40 @@ +FalClient | @fal-ai/client - v1.0.0

Interface FalClient

The main client type, it provides access to simple API model usage, +as well as access to the queue and storage APIs.

+

createFalClient

+
interface FalClient {
    queue: QueueClient;
    realtime: RealtimeClient;
    storage: StorageClient;
    stream: (<Output, Input>(endpointId: string, options: StreamOptions<Input>) => Promise<FalStream<Input, Output>>);
    streaming: StreamingClient;
    run<Output, Input>(endpointId: string, options: RunOptions<Input>): Promise<Result<Output>>;
    subscribe<Output, Input>(endpointId: string, options: RunOptions<Input> & QueueSubscribeOptions): Promise<Result<Output>>;
}

Properties

The queue client to interact with the queue API.

+
realtime: RealtimeClient

The realtime client to interact with the realtime API +and receive updates in real-time.

+
    +
  • #RealtimeClient
  • +
  • #RealtimeClient.connect
  • +
+
storage: StorageClient

The storage client to interact with the storage API.

+
stream: (<Output, Input>(endpointId: string, options: StreamOptions<Input>) => Promise<FalStream<Input, Output>>)

Calls a fal app that supports streaming and provides a streaming-capable +object as a result, that can be used to get partial results through either +AsyncIterator or through an event listener.

+

Type declaration

    • <Output, Input>(endpointId, options): Promise<FalStream<Input, Output>>
    • Calls a fal app that supports streaming and provides a streaming-capable +object as a result, that can be used to get partial results through either +AsyncIterator or through an event listener.

      +

      Type Parameters

      • Output = any
      • Input = Record<string, any>

      Parameters

      • endpointId: string

        the endpoint id, e.g. fal-ai/llavav15-13b.

        +
      • options: StreamOptions<Input>

        the request options, including the input payload.

        +

      Returns Promise<FalStream<Input, Output>>

      the FalStream instance.

      +

the endpoint id, e.g. fal-ai/llavav15-13b.

+

the request options, including the input payload.

+

the FalStream instance.

+
streaming: StreamingClient

The streaming client to interact with the streaming API.

+

#stream

+

Methods

  • Subscribes to updates for a specific request in the queue.

    +

    Type Parameters

    • Output = any
    • Input = Record<string, any>

    Parameters

    • endpointId: string

      The ID of the API endpoint.

      +
    • options: RunOptions<Input> & QueueSubscribeOptions

      Options to configure how the request is run and how updates are received.

      +

    Returns Promise<Result<Output>>

    A promise that resolves to the result of the request once it's completed.

    +
diff --git a/docs/reference/interfaces/InProgressQueueStatus.html b/docs/reference/interfaces/InProgressQueueStatus.html new file mode 100644 index 0000000..259921d --- /dev/null +++ b/docs/reference/interfaces/InProgressQueueStatus.html @@ -0,0 +1,5 @@ +InProgressQueueStatus | @fal-ai/client - v1.0.0

Interface InProgressQueueStatus

interface InProgressQueueStatus {
    logs: RequestLog[];
    request_id: string;
    response_url: string;
    status: "IN_PROGRESS";
}

Hierarchy

  • BaseQueueStatus
    • InProgressQueueStatus

Properties

logs: RequestLog[]
request_id: string
response_url: string
status: "IN_PROGRESS"
diff --git a/docs/reference/interfaces/InQueueQueueStatus.html b/docs/reference/interfaces/InQueueQueueStatus.html new file mode 100644 index 0000000..181f82e --- /dev/null +++ b/docs/reference/interfaces/InQueueQueueStatus.html @@ -0,0 +1,5 @@ +InQueueQueueStatus | @fal-ai/client - v1.0.0

Interface InQueueQueueStatus

interface InQueueQueueStatus {
    queue_position: number;
    request_id: string;
    response_url: string;
    status: "IN_QUEUE";
}

Hierarchy

  • BaseQueueStatus
    • InQueueQueueStatus

Properties

queue_position: number
request_id: string
response_url: string
status: "IN_QUEUE"
diff --git a/docs/reference/interfaces/QueueClient.html b/docs/reference/interfaces/QueueClient.html new file mode 100644 index 0000000..2934f69 --- /dev/null +++ b/docs/reference/interfaces/QueueClient.html @@ -0,0 +1,36 @@ +QueueClient | @fal-ai/client - v1.0.0

Interface QueueClient

Represents a request queue with methods for submitting requests, +checking their status, retrieving results, and subscribing to updates.

+
interface QueueClient {
    cancel(endpointId: string, options: BaseQueueOptions): Promise<void>;
    result<Output>(endpointId: string, options: BaseQueueOptions): Promise<Result<Output>>;
    status(endpointId: string, options: QueueStatusOptions): Promise<QueueStatus>;
    streamStatus(endpointId: string, options: QueueStatusStreamOptions): Promise<FalStream<unknown, QueueStatus>>;
    submit<Input>(endpointId: string, options: SubmitOptions<Input>): Promise<InQueueQueueStatus>;
    subscribeToStatus(endpointId: string, options: QueueStatusSubscriptionOptions): Promise<CompletedQueueStatus>;
}

Methods

  • Cancels a request in the queue.

    +

    Parameters

    • endpointId: string

      The ID of the function web endpoint.

      +
    • options: BaseQueueOptions

      Options to configure how the request +is run and how updates are received.

      +

    Returns Promise<void>

    A promise that resolves once the request is cancelled.

    +

    If the request cannot be cancelled.

    +
  • Retrieves the result of a specific request from the queue.

    +

    Type Parameters

    • Output

    Parameters

    • endpointId: string

      The ID of the function web endpoint.

      +
    • options: BaseQueueOptions

      Options to configure how the request is run.

      +

    Returns Promise<Result<Output>>

    A promise that resolves to the result of the request.

    +
  • Retrieves the status of a specific request in the queue.

    +

    Parameters

    • endpointId: string

      The ID of the function web endpoint.

      +
    • options: QueueStatusOptions

      Options to configure how the request is run.

      +

    Returns Promise<QueueStatus>

    A promise that resolves to the status of the request.

    +
  • Subscribes to updates for a specific request in the queue using HTTP streaming events.

    +

    Parameters

    • endpointId: string

      The ID of the function web endpoint.

      +
    • options: QueueStatusStreamOptions

      Options to configure how the request is run and how updates are received.

      +

    Returns Promise<FalStream<unknown, QueueStatus>>

    The streaming object that can be used to listen for updates.

    +
  • Submits a request to the queue.

    +

    Type Parameters

    • Input

    Parameters

    • endpointId: string

      The ID of the function web endpoint.

      +
    • options: SubmitOptions<Input>

      Options to configure how the request is run.

      +

    Returns Promise<InQueueQueueStatus>

    A promise that resolves to the result of enqueuing the request.

    +
  • Subscribes to updates for a specific request in the queue using polling or streaming. +See options.mode for more details.

    +

    Parameters

    • endpointId: string

      The ID of the function web endpoint.

      +
    • options: QueueStatusSubscriptionOptions

      Options to configure how the request is run and how updates are received.

      +

    Returns Promise<CompletedQueueStatus>

    A promise that resolves to the final status of the request.

    +
diff --git a/docs/reference/interfaces/RealtimeClient.html b/docs/reference/interfaces/RealtimeClient.html new file mode 100644 index 0000000..3e5d2d8 --- /dev/null +++ b/docs/reference/interfaces/RealtimeClient.html @@ -0,0 +1,6 @@ +RealtimeClient | @fal-ai/client - v1.0.0

Interface RealtimeClient

interface RealtimeClient {
    connect<Input, Output>(app: string, handler: RealtimeConnectionHandler<Output>): RealtimeConnection<Input>;
}

Methods

Methods

  • Connect to the realtime endpoint. The default implementation uses +WebSockets to connect to fal function endpoints that support WSS.

    +

    Type Parameters

    • Input = any
    • Output = any

    Parameters

    • app: string

      the app alias or identifier.

      +
    • handler: RealtimeConnectionHandler<Output>

      the connection handler.

      +

    Returns RealtimeConnection<Input>

diff --git a/docs/reference/interfaces/StorageClient.html b/docs/reference/interfaces/StorageClient.html new file mode 100644 index 0000000..0b2b01e --- /dev/null +++ b/docs/reference/interfaces/StorageClient.html @@ -0,0 +1,14 @@ +StorageClient | @fal-ai/client - v1.0.0

Interface StorageClient

File support for the client. This interface establishes the contract for +uploading files to the server and transforming the input to replace file +objects with URLs.

+
interface StorageClient {
    transformInput: ((input: Record<string, any>) => Promise<Record<string, any>>);
    upload: ((file: Blob) => Promise<string>);
}

Properties

Properties

transformInput: ((input: Record<string, any>) => Promise<Record<string, any>>)

Transform the input to replace file objects with URLs. This is used +to transform the input before sending it to the server and ensures +that the server receives URLs instead of file objects.

+

Type declaration

upload: ((file: Blob) => Promise<string>)

Upload a file to the server. Returns the URL of the uploaded file.

+

Type declaration

    • (file): Promise<string>
    • Parameters

      • file: Blob

        the file to upload

        +

      Returns Promise<string>

      the URL of the uploaded file

      +
diff --git a/docs/reference/interfaces/StreamingClient.html b/docs/reference/interfaces/StreamingClient.html new file mode 100644 index 0000000..48ba2ce --- /dev/null +++ b/docs/reference/interfaces/StreamingClient.html @@ -0,0 +1,9 @@ +StreamingClient | @fal-ai/client - v1.0.0

Interface StreamingClient

The streaming client interface.

+
interface StreamingClient {
    stream<Output, Input>(endpointId: string, options: StreamOptions<Input>): Promise<FalStream<Input, Output>>;
}

Methods

Methods

  • Calls a fal app that supports streaming and provides a streaming-capable +object as a result, that can be used to get partial results through either +AsyncIterator or through an event listener.

    +

    Type Parameters

    • Output = any
    • Input = Record<string, any>

    Parameters

    • endpointId: string

      the endpoint id, e.g. fal-ai/llavav15-13b.

      +
    • options: StreamOptions<Input>

      the request options, including the input payload.

      +

    Returns Promise<FalStream<Input, Output>>

    the FalStream instance.

    +
diff --git a/docs/reference/types/Metrics.html b/docs/reference/types/Metrics.html new file mode 100644 index 0000000..7640ca1 --- /dev/null +++ b/docs/reference/types/Metrics.html @@ -0,0 +1 @@ +Metrics | @fal-ai/client - v1.0.0

Type Alias Metrics

Metrics: {
    inference_time: number | null;
}
diff --git a/docs/reference/types/QueueStatus.html b/docs/reference/types/QueueStatus.html index 56a07ec..36bb739 100644 --- a/docs/reference/types/QueueStatus.html +++ b/docs/reference/types/QueueStatus.html @@ -1 +1 @@ -QueueStatus | @fal-ai/serverless-client - v0.14.2
QueueStatus: InProgressQueueStatus | CompletedQueueStatus | EnqueuedQueueStatus
+QueueStatus | @fal-ai/client - v1.0.0
diff --git a/docs/reference/types/RequestLog.html b/docs/reference/types/RequestLog.html new file mode 100644 index 0000000..1ad5440 --- /dev/null +++ b/docs/reference/types/RequestLog.html @@ -0,0 +1 @@ +RequestLog | @fal-ai/client - v1.0.0

Type Alias RequestLog

RequestLog: {
    level:
        | "STDERR"
        | "STDOUT"
        | "ERROR"
        | "INFO"
        | "WARN"
        | "DEBUG";
    message: string;
    source: "USER";
    timestamp: string;
}
diff --git a/docs/reference/types/RequestMiddleware.html b/docs/reference/types/RequestMiddleware.html index 5a8201b..d324f13 100644 --- a/docs/reference/types/RequestMiddleware.html +++ b/docs/reference/types/RequestMiddleware.html @@ -1 +1 @@ -RequestMiddleware | @fal-ai/serverless-client - v0.14.2

Type Alias RequestMiddleware

RequestMiddleware: ((request: RequestConfig) => Promise<RequestConfig>)
+RequestMiddleware | @fal-ai/client - v1.0.0

Type Alias RequestMiddleware

RequestMiddleware: ((request: RequestConfig) => Promise<RequestConfig>)
diff --git a/docs/reference/types/ResponseHandler.html b/docs/reference/types/ResponseHandler.html index e044200..f64caa3 100644 --- a/docs/reference/types/ResponseHandler.html +++ b/docs/reference/types/ResponseHandler.html @@ -1 +1 @@ -ResponseHandler | @fal-ai/serverless-client - v0.14.2

Type Alias ResponseHandler<Output>

ResponseHandler<Output>: ((response: Response) => Promise<Output>)

Type Parameters

  • Output
+ResponseHandler | @fal-ai/client - v1.0.0

Type Alias ResponseHandler<Output>

ResponseHandler<Output>: ((response: Response) => Promise<Output>)

Type Parameters

  • Output
diff --git a/docs/reference/types/Result.html b/docs/reference/types/Result.html new file mode 100644 index 0000000..7145a6a --- /dev/null +++ b/docs/reference/types/Result.html @@ -0,0 +1,3 @@ +Result | @fal-ai/client - v1.0.0

Type Alias Result<T>

Result<T>: {
    data: T;
    requestId: string;
}

Represents an API result, containing the data, +the request ID and any other relevant information.

+

Type Parameters

  • T
diff --git a/docs/reference/types/RunOptions.html b/docs/reference/types/RunOptions.html new file mode 100644 index 0000000..2344053 --- /dev/null +++ b/docs/reference/types/RunOptions.html @@ -0,0 +1,6 @@ +RunOptions | @fal-ai/client - v1.0.0

Type Alias RunOptions<Input>

RunOptions<Input>: {
    input?: Input;
    method?:
        | "get"
        | "post"
        | "put"
        | "delete"
        | string;
}

The function input and other configuration when running +the function, such as the HTTP method to use.

+

Type Parameters

  • Input

Type declaration

  • Optional Readonlyinput?: Input

    The function input. It will be submitted either as query params +or the body payload, depending on the method.

    +
  • Optional Readonlymethod?:
        | "get"
        | "post"
        | "put"
        | "delete"
        | string

    The HTTP method, defaults to post;

    +
diff --git a/docs/reference/types/UrlOptions.html b/docs/reference/types/UrlOptions.html new file mode 100644 index 0000000..dcb2988 --- /dev/null +++ b/docs/reference/types/UrlOptions.html @@ -0,0 +1,6 @@ +UrlOptions | @fal-ai/client - v1.0.0

Type Alias UrlOptions

UrlOptions: {
    path?: string;
    query?: Record<string, string>;
    subdomain?: string;
}

Type declaration

  • Optionalpath?: string

    The path to append to the function URL.

    +
  • Optional Readonlyquery?: Record<string, string>

    The query parameters to include in the URL.

    +
  • Optional Readonlysubdomain?: string

    If true, the function will use the queue to run the function +asynchronously and return the result in a separate call. This +influences how the URL is built.

    +
diff --git a/docs/reference/types/ValidationErrorInfo.html b/docs/reference/types/ValidationErrorInfo.html index 871b1ed..2792527 100644 --- a/docs/reference/types/ValidationErrorInfo.html +++ b/docs/reference/types/ValidationErrorInfo.html @@ -1 +1 @@ -ValidationErrorInfo | @fal-ai/serverless-client - v0.14.2

Type Alias ValidationErrorInfo

ValidationErrorInfo: {
    loc: (string | number)[];
    msg: string;
    type: string;
}
+ValidationErrorInfo | @fal-ai/client - v1.0.0

Type Alias ValidationErrorInfo

ValidationErrorInfo: {
    loc: (string | number)[];
    msg: string;
    type: string;
}
diff --git a/docs/reference/types/WebHookResponse.html b/docs/reference/types/WebHookResponse.html index 5b72639..c229ba5 100644 --- a/docs/reference/types/WebHookResponse.html +++ b/docs/reference/types/WebHookResponse.html @@ -1,4 +1,4 @@ -WebHookResponse | @fal-ai/serverless-client - v0.14.2

Type Alias WebHookResponse<Payload>

WebHookResponse<Payload>: {
    error: never;
    payload: Payload;
    request_id: string;
    status: "OK";
} | {
    error: string;
    payload: Payload;
    request_id: string;
    status: "ERROR";
}

Represents the response from a WebHook request. +WebHookResponse | @fal-ai/client - v1.0.0

Type Alias WebHookResponse<Payload>

WebHookResponse<Payload>: {
    error: never;
    payload: Payload;
    request_id: string;
    status: "OK";
} | {
    error: string;
    payload: Payload;
    request_id: string;
    status: "ERROR";
}

Represents the response from a WebHook request. This is a union type that varies based on the status property.

Type Parameters

  • Payload = any

    The type of the payload in the response. It defaults to any, allowing for flexibility in specifying the structure of the payload.

    @@ -10,4 +10,4 @@
  • payload: Payload

    The payload of the response, structure determined by the Payload type.

  • request_id: string

    The unique identifier for the request.

  • status: "ERROR"

    Indicates an unsuccessful response.

    -
+
diff --git a/docs/reference/variables/fal.html b/docs/reference/variables/fal.html new file mode 100644 index 0000000..bcc5d7a --- /dev/null +++ b/docs/reference/variables/fal.html @@ -0,0 +1,3 @@ +fal | @fal-ai/client - v1.0.0

Variable falConst

fal: SingletonFalClient = ...

Creates a singleton instance of the client. This is useful as a compatibility +layer for existing code that uses the clients version prior to 1.0.0.

+
diff --git a/docs/reference/variables/queue.html b/docs/reference/variables/queue.html deleted file mode 100644 index 68d1ce1..0000000 --- a/docs/reference/variables/queue.html +++ /dev/null @@ -1,4 +0,0 @@ -queue | @fal-ai/serverless-client - v0.14.2
queue: Queue = ...

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 .

-
diff --git a/docs/reference/variables/realtime.html b/docs/reference/variables/realtime.html deleted file mode 100644 index a6fdc0f..0000000 --- a/docs/reference/variables/realtime.html +++ /dev/null @@ -1,2 +0,0 @@ -realtime | @fal-ai/serverless-client - v0.14.2

Variable realtimeConst

realtime: RealtimeClient = ...

The default implementation of the realtime client.

-
diff --git a/docs/reference/variables/storage.html b/docs/reference/variables/storage.html deleted file mode 100644 index d406ad9..0000000 --- a/docs/reference/variables/storage.html +++ /dev/null @@ -1 +0,0 @@ -storage | @fal-ai/serverless-client - v0.14.2
storage: StorageSupport = ...
diff --git a/libs/client/README.md b/libs/client/README.md index 55feea0..ec7e8b7 100644 --- a/libs/client/README.md +++ b/libs/client/README.md @@ -1,17 +1,17 @@ # fal.ai JavaScript/TypeScript client library -![@fal-ai/serverless-client npm package](https://img.shields.io/npm/v/@fal-ai/serverless-client?color=%237527D7&label=%40fal-ai%2Fserverless-client&style=flat-square) +![@fal-ai/client npm package](https://img.shields.io/npm/v/@fal-ai/client?color=%237527D7&label=%40fal-ai%2Fclient&style=flat-square) ## Introduction -The `fal.ai` JavaScript Client Library provides a seamless way to interact with `fal` serverless functions from your JavaScript or TypeScript applications. With built-in support for various platforms, it ensures consistent behavior across web, Node.js, and React Native environments. +The `fal.ai` JavaScript Client Library provides a seamless way to interact with `fal` endpoints from your JavaScript or TypeScript applications. With built-in support for various platforms, it ensures consistent behavior across web, Node.js, and React Native environments. ## Getting started Before diving into the client-specific features, ensure you've set up your credentials: ```ts -import * as fal from "@fal-ai/serverless-client"; +import { fal } from "@fal-ai/client"; fal.config({ // Can also be auto-configured using environment variables: @@ -49,4 +49,4 @@ const result = await fal.subscribe("my-function-id", { ## More features -The client library offers a plethora of features designed to simplify your serverless journey with `fal.ai`. Dive into the [official documentation](https://fal.ai/docs) for a comprehensive guide. +The client library offers a plethora of features designed to simplify your journey with `fal.ai`. Dive into the [official documentation](https://fal.ai/docs) for a comprehensive guide. diff --git a/libs/client/package.json b/libs/client/package.json index a9903f1..803daa5 100644 --- a/libs/client/package.json +++ b/libs/client/package.json @@ -1,7 +1,7 @@ { - "name": "@fal-ai/serverless-client", - "description": "The fal serverless JS/TS client", - "version": "0.14.3", + "name": "@fal-ai/client", + "description": "The fal.ai client for JavaScript and TypeScript", + "version": "1.0.0", "license": "MIT", "repository": { "type": "git", @@ -10,10 +10,10 @@ }, "keywords": [ "fal", - "serverless", "client", "ai", - "ml" + "ml", + "typescript" ], "dependencies": { "@msgpack/msgpack": "^3.0.0-beta2", diff --git a/libs/client/src/auth.ts b/libs/client/src/auth.ts index b8ad4e5..da61035 100644 --- a/libs/client/src/auth.ts +++ b/libs/client/src/auth.ts @@ -1,22 +1,26 @@ -import { getRestApiUrl } from "./config"; +import { getRestApiUrl, RequiredConfig } from "./config"; import { dispatchRequest } from "./request"; -import { parseAppId } from "./utils"; +import { parseEndpointId } from "./utils"; export const TOKEN_EXPIRATION_SECONDS = 120; /** * Get a token to connect to the realtime endpoint. */ -export async function getTemporaryAuthToken(app: string): Promise { - const appId = parseAppId(app); - const token: string | object = await dispatchRequest( - "POST", - `${getRestApiUrl()}/tokens/`, - { +export async function getTemporaryAuthToken( + app: string, + config: RequiredConfig, +): Promise { + const appId = parseEndpointId(app); + const token: string | object = await dispatchRequest({ + method: "POST", + targetUrl: `${getRestApiUrl()}/tokens/`, + config, + input: { allowed_apps: [appId.alias], token_expiration: TOKEN_EXPIRATION_SECONDS, }, - ); + }); // keep this in case the response was wrapped (old versions of the proxy do that) // should be safe to remove in the future if (typeof token !== "string" && token["detail"]) { diff --git a/libs/client/src/function.spec.ts b/libs/client/src/client.spec.ts similarity index 86% rename from libs/client/src/function.spec.ts rename to libs/client/src/client.spec.ts index 9392037..5087e71 100644 --- a/libs/client/src/function.spec.ts +++ b/libs/client/src/client.spec.ts @@ -1,4 +1,4 @@ -import { buildUrl } from "./function"; +import { buildUrl } from "./request"; describe("The function test suite", () => { it("should build the URL with a function username/app-alias", () => { diff --git a/libs/client/src/client.ts b/libs/client/src/client.ts new file mode 100644 index 0000000..4cd241a --- /dev/null +++ b/libs/client/src/client.ts @@ -0,0 +1,122 @@ +import { Config, createConfig } from "./config"; +import { createQueueClient, QueueClient, QueueSubscribeOptions } from "./queue"; +import { createRealtimeClient, RealtimeClient } from "./realtime"; +import { buildUrl, dispatchRequest } from "./request"; +import { resultResponseHandler } from "./response"; +import { createStorageClient, StorageClient } from "./storage"; +import { createStreamingClient, StreamingClient } from "./streaming"; +import { Result, RunOptions } from "./types"; + +/** + * The main client type, it provides access to simple API model usage, + * as well as access to the `queue` and `storage` APIs. + * + * @see createFalClient + */ +export interface FalClient { + /** + * The queue client to interact with the queue API. + */ + readonly queue: QueueClient; + + /** + * The realtime client to interact with the realtime API + * and receive updates in real-time. + * @see #RealtimeClient + * @see #RealtimeClient.connect + */ + readonly realtime: RealtimeClient; + + /** + * The storage client to interact with the storage API. + */ + readonly storage: StorageClient; + + /** + * The streaming client to interact with the streaming API. + * @see #stream + */ + readonly streaming: StreamingClient; + + /** + * Runs a fal endpoints identified by its `endpointId`. + * + * @param endpointId the registered function revision id or alias. + * @returns the remote function output + */ + run>( + endpointId: string, + options: RunOptions, + ): Promise>; + + /** + * Subscribes to updates for a specific request in the queue. + * + * @param endpointId - The ID of the API endpoint. + * @param options - Options to configure how the request is run and how updates are received. + * @returns A promise that resolves to the result of the request once it's completed. + */ + subscribe>( + endpointId: string, + options: RunOptions & QueueSubscribeOptions, + ): Promise>; + + /** + * Calls a fal app that supports streaming and provides a streaming-capable + * object as a result, that can be used to get partial results through either + * `AsyncIterator` or through an event listener. + * + * @param endpointId the endpoint id, e.g. `fal-ai/llavav15-13b`. + * @param options the request options, including the input payload. + * @returns the `FalStream` instance. + */ + stream: StreamingClient["stream"]; +} + +/** + * Creates a new reference of the `FalClient`. + * @param userConfig Optional configuration to override the default settings. + * @returns a new instance of the `FalClient`. + */ +export function createFalClient(userConfig: Config = {}): FalClient { + const config = createConfig(userConfig); + const storage = createStorageClient({ config }); + const queue = createQueueClient({ config, storage }); + const streaming = createStreamingClient({ config, storage }); + const realtime = createRealtimeClient({ config }); + return { + queue, + realtime, + storage, + streaming, + stream: streaming.stream, + async run( + endpointId: string, + options: RunOptions = {}, + ): Promise> { + const input = options.input + ? await storage.transformInput(options.input) + : undefined; + return dispatchRequest>({ + method: options.method, + targetUrl: buildUrl(endpointId, options), + input: input as Input, + config: { + ...config, + responseHandler: resultResponseHandler, + }, + }); + }, + async subscribe( + endpointId: string, + options: RunOptions & QueueSubscribeOptions = {}, + ): Promise> { + const { request_id: requestId } = await queue.submit(endpointId, options); + if (options.onEnqueue) { + options.onEnqueue(requestId); + } + await queue.subscribeToStatus(endpointId, { requestId, ...options }); + return queue.result(endpointId, { requestId }); + }, + }; +} diff --git a/libs/client/src/config.spec.ts b/libs/client/src/config.spec.ts index 73f743c..fc873a6 100644 --- a/libs/client/src/config.spec.ts +++ b/libs/client/src/config.spec.ts @@ -1,12 +1,11 @@ -import { config, getConfig } from "./config"; +import { createConfig } from "./config"; describe("The config test suite", () => { it("should set the config variables accordingly", () => { const newConfig = { credentials: "key-id:key-secret", }; - config(newConfig); - const currentConfig = getConfig(); + const currentConfig = createConfig(newConfig); expect(currentConfig.credentials).toEqual(newConfig.credentials); }); }); diff --git a/libs/client/src/config.ts b/libs/client/src/config.ts index 985adf1..19309f8 100644 --- a/libs/client/src/config.ts +++ b/libs/client/src/config.ts @@ -21,7 +21,7 @@ export function resolveDefaultFetch(): FetchType { export type Config = { /** - * The credentials to use for the fal serverless client. When using the + * The credentials to use for the fal client. When using the * client in the browser, it's recommended to use a proxy server to avoid * exposing the credentials in the client's environment. * @@ -40,7 +40,7 @@ export type Config = { suppressLocalCredentialsWarning?: boolean; /** * The URL of the proxy server to use for the client requests. The proxy - * server should forward the requests to the fal serverless rest api. + * server should forward the requests to the fal api. */ proxyUrl?: string; /** @@ -97,15 +97,13 @@ const DEFAULT_CONFIG: Partial = { responseHandler: defaultResponseHandler, }; -let configuration: RequiredConfig; - /** - * Configures the fal serverless client. + * Configures the fal client. * * @param config the new configuration. */ -export function config(config: Config) { - configuration = { +export function createConfig(config: Config): RequiredConfig { + let configuration = { ...DEFAULT_CONFIG, ...config, fetch: config.fetch ?? resolveDefaultFetch(), @@ -114,8 +112,8 @@ export function config(config: Config) { configuration = { ...configuration, requestMiddleware: withMiddleware( - withProxy({ targetUrl: config.proxyUrl }), configuration.requestMiddleware, + withProxy({ targetUrl: config.proxyUrl }), ), }; } @@ -135,26 +133,11 @@ export function config(config: Config) { "That's not recommended for production use cases.", ); } -} - -/** - * Get the current fal serverless client configuration. - * - * @returns the current client configuration. - */ -export function getConfig(): RequiredConfig { - if (!configuration) { - console.info("Using default configuration for the fal client"); - return { - ...DEFAULT_CONFIG, - fetch: resolveDefaultFetch(), - } as RequiredConfig; - } return configuration; } /** - * @returns the URL of the fal serverless rest api endpoint. + * @returns the URL of the fal REST api endpoint. */ export function getRestApiUrl(): string { return "https://rest.alpha.fal.ai"; diff --git a/libs/client/src/function.ts b/libs/client/src/function.ts deleted file mode 100644 index 4b59705..0000000 --- a/libs/client/src/function.ts +++ /dev/null @@ -1,521 +0,0 @@ -import { dispatchRequest } from "./request"; -import { storageImpl } from "./storage"; -import { FalStream, StreamingConnectionMode } from "./streaming"; -import { - CompletedQueueStatus, - EnqueueResult, - QueueStatus, - RequestLog, -} from "./types"; -import { ensureAppIdFormat, isValidUrl, parseAppId } from "./utils"; - -/** - * The function input and other configuration when running - * the function, such as the HTTP method to use. - */ -type RunOptions = { - /** - * The path to the function, if any. Defaults to ``. - * @deprecated Pass the path as part of the app id itself, e.g. `fal-ai/sdxl/image-to-image` - */ - 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; - - /** - * If `true`, the function will automatically upload any files - * (i.e. instances of `Blob`). - * - * This is enabled by default. You can disable it by setting it to `false`. - */ - readonly autoUpload?: boolean; -}; - -type ExtraOptions = { - /** - * If `true`, the function will use the queue to run the function - * asynchronously and return the result in a separate call. This - * influences how the URL is built. - */ - readonly subdomain?: string; - - /** - * The query parameters to include in the URL. - */ - readonly query?: Record; -}; - -/** - * Builds the final url to run the function based on its `id` or alias and - * a the options from `RunOptions`. - * - * @private - * @param id the function id or alias - * @param options the run options - * @returns the final url to run the function - */ -export function buildUrl( - id: string, - options: RunOptions & ExtraOptions = {}, -): string { - const method = (options.method ?? "post").toLowerCase(); - const path = (options.path ?? "").replace(/^\//, "").replace(/\/{2,}/, "/"); - const input = options.input; - const params = { - ...(options.query || {}), - ...(method === "get" ? input : {}), - }; - - const queryParams = - Object.keys(params).length > 0 - ? `?${new URLSearchParams(params).toString()}` - : ""; - - // if a fal url is passed, just use it - if (isValidUrl(id)) { - const url = id.endsWith("/") ? id : `${id}/`; - return `${url}${path}${queryParams}`; - } - - const appId = ensureAppIdFormat(id); - const subdomain = options.subdomain ? `${options.subdomain}.` : ""; - const url = `https://${subdomain}fal.run/${appId}/${path}`; - return `${url.replace(/\/$/, "")}${queryParams}`; -} - -export async function send( - id: string, - options: RunOptions & ExtraOptions = {}, -): Promise { - const input = - options.input && options.autoUpload !== false - ? await storageImpl.transformInput(options.input) - : options.input; - return dispatchRequest( - options.method ?? "post", - buildUrl(id, options), - input as Input, - ); -} - -export type QueueStatusSubscriptionOptions = QueueStatusOptions & - Omit; - -/** - * Runs a fal serverless function identified by its `id`. - * - * @param id the registered function revision id or alias. - * @returns the remote function output - */ -export async function run( - id: string, - options: RunOptions = {}, -): Promise { - return send(id, options); -} - -type TimeoutId = ReturnType | undefined; - -const DEFAULT_POLL_INTERVAL = 500; - -/** - * Options for subscribing to the request queue. - */ -type QueueSubscribeOptions = { - /** - * The mode to use for subscribing to updates. It defaults to `polling`. - * You can also use client-side streaming by setting it to `streaming`. - * - * **Note:** Streaming is currently experimental and once stable, it will - * be the default mode. - * - * @see pollInterval - */ - mode?: "polling" | "streaming"; - - /** - * Callback function that is called when a request is enqueued. - * @param requestId - The unique identifier for the enqueued request. - */ - onEnqueue?: (requestId: string) => void; - - /** - * Callback function that is called when the status of the queue changes. - * @param status - The current status of the queue. - */ - onQueueUpdate?: (status: QueueStatus) => void; - - /** - * If `true`, the response will include the logs for the request. - * Defaults to `false`. - */ - logs?: boolean; - - /** - * The timeout (in milliseconds) for the request. If the request is not - * completed within this time, the subscription will be cancelled. - * - * Keep in mind that although the client resolves the function on a timeout, - * and will try to cancel the request on the server, the server might not be - * able to cancel the request if it's already running. - * - * Note: currently, the timeout is not enforced and the default is `undefined`. - * This behavior might change in the future. - */ - timeout?: number; - - /** - * The URL to send a webhook notification to when the request is completed. - * @see WebHookResponse - */ - webhookUrl?: string; -} & ( - | { - mode?: "polling"; - /** - * The interval (in milliseconds) at which to poll for updates. - * If not provided, a default value of `500` will be used. - * - * This value is ignored if `mode` is set to `streaming`. - */ - pollInterval?: number; - } - | { - mode: "streaming"; - - /** - * The connection mode to use for streaming updates. It defaults to `server`. - * Set to `client` if your server proxy doesn't support streaming. - */ - connectionMode?: StreamingConnectionMode; - } -); - -/** - * Options for submitting a request to the queue. - */ -type SubmitOptions = RunOptions & { - /** - * The URL to send a webhook notification to when the request is completed. - * @see WebHookResponse - */ - webhookUrl?: string; -}; - -type BaseQueueOptions = { - /** - * The unique identifier for the enqueued request. - */ - requestId: string; -}; - -type QueueStatusOptions = BaseQueueOptions & { - /** - * If `true`, the response will include the logs for the request. - * Defaults to `false`. - */ - logs?: boolean; -}; - -type QueueStatusStreamOptions = QueueStatusOptions & { - /** - * The connection mode to use for streaming updates. It defaults to `server`. - * Set to `client` if your server proxy doesn't support streaming. - */ - connectionMode?: StreamingConnectionMode; -}; - -/** - * Represents a request queue with methods for submitting requests, - * checking their status, retrieving results, and subscribing to updates. - */ -interface Queue { - /** - * Submits a request to the queue. - * - * @param endpointId - The ID of the function web endpoint. - * @param options - Options to configure how the request is run. - * @returns A promise that resolves to the result of enqueuing the request. - */ - submit( - endpointId: string, - options: SubmitOptions, - ): Promise; - - /** - * Retrieves the status of a specific request in the queue. - * - * @param endpointId - The ID of the function web endpoint. - * @param options - Options to configure how the request is run. - * @returns A promise that resolves to the status of the request. - */ - status(endpointId: string, options: QueueStatusOptions): Promise; - - /** - * Subscribes to updates for a specific request in the queue using HTTP streaming events. - * - * @param endpointId - The ID of the function web endpoint. - * @param options - Options to configure how the request is run and how updates are received. - * @returns The streaming object that can be used to listen for updates. - */ - streamStatus( - endpointId: string, - options: QueueStatusStreamOptions, - ): Promise>; - - /** - * Subscribes to updates for a specific request in the queue using polling or streaming. - * See `options.mode` for more details. - * - * @param endpointId - The ID of the function web endpoint. - * @param options - Options to configure how the request is run and how updates are received. - * @returns A promise that resolves to the final status of the request. - */ - subscribeToStatus( - endpointId: string, - options: QueueStatusSubscriptionOptions, - ): Promise; - - /** - * Retrieves the result of a specific request from the queue. - * - * @param endpointId - The ID of the function web endpoint. - * @param options - Options to configure how the request is run. - * @returns A promise that resolves to the result of the request. - */ - result( - endpointId: string, - options: BaseQueueOptions, - ): Promise; - - /** - * Cancels a request in the queue. - * - * @param endpointId - The ID of the function web endpoint. - * @param options - Options to configure how the request - * is run and how updates are received. - * @returns A promise that resolves once the request is cancelled. - * @throws {Error} If the request cannot be cancelled. - */ - cancel(endpointId: string, options: BaseQueueOptions): Promise; -} - -/** - * 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( - endpointId: string, - options: SubmitOptions, - ): Promise { - const { webhookUrl, path = "", ...runOptions } = options; - return send(endpointId, { - ...runOptions, - subdomain: "queue", - method: "post", - path: path, - query: webhookUrl ? { fal_webhook: webhookUrl } : undefined, - }); - }, - async status( - endpointId: string, - { requestId, logs = false }: QueueStatusOptions, - ): Promise { - const appId = parseAppId(endpointId); - const prefix = appId.namespace ? `${appId.namespace}/` : ""; - return send(`${prefix}${appId.owner}/${appId.alias}`, { - subdomain: "queue", - method: "get", - path: `/requests/${requestId}/status`, - input: { - logs: logs ? "1" : "0", - }, - }); - }, - - async streamStatus( - endpointId: string, - { requestId, logs = false, connectionMode }: QueueStatusStreamOptions, - ): Promise> { - const appId = parseAppId(endpointId); - const prefix = appId.namespace ? `${appId.namespace}/` : ""; - - const queryParams = { - logs: logs ? "1" : "0", - }; - - const url = buildUrl(`${prefix}${appId.owner}/${appId.alias}`, { - subdomain: "queue", - path: `/requests/${requestId}/status/stream`, - query: queryParams, - }); - - return new FalStream(endpointId, { - url, - method: "get", - connectionMode, - queryParams, - }); - }, - - async subscribeToStatus(endpointId, options): Promise { - const requestId = options.requestId; - const timeout = options.timeout; - let timeoutId: TimeoutId = undefined; - - const handleCancelError = () => { - // Ignore errors as the client will follow through with the timeout - // regardless of the server response. In case cancelation fails, we - // still want to reject the promise and consider the client call canceled. - }; - if (options.mode === "streaming") { - const status = await queue.streamStatus(endpointId, { - requestId, - logs: options.logs, - connectionMode: - "connectionMode" in options - ? (options.connectionMode as StreamingConnectionMode) - : undefined, - }); - const logs: RequestLog[] = []; - if (timeout) { - timeoutId = setTimeout(() => { - status.abort(); - queue.cancel(endpointId, { requestId }).catch(handleCancelError); - // TODO this error cannot bubble up to the user since it's thrown in - // a closure in the global scope due to setTimeout behavior. - // User will get a platform error instead. We should find a way to - // make this behavior aligned with polling. - throw new Error( - `Client timed out waiting for the request to complete after ${timeout}ms`, - ); - }, timeout); - } - status.on("data", (data: QueueStatus) => { - if (options.onQueueUpdate) { - // accumulate logs to match previous polling behavior - if ( - "logs" in data && - Array.isArray(data.logs) && - data.logs.length > 0 - ) { - logs.push(...data.logs); - } - options.onQueueUpdate("logs" in data ? { ...data, logs } : data); - } - }); - const doneStatus = await status.done(); - if (timeoutId) { - clearTimeout(timeoutId); - } - return doneStatus as CompletedQueueStatus; - } - // default to polling until status streaming is stable and faster - return new Promise((resolve, reject) => { - let pollingTimeoutId: TimeoutId; - // type resolution isn't great in this case, so check for its presence - // and and type so the typechecker behaves as expected - const pollInterval = - "pollInterval" in options && typeof options.pollInterval === "number" - ? (options.pollInterval ?? DEFAULT_POLL_INTERVAL) - : DEFAULT_POLL_INTERVAL; - - const clearScheduledTasks = () => { - if (timeoutId) { - clearTimeout(timeoutId); - } - if (pollingTimeoutId) { - clearTimeout(pollingTimeoutId); - } - }; - if (timeout) { - timeoutId = setTimeout(() => { - clearScheduledTasks(); - queue.cancel(endpointId, { requestId }).catch(handleCancelError); - reject( - new Error( - `Client timed out waiting for the request to complete after ${timeout}ms`, - ), - ); - }, timeout); - } - const poll = async () => { - try { - const requestStatus = await queue.status(endpointId, { - requestId, - logs: options.logs ?? false, - }); - if (options.onQueueUpdate) { - options.onQueueUpdate(requestStatus); - } - if (requestStatus.status === "COMPLETED") { - clearScheduledTasks(); - resolve(requestStatus); - return; - } - pollingTimeoutId = setTimeout(poll, pollInterval); - } catch (error) { - clearScheduledTasks(); - reject(error); - } - }; - poll().catch(reject); - }); - }, - - async result( - endpointId: string, - { requestId }: BaseQueueOptions, - ): Promise { - const appId = parseAppId(endpointId); - const prefix = appId.namespace ? `${appId.namespace}/` : ""; - return send(`${prefix}${appId.owner}/${appId.alias}`, { - subdomain: "queue", - method: "get", - path: `/requests/${requestId}`, - }); - }, - - async cancel( - endpointId: string, - { requestId }: BaseQueueOptions, - ): Promise { - const appId = parseAppId(endpointId); - const prefix = appId.namespace ? `${appId.namespace}/` : ""; - await send(`${prefix}${appId.owner}/${appId.alias}`, { - subdomain: "queue", - method: "put", - path: `/requests/${requestId}/cancel`, - }); - }, -}; - -/** - * Subscribes to updates for a specific request in the queue. - * - * @param endpointId - The ID of the function web endpoint. - * @param options - Options to configure how the request is run and how updates are received. - * @returns A promise that resolves to the result of the request once it's completed. - */ -export async function subscribe( - endpointId: string, - options: RunOptions & QueueSubscribeOptions = {}, -): Promise { - const { request_id: requestId } = await queue.submit(endpointId, options); - if (options.onEnqueue) { - options.onEnqueue(requestId); - } - await queue.subscribeToStatus(endpointId, { requestId, ...options }); - return queue.result(endpointId, { requestId }); -} diff --git a/libs/client/src/index.ts b/libs/client/src/index.ts index f8bcdb7..e76efa3 100644 --- a/libs/client/src/index.ts +++ b/libs/client/src/index.ts @@ -1,15 +1,59 @@ -export { config, getConfig } from "./config"; -export { queue, run, subscribe } from "./function"; +import { createFalClient, type FalClient } from "./client"; +import { Config } from "./config"; +import { StreamOptions } from "./streaming"; +import { RunOptions } from "./types"; + +export { createFalClient, type FalClient } from "./client"; export { withMiddleware, withProxy } from "./middleware"; export type { RequestMiddleware } from "./middleware"; -export { realtimeImpl as realtime } from "./realtime"; +export type { QueueClient } from "./queue"; +export type { RealtimeClient } from "./realtime"; export { ApiError, ValidationError } from "./response"; export type { ResponseHandler } from "./response"; -export { storageImpl as storage } from "./storage"; -export { stream } from "./streaming"; +export type { StorageClient } from "./storage"; +export type { StreamingClient } from "./streaming"; +export * from "./types"; export type { QueueStatus, ValidationErrorInfo, WebHookResponse, } from "./types"; -export { parseAppId } from "./utils"; +export { parseEndpointId } from "./utils"; + +type SingletonFalClient = { + config(config: Config): void; +} & FalClient; + +/** + * Creates a singleton instance of the client. This is useful as a compatibility + * layer for existing code that uses the clients version prior to 1.0.0. + */ +export const fal: SingletonFalClient = (function createSingletonFalClient() { + let currentInstance: FalClient = createFalClient(); + return { + config(config: Config) { + currentInstance = createFalClient(config); + }, + get queue() { + return currentInstance.queue; + }, + get realtime() { + return currentInstance.realtime; + }, + get storage() { + return currentInstance.storage; + }, + get streaming() { + return currentInstance.streaming; + }, + run(id: string, options: RunOptions) { + return currentInstance.run(id, options); + }, + subscribe(endpointId: string, options: RunOptions) { + return currentInstance.subscribe(endpointId, options); + }, + stream(endpointId: string, options: StreamOptions) { + return currentInstance.stream(endpointId, options); + }, + } satisfies SingletonFalClient; +})(); diff --git a/libs/client/src/middleware.ts b/libs/client/src/middleware.ts index 127c2fb..2d4ad43 100644 --- a/libs/client/src/middleware.ts +++ b/libs/client/src/middleware.ts @@ -26,12 +26,16 @@ export type RequestMiddleware = ( export function withMiddleware( ...middlewares: RequestMiddleware[] ): RequestMiddleware { - return (config) => - middlewares.reduce( - (configPromise, middleware) => - configPromise.then((req) => middleware(req)), - Promise.resolve(config), - ); + const isDefined = (middleware: RequestMiddleware): boolean => + typeof middleware === "function"; + + return async (config: RequestConfig) => { + let currentConfig = { ...config }; + for (const middleware of middlewares.filter(isDefined)) { + currentConfig = await middleware(currentConfig); + } + return currentConfig; + }; } export type RequestProxyConfig = { @@ -41,17 +45,22 @@ export type RequestProxyConfig = { export const TARGET_URL_HEADER = "x-fal-target-url"; export function withProxy(config: RequestProxyConfig): RequestMiddleware { + const passthrough = (requestConfig: RequestConfig) => + Promise.resolve(requestConfig); // when running on the server, we don't need to proxy the request if (typeof window === "undefined") { - return (requestConfig) => Promise.resolve(requestConfig); + return passthrough; } + // if x-fal-target-url is already set, we skip it return (requestConfig) => - Promise.resolve({ - ...requestConfig, - url: config.targetUrl, - headers: { - ...(requestConfig.headers || {}), - [TARGET_URL_HEADER]: requestConfig.url, - }, - }); + requestConfig.headers && TARGET_URL_HEADER in requestConfig + ? passthrough(requestConfig) + : Promise.resolve({ + ...requestConfig, + url: config.targetUrl, + headers: { + ...(requestConfig.headers || {}), + [TARGET_URL_HEADER]: requestConfig.url, + }, + }); } diff --git a/libs/client/src/queue.ts b/libs/client/src/queue.ts new file mode 100644 index 0000000..a6648a1 --- /dev/null +++ b/libs/client/src/queue.ts @@ -0,0 +1,420 @@ +import { RequiredConfig } from "./config"; +import { buildUrl, dispatchRequest } from "./request"; +import { resultResponseHandler } from "./response"; +import { StorageClient } from "./storage"; +import { FalStream, StreamingConnectionMode } from "./streaming"; +import { + CompletedQueueStatus, + InQueueQueueStatus, + QueueStatus, + RequestLog, + Result, + RunOptions, +} from "./types"; +import { parseEndpointId } from "./utils"; + +export type QueueStatusSubscriptionOptions = QueueStatusOptions & + Omit; + +type TimeoutId = ReturnType | undefined; + +const DEFAULT_POLL_INTERVAL = 500; + +/** + * Options for subscribing to the request queue. + */ +export type QueueSubscribeOptions = { + /** + * The mode to use for subscribing to updates. It defaults to `polling`. + * You can also use client-side streaming by setting it to `streaming`. + * + * **Note:** Streaming is currently experimental and once stable, it will + * be the default mode. + * + * @see pollInterval + */ + mode?: "polling" | "streaming"; + + /** + * Callback function that is called when a request is enqueued. + * @param requestId - The unique identifier for the enqueued request. + */ + onEnqueue?: (requestId: string) => void; + + /** + * Callback function that is called when the status of the queue changes. + * @param status - The current status of the queue. + */ + onQueueUpdate?: (status: QueueStatus) => void; + + /** + * If `true`, the response will include the logs for the request. + * Defaults to `false`. + */ + logs?: boolean; + + /** + * The timeout (in milliseconds) for the request. If the request is not + * completed within this time, the subscription will be cancelled. + * + * Keep in mind that although the client resolves the function on a timeout, + * and will try to cancel the request on the server, the server might not be + * able to cancel the request if it's already running. + * + * Note: currently, the timeout is not enforced and the default is `undefined`. + * This behavior might change in the future. + */ + timeout?: number; + + /** + * The URL to send a webhook notification to when the request is completed. + * @see WebHookResponse + */ + webhookUrl?: string; +} & ( + | { + mode?: "polling"; + /** + * The interval (in milliseconds) at which to poll for updates. + * If not provided, a default value of `500` will be used. + * + * This value is ignored if `mode` is set to `streaming`. + */ + pollInterval?: number; + } + | { + mode: "streaming"; + + /** + * The connection mode to use for streaming updates. It defaults to `server`. + * Set to `client` if your server proxy doesn't support streaming. + */ + connectionMode?: StreamingConnectionMode; + } +); + +/** + * Options for submitting a request to the queue. + */ +export type SubmitOptions = RunOptions & { + /** + * The URL to send a webhook notification to when the request is completed. + * @see WebHookResponse + */ + webhookUrl?: string; +}; + +type BaseQueueOptions = { + /** + * The unique identifier for the enqueued request. + */ + requestId: string; +}; + +export type QueueStatusOptions = BaseQueueOptions & { + /** + * If `true`, the response will include the logs for the request. + * Defaults to `false`. + */ + logs?: boolean; +}; + +export type QueueStatusStreamOptions = QueueStatusOptions & { + /** + * The connection mode to use for streaming updates. It defaults to `server`. + * Set to `client` if your server proxy doesn't support streaming. + */ + connectionMode?: StreamingConnectionMode; +}; + +/** + * Represents a request queue with methods for submitting requests, + * checking their status, retrieving results, and subscribing to updates. + */ +export interface QueueClient { + /** + * Submits a request to the queue. + * + * @param endpointId - The ID of the function web endpoint. + * @param options - Options to configure how the request is run. + * @returns A promise that resolves to the result of enqueuing the request. + */ + submit( + endpointId: string, + options: SubmitOptions, + ): Promise; + + /** + * Retrieves the status of a specific request in the queue. + * + * @param endpointId - The ID of the function web endpoint. + * @param options - Options to configure how the request is run. + * @returns A promise that resolves to the status of the request. + */ + status(endpointId: string, options: QueueStatusOptions): Promise; + + /** + * Subscribes to updates for a specific request in the queue using HTTP streaming events. + * + * @param endpointId - The ID of the function web endpoint. + * @param options - Options to configure how the request is run and how updates are received. + * @returns The streaming object that can be used to listen for updates. + */ + streamStatus( + endpointId: string, + options: QueueStatusStreamOptions, + ): Promise>; + + /** + * Subscribes to updates for a specific request in the queue using polling or streaming. + * See `options.mode` for more details. + * + * @param endpointId - The ID of the function web endpoint. + * @param options - Options to configure how the request is run and how updates are received. + * @returns A promise that resolves to the final status of the request. + */ + subscribeToStatus( + endpointId: string, + options: QueueStatusSubscriptionOptions, + ): Promise; + + /** + * Retrieves the result of a specific request from the queue. + * + * @param endpointId - The ID of the function web endpoint. + * @param options - Options to configure how the request is run. + * @returns A promise that resolves to the result of the request. + */ + result( + endpointId: string, + options: BaseQueueOptions, + ): Promise>; + + /** + * Cancels a request in the queue. + * + * @param endpointId - The ID of the function web endpoint. + * @param options - Options to configure how the request + * is run and how updates are received. + * @returns A promise that resolves once the request is cancelled. + * @throws {Error} If the request cannot be cancelled. + */ + cancel(endpointId: string, options: BaseQueueOptions): Promise; +} + +type QueueClientDependencies = { + config: RequiredConfig; + storage: StorageClient; +}; + +export const createQueueClient = ({ + config, + storage, +}: QueueClientDependencies): QueueClient => { + const ref: QueueClient = { + async submit( + endpointId: string, + options: SubmitOptions, + ): Promise { + const { webhookUrl, ...runOptions } = options; + const input = options.input + ? await storage.transformInput(options.input) + : undefined; + return dispatchRequest({ + method: options.method, + targetUrl: buildUrl(endpointId, { + ...runOptions, + subdomain: "queue", + query: webhookUrl ? { fal_webhook: webhookUrl } : undefined, + }), + input: input as Input, + config, + }); + }, + async status( + endpointId: string, + { requestId, logs = false }: QueueStatusOptions, + ): Promise { + const appId = parseEndpointId(endpointId); + const prefix = appId.namespace ? `${appId.namespace}/` : ""; + return dispatchRequest({ + method: "get", + targetUrl: buildUrl(`${prefix}${appId.owner}/${appId.alias}`, { + subdomain: "queue", + query: { logs: logs ? "1" : "0" }, + path: `/requests/${requestId}/status`, + }), + config, + }); + }, + + async streamStatus( + endpointId: string, + { requestId, logs = false, connectionMode }: QueueStatusStreamOptions, + ): Promise> { + const appId = parseEndpointId(endpointId); + const prefix = appId.namespace ? `${appId.namespace}/` : ""; + + const queryParams = { + logs: logs ? "1" : "0", + }; + + const url = buildUrl(`${prefix}${appId.owner}/${appId.alias}`, { + subdomain: "queue", + path: `/requests/${requestId}/status/stream`, + query: queryParams, + }); + + return new FalStream(endpointId, config, { + url, + method: "get", + connectionMode, + queryParams, + }); + }, + + async subscribeToStatus( + endpointId, + options, + ): Promise { + const requestId = options.requestId; + const timeout = options.timeout; + let timeoutId: TimeoutId = undefined; + + const handleCancelError = () => { + // Ignore errors as the client will follow through with the timeout + // regardless of the server response. In case cancelation fails, we + // still want to reject the promise and consider the client call canceled. + }; + if (options.mode === "streaming") { + const status = await ref.streamStatus(endpointId, { + requestId, + logs: options.logs, + connectionMode: + "connectionMode" in options + ? (options.connectionMode as StreamingConnectionMode) + : undefined, + }); + const logs: RequestLog[] = []; + if (timeout) { + timeoutId = setTimeout(() => { + status.abort(); + ref.cancel(endpointId, { requestId }).catch(handleCancelError); + // TODO this error cannot bubble up to the user since it's thrown in + // a closure in the global scope due to setTimeout behavior. + // User will get a platform error instead. We should find a way to + // make this behavior aligned with polling. + throw new Error( + `Client timed out waiting for the request to complete after ${timeout}ms`, + ); + }, timeout); + } + status.on("data", (data: QueueStatus) => { + if (options.onQueueUpdate) { + // accumulate logs to match previous polling behavior + if ( + "logs" in data && + Array.isArray(data.logs) && + data.logs.length > 0 + ) { + logs.push(...data.logs); + } + options.onQueueUpdate("logs" in data ? { ...data, logs } : data); + } + }); + const doneStatus = await status.done(); + if (timeoutId) { + clearTimeout(timeoutId); + } + return doneStatus as CompletedQueueStatus; + } + // default to polling until status streaming is stable and faster + return new Promise((resolve, reject) => { + let pollingTimeoutId: TimeoutId; + // type resolution isn't great in this case, so check for its presence + // and and type so the typechecker behaves as expected + const pollInterval = + "pollInterval" in options && typeof options.pollInterval === "number" + ? (options.pollInterval ?? DEFAULT_POLL_INTERVAL) + : DEFAULT_POLL_INTERVAL; + + const clearScheduledTasks = () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (pollingTimeoutId) { + clearTimeout(pollingTimeoutId); + } + }; + if (timeout) { + timeoutId = setTimeout(() => { + clearScheduledTasks(); + ref.cancel(endpointId, { requestId }).catch(handleCancelError); + reject( + new Error( + `Client timed out waiting for the request to complete after ${timeout}ms`, + ), + ); + }, timeout); + } + const poll = async () => { + try { + const requestStatus = await ref.status(endpointId, { + requestId, + logs: options.logs ?? false, + }); + if (options.onQueueUpdate) { + options.onQueueUpdate(requestStatus); + } + if (requestStatus.status === "COMPLETED") { + clearScheduledTasks(); + resolve(requestStatus); + return; + } + pollingTimeoutId = setTimeout(poll, pollInterval); + } catch (error) { + clearScheduledTasks(); + reject(error); + } + }; + poll().catch(reject); + }); + }, + + async result( + endpointId: string, + { requestId }: BaseQueueOptions, + ): Promise> { + const appId = parseEndpointId(endpointId); + const prefix = appId.namespace ? `${appId.namespace}/` : ""; + return dispatchRequest>({ + method: "get", + targetUrl: buildUrl(`${prefix}${appId.owner}/${appId.alias}`, { + subdomain: "queue", + path: `/requests/${requestId}`, + }), + config: { + ...config, + responseHandler: resultResponseHandler, + }, + }); + }, + + async cancel( + endpointId: string, + { requestId }: BaseQueueOptions, + ): Promise { + const appId = parseEndpointId(endpointId); + const prefix = appId.namespace ? `${appId.namespace}/` : ""; + await dispatchRequest({ + method: "put", + targetUrl: buildUrl(`${prefix}${appId.owner}/${appId.alias}`, { + subdomain: "queue", + path: `/requests/${requestId}/cancel`, + }), + config, + }); + }, + }; + return ref; +}; diff --git a/libs/client/src/realtime.ts b/libs/client/src/realtime.ts index 0dcb94c..a599d85 100644 --- a/libs/client/src/realtime.ts +++ b/libs/client/src/realtime.ts @@ -13,9 +13,10 @@ import { transition, } from "robot3"; import { TOKEN_EXPIRATION_SECONDS, getTemporaryAuthToken } from "./auth"; +import { RequiredConfig } from "./config"; import { ApiError } from "./response"; import { isBrowser } from "./runtime"; -import { ensureAppIdFormat, isReact, throttle } from "./utils"; +import { ensureEndpointIdFormat, isReact, throttle } from "./utils"; // Define the context interface Context { @@ -258,7 +259,7 @@ function buildRealtimeUrl( if (maxBuffering !== undefined) { queryParams.set("max_buffering", maxBuffering.toFixed(0)); } - const appId = ensureAppIdFormat(app); + const appId = ensureEndpointIdFormat(app); return `wss://fal.run/${appId}/realtime?${queryParams.toString()}`; } @@ -346,178 +347,186 @@ function isFalErrorResult(data: any): data is FalErrorResult { return data.type === "x-fal-error"; } -/** - * The default implementation of the realtime client. - */ -export const realtimeImpl: RealtimeClient = { - connect( - app: string, - handler: RealtimeConnectionHandler, - ): RealtimeConnection { - const { - // if running on React in the server, set clientOnly to true by default - clientOnly = isReact() && !isBrowser(), - connectionKey = crypto.randomUUID(), - maxBuffering, - throttleInterval = DEFAULT_THROTTLE_INTERVAL, - } = handler; - if (clientOnly && !isBrowser()) { - return NoOpConnection; - } - - let previousState: string | undefined; +type RealtimeClientDependencies = { + config: RequiredConfig; +}; - // 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) as RealtimeConnectionCallback; - 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" }); - getTemporaryAuthToken(app) - .then((token) => { - send({ type: "authenticated", token }); - const tokenExpirationTimeout = Math.round( - 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 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 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 = decode(new Uint8Array(event.data)); - onResult(result); - return; - } - if (event.data instanceof Uint8Array) { - const result = decode(event.data); - onResult(result); - return; - } - if (event.data instanceof Blob) { - event.data.arrayBuffer().then((buffer) => { - const result = decode(new Uint8Array(buffer)); - onResult(result); +export function createRealtimeClient({ + config, +}: RealtimeClientDependencies): RealtimeClient { + return { + connect( + app: string, + handler: RealtimeConnectionHandler, + ): RealtimeConnection { + const { + // if running on React in the server, set clientOnly to true by default + clientOnly = isReact() && !isBrowser(), + connectionKey = crypto.randomUUID(), + maxBuffering, + throttleInterval = DEFAULT_THROTTLE_INTERVAL, + } = handler; + if (clientOnly && !isBrowser()) { + return NoOpConnection; + } + + let previousState: string | undefined; + + // 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) as RealtimeConnectionCallback; + 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" }); + getTemporaryAuthToken(app, config) + .then((token) => { + send({ type: "authenticated", token }); + const tokenExpirationTimeout = Math.round( + TOKEN_EXPIRATION_SECONDS * 0.9 * 1000, + ); + setTimeout(() => { + send({ type: "expireToken" }); + }, tokenExpirationTimeout); + }) + .catch((error) => { + send({ type: "unauthorized", error }); }); - 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; + } + 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 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 ApiError({ - message: `${data.error}: ${data.reason}`, - // TODO better error status code - status: 400, - body: data, - }), - ); - return; - } - }; - } - previousState = machine.current; - }, - ); - - const send = (input: Input & Partial) => { - // Use throttled send to avoid sending too many messages - - const message = - input instanceof Uint8Array - ? input - : { - ...input, - request_id: input["request_id"] ?? crypto.randomUUID(), + onError(new ApiError({ message: "Unknown error", status: 500 })); }; + ws.onmessage = (event) => { + const { onResult } = getCallbacks(); - stateMachine.throttledSend({ - type: "send", - message, - }); - }; - - const close = () => { - stateMachine.send({ type: "close" }); - }; + // Handle binary messages as msgpack messages + if (event.data instanceof ArrayBuffer) { + const result = decode(new Uint8Array(event.data)); + onResult(result); + return; + } + if (event.data instanceof Uint8Array) { + const result = decode(event.data); + onResult(result); + return; + } + if (event.data instanceof Blob) { + event.data.arrayBuffer().then((buffer) => { + const result = decode(new Uint8Array(buffer)); + onResult(result); + }); + return; + } - return { - send, - close, - }; - }, -}; + // 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 ApiError({ + message: `${data.error}: ${data.reason}`, + // TODO better error status code + status: 400, + body: data, + }), + ); + return; + } + }; + } + previousState = machine.current; + }, + ); + + const send = (input: Input & Partial) => { + // Use throttled send to avoid sending too many messages + + const message = + input instanceof Uint8Array + ? input + : { + ...input, + request_id: input["request_id"] ?? crypto.randomUUID(), + }; + + stateMachine.throttledSend({ + type: "send", + message, + }); + }; + + const close = () => { + stateMachine.send({ type: "close" }); + }; + + return { + send, + close, + }; + }, + }; +} diff --git a/libs/client/src/request.ts b/libs/client/src/request.ts index 4e42c03..db663c5 100644 --- a/libs/client/src/request.ts +++ b/libs/client/src/request.ts @@ -1,6 +1,8 @@ -import { getConfig } from "./config"; +import { RequiredConfig } from "./config"; import { ResponseHandler } from "./response"; import { getUserAgent, isBrowser } from "./runtime"; +import { RunOptions, UrlOptions } from "./types"; +import { ensureEndpointIdFormat, isValidUrl } from "./utils"; const isCloudflareWorkers = typeof navigator !== "undefined" && @@ -10,27 +12,33 @@ type RequestOptions = { responseHandler?: ResponseHandler; }; +type RequestParams = { + method?: string; + targetUrl: string; + input?: Input; + config: RequiredConfig; + options?: RequestOptions & RequestInit; +}; + export async function dispatchRequest( - method: string, - targetUrl: string, - input: Input, - options: RequestOptions & RequestInit = {}, + params: RequestParams, ): Promise { + const { targetUrl, input, config, options = {} } = params; const { credentials: credentialsValue, requestMiddleware, responseHandler, fetch, - } = getConfig(); + } = config; const userAgent = isBrowser() ? {} : { "User-Agent": getUserAgent() }; const credentials = typeof credentialsValue === "function" ? credentialsValue() : credentialsValue; - const { url, headers } = await requestMiddleware({ + const { method, url, headers } = await requestMiddleware({ + method: (params.method ?? options.method ?? "post").toUpperCase(), url: targetUrl, - method: method.toUpperCase(), }); const authHeader = credentials ? { Authorization: `Key ${credentials}` } : {}; const requestHeaders = { @@ -58,3 +66,41 @@ export async function dispatchRequest( const handleResponse = customResponseHandler ?? responseHandler; return await handleResponse(response); } + +/** + * Builds the final url to run the function based on its `id` or alias and + * a the options from `RunOptions`. + * + * @private + * @param id the function id or alias + * @param options the run options + * @returns the final url to run the function + */ +export function buildUrl( + id: string, + options: RunOptions & UrlOptions = {}, +): string { + const method = (options.method ?? "post").toLowerCase(); + const path = (options.path ?? "").replace(/^\//, "").replace(/\/{2,}/, "/"); + const input = options.input; + const params = { + ...(options.query || {}), + ...(method === "get" ? input : {}), + }; + + const queryParams = + Object.keys(params).length > 0 + ? `?${new URLSearchParams(params).toString()}` + : ""; + + // if a fal url is passed, just use it + if (isValidUrl(id)) { + const url = id.endsWith("/") ? id : `${id}/`; + return `${url}${path}${queryParams}`; + } + + const appId = ensureEndpointIdFormat(id); + const subdomain = options.subdomain ? `${options.subdomain}.` : ""; + const url = `https://${subdomain}fal.run/${appId}/${path}`; + return `${url.replace(/\/$/, "")}${queryParams}`; +} diff --git a/libs/client/src/response.ts b/libs/client/src/response.ts index b532335..e310450 100644 --- a/libs/client/src/response.ts +++ b/libs/client/src/response.ts @@ -1,7 +1,14 @@ -import { ValidationErrorInfo } from "./types"; +import { RequiredConfig } from "./config"; +import { Result, ValidationErrorInfo } from "./types"; export type ResponseHandler = (response: Response) => Promise; +const REQUEST_ID_HEADER = "x-fal-request-id"; + +export type ResponseHandlerCreator = ( + config: RequiredConfig, +) => ResponseHandler; + type ApiErrorArgs = { message: string; status: number; @@ -81,3 +88,13 @@ export async function defaultResponseHandler( // TODO convert to either number or bool automatically return response.text() as Promise; } + +export async function resultResponseHandler( + response: Response, +): Promise> { + const data = await defaultResponseHandler(response); + return { + data, + requestId: response.headers.get(REQUEST_ID_HEADER) || "", + } satisfies Result; +} diff --git a/libs/client/src/runtime.spec.ts b/libs/client/src/runtime.spec.ts index 182ab57..1952f2f 100644 --- a/libs/client/src/runtime.spec.ts +++ b/libs/client/src/runtime.spec.ts @@ -14,6 +14,6 @@ describe("the runtime test suite", () => { }); it("should create the correct user agent identifier", () => { - expect(getUserAgent()).toMatch(/@fal-ai\/serverless-client/); + expect(getUserAgent()).toMatch(/@fal-ai\/client/); }); }); diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index cb51362..adb1981 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -1,4 +1,4 @@ -import { getConfig, getRestApiUrl } from "./config"; +import { getRestApiUrl, RequiredConfig } from "./config"; import { dispatchRequest } from "./request"; import { isPlainObject } from "./utils"; @@ -7,7 +7,7 @@ import { isPlainObject } from "./utils"; * uploading files to the server and transforming the input to replace file * objects with URLs. */ -export interface StorageSupport { +export interface StorageClient { /** * Upload a file to the server. Returns the URL of the uploaded file. * @param file the file to upload @@ -57,56 +57,71 @@ function getExtensionFromContentType(contentType: string): string { * @param file the file to upload * @returns the URL to upload the file to and the URL of the file once it is uploaded. */ -async function initiateUpload(file: Blob): Promise { +async function initiateUpload( + file: Blob, + config: RequiredConfig, +): Promise { const contentType = file.type || "application/octet-stream"; const filename = file.name || `${Date.now()}.${getExtensionFromContentType(contentType)}`; - return await dispatchRequest( - "POST", - `${getRestApiUrl()}/storage/upload/initiate`, - { + return await dispatchRequest({ + method: "POST", + targetUrl: `${getRestApiUrl()}/storage/upload/initiate`, + input: { content_type: contentType, file_name: filename, }, - ); + config, + }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any type KeyValuePair = [string, any]; -export const storageImpl: StorageSupport = { - upload: async (file: Blob) => { - const { fetch } = getConfig(); - const { upload_url: uploadUrl, file_url: url } = await initiateUpload(file); - const response = await fetch(uploadUrl, { - method: "PUT", - body: file, - headers: { - "Content-Type": file.type || "application/octet-stream", - }, - }); - const { responseHandler } = getConfig(); - await responseHandler(response); - return url; - }, +type StorageClientDependencies = { + config: RequiredConfig; +}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transformInput: async (input: any): Promise => { - if (Array.isArray(input)) { - return Promise.all(input.map((item) => storageImpl.transformInput(item))); - } else if (input instanceof Blob) { - return await storageImpl.upload(input); - } else if (isPlainObject(input)) { - const inputObject = input as Record; - const promises = Object.entries(inputObject).map( - async ([key, value]): Promise => { - return [key, await storageImpl.transformInput(value)]; - }, +export function createStorageClient({ + config, +}: StorageClientDependencies): StorageClient { + const ref: StorageClient = { + upload: async (file: Blob) => { + const { fetch, responseHandler } = config; + const { upload_url: uploadUrl, file_url: url } = await initiateUpload( + file, + config, ); - const results = await Promise.all(promises); - return Object.fromEntries(results); - } - // Return the input as is if it's neither an object nor a file/blob/data URI - return input; - }, -}; + const response = await fetch(uploadUrl, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type || "application/octet-stream", + }, + }); + await responseHandler(response); + return url; + }, + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transformInput: async (input: any): Promise => { + if (Array.isArray(input)) { + return Promise.all(input.map((item) => ref.transformInput(item))); + } else if (input instanceof Blob) { + return await ref.upload(input); + } else if (isPlainObject(input)) { + const inputObject = input as Record; + const promises = Object.entries(inputObject).map( + async ([key, value]): Promise => { + return [key, await ref.transformInput(value)]; + }, + ); + const results = await Promise.all(promises); + return Object.fromEntries(results); + } + // Return the input as is if it's neither an object nor a file/blob/data URI + return input; + }, + }; + return ref; +} diff --git a/libs/client/src/streaming.ts b/libs/client/src/streaming.ts index 2d972f4..ebfe5dd 100644 --- a/libs/client/src/streaming.ts +++ b/libs/client/src/streaming.ts @@ -1,10 +1,9 @@ import { createParser } from "eventsource-parser"; import { getTemporaryAuthToken } from "./auth"; -import { getConfig } from "./config"; -import { buildUrl } from "./function"; -import { dispatchRequest } from "./request"; +import { RequiredConfig } from "./config"; +import { buildUrl, dispatchRequest } from "./request"; import { ApiError, defaultResponseHandler } from "./response"; -import { storageImpl } from "./storage"; +import { type StorageClient } from "./storage"; export type StreamingConnectionMode = "client" | "server"; @@ -14,7 +13,7 @@ const CONTENT_TYPE_EVENT_STREAM = "text/event-stream"; * The stream API options. It requires the API input and also * offers configuration options. */ -type StreamOptions = { +export type StreamOptions = { /** * The endpoint URL. If not provided, it will be generated from the * `endpointId` and the `queryParams`. @@ -76,6 +75,7 @@ type EventHandler = (event: T) => void; */ export class FalStream { // properties + config: RequiredConfig; endpointId: string; url: string; options: StreamOptions; @@ -92,8 +92,13 @@ export class FalStream { private abortController = new AbortController(); - constructor(endpointId: string, options: StreamOptions) { + constructor( + endpointId: string, + config: RequiredConfig, + options: StreamOptions, + ) { this.endpointId = endpointId; + this.config = config; this.url = options.url ?? buildUrl(endpointId, { @@ -130,8 +135,8 @@ export class FalStream { if (connectionMode === "client") { // if we are in the browser, we need to get a temporary token // to authenticate the request - const token = await getTemporaryAuthToken(endpointId); - const { fetch } = getConfig(); + const token = await getTemporaryAuthToken(endpointId, this.config); + const { fetch } = this.config; const parsedUrl = new URL(this.url); parsedUrl.searchParams.set("fal_jwt_token", token); const response = await fetch(parsedUrl.toString(), { @@ -145,12 +150,18 @@ export class FalStream { }); return await this.handleResponse(response); } - return await dispatchRequest(method.toUpperCase(), this.url, input, { - headers: { - accept: options.accept ?? CONTENT_TYPE_EVENT_STREAM, + return await dispatchRequest({ + method: method.toUpperCase(), + targetUrl: this.url, + input, + config: this.config, + options: { + headers: { + accept: options.accept ?? CONTENT_TYPE_EVENT_STREAM, + }, + responseHandler: this.handleResponse, + signal: this.abortController.signal, }, - responseHandler: this.handleResponse, - signal: this.abortController.signal, }); } catch (error) { this.handleError(error); @@ -182,9 +193,9 @@ export class FalStream { return; } - const isEventStream = response.headers - .get("content-type") - .startsWith(CONTENT_TYPE_EVENT_STREAM); + const isEventStream = ( + response.headers.get("content-type") ?? "" + ).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(); @@ -317,24 +328,45 @@ export class FalStream { } /** - * Calls a fal app that supports streaming and provides a streaming-capable - * object as a result, that can be used to get partial results through either - * `AsyncIterator` or through an event listener. - * - * @param endpointId the endpoint id, e.g. `fal-ai/llavav15-13b`. - * @param options the request options, including the input payload. - * @returns the `FalStream` instance. + * The streaming client interface. */ -export async function stream, Output = any>( - endpointId: string, - options: StreamOptions, -): Promise> { - const input = - options.input && options.autoUpload !== false - ? await storageImpl.transformInput(options.input) - : options.input; - return new FalStream(endpointId, { - ...options, - input: input as Input, - }); +export interface StreamingClient { + /** + * Calls a fal app that supports streaming and provides a streaming-capable + * object as a result, that can be used to get partial results through either + * `AsyncIterator` or through an event listener. + * + * @param endpointId the endpoint id, e.g. `fal-ai/llavav15-13b`. + * @param options the request options, including the input payload. + * @returns the `FalStream` instance. + */ + stream>( + endpointId: string, + options: StreamOptions, + ): Promise>; +} + +type StreamingClientDependencies = { + config: RequiredConfig; + storage: StorageClient; +}; + +export function createStreamingClient({ + config, + storage, +}: StreamingClientDependencies): StreamingClient { + return { + async stream( + endpointId: string, + options: StreamOptions, + ) { + const input = options.input + ? await storage.transformInput(options.input) + : undefined; + return new FalStream(endpointId, config, { + ...options, + input: input as Input, + }); + }, + }; } diff --git a/libs/client/src/types.ts b/libs/client/src/types.ts index 5365b2d..d51b4e8 100644 --- a/libs/client/src/types.ts +++ b/libs/client/src/types.ts @@ -1,9 +1,46 @@ +/** + * Represents an API result, containing the data, + * the request ID and any other relevant information. + */ export type Result = { - result: T; + data: T; + requestId: string; }; -export type EnqueueResult = { - request_id: string; +/** + * The function input and other configuration when running + * the function, such as the HTTP method to use. + */ +export type RunOptions = { + /** + * 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; +}; + +export type UrlOptions = { + /** + * If `true`, the function will use the queue to run the function + * asynchronously and return the result in a separate call. This + * influences how the URL is built. + */ + readonly subdomain?: string; + + /** + * The query parameters to include in the URL. + */ + readonly query?: Record; + + /** + * The path to append to the function URL. + */ + path?: string; }; export type RequestLog = { @@ -18,7 +55,14 @@ export type Metrics = { }; interface BaseQueueStatus { - status: "IN_PROGRESS" | "COMPLETED" | "IN_QUEUE"; + status: "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED"; + request_id: string; +} + +export interface InQueueQueueStatus extends BaseQueueStatus { + status: "IN_QUEUE"; + queue_position: number; + response_url: string; } export interface InProgressQueueStatus extends BaseQueueStatus { @@ -31,19 +75,13 @@ export interface CompletedQueueStatus extends BaseQueueStatus { status: "COMPLETED"; response_url: string; logs: RequestLog[]; - metrics: Metrics; -} - -export interface EnqueuedQueueStatus extends BaseQueueStatus { - status: "IN_QUEUE"; - queue_position: number; - response_url: string; + metrics?: Metrics; } export type QueueStatus = | InProgressQueueStatus | CompletedQueueStatus - | EnqueuedQueueStatus; + | InQueueQueueStatus; export function isQueueStatus(obj: any): obj is QueueStatus { return obj && obj.status && obj.response_url; diff --git a/libs/client/src/utils.spec.ts b/libs/client/src/utils.spec.ts index 16ee2fe..e58a563 100644 --- a/libs/client/src/utils.spec.ts +++ b/libs/client/src/utils.spec.ts @@ -1,24 +1,24 @@ -import { ensureAppIdFormat, parseAppId } from "./utils"; +import { ensureEndpointIdFormat, parseEndpointId } from "./utils"; describe("The utils test suite", () => { it("shoud match a current appOwner/appId format", () => { const id = "fal-ai/fast-sdxl"; - expect(ensureAppIdFormat(id)).toBe(id); + expect(ensureEndpointIdFormat(id)).toBe(id); }); it("shoud match a current appOwner/appId/path format", () => { const id = "fal-ai/fast-sdxl/image-to-image"; - expect(ensureAppIdFormat(id)).toBe(id); + expect(ensureEndpointIdFormat(id)).toBe(id); }); it("should throw on an invalid app id format", () => { const id = "just-an-id"; - expect(() => ensureAppIdFormat(id)).toThrowError(); + expect(() => ensureEndpointIdFormat(id)).toThrowError(); }); it("should parse a current app id", () => { const id = "fal-ai/fast-sdxl"; - const parsed = parseAppId(id); + const parsed = parseEndpointId(id); expect(parsed).toEqual({ owner: "fal-ai", alias: "fast-sdxl", @@ -27,7 +27,7 @@ describe("The utils test suite", () => { it("should parse a current app id with path", () => { const id = "fal-ai/fast-sdxl/image-to-image"; - const parsed = parseAppId(id); + const parsed = parseEndpointId(id); expect(parsed).toEqual({ owner: "fal-ai", alias: "fast-sdxl", @@ -37,7 +37,7 @@ describe("The utils test suite", () => { it("should parse a current app id with namespace", () => { const id = "workflows/fal-ai/fast-sdxl"; - const parsed = parseAppId(id); + const parsed = parseEndpointId(id); expect(parsed).toEqual({ owner: "fal-ai", alias: "fast-sdxl", diff --git a/libs/client/src/utils.ts b/libs/client/src/utils.ts index 7184593..b0a28e0 100644 --- a/libs/client/src/utils.ts +++ b/libs/client/src/utils.ts @@ -1,4 +1,4 @@ -export function ensureAppIdFormat(id: string): string { +export function ensureEndpointIdFormat(id: string): string { const parts = id.split("/"); if (parts.length > 1) { return id; @@ -12,26 +12,26 @@ export function ensureAppIdFormat(id: string): string { ); } -const APP_NAMESPACES = ["workflows", "comfy"] as const; +const ENDPOINT_NAMESPACES = ["workflows", "comfy"] as const; -type AppNamespace = (typeof APP_NAMESPACES)[number]; +type EndpointNamespace = (typeof ENDPOINT_NAMESPACES)[number]; -export type AppId = { +export type EndpointId = { readonly owner: string; readonly alias: string; readonly path?: string; - readonly namespace?: AppNamespace; + readonly namespace?: EndpointNamespace; }; -export function parseAppId(id: string): AppId { - const normalizedId = ensureAppIdFormat(id); +export function parseEndpointId(id: string): EndpointId { + const normalizedId = ensureEndpointIdFormat(id); const parts = normalizedId.split("/"); - if (APP_NAMESPACES.includes(parts[0] as any)) { + if (ENDPOINT_NAMESPACES.includes(parts[0] as any)) { return { owner: parts[1], alias: parts[2], path: parts.slice(3).join("/") || undefined, - namespace: parts[0] as AppNamespace, + namespace: parts[0] as EndpointNamespace, }; } return { diff --git a/libs/proxy/README.md b/libs/proxy/README.md index 9df3fc1..0f8cbbb 100644 --- a/libs/proxy/README.md +++ b/libs/proxy/README.md @@ -1,15 +1,15 @@ # fal.ai proxy library -![@fal-ai/serverless-proxy npm package](https://img.shields.io/npm/v/@fal-ai/serverless-proxy?color=%237527D7&label=%40fal-ai%2Fserverless-proxy&style=flat-square) +![@fal-ai/server-proxy npm package](https://img.shields.io/npm/v/@fal-ai/server-proxy?color=%237527D7&label=%40fal-ai%2Fserver-proxy&style=flat-square) ## Introduction -The `@fal-ai/serverless-proxy` library enables you to route client requests through your own server, therefore safeguarding sensitive credentials. With built-in support for popular frameworks like Next.js and Express, setting up the proxy becomes a breeze. +The `@fal-ai/server-proxy` library enables you to route client requests through your own server, therefore safeguarding sensitive credentials. With built-in support for popular frameworks like Next.js and Express, setting up the proxy becomes a breeze. ### Install the proxy library: ``` -npm install --save @fal-ai/serverless-proxy +npm install --save @fal-ai/server-proxy ``` ## Next.js page router integration @@ -19,7 +19,7 @@ For Next.js applications using the page router: 1. Create an API route in your Next.js app, as a convention we suggest using `pages/api/fal/proxy.js` (or `.ts` if you're using TypeScript): 2. Re-export the proxy handler from the library as the default export: ```ts - export { handler as default } from "@fal-ai/serverless-proxy/nextjs"; + export { handler as default } from "@fal-ai/server-proxy/nextjs"; ``` 3. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key. @@ -31,9 +31,9 @@ For Next.js applications using the app router: 2. Re-export the proxy handler from the library as the default export: ```ts - import { route } from "@fal-ai/serverless-proxy/nextjs"; + import { route } from "@fal-ai/server-proxy/nextjs"; - export const { GET, POST } = route; + export const { GET, POST, PUT } = route; ``` 3. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key. @@ -49,7 +49,7 @@ For Express applications: 2. Add the proxy route and its handler. Note that if your client lives outside of the express app (i.e. the express app is solely used as an external API for other clients), you will need to allow CORS on the proxy route: ```ts - import * as falProxy from "@fal-ai/serverless-proxy/express"; + import * as falProxy from "@fal-ai/server-proxy/express"; app.all( falProxy.route, // '/api/fal/proxy' or you can use your own @@ -65,7 +65,7 @@ For Express applications: Once you've set up the proxy, you can configure the client to use it: ```ts -import * as fal from "@fal-ai/serverless-client"; +import { fal } from "@fal-ai/client"; fal.config({ proxyUrl: "/api/fal/proxy", // or https://my.app.com/api/fal/proxy diff --git a/libs/proxy/package.json b/libs/proxy/package.json index 68ae210..7fc56cd 100644 --- a/libs/proxy/package.json +++ b/libs/proxy/package.json @@ -1,6 +1,7 @@ { - "name": "@fal-ai/serverless-proxy", - "version": "0.9.0", + "name": "@fal-ai/server-proxy", + "description": "The fal.ai server proxy adapter for JavaScript and TypeScript Web frameworks", + "version": "1.0.0", "license": "MIT", "repository": { "type": "git", @@ -9,16 +10,17 @@ }, "keywords": [ "fal", - "serverless", "client", "next", "nextjs", "express", + "hono", "proxy" ], "exports": { ".": "./src/index.js", "./express": "./src/express.js", + "./hono": "./src/hono.js", "./nextjs": "./src/nextjs.js", "./remix": "./src/remix.js", "./svelte": "./src/svelte.js" @@ -28,6 +30,9 @@ "express": [ "src/express.d.ts" ], + "hono": [ + "src/hono.d.ts" + ], "nextjs": [ "src/nextjs.d.ts" ], @@ -45,6 +50,7 @@ "@remix-run/dev": "^2.0.0", "@sveltejs/kit": "^2.0.0", "express": "^4.0.0", + "hono": "^4.0.0", "next": "13.4 - 14", "react": "^18.0.0", "react-dom": "^18.0.0" @@ -59,6 +65,9 @@ "express": { "optional": true }, + "hono": { + "optional": true + }, "next": { "optional": true }, diff --git a/libs/proxy/src/hono.ts b/libs/proxy/src/hono.ts new file mode 100644 index 0000000..bf8f9cf --- /dev/null +++ b/libs/proxy/src/hono.ts @@ -0,0 +1,51 @@ +import { Context } from "hono"; +import { type StatusCode } from "hono/utils/http-status"; +import { + handleRequest, + HeaderValue, + resolveApiKeyFromEnv, + responsePassthrough, +} from "./index"; + +export type FalHonoProxyOptions = { + /** + * A function to resolve the API key used by the proxy. + * By default, it uses the `FAL_KEY` environment variable. + */ + resolveApiKey?: () => Promise; +}; + +type RouteHandler = (context: Context) => Promise; + +/** + * Creates a route handler that proxies requests to the fal API. + * + * This is a drop-in handler for Hono applications so that the client can be called + * directly from the client-side code while keeping API keys safe. + * + * @param param the proxy options. + * @returns a Hono route handler function. + */ +export function createRouteHandler({ + resolveApiKey = resolveApiKeyFromEnv, +}: FalHonoProxyOptions): RouteHandler { + const routeHandler: RouteHandler = async (context) => { + const responseHeaders: Record = {}; + const response = await handleRequest({ + id: "hono", + method: context.req.method, + respondWith: (status, data) => { + return context.json(data, status as StatusCode, responseHeaders); + }, + getHeaders: () => responseHeaders, + getHeader: (name) => context.req.header[name], + sendHeader: (name, value) => (responseHeaders[name] = value), + getRequestBody: async () => JSON.stringify(await context.req.json()), + sendResponse: responsePassthrough, + resolveApiKey, + }); + return response; + }; + + return routeHandler; +} diff --git a/libs/proxy/src/index.ts b/libs/proxy/src/index.ts index 59d6a62..0b01966 100644 --- a/libs/proxy/src/index.ts +++ b/libs/proxy/src/index.ts @@ -57,8 +57,8 @@ function getFalKey(): string | undefined { const EXCLUDED_HEADERS = ["content-length", "content-encoding"]; /** - * A request handler that proxies the request to the fal-serverless - * endpoint. This is useful so client-side calls to the fal-serverless endpoint + * A request handler that proxies the request to the fal API + * endpoint. This is useful so client-side calls to the fal endpoint * can be made without CORS issues and the correct credentials can be added * effortlessly. * @@ -93,7 +93,7 @@ export async function handleRequest( } }); - const proxyUserAgent = `@fal-ai/serverless-proxy/${behavior.id}`; + const proxyUserAgent = `@fal-ai/server-proxy/${behavior.id}`; const userAgent = singleHeaderValue(behavior.getHeader("user-agent")); const res = await fetch(targetUrl, { method: behavior.method, diff --git a/libs/proxy/src/nextjs.ts b/libs/proxy/src/nextjs.ts index 373fe59..d4d4eca 100644 --- a/libs/proxy/src/nextjs.ts +++ b/libs/proxy/src/nextjs.ts @@ -47,18 +47,14 @@ export const handler: NextApiHandler = async (request, response) => { * @returns a promise that resolves when the request is handled. */ async function routeHandler(request: NextRequest) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const responseHeaders: Record = {}; - - // check if response if from a streaming request - + const responseHeaders = new Headers(); return await handleRequest({ id: "nextjs-app-router", method: request.method, getRequestBody: async () => request.text(), getHeaders: () => fromHeaders(request.headers), getHeader: (name) => request.headers.get(name), - sendHeader: (name, value) => (responseHeaders[name] = value), + sendHeader: (name, value) => responseHeaders.set(name, value), respondWith: (status, data) => NextResponse.json(data, { status, diff --git a/libs/proxy/src/svelte.ts b/libs/proxy/src/svelte.ts index d1ee5b8..9c63e3d 100644 --- a/libs/proxy/src/svelte.ts +++ b/libs/proxy/src/svelte.ts @@ -21,10 +21,9 @@ export const createRequestHandler = ({ }: RequestHandlerParams = {}) => { const handler: RequestHandler = async ({ request }) => { const FAL_KEY = credentials || process.env.FAL_KEY || ""; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const responseHeaders: Record = { + const responseHeaders = new Headers({ "Content-Type": "application/json", - }; + }); return await handleRequest({ id: "svelte-app-router", method: request.method, diff --git a/package-lock.json b/package-lock.json index f4ed031..73d41ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,6 +89,7 @@ "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "fs-extra": "^11.1.0", + "hono": "^4.6.3", "husky": "^8.0.0", "jest": "29.4.3", "jest-environment-jsdom": "29.4.3", @@ -17458,6 +17459,15 @@ "tslib": "^2.0.3" } }, + "node_modules/hono": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.6.3.tgz", + "integrity": "sha512-0LeEuBNFeSHGqZ9sNVVgZjB1V5fmhkBSB0hZrpqStSMLOWgfLy0dHOvrjbJh0H2khsjet6rbHfWTHY0kpYThKQ==", + "dev": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/hook-std": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", diff --git a/package.json b/package.json index 61ffb15..3d6231f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "cspell", "prettier --write" ], - "libs/client/src/**/*.{ts}": [ + "libs/client/src/**/*.ts": [ "npm run docs:typedoc" ] }, @@ -109,6 +109,7 @@ "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "fs-extra": "^11.1.0", + "hono": "^4.6.3", "husky": "^8.0.0", "jest": "29.4.3", "jest-environment-jsdom": "29.4.3", diff --git a/tsconfig.base.json b/tsconfig.base.json index d3023e2..67d0d9a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,11 +15,11 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@fal-ai/client": ["libs/client/src/index.ts"], "@fal-ai/create-app": ["libs/create-app/src/index.ts"], - "@fal-ai/serverless-client": ["libs/client/src/index.ts"], - "@fal-ai/serverless-proxy": ["libs/proxy/src/index.ts"], - "@fal-ai/serverless-proxy/express": ["libs/proxy/src/express.ts"], - "@fal-ai/serverless-proxy/nextjs": ["libs/proxy/src/nextjs.ts"] + "@fal-ai/server-proxy": ["libs/proxy/src/index.ts"], + "@fal-ai/server-proxy/express": ["libs/proxy/src/express.ts"], + "@fal-ai/server-proxy/nextjs": ["libs/proxy/src/nextjs.ts"] } }, "exclude": ["node_modules/**", "tmp/**", "dist/**"]