Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db7608f7c3 | ||
|
|
b78f99e808 | ||
|
|
86dde2d286 | ||
|
|
596e0942c5 | ||
|
|
cb61713cce | ||
|
|
1b2b68fbae | ||
|
|
35a8037f5a | ||
|
|
7aef7ae796 | ||
|
|
1bfbeba934 | ||
|
|
fc4295894f | ||
|
|
ab0dce107d | ||
|
|
9436602443 | ||
|
|
66de31706c | ||
|
|
8925fe6336 | ||
|
|
fe8ca23d37 | ||
|
|
d48f20a59d | ||
|
|
b7b4abcbe2 | ||
|
|
3497d17046 | ||
|
|
8008e18221 | ||
|
|
1ca832e69d | ||
|
|
98bd72d1d7 | ||
|
|
f594f47793 | ||
|
|
4f9827720c | ||
|
|
0f761524fd | ||
|
|
574136e161 | ||
|
|
7a12abdbdf | ||
|
|
129ab04ea7 | ||
|
|
870fe479c3 | ||
|
|
8fecb94292 | ||
|
|
4092da0f0c | ||
|
|
26a2111705 | ||
|
|
1cb02407c8 | ||
|
|
6e8ce84851 | ||
|
|
570fb93876 | ||
|
|
821fd18f8f | ||
|
|
b7ef95341b | ||
|
|
5f5ef5614b | ||
|
|
cbd39f893e | ||
|
|
529e3d3f9d | ||
|
|
9d47418a69 | ||
|
|
c1b6e73231 | ||
|
|
d1ba4148f2 | ||
|
|
ef13e87d12 | ||
|
|
8b141b907f | ||
|
|
52a36cb882 | ||
|
|
3b844f071b | ||
|
|
d7c648994d | ||
|
|
4e422a1901 | ||
|
|
a5c6b34dfe | ||
|
|
ab1f851428 | ||
|
|
3f091d55c2 | ||
|
|
76a33fccca | ||
|
|
c75bb10e6b | ||
|
|
3da4b2a318 | ||
|
|
9a24ecaf84 | ||
|
|
7b20df6c71 | ||
|
|
74fa30cf4f | ||
|
|
f40dae90fb | ||
|
|
46211638f5 | ||
|
|
a28d6c3d30 | ||
|
|
6b74b9a3b2 | ||
|
|
e17f945666 | ||
|
|
aa46ecc82d | ||
|
|
8ea9146dd9 | ||
|
|
0a4bb2856d | ||
|
|
b4cd7daa81 | ||
|
|
b5712d5433 | ||
|
|
57b7eac05c | ||
|
|
9a9bd78ec6 | ||
|
|
ddcd681ca4 | ||
|
|
66de517ab5 | ||
|
|
b50255ded2 | ||
|
|
03f0b623ed | ||
|
|
fb6d803723 | ||
|
|
972986241c | ||
|
|
9eb0904600 | ||
|
|
fc86c818e0 | ||
|
|
d01152bdcf | ||
|
|
5a12139602 | ||
|
|
a0928c718f | ||
|
|
49d9edd4b1 | ||
|
|
3552c25b5c | ||
|
|
516e0a465d | ||
|
|
7cf5367409 | ||
|
|
3b237c0588 | ||
|
|
d9aa42d23a | ||
|
|
fd3213c53f | ||
|
|
baa8c92221 | ||
|
|
d55b432846 | ||
|
|
6266c7506e | ||
|
|
043e966e45 | ||
|
|
88fab2c7d7 | ||
|
|
224ccd8e32 | ||
|
|
4cf70ada0b | ||
|
|
f9bd5d4d6b | ||
|
|
1ec1db0658 | ||
|
|
2d94e84941 | ||
|
|
aab83907ba | ||
|
|
8c0d6b4c8a | ||
|
|
d82d1d1c29 | ||
|
|
0f09573948 | ||
|
|
2b6b36713e | ||
|
|
5d0aecc9d5 | ||
|
|
f21d8b2d8a | ||
|
|
f85173fa68 | ||
|
|
65a124de1f | ||
|
|
16893761c6 | ||
|
|
3fdc2e31eb | ||
|
|
8636a28f2f | ||
|
|
d56578da8f | ||
|
|
299109d421 | ||
|
|
4995b2949f | ||
|
|
6e6e675709 | ||
|
|
b04860aa25 | ||
|
|
e9e17ac211 | ||
|
|
1f0ff725ff | ||
|
|
447d89982c | ||
|
|
778ee27795 | ||
|
|
077816efb6 | ||
|
|
e4bacd1ca7 | ||
|
|
0e2c0db28f | ||
|
|
c943d81702 | ||
|
|
c2bc406a5f | ||
|
|
4e778b6c95 | ||
|
|
0437fa5dcc | ||
|
|
5614b8df38 | ||
|
|
70b6c4434e | ||
|
|
2e8dfbc988 | ||
|
|
abc47c4647 | ||
|
|
eedd2a7f2a | ||
|
|
f4ab575a8d | ||
|
|
747e2c6056 | ||
|
|
68c68417d8 | ||
|
|
ed384a62a8 | ||
|
|
3a1748bb39 | ||
|
|
66b8fd2c2e | ||
|
|
bc81c443ab | ||
|
|
ee5dbe3766 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@reffuse/monorepo",
|
||||
"packageManager": "bun@1.2.2",
|
||||
"packageManager": "bun@1.2.9",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
@@ -15,10 +15,9 @@
|
||||
"clean:node": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"code-narrator": "^1.0.17",
|
||||
"npm-check-updates": "^17.1.14",
|
||||
"npm-check-updates": "^17.1.18",
|
||||
"npm-sort": "^0.0.4",
|
||||
"turbo": "^2.4.4",
|
||||
"typescript": "^5.7.3"
|
||||
"turbo": "^2.5.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,41 +11,41 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@tanstack/react-router": "^1.112.7",
|
||||
"@tanstack/router-devtools": "^1.112.7",
|
||||
"@tanstack/router-plugin": "^1.112.7",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@tanstack/react-router": "^1.115.3",
|
||||
"@tanstack/react-router-devtools": "^1.115.3",
|
||||
"@tanstack/router-plugin": "^1.115.3",
|
||||
"@thilawyn/thilaschema": "^0.1.4",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react": "^19.1.1",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript-eslint": "^8.26.0",
|
||||
"vite": "^6.2.0"
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript-eslint": "^8.29.1",
|
||||
"vite": "^6.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.77.6",
|
||||
"@effect/platform-browser": "^0.56.6",
|
||||
"@effect/platform": "^0.80.8",
|
||||
"@effect/platform-browser": "^0.59.8",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reffuse/extension-lazyref": "workspace:*",
|
||||
"@reffuse/extension-query": "workspace:*",
|
||||
"@typed/async-data": "^0.13.1",
|
||||
"@typed/id": "^0.17.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.13.6",
|
||||
"lucide-react": "^0.477.0",
|
||||
"mobx": "^6.13.6",
|
||||
"effect": "^3.14.8",
|
||||
"lucide-react": "^0.487.0",
|
||||
"mobx": "^6.13.7",
|
||||
"reffuse": "workspace:*"
|
||||
},
|
||||
"overrides": {
|
||||
"effect": "^3.13.6",
|
||||
"@effect/platform": "^0.77.6",
|
||||
"@effect/platform-browser": "^0.56.6",
|
||||
"effect": "^3.14.8",
|
||||
"@effect/platform": "^0.80.8",
|
||||
"@effect/platform-browser": "^0.59.8",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"@typed/async-data": "^0.13.1"
|
||||
}
|
||||
|
||||
57
packages/example/src/VQueryErrorHandler.tsx
Normal file
57
packages/example/src/VQueryErrorHandler.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -5,11 +5,14 @@ import { Layer } from "effect"
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { ReffuseRuntime } from "reffuse"
|
||||
import { GlobalContext } from "./reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||
import { RootContext } from "./reffuse"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
|
||||
|
||||
const layer = Layer.empty.pipe(
|
||||
Layer.provideMerge(AppQueryClient.Live),
|
||||
Layer.provideMerge(AppQueryErrorHandler.Live),
|
||||
Layer.provideMerge(Clipboard.layer),
|
||||
Layer.provideMerge(Geolocation.layer),
|
||||
Layer.provideMerge(Permissions.layer),
|
||||
@@ -28,9 +31,9 @@ declare module "@tanstack/react-router" {
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ReffuseRuntime.Provider>
|
||||
<GlobalContext.Provider layer={layer}>
|
||||
<RootContext.Provider layer={layer}>
|
||||
<RouterProvider router={router} />
|
||||
</GlobalContext.Provider>
|
||||
</RootContext.Provider>
|
||||
</ReffuseRuntime.Provider>
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
21
packages/example/src/query.ts
Normal file
21
packages/example/src/query.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { HttpClientError } from "@effect/platform"
|
||||
import { QueryClient, QueryErrorHandler } from "@reffuse/extension-query"
|
||||
import { Effect } from "effect"
|
||||
|
||||
|
||||
export class AppQueryErrorHandler extends QueryErrorHandler.Service<AppQueryErrorHandler,
|
||||
HttpClientError.HttpClientError
|
||||
>()(
|
||||
"AppQueryErrorHandler",
|
||||
|
||||
(self, failure, defect) => self.pipe(
|
||||
Effect.catchTags({
|
||||
RequestError: failure,
|
||||
ResponseError: failure,
|
||||
}),
|
||||
|
||||
Effect.catchAllDefect(defect),
|
||||
),
|
||||
) {}
|
||||
|
||||
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({ ErrorHandler: AppQueryErrorHandler }) {}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { GlobalReffuse } from "@/reffuse"
|
||||
import { RootReffuse } from "@/reffuse"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { Uuid4Query } from "./services"
|
||||
|
||||
|
||||
export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>()
|
||||
|
||||
export const R = new class QueryReffuse extends GlobalReffuse.pipe(
|
||||
export const R = new class QueryReffuse extends RootReffuse.pipe(
|
||||
Reffuse.withContexts(QueryContext)
|
||||
) {}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { HttpClientError } from "@effect/platform"
|
||||
import { QueryService } from "@reffuse/extension-query"
|
||||
import { ParseResult, Schema } from "effect"
|
||||
|
||||
@@ -8,5 +7,5 @@ export const Result = Schema.Array(Schema.String)
|
||||
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
|
||||
readonly ["uuid4", number],
|
||||
typeof Result.Type,
|
||||
HttpClientError.HttpClientError | ParseResult.ParseError
|
||||
ParseResult.ParseError
|
||||
>() {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Uuid4Query } from "../services"
|
||||
|
||||
|
||||
export function Uuid4QueryService() {
|
||||
const runSync = R.useRunSync()
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||
const [state] = R.useRefState(query.state)
|
||||
@@ -25,7 +25,7 @@ export function Uuid4QueryService() {
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => runSync(query.refresh)}>Refresh</Button>
|
||||
<Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -3,19 +3,22 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||
import { QueryExtension } from "@reffuse/extension-query"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
||||
|
||||
|
||||
export const GlobalContext = ReffuseContext.make<
|
||||
export const RootContext = ReffuseContext.make<
|
||||
| AppQueryClient
|
||||
| AppQueryErrorHandler
|
||||
| Clipboard.Clipboard
|
||||
| Geolocation.Geolocation
|
||||
| Permissions.Permissions
|
||||
| HttpClient.HttpClient
|
||||
>()
|
||||
|
||||
export class GlobalReffuse extends Reffuse.Reffuse.pipe(
|
||||
export class RootReffuse extends Reffuse.Reffuse.pipe(
|
||||
Reffuse.withExtension(LazyRefExtension),
|
||||
Reffuse.withExtension(QueryExtension),
|
||||
Reffuse.withContexts(GlobalContext),
|
||||
Reffuse.withContexts(RootContext),
|
||||
) {}
|
||||
|
||||
export const R = new GlobalReffuse()
|
||||
export const R = new RootReffuse()
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as TodosImport } from './routes/todos'
|
||||
import { Route as TimeImport } from './routes/time'
|
||||
import { Route as TestsImport } from './routes/tests'
|
||||
import { Route as PromiseImport } from './routes/promise'
|
||||
@@ -19,10 +20,17 @@ import { Route as CountImport } from './routes/count'
|
||||
import { Route as BlankImport } from './routes/blank'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||
import { Route as QueryServiceImport } from './routes/query/service'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const TodosRoute = TodosImport.update({
|
||||
id: '/todos',
|
||||
path: '/todos',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const TimeRoute = TimeImport.update({
|
||||
id: '/time',
|
||||
path: '/time',
|
||||
@@ -71,6 +79,12 @@ const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryUsemutationRoute = QueryUsemutationImport.update({
|
||||
id: '/query/usemutation',
|
||||
path: '/query/usemutation',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const QueryServiceRoute = QueryServiceImport.update({
|
||||
id: '/query/service',
|
||||
path: '/query/service',
|
||||
@@ -130,6 +144,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof TimeImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/todos': {
|
||||
id: '/todos'
|
||||
path: '/todos'
|
||||
fullPath: '/todos'
|
||||
preLoaderRoute: typeof TodosImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/service': {
|
||||
id: '/query/service'
|
||||
path: '/query/service'
|
||||
@@ -137,6 +158,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof QueryServiceImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/usemutation': {
|
||||
id: '/query/usemutation'
|
||||
path: '/query/usemutation'
|
||||
fullPath: '/query/usemutation'
|
||||
preLoaderRoute: typeof QueryUsemutationImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/query/usequery': {
|
||||
id: '/query/usequery'
|
||||
path: '/query/usequery'
|
||||
@@ -157,7 +185,9 @@ export interface FileRoutesByFullPath {
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -169,7 +199,9 @@ export interface FileRoutesByTo {
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -182,7 +214,9 @@ export interface FileRoutesById {
|
||||
'/promise': typeof PromiseRoute
|
||||
'/tests': typeof TestsRoute
|
||||
'/time': typeof TimeRoute
|
||||
'/todos': typeof TodosRoute
|
||||
'/query/service': typeof QueryServiceRoute
|
||||
'/query/usemutation': typeof QueryUsemutationRoute
|
||||
'/query/usequery': typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -196,7 +230,9 @@ export interface FileRouteTypes {
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
@@ -207,7 +243,9 @@ export interface FileRouteTypes {
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
id:
|
||||
| '__root__'
|
||||
@@ -218,7 +256,9 @@ export interface FileRouteTypes {
|
||||
| '/promise'
|
||||
| '/tests'
|
||||
| '/time'
|
||||
| '/todos'
|
||||
| '/query/service'
|
||||
| '/query/usemutation'
|
||||
| '/query/usequery'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
@@ -231,7 +271,9 @@ export interface RootRouteChildren {
|
||||
PromiseRoute: typeof PromiseRoute
|
||||
TestsRoute: typeof TestsRoute
|
||||
TimeRoute: typeof TimeRoute
|
||||
TodosRoute: typeof TodosRoute
|
||||
QueryServiceRoute: typeof QueryServiceRoute
|
||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||
}
|
||||
|
||||
@@ -243,7 +285,9 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
PromiseRoute: PromiseRoute,
|
||||
TestsRoute: TestsRoute,
|
||||
TimeRoute: TimeRoute,
|
||||
TodosRoute: TodosRoute,
|
||||
QueryServiceRoute: QueryServiceRoute,
|
||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||
}
|
||||
|
||||
@@ -264,7 +308,9 @@ export const routeTree = rootRoute
|
||||
"/promise",
|
||||
"/tests",
|
||||
"/time",
|
||||
"/todos",
|
||||
"/query/service",
|
||||
"/query/usemutation",
|
||||
"/query/usequery"
|
||||
]
|
||||
},
|
||||
@@ -289,9 +335,15 @@ export const routeTree = rootRoute
|
||||
"/time": {
|
||||
"filePath": "time.tsx"
|
||||
},
|
||||
"/todos": {
|
||||
"filePath": "todos.tsx"
|
||||
},
|
||||
"/query/service": {
|
||||
"filePath": "query/service.tsx"
|
||||
},
|
||||
"/query/usemutation": {
|
||||
"filePath": "query/usemutation.tsx"
|
||||
},
|
||||
"/query/usequery": {
|
||||
"filePath": "query/usequery.tsx"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VQueryErrorHandler } from "@/VQueryErrorHandler"
|
||||
import { Container, Flex, Theme } from "@radix-ui/themes"
|
||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
||||
|
||||
import "@radix-ui/themes/styles.css"
|
||||
import "../index.css"
|
||||
@@ -26,6 +27,8 @@ function Root() {
|
||||
</Container>
|
||||
|
||||
<Outlet />
|
||||
|
||||
<VQueryErrorHandler />
|
||||
<TanStackRouterDevtools />
|
||||
</Theme>
|
||||
)
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
import { TodosContext } from "@/todos/reffuse"
|
||||
import { TodosState } from "@/todos/services"
|
||||
import { VTodos } from "@/todos/views/VTodos"
|
||||
import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Layer } from "effect"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: Index
|
||||
export const Route = createFileRoute('/')({
|
||||
component: RouteComponent
|
||||
})
|
||||
|
||||
function Index() {
|
||||
|
||||
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||
Layer.provideMerge(TodosState.make("todos"))
|
||||
), [])
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TodosContext.Provider layer={todosLayer}>
|
||||
<VTodos />
|
||||
</TodosContext.Provider>
|
||||
</Container>
|
||||
)
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/"!</div>
|
||||
}
|
||||
|
||||
81
packages/example/src/routes/query/usemutation.tsx
Normal file
81
packages/example/src/routes/query/usemutation.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { HttpClient } from "@effect/platform"
|
||||
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { Array, Console, Effect, flow, Option, Schema } from "effect"
|
||||
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/query/usequery")({
|
||||
const Result = Schema.Array(Schema.String)
|
||||
|
||||
function RouteComponent() {
|
||||
const runSync = R.useRunSync()
|
||||
const runFork = R.useRunFork()
|
||||
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
@@ -31,7 +31,7 @@ function RouteComponent() {
|
||||
),
|
||||
})
|
||||
|
||||
const [state] = R.useRefState(query.state)
|
||||
const [state] = R.useSubscribeRefs(query.state)
|
||||
|
||||
|
||||
return (
|
||||
@@ -59,7 +59,15 @@ function RouteComponent() {
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => runSync(query.refresh)}>Refresh</Button>
|
||||
<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>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { R } from "@/reffuse"
|
||||
import { Button, Flex } from "@radix-ui/themes"
|
||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Console, Effect, Stream } from "effect"
|
||||
import { useState } from "react"
|
||||
import { Console, Effect, Ref } from "effect"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/tests")({
|
||||
@@ -22,9 +21,9 @@ function RouteComponent() {
|
||||
Effect.delay("1 second"),
|
||||
), [])
|
||||
|
||||
const [reactValue, setReactValue] = useState("initial")
|
||||
const reactValueStream = R.useStreamFromValues([reactValue])
|
||||
R.useFork(() => Stream.runForEach(reactValueStream, Console.log), [reactValueStream])
|
||||
|
||||
const uuidRef = R.useRef("none")
|
||||
const anotherRef = R.useRef(69)
|
||||
|
||||
|
||||
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
|
||||
@@ -33,12 +32,16 @@ function RouteComponent() {
|
||||
|
||||
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
Effect.map(setReactValue),
|
||||
Effect.flatMap(v => Ref.set(uuidRef, v)),
|
||||
), [])
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
|
||||
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
|
||||
</R.SubscribeRefs>
|
||||
|
||||
<Button onClick={() => logValue("test")}>Log value</Button>
|
||||
<Button onClick={() => generateUuid()}>Generate UUID</Button>
|
||||
</Flex>
|
||||
|
||||
35
packages/example/src/routes/todos.tsx
Normal file
35
packages/example/src/routes/todos.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TodosContext } from "@/todos/reffuse"
|
||||
import { TodosState } from "@/todos/services"
|
||||
import { VTodos } from "@/todos/views/VTodos"
|
||||
import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Layer } from "effect"
|
||||
import { useMemo } from "react"
|
||||
|
||||
|
||||
export const Route = createFileRoute("/todos")({
|
||||
component: Todos
|
||||
})
|
||||
|
||||
function Todos() {
|
||||
|
||||
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||
Layer.provideMerge(TodosState.make("todos")),
|
||||
|
||||
Layer.merge(Layer.effectDiscard(
|
||||
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
|
||||
Effect.andThen(Console.log("TodosContext constructed"))
|
||||
)
|
||||
)),
|
||||
), [])
|
||||
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TodosContext.Provider layer={todosLayer}>
|
||||
<VTodos />
|
||||
</TodosContext.Provider>
|
||||
</Container>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { GlobalReffuse } from "@/reffuse"
|
||||
import { RootReffuse } from "@/reffuse"
|
||||
import { Reffuse, ReffuseContext } from "reffuse"
|
||||
import { TodosState } from "./services"
|
||||
|
||||
|
||||
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
|
||||
|
||||
export const R = new class TodosReffuse extends GlobalReffuse.pipe(
|
||||
export const R = new class TodosReffuse extends RootReffuse.pipe(
|
||||
Reffuse.withContexts(TodosContext)
|
||||
) {}
|
||||
|
||||
9
packages/extension-form/README.md
Normal file
9
packages/extension-form/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# LazyRef extension for Reffuse
|
||||
|
||||
Extension to integrate `@typed/lazy-ref` with Reffuse.
|
||||
|
||||
## Peer dependencies
|
||||
- `@typed/lazy-ref`
|
||||
- `reffuse` 0.1.3+
|
||||
- `effect` 3.13+
|
||||
- `react` & `@types/react` 19+
|
||||
40
packages/extension-form/package.json
Normal file
40
packages/extension-form/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@reffuse/extension-form",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
"./dist"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"url": "git+https://github.com/Thiladev/reffuse.git"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/*.d.ts",
|
||||
"default": "./dist/*.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"pack": "npm pack",
|
||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||
"clean:dist": "rm -rf dist",
|
||||
"clean:node": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"reffuse": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "^3.13.0",
|
||||
"react": "^19.0.0",
|
||||
"reffuse": "^0.1.7"
|
||||
}
|
||||
}
|
||||
9
packages/extension-form/src/FormExtension.ts
Normal file
9
packages/extension-form/src/FormExtension.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||
|
||||
|
||||
export const FormExtension = ReffuseExtension.make(() => ({
|
||||
useForm<A, E, R>(
|
||||
this: ReffuseNamespace.ReffuseNamespace<R>,
|
||||
) {
|
||||
},
|
||||
}))
|
||||
1
packages/extension-form/src/index.ts
Normal file
1
packages/extension-form/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./FormExtension.js"
|
||||
6
packages/extension-form/src/internal/Form.ts
Normal file
6
packages/extension-form/src/internal/Form.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
|
||||
export interface Form<A, I, R> {
|
||||
readonly schema: Schema.Schema<A, I, R>
|
||||
}
|
||||
69
packages/extension-form/src/internal/FormField.ts
Normal file
69
packages/extension-form/src/internal/FormField.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Effect, Schema } from "effect"
|
||||
import type * as Formify from "./Formify.js"
|
||||
|
||||
|
||||
export interface FormField<S extends Schema.Schema.Any> {
|
||||
readonly schema: S
|
||||
}
|
||||
|
||||
export const makeFormField = <S extends Schema.Schema.Any>(
|
||||
schema: S,
|
||||
get: Effect.Effect<S["Type"]>,
|
||||
set: (value: S["Type"]) => Effect.Effect<void>,
|
||||
): FormField<S> => {
|
||||
|
||||
}
|
||||
|
||||
export interface UnionFormField<
|
||||
S extends Schema.Union<Members>,
|
||||
Members extends ReadonlyArray<Schema.Schema.All>,
|
||||
> extends FormField<S> {
|
||||
readonly member: Formify.Formify<Members[number]>
|
||||
}
|
||||
|
||||
export interface TupleFormField<
|
||||
S extends Schema.TupleType<Elements, Rest>,
|
||||
Elements extends Schema.TupleType.Elements,
|
||||
Rest extends Schema.TupleType.Rest,
|
||||
> extends FormField<S> {
|
||||
readonly elements: [...{ readonly [K in keyof Elements]: Formify.Formify<Elements[K]> }]
|
||||
}
|
||||
|
||||
export interface ArrayFormField<
|
||||
S extends Schema.Array$<Value>,
|
||||
Value extends Schema.Schema.Any,
|
||||
> extends FormField<S> {
|
||||
readonly elements: readonly Formify.Formify<Value>[]
|
||||
}
|
||||
|
||||
export type StructFormField<
|
||||
S extends Schema.Struct<Fields>,
|
||||
Fields extends Schema.Struct.Fields,
|
||||
> = (
|
||||
& FormField<S>
|
||||
& { readonly fields: { readonly [K in keyof Fields]: Formify.Formify<Fields[K]> } }
|
||||
& {
|
||||
[K in keyof Fields as Fields[K] extends
|
||||
Schema.tag<infer _> ? K : never
|
||||
]: Fields[K] extends
|
||||
Schema.tag<infer Tag> ? Tag : never
|
||||
}
|
||||
)
|
||||
|
||||
export interface GenericFormField<S extends Schema.Schema.Any> extends FormField<S> {
|
||||
}
|
||||
|
||||
|
||||
export interface PropertySignatureFormField<
|
||||
S extends Schema.PropertySignature<TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R>,
|
||||
TypeToken extends Schema.PropertySignature.Token,
|
||||
Type,
|
||||
Key extends PropertyKey,
|
||||
EncodedToken extends Schema.PropertySignature.Token,
|
||||
Encoded,
|
||||
HasDefault extends boolean = false,
|
||||
R = never,
|
||||
> {
|
||||
readonly propertySignature: S
|
||||
readonly value: Type
|
||||
}
|
||||
51
packages/extension-form/src/internal/Formify.ts
Normal file
51
packages/extension-form/src/internal/Formify.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Schema } from "effect"
|
||||
import type * as FormField from "./FormField.js"
|
||||
|
||||
|
||||
export type Formify<S> = (
|
||||
S extends Schema.Union<infer Members> ? FormField.UnionFormField<S, Members> :
|
||||
S extends Schema.TupleType<infer Elements, infer Rest> ? FormField.TupleFormField<S, Elements, Rest> :
|
||||
S extends Schema.Array$<infer Value> ? FormField.ArrayFormField<S, Value> :
|
||||
S extends Schema.Struct<infer Fields> ? FormField.StructFormField<S, Fields> :
|
||||
S extends Schema.Schema.Any ? FormField.GenericFormField<S> :
|
||||
S extends Schema.PropertySignature<
|
||||
infer TypeToken,
|
||||
infer Type,
|
||||
infer Key,
|
||||
infer EncodedToken,
|
||||
infer Encoded,
|
||||
infer HasDefault,
|
||||
infer R
|
||||
> ? FormField.PropertySignatureFormField<S, TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R> :
|
||||
never
|
||||
)
|
||||
|
||||
|
||||
const Login = Schema.Union(
|
||||
Schema.Struct({
|
||||
_tag: Schema.tag("ByEmail"),
|
||||
email: Schema.String,
|
||||
password: Schema.RedactedFromSelf(Schema.String),
|
||||
}),
|
||||
|
||||
Schema.Struct({
|
||||
_tag: Schema.tag("ByPhone"),
|
||||
phone: Schema.String,
|
||||
password: Schema.RedactedFromSelf(Schema.String),
|
||||
}),
|
||||
|
||||
Schema.TaggedStruct("ByKey", {
|
||||
id: Schema.String,
|
||||
password: Schema.RedactedFromSelf(Schema.String),
|
||||
}),
|
||||
)
|
||||
type LoginForm = Formify<typeof Login>
|
||||
declare const loginForm: LoginForm
|
||||
|
||||
switch (loginForm.member._tag) {
|
||||
case "ByEmail":
|
||||
loginForm.member
|
||||
break
|
||||
case "ByPhone":
|
||||
break
|
||||
}
|
||||
37
packages/extension-form/src/internal/guards.ts
Normal file
37
packages/extension-form/src/internal/guards.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Array, Predicate, Record, Schema, Tuple } from "effect"
|
||||
|
||||
|
||||
export const isTupleSchema = (u: unknown): u is Schema.Tuple<any> => (
|
||||
Schema.isSchema(u) &&
|
||||
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) &&
|
||||
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest)
|
||||
)
|
||||
|
||||
export const isArraySchema = (u: unknown): u is Schema.Array$<any> => (
|
||||
Schema.isSchema(u) &&
|
||||
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) && Array.isEmptyArray(u.elements) &&
|
||||
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest) && Tuple.isTupleOf(u.rest, 1) &&
|
||||
Predicate.hasProperty(u, "value")
|
||||
)
|
||||
|
||||
export const isStructSchema = (u: unknown): u is Schema.Struct<any> => (
|
||||
Schema.isSchema(u) &&
|
||||
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) &&
|
||||
Predicate.hasProperty(u, "records") && Array.isArray(u.records) && Array.isEmptyArray(u.records)
|
||||
)
|
||||
|
||||
export const isRecordSchema = (u: unknown): u is Schema.Record$<any, any> => (
|
||||
Schema.isSchema(u) &&
|
||||
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) && Record.isEmptyRecord(u.fields) &&
|
||||
Predicate.hasProperty(u, "records") && Array.isArray(u.records) &&
|
||||
Predicate.hasProperty(u, "key") &&
|
||||
Predicate.hasProperty(u, "value")
|
||||
)
|
||||
|
||||
|
||||
const myTuple = Schema.Tuple(Schema.String)
|
||||
const myArray = Schema.Array(Schema.String)
|
||||
const myStruct = Schema.Struct({})
|
||||
const myRecord = Schema.Record({ key: Schema.String, value: Schema.String })
|
||||
|
||||
console.log(isArraySchema(myTuple))
|
||||
1
packages/extension-form/src/internal/index.ts
Normal file
1
packages/extension-form/src/internal/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as Form from "./Form.js"
|
||||
33
packages/extension-form/tsconfig.json
Normal file
33
packages/extension-form/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
// "allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "NodeNext",
|
||||
// "allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
// "noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
// Build
|
||||
"outDir": "./dist",
|
||||
"declaration": true
|
||||
},
|
||||
|
||||
"include": ["./src"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@reffuse/extension-lazyref",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -37,6 +37,6 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.13.0",
|
||||
"react": "^19.0.0",
|
||||
"reffuse": "^0.1.3"
|
||||
"reffuse": "^0.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
import * as LazyRef from "@typed/lazy-ref"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Effect, pipe, Stream } from "effect"
|
||||
import * as React from "react"
|
||||
import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse"
|
||||
import { ReffuseExtension, type ReffuseNamespace, SetStateAction } from "reffuse"
|
||||
|
||||
|
||||
export const LazyRefExtension = ReffuseExtension.make(() => ({
|
||||
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>(
|
||||
this: ReffuseHelpers.ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace.ReffuseNamespace<R>,
|
||||
ref: LazyRef.LazyRef<A, E, R>,
|
||||
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||
() => ref,
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||
))
|
||||
|
||||
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
||||
setReactStateValue(v)
|
||||
)), [ref])
|
||||
this.useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, (x, y) => x === y),
|
||||
v => Effect.sync(() => setReactStateValue(v)),
|
||||
), [ref])
|
||||
|
||||
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||
LazyRef.update(ref, prevState =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@reffuse/extension-query",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
@@ -39,6 +39,6 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"effect": "^3.13.0",
|
||||
"react": "^19.0.0",
|
||||
"reffuse": "^0.1.3"
|
||||
"reffuse": "^0.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
// 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,
|
||||
// }
|
||||
// })
|
||||
16
packages/extension-query/src/MutationService.ts
Normal file
16
packages/extension-query/src/MutationService.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type * as AsyncData from "@typed/async-data"
|
||||
import { Effect, type Fiber, type Stream, type SubscriptionRef } from "effect"
|
||||
|
||||
|
||||
export interface MutationService<K extends readonly unknown[], A, E> {
|
||||
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||
]>
|
||||
}
|
||||
|
||||
export const Tag = <const Id extends string>(id: Id) => <
|
||||
Self, K extends readonly unknown[], A, E = never,
|
||||
>() => Effect.Tag(id)<Self, MutationService<K, A, E>>()
|
||||
65
packages/extension-query/src/QueryClient.ts
Normal file
65
packages/extension-query/src/QueryClient.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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
|
||||
}
|
||||
)
|
||||
65
packages/extension-query/src/QueryErrorHandler.ts
Normal file
65
packages/extension-query/src/QueryErrorHandler.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
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,
|
||||
) {}
|
||||
@@ -1,21 +1,28 @@
|
||||
import type * as AsyncData from "@typed/async-data"
|
||||
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
|
||||
import * as QueryRunner from "./QueryRunner.js"
|
||||
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||
import type * as MutationService from "./MutationService.js"
|
||||
import * as QueryClient from "./QueryClient.js"
|
||||
import type * as QueryProgress from "./QueryProgress.js"
|
||||
import type * as QueryService from "./QueryService.js"
|
||||
import { MutationRunner, QueryRunner } from "./internal/index.js"
|
||||
|
||||
|
||||
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
||||
readonly key: Stream.Stream<K>
|
||||
readonly query: (key: K) => Effect.Effect<A, E, R>
|
||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||
readonly refreshOnWindowFocus?: boolean
|
||||
}
|
||||
|
||||
export interface UseQueryResult<K extends readonly unknown[], A, E> {
|
||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||
readonly 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>(
|
||||
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
|
||||
@@ -23,12 +30,40 @@ 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(() => ({
|
||||
useQuery<K extends readonly unknown[], A, E, R>(
|
||||
this: ReffuseHelpers.ReffuseHelpers<R>,
|
||||
props: UseQueryProps<K, A, E, R>,
|
||||
): UseQueryResult<K, A, E> {
|
||||
useQuery<
|
||||
QK extends readonly unknown[],
|
||||
QA,
|
||||
FallbackA,
|
||||
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({
|
||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||
key: props.key,
|
||||
query: props.query,
|
||||
}), [props.key])
|
||||
@@ -43,13 +78,45 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
||||
return React.useMemo(() => ({
|
||||
latestKey: runner.latestKeyRef,
|
||||
state: runner.stateRef,
|
||||
refresh: runner.forkRefresh,
|
||||
|
||||
forkRefresh: runner.forkRefresh,
|
||||
|
||||
layer: tag => Layer.succeed(tag, {
|
||||
latestKey: runner.latestKeyRef,
|
||||
state: runner.stateRef,
|
||||
refresh: runner.forkRefresh,
|
||||
forkRefresh: runner.forkRefresh,
|
||||
}),
|
||||
}), [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])
|
||||
},
|
||||
}))
|
||||
|
||||
37
packages/extension-query/src/QueryProgress.ts
Normal file
37
packages/extension-query/src/QueryProgress.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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 }
|
||||
}))
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
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,
|
||||
}
|
||||
})
|
||||
@@ -1,32 +1,16 @@
|
||||
import type * as AsyncData from "@typed/async-data"
|
||||
import { type Cause, Effect, type Fiber, type Option, type SubscriptionRef } from "effect"
|
||||
import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect"
|
||||
|
||||
|
||||
export interface QueryService<K extends readonly unknown[], A, E> {
|
||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||
readonly 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>>,
|
||||
]>
|
||||
}
|
||||
|
||||
export const Tag = <const Id extends string>(id: Id) => <
|
||||
Self, K extends readonly unknown[], A, E = never,
|
||||
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
|
||||
|
||||
|
||||
// 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,
|
||||
// }
|
||||
// }))
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export * as MutationService from "./MutationService.js"
|
||||
export * as QueryClient from "./QueryClient.js"
|
||||
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||
export * from "./QueryExtension.js"
|
||||
export * as QueryRunner from "./QueryRunner.js"
|
||||
export * as QueryProgress from "./QueryProgress.js"
|
||||
export * as QueryService from "./QueryService.js"
|
||||
|
||||
98
packages/extension-query/src/internal/MutationRunner.ts
Normal file
98
packages/extension-query/src/internal/MutationRunner.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
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,
|
||||
}
|
||||
})
|
||||
191
packages/extension-query/src/internal/QueryRunner.ts
Normal file
191
packages/extension-query/src/internal/QueryRunner.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { BrowserStream } from "@effect/platform-browser"
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { type Cause, type Context, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||
import type * as QueryClient from "../QueryClient.js"
|
||||
import * as QueryProgress from "../QueryProgress.js"
|
||||
import * as QueryState from "./QueryState.js"
|
||||
|
||||
|
||||
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
|
||||
readonly context: Context.Context<R>
|
||||
|
||||
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
|
||||
AsyncData.Success<A> | AsyncData.Failure<E>,
|
||||
Cause.NoSuchElementException
|
||||
>>>
|
||||
|
||||
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
||||
readonly forkFetch: Effect.Effect<readonly [
|
||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||
]>
|
||||
readonly forkRefresh: Effect.Effect<readonly [
|
||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||
]>
|
||||
|
||||
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
|
||||
readonly refreshOnWindowFocus: Effect.Effect<void>
|
||||
}
|
||||
|
||||
|
||||
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||
readonly key: Stream.Stream<K>
|
||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||
}
|
||||
|
||||
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
||||
{
|
||||
QueryClient,
|
||||
key,
|
||||
query,
|
||||
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||
): Effect.Effect<
|
||||
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
|
||||
never,
|
||||
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||
> => Effect.gen(function*() {
|
||||
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
||||
|
||||
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
|
||||
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
||||
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
|
||||
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
|
||||
Cause.NoSuchElementException
|
||||
>>())
|
||||
|
||||
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||
|
||||
const interrupt = fiberRef.pipe(
|
||||
Effect.flatMap(Option.match({
|
||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||
Effect.andThen(Fiber.interrupt(fiber))
|
||||
),
|
||||
onNone: () => Effect.void,
|
||||
}))
|
||||
)
|
||||
|
||||
const forkInterrupt = fiberRef.pipe(
|
||||
Effect.flatMap(Option.match({
|
||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
||||
Effect.asVoid,
|
||||
Effect.forkDaemon,
|
||||
))
|
||||
),
|
||||
onNone: () => Effect.forkDaemon(Effect.void),
|
||||
}))
|
||||
)
|
||||
|
||||
const run = Effect.Do.pipe(
|
||||
Effect.bind("state", () => queryStateTag),
|
||||
Effect.bind("client", () => QueryClient),
|
||||
Effect.bind("latestKey", () => latestKeyRef.pipe(Effect.flatMap(identity))),
|
||||
|
||||
Effect.flatMap(({ state, client, latestKey }) => query(latestKey).pipe(
|
||||
client.errorHandler.handle,
|
||||
Effect.matchCauseEffect({
|
||||
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
|
||||
Effect.tap(state.set)
|
||||
),
|
||||
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
|
||||
Effect.tap(state.set)
|
||||
),
|
||||
}),
|
||||
)),
|
||||
|
||||
Effect.provide(context),
|
||||
Effect.provide(QueryProgress.QueryProgress.Live),
|
||||
)
|
||||
|
||||
const forkFetch = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||
Effect.flatMap(stateQueue => queryStateTag.pipe(
|
||||
Effect.flatMap(state => interrupt.pipe(
|
||||
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
|
||||
Effect.andThen(Queue.shutdown(stateQueue))
|
||||
)).pipe(
|
||||
Effect.andThen(state.set(AsyncData.loading())),
|
||||
Effect.andThen(run),
|
||||
Effect.scoped,
|
||||
Effect.forkDaemon,
|
||||
)),
|
||||
|
||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||
)),
|
||||
|
||||
Effect.provide(QueryState.layer(
|
||||
queryStateTag,
|
||||
stateRef,
|
||||
value => Queue.offer(stateQueue, value).pipe(
|
||||
Effect.andThen(Ref.set(stateRef, value))
|
||||
),
|
||||
)),
|
||||
))
|
||||
)
|
||||
|
||||
const setInitialRefreshState = queryStateTag.pipe(
|
||||
Effect.flatMap(state => state.update(previous => {
|
||||
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
||||
return AsyncData.refreshing(previous)
|
||||
if (AsyncData.isRefreshing(previous))
|
||||
return AsyncData.refreshing(previous.previous)
|
||||
return AsyncData.loading()
|
||||
}))
|
||||
)
|
||||
|
||||
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||
Effect.flatMap(stateQueue => interrupt.pipe(
|
||||
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
|
||||
Effect.andThen(Queue.shutdown(stateQueue))
|
||||
)).pipe(
|
||||
Effect.andThen(setInitialRefreshState),
|
||||
Effect.andThen(run),
|
||||
Effect.scoped,
|
||||
Effect.forkDaemon,
|
||||
)),
|
||||
|
||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||
|
||||
Effect.provide(QueryState.layer(
|
||||
queryStateTag,
|
||||
stateRef,
|
||||
value => Queue.offer(stateQueue, value).pipe(
|
||||
Effect.andThen(Ref.set(stateRef, value))
|
||||
),
|
||||
)),
|
||||
))
|
||||
)
|
||||
|
||||
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
||||
Effect.andThen(Stream.runForEach(Stream.changes(key), latestKey =>
|
||||
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
||||
Effect.andThen(forkFetch)
|
||||
)
|
||||
))
|
||||
)
|
||||
|
||||
const refreshOnWindowFocus = Stream.runForEach(
|
||||
BrowserStream.fromEventListenerWindow("focus"),
|
||||
() => forkRefresh,
|
||||
)
|
||||
|
||||
return {
|
||||
context,
|
||||
|
||||
latestKeyRef,
|
||||
stateRef,
|
||||
fiberRef,
|
||||
|
||||
forkInterrupt,
|
||||
forkFetch,
|
||||
forkRefresh,
|
||||
|
||||
fetchOnKeyChange,
|
||||
refreshOnWindowFocus,
|
||||
}
|
||||
})
|
||||
24
packages/extension-query/src/internal/QueryState.ts
Normal file
24
packages/extension-query/src/internal/QueryState.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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),
|
||||
),
|
||||
})
|
||||
3
packages/extension-query/src/internal/index.ts
Normal file
3
packages/extension-query/src/internal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * as MutationRunner from "./MutationRunner.js"
|
||||
export * as QueryRunner from "./QueryRunner.js"
|
||||
export * as QueryState from "./QueryState.js"
|
||||
@@ -1,14 +0,0 @@
|
||||
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 }}
|
||||
###
|
||||
@@ -1,4 +0,0 @@
|
||||
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
|
||||
@@ -1,4 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
|
||||
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;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "reffuse",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.7",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"./README.md",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import type * as ReffuseContext from "./ReffuseContext.js"
|
||||
import type * as ReffuseExtension from "./ReffuseExtension.js"
|
||||
import * as ReffuseHelpers from "./ReffuseHelpers.js"
|
||||
import * as ReffuseNamespace from "./ReffuseNamespace.js"
|
||||
import type { Merge, StaticType } from "./types.js"
|
||||
|
||||
|
||||
export class Reffuse extends ReffuseHelpers.make() {}
|
||||
export class Reffuse extends ReffuseNamespace.makeClass() {}
|
||||
|
||||
|
||||
export const withContexts = <R2 extends Array<unknown>>(
|
||||
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
|
||||
) =>
|
||||
) => (
|
||||
<
|
||||
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R1>,
|
||||
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R1>,
|
||||
R1
|
||||
>(
|
||||
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R1>
|
||||
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R1>
|
||||
): (
|
||||
{
|
||||
new(): Merge<
|
||||
InstanceType<BaseClass>,
|
||||
{ constructor: ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]> }
|
||||
{ constructor: ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]> }
|
||||
>
|
||||
} &
|
||||
Merge<
|
||||
StaticType<BaseClass>,
|
||||
StaticType<ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]>>
|
||||
StaticType<ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]>>
|
||||
>
|
||||
) => class extends self {
|
||||
static readonly contexts = [...self.contexts, ...contexts]
|
||||
} as any
|
||||
)
|
||||
|
||||
|
||||
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) =>
|
||||
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) => (
|
||||
<
|
||||
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R>,
|
||||
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R>,
|
||||
R
|
||||
>(
|
||||
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R>
|
||||
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R>
|
||||
): (
|
||||
{ new(): Merge<InstanceType<BaseClass>, A> } &
|
||||
StaticType<BaseClass>
|
||||
@@ -45,3 +45,4 @@ export const withExtension = <A extends object>(extension: ReffuseExtension.Reff
|
||||
Object.assign(class_.prototype, extension())
|
||||
return class_ as any
|
||||
}
|
||||
)
|
||||
|
||||
160
packages/reffuse/src/ReffuseContext.ts
Normal file
160
packages/reffuse/src/ReffuseContext.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||
import * as React from "react"
|
||||
import * as ReffuseContext from "./ReffuseContext.js"
|
||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||
@@ -15,20 +15,26 @@ export interface ScopeOptions {
|
||||
}
|
||||
|
||||
|
||||
export abstract class ReffuseHelpers<R> {
|
||||
declare ["constructor"]: ReffuseHelpersClass<R>
|
||||
export abstract class ReffuseNamespace<R> {
|
||||
declare ["constructor"]: ReffuseNamespaceClass<R>
|
||||
|
||||
constructor() {
|
||||
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
|
||||
this.RefState = this.RefState.bind(this as any) as any
|
||||
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
|
||||
}
|
||||
|
||||
|
||||
useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> {
|
||||
useContext<R>(this: ReffuseNamespace<R>): Context.Context<R> {
|
||||
return ReffuseContext.useMergeAll(...this.constructor.contexts)
|
||||
}
|
||||
|
||||
useLayer<R>(this: ReffuseHelpers<R>): Layer.Layer<R> {
|
||||
useLayer<R>(this: ReffuseNamespace<R>): Layer.Layer<R> {
|
||||
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
|
||||
}
|
||||
|
||||
|
||||
useRunSync<R>(this: ReffuseHelpers<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
|
||||
useRunSync<R>(this: ReffuseNamespace<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
|
||||
const runtime = ReffuseRuntime.useRuntime()
|
||||
const context = this.useContext()
|
||||
|
||||
@@ -38,7 +44,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
), [runtime, context])
|
||||
}
|
||||
|
||||
useRunPromise<R>(this: ReffuseHelpers<R>): <A, E>(
|
||||
useRunPromise<R>(this: ReffuseNamespace<R>): <A, E>(
|
||||
effect: Effect.Effect<A, E, R>,
|
||||
options?: { readonly signal?: AbortSignal },
|
||||
) => Promise<A> {
|
||||
@@ -51,7 +57,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
), [runtime, context])
|
||||
}
|
||||
|
||||
useRunFork<R>(this: ReffuseHelpers<R>): <A, E>(
|
||||
useRunFork<R>(this: ReffuseNamespace<R>): <A, E>(
|
||||
effect: Effect.Effect<A, E, R>,
|
||||
options?: Runtime.RunForkOptions,
|
||||
) => Fiber.RuntimeFiber<A, E> {
|
||||
@@ -64,7 +70,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
), [runtime, context])
|
||||
}
|
||||
|
||||
useRunCallback<R>(this: ReffuseHelpers<R>): <A, E>(
|
||||
useRunCallback<R>(this: ReffuseNamespace<R>): <A, E>(
|
||||
effect: Effect.Effect<A, E, R>,
|
||||
options?: Runtime.RunCallbackOptions<A, E>,
|
||||
) => Runtime.Cancel<A, E> {
|
||||
@@ -87,7 +93,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
|
||||
*/
|
||||
useMemo<A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
options?: RenderOptions,
|
||||
@@ -101,48 +107,45 @@ export abstract class ReffuseHelpers<R> {
|
||||
}
|
||||
|
||||
useMemoScoped<A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||
deps: React.DependencyList,
|
||||
options?: RenderOptions & ScopeOptions,
|
||||
): A {
|
||||
const runSync = this.useRunSync()
|
||||
|
||||
// Calculate an initial version of the value so that it can be accessed during the first render
|
||||
const [initialScope, initialValue] = React.useMemo(() => Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||
Effect.flatMap(scope => effect().pipe(
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
Effect.map(value => [scope, value] as const),
|
||||
)),
|
||||
|
||||
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
||||
Effect.bind("isInitialRun", () => Ref.make(true)),
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
|
||||
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
|
||||
Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const),
|
||||
runSync,
|
||||
), [])
|
||||
|
||||
// Keep track of the state of the initial scope
|
||||
const initialScopeClosed = React.useRef(false)
|
||||
|
||||
const [value, setValue] = React.useState(initialValue)
|
||||
|
||||
React.useEffect(() => {
|
||||
const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe(
|
||||
Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })),
|
||||
Effect.when(() => !initialScopeClosed.current),
|
||||
React.useEffect(() => isInitialRun.pipe(
|
||||
Effect.if({
|
||||
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
||||
Effect.map(() =>
|
||||
() => runSync(Scope.close(initialScope, Exit.void))
|
||||
)
|
||||
),
|
||||
|
||||
const [scope, value] = closeInitialScopeIfNeeded.pipe(
|
||||
Effect.andThen(Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||
Effect.flatMap(scope => effect().pipe(
|
||||
Effect.provideService(Scope.Scope, scope),
|
||||
Effect.map(value => [scope, value] as const),
|
||||
))
|
||||
)),
|
||||
onFalse: () => Effect.Do.pipe(
|
||||
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
|
||||
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
|
||||
Effect.tap(({ value }) =>
|
||||
Effect.sync(() => setValue(value))
|
||||
),
|
||||
Effect.map(({ scope }) =>
|
||||
() => runSync(Scope.close(scope, Exit.void))
|
||||
),
|
||||
),
|
||||
}),
|
||||
|
||||
runSync,
|
||||
)
|
||||
|
||||
setValue(value)
|
||||
return () => { runSync(Scope.close(scope, Exit.void)) }
|
||||
}, [
|
||||
), [
|
||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||
...deps,
|
||||
])
|
||||
@@ -177,7 +180,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
* ```
|
||||
*/
|
||||
useEffect<A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||
deps?: React.DependencyList,
|
||||
options?: RenderOptions & ScopeOptions,
|
||||
@@ -225,7 +228,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
* ```
|
||||
*/
|
||||
useLayoutEffect<A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||
deps?: React.DependencyList,
|
||||
options?: RenderOptions & ScopeOptions,
|
||||
@@ -273,7 +276,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
* ```
|
||||
*/
|
||||
useFork<A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||
deps?: React.DependencyList,
|
||||
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
||||
@@ -296,7 +299,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
}
|
||||
|
||||
usePromise<A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||
deps?: React.DependencyList,
|
||||
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
||||
@@ -343,7 +346,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
}
|
||||
|
||||
useCallbackSync<Args extends unknown[], A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
options?: RenderOptions,
|
||||
@@ -357,7 +360,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
}
|
||||
|
||||
useCallbackPromise<Args extends unknown[], A, E, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||
deps: React.DependencyList,
|
||||
options?: { readonly signal?: AbortSignal } & RenderOptions,
|
||||
@@ -371,7 +374,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
}
|
||||
|
||||
useRef<A, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
value: A,
|
||||
): SubscriptionRef.SubscriptionRef<A> {
|
||||
return this.useMemo(
|
||||
@@ -381,6 +384,30 @@ export abstract class ReffuseHelpers<R> {
|
||||
)
|
||||
}
|
||||
|
||||
useSubscribeRefs<
|
||||
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
||||
R,
|
||||
>(
|
||||
this: ReffuseNamespace<R>,
|
||||
...refs: Refs
|
||||
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
|
||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
|
||||
|
||||
this.useFork(() => pipe(
|
||||
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
||||
streams => Stream.zipLatestAll(...streams),
|
||||
Stream.runForEach(v =>
|
||||
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
|
||||
),
|
||||
), refs)
|
||||
|
||||
return reactStateValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the state of a `SubscriptionRef` to the state of the React component.
|
||||
*
|
||||
@@ -389,15 +416,19 @@ export abstract class ReffuseHelpers<R> {
|
||||
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
|
||||
*/
|
||||
useRefState<A, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
ref: SubscriptionRef.SubscriptionRef<A>,
|
||||
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||
() => ref,
|
||||
[],
|
||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||
))
|
||||
|
||||
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
||||
setReactStateValue(v)
|
||||
)), [ref])
|
||||
this.useFork(() => Stream.runForEach(
|
||||
Stream.changesWith(ref.changes, (x, y) => x === y),
|
||||
v => Effect.sync(() => setReactStateValue(v)),
|
||||
), [ref])
|
||||
|
||||
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||
Ref.update(ref, prevState =>
|
||||
@@ -409,7 +440,7 @@ export abstract class ReffuseHelpers<R> {
|
||||
}
|
||||
|
||||
useStreamFromValues<const A extends React.DependencyList, R>(
|
||||
this: ReffuseHelpers<R>,
|
||||
this: ReffuseNamespace<R>,
|
||||
values: A,
|
||||
): Stream.Stream<A> {
|
||||
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
|
||||
@@ -420,27 +451,83 @@ export abstract class ReffuseHelpers<R> {
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
|
||||
SubscribeRefs<
|
||||
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
||||
R,
|
||||
>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly refs: Refs
|
||||
readonly children: (...args: [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode {
|
||||
return props.children(...this.useSubscribeRefs(...props.refs))
|
||||
}
|
||||
|
||||
RefState<A, R>(
|
||||
this: ReffuseNamespace<R>,
|
||||
props: {
|
||||
readonly ref: SubscriptionRef.SubscriptionRef<A>
|
||||
readonly children: (state: [A, React.Dispatch<React.SetStateAction<A>>]) => React.ReactNode
|
||||
},
|
||||
): React.ReactNode {
|
||||
return props.children(this.useRefState(props.ref))
|
||||
}
|
||||
|
||||
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 ReffuseHelpers<R> extends Pipeable.Pipeable {}
|
||||
export interface ReffuseNamespace<R> extends Pipeable.Pipeable {}
|
||||
|
||||
ReffuseHelpers.prototype.pipe = function pipe() {
|
||||
ReffuseNamespace.prototype.pipe = function pipe() {
|
||||
return Pipeable.pipeArguments(this, arguments)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable {
|
||||
new(): ReffuseHelpers<R>
|
||||
export interface ReffuseNamespaceClass<R> extends Pipeable.Pipeable {
|
||||
new(): ReffuseNamespace<R>
|
||||
make<Self>(this: new () => Self): Self
|
||||
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
|
||||
}
|
||||
|
||||
(ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() {
|
||||
(ReffuseNamespace as ReffuseNamespaceClass<any>).make = function make() {
|
||||
return new this()
|
||||
};
|
||||
|
||||
(ReffuseNamespace as ReffuseNamespaceClass<any>).pipe = function pipe() {
|
||||
return Pipeable.pipeArguments(this, arguments)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const make = (): ReffuseHelpersClass<never> =>
|
||||
class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) {
|
||||
export const makeClass = (): ReffuseNamespaceClass<never> => (
|
||||
class extends (ReffuseNamespace<never> as ReffuseNamespaceClass<never>) {
|
||||
static readonly contexts = []
|
||||
}
|
||||
)
|
||||
16
packages/reffuse/src/ReffuseRuntime.ts
Normal file
16
packages/reffuse/src/ReffuseRuntime.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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)
|
||||
@@ -1,15 +0,0 @@
|
||||
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)
|
||||
@@ -1,6 +1,6 @@
|
||||
export * as Reffuse from "./Reffuse.js"
|
||||
export * as ReffuseContext from "./ReffuseContext.js"
|
||||
export * as ReffuseExtension from "./ReffuseExtension.js"
|
||||
export * as ReffuseHelpers from "./ReffuseHelpers.js"
|
||||
export * as ReffuseNamespace from "./ReffuseNamespace.js"
|
||||
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||
export * as SetStateAction from "./SetStateAction.js"
|
||||
|
||||
@@ -8,14 +8,4 @@ export type CommonKeys<A, B> = Extract<keyof A, keyof B>
|
||||
*/
|
||||
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
|
||||
|
||||
export type Extend<Super, Self> =
|
||||
Extendable<Super, Self> extends true
|
||||
? Omit<Super, CommonKeys<Self, Super>> & Self
|
||||
: never
|
||||
|
||||
export type Extendable<Super, Self> =
|
||||
Pick<Self, CommonKeys<Self, Super>> extends Pick<Super, CommonKeys<Self, Super>>
|
||||
? true
|
||||
: false
|
||||
|
||||
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self
|
||||
|
||||
Reference in New Issue
Block a user