From b5bda7995189f109a434dc7ba0fbeb3d55489000 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 9 Jun 2023 18:22:08 +0000 Subject: [PATCH 01/34] Fix dev server integration tests following function config change (#221) ## Summary Internal dev server JSON provided at the `/dev` endpoint changed shape slightly. This accounts for the change and performs some vague code clean-up. Intentionally being more explicit about the two checks in tests rather than trying to group them and abstract the underlying data; they should be allowed to drift. --- src/examples/hello-world/index.test.ts | 36 +---- src/examples/parallel-reduce/index.test.ts | 38 +---- src/examples/parallel-work/index.test.ts | 38 +---- src/examples/polling/index.test.ts | 36 +---- src/examples/promise-all/index.test.ts | 38 +---- src/examples/promise-race/index.test.ts | 38 +---- src/examples/send-event/index.test.ts | 38 +---- src/examples/sequential-reduce/index.test.ts | 38 +---- src/test/helpers.ts | 146 +++++++++++++++---- 9 files changed, 155 insertions(+), 291 deletions(-) diff --git a/src/examples/hello-world/index.test.ts b/src/examples/hello-world/index.test.ts index fdf064824..af3eeed20 100644 --- a/src/examples/hello-world/index.test.ts +++ b/src/examples/hello-world/index.test.ts @@ -1,42 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; import { + checkIntrospection, eventRunWithName, - introspectionSchema, runHasTimeline, sendEvent, } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Hello World", - id: expect.stringMatching(/^.*-hello-world$/), - triggers: [{ event: "demo/hello.world" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-hello-world&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Hello World", + triggers: [{ event: "demo/hello.world" }], }); describe("run", () => { diff --git a/src/examples/parallel-reduce/index.test.ts b/src/examples/parallel-reduce/index.test.ts index b1feb0b2e..a7b9cf2d4 100644 --- a/src/examples/parallel-reduce/index.test.ts +++ b/src/examples/parallel-reduce/index.test.ts @@ -1,42 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; import { + checkIntrospection, eventRunWithName, - introspectionSchema, runHasTimeline, - sendEvent, + sendEvent } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Parallel Reduce", - id: expect.stringMatching(/^.*-parallel-reduce$/), - triggers: [{ event: "demo/parallel.reduce" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-parallel-reduce&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Parallel Reduce", + triggers: [{ event: "demo/parallel.reduce" }], }); describe("run", () => { diff --git a/src/examples/parallel-work/index.test.ts b/src/examples/parallel-work/index.test.ts index 2e13cd5c1..93bf29cf5 100644 --- a/src/examples/parallel-work/index.test.ts +++ b/src/examples/parallel-work/index.test.ts @@ -1,42 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; import { + checkIntrospection, eventRunWithName, - introspectionSchema, runHasTimeline, - sendEvent, + sendEvent } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Parallel Work", - id: expect.stringMatching(/^.*-parallel-work$/), - triggers: [{ event: "demo/parallel.work" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-parallel-work&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Parallel Work", + triggers: [{ event: "demo/parallel.work" }], }); describe("run", () => { diff --git a/src/examples/polling/index.test.ts b/src/examples/polling/index.test.ts index 6db8e4033..5c1a099a3 100644 --- a/src/examples/polling/index.test.ts +++ b/src/examples/polling/index.test.ts @@ -1,35 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; -import { introspectionSchema } from "../../test/helpers"; +import { checkIntrospection } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Polling", - id: expect.stringMatching(/^.*-polling$/), - triggers: [{ event: "demo/polling" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-polling&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Polling", + triggers: [{ event: "demo/polling" }], }); diff --git a/src/examples/promise-all/index.test.ts b/src/examples/promise-all/index.test.ts index 6e7b24d14..1fd89fab1 100644 --- a/src/examples/promise-all/index.test.ts +++ b/src/examples/promise-all/index.test.ts @@ -1,42 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; import { + checkIntrospection, eventRunWithName, - introspectionSchema, runHasTimeline, - sendEvent, + sendEvent } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Promise.all", - id: expect.stringMatching(/^.*-promise-all$/), - triggers: [{ event: "demo/promise.all" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-promise-all&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Promise.all", + triggers: [{ event: "demo/promise.all" }], }); describe("run", () => { diff --git a/src/examples/promise-race/index.test.ts b/src/examples/promise-race/index.test.ts index cd9a88fe9..395fa8363 100644 --- a/src/examples/promise-race/index.test.ts +++ b/src/examples/promise-race/index.test.ts @@ -1,44 +1,16 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; import { + checkIntrospection, eventRunWithName, - introspectionSchema, runHasTimeline, - sendEvent, + sendEvent } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Promise.race", - id: expect.stringMatching(/^.*-promise-race$/), - triggers: [{ event: "demo/promise.race" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-promise-race&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Promise.race", + triggers: [{ event: "demo/promise.race" }], }); describe("run", () => { diff --git a/src/examples/send-event/index.test.ts b/src/examples/send-event/index.test.ts index 3fcad06d9..a33459065 100644 --- a/src/examples/send-event/index.test.ts +++ b/src/examples/send-event/index.test.ts @@ -1,45 +1,17 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; import { + checkIntrospection, eventRunWithName, - introspectionSchema, receivedEventWithName, runHasTimeline, - sendEvent, + sendEvent } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Send event", - id: expect.stringMatching(/^.*-send-event$/), - triggers: [{ event: "demo/send.event" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-send-event&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Send event", + triggers: [{ event: "demo/send.event" }], }); describe("run", () => { diff --git a/src/examples/sequential-reduce/index.test.ts b/src/examples/sequential-reduce/index.test.ts index 0d3120ded..449c768a3 100644 --- a/src/examples/sequential-reduce/index.test.ts +++ b/src/examples/sequential-reduce/index.test.ts @@ -1,42 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import fetch from "cross-fetch"; import { + checkIntrospection, eventRunWithName, - introspectionSchema, runHasTimeline, - sendEvent, + sendEvent } from "../../test/helpers"; -describe("introspection", () => { - const specs = [ - { label: "SDK UI", url: "http://127.0.0.1:3000/api/inngest?introspect" }, - { label: "Dev server UI", url: "http://localhost:8288/dev" }, - ]; - - specs.forEach(({ label, url }) => { - test(`should show registered functions in ${label}`, async () => { - const res = await fetch(url); - const data = introspectionSchema.parse(await res.json()); - - expect(data.functions).toContainEqual({ - name: "Sequential Reduce", - id: expect.stringMatching(/^.*-sequential-reduce$/), - triggers: [{ event: "demo/sequential.reduce" }], - steps: { - step: { - id: "step", - name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - /^http.+\?fnId=.+-sequential-reduce&stepId=step$/ - ), - }, - }, - }, - }); - }); - }); +checkIntrospection({ + name: "Sequential Reduce", + triggers: [{ event: "demo/sequential.reduce" }], }); describe("run", () => { diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 4dfe70962..d7baa574b 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -5,6 +5,8 @@ import { Inngest } from "@local"; import { type ServeHandler } from "@local/components/InngestCommHandler"; import { envKeys, headerKeys } from "@local/helpers/consts"; +import { slugify } from "@local/helpers/strings"; +import { type FunctionTrigger } from "@local/types"; import { version } from "@local/version"; import fetch from "cross-fetch"; import { type Request, type Response } from "express"; @@ -713,35 +715,6 @@ export const testFramework = ( }); }; -/** - * A Zod schema for an introspection result from the SDK UI or the dev server. - */ -export const introspectionSchema = z.object({ - functions: z.array( - z.object({ - name: z.string(), - id: z.string(), - triggers: z.array( - z.object({ event: z.string() }).or( - z.object({ - cron: z.string(), - }) - ) - ), - steps: z.object({ - step: z.object({ - id: z.literal("step"), - name: z.literal("step"), - runtime: z.object({ - type: z.literal("http"), - url: z.string().url(), - }), - }), - }), - }) - ), -}); - /** * A test helper used to send events to a local, unsecured dev server. * @@ -971,3 +944,118 @@ export const runHasTimeline = async ( return; }; + +interface CheckIntrospection { + name: string; + triggers: FunctionTrigger[]; +} + +export const checkIntrospection = ({ name, triggers }: CheckIntrospection) => { + describe("introspection", () => { + it("should be registered in SDK UI", async () => { + const res = await fetch("http://127.0.0.1:3000/api/inngest?introspect"); + + const data = z + .object({ + functions: z.array( + z.object({ + name: z.string(), + id: z.string(), + triggers: z.array( + z.object({ event: z.string() }).or( + z.object({ + cron: z.string(), + }) + ) + ), + steps: z.object({ + step: z.object({ + id: z.literal("step"), + name: z.literal("step"), + runtime: z.object({ + type: z.literal("http"), + url: z.string().url(), + }), + }), + }), + }) + ), + }) + .parse(await res.json()); + + expect(data.functions).toContainEqual({ + name, + id: expect.stringMatching(new RegExp(`^.*-${slugify(name)}$`)), + triggers, + steps: { + step: { + id: "step", + name: "step", + runtime: { + type: "http", + url: expect.stringMatching( + new RegExp(`^http.+\\?fnId=.+-${slugify(name)}&stepId=step$`) + ), + }, + }, + }, + }); + }); + + it("should be registered in Dev Server UI", async () => { + const res = await fetch("http://localhost:8288/dev"); + + const data = z + .object({ + handlers: z.array( + z.object({ + sdk: z.object({ + functions: z.array( + z.object({ + name: z.string(), + id: z.string(), + triggers: z.array( + z.object({ event: z.string() }).or( + z.object({ + cron: z.string(), + }) + ) + ), + steps: z.object({ + step: z.object({ + id: z.literal("step"), + name: z.literal("step"), + runtime: z.object({ + type: z.literal("http"), + url: z.string().url(), + }), + }), + }), + }) + ), + }), + }) + ), + }) + .parse(await res.json()); + + expect(data.handlers[0]?.sdk.functions).toContainEqual({ + name, + id: expect.stringMatching(new RegExp(`^.*-${slugify(name)}$`)), + triggers, + steps: { + step: { + id: "step", + name: "step", + runtime: { + type: "http", + url: expect.stringMatching( + new RegExp(`^http.+\\?fnId=.+-${slugify(name)}&stepId=step$`) + ), + }, + }, + }, + }); + }); + }); +}; From cc3929dc8a60a2d4104871b213165b7c6f3191d5 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 9 Jun 2023 18:27:37 +0000 Subject: [PATCH 02/34] Fix creating a new comparison date when hashing step sleep IDs (#220) ## Summary Fixes a bug where we use two different `Date` objects to represent "now," breaking `step.sleep()` hashing in _very rare_ circumstances. Adds some tests that try to give the problem an opportunity to appear; it's hard to cause the problem to appear due to it being temporally reliant. --- .changeset/itchy-geckos-bathe.md | 5 +++++ src/components/InngestStepTools.test.ts | 2 +- src/helpers/strings.test.ts | 16 +++++++++++++- src/helpers/strings.ts | 29 +++++-------------------- src/types.ts | 2 +- 5 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 .changeset/itchy-geckos-bathe.md diff --git a/.changeset/itchy-geckos-bathe.md b/.changeset/itchy-geckos-bathe.md new file mode 100644 index 000000000..8f9693a20 --- /dev/null +++ b/.changeset/itchy-geckos-bathe.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Fix a very rare bug in which `step.sleep()` hashing could produce different IDs across different executions diff --git a/src/components/InngestStepTools.test.ts b/src/components/InngestStepTools.test.ts index cf4eeedba..7b6febc58 100644 --- a/src/components/InngestStepTools.test.ts +++ b/src/components/InngestStepTools.test.ts @@ -70,7 +70,7 @@ describe("waitForEvent", () => { void waitForEvent("event", { timeout: upcoming }); expect(getOp()).toMatchObject({ opts: { - timeout: expect.stringContaining("6d"), + timeout: expect.stringMatching(upcoming.toISOString()), }, }); }); diff --git a/src/helpers/strings.test.ts b/src/helpers/strings.test.ts index 13dc2322f..56641c204 100644 --- a/src/helpers/strings.test.ts +++ b/src/helpers/strings.test.ts @@ -1,4 +1,4 @@ -import { slugify } from "./strings"; +import { slugify, timeStr } from "./strings"; describe("slugify", () => { it("Generates a slug using hyphens", () => { @@ -28,3 +28,17 @@ describe("slugify", () => { } }); }); + +describe("timeStr", () => { + test("Converts milliseconds to a time string", () => { + expect(timeStr(1000)).toEqual("1s"); + }); + + test("converts ms string to a time string", () => { + expect(timeStr("1 day")).toEqual("1d"); + }); + + test("converts a date to an ISO string", () => { + expect(timeStr(new Date(0))).toEqual("1970-01-01T00:00:00.000Z"); + }); +}); diff --git a/src/helpers/strings.ts b/src/helpers/strings.ts index 034c18a12..cf442df81 100644 --- a/src/helpers/strings.ts +++ b/src/helpers/strings.ts @@ -46,30 +46,13 @@ export const timeStr = ( /** * The future date to use to convert to a time string. */ - input: string | number | Date, - - /** - * Optionally provide a date to use as the base for the calculation. - */ - now = new Date() -): TimeStr => { - let date = input; - - if (typeof date === "string" || typeof date === "number") { - const numTimeout = typeof date === "string" ? ms(date) : date; - date = new Date(Date.now() + numTimeout); - } - - now.setMilliseconds(0); - date.setMilliseconds(0); - - const isValidDate = !isNaN(date.getTime()); - - if (!isValidDate) { - throw new Error("Invalid date given to convert to time string"); + input: string | number | Date +): string => { + if (input instanceof Date) { + return input.toISOString(); } - const timeNum = date.getTime() - now.getTime(); + const milliseconds: number = typeof input === "string" ? ms(input) : input; const [, timeStr] = periods.reduce<[number, string]>( ([num, str], [suffix, period]) => { @@ -81,7 +64,7 @@ export const timeStr = ( return [num, str]; }, - [timeNum, ""] + [milliseconds, ""] ); return timeStr as TimeStr; diff --git a/src/types.ts b/src/types.ts index 76c09bd69..1592d1a5a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -927,7 +927,7 @@ export interface FunctionConfig { cancel?: { event: string; if?: string; - timeout?: TimeStr; + timeout?: string; }[]; } From 4226b8560ae3091d793e5fd69423f5de91505984 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 9 Jun 2023 18:32:14 +0000 Subject: [PATCH 03/34] Fix output hooks not running when main function body throws (#218) ## Summary Output hooks were not running if an asynchronous, non-step function's body threw. We expect to handle all errors, so the output hook should be appropriately reporting the problem here. This also ensures that the built-in logger middleware logs any error from a function's execution, as is expected. --- .changeset/flat-fans-nail.md | 5 +++ src/components/InngestCommHandler.ts | 17 ++++---- src/components/InngestFunction.test.ts | 18 +++++++-- src/components/InngestFunction.ts | 54 ++++++++++++++++---------- src/helpers/errors.ts | 8 ++-- 5 files changed, 66 insertions(+), 36 deletions(-) create mode 100644 .changeset/flat-fans-nail.md diff --git a/.changeset/flat-fans-nail.md b/.changeset/flat-fans-nail.md new file mode 100644 index 000000000..ccb9405f4 --- /dev/null +++ b/.changeset/flat-fans-nail.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Fix middleware `transformOutput` hook not running if an asynchronous, non-step function's body threw diff --git a/src/components/InngestCommHandler.ts b/src/components/InngestCommHandler.ts index ade54b029..29e375d4f 100644 --- a/src/components/InngestCommHandler.ts +++ b/src/components/InngestCommHandler.ts @@ -12,7 +12,7 @@ import { platformSupportsStreaming, skipDevServer, } from "../helpers/env"; -import { OutgoingOpError, serializeError } from "../helpers/errors"; +import { OutgoingResultError, serializeError } from "../helpers/errors"; import { cacheFn } from "../helpers/functions"; import { strBoolean } from "../helpers/scalar"; import { createStream } from "../helpers/stream"; @@ -874,7 +874,10 @@ export class InngestCommHandler< * altered by middleware, whereas `error` is the initial triggering * error. */ - throw new OutgoingOpError(ret[1]); + throw new OutgoingResultError({ + data: ret[1].data, + error: ret[1].error, + }); } return { @@ -889,20 +892,20 @@ export class InngestCommHandler< * * See {@link https://www.npmjs.com/package/serialize-error} */ - const isOutgoingOpError = unserializedErr instanceof OutgoingOpError; + const isOutgoingOpError = unserializedErr instanceof OutgoingResultError; let error: string; if (isOutgoingOpError) { error = - typeof unserializedErr.op.data === "string" - ? unserializedErr.op.data - : stringify(unserializedErr.op.data); + typeof unserializedErr.result.data === "string" + ? unserializedErr.result.data + : stringify(unserializedErr.result.data); } else { error = stringify(serializeError(unserializedErr)); } const isNonRetriableError = isOutgoingOpError - ? unserializedErr.op.error instanceof NonRetriableError + ? unserializedErr.result.error instanceof NonRetriableError : unserializedErr instanceof NonRetriableError; /** diff --git a/src/components/InngestFunction.test.ts b/src/components/InngestFunction.test.ts index 4b73ee0d3..983e2bc7c 100644 --- a/src/components/InngestFunction.test.ts +++ b/src/components/InngestFunction.test.ts @@ -9,7 +9,7 @@ import { type UnhashedOp, } from "@local/components/InngestStepTools"; import { ServerTiming } from "@local/helpers/ServerTiming"; -import { ErrCode } from "@local/helpers/errors"; +import { ErrCode, OutgoingResultError } from "@local/helpers/errors"; import { DefaultLogger, ProxyLogger, @@ -138,7 +138,7 @@ describe("runFn", () => { ); }); - test("bubble thrown error", async () => { + test("wrap thrown error", async () => { await expect( fn["runFn"]( { event: { name: "foo", data: { foo: "foo" } } }, @@ -147,7 +147,13 @@ describe("runFn", () => { timer, false ) - ).rejects.toThrow(stepErr); + ).rejects.toThrow( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + expect.objectContaining({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + result: expect.objectContaining({ error: stepErr }), + }) + ); }); }); }); @@ -245,7 +251,11 @@ describe("runFn", () => { if (t.expectedThrowMessage) { test("throws expected error", () => { - expect(retErr?.message ?? "").toContain(t.expectedThrowMessage); + expect( + retErr instanceof OutgoingResultError + ? (retErr.result.error as Error)?.message + : retErr?.message ?? "" + ).toContain(t.expectedThrowMessage); }); } else { test("returns expected value", () => { diff --git a/src/components/InngestFunction.ts b/src/components/InngestFunction.ts index 49aa75d6e..3309c0761 100644 --- a/src/components/InngestFunction.ts +++ b/src/components/InngestFunction.ts @@ -3,6 +3,7 @@ import { type ServerTiming } from "../helpers/ServerTiming"; import { internalEvents, queryKeys } from "../helpers/consts"; import { ErrCode, + OutgoingResultError, deserializeError, functionStoppedRunningErr, serializeError, @@ -293,6 +294,35 @@ export class InngestFunction< } ); + const createFinalError = async ( + err: unknown, + step?: OutgoingOp + ): Promise => { + await hookStack.afterExecution?.(); + + const result: Pick = { + error: err, + }; + + try { + result.data = serializeError(err); + } catch (serializationErr) { + console.warn( + "Could not serialize error to return to Inngest; stringifying instead", + serializationErr + ); + + result.data = err; + } + + const hookOutput = await applyHookToOutput(hookStack.transformOutput, { + result, + step, + }); + + return new OutgoingResultError(hookOutput); + }; + const state = createExecutionState(); const memoizingStop = timer.start("memoizing"); @@ -497,27 +527,7 @@ export class InngestFunction< runningStepStop(); }) .catch(async (err: Error) => { - await hookStack.afterExecution?.(); - - const result: Pick = { - error: err, - }; - - try { - result.data = serializeError(err); - } catch (serializationErr) { - console.warn( - "Could not serialize error to return to Inngest; stringifying instead", - serializationErr - ); - - result.data = err; - } - - return await applyHookToOutput(hookStack.transformOutput, { - result, - step: outgoingUserFnOp, - }); + return await createFinalError(err, outgoingUserFnOp); }) .then(async (data) => { await hookStack.afterExecution?.(); @@ -649,6 +659,8 @@ export class InngestFunction< await hookStack.afterExecution?.(); return ["discovery", discoveredOps]; + } catch (err) { + throw await createFinalError(err); } finally { await hookStack.beforeResponse?.(); } diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index e8dba1c3b..50a29b8d7 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -266,11 +266,11 @@ export const fixEventKeyMissingSteps = [ * * @internal */ -export class OutgoingOpError extends Error { - public readonly op: OutgoingOp; +export class OutgoingResultError extends Error { + public readonly result: Pick; - constructor(op: OutgoingOp) { + constructor(result: Pick) { super("OutgoingOpError"); - this.op = op; + this.result = result; } } From efc37dcf5446a6c7f0c52b5fe7d410729518ae55 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 9 Jun 2023 19:51:57 +0000 Subject: [PATCH 04/34] Add workflow that allows us to automatically backport merged PRs (#223) ## Summary This handy action allows us to backport a PR to a particular branch when merged using labels. ![image](https://github.com/inngest/inngest-js/assets/1736957/54f4295d-8bc8-4341-a277-0344f9e86615) Just add the label, then when the PR is merged a new PR will be opened to the relevant branch (e.g. `1.x`) to backport the squashed commit. This does not manage releasing; we'll use changesets for that. ## Related - #224 --- .github/workflows/backport.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 000000000..9a119f4f4 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,27 @@ +name: Backport + +on: + pull_request_target: + types: + - closed + - labeled + +jobs: + backport: + name: Backport + runs-on: ubuntu-latest + # Only react to merged PRs for security reasons. + # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. + if: > + github.event.pull_request.merged + && ( + github.event.action == 'closed' + || ( + github.event.action == 'labeled' + && contains(github.event.label.name, 'backport') + ) + ) + steps: + - uses: tibdex/backport@v2 + with: + github_token: ${{ secrets.CHANGESET_GITHUB_TOKEN }} From 634e2d6af168241ebf544512bdcb528e562ae511 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 9 Jun 2023 19:55:23 +0000 Subject: [PATCH 05/34] Add automated legacy releases to `*.x` branches (#224) ## Summary Use `*.x` branches such as `1.x` with changesets to create automated release PRs, similar to those to `@latest`. Here, we add a small script to generate a dynamic config for these branches so that changesets knows what to compare against. Also includes a small change to rename release PRs to either `Release @latest` or (for example) `Release @1.x`. ## Related - #223 --- .github/workflows/release.yml | 6 ++++++ scripts/generateReleaseConfig.js | 30 ++++++++++++++++++++++++++++++ scripts/release.js | 15 +++++++++++---- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 scripts/generateReleaseConfig.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 088598cb9..d90893a1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - '*.x' concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -21,11 +22,16 @@ jobs: with: persist-credentials: false - uses: ./.github/actions/setup-and-build + - run: node scripts/generateReleaseConfig.js + env: + BRANCH: ${{ github.ref_name }} - uses: changesets/action@v1 id: changesets with: publish: yarn release + title: ${{ github.ref_name == 'main' && 'Release @latest' || format('Release @{0}', github.ref_name) }} env: GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_ENV: test # disable npm access checks; they don't work in CI + BRANCH: ${{ github.ref_name }} diff --git a/scripts/generateReleaseConfig.js b/scripts/generateReleaseConfig.js new file mode 100644 index 000000000..70c7634f3 --- /dev/null +++ b/scripts/generateReleaseConfig.js @@ -0,0 +1,30 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const config = { + "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": process.env.BRANCH || "main", + "updateInternalDependencies": "patch", + "ignore": [], +}; + +console.log("Writing release config:", config); + +const rootDir = path.join(__dirname, ".."); +process.chdir(rootDir); + +const changesetDir = path.join(rootDir, ".changeset"); +const configName = "config.json"; +const configPath = path.join(changesetDir, configName); + +const serializedConfig = JSON.stringify(config, null, 2); + +fs.writeFileSync( + configPath, + serializedConfig, +); diff --git a/scripts/release.js b/scripts/release.js index 8236c7a13..c7b127f49 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -1,6 +1,13 @@ const path = require("path"); const { exec: rawExec, getExecOutput } = require("@actions/exec"); +const branch = process.env.BRANCH; +if (branch !== "main" && !branch.endsWith(".x")) { + throw new Error( + `Stopping release from branch ${branch}; only "main" and "*.x" branches are allowed to release`, + ); +} + const { version } = require("../package.json"); const tag = `v${version}`; @@ -25,12 +32,12 @@ const exec = async (...args) => { ["ls-remote", "--exit-code", "origin", "--tags", `refs/tags/${tag}`], { ignoreReturnCode: true, - } + }, ); if (exitCode === 0) { console.log( - `Action is not being published because version ${tag} is already published` + `Action is not being published because version ${tag} is already published`, ); return; } @@ -48,10 +55,10 @@ const exec = async (...args) => { ["publish", "--tag", distTag, "--access", "public", "--provenance"], { cwd: distDir, - } + }, ); // Tag and push the release commit await exec("changeset", ["tag"]); - await exec("git", ["push", "--follow-tags", "origin", "main"]); + await exec("git", ["push", "--follow-tags", "origin", branch]); })(); From ee3181fe8a9ad978c4b2273b894ddcddf5b9c2dd Mon Sep 17 00:00:00 2001 From: Jack Williams <1736957+jpwilliams@users.noreply.github.com> Date: Fri, 9 Jun 2023 21:10:52 +0100 Subject: [PATCH 06/34] Use `v*.x` branches for legacy releases --- .github/workflows/release.yml | 4 ++-- scripts/release.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d90893a1e..638181494 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: push: branches: - main - - '*.x' + - 'v*.x' concurrency: ${{ github.workflow }}-${{ github.ref }} @@ -29,7 +29,7 @@ jobs: id: changesets with: publish: yarn release - title: ${{ github.ref_name == 'main' && 'Release @latest' || format('Release @{0}', github.ref_name) }} + title: ${{ github.ref_name == 'main' && 'Release @latest' || format('Release {0}', github.ref_name) }} env: GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/scripts/release.js b/scripts/release.js index c7b127f49..c39f52dba 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -4,7 +4,7 @@ const { exec: rawExec, getExecOutput } = require("@actions/exec"); const branch = process.env.BRANCH; if (branch !== "main" && !branch.endsWith(".x")) { throw new Error( - `Stopping release from branch ${branch}; only "main" and "*.x" branches are allowed to release`, + `Stopping release from branch ${branch}; only "main" and "v*.x" branches are allowed to release`, ); } From 55c3b1c18d80dc6ebe73466eb8e107a45d2c6814 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Mon, 12 Jun 2023 12:41:48 +0000 Subject: [PATCH 07/34] Add support for publishing legacy versions in release script (#230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Releasing earlier versions to npm is a pain. This PR ensures we: 1. Find the actual latest version of the package 2. Publish the legacy version using the `latest` tag, ensuring semver updates work as expected 3. Re-tag `latest` as the version found in step 1 Following this, we should be ready to support backporting and legacy releases purely with labels and PRs. 🥳 ## Related - https://github.com/mermaid-js/mermaid/issues/4200 - https://github.com/bower/bower/issues/2041 - https://github.com/npm/npm/issues/6778 --- .github/workflows/release.yml | 2 +- README.md | 7 ++++++ scripts/release.js | 46 +++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 638181494..767c2e502 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - main - 'v*.x' -concurrency: ${{ github.workflow }}-${{ github.ref }} +concurrency: ${{ github.workflow }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 51fa40109..21e9acb04 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,13 @@ yarn add ./inngest.tgz && framework dev To release to production, we use [Changesets](https://github.com/changesets/changesets). This means that releasing and changelog generation is all managed through PRs, where a bot will guide you through the process of announcing changes in PRs and releasing them once merged to `main`. +#### Legacy versions + +Merging and releasing to previous major versions of the SDK is also supported. + +- Add a `backport v*.x` label (e.g. `backport v1.x`) to a PR to have a backport PR generated when the initial PR is merged. +- Merging into a `v*.x` branch creates a release PR (named **Release v1.x**, for example) the same as the `main` branch. Simply merge to release. + #### Snapshot versions If a local `inngest.tgz` isn't ideal, we can release a tagged version to npm. For now, this is relatively manual. For this, please ensure you are in an open PR branch for observability. diff --git a/scripts/release.js b/scripts/release.js index c39f52dba..0aa38af1e 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -8,11 +8,16 @@ if (branch !== "main" && !branch.endsWith(".x")) { ); } +console.log("branch:", branch); + const { version } = require("../package.json"); +console.log("version:", version); const tag = `v${version}`; +console.log("tag:", tag); const [, tagEnd = ""] = version.split("-"); const distTag = tagEnd.split(".")[0] || "latest"; +console.log("distTag:", distTag); const rootDir = path.join(__dirname, ".."); const distDir = path.join(rootDir, "dist"); @@ -46,10 +51,33 @@ const exec = async (...args) => { throw new Error(`git ls-remote exited with ${exitCode}:\n${stderr}`); } + // Get current latest version + const { exitCode: latestCode, stdout: latestStdout, stderr: latestStderr } = + await getExecOutput("npm", ["dist-tag", "ls"]); + + if (latestCode !== 0) { + throw new Error( + `npm dist-tag ls exited with ${latestCode}:\n${latestStderr}`, + ); + } + + const latestVersion = + latestStdout.split("\n").find((line) => line.startsWith("latest: "))?.split( + " ", + )[1]; + + if (!latestVersion) { + throw new Error(`Could not find "latest" dist-tag in:\n${latestStdout}`); + } + + console.log("latestVersion:", latestVersion); + // Release to npm await exec("npm", ["config", "set", "git-tag-version", "false"], { cwd: distDir, }); + + console.log("publishing", tag, "to dist tag:", distTag); await exec( "npm", ["publish", "--tag", distTag, "--access", "public", "--provenance"], @@ -58,7 +86,25 @@ const exec = async (...args) => { }, ); + // If this was a backport release, republish the "latest" tag at the actual latest version + if (branch !== "main" && distTag === "latest") { + console.log( + 'is backport release; updating "latest" tag to:', + latestVersion, + ); + + await exec("npm", [ + "dist-tag", + "add", + `inngest@${latestVersion}`, + "latest", + ]); + } + // Tag and push the release commit + console.log('running "changeset tag" to tag the release commit'); await exec("changeset", ["tag"]); + + console.log(`pushing git tags to origin/${branch}`); await exec("git", ["push", "--follow-tags", "origin", branch]); })(); From 3ef0b36d2995216cd5cb0f2582312678544ad767 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Mon, 12 Jun 2023 12:59:18 +0000 Subject: [PATCH 08/34] Add more info to 405s and ensure rejected GET is a 403 (#231) ## Summary Add more information when responding with a failure from the SDK. - Rejected `GET` now returns `403 Forbidden` instead of `405 Method Not Allowed` - Rejected `GET` now returns `isProd`, `skipDevServer`, and `showLandingPage` values - No actions being found remains a `405 Method Not Allowed` response - No actions being found now returns `isProd` and `skipDevServer` values --- .changeset/early-pots-fly.md | 5 +++++ src/components/InngestCommHandler.ts | 15 ++++++++++++--- src/test/helpers.ts | 15 ++++++++++++--- 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 .changeset/early-pots-fly.md diff --git a/.changeset/early-pots-fly.md b/.changeset/early-pots-fly.md new file mode 100644 index 000000000..ce94b54fb --- /dev/null +++ b/.changeset/early-pots-fly.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Add better visibility into serve handlers issues diff --git a/src/components/InngestCommHandler.ts b/src/components/InngestCommHandler.ts index 29e375d4f..8cc891d56 100644 --- a/src/components/InngestCommHandler.ts +++ b/src/components/InngestCommHandler.ts @@ -689,8 +689,13 @@ export class InngestCommHandler< if (this._isProd || !showLandingPage) { return { - status: 405, - body: "", + status: 403, + body: JSON.stringify({ + message: "Landing page requested but is disabled", + isProd: this._isProd, + skipDevServer: this._skipDevServer, + showLandingPage, + }), headers: {}, }; } @@ -756,7 +761,11 @@ export class InngestCommHandler< return { status: 405, - body: "", + body: JSON.stringify({ + message: "No action found; request was likely not POST, PUT, or GET", + isProd: this._isProd, + skipDevServer: this._skipDevServer, + }), headers: {}, }; } diff --git a/src/test/helpers.ts b/src/test/helpers.ts index d7baa574b..0b114862b 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -272,11 +272,14 @@ export const testFramework = ( ); expect(ret).toMatchObject({ - status: 405, + status: 403, headers: expect.objectContaining({ [headerKeys.SdkVersion]: expect.stringContaining("inngest-js:v"), [headerKeys.Framework]: expect.stringMatching(handler.name), }), + body: expect.stringContaining( + "Landing page requested but is disabled" + ), }); }); @@ -288,11 +291,14 @@ export const testFramework = ( ); expect(ret).toMatchObject({ - status: 405, + status: 403, headers: expect.objectContaining({ [headerKeys.SdkVersion]: expect.stringContaining("inngest-js:v"), [headerKeys.Framework]: expect.stringMatching(handler.name), }), + body: expect.stringContaining( + "Landing page requested but is disabled" + ), }); }); @@ -317,11 +323,14 @@ export const testFramework = ( }); expect(ret).toMatchObject({ - status: 405, + status: 403, headers: expect.objectContaining({ [headerKeys.SdkVersion]: expect.stringContaining("inngest-js:v"), [headerKeys.Framework]: expect.stringMatching(handler.name), }), + body: expect.stringContaining( + "Landing page requested but is disabled" + ), }); }); From 813108acc96a14185ac7b38d53553cd2c1cdf311 Mon Sep 17 00:00:00 2001 From: Inngest Release Bot <126702797+inngest-release-bot@users.noreply.github.com> Date: Mon, 12 Jun 2023 14:09:39 +0100 Subject: [PATCH 09/34] Release @latest (#222) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## inngest@2.0.1 ### Patch Changes - 3ef0b36: Add better visibility into serve handlers issues - 4226b85: Fix middleware `transformOutput` hook not running if an asynchronous, non-step function's body threw - cc3929d: Fix a very rare bug in which `step.sleep()` hashing could produce different IDs across different executions Co-authored-by: github-actions[bot] --- .changeset/early-pots-fly.md | 5 ----- .changeset/flat-fans-nail.md | 5 ----- .changeset/itchy-geckos-bathe.md | 5 ----- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 5 files changed, 9 insertions(+), 16 deletions(-) delete mode 100644 .changeset/early-pots-fly.md delete mode 100644 .changeset/flat-fans-nail.md delete mode 100644 .changeset/itchy-geckos-bathe.md diff --git a/.changeset/early-pots-fly.md b/.changeset/early-pots-fly.md deleted file mode 100644 index ce94b54fb..000000000 --- a/.changeset/early-pots-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Add better visibility into serve handlers issues diff --git a/.changeset/flat-fans-nail.md b/.changeset/flat-fans-nail.md deleted file mode 100644 index ccb9405f4..000000000 --- a/.changeset/flat-fans-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Fix middleware `transformOutput` hook not running if an asynchronous, non-step function's body threw diff --git a/.changeset/itchy-geckos-bathe.md b/.changeset/itchy-geckos-bathe.md deleted file mode 100644 index 8f9693a20..000000000 --- a/.changeset/itchy-geckos-bathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Fix a very rare bug in which `step.sleep()` hashing could produce different IDs across different executions diff --git a/CHANGELOG.md b/CHANGELOG.md index e54f3a9d6..6b8141d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # inngest +## 2.0.1 + +### Patch Changes + +- 3ef0b36: Add better visibility into serve handlers issues +- 4226b85: Fix middleware `transformOutput` hook not running if an asynchronous, non-step function's body threw +- cc3929d: Fix a very rare bug in which `step.sleep()` hashing could produce different IDs across different executions + ## 2.0.0 ### Major Changes diff --git a/package.json b/package.json index 619413dc5..763a2b72e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inngest", - "version": "2.0.0", + "version": "2.0.1", "description": "Official SDK for Inngest.com", "main": "./index.js", "types": "./index.d.ts", From 9c8a9d40e4cbde17675d0e83151ea59eb1f5c5af Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Mon, 12 Jun 2023 14:21:35 +0000 Subject: [PATCH 10/34] Add a more helpful summary to automated backport PRs (#237) ## Summary Adds a better PR body for backport PRs to describe what's happening. --- .github/workflows/backport.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 9a119f4f4..d8b8f9918 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,3 +25,13 @@ jobs: - uses: tibdex/backport@v2 with: github_token: ${{ secrets.CHANGESET_GITHUB_TOKEN }} + body_template: |- + ## Summary + + This is an automated backport of <%= mergeCommitSha %> from #<%= number %> to **<%= base %>**. It was created because a maintainer labeled #<%= number %> with the [backport <%= base %>](https://github.com/inngest/inngest-js/labels/backport%20<%= base %>) label. + + When this PR is merged, it will create a PR to release **<%= base %>** if a changeset is found. + + ## Related + + - #<%= number %> From f33464fd5ca9a79c9afee077a0d43efb8ea1637f Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Mon, 12 Jun 2023 14:24:03 +0000 Subject: [PATCH 11/34] Fix release script attempting to push to Yarn registry for setting tags (#236) ## Summary The release script in #224 and #230 almost worked. We see in run to release a `v1.x` backport that the following command failed: https://github.com/inngest/inngest-js/actions/runs/5244055017/jobs/9469531269 ``` $ npm dist-tag add inngest@2.0.1 latest npm ERR! code E401 npm ERR! 401 Unauthorized - PUT https://registry.yarnpkg.com/-/package/inngest/dist-tags/latest ``` Looks like it's defaulting to using Yarn's registry URL instsead of npm's. We get around this during publishing as we specify `publishConfig.registry` in our `package.json`, but it seems that `npm dist-tag` doesn't obey this same property. Let's manually set the registry for this particular command to ensure it tries to affect the right place. ## Related - #224 - #230 --- scripts/release.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/release.js b/scripts/release.js index 0aa38af1e..53a9fd987 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -10,7 +10,7 @@ if (branch !== "main" && !branch.endsWith(".x")) { console.log("branch:", branch); -const { version } = require("../package.json"); +const { version, publishConfig: { registry } } = require("../package.json"); console.log("version:", version); const tag = `v${version}`; console.log("tag:", tag); @@ -93,11 +93,15 @@ const exec = async (...args) => { latestVersion, ); + // `npm dist-tag` doesn't obey `publishConfig.registry`, so we must + // explicitly pass the registry URL here await exec("npm", [ "dist-tag", "add", `inngest@${latestVersion}`, "latest", + "--registry", + registry, ]); } From 023d761e5e7cb3b8b1f08e65a794cfc0a6094534 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 16 Jun 2023 17:00:26 +0000 Subject: [PATCH 12/34] Harden error serialization (#240) ## Summary There are some edge cases around error serialization that causes some non-useful errors to bubble up to users. Here we aim to cover more of these edge cases to return first-party errors when possible. --- .changeset/mean-turkeys-repeat.md | 5 + src/components/InngestCommHandler.ts | 14 +-- src/helpers/errors.ts | 135 ++++++++++++++++++++++----- 3 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 .changeset/mean-turkeys-repeat.md diff --git a/.changeset/mean-turkeys-repeat.md b/.changeset/mean-turkeys-repeat.md new file mode 100644 index 000000000..0366a980b --- /dev/null +++ b/.changeset/mean-turkeys-repeat.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Harden error serialization to ensure uncaught exceptions don't slip through during function runs diff --git a/src/components/InngestCommHandler.ts b/src/components/InngestCommHandler.ts index 8cc891d56..c999395b9 100644 --- a/src/components/InngestCommHandler.ts +++ b/src/components/InngestCommHandler.ts @@ -903,15 +903,11 @@ export class InngestCommHandler< */ const isOutgoingOpError = unserializedErr instanceof OutgoingResultError; - let error: string; - if (isOutgoingOpError) { - error = - typeof unserializedErr.result.data === "string" - ? unserializedErr.result.data - : stringify(unserializedErr.result.data); - } else { - error = stringify(serializeError(unserializedErr)); - } + const error = stringify( + serializeError( + isOutgoingOpError ? unserializedErr.result.data : unserializedErr + ) + ); const isNonRetriableError = isOutgoingOpError ? unserializedErr.result.error instanceof NonRetriableError diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 50a29b8d7..223cf10cb 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -1,27 +1,45 @@ import chalk from "chalk"; +import stringify from "json-stringify-safe"; import { deserializeError as cjsDeserializeError, serializeError as cjsSerializeError, + errorConstructors, + type SerializedError as CjsSerializedError, } from "serialize-error-cjs"; +import { z } from "zod"; import { type Inngest } from "../components/Inngest"; +import { NonRetriableError } from "../components/NonRetriableError"; import { type ClientOptions, type OutgoingOp } from "../types"; const SERIALIZED_KEY = "__serialized"; const SERIALIZED_VALUE = true; -export interface SerializedError { +/** + * Add first-class support for certain errors that we control, in addition to + * built-in errors such as `TypeError`. + * + * Adding these allows these non-standard errors to be correctly serialized, + * sent to Inngest, then deserialized back into the correct error type for users + * to react to correctly. + * + * Note that these errors only support `message?: string | undefined` as the + * input; more custom errors are not supported with this current strategy. + */ +errorConstructors.set( + "NonRetriableError", + NonRetriableError as ErrorConstructor +); + +export interface SerializedError extends Readonly { readonly [SERIALIZED_KEY]: typeof SERIALIZED_VALUE; - readonly name: string; - readonly message: string; - readonly stack: string; } /** - * Serialise an error to a plain object. + * Serialise an error to a serialized JSON string. * * Errors do not serialise nicely to JSON, so we use this function to convert - * them to a plain object. Doing this is also non-trivial for some errors, so - * we use the `serialize-error` package to do it for us. + * them to a serialized JSON string. Doing this is also non-trivial for some + * errors, so we use the `serialize-error` package to do it for us. * * See {@link https://www.npmjs.com/package/serialize-error} * @@ -32,29 +50,100 @@ export interface SerializedError { * Will not reserialise existing serialised errors. */ export const serializeError = (subject: unknown): SerializedError => { - if (isSerializedError(subject)) { - return subject as SerializedError; - } + try { + // Try to understand if this is already done. + // Will handle stringified errors. + const existingSerializedError = isSerializedError(subject); + + if (existingSerializedError) { + return existingSerializedError; + } - return { - ...cjsSerializeError(subject as Error), - [SERIALIZED_KEY]: SERIALIZED_VALUE, - } as const; + if (typeof subject === "object" && subject !== null) { + // Is an object, so let's try and serialize it. + const serializedErr = cjsSerializeError(subject as Error); + + // Serialization can succeed but assign no name or message, so we'll + // map over the result here to ensure we have everything. + // We'll just stringify the entire subject for the message, as this at + // least provides some context for the user. + return { + name: serializedErr.name || "Error", + message: + serializedErr.message || + stringify(subject) || + "Unknown error; error serialization could not find a message.", + stack: serializedErr.stack || "", + [SERIALIZED_KEY]: SERIALIZED_VALUE, + } as const; + } + + // If it's not an object, it's hard to parse this as an Error. In this case, + // we'll throw an error to start attempting backup strategies. + throw new Error("Error is not an object; strange throw value."); + } catch (err) { + try { + // If serialization fails, fall back to a regular Error and use the + // original object as the message for an Error. We don't know what this + // object looks like, so we can't do anything else with it. + return { + ...serializeError( + new Error(typeof subject === "string" ? subject : stringify(subject)) + ), + [SERIALIZED_KEY]: SERIALIZED_VALUE, + }; + } catch (err) { + // If this failed, then stringifying the object also failed, so we'll just + // return a completely generic error. + // Failing to stringify the object is very unlikely. + return { + name: "Could not serialize source error", + message: "Serializing the source error failed.", + stack: "", + [SERIALIZED_KEY]: SERIALIZED_VALUE, + }; + } + } }; /** - * Check if an object is a serialised error created by {@link serializeError}. + * Check if an object or a string is a serialised error created by + * {@link serializeError}. */ -export const isSerializedError = (value: unknown): boolean => { +export const isSerializedError = ( + value: unknown +): SerializedError | undefined => { try { - return ( - Object.prototype.hasOwnProperty.call(value, SERIALIZED_KEY) && - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (value as { [SERIALIZED_KEY]: unknown })[SERIALIZED_KEY] === - SERIALIZED_VALUE - ); + if (typeof value === "string") { + const parsed = z + .object({ + [SERIALIZED_KEY]: z.literal(SERIALIZED_VALUE), + name: z.enum([...errorConstructors.keys()] as [string, ...string[]]), + message: z.string(), + stack: z.string(), + }) + .passthrough() + .safeParse(JSON.parse(value)); + + if (parsed.success) { + return parsed.data as SerializedError; + } + } + + if (typeof value === "object" && value !== null) { + const objIsSerializedErr = + Object.prototype.hasOwnProperty.call(value, SERIALIZED_KEY) && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (value as { [SERIALIZED_KEY]: unknown })[SERIALIZED_KEY] === + SERIALIZED_VALUE; + + if (objIsSerializedErr) { + return value as SerializedError; + } + } } catch { - return false; + // no-op; we'll return undefined if parsing failed, as it isn't a serialized + // error } }; From ac7aab6ec3286b9d52082be4b7cd03653d8e29a8 Mon Sep 17 00:00:00 2001 From: Inngest Release Bot <126702797+inngest-release-bot@users.noreply.github.com> Date: Fri, 16 Jun 2023 18:38:57 +0100 Subject: [PATCH 13/34] Release @latest (#242) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## inngest@2.0.2 ### Patch Changes - 023d761: Harden error serialization to ensure uncaught exceptions don't slip through during function runs Co-authored-by: github-actions[bot] --- .changeset/mean-turkeys-repeat.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/mean-turkeys-repeat.md diff --git a/.changeset/mean-turkeys-repeat.md b/.changeset/mean-turkeys-repeat.md deleted file mode 100644 index 0366a980b..000000000 --- a/.changeset/mean-turkeys-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Harden error serialization to ensure uncaught exceptions don't slip through during function runs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8141d4d..5b3ebb4f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # inngest +## 2.0.2 + +### Patch Changes + +- 023d761: Harden error serialization to ensure uncaught exceptions don't slip through during function runs + ## 2.0.1 ### Patch Changes diff --git a/package.json b/package.json index 763a2b72e..d1e906dfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inngest", - "version": "2.0.1", + "version": "2.0.2", "description": "Official SDK for Inngest.com", "main": "./index.js", "types": "./index.d.ts", From b74477f371efd7e417b6b13a2edeedaad4eb405b Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Mon, 19 Jun 2023 13:23:04 +0000 Subject: [PATCH 14/34] Add optional step IDs (#245) ## Summary There are cases where function state recovery may want to be customized by the user to enable a different sort of state recovery. This change allows that by adding the ability to add an optional `id` to all step tooling, where it'll be used as the operation's hash. ```ts step.sendEvent(payloads, { id: "foo" }); step.waitForEvent("app/user.created", { id: "foo" }); step.run("Do something", () => {}, { id: "foo" }); step.sleep("5 days", { id: "foo" }); step.sleepUntil(someDate, { id: "foo" }); ``` --- .changeset/lucky-ants-juggle.md | 5 ++ src/components/InngestStepTools.test.ts | 36 ++++++++++++++ src/components/InngestStepTools.ts | 62 ++++++++++++++++++------- src/types.ts | 16 +++++++ 4 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 .changeset/lucky-ants-juggle.md diff --git a/.changeset/lucky-ants-juggle.md b/.changeset/lucky-ants-juggle.md new file mode 100644 index 000000000..110502ec0 --- /dev/null +++ b/.changeset/lucky-ants-juggle.md @@ -0,0 +1,5 @@ +--- +"inngest": minor +--- + +Add optional `id` property to all step tooling, allowing users to override state recovery diff --git a/src/components/InngestStepTools.test.ts b/src/components/InngestStepTools.test.ts index 7b6febc58..a3ea174b0 100644 --- a/src/components/InngestStepTools.test.ts +++ b/src/components/InngestStepTools.test.ts @@ -92,6 +92,13 @@ describe("waitForEvent", () => { }, }); }); + + test("uses custom `id` if given", () => { + void waitForEvent("event", { id: "custom", timeout: "2h" }); + expect(getOp()).toMatchObject({ + id: "custom", + }); + }); }); describe("run", () => { @@ -162,6 +169,13 @@ describe("run", () => { }> >(output); }); + + test("uses custom `id` if given", () => { + void run("step", () => undefined, { id: "custom" }); + expect(getOp()).toMatchObject({ + id: "custom", + }); + }); }); describe("sleep", () => { @@ -189,6 +203,13 @@ describe("sleep", () => { name: "1m", }); }); + + test("uses custom `id` if given", () => { + void sleep("1m", { id: "custom" }); + expect(getOp()).toMatchObject({ + id: "custom", + }); + }); }); describe("sleepUntil", () => { @@ -246,6 +267,13 @@ describe("sleepUntil", () => { "Invalid date or date string passed" ); }); + + test("uses custom `id` if given", () => { + void sleepUntil(new Date(), { id: "custom" }); + expect(getOp()).toMatchObject({ + id: "custom", + }); + }); }); describe("sendEvent", () => { @@ -292,6 +320,14 @@ describe("sendEvent", () => { expect(getOp()).toBeUndefined(); expect(sendSpy).toHaveBeenCalledWith({ name: "step", data: "foo" }); }); + + test("uses custom `id` if given", () => { + void sendEvent({ name: "step", data: "foo" }, { id: "custom" }); + + expect(getOp()).toMatchObject({ + id: "custom", + }); + }); }); describe("types", () => { diff --git a/src/components/InngestStepTools.ts b/src/components/InngestStepTools.ts index 79869423c..65fdb63c0 100644 --- a/src/components/InngestStepTools.ts +++ b/src/components/InngestStepTools.ts @@ -7,13 +7,17 @@ import { prettyError, } from "../helpers/errors"; import { timeStr } from "../helpers/strings"; -import { type ObjectPaths, type SendEventPayload } from "../helpers/types"; +import { + type ObjectPaths, + type PartialK, + type SendEventPayload, +} from "../helpers/types"; import { StepOpCode, type ClientOptions, type EventPayload, type HashedOp, - type Op, + type StepOpts, } from "../types"; import { type EventsFromOpts, type Inngest } from "./Inngest"; import { type ExecutionState } from "./InngestFunction"; @@ -47,16 +51,27 @@ export const createStepTools = < /** * Create a unique hash of an operation using only a subset of the operation's - * properties; will never use `data` and will guarantee the order of the object - * so we don't rely on individual tools for that. + * properties; will never use `data` and will guarantee the order of the + * object so we don't rely on individual tools for that. + * + * If the operation already contains an ID, the current ID will be used + * instead, so that users can provide their own IDs. */ const hashOp = ( /** - * The op to generate a hash from. We only use a subset of the op's properties - * when creating the hash. + * The op to generate a hash from. We only use a subset of the op's + * properties when creating the hash. */ - op: Op + op: PartialK ): HashedOp => { + /** + * If the op already has an ID, we don't need to generate one. This allows + * users to specify their own IDs. + */ + if (op.id) { + return op as HashedOp; + } + const obj = { parent: state.currentOp?.id ?? null, op: op.op, @@ -97,7 +112,7 @@ export const createStepTools = < * Arguments passed by the user. */ ...args: Parameters - ) => Omit, + ) => PartialK, "id">, opts?: { /** @@ -207,11 +222,13 @@ export const createStepTools = < */ sendEvent: createTool<{ >>( - payload: Payload + payload: Payload, + opts?: StepOpts ): Promise; }>( - () => { + (_payload, opts) => { return { + id: opts?.id, op: StepOpCode.StepPlanned, name: "sendEvent", }; @@ -278,7 +295,11 @@ export const createStepTools = < timeout: timeStr(typeof opts === "string" ? opts : opts.timeout), }; + let id: string | undefined; + if (typeof opts !== "string") { + id = opts?.id; + if (opts?.match) { matchOpts.if = `event.${opts.match} == async.${opts.match}`; } else if (opts?.if) { @@ -287,6 +308,7 @@ export const createStepTools = < } return { + id, op: StepOpCode.WaitForEvent, name: event as string, opts: matchOpts, @@ -323,7 +345,8 @@ export const createStepTools = < * call to `run`, meaning you can return and reason about return data * for next steps. */ - fn: T + fn: T, + opts?: StepOpts ) => Promise< /** * TODO Middleware can affect this. If run input middleware has returned @@ -338,8 +361,9 @@ export const createStepTools = < > > >( - (name) => { + (name, _fn, opts) => { return { + id: opts?.id, op: StepOpCode.StepPlanned, name, }; @@ -362,14 +386,16 @@ export const createStepTools = < /** * The amount of time to wait before continuing. */ - time: number | string + time: number | string, + opts?: StepOpts ) => Promise - >((time) => { + >((time, opts) => { /** * The presence of this operation in the returned stack indicates that the * sleep is over and we should continue execution. */ return { + id: opts?.id, op: StepOpCode.Sleep, name: timeStr(time), }; @@ -386,9 +412,10 @@ export const createStepTools = < /** * The date to wait until before continuing. */ - time: Date | string + time: Date | string, + opts?: StepOpts ) => Promise - >((time) => { + >((time, opts) => { const date = typeof time === "string" ? new Date(time) : time; /** @@ -397,6 +424,7 @@ export const createStepTools = < */ try { return { + id: opts?.id, op: StepOpCode.Sleep, name: date.toISOString(), }; @@ -426,7 +454,7 @@ export const createStepTools = < interface WaitForEventOpts< TriggeringEvent extends EventPayload, IncomingEvent extends EventPayload -> { +> extends StepOpts { /** * The step function will wait for the event for a maximum of this time, at * which point the event will be returned as `null` instead of any event data. diff --git a/src/types.ts b/src/types.ts index 1592d1a5a..a20609899 100644 --- a/src/types.ts +++ b/src/types.ts @@ -972,3 +972,19 @@ export type SupportedFrameworkName = | "redwoodjs" | "remix" | "deno/fresh"; + +/** + * A set of options that can be passed to any step to configure it. + */ +export interface StepOpts { + /** + * Passing an `id` for a step will overwrite the generated hash that is used + * by Inngest to pause and resume a function. + * + * This is useful if you want to ensure that a step is always the same ID even + * if the code changes. + * + * We recommend not using this unless you have a specific reason to do so. + */ + id?: string; +} From 7fe05ea0cff23510d109431fa74fa33689b53fad Mon Sep 17 00:00:00 2001 From: Inngest Release Bot <126702797+inngest-release-bot@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:01:43 +0100 Subject: [PATCH 15/34] Release @latest (#246) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## inngest@2.1.0 ### Minor Changes - b74477f: Add optional `id` property to all step tooling, allowing users to override state recovery Co-authored-by: github-actions[bot] --- .changeset/lucky-ants-juggle.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/lucky-ants-juggle.md diff --git a/.changeset/lucky-ants-juggle.md b/.changeset/lucky-ants-juggle.md deleted file mode 100644 index 110502ec0..000000000 --- a/.changeset/lucky-ants-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": minor ---- - -Add optional `id` property to all step tooling, allowing users to override state recovery diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3ebb4f2..98df4abeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # inngest +## 2.1.0 + +### Minor Changes + +- b74477f: Add optional `id` property to all step tooling, allowing users to override state recovery + ## 2.0.2 ### Patch Changes diff --git a/package.json b/package.json index d1e906dfa..5c85ca126 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inngest", - "version": "2.0.2", + "version": "2.1.0", "description": "Official SDK for Inngest.com", "main": "./index.js", "types": "./index.d.ts", From 591f73dbbfeb3e076764cdadc47906ad5630d9e9 Mon Sep 17 00:00:00 2001 From: Tony Holdstock-Brown Date: Fri, 30 Jun 2023 14:59:23 -0400 Subject: [PATCH 16/34] Always add the `ts` field when sending events. (#253) This is auto-filled by the event server when missing, and fixes any caching issues people may see if their server configs cache outgoing requests (uh... server actions :D) --------- Co-authored-by: Dan Farrelly --- .changeset/five-bats-begin.md | 5 +++++ src/components/Inngest.test.ts | 31 +++++++++++++++++++++++++++++++ src/components/Inngest.ts | 6 ++++++ 3 files changed, 42 insertions(+) create mode 100644 .changeset/five-bats-begin.md diff --git a/.changeset/five-bats-begin.md b/.changeset/five-bats-begin.md new file mode 100644 index 000000000..626e55475 --- /dev/null +++ b/.changeset/five-bats-begin.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Set `ts` field on sent events if undefined diff --git a/src/components/Inngest.test.ts b/src/components/Inngest.test.ts index 15599d3a2..3c5d10334 100644 --- a/src/components/Inngest.test.ts +++ b/src/components/Inngest.test.ts @@ -7,6 +7,7 @@ import { createClient } from "../test/helpers"; const testEvent: EventPayload = { name: "test", data: {}, + ts: 1688139903724, }; const testEventKey = "foo-bar-baz-test"; @@ -223,6 +224,36 @@ describe("send", () => { ); }); + test("should insert `ts` timestamp ", async () => { + const inngest = createClient({ name: "test" }); + inngest.setEventKey(testEventKey); + + const testEventWithoutTs = { + name: "test.without.ts", + data: {}, + }; + + const mockedFetch = jest.mocked(global.fetch); + + await expect(inngest.send(testEventWithoutTs)).resolves.toBeUndefined(); + + expect(mockedFetch).toHaveBeenCalledTimes(2); // 2nd for dev server check + expect(mockedFetch.mock.calls[1]).toHaveLength(2); + expect(typeof mockedFetch.mock.calls[1]?.[1]?.body).toBe("string"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const body: Array> = JSON.parse( + mockedFetch.mock.calls[1]?.[1]?.body as string + ); + expect(body).toHaveLength(1); + expect(body[0]).toEqual( + expect.objectContaining({ + ...testEventWithoutTs, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ts: expect.any(Number), + }) + ); + }); + test("should allow middleware to mutate input", async () => { const inngest = createClient({ name: "test", diff --git a/src/components/Inngest.ts b/src/components/Inngest.ts index 88b01ebc2..5693868df 100644 --- a/src/components/Inngest.ts +++ b/src/components/Inngest.ts @@ -323,6 +323,12 @@ export class Inngest { payloads = [...inputChanges.payloads]; } + // Ensure that we always add a "ts" field to events. This is auto-filled by the + // event server so is safe, and adding here fixes Next.js server action cache issues. + payloads = payloads.map((p) => + p.ts ? p : { ...p, ts: new Date().getTime() } + ); + /** * It can be valid for a user to send an empty list of events; if this * happens, show a warning that this may not be intended, but don't throw. From fd5e07d498ecd4eaaff85f800f43c36647a8d2dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 12:08:04 +0000 Subject: [PATCH 17/34] Bump semver from 6.3.0 to 6.3.1 in /landing (#260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1.
Release notes

Sourced from semver's releases.

v6.3.1

6.3.1 (2023-07-10)

Bug Fixes

Changelog

Sourced from semver's changelog.

6.3.1 (2023-07-10)

Bug Fixes

6.2.0

  • Coerce numbers to strings when passed to semver.coerce()
  • Add rtl option to coerce from right to left

6.1.3

  • Handle X-ranges properly in includePrerelease mode

6.1.2

  • Do not throw when testing invalid version strings

6.1.1

  • Add options support for semver.coerce()
  • Handle undefined version passed to Range.test

6.1.0

  • Add semver.compareBuild function
  • Support * in semver.intersects

6.0

  • Fix intersects logic.

    This is technically a bug fix, but since it is also a change to behavior that may require users updating their code, it is marked as a major version increment.

5.7

  • Add minVersion method

5.6

  • Move boolean loose param to an options object, with backwards-compatibility protection.
  • Add ability to opt out of special prerelease version handling with the includePrerelease option flag.

5.5

... (truncated)

Commits
Maintainer changes

This version was pushed to npm by lukekarrys, a new releaser for semver since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=semver&package-manager=npm_and_yarn&previous-version=6.3.0&new-version=6.3.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/inngest/inngest-js/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- landing/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/landing/yarn.lock b/landing/yarn.lock index 6966c6918..d2a4a327a 100644 --- a/landing/yarn.lock +++ b/landing/yarn.lock @@ -1171,9 +1171,9 @@ screenfull@^5.1.0: integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== set-harmonic-interval@^1.0.1: version "1.0.1" From 76e04a655ff98bc842f8f050a94d1cd57d58e93c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 12:08:27 +0000 Subject: [PATCH 18/34] Bump semver from 5.7.1 to 5.7.2 (#257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
Release notes

Sourced from semver's releases.

v5.7.2

5.7.2 (2023-07-10)

Bug Fixes

Changelog

Sourced from semver's changelog.

5.7.2 (2023-07-10)

Bug Fixes

5.7

  • Add minVersion method

5.6

  • Move boolean loose param to an options object, with backwards-compatibility protection.
  • Add ability to opt out of special prerelease version handling with the includePrerelease option flag.

5.5

  • Add version coercion capabilities

5.4

  • Add intersection checking

5.3

  • Add minSatisfying method

5.2

  • Add prerelease(v) that returns prerelease components

5.1

  • Add Backus-Naur for ranges
  • Remove excessively cute inspection methods

5.0

  • Remove AMD/Browserified build artifacts
  • Fix ltr and gtr when using the * range
  • Fix for range * with a prerelease identifier
Commits
Maintainer changes

This version was pushed to npm by lukekarrys, a new releaser for semver since your current version.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=semver&package-manager=npm_and_yarn&previous-version=5.7.1&new-version=5.7.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/inngest/inngest-js/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 535e9fc18..b7b9a258a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4578,27 +4578,34 @@ safe-regex-test@^1.0.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@7.x, semver@^7.3.5, semver@^7.3.7, semver@~7.3.0: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== +semver@7.x, semver@^7.3.5, semver@^7.3.7: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@~7.3.0: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" From 1cbf65ecf189b1a0ca0f554a45b466529cd7e3fd Mon Sep 17 00:00:00 2001 From: Aaron Harper Date: Wed, 12 Jul 2023 09:47:37 -0400 Subject: [PATCH 19/34] Include 'modified' in register response (#250) # Changes Include `modified` in register response. # Purpose The API will be able to forward the `modified` value to the UI during registration. This will give the UI enough information to know whether a deploy was created or deduped. We can use that to improve the deploy toast notification in the UI, like saying "No deploy created because function configuration is unchanged". # Testing I manually tested this with an API feature branch and the `modified` value in the response is `true` if a deploy was created and `false` if the deploy was deduped. --------- Co-authored-by: Jack Williams --- .changeset/hungry-mails-look.md | 5 +++++ etc/inngest.api.md | 1 + src/components/InngestCommHandler.ts | 12 +++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .changeset/hungry-mails-look.md diff --git a/.changeset/hungry-mails-look.md b/.changeset/hungry-mails-look.md new file mode 100644 index 000000000..ecfb0b5e4 --- /dev/null +++ b/.changeset/hungry-mails-look.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Alter registration response to include `modified` for deployment deduplication diff --git a/etc/inngest.api.md b/etc/inngest.api.md index f396278fe..f35f6e8e0 100644 --- a/etc/inngest.api.md +++ b/etc/inngest.api.md @@ -171,6 +171,7 @@ export class InngestCommHandler Record): Promise<{ status: number; message: string; + modified: boolean; }>; // Warning: (ae-forgotten-export) The symbol "RegisterRequest" needs to be exported by the entry point index.d.ts // diff --git a/src/components/InngestCommHandler.ts b/src/components/InngestCommHandler.ts index c999395b9..c65eca6ae 100644 --- a/src/components/InngestCommHandler.ts +++ b/src/components/InngestCommHandler.ts @@ -104,6 +104,7 @@ type FetchT = typeof fetch; const registerResSchema = z.object({ status: z.number().default(200), skipped: z.boolean().optional().default(false), + modified: z.boolean().optional().default(false), error: z.string().default("Successfully registered"), }); @@ -731,7 +732,7 @@ export class InngestCommHandler< if (registerRes) { this.upsertSigningKeyFromEnv(env); - const { status, message } = await this.register( + const { status, message, modified } = await this.register( this.reqUrl(actions.url), stringifyUnknown(env[envKeys.DevServerUrl]), registerRes.deployId, @@ -740,7 +741,7 @@ export class InngestCommHandler< return { status, - body: stringify({ message }), + body: stringify({ message, modified }), headers: { "Content-Type": "application/json", }, @@ -976,7 +977,7 @@ export class InngestCommHandler< devServerHost: string | undefined, deployId: string | undefined | null, getHeaders: () => Record - ): Promise<{ status: number; message: string }> { + ): Promise<{ status: number; message: string; modified: boolean }> { const body = this.registerBody(url); let res: globalThis.Response; @@ -1014,6 +1015,7 @@ export class InngestCommHandler< message: `Failed to register${ err instanceof Error ? `; ${err.message}` : "" }`, + modified: false, }; } @@ -1026,7 +1028,7 @@ export class InngestCommHandler< } catch (err) { this.log("warn", "Couldn't unpack register response:", err); } - const { status, error, skipped } = registerResSchema.parse(data); + const { status, error, skipped, modified } = registerResSchema.parse(data); // The dev server polls this endpoint to register functions every few // seconds, but we only want to log that we've registered functions if @@ -1043,7 +1045,7 @@ export class InngestCommHandler< ); } - return { status, message: error }; + return { status, message: error, modified }; } private get isProd() { From 9535d172f326f8f229dfa1a67052f7554a8d3739 Mon Sep 17 00:00:00 2001 From: Darwin <5746693+darwin67@users.noreply.github.com> Date: Wed, 12 Jul 2023 05:41:35 -1000 Subject: [PATCH 20/34] change to use local:pack instead of linking (#258) ## Summary Change integration testing with example repos to use tarball packaging instead of `yarn link` due to too much false negatives. Added a step to pre-build the SDK and allow it to be downloaded into each example repo to save build time. Fixed some linter warnings while at it. --------- Co-authored-by: Darwin D Wu --- .github/workflows/pr.yml | 56 +++++++++++--------- src/examples/parallel-reduce/index.test.ts | 2 +- src/examples/parallel-work/index.test.ts | 2 +- src/examples/promise-all/index.test.ts | 2 +- src/examples/promise-race/index.test.ts | 2 +- src/examples/send-event/index.test.ts | 2 +- src/examples/sequential-reduce/index.test.ts | 2 +- 7 files changed, 37 insertions(+), 31 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a10d449e1..0068d8ba4 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -6,7 +6,7 @@ on: workflow_call: push: branches: - - 'renovate/**' + - "renovate/**" concurrency: group: pr-${{ github.ref }} @@ -39,13 +39,13 @@ jobs: fail-fast: false matrix: tsVersion: - - 'latest' - - 'next' - - '~5.1.0' - - '~5.0.0' - - '~4.9.0' - - '~4.8.0' - - "~4.7.0" + - "latest" + - "next" + - "~5.1.0" + - "~5.0.0" + - "~4.9.0" + - "~4.8.0" + - "~4.7.0" steps: - uses: actions/checkout@v3 - uses: ./.github/actions/setup-and-build @@ -69,8 +69,23 @@ jobs: - uses: ./.github/actions/setup-and-build - run: yarn lint + package: + name: Package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-and-build + - name: Package as tarball + run: yarn local:pack + - name: Archive package tarball + uses: actions/upload-artifact@v3 + with: + name: inngestpkg + path: inngest.tgz + examples: name: Test examples + needs: package runs-on: ubuntu-latest strategy: fail-fast: false @@ -101,14 +116,6 @@ jobs: path: examples/${{ matrix.repo }} ref: main - # Get the SDK ready and link it locally - - name: Prepare SDK for linking - run: yarn prelink - working-directory: sdk - - name: Create link to SDK - run: yarn link - working-directory: sdk/dist - # If we find a deno.json file in the example repo, we need to pull Deno on # to the toolchain. - name: Check if Deno is used @@ -149,23 +156,22 @@ jobs: # Locally linking the lib to a Deno repo is harder, as Deno doesn't # support local linking between ESM and CJS. Instead, we run a manual # shim script to bend paths and adjust the target repo's import path. - - name: Link local SDK to example + - name: Download pre-built SDK + uses: actions/download-artifact@v3 + with: + name: inngestpkg + path: examples/${{ matrix.repo }} + + - name: Install pre-built SDK to example run: | if test -f deno.json; then deno run --allow-read --allow-write ../../../sdk/deno_compat/link.ts else yarnv=$(volta run --yarn $(cat ../../../sdk/package.json | jq -r .volta.yarn) yarn -v) - - if [[ $yarnv == 3* ]]; then - touch ../../../sdk/dist/yarn.lock - yarn link ../../../sdk/dist - else - volta run --yarn $(cat ../../../sdk/package.json | jq -r .volta.yarn) yarn link inngest - fi + volta run --yarn $(cat ../../../sdk/package.json | jq -r .volta.yarn) yarn add ./inngest.tgz fi working-directory: examples/${{ matrix.repo }} - # Copy any SDK function examples to the example repo so that we're always # testing many functions against many handlers. - name: Find inngest functions path in example diff --git a/src/examples/parallel-reduce/index.test.ts b/src/examples/parallel-reduce/index.test.ts index a7b9cf2d4..718ac5534 100644 --- a/src/examples/parallel-reduce/index.test.ts +++ b/src/examples/parallel-reduce/index.test.ts @@ -3,7 +3,7 @@ import { checkIntrospection, eventRunWithName, runHasTimeline, - sendEvent + sendEvent, } from "../../test/helpers"; checkIntrospection({ diff --git a/src/examples/parallel-work/index.test.ts b/src/examples/parallel-work/index.test.ts index 93bf29cf5..522e7e47f 100644 --- a/src/examples/parallel-work/index.test.ts +++ b/src/examples/parallel-work/index.test.ts @@ -3,7 +3,7 @@ import { checkIntrospection, eventRunWithName, runHasTimeline, - sendEvent + sendEvent, } from "../../test/helpers"; checkIntrospection({ diff --git a/src/examples/promise-all/index.test.ts b/src/examples/promise-all/index.test.ts index 1fd89fab1..ab10c5273 100644 --- a/src/examples/promise-all/index.test.ts +++ b/src/examples/promise-all/index.test.ts @@ -3,7 +3,7 @@ import { checkIntrospection, eventRunWithName, runHasTimeline, - sendEvent + sendEvent, } from "../../test/helpers"; checkIntrospection({ diff --git a/src/examples/promise-race/index.test.ts b/src/examples/promise-race/index.test.ts index 395fa8363..1d2af91a1 100644 --- a/src/examples/promise-race/index.test.ts +++ b/src/examples/promise-race/index.test.ts @@ -5,7 +5,7 @@ import { checkIntrospection, eventRunWithName, runHasTimeline, - sendEvent + sendEvent, } from "../../test/helpers"; checkIntrospection({ diff --git a/src/examples/send-event/index.test.ts b/src/examples/send-event/index.test.ts index a33459065..21cc117fa 100644 --- a/src/examples/send-event/index.test.ts +++ b/src/examples/send-event/index.test.ts @@ -6,7 +6,7 @@ import { eventRunWithName, receivedEventWithName, runHasTimeline, - sendEvent + sendEvent, } from "../../test/helpers"; checkIntrospection({ diff --git a/src/examples/sequential-reduce/index.test.ts b/src/examples/sequential-reduce/index.test.ts index 449c768a3..48615b04b 100644 --- a/src/examples/sequential-reduce/index.test.ts +++ b/src/examples/sequential-reduce/index.test.ts @@ -3,7 +3,7 @@ import { checkIntrospection, eventRunWithName, runHasTimeline, - sendEvent + sendEvent, } from "../../test/helpers"; checkIntrospection({ From d0a897688d9564d2a917ebd036e042f4584b0cf3 Mon Sep 17 00:00:00 2001 From: Darwin <5746693+darwin67@users.noreply.github.com> Date: Wed, 12 Jul 2023 05:53:04 -1000 Subject: [PATCH 21/34] INN-1403: Read events for batch handling (#216) Requires https://github.com/inngest/monorepo/pull/1396 to be released first. ## Summary Changing the parsing of data from SDK to use `events` instead of `event`. Over time, as SDK version gets updated, we can stop sending `event` data over to the SDK completely. For backwards compatibility, `event` should always be the first item of `events`. And if it's a single item batch, `[event] === events` should always be true. ### Side note Sneaking in some nix-shell configurations for replicating local development setups. ### Releases Can be released whenever with the next bug fix or minor update. Don't need to be an independent release of its own. --------- Co-authored-by: Darwin D Wu --- .changeset/strange-lizards-kiss.md | 36 +++++++++++ .envrc | 1 + .gitignore | 4 ++ etc/inngest.api.md | 5 ++ shell.nix | 14 ++++ src/api/api.ts | 84 ++++++++++++++++++++++++ src/api/schema.ts | 19 ++++++ src/components/Inngest.ts | 10 +++ src/components/InngestCommHandler.ts | 64 +++---------------- src/components/InngestFunction.ts | 2 +- src/helpers/consts.ts | 1 + src/helpers/functions.test.ts | 64 +++++++++++++++++++ src/helpers/functions.ts | 71 ++++++++++++++++++++ src/helpers/strings.ts | 13 ++++ src/test/helpers.ts | 24 ++++--- src/types.ts | 96 ++++++++++++++++++++++++++++ 16 files changed, 443 insertions(+), 65 deletions(-) create mode 100644 .changeset/strange-lizards-kiss.md create mode 100644 .envrc create mode 100644 shell.nix create mode 100644 src/api/api.ts create mode 100644 src/api/schema.ts create mode 100644 src/helpers/functions.test.ts diff --git a/.changeset/strange-lizards-kiss.md b/.changeset/strange-lizards-kiss.md new file mode 100644 index 000000000..ec08ae6b2 --- /dev/null +++ b/.changeset/strange-lizards-kiss.md @@ -0,0 +1,36 @@ +--- +"inngest": minor +--- + +Add support for batching events. + +Introduces a new configuration to function configurations. + +```ts +batchEvents?: { maxSize: 100, timeout: "5s" } +``` + +This will take Inngest start execution when one of the following conditions are met. + +1. The batch is full +2. Time is up + +When the SDK gets invoked, the list of events will be available via a newly exported field `events`. + +```ts +createFunction( + { name: "my func", batchEvents: { maxSize: 100, timeout: "5s" } }, + { event: "my/event" }, + async ({ event, events, step }) => { + // events is accessible with the list of events + // event will still be a single event object, which will be the + // 1st event of the list. + + const result = step.run("do something with events", () => { + return events.map(() => doSomething()); + }); + + return { success: true, result }; + } +); +``` diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..1d953f4bd --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore index 82ada6b91..11edfe717 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ temp/ coverage/ tsdoc-metadata.json + +/.direnv/ +/inngest.tgz +/landing/vite.config.ts.* diff --git a/etc/inngest.api.md b/etc/inngest.api.md index f35f6e8e0..9f6eddd65 100644 --- a/etc/inngest.api.md +++ b/etc/inngest.api.md @@ -80,6 +80,10 @@ export type FailureEventPayload

= { // @public export interface FunctionOptions, Event extends keyof Events & string> { + batchEvents?: { + maxSize: number; + timeout: TimeStrBatch; + }; // Warning: (ae-forgotten-export) The symbol "Cancellation" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -335,6 +339,7 @@ export type ZodEventSchemas = Record> { + const url = new URL(`/v0/runs/${runId}/actions`, this.baseUrl); + + return fetch(url, { + headers: { Authorization: `Bearer ${this.hashedKey}` }, + }) + .then(async (resp) => { + const data: unknown = await resp.json(); + + if (resp.ok) { + return ok(StepsSchema.parse(data)); + } else { + return err(ErrorSchema.parse(data)); + } + }) + .catch((error) => { + return err({ error: error as string, status: 500 }); + }); + } + + async getRunBatch( + runId: string + ): Promise> { + const url = new URL(`/v0/runs/${runId}/batch`, this.baseUrl); + + return fetch(url, { + headers: { Authorization: `Bearer ${this.hashedKey}` }, + }) + .then(async (resp) => { + const data: unknown = await resp.json(); + + if (resp.ok) { + return ok(BatchSchema.parse(data)); + } else { + return err(ErrorSchema.parse(data)); + } + }) + .catch((error) => { + return err({ error: error as string, status: 500 }); + }); + } +} diff --git a/src/api/schema.ts b/src/api/schema.ts new file mode 100644 index 000000000..ff280ea48 --- /dev/null +++ b/src/api/schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; +import { type EventPayload } from "../types"; + +export const ErrorSchema = z.object({ + error: z.string(), + status: z.number(), +}); +export type ErrorResponse = z.infer; + +export const StepsSchema = z.object({}).passthrough().default({}); +export type StepsResponse = z.infer; + +export const BatchSchema = z.array( + z + .object({}) + .passthrough() + .transform((v) => v as EventPayload) +); +export type BatchResponse = z.infer; diff --git a/src/components/Inngest.ts b/src/components/Inngest.ts index 5693868df..fbd595d37 100644 --- a/src/components/Inngest.ts +++ b/src/components/Inngest.ts @@ -1,3 +1,4 @@ +import { InngestApi } from "../api/api"; import { envKeys } from "../helpers/consts"; import { devServerAvailable, devServerUrl } from "../helpers/devserver"; import { @@ -89,6 +90,8 @@ export class Inngest { */ public readonly inngestBaseUrl: URL; + private readonly inngestApi: InngestApi; + /** * The absolute URL of the Inngest Cloud API. */ @@ -163,6 +166,13 @@ export class Inngest { inngestEnv: env, }); + const signingKey = processEnv(envKeys.SigningKey) || ""; + this.inngestApi = new InngestApi({ + baseUrl: + processEnv(envKeys.InngestApiBaseUrl) || "https://api.inngest.com", + signingKey: signingKey, + }); + this.fetch = getFetch(fetch); this.logger = logger; diff --git a/src/components/InngestCommHandler.ts b/src/components/InngestCommHandler.ts index c65eca6ae..9c04aa311 100644 --- a/src/components/InngestCommHandler.ts +++ b/src/components/InngestCommHandler.ts @@ -13,10 +13,10 @@ import { skipDevServer, } from "../helpers/env"; import { OutgoingResultError, serializeError } from "../helpers/errors"; -import { cacheFn } from "../helpers/functions"; +import { cacheFn, parseFnData } from "../helpers/functions"; import { strBoolean } from "../helpers/scalar"; import { createStream } from "../helpers/stream"; -import { stringify, stringifyUnknown } from "../helpers/strings"; +import { stringify, stringifyUnknown, hashSigningKey } from "../helpers/strings"; import { type MaybePromise } from "../helpers/types"; import { landing } from "../landing"; import { @@ -473,15 +473,7 @@ export class InngestCommHandler< // hashedSigningKey creates a sha256 checksum of the signing key with the // same signing key prefix. private get hashedSigningKey(): string { - if (!this.signingKey) { - return ""; - } - - const prefix = this.signingKey.match(/^signkey-[\w]+-/)?.shift() || ""; - const key = this.signingKey.replace(/^signkey-[\w]+-/, ""); - - // Decode the key from its hex representation into a bytestream - return `${prefix}${sha256().update(key, "hex").digest("hex")}`; + return hashSigningKey(this.signingKey); } /** @@ -646,6 +638,7 @@ export class InngestCommHandler< if (runRes) { this.upsertSigningKeyFromEnv(env); this.validateSignature(runRes.signature, runRes.data); + this.client["inngestApi"].setSigningKey(this.signingKey); const stepRes = await this.runStep( runRes.fnId, @@ -784,48 +777,11 @@ export class InngestCommHandler< throw new Error(`Could not find function with ID "${functionId}"`); } - // TODO PrettyError on parse failure; serve handler may be set up badly - const { event, steps, ctx } = z - .object({ - event: z.object({}).passthrough(), - /** - * When handling per-step errors, steps will need to be an object with - * either a `data` or an `error` key. - * - * For now, we support the current method of steps just being a map of - * step ID to step data. - * - * TODO When the executor does support per-step errors, we can uncomment - * the expected schema below. - */ - steps: z - .record( - z.any().refine((v) => typeof v !== "undefined", { - message: "Values in steps must be defined", - }) - ) - .optional() - .nullable(), - // steps: z.record(incomingOpSchema.passthrough()).optional().nullable(), - ctx: z - .object({ - run_id: z.string(), - stack: z - .object({ - stack: z - .array(z.string()) - .nullable() - .transform((v) => (Array.isArray(v) ? v : [])), - current: z.number(), - }) - .passthrough() - .optional() - .nullable(), - }) - .optional() - .nullable(), - }) - .parse(data); + const fndata = await parseFnData(data, this.client["inngestApi"]); + if (!fndata.ok) { + throw new Error(fndata.error); + } + const { event, events, steps, ctx } = fndata.value; /** * TODO When the executor does support per-step errors, this map will need @@ -847,7 +803,7 @@ export class InngestCommHandler< }) ?? []; const ret = await fn.fn["runFn"]( - { event, runId: ctx?.run_id }, + { event, events, runId: ctx?.run_id }, opStack, /** * TODO The executor is sending `"step"` as the step ID when it is not diff --git a/src/components/InngestFunction.ts b/src/components/InngestFunction.ts index 3309c0761..720c7c54d 100644 --- a/src/components/InngestFunction.ts +++ b/src/components/InngestFunction.ts @@ -267,7 +267,7 @@ export class InngestFunction< Record unknown> > >, - "event" | "runId" + "event" | "events" | "runId" >; const hookStack = await getHookStack( diff --git a/src/helpers/consts.ts b/src/helpers/consts.ts index ac778623c..997526b79 100644 --- a/src/helpers/consts.ts +++ b/src/helpers/consts.ts @@ -21,6 +21,7 @@ export enum envKeys { DevServerUrl = "INNGEST_DEVSERVER_URL", Environment = "INNGEST_ENV", BranchName = "BRANCH_NAME", + InngestApiBaseUrl = "INNGEST_API_BASE_URL", /** * The git branch of the commit the deployment was triggered by. Example: diff --git a/src/helpers/functions.test.ts b/src/helpers/functions.test.ts new file mode 100644 index 000000000..fe6e619fd --- /dev/null +++ b/src/helpers/functions.test.ts @@ -0,0 +1,64 @@ +import { type EventPayload } from "@local/types"; +import { parseFnData } from "@local/helpers/functions"; +import { InngestApi } from "@local/api/api"; + +const randomstr = (): string => { + return (Math.random() + 1).toString(36).substring(2); +}; + +const generateEvent = (): EventPayload => { + return { + name: randomstr(), + data: { hello: "world" }, + user: {}, + ts: 0, + }; +}; + +describe("#parseFnData", () => { + const API = new InngestApi({ signingKey: "something" }); + + [ + { + name: "should parse successfully for valid data", + data: { + event: generateEvent(), + events: [...Array(5).keys()].map(() => generateEvent()), + steps: {}, + ctx: { + run_id: randomstr(), + stack: { + stack: [randomstr()], + current: 0, + }, + }, + }, + isOk: true, + }, + { + name: "should return an error for missing event", + data: { + events: [...Array(5).keys()].map(() => generateEvent()), + steps: {}, + ctx: { + run_id: randomstr(), + stack: { + stack: [], + current: 0, + }, + }, + }, + isOk: false, + }, + { + name: "should return an error with empty object", + data: {}, + isOk: false, + }, + ].forEach((test) => { + it(test.name, async () => { + const result = await parseFnData(test.data, API); + expect(result.ok).toEqual(test.isOk); + }); + }); +}); diff --git a/src/helpers/functions.ts b/src/helpers/functions.ts index 45e34aab9..b66f83f44 100644 --- a/src/helpers/functions.ts +++ b/src/helpers/functions.ts @@ -1,4 +1,7 @@ import { type Await } from "./types"; +import { prettyError } from "./errors"; +import { fnDataSchema, type FnData, type Result, ok, err } from "../types"; +import { type InngestApi } from "../api/api"; /** * Wraps a function with a cache. When the returned function is run, it will @@ -58,3 +61,71 @@ export const waterfall = any)[]>( return chain; }; }; + +type ParseErr = string; +export const parseFnData = async ( + data: unknown, + api: InngestApi +): Promise> => { + try { + const result = fnDataSchema.parse(data); + + if (result.use_api) { + if (!result.ctx?.run_id) { + return err( + prettyError({ + whatHappened: "failed to attempt retrieving data from API", + consequences: "function execution can't continue", + why: "run_id is missing from context", + stack: true, + }) + ); + } + + const [evtResp, stepResp] = await Promise.all([ + api.getRunBatch(result.ctx.run_id), + api.getRunSteps(result.ctx.run_id), + ]); + + if (evtResp.ok) { + result.events = evtResp.value; + } else { + return err( + prettyError({ + whatHappened: "failed to retrieve list of events", + consequences: "function execution can't continue", + why: evtResp.error?.error, + stack: true, + }) + ); + } + + if (stepResp.ok) { + result.steps = stepResp.value; + } else { + return err( + prettyError({ + whatHappened: "failed to retrieve steps for function run", + consequences: "function execution can't continue", + why: stepResp.error?.error, + stack: true, + }) + ); + } + } + + return ok(result); + } catch (error) { + // print it out for now. + // move to something like protobuf so we don't have to deal with this + console.error(error); + + return err( + prettyError({ + whatHappened: "failed to parse data from executor", + consequences: "function execution can't continue", + stack: true, + }) + ); + } +}; diff --git a/src/helpers/strings.ts b/src/helpers/strings.ts index cf442df81..de6536ec6 100644 --- a/src/helpers/strings.ts +++ b/src/helpers/strings.ts @@ -1,4 +1,5 @@ import ms from "ms"; +import { sha256 } from "hash.js"; import { type TimeStr } from "../types"; export { default as stringify } from "json-stringify-safe"; @@ -83,3 +84,15 @@ export const stringifyUnknown = (input: unknown): string | undefined => { return input.toString(); } }; + +export const hashSigningKey = (signingKey: string | undefined): string => { + if (!signingKey) { + return ""; + } + + const prefix = signingKey.match(/^signkey-[\w]+-/)?.shift() || ""; + const key = signingKey.replace(/^signkey-[\w]+-/, ""); + + // Decode the key from its hex representation into a bytestream + return `${prefix}${sha256().update(key, "hex").digest("hex")}`; +}; diff --git a/src/test/helpers.ts b/src/test/helpers.ts index 0b114862b..cec18365e 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -658,7 +658,7 @@ export const testFramework = ( )}&s=expired`, }, url: "/api/inngest?fnId=test", - body: { event: {} }, + body: { event: {}, events: [{}] }, }, ], env @@ -674,21 +674,25 @@ export const testFramework = ( // This prevents us from having to rewrite the signature creation function in JS, which may // differ from the cloud/CLI version. test("should validate a signature with a key successfully", async () => { + const event = { + data: {}, + id: "", + name: "inngest/scheduled.timer", + ts: 1674082830001, + user: {}, + v: "1", + }; + const body = { ctx: { fn_id: "local-testing-local-cron", run_id: "01GQ3HTEZ01M7R8Z9PR1DMHDN1", step_id: "step", }, - event: { - data: {}, - id: "", - name: "inngest/scheduled.timer", - ts: 1674082830001, - user: {}, - v: "1", - }, + event, + events: [event], steps: {}, + use_api: false, }; const ret = await run( [ @@ -706,7 +710,7 @@ export const testFramework = ( method: "POST", headers: { [headerKeys.Signature]: - "t=1674082860&s=88b6453463050d1846743cbba0925bae7c1cf807f9c74bbd41b3d5cfc9c70d11", + "t=1687306735&s=70312c7815f611a4aa0b6f985910a85a6c232c845838d7f49f1d05fd8b2b0779", }, url: "/api/inngest?fnId=test&stepId=step", body, diff --git a/src/types.ts b/src/types.ts index a20609899..fdf362586 100644 --- a/src/types.ts +++ b/src/types.ts @@ -169,6 +169,8 @@ export type TimeStr = `${`${number}w` | ""}${`${number}d` | ""}${ | `${number}h` | ""}${`${number}m` | ""}${`${number}s` | ""}`; +export type TimeStrBatch = `${`${number}s`}`; + export type BaseContext< TOpts extends ClientOptions, TTrigger extends keyof EventsFromOpts & string, @@ -179,6 +181,11 @@ export type BaseContext< */ event: EventsFromOpts[TTrigger]; + events: [ + EventsFromOpts[TTrigger], + ...EventsFromOpts[TTrigger][] + ]; + /** * The run ID for the current function execution */ @@ -646,6 +653,27 @@ export interface FunctionOptions< */ concurrency?: number | { limit: number }; + /** + * batchEvents specifies the batch configuration on when this function + * should be invoked when one of the requirements are fulfilled. + */ + batchEvents?: { + /** + * The maximum number of events to be consumed in one batch, + * Currently allowed max value is 100. + */ + maxSize: number; + + /** + * How long to wait before invoking the function with a list of events. + * If timeout is reached, the function will be invoked with a batch + * even if it's not filled up to `maxSize`. + * + * Expects 1s to 60s. + */ + timeout: TimeStrBatch; + }; + fns?: Record; /** @@ -919,6 +947,10 @@ export interface FunctionConfig { } >; idempotency?: string; + batchEvents?: { + maxSize: number; + timeout: string; + }; throttle?: { key?: string; count: number; @@ -988,3 +1020,67 @@ export interface StepOpts { */ id?: string; } + +/** + * Simplified version of Rust style `Result` + * + * Make it easier to wrap functions with some kind of result. + * e.g. API calls + */ +export type Result = + | { ok: true; value: T } + | { ok: false; error: E | undefined }; + +export const ok = (data: T): Result => { + return { ok: true, value: data }; +}; + +export const err = (error?: E): Result => { + return { ok: false, error }; +}; + +/** + * Format of data send from the executor to the SDK + */ +export const fnDataSchema = z.object({ + event: z.object({}).passthrough(), + events: z.array(z.object({}).passthrough()).default([]), + /** + * When handling per-step errors, steps will need to be an object with + * either a `data` or an `error` key. + * + * For now, we support the current method of steps just being a map of + * step ID to step data. + * + * TODO When the executor does support per-step errors, we can uncomment + * the expected schema below. + */ + steps: z + .record( + z.any().refine((v) => typeof v !== "undefined", { + message: "Values in steps must be defined", + }) + ) + .optional() + .nullable(), + // steps: z.record(incomingOpSchema.passthrough()).optional().nullable(), + ctx: z + .object({ + run_id: z.string(), + stack: z + .object({ + stack: z + .array(z.string()) + .nullable() + .transform((v) => (Array.isArray(v) ? v : [])), + current: z.number(), + }) + .passthrough() + .optional() + .nullable(), + }) + .optional() + .nullable(), + use_api: z.boolean().default(false), +}); +export type FnData = z.infer; From e8cc1f4407ce7de0ba426b1ab4191f72edf05730 Mon Sep 17 00:00:00 2001 From: Inngest Release Bot <126702797+inngest-release-bot@users.noreply.github.com> Date: Wed, 12 Jul 2023 17:04:59 +0100 Subject: [PATCH 22/34] Release @latest (#255) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## inngest@2.2.0 ### Minor Changes - d0a8976: Add support for batching events. Introduces a new configuration to function configurations. ```ts batchEvents?: { maxSize: 100, timeout: "5s" } ``` This will take Inngest start execution when one of the following conditions are met. 1. The batch is full 2. Time is up When the SDK gets invoked, the list of events will be available via a newly exported field `events`. ```ts createFunction( { name: "my func", batchEvents: { maxSize: 100, timeout: "5s" } }, { event: "my/event" }, async ({ event, events, step }) => { // events is accessible with the list of events // event will still be a single event object, which will be the // 1st event of the list. const result = step.run("do something with events", () => { return events.map(() => doSomething()); }); return { success: true, result }; } ); ``` ### Patch Changes - 591f73d: Set `ts` field on sent events if undefined - 1cbf65e: Alter registration response to include `modified` for deployment deduplication Co-authored-by: github-actions[bot] --- .changeset/five-bats-begin.md | 5 ---- .changeset/hungry-mails-look.md | 5 ---- .changeset/strange-lizards-kiss.md | 36 ------------------------- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++ package.json | 2 +- 5 files changed, 43 insertions(+), 47 deletions(-) delete mode 100644 .changeset/five-bats-begin.md delete mode 100644 .changeset/hungry-mails-look.md delete mode 100644 .changeset/strange-lizards-kiss.md diff --git a/.changeset/five-bats-begin.md b/.changeset/five-bats-begin.md deleted file mode 100644 index 626e55475..000000000 --- a/.changeset/five-bats-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Set `ts` field on sent events if undefined diff --git a/.changeset/hungry-mails-look.md b/.changeset/hungry-mails-look.md deleted file mode 100644 index ecfb0b5e4..000000000 --- a/.changeset/hungry-mails-look.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Alter registration response to include `modified` for deployment deduplication diff --git a/.changeset/strange-lizards-kiss.md b/.changeset/strange-lizards-kiss.md deleted file mode 100644 index ec08ae6b2..000000000 --- a/.changeset/strange-lizards-kiss.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -"inngest": minor ---- - -Add support for batching events. - -Introduces a new configuration to function configurations. - -```ts -batchEvents?: { maxSize: 100, timeout: "5s" } -``` - -This will take Inngest start execution when one of the following conditions are met. - -1. The batch is full -2. Time is up - -When the SDK gets invoked, the list of events will be available via a newly exported field `events`. - -```ts -createFunction( - { name: "my func", batchEvents: { maxSize: 100, timeout: "5s" } }, - { event: "my/event" }, - async ({ event, events, step }) => { - // events is accessible with the list of events - // event will still be a single event object, which will be the - // 1st event of the list. - - const result = step.run("do something with events", () => { - return events.map(() => doSomething()); - }); - - return { success: true, result }; - } -); -``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 98df4abeb..728e08859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # inngest +## 2.2.0 + +### Minor Changes + +- d0a8976: Add support for batching events. + + Introduces a new configuration to function configurations. + + ```ts + batchEvents?: { maxSize: 100, timeout: "5s" } + ``` + + This will take Inngest start execution when one of the following conditions are met. + + 1. The batch is full + 2. Time is up + + When the SDK gets invoked, the list of events will be available via a newly exported field `events`. + + ```ts + createFunction( + { name: "my func", batchEvents: { maxSize: 100, timeout: "5s" } }, + { event: "my/event" }, + async ({ event, events, step }) => { + // events is accessible with the list of events + // event will still be a single event object, which will be the + // 1st event of the list. + + const result = step.run("do something with events", () => { + return events.map(() => doSomething()); + }); + + return { success: true, result }; + } + ); + ``` + +### Patch Changes + +- 591f73d: Set `ts` field on sent events if undefined +- 1cbf65e: Alter registration response to include `modified` for deployment deduplication + ## 2.1.0 ### Minor Changes diff --git a/package.json b/package.json index 5c85ca126..fd7983fd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inngest", - "version": "2.1.0", + "version": "2.2.0", "description": "Official SDK for Inngest.com", "main": "./index.js", "types": "./index.d.ts", From 5612351ee7656765ba3f4f85f6099f2f3ec8ac91 Mon Sep 17 00:00:00 2001 From: Jack Williams <1736957+jpwilliams@users.noreply.github.com> Date: Fri, 14 Jul 2023 17:39:35 +0100 Subject: [PATCH 23/34] Ensure introspection tests work against dev server 0.15 --- src/test/helpers.ts | 56 ++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/src/test/helpers.ts b/src/test/helpers.ts index cec18365e..a1bc92fbc 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -1020,54 +1020,42 @@ export const checkIntrospection = ({ name, triggers }: CheckIntrospection) => { const data = z .object({ - handlers: z.array( + functions: z.array( z.object({ - sdk: z.object({ - functions: z.array( + name: z.string(), + id: z.string(), + triggers: z.array( + z.object({ event: z.string() }).or( z.object({ - name: z.string(), - id: z.string(), - triggers: z.array( - z.object({ event: z.string() }).or( - z.object({ - cron: z.string(), - }) - ) - ), - steps: z.object({ - step: z.object({ - id: z.literal("step"), - name: z.literal("step"), - runtime: z.object({ - type: z.literal("http"), - url: z.string().url(), - }), - }), - }), + cron: z.string(), }) - ), - }), + ) + ), + steps: z.array( + z.object({ + id: z.string(), + name: z.string(), + uri: z.string().url(), + }) + ), }) ), }) .parse(await res.json()); - expect(data.handlers[0]?.sdk.functions).toContainEqual({ + expect(data.functions).toContainEqual({ name, id: expect.stringMatching(new RegExp(`^.*-${slugify(name)}$`)), triggers, - steps: { - step: { + steps: expect.arrayContaining([ + { id: "step", name: "step", - runtime: { - type: "http", - url: expect.stringMatching( - new RegExp(`^http.+\\?fnId=.+-${slugify(name)}&stepId=step$`) - ), - }, + uri: expect.stringMatching( + new RegExp(`^http.+\\?fnId=.+-${slugify(name)}&stepId=step$`) + ), }, - }, + ]), }); }); }); From 61091c0b18f2e0f000b6db1432a98abd342f9b26 Mon Sep 17 00:00:00 2001 From: Jack Williams <1736957+jpwilliams@users.noreply.github.com> Date: Fri, 14 Jul 2023 17:45:44 +0100 Subject: [PATCH 24/34] No longer ensure ID is the function's slug in `/dev` --- src/test/helpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/helpers.ts b/src/test/helpers.ts index a1bc92fbc..a68aef47d 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -1045,7 +1045,6 @@ export const checkIntrospection = ({ name, triggers }: CheckIntrospection) => { expect(data.functions).toContainEqual({ name, - id: expect.stringMatching(new RegExp(`^.*-${slugify(name)}$`)), triggers, steps: expect.arrayContaining([ { From c1f7cf0c470ba9e51e4697f17e0d8296136b204e Mon Sep 17 00:00:00 2001 From: Jack Williams <1736957+jpwilliams@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:01:29 +0100 Subject: [PATCH 25/34] Ensure non-strict matching for introspection tests --- src/test/helpers.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/test/helpers.ts b/src/test/helpers.ts index a68aef47d..2fbab8831 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -1043,19 +1043,21 @@ export const checkIntrospection = ({ name, triggers }: CheckIntrospection) => { }) .parse(await res.json()); - expect(data.functions).toContainEqual({ - name, - triggers, - steps: expect.arrayContaining([ - { - id: "step", - name: "step", - uri: expect.stringMatching( - new RegExp(`^http.+\\?fnId=.+-${slugify(name)}&stepId=step$`) - ), - }, - ]), - }); + expect(data.functions).toContainEqual( + expect.objectContaining({ + name, + triggers, + steps: expect.arrayContaining([ + { + id: "step", + name: "step", + uri: expect.stringMatching( + new RegExp(`^http.+\\?fnId=.+-${slugify(name)}&stepId=step$`) + ), + }, + ]), + }) + ); }); }); }; From 1120e2994b930df538647e53a350da149b8e7770 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 14 Jul 2023 18:08:28 +0100 Subject: [PATCH 26/34] Genercize mixed async error (#264) # Summary Genercize the overly-specific error regarding mixed async logic; the same symptom can be produced by many different causes. --- .changeset/spicy-paws-love.md | 5 +++++ src/components/Inngest.test.ts | 2 +- src/components/InngestCommHandler.ts | 6 +++++- src/components/InngestFunction.test.ts | 2 +- src/components/InngestFunction.ts | 16 +++++++++++++--- src/helpers/errors.ts | 2 +- 6 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 .changeset/spicy-paws-love.md diff --git a/.changeset/spicy-paws-love.md b/.changeset/spicy-paws-love.md new file mode 100644 index 000000000..322a62ca8 --- /dev/null +++ b/.changeset/spicy-paws-love.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Genercize mixed async error; the same symptom can be caused by a few different errors diff --git a/src/components/Inngest.test.ts b/src/components/Inngest.test.ts index 3c5d10334..5f9af4cd5 100644 --- a/src/components/Inngest.test.ts +++ b/src/components/Inngest.test.ts @@ -240,7 +240,7 @@ describe("send", () => { expect(mockedFetch).toHaveBeenCalledTimes(2); // 2nd for dev server check expect(mockedFetch.mock.calls[1]).toHaveLength(2); expect(typeof mockedFetch.mock.calls[1]?.[1]?.body).toBe("string"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any const body: Array> = JSON.parse( mockedFetch.mock.calls[1]?.[1]?.body as string ); diff --git a/src/components/InngestCommHandler.ts b/src/components/InngestCommHandler.ts index 9c04aa311..35328730d 100644 --- a/src/components/InngestCommHandler.ts +++ b/src/components/InngestCommHandler.ts @@ -16,7 +16,11 @@ import { OutgoingResultError, serializeError } from "../helpers/errors"; import { cacheFn, parseFnData } from "../helpers/functions"; import { strBoolean } from "../helpers/scalar"; import { createStream } from "../helpers/stream"; -import { stringify, stringifyUnknown, hashSigningKey } from "../helpers/strings"; +import { + hashSigningKey, + stringify, + stringifyUnknown, +} from "../helpers/strings"; import { type MaybePromise } from "../helpers/types"; import { landing } from "../landing"; import { diff --git a/src/components/InngestFunction.test.ts b/src/components/InngestFunction.test.ts index 983e2bc7c..f46ba6a01 100644 --- a/src/components/InngestFunction.test.ts +++ b/src/components/InngestFunction.test.ts @@ -940,7 +940,7 @@ describe("runFn", () => { }, "second run throws, as we find async logic during memoization": { stack: [{ id: A, data: "A" }], - expectedThrowMessage: ErrCode.ASYNC_DETECTED_DURING_MEMOIZATION, + expectedThrowMessage: ErrCode.NON_DETERMINISTIC_FUNCTION, }, }) ); diff --git a/src/components/InngestFunction.ts b/src/components/InngestFunction.ts index 720c7c54d..37747299e 100644 --- a/src/components/InngestFunction.ts +++ b/src/components/InngestFunction.ts @@ -6,6 +6,7 @@ import { OutgoingResultError, deserializeError, functionStoppedRunningErr, + prettyError, serializeError, } from "../helpers/errors"; import { resolveAfterPending, resolveNextTick } from "../helpers/promises"; @@ -464,9 +465,18 @@ export class InngestFunction< * undefined state. */ throw new NonRetriableError( - functionStoppedRunningErr( - ErrCode.ASYNC_DETECTED_DURING_MEMOIZATION - ) + prettyError({ + whatHappened: " Your function was stopped from running", + why: "We couldn't resume your function's state because it may have changed since the run started or there are async actions in-between steps that we haven't noticed in previous executions.", + consequences: + "Continuing to run the function may result in unexpected behaviour, so we've stopped your function to ensure nothing unexpected happened!", + toFixNow: + "Ensure that your function is either entirely step-based or entirely non-step-based, by either wrapping all asynchronous logic in `step.run()` calls or by removing all `step.*()` calls.", + otherwise: + "For more information on why step functions work in this manner, see https://www.inngest.com/docs/functions/multi-step#gotchas", + stack: true, + code: ErrCode.NON_DETERMINISTIC_FUNCTION, + }) ); } diff --git a/src/helpers/errors.ts b/src/helpers/errors.ts index 223cf10cb..2db405837 100644 --- a/src/helpers/errors.ts +++ b/src/helpers/errors.ts @@ -181,7 +181,7 @@ export const deserializeError = (subject: Partial): Error => { }; export enum ErrCode { - ASYNC_DETECTED_DURING_MEMOIZATION = "ASYNC_DETECTED_DURING_MEMOIZATION", + NON_DETERMINISTIC_FUNCTION = "NON_DETERMINISTIC_FUNCTION", ASYNC_DETECTED_AFTER_MEMOIZATION = "ASYNC_DETECTED_AFTER_MEMOIZATION", STEP_USED_AFTER_ASYNC = "STEP_USED_AFTER_ASYNC", NESTING_STEPS = "NESTING_STEPS", From fc63d770a15d23b2eed4e80507de65817bb782b6 Mon Sep 17 00:00:00 2001 From: Inngest Release Bot <126702797+inngest-release-bot@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:24:34 +0100 Subject: [PATCH 27/34] Release @latest (#265) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## inngest@2.2.1 ### Patch Changes - 1120e29: Genercize mixed async error; the same symptom can be caused by a few different errors Co-authored-by: github-actions[bot] --- .changeset/spicy-paws-love.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/spicy-paws-love.md diff --git a/.changeset/spicy-paws-love.md b/.changeset/spicy-paws-love.md deleted file mode 100644 index 322a62ca8..000000000 --- a/.changeset/spicy-paws-love.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Genercize mixed async error; the same symptom can be caused by a few different errors diff --git a/CHANGELOG.md b/CHANGELOG.md index 728e08859..06c1f4b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # inngest +## 2.2.1 + +### Patch Changes + +- 1120e29: Genercize mixed async error; the same symptom can be caused by a few different errors + ## 2.2.0 ### Minor Changes diff --git a/package.json b/package.json index fd7983fd8..78edfdddf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inngest", - "version": "2.2.0", + "version": "2.2.1", "description": "Official SDK for Inngest.com", "main": "./index.js", "types": "./index.d.ts", From 7792a62522da1725dacfcb454f702ca49aee06e3 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Fri, 14 Jul 2023 20:31:03 +0100 Subject: [PATCH 28/34] Add support for streaming to `inngest/remix` (#266) # Summary Adds streaming support to the `inngest/remix` handler. --- .changeset/three-ants-tell.md | 5 ++++ src/remix.ts | 50 ++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 .changeset/three-ants-tell.md diff --git a/.changeset/three-ants-tell.md b/.changeset/three-ants-tell.md new file mode 100644 index 000000000..5b30c37ba --- /dev/null +++ b/.changeset/three-ants-tell.md @@ -0,0 +1,5 @@ +--- +"inngest": minor +--- + +Add support for streaming to `inngest/remix` diff --git a/src/remix.ts b/src/remix.ts index d7d48afc3..2c7454618 100644 --- a/src/remix.ts +++ b/src/remix.ts @@ -1,5 +1,6 @@ import { InngestCommHandler, + type ActionResponse, type ServeHandler, } from "./components/InngestCommHandler"; import { headerKeys, queryKeys } from "./helpers/consts"; @@ -7,6 +8,32 @@ import { type SupportedFrameworkName } from "./types"; export const name: SupportedFrameworkName = "remix"; +const createNewResponse = ({ + body, + status, + headers, +}: ActionResponse): Response => { + /** + * If `Response` isn't included in this environment, it's probably a Node + * env that isn't already polyfilling. In this case, we can polyfill it + * here to be safe. + */ + let Res: typeof Response; + + if (typeof Response === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires + Res = require("cross-fetch").Response; + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + Res = Response; + } + + return new Res(body, { + status, + headers, + }); +}; + /** * In Remix, serve and register any declared functions with Inngest, making them * available to be triggered by events. @@ -66,27 +93,8 @@ export const serve: ServeHandler = (nameOrInngest, fns, opts): unknown => { }, }; }, - ({ body, status, headers }): Response => { - /** - * If `Response` isn't included in this environment, it's probably a Node - * env that isn't already polyfilling. In this case, we can polyfill it - * here to be safe. - */ - let Res: typeof Response; - - if (typeof Response === "undefined") { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires - Res = require("cross-fetch").Response; - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - Res = Response; - } - - return new Res(body, { - status, - headers, - }); - } + createNewResponse, + createNewResponse ); return handler.createHandler(); From 94099c4ffaebcf8bb10633841e7feb8628dd081a Mon Sep 17 00:00:00 2001 From: Inngest Release Bot <126702797+inngest-release-bot@users.noreply.github.com> Date: Fri, 14 Jul 2023 20:38:24 +0100 Subject: [PATCH 29/34] Release @latest (#267) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## inngest@2.3.0 ### Minor Changes - 7792a62: Add support for streaming to `inngest/remix` Co-authored-by: github-actions[bot] --- .changeset/three-ants-tell.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/three-ants-tell.md diff --git a/.changeset/three-ants-tell.md b/.changeset/three-ants-tell.md deleted file mode 100644 index 5b30c37ba..000000000 --- a/.changeset/three-ants-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": minor ---- - -Add support for streaming to `inngest/remix` diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c1f4b39..1ce8ebef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # inngest +## 2.3.0 + +### Minor Changes + +- 7792a62: Add support for streaming to `inngest/remix` + ## 2.2.1 ### Patch Changes diff --git a/package.json b/package.json index 78edfdddf..d6e9433d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inngest", - "version": "2.2.1", + "version": "2.3.0", "description": "Official SDK for Inngest.com", "main": "./index.js", "types": "./index.d.ts", From 6cb6719a88b1038431703a8c92fcdeab2e6d40c3 Mon Sep 17 00:00:00 2001 From: Tony Holdstock-Brown Date: Tue, 18 Jul 2023 08:12:11 -0400 Subject: [PATCH 30/34] Allow filtering of events within triggers (#251) This allows users to write arbitrary expressions on event triggers to conditionally run functions. --------- Co-authored-by: Jack Williams Co-authored-by: Jack Williams <1736957+jpwilliams@users.noreply.github.com> --- .changeset/rude-sheep-sing.md | 5 +++++ etc/inngest.api.md | 2 +- src/components/Inngest.ts | 15 ++++++++++++++- src/types.ts | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .changeset/rude-sheep-sing.md diff --git a/.changeset/rude-sheep-sing.md b/.changeset/rude-sheep-sing.md new file mode 100644 index 000000000..bd3b63243 --- /dev/null +++ b/.changeset/rude-sheep-sing.md @@ -0,0 +1,5 @@ +--- +"inngest": minor +--- + +Allow filtering of events within triggers diff --git a/etc/inngest.api.md b/etc/inngest.api.md index 9f6eddd65..3c7c667b0 100644 --- a/etc/inngest.api.md +++ b/etc/inngest.api.md @@ -339,7 +339,7 @@ export type ZodEventSchemas = Record { keyof EventsFromOpts & string >; + let sanitizedTrigger: FunctionTrigger & string>; + + if (typeof trigger === "string") { + sanitizedTrigger = { event: trigger }; + } else if (trigger.event) { + sanitizedTrigger = { + event: trigger.event, + expression: trigger.if, + }; + } else { + sanitizedTrigger = trigger; + } + return new InngestFunction( this, sanitizedOpts, - typeof trigger === "string" ? { event: trigger } : trigger, + sanitizedTrigger, // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any handler as any ); diff --git a/src/types.ts b/src/types.ts index fdf362586..9fa2c3789 100644 --- a/src/types.ts +++ b/src/types.ts @@ -603,6 +603,7 @@ export type TriggerOptions = | StrictUnion< | { event: T; + if?: string; } | { cron: string; From 9dd44f3038462278ccb3453e10dc3e79b3073f60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 18:17:09 +0000 Subject: [PATCH 31/34] Bump word-wrap from 1.2.3 to 1.2.4 (#269) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b7b9a258a..5be0c2dbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5315,9 +5315,9 @@ which@^2.0.1: isexe "^2.0.0" word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wrap-ansi@^6.2.0: version "6.2.0" From 55c889cbc990a0dc50392355238a6b3266d3ccf6 Mon Sep 17 00:00:00 2001 From: Darwin <5746693+darwin67@users.noreply.github.com> Date: Wed, 26 Jul 2023 14:33:39 -1000 Subject: [PATCH 32/34] Show the actual HTTP response error message for unknow HTTP status. (#273) We're getting reports of unknown error messages when sending events to Inngest's event API, but the LB has no record of them. Change the body parsing here to actually use the error body instead of just `Unknown Error` to give us a better idea of what's going on. --------- Co-authored-by: Darwin D Wu --- .changeset/dull-ligers-change.md | 5 +++++ src/components/Inngest.ts | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/dull-ligers-change.md diff --git a/.changeset/dull-ligers-change.md b/.changeset/dull-ligers-change.md new file mode 100644 index 000000000..582db71de --- /dev/null +++ b/.changeset/dull-ligers-change.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Expose raw error message if status is unknown diff --git a/src/components/Inngest.ts b/src/components/Inngest.ts index 0fea02667..0b1c2eee1 100644 --- a/src/components/Inngest.ts +++ b/src/components/Inngest.ts @@ -244,7 +244,11 @@ export class Inngest { case 500: errorMessage = "Internal server error"; break; + default: + errorMessage = await response.text(); + break; } + return new Error(`Inngest API Error: ${response.status} ${errorMessage}`); } From 060d5b09d92dcf40fb8db5fc4f51f5c5a5a9dfdd Mon Sep 17 00:00:00 2001 From: Inngest Release Bot <126702797+inngest-release-bot@users.noreply.github.com> Date: Thu, 27 Jul 2023 01:41:47 +0100 Subject: [PATCH 33/34] Release @latest (#268) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## inngest@2.4.0 ### Minor Changes - 6cb6719: Allow filtering of events within triggers ### Patch Changes - 55c889c: Expose raw error message if status is unknown Co-authored-by: github-actions[bot] --- .changeset/dull-ligers-change.md | 5 ----- .changeset/rude-sheep-sing.md | 5 ----- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 .changeset/dull-ligers-change.md delete mode 100644 .changeset/rude-sheep-sing.md diff --git a/.changeset/dull-ligers-change.md b/.changeset/dull-ligers-change.md deleted file mode 100644 index 582db71de..000000000 --- a/.changeset/dull-ligers-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": patch ---- - -Expose raw error message if status is unknown diff --git a/.changeset/rude-sheep-sing.md b/.changeset/rude-sheep-sing.md deleted file mode 100644 index bd3b63243..000000000 --- a/.changeset/rude-sheep-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"inngest": minor ---- - -Allow filtering of events within triggers diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce8ebef5..70e67f255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # inngest +## 2.4.0 + +### Minor Changes + +- 6cb6719: Allow filtering of events within triggers + +### Patch Changes + +- 55c889c: Expose raw error message if status is unknown + ## 2.3.0 ### Minor Changes diff --git a/package.json b/package.json index d6e9433d2..5c69642f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inngest", - "version": "2.3.0", + "version": "2.4.0", "description": "Official SDK for Inngest.com", "main": "./index.js", "types": "./index.d.ts", From c271eb1295102c6d5f3f579d3ab31c53702e49a8 Mon Sep 17 00:00:00 2001 From: Jack Williams Date: Thu, 27 Jul 2023 12:43:46 +0100 Subject: [PATCH 34/34] Add `x-inngest-no-retry: true` header when non-retriable (#241) ## Summary Send back an `x-inngest-no-retry: true` header from the SDK instead of relying on `400 Bad Request` returns. --- .changeset/lovely-buckets-taste.md | 5 +++++ etc/inngest.api.md | 2 ++ src/components/InngestCommHandler.ts | 12 +++++++++--- src/helpers/consts.ts | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 .changeset/lovely-buckets-taste.md diff --git a/.changeset/lovely-buckets-taste.md b/.changeset/lovely-buckets-taste.md new file mode 100644 index 000000000..8574780fe --- /dev/null +++ b/.changeset/lovely-buckets-taste.md @@ -0,0 +1,5 @@ +--- +"inngest": patch +--- + +Add `x-inngest-no-retry: true` header when non-retriable for internal executor changes diff --git a/etc/inngest.api.md b/etc/inngest.api.md index 3c7c667b0..86a7537d8 100644 --- a/etc/inngest.api.md +++ b/etc/inngest.api.md @@ -117,6 +117,8 @@ export enum headerKeys { // (undocumented) Framework = "x-inngest-framework", // (undocumented) + NoRetry = "x-inngest-no-retry", + // (undocumented) Platform = "x-inngest-platform", // (undocumented) SdkVersion = "x-inngest-sdk", diff --git a/src/components/InngestCommHandler.ts b/src/components/InngestCommHandler.ts index 35328730d..39f95f84f 100644 --- a/src/components/InngestCommHandler.ts +++ b/src/components/InngestCommHandler.ts @@ -652,6 +652,14 @@ export class InngestCommHandler< ); if (stepRes.status === 500 || stepRes.status === 400) { + const headers: Record = { + "Content-Type": "application/json", + }; + + if (stepRes.status === 400) { + headers[headerKeys.NoRetry] = "true"; + } + return { status: stepRes.status, body: stringify( @@ -662,9 +670,7 @@ export class InngestCommHandler< ) ) ), - headers: { - "Content-Type": "application/json", - }, + headers, }; } diff --git a/src/helpers/consts.ts b/src/helpers/consts.ts index 997526b79..aaf528da1 100644 --- a/src/helpers/consts.ts +++ b/src/helpers/consts.ts @@ -109,6 +109,7 @@ export enum headerKeys { Environment = "x-inngest-env", Platform = "x-inngest-platform", Framework = "x-inngest-framework", + NoRetry = "x-inngest-no-retry", } export const defaultDevServerHost = "http://127.0.0.1:8288/";