Skip to content

Commit f160a14

Browse files
author
Tim Smart
committed
Merge remote-tracking branch 'upstream/effectify' into explore
2 parents ba3301f + 5f20b20 commit f160a14

File tree

3 files changed

+340
-0
lines changed

3 files changed

+340
-0
lines changed

src/effectify.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as Z from "@effect/io/Effect"
2+
import { CustomPromisifyLegacy, CustomPromisifySymbol } from "node:util"
3+
4+
type Callback<E, A> = (e: E, a: A) => void
5+
6+
type Fn = (...args: any) => any
7+
8+
export type CustomPromisify<TCustom extends Fn> =
9+
| CustomPromisifySymbol<TCustom>
10+
| CustomPromisifyLegacy<TCustom>
11+
12+
type Parameters<F extends Function> = F extends (...args: infer P) => any ? P : never
13+
14+
export type Length<L extends unknown[]> = L["length"]
15+
16+
export type Tail<L extends unknown[]> = L extends readonly []
17+
? L
18+
: L extends readonly [unknown?, ...infer LTail]
19+
? LTail
20+
: L
21+
22+
export type Last<L extends unknown[]> = L[Length<Tail<L>>]
23+
24+
export type UnwrapPromise<T> = T extends Promise<infer A> ? A : never
25+
26+
export function effectify<
27+
X extends Fn,
28+
F extends CustomPromisify<X>,
29+
Cb = Last<Parameters<F>>,
30+
E = Cb extends Function ? NonNullable<Parameters<Cb>[0]> : never
31+
>(
32+
fn: F
33+
): (
34+
...args: F extends CustomPromisify<infer TCustom> ? Parameters<TCustom> : never[]
35+
) => Z.Effect<
36+
never,
37+
E,
38+
F extends CustomPromisify<infer TCustom> ? UnwrapPromise<ReturnType<TCustom>> : never
39+
>
40+
41+
export function effectify<E, A>(
42+
fn: (cb: Callback<E, A>) => void
43+
): () => Z.Effect<never, NonNullable<E>, A>
44+
45+
export function effectify<E, A, X1>(
46+
fn: (x1: X1, cb: Callback<E, A>) => void
47+
): (x1: X1) => Z.Effect<never, NonNullable<E>, A>
48+
49+
export function effectify<E, A, X1, X2>(
50+
fn: (x1: X1, x2: X2, cb: Callback<E, A>) => void
51+
): (x1: X1, x2: X2) => Z.Effect<never, NonNullable<E>, A>
52+
53+
export function effectify<E, A, X1, X2, X3>(
54+
fn: (x1: X1, x2: X2, x3: X3, cb: Callback<E, A>) => void
55+
): (x1: X1, x2: X2, x3: X3) => Z.Effect<never, NonNullable<E>, A>
56+
57+
export function effectify<E, A, X1, X2, X3, X4>(
58+
fn: (x1: X1, x2: X2, x3: X3, x4: X4, cb: Callback<E, A>) => void
59+
): (x1: X1, x2: X2, x3: X3, x4: X4) => Z.Effect<never, NonNullable<E>, A>
60+
61+
export function effectify<E, A, X1, X2, X3, X4, X5>(
62+
fn: (x1: X1, x2: X2, x3: X3, x4: X4, x5: X5, cb: Callback<E, A>) => void
63+
): (x1: X1, x2: X2, x3: X3, x4: X4, x5: X5) => Z.Effect<never, NonNullable<E>, A>
64+
65+
export function effectify<
66+
X extends Fn,
67+
F extends CustomPromisify<X>,
68+
E2,
69+
Cb = Last<Parameters<F>>,
70+
E1 = Cb extends Function ? NonNullable<Parameters<Cb>[0]> : never
71+
>(
72+
fn: F,
73+
mapError: (e: E1) => E2
74+
): (
75+
...args: F extends CustomPromisify<infer TCustom> ? Parameters<TCustom> : never[]
76+
) => Z.Effect<
77+
never,
78+
E2,
79+
F extends CustomPromisify<infer TCustom> ? UnwrapPromise<ReturnType<TCustom>> : never
80+
>
81+
82+
export function effectify<E1, E2, A>(
83+
fn: (cb: Callback<E1, A>) => void,
84+
mapError: (e: NonNullable<E1>) => E2
85+
): () => Z.Effect<never, E2, A>
86+
87+
export function effectify<E1, E2, A, X1>(
88+
fn: (x1: X1, cb: Callback<E1, A>) => void,
89+
mapError: (e: NonNullable<E1>) => E2
90+
): (x1: X1) => Z.Effect<never, E2, A>
91+
92+
export function effectify<E1, E2, A, X1, X2>(
93+
fn: (x1: X1, x2: X2, cb: Callback<E1, A>) => void,
94+
mapError: (e: NonNullable<E1>) => E2
95+
): (x1: X1, x2: X2) => Z.Effect<never, NonNullable<E2>, A>
96+
97+
export function effectify<E1, E2, A, X1, X2, X3>(
98+
fn: (x1: X1, x2: X2, x3: X3, cb: Callback<E2, A>) => void,
99+
mapError: (e: NonNullable<E1>) => E2
100+
): (x1: X1, x2: X2, x3: X3) => Z.Effect<never, E2, A>
101+
102+
export function effectify<E1, E2, A, X1, X2, X3, X4>(
103+
fn: (x1: X1, x2: X2, x3: X3, x4: X4, cb: Callback<E1, A>) => void,
104+
mapError: (e: NonNullable<E1>) => E2
105+
): (x1: X1, x2: X2, x3: X3, x4: X4) => Z.Effect<never, E2, A>
106+
107+
export function effectify<E1, E2, A, X1, X2, X3, X4, X5>(
108+
fn: (x1: X1, x2: X2, x3: X3, x4: X4, x5: X5, cb: Callback<E1, A>) => void,
109+
mapError: (e: NonNullable<E1>) => E2
110+
): (x1: X1, x2: X2, x3: X3, x4: X4, x5: X5) => Z.Effect<never, E2, A>
111+
112+
/**
113+
* Converts a callback-based async function into an `Effect`
114+
*
115+
* @param fn - the function to convert
116+
* @param mapError - mapping function for the error (defaults to identity)
117+
*/
118+
export function effectify(fn: Function, mapError?: Function) {
119+
return (...args: any[]) =>
120+
Z.async<never, unknown, unknown>((resume) => {
121+
fn(...args, (error: unknown, data: unknown) => {
122+
if (error) {
123+
resume(Z.fail(mapError ? mapError(error) : error))
124+
} else {
125+
resume(Z.succeed(data))
126+
}
127+
})
128+
})
129+
}

test/effectify.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as Effect from "@effect/io/Effect"
2+
import * as Exit from "@effect/io/Exit"
3+
import * as Option from "@fp-ts/data/Option"
4+
import * as Cause from "@effect/io/Cause"
5+
import * as it from "@effect/node/test/utils/extend"
6+
import { assert, describe } from "vitest"
7+
import { effectify } from "@effect/node/effectify"
8+
import { pipe } from "@fp-ts/data/Function"
9+
import fs from "node:fs"
10+
11+
export class TestError {
12+
readonly _tag = "TestError"
13+
constructor(readonly error: NodeJS.ErrnoException) {}
14+
}
15+
16+
export const readFile1 = effectify(fs.readFile)
17+
export const readFile2 = effectify(fs.readFile, (e) => new TestError(e))
18+
19+
describe.concurrent("effectify (readFile)", () => {
20+
it.effect("handles happy path", () =>
21+
Effect.gen(function* ($) {
22+
const x = yield* $(readFile1(__filename))
23+
assert.match(x.toString(), /^import/)
24+
})
25+
)
26+
27+
it.effect("preserves overloads (1)", () =>
28+
Effect.gen(function* ($) {
29+
const x = yield* $(readFile1(__filename, "utf8"))
30+
assert.match(x.toString(), /^import/)
31+
})
32+
)
33+
34+
it.effect("preserves overloads (2)", () =>
35+
Effect.gen(function* ($) {
36+
const { signal } = new AbortController()
37+
const x = yield* $(readFile1(__filename, { signal }))
38+
assert.match(x.toString(), /^import/)
39+
})
40+
)
41+
42+
it.effect("handles error path", () =>
43+
Effect.gen(function* ($) {
44+
const result = yield* $(
45+
pipe(
46+
readFile1(__filename + "!@#%$"),
47+
Effect.exit,
48+
Effect.map(
49+
Exit.match(
50+
(cause) =>
51+
pipe(
52+
cause,
53+
Cause.failureOption,
54+
Option.map((x) => x.code)
55+
),
56+
() => Option.none
57+
)
58+
)
59+
)
60+
)
61+
assert.deepStrictEqual(result, Option.some("ENOENT"))
62+
})
63+
)
64+
65+
it.effect("handles error path (with error mapping)", () =>
66+
Effect.gen(function* ($) {
67+
const result = yield* $(
68+
pipe(
69+
readFile2(__filename + "!@#%$"),
70+
Effect.exit,
71+
Effect.map(
72+
Exit.match(
73+
(cause) =>
74+
pipe(
75+
cause,
76+
Cause.failureOption,
77+
Option.map((x) => ({ tag: x._tag, code: x.error.code }))
78+
),
79+
() => Option.none
80+
)
81+
)
82+
)
83+
)
84+
assert.deepStrictEqual(result, Option.some({ tag: "TestError", code: "ENOENT" }))
85+
})
86+
)
87+
})

test/utils/extend.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import * as Effect from "@effect/io/Effect"
2+
import * as TestEnvironment from "@effect/io/internal/testing/testEnvironment"
3+
import * as Schedule from "@effect/io/Schedule"
4+
import type * as Scope from "@effect/io/Scope"
5+
import * as Duration from "@fp-ts/data/Duration"
6+
import { pipe } from "@fp-ts/data/Function"
7+
import type { TestAPI } from "vitest"
8+
import * as V from "vitest"
9+
10+
export type API = TestAPI<{}>
11+
12+
export const it: API = V.it
13+
14+
export const effect = (() => {
15+
const f = <E, A>(
16+
name: string,
17+
self: () => Effect.Effect<TestEnvironment.TestEnvironment, E, A>,
18+
timeout = 5_000
19+
) => {
20+
return it(
21+
name,
22+
() =>
23+
pipe(
24+
Effect.suspendSucceed(self),
25+
Effect.provideLayer(TestEnvironment.TestEnvironment),
26+
Effect.unsafeRunPromise
27+
),
28+
timeout
29+
)
30+
}
31+
return Object.assign(f, {
32+
skip: <E, A>(
33+
name: string,
34+
self: () => Effect.Effect<TestEnvironment.TestEnvironment, E, A>,
35+
timeout = 5_000
36+
) => {
37+
return it.skip(
38+
name,
39+
() =>
40+
pipe(
41+
Effect.suspendSucceed(self),
42+
Effect.provideLayer(TestEnvironment.TestEnvironment),
43+
Effect.unsafeRunPromise
44+
),
45+
timeout
46+
)
47+
},
48+
only: <E, A>(
49+
name: string,
50+
self: () => Effect.Effect<TestEnvironment.TestEnvironment, E, A>,
51+
timeout = 5_000
52+
) => {
53+
return it.only(
54+
name,
55+
() =>
56+
pipe(
57+
Effect.suspendSucceed(self),
58+
Effect.provideLayer(TestEnvironment.TestEnvironment),
59+
Effect.unsafeRunPromise
60+
),
61+
timeout
62+
)
63+
}
64+
})
65+
})()
66+
67+
export const live = <E, A>(
68+
name: string,
69+
self: () => Effect.Effect<never, E, A>,
70+
timeout = 5_000
71+
) => {
72+
return it(
73+
name,
74+
() => pipe(Effect.suspendSucceed(self), Effect.unsafeRunPromise),
75+
timeout
76+
)
77+
}
78+
79+
export const flakyTest = <R, E, A>(
80+
self: Effect.Effect<R, E, A>,
81+
timeout: Duration.Duration = Duration.seconds(30)
82+
) => {
83+
return pipe(
84+
Effect.resurrect(self),
85+
Effect.retry(
86+
pipe(
87+
Schedule.recurs(10),
88+
Schedule.compose(Schedule.elapsed()),
89+
Schedule.whileOutput(Duration.lessThanOrEqualTo(timeout))
90+
)
91+
),
92+
Effect.orDie
93+
)
94+
}
95+
96+
export const scoped = <E, A>(
97+
name: string,
98+
self: () => Effect.Effect<Scope.Scope | TestEnvironment.TestEnvironment, E, A>,
99+
timeout = 5_000
100+
) => {
101+
return it(
102+
name,
103+
() =>
104+
pipe(
105+
Effect.suspendSucceed(self),
106+
Effect.scoped,
107+
Effect.provideLayer(TestEnvironment.TestEnvironment),
108+
Effect.unsafeRunPromise
109+
),
110+
timeout
111+
)
112+
}
113+
114+
export const scopedLive = <E, A>(
115+
name: string,
116+
self: () => Effect.Effect<Scope.Scope, E, A>,
117+
timeout = 5_000
118+
) => {
119+
return it(
120+
name,
121+
() => pipe(Effect.suspendSucceed(self), Effect.scoped, Effect.unsafeRunPromise),
122+
timeout
123+
)
124+
}

0 commit comments

Comments
 (0)