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
This commit is contained in:
Daniel Rochetti 2024-04-06 01:19:38 -07:00 committed by GitHub
parent 335b817e9c
commit dfeb8689f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 82 additions and 13 deletions

View File

@ -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.9.0", "version": "0.9.1",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
import { getRestApiUrl } from './config'; import { getRestApiUrl } from './config';
import { dispatchRequest } from './request'; import { dispatchRequest } from './request';
import { ensureAppIdFormat } from './utils'; import { parseAppId } from './utils';
export const TOKEN_EXPIRATION_SECONDS = 120; 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. * Get a token to connect to the realtime endpoint.
*/ */
export async function getTemporaryAuthToken(app: string): Promise<string> { export async function getTemporaryAuthToken(app: string): Promise<string> {
const [, appAlias] = ensureAppIdFormat(app).split('/'); 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: [appAlias], allowed_apps: [appId.alias],
token_expiration: TOKEN_EXPIRATION_SECONDS, token_expiration: TOKEN_EXPIRATION_SECONDS,
} }
); );

View File

@ -1,7 +1,7 @@
import { dispatchRequest } from './request'; import { dispatchRequest } from './request';
import { storageImpl } from './storage'; import { storageImpl } from './storage';
import { EnqueueResult, QueueStatus } from './types'; 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 * The function input and other configuration when running
@ -284,8 +284,9 @@ export const queue: Queue = {
id: string, id: string,
{ requestId, logs = false }: QueueStatusOptions { requestId, logs = false }: QueueStatusOptions
): Promise<QueueStatus> { ): Promise<QueueStatus> {
const [appOwner, appAlias] = ensureAppIdFormat(id).split('/'); const appId = parseAppId(id);
return send(`${appOwner}/${appAlias}`, { const prefix = appId.namespace ? `${appId.namespace}/` : '';
return send(`${prefix}${appId.owner}/${appId.alias}`, {
subdomain: 'queue', subdomain: 'queue',
method: 'get', method: 'get',
path: `/requests/${requestId}/status`, path: `/requests/${requestId}/status`,
@ -298,8 +299,9 @@ export const queue: Queue = {
id: string, id: string,
{ requestId }: BaseQueueOptions { requestId }: BaseQueueOptions
): Promise<Output> { ): Promise<Output> {
const [appOwner, appAlias] = ensureAppIdFormat(id).split('/'); const appId = parseAppId(id);
return send(`${appOwner}/${appAlias}`, { const prefix = appId.namespace ? `${appId.namespace}/` : '';
return send(`${prefix}${appId.owner}/${appId.alias}`, {
subdomain: 'queue', subdomain: 'queue',
method: 'get', method: 'get',
path: `/requests/${requestId}`, path: `/requests/${requestId}`,

View File

@ -16,7 +16,7 @@ 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, parseAppId, throttle } from './utils';
// Define the context // Define the context
interface Context { interface Context {
@ -273,9 +273,9 @@ function buildRealtimeUrl(
queryParams.set('max_buffering', maxBuffering.toFixed(0)); queryParams.set('max_buffering', maxBuffering.toFixed(0));
} }
const appId = ensureAppIdFormat(app); const appId = ensureAppIdFormat(app);
const [, appAlias] = ensureAppIdFormat(app).split('/'); const { alias } = parseAppId(appId);
const suffix = 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()}`; return `wss://fal.run/${appId}/${suffix}?${queryParams.toString()}`;
} }

View File

@ -1,5 +1,5 @@
import uuid from 'uuid-random'; import uuid from 'uuid-random';
import { ensureAppIdFormat, isUUIDv4 } from './utils'; import { ensureAppIdFormat, isUUIDv4, parseAppId } from './utils';
describe('The utils test suite', () => { describe('The utils test suite', () => {
it('should match a valid v4 uuid', () => { it('should match a valid v4 uuid', () => {
@ -31,4 +31,42 @@ describe('The utils test suite', () => {
const id = 'just-an-id'; const id = 'just-an-id';
expect(() => ensureAppIdFormat(id)).toThrowError(); 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',
});
});
}); });

View File

@ -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) { export function isValidUrl(url: string) {
try { try {
const { host } = new URL(url); const { host } = new URL(url);