Skip to content

v3: edge runtime support #1172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/slow-kiwis-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---

v3: vercel edge runtime support
60 changes: 2 additions & 58 deletions docs/v3-openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -811,17 +811,7 @@ paths:
description: Whether to override existing variables or not
default: false
required: ["variables"]
multipart/form-data:
schema:
type: object
properties:
variables:
type: string
format: binary
override:
type: boolean
required:
- variables

responses:
"200":
description: Environment variables imported successfully
Expand Down Expand Up @@ -864,57 +854,11 @@ paths:
source: |-
import { envvars } from "@trigger.dev/sdk/v3";

// Import variables from an array
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: [
{
name: "SLACK_API_KEY",
value: "slack_123456"
}
],
override: false
});
- lang: typescript
label: Import variables from a read stream
source: |-
import { envvars } from "@trigger.dev/sdk/v3";
import { createReadStream } from "node:fs";

// Import variables in dotenv format from a file
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: createReadStream(".env"),
override: false
});
- lang: typescript
label: Import variables from a response
source: |-
import { envvars } from "@trigger.dev/sdk/v3";

// Import variables in dotenv format from a response
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: await fetch("https://example.com/.env"),
variables: { SLACK_API_KEY: "slack_key_1234" },
override: false
});
- lang: typescript
label: Import variables from a Buffer
source: |-
import { envvars } from "@trigger.dev/sdk/v3";

// Import variables in dotenv format from a buffer
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: Buffer.from("SLACK_API_KEY=slack_1234"),
override: false
});
- lang: typescript
label: Import variables from a File
source: |-
import { envvars } from "@trigger.dev/sdk/v3";

// Import variables in dotenv format from a file
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: new File(["SLACK_API_KEY=slack_1234"], ".env"),
override: false
});

"/api/v1/projects/{projectRef}/envvars/{env}/{name}":
parameters:
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-v3/src/utilities/configFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export async function readConfig(
write: true,
format: "cjs",
platform: "node",
target: ["es2018", "node18"],
target: ["es2020", "node18"],
outfile: builtConfigFilePath,
logLevel: "silent",
plugins: [
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@
"@opentelemetry/sdk-trace-base": "^1.22.0",
"@opentelemetry/sdk-trace-node": "^1.22.0",
"@opentelemetry/semantic-conventions": "^1.22.0",
"form-data-encoder": "^4.0.2",
"humanize-duration": "^3.27.3",
"socket.io-client": "4.7.4",
"superjson": "^2.2.1",
Expand All @@ -157,6 +156,7 @@
"@types/humanize-duration": "^3.27.1",
"@types/jest": "^29.5.3",
"@types/node": "20.12.7",
"@types/readable-stream": "^4.0.14",
"jest": "^29.6.2",
"rimraf": "^3.0.2",
"socket.io": "4.7.4",
Expand Down
264 changes: 1 addition & 263 deletions packages/core/src/v3/apiClient/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { fromZodError } from "zod-validation-error";
import { ApiConnectionError, ApiError } from "./errors";
import { RetryOptions } from "../schemas";
import { calculateNextRetryDelay } from "../utils/retries";
import { FormDataEncoder } from "form-data-encoder";
import { Readable } from "node:stream";

import {
CursorPage,
CursorPageParams,
Expand Down Expand Up @@ -114,59 +113,6 @@ export function zodfetchOffsetLimitPage<TItemSchema extends z.ZodTypeAny>(
return new OffsetLimitPagePromise(fetchResult, schema, url, params, requestInit, options);
}

export function zodupload<
TResponseBodySchema extends z.ZodTypeAny,
TBody = Record<string, unknown>,
>(
schema: TResponseBodySchema,
url: string,
body: TBody,
requestInit?: RequestInit,
options?: ZodFetchOptions
): ApiPromise<z.output<TResponseBodySchema>> {
const finalRequestInit = createMultipartFormRequestInit(body, requestInit);

return new ApiPromise(_doZodFetch(schema, url, finalRequestInit, options));
}

async function createMultipartFormRequestInit<TBody = Record<string, unknown>>(
body: TBody,
requestInit?: RequestInit
): Promise<RequestInit> {
const form = await createForm(body);
const encoder = new FormDataEncoder(form);

const finalHeaders: Record<string, string> = {};

for (const [key, value] of Object.entries(requestInit?.headers || {})) {
finalHeaders[key] = value as string;
}

for (const [key, value] of Object.entries(encoder.headers)) {
finalHeaders[key] = value;
}

finalHeaders["Content-Length"] = String(encoder.contentLength);

const finalRequestInit: RequestInit = {
...requestInit,
headers: finalHeaders,
body: Readable.from(encoder) as any,
// @ts-expect-error
duplex: "half",
};

return finalRequestInit;
}

const createForm = async <T = Record<string, unknown>>(body: T | undefined): Promise<FormData> => {
const form = new FormData();
await Promise.all(
Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))
);
return form;
};

type ZodFetchResult<T> = {
data: T;
response: Response;
Expand Down Expand Up @@ -324,214 +270,6 @@ function requestInitWithCache(requestInit?: RequestInit): RequestInit {
}
}

const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
if (value === undefined) return;
if (value == null) {
throw new TypeError(
`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`
);
}

// TODO: make nested formats configurable
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
form.append(key, String(value));
} else if (
isUploadable(value) ||
isBlobLike(value) ||
value instanceof Buffer ||
value instanceof ArrayBuffer
) {
const file = await toFile(value);
form.append(key, file as File);
} else if (Array.isArray(value)) {
await Promise.all(value.map((entry) => addFormValue(form, key + "[]", entry)));
} else if (typeof value === "object") {
await Promise.all(
Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop))
);
} else {
throw new TypeError(
`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`
);
}
};

export type ToFileInput = Uploadable | Exclude<BlobLikePart, string> | AsyncIterable<BlobLikePart>;

/**
* Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
* @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
* @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
* @param {Object=} options additional properties
* @param {string=} options.type the MIME type of the content
* @param {number=} options.lastModified the last modified timestamp
* @returns a {@link File} with the given properties
*/
export async function toFile(
value: ToFileInput | PromiseLike<ToFileInput>,
name?: string | null | undefined,
options?: FilePropertyBag | undefined
): Promise<FileLike> {
// If it's a promise, resolve it.
value = await value;

// Use the file's options if there isn't one provided
options ??= isFileLike(value) ? { lastModified: value.lastModified, type: value.type } : {};

if (isResponseLike(value)) {
const blob = await value.blob();
name ||= new URL(value.url).pathname.split(/[\\/]/).pop() ?? "unknown_file";

return new File([blob as any], name, options);
}

const bits = await getBytes(value);

name ||= getName(value) ?? "unknown_file";

if (!options.type) {
const type = (bits[0] as any)?.type;
if (typeof type === "string") {
options = { ...options, type };
}
}

return new File(bits, name, options);
}

function getName(value: any): string | undefined {
return (
getStringFromMaybeBuffer(value.name) ||
getStringFromMaybeBuffer(value.filename) ||
// For fs.ReadStream
getStringFromMaybeBuffer(value.path)?.split(/[\\/]/).pop()
);
}

const getStringFromMaybeBuffer = (x: string | Buffer | unknown): string | undefined => {
if (typeof x === "string") return x;
if (typeof Buffer !== "undefined" && x instanceof Buffer) return String(x);
return undefined;
};

async function getBytes(value: ToFileInput): Promise<Array<BlobPart>> {
let parts: Array<BlobPart> = [];
if (
typeof value === "string" ||
ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
value instanceof ArrayBuffer
) {
parts.push(value);
} else if (isBlobLike(value)) {
parts.push(await value.arrayBuffer());
} else if (
isAsyncIterableIterator(value) // includes Readable, ReadableStream, etc.
) {
for await (const chunk of value) {
parts.push(chunk as BlobPart); // TODO, consider validating?
}
} else {
throw new Error(
`Unexpected data type: ${typeof value}; constructor: ${value?.constructor
?.name}; props: ${propsForError(value)}`
);
}

return parts;
}

function propsForError(value: any): string {
const props = Object.getOwnPropertyNames(value);
return `[${props.map((p) => `"${p}"`).join(", ")}]`;
}

const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator<unknown> =>
value != null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";

/**
* Intended to match web.Blob, node.Blob, node-fetch.Blob, etc.
*/
export interface BlobLike {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
readonly size: number;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
readonly type: string;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
text(): Promise<string>;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
slice(start?: number, end?: number): BlobLike;
// unfortunately @types/node-fetch@^2.6.4 doesn't type the arrayBuffer method
}

/**
* Intended to match web.File, node.File, node-fetch.File, etc.
*/
export interface FileLike extends BlobLike {
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
readonly lastModified: number;
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
readonly name: string;
}

/**
* Intended to match web.Response, node.Response, node-fetch.Response, etc.
*/
export interface ResponseLike {
url: string;
blob(): Promise<BlobLike>;
}

export type Uploadable = FileLike | ResponseLike | Readable;

export const isResponseLike = (value: any): value is ResponseLike =>
value != null &&
typeof value === "object" &&
typeof value.url === "string" &&
typeof value.blob === "function";

export const isFileLike = (value: any): value is FileLike =>
value != null &&
typeof value === "object" &&
typeof value.name === "string" &&
typeof value.lastModified === "number" &&
isBlobLike(value);

/**
* The BlobLike type omits arrayBuffer() because @types/node-fetch@^2.6.4 lacks it; but this check
* adds the arrayBuffer() method type because it is available and used at runtime
*/
export const isBlobLike = (
value: any
): value is BlobLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
value != null &&
typeof value === "object" &&
typeof value.size === "number" &&
typeof value.type === "string" &&
typeof value.text === "function" &&
typeof value.slice === "function" &&
typeof value.arrayBuffer === "function";

export const isFsReadStream = (value: any): value is Readable => value instanceof Readable;

export const isUploadable = (value: any): value is Uploadable => {
return isFileLike(value) || isResponseLike(value) || isFsReadStream(value);
};

export type BlobLikePart =
| string
| ArrayBuffer
| ArrayBufferView
| BlobLike
| Uint8Array
| DataView;

export const isRecordLike = (value: any): value is Record<string, string> =>
value != null &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.keys(value).length > 0 &&
Object.keys(value).every((key) => typeof key === "string" && typeof value[key] === "string");

/**
* A subclass of `Promise` providing additional helper methods
* for interacting with the SDK.
Expand Down
Loading
Loading