Skip to content

Commit e810bfa

Browse files
author
Datner
committed
feature: change to stream-based implementation for greated flexibility
1 parent 08b4752 commit e810bfa

File tree

8 files changed

+152
-155
lines changed

8 files changed

+152
-155
lines changed

src/RuntimeProvider.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,39 @@
11
"use client"
22
import { pipe } from "@effect/data/Function"
3-
import * as Option from "@effect/data/Option"
43
import * as Effect from "@effect/io/Effect"
54
import * as Layer from "@effect/io/Layer"
6-
import * as LogLevel from "@effect/io/Logger/Level"
75
import type * as Runtime from "@effect/io/Runtime"
86
import * as Scope from "@effect/io/Scope"
97
import { createContext } from "react"
10-
import type { UseEffectCallback } from "react-effect/hooks/useEffectCallback"
11-
import { makeUseEffectCallback } from "react-effect/hooks/useEffectCallback"
12-
import type { UseEffectResult } from "react-effect/hooks/useEffectResult"
13-
import { makeUseEffectResult } from "react-effect/hooks/useEffectResult"
8+
import type { UseResult } from "react-effect/hooks/useResult"
9+
import { makeUseResult } from "react-effect/hooks/useResult"
10+
import type { UseResultCallback } from "react-effect/hooks/useResultCallback"
11+
import { makeUseResultCallback } from "react-effect/hooks/useResultCallback"
1412

1513
export { RuntimeContext } from "react-effect/internal/runtimeContext"
1614

17-
export interface ReactEffectBag<R, D> {
18-
readonly RuntimeContext: React.Context<Effect.Effect<never, D, Runtime.Runtime<R>>>
19-
readonly useEffectCallback: UseEffectCallback<R, D>
20-
readonly useEffectResult: UseEffectResult<R, D>
15+
export interface ReactEffectBag<R> {
16+
readonly RuntimeContext: React.Context<Runtime.Runtime<R>>
17+
readonly useResultCallback: UseResultCallback<R>
18+
readonly useResult: UseResult<R>
2119
}
2220

2321
export const makeFromLayer = <R, E>(
2422
layer: Layer.Layer<never, E, R>
25-
): ReactEffectBag<R, E> => {
23+
): ReactEffectBag<R> => {
2624
const scope = Effect.runSync(Scope.make())
2725

2826
const runtime = pipe(
2927
Layer.toRuntime(layer),
3028
Effect.provideService(Scope.Scope, scope),
31-
Effect.withUnhandledErrorLogLevel(Option.some(LogLevel.Fatal)),
32-
Effect.cached,
3329
Effect.runSync
3430
)
3531

3632
const RuntimeContext = createContext(runtime)
3733

3834
return {
3935
RuntimeContext,
40-
useEffectCallback: makeUseEffectCallback(RuntimeContext),
41-
useEffectResult: makeUseEffectResult(RuntimeContext)
36+
useResultCallback: makeUseResultCallback(RuntimeContext),
37+
useResult: makeUseResult(RuntimeContext)
4238
}
4339
}

src/hooks/useEffectCallback.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

src/hooks/useEffectResult.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/hooks/useResult.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as Effect from "@effect/io/Effect"
2+
import * as Fiber from "@effect/io/Fiber"
3+
import * as Runtime from "@effect/io/Runtime"
4+
import type * as Schedule from "@effect/io/Schedule"
5+
import * as Stream from "@effect/stream/Stream"
6+
import { useContext, useRef, useState } from "react"
7+
import type { ResultBag } from "react-effect/hooks/useResultBag"
8+
import { updateNext, useResultBag } from "react-effect/hooks/useResultBag"
9+
import type { RuntimeContext } from "react-effect/internal/runtimeContext"
10+
import * as Result from "react-effect/Result"
11+
12+
export type UseResult<R> = <R0 extends R, E, A>(
13+
effect: Effect.Effect<R0, E, A>
14+
) => ResultBag<E, A>
15+
16+
export const makeUseResult: <R>(
17+
runtimeContext: RuntimeContext<R>
18+
) => UseResult<R> = <R>(runtimeContext: RuntimeContext<R>) =>
19+
<R0 extends R, E, A>(stream: Stream.Stream<R0, E, A>) => {
20+
const runtime = useContext(runtimeContext)
21+
const prevRef = useRef<Stream.Stream<R0, E, A>>()
22+
const fiberRef = useRef<Fiber.RuntimeFiber<E, void>>()
23+
const [result, setResult] = useState<Result.Result<E, A>>(Result.waiting(Result.initial()))
24+
const [trackRef, resultBag] = useResultBag(result)
25+
26+
if (prevRef.current !== stream) {
27+
prevRef.current = stream
28+
if (fiberRef.current) {
29+
Effect.runSync(Fiber.interruptFork(fiberRef.current))
30+
}
31+
fiberRef.current = stream.pipe(
32+
Stream.tap((value) =>
33+
Effect.sync(() => {
34+
setResult(updateNext(Result.success(value), trackRef))
35+
})
36+
),
37+
Stream.tapErrorCause((cause) =>
38+
Effect.sync(() => {
39+
setResult(updateNext(Result.failCause(cause), trackRef))
40+
})
41+
),
42+
Stream.runDrain,
43+
Runtime.runFork(runtime)
44+
)
45+
}
46+
47+
trackRef.current.currentStatus = result._tag
48+
49+
return resultBag
50+
}

src/hooks/useResultBag.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import type { MutableRefObject } from "react"
77
import { useMemo, useRef } from "react"
88
import * as Result from "react-effect/Result"
99

10-
export interface ResultBag<D, E, A> {
11-
readonly result: Result.Result<D, E, A>
10+
export interface ResultBag<E, A> {
11+
readonly result: Result.Result<E, A>
1212
readonly dataUpdatedAt: Option.Option<Date>
1313
readonly errorUpdatedAt: Option.Option<Date>
1414
readonly errorRunningCount: number
1515
readonly failureCount: number
16-
readonly failureCause: Cause.Cause<D | E>
16+
readonly failureCause: Cause.Cause<E>
1717
readonly isError: boolean
1818
readonly isRefreshing: boolean
1919
readonly isRetrying: boolean
@@ -36,7 +36,7 @@ export interface TrackedProperties {
3636
runningErrorCount: number
3737
invocationCount: number
3838
interruptCount: number
39-
currentStatus: Result.Result<any, any, any>["_tag"]
39+
currentStatus: Result.Result<any, any>["_tag"]
4040
failureCause: Cause.Cause<unknown>
4141
}
4242

@@ -60,28 +60,31 @@ const optionDateGreaterThan = pipe(
6060
Order.greaterThan
6161
)
6262

63-
export const updateNext = <D, E, A>(
64-
next: Result.Result<D, E, A>,
63+
export const updateNext = <E, A>(
64+
next: Result.Result<E, A>,
6565
ref: MutableRefObject<TrackedProperties>
66-
): Result.Result<D, E, A> => {
66+
): Result.Result<E, A> => {
6767
switch (next._tag) {
6868
case "Initial": {
6969
break
7070
}
7171
case "Waiting": {
7272
break
7373
}
74-
case "Defect": {
75-
ref.current.currentDefectCount++
76-
ref.current.currentErrorCount++
77-
ref.current.runningErrorCount++
78-
break
79-
}
80-
case "Fail": {
81-
ref.current.currentFailureCount++
82-
ref.current.currentErrorCount++
83-
ref.current.runningErrorCount++
84-
ref.current.errorUpdatedAt = Option.some(new Date())
74+
case "Failure": {
75+
if (Cause.isFailure(next.cause)) {
76+
ref.current.currentFailureCount++
77+
ref.current.currentErrorCount++
78+
ref.current.runningErrorCount++
79+
ref.current.errorUpdatedAt = Option.some(new Date())
80+
break
81+
}
82+
if (!Cause.isInterruptedOnly(next.cause)) {
83+
ref.current.currentDefectCount++
84+
ref.current.currentErrorCount++
85+
ref.current.runningErrorCount++
86+
}
87+
8588
break
8689
}
8790
case "Success": {
@@ -94,10 +97,10 @@ export const updateNext = <D, E, A>(
9497
return next
9598
}
9699

97-
export const useResultBag = <D, E, A>(result: Result.Result<D, E, A>) => {
100+
export const useResultBag = <E, A>(result: Result.Result<E, A>) => {
98101
const trackedPropsRef = useRef<TrackedProperties>(initial)
99102

100-
const resultBag = useMemo((): ResultBag<D, E, A> => ({
103+
const resultBag = useMemo((): ResultBag<E, A> => ({
101104
result,
102105
get isLoading() {
103106
return Result.isLoading(result)
@@ -131,7 +134,7 @@ export const useResultBag = <D, E, A>(result: Result.Result<D, E, A>) => {
131134
return trackedPropsRef.current.currentFailureCount
132135
},
133136
get failureCause() {
134-
return trackedPropsRef.current.failureCause as Cause.Cause<D | E>
137+
return trackedPropsRef.current.failureCause as Cause.Cause<E>
135138
},
136139
get errorRunningCount() {
137140
return trackedPropsRef.current.runningErrorCount

src/hooks/useResultCallback.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as Effect from "@effect/io/Effect"
2+
import * as Fiber from "@effect/io/Fiber"
3+
import * as Queue from "@effect/io/Queue"
4+
import * as Runtime from "@effect/io/Runtime"
5+
import * as Stream from "@effect/stream/Stream"
6+
import { useCallback, useContext, useRef, useState } from "react"
7+
import type { ResultBag } from "react-effect/hooks/useResultBag"
8+
import { updateNext, useResultBag } from "react-effect/hooks/useResultBag"
9+
import type { RuntimeContext } from "react-effect/internal/runtimeContext"
10+
import * as Result from "react-effect/Result"
11+
12+
export type UseResultCallback<R> = <Args extends Array<any>, R0 extends R, E, A>(
13+
callback: (...args: Args) => Effect.Effect<R0, E, A>
14+
) => readonly [ResultBag<E, A>, (...args: Args) => void, Queue.Queue<Args>]
15+
16+
export const makeUseResultCallback: <R>(
17+
runtimeContext: RuntimeContext<R>
18+
) => UseResultCallback<R> = <R>(
19+
runtimeContext: RuntimeContext<R>
20+
) =>
21+
<Args extends Array<any>, R0 extends R, E, A>(f: (...args: Args) => Stream.Stream<R0, E, A>) => {
22+
const runtime = useContext(runtimeContext)
23+
const fiberRef = useRef<Fiber.RuntimeFiber<E, void>>()
24+
const prevRef = useRef<typeof f>()
25+
const queueRef = useRef<Queue.Queue<Args>>()
26+
if (!queueRef.current) {
27+
queueRef.current = Effect.runSync(Queue.unbounded())
28+
}
29+
const [result, setResult] = useState<Result.Result<E, A>>(Result.initial())
30+
const [trackRef, resultBag] = useResultBag(result)
31+
32+
if (prevRef.current !== f) {
33+
prevRef.current = f
34+
if (fiberRef.current) {
35+
Effect.runSync(Fiber.interruptFork(fiberRef.current))
36+
}
37+
fiberRef.current = Stream.fromQueue(queueRef.current).pipe(
38+
Stream.tap(() =>
39+
Effect.sync(() => {
40+
setResult((prev) => updateNext(Result.waiting(prev), trackRef))
41+
})
42+
),
43+
Stream.flatMap((args) => f(...args)),
44+
Stream.tap((value) =>
45+
Effect.sync(() => {
46+
setResult(updateNext(Result.success(value), trackRef))
47+
})
48+
),
49+
Stream.tapErrorCause((cause) =>
50+
Effect.sync(() => {
51+
setResult(updateNext(Result.failCause(cause), trackRef))
52+
})
53+
),
54+
Stream.runDrain,
55+
Runtime.runFork(runtime)
56+
)
57+
}
58+
trackRef.current.currentStatus = result._tag
59+
60+
const run = useCallback((...args: Args) => {
61+
trackRef.current.invocationCount++
62+
queueRef.current!.unsafeOffer(args)
63+
}, [])
64+
65+
return [resultBag, run, queueRef.current!] as const
66+
}

src/hooks/useStitch.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/internal/runtimeContext.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import type * as Effect from "@effect/io/Effect"
21
import type * as Runtime from "@effect/io/Runtime"
32

43
/* @internal */
5-
export type RuntimeContext<R, E> = React.Context<
6-
Effect.Effect<never, E, Runtime.Runtime<R>>
7-
>
4+
export type RuntimeContext<R> = React.Context<Runtime.Runtime<R>>

0 commit comments

Comments
 (0)