Compare commits
19 Commits
reffuse-ne
...
form
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db7608f7c3 | ||
|
|
b78f99e808 | ||
|
|
86dde2d286 | ||
|
|
596e0942c5 | ||
|
|
cb61713cce | ||
|
|
1b2b68fbae | ||
|
|
35a8037f5a | ||
|
|
7aef7ae796 | ||
|
|
1bfbeba934 | ||
|
|
fc4295894f | ||
|
|
ab0dce107d | ||
|
|
9436602443 | ||
|
|
66de31706c | ||
|
|
8925fe6336 | ||
|
|
fe8ca23d37 | ||
|
|
d48f20a59d | ||
|
|
b7b4abcbe2 | ||
|
|
129ab04ea7 | ||
|
|
870fe479c3 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@reffuse/monorepo",
|
"name": "@reffuse/monorepo",
|
||||||
"packageManager": "bun@1.2.13",
|
"packageManager": "bun@1.2.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./packages/*"
|
"./packages/*"
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-check-updates": "^18.0.1",
|
"npm-check-updates": "^17.1.18",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
"turbo": "^2.5.3",
|
"turbo": "^2.5.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# Reffuse
|
|
||||||
|
|
||||||
[Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
|
|
||||||
|
|
||||||
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
|
|
||||||
|
|
||||||
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
|
||||||
|
|
||||||
## Peer dependencies
|
|
||||||
- `effect` 3.13+
|
|
||||||
- `react` & `@types/react` 19+
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
|
|
||||||
import type { Mutable } from "effect/Types"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ReactHook from "./ReactHook.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReactComponent<E, R, P> {
|
|
||||||
(props: P): Effect.Effect<React.ReactNode, E, R>
|
|
||||||
readonly displayName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
|
||||||
|
|
||||||
export const withDisplayName: {
|
|
||||||
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
|
|
||||||
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
|
|
||||||
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
|
|
||||||
self: C,
|
|
||||||
displayName: string,
|
|
||||||
): C => {
|
|
||||||
(self as Mutable<C>).displayName = displayName
|
|
||||||
return self
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useFC: {
|
|
||||||
<E, R, P extends {} = {}>(
|
|
||||||
self: ReactComponent<E, R, P>,
|
|
||||||
options?: ReactHook.ScopeOptions,
|
|
||||||
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function* <E, R, P extends {}>(
|
|
||||||
self: ReactComponent<E, R, P>,
|
|
||||||
options?: ReactHook.ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
return React.useMemo(() => function ScopeProvider(props: P) {
|
|
||||||
const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
|
|
||||||
|
|
||||||
const FC = React.useMemo(() => {
|
|
||||||
const f = (props: P) => Runtime.runSync(runtime)(
|
|
||||||
Effect.provideService(self(props), Scope.Scope, scope)
|
|
||||||
)
|
|
||||||
if (self.displayName) f.displayName = self.displayName
|
|
||||||
return f
|
|
||||||
}, [scope])
|
|
||||||
|
|
||||||
return React.createElement(FC, props)
|
|
||||||
}, Array.from(
|
|
||||||
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
|
|
||||||
))
|
|
||||||
})
|
|
||||||
|
|
||||||
export const use: {
|
|
||||||
<E, R, P extends {} = {}>(
|
|
||||||
self: ReactComponent<E, R, P>,
|
|
||||||
fn: (Component: React.FC<P>) => React.ReactNode,
|
|
||||||
options?: ReactHook.ScopeOptions,
|
|
||||||
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function*(self, fn, options) {
|
|
||||||
return fn(yield* useFC(self, options))
|
|
||||||
})
|
|
||||||
|
|
||||||
export const withRuntime: {
|
|
||||||
<E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
|
|
||||||
<E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
|
|
||||||
} = Function.dual(2, <E, R, P extends {}>(
|
|
||||||
self: ReactComponent<E, R, P>,
|
|
||||||
context: React.Context<Runtime.Runtime<R>>,
|
|
||||||
): React.FC<P> => function WithRuntime(props) {
|
|
||||||
const runtime = React.useContext(context)
|
|
||||||
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
|
|
||||||
})
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import { SetStateAction } from "./types/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ScopeOptions {
|
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useScope: {
|
|
||||||
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
|
|
||||||
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
|
|
||||||
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
|
||||||
Effect.all([Ref.make(true), makeScope(options)])
|
|
||||||
), [])
|
|
||||||
const [scope, setScope] = React.useState(initialScope)
|
|
||||||
|
|
||||||
React.useEffect(() => Runtime.runSync(runtime)(
|
|
||||||
Effect.if(isInitialRun, {
|
|
||||||
onTrue: () => Effect.as(
|
|
||||||
Ref.set(isInitialRun, false),
|
|
||||||
() => closeScope(scope, runtime, options),
|
|
||||||
),
|
|
||||||
|
|
||||||
onFalse: () => makeScope(options).pipe(
|
|
||||||
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
|
||||||
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
), [])
|
|
||||||
|
|
||||||
return scope
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
const closeScope = (
|
|
||||||
scope: Scope.CloseableScope,
|
|
||||||
runtime: Runtime.Runtime<never>,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useMemo: {
|
|
||||||
<A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useOnce: {
|
|
||||||
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
|
||||||
} = Effect.fnUntraced(function* <A, E, R>(
|
|
||||||
factory: () => Effect.Effect<A, E, R>
|
|
||||||
) {
|
|
||||||
return yield* useMemo(factory, [])
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useMemoLayer: {
|
|
||||||
<ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>
|
|
||||||
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
|
||||||
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
|
||||||
layer: Layer.Layer<ROut, E, RIn>
|
|
||||||
) {
|
|
||||||
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useCallbackSync: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => A, never, R>
|
|
||||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useCallbackPromise: {
|
|
||||||
<Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
|
||||||
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<R>()
|
|
||||||
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useEffect: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const { scope, exit } = Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useLayoutEffect: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
const { scope, exit } = Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
|
||||||
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "sync") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useFork: {
|
|
||||||
<E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
|
||||||
} = Effect.fnUntraced(function* <E, R>(
|
|
||||||
effect: () => Effect.Effect<void, E, R>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & ScopeOptions,
|
|
||||||
) {
|
|
||||||
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const scope = Runtime.runSync(runtime)(options?.scope
|
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(options?.finalizerExecutionStrategy)
|
|
||||||
)
|
|
||||||
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
switch (options?.finalizerExecutionMode ?? "fork") {
|
|
||||||
case "sync":
|
|
||||||
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
case "fork":
|
|
||||||
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, deps)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useRefFromReactiveValue: {
|
|
||||||
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
|
||||||
} = Effect.fnUntraced(function*(value) {
|
|
||||||
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
|
||||||
yield* useEffect(() => Ref.set(ref, value), [value])
|
|
||||||
return ref
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeRefs: {
|
|
||||||
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
|
||||||
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
|
||||||
...refs: Refs
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
|
||||||
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useFork(() => pipe(
|
|
||||||
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
|
||||||
streams => Stream.zipLatestAll(...streams),
|
|
||||||
Stream.runForEach(v =>
|
|
||||||
Effect.sync(() => setReactStateValue(v))
|
|
||||||
),
|
|
||||||
), refs)
|
|
||||||
|
|
||||||
return reactStateValue as any
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useRefState: {
|
|
||||||
<A>(
|
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>
|
|
||||||
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
|
||||||
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(ref.changes, (x, y) => x === y),
|
|
||||||
v => Effect.sync(() => setReactStateValue(v)),
|
|
||||||
), [ref])
|
|
||||||
|
|
||||||
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
|
||||||
Ref.update(ref, prevState =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
|
||||||
),
|
|
||||||
[ref])
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export const useStreamFromReactiveValues: {
|
|
||||||
<const A extends React.DependencyList>(
|
|
||||||
values: A
|
|
||||||
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
|
||||||
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
|
|
||||||
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("latest", () => Ref.make(values)),
|
|
||||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
|
||||||
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
|
||||||
Effect.flatMap(a => Effect.map(
|
|
||||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrapScoped,
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
|
|
||||||
yield* useEffect(() => Ref.set(latest, values).pipe(
|
|
||||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
|
||||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
|
||||||
), values)
|
|
||||||
|
|
||||||
return stream
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useSubscribeStream: {
|
|
||||||
<A, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>
|
|
||||||
): Effect.Effect<Option.Option<A>, never, R>
|
|
||||||
<A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue: A,
|
|
||||||
): Effect.Effect<Option.Some<A>, never, R>
|
|
||||||
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue?: A,
|
|
||||||
) {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(
|
|
||||||
React.useMemo(() => initialValue
|
|
||||||
? Option.some(initialValue)
|
|
||||||
: Option.none(),
|
|
||||||
[])
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(stream, (x, y) => x === y),
|
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
|
||||||
), [stream])
|
|
||||||
|
|
||||||
return reactStateValue as Option.Some<A>
|
|
||||||
})
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReactManagedRuntime<R, ER> {
|
|
||||||
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
|
||||||
readonly context: React.Context<Runtime.Runtime<R>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <R, ER>(
|
|
||||||
layer: Layer.Layer<R, ER>,
|
|
||||||
memoMap?: Layer.MemoMap,
|
|
||||||
): ReactManagedRuntime<R, ER> => ({
|
|
||||||
runtime: ManagedRuntime.make(layer, memoMap),
|
|
||||||
context: React.createContext<Runtime.Runtime<R>>(null!),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
|
|
||||||
readonly runtime: ReactManagedRuntime<R, ER>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AsyncProvider<R, ER>(
|
|
||||||
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
|
|
||||||
): React.ReactNode {
|
|
||||||
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
|
||||||
|
|
||||||
return React.createElement(
|
|
||||||
React.Suspense,
|
|
||||||
suspenseProps,
|
|
||||||
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AsyncProviderInnerProps<R, ER> {
|
|
||||||
readonly runtime: ReactManagedRuntime<R, ER>
|
|
||||||
readonly promise: Promise<Runtime.Runtime<R>>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
function AsyncProviderInner<R, ER>(
|
|
||||||
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
|
|
||||||
): React.ReactNode {
|
|
||||||
const value = React.use(promise)
|
|
||||||
return React.createElement(runtime.context, { value }, children)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * as ReactComponent from "./ReactComponent.js"
|
|
||||||
export * as ReactHook from "./ReactHook.js"
|
|
||||||
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { Array, Function, Option, Predicate } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
||||||
|
|
||||||
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
|
||||||
D extends never ? [] :
|
|
||||||
T extends Seen ? [] :
|
|
||||||
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
|
||||||
T extends object ? ObjectPaths<T, D, Seen | T> :
|
|
||||||
never
|
|
||||||
)
|
|
||||||
|
|
||||||
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
|
||||||
[K in keyof T as K extends number ? K : never]:
|
|
||||||
| [K]
|
|
||||||
| [K, ...Paths<T[K], Prev[D], Seen>]
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
|
||||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
|
||||||
NonNullable<T[K]> extends infer V
|
|
||||||
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
|
||||||
: never
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
|
||||||
? Head extends keyof T
|
|
||||||
? ValueFromPath<T[Head], Tail>
|
|
||||||
: T extends readonly any[]
|
|
||||||
? Head extends number
|
|
||||||
? ValueFromPath<T[number], Tail>
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
: T
|
|
||||||
|
|
||||||
export type AnyKey = string | number | symbol
|
|
||||||
export type AnyPath = readonly AnyKey[]
|
|
||||||
|
|
||||||
|
|
||||||
export const unsafeGet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
|
||||||
path.reduce((acc: any, key: any) => acc?.[key], self)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const get: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
|
||||||
path.reduce(
|
|
||||||
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
|
||||||
? Predicate.hasProperty(acc.value, key)
|
|
||||||
? Option.some(acc.value[key])
|
|
||||||
: Option.none()
|
|
||||||
: acc,
|
|
||||||
|
|
||||||
Option.some(self),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const immutableSet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
|
||||||
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
|
||||||
const key = Array.head(path as AnyPath)
|
|
||||||
if (Option.isNone(key))
|
|
||||||
return Option.some(value as T)
|
|
||||||
if (!Predicate.hasProperty(self, key.value))
|
|
||||||
return Option.none()
|
|
||||||
|
|
||||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
|
||||||
if (Option.isNone(child))
|
|
||||||
return child
|
|
||||||
|
|
||||||
if (Array.isArray(self))
|
|
||||||
return typeof key.value === "number"
|
|
||||||
? Option.some([
|
|
||||||
...self.slice(0, key.value),
|
|
||||||
child.value,
|
|
||||||
...self.slice(key.value + 1),
|
|
||||||
] as T)
|
|
||||||
: Option.none()
|
|
||||||
|
|
||||||
if (typeof self === "object")
|
|
||||||
return Option.some(
|
|
||||||
Object.assign(
|
|
||||||
Object.create(Object.getPrototypeOf(self)),
|
|
||||||
{ ...self, [key.value]: child.value },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Option.none()
|
|
||||||
})
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
|
||||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
|
||||||
|
|
||||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
|
||||||
|
|
||||||
readonly [Unify.typeSymbol]?: unknown
|
|
||||||
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
|
||||||
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace SubscriptionSubRef {
|
|
||||||
export interface Variance<in out A, in out B> {
|
|
||||||
readonly [SubscriptionSubRefTypeId]: {
|
|
||||||
readonly _A: Types.Invariant<A>
|
|
||||||
readonly _B: Types.Invariant<B>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
|
||||||
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
|
||||||
SubscriptionRef?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const refVariance = { _A: (_: any) => _ }
|
|
||||||
const synchronizedRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
|
||||||
|
|
||||||
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
|
||||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
|
||||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
|
||||||
readonly [Ref.RefTypeId] = refVariance
|
|
||||||
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
|
||||||
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
|
||||||
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
|
||||||
|
|
||||||
readonly get: Effect.Effect<A>
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
readonly getter: (parentValue: B) => A,
|
|
||||||
readonly setter: (parentValue: B, value: A) => B,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
|
||||||
}
|
|
||||||
|
|
||||||
commit() {
|
|
||||||
return this.get
|
|
||||||
}
|
|
||||||
|
|
||||||
get changes(): Stream.Stream<A> {
|
|
||||||
return this.get.pipe(
|
|
||||||
Effect.map(a => this.parent.changes.pipe(
|
|
||||||
Stream.map(this.getter),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrap,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
|
||||||
return this.modifyEffect(a => Effect.succeed(f(a)))
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
|
||||||
return Effect.Do.pipe(
|
|
||||||
Effect.bind("b", () => Ref.get(this.parent)),
|
|
||||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
|
||||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
|
||||||
Effect.map(({ ca: [c] }) => c),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const makeFromGetSet = <A, B>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
getter: (parentValue: B) => A,
|
|
||||||
setter: (parentValue: B, value: A) => B,
|
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
|
||||||
|
|
||||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
path: P,
|
|
||||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
|
||||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * as PropertyPath from "./PropertyPath.js"
|
|
||||||
export * as SetStateAction from "./SetStateAction.js"
|
|
||||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
@@ -11,41 +11,41 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.24.0",
|
||||||
"@tanstack/react-router": "^1.120.3",
|
"@tanstack/react-router": "^1.115.3",
|
||||||
"@tanstack/react-router-devtools": "^1.120.3",
|
"@tanstack/react-router-devtools": "^1.115.3",
|
||||||
"@tanstack/router-plugin": "^1.120.3",
|
"@tanstack/router-plugin": "^1.115.3",
|
||||||
"@thilawyn/thilaschema": "^0.1.4",
|
"@thilawyn/thilaschema": "^0.1.4",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.1",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.24.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.1.0",
|
"globals": "^16.0.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"typescript-eslint": "^8.32.1",
|
"typescript-eslint": "^8.29.1",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.2.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform": "^0.82.1",
|
"@effect/platform": "^0.80.8",
|
||||||
"@effect/platform-browser": "^0.62.1",
|
"@effect/platform-browser": "^0.59.8",
|
||||||
"@radix-ui/themes": "^3.2.1",
|
"@radix-ui/themes": "^3.2.1",
|
||||||
"@reffuse/extension-lazyref": "workspace:*",
|
"@reffuse/extension-lazyref": "workspace:*",
|
||||||
"@reffuse/extension-query": "workspace:*",
|
"@reffuse/extension-query": "workspace:*",
|
||||||
"@typed/async-data": "^0.13.1",
|
"@typed/async-data": "^0.13.1",
|
||||||
"@typed/id": "^0.17.2",
|
"@typed/id": "^0.17.2",
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
"@typed/lazy-ref": "^0.3.3",
|
||||||
"effect": "^3.15.1",
|
"effect": "^3.14.8",
|
||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.487.0",
|
||||||
"mobx": "^6.13.7",
|
"mobx": "^6.13.7",
|
||||||
"reffuse": "workspace:*"
|
"reffuse": "workspace:*"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"effect": "^3.15.1",
|
"effect": "^3.14.8",
|
||||||
"@effect/platform": "^0.82.1",
|
"@effect/platform": "^0.80.8",
|
||||||
"@effect/platform-browser": "^0.62.1",
|
"@effect/platform-browser": "^0.59.8",
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
"@typed/lazy-ref": "^0.3.3",
|
||||||
"@typed/async-data": "^0.13.1"
|
"@typed/async-data": "^0.13.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||||
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { AppQueryErrorHandler } from "./query"
|
||||||
import { R } from "./reffuse"
|
import { R } from "./reffuse"
|
||||||
import { AppQueryErrorHandler } from "./services"
|
|
||||||
|
|
||||||
|
|
||||||
export function VQueryErrorHandler() {
|
export function VQueryErrorHandler() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const error = R.useSubscribeStream(
|
const error = R.useSubscribeStream(
|
||||||
R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
|
R.useMemo(() => AppQueryErrorHandler.pipe(
|
||||||
Effect.map(handler => handler.errors.pipe(
|
Effect.map(handler => handler.errors.pipe(
|
||||||
Stream.changes,
|
Stream.changes,
|
||||||
Stream.tap(Console.error),
|
Stream.tap(Console.error),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ThSchema } from "@thilawyn/thilaschema"
|
import { ThSchema } from "@thilawyn/thilaschema"
|
||||||
import { Schema } from "effect"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Effect, Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export class Todo extends Schema.Class<Todo>("Todo")({
|
export class Todo extends Schema.Class<Todo>("Todo")({
|
||||||
@@ -17,4 +18,9 @@ export const TodoFromJsonStruct = Schema.Struct({
|
|||||||
ThSchema.assertEncodedJsonifiable
|
ThSchema.assertEncodedJsonifiable
|
||||||
)
|
)
|
||||||
|
|
||||||
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
|
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
|
||||||
|
|
||||||
|
|
||||||
|
export const generateUniqueID = makeUuid4.pipe(
|
||||||
|
Effect.provide(GetRandomValues.CryptoRandom)
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { Layer } from "effect"
|
|||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { ReffuseRuntime } from "reffuse"
|
import { ReffuseRuntime } from "reffuse"
|
||||||
|
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||||
import { RootContext } from "./reffuse"
|
import { RootContext } from "./reffuse"
|
||||||
import { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
|
||||||
|
|
||||||
|
|
||||||
const layer = Layer.empty.pipe(
|
const layer = Layer.empty.pipe(
|
||||||
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
|
Layer.provideMerge(AppQueryClient.Live),
|
||||||
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
|
Layer.provideMerge(AppQueryErrorHandler.Live),
|
||||||
Layer.provideMerge(Clipboard.layer),
|
Layer.provideMerge(Clipboard.layer),
|
||||||
Layer.provideMerge(Geolocation.layer),
|
Layer.provideMerge(Geolocation.layer),
|
||||||
Layer.provideMerge(Permissions.layer),
|
Layer.provideMerge(Permissions.layer),
|
||||||
|
|||||||
21
packages/example/src/query.ts
Normal file
21
packages/example/src/query.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { HttpClientError } from "@effect/platform"
|
||||||
|
import { QueryClient, QueryErrorHandler } from "@reffuse/extension-query"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class AppQueryErrorHandler extends QueryErrorHandler.Service<AppQueryErrorHandler,
|
||||||
|
HttpClientError.HttpClientError
|
||||||
|
>()(
|
||||||
|
"AppQueryErrorHandler",
|
||||||
|
|
||||||
|
(self, failure, defect) => self.pipe(
|
||||||
|
Effect.catchTags({
|
||||||
|
RequestError: failure,
|
||||||
|
ResponseError: failure,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Effect.catchAllDefect(defect),
|
||||||
|
),
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({ ErrorHandler: AppQueryErrorHandler }) {}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { QueryRunner } from "@reffuse/extension-query"
|
import { QueryService } from "@reffuse/extension-query"
|
||||||
import { ParseResult, Schema } from "effect"
|
import { ParseResult, Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const Result = Schema.Array(Schema.String)
|
export const Result = Schema.Array(Schema.String)
|
||||||
|
|
||||||
export class Uuid4Query extends QueryRunner.Tag("Uuid4Query")<Uuid4Query,
|
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
|
||||||
readonly ["uuid4", number],
|
readonly ["uuid4", number],
|
||||||
typeof Result.Type,
|
typeof Result.Type,
|
||||||
ParseResult.ParseError
|
ParseResult.ParseError
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function Uuid4QueryService() {
|
|||||||
const runFork = R.useRunFork()
|
const runFork = R.useRunFork()
|
||||||
|
|
||||||
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
const [state] = R.useRefState(query.state)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
|||||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||||
import { QueryExtension } from "@reffuse/extension-query"
|
import { QueryExtension } from "@reffuse/extension-query"
|
||||||
import { Reffuse, ReffuseContext } from "reffuse"
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||||
|
|
||||||
|
|
||||||
export const RootContext = ReffuseContext.make<
|
export const RootContext = ReffuseContext.make<
|
||||||
| AppQueryClient.AppQueryClient
|
| AppQueryClient
|
||||||
| AppQueryErrorHandler.AppQueryErrorHandler
|
| AppQueryErrorHandler
|
||||||
| Clipboard.Clipboard
|
| Clipboard.Clipboard
|
||||||
| Geolocation.Geolocation
|
| Geolocation.Geolocation
|
||||||
| Permissions.Permissions
|
| Permissions.Permissions
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ import { Route as TimeImport } from './routes/time'
|
|||||||
import { Route as TestsImport } from './routes/tests'
|
import { Route as TestsImport } from './routes/tests'
|
||||||
import { Route as PromiseImport } from './routes/promise'
|
import { Route as PromiseImport } from './routes/promise'
|
||||||
import { Route as LazyrefImport } from './routes/lazyref'
|
import { Route as LazyrefImport } from './routes/lazyref'
|
||||||
import { Route as EffectComponentTestsImport } from './routes/effect-component-tests'
|
|
||||||
import { Route as CountImport } from './routes/count'
|
import { Route as CountImport } from './routes/count'
|
||||||
import { Route as BlankImport } from './routes/blank'
|
import { Route as BlankImport } from './routes/blank'
|
||||||
import { Route as IndexImport } from './routes/index'
|
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 QueryUsequeryImport } from './routes/query/usequery'
|
||||||
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||||
import { Route as QueryServiceImport } from './routes/query/service'
|
import { Route as QueryServiceImport } from './routes/query/service'
|
||||||
@@ -57,12 +55,6 @@ const LazyrefRoute = LazyrefImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const EffectComponentTestsRoute = EffectComponentTestsImport.update({
|
|
||||||
id: '/effect-component-tests',
|
|
||||||
path: '/effect-component-tests',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const CountRoute = CountImport.update({
|
const CountRoute = CountImport.update({
|
||||||
id: '/count',
|
id: '/count',
|
||||||
path: '/count',
|
path: '/count',
|
||||||
@@ -81,12 +73,6 @@ const IndexRoute = IndexImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const StreamsPullRoute = StreamsPullImport.update({
|
|
||||||
id: '/streams/pull',
|
|
||||||
path: '/streams/pull',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||||
id: '/query/usequery',
|
id: '/query/usequery',
|
||||||
path: '/query/usequery',
|
path: '/query/usequery',
|
||||||
@@ -130,13 +116,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof CountImport
|
preLoaderRoute: typeof CountImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/effect-component-tests': {
|
|
||||||
id: '/effect-component-tests'
|
|
||||||
path: '/effect-component-tests'
|
|
||||||
fullPath: '/effect-component-tests'
|
|
||||||
preLoaderRoute: typeof EffectComponentTestsImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/lazyref': {
|
'/lazyref': {
|
||||||
id: '/lazyref'
|
id: '/lazyref'
|
||||||
path: '/lazyref'
|
path: '/lazyref'
|
||||||
@@ -193,13 +172,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof QueryUsequeryImport
|
preLoaderRoute: typeof QueryUsequeryImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/streams/pull': {
|
|
||||||
id: '/streams/pull'
|
|
||||||
path: '/streams/pull'
|
|
||||||
fullPath: '/streams/pull'
|
|
||||||
preLoaderRoute: typeof StreamsPullImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +181,6 @@ export interface FileRoutesByFullPath {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
@@ -218,14 +189,12 @@ export interface FileRoutesByFullPath {
|
|||||||
'/query/service': typeof QueryServiceRoute
|
'/query/service': typeof QueryServiceRoute
|
||||||
'/query/usemutation': typeof QueryUsemutationRoute
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
'/streams/pull': typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
@@ -234,7 +203,6 @@ export interface FileRoutesByTo {
|
|||||||
'/query/service': typeof QueryServiceRoute
|
'/query/service': typeof QueryServiceRoute
|
||||||
'/query/usemutation': typeof QueryUsemutationRoute
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
'/streams/pull': typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -242,7 +210,6 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
'/effect-component-tests': typeof EffectComponentTestsRoute
|
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
@@ -251,7 +218,6 @@ export interface FileRoutesById {
|
|||||||
'/query/service': typeof QueryServiceRoute
|
'/query/service': typeof QueryServiceRoute
|
||||||
'/query/usemutation': typeof QueryUsemutationRoute
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
'/streams/pull': typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@@ -260,7 +226,6 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
| '/effect-component-tests'
|
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
@@ -269,13 +234,11 @@ export interface FileRouteTypes {
|
|||||||
| '/query/service'
|
| '/query/service'
|
||||||
| '/query/usemutation'
|
| '/query/usemutation'
|
||||||
| '/query/usequery'
|
| '/query/usequery'
|
||||||
| '/streams/pull'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
| '/effect-component-tests'
|
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
@@ -284,13 +247,11 @@ export interface FileRouteTypes {
|
|||||||
| '/query/service'
|
| '/query/service'
|
||||||
| '/query/usemutation'
|
| '/query/usemutation'
|
||||||
| '/query/usequery'
|
| '/query/usequery'
|
||||||
| '/streams/pull'
|
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
| '/effect-component-tests'
|
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
@@ -299,7 +260,6 @@ export interface FileRouteTypes {
|
|||||||
| '/query/service'
|
| '/query/service'
|
||||||
| '/query/usemutation'
|
| '/query/usemutation'
|
||||||
| '/query/usequery'
|
| '/query/usequery'
|
||||||
| '/streams/pull'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +267,6 @@ export interface RootRouteChildren {
|
|||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BlankRoute: typeof BlankRoute
|
BlankRoute: typeof BlankRoute
|
||||||
CountRoute: typeof CountRoute
|
CountRoute: typeof CountRoute
|
||||||
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
|
|
||||||
LazyrefRoute: typeof LazyrefRoute
|
LazyrefRoute: typeof LazyrefRoute
|
||||||
PromiseRoute: typeof PromiseRoute
|
PromiseRoute: typeof PromiseRoute
|
||||||
TestsRoute: typeof TestsRoute
|
TestsRoute: typeof TestsRoute
|
||||||
@@ -316,14 +275,12 @@ export interface RootRouteChildren {
|
|||||||
QueryServiceRoute: typeof QueryServiceRoute
|
QueryServiceRoute: typeof QueryServiceRoute
|
||||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||||
StreamsPullRoute: typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BlankRoute: BlankRoute,
|
BlankRoute: BlankRoute,
|
||||||
CountRoute: CountRoute,
|
CountRoute: CountRoute,
|
||||||
EffectComponentTestsRoute: EffectComponentTestsRoute,
|
|
||||||
LazyrefRoute: LazyrefRoute,
|
LazyrefRoute: LazyrefRoute,
|
||||||
PromiseRoute: PromiseRoute,
|
PromiseRoute: PromiseRoute,
|
||||||
TestsRoute: TestsRoute,
|
TestsRoute: TestsRoute,
|
||||||
@@ -332,7 +289,6 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
QueryServiceRoute: QueryServiceRoute,
|
QueryServiceRoute: QueryServiceRoute,
|
||||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||||
StreamsPullRoute: StreamsPullRoute,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@@ -348,7 +304,6 @@ export const routeTree = rootRoute
|
|||||||
"/",
|
"/",
|
||||||
"/blank",
|
"/blank",
|
||||||
"/count",
|
"/count",
|
||||||
"/effect-component-tests",
|
|
||||||
"/lazyref",
|
"/lazyref",
|
||||||
"/promise",
|
"/promise",
|
||||||
"/tests",
|
"/tests",
|
||||||
@@ -356,8 +311,7 @@ export const routeTree = rootRoute
|
|||||||
"/todos",
|
"/todos",
|
||||||
"/query/service",
|
"/query/service",
|
||||||
"/query/usemutation",
|
"/query/usemutation",
|
||||||
"/query/usequery",
|
"/query/usequery"
|
||||||
"/streams/pull"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@@ -369,9 +323,6 @@ export const routeTree = rootRoute
|
|||||||
"/count": {
|
"/count": {
|
||||||
"filePath": "count.tsx"
|
"filePath": "count.tsx"
|
||||||
},
|
},
|
||||||
"/effect-component-tests": {
|
|
||||||
"filePath": "effect-component-tests.tsx"
|
|
||||||
},
|
|
||||||
"/lazyref": {
|
"/lazyref": {
|
||||||
"filePath": "lazyref.tsx"
|
"filePath": "lazyref.tsx"
|
||||||
},
|
},
|
||||||
@@ -395,9 +346,6 @@ export const routeTree = rootRoute
|
|||||||
},
|
},
|
||||||
"/query/usequery": {
|
"/query/usequery": {
|
||||||
"filePath": "query/usequery.tsx"
|
"filePath": "query/usequery.tsx"
|
||||||
},
|
|
||||||
"/streams/pull": {
|
|
||||||
"filePath": "streams/pull.tsx"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { R } from "@/reffuse"
|
import { R } from "@/reffuse"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Effect, Ref } from "effect"
|
import { Ref } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/count")({
|
export const Route = createFileRoute("/count")({
|
||||||
@@ -11,13 +11,14 @@ function Count() {
|
|||||||
|
|
||||||
const runSync = R.useRunSync()
|
const runSync = R.useRunSync()
|
||||||
|
|
||||||
const countRef = R.useRef(() => Effect.succeed(0))
|
const countRef = R.useRef(0)
|
||||||
const [count] = R.useSubscribeRefs(countRef)
|
const [count] = R.useRefState(countRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
|
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
||||||
|
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
|
||||||
count is {count}
|
count is {count}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Box, TextField } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
|
|
||||||
import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
|
|
||||||
|
|
||||||
|
|
||||||
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
|
|
||||||
Console.log("Runtime built."),
|
|
||||||
() => Console.log("Runtime destroyed."),
|
|
||||||
))
|
|
||||||
|
|
||||||
class TestService extends Effect.Service<TestService>()("TestService", {
|
|
||||||
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
class SubService extends Effect.Service<SubService>()("SubService", {
|
|
||||||
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
|
|
||||||
Layer.provideMerge(LogLive),
|
|
||||||
Layer.provideMerge(TestService.Default),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/effect-component-tests")({
|
|
||||||
component: RouteComponent,
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return (
|
|
||||||
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
|
|
||||||
<MyRoute />
|
|
||||||
</ReactManagedRuntime.AsyncProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const MyRoute = pipe(
|
|
||||||
Effect.fn(function*() {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
|
|
||||||
const service = yield* TestService
|
|
||||||
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
|
||||||
|
|
||||||
// const MyTestComponentFC = yield* Effect.provide(
|
|
||||||
// ReactComponent.useFC(MyTestComponent),
|
|
||||||
// yield* ReactHook.useMemoLayer(SubService.Default),
|
|
||||||
// )
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<Box>
|
|
||||||
<TextField.Root
|
|
||||||
value={value}
|
|
||||||
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
|
|
||||||
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{/* {Array.range(0, 3).map(k =>
|
|
||||||
<MyTestComponentFC key={k} />
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{yield* pipe(
|
|
||||||
Array.range(0, 3),
|
|
||||||
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
|
|
||||||
<FC key={k} />
|
|
||||||
)),
|
|
||||||
Effect.all,
|
|
||||||
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}),
|
|
||||||
|
|
||||||
ReactComponent.withDisplayName("MyRoute"),
|
|
||||||
ReactComponent.withRuntime(runtime.context),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const MyTestComponent = pipe(
|
|
||||||
Effect.fn(function*() {
|
|
||||||
const runtime = yield* Effect.runtime()
|
|
||||||
|
|
||||||
const service = yield* SubService
|
|
||||||
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
|
||||||
|
|
||||||
// yield* ReactHook.useMemo(() => Effect.andThen(
|
|
||||||
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
|
|
||||||
// Console.log("MyTestComponent mounted"),
|
|
||||||
// ), [])
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<Box>
|
|
||||||
<TextField.Root
|
|
||||||
value={value}
|
|
||||||
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
}),
|
|
||||||
|
|
||||||
ReactComponent.withDisplayName("MyTestComponent"),
|
|
||||||
)
|
|
||||||
@@ -4,7 +4,7 @@ import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
|
|||||||
import { R } from "@/reffuse"
|
import { R } from "@/reffuse"
|
||||||
import { HttpClient } from "@effect/platform"
|
import { HttpClient } from "@effect/platform"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Console, Effect, Layer, Schema } from "effect"
|
import { Console, Effect, Schema } from "effect"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|
||||||
@@ -14,21 +14,18 @@ export const Route = createFileRoute("/query/service")({
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const query = R.useQuery({
|
const query = R.useQuery({
|
||||||
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
key: R.useStreamFromValues(["uuid4", 10 as number]),
|
||||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
Effect.andThen(Effect.sleep("500 millis")),
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
Effect.andThen(Effect.map(
|
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
HttpClient.HttpClient,
|
|
||||||
HttpClient.withTracerPropagation(false),
|
HttpClient.withTracerPropagation(false),
|
||||||
)),
|
|
||||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
|
||||||
Effect.flatMap(res => res.json),
|
Effect.flatMap(res => res.json),
|
||||||
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query])
|
const layer = useMemo(() => query.layer(Uuid4Query.Uuid4Query), [query])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryContext.Provider layer={layer}>
|
<QueryContext.Provider layer={layer}>
|
||||||
|
|||||||
@@ -29,18 +29,15 @@ function RouteComponent() {
|
|||||||
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
||||||
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
||||||
)),
|
)),
|
||||||
Effect.andThen(Effect.map(
|
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
HttpClient.HttpClient,
|
|
||||||
HttpClient.withTracerPropagation(false),
|
HttpClient.withTracerPropagation(false),
|
||||||
)),
|
|
||||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
|
||||||
Effect.flatMap(res => res.json),
|
Effect.flatMap(res => res.json),
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [state] = R.useSubscribeRefs(mutation.stateRef)
|
const [state] = R.useSubscribeRefs(mutation.state)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,21 +20,18 @@ function RouteComponent() {
|
|||||||
const [count, setCount] = useState(1)
|
const [count, setCount] = useState(1)
|
||||||
|
|
||||||
const query = R.useQuery({
|
const query = R.useQuery({
|
||||||
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
key: R.useStreamFromValues(["uuid4", count]),
|
||||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
Effect.andThen(Effect.sleep("500 millis")),
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
Effect.andThen(Effect.map(
|
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
HttpClient.HttpClient,
|
|
||||||
HttpClient.withTracerPropagation(false),
|
HttpClient.withTracerPropagation(false),
|
||||||
)),
|
|
||||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
|
||||||
Effect.flatMap(res => res.json),
|
Effect.flatMap(res => res.json),
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
const [state] = R.useSubscribeRefs(query.state)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
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,21 +2,7 @@ import { R } from "@/reffuse"
|
|||||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
import { Console, Effect, Option } from "effect"
|
import { Console, Effect, Ref } from "effect"
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
interface Node {
|
|
||||||
value: string
|
|
||||||
left?: Leaf
|
|
||||||
right?: Leaf
|
|
||||||
}
|
|
||||||
interface Leaf {
|
|
||||||
node: Node
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/tests")({
|
export const Route = createFileRoute("/tests")({
|
||||||
@@ -24,39 +10,40 @@ export const Route = createFileRoute("/tests")({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const runSync = R.useRunSync()
|
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||||
|
// Effect.andThen(makeUuid4),
|
||||||
|
// Effect.provide(GetRandomValues.CryptoRandom),
|
||||||
|
// ), [])
|
||||||
|
// console.log(value)
|
||||||
|
|
||||||
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
|
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||||
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
|
Effect.andThen(Console.log("ouient")),
|
||||||
Effect.tap(v => Effect.sync(() => setUuid(v)))
|
Effect.delay("1 second"),
|
||||||
), [])
|
), [])
|
||||||
|
|
||||||
const uuidStream = R.useStreamFromReactiveValues([uuid])
|
|
||||||
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
|
|
||||||
|
|
||||||
const [, scopeLayer] = R.useScope([uuid])
|
const uuidRef = R.useRef("none")
|
||||||
|
const anotherRef = R.useRef(69)
|
||||||
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
|
|
||||||
Effect.andThen(Console.log("Scope changed")),
|
|
||||||
Effect.provide(scopeLayer),
|
|
||||||
runSync,
|
|
||||||
), [scopeLayer, runSync])
|
|
||||||
|
|
||||||
|
|
||||||
const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
|
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
|
||||||
const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
|
yield* Effect.log(value)
|
||||||
|
}), [])
|
||||||
|
|
||||||
|
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
|
||||||
|
Effect.provide(GetRandomValues.CryptoRandom),
|
||||||
|
Effect.flatMap(v => Ref.set(uuidRef, v)),
|
||||||
|
), [])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" justify="center" align="center" gap="2">
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
<Text>{uuid}</Text>
|
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
|
||||||
<Button onClick={generateUuid}>Generate UUID</Button>
|
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
|
||||||
<Text>
|
</R.SubscribeRefs>
|
||||||
{Option.match(uuidStreamLatestValue, {
|
|
||||||
onSome: ([v]) => v,
|
<Button onClick={() => logValue("test")}>Log value</Button>
|
||||||
onNone: () => <></>,
|
<Button onClick={() => generateUuid()}>Generate UUID</Button>
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function Todos() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
|
<TodosContext.Provider layer={todosLayer}>
|
||||||
<VTodos />
|
<VTodos />
|
||||||
</TodosContext.Provider>
|
</TodosContext.Provider>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { QueryClient } from "@reffuse/extension-query"
|
|
||||||
import * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
|
||||||
|
|
||||||
|
|
||||||
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({
|
|
||||||
errorHandler: AppQueryErrorHandler.AppQueryErrorHandler
|
|
||||||
}) {}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { HttpClientError } from "@effect/platform"
|
|
||||||
import { QueryErrorHandler } from "@reffuse/extension-query"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export class AppQueryErrorHandler extends Effect.Service<AppQueryErrorHandler>()("AppQueryErrorHandler", {
|
|
||||||
effect: QueryErrorHandler.make<HttpClientError.HttpClientError>()(
|
|
||||||
(self, failure, defect) => self.pipe(
|
|
||||||
Effect.catchTag("RequestError", "ResponseError", failure),
|
|
||||||
Effect.catchAllDefect(defect),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}) {}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * as AppQueryClient from "./AppQueryClient"
|
export {}
|
||||||
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
|
||||||
|
|||||||
@@ -2,43 +2,68 @@ import { Todo } from "@/domain"
|
|||||||
import { KeyValueStore } from "@effect/platform"
|
import { KeyValueStore } from "@effect/platform"
|
||||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
import { PlatformError } from "@effect/platform/Error"
|
import { PlatformError } from "@effect/platform/Error"
|
||||||
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
||||||
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
||||||
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
|
||||||
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
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>
|
||||||
}>() {}
|
}>() {}
|
||||||
|
|
||||||
|
|
||||||
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
|
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(
|
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
||||||
Effect.flatMap(kv => kv.get(key)),
|
Effect.flatMap(kv => kv.get(key)),
|
||||||
Effect.flatMap(identity),
|
Effect.flatMap(identity),
|
||||||
Effect.flatMap(Schema.decode(
|
Effect.flatMap(Schema.parseJson().pipe(
|
||||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||||
|
Schema.decode,
|
||||||
)),
|
)),
|
||||||
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
|
Effect.flatMap(v => Ref.set(todos, v)),
|
||||||
|
|
||||||
|
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
|
||||||
|
|
||||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
)
|
)
|
||||||
|
|
||||||
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
|
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
|
||||||
Effect.flatMap(kv => values.pipe(
|
Effect.flatMap(([kv, values]) => values.pipe(
|
||||||
Schema.encode(
|
Schema.parseJson().pipe(
|
||||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||||
|
Schema.encode,
|
||||||
),
|
),
|
||||||
Effect.flatMap(v => kv.set(key, v)),
|
Effect.flatMap(v => kv.set(key, v)),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
)
|
)
|
||||||
|
|
||||||
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
|
||||||
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
|
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
|
||||||
const save = Effect.flatMap(todos, writeToLocalStorage)
|
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
|
||||||
|
|
||||||
// Sync changes with local storage
|
// const moveUp = (index: number) => Effect.gen(function*() {
|
||||||
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
|
|
||||||
|
|
||||||
return { todos, load, save }
|
// })
|
||||||
|
|
||||||
|
yield* readFromLocalStorage
|
||||||
|
|
||||||
|
return {
|
||||||
|
todos,
|
||||||
|
readFromLocalStorage,
|
||||||
|
saveToLocalStorage,
|
||||||
|
prepend,
|
||||||
|
replace,
|
||||||
|
remove,
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import { Todo } from "@/domain"
|
import { Todo } from "@/domain"
|
||||||
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { Effect, Option, SubscriptionRef } from "effect"
|
||||||
import { Chunk, Effect, Option, Ref } from "effect"
|
|
||||||
import { R } from "../reffuse"
|
import { R } from "../reffuse"
|
||||||
import { TodosState } from "../services"
|
import { TodosState } from "../services"
|
||||||
|
|
||||||
|
|
||||||
const createEmptyTodo = makeUuid4.pipe(
|
const createEmptyTodo = Todo.generateUniqueID.pipe(
|
||||||
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
|
Effect.map(id => Todo.Todo.make({
|
||||||
Effect.provide(GetRandomValues.CryptoRandom),
|
id,
|
||||||
|
content: "",
|
||||||
|
completedAt: Option.none(),
|
||||||
|
}, true))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
export function VNewTodo() {
|
export function VNewTodo() {
|
||||||
|
|
||||||
const todoRef = R.useRef(() => createEmptyTodo)
|
const runSync = R.useRunSync()
|
||||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
|
|
||||||
|
|
||||||
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
|
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)), [])
|
||||||
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
|
const [todo, setTodo] = R.useRefState(todoRef)
|
||||||
Effect.andThen(createEmptyTodo),
|
|
||||||
Effect.flatMap(v => Ref.set(todoRef, v)),
|
|
||||||
), [todoRef])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,12 +27,23 @@ export function VNewTodo() {
|
|||||||
<Card>
|
<Card>
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
<TextArea
|
<TextArea
|
||||||
value={content}
|
value={todo.content}
|
||||||
onChange={e => setContent(e.target.value)}
|
onChange={e => setTodo(prev =>
|
||||||
|
Todo.Todo.make({ ...prev, content: e.target.value }, true)
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex direction="row" justify="center" align="center">
|
<Flex direction="row" justify="center" align="center">
|
||||||
<Button onClick={add}>Add</Button>
|
<Button
|
||||||
|
onClick={() => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.prepend(todo)),
|
||||||
|
Effect.andThen(createEmptyTodo),
|
||||||
|
Effect.map(setTodo),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
import { Todo } from "@/domain"
|
import { Todo } from "@/domain"
|
||||||
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||||
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Delete } from "lucide-react"
|
import { Delete } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { R } from "../reffuse"
|
import { R } from "../reffuse"
|
||||||
|
import { TodosState } from "../services"
|
||||||
|
|
||||||
|
|
||||||
export interface VTodoProps {
|
export interface VTodoProps {
|
||||||
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
|
readonly index: number
|
||||||
readonly remove: Effect.Effect<void>
|
readonly todo: Todo.Todo
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VTodo({ todoRef, remove }: VTodoProps) {
|
export function VTodo({ index, todo }: VTodoProps) {
|
||||||
|
|
||||||
const runSync = R.useRunSync()
|
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)
|
const editorMode = useState(false)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,8 +23,14 @@ export function VTodo({ todoRef, remove }: VTodoProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<Flex direction="column" align="stretch" gap="1">
|
<Flex direction="column" align="stretch" gap="1">
|
||||||
<TextArea
|
<TextArea
|
||||||
value={content}
|
value={todo.content}
|
||||||
onChange={e => setContent(e.target.value)}
|
onChange={e => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.replace(
|
||||||
|
index,
|
||||||
|
Todo.Todo.make({ ...todo, content: e.target.value }, true),
|
||||||
|
)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
disabled={!editorMode}
|
disabled={!editorMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -40,7 +38,12 @@ export function VTodo({ todoRef, remove }: VTodoProps) {
|
|||||||
<Box></Box>
|
<Box></Box>
|
||||||
|
|
||||||
<Flex direction="row" align="center" gap="1">
|
<Flex direction="row" align="center" gap="1">
|
||||||
<IconButton onClick={() => runSync(remove)}>
|
<IconButton
|
||||||
|
onClick={() => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.remove(index)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box, Flex } from "@radix-ui/themes"
|
import { Box, Flex } from "@radix-ui/themes"
|
||||||
import { Chunk, Effect, Ref } from "effect"
|
import { Chunk, Effect, Stream } from "effect"
|
||||||
import { R } from "../reffuse"
|
import { R } from "../reffuse"
|
||||||
import { TodosState } from "../services"
|
import { TodosState } from "../services"
|
||||||
import { VNewTodo } from "./VNewTodo"
|
import { VNewTodo } from "./VNewTodo"
|
||||||
@@ -8,8 +8,15 @@ import { VTodo } from "./VTodo"
|
|||||||
|
|
||||||
export function VTodos() {
|
export function VTodos() {
|
||||||
|
|
||||||
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
|
// Sync changes to the todos with the local storage
|
||||||
const [todos] = R.useSubscribeRefs(todosRef)
|
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 [todos] = R.useRefState(todosRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -20,16 +27,7 @@ export function VTodos() {
|
|||||||
|
|
||||||
{Chunk.map(todos, (todo, index) => (
|
{Chunk.map(todos, (todo, index) => (
|
||||||
<Box key={todo.id} width="500px">
|
<Box key={todo.id} width="500px">
|
||||||
<R.SubRefFromGetSet
|
<VTodo index={index} todo={todo} />
|
||||||
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>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
9
packages/extension-form/README.md
Normal file
9
packages/extension-form/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# LazyRef extension for Reffuse
|
||||||
|
|
||||||
|
Extension to integrate `@typed/lazy-ref` with Reffuse.
|
||||||
|
|
||||||
|
## Peer dependencies
|
||||||
|
- `@typed/lazy-ref`
|
||||||
|
- `reffuse` 0.1.3+
|
||||||
|
- `effect` 3.13+
|
||||||
|
- `react` & `@types/react` 19+
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-components",
|
"name": "@reffuse/extension-form",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -16,10 +16,6 @@
|
|||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./types": {
|
|
||||||
"types": "./dist/types/index.d.ts",
|
|
||||||
"default": "./dist/types/index.js"
|
|
||||||
},
|
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./dist/*.d.ts",
|
"types": "./dist/*.d.ts",
|
||||||
"default": "./dist/*.js"
|
"default": "./dist/*.js"
|
||||||
@@ -33,12 +29,12 @@
|
|||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "^19.0.0",
|
|
||||||
"effect": "^3.15.0",
|
|
||||||
"react": "^19.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/language-service": "^0.23.3"
|
"reffuse": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"effect": "^3.13.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"reffuse": "^0.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
9
packages/extension-form/src/FormExtension.ts
Normal file
9
packages/extension-form/src/FormExtension.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||||
|
|
||||||
|
|
||||||
|
export const FormExtension = ReffuseExtension.make(() => ({
|
||||||
|
useForm<A, E, R>(
|
||||||
|
this: ReffuseNamespace.ReffuseNamespace<R>,
|
||||||
|
) {
|
||||||
|
},
|
||||||
|
}))
|
||||||
1
packages/extension-form/src/index.ts
Normal file
1
packages/extension-form/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./FormExtension.js"
|
||||||
6
packages/extension-form/src/internal/Form.ts
Normal file
6
packages/extension-form/src/internal/Form.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface Form<A, I, R> {
|
||||||
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
|
}
|
||||||
69
packages/extension-form/src/internal/FormField.ts
Normal file
69
packages/extension-form/src/internal/FormField.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { Effect, Schema } from "effect"
|
||||||
|
import type * as Formify from "./Formify.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface FormField<S extends Schema.Schema.Any> {
|
||||||
|
readonly schema: S
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeFormField = <S extends Schema.Schema.Any>(
|
||||||
|
schema: S,
|
||||||
|
get: Effect.Effect<S["Type"]>,
|
||||||
|
set: (value: S["Type"]) => Effect.Effect<void>,
|
||||||
|
): FormField<S> => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnionFormField<
|
||||||
|
S extends Schema.Union<Members>,
|
||||||
|
Members extends ReadonlyArray<Schema.Schema.All>,
|
||||||
|
> extends FormField<S> {
|
||||||
|
readonly member: Formify.Formify<Members[number]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TupleFormField<
|
||||||
|
S extends Schema.TupleType<Elements, Rest>,
|
||||||
|
Elements extends Schema.TupleType.Elements,
|
||||||
|
Rest extends Schema.TupleType.Rest,
|
||||||
|
> extends FormField<S> {
|
||||||
|
readonly elements: [...{ readonly [K in keyof Elements]: Formify.Formify<Elements[K]> }]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArrayFormField<
|
||||||
|
S extends Schema.Array$<Value>,
|
||||||
|
Value extends Schema.Schema.Any,
|
||||||
|
> extends FormField<S> {
|
||||||
|
readonly elements: readonly Formify.Formify<Value>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StructFormField<
|
||||||
|
S extends Schema.Struct<Fields>,
|
||||||
|
Fields extends Schema.Struct.Fields,
|
||||||
|
> = (
|
||||||
|
& FormField<S>
|
||||||
|
& { readonly fields: { readonly [K in keyof Fields]: Formify.Formify<Fields[K]> } }
|
||||||
|
& {
|
||||||
|
[K in keyof Fields as Fields[K] extends
|
||||||
|
Schema.tag<infer _> ? K : never
|
||||||
|
]: Fields[K] extends
|
||||||
|
Schema.tag<infer Tag> ? Tag : never
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface GenericFormField<S extends Schema.Schema.Any> extends FormField<S> {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface PropertySignatureFormField<
|
||||||
|
S extends Schema.PropertySignature<TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R>,
|
||||||
|
TypeToken extends Schema.PropertySignature.Token,
|
||||||
|
Type,
|
||||||
|
Key extends PropertyKey,
|
||||||
|
EncodedToken extends Schema.PropertySignature.Token,
|
||||||
|
Encoded,
|
||||||
|
HasDefault extends boolean = false,
|
||||||
|
R = never,
|
||||||
|
> {
|
||||||
|
readonly propertySignature: S
|
||||||
|
readonly value: Type
|
||||||
|
}
|
||||||
51
packages/extension-form/src/internal/Formify.ts
Normal file
51
packages/extension-form/src/internal/Formify.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
import type * as FormField from "./FormField.js"
|
||||||
|
|
||||||
|
|
||||||
|
export type Formify<S> = (
|
||||||
|
S extends Schema.Union<infer Members> ? FormField.UnionFormField<S, Members> :
|
||||||
|
S extends Schema.TupleType<infer Elements, infer Rest> ? FormField.TupleFormField<S, Elements, Rest> :
|
||||||
|
S extends Schema.Array$<infer Value> ? FormField.ArrayFormField<S, Value> :
|
||||||
|
S extends Schema.Struct<infer Fields> ? FormField.StructFormField<S, Fields> :
|
||||||
|
S extends Schema.Schema.Any ? FormField.GenericFormField<S> :
|
||||||
|
S extends Schema.PropertySignature<
|
||||||
|
infer TypeToken,
|
||||||
|
infer Type,
|
||||||
|
infer Key,
|
||||||
|
infer EncodedToken,
|
||||||
|
infer Encoded,
|
||||||
|
infer HasDefault,
|
||||||
|
infer R
|
||||||
|
> ? FormField.PropertySignatureFormField<S, TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R> :
|
||||||
|
never
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const Login = Schema.Union(
|
||||||
|
Schema.Struct({
|
||||||
|
_tag: Schema.tag("ByEmail"),
|
||||||
|
email: Schema.String,
|
||||||
|
password: Schema.RedactedFromSelf(Schema.String),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Schema.Struct({
|
||||||
|
_tag: Schema.tag("ByPhone"),
|
||||||
|
phone: Schema.String,
|
||||||
|
password: Schema.RedactedFromSelf(Schema.String),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Schema.TaggedStruct("ByKey", {
|
||||||
|
id: Schema.String,
|
||||||
|
password: Schema.RedactedFromSelf(Schema.String),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
type LoginForm = Formify<typeof Login>
|
||||||
|
declare const loginForm: LoginForm
|
||||||
|
|
||||||
|
switch (loginForm.member._tag) {
|
||||||
|
case "ByEmail":
|
||||||
|
loginForm.member
|
||||||
|
break
|
||||||
|
case "ByPhone":
|
||||||
|
break
|
||||||
|
}
|
||||||
37
packages/extension-form/src/internal/guards.ts
Normal file
37
packages/extension-form/src/internal/guards.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Array, Predicate, Record, Schema, Tuple } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export const isTupleSchema = (u: unknown): u is Schema.Tuple<any> => (
|
||||||
|
Schema.isSchema(u) &&
|
||||||
|
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) &&
|
||||||
|
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const isArraySchema = (u: unknown): u is Schema.Array$<any> => (
|
||||||
|
Schema.isSchema(u) &&
|
||||||
|
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) && Array.isEmptyArray(u.elements) &&
|
||||||
|
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest) && Tuple.isTupleOf(u.rest, 1) &&
|
||||||
|
Predicate.hasProperty(u, "value")
|
||||||
|
)
|
||||||
|
|
||||||
|
export const isStructSchema = (u: unknown): u is Schema.Struct<any> => (
|
||||||
|
Schema.isSchema(u) &&
|
||||||
|
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) &&
|
||||||
|
Predicate.hasProperty(u, "records") && Array.isArray(u.records) && Array.isEmptyArray(u.records)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const isRecordSchema = (u: unknown): u is Schema.Record$<any, any> => (
|
||||||
|
Schema.isSchema(u) &&
|
||||||
|
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) && Record.isEmptyRecord(u.fields) &&
|
||||||
|
Predicate.hasProperty(u, "records") && Array.isArray(u.records) &&
|
||||||
|
Predicate.hasProperty(u, "key") &&
|
||||||
|
Predicate.hasProperty(u, "value")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const myTuple = Schema.Tuple(Schema.String)
|
||||||
|
const myArray = Schema.Array(Schema.String)
|
||||||
|
const myStruct = Schema.Struct({})
|
||||||
|
const myRecord = Schema.Record({ key: Schema.String, value: Schema.String })
|
||||||
|
|
||||||
|
console.log(isArraySchema(myTuple))
|
||||||
1
packages/extension-form/src/internal/index.ts
Normal file
1
packages/extension-form/src/internal/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as Form from "./Form.js"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@reffuse/extension-lazyref",
|
"name": "@reffuse/extension-lazyref",
|
||||||
"version": "0.1.4",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typed/lazy-ref": "^0.3.0",
|
"@typed/lazy-ref": "^0.3.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"effect": "^3.15.0",
|
"effect": "^3.13.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"reffuse": "^0.1.8"
|
"reffuse": "^0.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as LazyRef from "@typed/lazy-ref"
|
import * as LazyRef from "@typed/lazy-ref"
|
||||||
import { Effect, pipe, Stream } from "effect"
|
import { Effect, pipe, Stream } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
import { ReffuseExtension, type ReffuseNamespace, SetStateAction } from "reffuse"
|
||||||
import { SetStateAction } from "reffuse/types"
|
|
||||||
|
|
||||||
|
|
||||||
export const LazyRefExtension = ReffuseExtension.make(() => ({
|
export const LazyRefExtension = ReffuseExtension.make(() => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@reffuse/extension-query",
|
"name": "@reffuse/extension-query",
|
||||||
"version": "0.1.5",
|
"version": "0.1.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
"@effect/platform-browser": "^0.56.0",
|
"@effect/platform-browser": "^0.56.0",
|
||||||
"@typed/async-data": "^0.13.0",
|
"@typed/async-data": "^0.13.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"effect": "^3.15.0",
|
"effect": "^3.13.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"reffuse": "^0.1.11"
|
"reffuse": "^0.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
packages/extension-query/src/MutationService.ts
Normal file
16
packages/extension-query/src/MutationService.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type * as AsyncData from "@typed/async-data"
|
||||||
|
import { Effect, type Fiber, type Stream, type SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface MutationService<K extends readonly unknown[], A, E> {
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
|
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never,
|
||||||
|
>() => Effect.Tag(id)<Self, MutationService<K, A, E>>()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Context, Effect, identity, Layer } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import type { Mutable } from "effect/Types"
|
import type { Mutable } from "effect/Types"
|
||||||
import * as QueryErrorHandler from "./QueryErrorHandler.js"
|
import * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||||
|
|
||||||
@@ -8,17 +8,6 @@ export interface QueryClient<FallbackA, HandledE> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<FallbackA, HandledE> {
|
|
||||||
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <FallbackA, HandledE>(
|
|
||||||
{ errorHandler }: MakeProps<FallbackA, HandledE>
|
|
||||||
): Effect.Effect<QueryClient<FallbackA, HandledE>> => Effect.Do.pipe(
|
|
||||||
Effect.let("errorHandler", () => errorHandler)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const id = "@reffuse/extension-query/QueryClient"
|
const id = "@reffuse/extension-query/QueryClient"
|
||||||
|
|
||||||
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
|
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
|
||||||
@@ -30,28 +19,46 @@ export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
|
|||||||
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
|
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
|
||||||
|
|
||||||
|
|
||||||
export interface ServiceProps<FallbackA = never, HandledE = never, E = never, R = never> {
|
export interface ServiceProps<EH, FallbackA, HandledE> {
|
||||||
readonly errorHandler?: Effect.Effect<QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, E, R>
|
readonly ErrorHandler?: Context.Tag<EH, QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
|
export interface ServiceResult<Self, EH, FallbackA, HandledE> extends Context.TagClass<
|
||||||
Self,
|
Self,
|
||||||
typeof id,
|
typeof id,
|
||||||
QueryClient<FallbackA, HandledE>
|
QueryClient<FallbackA, HandledE>
|
||||||
> {
|
> {
|
||||||
readonly Default: Layer.Layer<Self, E, R>
|
readonly Live: Layer.Layer<
|
||||||
|
Self | (EH extends QueryErrorHandler.DefaultQueryErrorHandler ? EH : never),
|
||||||
|
never,
|
||||||
|
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Service = <Self>() => (
|
export const Service = <Self>() => (
|
||||||
<FallbackA = never, HandledE = never, E = never, R = never>(
|
<
|
||||||
props?: ServiceProps<FallbackA, HandledE, E, R>
|
EH = QueryErrorHandler.DefaultQueryErrorHandler,
|
||||||
): ServiceResult<Self, FallbackA, HandledE, E, R> => {
|
FallbackA = QueryErrorHandler.Fallback<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>,
|
||||||
const TagClass = Context.Tag(id)() as ServiceResult<Self, FallbackA, HandledE, E, R>
|
HandledE = QueryErrorHandler.Error<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>,
|
||||||
|
>(
|
||||||
|
props?: ServiceProps<EH, FallbackA, HandledE>
|
||||||
|
): ServiceResult<Self, EH, FallbackA, HandledE> => {
|
||||||
|
const TagClass = Context.Tag(id)() as ServiceResult<Self, EH, FallbackA, HandledE>
|
||||||
|
|
||||||
(TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
|
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.Do.pipe(
|
||||||
props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
|
Effect.bind("errorHandler", () =>
|
||||||
errorHandler => make({ errorHandler }),
|
(props?.ErrorHandler ?? QueryErrorHandler.DefaultQueryErrorHandler) as Effect.Effect<
|
||||||
))
|
QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>,
|
||||||
|
never,
|
||||||
|
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
|
||||||
|
>
|
||||||
|
)
|
||||||
|
)).pipe(
|
||||||
|
Layer.provideMerge((props?.ErrorHandler
|
||||||
|
? Layer.empty
|
||||||
|
: QueryErrorHandler.DefaultQueryErrorHandler.Live
|
||||||
|
) as Layer.Layer<EH>)
|
||||||
|
)
|
||||||
|
|
||||||
return TagClass
|
return TagClass
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Cause, Effect, PubSub, Stream } from "effect"
|
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect"
|
||||||
|
import type { Mutable } from "effect/Types"
|
||||||
|
|
||||||
|
|
||||||
export interface QueryErrorHandler<FallbackA, HandledE> {
|
export interface QueryErrorHandler<FallbackA, HandledE> {
|
||||||
@@ -10,31 +11,55 @@ export type Fallback<T> = T extends QueryErrorHandler<infer A, any> ? A : never
|
|||||||
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
|
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
|
||||||
|
|
||||||
|
|
||||||
export const make = <HandledE = never>() => (
|
export interface ServiceResult<
|
||||||
<FallbackA>(
|
Self,
|
||||||
|
Id extends string,
|
||||||
|
FallbackA,
|
||||||
|
HandledE,
|
||||||
|
> extends Context.TagClass<
|
||||||
|
Self,
|
||||||
|
Id,
|
||||||
|
QueryErrorHandler<FallbackA, HandledE>
|
||||||
|
> {
|
||||||
|
readonly Live: Layer.Layer<Self>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Service = <Self, HandledE = never>() => (
|
||||||
|
<const Id extends string, FallbackA>(
|
||||||
|
id: Id,
|
||||||
f: (
|
f: (
|
||||||
self: Effect.Effect<never, HandledE>,
|
self: Effect.Effect<never, HandledE>,
|
||||||
failure: (failure: HandledE) => Effect.Effect<never>,
|
failure: (failure: HandledE) => Effect.Effect<never>,
|
||||||
defect: (defect: unknown) => Effect.Effect<never>,
|
defect: (defect: unknown) => Effect.Effect<never>,
|
||||||
) => Effect.Effect<FallbackA>
|
) => Effect.Effect<FallbackA>,
|
||||||
): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
|
): ServiceResult<Self, Id, FallbackA, HandledE> => {
|
||||||
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
|
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, FallbackA, HandledE>
|
||||||
const errors = Stream.fromPubSub(pubsub)
|
|
||||||
|
(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 handle = <A, E, R>(
|
const handle = <A, E, R>(
|
||||||
self: Effect.Effect<A, E, R>
|
self: Effect.Effect<A, E, R>
|
||||||
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
||||||
self as unknown as Effect.Effect<never, HandledE, never>,
|
self as unknown as Effect.Effect<never, HandledE, never>,
|
||||||
(failure: HandledE) => Effect.andThen(
|
(failure: HandledE) => Queue.offer(queue, Cause.fail(failure)).pipe(
|
||||||
PubSub.publish(pubsub, Cause.fail(failure)),
|
Effect.andThen(Effect.failCause(Cause.empty))
|
||||||
Effect.failCause(Cause.empty),
|
|
||||||
),
|
),
|
||||||
(defect: unknown) => Effect.andThen(
|
(defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe(
|
||||||
PubSub.publish(pubsub, Cause.die(defect)),
|
Effect.andThen(Effect.failCause(Cause.empty))
|
||||||
Effect.failCause(Cause.empty),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return { errors, handle }
|
return { errors, handle }
|
||||||
})
|
}))
|
||||||
|
|
||||||
|
return TagClass
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export class DefaultQueryErrorHandler extends Service<DefaultQueryErrorHandler>()(
|
||||||
|
"@reffuse/extension-query/DefaultQueryErrorHandler",
|
||||||
|
identity,
|
||||||
|
) {}
|
||||||
|
|||||||
@@ -1,21 +1,53 @@
|
|||||||
import type { Effect, Stream } from "effect"
|
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 ReffuseNamespace } from "reffuse"
|
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||||
import * as MutationRunner from "./MutationRunner.js"
|
import type * as MutationService from "./MutationService.js"
|
||||||
import * as QueryClient from "./QueryClient.js"
|
import * as QueryClient from "./QueryClient.js"
|
||||||
import type * as QueryProgress from "./QueryProgress.js"
|
import type * as QueryProgress from "./QueryProgress.js"
|
||||||
import * as QueryRunner from "./QueryRunner.js"
|
import type * as QueryService from "./QueryService.js"
|
||||||
|
import { MutationRunner, QueryRunner } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
||||||
readonly key: Stream.Stream<K>
|
readonly key: Stream.Stream<K>
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
readonly options?: QueryRunner.RunOptions
|
readonly refreshOnWindowFocus?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UseQueryResult<K extends readonly unknown[], A, E> {
|
||||||
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
|
||||||
|
readonly forkRefresh: Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
|
||||||
|
readonly layer: <Self, Id extends string>(
|
||||||
|
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
|
||||||
|
) => Layer.Layer<Self>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
|
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
|
||||||
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UseMutationResult<K extends readonly unknown[], A, E> {
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
|
||||||
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
|
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
|
||||||
|
readonly layer: <Self, Id extends string>(
|
||||||
|
tag: Context.TagClass<Self, Id, MutationService.MutationService<K, A, E>>
|
||||||
|
) => Layer.Layer<Self>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const QueryExtension = ReffuseExtension.make(() => ({
|
export const QueryExtension = ReffuseExtension.make(() => ({
|
||||||
useQuery<
|
useQuery<
|
||||||
@@ -29,16 +61,32 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
|||||||
>(
|
>(
|
||||||
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||||
props: UseQueryProps<QK, QA, QE, QR>,
|
props: UseQueryProps<QK, QA, QE, QR>,
|
||||||
): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||||
const runner = this.useMemo(() => QueryRunner.make({
|
const runner = this.useMemo(() => QueryRunner.make({
|
||||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||||
key: props.key,
|
key: props.key,
|
||||||
query: props.query,
|
query: props.query,
|
||||||
}), [props.key])
|
}), [props.key])
|
||||||
|
|
||||||
this.useFork(() => QueryRunner.run(runner, props.options), [runner])
|
this.useFork(() => runner.fetchOnKeyChange, [runner])
|
||||||
|
|
||||||
return runner
|
this.useFork(() => (props.refreshOnWindowFocus ?? true)
|
||||||
|
? runner.refreshOnWindowFocus
|
||||||
|
: Effect.void,
|
||||||
|
[props.refreshOnWindowFocus, runner])
|
||||||
|
|
||||||
|
return React.useMemo(() => ({
|
||||||
|
latestKey: runner.latestKeyRef,
|
||||||
|
state: runner.stateRef,
|
||||||
|
|
||||||
|
forkRefresh: runner.forkRefresh,
|
||||||
|
|
||||||
|
layer: tag => Layer.succeed(tag, {
|
||||||
|
latestKey: runner.latestKeyRef,
|
||||||
|
state: runner.stateRef,
|
||||||
|
forkRefresh: runner.forkRefresh,
|
||||||
|
}),
|
||||||
|
}), [runner])
|
||||||
},
|
},
|
||||||
|
|
||||||
useMutation<
|
useMutation<
|
||||||
@@ -52,10 +100,23 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
|||||||
>(
|
>(
|
||||||
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||||
props: UseMutationProps<QK, QA, QE, QR>,
|
props: UseMutationProps<QK, QA, QE, QR>,
|
||||||
): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
): UseMutationResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||||
return this.useMemo(() => MutationRunner.make({
|
const runner = this.useMemo(() => MutationRunner.make({
|
||||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||||
mutation: props.mutation,
|
mutation: props.mutation,
|
||||||
}), [])
|
}), [])
|
||||||
|
|
||||||
|
return React.useMemo(() => ({
|
||||||
|
state: runner.stateRef,
|
||||||
|
|
||||||
|
mutate: runner.mutate,
|
||||||
|
forkMutate: runner.forkMutate,
|
||||||
|
|
||||||
|
layer: tag => Layer.succeed(tag, {
|
||||||
|
state: runner.stateRef,
|
||||||
|
mutate: runner.mutate,
|
||||||
|
forkMutate: runner.forkMutate,
|
||||||
|
}),
|
||||||
|
}), [runner])
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryPro
|
|||||||
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
|
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
|
||||||
) => Effect.Effect<void>
|
) => Effect.Effect<void>
|
||||||
}>() {
|
}>() {
|
||||||
static readonly Default: Layer.Layer<
|
static readonly Live: Layer.Layer<
|
||||||
QueryProgress,
|
QueryProgress,
|
||||||
never,
|
never,
|
||||||
QueryState.QueryState<any, any>
|
QueryState.QueryState<any, any>
|
||||||
|
|||||||
@@ -1,193 +0,0 @@
|
|||||||
import { BrowserStream } from "@effect/platform-browser"
|
|
||||||
import * as AsyncData from "@typed/async-data"
|
|
||||||
import { type Cause, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import type * as QueryClient from "./QueryClient.js"
|
|
||||||
import * as QueryProgress from "./QueryProgress.js"
|
|
||||||
import { QueryState } from "./internal/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface QueryRunner<K extends readonly unknown[], A, E> {
|
|
||||||
readonly queryKey: Stream.Stream<K>
|
|
||||||
readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
|
||||||
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
|
|
||||||
AsyncData.Success<A> | AsyncData.Failure<E>,
|
|
||||||
Cause.NoSuchElementException
|
|
||||||
>>>
|
|
||||||
|
|
||||||
readonly interrupt: Effect.Effect<void>
|
|
||||||
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void>>
|
|
||||||
readonly forkFetch: (keyValue: K) => Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
readonly forkRefresh: Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const Tag = <const Id extends string>(id: Id) => <
|
|
||||||
Self, K extends readonly unknown[], A, E = never
|
|
||||||
>() => Effect.Tag(id)<Self, QueryRunner<K, A, E>>()
|
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
|
||||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
|
||||||
readonly key: Stream.Stream<K>
|
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
|
||||||
{
|
|
||||||
QueryClient,
|
|
||||||
key,
|
|
||||||
query,
|
|
||||||
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
|
||||||
): Effect.Effect<
|
|
||||||
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
|
||||||
never,
|
|
||||||
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
|
||||||
> => Effect.gen(function*() {
|
|
||||||
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
|
||||||
|
|
||||||
const latestKeyValueRef = yield* SubscriptionRef.make(Option.none<K>())
|
|
||||||
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
|
||||||
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
|
|
||||||
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
|
|
||||||
Cause.NoSuchElementException
|
|
||||||
>>())
|
|
||||||
|
|
||||||
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
|
||||||
|
|
||||||
const interrupt = Effect.flatMap(fiberRef, Option.match({
|
|
||||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Fiber.interrupt(fiber))
|
|
||||||
),
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const forkInterrupt = Effect.flatMap(fiberRef, Option.match({
|
|
||||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
|
||||||
Effect.asVoid,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
))
|
|
||||||
),
|
|
||||||
onNone: () => Effect.forkDaemon(Effect.void),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const run = (keyValue: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
|
||||||
Effect.flatMap(([client, state]) => Ref.set(latestKeyValueRef, Option.some(keyValue)).pipe(
|
|
||||||
Effect.andThen(query(keyValue)),
|
|
||||||
client.errorHandler.handle,
|
|
||||||
Effect.matchCauseEffect({
|
|
||||||
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
|
||||||
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.provide(context),
|
|
||||||
Effect.provide(QueryProgress.QueryProgress.Default),
|
|
||||||
)
|
|
||||||
|
|
||||||
const forkFetch = (keyValue: K) => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
|
||||||
Effect.flatMap(stateQueue => queryStateTag.pipe(
|
|
||||||
Effect.flatMap(state => interrupt.pipe(
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.addFinalizer(() => Effect.andThen(
|
|
||||||
Ref.set(fiberRef, Option.none()),
|
|
||||||
Queue.shutdown(stateQueue),
|
|
||||||
)).pipe(
|
|
||||||
Effect.andThen(state.set(AsyncData.loading())),
|
|
||||||
Effect.andThen(run(keyValue)),
|
|
||||||
Effect.scoped,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
|
||||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.provide(QueryState.layer(
|
|
||||||
queryStateTag,
|
|
||||||
stateRef,
|
|
||||||
value => Effect.andThen(
|
|
||||||
Queue.offer(stateQueue, value),
|
|
||||||
Ref.set(stateRef, value),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
const setInitialRefreshState = Effect.flatMap(queryStateTag, state => state.update(previous => {
|
|
||||||
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
|
||||||
return AsyncData.refreshing(previous)
|
|
||||||
if (AsyncData.isRefreshing(previous))
|
|
||||||
return AsyncData.refreshing(previous.previous)
|
|
||||||
return AsyncData.loading()
|
|
||||||
}))
|
|
||||||
|
|
||||||
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
|
||||||
Effect.flatMap(stateQueue => interrupt.pipe(
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.addFinalizer(() => Effect.andThen(
|
|
||||||
Ref.set(fiberRef, Option.none()),
|
|
||||||
Queue.shutdown(stateQueue),
|
|
||||||
)).pipe(
|
|
||||||
Effect.andThen(setInitialRefreshState),
|
|
||||||
Effect.andThen(latestKeyValueRef.pipe(
|
|
||||||
Effect.flatMap(identity),
|
|
||||||
Effect.flatMap(run),
|
|
||||||
)),
|
|
||||||
Effect.scoped,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
|
||||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
|
||||||
|
|
||||||
Effect.provide(QueryState.layer(
|
|
||||||
queryStateTag,
|
|
||||||
stateRef,
|
|
||||||
value => Effect.andThen(
|
|
||||||
Queue.offer(stateQueue, value),
|
|
||||||
Ref.set(stateRef, value),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
queryKey: key,
|
|
||||||
latestKeyValueRef,
|
|
||||||
stateRef,
|
|
||||||
fiberRef,
|
|
||||||
|
|
||||||
interrupt,
|
|
||||||
forkInterrupt,
|
|
||||||
forkFetch,
|
|
||||||
forkRefresh,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export interface RunOptions {
|
|
||||||
readonly refreshOnWindowFocus?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = <K extends readonly unknown[], A, E>(
|
|
||||||
self: QueryRunner<K, A, E>,
|
|
||||||
options?: RunOptions,
|
|
||||||
): Effect.Effect<void, never, Scope.Scope> => Effect.gen(function*() {
|
|
||||||
if (typeof window !== "undefined" && (options?.refreshOnWindowFocus ?? true))
|
|
||||||
yield* Effect.forkScoped(
|
|
||||||
Stream.runForEach(BrowserStream.fromEventListenerWindow("focus"), () => self.forkRefresh)
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* Effect.addFinalizer(() => self.interrupt)
|
|
||||||
yield* Stream.runForEach(Stream.changes(self.queryKey), latestKey => self.forkFetch(latestKey))
|
|
||||||
})
|
|
||||||
16
packages/extension-query/src/QueryService.ts
Normal file
16
packages/extension-query/src/QueryService.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryService<K extends readonly unknown[], A, E> {
|
||||||
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly forkRefresh: Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never,
|
||||||
|
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as MutationRunner from "./MutationRunner.js"
|
export * as MutationService from "./MutationService.js"
|
||||||
export * as QueryClient from "./QueryClient.js"
|
export * as QueryClient from "./QueryClient.js"
|
||||||
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||||
export * from "./QueryExtension.js"
|
export * from "./QueryExtension.js"
|
||||||
export * as QueryProgress from "./QueryProgress.js"
|
export * as QueryProgress from "./QueryProgress.js"
|
||||||
export * as QueryRunner from "./QueryRunner.js"
|
export * as QueryService from "./QueryService.js"
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as AsyncData from "@typed/async-data"
|
import * as AsyncData from "@typed/async-data"
|
||||||
import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
import { type Context, Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
import type * as QueryClient from "./QueryClient.js"
|
import type * as QueryClient from "../QueryClient.js"
|
||||||
import * as QueryProgress from "./QueryProgress.js"
|
import * as QueryProgress from "../QueryProgress.js"
|
||||||
import { QueryState } from "./internal/index.js"
|
import * as QueryState from "./QueryState.js"
|
||||||
|
|
||||||
|
|
||||||
export interface MutationRunner<K extends readonly unknown[], A, E> {
|
export interface MutationRunner<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly context: Context.Context<R>
|
||||||
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
|
||||||
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
@@ -16,11 +17,6 @@ export interface MutationRunner<K extends readonly unknown[], A, E> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const Tag = <const Id extends string>(id: Id) => <
|
|
||||||
Self, K extends readonly unknown[], A, E = never,
|
|
||||||
>() => Effect.Tag(id)<Self, MutationRunner<K, A, E>>()
|
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
||||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||||
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
@@ -32,7 +28,7 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
|||||||
mutation,
|
mutation,
|
||||||
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
|
||||||
never,
|
never,
|
||||||
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||||
> => Effect.gen(function*() {
|
> => Effect.gen(function*() {
|
||||||
@@ -41,18 +37,25 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
|||||||
|
|
||||||
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||||
|
|
||||||
const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
const run = (key: K) => Effect.Do.pipe(
|
||||||
Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
|
Effect.bind("state", () => queryStateTag),
|
||||||
|
Effect.bind("client", () => QueryClient),
|
||||||
|
|
||||||
|
Effect.flatMap(({ state, client }) => state.set(AsyncData.loading()).pipe(
|
||||||
Effect.andThen(mutation(key)),
|
Effect.andThen(mutation(key)),
|
||||||
client.errorHandler.handle,
|
client.errorHandler.handle,
|
||||||
Effect.matchCauseEffect({
|
Effect.matchCauseEffect({
|
||||||
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
|
||||||
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
Effect.tap(state.set)
|
||||||
|
),
|
||||||
|
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
|
||||||
|
Effect.tap(state.set)
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
Effect.provide(context),
|
Effect.provide(context),
|
||||||
Effect.provide(QueryProgress.QueryProgress.Default),
|
Effect.provide(QueryProgress.QueryProgress.Live),
|
||||||
)
|
)
|
||||||
|
|
||||||
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
|
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
|
||||||
@@ -61,11 +64,11 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
|||||||
value => Ref.set(globalStateRef, value),
|
value => Ref.set(globalStateRef, value),
|
||||||
))
|
))
|
||||||
|
|
||||||
const forkMutate = (...key: K) => Effect.all([
|
const forkMutate = (...key: K) => Effect.Do.pipe(
|
||||||
Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>()),
|
Effect.bind("stateRef", () => Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())),
|
||||||
Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>(),
|
Effect.bind("stateQueue", () => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>()),
|
||||||
]).pipe(
|
|
||||||
Effect.flatMap(([stateRef, stateQueue]) =>
|
Effect.flatMap(({ stateRef, stateQueue }) =>
|
||||||
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
|
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
|
||||||
Effect.andThen(run(key)),
|
Effect.andThen(run(key)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
191
packages/extension-query/src/internal/QueryRunner.ts
Normal file
191
packages/extension-query/src/internal/QueryRunner.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { BrowserStream } from "@effect/platform-browser"
|
||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Cause, type Context, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import type * as QueryClient from "../QueryClient.js"
|
||||||
|
import * as QueryProgress from "../QueryProgress.js"
|
||||||
|
import * as QueryState from "./QueryState.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly context: Context.Context<R>
|
||||||
|
|
||||||
|
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
|
||||||
|
AsyncData.Success<A> | AsyncData.Failure<E>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
>>>
|
||||||
|
|
||||||
|
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
||||||
|
readonly forkFetch: Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
readonly forkRefresh: Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
|
||||||
|
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
|
||||||
|
readonly refreshOnWindowFocus: Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
||||||
|
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
||||||
|
{
|
||||||
|
QueryClient,
|
||||||
|
key,
|
||||||
|
query,
|
||||||
|
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||||
|
): Effect.Effect<
|
||||||
|
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
|
||||||
|
never,
|
||||||
|
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||||
|
> => Effect.gen(function*() {
|
||||||
|
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
||||||
|
|
||||||
|
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
|
||||||
|
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
||||||
|
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
|
||||||
|
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
>>())
|
||||||
|
|
||||||
|
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||||
|
|
||||||
|
const interrupt = fiberRef.pipe(
|
||||||
|
Effect.flatMap(Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkInterrupt = fiberRef.pipe(
|
||||||
|
Effect.flatMap(Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
||||||
|
Effect.asVoid,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.forkDaemon(Effect.void),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const run = Effect.Do.pipe(
|
||||||
|
Effect.bind("state", () => queryStateTag),
|
||||||
|
Effect.bind("client", () => QueryClient),
|
||||||
|
Effect.bind("latestKey", () => latestKeyRef.pipe(Effect.flatMap(identity))),
|
||||||
|
|
||||||
|
Effect.flatMap(({ state, client, latestKey }) => query(latestKey).pipe(
|
||||||
|
client.errorHandler.handle,
|
||||||
|
Effect.matchCauseEffect({
|
||||||
|
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
|
||||||
|
Effect.tap(state.set)
|
||||||
|
),
|
||||||
|
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
|
||||||
|
Effect.tap(state.set)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(context),
|
||||||
|
Effect.provide(QueryProgress.QueryProgress.Live),
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkFetch = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||||
|
Effect.flatMap(stateQueue => queryStateTag.pipe(
|
||||||
|
Effect.flatMap(state => interrupt.pipe(
|
||||||
|
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Queue.shutdown(stateQueue))
|
||||||
|
)).pipe(
|
||||||
|
Effect.andThen(state.set(AsyncData.loading())),
|
||||||
|
Effect.andThen(run),
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||||
|
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Queue.offer(stateQueue, value).pipe(
|
||||||
|
Effect.andThen(Ref.set(stateRef, value))
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const setInitialRefreshState = queryStateTag.pipe(
|
||||||
|
Effect.flatMap(state => state.update(previous => {
|
||||||
|
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
||||||
|
return AsyncData.refreshing(previous)
|
||||||
|
if (AsyncData.isRefreshing(previous))
|
||||||
|
return AsyncData.refreshing(previous.previous)
|
||||||
|
return AsyncData.loading()
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||||
|
Effect.flatMap(stateQueue => interrupt.pipe(
|
||||||
|
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Queue.shutdown(stateQueue))
|
||||||
|
)).pipe(
|
||||||
|
Effect.andThen(setInitialRefreshState),
|
||||||
|
Effect.andThen(run),
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||||
|
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Queue.offer(stateQueue, value).pipe(
|
||||||
|
Effect.andThen(Ref.set(stateRef, value))
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
||||||
|
Effect.andThen(Stream.runForEach(Stream.changes(key), latestKey =>
|
||||||
|
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
||||||
|
Effect.andThen(forkFetch)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshOnWindowFocus = Stream.runForEach(
|
||||||
|
BrowserStream.fromEventListenerWindow("focus"),
|
||||||
|
() => forkRefresh,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
|
||||||
|
latestKeyRef,
|
||||||
|
stateRef,
|
||||||
|
fiberRef,
|
||||||
|
|
||||||
|
forkInterrupt,
|
||||||
|
forkFetch,
|
||||||
|
forkRefresh,
|
||||||
|
|
||||||
|
fetchOnKeyChange,
|
||||||
|
refreshOnWindowFocus,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
export * as MutationRunner from "./MutationRunner.js"
|
||||||
|
export * as QueryRunner from "./QueryRunner.js"
|
||||||
export * as QueryState from "./QueryState.js"
|
export * as QueryState from "./QueryState.js"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "reffuse",
|
"name": "reffuse",
|
||||||
"version": "0.1.13",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -16,10 +16,6 @@
|
|||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./types": {
|
|
||||||
"types": "./dist/types/index.d.ts",
|
|
||||||
"default": "./dist/types/index.js"
|
|
||||||
},
|
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./dist/*.d.ts",
|
"types": "./dist/*.d.ts",
|
||||||
"default": "./dist/*.js"
|
"default": "./dist/*.js"
|
||||||
@@ -35,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"effect": "^3.15.0",
|
"effect": "^3.13.0",
|
||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type * as ReffuseContext from "./ReffuseContext.js"
|
import type * as ReffuseContext from "./ReffuseContext.js"
|
||||||
import type * as ReffuseExtension from "./ReffuseExtension.js"
|
import type * as ReffuseExtension from "./ReffuseExtension.js"
|
||||||
import * as ReffuseNamespace from "./ReffuseNamespace.js"
|
import * as ReffuseNamespace from "./ReffuseNamespace.js"
|
||||||
import type { Merge, StaticType } from "./utils.js"
|
import type { Merge, StaticType } from "./types.js"
|
||||||
|
|
||||||
|
|
||||||
export class Reffuse extends ReffuseNamespace.makeClass() {}
|
export class Reffuse extends ReffuseNamespace.makeClass() {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect"
|
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
|
||||||
@@ -25,8 +25,6 @@ export type R<T> = T extends ReffuseContext<infer R> ? R : never
|
|||||||
export type ReactProvider<R> = React.FC<{
|
export type ReactProvider<R> = React.FC<{
|
||||||
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||||
readonly scope?: Scope.Scope
|
readonly scope?: Scope.Scope
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
readonly children?: React.ReactNode
|
readonly children?: React.ReactNode
|
||||||
}>
|
}>
|
||||||
|
|
||||||
@@ -34,25 +32,16 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
|
|||||||
return function ReffuseContextReactProvider(props) {
|
return function ReffuseContextReactProvider(props) {
|
||||||
const runtime = ReffuseRuntime.useRuntime()
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
|
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
|
||||||
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
|
|
||||||
|
|
||||||
const makeScope = React.useMemo(() => props.scope
|
const makeScope = React.useMemo(() => props.scope
|
||||||
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
? Scope.fork(props.scope, ExecutionStrategy.sequential)
|
||||||
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
: Scope.make(),
|
||||||
[props.scope])
|
[props.scope])
|
||||||
|
|
||||||
const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
|
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe(
|
||||||
Effect.provide(props.layer),
|
Effect.provide(props.layer),
|
||||||
Effect.provideService(Scope.Scope, scope),
|
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(
|
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
||||||
Effect.bind("isInitialRun", () => Ref.make(true)),
|
Effect.bind("isInitialRun", () => Ref.make(true)),
|
||||||
@@ -68,7 +57,7 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
|
|||||||
Effect.if({
|
Effect.if({
|
||||||
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
||||||
Effect.map(() =>
|
Effect.map(() =>
|
||||||
() => closeScope(initialScope)
|
() => runSync(Scope.close(initialScope, Exit.void))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -79,13 +68,13 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
|
|||||||
Effect.sync(() => setValue(context))
|
Effect.sync(() => setValue(context))
|
||||||
),
|
),
|
||||||
Effect.map(({ scope }) =>
|
Effect.map(({ scope }) =>
|
||||||
() => closeScope(scope)
|
() => runSync(Scope.close(scope, Exit.void))
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
runSync,
|
runSync,
|
||||||
), [makeScope, runSync, runFork])
|
), [makeScope, makeContext, runSync])
|
||||||
|
|
||||||
return React.createElement(Context, { ...props, value })
|
return React.createElement(Context, { ...props, value })
|
||||||
}
|
}
|
||||||
@@ -95,7 +84,6 @@ export type AsyncReactProvider<R> = React.FC<{
|
|||||||
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||||
readonly scope?: Scope.Scope
|
readonly scope?: Scope.Scope
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
readonly fallback?: React.ReactNode
|
readonly fallback?: React.ReactNode
|
||||||
readonly children?: React.ReactNode
|
readonly children?: React.ReactNode
|
||||||
}>
|
}>
|
||||||
@@ -124,7 +112,7 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
|
|||||||
|
|
||||||
const scope = runSync(props.scope
|
const scope = runSync(props.scope
|
||||||
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
: Scope.make(props.finalizerExecutionStrategy)
|
||||||
)
|
)
|
||||||
|
|
||||||
Effect.context<R>().pipe(
|
Effect.context<R>().pipe(
|
||||||
@@ -138,13 +126,7 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
|
|||||||
effect => runFork(effect, { ...props, scope }),
|
effect => runFork(effect, { ...props, scope }),
|
||||||
)
|
)
|
||||||
|
|
||||||
return () => Scope.close(scope, Exit.void).pipe(
|
return () => { runFork(Scope.close(scope, Exit.void)) }
|
||||||
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
|
|
||||||
Match.when("sync", () => { runSync(effect) }),
|
|
||||||
Match.when("fork", () => { runFork(effect) }),
|
|
||||||
Match.exhaustive,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}, [props.layer, runSync, runFork])
|
}, [props.layer, runSync, runFork])
|
||||||
|
|
||||||
return React.createElement(React.Suspense, {
|
return React.createElement(React.Suspense, {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ReffuseContext from "./ReffuseContext.js"
|
import * as ReffuseContext from "./ReffuseContext.js"
|
||||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
import { type PropertyPath, SetStateAction, SubscriptionSubRef } from "./types/index.js"
|
import * as SetStateAction from "./SetStateAction.js"
|
||||||
|
|
||||||
|
|
||||||
export interface RenderOptions {
|
export interface RenderOptions {
|
||||||
@@ -14,22 +14,11 @@ export interface ScopeOptions {
|
|||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
|
|
||||||
readonly scope?: Scope.Scope
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
|
|
||||||
[K in keyof T]: Effect.Effect.Success<T[K]>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export abstract class ReffuseNamespace<R> {
|
export abstract class ReffuseNamespace<R> {
|
||||||
declare ["constructor"]: ReffuseNamespaceClass<R>
|
declare ["constructor"]: ReffuseNamespaceClass<R>
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
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.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
|
||||||
this.RefState = this.RefState.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
|
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
|
||||||
@@ -94,56 +83,6 @@ export abstract class ReffuseNamespace<R> {
|
|||||||
), [runtime, context])
|
), [runtime, context])
|
||||||
}
|
}
|
||||||
|
|
||||||
useScope<R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
deps: React.DependencyList = [],
|
|
||||||
options?: UseScopeOptions,
|
|
||||||
): readonly [scope: Scope.Scope, layer: Layer.Layer<Scope.Scope>] {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
const runFork = this.useRunFork()
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
React.useEffect(() => isInitialRun.pipe(
|
|
||||||
Effect.if({
|
|
||||||
onTrue: () => Effect.as(
|
|
||||||
Ref.set(isInitialRun, false),
|
|
||||||
() => closeScope(initialScope),
|
|
||||||
),
|
|
||||||
|
|
||||||
onFalse: () => makeScope.pipe(
|
|
||||||
Effect.tap(v => Effect.sync(() => setScope(v))),
|
|
||||||
Effect.map(v => () => closeScope(v)),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
runSync,
|
|
||||||
), [
|
|
||||||
makeScope,
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
|
|
||||||
return React.useMemo(() => [scope, Layer.succeed(Scope.Scope, scope)] as const, [scope])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reffuse equivalent to `React.useMemo`.
|
* Reffuse equivalent to `React.useMemo`.
|
||||||
*
|
*
|
||||||
@@ -167,6 +106,53 @@ 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`.
|
* Reffuse equivalent to `React.useEffect`.
|
||||||
*
|
*
|
||||||
@@ -387,46 +373,14 @@ export abstract class ReffuseNamespace<R> {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
useRef<A, E, R>(
|
useRef<A, R>(
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
initialValue: () => Effect.Effect<A, E, R>,
|
|
||||||
): SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
return this.useMemo(
|
|
||||||
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
|
|
||||||
[],
|
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useRefFromReactiveValue<A, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
this: ReffuseNamespace<R>,
|
||||||
value: A,
|
value: A,
|
||||||
): SubscriptionRef.SubscriptionRef<A> {
|
): SubscriptionRef.SubscriptionRef<A> {
|
||||||
const ref = this.useRef(() => Effect.succeed(value))
|
return this.useMemo(
|
||||||
this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
|
() => SubscriptionRef.make(value),
|
||||||
return ref
|
[],
|
||||||
}
|
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
||||||
|
|
||||||
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,
|
|
||||||
): SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> {
|
|
||||||
return React.useMemo(
|
|
||||||
() => SubscriptionSubRef.makeFromPath(parent, path),
|
|
||||||
[parent, ...path],
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,18 +390,18 @@ export abstract class ReffuseNamespace<R> {
|
|||||||
>(
|
>(
|
||||||
this: ReffuseNamespace<R>,
|
this: ReffuseNamespace<R>,
|
||||||
...refs: Refs
|
...refs: Refs
|
||||||
): RefsA<Refs> {
|
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||||
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
|
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
|
||||||
[],
|
[],
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||||
) as RefsA<Refs>)
|
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
|
||||||
|
|
||||||
this.useFork(() => pipe(
|
this.useFork(() => pipe(
|
||||||
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
||||||
streams => Stream.zipLatestAll(...streams),
|
streams => Stream.zipLatestAll(...streams),
|
||||||
Stream.runForEach(v =>
|
Stream.runForEach(v =>
|
||||||
Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
|
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
|
||||||
),
|
),
|
||||||
), refs)
|
), refs)
|
||||||
|
|
||||||
@@ -485,87 +439,35 @@ export abstract class ReffuseNamespace<R> {
|
|||||||
return [reactStateValue, setValue]
|
return [reactStateValue, setValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
useStreamFromReactiveValues<const A extends React.DependencyList, R>(
|
useStreamFromValues<const A extends React.DependencyList, R>(
|
||||||
this: ReffuseNamespace<R>,
|
this: ReffuseNamespace<R>,
|
||||||
values: A,
|
values: A,
|
||||||
): Stream.Stream<A> {
|
): Stream.Stream<A> {
|
||||||
const [, scopeLayer] = this.useScope([], { finalizerExecutionMode: "fork" })
|
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
|
||||||
|
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
|
||||||
|
), [])
|
||||||
|
|
||||||
const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
|
this.useEffect(() => Queue.offer(queue, values), values)
|
||||||
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.provide(scopeLayer),
|
|
||||||
), [scopeLayer], { 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
|
return stream
|
||||||
}
|
}
|
||||||
|
|
||||||
useSubscribeStream<A, E, R>(
|
useSubscribeStream<A, InitialA extends A | undefined, E, R>(
|
||||||
this: ReffuseNamespace<R>,
|
this: ReffuseNamespace<R>,
|
||||||
stream: Stream.Stream<A, E, R>,
|
stream: Stream.Stream<A, E, R>,
|
||||||
): Option.Option<A>
|
initialValue?: InitialA,
|
||||||
useSubscribeStream<A, E, IE, R>(
|
): InitialA extends A ? Option.Some<A> : Option.Option<A> {
|
||||||
this: ReffuseNamespace<R>,
|
const [reactStateValue, setReactStateValue] = React.useState<Option.Option<A>>(Option.fromNullable(initialValue))
|
||||||
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(
|
this.useFork(() => Stream.runForEach(
|
||||||
Stream.changesWith(stream, (x, y) => x === y),
|
Stream.changesWith(stream, (x, y) => x === y),
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||||
), [stream])
|
), [stream])
|
||||||
|
|
||||||
return reactStateValue
|
return reactStateValue as InitialA extends A ? Option.Some<A> : Option.Option<A>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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>,
|
|
||||||
readonly path: P,
|
|
||||||
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B>) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode {
|
|
||||||
return props.children(this.useSubRefFromPath(props.parent, props.path))
|
|
||||||
}
|
|
||||||
|
|
||||||
SubscribeRefs<
|
SubscribeRefs<
|
||||||
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
||||||
R,
|
R,
|
||||||
@@ -573,7 +475,7 @@ export abstract class ReffuseNamespace<R> {
|
|||||||
this: ReffuseNamespace<R>,
|
this: ReffuseNamespace<R>,
|
||||||
props: {
|
props: {
|
||||||
readonly refs: Refs
|
readonly refs: Refs
|
||||||
readonly children: (...args: RefsA<Refs>) => React.ReactNode
|
readonly children: (...args: [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]) => React.ReactNode
|
||||||
},
|
},
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
return props.children(...this.useSubscribeRefs(...props.refs))
|
return props.children(...this.useSubscribeRefs(...props.refs))
|
||||||
@@ -589,30 +491,15 @@ export abstract class ReffuseNamespace<R> {
|
|||||||
return props.children(this.useRefState(props.ref))
|
return props.children(this.useRefState(props.ref))
|
||||||
}
|
}
|
||||||
|
|
||||||
SubscribeStream<A, E, R>(
|
SubscribeStream<A, InitialA extends A | undefined, E, R>(
|
||||||
this: ReffuseNamespace<R>,
|
this: ReffuseNamespace<R>,
|
||||||
props: {
|
props: {
|
||||||
readonly stream: Stream.Stream<A, E, R>
|
readonly stream: Stream.Stream<A, E, R>
|
||||||
readonly children: (latestValue: Option.Option<A>) => React.ReactNode
|
readonly initialValue?: InitialA
|
||||||
},
|
readonly children: (latestValue: InitialA extends A ? Option.Some<A> : 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 {
|
): React.ReactNode {
|
||||||
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
|
return props.children(this.useSubscribeStream(props.stream, props.initialValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * as ReffuseContext from "./ReffuseContext.js"
|
|||||||
export * as ReffuseExtension from "./ReffuseExtension.js"
|
export * as ReffuseExtension from "./ReffuseExtension.js"
|
||||||
export * as ReffuseNamespace from "./ReffuseNamespace.js"
|
export * as ReffuseNamespace from "./ReffuseNamespace.js"
|
||||||
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
export * as SetStateAction from "./SetStateAction.js"
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { Array, Function, Option, Predicate } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
||||||
|
|
||||||
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
|
||||||
D extends never ? [] :
|
|
||||||
T extends Seen ? [] :
|
|
||||||
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
|
||||||
T extends object ? ObjectPaths<T, D, Seen | T> :
|
|
||||||
never
|
|
||||||
)
|
|
||||||
|
|
||||||
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
|
||||||
[K in keyof T as K extends number ? K : never]:
|
|
||||||
| [K]
|
|
||||||
| [K, ...Paths<T[K], Prev[D], Seen>]
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
|
||||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
|
||||||
NonNullable<T[K]> extends infer V
|
|
||||||
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
|
||||||
: never
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
|
||||||
? Head extends keyof T
|
|
||||||
? ValueFromPath<T[Head], Tail>
|
|
||||||
: T extends readonly any[]
|
|
||||||
? Head extends number
|
|
||||||
? ValueFromPath<T[number], Tail>
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
: T
|
|
||||||
|
|
||||||
export type AnyKey = string | number | symbol
|
|
||||||
export type AnyPath = readonly AnyKey[]
|
|
||||||
|
|
||||||
|
|
||||||
export const unsafeGet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
|
||||||
path.reduce((acc: any, key: any) => acc?.[key], self)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const get: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
|
||||||
path.reduce(
|
|
||||||
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
|
||||||
? Predicate.hasProperty(acc.value, key)
|
|
||||||
? Option.some(acc.value[key])
|
|
||||||
: Option.none()
|
|
||||||
: acc,
|
|
||||||
|
|
||||||
Option.some(self),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const immutableSet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
|
||||||
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
|
||||||
const key = Array.head(path as AnyPath)
|
|
||||||
if (Option.isNone(key))
|
|
||||||
return Option.some(value as T)
|
|
||||||
if (!Predicate.hasProperty(self, key.value))
|
|
||||||
return Option.none()
|
|
||||||
|
|
||||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
|
||||||
if (Option.isNone(child))
|
|
||||||
return child
|
|
||||||
|
|
||||||
if (Array.isArray(self))
|
|
||||||
return typeof key.value === "number"
|
|
||||||
? Option.some([
|
|
||||||
...self.slice(0, key.value),
|
|
||||||
child.value,
|
|
||||||
...self.slice(key.value + 1),
|
|
||||||
] as T)
|
|
||||||
: Option.none()
|
|
||||||
|
|
||||||
if (typeof self === "object")
|
|
||||||
return Option.some(
|
|
||||||
Object.assign(
|
|
||||||
Object.create(Object.getPrototypeOf(self)),
|
|
||||||
{ ...self, [key.value]: child.value },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Option.none()
|
|
||||||
})
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Function } from "effect"
|
|
||||||
import type * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const value: {
|
|
||||||
<S>(prevState: S): (self: React.SetStateAction<S>) => S
|
|
||||||
<S>(self: React.SetStateAction<S>, prevState: S): S
|
|
||||||
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
|
|
||||||
typeof self === "function"
|
|
||||||
? (self as (prevState: S) => S)(prevState)
|
|
||||||
: self
|
|
||||||
)
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
|
||||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
|
||||||
|
|
||||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
|
||||||
|
|
||||||
readonly [Unify.typeSymbol]?: unknown
|
|
||||||
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
|
||||||
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace SubscriptionSubRef {
|
|
||||||
export interface Variance<in out A, in out B> {
|
|
||||||
readonly [SubscriptionSubRefTypeId]: {
|
|
||||||
readonly _A: Types.Invariant<A>
|
|
||||||
readonly _B: Types.Invariant<B>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
|
||||||
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
|
||||||
SubscriptionRef?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const refVariance = { _A: (_: any) => _ }
|
|
||||||
const synchronizedRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
|
||||||
|
|
||||||
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
|
||||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
|
||||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
|
||||||
readonly [Ref.RefTypeId] = refVariance
|
|
||||||
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
|
||||||
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
|
||||||
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
|
||||||
|
|
||||||
readonly get: Effect.Effect<A>
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
readonly getter: (parentValue: B) => A,
|
|
||||||
readonly setter: (parentValue: B, value: A) => B,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
|
||||||
}
|
|
||||||
|
|
||||||
commit() {
|
|
||||||
return this.get
|
|
||||||
}
|
|
||||||
|
|
||||||
get changes(): Stream.Stream<A> {
|
|
||||||
return this.get.pipe(
|
|
||||||
Effect.map(a => this.parent.changes.pipe(
|
|
||||||
Stream.map(this.getter),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrap,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
|
||||||
return this.modifyEffect(a => Effect.succeed(f(a)))
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
|
||||||
return Effect.Do.pipe(
|
|
||||||
Effect.bind("b", () => Ref.get(this.parent)),
|
|
||||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
|
||||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
|
||||||
Effect.map(({ ca: [c] }) => c),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const makeFromGetSet = <A, B>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
getter: (parentValue: B) => A,
|
|
||||||
setter: (parentValue: B, value: A) => B,
|
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
|
||||||
|
|
||||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
path: P,
|
|
||||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
|
||||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * as PropertyPath from "./PropertyPath.js"
|
|
||||||
export * as SetStateAction from "./SetStateAction.js"
|
|
||||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
@@ -26,13 +26,7 @@
|
|||||||
|
|
||||||
// Build
|
// Build
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"declaration": true,
|
"declaration": true
|
||||||
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "@effect/language-service"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["./src"]
|
"include": ["./src"]
|
||||||
|
|||||||
Reference in New Issue
Block a user