1 Commits

Author SHA1 Message Date
Julien Valverdé
acce65c6a4 Code Narrator test
All checks were successful
Lint / lint (push) Successful in 17s
2025-03-11 03:28:00 +01:00
51 changed files with 1140 additions and 1754 deletions

633
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/monorepo", "name": "@reffuse/monorepo",
"packageManager": "bun@1.2.9", "packageManager": "bun@1.2.2",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"./packages/*" "./packages/*"
@@ -15,9 +15,10 @@
"clean:node": "rm -rf node_modules" "clean:node": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"npm-check-updates": "^17.1.18", "code-narrator": "^1.0.17",
"npm-check-updates": "^17.1.14",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
"turbo": "^2.5.0", "turbo": "^2.4.4",
"typescript": "^5.8.3" "typescript": "^5.7.3"
} }
} }

View File

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

View File

@@ -1,57 +0,0 @@
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"
export function VQueryErrorHandler() {
const [open, setOpen] = useState(false)
const error = R.useSubscribeStream(
R.useMemo(() => AppQueryErrorHandler.pipe(
Effect.map(handler => handler.errors.pipe(
Stream.changes,
Stream.tap(Console.error),
Stream.tap(() => Effect.sync(() => setOpen(true))),
))
), [])
)
if (Option.isNone(error))
return <></>
return (
<AlertDialog.Root open={open}>
<AlertDialog.Content maxWidth="450px">
<AlertDialog.Title>Error</AlertDialog.Title>
<AlertDialog.Description size="2">
{Either.match(Cause.failureOrCause(error.value), {
onLeft: flow(
Match.value,
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
Match.exhaustive,
),
onRight: flow(
Cause.dieOption,
Option.match({
onSome: () => <Text>Unrecoverable defect</Text>,
onNone: () => <Text>Unknown error</Text>,
}),
),
})}
</AlertDialog.Description>
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={() => setOpen(false)}>
Ok
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
)
}

View File

@@ -5,14 +5,11 @@ import { Layer } from "effect"
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { ReffuseRuntime } from "reffuse" import { ReffuseRuntime } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query" import { GlobalContext } from "./reffuse"
import { RootContext } from "./reffuse"
import { routeTree } from "./routeTree.gen" import { routeTree } from "./routeTree.gen"
const layer = Layer.empty.pipe( const layer = Layer.empty.pipe(
Layer.provideMerge(AppQueryClient.Live),
Layer.provideMerge(AppQueryErrorHandler.Live),
Layer.provideMerge(Clipboard.layer), Layer.provideMerge(Clipboard.layer),
Layer.provideMerge(Geolocation.layer), Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer), Layer.provideMerge(Permissions.layer),
@@ -31,9 +28,9 @@ declare module "@tanstack/react-router" {
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<ReffuseRuntime.Provider> <ReffuseRuntime.Provider>
<RootContext.Provider layer={layer}> <GlobalContext.Provider layer={layer}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</RootContext.Provider> </GlobalContext.Provider>
</ReffuseRuntime.Provider> </ReffuseRuntime.Provider>
</StrictMode> </StrictMode>
) )

View File

@@ -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 }) {}

View File

@@ -1,10 +1,10 @@
import { RootReffuse } from "@/reffuse" import { GlobalReffuse } from "@/reffuse"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { Uuid4Query } from "./services" import { Uuid4Query } from "./services"
export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>() export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>()
export const R = new class QueryReffuse extends RootReffuse.pipe( export const R = new class QueryReffuse extends GlobalReffuse.pipe(
Reffuse.withContexts(QueryContext) Reffuse.withContexts(QueryContext)
) {} ) {}

View File

@@ -1,3 +1,4 @@
import { HttpClientError } from "@effect/platform"
import { QueryService } from "@reffuse/extension-query" import { QueryService } from "@reffuse/extension-query"
import { ParseResult, Schema } from "effect" import { ParseResult, Schema } from "effect"
@@ -7,5 +8,5 @@ export const Result = Schema.Array(Schema.String)
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query, export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
readonly ["uuid4", number], readonly ["uuid4", number],
typeof Result.Type, typeof Result.Type,
ParseResult.ParseError HttpClientError.HttpClientError | ParseResult.ParseError
>() {} >() {}

View File

@@ -5,7 +5,7 @@ import { Uuid4Query } from "../services"
export function Uuid4QueryService() { export function Uuid4QueryService() {
const runFork = R.useRunFork() const runSync = R.useRunSync()
const query = R.useMemo(() => Uuid4Query.Uuid4Query, []) const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
const [state] = R.useRefState(query.state) const [state] = R.useRefState(query.state)
@@ -25,7 +25,7 @@ export function Uuid4QueryService() {
})} })}
</Text> </Text>
<Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button> <Button onClick={() => runSync(query.refresh)}>Refresh</Button>
</Flex> </Flex>
</Container> </Container>
) )

View File

@@ -3,22 +3,19 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { LazyRefExtension } from "@reffuse/extension-lazyref" import { LazyRefExtension } from "@reffuse/extension-lazyref"
import { QueryExtension } from "@reffuse/extension-query" import { QueryExtension } from "@reffuse/extension-query"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
export const RootContext = ReffuseContext.make< export const GlobalContext = ReffuseContext.make<
| AppQueryClient
| AppQueryErrorHandler
| Clipboard.Clipboard | Clipboard.Clipboard
| Geolocation.Geolocation | Geolocation.Geolocation
| Permissions.Permissions | Permissions.Permissions
| HttpClient.HttpClient | HttpClient.HttpClient
>() >()
export class RootReffuse extends Reffuse.Reffuse.pipe( export class GlobalReffuse extends Reffuse.Reffuse.pipe(
Reffuse.withExtension(LazyRefExtension), Reffuse.withExtension(LazyRefExtension),
Reffuse.withExtension(QueryExtension), Reffuse.withExtension(QueryExtension),
Reffuse.withContexts(RootContext), Reffuse.withContexts(GlobalContext),
) {} ) {}
export const R = new RootReffuse() export const R = new GlobalReffuse()

View File

@@ -11,7 +11,6 @@
// Import Routes // Import Routes
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as TodosImport } from './routes/todos'
import { Route as TimeImport } from './routes/time' import { Route as TimeImport } from './routes/time'
import { Route as TestsImport } from './routes/tests' import { Route as TestsImport } from './routes/tests'
import { Route as PromiseImport } from './routes/promise' import { Route as PromiseImport } from './routes/promise'
@@ -20,17 +19,10 @@ import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank' import { Route as BlankImport } from './routes/blank'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as QueryUsequeryImport } from './routes/query/usequery' import { Route as QueryUsequeryImport } from './routes/query/usequery'
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
import { Route as QueryServiceImport } from './routes/query/service' import { Route as QueryServiceImport } from './routes/query/service'
// Create/Update Routes // Create/Update Routes
const TodosRoute = TodosImport.update({
id: '/todos',
path: '/todos',
getParentRoute: () => rootRoute,
} as any)
const TimeRoute = TimeImport.update({ const TimeRoute = TimeImport.update({
id: '/time', id: '/time',
path: '/time', path: '/time',
@@ -79,12 +71,6 @@ const QueryUsequeryRoute = QueryUsequeryImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const QueryUsemutationRoute = QueryUsemutationImport.update({
id: '/query/usemutation',
path: '/query/usemutation',
getParentRoute: () => rootRoute,
} as any)
const QueryServiceRoute = QueryServiceImport.update({ const QueryServiceRoute = QueryServiceImport.update({
id: '/query/service', id: '/query/service',
path: '/query/service', path: '/query/service',
@@ -144,13 +130,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TimeImport preLoaderRoute: typeof TimeImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/todos': {
id: '/todos'
path: '/todos'
fullPath: '/todos'
preLoaderRoute: typeof TodosImport
parentRoute: typeof rootRoute
}
'/query/service': { '/query/service': {
id: '/query/service' id: '/query/service'
path: '/query/service' path: '/query/service'
@@ -158,13 +137,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof QueryServiceImport preLoaderRoute: typeof QueryServiceImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/query/usemutation': {
id: '/query/usemutation'
path: '/query/usemutation'
fullPath: '/query/usemutation'
preLoaderRoute: typeof QueryUsemutationImport
parentRoute: typeof rootRoute
}
'/query/usequery': { '/query/usequery': {
id: '/query/usequery' id: '/query/usequery'
path: '/query/usequery' path: '/query/usequery'
@@ -185,9 +157,7 @@ export interface FileRoutesByFullPath {
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
} }
@@ -199,9 +169,7 @@ export interface FileRoutesByTo {
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
} }
@@ -214,9 +182,7 @@ export interface FileRoutesById {
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
} }
@@ -230,9 +196,7 @@ export interface FileRouteTypes {
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
@@ -243,9 +207,7 @@ export interface FileRouteTypes {
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
id: id:
| '__root__' | '__root__'
@@ -256,9 +218,7 @@ export interface FileRouteTypes {
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -271,9 +231,7 @@ export interface RootRouteChildren {
PromiseRoute: typeof PromiseRoute PromiseRoute: typeof PromiseRoute
TestsRoute: typeof TestsRoute TestsRoute: typeof TestsRoute
TimeRoute: typeof TimeRoute TimeRoute: typeof TimeRoute
TodosRoute: typeof TodosRoute
QueryServiceRoute: typeof QueryServiceRoute QueryServiceRoute: typeof QueryServiceRoute
QueryUsemutationRoute: typeof QueryUsemutationRoute
QueryUsequeryRoute: typeof QueryUsequeryRoute QueryUsequeryRoute: typeof QueryUsequeryRoute
} }
@@ -285,9 +243,7 @@ const rootRouteChildren: RootRouteChildren = {
PromiseRoute: PromiseRoute, PromiseRoute: PromiseRoute,
TestsRoute: TestsRoute, TestsRoute: TestsRoute,
TimeRoute: TimeRoute, TimeRoute: TimeRoute,
TodosRoute: TodosRoute,
QueryServiceRoute: QueryServiceRoute, QueryServiceRoute: QueryServiceRoute,
QueryUsemutationRoute: QueryUsemutationRoute,
QueryUsequeryRoute: QueryUsequeryRoute, QueryUsequeryRoute: QueryUsequeryRoute,
} }
@@ -308,9 +264,7 @@ export const routeTree = rootRoute
"/promise", "/promise",
"/tests", "/tests",
"/time", "/time",
"/todos",
"/query/service", "/query/service",
"/query/usemutation",
"/query/usequery" "/query/usequery"
] ]
}, },
@@ -335,15 +289,9 @@ export const routeTree = rootRoute
"/time": { "/time": {
"filePath": "time.tsx" "filePath": "time.tsx"
}, },
"/todos": {
"filePath": "todos.tsx"
},
"/query/service": { "/query/service": {
"filePath": "query/service.tsx" "filePath": "query/service.tsx"
}, },
"/query/usemutation": {
"filePath": "query/usemutation.tsx"
},
"/query/usequery": { "/query/usequery": {
"filePath": "query/usequery.tsx" "filePath": "query/usequery.tsx"
} }

View File

@@ -1,7 +1,6 @@
import { VQueryErrorHandler } from "@/VQueryErrorHandler"
import { Container, Flex, Theme } from "@radix-ui/themes" import { Container, Flex, Theme } from "@radix-ui/themes"
import { createRootRoute, Link, Outlet } from "@tanstack/react-router" import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import "@radix-ui/themes/styles.css" import "@radix-ui/themes/styles.css"
import "../index.css" import "../index.css"
@@ -27,8 +26,6 @@ function Root() {
</Container> </Container>
<Outlet /> <Outlet />
<VQueryErrorHandler />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</Theme> </Theme>
) )

View File

@@ -1,10 +1,29 @@
import { TodosContext } from "@/todos/reffuse"
import { TodosState } from "@/todos/services"
import { VTodos } from "@/todos/views/VTodos"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute('/')({ export const Route = createFileRoute("/")({
component: RouteComponent component: Index
}) })
function RouteComponent() { function Index() {
return <div>Hello "/"!</div>
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos"))
), [])
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</Container>
)
} }

View File

@@ -1,81 +0,0 @@
import { R } from "@/reffuse"
import { HttpClient } from "@effect/platform"
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
import { QueryProgress } from "@reffuse/extension-query"
import { createFileRoute } from "@tanstack/react-router"
import * as AsyncData from "@typed/async-data"
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
import { useState } from "react"
export const Route = createFileRoute("/query/usemutation")({
component: RouteComponent
})
const Result = Schema.Array(Schema.String)
function RouteComponent() {
const runFork = R.useRunFork()
const [count, setCount] = useState(1)
const mutation = R.useMutation({
mutation: ([count]: readonly [count: number]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(QueryProgress.QueryProgress.update(() =>
AsyncData.Progress.make({ loaded: 0, total: Option.some(100) })
)),
Effect.andThen(Effect.sleep("500 millis")),
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.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)),
Effect.scoped,
)
})
const [state] = R.useSubscribeRefs(mutation.state)
return (
<Container>
<Flex direction="column" align="center" gap="2">
<Slider
min={1}
max={100}
value={[count]}
onValueChange={flow(
Array.head,
Option.getOrThrow,
setCount,
)}
/>
<Text>
{AsyncData.match(state, {
NoData: () => "No data yet",
Loading: progress =>
`Loading...
${ Option.match(progress, {
onSome: ({ loaded, total }) => ` (${ loaded }/${ Option.getOrElse(total, () => "unknown") })`,
onNone: () => "",
}) }`,
Success: value => `Value: ${ value }`,
Failure: cause => `Error: ${ cause }`,
})}
</Text>
<Button onClick={() => mutation.forkMutate(count).pipe(
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
Effect.andThen(Console.log("Mutation done.")),
runFork,
)}>
Get
</Button>
</Flex>
</Container>
)
}

View File

@@ -3,7 +3,7 @@ import { HttpClient } from "@effect/platform"
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes" import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import * as AsyncData from "@typed/async-data" import * as AsyncData from "@typed/async-data"
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect" import { Array, Console, Effect, flow, Option, Schema } from "effect"
import { useState } from "react" import { useState } from "react"
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/query/usequery")({
const Result = Schema.Array(Schema.String) const Result = Schema.Array(Schema.String)
function RouteComponent() { function RouteComponent() {
const runFork = R.useRunFork() const runSync = R.useRunSync()
const [count, setCount] = useState(1) const [count, setCount] = useState(1)
@@ -31,7 +31,7 @@ function RouteComponent() {
), ),
}) })
const [state] = R.useSubscribeRefs(query.state) const [state] = R.useRefState(query.state)
return ( return (
@@ -59,15 +59,7 @@ function RouteComponent() {
})} })}
</Text> </Text>
<Button <Button onClick={() => runSync(query.refresh)}>Refresh</Button>
onClick={() => query.forkRefresh.pipe(
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
Effect.andThen(Console.log("Refresh finished or stopped")),
runFork,
)}
>
Refresh
</Button>
</Flex> </Flex>
</Container> </Container>
) )

View File

@@ -1,8 +1,9 @@
import { R } from "@/reffuse" import { R } from "@/reffuse"
import { Button, Flex, Text } from "@radix-ui/themes" import { Button, Flex } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id" import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Ref } from "effect" import { Console, Effect, Stream } from "effect"
import { useState } from "react"
export const Route = createFileRoute("/tests")({ export const Route = createFileRoute("/tests")({
@@ -10,9 +11,6 @@ export const Route = createFileRoute("/tests")({
}) })
function RouteComponent() { function RouteComponent() {
const deepRef = R.useRef({ value: "poulet" })
const deepValueRef = R.useSubRef(deepRef, ["value"])
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe( // const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
// Effect.andThen(makeUuid4), // Effect.andThen(makeUuid4),
// Effect.provide(GetRandomValues.CryptoRandom), // Effect.provide(GetRandomValues.CryptoRandom),
@@ -24,9 +22,9 @@ function RouteComponent() {
Effect.delay("1 second"), Effect.delay("1 second"),
), []) ), [])
const [reactValue, setReactValue] = useState("initial")
const uuidRef = R.useRef("none") const reactValueStream = R.useStreamFromValues([reactValue])
const anotherRef = R.useRef(69) R.useFork(() => Stream.runForEach(reactValueStream, Console.log), [reactValueStream])
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) { const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
@@ -35,21 +33,12 @@ function RouteComponent() {
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe( const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom), Effect.provide(GetRandomValues.CryptoRandom),
Effect.tap(v => Ref.set(uuidRef, v)), Effect.map(setReactValue),
Effect.tap(v => Ref.set(deepValueRef, v)),
), []) ), [])
return ( return (
<Flex direction="row" justify="center" align="center" gap="2"> <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={() => logValue("test")}>Log value</Button>
<Button onClick={() => generateUuid()}>Generate UUID</Button> <Button onClick={() => generateUuid()}>Generate UUID</Button>
</Flex> </Flex>

View File

@@ -1,35 +0,0 @@
import { TodosContext } from "@/todos/reffuse"
import { TodosState } from "@/todos/services"
import { VTodos } from "@/todos/views/VTodos"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/todos")({
component: Todos
})
function Todos() {
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos")),
Layer.merge(Layer.effectDiscard(
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
Effect.andThen(Console.log("TodosContext constructed"))
)
)),
), [])
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</Container>
)
}

View File

@@ -1,10 +1,10 @@
import { RootReffuse } from "@/reffuse" import { GlobalReffuse } from "@/reffuse"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { TodosState } from "./services" import { TodosState } from "./services"
export const TodosContext = ReffuseContext.make<TodosState.TodosState>() export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
export const R = new class TodosReffuse extends RootReffuse.pipe( export const R = new class TodosReffuse extends GlobalReffuse.pipe(
Reffuse.withContexts(TodosContext) Reffuse.withContexts(TodosContext)
) {} ) {}

View File

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

View File

@@ -1,49 +1,20 @@
import * as LazyRef from "@typed/lazy-ref" import * as LazyRef from "@typed/lazy-ref"
import { Effect, pipe, Stream } from "effect" import { Effect, Stream } from "effect"
import * as React from "react" import * as React from "react"
import { ReffuseExtension, type ReffuseNamespace } from "reffuse" import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse"
import { SetStateAction } from "reffuse/types"
export const LazyRefExtension = ReffuseExtension.make(() => ({ export const LazyRefExtension = ReffuseExtension.make(() => ({
useSubscribeLazyRefs<
const Refs extends readonly LazyRef.LazyRef<any>[],
R,
>(
this: ReffuseNamespace.ReffuseNamespace<R>,
...refs: Refs
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly LazyRef.LazyRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
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]> }]))
),
), refs)
return reactStateValue
},
useLazyRefState<A, E, R>( useLazyRefState<A, E, R>(
this: ReffuseNamespace.ReffuseNamespace<R>, this: ReffuseHelpers.ReffuseHelpers<R>,
ref: LazyRef.LazyRef<A, E, R>, ref: LazyRef.LazyRef<A, E, R>,
): [A, React.Dispatch<React.SetStateAction<A>>] { ): [A, React.Dispatch<React.SetStateAction<A>>] {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo( const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
() => ref, const [reactStateValue, setReactStateValue] = React.useState(initialState)
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach( this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
Stream.changesWith(ref.changes, (x, y) => x === y), setReactStateValue(v)
v => Effect.sync(() => setReactStateValue(v)), )), [ref])
), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) => const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
LazyRef.update(ref, prevState => LazyRef.update(ref, prevState =>

View File

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

View File

@@ -0,0 +1,134 @@
// import { BrowserStream } from "@effect/platform-browser"
// import * as AsyncData from "@typed/async-data"
// import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
// export interface MutationRunner<K extends readonly unknown[], A, E, R> {
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
// readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
// readonly forkMutate: Effect.Effect<Fiber.RuntimeFiber<void>>
// }
// export interface MakeProps<K extends readonly unknown[], A, E, R> {
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
// }
// export const make = <K extends readonly unknown[], A, E, R>(
// { key, query }: MakeProps<K, A, E, R>
// ): Effect.Effect<MutationRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
// const context = yield* Effect.context<R>()
// const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>())
// 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 forkFetch = interrupt.pipe(
// Effect.andThen(
// Ref.set(stateRef, AsyncData.loading()).pipe(
// Effect.andThen(latestKeyRef),
// Effect.flatMap(identity),
// Effect.flatMap(key => query(key).pipe(
// Effect.matchCauseEffect({
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
// })
// )),
// Effect.provide(context),
// Effect.fork,
// )
// ),
// Effect.flatMap(fiber =>
// Ref.set(fiberRef, Option.some(fiber)).pipe(
// Effect.andThen(Fiber.join(fiber)),
// Effect.andThen(Ref.set(fiberRef, Option.none())),
// )
// ),
// Effect.forkDaemon,
// )
// const forkRefresh = interrupt.pipe(
// Effect.andThen(
// Ref.update(stateRef, previous => {
// if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
// return AsyncData.refreshing(previous)
// if (AsyncData.isRefreshing(previous))
// return AsyncData.refreshing(previous.previous)
// return AsyncData.loading()
// }).pipe(
// Effect.andThen(latestKeyRef),
// Effect.flatMap(identity),
// Effect.flatMap(key => query(key).pipe(
// Effect.matchCauseEffect({
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
// })
// )),
// Effect.provide(context),
// Effect.fork,
// )
// ),
// Effect.flatMap(fiber =>
// Ref.set(fiberRef, Option.some(fiber)).pipe(
// Effect.andThen(Fiber.join(fiber)),
// Effect.andThen(Ref.set(fiberRef, Option.none())),
// )
// ),
// Effect.forkDaemon,
// )
// const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
// Effect.andThen(Stream.runForEach(key, latestKey =>
// Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
// Effect.andThen(forkFetch)
// )
// ))
// )
// const refreshOnWindowFocus = Stream.runForEach(
// BrowserStream.fromEventListenerWindow("focus"),
// () => forkRefresh,
// )
// return {
// query,
// latestKeyRef,
// stateRef,
// fiberRef,
// forkInterrupt,
// forkFetch,
// forkRefresh,
// fetchOnKeyChange,
// refreshOnWindowFocus,
// }
// })

View File

@@ -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>>()

View File

@@ -1,65 +0,0 @@
import { Context, Effect, Layer } from "effect"
import type { Mutable } from "effect/Types"
import * as QueryErrorHandler from "./QueryErrorHandler.js"
export interface QueryClient<FallbackA, HandledE> {
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
}
const id = "@reffuse/extension-query/QueryClient"
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
TagClassShape<FallbackA, HandledE>,
typeof id,
QueryClient<FallbackA, HandledE>
>
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 ServiceResult<Self, EH, FallbackA, HandledE> 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
>
}
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>
(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>)
)
return TagClass
}
)

View File

@@ -1,65 +0,0 @@
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect"
import type { Mutable } from "effect/Types"
export interface QueryErrorHandler<FallbackA, HandledE> {
readonly errors: Stream.Stream<Cause.Cause<HandledE>>
readonly handle: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R>
}
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,
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>
(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) => 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
}
)
export class DefaultQueryErrorHandler extends Service<DefaultQueryErrorHandler>()(
"@reffuse/extension-query/DefaultQueryErrorHandler",
identity,
) {}

View File

@@ -1,28 +1,21 @@
import type * as AsyncData from "@typed/async-data" 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 { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import { ReffuseExtension, type ReffuseNamespace } from "reffuse" import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
import type * as MutationService from "./MutationService.js" import * as QueryRunner from "./QueryRunner.js"
import * as QueryClient from "./QueryClient.js"
import type * as QueryProgress from "./QueryProgress.js"
import type * as QueryService from "./QueryService.js" import type * as QueryService from "./QueryService.js"
import { MutationRunner, QueryRunner } from "./internal/index.js"
export interface UseQueryProps<K extends readonly unknown[], A, E, R> { export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
readonly key: Stream.Stream<K> readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress> readonly query: (key: K) => Effect.Effect<A, E, R>
readonly refreshOnWindowFocus?: boolean readonly refreshOnWindowFocus?: boolean
} }
export interface UseQueryResult<K extends readonly unknown[], A, E> { export interface UseQueryResult<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>> readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>> readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
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>( readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>> tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
@@ -30,40 +23,12 @@ export interface UseQueryResult<K extends readonly unknown[], A, E> {
} }
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(() => ({ export const QueryExtension = ReffuseExtension.make(() => ({
useQuery< useQuery<K extends readonly unknown[], A, E, R>(
QK extends readonly unknown[], this: ReffuseHelpers.ReffuseHelpers<R>,
QA, props: UseQueryProps<K, A, E, R>,
FallbackA, ): UseQueryResult<K, A, E> {
QE,
HandledE,
QR extends R,
R,
>(
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseQueryProps<QK, QA, QE, QR>,
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => QueryRunner.make({ const runner = this.useMemo(() => QueryRunner.make({
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
key: props.key, key: props.key,
query: props.query, query: props.query,
}), [props.key]) }), [props.key])
@@ -78,45 +43,13 @@ export const QueryExtension = ReffuseExtension.make(() => ({
return React.useMemo(() => ({ return React.useMemo(() => ({
latestKey: runner.latestKeyRef, latestKey: runner.latestKeyRef,
state: runner.stateRef, state: runner.stateRef,
refresh: runner.forkRefresh,
forkRefresh: runner.forkRefresh,
layer: tag => Layer.succeed(tag, { layer: tag => Layer.succeed(tag, {
latestKey: runner.latestKeyRef, latestKey: runner.latestKeyRef,
state: runner.stateRef, state: runner.stateRef,
forkRefresh: runner.forkRefresh, refresh: runner.forkRefresh,
}), }),
}), [runner]) }), [runner])
}, }
useMutation<
QK extends readonly unknown[],
QA,
FallbackA,
QE,
HandledE,
QR extends R,
R,
>(
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({
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])
},
})) }))

View File

@@ -1,37 +0,0 @@
import * as AsyncData from "@typed/async-data"
import { Effect, flow, Layer, Match, Option } from "effect"
import { QueryState } from "./internal/index.js"
export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryProgress")<QueryProgress, {
readonly get: Effect.Effect<Option.Option<AsyncData.Progress>>
readonly update: (
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
) => Effect.Effect<void>
}>() {
static readonly Live: Layer.Layer<
QueryProgress,
never,
QueryState.QueryState<any, any>
> = Layer.effect(this, Effect.gen(function*() {
const state = yield* QueryState.makeTag()
const get = state.get.pipe(
Effect.map(flow(Match.value,
Match.tag("Loading", v => v.progress),
Match.tag("Refreshing", v => v.progress),
Match.orElse(() => Option.none()),
))
)
const update = (f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress) => get.pipe(
Effect.map(f),
Effect.flatMap(progress => state.update(previous =>
AsyncData.updateProgress(previous, progress)
)),
)
return { get, update }
}))
}

View File

@@ -0,0 +1,144 @@
import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data"
import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
readonly query: (key: K) => Effect.Effect<A, E, R>
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>>
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly forkFetch: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly forkRefresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
readonly refreshOnWindowFocus: Effect.Effect<void>
}
export interface MakeProps<K extends readonly unknown[], A, E, R> {
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R>
}
export const make = <K extends readonly unknown[], A, E, R>(
{ key, query }: MakeProps<K, A, E, R>
): Effect.Effect<QueryRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
const context = yield* Effect.context<R>()
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>())
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 forkFetch = interrupt.pipe(
Effect.andThen(
Ref.set(stateRef, AsyncData.loading()).pipe(
Effect.andThen(latestKeyRef),
Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe(
Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
})
)),
Effect.provide(context),
Effect.fork,
)
),
Effect.flatMap(fiber =>
Ref.set(fiberRef, Option.some(fiber)).pipe(
Effect.andThen(Fiber.join(fiber)),
Effect.andThen(Ref.set(fiberRef, Option.none())),
)
),
Effect.forkDaemon,
)
const forkRefresh = interrupt.pipe(
Effect.andThen(
Ref.update(stateRef, previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous)
return AsyncData.loading()
}).pipe(
Effect.andThen(latestKeyRef),
Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe(
Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
})
)),
Effect.provide(context),
Effect.fork,
)
),
Effect.flatMap(fiber =>
Ref.set(fiberRef, Option.some(fiber)).pipe(
Effect.andThen(Fiber.join(fiber)),
Effect.andThen(Ref.set(fiberRef, Option.none())),
)
),
Effect.forkDaemon,
)
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
Effect.andThen(Stream.runForEach(key, latestKey =>
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
Effect.andThen(forkFetch)
)
))
)
const refreshOnWindowFocus = Stream.runForEach(
BrowserStream.fromEventListenerWindow("focus"),
() => forkRefresh,
)
return {
query,
latestKeyRef,
stateRef,
fiberRef,
forkInterrupt,
forkFetch,
forkRefresh,
fetchOnKeyChange,
refreshOnWindowFocus,
}
})

View File

@@ -1,16 +1,32 @@
import type * as AsyncData from "@typed/async-data" import type * as AsyncData from "@typed/async-data"
import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect" import { type Cause, Effect, type Fiber, type Option, type SubscriptionRef } from "effect"
export interface QueryService<K extends readonly unknown[], A, E> { export interface QueryService<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>> readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>> readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly forkRefresh: Effect.Effect<readonly [ readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
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) => < export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never, Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>() >() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
// export interface LayerProps<A, E, R> {
// readonly query: Effect.Effect<A, E, R>
// }
// export const layer = <Self, Id extends string, A, E, R>(
// tag: Context.TagClass<Self, Id, QueryService<A, E>>,
// props: LayerProps<A, E, R>,
// ): Layer.Layer<Self, never, R> => Layer.effect(tag, Effect.gen(function*() {
// const runner = yield* QueryRunner.make({
// query: props.query
// })
// return {
// state: runner.stateRef,
// refresh: runner.forkRefresh,
// }
// }))

View File

@@ -1,6 +1,3 @@
export * as MutationService from "./MutationService.js"
export * as QueryClient from "./QueryClient.js"
export * as QueryErrorHandler from "./QueryErrorHandler.js"
export * from "./QueryExtension.js" export * from "./QueryExtension.js"
export * as QueryProgress from "./QueryProgress.js" export * as QueryRunner from "./QueryRunner.js"
export * as QueryService from "./QueryService.js" export * as QueryService from "./QueryService.js"

View File

@@ -1,98 +0,0 @@
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"
export interface MutationRunner<K extends readonly unknown[], A, E, R> {
readonly context: Context.Context<R>
readonly stateRef: 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 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>
}
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
{
QueryClient,
mutation,
}: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect<
MutationRunner<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 globalStateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
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(
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)
),
}),
)),
Effect.provide(context),
Effect.provide(QueryProgress.QueryProgress.Live),
)
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
queryStateTag,
globalStateRef,
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 }) =>
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
Effect.andThen(run(key)),
Effect.scoped,
Effect.forkDaemon,
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)),
Effect.andThen(Ref.set(globalStateRef, value)),
),
)),
)
)
)
return {
context,
stateRef: globalStateRef,
mutate,
forkMutate,
}
})

View File

@@ -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,
}
})

View File

@@ -1,24 +0,0 @@
import type * as AsyncData from "@typed/async-data"
import { Context, Effect, Layer } from "effect"
export interface QueryState<A, E> {
readonly get: Effect.Effect<AsyncData.AsyncData<A, E>>
readonly set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>
readonly update: (f: (previous: AsyncData.AsyncData<A, E>) => AsyncData.AsyncData<A, E>) => Effect.Effect<void>
}
export const makeTag = <A, E>(): Context.Tag<QueryState<A, E>, QueryState<A, E>> => Context.GenericTag("@reffuse/query-extension/QueryState")
export const layer = <A, E>(
tag: Context.Tag<QueryState<A, E>, QueryState<A, E>>,
get: Effect.Effect<AsyncData.AsyncData<A, E>>,
set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>,
): Layer.Layer<QueryState<A, E>> => Layer.succeed(tag, {
get,
set,
update: f => get.pipe(
Effect.map(f),
Effect.flatMap(set),
),
})

View File

@@ -1,3 +0,0 @@
export * as MutationRunner from "./MutationRunner.js"
export * as QueryRunner from "./QueryRunner.js"
export * as QueryState from "./QueryState.js"

View File

@@ -0,0 +1,14 @@
Create a "ReadMe" guide for the project, named "{{ projectName }}".
Include the following:
Title, Description,
Getting Started by installing npm package, how to run it with npx
Configuration is optional and will be generated on first run
Reporting bugs, repository and homepage
Versioning
Authors
License
This is the entry file:
###
{{ entryFileContent }}
###

View File

@@ -0,0 +1,4 @@
Show how developer would add HowTo in config file,
args property is used to inject properties into liquid template, any property set in args can be access in liquid template with {{ keyName }}
file property appends extracted content of a file to liquid template, using JSONPath or the extract property that uses LLM to extract content from file
Developers MUST create a liquid template in .code-narrator/gpt_questions, this template file is used to ask GPT question

View File

@@ -0,0 +1,4 @@
Give title and short description that this is an overview file for files located in directory
Give short description of each file that is provided
Add link to each file, link should be the filename

View File

@@ -0,0 +1,101 @@
const ConfigurationBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Configuration/ConfigurationBuilder");
const FilesBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Files/FilesBuilder");
const FoldersBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Folders/FoldersBuilder");
const UserDefinedBuilder = require("code-narrator/dist/src/documentation/plugins/builders/UserDefined/UserDefinedBuilder");
/**
* You can find the documentation about code-narrator.config.js at
* https://github.com/ingig/code-narrator/blob/master/docs/Configuration/code-narrator.config.js.md
*
* @type {ICodeNarratorConfig}
*/
const config = {
// App specific configuration files. This could be something like project_name.json
config_files: [
],
project_file: "package.json",
entry_file: "./dist/index.js",
cli_file: "",
project_path: "./",
source_path: "src",
documentation_path: "./docs",
test_path: "test",
exclude: [
"/node_modules",
".env",
"/.idea",
"/.git",
".gitignore",
"/.code-narrator",
"/dist",
"/build",
"package-lock.json",
],
// Indicates if the documentation should create a README file in root of project
readmeRoot: true,
// Url to the repository, code-narrator tries to extract this from project file
repository_url: "git+https://github.com/Thiladev/reffuse.git",
// These are the plugins used when building documentation. You can create your own plugin. Checkout the code-narrator docs HowTo create a builder plugin
builderPlugins: [
ConfigurationBuilder,
FilesBuilder,
FoldersBuilder,
UserDefinedBuilder,
],
// These are system commends send to GPT with every query
gptSystemCommands: [
"Act as a documentation expert for software",
"If there is :::note, :::info, :::caution, :::tip, :::danger in the text, extract that from its location and format it correctly",
"Return your answer in {DocumentationType} format",
"If you notice any secret information, replace it with ***** in your response",
],
documentation_type: "md",
document_file_extension: ".md",
folderRootFileName: "README",
cache_file: ".code-narrator/cache.json",
gptModel: "gpt-4",
aiService: undefined,
project_name: "reffuse",
include: [
"src/**/*",
],
// Array of user defined documentations. See code-narrator How to create a user defined builder
builders: [
{
name: "README",
type: "README",
template: "README",
sidebarPosition: 1,
args: {
entryFileContent: "content(package.json)",
aiService: undefined,
},
aiService: undefined,
},
{
name: "HowTo Overview",
type: "README",
template: "overview_readme",
path: "howto",
files: [
{
path: "howto/*.md",
aiService: undefined,
},
],
pages: [
{
name: "HowTo Example",
type: "howto",
template: "howto_create_howto",
aiService: undefined,
},
],
aiService: undefined,
},
],
}
module.exports = config;

View File

@@ -1,6 +1,6 @@
{ {
"name": "reffuse", "name": "reffuse",
"version": "0.1.9", "version": "0.1.3",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -16,10 +16,6 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"default": "./dist/index.js" "default": "./dist/index.js"
}, },
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./*": { "./*": {
"types": "./dist/*.d.ts", "types": "./dist/*.d.ts",
"default": "./dist/*.js" "default": "./dist/*.js"

View File

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

View File

@@ -1,160 +0,0 @@
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider = makeProvider(this.Context)
readonly AsyncProvider = makeAsyncProvider(this.Context)
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
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 children?: React.ReactNode
}>
const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> => {
return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const makeScope = React.useMemo(() => props.scope
? Scope.fork(props.scope, ExecutionStrategy.sequential)
: Scope.make(),
[props.scope])
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe(
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
), [props.layer])
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
Effect.bind("scope", () => makeScope),
Effect.bind("context", ({ scope }) => makeContext(scope)),
Effect.map(({ isInitialRun, scope, context }) => [isInitialRun, scope, context] as const),
runSync,
), [])
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
)
),
onFalse: () => Effect.Do.pipe(
Effect.bind("scope", () => makeScope),
Effect.bind("context", ({ scope }) => makeContext(scope)),
Effect.tap(({ context }) =>
Effect.sync(() => setValue(context))
),
Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
),
),
}),
runSync,
), [makeScope, makeContext, runSync])
return React.createElement(Context, { ...props, value })
}
}
export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> => {
function ReffuseContextAsyncReactProviderInner({ promise, children }: {
readonly promise: Promise<Context.Context<R>>
readonly children?: React.ReactNode
}) {
return React.createElement(Context, {
value: React.use(promise),
children,
})
}
return function ReffuseContextAsyncReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
const [promise, setPromise] = React.useState(Promise.withResolvers<Context.Context<R>>().promise)
React.useEffect(() => {
const { promise, resolve, reject } = Promise.withResolvers<Context.Context<R>>()
setPromise(promise)
const scope = runSync(props.scope
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy)
)
Effect.context<R>().pipe(
Effect.match({
onSuccess: resolve,
onFailure: reject,
}),
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
effect => runFork(effect, { ...props, scope }),
)
return () => { runFork(Scope.close(scope, Exit.void)) }
}, [props.layer, runSync, runFork])
return React.createElement(React.Suspense, {
children: React.createElement(ReffuseContextAsyncReactProviderInner, { ...props, promise }),
fallback: props.fallback,
})
}
}
export const make = <R = never>() => new ReffuseContext<R>()
export const useMergeAll = <T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Context.Context<T[number]> => {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export const useMergeAllLayers = <T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Layer.Layer<T[number]> => {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Array.isNonEmptyArray(values)
? Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
)
: Layer.empty as Layer.Layer<T[number]>,
values)
}

View File

@@ -0,0 +1,111 @@
import { Array, Context, Effect, Layer, Runtime } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider = makeProvider(this.Context)
readonly AsyncProvider = makeAsyncProvider(this.Context)
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly children?: React.ReactNode
}>
function makeProvider<R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> {
return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const value = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runSync(runtime),
), [props.layer, runtime])
return (
<Context
{...props}
value={value}
/>
)
}
}
export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
function makeAsyncProvider<R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> {
function Inner({ promise, children }: {
readonly promise: Promise<Context.Context<R>>
readonly children?: React.ReactNode
}) {
const value = React.use(promise)
return (
<Context
value={value}
children={children}
/>
)
}
return function ReffuseContextAsyncReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const promise = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runPromise(runtime),
), [props.layer, runtime])
return (
<React.Suspense fallback={props.fallback}>
<Inner
{...props}
promise={promise}
/>
</React.Suspense>
)
}
}
export function make<R = never>() {
return new ReffuseContext<R>()
}
export function useMergeAll<T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Context.Context<T[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export function useMergeAllLayers<T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Layer.Layer<T[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Array.isNonEmptyArray(values)
? Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
)
: Layer.empty as Layer.Layer<T[number]>,
values)
}

View File

@@ -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, type Layer, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import * as ReffuseContext from "./ReffuseContext.js" import * as ReffuseContext from "./ReffuseContext.js"
import * as ReffuseRuntime from "./ReffuseRuntime.js" import * as ReffuseRuntime from "./ReffuseRuntime.js"
import { type PropertyPath, SetStateAction, SubscriptionSubRef } from "./types/index.js" import * as SetStateAction from "./SetStateAction.js"
export interface RenderOptions { export interface RenderOptions {
@@ -14,32 +14,21 @@ export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
} }
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
[K in keyof T]: Effect.Effect.Success<T[K]> export abstract class ReffuseHelpers<R> {
} declare ["constructor"]: ReffuseHelpersClass<R>
export abstract class ReffuseNamespace<R> { useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> {
declare ["constructor"]: ReffuseNamespaceClass<R>
constructor() {
this.SubRef = this.SubRef.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
}
useContext<R>(this: ReffuseNamespace<R>): Context.Context<R> {
return ReffuseContext.useMergeAll(...this.constructor.contexts) return ReffuseContext.useMergeAll(...this.constructor.contexts)
} }
useLayer<R>(this: ReffuseNamespace<R>): Layer.Layer<R> { useLayer<R>(this: ReffuseHelpers<R>): Layer.Layer<R> {
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts) return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
} }
useRunSync<R>(this: ReffuseNamespace<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A { useRunSync<R>(this: ReffuseHelpers<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
const runtime = ReffuseRuntime.useRuntime() const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext() const context = this.useContext()
@@ -49,7 +38,7 @@ export abstract class ReffuseNamespace<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunPromise<R>(this: ReffuseNamespace<R>): <A, E>( useRunPromise<R>(this: ReffuseHelpers<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal }, options?: { readonly signal?: AbortSignal },
) => Promise<A> { ) => Promise<A> {
@@ -62,7 +51,7 @@ export abstract class ReffuseNamespace<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunFork<R>(this: ReffuseNamespace<R>): <A, E>( useRunFork<R>(this: ReffuseHelpers<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions, options?: Runtime.RunForkOptions,
) => Fiber.RuntimeFiber<A, E> { ) => Fiber.RuntimeFiber<A, E> {
@@ -75,7 +64,7 @@ export abstract class ReffuseNamespace<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunCallback<R>(this: ReffuseNamespace<R>): <A, E>( useRunCallback<R>(this: ReffuseHelpers<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: Runtime.RunCallbackOptions<A, E>, options?: Runtime.RunCallbackOptions<A, E>,
) => Runtime.Cancel<A, E> { ) => Runtime.Cancel<A, E> {
@@ -98,7 +87,7 @@ export abstract class ReffuseNamespace<R> {
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`. * You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*/ */
useMemo<A, E, R>( useMemo<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R>, effect: () => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions, options?: RenderOptions,
@@ -112,45 +101,48 @@ export abstract class ReffuseNamespace<R> {
} }
useMemoScoped<A, E, R>( useMemoScoped<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
): A { ): A {
const runSync = this.useRunSync() const runSync = this.useRunSync()
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe( // Calculate an initial version of the value so that it can be accessed during the first render
Effect.bind("isInitialRun", () => Ref.make(true)), const [initialScope, initialValue] = React.useMemo(() => Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)), Effect.flatMap(scope => effect().pipe(
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)), Effect.provideService(Scope.Scope, scope),
Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const), Effect.map(value => [scope, value] as const),
)),
runSync, runSync,
), []) ), [])
// Keep track of the state of the initial scope
const initialScopeClosed = React.useRef(false)
const [value, setValue] = React.useState(initialValue) const [value, setValue] = React.useState(initialValue)
React.useEffect(() => isInitialRun.pipe( React.useEffect(() => {
Effect.if({ const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe(
onTrue: () => Ref.set(isInitialRun, false).pipe( Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })),
Effect.map(() => Effect.when(() => !initialScopeClosed.current),
() => runSync(Scope.close(initialScope, Exit.void))
) )
),
onFalse: () => Effect.Do.pipe( const [scope, value] = closeInitialScopeIfNeeded.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)), Effect.andThen(Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)), Effect.flatMap(scope => effect().pipe(
Effect.tap(({ value }) => Effect.provideService(Scope.Scope, scope),
Effect.sync(() => setValue(value)) Effect.map(value => [scope, value] as const),
), ))
Effect.map(({ scope }) => )),
() => runSync(Scope.close(scope, Exit.void))
),
),
}),
runSync, runSync,
), [ )
setValue(value)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync], ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps, ...deps,
]) ])
@@ -185,7 +177,7 @@ export abstract class ReffuseNamespace<R> {
* ``` * ```
*/ */
useEffect<A, E, R>( useEffect<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
@@ -233,7 +225,7 @@ export abstract class ReffuseNamespace<R> {
* ``` * ```
*/ */
useLayoutEffect<A, E, R>( useLayoutEffect<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
@@ -281,7 +273,7 @@ export abstract class ReffuseNamespace<R> {
* ``` * ```
*/ */
useFork<A, E, R>( useFork<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions, options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -304,7 +296,7 @@ export abstract class ReffuseNamespace<R> {
} }
usePromise<A, E, R>( usePromise<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions, options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -351,7 +343,7 @@ export abstract class ReffuseNamespace<R> {
} }
useCallbackSync<Args extends unknown[], A, E, R>( useCallbackSync<Args extends unknown[], A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>, callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions, options?: RenderOptions,
@@ -365,7 +357,7 @@ export abstract class ReffuseNamespace<R> {
} }
useCallbackPromise<Args extends unknown[], A, E, R>( useCallbackPromise<Args extends unknown[], A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>, callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: { readonly signal?: AbortSignal } & RenderOptions, options?: { readonly signal?: AbortSignal } & RenderOptions,
@@ -379,7 +371,7 @@ export abstract class ReffuseNamespace<R> {
} }
useRef<A, R>( useRef<A, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
value: A, value: A,
): SubscriptionRef.SubscriptionRef<A> { ): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo( return this.useMemo(
@@ -389,41 +381,6 @@ export abstract class ReffuseNamespace<R> {
) )
} }
useSubRef<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
): RefsA<Refs> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) 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 RefsA<Refs>))
),
), refs)
return reactStateValue
}
/** /**
* Binds the state of a `SubscriptionRef` to the state of the React component. * Binds the state of a `SubscriptionRef` to the state of the React component.
* *
@@ -432,19 +389,15 @@ export abstract class ReffuseNamespace<R> {
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render. * Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
*/ */
useRefState<A, R>( useRefState<A, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
ref: SubscriptionRef.SubscriptionRef<A>, ref: SubscriptionRef.SubscriptionRef<A>,
): [A, React.Dispatch<React.SetStateAction<A>>] { ): [A, React.Dispatch<React.SetStateAction<A>>] {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo( const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
() => ref, const [reactStateValue, setReactStateValue] = React.useState(initialState)
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach( this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
Stream.changesWith(ref.changes, (x, y) => x === y), setReactStateValue(v)
v => Effect.sync(() => setReactStateValue(v)), )), [ref])
), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) => const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
Ref.update(ref, prevState => Ref.update(ref, prevState =>
@@ -456,7 +409,7 @@ export abstract class ReffuseNamespace<R> {
} }
useStreamFromValues<const A extends React.DependencyList, R>( useStreamFromValues<const A extends React.DependencyList, R>(
this: ReffuseNamespace<R>, this: ReffuseHelpers<R>,
values: A, values: A,
): Stream.Stream<A> { ): Stream.Stream<A> {
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe( const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
@@ -467,94 +420,27 @@ export abstract class ReffuseNamespace<R> {
return stream return stream
} }
useSubscribeStream<A, InitialA extends A | undefined, 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))
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>
} }
SubRef<B, const P extends PropertyPath.Paths<B>, R>( export interface ReffuseHelpers<R> extends Pipeable.Pipeable {}
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.useSubRef(props.parent, props.path))
}
SubscribeRefs< ReffuseHelpers.prototype.pipe = function pipe() {
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
>(
this: ReffuseNamespace<R>,
props: {
readonly refs: Refs
readonly children: (...args: RefsA<Refs>) => React.ReactNode
},
): React.ReactNode {
return props.children(...this.useSubscribeRefs(...props.refs))
}
RefState<A, R>(
this: ReffuseNamespace<R>,
props: {
readonly ref: SubscriptionRef.SubscriptionRef<A>
readonly children: (state: [A, React.Dispatch<React.SetStateAction<A>>]) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useRefState(props.ref))
}
SubscribeStream<A, InitialA extends A | undefined, 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
},
): React.ReactNode {
return props.children(this.useSubscribeStream(props.stream, props.initialValue))
}
}
export interface ReffuseNamespace<R> extends Pipeable.Pipeable {}
ReffuseNamespace.prototype.pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments) return Pipeable.pipeArguments(this, arguments)
}; }
export interface ReffuseNamespaceClass<R> extends Pipeable.Pipeable { export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable {
new(): ReffuseNamespace<R> new(): ReffuseHelpers<R>
make<Self>(this: new () => Self): Self
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[] readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
} }
(ReffuseNamespace as ReffuseNamespaceClass<any>).make = function make() { (ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() {
return new this()
};
(ReffuseNamespace as ReffuseNamespaceClass<any>).pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments) return Pipeable.pipeArguments(this, arguments)
}; }
export const makeClass = (): ReffuseNamespaceClass<never> => ( export const make = (): ReffuseHelpersClass<never> =>
class extends (ReffuseNamespace<never> as ReffuseNamespaceClass<never>) { class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) {
static readonly contexts = [] static readonly contexts = []
} }
)

View File

@@ -1,16 +0,0 @@
import { Runtime } from "effect"
import * as React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = function ReffuseRuntimeReactProvider(props: {
readonly children?: React.ReactNode
}) {
return React.createElement(Context, {
...props,
value: Runtime.defaultRuntime,
})
}
export const useRuntime = () => React.useContext(Context)

View File

@@ -0,0 +1,15 @@
import { Runtime } from "effect"
import * as React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = (props: { readonly children?: React.ReactNode }) => (
<Context
{...props}
value={Runtime.defaultRuntime}
/>
)
Provider.displayName = "ReffuseRuntimeReactProvider"
export const useRuntime = () => React.useContext(Context)

View File

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

View File

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

View File

@@ -1,94 +0,0 @@
import { Array, Function, Option, Predicate } from "effect"
export type Paths<T> = [] | (
T extends readonly any[] ? ArrayPaths<T> :
T extends object ? ObjectPaths<T> :
never
)
export type ArrayPaths<T extends readonly any[]> = {
[K in keyof T as K extends number ? K : never]:
| [K]
| [K, ...Paths<T[K]>]
} extends infer O
? O[keyof O]
: never
export type ObjectPaths<T extends object> = {
[K in keyof T as K extends string | number | symbol ? K : never]:
| [K]
| [K, ...Paths<T[K]>]
} 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()
})

View File

@@ -1,100 +0,0 @@
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
import * as PropertyPath from "./PropertyPath.js"
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
readonly parent: SubscriptionRef.SubscriptionRef<B>
readonly [Unify.typeSymbol]?: unknown
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
}
export declare namespace SubscriptionSubRef {
export interface Variance<in out A, in out B> {
readonly [SubscriptionSubRefTypeId]: {
readonly _A: Types.Invariant<A>
readonly _B: Types.Invariant<B>
}
}
}
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
}
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
SubscriptionRef?: true
}
const refVariance = { _A: (_: any) => _ }
const synchronizedRefVariance = { _A: (_: any) => _ }
const subscriptionRefVariance = { _A: (_: any) => _ }
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
readonly [Ref.RefTypeId] = refVariance
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
readonly get: Effect.Effect<A>
constructor(
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
) {
super()
this.get = Ref.get(this.parent).pipe(Effect.map(this.getter))
}
commit() {
return this.get
}
get changes(): Stream.Stream<A> {
return this.get.pipe(
Effect.map(a => this.parent.changes.pipe(
Stream.map(this.getter),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrap,
)
}
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
return this.modifyEffect(a => Effect.succeed(f(a)))
}
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
return Effect.Do.pipe(
Effect.bind("b", () => Ref.get(this.parent)),
Effect.bind("ca", ({ b }) => f(this.getter(b))),
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
Effect.map(({ ca: [c] }) => c),
)
}
}
export const makeFromGetSet = <A, B>(
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
parent,
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
)

View File

@@ -1,3 +0,0 @@
export * as PropertyPath from "./PropertyPath.js"
export * as SetStateAction from "./SetStateAction.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"