feat(client): binary chunk streaming support (#77)

* feat(client): binary chunk streaming support

* fix: buffer update

* fix: audio playing

* fix: media resource setup

* feat: allow streaming through the proxy

* fix: legacy urls env

* feat: streaming connection mode

* chore: bump client alpha version

* feat(proxy): enable response streaming when supported

* fix: queue streaming

* chore: deprecated endpoint id cleanup

* fix: client tests

* chore: demo page updates

* chore: bump version for release
This commit is contained in:
Daniel Rochetti 2024-08-06 09:09:40 -07:00 committed by GitHub
parent d9ea6c7dd3
commit 6edbf2948d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 281 additions and 185 deletions

View File

@ -25,10 +25,15 @@ function Error(props: ErrorProps) {
);
}
const DEFAULT_ENDPOINT_ID = 'fal-ai/fast-sdxl';
const DEFAULT_INPUT = `{
"prompt": "A beautiful sunset over the ocean"
}`;
export default function Home() {
// Input state
const [endpointId, setEndpointId] = useState<string>('');
const [input, setInput] = useState<string>('{}');
const [endpointId, setEndpointId] = useState<string>(DEFAULT_ENDPOINT_ID);
const [input, setInput] = useState<string>(DEFAULT_INPUT);
// Result state
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

View File

@ -1,7 +1,7 @@
{
"name": "@fal-ai/serverless-client",
"description": "The fal serverless JS/TS client",
"version": "0.14.0-alpha.3",
"version": "0.14.0",
"license": "MIT",
"repository": {
"type": "git",

View File

@ -1,19 +1,6 @@
import uuid from 'uuid-random';
import { buildUrl } from './function';
describe('The function test suite', () => {
it('should build the URL with a function UUIDv4', () => {
const id = uuid();
const url = buildUrl(`12345/${id}`);
expect(url).toMatch(`trigger/12345/${id}`);
});
it('should build the URL with a function user-id-app-alias', () => {
const alias = '12345-some-alias';
const url = buildUrl(alias);
expect(url).toMatch(`fal.run/12345/some-alias`);
});
it('should build the URL with a function username/app-alias', () => {
const alias = 'fal-ai/text-to-image';
const url = buildUrl(alias);

View File

@ -1,14 +1,13 @@
import { getTemporaryAuthToken } from './auth';
import { dispatchRequest } from './request';
import { storageImpl } from './storage';
import { FalStream } from './streaming';
import { FalStream, StreamingConnectionMode } from './streaming';
import {
CompletedQueueStatus,
EnqueueResult,
QueueStatus,
RequestLog,
} from './types';
import { ensureAppIdFormat, isUUIDv4, isValidUrl, parseAppId } from './utils';
import { ensureAppIdFormat, isValidUrl, parseAppId } from './utils';
/**
* The function input and other configuration when running
@ -80,7 +79,6 @@ export function buildUrl<Input>(
Object.keys(params).length > 0
? `?${new URLSearchParams(params).toString()}`
: '';
const parts = id.split('/');
// if a fal url is passed, just use it
if (isValidUrl(id)) {
@ -88,12 +86,6 @@ export function buildUrl<Input>(
return `${url}${path}${queryParams}`;
}
// TODO remove this after some time, fal.run should be preferred
if (parts.length === 2 && isUUIDv4(parts[1])) {
const host = 'gateway.shark.fal.ai';
return `https://${host}/trigger/${id}/${path}${queryParams}`;
}
const appId = ensureAppIdFormat(id);
const subdomain = options.subdomain ? `${options.subdomain}.` : '';
const url = `https://${subdomain}fal.run/${appId}/${path}`;
@ -199,6 +191,12 @@ type QueueSubscribeOptions = {
}
| {
mode: 'streaming';
/**
* The connection mode to use for streaming updates. It defaults to `server`.
* Set to `client` if your server proxy doesn't support streaming.
*/
connectionMode?: StreamingConnectionMode;
}
);
@ -228,6 +226,14 @@ type QueueStatusOptions = BaseQueueOptions & {
logs?: boolean;
};
type QueueStatusStreamOptions = QueueStatusOptions & {
/**
* The connection mode to use for streaming updates. It defaults to `server`.
* Set to `client` if your server proxy doesn't support streaming.
*/
connectionMode?: StreamingConnectionMode;
};
/**
* Represents a request queue with methods for submitting requests,
* checking their status, retrieving results, and subscribing to updates.
@ -263,7 +269,7 @@ interface Queue {
*/
streamStatus(
endpointId: string,
options: QueueStatusOptions
options: QueueStatusStreamOptions
): Promise<FalStream<unknown, QueueStatus>>;
/**
@ -340,24 +346,26 @@ export const queue: Queue = {
async streamStatus(
endpointId: string,
{ requestId, logs = false }: QueueStatusOptions
{ requestId, logs = false, connectionMode }: QueueStatusStreamOptions
): Promise<FalStream<unknown, QueueStatus>> {
const appId = parseAppId(endpointId);
const prefix = appId.namespace ? `${appId.namespace}/` : '';
const token = await getTemporaryAuthToken(endpointId);
const queryParams = {
logs: logs ? '1' : '0',
};
const url = buildUrl(`${prefix}${appId.owner}/${appId.alias}`, {
subdomain: 'queue',
path: `/requests/${requestId}/status/stream`,
query: queryParams,
});
const queryParams = new URLSearchParams({
fal_jwt_token: token,
logs: logs ? '1' : '0',
});
return new FalStream<unknown, QueueStatus>(`${url}?${queryParams}`, {
input: {},
return new FalStream<unknown, QueueStatus>(endpointId, {
url,
method: 'get',
connectionMode,
queryParams,
});
},
@ -375,6 +383,10 @@ export const queue: Queue = {
const status = await queue.streamStatus(endpointId, {
requestId,
logs: options.logs,
connectionMode:
'connectionMode' in options
? (options.connectionMode as StreamingConnectionMode)
: undefined,
});
const logs: RequestLog[] = [];
if (timeout) {
@ -390,7 +402,7 @@ export const queue: Queue = {
);
}, timeout);
}
status.on('message', (data: QueueStatus) => {
status.on('data', (data: QueueStatus) => {
if (options.onQueueUpdate) {
// accumulate logs to match previous polling behavior
if (

View File

@ -1,14 +1,20 @@
import { getConfig } from './config';
import { ResponseHandler } from './response';
import { getUserAgent, isBrowser } from './runtime';
const isCloudflareWorkers =
typeof navigator !== 'undefined' &&
navigator?.userAgent === 'Cloudflare-Workers';
type RequestOptions = {
responseHandler?: ResponseHandler<any>;
};
export async function dispatchRequest<Input, Output>(
method: string,
targetUrl: string,
input: Input
input: Input,
options: RequestOptions & RequestInit = {}
): Promise<Output> {
const {
credentials: credentialsValue,
@ -39,14 +45,21 @@ export async function dispatchRequest<Input, Output>(
...userAgent,
...(headers ?? {}),
} as HeadersInit;
const { responseHandler: customResponseHandler, ...requestInit } = options;
const response = await fetch(url, {
...requestInit,
method,
headers: requestHeaders,
headers: {
...requestHeaders,
...(requestInit.headers ?? {}),
},
...(!isCloudflareWorkers && { mode: 'cors' }),
body:
method.toLowerCase() !== 'get' && input
? JSON.stringify(input)
: undefined,
});
return await responseHandler(response);
const handleResponse = customResponseHandler ?? responseHandler;
return await handleResponse(response);
}

View File

@ -2,19 +2,33 @@ import { createParser } from 'eventsource-parser';
import { getTemporaryAuthToken } from './auth';
import { getConfig } from './config';
import { buildUrl } from './function';
import { dispatchRequest } from './request';
import { ApiError, defaultResponseHandler } from './response';
import { storageImpl } from './storage';
export type StreamingConnectionMode = 'client' | 'server';
/**
* The stream API options. It requires the API input and also
* offers configuration options.
*/
type StreamOptions<Input> = {
/**
* The endpoint URL. If not provided, it will be generated from the
* `endpointId` and the `queryParams`.
*/
readonly url?: string;
/**
* The API input payload.
*/
readonly input?: Input;
/**
* The query parameters to be sent with the request.
*/
readonly queryParams?: Record<string, string>;
/**
* The maximum time interval in milliseconds between stream chunks. Defaults to 15s.
*/
@ -30,19 +44,37 @@ type StreamOptions<Input> = {
* The HTTP method, defaults to `post`;
*/
readonly method?: 'get' | 'post' | 'put' | 'delete' | string;
/**
* The content type the client accepts as response.
* By default this is set to `text/event-stream`.
*/
readonly accept?: string;
/**
* The streaming connection mode. This is used to determine
* whether the streaming will be done from the browser itself (client)
* or through your own server, either when running on NodeJS or when
* using a proxy that supports streaming.
*
* It defaults to `server`. Set to `client` if your server proxy doesn't
* support streaming.
*/
readonly connectionMode?: StreamingConnectionMode;
};
const EVENT_STREAM_TIMEOUT = 15 * 1000;
type FalStreamEventType = 'message' | 'error' | 'done';
type FalStreamEventType = 'data' | 'error' | 'done';
type EventHandler = (event: any) => void;
type EventHandler<T = any> = (event: T) => void;
/**
* The class representing a streaming response. With t
*/
export class FalStream<Input, Output> {
// properties
endpointId: string;
url: string;
options: StreamOptions<Input>;
@ -58,8 +90,14 @@ export class FalStream<Input, Output> {
private abortController = new AbortController();
constructor(url: string, options: StreamOptions<Input>) {
this.url = url;
constructor(endpointId: string, options: StreamOptions<Input>) {
this.endpointId = endpointId;
this.url =
options.url ??
buildUrl(endpointId, {
path: '/stream',
query: options.queryParams,
});
this.options = options;
this.donePromise = new Promise<Output>((resolve, reject) => {
if (this.streamClosed) {
@ -84,20 +122,34 @@ export class FalStream<Input, Output> {
}
private start = async () => {
const { url, options } = this;
const { input, method = 'post' } = options;
const { fetch = global.fetch } = getConfig();
const { endpointId, options } = this;
const { input, method = 'post', connectionMode = 'server' } = options;
try {
const response = await fetch(url, {
method: method.toUpperCase(),
if (connectionMode === 'client') {
// if we are in the browser, we need to get a temporary token
// to authenticate the request
const token = await getTemporaryAuthToken(endpointId);
const { fetch = global.fetch } = getConfig();
const parsedUrl = new URL(this.url);
parsedUrl.searchParams.set('fal_jwt_token', token);
const response = await fetch(parsedUrl.toString(), {
method: method.toUpperCase(),
headers: {
accept: options.accept ?? 'text/event-stream',
'content-type': 'application/json',
},
body: input && method !== 'get' ? JSON.stringify(input) : undefined,
signal: this.abortController.signal,
});
return await this.handleResponse(response);
}
return await dispatchRequest(method.toUpperCase(), this.url, input, {
headers: {
accept: 'text/event-stream',
'content-type': 'application/json',
accept: options.accept ?? 'text/event-stream',
},
body: input && method !== 'get' ? JSON.stringify(input) : undefined,
responseHandler: this.handleResponse,
signal: this.abortController.signal,
});
this.handleResponse(response);
} catch (error) {
this.handleError(error);
}
@ -127,6 +179,25 @@ export class FalStream<Input, Output> {
);
return;
}
// any response that is not a text/event-stream will be handled as a binary stream
if (response.headers.get('content-type') !== 'text/event-stream') {
const reader = body.getReader();
const emitRawChunk = () => {
reader.read().then(({ done, value }) => {
if (done) {
this.emit('done', this.currentData);
return;
}
this.currentData = value as Output;
this.emit('data', value);
emitRawChunk();
});
};
emitRawChunk();
return;
}
const decoder = new TextDecoder('utf-8');
const reader = response.body.getReader();
@ -138,7 +209,10 @@ export class FalStream<Input, Output> {
const parsedData = JSON.parse(data);
this.buffer.push(parsedData);
this.currentData = parsedData;
this.emit('message', parsedData);
this.emit('data', parsedData);
// also emit 'message'for backwards compatibility
this.emit('message' as any, parsedData);
} catch (e) {
this.emit('error', e);
}
@ -242,27 +316,19 @@ export class FalStream<Input, Output> {
* object as a result, that can be used to get partial results through either
* `AsyncIterator` or through an event listener.
*
* @param appId the app id, e.g. `fal-ai/llavav15-13b`.
* @param endpointId the endpoint id, e.g. `fal-ai/llavav15-13b`.
* @param options the request options, including the input payload.
* @returns the `FalStream` instance.
*/
export async function stream<Input = Record<string, any>, Output = any>(
appId: string,
endpointId: string,
options: StreamOptions<Input>
): Promise<FalStream<Input, Output>> {
const token = await getTemporaryAuthToken(appId);
const url = buildUrl(appId, { path: '/stream' });
const input =
options.input && options.autoUpload !== false
? await storageImpl.transformInput(options.input)
: options.input;
const queryParams = new URLSearchParams({
fal_jwt_token: token,
});
return new FalStream<Input, Output>(`${url}?${queryParams}`, {
return new FalStream<Input, Output>(endpointId, {
...options,
input: input as Input,
});

View File

@ -1,22 +1,6 @@
import uuid from 'uuid-random';
import { ensureAppIdFormat, isUUIDv4, parseAppId } from './utils';
import { ensureAppIdFormat, parseAppId } from './utils';
describe('The utils test suite', () => {
it('should match a valid v4 uuid', () => {
const id = uuid();
expect(isUUIDv4(id)).toBe(true);
});
it('should not match invalid v4 id', () => {
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);
@ -32,15 +16,6 @@ describe('The utils test suite', () => {
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);

View File

@ -1,12 +1,3 @@
export function isUUIDv4(id: string): boolean {
return (
typeof id === 'string' &&
id.length === 36 &&
id[14] === '4' &&
['8', '9', 'a', 'b'].includes(id[19])
);
}
export function ensureAppIdFormat(id: string): string {
const parts = id.split('/');
if (parts.length > 1) {

View File

@ -1,6 +1,6 @@
{
"name": "@fal-ai/serverless-proxy",
"version": "0.7.5",
"version": "0.8.0",
"license": "MIT",
"repository": {
"type": "git",

View File

@ -17,11 +17,37 @@ export const handler: RequestHandler = async (request, response, next) => {
await handleRequest({
id: 'express',
method: request.method,
respondWith: (status, data) => response.status(status).json(data),
getRequestBody: async () => JSON.stringify(request.body),
getHeaders: () => request.headers,
getHeader: (name) => request.headers[name],
sendHeader: (name, value) => response.setHeader(name, value),
getBody: async () => JSON.stringify(request.body),
respondWith: (status, data) => response.status(status).json(data),
sendResponse: async (res) => {
if (res.body instanceof ReadableStream) {
const reader = res.body.getReader();
const stream = async () => {
const { done, value } = await reader.read();
if (done) {
response.end();
return response;
}
response.write(value);
return await stream();
};
return await stream().catch((error) => {
if (!response.headersSent) {
response.status(500).send(error.message);
} else {
response.end();
}
});
}
if (res.headers.get('content-type')?.includes('application/json')) {
return response.status(res.status).json(await res.json());
}
return response.status(res.status).send(await res.text());
},
});
next();
};

View File

@ -19,10 +19,11 @@ export interface ProxyBehavior<ResponseType> {
method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
respondWith(status: number, data: string | any): ResponseType;
sendResponse(response: Response): Promise<ResponseType>;
getHeaders(): Record<string, HeaderValue>;
getHeader(name: string): HeaderValue;
sendHeader(name: string, value: string): void;
getBody(): Promise<string | undefined>;
getRequestBody(): Promise<string | undefined>;
resolveApiKey?: () => Promise<string | undefined>;
}
@ -109,7 +110,7 @@ export async function handleRequest<ResponseType>(
body:
behavior.method?.toUpperCase() === 'GET'
? undefined
: await behavior.getBody(),
: await behavior.getRequestBody(),
});
// copy headers from fal to the proxied response
@ -119,12 +120,7 @@ export async function handleRequest<ResponseType>(
}
});
if (res.headers.get('content-type')?.includes('application/json')) {
const data = await res.json();
return behavior.respondWith(res.status, data);
}
const data = await res.text();
return behavior.respondWith(res.status, data);
return behavior.sendResponse(res);
}
export function fromHeaders(
@ -138,3 +134,5 @@ export function fromHeaders(
});
return result;
}
export const responsePassthrough = (res: Response) => Promise.resolve(res);

View File

@ -1,6 +1,11 @@
import { NextResponse, type NextRequest } from 'next/server';
import type { NextApiHandler } from 'next/types';
import { DEFAULT_PROXY_ROUTE, fromHeaders, handleRequest } from './index';
import {
DEFAULT_PROXY_ROUTE,
fromHeaders,
handleRequest,
responsePassthrough,
} from './index';
/**
* The default Next API route for the fal.ai client proxy.
@ -11,6 +16,8 @@ export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE;
* The Next API route handler for the fal.ai client proxy.
* Use it with the /pages router in Next.js.
*
* Note: the page routers proxy doesn't support streaming responses.
*
* @param request the Next API request object.
* @param response the Next API response object.
* @returns a promise that resolves when the request is handled.
@ -19,11 +26,17 @@ export const handler: NextApiHandler = async (request, response) => {
return handleRequest({
id: 'nextjs-page-router',
method: request.method || 'POST',
respondWith: (status, data) => response.status(status).json(data),
getRequestBody: async () => JSON.stringify(request.body),
getHeaders: () => request.headers,
getHeader: (name) => request.headers[name],
sendHeader: (name, value) => response.setHeader(name, value),
getBody: async () => JSON.stringify(request.body),
respondWith: (status, data) => response.status(status).json(data),
sendResponse: async (res) => {
if (res.headers.get('content-type')?.includes('application/json')) {
return response.status(res.status).json(await res.json());
}
return response.status(res.status).send(await res.text());
},
});
};
@ -36,18 +49,22 @@ export const handler: NextApiHandler = async (request, response) => {
async function routeHandler(request: NextRequest) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responseHeaders: Record<string, any> = {};
// check if response if from a streaming request
return await handleRequest({
id: 'nextjs-app-router',
method: request.method,
getRequestBody: async () => request.text(),
getHeaders: () => fromHeaders(request.headers),
getHeader: (name) => request.headers.get(name),
sendHeader: (name, value) => (responseHeaders[name] = value),
respondWith: (status, data) =>
NextResponse.json(data, {
status,
headers: responseHeaders,
}),
getHeaders: () => fromHeaders(request.headers),
getHeader: (name) => request.headers.get(name),
sendHeader: (name, value) => (responseHeaders[name] = value),
getBody: async () => request.text(),
sendResponse: responsePassthrough,
});
}

View File

@ -1,5 +1,5 @@
import { type RequestHandler } from '@sveltejs/kit';
import { fromHeaders, handleRequest } from './index';
import { fromHeaders, handleRequest, responsePassthrough } from './index';
type RequestHandlerParams = {
/**
@ -28,16 +28,17 @@ export const createRequestHandler = ({
return await handleRequest({
id: 'svelte-app-router',
method: request.method,
getRequestBody: async () => request.text(),
getHeaders: () => fromHeaders(request.headers),
getHeader: (name) => request.headers.get(name),
sendHeader: (name, value) => (responseHeaders[name] = value),
resolveApiKey: () => Promise.resolve(FAL_KEY),
respondWith: (status, data) =>
new Response(JSON.stringify(data), {
status,
headers: responseHeaders,
}),
getHeaders: () => fromHeaders(request.headers),
getHeader: (name) => request.headers.get(name),
sendHeader: (name, value) => (responseHeaders[name] = value),
getBody: async () => request.text(),
resolveApiKey: () => Promise.resolve(FAL_KEY),
sendResponse: responsePassthrough,
});
};
return {

125
package-lock.json generated
View File

@ -31,7 +31,7 @@
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.5",
"next": "^14.0.3",
"next": "^14.2.5",
"open": "^10.0.3",
"ora": "^8.0.1",
"react": "^18.2.0",
@ -4764,9 +4764,9 @@
}
},
"node_modules/@next/env": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz",
"integrity": "sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA=="
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
"integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "14.0.3",
@ -4798,9 +4798,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz",
"integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz",
"integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==",
"cpu": [
"arm64"
],
@ -4813,9 +4813,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz",
"integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz",
"integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==",
"cpu": [
"x64"
],
@ -4828,9 +4828,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz",
"integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz",
"integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==",
"cpu": [
"arm64"
],
@ -4843,9 +4843,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz",
"integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz",
"integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==",
"cpu": [
"arm64"
],
@ -4858,9 +4858,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz",
"integrity": "sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz",
"integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==",
"cpu": [
"x64"
],
@ -4873,9 +4873,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz",
"integrity": "sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz",
"integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==",
"cpu": [
"x64"
],
@ -4888,9 +4888,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz",
"integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz",
"integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==",
"cpu": [
"arm64"
],
@ -4903,9 +4903,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz",
"integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz",
"integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==",
"cpu": [
"ia32"
],
@ -4918,9 +4918,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz",
"integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz",
"integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==",
"cpu": [
"x64"
],
@ -7616,17 +7616,16 @@
}
},
"node_modules/@swc/counter": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz",
"integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==",
"devOptional": true,
"peer": true
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/helpers": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
@ -10369,9 +10368,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001558",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz",
"integrity": "sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==",
"version": "1.0.30001643",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
"integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
"funding": [
{
"type": "opencollective",
@ -15782,7 +15781,8 @@
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true
},
"node_modules/glob/node_modules/minimatch": {
"version": "3.1.2",
@ -20608,17 +20608,17 @@
"dev": true
},
"node_modules/next": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz",
"integrity": "sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==",
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz",
"integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==",
"dependencies": {
"@next/env": "14.0.3",
"@swc/helpers": "0.5.2",
"@next/env": "14.2.5",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1",
"watchpack": "2.4.0"
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
@ -20627,18 +20627,19 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.0.3",
"@next/swc-darwin-x64": "14.0.3",
"@next/swc-linux-arm64-gnu": "14.0.3",
"@next/swc-linux-arm64-musl": "14.0.3",
"@next/swc-linux-x64-gnu": "14.0.3",
"@next/swc-linux-x64-musl": "14.0.3",
"@next/swc-win32-arm64-msvc": "14.0.3",
"@next/swc-win32-ia32-msvc": "14.0.3",
"@next/swc-win32-x64-msvc": "14.0.3"
"@next/swc-darwin-arm64": "14.2.5",
"@next/swc-darwin-x64": "14.2.5",
"@next/swc-linux-arm64-gnu": "14.2.5",
"@next/swc-linux-arm64-musl": "14.2.5",
"@next/swc-linux-x64-gnu": "14.2.5",
"@next/swc-linux-x64-musl": "14.2.5",
"@next/swc-win32-arm64-msvc": "14.2.5",
"@next/swc-win32-ia32-msvc": "14.2.5",
"@next/swc-win32-x64-msvc": "14.2.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
@ -20647,6 +20648,9 @@
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"sass": {
"optional": true
}
@ -30163,6 +30167,7 @@
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"

View File

@ -47,7 +47,7 @@
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^2.0.6",
"js-base64": "^3.7.5",
"next": "^14.0.3",
"next": "^14.2.5",
"open": "^10.0.3",
"ora": "^8.0.1",
"react": "^18.2.0",