0.1.11 (#14)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 13s

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/14
This commit was merged in pull request #14.
This commit is contained in:
Julien Valverdé
2025-05-19 14:01:41 +02:00
parent 64943deaab
commit 2c467dc6ec
17 changed files with 217 additions and 215 deletions

View File

@@ -12,12 +12,12 @@
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@tanstack/react-router": "^1.120.2",
"@tanstack/react-router-devtools": "^1.120.2",
"@tanstack/router-plugin": "^1.120.2",
"@tanstack/react-router": "^1.120.3",
"@tanstack/react-router-devtools": "^1.120.3",
"@tanstack/router-plugin": "^1.120.3",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -25,27 +25,27 @@
"globals": "^16.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript-eslint": "^8.32.0",
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
},
"dependencies": {
"@effect/platform": "^0.80.21",
"@effect/platform-browser": "^0.60.12",
"@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.62.1",
"@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.21",
"lucide-react": "^0.508.0",
"effect": "^3.15.1",
"lucide-react": "^0.510.0",
"mobx": "^6.13.7",
"reffuse": "workspace:*"
},
"overrides": {
"effect": "^3.14.21",
"@effect/platform": "^0.80.21",
"@effect/platform-browser": "^0.60.12",
"effect": "^3.15.1",
"@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.62.1",
"@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1"
}

View File

@@ -1,6 +1,5 @@
import { ThSchema } from "@thilawyn/thilaschema"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect, Schema } from "effect"
import { Schema } from "effect"
export class Todo extends Schema.Class<Todo>("Todo")({
@@ -18,9 +17,4 @@ export const TodoFromJsonStruct = Schema.Struct({
ThSchema.assertEncodedJsonifiable
)
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
export const generateUniqueID = makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom)
)
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)

View File

@@ -9,11 +9,7 @@ export class AppQueryErrorHandler extends QueryErrorHandler.Service<AppQueryErro
"AppQueryErrorHandler",
(self, failure, defect) => self.pipe(
Effect.catchTags({
RequestError: failure,
ResponseError: failure,
}),
Effect.catchTag("RequestError", "ResponseError", failure),
Effect.catchAllDefect(defect),
),
) {}

View File

@@ -2,7 +2,7 @@ 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, Option, Scope } from "effect"
import { Console, Effect, Option } from "effect"
import { useEffect, useState } from "react"
@@ -24,13 +24,13 @@ function RouteComponent() {
const uuidStream = R.useStreamFromReactiveValues([uuid])
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
const scope = R.useScope([uuid])
const [, scopeLayer] = R.useScope([uuid])
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
Effect.andThen(Console.log("Scope changed")),
Effect.provideService(Scope.Scope, scope),
Effect.provide(scopeLayer),
runSync,
), [scope, runSync])
), [scopeLayer, runSync])
return (
<Flex direction="column" justify="center" align="center" gap="2">

View File

@@ -26,7 +26,7 @@ function Todos() {
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
<VTodos />
</TodosContext.Provider>
</Container>

View File

@@ -2,68 +2,43 @@ import { Todo } from "@/domain"
import { KeyValueStore } from "@effect/platform"
import { BrowserKeyValueStore } from "@effect/platform-browser"
import { PlatformError } from "@effect/platform/Error"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
export class TodosState extends Context.Tag("TodosState")<TodosState, {
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
readonly remove: (index: number) => Effect.Effect<void>
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
}>() {}
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => kv.get(key)),
Effect.flatMap(identity),
Effect.flatMap(Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.decode,
Effect.flatMap(Schema.decode(
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
)),
Effect.flatMap(v => Ref.set(todos, v)),
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
Effect.flatMap(([kv, values]) => values.pipe(
Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.encode,
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => values.pipe(
Schema.encode(
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
),
Effect.flatMap(v => kv.set(key, v)),
)),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
const save = Effect.flatMap(todos, writeToLocalStorage)
// const moveUp = (index: number) => Effect.gen(function*() {
// Sync changes with local storage
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
// })
yield* readFromLocalStorage
return {
todos,
readFromLocalStorage,
saveToLocalStorage,
prepend,
replace,
remove,
}
return { todos, load, save }
}))

View File

@@ -1,25 +1,27 @@
import { Todo } from "@/domain"
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
import { Effect, Option, SubscriptionRef } from "effect"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, Effect, Option, Ref } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
const createEmptyTodo = Todo.generateUniqueID.pipe(
Effect.map(id => Todo.Todo.make({
id,
content: "",
completedAt: Option.none(),
}, true))
const createEmptyTodo = makeUuid4.pipe(
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
Effect.provide(GetRandomValues.CryptoRandom),
)
export function VNewTodo() {
const runSync = R.useRunSync()
const todoRef = R.useRef(() => createEmptyTodo)
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)), [])
const [todo, setTodo] = R.useRefState(todoRef)
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
Effect.andThen(createEmptyTodo),
Effect.flatMap(v => Ref.set(todoRef, v)),
), [todoRef])
return (
@@ -27,23 +29,12 @@ export function VNewTodo() {
<Card>
<Flex direction="column" align="stretch" gap="2">
<TextArea
value={todo.content}
onChange={e => setTodo(prev =>
Todo.Todo.make({ ...prev, content: e.target.value }, true)
)}
value={content}
onChange={e => setContent(e.target.value)}
/>
<Flex direction="row" justify="center" align="center">
<Button
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.prepend(todo)),
Effect.andThen(createEmptyTodo),
Effect.map(setTodo),
runSync,
)}
>
Add
</Button>
<Button onClick={add}>Add</Button>
</Flex>
</Flex>
</Card>

View File

@@ -1,20 +1,28 @@
import { Todo } from "@/domain"
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
import { Effect } from "effect"
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
import { Delete } from "lucide-react"
import { useState } from "react"
import { R } from "../reffuse"
import { TodosState } from "../services"
export interface VTodoProps {
readonly index: number
readonly todo: Todo.Todo
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
readonly remove: Effect.Effect<void>
}
export function VTodo({ index, todo }: VTodoProps) {
export function VTodo({ todoRef, remove }: VTodoProps) {
const runSync = R.useRunSync()
const localTodoRef = R.useRef(() => todoRef)
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
R.useFork(() => localTodoRef.changes.pipe(
Stream.debounce("250 millis"),
Stream.runForEach(v => Ref.set(todoRef, v)),
), [localTodoRef])
const editorMode = useState(false)
@@ -23,14 +31,8 @@ export function VTodo({ index, todo }: VTodoProps) {
<Card>
<Flex direction="column" align="stretch" gap="1">
<TextArea
value={todo.content}
onChange={e => TodosState.TodosState.pipe(
Effect.flatMap(state => state.replace(
index,
Todo.Todo.make({ ...todo, content: e.target.value }, true),
)),
runSync,
)}
value={content}
onChange={e => setContent(e.target.value)}
disabled={!editorMode}
/>
@@ -38,12 +40,7 @@ export function VTodo({ index, todo }: VTodoProps) {
<Box></Box>
<Flex direction="row" align="center" gap="1">
<IconButton
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
<IconButton onClick={() => runSync(remove)}>
<Delete />
</IconButton>
</Flex>

View File

@@ -1,5 +1,5 @@
import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect"
import { Chunk, Effect, Ref } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo"
@@ -8,14 +8,7 @@ import { VTodo } from "./VTodo"
export function VTodos() {
// Sync changes to the todos with the local storage
R.useFork(() => TodosState.TodosState.pipe(
Effect.flatMap(state =>
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
)
), [])
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
const [todos] = R.useSubscribeRefs(todosRef)
@@ -27,7 +20,16 @@ export function VTodos() {
{Chunk.map(todos, (todo, index) => (
<Box key={todo.id} width="500px">
<VTodo index={index} todo={todo} />
<R.SubRefFromGetSet
parent={todosRef}
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
>
{ref => <VTodo
todoRef={ref}
remove={Ref.update(todosRef, Chunk.remove(index))}
/>}
</R.SubRefFromGetSet>
</Box>
))}
</Flex>

View File

@@ -35,7 +35,7 @@
"peerDependencies": {
"@typed/lazy-ref": "^0.3.0",
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"effect": "^3.15.0",
"react": "^19.0.0",
"reffuse": "^0.1.8"
}

View File

@@ -37,7 +37,7 @@
"@effect/platform-browser": "^0.56.0",
"@typed/async-data": "^0.13.0",
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"effect": "^3.15.0",
"react": "^19.0.0",
"reffuse": "^0.1.6"
}

View File

@@ -1,6 +1,6 @@
{
"name": "reffuse",
"version": "0.1.10",
"version": "0.1.11",
"type": "module",
"files": [
"./README.md",
@@ -35,7 +35,7 @@
},
"peerDependencies": {
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"effect": "^3.15.0",
"react": "^19.0.0"
}
}

View File

@@ -1,4 +1,4 @@
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect"
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
@@ -25,6 +25,8 @@ export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly children?: React.ReactNode
}>
@@ -32,16 +34,25 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
const makeScope = React.useMemo(() => props.scope
? Scope.fork(props.scope, ExecutionStrategy.sequential)
: Scope.make(),
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
[props.scope])
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe(
const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
), [props.layer])
)
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
@@ -57,7 +68,7 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
() => closeScope(initialScope)
)
),
@@ -68,13 +79,13 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
Effect.sync(() => setValue(context))
),
Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
() => closeScope(scope)
),
),
}),
runSync,
), [makeScope, makeContext, runSync])
), [makeScope, runSync, runFork])
return React.createElement(Context, { ...props, value })
}
@@ -84,6 +95,7 @@ export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
@@ -112,7 +124,7 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
const scope = runSync(props.scope
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy)
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
)
Effect.context<R>().pipe(
@@ -126,7 +138,13 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
effect => runFork(effect, { ...props, scope }),
)
return () => { runFork(Scope.close(scope, Exit.void)) }
return () => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
}, [props.layer, runSync, runFork])
return React.createElement(React.Suspense, {

View File

@@ -1,4 +1,4 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, 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"
@@ -15,6 +15,7 @@ export interface ScopeOptions {
}
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
readonly scope?: Scope.Scope
readonly finalizerExecutionMode?: "sync" | "fork"
}
@@ -27,7 +28,8 @@ export abstract class ReffuseNamespace<R> {
declare ["constructor"]: ReffuseNamespaceClass<R>
constructor() {
this.SubRef = this.SubRef.bind(this as any) as any
this.SubRefFromGetSet = this.SubRefFromGetSet.bind(this as any) as any
this.SubRefFromPath = this.SubRefFromPath.bind(this as any) as any
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
this.RefState = this.RefState.bind(this as any) as any
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
@@ -96,14 +98,26 @@ export abstract class ReffuseNamespace<R> {
this: ReffuseNamespace<R>,
deps: React.DependencyList = [],
options?: UseScopeOptions,
): Scope.Scope {
): readonly [scope: Scope.Scope, layer: Layer.Layer<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 makeScope = React.useMemo(() => options?.scope
? Scope.fork(options.scope, options.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
[options?.scope])
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
const [isInitialRun, initialScope] = React.useMemo(() => runSync(
Effect.all([Ref.make(true), makeScope])
), [makeScope])
const [scope, setScope] = React.useState(initialScope)
@@ -111,34 +125,23 @@ export abstract class ReffuseNamespace<R> {
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,
)
),
() => closeScope(initialScope),
),
onFalse: () => Scope.make(options?.finalizerExecutionStrategy).pipe(
onFalse: () => makeScope.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,
)
)),
Effect.map(v => () => closeScope(v)),
),
}),
runSync,
), [
makeScope,
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
...deps,
])
return scope
return React.useMemo(() => [scope, Layer.succeed(Scope.Scope, scope)] as const, [scope])
}
/**
@@ -404,7 +407,19 @@ export abstract class ReffuseNamespace<R> {
return ref
}
useSubRef<B, const P extends PropertyPath.Paths<B>, R>(
useSubRefFromGetSet<A, B, R>(
this: ReffuseNamespace<R>,
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef.SubscriptionSubRef<A, B> {
return React.useMemo(
() => SubscriptionSubRef.makeFromGetSet(parent, getter, setter),
[parent],
)
}
useSubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
@@ -474,7 +489,7 @@ export abstract class ReffuseNamespace<R> {
this: ReffuseNamespace<R>,
values: A,
): Stream.Stream<A> {
const scope = this.useScope()
const [, scopeLayer] = this.useScope([], { finalizerExecutionMode: "fork" })
const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
Effect.bind("latest", () => Ref.make(values)),
@@ -486,8 +501,8 @@ export abstract class ReffuseNamespace<R> {
)),
Stream.unwrapScoped,
)),
Effect.provideService(Scope.Scope, scope),
), [scope], { doNotReExecuteOnRuntimeOrContextChange: true })
Effect.provide(scopeLayer),
), [scopeLayer], { doNotReExecuteOnRuntimeOrContextChange: true })
this.useEffect(() => Ref.set(latest, values).pipe(
Effect.andThen(PubSub.publish(pubsub, values)),
@@ -528,7 +543,19 @@ export abstract class ReffuseNamespace<R> {
}
SubRef<B, const P extends PropertyPath.Paths<B>, R>(
SubRefFromGetSet<A, B, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<A, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRefFromGetSet(props.parent, props.getter, props.setter))
}
SubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
@@ -536,7 +563,7 @@ export abstract class ReffuseNamespace<R> {
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRef(props.parent, props.path))
return props.children(this.useSubRefFromPath(props.parent, props.path))
}
SubscribeRefs<

View File

@@ -52,7 +52,7 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
readonly setter: (parentValue: B, value: A) => B,
) {
super()
this.get = Ref.get(this.parent).pipe(Effect.map(this.getter))
this.get = Effect.map(Ref.get(this.parent), this.getter)
}
commit() {