0.1.6 (#9)
All checks were successful
Publish / publish (push) Successful in 29s
Lint / lint (push) Successful in 13s

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/9
This commit was merged in pull request #9.
This commit is contained in:
Julien Valverdé
2025-04-12 23:58:25 +02:00
parent d7c648994d
commit 4092da0f0c
15 changed files with 320 additions and 268 deletions

View File

@@ -11,41 +11,41 @@
"preview": "vite preview"
},
"devDependencies": {
"@eslint/js": "^9.23.0",
"@tanstack/react-router": "^1.114.27",
"@tanstack/react-router-devtools": "^1.114.27",
"@tanstack/router-plugin": "^1.114.27",
"@eslint/js": "^9.24.0",
"@tanstack/react-router": "^1.115.3",
"@tanstack/react-router-devtools": "^1.115.3",
"@tanstack/router-plugin": "^1.115.3",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.23.0",
"eslint": "^9.24.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript-eslint": "^8.28.0",
"vite": "^6.2.3"
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript-eslint": "^8.29.1",
"vite": "^6.2.6"
},
"dependencies": {
"@effect/platform": "^0.80.1",
"@effect/platform-browser": "^0.59.1",
"@effect/platform": "^0.80.8",
"@effect/platform-browser": "^0.59.8",
"@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.1",
"@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3",
"effect": "^3.14.1",
"lucide-react": "^0.483.0",
"effect": "^3.14.8",
"lucide-react": "^0.487.0",
"mobx": "^6.13.7",
"reffuse": "workspace:*"
},
"overrides": {
"effect": "^3.14.1",
"@effect/platform": "^0.80.1",
"@effect/platform-browser": "^0.59.1",
"effect": "^3.14.8",
"@effect/platform": "^0.80.8",
"@effect/platform-browser": "^0.59.8",
"@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1"
}

View File

@@ -37,7 +37,7 @@ function RouteComponent() {
)
})
const [state] = R.useRefState(mutation.state)
const [state] = R.useSubscribeRefs(mutation.state)
return (

View File

@@ -31,7 +31,7 @@ function RouteComponent() {
),
})
const [state] = R.useRefState(query.state)
const [state] = R.useSubscribeRefs(query.state)
return (

View File

@@ -1,9 +1,8 @@
import { R } from "@/reffuse"
import { Button, Flex } from "@radix-ui/themes"
import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Stream } from "effect"
import { useState } from "react"
import { Console, Effect, Ref } from "effect"
export const Route = createFileRoute("/tests")({
@@ -22,9 +21,9 @@ function RouteComponent() {
Effect.delay("1 second"),
), [])
const [reactValue, setReactValue] = useState("initial")
const reactValueStream = R.useStreamFromValues([reactValue])
R.useFork(() => Stream.runForEach(reactValueStream, Console.log), [reactValueStream])
const uuidRef = R.useRef("none")
const anotherRef = R.useRef(69)
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
@@ -33,12 +32,16 @@ function RouteComponent() {
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom),
Effect.map(setReactValue),
Effect.flatMap(v => Ref.set(uuidRef, v)),
), [])
return (
<Flex direction="row" justify="center" align="center" gap="2">
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
</R.SubscribeRefs>
<Button onClick={() => logValue("test")}>Log value</Button>
<Button onClick={() => generateUuid()}>Generate UUID</Button>
</Flex>

View File

@@ -1,6 +1,6 @@
{
"name": "@reffuse/extension-lazyref",
"version": "0.1.1",
"version": "0.1.2",
"type": "module",
"files": [
"./README.md",
@@ -37,6 +37,6 @@
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"react": "^19.0.0",
"reffuse": "^0.1.4"
"reffuse": "^0.1.6"
}
}

View File

@@ -1,12 +1,12 @@
import * as LazyRef from "@typed/lazy-ref"
import { Effect, Stream } from "effect"
import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse"
import { ReffuseExtension, type ReffuseNamespace, SetStateAction } from "reffuse"
export const LazyRefExtension = ReffuseExtension.make(() => ({
useLazyRefState<A, E, R>(
this: ReffuseHelpers.ReffuseHelpers<R>,
this: ReffuseNamespace.ReffuseNamespace<R>,
ref: LazyRef.LazyRef<A, E, R>,
): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })

View File

@@ -1,6 +1,6 @@
{
"name": "@reffuse/extension-query",
"version": "0.1.2",
"version": "0.1.3",
"type": "module",
"files": [
"./README.md",
@@ -39,6 +39,6 @@
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"react": "^19.0.0",
"reffuse": "^0.1.4"
"reffuse": "^0.1.6"
}
}

View File

@@ -1,7 +1,7 @@
import type * as AsyncData from "@typed/async-data"
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import type * as MutationService from "./MutationService.js"
import * as QueryClient from "./QueryClient.js"
import type * as QueryProgress from "./QueryProgress.js"
@@ -59,7 +59,7 @@ export const QueryExtension = ReffuseExtension.make(() => ({
QR extends R,
R,
>(
this: ReffuseHelpers.ReffuseHelpers<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseQueryProps<QK, QA, QE, QR>,
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => QueryRunner.make({
@@ -98,7 +98,7 @@ export const QueryExtension = ReffuseExtension.make(() => ({
QR extends R,
R,
>(
this: ReffuseHelpers.ReffuseHelpers<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseMutationProps<QK, QA, QE, QR>,
): UseMutationResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => MutationRunner.make({

View File

@@ -1,6 +1,6 @@
{
"name": "reffuse",
"version": "0.1.5",
"version": "0.1.6",
"type": "module",
"files": [
"./README.md",

View File

@@ -1,30 +1,30 @@
import type * as ReffuseContext from "./ReffuseContext.js"
import type * as ReffuseExtension from "./ReffuseExtension.js"
import * as ReffuseHelpers from "./ReffuseHelpers.js"
import * as ReffuseNamespace from "./ReffuseNamespace.js"
import type { Merge, StaticType } from "./types.js"
export class Reffuse extends ReffuseHelpers.make() {}
export class Reffuse extends ReffuseNamespace.makeClass() {}
export const withContexts = <R2 extends Array<unknown>>(
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
) => (
<
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R1>,
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R1>,
R1
>(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R1>
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R1>
): (
{
new(): Merge<
InstanceType<BaseClass>,
{ constructor: ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]> }
{ constructor: ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]> }
>
} &
Merge<
StaticType<BaseClass>,
StaticType<ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]>>
StaticType<ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]>>
>
) => class extends self {
static readonly contexts = [...self.contexts, ...contexts]
@@ -33,10 +33,10 @@ export const withContexts = <R2 extends Array<unknown>>(
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) => (
<
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R>,
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R>,
R
>(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R>
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R>
): (
{ new(): Merge<InstanceType<BaseClass>, A> } &
StaticType<BaseClass>

View File

@@ -1,4 +1,4 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import { Array, type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, pipe, Pipeable, Queue, 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,20 +15,25 @@ export interface ScopeOptions {
}
export abstract class ReffuseHelpers<R> {
declare ["constructor"]: ReffuseHelpersClass<R>
export abstract class ReffuseNamespace<R> {
declare ["constructor"]: ReffuseNamespaceClass<R>
constructor() {
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
this.RefState = this.RefState.bind(this as any) as any
}
useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> {
useContext<R>(this: ReffuseNamespace<R>): Context.Context<R> {
return ReffuseContext.useMergeAll(...this.constructor.contexts)
}
useLayer<R>(this: ReffuseHelpers<R>): Layer.Layer<R> {
useLayer<R>(this: ReffuseNamespace<R>): Layer.Layer<R> {
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
}
useRunSync<R>(this: ReffuseHelpers<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
useRunSync<R>(this: ReffuseNamespace<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
@@ -38,7 +43,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context])
}
useRunPromise<R>(this: ReffuseHelpers<R>): <A, E>(
useRunPromise<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal },
) => Promise<A> {
@@ -51,7 +56,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context])
}
useRunFork<R>(this: ReffuseHelpers<R>): <A, E>(
useRunFork<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions,
) => Fiber.RuntimeFiber<A, E> {
@@ -64,7 +69,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context])
}
useRunCallback<R>(this: ReffuseHelpers<R>): <A, E>(
useRunCallback<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunCallbackOptions<A, E>,
) => Runtime.Cancel<A, E> {
@@ -87,7 +92,7 @@ export abstract class ReffuseHelpers<R> {
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*/
useMemo<A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
options?: RenderOptions,
@@ -101,7 +106,7 @@ export abstract class ReffuseHelpers<R> {
}
useMemoScoped<A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList,
options?: RenderOptions & ScopeOptions,
@@ -174,7 +179,7 @@ export abstract class ReffuseHelpers<R> {
* ```
*/
useEffect<A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions,
@@ -222,7 +227,7 @@ export abstract class ReffuseHelpers<R> {
* ```
*/
useLayoutEffect<A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions,
@@ -270,7 +275,7 @@ export abstract class ReffuseHelpers<R> {
* ```
*/
useFork<A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -293,7 +298,7 @@ export abstract class ReffuseHelpers<R> {
}
usePromise<A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -340,7 +345,7 @@ export abstract class ReffuseHelpers<R> {
}
useCallbackSync<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
options?: RenderOptions,
@@ -354,7 +359,7 @@ export abstract class ReffuseHelpers<R> {
}
useCallbackPromise<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
options?: { readonly signal?: AbortSignal } & RenderOptions,
@@ -368,7 +373,7 @@ export abstract class ReffuseHelpers<R> {
}
useRef<A, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
value: A,
): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
@@ -378,6 +383,30 @@ export abstract class ReffuseHelpers<R> {
)
}
useSubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
>(
this: ReffuseNamespace<R>,
...refs: Refs
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
this.useFork(() => pipe(refs as readonly SubscriptionRef.SubscriptionRef<any>[],
Array.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
),
), refs)
return reactStateValue
}
/**
* Binds the state of a `SubscriptionRef` to the state of the React component.
*
@@ -386,7 +415,7 @@ export abstract class ReffuseHelpers<R> {
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
*/
useRefState<A, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
ref: SubscriptionRef.SubscriptionRef<A>,
): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
@@ -407,7 +436,7 @@ export abstract class ReffuseHelpers<R> {
}
useStreamFromValues<const A extends React.DependencyList, R>(
this: ReffuseHelpers<R>,
this: ReffuseNamespace<R>,
values: A,
): Stream.Stream<A> {
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
@@ -418,33 +447,57 @@ export abstract class ReffuseHelpers<R> {
return stream
}
SubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
>(
this: ReffuseNamespace<R>,
props: {
readonly refs: Refs
readonly children: (...args: [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]) => React.ReactNode
},
): React.ReactNode {
return props.children(...this.useSubscribeRefs(...props.refs))
}
RefState<A, R>(
this: ReffuseNamespace<R>,
props: {
readonly ref: SubscriptionRef.SubscriptionRef<A>
readonly children: (state: [A, React.Dispatch<React.SetStateAction<A>>]) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useRefState(props.ref))
}
}
export interface ReffuseHelpers<R> extends Pipeable.Pipeable {}
export interface ReffuseNamespace<R> extends Pipeable.Pipeable {}
ReffuseHelpers.prototype.pipe = function pipe() {
ReffuseNamespace.prototype.pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments)
};
export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable {
new(): ReffuseHelpers<R>
export interface ReffuseNamespaceClass<R> extends Pipeable.Pipeable {
new(): ReffuseNamespace<R>
make<Self>(this: new () => Self): Self
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
}
(ReffuseHelpers as ReffuseHelpersClass<any>).make = function make() {
(ReffuseNamespace as ReffuseNamespaceClass<any>).make = function make() {
return new this()
};
(ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() {
(ReffuseNamespace as ReffuseNamespaceClass<any>).pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments)
};
export const make = (): ReffuseHelpersClass<never> => (
class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) {
export const makeClass = (): ReffuseNamespaceClass<never> => (
class extends (ReffuseNamespace<never> as ReffuseNamespaceClass<never>) {
static readonly contexts = []
}
)

View File

@@ -1,6 +1,6 @@
export * as Reffuse from "./Reffuse.js"
export * as ReffuseContext from "./ReffuseContext.js"
export * as ReffuseExtension from "./ReffuseExtension.js"
export * as ReffuseHelpers from "./ReffuseHelpers.js"
export * as ReffuseNamespace from "./ReffuseNamespace.js"
export * as ReffuseRuntime from "./ReffuseRuntime.js"
export * as SetStateAction from "./SetStateAction.js"

View File

@@ -8,14 +8,4 @@ export type CommonKeys<A, B> = Extract<keyof A, keyof B>
*/
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
export type Extend<Super, Self> =
Extendable<Super, Self> extends true
? Omit<Super, CommonKeys<Self, Super>> & Self
: never
export type Extendable<Super, Self> =
Pick<Self, CommonKeys<Self, Super>> extends Pick<Super, CommonKeys<Self, Super>>
? true
: false
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self