0.1.10 (#13)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/13
This commit was merged in pull request #13.
This commit is contained in:
@@ -11,41 +11,41 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@tanstack/react-router": "^1.117.1",
|
||||
"@tanstack/react-router-devtools": "^1.117.1",
|
||||
"@tanstack/router-plugin": "^1.117.2",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@tanstack/react-router": "^1.120.2",
|
||||
"@tanstack/react-router-devtools": "^1.120.2",
|
||||
"@tanstack/router-plugin": "^1.120.2",
|
||||
"@thilawyn/thilaschema": "^0.1.4",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react": "^19.1.3",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.0.0",
|
||||
"globals": "^16.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript-eslint": "^8.31.0",
|
||||
"vite": "^6.3.3"
|
||||
"typescript-eslint": "^8.32.0",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.80.14",
|
||||
"@effect/platform-browser": "^0.60.5",
|
||||
"@effect/platform": "^0.80.21",
|
||||
"@effect/platform-browser": "^0.60.12",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reffuse/extension-lazyref": "workspace:*",
|
||||
"@reffuse/extension-query": "workspace:*",
|
||||
"@typed/async-data": "^0.13.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.14.14",
|
||||
"lucide-react": "^0.503.0",
|
||||
"effect": "^3.14.21",
|
||||
"lucide-react": "^0.508.0",
|
||||
"mobx": "^6.13.7",
|
||||
"reffuse": "workspace:*"
|
||||
},
|
||||
"overrides": {
|
||||
"effect": "^3.14.14",
|
||||
"@effect/platform": "^0.80.14",
|
||||
"@effect/platform-browser": "^0.60.5",
|
||||
"effect": "^3.14.21",
|
||||
"@effect/platform": "^0.80.21",
|
||||
"@effect/platform-browser": "^0.60.12",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"@typed/async-data": "^0.13.1"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
import { AppQueryErrorHandler } from "./query"
|
||||
import { AppQueryClient } from "./query"
|
||||
import { R } from "./reffuse"
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ export function VQueryErrorHandler() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const error = R.useSubscribeStream(
|
||||
R.useMemo(() => AppQueryErrorHandler.pipe(
|
||||
Effect.map(handler => handler.errors.pipe(
|
||||
R.useMemo(() => AppQueryClient.pipe(
|
||||
Effect.map(client => client.errorHandler.errors.pipe(
|
||||
Stream.changes,
|
||||
Stream.tap(Console.error),
|
||||
Stream.tap(() => Effect.sync(() => setOpen(true))),
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||
import { QueryExtension } from "@reffuse/extension-query"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||
import { AppQueryClient } from "./query"
|
||||
|
||||
|
||||
export const RootContext = ReffuseContext.make<
|
||||
| AppQueryClient
|
||||
| AppQueryErrorHandler
|
||||
| Clipboard.Clipboard
|
||||
| Geolocation.Geolocation
|
||||
| Permissions.Permissions
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Route as LazyrefImport } from './routes/lazyref'
|
||||
import { Route as CountImport } from './routes/count'
|
||||
import { Route as BlankImport } from './routes/blank'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as StreamsPullImport } from './routes/streams/pull'
|
||||
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||
import { Route as QueryServiceImport } from './routes/query/service'
|
||||
@@ -73,6 +74,12 @@ const IndexRoute = IndexImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const StreamsPullRoute = StreamsPullImport.update({
|
||||
id: '/streams/pull',
|
||||
path: '/streams/pull',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||
id: '/query/usequery',
|
||||
path: '/query/usequery',
|
||||
@@ -172,6 +179,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof QueryUsequeryImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/streams/pull': {
|
||||
id: '/streams/pull'
|
||||
path: '/streams/pull'
|
||||
fullPath: '/streams/pull'
|
||||
preLoaderRoute: typeof StreamsPullImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +203,7 @@ export interface FileRoutesByFullPath {
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
@@ -203,6 +218,7 @@ export interface FileRoutesByTo {
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
@@ -218,6 +234,7 @@ export interface FileRoutesById {
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
@@ -234,6 +251,7 @@ export interface FileRouteTypes {
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -247,6 +265,7 @@ export interface FileRouteTypes {
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -260,6 +279,7 @@ export interface FileRouteTypes {
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
@@ -275,6 +295,7 @@ export interface RootRouteChildren {
|
||||
QueryServiceRoute: typeof QueryServiceRoute
|
||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||
StreamsPullRoute: typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
@@ -289,6 +310,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
QueryServiceRoute: QueryServiceRoute,
|
||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||
StreamsPullRoute: StreamsPullRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
@@ -311,7 +333,8 @@ export const routeTree = rootRoute
|
||||
"/todos",
|
||||
"/query/service",
|
||||
"/query/usemutation",
|
||||
"/query/usequery"
|
||||
"/query/usequery",
|
||||
"/streams/pull"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
@@ -346,6 +369,9 @@ export const routeTree = rootRoute
|
||||
},
|
||||
"/query/usequery": {
|
||||
"filePath": "query/usequery.tsx"
|
||||
},
|
||||
"/streams/pull": {
|
||||
"filePath": "streams/pull.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Ref } from "effect"
|
||||
import { Effect, Ref } from "effect"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/count")({
|
||||
@@ -11,14 +11,13 @@ function Count() {
|
||||
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
const countRef = R.useRef(0)
|
||||
const [count] = R.useRefState(countRef)
|
||||
const countRef = R.useRef(() => Effect.succeed(0))
|
||||
const [count] = R.useSubscribeRefs(countRef)
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
||||
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
|
||||
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
|
||||
count is {count}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/query/service")({
|
||||
|
||||
function RouteComponent() {
|
||||
const query = R.useQuery({
|
||||
key: R.useStreamFromValues(["uuid4", 10 as number]),
|
||||
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
|
||||
@@ -20,7 +20,7 @@ function RouteComponent() {
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const query = R.useQuery({
|
||||
key: R.useStreamFromValues(["uuid4", count]),
|
||||
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
|
||||
34
packages/example/src/routes/streams/pull.tsx
Normal file
34
packages/example/src/routes/streams/pull.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Chunk, Effect, Exit, Option, Queue, Random, Scope, Stream } from "effect"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/streams/pull")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const stream = useMemo(() => Stream.repeatEffect(Random.nextInt), [])
|
||||
const streamScope = R.useScope([stream], { finalizerExecutionMode: "fork" })
|
||||
|
||||
const queue = R.useMemo(() => Effect.provideService(Stream.toQueueOfElements(stream), Scope.Scope, streamScope), [streamScope])
|
||||
|
||||
const [value, setValue] = useState(Option.none<number>())
|
||||
const pullLatest = R.useCallbackSync(() => Queue.takeAll(queue).pipe(
|
||||
Effect.flatMap(Chunk.last),
|
||||
Effect.flatMap(Exit.matchEffect({
|
||||
onSuccess: Effect.succeed,
|
||||
onFailure: Effect.fail,
|
||||
})),
|
||||
Effect.tap(v => Effect.sync(() => setValue(Option.some(v)))),
|
||||
), [queue])
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
{Option.isSome(value) && <Text>{value.value}</Text>}
|
||||
<Button onClick={pullLatest}>Pull latest</Button>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,11 @@ import { R } from "@/reffuse"
|
||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Console, Effect, Ref } from "effect"
|
||||
import { Console, Effect, Option, Scope } from "effect"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
|
||||
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
|
||||
|
||||
|
||||
export const Route = createFileRoute("/tests")({
|
||||
@@ -10,48 +14,34 @@ export const Route = createFileRoute("/tests")({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const deepRef = R.useRef({ value: "poulet" })
|
||||
const deepValueRef = R.useSubRef(deepRef, ["value"])
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||
// Effect.andThen(makeUuid4),
|
||||
// Effect.provide(GetRandomValues.CryptoRandom),
|
||||
// ), [])
|
||||
// console.log(value)
|
||||
|
||||
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||
Effect.andThen(Console.log("ouient")),
|
||||
Effect.delay("1 second"),
|
||||
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
|
||||
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
|
||||
Effect.tap(v => Effect.sync(() => setUuid(v)))
|
||||
), [])
|
||||
|
||||
const uuidStream = R.useStreamFromReactiveValues([uuid])
|
||||
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
|
||||
|
||||
const uuidRef = R.useRef("none")
|
||||
const anotherRef = R.useRef(69)
|
||||
|
||||
|
||||
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
|
||||
yield* Effect.log(value)
|
||||
}), [])
|
||||
|
||||
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
Effect.tap(v => Ref.set(uuidRef, v)),
|
||||
Effect.tap(v => Ref.set(deepValueRef, v)),
|
||||
), [])
|
||||
const scope = R.useScope([uuid])
|
||||
|
||||
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
|
||||
Effect.andThen(Console.log("Scope changed")),
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
runSync,
|
||||
), [scope, runSync])
|
||||
|
||||
return (
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
|
||||
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
|
||||
</R.SubscribeRefs>
|
||||
|
||||
<R.SubscribeRefs refs={[deepRef, deepValueRef]}>
|
||||
{(deep, deepValue) => <Text>{JSON.stringify(deep)} / {deepValue}</Text>}
|
||||
</R.SubscribeRefs>
|
||||
|
||||
<Button onClick={() => logValue("test")}>Log value</Button>
|
||||
<Button onClick={() => generateUuid()}>Generate UUID</Button>
|
||||
<Flex direction="column" justify="center" align="center" gap="2">
|
||||
<Text>{uuid}</Text>
|
||||
<Button onClick={generateUuid}>Generate UUID</Button>
|
||||
<Text>
|
||||
{Option.match(uuidStreamLatestValue, {
|
||||
onSome: ([v]) => v,
|
||||
onNone: () => <></>,
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function VTodos() {
|
||||
), [])
|
||||
|
||||
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
|
||||
const [todos] = R.useRefState(todosRef)
|
||||
const [todos] = R.useSubscribeRefs(todosRef)
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect"
|
||||
import { Cause, Context, Effect, identity, Layer, PubSub, Stream } from "effect"
|
||||
import type { Mutable } from "effect/Types"
|
||||
|
||||
|
||||
@@ -36,17 +36,17 @@ export const Service = <Self, HandledE = never>() => (
|
||||
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, FallbackA, HandledE>
|
||||
|
||||
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.gen(function*() {
|
||||
const queue = yield* Queue.unbounded<Cause.Cause<HandledE>>()
|
||||
const errors = Stream.fromQueue(queue)
|
||||
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
|
||||
const errors = Stream.fromPubSub(pubsub)
|
||||
|
||||
const handle = <A, E, R>(
|
||||
self: Effect.Effect<A, E, R>
|
||||
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
||||
self as unknown as Effect.Effect<never, HandledE, never>,
|
||||
(failure: HandledE) => Queue.offer(queue, Cause.fail(failure)).pipe(
|
||||
(failure: HandledE) => PubSub.publish(pubsub, Cause.fail(failure)).pipe(
|
||||
Effect.andThen(Effect.failCause(Cause.empty))
|
||||
),
|
||||
(defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe(
|
||||
(defect: unknown) => PubSub.publish(pubsub, Cause.die(defect)).pipe(
|
||||
Effect.andThen(Effect.failCause(Cause.empty))
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reffuse",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import * as ReffuseContext from "./ReffuseContext.js"
|
||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||
@@ -14,6 +14,10 @@ export interface ScopeOptions {
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
}
|
||||
|
||||
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
}
|
||||
|
||||
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
|
||||
[K in keyof T]: Effect.Effect.Success<T[K]>
|
||||
}
|
||||
@@ -88,6 +92,55 @@ export abstract class ReffuseNamespace<R> {
|
||||
), [runtime, context])
|
||||
}
|
||||
|
||||
useScope<R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
deps: React.DependencyList = [],
|
||||
options?: UseScopeOptions,
|
||||
): Scope.Scope {
|
||||
const runSync = this.useRunSync()
|
||||
const runFork = this.useRunFork()
|
||||
|
||||
const [isInitialRun, initialScope] = React.useMemo(() => runSync(Effect.all([
|
||||
Ref.make(true),
|
||||
Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||
])), [])
|
||||
|
||||
const [scope, setScope] = React.useState(initialScope)
|
||||
|
||||
React.useEffect(() => isInitialRun.pipe(
|
||||
Effect.if({
|
||||
onTrue: () => Effect.as(
|
||||
Ref.set(isInitialRun, false),
|
||||
() => Scope.close(initialScope, Exit.void).pipe(
|
||||
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
|
||||
Match.when("sync", () => { runSync(effect) }),
|
||||
Match.when("fork", () => { runFork(effect) }),
|
||||
Match.exhaustive,
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
onFalse: () => Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||
Effect.tap(v => Effect.sync(() => setScope(v))),
|
||||
Effect.map(v => () => Scope.close(v, Exit.void).pipe(
|
||||
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
|
||||
Match.when("sync", () => { runSync(effect) }),
|
||||
Match.when("fork", () => { runFork(effect) }),
|
||||
Match.exhaustive,
|
||||
)
|
||||
)),
|
||||
),
|
||||
}),
|
||||
|
||||
runSync,
|
||||
), [
|
||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
|
||||
...deps,
|
||||
])
|
||||
|
||||
return scope
|
||||
}
|
||||
|
||||
/**
|
||||
* Reffuse equivalent to `React.useMemo`.
|
||||
*
|
||||
@@ -111,53 +164,6 @@ export abstract class ReffuseNamespace<R> {
|
||||
])
|
||||
}
|
||||
|
||||
useMemoScoped<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||
deps: React.DependencyList,
|
||||
options?: RenderOptions & ScopeOptions,
|
||||
): A {
|
||||
const runSync = this.useRunSync()
|
||||
|
||||
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
||||
Effect.bind("isInitialRun", () => Ref.make(true)),
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
|
||||
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
|
||||
Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const),
|
||||
runSync,
|
||||
), [])
|
||||
|
||||
const [value, setValue] = React.useState(initialValue)
|
||||
|
||||
React.useEffect(() => isInitialRun.pipe(
|
||||
Effect.if({
|
||||
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
||||
Effect.map(() =>
|
||||
() => runSync(Scope.close(initialScope, Exit.void))
|
||||
)
|
||||
),
|
||||
|
||||
onFalse: () => Effect.Do.pipe(
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
|
||||
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
|
||||
Effect.tap(({ value }) =>
|
||||
Effect.sync(() => setValue(value))
|
||||
),
|
||||
Effect.map(({ scope }) =>
|
||||
() => runSync(Scope.close(scope, Exit.void))
|
||||
),
|
||||
),
|
||||
}),
|
||||
|
||||
runSync,
|
||||
), [
|
||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||
...deps,
|
||||
])
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Reffuse equivalent to `React.useEffect`.
|
||||
*
|
||||
@@ -378,17 +384,26 @@ export abstract class ReffuseNamespace<R> {
|
||||
])
|
||||
}
|
||||
|
||||
useRef<A, R>(
|
||||
useRef<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
value: A,
|
||||
initialValue: () => Effect.Effect<A, E, R>,
|
||||
): SubscriptionRef.SubscriptionRef<A> {
|
||||
return this.useMemo(
|
||||
() => SubscriptionRef.make(value),
|
||||
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
||||
)
|
||||
}
|
||||
|
||||
useRefFromReactiveValue<A, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
value: A,
|
||||
): SubscriptionRef.SubscriptionRef<A> {
|
||||
const ref = this.useRef(() => Effect.succeed(value))
|
||||
this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||
return ref
|
||||
}
|
||||
|
||||
useSubRef<B, const P extends PropertyPath.Paths<B>, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
@@ -455,32 +470,61 @@ export abstract class ReffuseNamespace<R> {
|
||||
return [reactStateValue, setValue]
|
||||
}
|
||||
|
||||
useStreamFromValues<const A extends React.DependencyList, R>(
|
||||
useStreamFromReactiveValues<const A extends React.DependencyList, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
values: A,
|
||||
): Stream.Stream<A> {
|
||||
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
|
||||
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
|
||||
), [])
|
||||
const scope = this.useScope()
|
||||
|
||||
this.useEffect(() => Queue.offer(queue, values), values)
|
||||
const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
|
||||
Effect.bind("latest", () => Ref.make(values)),
|
||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||
Effect.let("stream", ({ latest, pubsub }) => Ref.get(latest).pipe(
|
||||
Effect.flatMap(a => Effect.map(
|
||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||
s => Stream.concat(Stream.make(a), s),
|
||||
)),
|
||||
Stream.unwrapScoped,
|
||||
)),
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
), [scope], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||
|
||||
this.useEffect(() => Ref.set(latest, values).pipe(
|
||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||
), values, { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
useSubscribeStream<A, InitialA extends A | undefined, E, R>(
|
||||
useSubscribeStream<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue?: InitialA,
|
||||
): InitialA extends A ? Option.Some<A> : Option.Option<A> {
|
||||
const [reactStateValue, setReactStateValue] = React.useState<Option.Option<A>>(Option.fromNullable(initialValue))
|
||||
): Option.Option<A>
|
||||
useSubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue: () => Effect.Effect<A, IE, R>,
|
||||
): Option.Some<A>
|
||||
useSubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue?: () => Effect.Effect<A, IE, R>,
|
||||
): Option.Option<A> {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||
() => initialValue
|
||||
? Effect.map(initialValue(), Option.some)
|
||||
: Effect.succeed(Option.none()),
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||
))
|
||||
|
||||
this.useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(stream, (x, y) => x === y),
|
||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||
), [stream])
|
||||
|
||||
return reactStateValue as InitialA extends A ? Option.Some<A> : Option.Option<A>
|
||||
return reactStateValue
|
||||
}
|
||||
|
||||
|
||||
@@ -518,15 +562,30 @@ export abstract class ReffuseNamespace<R> {
|
||||
return props.children(this.useRefState(props.ref))
|
||||
}
|
||||
|
||||
SubscribeStream<A, InitialA extends A | undefined, E, R>(
|
||||
SubscribeStream<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly stream: Stream.Stream<A, E, R>
|
||||
readonly initialValue?: InitialA
|
||||
readonly children: (latestValue: InitialA extends A ? Option.Some<A> : Option.Option<A>) => React.ReactNode
|
||||
readonly children: (latestValue: Option.Option<A>) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode
|
||||
SubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly stream: Stream.Stream<A, E, R>
|
||||
readonly initialValue: () => Effect.Effect<A, IE, R>
|
||||
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode
|
||||
SubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly stream: Stream.Stream<A, E, R>
|
||||
readonly initialValue?: () => Effect.Effect<A, IE, R>
|
||||
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode {
|
||||
return props.children(this.useSubscribeStream(props.stream, props.initialValue))
|
||||
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user