diff --git a/packages/inngest/src/components/InngestFunction.test.ts b/packages/inngest/src/components/InngestFunction.test.ts index 7b4113c33..9f885a6eb 100644 --- a/packages/inngest/src/components/InngestFunction.test.ts +++ b/packages/inngest/src/components/InngestFunction.test.ts @@ -7,15 +7,15 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { jest } from "@jest/globals"; import { EventSchemas, InngestMiddleware, type EventPayload } from "@local"; +import { InngestFunction } from "@local/components/InngestFunction"; +import { STEP_INDEXING_SUFFIX } from "@local/components/InngestStepTools"; +import { NonRetriableError } from "@local/components/NonRetriableError"; import { - _internals, type ExecutionResult, type ExecutionResults, type InngestExecutionOptions, -} from "@local/components/InngestExecution"; -import { InngestFunction } from "@local/components/InngestFunction"; -import { STEP_INDEXING_SUFFIX } from "@local/components/InngestStepTools"; -import { NonRetriableError } from "@local/components/NonRetriableError"; +} from "@local/components/execution/InngestExecution"; +import { _internals } from "@local/components/execution/v1"; import { ServerTiming } from "@local/helpers/ServerTiming"; import { internalEvents } from "@local/helpers/consts"; import { @@ -146,6 +146,7 @@ describe("runFn", () => { const execution = fn["createExecution"]({ data: { event: { name: "foo", data: { foo: "foo" } } }, + runId: "run", stepState: {}, stepCompletionOrder: [], }); @@ -185,6 +186,7 @@ describe("runFn", () => { const execution = fn["createExecution"]({ data: { event: { name: "foo", data: { foo: "foo" } } }, stepState: {}, + runId: "run", stepCompletionOrder: [], }); @@ -216,6 +218,7 @@ describe("runFn", () => { ) => { const execution = fn["createExecution"]({ data: { event: opts?.event || { name: "foo", data: {} } }, + runId: "run", stepState, stepCompletionOrder: opts?.stackOrder ?? Object.keys(stepState), isFailureHandler: Boolean(opts?.onFailure), diff --git a/packages/inngest/src/components/InngestStepTools.test.ts b/packages/inngest/src/components/InngestStepTools.test.ts index 470cb242b..c39d5f2de 100644 --- a/packages/inngest/src/components/InngestStepTools.test.ts +++ b/packages/inngest/src/components/InngestStepTools.test.ts @@ -1,121 +1,93 @@ +/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { EventSchemas } from "@local/components/EventSchemas"; -import { - type AnyInngest, - type EventsFromOpts, -} from "@local/components/Inngest"; -import { - InngestExecution, - type MemoizedOp, -} from "@local/components/InngestExecution"; +import { type EventsFromOpts } from "@local/components/Inngest"; import { createStepTools, - type FoundStep, + getStepOptions, } from "@local/components/InngestStepTools"; -import { - StepOpCode, - type ClientOptions, - type EventPayload, -} from "@local/types"; +import { StepOpCode, type ClientOptions } from "@local/types"; import ms from "ms"; import { assertType } from "type-plus"; import { createClient } from "../test/helpers"; -const getStepTools = ({ - client = createClient({ id: "test" }), - stepState = {}, -}: { - client?: AnyInngest; - stepState?: Record; -} = {}): { - tools: ReturnType; - getOp: GetOp; -} => { - const fn = client.createFunction({ id: "test" }, { event: "any" }, () => { - /** no-op */ - }); - - const execution = new InngestExecution({ - client, - fn, - data: {}, - stepState, - stepCompletionOrder: Object.keys(stepState), - }); - - const tools = createStepTools(client, execution.state); - const getOp = () => - new Promise((resolve) => { - setTimeout(() => - setTimeout(() => resolve(Object.values(execution.state.steps)[0])) - ); - }); - - return { tools, getOp }; +const getStepTools = () => { + const step = createStepTools( + createClient({ id: "test" }), + {}, + ({ args, matchOp }) => { + const stepOptions = getStepOptions(args[0]); + return Promise.resolve(matchOp(stepOptions, ...args.slice(1))); + } + ); + + return step; }; -type StepTools = ReturnType["tools"]; -type GetOp = () => Promise; +type StepTools = ReturnType; describe("waitForEvent", () => { - let waitForEvent: StepTools["waitForEvent"]; - let getOp: GetOp; + let step: StepTools; beforeEach(() => { - ({ - tools: { waitForEvent }, - getOp, - } = getStepTools()); + step = getStepTools(); }); test("return WaitForEvent step op code", async () => { - void waitForEvent("id", { event: "event", timeout: "2h" }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", timeout: "2h" }) + ).resolves.toMatchObject({ op: StepOpCode.WaitForEvent, }); }); test("returns `id` as ID", async () => { - void waitForEvent("id", { event: "event", timeout: "2h" }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", timeout: "2h" }) + ).resolves.toMatchObject({ id: "id", }); }); test("returns ID by default", async () => { - void waitForEvent("id", { event: "event", timeout: "2h" }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", timeout: "2h" }) + ).resolves.toMatchObject({ displayName: "id", }); }); test("returns specific name if given", async () => { - void waitForEvent( - { id: "id", name: "name" }, - { event: "event", timeout: "2h" } - ); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent( + { id: "id", name: "name" }, + { event: "event", timeout: "2h" } + ) + ).resolves.toMatchObject({ displayName: "name", }); }); test("return event name as name", async () => { - void waitForEvent("id", { event: "event", timeout: "2h" }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", timeout: "2h" }) + ).resolves.toMatchObject({ name: "event", }); }); test("return blank opts if none given", async () => { - void waitForEvent("id", { event: "event", timeout: "2h" }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", timeout: "2h" }) + ).resolves.toMatchObject({ opts: {}, }); }); test("return TTL if string `timeout` given", async () => { - void waitForEvent("id", { event: "event", timeout: "1m" }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", timeout: "1m" }) + ).resolves.toMatchObject({ opts: { timeout: "1m", }, @@ -127,8 +99,9 @@ describe("waitForEvent", () => { upcoming.setDate(upcoming.getDate() + 6); upcoming.setHours(upcoming.getHours() + 1); - void waitForEvent("id", { event: "event", timeout: upcoming }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", timeout: upcoming }) + ).resolves.toMatchObject({ opts: { timeout: expect.stringMatching(upcoming.toISOString()), }, @@ -136,8 +109,9 @@ describe("waitForEvent", () => { }); test("return simple field match if `match` string given", async () => { - void waitForEvent("id", { event: "event", match: "name", timeout: "2h" }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { event: "event", match: "name", timeout: "2h" }) + ).resolves.toMatchObject({ opts: { if: "event.name == async.name", }, @@ -145,12 +119,13 @@ describe("waitForEvent", () => { }); test("return custom match statement if `if` given", async () => { - void waitForEvent("id", { - event: "event", - if: "name == 123", - timeout: "2h", - }); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.waitForEvent("id", { + event: "event", + if: "name == 123", + timeout: "2h", + }) + ).resolves.toMatchObject({ opts: { if: "name == 123", }, @@ -160,7 +135,7 @@ describe("waitForEvent", () => { describe("type errors", () => { test("does not allow both `match` and `if`", () => { // @ts-expect-error `match` and `if` cannot be defined together - void waitForEvent("id", { + void step.waitForEvent("id", { event: "event", match: "name", if: "name", @@ -171,40 +146,34 @@ describe("waitForEvent", () => { }); describe("run", () => { - let run: StepTools["run"]; - let getOp: GetOp; + let step: StepTools; beforeEach(() => { - ({ - tools: { run }, - getOp, - } = getStepTools()); + step = getStepTools(); }); test("return Step step op code", async () => { - void run("step", () => undefined); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.run("step", () => undefined)).resolves.toMatchObject({ op: StepOpCode.StepPlanned, }); }); test("returns `id` as ID", async () => { - void run("id", () => undefined); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.run("id", () => undefined)).resolves.toMatchObject({ id: "id", }); }); test("return ID by default", async () => { - void run("id", () => undefined); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.run("id", () => undefined)).resolves.toMatchObject({ displayName: "id", }); }); test("return specific name if given", async () => { - void run({ id: "id", name: "name" }, () => undefined); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.run({ id: "id", name: "name" }, () => undefined) + ).resolves.toMatchObject({ displayName: "name", }); }); @@ -230,7 +199,7 @@ describe("run", () => { set: new Set(), }; - const output = run("step", () => input); + const output = step.run("step", () => input); assertType< Promise<{ @@ -254,62 +223,51 @@ describe("run", () => { }); describe("sleep", () => { - let sleep: StepTools["sleep"]; - let getOp: GetOp; + let step: StepTools; beforeEach(() => { - ({ - tools: { sleep }, - getOp, - } = getStepTools()); + step = getStepTools(); }); test("return id", async () => { - void sleep("id", "1m"); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleep("id", "1m")).resolves.toMatchObject({ id: "id", }); }); test("return Sleep step op code", async () => { - void sleep("id", "1m"); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleep("id", "1m")).resolves.toMatchObject({ op: StepOpCode.Sleep, }); }); test("return ID by default", async () => { - void sleep("id", "1m"); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleep("id", "1m")).resolves.toMatchObject({ displayName: "id", }); }); test("return specific name if given", async () => { - void sleep({ id: "id", name: "name" }, "1m"); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.sleep({ id: "id", name: "name" }, "1m") + ).resolves.toMatchObject({ displayName: "name", }); }); }); describe("sleepUntil", () => { - let sleepUntil: StepTools["sleepUntil"]; - let getOp: GetOp; + let step: StepTools; beforeEach(() => { - ({ - tools: { sleepUntil }, - getOp, - } = getStepTools()); + step = getStepTools(); }); test("return id", async () => { const future = new Date(); future.setDate(future.getDate() + 1); - void sleepUntil("id", future); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleepUntil("id", future)).resolves.toMatchObject({ id: "id", }); }); @@ -318,8 +276,7 @@ describe("sleepUntil", () => { const future = new Date(); future.setDate(future.getDate() + 1); - void sleepUntil("id", future); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleepUntil("id", future)).resolves.toMatchObject({ displayName: "id", }); }); @@ -328,8 +285,9 @@ describe("sleepUntil", () => { const future = new Date(); future.setDate(future.getDate() + 1); - void sleepUntil({ id: "id", name: "name" }, future); - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.sleepUntil({ id: "id", name: "name" }, future) + ).resolves.toMatchObject({ displayName: "name", }); }); @@ -338,8 +296,7 @@ describe("sleepUntil", () => { const future = new Date(); future.setDate(future.getDate() + 1); - void sleepUntil("id", future); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleepUntil("id", future)).resolves.toMatchObject({ op: StepOpCode.Sleep, }); }); @@ -347,8 +304,7 @@ describe("sleepUntil", () => { test("parses dates", async () => { const next = new Date(); - void sleepUntil("id", next); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleepUntil("id", next)).resolves.toMatchObject({ name: next.toISOString(), }); }); @@ -356,8 +312,7 @@ describe("sleepUntil", () => { test("parses ISO strings", async () => { const next = new Date(new Date().valueOf() + ms("6d")).toISOString(); - void sleepUntil("id", next); - await expect(getOp()).resolves.toMatchObject({ + await expect(step.sleepUntil("id", next)).resolves.toMatchObject({ name: next, }); }); @@ -365,7 +320,7 @@ describe("sleepUntil", () => { test("throws if invalid date given", async () => { const next = new Date("bad"); - await expect(() => sleepUntil("id", next)).rejects.toThrow( + await expect(() => step.sleepUntil("id", next)).rejects.toThrow( "Invalid date or date string passed" ); }); @@ -373,7 +328,7 @@ describe("sleepUntil", () => { test("throws if invalid time string given", async () => { const next = "bad"; - await expect(() => sleepUntil("id", next)).rejects.toThrow( + await expect(() => step.sleepUntil("id", next)).rejects.toThrow( "Invalid date or date string passed" ); }); @@ -381,79 +336,49 @@ describe("sleepUntil", () => { describe("sendEvent", () => { describe("runtime", () => { - const fetchMock = jest.fn( - (url: string, opts: { body: string }) => - Promise.resolve({ - status: 200, - json: () => - Promise.resolve({ - status: 200, - ids: (JSON.parse(opts.body) as EventPayload[]).map( - () => "test-id" - ), - }), - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ) as unknown as typeof fetch; - - const client = createClient({ - id: "test", - fetch: fetchMock, - eventKey: "123", - }); - - let sendEvent: StepTools["sendEvent"]; - let getOp: GetOp; - + let step: StepTools; beforeEach(() => { - ({ - tools: { sendEvent }, - getOp, - } = getStepTools({ - client, - stepState: { - /** - * We define a fake step here to ensure that the `sendEvent` tool - * doesn't run inline, which it will do if no other steps have been - * detected before it's been run. - */ - "fake-other-step": { id: "fake-other-step" }, - }, - })); + step = getStepTools(); }); test("return id", async () => { - void sendEvent("id", { name: "step", data: "foo" }); - - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.sendEvent("id", { name: "step", data: "foo" }) + ).resolves.toMatchObject({ id: "id", }); }); test("return Step step op code", async () => { - void sendEvent("id", { name: "step", data: "foo" }); - - await expect(getOp()).resolves.toMatchObject({ + await expect( + step.sendEvent("id", { name: "step", data: "foo" }) + ).resolves.toMatchObject({ op: StepOpCode.StepPlanned, }); }); test("return ID by default", async () => { - void sendEvent("id", { name: "step", data: "foo" }); - - await expect(getOp()).resolves.toMatchObject({ displayName: "id" }); + await expect( + step.sendEvent("id", { name: "step", data: "foo" }) + ).resolves.toMatchObject({ displayName: "id" }); }); test("return specific name if given", async () => { - void sendEvent({ id: "id", name: "name" }, { name: "step", data: "foo" }); - - await expect(getOp()).resolves.toMatchObject({ displayName: "name" }); + await expect( + step.sendEvent( + { id: "id", name: "name" }, + { name: "step", data: "foo" } + ) + ).resolves.toMatchObject({ displayName: "name" }); }); test("retain legacy `name` field for backwards compatibility with <=v2", async () => { - void sendEvent({ id: "id", name: "name" }, { name: "step", data: "foo" }); - - await expect(getOp()).resolves.toMatchObject({ name: "sendEvent" }); + await expect( + step.sendEvent( + { id: "id", name: "name" }, + { name: "step", data: "foo" } + ) + ).resolves.toMatchObject({ name: "sendEvent" }); }); }); @@ -497,7 +422,12 @@ describe("sendEvent", () => { }); const sendEvent: ReturnType< - typeof createStepTools, "foo"> + typeof createStepTools< + typeof opts, + EventsFromOpts, + "foo", + {} + > // eslint-disable-next-line @typescript-eslint/no-explicit-any >["sendEvent"] = (() => undefined) as any; diff --git a/packages/inngest/src/components/InngestStepTools.ts b/packages/inngest/src/components/InngestStepTools.ts index 4546c029e..3dc54d049 100644 --- a/packages/inngest/src/components/InngestStepTools.ts +++ b/packages/inngest/src/components/InngestStepTools.ts @@ -45,7 +45,7 @@ export type MatchOpFn< export type StepHandler = (info: { matchOp: MatchOpFn; opts?: StepToolOptions; - args: unknown[]; + args: [StepOptionsOrId, ...unknown[]]; }) => Promise; export interface StepToolOptions< @@ -134,9 +134,9 @@ export const createStepTools = < matchOp: MatchOpFn, opts?: StepToolOptions ): T => { - // return (async (...args: Parameters): Promise => { return (async (...args: Parameters): Promise => { - return stepHandler({ args, matchOp, opts }); + const parsedArgs = args as unknown as [StepOptionsOrId, ...unknown[]]; + return stepHandler({ args: parsedArgs, matchOp, opts }); }) as T; }; @@ -183,7 +183,7 @@ export const createStepTools = < id, op: StepOpCode.StepPlanned, name: "sendEvent", - displayName: name, + displayName: name ?? id, }; }, { @@ -238,7 +238,7 @@ export const createStepTools = < op: StepOpCode.WaitForEvent, name: opts.event, opts: matchOpts, - displayName: name, + displayName: name ?? id, }; } ), @@ -287,7 +287,7 @@ export const createStepTools = < id, op: StepOpCode.StepPlanned, name: id, - displayName: name, + displayName: name ?? id, }; }, { fn: (stepOptions, fn) => fn() } @@ -321,7 +321,7 @@ export const createStepTools = < id, op: StepOpCode.Sleep, name: timeStr(time), - displayName: name, + displayName: name ?? id, }; }), @@ -352,7 +352,7 @@ export const createStepTools = < id, op: StepOpCode.Sleep, name: date.toISOString(), - displayName: name, + displayName: name ?? id, }; } catch (err) { /** diff --git a/packages/inngest/src/components/execution/v0.ts b/packages/inngest/src/components/execution/v0.ts index d68cef9a5..779affecf 100644 --- a/packages/inngest/src/components/execution/v0.ts +++ b/packages/inngest/src/components/execution/v0.ts @@ -27,7 +27,6 @@ import { type IncomingOp, type OpStack, type OutgoingOp, - type StepOptionsOrId, } from "../../types"; import { getHookStack, type RunHookStack } from "../InngestMiddleware"; import { @@ -398,13 +397,8 @@ export class V0InngestExecution this.#state.hasUsedTools = true; - const [stepOptionsOrId, ...remainingArgs] = args as unknown as [ - StepOptionsOrId, - ...unknown[] - ]; - - const stepOptions = getStepOptions(stepOptionsOrId); - const opId = hashOp(matchOp(stepOptions, ...remainingArgs)); + const stepOptions = getStepOptions(args[0]); + const opId = hashOp(matchOp(stepOptions, ...args.slice(1))); return new Promise((resolve, reject) => { this.#state.tickOps[opId.id] = { diff --git a/packages/inngest/src/components/execution/v1.ts b/packages/inngest/src/components/execution/v1.ts index 56439b286..4b93850a4 100644 --- a/packages/inngest/src/components/execution/v1.ts +++ b/packages/inngest/src/components/execution/v1.ts @@ -25,7 +25,6 @@ import { type EventPayload, type FailureEventArgs, type OutgoingOp, - type StepOptionsOrId, } from "../../types"; import { getHookStack, type RunHookStack } from "../InngestMiddleware"; import { @@ -684,14 +683,8 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution { ); } - const [stepOptionsOrId, ...remainingArgs] = args as unknown as [ - StepOptionsOrId, - ...unknown[] - ]; - - const stepOptions = getStepOptions(stepOptionsOrId); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const opId = matchOp(stepOptions, ...remainingArgs); + const stepOptions = getStepOptions(args[0]); + const opId = matchOp(stepOptions, ...args.slice(1)); if (this.#state.steps[opId.id]) { const originalId = opId.id;