Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remix proxy support #36

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nxreleaserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"changelog": true,
"npm": true,
"github": true,
"repositoryUrl": "https://github.com/fal-ai/serverless-js",
"repositoryUrl": "https://github.com/fal-ai/fal-js",
"branches": ["main"]
}
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

![@fal-ai/serverless-client npm package](https://img.shields.io/npm/v/@fal-ai/serverless-client?color=%237527D7&label=client&style=flat-square)
![@fal-ai/serverless-proxy npm package](https://img.shields.io/npm/v/@fal-ai/serverless-proxy?color=%237527D7&label=proxy&style=flat-square)
![Build](https://img.shields.io/github/actions/workflow/status/fal-ai/serverless-js/build.yml?style=flat-square)
![License](https://img.shields.io/github/license/fal-ai/serverless-js?style=flat-square)
![Build](https://img.shields.io/github/actions/workflow/status/fal-ai/fal-js/build.yml?style=flat-square)
![License](https://img.shields.io/github/license/fal-ai/fal-js?style=flat-square)

## About the Project

Expand Down Expand Up @@ -55,7 +55,7 @@ For example, if you are using Next.js, you can:
```sh
npm install --save @fal-ai/serverless-proxy
```
2. Add the proxy as an API endpoint of your app, see an example here in [pages/api/\fal/proxy.ts](https://github.com/fal-ai/serverless-js/blob/main/apps/demo-nextjs-app/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-app/pages/api/fal/proxy.ts)
```ts
export { handler as default } from '@fal-ai/serverless-proxy/nextjs';
```
Expand All @@ -74,7 +74,7 @@ See [libs/proxy](./libs/proxy/) for more details.

### The example Next.js app

You can find a minimal Next.js + fal application examples in [apps/demo-nextjs-page-router/](https://github.com/fal-ai/serverless-js/tree/main/apps/demo-nextjs-page-router).
You can find a minimal Next.js + fal application examples in [apps/demo-nextjs-page-router/](https://github.com/fal-ai/fal-js/tree/main/apps/demo-nextjs-page-router).

1. Run `npm install` on the repository root.
2. Create a `.env.local` file and add your API Key as `FAL_KEY` environment variable (or export it any other way your prefer).
Expand All @@ -84,22 +84,22 @@ Check our [Next.js integration docs](https://fal.ai/docs/integrations/nextjs) fo

## Roadmap

See the [open feature requests](https://github.com/fal-ai/serverless-js/labels/enhancement) for a list of proposed features and join the discussion.
See the [open feature requests](https://github.com/fal-ai/fal-js/labels/enhancement) for a list of proposed features and join the discussion.

## Contributing

Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.

1. Make sure you read our [Code of Conduct](https://github.com/fal-ai/serverless-js/blob/main/CODE_OF_CONDUCT.md)
1. Make sure you read our [Code of Conduct](https://github.com/fal-ai/fal-js/blob/main/CODE_OF_CONDUCT.md)
2. Fork the project and clone your fork
3. Setup the local environment with `npm install`
4. Create a feature branch (`git checkout -b feature/add-cool-thing`) or a bugfix branch (`git checkout -b fix/smash-that-bug`)
5. Commit the changes (`git commit -m 'feat(client): added a cool thing'`) - use [conventional commits](https://conventionalcommits.org)
6. Push to the branch (`git push --set-upstream origin feature/add-cool-thing`)
7. Open a Pull Request

Check the [good first issue queue](https://github.com/fal-ai/serverless-js/labels/good+first+issue), your contribution will be welcome!
Check the [good first issue queue](https://github.com/fal-ai/fal-js/labels/good+first+issue), your contribution will be welcome!

## License

Distributed under the MIT License. See [LICENSE](https://github.com/fal-ai/serverless-js/blob/main/LICENSE) for more information.
Distributed under the MIT License. See [LICENSE](https://github.com/fal-ai/fal-js/blob/main/LICENSE) for more information.
4 changes: 4 additions & 0 deletions apps/demo-remix-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.cache
build
public/build
.env
54 changes: 54 additions & 0 deletions apps/demo-remix-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Welcome to Nx + Remix!

- [Remix Docs](https://remix.run/docs)
- [Nx Docs](https://nx.dev)

## Development

From your terminal:

```sh
npx nx dev demo-remix-app
```

This starts your app in development mode, rebuilding assets on file changes.

## Deployment

First, build your app for production:

```sh
npx nx build demo-remix-app
```

Then run the app in production mode:

```sh
npx nx start demo-remix-app
```

Now you'll need to pick a host to deploy it to.

### DIY

If you're familiar with deploying node applications, the built-in Remix app server is production-ready.

Make sure to deploy the output of `remix build`

- `packages/demo-remix-app/build/`
- `packages/demo-remix-app/public/build/`

### Using a Template

When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.

```sh
cd ..
# create a new project, and pick a pre-configured host
npx create-remix@latest
cd my-new-remix-app
# remove the new project's app (not the old one!)
rm -rf app
# copy your app over
cp -R ../my-old-remix-app/app app
```
18 changes: 18 additions & 0 deletions apps/demo-remix-app/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* By default, Remix will handle hydrating your app on the client for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.client
*/

import { RemixBrowser } from '@remix-run/react';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';

startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});
137 changes: 137 additions & 0 deletions apps/demo-remix-app/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* By default, Remix will handle generating the HTTP Response for you.
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
* For more information, see https://remix.run/file-conventions/entry.server
*/

import { PassThrough } from 'node:stream';

import type { AppLoadContext, EntryContext } from '@remix-run/node';
import { createReadableStreamFromReadable } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';

const ABORT_DELAY = 5_000;

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext
) {
return isbot(request.headers.get('user-agent'))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
);
}

function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}

function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}
94 changes: 94 additions & 0 deletions apps/demo-remix-app/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as fal from '@fal-ai/serverless-client';
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import { useMemo, useState } from 'react';

fal.config({
proxyUrl: '/fal/proxy',
});

const DEFAULT_PROMPT =
'a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd';

export default function App() {
// Input state
const [prompt, setPrompt] = useState<string>(DEFAULT_PROMPT);
// Result state
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);

const image = useMemo(() => {
if (!result) {
return null;
}
return result.images[0];
}, [result]);

const reset = () => {
setLoading(false);
setResult(null);
};

const generateImage = async () => {
reset();
setLoading(true);
try {
const result = await fal.subscribe('110602490-lora', {
input: {
prompt,
model_name: 'stabilityai/stable-diffusion-xl-base-1.0',
image_size: 'square_hd',
},
pollInterval: 1000, // Default is 1000 (every 1s)
logs: true,
onQueueUpdate(update) {
console.log(update);
},
});
setResult(result);
} catch (error: any) {
console.error(error);
} finally {
setLoading(false);
}
};
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />

<div>
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>
<button onClick={generateImage}>Generate</button>
</div>
<div>
{loading && <div>Loading...</div>}
{image && (
<div>
<img src={image.url} alt="" />
</div>
)}
</div>
</body>
</html>
);
}
Loading
Loading