chore: upgrade dependencies and tooling (#81)
* chore: remove uuid-random dependency * chore: upgrade prettier * chore(client): bump version for release
This commit is contained in:
parent
4eb8111cdc
commit
c3a3c3d21a
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -16,8 +16,8 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
node-version: "18.x"
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Format check
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -17,9 +17,9 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@fal'
|
||||
node-version: "16.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
scope: "@fal"
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Publish to NPM
|
||||
|
||||
12
README.md
12
README.md
@ -27,17 +27,17 @@ This client library is crafted as a lightweight layer atop platform standards li
|
||||
2. Start by configuring your credentials:
|
||||
|
||||
```ts
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
|
||||
fal.config({
|
||||
// Can also be auto-configured using environment variables:
|
||||
credentials: 'FAL_KEY',
|
||||
credentials: "FAL_KEY",
|
||||
});
|
||||
```
|
||||
|
||||
3. Retrieve your function id and execute it:
|
||||
```ts
|
||||
const result = await fal.run('user/app-alias');
|
||||
const result = await fal.run("user/app-alias");
|
||||
```
|
||||
|
||||
The result's type is contingent upon your Python function's output. Types in Python are mapped to their corresponding types in JavaScript.
|
||||
@ -56,13 +56,13 @@ For example, if you are using Next.js, you can:
|
||||
```
|
||||
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/serverless-proxy/nextjs";
|
||||
```
|
||||
3. Configure the client to use the proxy:
|
||||
```ts
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy',
|
||||
proxyUrl: "/api/fal/proxy",
|
||||
});
|
||||
```
|
||||
4. Make sure your server has `FAL_KEY` as environment variable with a valid API Key. That's it! Now your client calls will route through your server proxy, so your credentials are protected.
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'demo-express-app',
|
||||
preset: '../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
displayName: "demo-express-app",
|
||||
preset: "../../jest.preset.js",
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../coverage/apps/demo-express-app',
|
||||
moduleFileExtensions: ["ts", "js", "html"],
|
||||
coverageDirectory: "../../coverage/apps/demo-express-app",
|
||||
};
|
||||
|
||||
@ -3,34 +3,34 @@
|
||||
* 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 cors from 'cors';
|
||||
import { configDotenv } from 'dotenv';
|
||||
import express from 'express';
|
||||
import * as path from 'path';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import * as falProxy from "@fal-ai/serverless-proxy/express";
|
||||
import cors from "cors";
|
||||
import { configDotenv } from "dotenv";
|
||||
import express from "express";
|
||||
import * as path from "path";
|
||||
|
||||
configDotenv({ path: './env.local' });
|
||||
configDotenv({ path: "./env.local" });
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middlewares
|
||||
app.use('/assets', express.static(path.join(__dirname, 'assets')));
|
||||
app.use("/assets", express.static(path.join(__dirname, "assets")));
|
||||
app.use(express.json());
|
||||
|
||||
// fal.ai client proxy
|
||||
app.all(falProxy.route, cors(), falProxy.handler);
|
||||
|
||||
// Your API endpoints
|
||||
app.get('/api', (req, res) => {
|
||||
res.send({ message: 'Welcome to demo-express-app!' });
|
||||
app.get("/api", (req, res) => {
|
||||
res.send({ message: "Welcome to demo-express-app!" });
|
||||
});
|
||||
|
||||
app.get('/fal-on-server', async (req, res) => {
|
||||
const result = await fal.run('110602490-lcm', {
|
||||
app.get("/fal-on-server", async (req, res) => {
|
||||
const result = await fal.run("110602490-lcm", {
|
||||
input: {
|
||||
prompt:
|
||||
'a black cat with glowing eyes, cute, adorable, disney, pixar, highly detailed, 8k',
|
||||
"a black cat with glowing eyes, cute, adorable, disney, pixar, highly detailed, 8k",
|
||||
},
|
||||
});
|
||||
res.send(result);
|
||||
@ -40,4 +40,4 @@ const port = process.env.PORT || 3333;
|
||||
const server = app.listen(port, () => {
|
||||
console.log(`Listening at http://localhost:${port}/api`);
|
||||
});
|
||||
server.on('error', console.error);
|
||||
server.on("error", console.error);
|
||||
|
||||
@ -4,13 +4,13 @@
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json",
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
const { composePlugins, withNx } = require("@nx/webpack");
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(withNx(), (config) => {
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import { route } from '@fal-ai/serverless-proxy/nextjs';
|
||||
import { route } from "@fal-ai/serverless-proxy/nextjs";
|
||||
|
||||
export const { GET, POST, PUT } = route;
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { MutableRefObject, useEffect, useRef, useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy',
|
||||
proxyUrl: "/api/fal/proxy",
|
||||
});
|
||||
|
||||
const EMPTY_IMG =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjOHPmzH8ACDADZKt3GNsAAAAASUVORK5CYII=';
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjOHPmzH8ACDADZKt3GNsAAAAASUVORK5CYII=";
|
||||
|
||||
type WebcamOptions = {
|
||||
videoRef: MutableRefObject<HTMLVideoElement | null>;
|
||||
@ -65,7 +65,7 @@ const useWebcam = ({
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
const context = canvas.getContext("2d");
|
||||
if (context === null) {
|
||||
return;
|
||||
}
|
||||
@ -80,7 +80,7 @@ const useWebcam = ({
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
height,
|
||||
);
|
||||
|
||||
// Callback with frame data
|
||||
@ -92,8 +92,8 @@ const useWebcam = ({
|
||||
onFrameUpdate(frameData);
|
||||
});
|
||||
},
|
||||
'image/jpeg',
|
||||
0.7
|
||||
"image/jpeg",
|
||||
0.7,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -143,22 +143,22 @@ export default function WebcamPage() {
|
||||
const previewRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const { send } = fal.realtime.connect<LCMInput, LCMOutput>(
|
||||
'fal-ai/fast-turbo-diffusion/image-to-image',
|
||||
"fal-ai/fast-turbo-diffusion/image-to-image",
|
||||
{
|
||||
connectionKey: 'camera-turbo-demo',
|
||||
connectionKey: "camera-turbo-demo",
|
||||
// not throttling the client, handling throttling of the camera itself
|
||||
// and letting all requests through in real-time
|
||||
throttleInterval: 0,
|
||||
onResult(result) {
|
||||
if (processedImageRef.current && result.images && result.images[0]) {
|
||||
const blob = new Blob([result.images[0].content], {
|
||||
type: 'image/jpeg',
|
||||
type: "image/jpeg",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
processedImageRef.current.src = url;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const onFrameUpdate = (data: Uint8Array) => {
|
||||
@ -166,7 +166,7 @@ export default function WebcamPage() {
|
||||
return;
|
||||
}
|
||||
send({
|
||||
prompt: 'a picture of george clooney, elegant, in a suit, 8k, uhd',
|
||||
prompt: "a picture of george clooney, elegant, in a suit, 8k, uhd",
|
||||
image_bytes: data,
|
||||
num_inference_steps: 3,
|
||||
strength: 0.6,
|
||||
@ -182,29 +182,29 @@ export default function WebcamPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="flex-col px-32 mx-auto my-20">
|
||||
<h1 className="text-4xl font-mono mb-8 text-current text-center">
|
||||
<main className="mx-auto my-20 flex-col px-32">
|
||||
<h1 className="mb-8 text-center font-mono text-4xl text-current">
|
||||
fal<code className="font-light text-pink-600">camera</code>
|
||||
</h1>
|
||||
<video ref={videoRef} style={{ display: 'none' }}></video>
|
||||
<div className="py-12 flex items-center justify-center">
|
||||
<video ref={videoRef} style={{ display: "none" }}></video>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<button
|
||||
className="py-3 px-4 bg-indigo-700 text-white text-lg rounded"
|
||||
className="rounded bg-indigo-700 py-3 px-4 text-lg text-white"
|
||||
onClick={() => {
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
>
|
||||
{enabled ? 'Stop' : 'Start'}
|
||||
{enabled ? "Stop" : "Start"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4 justify-between">
|
||||
<div className="flex flex-col justify-between space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<canvas ref={previewRef} width="512" height="512"></canvas>
|
||||
<img
|
||||
ref={processedImageRef}
|
||||
src={EMPTY_IMG}
|
||||
width={512}
|
||||
height={512}
|
||||
className="min-w-[512px] min-h-[512px]"
|
||||
className="min-h-[512px] min-w-[512px]"
|
||||
alt="generated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
// @snippet:start(client.config)
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy', // the built-int nextjs proxy
|
||||
proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy
|
||||
// proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy
|
||||
});
|
||||
// @snippet:end
|
||||
@ -35,7 +35,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
@ -44,7 +44,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT =
|
||||
'photograph of victorian woman with wings, sky clouds, meadow grass';
|
||||
"photograph of victorian woman with wings, sky clouds, meadow grass";
|
||||
|
||||
export default function ComfyImageToImagePage() {
|
||||
// @snippet:start("client.ui.state")
|
||||
@ -84,7 +84,7 @@ export default function ComfyImageToImagePage() {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result: Result = await fal.subscribe(
|
||||
'comfy/fal-ai/image-to-image',
|
||||
"comfy/fal-ai/image-to-image",
|
||||
{
|
||||
input: {
|
||||
prompt: prompt,
|
||||
@ -94,13 +94,13 @@ export default function ComfyImageToImagePage() {
|
||||
onQueueUpdate(update) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
update.status === "IN_PROGRESS" ||
|
||||
update.status === "COMPLETED"
|
||||
) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
setResult(getImageURL(result));
|
||||
} catch (error: any) {
|
||||
@ -112,16 +112,16 @@ export default function ComfyImageToImagePage() {
|
||||
// @snippet:end
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">
|
||||
Comfy SD1.5 - Image to Image
|
||||
</h1>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="image" className="block mb-2 text-current">
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="image" className="mb-2 block text-current">
|
||||
Image
|
||||
</label>
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="mx-auto flex">
|
||||
{imageFile && (
|
||||
<img
|
||||
@ -133,7 +133,7 @@ export default function ComfyImageToImagePage() {
|
||||
</div>
|
||||
|
||||
<input
|
||||
className="w-full text-sm p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-sm dark:border-white/10 dark:bg-white/5"
|
||||
id="image_url"
|
||||
name="image_url"
|
||||
type="file"
|
||||
@ -143,12 +143,12 @@ export default function ComfyImageToImagePage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="prompt" className="mb-2 block text-current">
|
||||
Prompt
|
||||
</label>
|
||||
<input
|
||||
className="w-full text-lg p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-lg dark:border-white/10 dark:bg-white/5"
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="Imagine..."
|
||||
@ -164,15 +164,15 @@ export default function ComfyImageToImagePage() {
|
||||
e.preventDefault();
|
||||
generateVideo();
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Image'}
|
||||
{loading ? "Generating..." : "Generate Image"}
|
||||
</button>
|
||||
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="mx-auto">
|
||||
{video && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@ -181,20 +181,20 @@ export default function ComfyImageToImagePage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
<p className="text-current/80 text-sm">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
: "// result pending..."}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.filter(Boolean).join('\n')}
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{logs.filter(Boolean).join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
// @snippet:start(client.config)
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy', // the built-int nextjs proxy
|
||||
proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy
|
||||
// proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy
|
||||
});
|
||||
// @snippet:end
|
||||
@ -35,7 +35,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
@ -80,7 +80,7 @@ export default function ComfyImageToVideoPage() {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result: Result = await fal.subscribe(
|
||||
'comfy/fal-ai/image-to-video',
|
||||
"comfy/fal-ai/image-to-video",
|
||||
{
|
||||
input: {
|
||||
loadimage_1: imageFile,
|
||||
@ -89,13 +89,13 @@ export default function ComfyImageToVideoPage() {
|
||||
onQueueUpdate(update) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
update.status === "IN_PROGRESS" ||
|
||||
update.status === "COMPLETED"
|
||||
) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
setResult(getImageURL(result));
|
||||
} catch (error: any) {
|
||||
@ -107,14 +107,14 @@ export default function ComfyImageToVideoPage() {
|
||||
// @snippet:end
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">Comfy SVD - Image to Video</h1>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="image" className="block mb-2 text-current">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">Comfy SVD - Image to Video</h1>
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="image" className="mb-2 block text-current">
|
||||
Image
|
||||
</label>
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="mx-auto flex">
|
||||
{imageFile && (
|
||||
<img
|
||||
@ -126,7 +126,7 @@ export default function ComfyImageToVideoPage() {
|
||||
</div>
|
||||
|
||||
<input
|
||||
className="w-full text-sm p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-sm dark:border-white/10 dark:bg-white/5"
|
||||
id="image_url"
|
||||
name="image_url"
|
||||
type="file"
|
||||
@ -142,15 +142,15 @@ export default function ComfyImageToVideoPage() {
|
||||
e.preventDefault();
|
||||
generateVideo();
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Video'}
|
||||
{loading ? "Generating..." : "Generate Video"}
|
||||
</button>
|
||||
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="mx-auto">
|
||||
{video && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@ -159,20 +159,20 @@ export default function ComfyImageToVideoPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
<p className="text-current/80 text-sm">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
: "// result pending..."}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.filter(Boolean).join('\n')}
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{logs.filter(Boolean).join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,35 +1,35 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter(); // Use correct router
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container mx-auto flex flex-col items-center justify-center w-full flex-1 py-12 px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
Serverless Comfy Workflow Examples powered by{' '}
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container mx-auto flex w-full flex-1 flex-col items-center justify-center py-12 px-4 text-center sm:px-6 lg:px-8">
|
||||
<h1 className="mb-8 text-4xl font-bold">
|
||||
Serverless Comfy Workflow Examples powered by{" "}
|
||||
<code className="font-light text-pink-600">fal</code>
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-gray-400 max-w-2xl">
|
||||
<p className="mt-2 max-w-2xl text-lg text-gray-400">
|
||||
Learn how to use our fal-js to execute Comfy workflows.
|
||||
</p>
|
||||
<div className="mt-12 grid grid-cols-1 gap-3 md:grid-cols-3 lg:grid-cols-3">
|
||||
<button
|
||||
onClick={() => router.push('/comfy/text-to-image')}
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg shadow-md transition-transform transform hover:-translate-y-1"
|
||||
onClick={() => router.push("/comfy/text-to-image")}
|
||||
className="transform rounded-lg bg-blue-600 px-6 py-3 text-white shadow-md transition-transform hover:-translate-y-1 hover:bg-blue-500"
|
||||
>
|
||||
Text to Image
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/comfy/image-to-image')}
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg shadow-md transition-transform transform hover:-translate-y-1"
|
||||
onClick={() => router.push("/comfy/image-to-image")}
|
||||
className="transform rounded-lg bg-blue-600 px-6 py-3 text-white shadow-md transition-transform hover:-translate-y-1 hover:bg-blue-500"
|
||||
>
|
||||
Image to Image
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/comfy/image-to-video')}
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg shadow-md transition-transform transform hover:-translate-y-1"
|
||||
onClick={() => router.push("/comfy/image-to-video")}
|
||||
className="transform rounded-lg bg-blue-600 px-6 py-3 text-white shadow-md transition-transform hover:-translate-y-1 hover:bg-blue-500"
|
||||
>
|
||||
Image to Video
|
||||
</button>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
// @snippet:start(client.config)
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy', // the built-int nextjs proxy
|
||||
proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy
|
||||
// proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy
|
||||
});
|
||||
// @snippet:end
|
||||
@ -35,7 +35,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
@ -44,7 +44,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT =
|
||||
'a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd';
|
||||
"a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd";
|
||||
|
||||
export default function ComfyTextToImagePage() {
|
||||
// @snippet:start("client.ui.state")
|
||||
@ -82,7 +82,7 @@ export default function ComfyTextToImagePage() {
|
||||
setLoading(true);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result: Result = await fal.subscribe('comfy/fal-ai/text-to-image', {
|
||||
const result: Result = await fal.subscribe("comfy/fal-ai/text-to-image", {
|
||||
input: {
|
||||
prompt: prompt,
|
||||
},
|
||||
@ -90,8 +90,8 @@ export default function ComfyTextToImagePage() {
|
||||
onQueueUpdate(update) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
update.status === "IN_PROGRESS" ||
|
||||
update.status === "COMPLETED"
|
||||
) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
@ -107,15 +107,15 @@ export default function ComfyTextToImagePage() {
|
||||
// @snippet:end
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">Comfy SDXL - Text to Image</h1>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">Comfy SDXL - Text to Image</h1>
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="prompt" className="mb-2 block text-current">
|
||||
Prompt
|
||||
</label>
|
||||
<input
|
||||
className="w-full text-lg p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-lg dark:border-white/10 dark:bg-white/5"
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="Imagine..."
|
||||
@ -131,15 +131,15 @@ export default function ComfyTextToImagePage() {
|
||||
e.preventDefault();
|
||||
generateImage();
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Image'}
|
||||
{loading ? "Generating..." : "Generate Image"}
|
||||
</button>
|
||||
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="mx-auto">
|
||||
{image && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@ -148,20 +148,20 @@ export default function ComfyTextToImagePage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
<p className="text-current/80 text-sm">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
: "// result pending..."}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.filter(Boolean).join('\n')}
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{logs.filter(Boolean).join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import './global.css';
|
||||
import "./global.css";
|
||||
|
||||
export const metadata = {
|
||||
title: 'Welcome to demo-nextjs-app-router',
|
||||
description: 'Generated by create-nx-workspace',
|
||||
title: "Welcome to demo-nextjs-app-router",
|
||||
description: "Generated by create-nx-workspace",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
// @snippet:start(client.config)
|
||||
fal.config({
|
||||
// credentials: 'FAL_KEY_ID:FAL_KEY_SECRET',
|
||||
proxyUrl: '/api/fal/proxy', // the built-int nextjs proxy
|
||||
proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy
|
||||
// proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy
|
||||
});
|
||||
// @snippet:end
|
||||
@ -32,7 +32,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
@ -41,7 +41,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT =
|
||||
'(masterpiece:1.4), (best quality), (detailed), Medieval village scene with busy streets and castle in the distance';
|
||||
"(masterpiece:1.4), (best quality), (detailed), Medieval village scene with busy streets and castle in the distance";
|
||||
|
||||
export default function Home() {
|
||||
// @snippet:start("client.ui.state")
|
||||
@ -79,18 +79,18 @@ export default function Home() {
|
||||
setLoading(true);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result: Result = await fal.subscribe('fal-ai/illusion-diffusion', {
|
||||
const result: Result = await fal.subscribe("fal-ai/illusion-diffusion", {
|
||||
input: {
|
||||
prompt,
|
||||
image_url: imageFile,
|
||||
image_size: 'square_hd',
|
||||
image_size: "square_hd",
|
||||
},
|
||||
logs: true,
|
||||
onQueueUpdate(update) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
update.status === "IN_PROGRESS" ||
|
||||
update.status === "COMPLETED"
|
||||
) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
@ -106,17 +106,17 @@ export default function Home() {
|
||||
// @snippet:end
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">
|
||||
Hello <code className="font-light text-pink-600">fal</code>
|
||||
</h1>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="prompt" className="mb-2 block text-current">
|
||||
Image
|
||||
</label>
|
||||
<input
|
||||
className="w-full text-lg p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-lg dark:border-white/10 dark:bg-white/5"
|
||||
id="image_url"
|
||||
name="image_url"
|
||||
type="file"
|
||||
@ -125,12 +125,12 @@ export default function Home() {
|
||||
onChange={(e) => setImageFile(e.target.files?.[0] ?? null)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="prompt" className="mb-2 block text-current">
|
||||
Prompt
|
||||
</label>
|
||||
<input
|
||||
className="w-full text-lg p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-lg dark:border-white/10 dark:bg-white/5"
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="Imagine..."
|
||||
@ -146,15 +146,15 @@ export default function Home() {
|
||||
e.preventDefault();
|
||||
generateImage();
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Image'}
|
||||
{loading ? "Generating..." : "Generate Image"}
|
||||
</button>
|
||||
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="mx-auto">
|
||||
{image && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@ -163,20 +163,20 @@ export default function Home() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
<p className="text-current/80 text-sm">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
: "// result pending..."}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.filter(Boolean).join('\n')}
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{logs.filter(Boolean).join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useState } from "react";
|
||||
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy',
|
||||
proxyUrl: "/api/fal/proxy",
|
||||
});
|
||||
|
||||
type ErrorProps = {
|
||||
@ -17,7 +17,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
@ -25,7 +25,7 @@ function Error(props: ErrorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_ENDPOINT_ID = 'fal-ai/fast-sdxl';
|
||||
const DEFAULT_ENDPOINT_ID = "fal-ai/fast-sdxl";
|
||||
const DEFAULT_INPUT = `{
|
||||
"prompt": "A beautiful sunset over the ocean"
|
||||
}`;
|
||||
@ -58,15 +58,15 @@ export default function Home() {
|
||||
input: JSON.parse(input),
|
||||
logs: true,
|
||||
// mode: "streaming",
|
||||
mode: 'polling',
|
||||
mode: "polling",
|
||||
pollInterval: 1000,
|
||||
onQueueUpdate(update) {
|
||||
console.log('queue update');
|
||||
console.log("queue update");
|
||||
console.log(update);
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
update.status === "IN_PROGRESS" ||
|
||||
update.status === "COMPLETED"
|
||||
) {
|
||||
if (update.logs && update.logs.length > logs.length) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
@ -83,18 +83,18 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">
|
||||
<code className="font-light text-pink-600">fal</code>
|
||||
<code>queue</code>
|
||||
</h1>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="prompt" className="mb-2 block text-current">
|
||||
Endpoint ID
|
||||
</label>
|
||||
<input
|
||||
className="w-full text-base p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-base dark:border-white/10 dark:bg-white/5"
|
||||
id="endpointId"
|
||||
name="endpointId"
|
||||
autoComplete="off"
|
||||
@ -104,12 +104,12 @@ export default function Home() {
|
||||
onChange={(e) => setEndpointId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="prompt" className="mb-2 block text-current">
|
||||
JSON Input
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full text-sm p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10 font-mono"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 font-mono text-sm dark:border-white/10 dark:bg-white/5"
|
||||
id="input"
|
||||
name="Input"
|
||||
placeholder="JSON"
|
||||
@ -126,31 +126,31 @@ export default function Home() {
|
||||
e.preventDefault();
|
||||
run();
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Running...' : 'Run'}
|
||||
{loading ? "Running..." : "Run"}
|
||||
</button>
|
||||
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
<p className="text-current/80 text-sm">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
: "// result pending..."}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.join('\n')}
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{logs.join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import { DrawingCanvas } from '../../components/drawing';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { ChangeEvent, useRef, useState } from "react";
|
||||
import { DrawingCanvas } from "../../components/drawing";
|
||||
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy',
|
||||
proxyUrl: "/api/fal/proxy",
|
||||
});
|
||||
|
||||
const PROMPT_EXPANDED =
|
||||
', beautiful, colorful, highly detailed, best quality, uhd';
|
||||
", beautiful, colorful, highly detailed, best quality, uhd";
|
||||
|
||||
const PROMPT = 'a moon in the night sky';
|
||||
const PROMPT = "a moon in the night sky";
|
||||
|
||||
const defaults = {
|
||||
model_name: 'runwayml/stable-diffusion-v1-5',
|
||||
image_size: 'square',
|
||||
model_name: "runwayml/stable-diffusion-v1-5",
|
||||
image_size: "square",
|
||||
num_inference_steps: 4,
|
||||
seed: 6252023,
|
||||
};
|
||||
@ -28,17 +28,17 @@ export default function RealtimePage() {
|
||||
const outputCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
const { send } = fal.realtime.connect(
|
||||
'fal-ai/fast-lcm-diffusion/image-to-image',
|
||||
"fal-ai/fast-lcm-diffusion/image-to-image",
|
||||
{
|
||||
connectionKey: 'realtime-demo',
|
||||
connectionKey: "realtime-demo",
|
||||
throttleInterval: 128,
|
||||
onResult(result) {
|
||||
if (result.images && result.images[0] && result.images[0].content) {
|
||||
const canvas = outputCanvasRef.current;
|
||||
const context = canvas?.getContext('2d');
|
||||
const context = canvas?.getContext("2d");
|
||||
if (canvas && context) {
|
||||
const imageBytes: Uint8Array = result.images[0].content;
|
||||
const blob = new Blob([imageBytes], { type: 'image/png' });
|
||||
const blob = new Blob([imageBytes], { type: "image/png" });
|
||||
createImageBitmap(blob)
|
||||
.then((bitmap) => {
|
||||
context.drawImage(bitmap, 0, 0);
|
||||
@ -47,7 +47,7 @@ export default function RealtimePage() {
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const handlePromptChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -63,18 +63,18 @@ export default function RealtimePage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-900 text-neutral-50">
|
||||
<main className="container flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-mono mb-8 text-neutral-50">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10">
|
||||
<h1 className="mb-8 font-mono text-4xl text-neutral-50">
|
||||
fal<code className="font-light text-pink-600">realtime</code>
|
||||
</h1>
|
||||
<div className="w-full max-w-full text-neutral-400">
|
||||
<input
|
||||
className="italic text-xl px-3 py-2 border border-white/10 rounded-md bg-white/5 w-full"
|
||||
className="w-full rounded-md border border-white/10 bg-white/5 px-3 py-2 text-xl italic"
|
||||
value={prompt}
|
||||
onChange={handlePromptChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row space-x-4">
|
||||
<div className="flex flex-col space-x-4 md:flex-row">
|
||||
<div className="flex-1">
|
||||
<DrawingCanvas
|
||||
onCanvasChange={({ imageData }) => {
|
||||
@ -90,7 +90,7 @@ export default function RealtimePage() {
|
||||
<div className="flex-1">
|
||||
<div>
|
||||
<canvas
|
||||
className="w-[512px] h-[512px]"
|
||||
className="h-[512px] w-[512px]"
|
||||
width="512"
|
||||
height="512"
|
||||
ref={outputCanvasRef}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useState } from "react";
|
||||
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy',
|
||||
proxyUrl: "/api/fal/proxy",
|
||||
});
|
||||
|
||||
type LlavaInput = {
|
||||
@ -25,59 +25,59 @@ type LlavaOutput = {
|
||||
};
|
||||
|
||||
export default function StreamingDemo() {
|
||||
const [answer, setAnswer] = useState<string>('');
|
||||
const [streamStatus, setStreamStatus] = useState<string>('idle');
|
||||
const [answer, setAnswer] = useState<string>("");
|
||||
const [streamStatus, setStreamStatus] = useState<string>("idle");
|
||||
|
||||
const runInference = async () => {
|
||||
const stream = await fal.stream<LlavaInput, LlavaOutput>(
|
||||
'fal-ai/llavav15-13b',
|
||||
"fal-ai/llavav15-13b",
|
||||
{
|
||||
input: {
|
||||
prompt:
|
||||
'Do you know who drew this picture and what is the name of it?',
|
||||
image_url: 'https://llava-vl.github.io/static/images/monalisa.jpg',
|
||||
"Do you know who drew this picture and what is the name of it?",
|
||||
image_url: "https://llava-vl.github.io/static/images/monalisa.jpg",
|
||||
max_new_tokens: 100,
|
||||
temperature: 0.2,
|
||||
top_p: 1,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
setStreamStatus('running');
|
||||
setStreamStatus("running");
|
||||
|
||||
for await (const partial of stream) {
|
||||
setAnswer(partial.output);
|
||||
}
|
||||
|
||||
const result = await stream.done();
|
||||
setStreamStatus('done');
|
||||
setStreamStatus("done");
|
||||
setAnswer(result.output);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
Hello <code className="text-pink-600">fal</code> +{' '}
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">
|
||||
Hello <code className="text-pink-600">fal</code> +{" "}
|
||||
<code className="text-indigo-500">streaming</code>
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
<button
|
||||
onClick={runInference}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline disabled:opacity-70"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none disabled:opacity-70"
|
||||
>
|
||||
Run inference
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Answer</h2>
|
||||
<span>
|
||||
streaming: <code className="font-semibold">{streamStatus}</code>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-lg p-4 border min-h-[12rem] border-gray-300 bg-gray-200 dark:bg-gray-800 dark:border-gray-700 rounded">
|
||||
<p className="min-h-[12rem] rounded border border-gray-300 bg-gray-200 p-4 text-lg dark:border-gray-700 dark:bg-gray-800">
|
||||
{answer}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
fal.config({
|
||||
// credentials: 'FAL_KEY_ID:FAL_KEY_SECRET',
|
||||
proxyUrl: '/api/fal/proxy',
|
||||
proxyUrl: "/api/fal/proxy",
|
||||
});
|
||||
|
||||
type ErrorProps = {
|
||||
@ -18,7 +18,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
@ -33,7 +33,7 @@ type RecorderOptions = {
|
||||
function useMediaRecorder({ maxDuration = 10000 }: RecorderOptions = {}) {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
const record = useCallback(async () => {
|
||||
@ -44,16 +44,16 @@ function useMediaRecorder({ maxDuration = 10000 }: RecorderOptions = {}) {
|
||||
setMediaRecorder(recorder);
|
||||
return new Promise<File>((resolve, reject) => {
|
||||
try {
|
||||
recorder.addEventListener('dataavailable', (event) => {
|
||||
recorder.addEventListener("dataavailable", (event) => {
|
||||
audioChunks.push(event.data);
|
||||
});
|
||||
recorder.addEventListener('stop', async () => {
|
||||
const fileOptions = { type: 'audio/wav' };
|
||||
recorder.addEventListener("stop", async () => {
|
||||
const fileOptions = { type: "audio/wav" };
|
||||
const audioBlob = new Blob(audioChunks, fileOptions);
|
||||
const audioFile = new File(
|
||||
[audioBlob],
|
||||
`recording_${Date.now()}.wav`,
|
||||
fileOptions
|
||||
fileOptions,
|
||||
);
|
||||
setIsRecording(false);
|
||||
resolve(audioFile);
|
||||
@ -108,17 +108,17 @@ export default function WhisperDemo() {
|
||||
setLoading(true);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result = await fal.subscribe('fal-ai/whisper', {
|
||||
const result = await fal.subscribe("fal-ai/whisper", {
|
||||
input: {
|
||||
file_name: 'recording.wav',
|
||||
file_name: "recording.wav",
|
||||
audio_url: audioFile,
|
||||
},
|
||||
logs: true,
|
||||
onQueueUpdate(update) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
update.status === "IN_PROGRESS" ||
|
||||
update.status === "COMPLETED"
|
||||
) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
@ -133,10 +133,10 @@ export default function WhisperDemo() {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
Hello <code className="text-pink-600">fal</code> and{' '}
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">
|
||||
Hello <code className="text-pink-600">fal</code> and{" "}
|
||||
<code className="text-indigo-500">whisper</code>
|
||||
</h1>
|
||||
|
||||
@ -155,10 +155,10 @@ export default function WhisperDemo() {
|
||||
setError(e);
|
||||
}
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline disabled:opacity-70"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none disabled:opacity-70"
|
||||
disabled={loading}
|
||||
>
|
||||
{isRecording ? 'Stop Recording' : 'Record'}
|
||||
{isRecording ? "Stop Recording" : "Record"}
|
||||
</button>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
@ -171,38 +171,38 @@ export default function WhisperDemo() {
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline disabled:opacity-70"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none disabled:opacity-70"
|
||||
disabled={loading || !audioFile}
|
||||
>
|
||||
{loading ? 'Transcribing...' : 'Transcribe'}
|
||||
{loading ? "Transcribing..." : "Transcribe"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{audioFileLocalUrl && (
|
||||
<div className="text-lg w-full my-2">
|
||||
<div className="my-2 w-full text-lg">
|
||||
<audio className="mx-auto" controls src={audioFileLocalUrl} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
<p className="text-current/80 text-sm">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-96 rounded whitespace-pre overflow-auto w-full">
|
||||
<pre className="h-96 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
: "// result pending..."}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.filter(Boolean).join('\n')}
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{logs.filter(Boolean).join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { type Excalidraw } from '@excalidraw/excalidraw';
|
||||
import { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types';
|
||||
import { type Excalidraw } from "@excalidraw/excalidraw";
|
||||
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import {
|
||||
AppState,
|
||||
ExcalidrawImperativeAPI,
|
||||
} from '@excalidraw/excalidraw/types/types';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import initialDrawing from './drawingState.json';
|
||||
} from "@excalidraw/excalidraw/types/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import initialDrawing from "./drawingState.json";
|
||||
|
||||
export type CanvasChangeEvent = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
@ -22,7 +22,7 @@ export async function blobToBase64(blob: Blob): Promise<string> {
|
||||
reader.readAsDataURL(blob);
|
||||
return new Promise<string>((resolve) => {
|
||||
reader.onloadend = () => {
|
||||
resolve(reader.result?.toString() || '');
|
||||
resolve(reader.result?.toString() || "");
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -41,17 +41,17 @@ export function DrawingCanvas({ onCanvasChange }: DrawingCanvasProps) {
|
||||
const [sceneData, setSceneData] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
import('@excalidraw/excalidraw').then((comp) =>
|
||||
setExcalidrawComponent(comp.Excalidraw)
|
||||
import("@excalidraw/excalidraw").then((comp) =>
|
||||
setExcalidrawComponent(comp.Excalidraw),
|
||||
);
|
||||
const onResize = () => {
|
||||
if (excalidrawAPI) {
|
||||
excalidrawAPI.refresh();
|
||||
}
|
||||
};
|
||||
window.addEventListener('resize', onResize);
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -61,17 +61,17 @@ export function DrawingCanvas({ onCanvasChange }: DrawingCanvasProps) {
|
||||
return;
|
||||
}
|
||||
const { exportToBlob, convertToExcalidrawElements, serializeAsJSON } =
|
||||
await import('@excalidraw/excalidraw');
|
||||
await import("@excalidraw/excalidraw");
|
||||
|
||||
const [boundingBoxElement] = convertToExcalidrawElements([
|
||||
{
|
||||
type: 'rectangle',
|
||||
type: "rectangle",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 512,
|
||||
height: 512,
|
||||
fillStyle: 'solid',
|
||||
backgroundColor: 'cyan',
|
||||
fillStyle: "solid",
|
||||
backgroundColor: "cyan",
|
||||
},
|
||||
]);
|
||||
|
||||
@ -79,7 +79,7 @@ export function DrawingCanvas({ onCanvasChange }: DrawingCanvasProps) {
|
||||
elements,
|
||||
appState,
|
||||
excalidrawAPI.getFiles(),
|
||||
'local'
|
||||
"local",
|
||||
);
|
||||
if (newSceneData !== sceneData) {
|
||||
setSceneData(newSceneData);
|
||||
@ -93,7 +93,7 @@ export function DrawingCanvas({ onCanvasChange }: DrawingCanvasProps) {
|
||||
},
|
||||
},
|
||||
files: excalidrawAPI.getFiles(),
|
||||
mimeType: 'image/webp',
|
||||
mimeType: "image/webp",
|
||||
quality: 0.5,
|
||||
exportPadding: 0,
|
||||
getDimensions: () => {
|
||||
@ -104,11 +104,11 @@ export function DrawingCanvas({ onCanvasChange }: DrawingCanvasProps) {
|
||||
onCanvasChange({ elements, appState, imageData });
|
||||
}
|
||||
},
|
||||
[excalidrawAPI, onCanvasChange, sceneData]
|
||||
[excalidrawAPI, onCanvasChange, sceneData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '560px', width: '560px' }}>
|
||||
<div style={{ height: "560px", width: "560px" }}>
|
||||
{ExcalidrawComponent && (
|
||||
<ExcalidrawComponent
|
||||
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||
|
||||
2
apps/demo-nextjs-app-router/index.d.ts
vendored
2
apps/demo-nextjs-app-router/index.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module '*.svg' {
|
||||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export const ReactComponent: any;
|
||||
export default content;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'demo-nextjs-app-router',
|
||||
preset: '../../jest.preset.js',
|
||||
displayName: "demo-nextjs-app-router",
|
||||
preset: "../../jest.preset.js",
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
||||
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nx/react/plugins/jest",
|
||||
"^.+\\.[tj]sx?$": ["babel-jest", { presets: ["@nx/next/babel"] }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/demo-nextjs-app-router',
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
|
||||
coverageDirectory: "../../coverage/apps/demo-nextjs-app-router",
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//@ts-check
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { composePlugins, withNx } = require('@nx/next');
|
||||
const { composePlugins, withNx } = require("@nx/next");
|
||||
|
||||
/**
|
||||
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const { join } = require('path');
|
||||
const { join } = require("path");
|
||||
|
||||
// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
|
||||
// option from your application's configuration (i.e. project.json).
|
||||
@ -8,7 +8,7 @@ const { join } = require('path');
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
config: join(__dirname, "tailwind.config.js"),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||
const { join } = require('path');
|
||||
const { createGlobPatternsForDependencies } = require("@nx/react/tailwind");
|
||||
const { join } = require("path");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
join(
|
||||
__dirname,
|
||||
'{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}'
|
||||
"{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}",
|
||||
),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
darkMode: 'class',
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@ -13,10 +13,10 @@
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next",
|
||||
},
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"types": ["jest", "node"],
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
@ -25,12 +25,12 @@
|
||||
"**/*.jsx",
|
||||
"../../apps/demo-nextjs-app-router/.next/types/**/*.ts",
|
||||
"../../dist/apps/demo-nextjs-app-router/.next/types/**/*.ts",
|
||||
"next-env.d.ts",
|
||||
"next-env.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jest.config.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts",
|
||||
],
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
|
||||
2
apps/demo-nextjs-page-router/index.d.ts
vendored
2
apps/demo-nextjs-page-router/index.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare module '*.svg' {
|
||||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export const ReactComponent: any;
|
||||
export default content;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'demo-nextjs-page-router',
|
||||
preset: '../../jest.preset.js',
|
||||
displayName: "demo-nextjs-page-router",
|
||||
preset: "../../jest.preset.js",
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }],
|
||||
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nx/react/plugins/jest",
|
||||
"^.+\\.[tj]sx?$": ["babel-jest", { presets: ["@nx/next/babel"] }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/apps/demo-nextjs-page-router',
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
|
||||
coverageDirectory: "../../coverage/apps/demo-nextjs-page-router",
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//@ts-check
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { withNx } = require('@nx/next/plugins/with-nx');
|
||||
const { withNx } = require("@nx/next/plugins/with-nx");
|
||||
|
||||
/**
|
||||
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import './styles.css';
|
||||
import { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import "./styles.css";
|
||||
|
||||
function CustomApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
|
||||
@ -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/serverless-proxy/nextjs";
|
||||
// @snippet:end
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
// @snippet:start(client.config)
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy', // the built-int nextjs proxy
|
||||
proxyUrl: "/api/fal/proxy", // the built-int nextjs proxy
|
||||
// proxyUrl: 'http://localhost:3333/api/fal/proxy', // or your own external proxy
|
||||
});
|
||||
// @snippet:end
|
||||
@ -29,7 +29,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="p-4 mb-4 text-sm text-red-800 rounded bg-red-50 dark:bg-gray-800 dark:text-red-400"
|
||||
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
<span className="font-medium">Error</span> {props.error.message}
|
||||
@ -38,7 +38,7 @@ function Error(props: ErrorProps) {
|
||||
}
|
||||
|
||||
const DEFAULT_PROMPT =
|
||||
'a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd';
|
||||
"a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd";
|
||||
|
||||
export function Index() {
|
||||
// @snippet:start("client.ui.state")
|
||||
@ -72,18 +72,18 @@ export function Index() {
|
||||
setLoading(true);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result: Result = await fal.subscribe('fal-ai/lora', {
|
||||
const result: Result = await fal.subscribe("fal-ai/lora", {
|
||||
input: {
|
||||
prompt,
|
||||
model_name: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||
image_size: 'square_hd',
|
||||
model_name: "stabilityai/stable-diffusion-xl-base-1.0",
|
||||
image_size: "square_hd",
|
||||
},
|
||||
logs: true,
|
||||
onQueueUpdate(update) {
|
||||
setElapsedTime(Date.now() - start);
|
||||
if (
|
||||
update.status === 'IN_PROGRESS' ||
|
||||
update.status === 'COMPLETED'
|
||||
update.status === "IN_PROGRESS" ||
|
||||
update.status === "COMPLETED"
|
||||
) {
|
||||
setLogs((update.logs || []).map((log) => log.message));
|
||||
}
|
||||
@ -99,17 +99,17 @@ export function Index() {
|
||||
// @snippet:end
|
||||
};
|
||||
return (
|
||||
<div className="min-h-screen dark:bg-gray-900 bg-gray-100">
|
||||
<main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
|
||||
<h1 className="text-4xl font-bold mb-8">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
||||
<h1 className="mb-8 text-4xl font-bold">
|
||||
Hello <code className="font-light text-pink-600">fal</code>
|
||||
</h1>
|
||||
<div className="text-lg w-full">
|
||||
<label htmlFor="prompt" className="block mb-2 text-current">
|
||||
<div className="w-full text-lg">
|
||||
<label htmlFor="prompt" className="mb-2 block text-current">
|
||||
Prompt
|
||||
</label>
|
||||
<input
|
||||
className="w-full text-lg p-2 rounded bg-black/10 dark:bg-white/5 border border-black/20 dark:border-white/10"
|
||||
className="w-full rounded border border-black/20 bg-black/10 p-2 text-lg dark:border-white/10 dark:bg-white/5"
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="Imagine..."
|
||||
@ -125,15 +125,15 @@ export function Index() {
|
||||
e.preventDefault();
|
||||
generateImage();
|
||||
}}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-lg py-3 px-6 mx-auto rounded focus:outline-none focus:shadow-outline"
|
||||
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Generating...' : 'Generate Image'}
|
||||
{loading ? "Generating..." : "Generate Image"}
|
||||
</button>
|
||||
|
||||
<Error error={error} />
|
||||
|
||||
<div className="w-full flex flex-col space-y-4">
|
||||
<div className="flex w-full flex-col space-y-4">
|
||||
<div className="mx-auto">
|
||||
{image && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@ -142,20 +142,20 @@ export function Index() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">JSON Result</h3>
|
||||
<p className="text-sm text-current/80">
|
||||
<p className="text-current/80 text-sm">
|
||||
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
||||
</p>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{result
|
||||
? JSON.stringify(result, null, 2)
|
||||
: '// result pending...'}
|
||||
: "// result pending..."}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-light">Logs</h3>
|
||||
<pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
|
||||
{logs.filter(Boolean).join('\n')}
|
||||
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
||||
{logs.filter(Boolean).join("\n")}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const { join } = require('path');
|
||||
const { join } = require("path");
|
||||
|
||||
// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
|
||||
// option from your application's configuration (i.e. project.json).
|
||||
@ -8,7 +8,7 @@ const { join } = require('path');
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {
|
||||
config: join(__dirname, 'tailwind.config.js'),
|
||||
config: join(__dirname, "tailwind.config.js"),
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import '@inrupt/jest-jsdom-polyfills';
|
||||
import "@inrupt/jest-jsdom-polyfills";
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
import Index from '../pages/index';
|
||||
import Index from "../pages/index";
|
||||
|
||||
describe('Index', () => {
|
||||
xit('should render successfully', () => {
|
||||
describe("Index", () => {
|
||||
xit("should render successfully", () => {
|
||||
const { baseElement } = render(<Index />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
||||
const { join } = require('path');
|
||||
const { createGlobPatternsForDependencies } = require("@nx/react/tailwind");
|
||||
const { join } = require("path");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
join(
|
||||
__dirname,
|
||||
'{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}'
|
||||
"{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}",
|
||||
),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
darkMode: 'class',
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"types": ["jest", "node"],
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts"],
|
||||
"exclude": ["node_modules", "jest.config.ts"],
|
||||
"exclude": ["node_modules", "jest.config.ts"]
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { getJestProjects } from '@nx/jest';
|
||||
import { getJestProjects } from "@nx/jest";
|
||||
|
||||
export default {
|
||||
projects: getJestProjects(),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const nxPreset = require('@nx/jest/preset').default;
|
||||
const nxPreset = require("@nx/jest/preset").default;
|
||||
|
||||
module.exports = {
|
||||
...nxPreset,
|
||||
|
||||
@ -11,12 +11,12 @@ The `fal.ai` JavaScript Client Library provides a seamless way to interact with
|
||||
Before diving into the client-specific features, ensure you've set up your credentials:
|
||||
|
||||
```ts
|
||||
import * as fal from '@fal-ai/serverless-client';
|
||||
import * as fal from "@fal-ai/serverless-client";
|
||||
|
||||
fal.config({
|
||||
// Can also be auto-configured using environment variables:
|
||||
// Either a single FAL_KEY or a combination of FAL_KEY_ID and FAL_KEY_SECRET
|
||||
credentials: 'FAL_KEY_ID:FAL_KEY_SECRET',
|
||||
credentials: "FAL_KEY_ID:FAL_KEY_SECRET",
|
||||
});
|
||||
```
|
||||
|
||||
@ -27,8 +27,8 @@ fal.config({
|
||||
The `fal.run` method is the simplest way to execute a function. It returns a promise that resolves to the function's result:
|
||||
|
||||
```ts
|
||||
const result = await fal.run('my-function-id', {
|
||||
input: { foo: 'bar' },
|
||||
const result = await fal.run("my-function-id", {
|
||||
input: { foo: "bar" },
|
||||
});
|
||||
```
|
||||
|
||||
@ -37,10 +37,10 @@ const result = await fal.run('my-function-id', {
|
||||
The `fal.subscribe` method offers a powerful way to rely on the [queue system](https://www.fal.ai/docs/function-endpoints/queue) to execute long-running functions. It returns the result once it's done like any other async function, so your don't have to deal with queue status updates yourself. However, it does support queue events, in case you want to listen and react to them:
|
||||
|
||||
```ts
|
||||
const result = await fal.subscribe('my-function-id', {
|
||||
input: { foo: 'bar' },
|
||||
const result = await fal.subscribe("my-function-id", {
|
||||
input: { foo: "bar" },
|
||||
onQueueUpdate(update) {
|
||||
if (update.status === 'IN_QUEUE') {
|
||||
if (update.status === "IN_QUEUE") {
|
||||
console.log(`Your position in the queue is ${update.position}`);
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'client',
|
||||
preset: '../../jest.preset.js',
|
||||
displayName: "client",
|
||||
preset: "../../jest.preset.js",
|
||||
globals: {},
|
||||
testEnvironment: 'node',
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'ts-jest',
|
||||
"^.+\\.[tj]sx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
tsconfig: "<rootDir>/tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/client',
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
|
||||
coverageDirectory: "../../coverage/libs/client",
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@fal-ai/serverless-client",
|
||||
"description": "The fal serverless JS/TS client",
|
||||
"version": "0.14.1-alpha.3",
|
||||
"version": "0.14.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -18,8 +18,7 @@
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"robot3": "^0.4.1",
|
||||
"uuid-random": "^1.3.2"
|
||||
"robot3": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { getRestApiUrl } from './config';
|
||||
import { dispatchRequest } from './request';
|
||||
import { parseAppId } from './utils';
|
||||
import { getRestApiUrl } from "./config";
|
||||
import { dispatchRequest } from "./request";
|
||||
import { parseAppId } from "./utils";
|
||||
|
||||
export const TOKEN_EXPIRATION_SECONDS = 120;
|
||||
|
||||
@ -10,17 +10,17 @@ export const TOKEN_EXPIRATION_SECONDS = 120;
|
||||
export async function getTemporaryAuthToken(app: string): Promise<string> {
|
||||
const appId = parseAppId(app);
|
||||
const token: string | object = await dispatchRequest<any, string>(
|
||||
'POST',
|
||||
"POST",
|
||||
`${getRestApiUrl()}/tokens/`,
|
||||
{
|
||||
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']) {
|
||||
return token['detail'];
|
||||
if (typeof token !== "string" && token["detail"]) {
|
||||
return token["detail"];
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { config, getConfig } from './config';
|
||||
import { config, getConfig } from "./config";
|
||||
|
||||
describe('The config test suite', () => {
|
||||
it('should set the config variables accordingly', () => {
|
||||
describe("The config test suite", () => {
|
||||
it("should set the config variables accordingly", () => {
|
||||
const newConfig = {
|
||||
credentials: 'key-id:key-secret',
|
||||
credentials: "key-id:key-secret",
|
||||
};
|
||||
config(newConfig);
|
||||
const currentConfig = getConfig();
|
||||
|
||||
@ -2,18 +2,18 @@ import {
|
||||
withMiddleware,
|
||||
withProxy,
|
||||
type RequestMiddleware,
|
||||
} from './middleware';
|
||||
import type { ResponseHandler } from './response';
|
||||
import { defaultResponseHandler } from './response';
|
||||
} from "./middleware";
|
||||
import type { ResponseHandler } from "./response";
|
||||
import { defaultResponseHandler } from "./response";
|
||||
|
||||
export type CredentialsResolver = () => string | undefined;
|
||||
|
||||
type FetchType = typeof fetch;
|
||||
|
||||
export function resolveDefaultFetch(): FetchType {
|
||||
if (typeof fetch === 'undefined') {
|
||||
if (typeof fetch === "undefined") {
|
||||
throw new Error(
|
||||
'Your environment does not support fetch. Please provide your own fetch implementation.'
|
||||
"Your environment does not support fetch. Please provide your own fetch implementation.",
|
||||
);
|
||||
}
|
||||
return fetch;
|
||||
@ -70,11 +70,11 @@ export type RequiredConfig = Required<Config>;
|
||||
*/
|
||||
function hasEnvVariables(): boolean {
|
||||
return (
|
||||
typeof process !== 'undefined' &&
|
||||
typeof process !== "undefined" &&
|
||||
process.env &&
|
||||
(typeof process.env.FAL_KEY !== 'undefined' ||
|
||||
(typeof process.env.FAL_KEY_ID !== 'undefined' &&
|
||||
typeof process.env.FAL_KEY_SECRET !== 'undefined'))
|
||||
(typeof process.env.FAL_KEY !== "undefined" ||
|
||||
(typeof process.env.FAL_KEY_ID !== "undefined" &&
|
||||
typeof process.env.FAL_KEY_SECRET !== "undefined"))
|
||||
);
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export const credentialsFromEnv: CredentialsResolver = () => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof process.env.FAL_KEY !== 'undefined') {
|
||||
if (typeof process.env.FAL_KEY !== "undefined") {
|
||||
return process.env.FAL_KEY;
|
||||
}
|
||||
|
||||
@ -115,19 +115,19 @@ export function config(config: Config) {
|
||||
...configuration,
|
||||
requestMiddleware: withMiddleware(
|
||||
withProxy({ targetUrl: config.proxyUrl }),
|
||||
configuration.requestMiddleware
|
||||
configuration.requestMiddleware,
|
||||
),
|
||||
};
|
||||
}
|
||||
const { credentials, suppressLocalCredentialsWarning } = configuration;
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window !== "undefined" &&
|
||||
credentials &&
|
||||
!suppressLocalCredentialsWarning
|
||||
) {
|
||||
console.warn(
|
||||
"The fal credentials are exposed in the browser's environment. " +
|
||||
"That's not recommended for production use cases."
|
||||
"That's not recommended for production use cases.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -139,7 +139,7 @@ export function config(config: Config) {
|
||||
*/
|
||||
export function getConfig(): RequiredConfig {
|
||||
if (!configuration) {
|
||||
console.info('Using default configuration for the fal client');
|
||||
console.info("Using default configuration for the fal client");
|
||||
return {
|
||||
...DEFAULT_CONFIG,
|
||||
fetch: resolveDefaultFetch(),
|
||||
@ -152,5 +152,5 @@ export function getConfig(): RequiredConfig {
|
||||
* @returns the URL of the fal serverless rest api endpoint.
|
||||
*/
|
||||
export function getRestApiUrl(): string {
|
||||
return 'https://rest.alpha.fal.ai';
|
||||
return "https://rest.alpha.fal.ai";
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { buildUrl } from './function';
|
||||
import { buildUrl } from "./function";
|
||||
|
||||
describe('The function test suite', () => {
|
||||
it('should build the URL with a function username/app-alias', () => {
|
||||
const alias = 'fal-ai/text-to-image';
|
||||
describe("The function test suite", () => {
|
||||
it("should build the URL with a function username/app-alias", () => {
|
||||
const alias = "fal-ai/text-to-image";
|
||||
const url = buildUrl(alias);
|
||||
expect(url).toMatch(`fal.run/${alias}`);
|
||||
});
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { dispatchRequest } from './request';
|
||||
import { storageImpl } from './storage';
|
||||
import { FalStream, StreamingConnectionMode } from './streaming';
|
||||
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';
|
||||
} from "./types";
|
||||
import { ensureAppIdFormat, isValidUrl, parseAppId } from "./utils";
|
||||
|
||||
/**
|
||||
* The function input and other configuration when running
|
||||
@ -29,7 +29,7 @@ type RunOptions<Input> = {
|
||||
/**
|
||||
* The HTTP method, defaults to `post`;
|
||||
*/
|
||||
readonly method?: 'get' | 'post' | 'put' | 'delete' | string;
|
||||
readonly method?: "get" | "post" | "put" | "delete" | string;
|
||||
|
||||
/**
|
||||
* If `true`, the function will automatically upload any files
|
||||
@ -65,50 +65,50 @@ type ExtraOptions = {
|
||||
*/
|
||||
export function buildUrl<Input>(
|
||||
id: string,
|
||||
options: RunOptions<Input> & ExtraOptions = {}
|
||||
options: RunOptions<Input> & ExtraOptions = {},
|
||||
): string {
|
||||
const method = (options.method ?? 'post').toLowerCase();
|
||||
const path = (options.path ?? '').replace(/^\//, '').replace(/\/{2,}/, '/');
|
||||
const method = (options.method ?? "post").toLowerCase();
|
||||
const path = (options.path ?? "").replace(/^\//, "").replace(/\/{2,}/, "/");
|
||||
const input = options.input;
|
||||
const params = {
|
||||
...(options.query || {}),
|
||||
...(method === 'get' ? input : {}),
|
||||
...(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}/`;
|
||||
const url = id.endsWith("/") ? id : `${id}/`;
|
||||
return `${url}${path}${queryParams}`;
|
||||
}
|
||||
|
||||
const appId = ensureAppIdFormat(id);
|
||||
const subdomain = options.subdomain ? `${options.subdomain}.` : '';
|
||||
const subdomain = options.subdomain ? `${options.subdomain}.` : "";
|
||||
const url = `https://${subdomain}fal.run/${appId}/${path}`;
|
||||
return `${url.replace(/\/$/, '')}${queryParams}`;
|
||||
return `${url.replace(/\/$/, "")}${queryParams}`;
|
||||
}
|
||||
|
||||
export async function send<Input, Output>(
|
||||
id: string,
|
||||
options: RunOptions<Input> & ExtraOptions = {}
|
||||
options: RunOptions<Input> & ExtraOptions = {},
|
||||
): Promise<Output> {
|
||||
const input =
|
||||
options.input && options.autoUpload !== false
|
||||
? await storageImpl.transformInput(options.input)
|
||||
: options.input;
|
||||
return dispatchRequest<Input, Output>(
|
||||
options.method ?? 'post',
|
||||
options.method ?? "post",
|
||||
buildUrl(id, options),
|
||||
input as Input
|
||||
input as Input,
|
||||
);
|
||||
}
|
||||
|
||||
export type QueueStatusSubscriptionOptions = QueueStatusOptions &
|
||||
Omit<QueueSubscribeOptions, 'onEnqueue' | 'webhookUrl'>;
|
||||
Omit<QueueSubscribeOptions, "onEnqueue" | "webhookUrl">;
|
||||
|
||||
/**
|
||||
* Runs a fal serverless function identified by its `id`.
|
||||
@ -118,7 +118,7 @@ export type QueueStatusSubscriptionOptions = QueueStatusOptions &
|
||||
*/
|
||||
export async function run<Input, Output>(
|
||||
id: string,
|
||||
options: RunOptions<Input> = {}
|
||||
options: RunOptions<Input> = {},
|
||||
): Promise<Output> {
|
||||
return send(id, options);
|
||||
}
|
||||
@ -140,7 +140,7 @@ type QueueSubscribeOptions = {
|
||||
*
|
||||
* @see pollInterval
|
||||
*/
|
||||
mode?: 'polling' | 'streaming';
|
||||
mode?: "polling" | "streaming";
|
||||
|
||||
/**
|
||||
* Callback function that is called when a request is enqueued.
|
||||
@ -180,7 +180,7 @@ type QueueSubscribeOptions = {
|
||||
webhookUrl?: string;
|
||||
} & (
|
||||
| {
|
||||
mode?: 'polling';
|
||||
mode?: "polling";
|
||||
/**
|
||||
* The interval (in milliseconds) at which to poll for updates.
|
||||
* If not provided, a default value of `500` will be used.
|
||||
@ -190,7 +190,7 @@ type QueueSubscribeOptions = {
|
||||
pollInterval?: number;
|
||||
}
|
||||
| {
|
||||
mode: 'streaming';
|
||||
mode: "streaming";
|
||||
|
||||
/**
|
||||
* The connection mode to use for streaming updates. It defaults to `server`.
|
||||
@ -248,7 +248,7 @@ interface Queue {
|
||||
*/
|
||||
submit<Input>(
|
||||
endpointId: string,
|
||||
options: SubmitOptions<Input>
|
||||
options: SubmitOptions<Input>,
|
||||
): Promise<EnqueueResult>;
|
||||
|
||||
/**
|
||||
@ -269,7 +269,7 @@ interface Queue {
|
||||
*/
|
||||
streamStatus(
|
||||
endpointId: string,
|
||||
options: QueueStatusStreamOptions
|
||||
options: QueueStatusStreamOptions,
|
||||
): Promise<FalStream<unknown, QueueStatus>>;
|
||||
|
||||
/**
|
||||
@ -282,7 +282,7 @@ interface Queue {
|
||||
*/
|
||||
subscribeToStatus(
|
||||
endpointId: string,
|
||||
options: QueueStatusSubscriptionOptions
|
||||
options: QueueStatusSubscriptionOptions,
|
||||
): Promise<CompletedQueueStatus>;
|
||||
|
||||
/**
|
||||
@ -294,7 +294,7 @@ interface Queue {
|
||||
*/
|
||||
result<Output>(
|
||||
endpointId: string,
|
||||
options: BaseQueueOptions
|
||||
options: BaseQueueOptions,
|
||||
): Promise<Output>;
|
||||
|
||||
/**
|
||||
@ -317,53 +317,53 @@ interface Queue {
|
||||
export const queue: Queue = {
|
||||
async submit<Input>(
|
||||
endpointId: string,
|
||||
options: SubmitOptions<Input>
|
||||
options: SubmitOptions<Input>,
|
||||
): Promise<EnqueueResult> {
|
||||
const { webhookUrl, path = '', ...runOptions } = options;
|
||||
const { webhookUrl, path = "", ...runOptions } = options;
|
||||
return send(endpointId, {
|
||||
...runOptions,
|
||||
subdomain: 'queue',
|
||||
method: 'post',
|
||||
subdomain: "queue",
|
||||
method: "post",
|
||||
path: path,
|
||||
query: webhookUrl ? { fal_webhook: webhookUrl } : undefined,
|
||||
});
|
||||
},
|
||||
async status(
|
||||
endpointId: string,
|
||||
{ requestId, logs = false }: QueueStatusOptions
|
||||
{ requestId, logs = false }: QueueStatusOptions,
|
||||
): Promise<QueueStatus> {
|
||||
const appId = parseAppId(endpointId);
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : '';
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : "";
|
||||
return send(`${prefix}${appId.owner}/${appId.alias}`, {
|
||||
subdomain: 'queue',
|
||||
method: 'get',
|
||||
subdomain: "queue",
|
||||
method: "get",
|
||||
path: `/requests/${requestId}/status`,
|
||||
input: {
|
||||
logs: logs ? '1' : '0',
|
||||
logs: logs ? "1" : "0",
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async streamStatus(
|
||||
endpointId: string,
|
||||
{ requestId, logs = false, connectionMode }: QueueStatusStreamOptions
|
||||
{ requestId, logs = false, connectionMode }: QueueStatusStreamOptions,
|
||||
): Promise<FalStream<unknown, QueueStatus>> {
|
||||
const appId = parseAppId(endpointId);
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : '';
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : "";
|
||||
|
||||
const queryParams = {
|
||||
logs: logs ? '1' : '0',
|
||||
logs: logs ? "1" : "0",
|
||||
};
|
||||
|
||||
const url = buildUrl(`${prefix}${appId.owner}/${appId.alias}`, {
|
||||
subdomain: 'queue',
|
||||
subdomain: "queue",
|
||||
path: `/requests/${requestId}/status/stream`,
|
||||
query: queryParams,
|
||||
});
|
||||
|
||||
return new FalStream<unknown, QueueStatus>(endpointId, {
|
||||
url,
|
||||
method: 'get',
|
||||
method: "get",
|
||||
connectionMode,
|
||||
queryParams,
|
||||
});
|
||||
@ -379,12 +379,12 @@ export const queue: Queue = {
|
||||
// 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') {
|
||||
if (options.mode === "streaming") {
|
||||
const status = await queue.streamStatus(endpointId, {
|
||||
requestId,
|
||||
logs: options.logs,
|
||||
connectionMode:
|
||||
'connectionMode' in options
|
||||
"connectionMode" in options
|
||||
? (options.connectionMode as StreamingConnectionMode)
|
||||
: undefined,
|
||||
});
|
||||
@ -398,21 +398,21 @@ export const queue: Queue = {
|
||||
// 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`
|
||||
`Client timed out waiting for the request to complete after ${timeout}ms`,
|
||||
);
|
||||
}, timeout);
|
||||
}
|
||||
status.on('data', (data: QueueStatus) => {
|
||||
status.on("data", (data: QueueStatus) => {
|
||||
if (options.onQueueUpdate) {
|
||||
// accumulate logs to match previous polling behavior
|
||||
if (
|
||||
'logs' in data &&
|
||||
"logs" in data &&
|
||||
Array.isArray(data.logs) &&
|
||||
data.logs.length > 0
|
||||
) {
|
||||
logs.push(...data.logs);
|
||||
}
|
||||
options.onQueueUpdate('logs' in data ? { ...data, logs } : data);
|
||||
options.onQueueUpdate("logs" in data ? { ...data, logs } : data);
|
||||
}
|
||||
});
|
||||
const doneStatus = await status.done();
|
||||
@ -427,8 +427,8 @@ export const queue: Queue = {
|
||||
// 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
|
||||
"pollInterval" in options && typeof options.pollInterval === "number"
|
||||
? (options.pollInterval ?? DEFAULT_POLL_INTERVAL)
|
||||
: DEFAULT_POLL_INTERVAL;
|
||||
|
||||
const clearScheduledTasks = () => {
|
||||
@ -445,8 +445,8 @@ export const queue: Queue = {
|
||||
queue.cancel(endpointId, { requestId }).catch(handleCancelError);
|
||||
reject(
|
||||
new Error(
|
||||
`Client timed out waiting for the request to complete after ${timeout}ms`
|
||||
)
|
||||
`Client timed out waiting for the request to complete after ${timeout}ms`,
|
||||
),
|
||||
);
|
||||
}, timeout);
|
||||
}
|
||||
@ -459,7 +459,7 @@ export const queue: Queue = {
|
||||
if (options.onQueueUpdate) {
|
||||
options.onQueueUpdate(requestStatus);
|
||||
}
|
||||
if (requestStatus.status === 'COMPLETED') {
|
||||
if (requestStatus.status === "COMPLETED") {
|
||||
clearScheduledTasks();
|
||||
resolve(requestStatus);
|
||||
return;
|
||||
@ -476,26 +476,26 @@ export const queue: Queue = {
|
||||
|
||||
async result<Output>(
|
||||
endpointId: string,
|
||||
{ requestId }: BaseQueueOptions
|
||||
{ requestId }: BaseQueueOptions,
|
||||
): Promise<Output> {
|
||||
const appId = parseAppId(endpointId);
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : '';
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : "";
|
||||
return send(`${prefix}${appId.owner}/${appId.alias}`, {
|
||||
subdomain: 'queue',
|
||||
method: 'get',
|
||||
subdomain: "queue",
|
||||
method: "get",
|
||||
path: `/requests/${requestId}`,
|
||||
});
|
||||
},
|
||||
|
||||
async cancel(
|
||||
endpointId: string,
|
||||
{ requestId }: BaseQueueOptions
|
||||
{ requestId }: BaseQueueOptions,
|
||||
): Promise<void> {
|
||||
const appId = parseAppId(endpointId);
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : '';
|
||||
const prefix = appId.namespace ? `${appId.namespace}/` : "";
|
||||
await send(`${prefix}${appId.owner}/${appId.alias}`, {
|
||||
subdomain: 'queue',
|
||||
method: 'put',
|
||||
subdomain: "queue",
|
||||
method: "put",
|
||||
path: `/requests/${requestId}/cancel`,
|
||||
});
|
||||
},
|
||||
@ -510,7 +510,7 @@ export const queue: Queue = {
|
||||
*/
|
||||
export async function subscribe<Input, Output>(
|
||||
endpointId: string,
|
||||
options: RunOptions<Input> & QueueSubscribeOptions = {}
|
||||
options: RunOptions<Input> & QueueSubscribeOptions = {},
|
||||
): Promise<Output> {
|
||||
const { request_id: requestId } = await queue.submit(endpointId, options);
|
||||
if (options.onEnqueue) {
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
export { config, getConfig } from './config';
|
||||
export { queue, run, subscribe } from './function';
|
||||
export { withMiddleware, withProxy } from './middleware';
|
||||
export type { RequestMiddleware } from './middleware';
|
||||
export { realtimeImpl as realtime } from './realtime';
|
||||
export { ApiError, ValidationError } from './response';
|
||||
export type { ResponseHandler } from './response';
|
||||
export { storageImpl as storage } from './storage';
|
||||
export { stream } from './streaming';
|
||||
export { config, getConfig } from "./config";
|
||||
export { queue, run, subscribe } from "./function";
|
||||
export { withMiddleware, withProxy } from "./middleware";
|
||||
export type { RequestMiddleware } from "./middleware";
|
||||
export { realtimeImpl as realtime } from "./realtime";
|
||||
export { ApiError, ValidationError } from "./response";
|
||||
export type { ResponseHandler } from "./response";
|
||||
export { storageImpl as storage } from "./storage";
|
||||
export { stream } from "./streaming";
|
||||
export type {
|
||||
QueueStatus,
|
||||
ValidationErrorInfo,
|
||||
WebHookResponse,
|
||||
} from './types';
|
||||
export { parseAppId } from './utils';
|
||||
} from "./types";
|
||||
export { parseAppId } from "./utils";
|
||||
|
||||
@ -13,7 +13,7 @@ export type RequestConfig = {
|
||||
};
|
||||
|
||||
export type RequestMiddleware = (
|
||||
request: RequestConfig
|
||||
request: RequestConfig,
|
||||
) => Promise<RequestConfig>;
|
||||
|
||||
/**
|
||||
@ -29,7 +29,7 @@ export function withMiddleware(
|
||||
middlewares.reduce(
|
||||
(configPromise, middleware) =>
|
||||
configPromise.then((req) => middleware(req)),
|
||||
Promise.resolve(config)
|
||||
Promise.resolve(config),
|
||||
);
|
||||
}
|
||||
|
||||
@ -37,11 +37,11 @@ export type RequestProxyConfig = {
|
||||
targetUrl: string;
|
||||
};
|
||||
|
||||
export const TARGET_URL_HEADER = 'x-fal-target-url';
|
||||
export const TARGET_URL_HEADER = "x-fal-target-url";
|
||||
|
||||
export function withProxy(config: RequestProxyConfig): RequestMiddleware {
|
||||
// when running on the server, we don't need to proxy the request
|
||||
if (typeof window === 'undefined') {
|
||||
if (typeof window === "undefined") {
|
||||
return (requestConfig) => Promise.resolve(requestConfig);
|
||||
}
|
||||
return (requestConfig) =>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { decode, encode } from '@msgpack/msgpack';
|
||||
import { decode, encode } from "@msgpack/msgpack";
|
||||
import {
|
||||
ContextFunction,
|
||||
InterpretOnChangeFunction,
|
||||
@ -11,12 +11,11 @@ import {
|
||||
reduce,
|
||||
state,
|
||||
transition,
|
||||
} from 'robot3';
|
||||
import uuid from 'uuid-random';
|
||||
import { TOKEN_EXPIRATION_SECONDS, getTemporaryAuthToken } from './auth';
|
||||
import { ApiError } from './response';
|
||||
import { isBrowser } from './runtime';
|
||||
import { ensureAppIdFormat, isReact, throttle } from './utils';
|
||||
} from "robot3";
|
||||
import { TOKEN_EXPIRATION_SECONDS, getTemporaryAuthToken } from "./auth";
|
||||
import { ApiError } from "./response";
|
||||
import { isBrowser } from "./runtime";
|
||||
import { ensureAppIdFormat, isReact, throttle } from "./utils";
|
||||
|
||||
// Define the context
|
||||
interface Context {
|
||||
@ -30,13 +29,13 @@ const initialState: ContextFunction<Context> = () => ({
|
||||
enqueuedMessage: undefined,
|
||||
});
|
||||
|
||||
type SendEvent = { type: 'send'; message: any };
|
||||
type AuthenticatedEvent = { type: 'authenticated'; token: string };
|
||||
type InitiateAuthEvent = { type: 'initiateAuth' };
|
||||
type UnauthorizedEvent = { type: 'unauthorized'; error: Error };
|
||||
type ConnectedEvent = { type: 'connected'; websocket: WebSocket };
|
||||
type SendEvent = { type: "send"; message: any };
|
||||
type AuthenticatedEvent = { type: "authenticated"; token: string };
|
||||
type InitiateAuthEvent = { type: "initiateAuth" };
|
||||
type UnauthorizedEvent = { type: "unauthorized"; error: Error };
|
||||
type ConnectedEvent = { type: "connected"; websocket: WebSocket };
|
||||
type ConnectionClosedEvent = {
|
||||
type: 'connectionClosed';
|
||||
type: "connectionClosed";
|
||||
code: number;
|
||||
reason: string;
|
||||
};
|
||||
@ -109,7 +108,7 @@ function setToken(context: Context, event: AuthenticatedEvent): Context {
|
||||
|
||||
function connectionEstablished(
|
||||
context: Context,
|
||||
event: ConnectedEvent
|
||||
event: ConnectedEvent,
|
||||
): Context {
|
||||
return {
|
||||
...context,
|
||||
@ -119,42 +118,42 @@ function connectionEstablished(
|
||||
|
||||
// State machine
|
||||
const connectionStateMachine = createMachine(
|
||||
'idle',
|
||||
"idle",
|
||||
{
|
||||
idle: state(
|
||||
transition('send', 'connecting', reduce(enqueueMessage)),
|
||||
transition('expireToken', 'idle', reduce(expireToken))
|
||||
transition("send", "connecting", reduce(enqueueMessage)),
|
||||
transition("expireToken", "idle", reduce(expireToken)),
|
||||
),
|
||||
connecting: state(
|
||||
transition('connecting', 'connecting'),
|
||||
transition('connected', 'active', reduce(connectionEstablished)),
|
||||
transition('connectionClosed', 'idle', reduce(closeConnection)),
|
||||
transition('send', 'connecting', reduce(enqueueMessage)),
|
||||
transition("connecting", "connecting"),
|
||||
transition("connected", "active", reduce(connectionEstablished)),
|
||||
transition("connectionClosed", "idle", reduce(closeConnection)),
|
||||
transition("send", "connecting", reduce(enqueueMessage)),
|
||||
|
||||
immediate('authRequired', guard(noToken))
|
||||
immediate("authRequired", guard(noToken)),
|
||||
),
|
||||
authRequired: state(
|
||||
transition('initiateAuth', 'authInProgress'),
|
||||
transition('send', 'authRequired', reduce(enqueueMessage))
|
||||
transition("initiateAuth", "authInProgress"),
|
||||
transition("send", "authRequired", reduce(enqueueMessage)),
|
||||
),
|
||||
authInProgress: state(
|
||||
transition('authenticated', 'connecting', reduce(setToken)),
|
||||
transition("authenticated", "connecting", reduce(setToken)),
|
||||
transition(
|
||||
'unauthorized',
|
||||
'idle',
|
||||
"unauthorized",
|
||||
"idle",
|
||||
reduce(expireToken),
|
||||
reduce(closeConnection)
|
||||
reduce(closeConnection),
|
||||
),
|
||||
transition('send', 'authInProgress', reduce(enqueueMessage))
|
||||
transition("send", "authInProgress", reduce(enqueueMessage)),
|
||||
),
|
||||
active: state(
|
||||
transition('send', 'active', reduce(sendMessage)),
|
||||
transition('unauthorized', 'idle', reduce(expireToken)),
|
||||
transition('connectionClosed', 'idle', reduce(closeConnection))
|
||||
transition("send", "active", reduce(sendMessage)),
|
||||
transition("unauthorized", "idle", reduce(expireToken)),
|
||||
transition("connectionClosed", "idle", reduce(closeConnection)),
|
||||
),
|
||||
failed: state(transition('send', 'failed')),
|
||||
failed: state(transition("send", "failed")),
|
||||
},
|
||||
initialState
|
||||
initialState,
|
||||
);
|
||||
|
||||
type WithRequestId = {
|
||||
@ -237,7 +236,7 @@ export interface RealtimeClient {
|
||||
*/
|
||||
connect<Input = any, Output = any>(
|
||||
app: string,
|
||||
handler: RealtimeConnectionHandler<Output>
|
||||
handler: RealtimeConnectionHandler<Output>,
|
||||
): RealtimeConnection<Input>;
|
||||
}
|
||||
|
||||
@ -248,16 +247,16 @@ type RealtimeUrlParams = {
|
||||
|
||||
function buildRealtimeUrl(
|
||||
app: string,
|
||||
{ token, maxBuffering }: RealtimeUrlParams
|
||||
{ token, maxBuffering }: RealtimeUrlParams,
|
||||
): string {
|
||||
if (maxBuffering !== undefined && (maxBuffering < 1 || maxBuffering > 60)) {
|
||||
throw new Error('The `maxBuffering` must be between 1 and 60 (inclusive)');
|
||||
throw new Error("The `maxBuffering` must be between 1 and 60 (inclusive)");
|
||||
}
|
||||
const queryParams = new URLSearchParams({
|
||||
fal_jwt_token: token,
|
||||
});
|
||||
if (maxBuffering !== undefined) {
|
||||
queryParams.set('max_buffering', maxBuffering.toFixed(0));
|
||||
queryParams.set("max_buffering", maxBuffering.toFixed(0));
|
||||
}
|
||||
const appId = ensureAppIdFormat(app);
|
||||
return `wss://fal.run/${appId}/realtime?${queryParams.toString()}`;
|
||||
@ -267,7 +266,7 @@ const DEFAULT_THROTTLE_INTERVAL = 128;
|
||||
|
||||
function isUnauthorizedError(message: any): boolean {
|
||||
// TODO we need better protocol definition with error codes
|
||||
return message['status'] === 'error' && message['error'] === 'Unauthorized';
|
||||
return message["status"] === "error" && message["error"] === "Unauthorized";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -281,7 +280,7 @@ const WebSocketErrorCodes = {
|
||||
type ConnectionStateMachine = Service<typeof connectionStateMachine> & {
|
||||
throttledSend: (
|
||||
event: Event,
|
||||
payload?: any
|
||||
payload?: any,
|
||||
) => void | Promise<void> | undefined;
|
||||
};
|
||||
|
||||
@ -291,7 +290,7 @@ type ConnectionOnChange = InterpretOnChangeFunction<
|
||||
|
||||
type RealtimeConnectionCallback = Pick<
|
||||
RealtimeConnectionHandler<any>,
|
||||
'onResult' | 'onError'
|
||||
"onResult" | "onError"
|
||||
>;
|
||||
|
||||
const connectionCache = new Map<string, ConnectionStateMachine>();
|
||||
@ -299,7 +298,7 @@ const connectionCallbacks = new Map<string, RealtimeConnectionCallback>();
|
||||
function reuseInterpreter(
|
||||
key: string,
|
||||
throttleInterval: number,
|
||||
onChange: ConnectionOnChange
|
||||
onChange: ConnectionOnChange,
|
||||
) {
|
||||
if (!connectionCache.has(key)) {
|
||||
const machine = interpret(connectionStateMachine, onChange);
|
||||
@ -331,20 +330,20 @@ const NoOpConnection: RealtimeConnection<any> = {
|
||||
|
||||
function isSuccessfulResult(data: any): boolean {
|
||||
return (
|
||||
data.status !== 'error' &&
|
||||
data.type !== 'x-fal-message' &&
|
||||
data.status !== "error" &&
|
||||
data.type !== "x-fal-message" &&
|
||||
!isFalErrorResult(data)
|
||||
);
|
||||
}
|
||||
|
||||
type FalErrorResult = {
|
||||
type: 'x-fal-error';
|
||||
type: "x-fal-error";
|
||||
error: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
function isFalErrorResult(data: any): data is FalErrorResult {
|
||||
return data.type === 'x-fal-error';
|
||||
return data.type === "x-fal-error";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -353,12 +352,12 @@ function isFalErrorResult(data: any): data is FalErrorResult {
|
||||
export const realtimeImpl: RealtimeClient = {
|
||||
connect<Input, Output>(
|
||||
app: string,
|
||||
handler: RealtimeConnectionHandler<Output>
|
||||
handler: RealtimeConnectionHandler<Output>,
|
||||
): RealtimeConnection<Input> {
|
||||
const {
|
||||
// if running on React in the server, set clientOnly to true by default
|
||||
clientOnly = isReact() && !isBrowser(),
|
||||
connectionKey = uuid(),
|
||||
connectionKey = crypto.randomUUID(),
|
||||
maxBuffering,
|
||||
throttleInterval = DEFAULT_THROTTLE_INTERVAL,
|
||||
} = handler;
|
||||
@ -384,39 +383,39 @@ export const realtimeImpl: RealtimeClient = {
|
||||
throttleInterval,
|
||||
({ context, machine, send }) => {
|
||||
const { enqueuedMessage, token } = context;
|
||||
if (machine.current === 'active' && enqueuedMessage) {
|
||||
send({ type: 'send', message: enqueuedMessage });
|
||||
if (machine.current === "active" && enqueuedMessage) {
|
||||
send({ type: "send", message: enqueuedMessage });
|
||||
}
|
||||
if (
|
||||
machine.current === 'authRequired' &&
|
||||
machine.current === "authRequired" &&
|
||||
token === undefined &&
|
||||
previousState !== machine.current
|
||||
) {
|
||||
send({ type: 'initiateAuth' });
|
||||
send({ type: "initiateAuth" });
|
||||
getTemporaryAuthToken(app)
|
||||
.then((token) => {
|
||||
send({ type: 'authenticated', token });
|
||||
send({ type: "authenticated", token });
|
||||
const tokenExpirationTimeout = Math.round(
|
||||
TOKEN_EXPIRATION_SECONDS * 0.9 * 1000
|
||||
TOKEN_EXPIRATION_SECONDS * 0.9 * 1000,
|
||||
);
|
||||
setTimeout(() => {
|
||||
send({ type: 'expireToken' });
|
||||
send({ type: "expireToken" });
|
||||
}, tokenExpirationTimeout);
|
||||
})
|
||||
.catch((error) => {
|
||||
send({ type: 'unauthorized', error });
|
||||
send({ type: "unauthorized", error });
|
||||
});
|
||||
}
|
||||
if (
|
||||
machine.current === 'connecting' &&
|
||||
machine.current === "connecting" &&
|
||||
previousState !== machine.current &&
|
||||
token !== undefined
|
||||
) {
|
||||
const ws = new WebSocket(
|
||||
buildRealtimeUrl(app, { token, maxBuffering })
|
||||
buildRealtimeUrl(app, { token, maxBuffering }),
|
||||
);
|
||||
ws.onopen = () => {
|
||||
send({ type: 'connected', websocket: ws });
|
||||
send({ type: "connected", websocket: ws });
|
||||
};
|
||||
ws.onclose = (event) => {
|
||||
if (event.code !== WebSocketErrorCodes.NORMAL_CLOSURE) {
|
||||
@ -425,15 +424,15 @@ export const realtimeImpl: RealtimeClient = {
|
||||
new ApiError({
|
||||
message: `Error closing the connection: ${event.reason}`,
|
||||
status: event.code,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
send({ type: 'connectionClosed', code: 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 }));
|
||||
onError(new ApiError({ message: "Unknown error", status: 500 }));
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
const { onResult } = getCallbacks();
|
||||
@ -464,7 +463,7 @@ export const realtimeImpl: RealtimeClient = {
|
||||
// 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') });
|
||||
send({ type: "unauthorized", error: new Error("Unauthorized") });
|
||||
return;
|
||||
}
|
||||
if (isSuccessfulResult(data)) {
|
||||
@ -472,7 +471,7 @@ export const realtimeImpl: RealtimeClient = {
|
||||
return;
|
||||
}
|
||||
if (isFalErrorResult(data)) {
|
||||
if (data.error === 'TIMEOUT') {
|
||||
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.
|
||||
@ -485,14 +484,14 @@ export const realtimeImpl: RealtimeClient = {
|
||||
// TODO better error status code
|
||||
status: 400,
|
||||
body: data,
|
||||
})
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
previousState = machine.current;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const send = (input: Input & Partial<WithRequestId>) => {
|
||||
@ -503,17 +502,17 @@ export const realtimeImpl: RealtimeClient = {
|
||||
? input
|
||||
: {
|
||||
...input,
|
||||
request_id: input['request_id'] ?? uuid(),
|
||||
request_id: input["request_id"] ?? crypto.randomUUID(),
|
||||
};
|
||||
|
||||
stateMachine.throttledSend({
|
||||
type: 'send',
|
||||
type: "send",
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
stateMachine.send({ type: 'close' });
|
||||
stateMachine.send({ type: "close" });
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { getConfig } from './config';
|
||||
import { ResponseHandler } from './response';
|
||||
import { getUserAgent, isBrowser } from './runtime';
|
||||
import { getConfig } from "./config";
|
||||
import { ResponseHandler } from "./response";
|
||||
import { getUserAgent, isBrowser } from "./runtime";
|
||||
|
||||
const isCloudflareWorkers =
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator?.userAgent === 'Cloudflare-Workers';
|
||||
typeof navigator !== "undefined" &&
|
||||
navigator?.userAgent === "Cloudflare-Workers";
|
||||
|
||||
type RequestOptions = {
|
||||
responseHandler?: ResponseHandler<any>;
|
||||
@ -14,7 +14,7 @@ export async function dispatchRequest<Input, Output>(
|
||||
method: string,
|
||||
targetUrl: string,
|
||||
input: Input,
|
||||
options: RequestOptions & RequestInit = {}
|
||||
options: RequestOptions & RequestInit = {},
|
||||
): Promise<Output> {
|
||||
const {
|
||||
credentials: credentialsValue,
|
||||
@ -22,9 +22,9 @@ export async function dispatchRequest<Input, Output>(
|
||||
responseHandler,
|
||||
fetch,
|
||||
} = getConfig();
|
||||
const userAgent = isBrowser() ? {} : { 'User-Agent': getUserAgent() };
|
||||
const userAgent = isBrowser() ? {} : { "User-Agent": getUserAgent() };
|
||||
const credentials =
|
||||
typeof credentialsValue === 'function'
|
||||
typeof credentialsValue === "function"
|
||||
? credentialsValue()
|
||||
: credentialsValue;
|
||||
|
||||
@ -34,8 +34,8 @@ export async function dispatchRequest<Input, Output>(
|
||||
const authHeader = credentials ? { Authorization: `Key ${credentials}` } : {};
|
||||
const requestHeaders = {
|
||||
...authHeader,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...userAgent,
|
||||
...(headers ?? {}),
|
||||
} as HeadersInit;
|
||||
@ -48,9 +48,9 @@ export async function dispatchRequest<Input, Output>(
|
||||
...requestHeaders,
|
||||
...(requestInit.headers ?? {}),
|
||||
},
|
||||
...(!isCloudflareWorkers && { mode: 'cors' }),
|
||||
...(!isCloudflareWorkers && { mode: "cors" }),
|
||||
body:
|
||||
method.toLowerCase() !== 'get' && input
|
||||
method.toLowerCase() !== "get" && input
|
||||
? JSON.stringify(input)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ValidationErrorInfo } from './types';
|
||||
import { ValidationErrorInfo } from "./types";
|
||||
|
||||
export type ResponseHandler<Output> = (response: Response) => Promise<Output>;
|
||||
|
||||
@ -14,7 +14,7 @@ export class ApiError<Body> extends Error {
|
||||
public readonly body: Body;
|
||||
constructor({ message, status, body }: ApiErrorArgs) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
@ -27,18 +27,18 @@ type ValidationErrorBody = {
|
||||
export class ValidationError extends ApiError<ValidationErrorBody> {
|
||||
constructor(args: ApiErrorArgs) {
|
||||
super(args);
|
||||
this.name = 'ValidationError';
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
|
||||
get fieldErrors(): ValidationErrorInfo[] {
|
||||
// NOTE: this is a hack to support both FastAPI/Pydantic errors
|
||||
// and some custom 422 errors that might not be in the Pydantic format.
|
||||
if (typeof this.body.detail === 'string') {
|
||||
if (typeof this.body.detail === "string") {
|
||||
return [
|
||||
{
|
||||
loc: ['body'],
|
||||
loc: ["body"],
|
||||
msg: this.body.detail,
|
||||
type: 'value_error',
|
||||
type: "value_error",
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -47,18 +47,18 @@ export class ValidationError extends ApiError<ValidationErrorBody> {
|
||||
|
||||
getFieldErrors(field: string): ValidationErrorInfo[] {
|
||||
return this.fieldErrors.filter(
|
||||
(error) => error.loc[error.loc.length - 1] === field
|
||||
(error) => error.loc[error.loc.length - 1] === field,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function defaultResponseHandler<Output>(
|
||||
response: Response
|
||||
response: Response,
|
||||
): Promise<Output> {
|
||||
const { status, statusText } = response;
|
||||
const contentType = response.headers.get('Content-Type') ?? '';
|
||||
const contentType = response.headers.get("Content-Type") ?? "";
|
||||
if (!response.ok) {
|
||||
if (contentType.includes('application/json')) {
|
||||
if (contentType.includes("application/json")) {
|
||||
const body = await response.json();
|
||||
const ErrorType = status === 422 ? ValidationError : ApiError;
|
||||
throw new ErrorType({
|
||||
@ -69,13 +69,13 @@ export async function defaultResponseHandler<Output>(
|
||||
}
|
||||
throw new ApiError({ message: `HTTP ${status}: ${statusText}`, status });
|
||||
}
|
||||
if (contentType.includes('application/json')) {
|
||||
if (contentType.includes("application/json")) {
|
||||
return response.json() as Promise<Output>;
|
||||
}
|
||||
if (contentType.includes('text/html')) {
|
||||
if (contentType.includes("text/html")) {
|
||||
return response.text() as Promise<Output>;
|
||||
}
|
||||
if (contentType.includes('application/octet-stream')) {
|
||||
if (contentType.includes("application/octet-stream")) {
|
||||
return response.arrayBuffer() as Promise<Output>;
|
||||
}
|
||||
// TODO convert to either number or bool automatically
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { getUserAgent, isBrowser } from './runtime';
|
||||
import { getUserAgent, isBrowser } from "./runtime";
|
||||
|
||||
describe('the runtime test suite', () => {
|
||||
it('should return false when calling isBrowser() on a test', () => {
|
||||
describe("the runtime test suite", () => {
|
||||
it("should return false when calling isBrowser() on a test", () => {
|
||||
expect(isBrowser()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when calling isBrowser() and window is present', () => {
|
||||
it("should return true when calling isBrowser() and window is present", () => {
|
||||
global.window = {
|
||||
document: {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -13,7 +13,7 @@ describe('the runtime test suite', () => {
|
||||
expect(isBrowser()).toBe(true);
|
||||
});
|
||||
|
||||
it('should create the correct user agent identifier', () => {
|
||||
it("should create the correct user agent identifier", () => {
|
||||
expect(getUserAgent()).toMatch(/@fal-ai\/serverless-client/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
export function isBrowser(): boolean {
|
||||
return (
|
||||
typeof window !== 'undefined' && typeof window.document !== 'undefined'
|
||||
typeof window !== "undefined" && typeof window.document !== "undefined"
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export function getUserAgent(): string {
|
||||
if (memoizedUserAgent !== null) {
|
||||
return memoizedUserAgent;
|
||||
}
|
||||
const packageInfo = require('../package.json');
|
||||
const packageInfo = require("../package.json");
|
||||
memoizedUserAgent = `${packageInfo.name}/${packageInfo.version}`;
|
||||
return memoizedUserAgent;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { getConfig, getRestApiUrl } from './config';
|
||||
import { dispatchRequest } from './request';
|
||||
import { isPlainObject } from './utils';
|
||||
import { getConfig, getRestApiUrl } from "./config";
|
||||
import { dispatchRequest } from "./request";
|
||||
import { isPlainObject } from "./utils";
|
||||
|
||||
/**
|
||||
* File support for the client. This interface establishes the contract for
|
||||
@ -46,8 +46,8 @@ type InitiateUploadData = {
|
||||
* @returns the file extension or `bin` if the content type is not recognized.
|
||||
*/
|
||||
function getExtensionFromContentType(contentType: string): string {
|
||||
const [_, fileType] = contentType.split('/');
|
||||
return fileType.split(/[-;]/)[0] ?? 'bin';
|
||||
const [_, fileType] = contentType.split("/");
|
||||
return fileType.split(/[-;]/)[0] ?? "bin";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,16 +58,16 @@ function getExtensionFromContentType(contentType: string): string {
|
||||
* @returns the URL to upload the file to and the URL of the file once it is uploaded.
|
||||
*/
|
||||
async function initiateUpload(file: Blob): Promise<InitiateUploadResult> {
|
||||
const contentType = file.type || 'application/octet-stream';
|
||||
const contentType = file.type || "application/octet-stream";
|
||||
const filename =
|
||||
file.name || `${Date.now()}.${getExtensionFromContentType(contentType)}`;
|
||||
return await dispatchRequest<InitiateUploadData, InitiateUploadResult>(
|
||||
'POST',
|
||||
"POST",
|
||||
`${getRestApiUrl()}/storage/upload/initiate`,
|
||||
{
|
||||
content_type: contentType,
|
||||
file_name: filename,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -79,10 +79,10 @@ export const storageImpl: StorageSupport = {
|
||||
const { fetch } = getConfig();
|
||||
const { upload_url: uploadUrl, file_url: url } = await initiateUpload(file);
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: {
|
||||
'Content-Type': file.type || 'application/octet-stream',
|
||||
"Content-Type": file.type || "application/octet-stream",
|
||||
},
|
||||
});
|
||||
const { responseHandler } = getConfig();
|
||||
@ -101,7 +101,7 @@ export const storageImpl: StorageSupport = {
|
||||
const promises = Object.entries(inputObject).map(
|
||||
async ([key, value]): Promise<KeyValuePair> => {
|
||||
return [key, await storageImpl.transformInput(value)];
|
||||
}
|
||||
},
|
||||
);
|
||||
const results = await Promise.all(promises);
|
||||
return Object.fromEntries(results);
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { createParser } from 'eventsource-parser';
|
||||
import { getTemporaryAuthToken } from './auth';
|
||||
import { getConfig } from './config';
|
||||
import { buildUrl } from './function';
|
||||
import { dispatchRequest } from './request';
|
||||
import { ApiError, defaultResponseHandler } from './response';
|
||||
import { storageImpl } from './storage';
|
||||
import { createParser } from "eventsource-parser";
|
||||
import { getTemporaryAuthToken } from "./auth";
|
||||
import { getConfig } from "./config";
|
||||
import { buildUrl } from "./function";
|
||||
import { dispatchRequest } from "./request";
|
||||
import { ApiError, defaultResponseHandler } from "./response";
|
||||
import { storageImpl } from "./storage";
|
||||
|
||||
export type StreamingConnectionMode = 'client' | 'server';
|
||||
export type StreamingConnectionMode = "client" | "server";
|
||||
|
||||
/**
|
||||
* The stream API options. It requires the API input and also
|
||||
@ -43,7 +43,7 @@ type StreamOptions<Input> = {
|
||||
/**
|
||||
* The HTTP method, defaults to `post`;
|
||||
*/
|
||||
readonly method?: 'get' | 'post' | 'put' | 'delete' | string;
|
||||
readonly method?: "get" | "post" | "put" | "delete" | string;
|
||||
|
||||
/**
|
||||
* The content type the client accepts as response.
|
||||
@ -65,7 +65,7 @@ type StreamOptions<Input> = {
|
||||
|
||||
const EVENT_STREAM_TIMEOUT = 15 * 1000;
|
||||
|
||||
type FalStreamEventType = 'data' | 'error' | 'done';
|
||||
type FalStreamEventType = "data" | "error" | "done";
|
||||
|
||||
type EventHandler<T = any> = (event: T) => void;
|
||||
|
||||
@ -95,7 +95,7 @@ export class FalStream<Input, Output> {
|
||||
this.url =
|
||||
options.url ??
|
||||
buildUrl(endpointId, {
|
||||
path: '/stream',
|
||||
path: "/stream",
|
||||
query: options.queryParams,
|
||||
});
|
||||
this.options = options;
|
||||
@ -103,17 +103,17 @@ export class FalStream<Input, Output> {
|
||||
if (this.streamClosed) {
|
||||
reject(
|
||||
new ApiError({
|
||||
message: 'Streaming connection is already closed.',
|
||||
message: "Streaming connection is already closed.",
|
||||
status: 400,
|
||||
body: undefined,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.on('done', (data) => {
|
||||
this.on("done", (data) => {
|
||||
this.streamClosed = true;
|
||||
resolve(data);
|
||||
});
|
||||
this.on('error', (error) => {
|
||||
this.on("error", (error) => {
|
||||
this.streamClosed = true;
|
||||
reject(error);
|
||||
});
|
||||
@ -123,29 +123,29 @@ export class FalStream<Input, Output> {
|
||||
|
||||
private start = async () => {
|
||||
const { endpointId, options } = this;
|
||||
const { input, method = 'post', connectionMode = 'server' } = options;
|
||||
const { input, method = "post", connectionMode = "server" } = options;
|
||||
try {
|
||||
if (connectionMode === 'client') {
|
||||
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 parsedUrl = new URL(this.url);
|
||||
parsedUrl.searchParams.set('fal_jwt_token', token);
|
||||
parsedUrl.searchParams.set("fal_jwt_token", token);
|
||||
const response = await fetch(parsedUrl.toString(), {
|
||||
method: method.toUpperCase(),
|
||||
headers: {
|
||||
accept: options.accept ?? 'text/event-stream',
|
||||
'content-type': 'application/json',
|
||||
accept: options.accept ?? "text/event-stream",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: input && method !== 'get' ? JSON.stringify(input) : undefined,
|
||||
body: input && method !== "get" ? JSON.stringify(input) : undefined,
|
||||
signal: this.abortController.signal,
|
||||
});
|
||||
return await this.handleResponse(response);
|
||||
}
|
||||
return await dispatchRequest(method.toUpperCase(), this.url, input, {
|
||||
headers: {
|
||||
accept: options.accept ?? 'text/event-stream',
|
||||
accept: options.accept ?? "text/event-stream",
|
||||
},
|
||||
responseHandler: this.handleResponse,
|
||||
signal: this.abortController.signal,
|
||||
@ -162,7 +162,7 @@ export class FalStream<Input, Output> {
|
||||
// so the exception gets converted to ApiError correctly
|
||||
await defaultResponseHandler(response);
|
||||
} catch (error) {
|
||||
this.emit('error', error);
|
||||
this.emit("error", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -170,27 +170,27 @@ export class FalStream<Input, Output> {
|
||||
const body = response.body;
|
||||
if (!body) {
|
||||
this.emit(
|
||||
'error',
|
||||
"error",
|
||||
new ApiError({
|
||||
message: 'Response body is empty.',
|
||||
message: "Response body is empty.",
|
||||
status: 400,
|
||||
body: undefined,
|
||||
})
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// any response that is not a text/event-stream will be handled as a binary stream
|
||||
if (response.headers.get('content-type') !== 'text/event-stream') {
|
||||
if (response.headers.get("content-type") !== "text/event-stream") {
|
||||
const reader = body.getReader();
|
||||
const emitRawChunk = () => {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
this.emit('done', this.currentData);
|
||||
this.emit("done", this.currentData);
|
||||
return;
|
||||
}
|
||||
this.currentData = value as Output;
|
||||
this.emit('data', value);
|
||||
this.emit("data", value);
|
||||
emitRawChunk();
|
||||
});
|
||||
};
|
||||
@ -198,23 +198,23 @@ export class FalStream<Input, Output> {
|
||||
return;
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const reader = response.body.getReader();
|
||||
|
||||
const parser = createParser((event) => {
|
||||
if (event.type === 'event') {
|
||||
if (event.type === "event") {
|
||||
const data = event.data;
|
||||
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
this.buffer.push(parsedData);
|
||||
this.currentData = parsedData;
|
||||
this.emit('data', parsedData);
|
||||
this.emit("data", parsedData);
|
||||
|
||||
// also emit 'message'for backwards compatibility
|
||||
this.emit('message' as any, parsedData);
|
||||
this.emit("message" as any, parsedData);
|
||||
} catch (e) {
|
||||
this.emit('error', e);
|
||||
this.emit("error", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -229,18 +229,18 @@ export class FalStream<Input, Output> {
|
||||
|
||||
if (Date.now() - this.lastEventTimestamp > timeout) {
|
||||
this.emit(
|
||||
'error',
|
||||
"error",
|
||||
new ApiError({
|
||||
message: `Event stream timed out after ${(timeout / 1000).toFixed(0)} seconds with no messages.`,
|
||||
status: 408,
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
readPartialResponse().catch(this.handleError);
|
||||
} else {
|
||||
this.emit('done', this.currentData);
|
||||
this.emit("done", this.currentData);
|
||||
}
|
||||
};
|
||||
|
||||
@ -253,10 +253,10 @@ export class FalStream<Input, Output> {
|
||||
error instanceof ApiError
|
||||
? error
|
||||
: new ApiError({
|
||||
message: error.message ?? 'An unknown error occurred',
|
||||
message: error.message ?? "An unknown error occurred",
|
||||
status: 500,
|
||||
});
|
||||
this.emit('error', apiError);
|
||||
this.emit("error", apiError);
|
||||
return;
|
||||
};
|
||||
|
||||
@ -277,8 +277,8 @@ export class FalStream<Input, Output> {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
let running = true;
|
||||
const stopAsyncIterator = () => (running = false);
|
||||
this.on('error', stopAsyncIterator);
|
||||
this.on('done', stopAsyncIterator);
|
||||
this.on("error", stopAsyncIterator);
|
||||
this.on("done", stopAsyncIterator);
|
||||
while (running) {
|
||||
const data = this.buffer.shift();
|
||||
if (data) {
|
||||
@ -322,7 +322,7 @@ export class FalStream<Input, Output> {
|
||||
*/
|
||||
export async function stream<Input = Record<string, any>, Output = any>(
|
||||
endpointId: string,
|
||||
options: StreamOptions<Input>
|
||||
options: StreamOptions<Input>,
|
||||
): Promise<FalStream<Input, Output>> {
|
||||
const input =
|
||||
options.input && options.autoUpload !== false
|
||||
|
||||
@ -8,8 +8,8 @@ export type EnqueueResult = {
|
||||
|
||||
export type RequestLog = {
|
||||
message: string;
|
||||
level: 'STDERR' | 'STDOUT' | 'ERROR' | 'INFO' | 'WARN' | 'DEBUG';
|
||||
source: 'USER';
|
||||
level: "STDERR" | "STDOUT" | "ERROR" | "INFO" | "WARN" | "DEBUG";
|
||||
source: "USER";
|
||||
timestamp: string; // Using string to represent date-time format, but you could also use 'Date' type if you're going to construct Date objects.
|
||||
};
|
||||
|
||||
@ -18,24 +18,24 @@ export type Metrics = {
|
||||
};
|
||||
|
||||
interface BaseQueueStatus {
|
||||
status: 'IN_PROGRESS' | 'COMPLETED' | 'IN_QUEUE';
|
||||
status: "IN_PROGRESS" | "COMPLETED" | "IN_QUEUE";
|
||||
}
|
||||
|
||||
export interface InProgressQueueStatus extends BaseQueueStatus {
|
||||
status: 'IN_PROGRESS';
|
||||
status: "IN_PROGRESS";
|
||||
response_url: string;
|
||||
logs: RequestLog[];
|
||||
}
|
||||
|
||||
export interface CompletedQueueStatus extends BaseQueueStatus {
|
||||
status: 'COMPLETED';
|
||||
status: "COMPLETED";
|
||||
response_url: string;
|
||||
logs: RequestLog[];
|
||||
metrics: Metrics;
|
||||
}
|
||||
|
||||
export interface EnqueuedQueueStatus extends BaseQueueStatus {
|
||||
status: 'IN_QUEUE';
|
||||
status: "IN_QUEUE";
|
||||
queue_position: number;
|
||||
response_url: string;
|
||||
}
|
||||
@ -50,7 +50,7 @@ export function isQueueStatus(obj: any): obj is QueueStatus {
|
||||
}
|
||||
|
||||
export function isCompletedQueueStatus(obj: any): obj is CompletedQueueStatus {
|
||||
return isQueueStatus(obj) && obj.status === 'COMPLETED';
|
||||
return isQueueStatus(obj) && obj.status === "COMPLETED";
|
||||
}
|
||||
|
||||
export type ValidationErrorInfo = {
|
||||
@ -69,7 +69,7 @@ export type ValidationErrorInfo = {
|
||||
export type WebHookResponse<Payload = any> =
|
||||
| {
|
||||
/** Indicates a successful response. */
|
||||
status: 'OK';
|
||||
status: "OK";
|
||||
/** The payload of the response, structure determined by the Payload type. */
|
||||
payload: Payload;
|
||||
/** Error is never present in a successful response. */
|
||||
@ -79,7 +79,7 @@ export type WebHookResponse<Payload = any> =
|
||||
}
|
||||
| {
|
||||
/** Indicates an unsuccessful response. */
|
||||
status: 'ERROR';
|
||||
status: "ERROR";
|
||||
/** The payload of the response, structure determined by the Payload type. */
|
||||
payload: Payload;
|
||||
/** Description of the error that occurred. */
|
||||
|
||||
@ -1,47 +1,47 @@
|
||||
import { ensureAppIdFormat, parseAppId } from './utils';
|
||||
import { ensureAppIdFormat, parseAppId } from "./utils";
|
||||
|
||||
describe('The utils test suite', () => {
|
||||
it('shoud match a current appOwner/appId format', () => {
|
||||
const id = 'fal-ai/fast-sdxl';
|
||||
describe("The utils test suite", () => {
|
||||
it("shoud match a current appOwner/appId format", () => {
|
||||
const id = "fal-ai/fast-sdxl";
|
||||
expect(ensureAppIdFormat(id)).toBe(id);
|
||||
});
|
||||
|
||||
it('shoud match a current appOwner/appId/path format', () => {
|
||||
const id = 'fal-ai/fast-sdxl/image-to-image';
|
||||
it("shoud match a current appOwner/appId/path format", () => {
|
||||
const id = "fal-ai/fast-sdxl/image-to-image";
|
||||
expect(ensureAppIdFormat(id)).toBe(id);
|
||||
});
|
||||
|
||||
it('should throw on an invalid app id format', () => {
|
||||
const id = 'just-an-id';
|
||||
it("should throw on an invalid app id format", () => {
|
||||
const id = "just-an-id";
|
||||
expect(() => ensureAppIdFormat(id)).toThrowError();
|
||||
});
|
||||
|
||||
it('should parse a current app id', () => {
|
||||
const id = 'fal-ai/fast-sdxl';
|
||||
it("should parse a current app id", () => {
|
||||
const id = "fal-ai/fast-sdxl";
|
||||
const parsed = parseAppId(id);
|
||||
expect(parsed).toEqual({
|
||||
owner: 'fal-ai',
|
||||
alias: 'fast-sdxl',
|
||||
owner: "fal-ai",
|
||||
alias: "fast-sdxl",
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse a current app id with path', () => {
|
||||
const id = 'fal-ai/fast-sdxl/image-to-image';
|
||||
it("should parse a current app id with path", () => {
|
||||
const id = "fal-ai/fast-sdxl/image-to-image";
|
||||
const parsed = parseAppId(id);
|
||||
expect(parsed).toEqual({
|
||||
owner: 'fal-ai',
|
||||
alias: 'fast-sdxl',
|
||||
path: 'image-to-image',
|
||||
owner: "fal-ai",
|
||||
alias: "fast-sdxl",
|
||||
path: "image-to-image",
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse a current app id with namespace', () => {
|
||||
const id = 'workflows/fal-ai/fast-sdxl';
|
||||
it("should parse a current app id with namespace", () => {
|
||||
const id = "workflows/fal-ai/fast-sdxl";
|
||||
const parsed = parseAppId(id);
|
||||
expect(parsed).toEqual({
|
||||
owner: 'fal-ai',
|
||||
alias: 'fast-sdxl',
|
||||
namespace: 'workflows',
|
||||
owner: "fal-ai",
|
||||
alias: "fast-sdxl",
|
||||
namespace: "workflows",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export function ensureAppIdFormat(id: string): string {
|
||||
const parts = id.split('/');
|
||||
const parts = id.split("/");
|
||||
if (parts.length > 1) {
|
||||
return id;
|
||||
}
|
||||
@ -8,11 +8,11 @@ export function ensureAppIdFormat(id: string): string {
|
||||
return `${appOwner}/${appId}`;
|
||||
}
|
||||
throw new Error(
|
||||
`Invalid app id: ${id}. Must be in the format <appOwner>/<appId>`
|
||||
`Invalid app id: ${id}. Must be in the format <appOwner>/<appId>`,
|
||||
);
|
||||
}
|
||||
|
||||
const APP_NAMESPACES = ['workflows', 'comfy'] as const;
|
||||
const APP_NAMESPACES = ["workflows", "comfy"] as const;
|
||||
|
||||
type AppNamespace = (typeof APP_NAMESPACES)[number];
|
||||
|
||||
@ -25,19 +25,19 @@ export type AppId = {
|
||||
|
||||
export function parseAppId(id: string): AppId {
|
||||
const normalizedId = ensureAppIdFormat(id);
|
||||
const parts = normalizedId.split('/');
|
||||
const parts = normalizedId.split("/");
|
||||
if (APP_NAMESPACES.includes(parts[0] as any)) {
|
||||
return {
|
||||
owner: parts[1],
|
||||
alias: parts[2],
|
||||
path: parts.slice(3).join('/') || undefined,
|
||||
path: parts.slice(3).join("/") || undefined,
|
||||
namespace: parts[0] as AppNamespace,
|
||||
};
|
||||
}
|
||||
return {
|
||||
owner: parts[0],
|
||||
alias: parts[1],
|
||||
path: parts.slice(2).join('/') || undefined,
|
||||
path: parts.slice(2).join("/") || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ export function isValidUrl(url: string) {
|
||||
export function throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
limit: number,
|
||||
leading = false
|
||||
leading = false,
|
||||
): (...funcArgs: Parameters<T>) => ReturnType<T> | void {
|
||||
let lastFunc: NodeJS.Timeout | null;
|
||||
let lastRan: number;
|
||||
@ -75,7 +75,7 @@ export function throttle<T extends (...args: any[]) => any>(
|
||||
lastRan = Date.now();
|
||||
}
|
||||
},
|
||||
limit - (Date.now() - lastRan)
|
||||
limit - (Date.now() - lastRan),
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -97,8 +97,8 @@ export function isReact() {
|
||||
const stack = new Error().stack;
|
||||
isRunningInReact =
|
||||
!!stack &&
|
||||
(stack.includes('node_modules/react-dom/') ||
|
||||
stack.includes('node_modules/next/'));
|
||||
(stack.includes("node_modules/react-dom/") ||
|
||||
stack.includes("node_modules/next/"));
|
||||
}
|
||||
return isRunningInReact;
|
||||
}
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'create-app',
|
||||
preset: '../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
displayName: "create-app",
|
||||
preset: "../../jest.preset.js",
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
||||
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||
coverageDirectory: '../../coverage/libs/create-app',
|
||||
moduleFileExtensions: ["ts", "js", "html"],
|
||||
coverageDirectory: "../../coverage/libs/create-app",
|
||||
};
|
||||
|
||||
@ -1,32 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { input } from '@inquirer/prompts';
|
||||
import select from '@inquirer/select';
|
||||
import chalk from 'chalk';
|
||||
import childProcess from 'child_process';
|
||||
import { Command } from 'commander';
|
||||
import { execa, execaCommand } from 'execa';
|
||||
import fs from 'fs';
|
||||
import open from 'open';
|
||||
import ora from 'ora';
|
||||
import path from 'path';
|
||||
import { input } from "@inquirer/prompts";
|
||||
import select from "@inquirer/select";
|
||||
import chalk from "chalk";
|
||||
import childProcess from "child_process";
|
||||
import { Command } from "commander";
|
||||
import { execa, execaCommand } from "execa";
|
||||
import fs from "fs";
|
||||
import open from "open";
|
||||
import ora from "ora";
|
||||
import path from "path";
|
||||
|
||||
const program = new Command();
|
||||
const log = console.log;
|
||||
const repoUrl = 'https://github.com/fal-ai/fal-nextjs-template.git';
|
||||
const repoUrl = "https://github.com/fal-ai/fal-nextjs-template.git";
|
||||
const green = chalk.green;
|
||||
const purple = chalk.hex('#6e40c9');
|
||||
const purple = chalk.hex("#6e40c9");
|
||||
|
||||
async function main() {
|
||||
const spinner = ora({
|
||||
text: 'Creating codebase',
|
||||
text: "Creating codebase",
|
||||
});
|
||||
try {
|
||||
const kebabRegez = /^([a-z]+)(-[a-z0-9]+)*$/;
|
||||
|
||||
program
|
||||
.name('The fal.ai App Generator')
|
||||
.description('Generate full stack AI apps integrated with fal.ai');
|
||||
.name("The fal.ai App Generator")
|
||||
.description("Generate full stack AI apps integrated with fal.ai");
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -35,11 +35,11 @@ async function main() {
|
||||
|
||||
if (!appName || !kebabRegez.test(args[0])) {
|
||||
appName = await input({
|
||||
message: 'Enter your app name',
|
||||
default: 'model-playground',
|
||||
message: "Enter your app name",
|
||||
default: "model-playground",
|
||||
validate: (d) => {
|
||||
if (!kebabRegez.test(d)) {
|
||||
return 'please enter your app name in the format of my-app-name';
|
||||
return "please enter your app name in the format of my-app-name";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@ -47,24 +47,24 @@ async function main() {
|
||||
}
|
||||
|
||||
const hasFalEnv = await select({
|
||||
message: 'Do you have a fal.ai API key?',
|
||||
message: "Do you have a fal.ai API key?",
|
||||
choices: [
|
||||
{
|
||||
name: 'Yes',
|
||||
name: "Yes",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'No',
|
||||
name: "No",
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!hasFalEnv) {
|
||||
await open('https://www.fal.ai/dashboard');
|
||||
await open("https://www.fal.ai/dashboard");
|
||||
}
|
||||
|
||||
const fal_api_key = await input({ message: 'Fal AI API Key' });
|
||||
const fal_api_key = await input({ message: "Fal AI API Key" });
|
||||
|
||||
const envs = `
|
||||
# environment, either PRODUCTION or DEVELOPMENT
|
||||
@ -77,9 +77,9 @@ async function main() {
|
||||
log(`\nInitializing project. \n`);
|
||||
|
||||
spinner.start();
|
||||
await execa('git', ['clone', repoUrl, appName]);
|
||||
await execa("git", ["clone", repoUrl, appName]);
|
||||
|
||||
let packageJson = fs.readFileSync(`${appName}/package.json`, 'utf8');
|
||||
let packageJson = fs.readFileSync(`${appName}/package.json`, "utf8");
|
||||
const packageObj = JSON.parse(packageJson);
|
||||
packageObj.name = appName;
|
||||
packageJson = JSON.stringify(packageObj, null, 2);
|
||||
@ -87,47 +87,47 @@ async function main() {
|
||||
fs.writeFileSync(`${appName}/.env.local`, envs);
|
||||
|
||||
process.chdir(path.join(process.cwd(), appName));
|
||||
await execa('rm', ['-rf', '.git']);
|
||||
await execa('git', ['init']);
|
||||
await execa("rm", ["-rf", ".git"]);
|
||||
await execa("git", ["init"]);
|
||||
|
||||
spinner.text = '';
|
||||
let startCommand = '';
|
||||
spinner.text = "";
|
||||
let startCommand = "";
|
||||
|
||||
if (isBunInstalled()) {
|
||||
spinner.text = 'Installing dependencies';
|
||||
await execaCommand('bun install').pipeStdout(process.stdout);
|
||||
spinner.text = '';
|
||||
startCommand = 'bun dev';
|
||||
console.log('\n');
|
||||
spinner.text = "Installing dependencies";
|
||||
await execaCommand("bun install").pipeStdout(process.stdout);
|
||||
spinner.text = "";
|
||||
startCommand = "bun dev";
|
||||
console.log("\n");
|
||||
} else if (isYarnInstalled()) {
|
||||
await execaCommand('yarn').pipeStdout(process.stdout);
|
||||
startCommand = 'yarn dev';
|
||||
await execaCommand("yarn").pipeStdout(process.stdout);
|
||||
startCommand = "yarn dev";
|
||||
} else {
|
||||
spinner.text = 'Installing dependencies';
|
||||
await execa('npm', ['install', '--verbose']).pipeStdout(process.stdout);
|
||||
spinner.text = '';
|
||||
startCommand = 'npm run dev';
|
||||
spinner.text = "Installing dependencies";
|
||||
await execa("npm", ["install", "--verbose"]).pipeStdout(process.stdout);
|
||||
spinner.text = "";
|
||||
startCommand = "npm run dev";
|
||||
}
|
||||
|
||||
spinner.stop();
|
||||
await execa('git', ['add', '.']);
|
||||
await execa('git', ['commit', '-m', 'Initial commit']);
|
||||
await execa("git", ["add", "."]);
|
||||
await execa("git", ["commit", "-m", "Initial commit"]);
|
||||
|
||||
process.chdir('../');
|
||||
process.chdir("../");
|
||||
log(
|
||||
`${green.bold('Success!')} Created ${purple.bold(
|
||||
appName
|
||||
)} at ${process.cwd()} \n`
|
||||
`${green.bold("Success!")} Created ${purple.bold(
|
||||
appName,
|
||||
)} at ${process.cwd()} \n`,
|
||||
);
|
||||
log(
|
||||
`To get started, change into the new directory and run ${chalk.cyan(
|
||||
startCommand
|
||||
)}\n`
|
||||
startCommand,
|
||||
)}\n`,
|
||||
);
|
||||
} catch (err) {
|
||||
log('\n');
|
||||
log("\n");
|
||||
if (err.exitCode == 128) {
|
||||
log('Error: directory already exists.');
|
||||
log("Error: directory already exists.");
|
||||
}
|
||||
spinner.stop();
|
||||
}
|
||||
@ -137,7 +137,7 @@ main();
|
||||
|
||||
function isYarnInstalled() {
|
||||
try {
|
||||
childProcess.execSync('yarn --version');
|
||||
childProcess.execSync("yarn --version");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@ -146,7 +146,7 @@ function isYarnInstalled() {
|
||||
|
||||
function isBunInstalled() {
|
||||
try {
|
||||
childProcess.execSync('bun --version');
|
||||
childProcess.execSync("bun --version");
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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/serverless-proxy/nextjs";
|
||||
```
|
||||
3. Ensure you've set the `FAL_KEY` as an environment variable in your server, containing a valid API Key.
|
||||
|
||||
@ -31,7 +31,7 @@ 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/serverless-proxy/nextjs";
|
||||
|
||||
export const { GET, POST } = route;
|
||||
```
|
||||
@ -49,12 +49,12 @@ 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/serverless-proxy/express";
|
||||
|
||||
app.all(
|
||||
falProxy.route, // '/api/fal/proxy' or you can use your own
|
||||
cors(), // if external clients will use the proxy
|
||||
falProxy.handler
|
||||
falProxy.handler,
|
||||
);
|
||||
```
|
||||
|
||||
@ -65,10 +65,10 @@ 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 * as fal from "@fal-ai/serverless-client";
|
||||
|
||||
fal.config({
|
||||
proxyUrl: '/api/fal/proxy', // or https://my.app.com/api/fal/proxy
|
||||
proxyUrl: "/api/fal/proxy", // or https://my.app.com/api/fal/proxy
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'proxy',
|
||||
preset: '../../jest.preset.js',
|
||||
displayName: "proxy",
|
||||
preset: "../../jest.preset.js",
|
||||
globals: {},
|
||||
testEnvironment: 'node',
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'ts-jest',
|
||||
"^.+\\.[tj]sx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
tsconfig: "<rootDir>/tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/proxy',
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
|
||||
coverageDirectory: "../../coverage/libs/proxy",
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
import { DEFAULT_PROXY_ROUTE, handleRequest } from './index';
|
||||
import type { RequestHandler } from "express";
|
||||
import { DEFAULT_PROXY_ROUTE, handleRequest } from "./index";
|
||||
|
||||
/**
|
||||
* The default Express route for the fal.ai client proxy.
|
||||
@ -15,7 +15,7 @@ export const route = DEFAULT_PROXY_ROUTE;
|
||||
*/
|
||||
export const handler: RequestHandler = async (request, response, next) => {
|
||||
await handleRequest({
|
||||
id: 'express',
|
||||
id: "express",
|
||||
method: request.method,
|
||||
getRequestBody: async () => JSON.stringify(request.body),
|
||||
getHeaders: () => request.headers,
|
||||
@ -43,7 +43,7 @@ export const handler: RequestHandler = async (request, response, next) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (res.headers.get('content-type')?.includes('application/json')) {
|
||||
if (res.headers.get("content-type")?.includes("application/json")) {
|
||||
return response.status(res.status).json(await res.json());
|
||||
}
|
||||
return response.status(res.status).send(await res.text());
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export const TARGET_URL_HEADER = 'x-fal-target-url';
|
||||
export const TARGET_URL_HEADER = "x-fal-target-url";
|
||||
|
||||
export const DEFAULT_PROXY_ROUTE = '/api/fal/proxy';
|
||||
export const DEFAULT_PROXY_ROUTE = "/api/fal/proxy";
|
||||
|
||||
const FAL_KEY = process.env.FAL_KEY;
|
||||
const FAL_KEY_ID = process.env.FAL_KEY_ID;
|
||||
@ -54,7 +54,7 @@ function getFalKey(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const EXCLUDED_HEADERS = ['content-length', 'content-encoding'];
|
||||
const EXCLUDED_HEADERS = ["content-length", "content-encoding"];
|
||||
|
||||
/**
|
||||
* A request handler that proxies the request to the fal-serverless
|
||||
@ -66,7 +66,7 @@ const EXCLUDED_HEADERS = ['content-length', 'content-encoding'];
|
||||
* @returns Promise<any> the promise that will be resolved once the request is done.
|
||||
*/
|
||||
export async function handleRequest<ResponseType>(
|
||||
behavior: ProxyBehavior<ResponseType>
|
||||
behavior: ProxyBehavior<ResponseType>,
|
||||
) {
|
||||
const targetUrl = singleHeaderValue(behavior.getHeader(TARGET_URL_HEADER));
|
||||
if (!targetUrl) {
|
||||
@ -82,33 +82,33 @@ export async function handleRequest<ResponseType>(
|
||||
? await behavior.resolveApiKey()
|
||||
: getFalKey();
|
||||
if (!falKey) {
|
||||
return behavior.respondWith(401, 'Missing fal.ai credentials');
|
||||
return behavior.respondWith(401, "Missing fal.ai credentials");
|
||||
}
|
||||
|
||||
// pass over headers prefixed with x-fal-*
|
||||
const headers: Record<string, HeaderValue> = {};
|
||||
Object.keys(behavior.getHeaders()).forEach((key) => {
|
||||
if (key.toLowerCase().startsWith('x-fal-')) {
|
||||
if (key.toLowerCase().startsWith("x-fal-")) {
|
||||
headers[key.toLowerCase()] = behavior.getHeader(key);
|
||||
}
|
||||
});
|
||||
|
||||
const proxyUserAgent = `@fal-ai/serverless-proxy/${behavior.id}`;
|
||||
const userAgent = singleHeaderValue(behavior.getHeader('user-agent'));
|
||||
const userAgent = singleHeaderValue(behavior.getHeader("user-agent"));
|
||||
const res = await fetch(targetUrl, {
|
||||
method: behavior.method,
|
||||
headers: {
|
||||
...headers,
|
||||
authorization:
|
||||
singleHeaderValue(behavior.getHeader('authorization')) ??
|
||||
singleHeaderValue(behavior.getHeader("authorization")) ??
|
||||
`Key ${falKey}`,
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
'user-agent': userAgent,
|
||||
'x-fal-client-proxy': proxyUserAgent,
|
||||
accept: "application/json",
|
||||
"content-type": "application/json",
|
||||
"user-agent": userAgent,
|
||||
"x-fal-client-proxy": proxyUserAgent,
|
||||
} as HeadersInit,
|
||||
body:
|
||||
behavior.method?.toUpperCase() === 'GET'
|
||||
behavior.method?.toUpperCase() === "GET"
|
||||
? undefined
|
||||
: await behavior.getRequestBody(),
|
||||
});
|
||||
@ -124,7 +124,7 @@ export async function handleRequest<ResponseType>(
|
||||
}
|
||||
|
||||
export function fromHeaders(
|
||||
headers: Headers
|
||||
headers: Headers,
|
||||
): Record<string, string | string[]> {
|
||||
// TODO once Header.entries() is available, use that instead
|
||||
// Object.fromEntries(headers.entries());
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
import type { NextApiHandler } from 'next/types';
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import type { NextApiHandler } from "next/types";
|
||||
import {
|
||||
DEFAULT_PROXY_ROUTE,
|
||||
fromHeaders,
|
||||
handleRequest,
|
||||
responsePassthrough,
|
||||
} from './index';
|
||||
} from "./index";
|
||||
|
||||
/**
|
||||
* The default Next API route for the fal.ai client proxy.
|
||||
@ -24,15 +24,15 @@ export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE;
|
||||
*/
|
||||
export const handler: NextApiHandler = async (request, response) => {
|
||||
return handleRequest({
|
||||
id: 'nextjs-page-router',
|
||||
method: request.method || 'POST',
|
||||
id: "nextjs-page-router",
|
||||
method: request.method || "POST",
|
||||
getRequestBody: async () => JSON.stringify(request.body),
|
||||
getHeaders: () => request.headers,
|
||||
getHeader: (name) => request.headers[name],
|
||||
sendHeader: (name, value) => response.setHeader(name, value),
|
||||
respondWith: (status, data) => response.status(status).json(data),
|
||||
sendResponse: async (res) => {
|
||||
if (res.headers.get('content-type')?.includes('application/json')) {
|
||||
if (res.headers.get("content-type")?.includes("application/json")) {
|
||||
return response.status(res.status).json(await res.json());
|
||||
}
|
||||
return response.status(res.status).send(await res.text());
|
||||
@ -53,7 +53,7 @@ async function routeHandler(request: NextRequest) {
|
||||
// check if response if from a streaming request
|
||||
|
||||
return await handleRequest({
|
||||
id: 'nextjs-app-router',
|
||||
id: "nextjs-app-router",
|
||||
method: request.method,
|
||||
getRequestBody: async () => request.text(),
|
||||
getHeaders: () => fromHeaders(request.headers),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { type RequestHandler } from '@sveltejs/kit';
|
||||
import { fromHeaders, handleRequest, responsePassthrough } from './index';
|
||||
import { type RequestHandler } from "@sveltejs/kit";
|
||||
import { fromHeaders, handleRequest, responsePassthrough } from "./index";
|
||||
|
||||
type RequestHandlerParams = {
|
||||
/**
|
||||
@ -20,13 +20,13 @@ export const createRequestHandler = ({
|
||||
credentials,
|
||||
}: RequestHandlerParams = {}) => {
|
||||
const handler: RequestHandler = async ({ request }) => {
|
||||
const FAL_KEY = credentials || process.env.FAL_KEY || '';
|
||||
const FAL_KEY = credentials || process.env.FAL_KEY || "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const responseHeaders: Record<string, any> = {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
return await handleRequest({
|
||||
id: 'svelte-app-router',
|
||||
id: "svelte-app-router",
|
||||
method: request.method,
|
||||
getRequestBody: async () => request.text(),
|
||||
getHeaders: () => fromHeaders(request.headers),
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json",
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json",
|
||||
},
|
||||
],
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
91
package-lock.json
generated
91
package-lock.json
generated
@ -39,8 +39,7 @@
|
||||
"regenerator-runtime": "0.13.7",
|
||||
"robot3": "^0.4.1",
|
||||
"ts-morph": "^17.0.1",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid-random": "^1.3.2"
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.0",
|
||||
@ -97,8 +96,9 @@
|
||||
"nx-cloud": "16.4.0",
|
||||
"organize-imports-cli": "^0.10.0",
|
||||
"postcss": "8.4.21",
|
||||
"prettier": "^3.2.4",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"secretlint": "^7.0.7",
|
||||
"tailwindcss": "3.2.7",
|
||||
"ts-jest": "29.1.1",
|
||||
@ -25563,9 +25563,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz",
|
||||
"integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
@ -25597,6 +25597,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz",
|
||||
"integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.21.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||
"@prettier/plugin-pug": "*",
|
||||
"@shopify/prettier-plugin-liquid": "*",
|
||||
"@trivago/prettier-plugin-sort-imports": "*",
|
||||
"@zackad/prettier-plugin-twig-melody": "*",
|
||||
"prettier": "^3.0",
|
||||
"prettier-plugin-astro": "*",
|
||||
"prettier-plugin-css-order": "*",
|
||||
"prettier-plugin-import-sort": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-sort-imports": "*",
|
||||
"prettier-plugin-style-order": "*",
|
||||
"prettier-plugin-svelte": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ianvs/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@shopify/prettier-plugin-liquid": {
|
||||
"optional": true
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@zackad/prettier-plugin-twig-melody": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-astro": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-css-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-import-sort": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-jsdoc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-style-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
@ -29956,11 +30030,6 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid-random": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz",
|
||||
"integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ=="
|
||||
},
|
||||
"node_modules/v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
|
||||
12
package.json
12
package.json
@ -55,8 +55,7 @@
|
||||
"regenerator-runtime": "0.13.7",
|
||||
"robot3": "^0.4.1",
|
||||
"ts-morph": "^17.0.1",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid-random": "^1.3.2"
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.0",
|
||||
@ -113,8 +112,9 @@
|
||||
"nx-cloud": "16.4.0",
|
||||
"organize-imports-cli": "^0.10.0",
|
||||
"postcss": "8.4.21",
|
||||
"prettier": "^3.2.4",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"secretlint": "^7.0.7",
|
||||
"tailwindcss": "3.2.7",
|
||||
"ts-jest": "29.1.1",
|
||||
@ -122,5 +122,11 @@
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"prettier": {
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-tailwindcss"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user