From e4bab020145062351fccb31b87e98c2cbbe32dcd Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Thu, 12 Oct 2023 12:26:54 -0700 Subject: [PATCH 1/9] feat: client file upload --- apps/demo-nextjs-app/pages/index.tsx | 2 +- libs/client/package.json | 2 +- libs/client/src/file.ts | 81 ++++++++++++++++++++++++++++ libs/client/src/function.ts | 15 ++++-- libs/client/src/index.ts | 2 + 5 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 libs/client/src/file.ts diff --git a/apps/demo-nextjs-app/pages/index.tsx b/apps/demo-nextjs-app/pages/index.tsx index c56dbfd..2c16f3a 100644 --- a/apps/demo-nextjs-app/pages/index.tsx +++ b/apps/demo-nextjs-app/pages/index.tsx @@ -71,7 +71,7 @@ export function Index() { setLoading(true); const start = Date.now(); try { - const result: Result = await fal.queue.subscribe('110602490-lora', { + const result: Result = await fal.subscribe('110602490-lora', { input: { prompt, model_name: 'stabilityai/stable-diffusion-xl-base-1.0', diff --git a/libs/client/package.json b/libs/client/package.json index e372661..4c5ed4e 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.3.2", + "version": "0.4.0", "license": "MIT", "repository": { "type": "git", diff --git a/libs/client/src/file.ts b/libs/client/src/file.ts new file mode 100644 index 0000000..5d4ab74 --- /dev/null +++ b/libs/client/src/file.ts @@ -0,0 +1,81 @@ +export type UploadOptions = { + filename?: string; +}; + +/** + * File support for the client. This interface establishes the contract for + * uploading files to the server and transforming the input to replace file + * objects with URLs. + */ +export interface FileSupport { + /** + * Upload a file to the server. Returns the URL of the uploaded file. + * @param file the file to upload + * @param options optional parameters, such as custom file name + * @returns the URL of the uploaded file + */ + upload: (file: Blob, options?: UploadOptions) => Promise; + + /** + * Transform the input to replace file objects with URLs. This is used + * to transform the input before sending it to the server and ensures + * that the server receives URLs instead of file objects. + * + * @param input the input to transform. + * @returns the transformed input. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transformInput: (input: Record) => Promise>; +} + +function isDataUri(uri: string): boolean { + // avoid uri parsing if it doesn't start with data: + if (!uri.startsWith('data:')) { + return false; + } + try { + const url = new URL(uri); + return url.protocol === 'data:'; + } catch (_) { + return false; + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type KeyValuePair = [string, any]; + +export const fileSupport: FileSupport = { + upload: async (file: Blob, options?: UploadOptions) => { + const { filename } = options || {}; + const formData = new FormData(); + formData.append('file', file, filename ?? file.name); + + const response = await fetch('https://rest.alpha.fal.ai/storage/upload', { + method: 'POST', + body: formData, + }); + const { url } = await response.json(); + return url; + }, + + transformInput: async (input: Record) => { + const promises = Object.entries(input).map(async ([key, value]) => { + if ( + value instanceof Blob || + (typeof value === 'string' && isDataUri(value)) + ) { + let blob = value; + // if string is a data uri, convert to blob + if (typeof value === 'string' && isDataUri(value)) { + const response = await fetch(value); + blob = await response.blob(); + } + const url = await fileSupport.upload(blob as Blob); + return [key, url]; + } + return [key, value] as KeyValuePair; + }); + const results = await Promise.all(promises); + return Object.fromEntries(results); + }, +}; diff --git a/libs/client/src/function.ts b/libs/client/src/function.ts index 01f5ae0..5566cf6 100644 --- a/libs/client/src/function.ts +++ b/libs/client/src/function.ts @@ -1,4 +1,5 @@ import { getConfig } from './config'; +import { fileSupport } from './file'; import { getUserAgent, isBrowser } from './runtime'; import { EnqueueResult, QueueStatus } from './types'; import { isUUIDv4, isValidUrl } from './utils'; @@ -23,6 +24,11 @@ type RunOptions = { * The HTTP method, defaults to `post`; */ readonly method?: 'get' | 'post' | 'put' | 'delete' | string; + + /** + * Whether to auto-upload files in the input. Defaults to `true`. + */ + readonly uploadFiles?: boolean; }; /** @@ -99,14 +105,15 @@ export async function run( ...userAgent, ...(headers ?? {}), } as HeadersInit; + const input = + options.input && options.uploadFiles !== false + ? await fileSupport.transformInput(options.input) + : options.input; const response = await fetch(url, { method, headers: requestHeaders, mode: 'cors', - body: - method !== 'get' && options.input - ? JSON.stringify(options.input) - : undefined, + body: method !== 'get' && input ? JSON.stringify(input) : undefined, }); return await responseHandler(response); } diff --git a/libs/client/src/index.ts b/libs/client/src/index.ts index 14a832b..8a1c705 100644 --- a/libs/client/src/index.ts +++ b/libs/client/src/index.ts @@ -1,4 +1,6 @@ export { config, getConfig } from './config'; +export { fileSupport as file } from './file'; +export type { UploadOptions } from './file'; export { queue, run, subscribe } from './function'; export { withMiddleware, withProxy } from './middleware'; export type { RequestMiddleware } from './middleware'; From 4a7dc059f75919709e523cef652629bcfe5369a3 Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Tue, 24 Oct 2023 07:38:05 -0700 Subject: [PATCH 2/9] feat: signed upload --- libs/client/src/function.ts | 58 +++++-------------------- libs/client/src/index.ts | 4 +- libs/client/src/request.ts | 47 ++++++++++++++++++++ libs/client/src/{file.ts => storage.ts} | 50 +++++++++++++++++---- 4 files changed, 100 insertions(+), 59 deletions(-) create mode 100644 libs/client/src/request.ts rename libs/client/src/{file.ts => storage.ts} (66%) diff --git a/libs/client/src/function.ts b/libs/client/src/function.ts index 5566cf6..b9672ed 100644 --- a/libs/client/src/function.ts +++ b/libs/client/src/function.ts @@ -1,6 +1,6 @@ import { getConfig } from './config'; -import { fileSupport } from './file'; -import { getUserAgent, isBrowser } from './runtime'; +import { storageImpl } from './storage'; +import { dispatchRequest } from './request'; import { EnqueueResult, QueueStatus } from './types'; import { isUUIDv4, isValidUrl } from './utils'; @@ -24,11 +24,6 @@ type RunOptions = { * The HTTP method, defaults to `post`; */ readonly method?: 'get' | 'post' | 'put' | 'delete' | string; - - /** - * Whether to auto-upload files in the input. Defaults to `true`. - */ - readonly uploadFiles?: boolean; }; /** @@ -67,7 +62,6 @@ export function buildUrl( /** * Runs a fal serverless function identified by its `id`. - * TODO: expand documentation and provide examples * * @param id the registered function revision id or alias. * @returns the remote function output @@ -76,46 +70,14 @@ export async function run( id: string, options: RunOptions = {} ): Promise { - const { - credentials: credentialsValue, - requestMiddleware, - responseHandler, - } = getConfig(); - const method = (options.method ?? 'post').toLowerCase(); - const userAgent = isBrowser() ? {} : { 'User-Agent': getUserAgent() }; - const credentials = - typeof credentialsValue === 'function' - ? credentialsValue() - : credentialsValue; - - const { url, headers } = await requestMiddleware({ - url: buildUrl(id, options), - }); - const authHeader = credentials ? { Authorization: `Key ${credentials}` } : {}; - if (typeof window !== 'undefined' && credentials) { - console.warn( - "The fal credentials are exposed in the browser's environment. " + - "That's not recommended for production use cases." - ); - } - const requestHeaders = { - ...authHeader, - Accept: 'application/json', - 'Content-Type': 'application/json', - ...userAgent, - ...(headers ?? {}), - } as HeadersInit; - const input = - options.input && options.uploadFiles !== false - ? await fileSupport.transformInput(options.input) - : options.input; - const response = await fetch(url, { - method, - headers: requestHeaders, - mode: 'cors', - body: method !== 'get' && input ? JSON.stringify(input) : undefined, - }); - return await responseHandler(response); + const input = options.input + ? await storageImpl.transformInput(options.input) + : options.input; + return dispatchRequest( + options.method ?? 'post', + buildUrl(id, options), + input as Input + ); } /** diff --git a/libs/client/src/index.ts b/libs/client/src/index.ts index 8a1c705..7f4cdfa 100644 --- a/libs/client/src/index.ts +++ b/libs/client/src/index.ts @@ -1,6 +1,6 @@ export { config, getConfig } from './config'; -export { fileSupport as file } from './file'; -export type { UploadOptions } from './file'; +export { storageImpl as storage } from './storage'; +export type { UploadOptions } from './storage'; export { queue, run, subscribe } from './function'; export { withMiddleware, withProxy } from './middleware'; export type { RequestMiddleware } from './middleware'; diff --git a/libs/client/src/request.ts b/libs/client/src/request.ts new file mode 100644 index 0000000..bf86683 --- /dev/null +++ b/libs/client/src/request.ts @@ -0,0 +1,47 @@ +import { getConfig } from './config'; +import { getUserAgent, isBrowser } from './runtime'; + +export async function dispatchRequest( + method: string, + targetUrl: string, + input: Input +): Promise { + const { + credentials: credentialsValue, + requestMiddleware, + responseHandler, + } = getConfig(); + const userAgent = isBrowser() ? {} : { 'User-Agent': getUserAgent() }; + const credentials = + typeof credentialsValue === 'function' + ? credentialsValue() + : credentialsValue; + + const { url, headers } = await requestMiddleware({ + url: targetUrl, + }); + const authHeader = credentials ? { Authorization: `Key ${credentials}` } : {}; + if (typeof window !== 'undefined' && credentials) { + console.warn( + "The fal credentials are exposed in the browser's environment. " + + "That's not recommended for production use cases." + ); + } + const requestHeaders = { + ...authHeader, + Accept: 'application/json', + 'Content-Type': 'application/json', + ...userAgent, + ...(headers ?? {}), + } as HeadersInit; + const response = await fetch(url, { + method, + headers: requestHeaders, + mode: 'cors', + body: + method.toLowerCase() !== 'get' && input + ? JSON.stringify(input) + : undefined, + }); + return await responseHandler(response); +} diff --git a/libs/client/src/file.ts b/libs/client/src/storage.ts similarity index 66% rename from libs/client/src/file.ts rename to libs/client/src/storage.ts index 5d4ab74..9cd2560 100644 --- a/libs/client/src/file.ts +++ b/libs/client/src/storage.ts @@ -1,3 +1,6 @@ +import { getConfig } from './config'; +import { dispatchRequest } from './request'; + export type UploadOptions = { filename?: string; }; @@ -7,7 +10,7 @@ export type UploadOptions = { * uploading files to the server and transforming the input to replace file * objects with URLs. */ -export interface FileSupport { +export interface StorageSupport { /** * Upload a file to the server. Returns the URL of the uploaded file. * @param file the file to upload @@ -41,20 +44,49 @@ function isDataUri(uri: string): boolean { } } +type UploadSignatureResponse = { + signature: string; +}; + +type UploadSignatureData = { + file_name: string; + file_size: number; +}; + +async function createUploadSignature(file: Blob): Promise { + const { signature } = await dispatchRequest< + UploadSignatureData, + UploadSignatureResponse + >('POST', 'https://rest.alpha.fal.ai/storage/upload/signature', { + file_name: file.name, + file_size: file.size, + }); + return signature; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type KeyValuePair = [string, any]; -export const fileSupport: FileSupport = { +export const storageImpl: StorageSupport = { upload: async (file: Blob, options?: UploadOptions) => { + const signature = await createUploadSignature(file); + const { filename } = options || {}; const formData = new FormData(); formData.append('file', file, filename ?? file.name); - - const response = await fetch('https://rest.alpha.fal.ai/storage/upload', { - method: 'POST', - body: formData, - }); - const { url } = await response.json(); + const response = await fetch( + 'https://rest.alpha.fal.ai/storage/upload/signed', + { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Signature ${signature}`, + }, + body: formData, + } + ); + const { responseHandler } = getConfig(); + const { url } = await responseHandler(response); return url; }, @@ -70,7 +102,7 @@ export const fileSupport: FileSupport = { const response = await fetch(value); blob = await response.blob(); } - const url = await fileSupport.upload(blob as Blob); + const url = await storageImpl.upload(blob as Blob); return [key, url]; } return [key, value] as KeyValuePair; From ffa95bb82c00eea4f504ea99138e993ce750d06d Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Tue, 24 Oct 2023 16:47:58 -0700 Subject: [PATCH 3/9] fix: add multipart header --- libs/client/fal-js-client.zip | Bin 0 -> 12140 bytes libs/client/src/storage.ts | 1 + 2 files changed, 1 insertion(+) create mode 100644 libs/client/fal-js-client.zip diff --git a/libs/client/fal-js-client.zip b/libs/client/fal-js-client.zip new file mode 100644 index 0000000000000000000000000000000000000000..10c4b25a8703bba7c27a9af8aeac6cc164a93e08 GIT binary patch literal 12140 zcmb7qWl&w&5-t`XcyM>O;4VRf1b26WySuwm|6WKbX=NFXRU5ha}Sg9-sKARx5&KtO;$AXy-!6T zIP3eLty*+n=TDhwDa#(icp|a`RtKmQ?@=2Bj1@TdojF#dCk&jjdQ3?Tw;PL;V*X1 z-V*$N&bu}v8W>JOFD+0EzsyD-RDft}t7(TK3W5n6n*rH}^t$SW*+N0wN7}!p>Z`ic zMiLGE3pu)hn3H$a%~#nPp~3N(ntW99UD2|KJ?5?w!-#=&bY{*Asn(LJoLgM zxth`q&UgGQgi>m>7`^VWPsl-I#-Z$`JC{^$I7Db5sktLXNeO0D5j79T#Ss~@QF}zf ztxjiS;Ug0*2`I^%XH6OQ9Xb>P9r18i!};MNKPEbAQfnEUfpUMt&Z&4pXHPO*8`X%s zsyTYR`#QT>ce$%`=W7C|)`gQGdf@n^Z{g8`IS)qp8EIB%L|pBxX-`hU4Q}fUZC~yZ zj&*{bww2q3B~XoU8`@9*vE+dys*q#gRyso$QCzUYS zu~S?qF}HLQ-qu@W_>64g1L3HA7tv(@~y~@L3t>RPw8mrqzryQGG$@BK?7zhZL+HE;|n<7zz-;+ zvmqr$5mdG{rk|vXL z?C!Xob=iK^NjV4I)*}k^5w9`aE@ViRb#DknVc$-@1Vmrj)n?1tFyLnj=7$HXjXA`M z&G2Z<+&kJ zPyvZ-J`6NYP7<1=HJc#dCtQ@{=3{5<{8JR+u}tkUB&vFzgDG60%|(wG6K84>#!$k z!*a|1anL5(%uy35%3W0sk-@}bDntX5JKkr}!6E7iAE+k}K0X`Fp;TwV| z0vDubnIl`@EEdUikwUwc-lUK%oo}_QgKXyK97EX=SIm*2Vz**t6&LVI&Zx{HVp+oH z&bUeqf)b#fnfc2Dr|G?fiGwRogt)T8(|iM~#%#m1OzHj<)`z~U#%bT}yeSk{t(|>3 zZJ1$|=}65Rv!}l3fCeF#Y0;b)L4-2-VtR3J(UjB* zFWLZdIJZZflYxXWfu(^mycOSw*4$UVPBf*lzvuptnxa!@x%aKwk278CFN4HEi6}vU zf@+`uP9 z!(4-FV&%(rqO=lX%jv@suZX_uJbD){ny}{y;(X;eV??~~V>2R>jX)g(n;ozYLB|D= zN3tnC&)-+YvuQU04vkwwveWJgbXh!EQ0UW9Uph5o<~0y6jY&W|0;ZqBz_pSyLCUhr zY&%-ut4tXN%!99_J!gGl4kL@) zKBEo1;|a9+8lT4IJ79-&|0j6y5gU!57ONzNhqG0QEm|A0l`qGJ17Zft;>F=pAv4-q z7(M)p_^Jmv*@*8&!*CJ=H&a|dhtP_&)dOdniuAF#j2as?=>6y=J{aOQu3xiB1oiQl zY8OIn7&N0Q59-^w;2Ihxmn;SLs|>G+7x$OdK*Orrlbw=yPWf5sNkx62M^~ z&SGuFD6Ce5T1n$N5?nPMuiH_|X+ZkhI-*PEqe-zwj^!*_P8|+Io&+(CQn7?A#h@Y| zhfiCcj1_i|Je9YiduQ~Q7M#+}96{{=&`kezqhxTEm=V@iC(SRz;d=T7k%)g@nSR|zO$TG(#Gl_iWmcys=LE502w=MNQQeV- zQck=ITU8ufBMs9kA6H|$ z3Zg@oOw>J}?nYcXGk2b8FPhO!dHf8Kx_9vY{{CnSySRb@f(auC5D@b3_s7uQLeI|F z(&A4@iI0*n^`k=wIDHObhZNqUG;b*A)m#+MYK?Q5lgNfEjMhJb=zLi%EK8ZJ9{+?m zz{SXAS&0=#TA4YlSx7sEccSAXRsu&@&I>L(K`@`GI+=DrRmw^#dUvrd*Z)B+`f%2_ zxN6t0*ijijcLOsrM(AU>-ziiS6PTf3%*4A&^lG(AU?9ZMFFAdRrMBMca?g*DmLjlm z1c`0)C8E+io&z5j4~Hgns~C%8zuT`S1wmWUsv4o`i}7

Q?L3B6J@tsbaHnw&%v5(i!5 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 qBk?W8F4 Date: Tue, 24 Oct 2023 16:48:57 -0700 Subject: [PATCH 4/9] fix: remove unused file --- libs/client/fal-js-client.zip | Bin 12140 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 libs/client/fal-js-client.zip diff --git a/libs/client/fal-js-client.zip b/libs/client/fal-js-client.zip deleted file mode 100644 index 10c4b25a8703bba7c27a9af8aeac6cc164a93e08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12140 zcmb7qWl&w&5-t`XcyM>O;4VRf1b26WySuwm|6WKbX=NFXRU5ha}Sg9-sKARx5&KtO;$AXy-!6T zIP3eLty*+n=TDhwDa#(icp|a`RtKmQ?@=2Bj1@TdojF#dCk&jjdQ3?Tw;PL;V*X1 z-V*$N&bu}v8W>JOFD+0EzsyD-RDft}t7(TK3W5n6n*rH}^t$SW*+N0wN7}!p>Z`ic zMiLGE3pu)hn3H$a%~#nPp~3N(ntW99UD2|KJ?5?w!-#=&bY{*Asn(LJoLgM zxth`q&UgGQgi>m>7`^VWPsl-I#-Z$`JC{^$I7Db5sktLXNeO0D5j79T#Ss~@QF}zf ztxjiS;Ug0*2`I^%XH6OQ9Xb>P9r18i!};MNKPEbAQfnEUfpUMt&Z&4pXHPO*8`X%s zsyTYR`#QT>ce$%`=W7C|)`gQGdf@n^Z{g8`IS)qp8EIB%L|pBxX-`hU4Q}fUZC~yZ zj&*{bww2q3B~XoU8`@9*vE+dys*q#gRyso$QCzUYS zu~S?qF}HLQ-qu@W_>64g1L3HA7tv(@~y~@L3t>RPw8mrqzryQGG$@BK?7zhZL+HE;|n<7zz-;+ zvmqr$5mdG{rk|vXL z?C!Xob=iK^NjV4I)*}k^5w9`aE@ViRb#DknVc$-@1Vmrj)n?1tFyLnj=7$HXjXA`M z&G2Z<+&kJ zPyvZ-J`6NYP7<1=HJc#dCtQ@{=3{5<{8JR+u}tkUB&vFzgDG60%|(wG6K84>#!$k z!*a|1anL5(%uy35%3W0sk-@}bDntX5JKkr}!6E7iAE+k}K0X`Fp;TwV| z0vDubnIl`@EEdUikwUwc-lUK%oo}_QgKXyK97EX=SIm*2Vz**t6&LVI&Zx{HVp+oH z&bUeqf)b#fnfc2Dr|G?fiGwRogt)T8(|iM~#%#m1OzHj<)`z~U#%bT}yeSk{t(|>3 zZJ1$|=}65Rv!}l3fCeF#Y0;b)L4-2-VtR3J(UjB* zFWLZdIJZZflYxXWfu(^mycOSw*4$UVPBf*lzvuptnxa!@x%aKwk278CFN4HEi6}vU zf@+`uP9 z!(4-FV&%(rqO=lX%jv@suZX_uJbD){ny}{y;(X;eV??~~V>2R>jX)g(n;ozYLB|D= zN3tnC&)-+YvuQU04vkwwveWJgbXh!EQ0UW9Uph5o<~0y6jY&W|0;ZqBz_pSyLCUhr zY&%-ut4tXN%!99_J!gGl4kL@) zKBEo1;|a9+8lT4IJ79-&|0j6y5gU!57ONzNhqG0QEm|A0l`qGJ17Zft;>F=pAv4-q z7(M)p_^Jmv*@*8&!*CJ=H&a|dhtP_&)dOdniuAF#j2as?=>6y=J{aOQu3xiB1oiQl zY8OIn7&N0Q59-^w;2Ihxmn;SLs|>G+7x$OdK*Orrlbw=yPWf5sNkx62M^~ z&SGuFD6Ce5T1n$N5?nPMuiH_|X+ZkhI-*PEqe-zwj^!*_P8|+Io&+(CQn7?A#h@Y| zhfiCcj1_i|Je9YiduQ~Q7M#+}96{{=&`kezqhxTEm=V@iC(SRz;d=T7k%)g@nSR|zO$TG(#Gl_iWmcys=LE502w=MNQQeV- zQck=ITU8ufBMs9kA6H|$ z3Zg@oOw>J}?nYcXGk2b8FPhO!dHf8Kx_9vY{{CnSySRb@f(auC5D@b3_s7uQLeI|F z(&A4@iI0*n^`k=wIDHObhZNqUG;b*A)m#+MYK?Q5lgNfEjMhJb=zLi%EK8ZJ9{+?m zz{SXAS&0=#TA4YlSx7sEccSAXRsu&@&I>L(K`@`GI+=DrRmw^#dUvrd*Z)B+`f%2_ zxN6t0*ijijcLOsrM(AU>-ziiS6PTf3%*4A&^lG(AU?9ZMFFAdRrMBMca?g*DmLjlm z1c`0)C8E+io&z5j4~Hgns~C%8zuT`S1wmWUsv4o`i}7

Q?L3B6J@tsbaHnw&%v5(i!5 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 qBk?W8F4 Date: Thu, 2 Nov 2023 02:24:44 -0700 Subject: [PATCH 5/9] chore(wip): own signature impl --- libs/client/src/storage.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index 373b8c9..cf3eceb 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -57,7 +57,7 @@ async function createUploadSignature(file: Blob): Promise { const { signature } = await dispatchRequest< UploadSignatureData, UploadSignatureResponse - >('POST', 'https://rest.alpha.fal.ai/storage/upload/signature', { + >('POST', 'https://rest.daniel.shark.fal.ai/storage/upload/signature', { file_name: file.name, file_size: file.size, }); @@ -75,13 +75,12 @@ export const storageImpl: StorageSupport = { const formData = new FormData(); formData.append('file', file, filename ?? file.name); const response = await fetch( - 'https://rest.alpha.fal.ai/storage/upload/signed', + 'https://rest.daniel.shark.fal.ai/storage/upload/signed', { method: 'POST', headers: { Accept: 'application/json', Authorization: `Signature ${signature}`, - ContentType: 'multipart/form-data', }, body: formData, } From 3dc26655eb65af168a86da81be2dbcde848ab26c Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Thu, 2 Nov 2023 09:10:55 -0700 Subject: [PATCH 6/9] feat: use gcs presigned upload --- libs/client/package.json | 2 +- libs/client/src/storage.ts | 55 +++++++++++++++----------------------- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/libs/client/package.json b/libs/client/package.json index 4c49e5e..05fc8eb 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.4.2", + "version": "0.5.0-alpha.0", "license": "MIT", "repository": { "type": "git", diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index cf3eceb..5cd4be7 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -1,9 +1,6 @@ import { getConfig } from './config'; import { dispatchRequest } from './request'; -export type UploadOptions = { - filename?: string; -}; /** * File support for the client. This interface establishes the contract for @@ -17,7 +14,7 @@ export interface StorageSupport { * @param options optional parameters, such as custom file name * @returns the URL of the uploaded file */ - upload: (file: Blob, options?: UploadOptions) => Promise; + upload: (file: Blob) => Promise; /** * Transform the input to replace file objects with URLs. This is used @@ -44,49 +41,39 @@ function isDataUri(uri: string): boolean { } } -type UploadSignatureResponse = { - signature: string; +type InitiateUploadResult = { + file_url: string; + upload_url: string; }; -type UploadSignatureData = { +type InitiateUploadData = { file_name: string; - file_size: number; + content_type: string | null; }; -async function createUploadSignature(file: Blob): Promise { - const { signature } = await dispatchRequest< - UploadSignatureData, - UploadSignatureResponse - >('POST', 'https://rest.daniel.shark.fal.ai/storage/upload/signature', { - file_name: file.name, - file_size: file.size, - }); - return signature; +async function initiateUpload(file: Blob): Promise { + return await dispatchRequest( + 'POST', + 'https://rest.daniel.shark.fal.ai/storage/upload/initiate', + { file_name: file.name, content_type: file.type || 'application/octet-stream' } + ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any type KeyValuePair = [string, any]; export const storageImpl: StorageSupport = { - upload: async (file: Blob, options?: UploadOptions) => { - const signature = await createUploadSignature(file); - - const { filename } = options || {}; - const formData = new FormData(); - formData.append('file', file, filename ?? file.name); - const response = await fetch( - 'https://rest.daniel.shark.fal.ai/storage/upload/signed', - { - method: 'POST', - headers: { - Accept: 'application/json', - Authorization: `Signature ${signature}`, - }, - body: formData, + upload: async (file: Blob) => { + const { upload_url: uploadUrl, file_url: url } = await initiateUpload(file); + const response = await fetch(uploadUrl, { + method: 'PUT', + body: file, + headers: { + 'Content-Type': file.type || 'application/octet-stream', } - ); + }); const { responseHandler } = getConfig(); - const { url } = await responseHandler(response); + await responseHandler(response); return url; }, From 698b95a48dbcea3935a692637327481e8e43b6a0 Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Thu, 2 Nov 2023 09:20:58 -0700 Subject: [PATCH 7/9] fix: invalid export --- libs/client/src/index.ts | 1 - libs/client/src/storage.ts | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libs/client/src/index.ts b/libs/client/src/index.ts index 7f4cdfa..7d1d611 100644 --- a/libs/client/src/index.ts +++ b/libs/client/src/index.ts @@ -1,6 +1,5 @@ export { config, getConfig } from './config'; export { storageImpl as storage } from './storage'; -export type { UploadOptions } from './storage'; export { queue, run, subscribe } from './function'; export { withMiddleware, withProxy } from './middleware'; export type { RequestMiddleware } from './middleware'; diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index 5cd4be7..5229500 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -1,7 +1,6 @@ import { getConfig } from './config'; import { dispatchRequest } from './request'; - /** * File support for the client. This interface establishes the contract for * uploading files to the server and transforming the input to replace file @@ -55,7 +54,10 @@ async function initiateUpload(file: Blob): Promise { return await dispatchRequest( 'POST', 'https://rest.daniel.shark.fal.ai/storage/upload/initiate', - { file_name: file.name, content_type: file.type || 'application/octet-stream' } + { + file_name: file.name, + content_type: file.type || 'application/octet-stream', + } ); } @@ -70,7 +72,7 @@ export const storageImpl: StorageSupport = { body: file, headers: { 'Content-Type': file.type || 'application/octet-stream', - } + }, }); const { responseHandler } = getConfig(); await responseHandler(response); From 2d77e5d932f80a1cfd3f4073fd8c5fe675ec4b13 Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Thu, 2 Nov 2023 10:35:31 -0700 Subject: [PATCH 8/9] fix: rest api host url --- libs/client/src/storage.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index 5229500..98b70f3 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -50,10 +50,15 @@ type InitiateUploadData = { content_type: string | null; }; +function getRestApiUrl(): string { + const { host } = getConfig(); + return host.replace('gateway', 'rest'); +} + async function initiateUpload(file: Blob): Promise { return await dispatchRequest( 'POST', - 'https://rest.daniel.shark.fal.ai/storage/upload/initiate', + `https://${getRestApiUrl()}/storage/upload/initiate`, { file_name: file.name, content_type: file.type || 'application/octet-stream', From f55382cecca58ab3eb8a4404d368dfe1351d6594 Mon Sep 17 00:00:00 2001 From: Daniel Rochetti Date: Thu, 2 Nov 2023 19:24:38 -0700 Subject: [PATCH 9/9] feat: final upload logic and sample --- apps/demo-nextjs-app-router/app/page.tsx | 63 ++++++++++++++++-------- libs/client/package.json | 2 +- libs/client/src/storage.ts | 1 + 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/apps/demo-nextjs-app-router/app/page.tsx b/apps/demo-nextjs-app-router/app/page.tsx index 2313dd1..56cf998 100644 --- a/apps/demo-nextjs-app-router/app/page.tsx +++ b/apps/demo-nextjs-app-router/app/page.tsx @@ -19,7 +19,7 @@ type Image = { file_size: number; }; type Result = { - images: Image[]; + image: Image; }; // @snippet:end @@ -42,12 +42,13 @@ function Error(props: ErrorProps) { } const DEFAULT_PROMPT = - 'a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd'; + '(masterpiece:1.4), (best quality), (detailed), Medieval village scene with busy streets and castle in the distance'; export default function Home() { // @snippet:start("client.ui.state") // Input state const [prompt, setPrompt] = useState(DEFAULT_PROMPT); + const [imageFile, setImageFile] = useState(null); // Result state const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -59,7 +60,10 @@ export default function Home() { if (!result) { return null; } - return result.images[0]; + if (result.image) { + return result.image; + } + return null; }, [result]); const reset = () => { @@ -76,24 +80,27 @@ export default function Home() { setLoading(true); const start = Date.now(); try { - 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) - logs: true, - onQueueUpdate(update) { - setElapsedTime(Date.now() - start); - if ( - update.status === 'IN_PROGRESS' || - update.status === 'COMPLETED' - ) { - setLogs((update.logs || []).map((log) => log.message)); - } - }, - }); + const result: Result = await fal.subscribe( + '54285744-illusion-diffusion', + { + input: { + prompt, + image_url: imageFile, + image_size: 'square_hd', + }, + pollInterval: 5000, // Default is 1000 (every 1s) + logs: true, + onQueueUpdate(update) { + setElapsedTime(Date.now() - start); + if ( + update.status === 'IN_PROGRESS' || + update.status === 'COMPLETED' + ) { + setLogs((update.logs || []).map((log) => log.message)); + } + }, + } + ); setResult(result); } catch (error: any) { setError(error); @@ -109,6 +116,20 @@ export default function Home() {

Hello fal

+
+ + setImageFile(e.target.files?.[0] ?? null)} + /> +