diff --git a/apps/demo-nextjs-app-router/app/page.tsx b/apps/demo-nextjs-app-router/app/page.tsx index e3a6062..a24c7c5 100644 --- a/apps/demo-nextjs-app-router/app/page.tsx +++ b/apps/demo-nextjs-app-router/app/page.tsx @@ -82,7 +82,7 @@ export default function Home() { const start = Date.now(); try { const result: Result = await fal.subscribe( - '54285744-illusion-diffusion', + '54285744/illusion-diffusion', { input: { prompt, diff --git a/apps/demo-nextjs-page-router/pages/index.tsx b/apps/demo-nextjs-page-router/pages/index.tsx index 0f121ab..6bde8b6 100644 --- a/apps/demo-nextjs-page-router/pages/index.tsx +++ b/apps/demo-nextjs-page-router/pages/index.tsx @@ -74,13 +74,13 @@ export function Index() { setLoading(true); const start = Date.now(); try { - const result: Result = await fal.subscribe('110602490-lora', { + const result: Result = await fal.subscribe('110602490/lora', { input: { prompt, model_name: 'stabilityai/stable-diffusion-xl-base-1.0', image_size: 'square_hd', }, - pollInterval: 5000, // Default is 1000 (every 1s) + pollInterval: 3000, // Default is 1000 (every 1s) logs: true, onQueueUpdate(update) { setElapsedTime(Date.now() - start); diff --git a/libs/client/src/function.spec.ts b/libs/client/src/function.spec.ts index 4b695fb..6c47667 100644 --- a/libs/client/src/function.spec.ts +++ b/libs/client/src/function.spec.ts @@ -1,5 +1,4 @@ import uuid from 'uuid-random'; -import { getConfig } from './config'; import { buildUrl } from './function'; describe('The function test suite', () => { @@ -9,10 +8,10 @@ describe('The function test suite', () => { expect(url).toMatch(`trigger/12345/${id}`); }); - it('should build the URL with a function user-id/app-alias', () => { + it('should build the URL with a function user-id-app-alias', () => { const alias = '12345-some-alias'; const url = buildUrl(alias); - expect(url).toMatch(`${alias}.gateway.alpha.fal.ai`); + expect(url).toMatch(`fal.run/12345/some-alias`); }); it('should build the URL with a function username/app-alias', () => { diff --git a/libs/client/src/function.ts b/libs/client/src/function.ts index 89dd934..92f297a 100644 --- a/libs/client/src/function.ts +++ b/libs/client/src/function.ts @@ -1,8 +1,7 @@ -import { getConfig } from './config'; import { dispatchRequest } from './request'; import { storageImpl } from './storage'; import { EnqueueResult, QueueStatus } from './types'; -import { isLegacyAppId, isUUIDv4, isValidUrl } from './utils'; +import { ensureAppIdFormat, isUUIDv4, isValidUrl } from './utils'; /** * The function input and other configuration when running @@ -36,6 +35,15 @@ type RunOptions = { readonly autoUpload?: boolean; }; +type ExtraOptions = { + /** + * If `true`, the function will use the queue to run the function + * asynchronously and return the result in a separate call. This + * influences how the URL is built. + */ + readonly subdomain?: string; +}; + /** * Builds the final url to run the function based on its `id` or alias and * a the options from `RunOptions`. @@ -47,7 +55,7 @@ type RunOptions = { */ export function buildUrl( id: string, - options: RunOptions = {} + options: RunOptions & ExtraOptions = {} ): string { const method = (options.method ?? 'post').toLowerCase(); const path = (options.path ?? '').replace(/^\//, '').replace(/\/{2,}/, '/'); @@ -65,26 +73,20 @@ export function buildUrl( } // TODO remove this after some time, fal.run should be preferred - const host = 'gateway.alpha.fal.ai'; if (parts.length === 2 && isUUIDv4(parts[1])) { + const host = 'gateway.shark.fal.ai'; return `https://${host}/trigger/${id}/${path}${queryParams}`; } - if (isLegacyAppId(id)) { - return `https://${id}.${host}/${path}${queryParams}`; - } - return `https://fal.run/${id}/${path}${queryParams}`; + const appId = ensureAppIdFormat(id); + const subdomain = options.subdomain ? `${options.subdomain}.` : ''; + const url = `https://${subdomain}matteo.shark.fal.run/${appId}/${path}`; + return `${url.replace(/\/$/, '')}${queryParams}`; } -/** - * Runs a fal serverless function identified by its `id`. - * - * @param id the registered function revision id or alias. - * @returns the remote function output - */ -export async function run( +export async function send( id: string, - options: RunOptions = {} + options: RunOptions & ExtraOptions = {} ): Promise { const input = options.input && options.autoUpload !== false @@ -97,6 +99,19 @@ export async function run( ); } +/** + * Runs a fal serverless function identified by its `id`. + * + * @param id the registered function revision id or alias. + * @returns the remote function output + */ +export async function run( + id: string, + options: RunOptions = {} +): Promise { + return send(id, options); +} + /** * Subscribes to updates for a specific request in the queue. * @@ -257,19 +272,21 @@ export const queue: Queue = { const query = webhookUrl ? '?' + new URLSearchParams({ fal_webhook: webhookUrl }).toString() : ''; - return run(id, { + return send(id, { ...runOptions, + subdomain: 'queue', method: 'post', - path: '/fal/queue/submit' + path + query, + path: path + query, }); }, async status( id: string, { requestId, logs = false }: QueueStatusOptions ): Promise { - return run(id, { + return send(id, { + subdomain: 'queue', method: 'get', - path: `/fal/queue/requests/${requestId}/status`, + path: `/requests/${requestId}/status`, input: { logs: logs ? '1' : '0', }, @@ -279,9 +296,10 @@ export const queue: Queue = { id: string, { requestId }: BaseQueueOptions ): Promise { - return run(id, { + return send(id, { + subdomain: 'queue', method: 'get', - path: `/fal/queue/requests/${requestId}/response`, + path: `/requests/${requestId}`, }); }, subscribe, diff --git a/libs/client/src/realtime.ts b/libs/client/src/realtime.ts index 1b23337..1edfa5d 100644 --- a/libs/client/src/realtime.ts +++ b/libs/client/src/realtime.ts @@ -13,11 +13,11 @@ import { transition, } from 'robot3'; import uuid from 'uuid-random'; -import { getConfig, getRestApiUrl } from './config'; +import { getRestApiUrl } from './config'; import { dispatchRequest } from './request'; import { ApiError } from './response'; import { isBrowser } from './runtime'; -import { isLegacyAppId, isReact, throttle } from './utils'; +import { ensureAppIdFormat, isReact, throttle } from './utils'; // Define the context interface Context { @@ -262,10 +262,8 @@ function buildRealtimeUrl( if (maxBuffering !== undefined) { queryParams.set('max_buffering', maxBuffering.toFixed(0)); } - if (isLegacyAppId(app)) { - return `wss://${app}.gateway.alpha.fal.ai/ws?${queryParams.toString()}`; - } - return `wss://fal.run/${app}/ws?${queryParams.toString()}`; + const appId = ensureAppIdFormat(app); + return `wss://fal.run/${appId}/ws?${queryParams.toString()}`; } const TOKEN_EXPIRATION_SECONDS = 120; @@ -287,7 +285,7 @@ async function getToken(app: string): Promise { const [_, ...appAlias] = app.split('-'); const token: string | object = await dispatchRequest( 'POST', - `https://${getRestApiUrl()}/tokens/`, + `${getRestApiUrl()}/tokens/`, { allowed_apps: [appAlias.join('-')], token_expiration: TOKEN_EXPIRATION_SECONDS, diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index d150114..203bbbd 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -75,7 +75,7 @@ async function initiateUpload(file: Blob): Promise { file.name || `${Date.now()}.${getExtensionFromContentType(contentType)}`; return await dispatchRequest( 'POST', - `https://${getRestApiUrl()}/storage/upload/initiate`, + `${getRestApiUrl()}/storage/upload/initiate`, { content_type: contentType, file_name: filename, diff --git a/libs/client/src/utils.spec.ts b/libs/client/src/utils.spec.ts index 0140d96..e995936 100644 --- a/libs/client/src/utils.spec.ts +++ b/libs/client/src/utils.spec.ts @@ -1,5 +1,5 @@ import uuid from 'uuid-random'; -import { isUUIDv4 } from './utils'; +import { ensureAppIdFormat, isUUIDv4 } from './utils'; describe('The utils test suite', () => { it('should match a valid v4 uuid', () => { @@ -11,4 +11,19 @@ describe('The utils test suite', () => { const id = 'e726b886-e2c2-11ed-b5ea-0242ac120002'; expect(isUUIDv4(id)).toBe(false); }); + + it('shoud match match a legacy appOwner-appId format', () => { + const id = '12345-abcde-fgh'; + expect(ensureAppIdFormat(id)).toBe('12345/abcde-fgh'); + }); + + it('shoud match a current appOwner/appId format', () => { + const id = 'fal-ai/fast-sdxl'; + expect(ensureAppIdFormat(id)).toBe(id); + }); + + it('should throw on an invalid app id format', () => { + const id = 'just-an-id'; + expect(() => ensureAppIdFormat(id)).toThrowError(); + }); }); diff --git a/libs/client/src/utils.ts b/libs/client/src/utils.ts index 85ae140..841f738 100644 --- a/libs/client/src/utils.ts +++ b/libs/client/src/utils.ts @@ -7,8 +7,18 @@ export function isUUIDv4(id: string): boolean { ); } -export function isLegacyAppId(id: string): boolean { - return /^\d+-[a-zA-Z0-9-]+/.test(id); +export function ensureAppIdFormat(id: string): string { + const parts = id.split('/'); + if (parts.length === 2) { + return id; + } + const [, appOwner, appId] = /^([0-9]+)-([a-zA-Z0-9-]+)$/.exec(id); + if (appOwner && appId) { + return `${appOwner}/${appId}`; + } + throw new Error( + `Invalid app id: ${id}. Must be in the format /` + ); } export function isValidUrl(url: string) {