Skip to content

Commit

Permalink
Add experimental @inngest/test package (#688)
Browse files Browse the repository at this point in the history
## Summary
<!-- Succinctly describe your change, providing context, what you've
changed, and why. -->

Adds a preliminary, experimental `@inngest/test` package based on the
RFC at inngest/inngest#1680.

This first PR is intended to release some internal APIs such that we can
utilize them in `@inngest/test` in later changes.

## Checklist
<!-- Tick these items off as you progress. -->
<!-- If an item isn't applicable, ideally please strikeout the item by
wrapping it in "~~"" and suffix it with "N/A My reason for skipping
this." -->
<!-- e.g. "- [ ] ~~Added tests~~ N/A Only touches docs" -->

- [ ] ~Added a [docs PR](https://github.com/inngest/website) that
references this PR~ N/A Later during the release of `@inngest/test`
- [ ] ~Added unit/integration tests~ N/A
- [x] Added changesets if applicable

## Related
<!-- A space for any related links, issues, or PRs. -->
<!-- Linear issues are autolinked. -->
<!-- e.g. - INN-123 -->
<!-- GitHub issues/PRs can be linked using shorthand. -->
<!-- e.g. "- inngest/inngest#123" -->
<!-- Feel free to remove this section if there are no applicable related
links.-->
- inngest/inngest#1680
  • Loading branch information
jpwilliams authored Sep 7, 2024
1 parent 79069e1 commit 58549f3
Show file tree
Hide file tree
Showing 22 changed files with 1,043 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/few-brooms-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inngest": patch
---

Expose some internal execution logic to make way for a new `@inngest/test` package
52 changes: 52 additions & 0 deletions .github/workflows/prerelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,55 @@ jobs:

The last release was built and published from ${{ github.event.pull_request.head.sha }}.
edit-mode: replace

prerelease_test:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
defaults:
run:
working-directory: packages/test
if: contains(github.event.pull_request.labels.*.name, 'prerelease/test')
steps:
- uses: actions/checkout@v3
with:
persist-credentials: false

- uses: ./.github/actions/setup-and-build
with:
install-dependencies: false
build: false

- run: pnpm install

- run: pnpm build

- name: Prerelease PR
run: node ../../scripts/release/prerelease.js
env:
TAG: pr-${{ github.event.pull_request.number }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_ENV: test # disable npm access checks; they don't work in CI
DIST_DIR: dist

- name: Update PR with latest prerelease
uses: edumserrano/find-create-or-update-comment@v1
with:
token: ${{ secrets.CHANGESET_GITHUB_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
body-includes: "<!-- pr-prerelease-comment-test -->"
comment-author: "inngest-release-bot"
body:
| # can be a single value or you can compose text with multi-line values
<!-- pr-prerelease-comment-test -->
A user has added the <kbd>[prerelease/test](https://github.com/inngest/inngest-js/labels/prerelease%2Ftest)</kbd> label, so this PR will be published to npm with the tag `pr-${{ github.event.pull_request.number }}`. It will be updated with the latest changes as you push commits to this PR.

You can install this prerelease version with:

```sh
npm install @inngest/test@pr-${{ github.event.pull_request.number }}
```

The last release was built and published from ${{ github.event.pull_request.head.sha }}.
edit-mode: replace
2 changes: 1 addition & 1 deletion packages/inngest/src/components/InngestMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ describe("stacking and inference", () => {
},
}),
],
}).createFunction({ id: "" }, { event: "" }, ({ step }) => {
}).createFunction({ id: "" }, { event: "" }, () => {
throw new Error("test error");
});

Expand Down
3 changes: 2 additions & 1 deletion packages/inngest/src/components/InngestStepTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ import { type InngestExecution } from "./execution/InngestExecution";
export interface FoundStep extends HashedOp {
hashedId: string;
fn?: (...args: unknown[]) => unknown;
rawArgs: unknown[];
fulfilled: boolean;
handled: boolean;

/**
* Returns a boolean representing whether or not the step was handled on this
* invocation.
*/
handle: () => boolean;
handle: () => Promise<boolean>;
}

export type MatchOpFn<
Expand Down
19 changes: 18 additions & 1 deletion packages/inngest/src/components/execution/InngestExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ export interface ExecutionResults {
}

export type ExecutionResult = {
[K in keyof ExecutionResults]: Simplify<{ type: K } & ExecutionResults[K]>;
[K in keyof ExecutionResults]: Simplify<
{
type: K;
ctx: Context.Any;
ops: Record<string, MemoizedOp>;
} & ExecutionResults[K]
>;
}[keyof ExecutionResults];

export type ExecutionResultHandler<T = ActionResponse> = (
Expand All @@ -31,6 +37,11 @@ export type ExecutionResultHandlers<T = ActionResponse> = {
};

export interface MemoizedOp extends IncomingOp {
/**
* If the step has been hit during this run, these will be the arguments
* passed to it.
*/
rawArgs?: unknown[];
fulfilled?: boolean;
seen?: boolean;
}
Expand Down Expand Up @@ -63,6 +74,12 @@ export interface InngestExecutionOptions {
timer?: ServerTiming;
isFailureHandler?: boolean;
disableImmediateExecution?: boolean;

/**
* Provide the ability to transform the context passed to the function before
* the execution starts.
*/
transformCtx?: (ctx: Readonly<Context.Any>) => Context.Any;
}

export type InngestExecutionFactory = (
Expand Down
47 changes: 43 additions & 4 deletions packages/inngest/src/components/execution/v0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
type IInngestExecution,
type InngestExecutionFactory,
type InngestExecutionOptions,
type MemoizedOp,
} from "./InngestExecution";

export const createV0InngestExecution: InngestExecutionFactory = (options) => {
Expand Down Expand Up @@ -188,7 +189,12 @@ export class V0InngestExecution

const { type: _type, ...rest } = result;

return { type: "step-ran", step: { ...outgoingUserFnOp, ...rest } };
return {
type: "step-ran",
ctx: this.fnArg,
ops: this.ops,
step: { ...outgoingUserFnOp, ...rest },
};
}

if (!discoveredOps.length) {
Expand Down Expand Up @@ -235,6 +241,8 @@ export class V0InngestExecution

return {
type: "steps-found",
ctx: this.fnArg,
ops: this.ops,
steps: discoveredOps as [OutgoingOp, ...OutgoingOp[]],
};
} catch (error) {
Expand Down Expand Up @@ -312,6 +320,24 @@ export class V0InngestExecution
return state;
}

get ops(): Record<string, MemoizedOp> {
return Object.fromEntries(
Object.entries(this.state.allFoundOps).map<[string, MemoizedOp]>(
([id, op]) => [
id,
{
id: op.id,
rawArgs: op.rawArgs,
data: op.data,
error: op.error,
fulfilled: op.fulfilled,
seen: true,
},
]
)
);
}

private getUserFnToRun(): Handler.Any {
if (!this.options.isFailureHandler) {
return this.options.fn["fn"];
Expand Down Expand Up @@ -406,6 +432,7 @@ export class V0InngestExecution
this.state.tickOps[opId.id] = {
...opId,
...(opts?.fn ? { fn: () => opts.fn?.(...args) } : {}),
rawArgs: args,
resolve,
reject,
fulfilled: false,
Expand All @@ -431,7 +458,7 @@ export class V0InngestExecution
};
}

return fnArg;
return this.options.transformCtx?.(fnArg) ?? fnArg;
}

/**
Expand Down Expand Up @@ -508,14 +535,26 @@ export class V0InngestExecution

const serializedError = serializeError(error);

return { type: "function-rejected", error: serializedError, retriable };
return {
type: "function-rejected",
ctx: this.fnArg,
ops: this.ops,
error: serializedError,
retriable,
};
}

return { type: "function-resolved", data: undefinedToNull(data) };
return {
type: "function-resolved",
ctx: this.fnArg,
ops: this.ops,
data: undefinedToNull(data),
};
}
}

interface TickOp extends HashedOp {
rawArgs: unknown[];
fn?: (...args: unknown[]) => unknown;
fulfilled: boolean;
resolve: (value: MaybePromise<unknown>) => void;
Expand Down
39 changes: 34 additions & 5 deletions packages/inngest/src/components/execution/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {
if (transformResult.type === "function-resolved") {
return {
type: "step-ran",
ctx: transformResult.ctx,
ops: transformResult.ops,
step: _internals.hashOp({
...stepResult,
data: transformResult.data,
Expand All @@ -188,6 +190,8 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {
} else if (transformResult.type === "function-rejected") {
return {
type: "step-ran",
ctx: transformResult.ctx,
ops: transformResult.ops,
step: _internals.hashOp({
...stepResult,
error: transformResult.error,
Expand All @@ -205,6 +209,8 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {
if (newSteps) {
return {
type: "steps-found",
ctx: this.fnArg,
ops: this.ops,
steps: newSteps,
};
}
Expand All @@ -215,7 +221,7 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {
* timed out or have otherwise decided that it doesn't exist.
*/
"step-not-found": ({ step }) => {
return { type: "step-not-found", step };
return { type: "step-not-found", ctx: this.fnArg, ops: this.ops, step };
},
};
}
Expand Down Expand Up @@ -563,10 +569,21 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {

const serializedError = minifyPrettyError(serializeError(error));

return { type: "function-rejected", error: serializedError, retriable };
return {
type: "function-rejected",
ctx: this.fnArg,
ops: this.ops,
error: serializedError,
retriable,
};
}

return { type: "function-resolved", data: undefinedToNull(data) };
return {
type: "function-resolved",
ctx: this.fnArg,
ops: this.ops,
data: undefinedToNull(data),
};
}

private createExecutionState(): V1ExecutionState {
Expand Down Expand Up @@ -611,6 +628,10 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {
return state;
}

get ops(): Record<string, MemoizedOp> {
return this.state.steps;
}

private createFnArg(): Context.Any {
const step = this.createStepTools();

Expand All @@ -633,7 +654,7 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {
};
}

return fnArg;
return this.options.transformCtx?.(fnArg) ?? fnArg;
}

private createStepTools(): ReturnType<typeof createStepTools> {
Expand Down Expand Up @@ -839,13 +860,14 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {

const step: FoundStep = {
...opId,
rawArgs: args,
hashedId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
fn: opts?.fn ? () => opts.fn?.(...args) : undefined,
fulfilled: Boolean(stepState),
displayName: opId.displayName ?? opId.id,
handled: false,
handle: () => {
handle: async () => {
if (step.handled) {
return false;
}
Expand All @@ -855,6 +877,13 @@ class V1InngestExecution extends InngestExecution implements IInngestExecution {
if (stepState) {
stepState.fulfilled = true;

// For some execution scenarios such as testing, `data` and/or
// `error` may be `Promises`. This could also be the case for future
// middleware applications. For this reason, we'll make sure the
// values are fully resolved before continuing.
await stepState.data;
await stepState.error;

if (typeof stepState.data !== "undefined") {
resolve(stepState.data);
} else {
Expand Down
14 changes: 8 additions & 6 deletions packages/inngest/src/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {
} from "@local/components/InngestStepTools";
import {
ExecutionVersion,
IInngestExecution,
InngestExecution,
InngestExecutionOptions,
type IInngestExecution,
type InngestExecution,
type InngestExecutionOptions,
PREFERRED_EXECUTION_VERSION,
} from "@local/components/execution/InngestExecution";
import { ServerTiming } from "@local/helpers/ServerTiming";
Expand All @@ -25,7 +25,7 @@ import {
} from "@local/helpers/consts";
import { type Env } from "@local/helpers/env";
import { slugify } from "@local/helpers/strings";
import { EventPayload, type FunctionConfig } from "@local/types";
import { type EventPayload, type FunctionConfig } from "@local/types";
import { fromPartial } from "@total-typescript/shoehorn";
import fetch from "cross-fetch";
import { type Request, type Response } from "express";
Expand Down Expand Up @@ -120,7 +120,7 @@ export type StepTools = ReturnType<typeof getStepTools>;
* Given an Inngest function and the appropriate execution state, return the
* resulting data from this execution.
*/
export const runFnWithStack = (
export const runFnWithStack = async (
fn: InngestFunction.Any,
stepState: InngestExecutionOptions["stepState"],
opts?: {
Expand Down Expand Up @@ -150,7 +150,9 @@ export const runFnWithStack = (
},
});

return execution.start();
const { ctx: _ctx, ops: _ops, ...rest } = await execution.start();

return rest;
};

const inngest = createClient({ id: "test", eventKey: "event-key-123" });
Expand Down
2 changes: 2 additions & 0 deletions packages/test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
node_modules
Loading

0 comments on commit 58549f3

Please sign in to comment.