Created
April 23, 2023 20:14
-
-
Save mattiamanzati/0d5c7170ce2fd88a730f46226359e53a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as ENV from "@effect/data/Context" | |
import { Tag } from "@effect/data/Context" | |
import * as DU from "@effect/data/Duration" | |
import { pipe } from "@effect/data/Function" | |
import * as O from "@effect/data/Option" | |
import * as D from "@effect/io/Deferred" | |
import * as T from "@effect/io/Effect" | |
import type * as EX from "@effect/io/Exit" | |
import * as FID from "@effect/io/Fiber/Id" | |
import * as HUB from "@effect/io/Hub" | |
import * as Q from "@effect/io/Queue" | |
import * as RU from "@effect/io/Runtime" | |
import * as S from "@effect/stream/Stream" | |
import * as C from "@zuffellato/fw/Case" | |
import * as React from "react" | |
import * as RN from "react-native" | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires | |
const { unstable_batchedUpdates } = | |
RN.Platform.OS === "web" ? require("react-dom") : require("react-native") | |
export const RuntimeContext = React.createContext<() => RU.Runtime<never>>(() => { | |
throw new Error("Missing top level UI RuntimeProvider!") | |
}) | |
const unitEffect = T.unit() | |
export function effectOrNoop<R, E>( | |
effect?: T.Effect<R, E, void> | |
): T.Effect<R, E, void> { | |
return effect || unitEffect | |
} | |
export function RuntimeProvider(props: React.PropsWithChildren<{}>) { | |
const [runtime, setRuntime] = React.useState<O.Option<RU.Runtime<never>>>(O.none()) | |
React.useEffect(() => { | |
const cancel = T.runCallback( | |
pipe( | |
T.runtime<never>(), | |
T.flatMap((value) => T.sync(() => setRuntime(O.some(value)))), | |
T.zipRight(T.never()) | |
), | |
() => undefined | |
) | |
return () => cancel(FID.none, () => undefined) | |
}, []) | |
const value = React.useCallback(() => O.getOrUndefined(runtime)!, [runtime]) | |
if (O.isNone(runtime)) return null | |
return React.createElement(RuntimeContext.Provider, { value }, props.children) | |
} | |
export const RetryPolicyContext = React.createContext(T.orDie) | |
export function useEffect<R, E, A>(eff: T.Effect<R, E, A>) { | |
const retryPolicy = React.useContext(RetryPolicyContext) | |
const runtime = React.useContext(RuntimeContext) | |
return React.useCallback( | |
(args: ENV.Context<R>, cb?: (ex: EX.Exit<E, A>) => void) => { | |
const cancel = RU.runCallback(runtime())( | |
pipe(retryPolicy(eff), T.provideContext(args)), | |
cb ? cb : () => undefined | |
) | |
return () => cancel(FID.none, () => undefined) | |
}, | |
[eff, retryPolicy] | |
) | |
} | |
const _countReducer = (state: number, action: 1 | -1) => state + action | |
export function useEffectSingleton<R, E, A>(eff: T.Effect<R, E, A>) { | |
const [state, dispatch] = React.useReducer(_countReducer, 0) | |
const _run = useEffect(eff) | |
const run = React.useCallback( | |
(args: ENV.Context<R>, cb?: (ex: EX.Exit<E, A>) => void) => { | |
dispatch(1) | |
return _run(args, (exit) => { | |
if (cb) cb(exit) | |
dispatch(-1) | |
}) | |
}, | |
[_run] | |
) | |
return [run, state > 0] as const | |
} | |
const _reducer = <A>(_: O.Option<A>, action: A) => O.some(action) | |
export function useStreamLatestValue_old<A>( | |
stream: S.Stream<never, never, A> | |
): O.Option<A> { | |
const [state, dispatch] = React.useReducer(_reducer, O.none()) | |
const eff = React.useMemo( | |
() => | |
pipe( | |
stream, | |
S.changesWith((a, b) => a === b || JSON.stringify(a) === JSON.stringify(b)), | |
S.map(dispatch), | |
S.runDrain | |
), | |
[stream] | |
) | |
const runInterrupt = useEffect(eff) | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
// eslint-disable-next-line @typescript-eslint/no-empty-function | |
React.useEffect(() => runInterrupt(ENV.empty(), () => {}), [eff, runInterrupt]) | |
return state as O.Option<A> | |
} | |
type BatchedStream = S.Stream<never, never, () => void> | |
const StreamRunnerContext = React.createContext(T.runSync(Q.unbounded<BatchedStream>())) | |
let pending: (() => void)[] = [] | |
export function StreamRunnerProvider(props: React.PropsWithChildren<{}>) { | |
const queue = React.useContext(StreamRunnerContext) | |
const eff = React.useMemo( | |
() => | |
pipe( | |
S.fromQueue(queue, 100), | |
S.flattenParUnbounded, | |
S.map((fn) => pending.push(fn)), | |
S.debounce(DU.millis(0)), | |
S.mapEffect(() => | |
T.sync(() => { | |
unstable_batchedUpdates(() => { | |
console.log("items", pending.length) | |
pending.forEach((_) => _()) | |
pending = [] | |
}) | |
}) | |
), | |
S.runDrain | |
), | |
[] | |
) | |
const runInterrupt = useEffect(eff) | |
// eslint-disable-next-line @typescript-eslint/no-unused-vars | |
// eslint-disable-next-line @typescript-eslint/no-empty-function | |
React.useEffect(() => runInterrupt(ENV.empty(), () => {}), [eff, runInterrupt]) | |
return React.createElement(React.Fragment, { children: props.children }) | |
} | |
export function useStreamLatestValue<A>( | |
stream: S.Stream<never, never, A> | |
): O.Option<A> { | |
const [state, dispatch] = React.useReducer(_reducer, O.none()) | |
const queue = React.useContext(StreamRunnerContext) | |
React.useEffect(() => { | |
const def = T.runSync(D.make<never, void>()) | |
const eff = pipe( | |
stream, | |
S.changesWith((a, b) => a === b || JSON.stringify(a) === JSON.stringify(b)), | |
S.map((v) => () => dispatch(v)), | |
S.interruptWhen(D.await(def)) | |
) | |
T.runSync(Q.offer(queue, eff)) | |
return () => T.runSync(T.asUnit(D.interrupt(def))) | |
}, [stream]) | |
return state as O.Option<A> | |
} | |
export class RefetchAllEvent extends C.Tagged("RefetchAllEvent")<{}> {} | |
export type Event = RefetchAllEvent | |
export interface EventHub extends HUB.Hub<Event> {} | |
export const EventHub = Tag<EventHub>() | |
export const EventHubContext = React.createContext<EventHub>(T.runSync(HUB.unbounded())) | |
export function EventHubProvider(props: React.PropsWithChildren<{}>) { | |
const hub = React.useMemo(() => T.runSync(HUB.unbounded<Event>()), []) | |
return React.createElement(EventHubContext.Provider, { value: hub }, props.children) | |
} | |
export function invalidate<R, E, A>( | |
eff: T.Effect<R, E, A> | |
): T.Effect<R | EventHub, E, A> { | |
return pipe( | |
eff, | |
T.zipLeft( | |
T.flatMap(EventHub, (eventHub) => eventHub.publish(new RefetchAllEvent())) | |
) | |
) | |
} | |
export function useQuery<R, E, A>( | |
fn: () => T.Effect<R, E, A>, | |
deps: any[] | |
): S.Stream<R, E, A> { | |
const eff = React.useMemo(fn, deps) | |
const eventHub = React.useContext(EventHubContext) | |
return React.useMemo( | |
() => | |
pipe( | |
S.succeed(new RefetchAllEvent()), | |
S.concat(S.fromHub(eventHub)), | |
S.flatMapParSwitch(1, () => S.fromEffect(eff)) | |
), | |
[eff, eventHub] | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment