diff --git a/.changeset/fix-fn-iterator-result.md b/.changeset/fix-fn-iterator-result.md new file mode 100644 index 00000000000..162bcfd388a --- /dev/null +++ b/.changeset/fix-fn-iterator-result.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +fix(Effect): handle transpiled generator bodies in `Effect.fn` + +`Effect.fn(name)(body)` crashed at runtime with `RuntimeException: Not a valid effect: {}` when `body` was a generator function lowered by a compiler (e.g. `babel-preset-expo` on React Native / Hermes) into a plain function returning an iterator IIFE. Such a body fails the `isGeneratorFunction` check, so its return value was passed through as if it were an `Effect`. We now duck-type the result and re-wrap it with `fromIterator` when it is an iterator. diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index d9143c239b6..52136152c41 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -14707,7 +14707,30 @@ function fnApply(options: { effect = core.fromIterator(() => options.body.apply(options.self, options.args)) } else { try { - effect = options.body.apply(options.self, options.args) + const result = options.body.apply(options.self, options.args) + // Some compilers (e.g. `babel-preset-expo` on React Native / Hermes) + // lower destructured-param generators into a plain function that returns + // an iterator IIFE. Such a body fails `isGeneratorFunction` but its + // return value is an iterator, not an Effect. Detect and re-wrap. + if ( + result !== null && + typeof result === "object" && + (result as any)[EffectTypeId] === undefined && + typeof (result as any).next === "function" && + typeof (result as any)[Symbol.iterator] === "function" + ) { + let firstIterator: any = result + effect = core.fromIterator(() => { + if (firstIterator !== null) { + const it = firstIterator + firstIterator = null + return it + } + return options.body.apply(options.self, options.args) + }) + } else { + effect = result + } } catch (error) { fnError = error effect = die(error) diff --git a/packages/effect/test/Effect/fn.test.ts b/packages/effect/test/Effect/fn.test.ts index a5416625a5a..2bc053b5d18 100644 --- a/packages/effect/test/Effect/fn.test.ts +++ b/packages/effect/test/Effect/fn.test.ts @@ -76,6 +76,23 @@ describe("Effect.fn", () => { strictEqual(fn2.length, 1) strictEqual(Effect.runSync(fn2(2)), 2) }) + + it.effect("handles a non-generator body that returns an iterator (transpiled generator)", () => + Effect.gen(function*() { + // Mimics `babel-preset-expo` lowering `function*({a}) { return a }` into + // a plain function that returns a generator IIFE iterator. + const body = function(arg: { a: number }) { + const a = arg.a + return (function*() { + return a + })() + } + const fn = Effect.fn("test")(body as any) + const v = yield* fn({ a: 42 }) as Effect.Effect + strictEqual(v, 42) + const v2 = yield* fn({ a: 7 }) as Effect.Effect + strictEqual(v2, 7) + })) }) describe("Effect.fnUntraced", () => {