Skip to content

Commit

Permalink
Fix path matching types getting lost in recursive types (#354)
Browse files Browse the repository at this point in the history
## Summary
<!-- Succinctly describe your change, providing context, what you've
changed, and why. -->

Fixes a "_Type instantiation is excessively deep and possibly infinite_"
bug when attempting to generate paths for events with recursive types,
such as a common `JsonValue` type.

```ts
type JsonObject = { [Key in string]?: JsonValue };
type JsonArray = Array<JsonValue>;
type JsonValue =
  | string
  | number
  | boolean
  | JsonObject
  | JsonArray
  | null;
```

We refactor the way paths are generated a little here, most notably
ensuring we skip known deep/recursive types and don't needlessly iterate
those we've seen before.

## 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 Bug fix
- [x] Added unit/integration tests
- [x] Added changesets if applicable
  • Loading branch information
jpwilliams authored Oct 10, 2023
1 parent 933b998 commit e2f68d6
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-peaches-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inngest": patch
---

Fix path matching types getting lost in certain recursive event types
1 change: 1 addition & 0 deletions packages/inngest/etc/inngest.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
```ts

import { IsEqual } from 'type-fest';
import { Jsonify } from 'type-fest';
import { Simplify } from 'type-fest';
import { z as z_2 } from 'zod';
Expand Down
2 changes: 1 addition & 1 deletion packages/inngest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"json-stringify-safe": "^5.0.1",
"ms": "^2.1.3",
"serialize-error-cjs": "^0.1.3",
"type-fest": "^3.5.1",
"type-fest": "^3.13.1",
"zod": "~3.21.4"
},
"devDependencies": {
Expand Down
42 changes: 42 additions & 0 deletions packages/inngest/src/components/EventSchemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,5 +816,47 @@ describe("EventSchemas", () => {
}
);
});

test("does not infinitely recurse when matching events with recursive types", () => {
type JsonObject = { [Key in string]?: JsonValue };
type JsonArray = Array<JsonValue>;
type JsonValue =
| string
| number
| boolean
| JsonObject
| JsonArray
| null;

interface TestEvent extends EventPayload {
name: "test.event";
data: { id: string; other: JsonValue; yer: string[] };
}

interface TestEvent2 extends EventPayload {
name: "test.event2";
data: { id: string; somethingElse: JsonValue };
}

const schemas = new EventSchemas().fromUnion<TestEvent | TestEvent2>();

const inngest = new Inngest({
id: "test",
schemas,
eventKey: "test-key-123",
});

inngest.createFunction(
{ id: "test", cancelOn: [{ event: "test.event2", match: "data.id" }] },
{ event: "test.event" },
({ step }) => {
void step.waitForEvent("id", {
event: "test.event2",
match: "data.id",
timeout: "1h",
});
}
);
});
});
});
67 changes: 55 additions & 12 deletions packages/inngest/src/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Simplify } from "type-fest";
import { type IsEqual, type Simplify } from "type-fest";
import { type EventPayload } from "../types";

/**
Expand Down Expand Up @@ -35,24 +35,67 @@ export type SendEventPayload<Events extends Record<string, EventPayload>> =
* A list of simple, JSON-compatible, primitive types that contain no other
* values.
*/
export type Primitive = string | number | boolean | undefined | null;
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint;

/**
* Given a key and a value, create a string that would be used to access that
* property in code.
* Returns `true` if `T` is a tuple, else `false`.
*/
type StringPath<K extends string | number, V> = V extends Primitive
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type IsTuple<T extends ReadonlyArray<any>> = number extends T["length"]
? false
: true;

/**
* Given a tuple `T`, return the keys of that tuple, excluding any shared or
* generic keys like `number` and standard array methods.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TupleKeys<T extends ReadonlyArray<any>> = Exclude<keyof T, keyof any[]>;

/**
* Returns `true` if `T1` matches anything in the union `T2`, else` never`.
*/
type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never;

/**
* A helper for concatenating an existing path `K` with new paths from the
* value `V`, making sure to skip those we've already seen in
* `TraversedTypes`.
*
* Purposefully skips some primitive objects to avoid building unsupported or
* recursive paths.
*/
type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| Date
? `${K}`
: true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: `${K}` | `${K}.${Path<V>}`;
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;

/**
* Given an object or array, recursively return all string paths used to access
* properties within those objects.
* Start iterating over a given object `T` and return all string paths used to
* access properties within that object as if you were in code.
*/
type Path<T> = T extends Array<infer V>
? StringPath<number, V>
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: PathImpl<number, V, TraversedTypes>
: {
[K in keyof T]-?: StringPath<K & string, T[K]>;
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];

/**
Expand All @@ -63,7 +106,7 @@ type Path<T> = T extends Array<infer V>
* paths of known objects.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ObjectPaths<T extends Record<string, any>> = Path<T>;
export type ObjectPaths<T> = T extends any ? PathInternal<T> : never;

/**
* Returns all keys from objects in the union `T`.
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit e2f68d6

Please sign in to comment.