Compare commits
7 Commits
extension-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e089bf9fee | ||
|
|
30b72b5b52 | ||
|
|
6dc0a548cd | ||
|
|
2a29f19ece | ||
|
|
2c467dc6ec | ||
|
|
64943deaab | ||
|
|
bc8c96635c |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@reffuse/monorepo",
|
||||
"packageManager": "bun@1.2.9",
|
||||
"packageManager": "bun@1.2.13",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
@@ -15,9 +15,9 @@
|
||||
"clean:node": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"npm-check-updates": "^17.1.18",
|
||||
"npm-check-updates": "^18.0.1",
|
||||
"npm-sort": "^0.0.4",
|
||||
"turbo": "^2.5.0",
|
||||
"turbo": "^2.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,41 +11,41 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@tanstack/react-router": "^1.115.3",
|
||||
"@tanstack/react-router-devtools": "^1.115.3",
|
||||
"@tanstack/router-plugin": "^1.115.3",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@tanstack/react-router": "^1.120.3",
|
||||
"@tanstack/react-router-devtools": "^1.120.3",
|
||||
"@tanstack/router-plugin": "^1.120.3",
|
||||
"@thilawyn/thilaschema": "^0.1.4",
|
||||
"@types/react": "^19.1.1",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.24.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript-eslint": "^8.29.1",
|
||||
"vite": "^6.2.6"
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.80.8",
|
||||
"@effect/platform-browser": "^0.59.8",
|
||||
"@effect/platform": "^0.82.1",
|
||||
"@effect/platform-browser": "^0.62.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reffuse/extension-lazyref": "workspace:*",
|
||||
"@reffuse/extension-query": "workspace:*",
|
||||
"@typed/async-data": "^0.13.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.14.8",
|
||||
"lucide-react": "^0.487.0",
|
||||
"effect": "^3.15.1",
|
||||
"lucide-react": "^0.510.0",
|
||||
"mobx": "^6.13.7",
|
||||
"reffuse": "workspace:*"
|
||||
},
|
||||
"overrides": {
|
||||
"effect": "^3.14.8",
|
||||
"@effect/platform": "^0.80.8",
|
||||
"@effect/platform-browser": "^0.59.8",
|
||||
"effect": "^3.15.1",
|
||||
"@effect/platform": "^0.82.1",
|
||||
"@effect/platform-browser": "^0.62.1",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"@typed/async-data": "^0.13.1"
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
import { AppQueryErrorHandler } from "./query"
|
||||
import { R } from "./reffuse"
|
||||
import { AppQueryErrorHandler } from "./services"
|
||||
|
||||
|
||||
export function VQueryErrorHandler() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const error = R.useSubscribeStream(
|
||||
R.useMemo(() => AppQueryErrorHandler.pipe(
|
||||
R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
|
||||
Effect.map(handler => handler.errors.pipe(
|
||||
Stream.changes,
|
||||
Stream.tap(Console.error),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ThSchema } from "@thilawyn/thilaschema"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Schema } from "effect"
|
||||
|
||||
|
||||
export class Todo extends Schema.Class<Todo>("Todo")({
|
||||
@@ -18,9 +17,4 @@ export const TodoFromJsonStruct = Schema.Struct({
|
||||
ThSchema.assertEncodedJsonifiable
|
||||
)
|
||||
|
||||
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
|
||||
|
||||
|
||||
export const generateUniqueID = makeUuid4.pipe(
|
||||
Effect.provide(GetRandomValues.CryptoRandom)
|
||||
)
|
||||
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
|
||||
|
||||
@@ -5,14 +5,14 @@ import { Layer } from "effect"
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { ReffuseRuntime } from "reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||
import { RootContext } from "./reffuse"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||
|
||||
|
||||
const layer = Layer.empty.pipe(
|
||||
Layer.provideMerge(AppQueryClient.Live),
|
||||
Layer.provideMerge(AppQueryErrorHandler.Live),
|
||||
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
|
||||
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
|
||||
Layer.provideMerge(Clipboard.layer),
|
||||
Layer.provideMerge(Geolocation.layer),
|
||||
Layer.provideMerge(Permissions.layer),
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
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 { QueryService } from "@reffuse/extension-query"
|
||||
import { QueryRunner } from "@reffuse/extension-query"
|
||||
import { ParseResult, Schema } from "effect"
|
||||
|
||||
|
||||
export const Result = Schema.Array(Schema.String)
|
||||
|
||||
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
|
||||
export class Uuid4Query extends QueryRunner.Tag("Uuid4Query")<Uuid4Query,
|
||||
readonly ["uuid4", number],
|
||||
typeof Result.Type,
|
||||
ParseResult.ParseError
|
||||
|
||||
@@ -8,7 +8,7 @@ export function Uuid4QueryService() {
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||
const [state] = R.useRefState(query.state)
|
||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||
import { QueryExtension } from "@reffuse/extension-query"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||
|
||||
|
||||
export const RootContext = ReffuseContext.make<
|
||||
| AppQueryClient
|
||||
| AppQueryErrorHandler
|
||||
| AppQueryClient.AppQueryClient
|
||||
| AppQueryErrorHandler.AppQueryErrorHandler
|
||||
| Clipboard.Clipboard
|
||||
| Geolocation.Geolocation
|
||||
| Permissions.Permissions
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Route as LazyrefImport } from './routes/lazyref'
|
||||
import { Route as CountImport } from './routes/count'
|
||||
import { Route as BlankImport } from './routes/blank'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as StreamsPullImport } from './routes/streams/pull'
|
||||
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||
import { Route as QueryServiceImport } from './routes/query/service'
|
||||
@@ -73,6 +74,12 @@ const IndexRoute = IndexImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const StreamsPullRoute = StreamsPullImport.update({
|
||||
id: '/streams/pull',
|
||||
path: '/streams/pull',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||
id: '/query/usequery',
|
||||
path: '/query/usequery',
|
||||
@@ -172,6 +179,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof QueryUsequeryImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/streams/pull': {
|
||||
id: '/streams/pull'
|
||||
path: '/streams/pull'
|
||||
fullPath: '/streams/pull'
|
||||
preLoaderRoute: typeof StreamsPullImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +203,7 @@ export interface FileRoutesByFullPath {
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
@@ -203,6 +218,7 @@ export interface FileRoutesByTo {
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
@@ -218,6 +234,7 @@ export interface FileRoutesById {
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
'/streams/pull': typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
@@ -234,6 +251,7 @@ export interface FileRouteTypes {
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -247,6 +265,7 @@ export interface FileRouteTypes {
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -260,6 +279,7 @@ export interface FileRouteTypes {
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
| '/streams/pull'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
||||
@@ -275,6 +295,7 @@ export interface RootRouteChildren {
|
||||
QueryServiceRoute: typeof QueryServiceRoute
|
||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||
StreamsPullRoute: typeof StreamsPullRoute
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
@@ -289,6 +310,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
QueryServiceRoute: QueryServiceRoute,
|
||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||
StreamsPullRoute: StreamsPullRoute,
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute
|
||||
@@ -311,7 +333,8 @@ export const routeTree = rootRoute
|
||||
"/todos",
|
||||
"/query/service",
|
||||
"/query/usemutation",
|
||||
"/query/usequery"
|
||||
"/query/usequery",
|
||||
"/streams/pull"
|
||||
]
|
||||
},
|
||||
"/": {
|
||||
@@ -346,6 +369,9 @@ export const routeTree = rootRoute
|
||||
},
|
||||
"/query/usequery": {
|
||||
"filePath": "query/usequery.tsx"
|
||||
},
|
||||
"/streams/pull": {
|
||||
"filePath": "streams/pull.tsx"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Ref } from "effect"
|
||||
import { Effect, Ref } from "effect"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/count")({
|
||||
@@ -11,14 +11,13 @@ function Count() {
|
||||
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
const countRef = R.useRef(0)
|
||||
const [count] = R.useRefState(countRef)
|
||||
const countRef = R.useRef(() => Effect.succeed(0))
|
||||
const [count] = R.useSubscribeRefs(countRef)
|
||||
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
||||
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
|
||||
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
|
||||
count is {count}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
|
||||
import { R } from "@/reffuse"
|
||||
import { HttpClient } from "@effect/platform"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Schema } from "effect"
|
||||
import { Console, Effect, Layer, Schema } from "effect"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
@@ -14,18 +14,21 @@ export const Route = createFileRoute("/query/service")({
|
||||
|
||||
function RouteComponent() {
|
||||
const query = R.useQuery({
|
||||
key: R.useStreamFromValues(["uuid4", 10 as number]),
|
||||
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
HttpClient.withTracerPropagation(false),
|
||||
Effect.andThen(Effect.map(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.withTracerPropagation(false),
|
||||
)),
|
||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
||||
Effect.scoped,
|
||||
),
|
||||
})
|
||||
|
||||
const layer = useMemo(() => query.layer(Uuid4Query.Uuid4Query), [query])
|
||||
const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query])
|
||||
|
||||
return (
|
||||
<QueryContext.Provider layer={layer}>
|
||||
|
||||
@@ -29,15 +29,18 @@ function RouteComponent() {
|
||||
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
||||
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
||||
)),
|
||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
HttpClient.withTracerPropagation(false),
|
||||
Effect.andThen(Effect.map(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.withTracerPropagation(false),
|
||||
)),
|
||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||
Effect.scoped,
|
||||
)
|
||||
})
|
||||
|
||||
const [state] = R.useSubscribeRefs(mutation.state)
|
||||
const [state] = R.useSubscribeRefs(mutation.stateRef)
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,18 +20,21 @@ function RouteComponent() {
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const query = R.useQuery({
|
||||
key: R.useStreamFromValues(["uuid4", count]),
|
||||
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||
Effect.andThen(Effect.sleep("500 millis")),
|
||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
HttpClient.withTracerPropagation(false),
|
||||
Effect.andThen(Effect.map(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.withTracerPropagation(false),
|
||||
)),
|
||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||
Effect.flatMap(res => res.json),
|
||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||
Effect.scoped,
|
||||
),
|
||||
})
|
||||
|
||||
const [state] = R.useSubscribeRefs(query.state)
|
||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||
|
||||
|
||||
return (
|
||||
|
||||
34
packages/example/src/routes/streams/pull.tsx
Normal file
34
packages/example/src/routes/streams/pull.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Chunk, Effect, Exit, Option, Queue, Random, Scope, Stream } from "effect"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/streams/pull")({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const stream = useMemo(() => Stream.repeatEffect(Random.nextInt), [])
|
||||
const streamScope = R.useScope([stream], { finalizerExecutionMode: "fork" })
|
||||
|
||||
const queue = R.useMemo(() => Effect.provideService(Stream.toQueueOfElements(stream), Scope.Scope, streamScope), [streamScope])
|
||||
|
||||
const [value, setValue] = useState(Option.none<number>())
|
||||
const pullLatest = R.useCallbackSync(() => Queue.takeAll(queue).pipe(
|
||||
Effect.flatMap(Chunk.last),
|
||||
Effect.flatMap(Exit.matchEffect({
|
||||
onSuccess: Effect.succeed,
|
||||
onFailure: Effect.fail,
|
||||
})),
|
||||
Effect.tap(v => Effect.sync(() => setValue(Option.some(v)))),
|
||||
), [queue])
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
{Option.isSome(value) && <Text>{value.value}</Text>}
|
||||
<Button onClick={pullLatest}>Pull latest</Button>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,21 @@ import { R } from "@/reffuse"
|
||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Console, Effect, Ref } from "effect"
|
||||
import { Console, Effect, Option } 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")({
|
||||
@@ -10,52 +24,39 @@ export const Route = createFileRoute("/tests")({
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const deepRef = R.useRef({ value: "poulet" })
|
||||
const deepValueRef = R.useSubRefFromGetSet(
|
||||
deepRef,
|
||||
b => b.value,
|
||||
(b, a) => ({ ...b, value: a }),
|
||||
)
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||
// Effect.andThen(makeUuid4),
|
||||
// Effect.provide(GetRandomValues.CryptoRandom),
|
||||
// ), [])
|
||||
// console.log(value)
|
||||
|
||||
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||
Effect.andThen(Console.log("ouient")),
|
||||
Effect.delay("1 second"),
|
||||
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
|
||||
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
|
||||
Effect.tap(v => Effect.sync(() => setUuid(v)))
|
||||
), [])
|
||||
|
||||
const uuidStream = R.useStreamFromReactiveValues([uuid])
|
||||
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
|
||||
|
||||
const uuidRef = R.useRef("none")
|
||||
const anotherRef = R.useRef(69)
|
||||
const [, scopeLayer] = R.useScope([uuid])
|
||||
|
||||
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
|
||||
Effect.andThen(Console.log("Scope changed")),
|
||||
Effect.provide(scopeLayer),
|
||||
runSync,
|
||||
), [scopeLayer, runSync])
|
||||
|
||||
|
||||
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
|
||||
yield* Effect.log(value)
|
||||
}), [])
|
||||
|
||||
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
Effect.tap(v => Ref.set(uuidRef, v)),
|
||||
Effect.tap(v => Ref.set(deepValueRef, v)),
|
||||
), [])
|
||||
const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
|
||||
const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
|
||||
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
|
||||
</R.SubscribeRefs>
|
||||
|
||||
<R.SubscribeRefs refs={[deepRef, deepValueRef]}>
|
||||
{(deep, deepValue) => <Text>{JSON.stringify(deep)} / {deepValue}</Text>}
|
||||
</R.SubscribeRefs>
|
||||
|
||||
<Button onClick={() => logValue("test")}>Log value</Button>
|
||||
<Button onClick={() => generateUuid()}>Generate UUID</Button>
|
||||
<Flex direction="column" justify="center" align="center" gap="2">
|
||||
<Text>{uuid}</Text>
|
||||
<Button onClick={generateUuid}>Generate UUID</Button>
|
||||
<Text>
|
||||
{Option.match(uuidStreamLatestValue, {
|
||||
onSome: ([v]) => v,
|
||||
onNone: () => <></>,
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ function Todos() {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TodosContext.Provider layer={todosLayer}>
|
||||
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
|
||||
<VTodos />
|
||||
</TodosContext.Provider>
|
||||
</Container>
|
||||
|
||||
7
packages/example/src/services/AppQueryClient.ts
Normal file
7
packages/example/src/services/AppQueryClient.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { QueryClient } from "@reffuse/extension-query"
|
||||
import * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||
|
||||
|
||||
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({
|
||||
errorHandler: AppQueryErrorHandler.AppQueryErrorHandler
|
||||
}) {}
|
||||
13
packages/example/src/services/AppQueryErrorHandler.ts
Normal file
13
packages/example/src/services/AppQueryErrorHandler.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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 +1,2 @@
|
||||
export {}
|
||||
export * as AppQueryClient from "./AppQueryClient"
|
||||
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||
|
||||
@@ -2,68 +2,43 @@ import { Todo } from "@/domain"
|
||||
import { KeyValueStore } from "@effect/platform"
|
||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||
import { PlatformError } from "@effect/platform/Error"
|
||||
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
|
||||
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
||||
|
||||
|
||||
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
||||
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
||||
|
||||
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
|
||||
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
|
||||
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
|
||||
readonly remove: (index: number) => Effect.Effect<void>
|
||||
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
|
||||
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
|
||||
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||
}>() {}
|
||||
|
||||
|
||||
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
|
||||
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
|
||||
|
||||
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
||||
Effect.flatMap(kv => kv.get(key)),
|
||||
Effect.flatMap(identity),
|
||||
Effect.flatMap(Schema.parseJson().pipe(
|
||||
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||
Schema.decode,
|
||||
Effect.flatMap(Schema.decode(
|
||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||
)),
|
||||
Effect.flatMap(v => Ref.set(todos, v)),
|
||||
|
||||
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
|
||||
|
||||
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
)
|
||||
|
||||
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
|
||||
Effect.flatMap(([kv, values]) => values.pipe(
|
||||
Schema.parseJson().pipe(
|
||||
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||
Schema.encode,
|
||||
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
|
||||
Effect.flatMap(kv => values.pipe(
|
||||
Schema.encode(
|
||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||
),
|
||||
Effect.flatMap(v => kv.set(key, v)),
|
||||
)),
|
||||
|
||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||
)
|
||||
|
||||
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
|
||||
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
|
||||
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
|
||||
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
|
||||
const save = Effect.flatMap(todos, writeToLocalStorage)
|
||||
|
||||
// const moveUp = (index: number) => Effect.gen(function*() {
|
||||
// Sync changes with local storage
|
||||
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
|
||||
|
||||
// })
|
||||
|
||||
yield* readFromLocalStorage
|
||||
|
||||
return {
|
||||
todos,
|
||||
readFromLocalStorage,
|
||||
saveToLocalStorage,
|
||||
prepend,
|
||||
replace,
|
||||
remove,
|
||||
}
|
||||
return { todos, load, save }
|
||||
}))
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
||||
import { Effect, Option, SubscriptionRef } from "effect"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, Effect, Option, Ref } from "effect"
|
||||
import { R } from "../reffuse"
|
||||
import { TodosState } from "../services"
|
||||
|
||||
|
||||
const createEmptyTodo = Todo.generateUniqueID.pipe(
|
||||
Effect.map(id => Todo.Todo.make({
|
||||
id,
|
||||
content: "",
|
||||
completedAt: Option.none(),
|
||||
}, true))
|
||||
const createEmptyTodo = makeUuid4.pipe(
|
||||
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
)
|
||||
|
||||
|
||||
export function VNewTodo() {
|
||||
|
||||
const runSync = R.useRunSync()
|
||||
const todoRef = R.useRef(() => createEmptyTodo)
|
||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
|
||||
|
||||
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)), [])
|
||||
const [todo, setTodo] = R.useRefState(todoRef)
|
||||
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
|
||||
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
|
||||
Effect.andThen(createEmptyTodo),
|
||||
Effect.flatMap(v => Ref.set(todoRef, v)),
|
||||
), [todoRef])
|
||||
|
||||
|
||||
return (
|
||||
@@ -27,23 +29,12 @@ export function VNewTodo() {
|
||||
<Card>
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextArea
|
||||
value={todo.content}
|
||||
onChange={e => setTodo(prev =>
|
||||
Todo.Todo.make({ ...prev, content: e.target.value }, true)
|
||||
)}
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="center" align="center">
|
||||
<Button
|
||||
onClick={() => TodosState.TodosState.pipe(
|
||||
Effect.flatMap(state => state.prepend(todo)),
|
||||
Effect.andThen(createEmptyTodo),
|
||||
Effect.map(setTodo),
|
||||
runSync,
|
||||
)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button onClick={add}>Add</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
|
||||
import { Delete } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { R } from "../reffuse"
|
||||
import { TodosState } from "../services"
|
||||
|
||||
|
||||
export interface VTodoProps {
|
||||
readonly index: number
|
||||
readonly todo: Todo.Todo
|
||||
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
|
||||
readonly remove: Effect.Effect<void>
|
||||
}
|
||||
|
||||
export function VTodo({ index, todo }: VTodoProps) {
|
||||
export function VTodo({ todoRef, remove }: VTodoProps) {
|
||||
|
||||
const runSync = R.useRunSync()
|
||||
|
||||
const localTodoRef = R.useRef(() => todoRef)
|
||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
|
||||
|
||||
R.useFork(() => localTodoRef.changes.pipe(
|
||||
Stream.debounce("250 millis"),
|
||||
Stream.runForEach(v => Ref.set(todoRef, v)),
|
||||
), [localTodoRef])
|
||||
|
||||
const editorMode = useState(false)
|
||||
|
||||
|
||||
@@ -23,14 +31,8 @@ export function VTodo({ index, todo }: VTodoProps) {
|
||||
<Card>
|
||||
<Flex direction="column" align="stretch" gap="1">
|
||||
<TextArea
|
||||
value={todo.content}
|
||||
onChange={e => TodosState.TodosState.pipe(
|
||||
Effect.flatMap(state => state.replace(
|
||||
index,
|
||||
Todo.Todo.make({ ...todo, content: e.target.value }, true),
|
||||
)),
|
||||
runSync,
|
||||
)}
|
||||
value={content}
|
||||
onChange={e => setContent(e.target.value)}
|
||||
disabled={!editorMode}
|
||||
/>
|
||||
|
||||
@@ -38,12 +40,7 @@ export function VTodo({ index, todo }: VTodoProps) {
|
||||
<Box></Box>
|
||||
|
||||
<Flex direction="row" align="center" gap="1">
|
||||
<IconButton
|
||||
onClick={() => TodosState.TodosState.pipe(
|
||||
Effect.flatMap(state => state.remove(index)),
|
||||
runSync,
|
||||
)}
|
||||
>
|
||||
<IconButton onClick={() => runSync(remove)}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Flex } from "@radix-ui/themes"
|
||||
import { Chunk, Effect, Stream } from "effect"
|
||||
import { Chunk, Effect, Ref } from "effect"
|
||||
import { R } from "../reffuse"
|
||||
import { TodosState } from "../services"
|
||||
import { VNewTodo } from "./VNewTodo"
|
||||
@@ -8,15 +8,8 @@ import { VTodo } from "./VTodo"
|
||||
|
||||
export function VTodos() {
|
||||
|
||||
// Sync changes to the todos with the local storage
|
||||
R.useFork(() => TodosState.TodosState.pipe(
|
||||
Effect.flatMap(state =>
|
||||
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
|
||||
)
|
||||
), [])
|
||||
|
||||
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
|
||||
const [todos] = R.useRefState(todosRef)
|
||||
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
|
||||
const [todos] = R.useSubscribeRefs(todosRef)
|
||||
|
||||
|
||||
return (
|
||||
@@ -27,7 +20,16 @@ export function VTodos() {
|
||||
|
||||
{Chunk.map(todos, (todo, index) => (
|
||||
<Box key={todo.id} width="500px">
|
||||
<VTodo index={index} todo={todo} />
|
||||
<R.SubRefFromGetSet
|
||||
parent={todosRef}
|
||||
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
|
||||
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
|
||||
>
|
||||
{ref => <VTodo
|
||||
todoRef={ref}
|
||||
remove={Ref.update(todosRef, Chunk.remove(index))}
|
||||
/>}
|
||||
</R.SubRefFromGetSet>
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"peerDependencies": {
|
||||
"@typed/lazy-ref": "^0.3.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.13.0",
|
||||
"effect": "^3.15.0",
|
||||
"react": "^19.0.0",
|
||||
"reffuse": "^0.1.8"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@reffuse/extension-query",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.5",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -37,8 +37,8 @@
|
||||
"@effect/platform-browser": "^0.56.0",
|
||||
"@typed/async-data": "^0.13.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.13.0",
|
||||
"effect": "^3.15.0",
|
||||
"react": "^19.0.0",
|
||||
"reffuse": "^0.1.6"
|
||||
"reffuse": "^0.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { type Context, Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
||||
import type * as QueryClient from "../QueryClient.js"
|
||||
import * as QueryProgress from "../QueryProgress.js"
|
||||
import * as QueryState from "./QueryState.js"
|
||||
import { Effect, type Fiber, Queue, Ref, 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 MutationRunner<K extends readonly unknown[], A, E, R> {
|
||||
readonly context: Context.Context<R>
|
||||
export interface MutationRunner<K extends readonly unknown[], A, E> {
|
||||
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||
|
||||
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||
@@ -17,6 +16,11 @@ export interface MutationRunner<K extends readonly unknown[], A, E, R> {
|
||||
}
|
||||
|
||||
|
||||
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> {
|
||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||
@@ -28,7 +32,7 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
||||
mutation,
|
||||
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||
): Effect.Effect<
|
||||
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
|
||||
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
||||
never,
|
||||
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||
> => Effect.gen(function*() {
|
||||
@@ -37,25 +41,18 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
||||
|
||||
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||
|
||||
const run = (key: K) => Effect.Do.pipe(
|
||||
Effect.bind("state", () => queryStateTag),
|
||||
Effect.bind("client", () => QueryClient),
|
||||
|
||||
Effect.flatMap(({ state, client }) => state.set(AsyncData.loading()).pipe(
|
||||
const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
||||
Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
|
||||
Effect.andThen(mutation(key)),
|
||||
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)
|
||||
),
|
||||
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.Live),
|
||||
Effect.provide(QueryProgress.QueryProgress.Default),
|
||||
)
|
||||
|
||||
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
|
||||
@@ -64,11 +61,11 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
||||
value => Ref.set(globalStateRef, value),
|
||||
))
|
||||
|
||||
const forkMutate = (...key: K) => Effect.Do.pipe(
|
||||
Effect.bind("stateRef", () => Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())),
|
||||
Effect.bind("stateQueue", () => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>()),
|
||||
|
||||
Effect.flatMap(({ stateRef, stateQueue }) =>
|
||||
const forkMutate = (...key: K) => Effect.all([
|
||||
Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>()),
|
||||
Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>(),
|
||||
]).pipe(
|
||||
Effect.flatMap(([stateRef, stateQueue]) =>
|
||||
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
|
||||
Effect.andThen(run(key)),
|
||||
Effect.scoped,
|
||||
@@ -1,16 +0,0 @@
|
||||
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, Layer } from "effect"
|
||||
import { Context, Effect, identity, Layer } from "effect"
|
||||
import type { Mutable } from "effect/Types"
|
||||
import * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||
|
||||
@@ -8,6 +8,17 @@ 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"
|
||||
|
||||
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
|
||||
@@ -19,46 +30,28 @@ export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
|
||||
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
|
||||
|
||||
|
||||
export interface ServiceProps<EH, FallbackA, HandledE> {
|
||||
readonly ErrorHandler?: Context.Tag<EH, QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>>
|
||||
export interface ServiceProps<FallbackA = never, HandledE = never, E = never, R = never> {
|
||||
readonly errorHandler?: Effect.Effect<QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, E, R>
|
||||
}
|
||||
|
||||
export interface ServiceResult<Self, EH, FallbackA, HandledE> extends Context.TagClass<
|
||||
export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
|
||||
Self,
|
||||
typeof id,
|
||||
QueryClient<FallbackA, HandledE>
|
||||
> {
|
||||
readonly Live: Layer.Layer<
|
||||
Self | (EH extends QueryErrorHandler.DefaultQueryErrorHandler ? EH : never),
|
||||
never,
|
||||
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
|
||||
>
|
||||
readonly Default: Layer.Layer<Self, E, R>
|
||||
}
|
||||
|
||||
export const Service = <Self>() => (
|
||||
<
|
||||
EH = QueryErrorHandler.DefaultQueryErrorHandler,
|
||||
FallbackA = QueryErrorHandler.Fallback<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>,
|
||||
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>
|
||||
<FallbackA = never, HandledE = never, E = never, R = never>(
|
||||
props?: ServiceProps<FallbackA, HandledE, E, R>
|
||||
): ServiceResult<Self, FallbackA, HandledE, E, R> => {
|
||||
const TagClass = Context.Tag(id)() as ServiceResult<Self, FallbackA, HandledE, E, R>
|
||||
|
||||
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.Do.pipe(
|
||||
Effect.bind("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>)
|
||||
)
|
||||
(TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
|
||||
props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
|
||||
errorHandler => make({ errorHandler }),
|
||||
))
|
||||
|
||||
return TagClass
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect"
|
||||
import type { Mutable } from "effect/Types"
|
||||
import { Cause, Effect, PubSub, Stream } from "effect"
|
||||
|
||||
|
||||
export interface QueryErrorHandler<FallbackA, HandledE> {
|
||||
@@ -11,55 +10,31 @@ 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 interface ServiceResult<
|
||||
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,
|
||||
export const make = <HandledE = never>() => (
|
||||
<FallbackA>(
|
||||
f: (
|
||||
self: Effect.Effect<never, HandledE>,
|
||||
failure: (failure: HandledE) => Effect.Effect<never>,
|
||||
defect: (defect: unknown) => Effect.Effect<never>,
|
||||
) => Effect.Effect<FallbackA>,
|
||||
): ServiceResult<Self, Id, FallbackA, HandledE> => {
|
||||
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, FallbackA, HandledE>
|
||||
) => Effect.Effect<FallbackA>
|
||||
): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
|
||||
const pubsub = yield* PubSub.unbounded<Cause.Cause<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>(
|
||||
self: Effect.Effect<A, E, R>
|
||||
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
||||
self as unknown as Effect.Effect<never, HandledE, never>,
|
||||
(failure: HandledE) => Effect.andThen(
|
||||
PubSub.publish(pubsub, Cause.fail(failure)),
|
||||
Effect.failCause(Cause.empty),
|
||||
),
|
||||
(defect: unknown) => Effect.andThen(
|
||||
PubSub.publish(pubsub, Cause.die(defect)),
|
||||
Effect.failCause(Cause.empty),
|
||||
),
|
||||
)
|
||||
|
||||
const handle = <A, E, R>(
|
||||
self: Effect.Effect<A, E, R>
|
||||
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
||||
self as unknown as Effect.Effect<never, HandledE, never>,
|
||||
(failure: HandledE) => Queue.offer(queue, Cause.fail(failure)).pipe(
|
||||
Effect.andThen(Effect.failCause(Cause.empty))
|
||||
),
|
||||
(defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe(
|
||||
Effect.andThen(Effect.failCause(Cause.empty))
|
||||
),
|
||||
)
|
||||
|
||||
return { errors, handle }
|
||||
}))
|
||||
|
||||
return TagClass
|
||||
}
|
||||
return { errors, handle }
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
export class DefaultQueryErrorHandler extends Service<DefaultQueryErrorHandler>()(
|
||||
"@reffuse/extension-query/DefaultQueryErrorHandler",
|
||||
identity,
|
||||
) {}
|
||||
|
||||
@@ -1,53 +1,21 @@
|
||||
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 type { Effect, Stream } from "effect"
|
||||
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||
import type * as MutationService from "./MutationService.js"
|
||||
import * as MutationRunner from "./MutationRunner.js"
|
||||
import * as QueryClient from "./QueryClient.js"
|
||||
import type * as QueryProgress from "./QueryProgress.js"
|
||||
import type * as QueryService from "./QueryService.js"
|
||||
import { MutationRunner, QueryRunner } from "./internal/index.js"
|
||||
import * as QueryRunner from "./QueryRunner.js"
|
||||
|
||||
|
||||
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
||||
readonly key: Stream.Stream<K>
|
||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||
readonly refreshOnWindowFocus?: boolean
|
||||
readonly options?: QueryRunner.RunOptions
|
||||
}
|
||||
|
||||
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> {
|
||||
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(() => ({
|
||||
useQuery<
|
||||
@@ -61,32 +29,16 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
||||
>(
|
||||
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||
props: UseQueryProps<QK, QA, QE, QR>,
|
||||
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||
): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||
const runner = this.useMemo(() => QueryRunner.make({
|
||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||
key: props.key,
|
||||
query: props.query,
|
||||
}), [props.key])
|
||||
|
||||
this.useFork(() => runner.fetchOnKeyChange, [runner])
|
||||
this.useFork(() => QueryRunner.run(runner, props.options), [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])
|
||||
return runner
|
||||
},
|
||||
|
||||
useMutation<
|
||||
@@ -100,23 +52,10 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
||||
>(
|
||||
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||
props: UseMutationProps<QK, QA, QE, QR>,
|
||||
): UseMutationResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||
const runner = this.useMemo(() => MutationRunner.make({
|
||||
): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||
return this.useMemo(() => MutationRunner.make({
|
||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||
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
|
||||
) => Effect.Effect<void>
|
||||
}>() {
|
||||
static readonly Live: Layer.Layer<
|
||||
static readonly Default: Layer.Layer<
|
||||
QueryProgress,
|
||||
never,
|
||||
QueryState.QueryState<any, any>
|
||||
|
||||
193
packages/extension-query/src/QueryRunner.ts
Normal file
193
packages/extension-query/src/QueryRunner.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
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))
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
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 MutationService from "./MutationService.js"
|
||||
export * as MutationRunner from "./MutationRunner.js"
|
||||
export * as QueryClient from "./QueryClient.js"
|
||||
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||
export * from "./QueryExtension.js"
|
||||
export * as QueryProgress from "./QueryProgress.js"
|
||||
export * as QueryService from "./QueryService.js"
|
||||
export * as QueryRunner from "./QueryRunner.js"
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
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,3 +1 @@
|
||||
export * as MutationRunner from "./MutationRunner.js"
|
||||
export * as QueryRunner from "./QueryRunner.js"
|
||||
export * as QueryState from "./QueryState.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reffuse",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.13",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.13.0",
|
||||
"effect": "^3.15.0",
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect"
|
||||
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect"
|
||||
import * as React from "react"
|
||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||
|
||||
@@ -25,6 +25,8 @@ export type R<T> = T extends ReffuseContext<infer R> ? R : never
|
||||
export type ReactProvider<R> = React.FC<{
|
||||
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||
readonly scope?: Scope.Scope
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
readonly children?: React.ReactNode
|
||||
}>
|
||||
|
||||
@@ -32,16 +34,25 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
|
||||
return function ReffuseContextReactProvider(props) {
|
||||
const runtime = ReffuseRuntime.useRuntime()
|
||||
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
|
||||
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
|
||||
|
||||
const makeScope = React.useMemo(() => props.scope
|
||||
? Scope.fork(props.scope, ExecutionStrategy.sequential)
|
||||
: Scope.make(),
|
||||
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||
[props.scope])
|
||||
|
||||
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe(
|
||||
const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
|
||||
Effect.provide(props.layer),
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
), [props.layer])
|
||||
)
|
||||
|
||||
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
|
||||
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
|
||||
Match.when("sync", () => { runSync(effect) }),
|
||||
Match.when("fork", () => { runFork(effect) }),
|
||||
Match.exhaustive,
|
||||
)
|
||||
)
|
||||
|
||||
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
||||
Effect.bind("isInitialRun", () => Ref.make(true)),
|
||||
@@ -57,7 +68,7 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
|
||||
Effect.if({
|
||||
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
||||
Effect.map(() =>
|
||||
() => runSync(Scope.close(initialScope, Exit.void))
|
||||
() => closeScope(initialScope)
|
||||
)
|
||||
),
|
||||
|
||||
@@ -68,13 +79,13 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
|
||||
Effect.sync(() => setValue(context))
|
||||
),
|
||||
Effect.map(({ scope }) =>
|
||||
() => runSync(Scope.close(scope, Exit.void))
|
||||
() => closeScope(scope)
|
||||
),
|
||||
),
|
||||
}),
|
||||
|
||||
runSync,
|
||||
), [makeScope, makeContext, runSync])
|
||||
), [makeScope, runSync, runFork])
|
||||
|
||||
return React.createElement(Context, { ...props, value })
|
||||
}
|
||||
@@ -84,6 +95,7 @@ export type AsyncReactProvider<R> = React.FC<{
|
||||
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||
readonly scope?: Scope.Scope
|
||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||
readonly fallback?: React.ReactNode
|
||||
readonly children?: React.ReactNode
|
||||
}>
|
||||
@@ -112,7 +124,7 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
|
||||
|
||||
const scope = runSync(props.scope
|
||||
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
: Scope.make(props.finalizerExecutionStrategy)
|
||||
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||
)
|
||||
|
||||
Effect.context<R>().pipe(
|
||||
@@ -126,7 +138,13 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
|
||||
effect => runFork(effect, { ...props, scope }),
|
||||
)
|
||||
|
||||
return () => { runFork(Scope.close(scope, Exit.void)) }
|
||||
return () => Scope.close(scope, Exit.void).pipe(
|
||||
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
|
||||
Match.when("sync", () => { runSync(effect) }),
|
||||
Match.when("fork", () => { runFork(effect) }),
|
||||
Match.exhaustive,
|
||||
)
|
||||
)
|
||||
}, [props.layer, runSync, runFork])
|
||||
|
||||
return React.createElement(React.Suspense, {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import * as ReffuseContext from "./ReffuseContext.js"
|
||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||
import { SetStateAction, SubscriptionSubRef } from "./types/index.js"
|
||||
import { type PropertyPath, SetStateAction, SubscriptionSubRef } from "./types/index.js"
|
||||
|
||||
|
||||
export interface RenderOptions {
|
||||
@@ -14,11 +14,22 @@ export interface ScopeOptions {
|
||||
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> {
|
||||
declare ["constructor"]: ReffuseNamespaceClass<R>
|
||||
|
||||
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.RefState = this.RefState.bind(this as any) as any
|
||||
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
|
||||
@@ -83,6 +94,56 @@ export abstract class ReffuseNamespace<R> {
|
||||
), [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`.
|
||||
*
|
||||
@@ -106,53 +167,6 @@ export abstract class ReffuseNamespace<R> {
|
||||
])
|
||||
}
|
||||
|
||||
useMemoScoped<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||
deps: React.DependencyList,
|
||||
options?: RenderOptions & ScopeOptions,
|
||||
): A {
|
||||
const runSync = this.useRunSync()
|
||||
|
||||
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
||||
Effect.bind("isInitialRun", () => Ref.make(true)),
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
|
||||
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
|
||||
Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const),
|
||||
runSync,
|
||||
), [])
|
||||
|
||||
const [value, setValue] = React.useState(initialValue)
|
||||
|
||||
React.useEffect(() => isInitialRun.pipe(
|
||||
Effect.if({
|
||||
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
||||
Effect.map(() =>
|
||||
() => runSync(Scope.close(initialScope, Exit.void))
|
||||
)
|
||||
),
|
||||
|
||||
onFalse: () => Effect.Do.pipe(
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
|
||||
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
|
||||
Effect.tap(({ value }) =>
|
||||
Effect.sync(() => setValue(value))
|
||||
),
|
||||
Effect.map(({ scope }) =>
|
||||
() => runSync(Scope.close(scope, Exit.void))
|
||||
),
|
||||
),
|
||||
}),
|
||||
|
||||
runSync,
|
||||
), [
|
||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||
...deps,
|
||||
])
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Reffuse equivalent to `React.useEffect`.
|
||||
*
|
||||
@@ -373,17 +387,26 @@ export abstract class ReffuseNamespace<R> {
|
||||
])
|
||||
}
|
||||
|
||||
useRef<A, R>(
|
||||
useRef<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
value: A,
|
||||
initialValue: () => Effect.Effect<A, E, R>,
|
||||
): SubscriptionRef.SubscriptionRef<A> {
|
||||
return this.useMemo(
|
||||
() => SubscriptionRef.make(value),
|
||||
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
||||
)
|
||||
}
|
||||
|
||||
useRefFromReactiveValue<A, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
value: A,
|
||||
): SubscriptionRef.SubscriptionRef<A> {
|
||||
const ref = this.useRef(() => Effect.succeed(value))
|
||||
this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||
return ref
|
||||
}
|
||||
|
||||
useSubRefFromGetSet<A, B, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||
@@ -396,24 +419,35 @@ export abstract class ReffuseNamespace<R> {
|
||||
)
|
||||
}
|
||||
|
||||
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],
|
||||
)
|
||||
}
|
||||
|
||||
useSubscribeRefs<
|
||||
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
||||
R,
|
||||
>(
|
||||
this: ReffuseNamespace<R>,
|
||||
...refs: Refs
|
||||
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
|
||||
): RefsA<Refs> {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
|
||||
) as RefsA<Refs>)
|
||||
|
||||
this.useFork(() => pipe(
|
||||
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
||||
streams => Stream.zipLatestAll(...streams),
|
||||
Stream.runForEach(v =>
|
||||
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
|
||||
Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
|
||||
),
|
||||
), refs)
|
||||
|
||||
@@ -451,35 +485,87 @@ export abstract class ReffuseNamespace<R> {
|
||||
return [reactStateValue, setValue]
|
||||
}
|
||||
|
||||
useStreamFromValues<const A extends React.DependencyList, R>(
|
||||
useStreamFromReactiveValues<const A extends React.DependencyList, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
values: A,
|
||||
): Stream.Stream<A> {
|
||||
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
|
||||
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
|
||||
), [])
|
||||
const [, scopeLayer] = this.useScope([], { finalizerExecutionMode: "fork" })
|
||||
|
||||
this.useEffect(() => Queue.offer(queue, values), values)
|
||||
const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
|
||||
Effect.bind("latest", () => Ref.make(values)),
|
||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||
Effect.let("stream", ({ latest, pubsub }) => Ref.get(latest).pipe(
|
||||
Effect.flatMap(a => Effect.map(
|
||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||
s => Stream.concat(Stream.make(a), s),
|
||||
)),
|
||||
Stream.unwrapScoped,
|
||||
)),
|
||||
Effect.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
|
||||
}
|
||||
|
||||
useSubscribeStream<A, InitialA extends A | undefined, E, R>(
|
||||
useSubscribeStream<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue?: InitialA,
|
||||
): InitialA extends A ? Option.Some<A> : Option.Option<A> {
|
||||
const [reactStateValue, setReactStateValue] = React.useState<Option.Option<A>>(Option.fromNullable(initialValue))
|
||||
): Option.Option<A>
|
||||
useSubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue: () => Effect.Effect<A, IE, R>,
|
||||
): Option.Some<A>
|
||||
useSubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
stream: Stream.Stream<A, E, R>,
|
||||
initialValue?: () => Effect.Effect<A, IE, R>,
|
||||
): Option.Option<A> {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||
() => initialValue
|
||||
? Effect.map(initialValue(), Option.some)
|
||||
: Effect.succeed(Option.none()),
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||
))
|
||||
|
||||
this.useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(stream, (x, y) => x === y),
|
||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||
), [stream])
|
||||
|
||||
return reactStateValue as InitialA extends A ? Option.Some<A> : Option.Option<A>
|
||||
return reactStateValue
|
||||
}
|
||||
|
||||
|
||||
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<
|
||||
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
||||
R,
|
||||
@@ -487,7 +573,7 @@ export abstract class ReffuseNamespace<R> {
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly refs: Refs
|
||||
readonly children: (...args: [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]) => React.ReactNode
|
||||
readonly children: (...args: RefsA<Refs>) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode {
|
||||
return props.children(...this.useSubscribeRefs(...props.refs))
|
||||
@@ -503,15 +589,30 @@ export abstract class ReffuseNamespace<R> {
|
||||
return props.children(this.useRefState(props.ref))
|
||||
}
|
||||
|
||||
SubscribeStream<A, InitialA extends A | undefined, E, R>(
|
||||
SubscribeStream<A, E, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly stream: Stream.Stream<A, E, R>
|
||||
readonly initialValue?: InitialA
|
||||
readonly children: (latestValue: InitialA extends A ? Option.Some<A> : Option.Option<A>) => React.ReactNode
|
||||
readonly children: (latestValue: Option.Option<A>) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode
|
||||
SubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly stream: Stream.Stream<A, E, R>
|
||||
readonly initialValue: () => Effect.Effect<A, IE, R>
|
||||
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode
|
||||
SubscribeStream<A, E, IE, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly stream: Stream.Stream<A, E, R>
|
||||
readonly initialValue?: () => Effect.Effect<A, IE, R>
|
||||
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode {
|
||||
return props.children(this.useSubscribeStream(props.stream, props.initialValue))
|
||||
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
99
packages/reffuse/src/types/PropertyPath.ts
Normal file
99
packages/reffuse/src/types/PropertyPath.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
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,10 +1,16 @@
|
||||
import { Effect, Effectable, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types } from "effect"
|
||||
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 {
|
||||
@@ -16,6 +22,14 @@ export declare namespace SubscriptionSubRef {
|
||||
}
|
||||
}
|
||||
|
||||
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) => _ }
|
||||
@@ -38,7 +52,7 @@ class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> imp
|
||||
readonly setter: (parentValue: B, value: A) => B,
|
||||
) {
|
||||
super()
|
||||
this.get = Ref.get(this.parent).pipe(Effect.map(this.getter))
|
||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
||||
}
|
||||
|
||||
commit() {
|
||||
@@ -75,3 +89,12 @@ export const makeFromGetSet = <A, 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,2 +1,3 @@
|
||||
export * as PropertyPath from "./PropertyPath.js"
|
||||
export * as SetStateAction from "./SetStateAction.js"
|
||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||
|
||||
Reference in New Issue
Block a user