Skip to content
Merged
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 libs/client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fal-ai/client",
"description": "The fal.ai client for JavaScript and TypeScript",
"version": "1.9.1",
"version": "1.9.2",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/client/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

/**
* A function that provides a temporary authentication token.
* @param app - The app/endpoint identifier
* @param app - The app/endpoint identifier, including the path (e.g. "fal-ai/myapp/realtime")
* @returns A promise that resolves to the token string
*/
export type TokenProvider = (app: string) => Promise<string>;
Expand All @@ -19,7 +19,7 @@
config: RequiredConfig,
): Promise<string> {
const appId = parseEndpointId(app);
const token: string | object = await dispatchRequest<any, string>({

Check warning on line 22 in libs/client/src/auth.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
method: "POST",
targetUrl: `${getRestApiUrl()}/tokens/`,
config,
Expand Down
4 changes: 2 additions & 2 deletions libs/client/src/realtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
class MockWebSocket {
public readonly url: string;
public onopen?: () => void;
public onclose?: (event: any) => void;

Check warning on line 18 in libs/client/src/realtime.spec.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
public onerror?: (event: any) => void;

Check warning on line 19 in libs/client/src/realtime.spec.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
public onmessage?: (event: any) => void;

Check warning on line 20 in libs/client/src/realtime.spec.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
public readyState = MockWebSocket.CONNECTING;
public readonly send = jest.fn();
public readonly close = jest.fn();
Expand Down Expand Up @@ -48,7 +48,7 @@
beforeAll(() => {
// minimal fetch stub to satisfy createConfig
// eslint-disable-next-line @typescript-eslint/no-empty-function
global.fetch = jest.fn(() => {}) as any;

Check warning on line 51 in libs/client/src/realtime.spec.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
});

beforeEach(() => {
Expand All @@ -64,14 +64,14 @@
// eslint-disable-next-line @typescript-eslint/no-empty-function
jest.spyOn(console, "warn").mockImplementation(() => {});
// Provide a minimal crypto polyfill for randomUUID used by the client
(global as any).crypto = {

Check warning on line 67 in libs/client/src/realtime.spec.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
randomUUID: () => "00000000-0000-0000-0000-000000000000",
};
// @ts-expect-error override global
global.WebSocket = WebSocketMock;
config = createConfig({
credentials: "test-key",
fetch: global.fetch as any,

Check warning on line 74 in libs/client/src/realtime.spec.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
});
});

Expand Down Expand Up @@ -333,7 +333,7 @@
await Promise.resolve();

expect(customTokenProvider).toHaveBeenCalledTimes(1);
expect(customTokenProvider).toHaveBeenCalledWith("123-myapp");
expect(customTokenProvider).toHaveBeenCalledWith("123/myapp/realtime");
expect(getTemporaryAuthToken).not.toHaveBeenCalled();

expect(WebSocketMock).toHaveBeenCalledTimes(1);
Expand All @@ -359,7 +359,7 @@
await Promise.resolve();
await Promise.resolve();

expect(customTokenProvider).toHaveBeenCalledWith("456-otherapp");
expect(customTokenProvider).toHaveBeenCalledWith("456/otherapp/realtime");
expect(getTemporaryAuthToken).not.toHaveBeenCalled();
});

Expand Down
16 changes: 12 additions & 4 deletions libs/client/src/realtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
import { RequiredConfig } from "./config";
import { ApiError } from "./response";
import { isBrowser } from "./runtime";
import { ensureEndpointIdFormat, isReact, throttle } from "./utils";
import {
ensureEndpointIdFormat,
isReact,
resolveEndpointPath,
throttle,
} from "./utils";

// Define the context
interface Context {
Expand Down Expand Up @@ -312,8 +317,8 @@
queryParams.set("max_buffering", maxBuffering.toFixed(0));
}
const appId = ensureEndpointIdFormat(app);
const normalizedPath = path ? `/${path.replace(/^\/+/, "")}` : "/realtime";
return `wss://fal.run/${appId}${normalizedPath}?${queryParams.toString()}`;
const resolvedPath = resolveEndpointPath(app, path, "/realtime") ?? "";
return `wss://fal.run/${appId}${resolvedPath}?${queryParams.toString()}`;
}

const DEFAULT_THROTTLE_INTERVAL = 128;
Expand Down Expand Up @@ -567,8 +572,11 @@
) {
send({ type: "initiateAuth" });
// Use custom tokenProvider if provided, otherwise use default
const appId = ensureEndpointIdFormat(app);
const resolvedPath =
resolveEndpointPath(app, path, "/realtime") ?? "";
const fetchToken = tokenProvider
? () => tokenProvider(app)
? () => tokenProvider(`${appId}${resolvedPath}`)
: () => {
console.warn(
"[fal.realtime] Using the default token provider is deprecated. " +
Expand Down Expand Up @@ -640,7 +648,7 @@
}
send({ type: "connectionClosed", code: event.code });
};
ws.onerror = (event) => {

Check warning on line 651 in libs/client/src/realtime.ts

View workflow job for this annotation

GitHub Actions / build

'event' is defined but never used
// TODO specify error protocol for identified errors
const { onError = noop } = getCallbacks();
onError(new ApiError({ message: "Unknown error", status: 500 }));
Expand Down
8 changes: 6 additions & 2 deletions libs/client/src/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { buildUrl, dispatchRequest } from "./request";
import { ApiError, defaultResponseHandler } from "./response";
import { type StorageClient } from "./storage";
import { EndpointType, InputType, OutputType } from "./types/client";
import { ensureEndpointIdFormat, resolveEndpointPath } from "./utils";

export type StreamingConnectionMode = "client" | "server";

Expand Down Expand Up @@ -116,7 +117,7 @@ export class FalStream<Input, Output> {
this.url =
options.url ??
buildUrl(endpointId, {
path: "/stream",
path: resolveEndpointPath(endpointId, undefined, "/stream"),
query: options.queryParams,
});
this.options = options;
Expand Down Expand Up @@ -166,8 +167,11 @@ export class FalStream<Input, Output> {
if (connectionMode === "client") {
// if we are in the browser, we need to get a temporary token
// to authenticate the request
const appId = ensureEndpointIdFormat(endpointId);
const resolvedPath =
resolveEndpointPath(endpointId, undefined, "/stream") ?? "";
const fetchToken = tokenProvider
? () => tokenProvider(endpointId)
? () => tokenProvider(`${appId}${resolvedPath}`)
: () => {
console.warn(
"[fal.stream] Using the default token provider is deprecated. " +
Expand Down
24 changes: 24 additions & 0 deletions libs/client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@ export function parseEndpointId(id: string): EndpointId {
};
}

/**
* Resolves the endpoint path, normalizing it and applying a default.
* If no explicit path is provided and the app already ends with the
* default path, returns undefined to avoid duplication.
*
* @param app - The app/endpoint identifier
* @param path - An explicitly provided path (always used if present)
* @param defaultPath - The default path to use (e.g. "/realtime")
* @returns The resolved path, or undefined if the app already includes it
*/
export function resolveEndpointPath(
app: string,
path: string | undefined,
defaultPath: string,
): string | undefined {
if (path) {
return `/${path.replace(/^\/+/, "")}`;
}
if (app.endsWith(defaultPath)) {
return undefined;
}
return defaultPath;
}

export function isValidUrl(url: string) {
try {
const { host } = new URL(url);
Expand Down
Loading