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