Compare commits
12 Commits
66de517ab5
...
46211638f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46211638f5 | ||
|
|
a28d6c3d30 | ||
|
|
6b74b9a3b2 | ||
|
|
e17f945666 | ||
|
|
aa46ecc82d | ||
|
|
8ea9146dd9 | ||
|
|
0a4bb2856d | ||
|
|
b4cd7daa81 | ||
|
|
b5712d5433 | ||
|
|
57b7eac05c | ||
|
|
9a9bd78ec6 | ||
|
|
ddcd681ca4 |
@@ -11,6 +11,7 @@
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as TodosImport } from './routes/todos'
|
||||
import { Route as TimeImport } from './routes/time'
|
||||
import { Route as TestsImport } from './routes/tests'
|
||||
import { Route as PromiseImport } from './routes/promise'
|
||||
@@ -24,6 +25,12 @@ import { Route as QueryServiceImport } from './routes/query/service'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const TodosRoute = TodosImport.update({
|
||||
id: '/todos',
|
||||
path: '/todos',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const TimeRoute = TimeImport.update({
|
||||
id: '/time',
|
||||
path: '/time',
|
||||
@@ -137,6 +144,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof TimeImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/todos': {
|
||||
id: '/todos'
|
||||
path: '/todos'
|
||||
fullPath: '/todos'
|
||||
preLoaderRoute: typeof TodosImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/service': {
|
||||
id: '/query/service'
|
||||
path: '/query/service'
|
||||
@@ -171,6 +185,7 @@ export interface FileRoutesByFullPath {
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
@@ -184,6 +199,7 @@ export interface FileRoutesByTo {
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
@@ -198,6 +214,7 @@ export interface FileRoutesById {
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
@@ -213,6 +230,7 @@ export interface FileRouteTypes {
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
@@ -225,6 +243,7 @@ export interface FileRouteTypes {
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
@@ -237,6 +256,7 @@ export interface FileRouteTypes {
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
@@ -251,6 +271,7 @@ export interface RootRouteChildren {
|
||||
PromiseRoute: typeof PromiseRoute
|
||||
TestsRoute: typeof TestsRoute
|
||||
TimeRoute: typeof TimeRoute
|
||||
TodosRoute: typeof TodosRoute
|
||||
QueryServiceRoute: typeof QueryServiceRoute
|
||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||
@@ -264,6 +285,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
PromiseRoute: PromiseRoute,
|
||||
TestsRoute: TestsRoute,
|
||||
TimeRoute: TimeRoute,
|
||||
TodosRoute: TodosRoute,
|
||||
QueryServiceRoute: QueryServiceRoute,
|
||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||
@@ -286,6 +308,7 @@ export const routeTree = rootRoute
|
||||
"/promise",
|
||||
"/tests",
|
||||
"/time",
|
||||
"/todos",
|
||||
"/query/service",
|
||||
"/query/usemutation",
|
||||
"/query/usequery"
|
||||
@@ -312,6 +335,9 @@ export const routeTree = rootRoute
|
||||
"/time": {
|
||||
"filePath": "time.tsx"
|
||||
},
|
||||
"/todos": {
|
||||
"filePath": "todos.tsx"
|
||||
},
|
||||
"/query/service": {
|
||||
"filePath": "query/service.tsx"
|
||||
},
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
import { TodosContext } from "@/todos/reffuse"
|
||||
import { TodosState } from "@/todos/services"
|
||||
import { VTodos } from "@/todos/views/VTodos"
|
||||
import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Layer } from "effect"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: Index
|
||||
export const Route = createFileRoute('/')({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function Index() {
|
||||
|
||||
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||
Layer.provideMerge(TodosState.make("todos"))
|
||||
), [])
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TodosContext.Provider layer={todosLayer}>
|
||||
<VTodos />
|
||||
</TodosContext.Provider>
|
||||
</Container>
|
||||
)
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/"!</div>
|
||||
}
|
||||
|
||||
35
packages/example/src/routes/todos.tsx
Normal file
35
packages/example/src/routes/todos.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TodosContext } from "@/todos/reffuse"
|
||||
import { TodosState } from "@/todos/services"
|
||||
import { VTodos } from "@/todos/views/VTodos"
|
||||
import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Layer } from "effect"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/todos")({
|
||||
component: Todos
|
||||
})
|
||||
|
||||
function Todos() {
|
||||
|
||||
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||
Layer.provideMerge(TodosState.make("todos")),
|
||||
|
||||
Layer.merge(Layer.effectDiscard(
|
||||
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
|
||||
Effect.andThen(Console.log("TodosContext constructed"))
|
||||
)
|
||||
)),
|
||||
), [])
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TodosContext.Provider layer={todosLayer}>
|
||||
<VTodos />
|
||||
</TodosContext.Provider>
|
||||
</Container>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Array, Context, Effect, Layer, Runtime } from "effect"
|
||||
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||
|
||||
@@ -23,25 +23,67 @@ export type R<T> = T extends ReffuseContext<infer R> ? R : never
|
||||
|
||||
|
||||
export type ReactProvider<R> = React.FC<{
|
||||
readonly layer: Layer.Layer<R, unknown>
|
||||
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||
readonly scope?: Scope.Scope
|
||||
readonly children?: React.ReactNode
|
||||
}>
|
||||
|
||||
const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> => {
|
||||
return function ReffuseContextReactProvider(props) {
|
||||
const runtime = ReffuseRuntime.useRuntime()
|
||||
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
|
||||
|
||||
const value = React.useMemo(() => Effect.context<R>().pipe(
|
||||
const makeScope = React.useMemo(() => props.scope
|
||||
? Scope.fork(props.scope, ExecutionStrategy.sequential)
|
||||
: Scope.make(),
|
||||
[props.scope])
|
||||
|
||||
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe(
|
||||
Effect.provide(props.layer),
|
||||
Runtime.runSync(runtime),
|
||||
), [props.layer, runtime])
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
), [props.layer])
|
||||
|
||||
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
||||
Effect.bind("isInitialRun", () => Ref.make(true)),
|
||||
Effect.bind("scope", () => makeScope),
|
||||
Effect.bind("context", ({ scope }) => makeContext(scope)),
|
||||
Effect.map(({ isInitialRun, scope, context }) => [isInitialRun, scope, context] 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", () => makeScope),
|
||||
Effect.bind("context", ({ scope }) => makeContext(scope)),
|
||||
Effect.tap(({ context }) =>
|
||||
Effect.sync(() => setValue(context))
|
||||
),
|
||||
Effect.map(({ scope }) =>
|
||||
() => runSync(Scope.close(scope, Exit.void))
|
||||
),
|
||||
),
|
||||
}),
|
||||
|
||||
runSync,
|
||||
), [makeScope, makeContext, runSync])
|
||||
|
||||
return React.createElement(Context, { ...props, value })
|
||||
}
|
||||
}
|
||||
|
||||
export type AsyncReactProvider<R> = React.FC<{
|
||||
readonly layer: Layer.Layer<R, unknown>
|
||||
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||
readonly scope?: Scope.Scope
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
readonly fallback?: React.ReactNode
|
||||
readonly children?: React.ReactNode
|
||||
}>
|
||||
@@ -59,11 +101,33 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
|
||||
|
||||
return function ReffuseContextAsyncReactProvider(props) {
|
||||
const runtime = ReffuseRuntime.useRuntime()
|
||||
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
|
||||
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
|
||||
|
||||
const promise = React.useMemo(() => Effect.context<R>().pipe(
|
||||
Effect.provide(props.layer),
|
||||
Runtime.runPromise(runtime),
|
||||
), [props.layer, runtime])
|
||||
const [promise, setPromise] = React.useState(Promise.withResolvers<Context.Context<R>>().promise)
|
||||
|
||||
React.useEffect(() => {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<Context.Context<R>>()
|
||||
setPromise(promise)
|
||||
|
||||
const scope = runSync(props.scope
|
||||
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
: Scope.make(props.finalizerExecutionStrategy)
|
||||
)
|
||||
|
||||
Effect.context<R>().pipe(
|
||||
Effect.match({
|
||||
onSuccess: resolve,
|
||||
onFailure: reject,
|
||||
}),
|
||||
|
||||
Effect.provide(props.layer),
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
effect => runFork(effect, { ...props, scope }),
|
||||
)
|
||||
|
||||
return () => { runFork(Scope.close(scope, Exit.void)) }
|
||||
}, [props.layer, runSync, runFork])
|
||||
|
||||
return React.createElement(React.Suspense, {
|
||||
children: React.createElement(ReffuseContextAsyncReactProviderInner, { ...props, promise }),
|
||||
|
||||
@@ -108,41 +108,38 @@ export abstract class ReffuseHelpers<R> {
|
||||
): A {
|
||||
const runSync = this.useRunSync()
|
||||
|
||||
// Calculate an initial version of the value so that it can be accessed during the first render
|
||||
const [initialScope, initialValue] = React.useMemo(() => Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||
Effect.flatMap(scope => effect().pipe(
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
Effect.map(value => [scope, value] as const),
|
||||
)),
|
||||
|
||||
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,
|
||||
), [])
|
||||
|
||||
// Keep track of the state of the initial scope
|
||||
const initialScopeClosed = React.useRef(false)
|
||||
|
||||
const [value, setValue] = React.useState(initialValue)
|
||||
|
||||
React.useEffect(() => {
|
||||
const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe(
|
||||
Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })),
|
||||
Effect.when(() => !initialScopeClosed.current),
|
||||
)
|
||||
React.useEffect(() => isInitialRun.pipe(
|
||||
Effect.if({
|
||||
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
||||
Effect.map(() =>
|
||||
() => runSync(Scope.close(initialScope, Exit.void))
|
||||
)
|
||||
),
|
||||
|
||||
const [scope, value] = closeInitialScopeIfNeeded.pipe(
|
||||
Effect.andThen(Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||
Effect.flatMap(scope => effect().pipe(
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
Effect.map(value => [scope, value] as const),
|
||||
))
|
||||
)),
|
||||
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,
|
||||
)
|
||||
|
||||
setValue(value)
|
||||
return () => { runSync(Scope.close(scope, Exit.void)) }
|
||||
}, [
|
||||
runSync,
|
||||
), [
|
||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||
...deps,
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user