From dfeb8689f4b21945db099e241680dad16fcff744 Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Sat, 6 Apr 2024 01:19:38 -0700 Subject: [PATCH] feat: support app id with namespaces (#61) * feat: support app id with namespaces * chore: remove unwanted page * fix: queue status url * chore: bump version for release --- libs/client/package.json | 2 +- libs/client/src/auth.ts | 6 +++--- libs/client/src/function.ts | 12 ++++++----- libs/client/src/realtime.ts | 6 +++--- libs/client/src/utils.spec.ts | 40 ++++++++++++++++++++++++++++++++++- libs/client/src/utils.ts | 29 +++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/libs/client/package.json b/libs/client/package.json index 364dfeb..35d4379 100644 --- a/libs/client/package.json +++ b/libs/client/package.json @@ -1,7 +1,7 @@ { "name": "@fal-ai/serverless-client", "description": "The fal serverless JS/TS client", - "version": "0.9.0", + "version": "0.9.1", "license": "MIT", "repository": { "type": "git", diff --git a/libs/client/src/auth.ts b/libs/client/src/auth.ts index 6c748b3..403f27c 100644 --- a/libs/client/src/auth.ts +++ b/libs/client/src/auth.ts @@ -1,6 +1,6 @@ import { getRestApiUrl } from './config'; import { dispatchRequest } from './request'; -import { ensureAppIdFormat } from './utils'; +import { parseAppId } from './utils'; export const TOKEN_EXPIRATION_SECONDS = 120; @@ -8,12 +8,12 @@ export const TOKEN_EXPIRATION_SECONDS = 120; * Get a token to connect to the realtime endpoint. */ export async function getTemporaryAuthToken(app: string): Promise { - const [, appAlias] = ensureAppIdFormat(app).split('/'); + const appId = parseAppId(app); const token: string | object = await dispatchRequest( 'POST', `${getRestApiUrl()}/tokens/`, { - allowed_apps: [appAlias], + allowed_apps: [appId.alias], token_expiration: TOKEN_EXPIRATION_SECONDS, } ); diff --git a/libs/client/src/function.ts b/libs/client/src/function.ts index 1be73c5..51ee3fe 100644 --- a/libs/client/src/function.ts +++ b/libs/client/src/function.ts @@ -1,7 +1,7 @@ import { dispatchRequest } from './request'; import { storageImpl } from './storage'; import { EnqueueResult, QueueStatus } from './types'; -import { ensureAppIdFormat, isUUIDv4, isValidUrl } from './utils'; +import { ensureAppIdFormat, isUUIDv4, isValidUrl, parseAppId } from './utils'; /** * The function input and other configuration when running @@ -284,8 +284,9 @@ export const queue: Queue = { id: string, { requestId, logs = false }: QueueStatusOptions ): Promise { - const [appOwner, appAlias] = ensureAppIdFormat(id).split('/'); - return send(`${appOwner}/${appAlias}`, { + const appId = parseAppId(id); + const prefix = appId.namespace ? `${appId.namespace}/` : ''; + return send(`${prefix}${appId.owner}/${appId.alias}`, { subdomain: 'queue', method: 'get', path: `/requests/${requestId}/status`, @@ -298,8 +299,9 @@ export const queue: Queue = { id: string, { requestId }: BaseQueueOptions ): Promise { - const [appOwner, appAlias] = ensureAppIdFormat(id).split('/'); - return send(`${appOwner}/${appAlias}`, { + const appId = parseAppId(id); + const prefix = appId.namespace ? `${appId.namespace}/` : ''; + return send(`${prefix}${appId.owner}/${appId.alias}`, { subdomain: 'queue', method: 'get', path: `/requests/${requestId}`, diff --git a/libs/client/src/realtime.ts b/libs/client/src/realtime.ts index 9b66690..3bb65bd 100644 --- a/libs/client/src/realtime.ts +++ b/libs/client/src/realtime.ts @@ -16,7 +16,7 @@ import uuid from 'uuid-random'; import { TOKEN_EXPIRATION_SECONDS, getTemporaryAuthToken } from './auth'; import { ApiError } from './response'; import { isBrowser } from './runtime'; -import { ensureAppIdFormat, isReact, throttle } from './utils'; +import { ensureAppIdFormat, isReact, parseAppId, throttle } from './utils'; // Define the context interface Context { @@ -273,9 +273,9 @@ function buildRealtimeUrl( queryParams.set('max_buffering', maxBuffering.toFixed(0)); } const appId = ensureAppIdFormat(app); - const [, appAlias] = ensureAppIdFormat(app).split('/'); + const { alias } = parseAppId(appId); const suffix = - LEGACY_APPS.includes(appAlias) || !app.includes('/') ? 'ws' : 'realtime'; + LEGACY_APPS.includes(alias) || !app.includes('/') ? 'ws' : 'realtime'; return `wss://fal.run/${appId}/${suffix}?${queryParams.toString()}`; } diff --git a/libs/client/src/utils.spec.ts b/libs/client/src/utils.spec.ts index 443a11e..dcd6d58 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 { ensureAppIdFormat, isUUIDv4 } from './utils'; +import { ensureAppIdFormat, isUUIDv4, parseAppId } from './utils'; describe('The utils test suite', () => { it('should match a valid v4 uuid', () => { @@ -31,4 +31,42 @@ describe('The utils test suite', () => { const id = 'just-an-id'; expect(() => ensureAppIdFormat(id)).toThrowError(); }); + + it('should parse a legacy app id', () => { + const id = '12345-abcde-fgh'; + const parsed = parseAppId(id); + expect(parsed).toEqual({ + owner: '12345', + alias: 'abcde-fgh', + }); + }); + + it('should parse a current app id', () => { + const id = 'fal-ai/fast-sdxl'; + const parsed = parseAppId(id); + expect(parsed).toEqual({ + owner: 'fal-ai', + alias: 'fast-sdxl', + }); + }); + + it('should parse a current app id with path', () => { + const id = 'fal-ai/fast-sdxl/image-to-image'; + const parsed = parseAppId(id); + expect(parsed).toEqual({ + owner: 'fal-ai', + alias: 'fast-sdxl', + path: 'image-to-image', + }); + }); + + it('should parse a current app id with namespace', () => { + const id = 'workflows/fal-ai/fast-sdxl'; + const parsed = parseAppId(id); + expect(parsed).toEqual({ + owner: 'fal-ai', + alias: 'fast-sdxl', + namespace: 'workflows', + }); + }); }); diff --git a/libs/client/src/utils.ts b/libs/client/src/utils.ts index 5de1198..8f6fd22 100644 --- a/libs/client/src/utils.ts +++ b/libs/client/src/utils.ts @@ -21,6 +21,35 @@ export function ensureAppIdFormat(id: string): string { ); } +const APP_NAMESPACES = ['workflows'] as const; + +type AppNamespace = (typeof APP_NAMESPACES)[number]; + +export type AppId = { + readonly owner: string; + readonly alias: string; + readonly path?: string; + readonly namespace?: AppNamespace; +}; + +export function parseAppId(id: string): AppId { + const normalizedId = ensureAppIdFormat(id); + const parts = normalizedId.split('/'); + if (APP_NAMESPACES.includes(parts[0] as any)) { + return { + owner: parts[1], + alias: parts[2], + path: parts.slice(3).join('/') || undefined, + namespace: parts[0] as AppNamespace, + }; + } + return { + owner: parts[0], + alias: parts[1], + path: parts.slice(2).join('/') || undefined, + }; +} + export function isValidUrl(url: string) { try { const { host } = new URL(url);