Compare commits
20 Commits
next
...
b3ec1c4f49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3ec1c4f49 | ||
|
|
6aafadb4ad | ||
|
|
c4b902b110 | ||
|
|
7e239b0d1e | ||
|
|
038f38d32c | ||
|
|
e8580ec49e | ||
|
|
8ae59bdd93 | ||
|
|
e1a85fbb7e | ||
|
|
4e0cec051f | ||
|
|
6373919fc4 | ||
|
|
5f455295ad | ||
|
|
00bf5a3c63 | ||
|
|
9aa86f19f0 | ||
|
|
64c07a62e6 | ||
|
|
100169952c | ||
|
|
8624a507b3 | ||
|
|
fcd37a5910 | ||
|
|
c91b538c97 | ||
|
|
53232729c3 | ||
|
|
107d1ba359 |
@@ -8,9 +8,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Build
|
- name: Lint TypeScript
|
||||||
run: bun run build
|
run: npm run lint:tsc
|
||||||
|
|||||||
@@ -11,30 +11,18 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v1
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: npm run build
|
||||||
- name: Publish reffuse
|
- name: Publish
|
||||||
uses: JS-DevTools/npm-publish@v3
|
run: |
|
||||||
with:
|
npm config set @thilawyn:registry https://git.valverde.cloud/api/packages/thilawyn/npm/
|
||||||
package: packages/reffuse
|
npm config set -- //git.valverde.cloud/api/packages/thilawyn/npm/:_authToken "${{ vars.NODE_AUTH_TOKEN }}"
|
||||||
access: public
|
npm publish
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
|
||||||
registry: https://registry.npmjs.org
|
|
||||||
- name: Publish @reffuse/extension-lazyref
|
|
||||||
uses: JS-DevTools/npm-publish@v3
|
|
||||||
with:
|
|
||||||
package: packages/extension-lazyref
|
|
||||||
access: public
|
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
|
||||||
registry: https://registry.npmjs.org
|
|
||||||
- name: Publish @reffuse/extension-query
|
|
||||||
uses: JS-DevTools/npm-publish@v3
|
|
||||||
with:
|
|
||||||
package: packages/extension-query
|
|
||||||
access: public
|
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
|
||||||
registry: https://registry.npmjs.org
|
|
||||||
|
|||||||
@@ -18,6 +18,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Build
|
- name: Build
|
||||||
run: bun run build
|
run: npm run build
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: bun run pack
|
run: npm pack --dry-run
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -130,4 +130,3 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
.turbo
|
|
||||||
|
|||||||
1
.npmrc
1
.npmrc
@@ -1 +0,0 @@
|
|||||||
@thilawyn:registry=https://git.valverde.cloud/api/packages/thilawyn/npm/
|
|
||||||
10
README.md
10
README.md
@@ -1,9 +1,3 @@
|
|||||||
# Reffuse Monorepo
|
# Reffuse
|
||||||
|
|
||||||
Reffuse is a [Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
|
Effect integration for React
|
||||||
|
|
||||||
This monorepo contains:
|
|
||||||
- [The `reffuse` library](packages/reffuse)
|
|
||||||
- [`@reffuse/extension-lazyref`, a LazyRef integration for Reffuse](packages/extension-lazyref)
|
|
||||||
- [`@reffuse/extension-query`, TanStack Query style hooks for Reffuse](packages/extension-query)
|
|
||||||
- [An example project](packges/example)
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[install.scopes]
|
|
||||||
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"
|
|
||||||
15
package.json
15
package.json
@@ -1,23 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@reffuse/monorepo",
|
|
||||||
"packageManager": "bun@1.2.13",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": ["./packages/*"],
|
||||||
"./packages/*"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo build --filter=!@reffuse/example",
|
|
||||||
"lint:tsc": "turbo lint:tsc",
|
|
||||||
"pack": "turbo pack --filter=!@reffuse/example",
|
|
||||||
"publish": "turbo publish --filter=!@reffuse/example",
|
|
||||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-check-updates": "^18.0.1",
|
"npm-check-updates": "^17.1.13"
|
||||||
"npm-sort": "^0.0.4",
|
|
||||||
"turbo": "^2.5.3",
|
|
||||||
"typescript": "^5.8.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "@reffuse/example",
|
"name": "example",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint:tsc": "tsc --noEmit",
|
"lint": "eslint .",
|
||||||
"lint:eslint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.17.0",
|
||||||
"@tanstack/react-router": "^1.120.3",
|
"@tanstack/react-router": "^1.95.3",
|
||||||
"@tanstack/react-router-devtools": "^1.120.3",
|
"@tanstack/router-devtools": "^1.95.3",
|
||||||
"@tanstack/router-plugin": "^1.120.3",
|
"@tanstack/router-plugin": "^1.95.3",
|
||||||
"@thilawyn/thilaschema": "^0.1.4",
|
"@types/react": "^19.0.4",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9.26.0",
|
"effect": "^3.12.1",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"globals": "^16.1.0",
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
"react": "^19.1.0",
|
"globals": "^15.14.0",
|
||||||
"react-dom": "^19.1.0",
|
"postcss": "^8.4.49",
|
||||||
"typescript-eslint": "^8.32.1",
|
"react": "^19.0.0",
|
||||||
"vite": "^6.3.5"
|
"react-dom": "^19.0.0",
|
||||||
},
|
"tailwindcss": "^3.4.17",
|
||||||
"dependencies": {
|
"typescript": "~5.6.2",
|
||||||
"@effect/platform": "^0.82.1",
|
"typescript-eslint": "^8.18.2",
|
||||||
"@effect/platform-browser": "^0.62.1",
|
"vite": "^6.0.5"
|
||||||
"@radix-ui/themes": "^3.2.1",
|
|
||||||
"@reffuse/extension-lazyref": "workspace:*",
|
|
||||||
"@reffuse/extension-query": "workspace:*",
|
|
||||||
"@typed/async-data": "^0.13.1",
|
|
||||||
"@typed/id": "^0.17.2",
|
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
|
||||||
"effect": "^3.15.1",
|
|
||||||
"lucide-react": "^0.510.0",
|
|
||||||
"mobx": "^6.13.7",
|
|
||||||
"reffuse": "workspace:*"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"effect": "^3.15.1",
|
|
||||||
"@effect/platform": "^0.82.1",
|
|
||||||
"@effect/platform-browser": "^0.62.1",
|
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
|
||||||
"@typed/async-data": "^0.13.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
packages/example/postcss.config.js
Normal file
6
packages/example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
|
||||||
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { R } from "./reffuse"
|
|
||||||
import { AppQueryErrorHandler } from "./services"
|
|
||||||
|
|
||||||
|
|
||||||
export function VQueryErrorHandler() {
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
const error = R.useSubscribeStream(
|
|
||||||
R.useMemo(() => AppQueryErrorHandler.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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { ThSchema } from "@thilawyn/thilaschema"
|
|
||||||
import { Schema } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export class Todo extends Schema.Class<Todo>("Todo")({
|
|
||||||
_tag: Schema.tag("Todo"),
|
|
||||||
id: Schema.String,
|
|
||||||
content: Schema.String,
|
|
||||||
completedAt: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
|
|
||||||
export const TodoFromJsonStruct = Schema.Struct({
|
|
||||||
...Todo.fields,
|
|
||||||
completedAt: Schema.Option(Schema.DateTimeUtc),
|
|
||||||
}).pipe(
|
|
||||||
ThSchema.assertEncodedJsonifiable
|
|
||||||
)
|
|
||||||
|
|
||||||
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * as Todo from "./Todo"
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
import { FetchHttpClient } from "@effect/platform"
|
|
||||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
|
||||||
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||||
import { Layer } from "effect"
|
|
||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { ReffuseRuntime } from "reffuse"
|
import "./index.css"
|
||||||
import { RootContext } from "./reffuse"
|
import { routeTree } from './routeTree.gen'
|
||||||
import { routeTree } from "./routeTree.gen"
|
|
||||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
|
||||||
|
|
||||||
|
|
||||||
const layer = Layer.empty.pipe(
|
|
||||||
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
|
|
||||||
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
|
|
||||||
Layer.provideMerge(Clipboard.layer),
|
|
||||||
Layer.provideMerge(Geolocation.layer),
|
|
||||||
Layer.provideMerge(Permissions.layer),
|
|
||||||
Layer.provideMerge(FetchHttpClient.layer),
|
|
||||||
)
|
|
||||||
|
|
||||||
const router = createRouter({ routeTree })
|
const router = createRouter({ routeTree })
|
||||||
|
|
||||||
declare module "@tanstack/react-router" {
|
declare module "@tanstack/react-router" {
|
||||||
@@ -27,13 +13,8 @@ declare module "@tanstack/react-router" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ReffuseRuntime.Provider>
|
<RouterProvider router={router} />
|
||||||
<RootContext.Provider layer={layer}>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</RootContext.Provider>
|
|
||||||
</ReffuseRuntime.Provider>
|
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
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 RootReffuse.pipe(
|
|
||||||
Reffuse.withContexts(QueryContext)
|
|
||||||
) {}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { QueryRunner } from "@reffuse/extension-query"
|
|
||||||
import { ParseResult, Schema } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export const Result = Schema.Array(Schema.String)
|
|
||||||
|
|
||||||
export class Uuid4Query extends QueryRunner.Tag("Uuid4Query")<Uuid4Query,
|
|
||||||
readonly ["uuid4", number],
|
|
||||||
typeof Result.Type,
|
|
||||||
ParseResult.ParseError
|
|
||||||
>() {}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * as Uuid4Query from "./Uuid4Query"
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
|
||||||
import * as AsyncData from "@typed/async-data"
|
|
||||||
import { R } from "../reffuse"
|
|
||||||
import { Uuid4Query } from "../services"
|
|
||||||
|
|
||||||
|
|
||||||
export function Uuid4QueryService() {
|
|
||||||
const runFork = R.useRunFork()
|
|
||||||
|
|
||||||
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
|
||||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Flex direction="column" align="center" gap="2">
|
|
||||||
<Text>
|
|
||||||
{AsyncData.match(state, {
|
|
||||||
NoData: () => "No data yet",
|
|
||||||
Loading: () => "Loading...",
|
|
||||||
Success: (value, { isRefreshing, isOptimistic }) =>
|
|
||||||
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
|
|
||||||
Failure: (cause, { isRefreshing }) =>
|
|
||||||
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button>
|
|
||||||
</Flex>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,5 @@
|
|||||||
import { HttpClient } from "@effect/platform"
|
import { make } from "@thilawyn/reffuse/Reffuse"
|
||||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
import { Layer } from "effect"
|
||||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
|
||||||
import { QueryExtension } from "@reffuse/extension-query"
|
|
||||||
import { Reffuse, ReffuseContext } from "reffuse"
|
|
||||||
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
|
||||||
|
|
||||||
|
|
||||||
export const RootContext = ReffuseContext.make<
|
export const Reffuse = make(Layer.empty)
|
||||||
| AppQueryClient.AppQueryClient
|
|
||||||
| AppQueryErrorHandler.AppQueryErrorHandler
|
|
||||||
| Clipboard.Clipboard
|
|
||||||
| Geolocation.Geolocation
|
|
||||||
| Permissions.Permissions
|
|
||||||
| HttpClient.HttpClient
|
|
||||||
>()
|
|
||||||
|
|
||||||
export class RootReffuse extends Reffuse.Reffuse.pipe(
|
|
||||||
Reffuse.withExtension(LazyRefExtension),
|
|
||||||
Reffuse.withExtension(QueryExtension),
|
|
||||||
Reffuse.withContexts(RootContext),
|
|
||||||
) {}
|
|
||||||
|
|
||||||
export const R = new RootReffuse()
|
|
||||||
|
|||||||
@@ -11,93 +11,30 @@
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as TodosImport } from './routes/todos'
|
|
||||||
import { Route as TimeImport } from './routes/time'
|
import { Route as TimeImport } from './routes/time'
|
||||||
import { Route as TestsImport } from './routes/tests'
|
|
||||||
import { Route as PromiseImport } from './routes/promise'
|
|
||||||
import { Route as LazyrefImport } from './routes/lazyref'
|
|
||||||
import { Route as CountImport } from './routes/count'
|
import { Route as CountImport } from './routes/count'
|
||||||
import { Route as BlankImport } from './routes/blank'
|
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
import { Route as StreamsPullImport } from './routes/streams/pull'
|
|
||||||
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
|
||||||
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
|
||||||
import { Route as QueryServiceImport } from './routes/query/service'
|
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const TodosRoute = TodosImport.update({
|
|
||||||
id: '/todos',
|
|
||||||
path: '/todos',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const TimeRoute = TimeImport.update({
|
const TimeRoute = TimeImport.update({
|
||||||
id: '/time',
|
id: '/time',
|
||||||
path: '/time',
|
path: '/time',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const TestsRoute = TestsImport.update({
|
|
||||||
id: '/tests',
|
|
||||||
path: '/tests',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const PromiseRoute = PromiseImport.update({
|
|
||||||
id: '/promise',
|
|
||||||
path: '/promise',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const LazyrefRoute = LazyrefImport.update({
|
|
||||||
id: '/lazyref',
|
|
||||||
path: '/lazyref',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const CountRoute = CountImport.update({
|
const CountRoute = CountImport.update({
|
||||||
id: '/count',
|
id: '/count',
|
||||||
path: '/count',
|
path: '/count',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const BlankRoute = BlankImport.update({
|
|
||||||
id: '/blank',
|
|
||||||
path: '/blank',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const IndexRoute = IndexImport.update({
|
const IndexRoute = IndexImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
const StreamsPullRoute = StreamsPullImport.update({
|
|
||||||
id: '/streams/pull',
|
|
||||||
path: '/streams/pull',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
|
||||||
id: '/query/usequery',
|
|
||||||
path: '/query/usequery',
|
|
||||||
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',
|
|
||||||
getParentRoute: () => rootRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -109,13 +46,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/blank': {
|
|
||||||
id: '/blank'
|
|
||||||
path: '/blank'
|
|
||||||
fullPath: '/blank'
|
|
||||||
preLoaderRoute: typeof BlankImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/count': {
|
'/count': {
|
||||||
id: '/count'
|
id: '/count'
|
||||||
path: '/count'
|
path: '/count'
|
||||||
@@ -123,27 +53,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof CountImport
|
preLoaderRoute: typeof CountImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/lazyref': {
|
|
||||||
id: '/lazyref'
|
|
||||||
path: '/lazyref'
|
|
||||||
fullPath: '/lazyref'
|
|
||||||
preLoaderRoute: typeof LazyrefImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/promise': {
|
|
||||||
id: '/promise'
|
|
||||||
path: '/promise'
|
|
||||||
fullPath: '/promise'
|
|
||||||
preLoaderRoute: typeof PromiseImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/tests': {
|
|
||||||
id: '/tests'
|
|
||||||
path: '/tests'
|
|
||||||
fullPath: '/tests'
|
|
||||||
preLoaderRoute: typeof TestsImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/time': {
|
'/time': {
|
||||||
id: '/time'
|
id: '/time'
|
||||||
path: '/time'
|
path: '/time'
|
||||||
@@ -151,41 +60,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof TimeImport
|
preLoaderRoute: typeof TimeImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
'/todos': {
|
|
||||||
id: '/todos'
|
|
||||||
path: '/todos'
|
|
||||||
fullPath: '/todos'
|
|
||||||
preLoaderRoute: typeof TodosImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/query/service': {
|
|
||||||
id: '/query/service'
|
|
||||||
path: '/query/service'
|
|
||||||
fullPath: '/query/service'
|
|
||||||
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'
|
|
||||||
fullPath: '/query/usequery'
|
|
||||||
preLoaderRoute: typeof QueryUsequeryImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
'/streams/pull': {
|
|
||||||
id: '/streams/pull'
|
|
||||||
path: '/streams/pull'
|
|
||||||
fullPath: '/streams/pull'
|
|
||||||
preLoaderRoute: typeof StreamsPullImport
|
|
||||||
parentRoute: typeof rootRoute
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,124 +67,42 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
|
||||||
'/promise': typeof PromiseRoute
|
|
||||||
'/tests': typeof TestsRoute
|
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
'/todos': typeof TodosRoute
|
|
||||||
'/query/service': typeof QueryServiceRoute
|
|
||||||
'/query/usemutation': typeof QueryUsemutationRoute
|
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
|
||||||
'/streams/pull': typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
|
||||||
'/promise': typeof PromiseRoute
|
|
||||||
'/tests': typeof TestsRoute
|
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
'/todos': typeof TodosRoute
|
|
||||||
'/query/service': typeof QueryServiceRoute
|
|
||||||
'/query/usemutation': typeof QueryUsemutationRoute
|
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
|
||||||
'/streams/pull': typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
|
||||||
'/promise': typeof PromiseRoute
|
|
||||||
'/tests': typeof TestsRoute
|
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
'/todos': typeof TodosRoute
|
|
||||||
'/query/service': typeof QueryServiceRoute
|
|
||||||
'/query/usemutation': typeof QueryUsemutationRoute
|
|
||||||
'/query/usequery': typeof QueryUsequeryRoute
|
|
||||||
'/streams/pull': typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths: '/' | '/count' | '/time'
|
||||||
| '/'
|
|
||||||
| '/blank'
|
|
||||||
| '/count'
|
|
||||||
| '/lazyref'
|
|
||||||
| '/promise'
|
|
||||||
| '/tests'
|
|
||||||
| '/time'
|
|
||||||
| '/todos'
|
|
||||||
| '/query/service'
|
|
||||||
| '/query/usemutation'
|
|
||||||
| '/query/usequery'
|
|
||||||
| '/streams/pull'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to: '/' | '/count' | '/time'
|
||||||
| '/'
|
id: '__root__' | '/' | '/count' | '/time'
|
||||||
| '/blank'
|
|
||||||
| '/count'
|
|
||||||
| '/lazyref'
|
|
||||||
| '/promise'
|
|
||||||
| '/tests'
|
|
||||||
| '/time'
|
|
||||||
| '/todos'
|
|
||||||
| '/query/service'
|
|
||||||
| '/query/usemutation'
|
|
||||||
| '/query/usequery'
|
|
||||||
| '/streams/pull'
|
|
||||||
id:
|
|
||||||
| '__root__'
|
|
||||||
| '/'
|
|
||||||
| '/blank'
|
|
||||||
| '/count'
|
|
||||||
| '/lazyref'
|
|
||||||
| '/promise'
|
|
||||||
| '/tests'
|
|
||||||
| '/time'
|
|
||||||
| '/todos'
|
|
||||||
| '/query/service'
|
|
||||||
| '/query/usemutation'
|
|
||||||
| '/query/usequery'
|
|
||||||
| '/streams/pull'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BlankRoute: typeof BlankRoute
|
|
||||||
CountRoute: typeof CountRoute
|
CountRoute: typeof CountRoute
|
||||||
LazyrefRoute: typeof LazyrefRoute
|
|
||||||
PromiseRoute: typeof PromiseRoute
|
|
||||||
TestsRoute: typeof TestsRoute
|
|
||||||
TimeRoute: typeof TimeRoute
|
TimeRoute: typeof TimeRoute
|
||||||
TodosRoute: typeof TodosRoute
|
|
||||||
QueryServiceRoute: typeof QueryServiceRoute
|
|
||||||
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
|
||||||
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
|
||||||
StreamsPullRoute: typeof StreamsPullRoute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BlankRoute: BlankRoute,
|
|
||||||
CountRoute: CountRoute,
|
CountRoute: CountRoute,
|
||||||
LazyrefRoute: LazyrefRoute,
|
|
||||||
PromiseRoute: PromiseRoute,
|
|
||||||
TestsRoute: TestsRoute,
|
|
||||||
TimeRoute: TimeRoute,
|
TimeRoute: TimeRoute,
|
||||||
TodosRoute: TodosRoute,
|
|
||||||
QueryServiceRoute: QueryServiceRoute,
|
|
||||||
QueryUsemutationRoute: QueryUsemutationRoute,
|
|
||||||
QueryUsequeryRoute: QueryUsequeryRoute,
|
|
||||||
StreamsPullRoute: StreamsPullRoute,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@@ -324,54 +116,18 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
"/blank",
|
|
||||||
"/count",
|
"/count",
|
||||||
"/lazyref",
|
"/time"
|
||||||
"/promise",
|
|
||||||
"/tests",
|
|
||||||
"/time",
|
|
||||||
"/todos",
|
|
||||||
"/query/service",
|
|
||||||
"/query/usemutation",
|
|
||||||
"/query/usequery",
|
|
||||||
"/streams/pull"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
},
|
},
|
||||||
"/blank": {
|
|
||||||
"filePath": "blank.tsx"
|
|
||||||
},
|
|
||||||
"/count": {
|
"/count": {
|
||||||
"filePath": "count.tsx"
|
"filePath": "count.tsx"
|
||||||
},
|
},
|
||||||
"/lazyref": {
|
|
||||||
"filePath": "lazyref.tsx"
|
|
||||||
},
|
|
||||||
"/promise": {
|
|
||||||
"filePath": "promise.tsx"
|
|
||||||
},
|
|
||||||
"/tests": {
|
|
||||||
"filePath": "tests.tsx"
|
|
||||||
},
|
|
||||||
"/time": {
|
"/time": {
|
||||||
"filePath": "time.tsx"
|
"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"
|
|
||||||
},
|
|
||||||
"/streams/pull": {
|
|
||||||
"filePath": "streams/pull.tsx"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { VQueryErrorHandler } from "@/VQueryErrorHandler"
|
import { Reffuse } from "@/reffuse"
|
||||||
import { Container, Flex, Theme } from "@radix-ui/themes"
|
|
||||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
||||||
|
|
||||||
import "@radix-ui/themes/styles.css"
|
|
||||||
import "../index.css"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
@@ -13,23 +9,15 @@ export const Route = createRootRoute({
|
|||||||
|
|
||||||
function Root() {
|
function Root() {
|
||||||
return (
|
return (
|
||||||
<Theme>
|
<Reffuse.Provider>
|
||||||
<Container>
|
<div className="container mx-auto flex-row justify-center items-center gap-2 mb-4">
|
||||||
<Flex direction="row" justify="center" align="center" gap="2">
|
<Link to="/">Index</Link>
|
||||||
<Link to="/">Index</Link>
|
<Link to="/time">Time</Link>
|
||||||
<Link to="/time">Time</Link>
|
<Link to="/count">Count</Link>
|
||||||
<Link to="/count">Count</Link>
|
</div>
|
||||||
<Link to="/tests">Tests</Link>
|
|
||||||
<Link to="/promise">Promise</Link>
|
|
||||||
<Link to="/query/usequery">Query</Link>
|
|
||||||
<Link to="/blank">Blank</Link>
|
|
||||||
</Flex>
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
||||||
<VQueryErrorHandler />
|
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</Theme>
|
</Reffuse.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/blank')({
|
|
||||||
component: RouteComponent,
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/blank"!</div>
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { R } from "@/reffuse"
|
import { Reffuse } from "@/reffuse"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Effect, Ref } from "effect"
|
import { Ref } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/count")({
|
export const Route = createFileRoute("/count")({
|
||||||
@@ -9,15 +9,16 @@ export const Route = createFileRoute("/count")({
|
|||||||
|
|
||||||
function Count() {
|
function Count() {
|
||||||
|
|
||||||
const runSync = R.useRunSync()
|
const runtime = Reffuse.useRuntime()
|
||||||
|
|
||||||
const countRef = R.useRef(() => Effect.succeed(0))
|
const countRef = Reffuse.useRef(0)
|
||||||
const [count] = R.useSubscribeRefs(countRef)
|
const [count] = Reffuse.useRefState(countRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
|
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
||||||
|
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runtime.runSync)}>
|
||||||
count is {count}
|
count is {count}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({
|
export const Route = createFileRoute("/")({
|
||||||
component: RouteComponent
|
component: Index
|
||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function Index() {
|
||||||
return <div>Hello "/"!</div>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { R } from "@/reffuse"
|
|
||||||
import { Button, Text } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import * as LazyRef from "@typed/lazy-ref"
|
|
||||||
import { Suspense, use } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/lazyref")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const promise = R.usePromise(() => LazyRef.of(0), [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<Text>Loading...</Text>}>
|
|
||||||
<LazyRefComponent promise={promise} />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LazyRefComponent({ promise }: { readonly promise: Promise<LazyRef.LazyRef<number>> }) {
|
|
||||||
const ref = use(promise)
|
|
||||||
const [value, setValue] = R.useLazyRefState(ref)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={() => setValue(prev => prev + 1)}>
|
|
||||||
{value}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { R } from "@/reffuse"
|
|
||||||
import { HttpClient } from "@effect/platform"
|
|
||||||
import { Text } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { Console, Effect, Schema } from "effect"
|
|
||||||
import { Suspense, use } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/promise")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const Result = Schema.Tuple(Schema.String)
|
|
||||||
type Result = typeof Result.Type
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const promise = R.usePromise(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
|
||||||
Effect.andThen(HttpClient.get("https://www.uuidtools.com/api/generate/v4")),
|
|
||||||
HttpClient.withTracerPropagation(false),
|
|
||||||
Effect.flatMap(res => res.json),
|
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
|
||||||
), [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<Text>Loading...</Text>}>
|
|
||||||
<AsyncComponent promise={promise} />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AsyncComponent({ promise }: { readonly promise: Promise<Result> }) {
|
|
||||||
const [uuid] = use(promise)
|
|
||||||
return <Text>{uuid}</Text>
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { QueryContext } from "@/query/reffuse"
|
|
||||||
import { Uuid4Query } from "@/query/services"
|
|
||||||
import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
|
|
||||||
import { R } from "@/reffuse"
|
|
||||||
import { HttpClient } from "@effect/platform"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { Console, Effect, Layer, Schema } from "effect"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/query/service")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const query = R.useQuery({
|
|
||||||
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
|
||||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
|
||||||
Effect.andThen(Effect.sleep("500 millis")),
|
|
||||||
Effect.andThen(Effect.map(
|
|
||||||
HttpClient.HttpClient,
|
|
||||||
HttpClient.withTracerPropagation(false),
|
|
||||||
)),
|
|
||||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
|
||||||
Effect.flatMap(res => res.json),
|
|
||||||
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
|
||||||
Effect.scoped,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryContext.Provider layer={layer}>
|
|
||||||
<Uuid4QueryService />
|
|
||||||
</QueryContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { R } from "@/reffuse"
|
|
||||||
import { HttpClient } from "@effect/platform"
|
|
||||||
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
|
||||||
import { QueryProgress } from "@reffuse/extension-query"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import * as AsyncData from "@typed/async-data"
|
|
||||||
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/query/usemutation")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const Result = Schema.Array(Schema.String)
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const runFork = R.useRunFork()
|
|
||||||
|
|
||||||
const [count, setCount] = useState(1)
|
|
||||||
|
|
||||||
const mutation = R.useMutation({
|
|
||||||
mutation: ([count]: readonly [count: number]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
|
||||||
Effect.andThen(QueryProgress.QueryProgress.update(() =>
|
|
||||||
AsyncData.Progress.make({ loaded: 0, total: Option.some(100) })
|
|
||||||
)),
|
|
||||||
Effect.andThen(Effect.sleep("500 millis")),
|
|
||||||
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
|
||||||
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
|
||||||
)),
|
|
||||||
Effect.andThen(Effect.map(
|
|
||||||
HttpClient.HttpClient,
|
|
||||||
HttpClient.withTracerPropagation(false),
|
|
||||||
)),
|
|
||||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
|
||||||
Effect.flatMap(res => res.json),
|
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
|
||||||
Effect.scoped,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const [state] = R.useSubscribeRefs(mutation.stateRef)
|
|
||||||
|
|
||||||
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { R } from "@/reffuse"
|
|
||||||
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, Stream } from "effect"
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/query/usequery")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const Result = Schema.Array(Schema.String)
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const runFork = R.useRunFork()
|
|
||||||
|
|
||||||
const [count, setCount] = useState(1)
|
|
||||||
|
|
||||||
const query = R.useQuery({
|
|
||||||
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
|
||||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
|
||||||
Effect.andThen(Effect.sleep("500 millis")),
|
|
||||||
Effect.andThen(Effect.map(
|
|
||||||
HttpClient.HttpClient,
|
|
||||||
HttpClient.withTracerPropagation(false),
|
|
||||||
)),
|
|
||||||
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
|
||||||
Effect.flatMap(res => res.json),
|
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
|
||||||
Effect.scoped,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
const [state] = R.useSubscribeRefs(query.stateRef)
|
|
||||||
|
|
||||||
|
|
||||||
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: () => "Loading...",
|
|
||||||
Success: (value, { isRefreshing, isOptimistic }) =>
|
|
||||||
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
|
|
||||||
Failure: (cause, { isRefreshing }) =>
|
|
||||||
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<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,34 +0,0 @@
|
|||||||
import { R } from "@/reffuse"
|
|
||||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { Chunk, Effect, Exit, Option, Queue, Random, Scope, Stream } from "effect"
|
|
||||||
import { useMemo, useState } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/streams/pull")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const stream = useMemo(() => Stream.repeatEffect(Random.nextInt), [])
|
|
||||||
const streamScope = R.useScope([stream], { finalizerExecutionMode: "fork" })
|
|
||||||
|
|
||||||
const queue = R.useMemo(() => Effect.provideService(Stream.toQueueOfElements(stream), Scope.Scope, streamScope), [streamScope])
|
|
||||||
|
|
||||||
const [value, setValue] = useState(Option.none<number>())
|
|
||||||
const pullLatest = R.useCallbackSync(() => Queue.takeAll(queue).pipe(
|
|
||||||
Effect.flatMap(Chunk.last),
|
|
||||||
Effect.flatMap(Exit.matchEffect({
|
|
||||||
onSuccess: Effect.succeed,
|
|
||||||
onFailure: Effect.fail,
|
|
||||||
})),
|
|
||||||
Effect.tap(v => Effect.sync(() => setValue(Option.some(v)))),
|
|
||||||
), [queue])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" align="center" gap="2">
|
|
||||||
{Option.isSome(value) && <Text>{value.value}</Text>}
|
|
||||||
<Button onClick={pullLatest}>Pull latest</Button>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { R } from "@/reffuse"
|
|
||||||
import { Button, Flex, Text } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
|
||||||
import { Console, Effect, Option } from "effect"
|
|
||||||
import { useEffect, useState } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
interface Node {
|
|
||||||
value: string
|
|
||||||
left?: Leaf
|
|
||||||
right?: Leaf
|
|
||||||
}
|
|
||||||
interface Leaf {
|
|
||||||
node: Node
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/tests")({
|
|
||||||
component: RouteComponent
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const runSync = R.useRunSync()
|
|
||||||
|
|
||||||
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
|
|
||||||
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
|
|
||||||
Effect.tap(v => Effect.sync(() => setUuid(v)))
|
|
||||||
), [])
|
|
||||||
|
|
||||||
const uuidStream = R.useStreamFromReactiveValues([uuid])
|
|
||||||
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
|
|
||||||
|
|
||||||
const [, scopeLayer] = R.useScope([uuid])
|
|
||||||
|
|
||||||
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
|
|
||||||
Effect.andThen(Console.log("Scope changed")),
|
|
||||||
Effect.provide(scopeLayer),
|
|
||||||
runSync,
|
|
||||||
), [scopeLayer, runSync])
|
|
||||||
|
|
||||||
|
|
||||||
const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
|
|
||||||
const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" justify="center" align="center" gap="2">
|
|
||||||
<Text>{uuid}</Text>
|
|
||||||
<Button onClick={generateUuid}>Generate UUID</Button>
|
|
||||||
<Text>
|
|
||||||
{Option.match(uuidStreamLatestValue, {
|
|
||||||
onSome: ([v]) => v,
|
|
||||||
onNone: () => <></>,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { R } from "@/reffuse"
|
import { Reffuse } from "@/reffuse"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Console, DateTime, Effect, Ref, Schedule, Stream, SubscriptionRef } from "effect"
|
import { Console, DateTime, Effect, Ref, Schedule, Stream } from "effect"
|
||||||
|
|
||||||
|
|
||||||
const timeEverySecond = Stream.repeatEffectWithSchedule(
|
const timeEverySecond = Stream.repeatEffectWithSchedule(
|
||||||
@@ -15,13 +15,23 @@ export const Route = createFileRoute("/time")({
|
|||||||
|
|
||||||
function Time() {
|
function Time() {
|
||||||
|
|
||||||
const timeRef = R.useMemo(() => DateTime.now.pipe(Effect.flatMap(SubscriptionRef.make)), [])
|
const timeRef = Reffuse.useRefFromEffect(DateTime.now)
|
||||||
|
|
||||||
R.useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||||
Effect.andThen(Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v)))
|
Effect.flatMap(() =>
|
||||||
|
Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v))
|
||||||
|
)
|
||||||
), [timeRef])
|
), [timeRef])
|
||||||
|
// Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||||
|
// Effect.flatMap(() => DateTime.now),
|
||||||
|
// Effect.flatMap(v => Ref.set(timeRef, v)),
|
||||||
|
// Effect.repeat(Schedule.intersect(
|
||||||
|
// Schedule.forever,
|
||||||
|
// Schedule.spaced("1 second"),
|
||||||
|
// )),
|
||||||
|
// ), [timeRef])
|
||||||
|
|
||||||
const [time] = R.useRefState(timeRef)
|
const [time] = Reffuse.useRefState(timeRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { TodosContext } from "@/todos/reffuse"
|
|
||||||
import { TodosState } from "@/todos/services"
|
|
||||||
import { VTodos } from "@/todos/views/VTodos"
|
|
||||||
import { Container } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { Console, Effect, Layer } from "effect"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/todos")({
|
|
||||||
component: Todos
|
|
||||||
})
|
|
||||||
|
|
||||||
function Todos() {
|
|
||||||
|
|
||||||
const todosLayer = useMemo(() => Layer.empty.pipe(
|
|
||||||
Layer.provideMerge(TodosState.make("todos")),
|
|
||||||
|
|
||||||
Layer.merge(Layer.effectDiscard(
|
|
||||||
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
|
|
||||||
Effect.andThen(Console.log("TodosContext constructed"))
|
|
||||||
)
|
|
||||||
)),
|
|
||||||
), [])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
|
|
||||||
<VTodos />
|
|
||||||
</TodosContext.Provider>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { QueryClient } from "@reffuse/extension-query"
|
|
||||||
import * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
|
||||||
|
|
||||||
|
|
||||||
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({
|
|
||||||
errorHandler: AppQueryErrorHandler.AppQueryErrorHandler
|
|
||||||
}) {}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { HttpClientError } from "@effect/platform"
|
|
||||||
import { QueryErrorHandler } from "@reffuse/extension-query"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export class AppQueryErrorHandler extends Effect.Service<AppQueryErrorHandler>()("AppQueryErrorHandler", {
|
|
||||||
effect: QueryErrorHandler.make<HttpClientError.HttpClientError>()(
|
|
||||||
(self, failure, defect) => self.pipe(
|
|
||||||
Effect.catchTag("RequestError", "ResponseError", failure),
|
|
||||||
Effect.catchAllDefect(defect),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}) {}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * as AppQueryClient from "./AppQueryClient"
|
|
||||||
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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 RootReffuse.pipe(
|
|
||||||
Reffuse.withContexts(TodosContext)
|
|
||||||
) {}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Todo } from "@/domain"
|
|
||||||
import { KeyValueStore } from "@effect/platform"
|
|
||||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
|
||||||
import { PlatformError } from "@effect/platform/Error"
|
|
||||||
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
|
||||||
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
|
||||||
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
|
||||||
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
|
||||||
}>() {}
|
|
||||||
|
|
||||||
|
|
||||||
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
|
|
||||||
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
|
||||||
Effect.flatMap(kv => kv.get(key)),
|
|
||||||
Effect.flatMap(identity),
|
|
||||||
Effect.flatMap(Schema.decode(
|
|
||||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
|
||||||
)),
|
|
||||||
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
|
|
||||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
|
||||||
)
|
|
||||||
|
|
||||||
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
|
|
||||||
Effect.flatMap(kv => values.pipe(
|
|
||||||
Schema.encode(
|
|
||||||
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
|
||||||
),
|
|
||||||
Effect.flatMap(v => kv.set(key, v)),
|
|
||||||
)),
|
|
||||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
|
||||||
)
|
|
||||||
|
|
||||||
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
|
||||||
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
|
|
||||||
const save = Effect.flatMap(todos, writeToLocalStorage)
|
|
||||||
|
|
||||||
// Sync changes with local storage
|
|
||||||
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
|
|
||||||
|
|
||||||
return { todos, load, save }
|
|
||||||
}))
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * as TodosState from "./TodosState"
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Todo } from "@/domain"
|
|
||||||
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
|
||||||
import { Chunk, Effect, Option, Ref } from "effect"
|
|
||||||
import { R } from "../reffuse"
|
|
||||||
import { TodosState } from "../services"
|
|
||||||
|
|
||||||
|
|
||||||
const createEmptyTodo = makeUuid4.pipe(
|
|
||||||
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
|
|
||||||
Effect.provide(GetRandomValues.CryptoRandom),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
export function VNewTodo() {
|
|
||||||
|
|
||||||
const todoRef = R.useRef(() => createEmptyTodo)
|
|
||||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
|
|
||||||
|
|
||||||
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
|
|
||||||
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
|
|
||||||
Effect.andThen(createEmptyTodo),
|
|
||||||
Effect.flatMap(v => Ref.set(todoRef, v)),
|
|
||||||
), [todoRef])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Card>
|
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
|
||||||
<TextArea
|
|
||||||
value={content}
|
|
||||||
onChange={e => setContent(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Flex direction="row" justify="center" align="center">
|
|
||||||
<Button onClick={add}>Add</Button>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { Todo } from "@/domain"
|
|
||||||
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
|
||||||
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
|
|
||||||
import { Delete } from "lucide-react"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { R } from "../reffuse"
|
|
||||||
|
|
||||||
|
|
||||||
export interface VTodoProps {
|
|
||||||
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
|
|
||||||
readonly remove: Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VTodo({ todoRef, remove }: VTodoProps) {
|
|
||||||
|
|
||||||
const runSync = R.useRunSync()
|
|
||||||
|
|
||||||
const localTodoRef = R.useRef(() => todoRef)
|
|
||||||
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
|
|
||||||
|
|
||||||
R.useFork(() => localTodoRef.changes.pipe(
|
|
||||||
Stream.debounce("250 millis"),
|
|
||||||
Stream.runForEach(v => Ref.set(todoRef, v)),
|
|
||||||
), [localTodoRef])
|
|
||||||
|
|
||||||
const editorMode = useState(false)
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Card>
|
|
||||||
<Flex direction="column" align="stretch" gap="1">
|
|
||||||
<TextArea
|
|
||||||
value={content}
|
|
||||||
onChange={e => setContent(e.target.value)}
|
|
||||||
disabled={!editorMode}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Flex direction="row" justify="between" align="center">
|
|
||||||
<Box></Box>
|
|
||||||
|
|
||||||
<Flex direction="row" align="center" gap="1">
|
|
||||||
<IconButton onClick={() => runSync(remove)}>
|
|
||||||
<Delete />
|
|
||||||
</IconButton>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Box, Flex } from "@radix-ui/themes"
|
|
||||||
import { Chunk, Effect, Ref } from "effect"
|
|
||||||
import { R } from "../reffuse"
|
|
||||||
import { TodosState } from "../services"
|
|
||||||
import { VNewTodo } from "./VNewTodo"
|
|
||||||
import { VTodo } from "./VTodo"
|
|
||||||
|
|
||||||
|
|
||||||
export function VTodos() {
|
|
||||||
|
|
||||||
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
|
|
||||||
const [todos] = R.useSubscribeRefs(todosRef)
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" align="center" gap="3">
|
|
||||||
<Box width="500px">
|
|
||||||
<VNewTodo />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{Chunk.map(todos, (todo, index) => (
|
|
||||||
<Box key={todo.id} width="500px">
|
|
||||||
<R.SubRefFromGetSet
|
|
||||||
parent={todosRef}
|
|
||||||
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
|
|
||||||
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
|
|
||||||
>
|
|
||||||
{ref => <VTodo
|
|
||||||
todoRef={ref}
|
|
||||||
remove={Ref.update(todosRef, Chunk.remove(index))}
|
|
||||||
/>}
|
|
||||||
</R.SubRefFromGetSet>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
11
packages/example/tailwind.config.js
Normal file
11
packages/example/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import("tailwindcss").Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# LazyRef extension for Reffuse
|
|
||||||
|
|
||||||
Extension to integrate `@typed/lazy-ref` with Reffuse.
|
|
||||||
|
|
||||||
## Peer dependencies
|
|
||||||
- `@typed/lazy-ref`
|
|
||||||
- `reffuse` 0.1.3+
|
|
||||||
- `effect` 3.13+
|
|
||||||
- `react` & `@types/react` 19+
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@reffuse/extension-lazyref",
|
|
||||||
"version": "0.1.4",
|
|
||||||
"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": {
|
|
||||||
"@typed/lazy-ref": "^0.3.0",
|
|
||||||
"@types/react": "^19.0.0",
|
|
||||||
"effect": "^3.15.0",
|
|
||||||
"react": "^19.0.0",
|
|
||||||
"reffuse": "^0.1.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import * as LazyRef from "@typed/lazy-ref"
|
|
||||||
import { Effect, pipe, Stream } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
|
||||||
import { SetStateAction } from "reffuse/types"
|
|
||||||
|
|
||||||
|
|
||||||
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: ReffuseNamespace.ReffuseNamespace<R>,
|
|
||||||
ref: LazyRef.LazyRef<A, E, R>,
|
|
||||||
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
|
||||||
() => ref,
|
|
||||||
[],
|
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
|
||||||
))
|
|
||||||
|
|
||||||
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 =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
|
||||||
),
|
|
||||||
[ref])
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"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,10 +0,0 @@
|
|||||||
# Reffuse Query
|
|
||||||
|
|
||||||
TanStack Query style hooks for Reffuse.
|
|
||||||
|
|
||||||
## Peer dependencies
|
|
||||||
- `reffuse` 0.1.3+
|
|
||||||
- `effect` 3.13+
|
|
||||||
- `@effect/platform` & `@effect/platform-browser`
|
|
||||||
- `react` & `@types/react` 19+
|
|
||||||
- `@typed/async-data`
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@reffuse/extension-query",
|
|
||||||
"version": "0.1.5",
|
|
||||||
"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/platform": "^0.77.0",
|
|
||||||
"@effect/platform-browser": "^0.56.0",
|
|
||||||
"@typed/async-data": "^0.13.0",
|
|
||||||
"@types/react": "^19.0.0",
|
|
||||||
"effect": "^3.15.0",
|
|
||||||
"react": "^19.0.0",
|
|
||||||
"reffuse": "^0.1.11"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import * as AsyncData from "@typed/async-data"
|
|
||||||
import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
|
||||||
import type * as QueryClient from "./QueryClient.js"
|
|
||||||
import * as QueryProgress from "./QueryProgress.js"
|
|
||||||
import { QueryState } from "./internal/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface MutationRunner<K extends readonly unknown[], A, E> {
|
|
||||||
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 const Tag = <const Id extends string>(id: Id) => <
|
|
||||||
Self, K extends readonly unknown[], A, E = never,
|
|
||||||
>() => Effect.Tag(id)<Self, MutationRunner<K, A, E>>()
|
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
|
||||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
|
||||||
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
|
||||||
}
|
|
||||||
|
|
||||||
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>>,
|
|
||||||
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.all([QueryClient, queryStateTag]).pipe(
|
|
||||||
Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
|
|
||||||
Effect.andThen(mutation(key)),
|
|
||||||
client.errorHandler.handle,
|
|
||||||
Effect.matchCauseEffect({
|
|
||||||
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
|
||||||
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.provide(context),
|
|
||||||
Effect.provide(QueryProgress.QueryProgress.Default),
|
|
||||||
)
|
|
||||||
|
|
||||||
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
|
|
||||||
queryStateTag,
|
|
||||||
globalStateRef,
|
|
||||||
value => Ref.set(globalStateRef, value),
|
|
||||||
))
|
|
||||||
|
|
||||||
const forkMutate = (...key: K) => Effect.all([
|
|
||||||
Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>()),
|
|
||||||
Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>(),
|
|
||||||
]).pipe(
|
|
||||||
Effect.flatMap(([stateRef, stateQueue]) =>
|
|
||||||
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
|
|
||||||
Effect.andThen(run(key)),
|
|
||||||
Effect.scoped,
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Context, Effect, identity, 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>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<FallbackA, HandledE> {
|
|
||||||
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <FallbackA, HandledE>(
|
|
||||||
{ errorHandler }: MakeProps<FallbackA, HandledE>
|
|
||||||
): Effect.Effect<QueryClient<FallbackA, HandledE>> => Effect.Do.pipe(
|
|
||||||
Effect.let("errorHandler", () => errorHandler)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const id = "@reffuse/extension-query/QueryClient"
|
|
||||||
|
|
||||||
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
|
|
||||||
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<FallbackA = never, HandledE = never, E = never, R = never> {
|
|
||||||
readonly errorHandler?: Effect.Effect<QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, E, R>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
|
|
||||||
Self,
|
|
||||||
typeof id,
|
|
||||||
QueryClient<FallbackA, HandledE>
|
|
||||||
> {
|
|
||||||
readonly Default: Layer.Layer<Self, E, R>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Service = <Self>() => (
|
|
||||||
<FallbackA = never, HandledE = never, E = never, R = never>(
|
|
||||||
props?: ServiceProps<FallbackA, HandledE, E, R>
|
|
||||||
): ServiceResult<Self, FallbackA, HandledE, E, R> => {
|
|
||||||
const TagClass = Context.Tag(id)() as ServiceResult<Self, FallbackA, HandledE, E, R>
|
|
||||||
|
|
||||||
(TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
|
|
||||||
props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
|
|
||||||
errorHandler => make({ errorHandler }),
|
|
||||||
))
|
|
||||||
|
|
||||||
return TagClass
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Cause, Effect, PubSub, Stream } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
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 const make = <HandledE = never>() => (
|
|
||||||
<FallbackA>(
|
|
||||||
f: (
|
|
||||||
self: Effect.Effect<never, HandledE>,
|
|
||||||
failure: (failure: HandledE) => Effect.Effect<never>,
|
|
||||||
defect: (defect: unknown) => Effect.Effect<never>,
|
|
||||||
) => Effect.Effect<FallbackA>
|
|
||||||
): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
|
|
||||||
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
|
|
||||||
const errors = Stream.fromPubSub(pubsub)
|
|
||||||
|
|
||||||
const handle = <A, E, R>(
|
|
||||||
self: Effect.Effect<A, E, R>
|
|
||||||
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
|
||||||
self as unknown as Effect.Effect<never, HandledE, never>,
|
|
||||||
(failure: HandledE) => Effect.andThen(
|
|
||||||
PubSub.publish(pubsub, Cause.fail(failure)),
|
|
||||||
Effect.failCause(Cause.empty),
|
|
||||||
),
|
|
||||||
(defect: unknown) => Effect.andThen(
|
|
||||||
PubSub.publish(pubsub, Cause.die(defect)),
|
|
||||||
Effect.failCause(Cause.empty),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return { errors, handle }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { Effect, Stream } from "effect"
|
|
||||||
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
|
||||||
import * as MutationRunner from "./MutationRunner.js"
|
|
||||||
import * as QueryClient from "./QueryClient.js"
|
|
||||||
import type * as QueryProgress from "./QueryProgress.js"
|
|
||||||
import * as QueryRunner from "./QueryRunner.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
|
||||||
readonly key: Stream.Stream<K>
|
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
|
||||||
readonly options?: QueryRunner.RunOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
|
|
||||||
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const QueryExtension = ReffuseExtension.make(() => ({
|
|
||||||
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>,
|
|
||||||
): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
|
||||||
const runner = this.useMemo(() => QueryRunner.make({
|
|
||||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
|
||||||
key: props.key,
|
|
||||||
query: props.query,
|
|
||||||
}), [props.key])
|
|
||||||
|
|
||||||
this.useFork(() => QueryRunner.run(runner, props.options), [runner])
|
|
||||||
|
|
||||||
return 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>,
|
|
||||||
): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
|
||||||
return this.useMemo(() => MutationRunner.make({
|
|
||||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
|
||||||
mutation: props.mutation,
|
|
||||||
}), [])
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import * as AsyncData from "@typed/async-data"
|
|
||||||
import { Effect, flow, Layer, Match, Option } from "effect"
|
|
||||||
import { QueryState } from "./internal/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryProgress")<QueryProgress, {
|
|
||||||
readonly get: Effect.Effect<Option.Option<AsyncData.Progress>>
|
|
||||||
|
|
||||||
readonly update: (
|
|
||||||
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
|
|
||||||
) => Effect.Effect<void>
|
|
||||||
}>() {
|
|
||||||
static readonly Default: 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,193 +0,0 @@
|
|||||||
import { BrowserStream } from "@effect/platform-browser"
|
|
||||||
import * as AsyncData from "@typed/async-data"
|
|
||||||
import { type Cause, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import type * as QueryClient from "./QueryClient.js"
|
|
||||||
import * as QueryProgress from "./QueryProgress.js"
|
|
||||||
import { QueryState } from "./internal/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface QueryRunner<K extends readonly unknown[], A, E> {
|
|
||||||
readonly queryKey: Stream.Stream<K>
|
|
||||||
readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
|
||||||
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
|
|
||||||
AsyncData.Success<A> | AsyncData.Failure<E>,
|
|
||||||
Cause.NoSuchElementException
|
|
||||||
>>>
|
|
||||||
|
|
||||||
readonly interrupt: Effect.Effect<void>
|
|
||||||
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void>>
|
|
||||||
readonly forkFetch: (keyValue: K) => Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
readonly forkRefresh: Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const Tag = <const Id extends string>(id: Id) => <
|
|
||||||
Self, K extends readonly unknown[], A, E = never
|
|
||||||
>() => Effect.Tag(id)<Self, QueryRunner<K, A, E>>()
|
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
|
||||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
|
||||||
readonly key: Stream.Stream<K>
|
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
|
||||||
{
|
|
||||||
QueryClient,
|
|
||||||
key,
|
|
||||||
query,
|
|
||||||
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
|
||||||
): Effect.Effect<
|
|
||||||
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
|
||||||
never,
|
|
||||||
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
|
||||||
> => Effect.gen(function*() {
|
|
||||||
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
|
||||||
|
|
||||||
const latestKeyValueRef = yield* SubscriptionRef.make(Option.none<K>())
|
|
||||||
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
|
||||||
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
|
|
||||||
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
|
|
||||||
Cause.NoSuchElementException
|
|
||||||
>>())
|
|
||||||
|
|
||||||
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
|
||||||
|
|
||||||
const interrupt = Effect.flatMap(fiberRef, Option.match({
|
|
||||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Fiber.interrupt(fiber))
|
|
||||||
),
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const forkInterrupt = Effect.flatMap(fiberRef, Option.match({
|
|
||||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
|
||||||
Effect.asVoid,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
))
|
|
||||||
),
|
|
||||||
onNone: () => Effect.forkDaemon(Effect.void),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const run = (keyValue: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
|
||||||
Effect.flatMap(([client, state]) => Ref.set(latestKeyValueRef, Option.some(keyValue)).pipe(
|
|
||||||
Effect.andThen(query(keyValue)),
|
|
||||||
client.errorHandler.handle,
|
|
||||||
Effect.matchCauseEffect({
|
|
||||||
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
|
||||||
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.provide(context),
|
|
||||||
Effect.provide(QueryProgress.QueryProgress.Default),
|
|
||||||
)
|
|
||||||
|
|
||||||
const forkFetch = (keyValue: K) => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
|
||||||
Effect.flatMap(stateQueue => queryStateTag.pipe(
|
|
||||||
Effect.flatMap(state => interrupt.pipe(
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.addFinalizer(() => Effect.andThen(
|
|
||||||
Ref.set(fiberRef, Option.none()),
|
|
||||||
Queue.shutdown(stateQueue),
|
|
||||||
)).pipe(
|
|
||||||
Effect.andThen(state.set(AsyncData.loading())),
|
|
||||||
Effect.andThen(run(keyValue)),
|
|
||||||
Effect.scoped,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
|
||||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.provide(QueryState.layer(
|
|
||||||
queryStateTag,
|
|
||||||
stateRef,
|
|
||||||
value => Effect.andThen(
|
|
||||||
Queue.offer(stateQueue, value),
|
|
||||||
Ref.set(stateRef, value),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
const setInitialRefreshState = Effect.flatMap(queryStateTag, state => state.update(previous => {
|
|
||||||
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
|
||||||
return AsyncData.refreshing(previous)
|
|
||||||
if (AsyncData.isRefreshing(previous))
|
|
||||||
return AsyncData.refreshing(previous.previous)
|
|
||||||
return AsyncData.loading()
|
|
||||||
}))
|
|
||||||
|
|
||||||
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
|
||||||
Effect.flatMap(stateQueue => interrupt.pipe(
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.addFinalizer(() => Effect.andThen(
|
|
||||||
Ref.set(fiberRef, Option.none()),
|
|
||||||
Queue.shutdown(stateQueue),
|
|
||||||
)).pipe(
|
|
||||||
Effect.andThen(setInitialRefreshState),
|
|
||||||
Effect.andThen(latestKeyValueRef.pipe(
|
|
||||||
Effect.flatMap(identity),
|
|
||||||
Effect.flatMap(run),
|
|
||||||
)),
|
|
||||||
Effect.scoped,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
|
||||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
|
||||||
|
|
||||||
Effect.provide(QueryState.layer(
|
|
||||||
queryStateTag,
|
|
||||||
stateRef,
|
|
||||||
value => Effect.andThen(
|
|
||||||
Queue.offer(stateQueue, value),
|
|
||||||
Ref.set(stateRef, value),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
queryKey: key,
|
|
||||||
latestKeyValueRef,
|
|
||||||
stateRef,
|
|
||||||
fiberRef,
|
|
||||||
|
|
||||||
interrupt,
|
|
||||||
forkInterrupt,
|
|
||||||
forkFetch,
|
|
||||||
forkRefresh,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export interface RunOptions {
|
|
||||||
readonly refreshOnWindowFocus?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = <K extends readonly unknown[], A, E>(
|
|
||||||
self: QueryRunner<K, A, E>,
|
|
||||||
options?: RunOptions,
|
|
||||||
): Effect.Effect<void, never, Scope.Scope> => Effect.gen(function*() {
|
|
||||||
if (typeof window !== "undefined" && (options?.refreshOnWindowFocus ?? true))
|
|
||||||
yield* Effect.forkScoped(
|
|
||||||
Stream.runForEach(BrowserStream.fromEventListenerWindow("focus"), () => self.forkRefresh)
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* Effect.addFinalizer(() => self.interrupt)
|
|
||||||
yield* Stream.runForEach(Stream.changes(self.queryKey), latestKey => self.forkFetch(latestKey))
|
|
||||||
})
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export * as MutationRunner from "./MutationRunner.js"
|
|
||||||
export * as QueryClient from "./QueryClient.js"
|
|
||||||
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
|
||||||
export * from "./QueryExtension.js"
|
|
||||||
export * as QueryProgress from "./QueryProgress.js"
|
|
||||||
export * as QueryRunner from "./QueryRunner.js"
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type * as AsyncData from "@typed/async-data"
|
|
||||||
import { Context, Effect, Layer } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export interface QueryState<A, E> {
|
|
||||||
readonly get: Effect.Effect<AsyncData.AsyncData<A, E>>
|
|
||||||
readonly set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>
|
|
||||||
readonly update: (f: (previous: AsyncData.AsyncData<A, E>) => AsyncData.AsyncData<A, E>) => Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeTag = <A, E>(): Context.Tag<QueryState<A, E>, QueryState<A, E>> => Context.GenericTag("@reffuse/query-extension/QueryState")
|
|
||||||
|
|
||||||
export const layer = <A, E>(
|
|
||||||
tag: Context.Tag<QueryState<A, E>, QueryState<A, E>>,
|
|
||||||
get: Effect.Effect<AsyncData.AsyncData<A, E>>,
|
|
||||||
set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>,
|
|
||||||
): Layer.Layer<QueryState<A, E>> => Layer.succeed(tag, {
|
|
||||||
get,
|
|
||||||
set,
|
|
||||||
update: f => get.pipe(
|
|
||||||
Effect.map(f),
|
|
||||||
Effect.flatMap(set),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * as QueryState from "./QueryState.js"
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"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,11 +0,0 @@
|
|||||||
# Reffuse
|
|
||||||
|
|
||||||
[Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
|
|
||||||
|
|
||||||
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
|
|
||||||
|
|
||||||
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
|
||||||
|
|
||||||
## Peer dependencies
|
|
||||||
- `effect` 3.13+
|
|
||||||
- `react` & `@types/react` 19+
|
|
||||||
@@ -1,25 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "reffuse",
|
"name": "@thilawyn/reffuse",
|
||||||
"version": "0.1.13",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
|
||||||
"./dist"
|
"./dist"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"repository": {
|
|
||||||
"url": "git+https://github.com/Thiladev/reffuse.git"
|
|
||||||
},
|
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"./types": {
|
|
||||||
"types": "./dist/types/index.d.ts",
|
|
||||||
"default": "./dist/types/index.js"
|
|
||||||
},
|
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./dist/*.d.ts",
|
"types": "./dist/*.d.ts",
|
||||||
"default": "./dist/*.js"
|
"default": "./dist/*.js"
|
||||||
@@ -28,14 +19,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint:tsc": "tsc --noEmit",
|
"lint:tsc": "tsc --noEmit",
|
||||||
"pack": "npm pack",
|
|
||||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.4",
|
||||||
"effect": "^3.15.0",
|
"effect": "^3.12.1",
|
||||||
"react": "^19.0.0"
|
"react": "^19.0.0",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import type * as ReffuseContext from "./ReffuseContext.js"
|
|
||||||
import type * as ReffuseExtension from "./ReffuseExtension.js"
|
|
||||||
import * as ReffuseNamespace from "./ReffuseNamespace.js"
|
|
||||||
import type { Merge, StaticType } from "./utils.js"
|
|
||||||
|
|
||||||
|
|
||||||
export class Reffuse extends ReffuseNamespace.makeClass() {}
|
|
||||||
|
|
||||||
|
|
||||||
export const withContexts = <R2 extends Array<unknown>>(
|
|
||||||
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
|
|
||||||
) => (
|
|
||||||
<
|
|
||||||
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R1>,
|
|
||||||
R1
|
|
||||||
>(
|
|
||||||
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R1>
|
|
||||||
): (
|
|
||||||
{
|
|
||||||
new(): Merge<
|
|
||||||
InstanceType<BaseClass>,
|
|
||||||
{ constructor: ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]> }
|
|
||||||
>
|
|
||||||
} &
|
|
||||||
Merge<
|
|
||||||
StaticType<BaseClass>,
|
|
||||||
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>) => (
|
|
||||||
<
|
|
||||||
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R>,
|
|
||||||
R
|
|
||||||
>(
|
|
||||||
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R>
|
|
||||||
): (
|
|
||||||
{ new(): Merge<InstanceType<BaseClass>, A> } &
|
|
||||||
StaticType<BaseClass>
|
|
||||||
) => {
|
|
||||||
const class_ = class extends self {}
|
|
||||||
Object.assign(class_.prototype, extension())
|
|
||||||
return class_ as any
|
|
||||||
}
|
|
||||||
)
|
|
||||||
86
packages/reffuse/src/Reffuse.tsx
Normal file
86
packages/reffuse/src/Reffuse.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Effect, Fiber, Layer, ManagedRuntime, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export class Reffuse<R, ER> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||||
|
) {
|
||||||
|
this.Context = React.createContext<ManagedRuntime.ManagedRuntime<R, ER>>(null!)
|
||||||
|
|
||||||
|
this.Provider = (props: { readonly children?: React.ReactNode }) => (
|
||||||
|
<this.Context
|
||||||
|
{...props}
|
||||||
|
value={runtime}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const context = runtime.runtimeEffect.pipe(
|
||||||
|
Effect.map(r => Layer.succeedContext(r.context)),
|
||||||
|
runtime.runSync,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly Context: React.Context<ManagedRuntime.ManagedRuntime<R, ER>>
|
||||||
|
readonly Provider: React.FC<{ readonly children?: React.ReactNode }>
|
||||||
|
|
||||||
|
|
||||||
|
useRuntime(): ManagedRuntime.ManagedRuntime<R, ER> {
|
||||||
|
return React.useContext(this.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useFork<A, E>(
|
||||||
|
self: Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions,
|
||||||
|
): void {
|
||||||
|
const runtime = this.useRuntime()
|
||||||
|
|
||||||
|
return React.useEffect(() => {
|
||||||
|
const fiber = runtime.runFork(Effect.scoped(self), options)
|
||||||
|
return () => { runtime.runFork(Fiber.interrupt(fiber)) }
|
||||||
|
}, [runtime, ...deps ?? []])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRef<A>(value: A): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
const runtime = this.useRuntime()
|
||||||
|
return React.useMemo(() => runtime.runSync(SubscriptionRef.make(value)), [])
|
||||||
|
}
|
||||||
|
|
||||||
|
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, R>): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
const runtime = this.useRuntime()
|
||||||
|
|
||||||
|
return React.useMemo(() => runtime.runSync(effect.pipe(
|
||||||
|
Effect.flatMap(SubscriptionRef.make)
|
||||||
|
)), [])
|
||||||
|
}
|
||||||
|
|
||||||
|
useRefState<A>(ref: SubscriptionRef.SubscriptionRef<A>): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||||
|
const runtime = this.useRuntime()
|
||||||
|
|
||||||
|
const initialState = React.useMemo(() => runtime.runSync(ref), [ref])
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||||
|
|
||||||
|
this.useFork(Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
||||||
|
setReactStateValue(v)
|
||||||
|
)), [ref])
|
||||||
|
|
||||||
|
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
|
||||||
|
runtime.runSync(Ref.update(ref, previousState =>
|
||||||
|
typeof setStateAction === "function"
|
||||||
|
? (setStateAction as (prevState: A) => A)(previousState)
|
||||||
|
: setStateAction
|
||||||
|
)),
|
||||||
|
[ref])
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const make = <ROut, E>(layer: Layer.Layer<ROut, E, never>): Reffuse<ROut, E> =>
|
||||||
|
new Reffuse(ManagedRuntime.make(layer))
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, 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 finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
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 runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
|
|
||||||
|
|
||||||
const makeScope = React.useMemo(() => props.scope
|
|
||||||
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
|
||||||
[props.scope])
|
|
||||||
|
|
||||||
const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
|
|
||||||
Effect.provide(props.layer),
|
|
||||||
Effect.provideService(Scope.Scope, scope),
|
|
||||||
)
|
|
||||||
|
|
||||||
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
|
|
||||||
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
|
|
||||||
Match.when("sync", () => { runSync(effect) }),
|
|
||||||
Match.when("fork", () => { runFork(effect) }),
|
|
||||||
Match.exhaustive,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("isInitialRun", () => Ref.make(true)),
|
|
||||||
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(() =>
|
|
||||||
() => closeScope(initialScope)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
onFalse: () => Effect.Do.pipe(
|
|
||||||
Effect.bind("scope", () => makeScope),
|
|
||||||
Effect.bind("context", ({ scope }) => makeContext(scope)),
|
|
||||||
Effect.tap(({ context }) =>
|
|
||||||
Effect.sync(() => setValue(context))
|
|
||||||
),
|
|
||||||
Effect.map(({ scope }) =>
|
|
||||||
() => closeScope(scope)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
runSync,
|
|
||||||
), [makeScope, runSync, runFork])
|
|
||||||
|
|
||||||
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 finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
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 ?? ExecutionStrategy.sequential)
|
|
||||||
)
|
|
||||||
|
|
||||||
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 () => Scope.close(scope, Exit.void).pipe(
|
|
||||||
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
|
|
||||||
Match.when("sync", () => { runSync(effect) }),
|
|
||||||
Match.when("fork", () => { runFork(effect) }),
|
|
||||||
Match.exhaustive,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}, [props.layer, runSync, runFork])
|
|
||||||
|
|
||||||
return React.createElement(React.Suspense, {
|
|
||||||
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,7 +0,0 @@
|
|||||||
export interface ReffuseExtension<A extends object> {
|
|
||||||
(): A
|
|
||||||
readonly Type: A
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <A extends object>(extension: () => A): ReffuseExtension<A> =>
|
|
||||||
extension as ReffuseExtension<A>
|
|
||||||
@@ -1,646 +0,0 @@
|
|||||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ReffuseContext from "./ReffuseContext.js"
|
|
||||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
|
||||||
import { type PropertyPath, SetStateAction, SubscriptionSubRef } from "./types/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface RenderOptions {
|
|
||||||
/** Prevents re-executing the effect when the Effect runtime or context changes. Defaults to `false`. */
|
|
||||||
readonly doNotReExecuteOnRuntimeOrContextChange?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScopeOptions {
|
|
||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
|
|
||||||
readonly scope?: Scope.Scope
|
|
||||||
readonly finalizerExecutionMode?: "sync" | "fork"
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
|
|
||||||
[K in keyof T]: Effect.Effect.Success<T[K]>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export abstract class ReffuseNamespace<R> {
|
|
||||||
declare ["constructor"]: ReffuseNamespaceClass<R>
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.SubRefFromGetSet = this.SubRefFromGetSet.bind(this as any) as any
|
|
||||||
this.SubRefFromPath = this.SubRefFromPath.bind(this as any) as any
|
|
||||||
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
|
|
||||||
this.RefState = this.RefState.bind(this as any) as any
|
|
||||||
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
useContext<R>(this: ReffuseNamespace<R>): Context.Context<R> {
|
|
||||||
return ReffuseContext.useMergeAll(...this.constructor.contexts)
|
|
||||||
}
|
|
||||||
|
|
||||||
useLayer<R>(this: ReffuseNamespace<R>): Layer.Layer<R> {
|
|
||||||
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
useRunSync<R>(this: ReffuseNamespace<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
|
|
||||||
const runtime = ReffuseRuntime.useRuntime()
|
|
||||||
const context = this.useContext()
|
|
||||||
|
|
||||||
return React.useCallback(effect => effect.pipe(
|
|
||||||
Effect.provide(context),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
), [runtime, context])
|
|
||||||
}
|
|
||||||
|
|
||||||
useRunPromise<R>(this: ReffuseNamespace<R>): <A, E>(
|
|
||||||
effect: Effect.Effect<A, E, R>,
|
|
||||||
options?: { readonly signal?: AbortSignal },
|
|
||||||
) => Promise<A> {
|
|
||||||
const runtime = ReffuseRuntime.useRuntime()
|
|
||||||
const context = this.useContext()
|
|
||||||
|
|
||||||
return React.useCallback((effect, options) => effect.pipe(
|
|
||||||
Effect.provide(context),
|
|
||||||
effect => Runtime.runPromise(runtime)(effect, options),
|
|
||||||
), [runtime, context])
|
|
||||||
}
|
|
||||||
|
|
||||||
useRunFork<R>(this: ReffuseNamespace<R>): <A, E>(
|
|
||||||
effect: Effect.Effect<A, E, R>,
|
|
||||||
options?: Runtime.RunForkOptions,
|
|
||||||
) => Fiber.RuntimeFiber<A, E> {
|
|
||||||
const runtime = ReffuseRuntime.useRuntime()
|
|
||||||
const context = this.useContext()
|
|
||||||
|
|
||||||
return React.useCallback((effect, options) => effect.pipe(
|
|
||||||
Effect.provide(context),
|
|
||||||
effect => Runtime.runFork(runtime)(effect, options),
|
|
||||||
), [runtime, context])
|
|
||||||
}
|
|
||||||
|
|
||||||
useRunCallback<R>(this: ReffuseNamespace<R>): <A, E>(
|
|
||||||
effect: Effect.Effect<A, E, R>,
|
|
||||||
options?: Runtime.RunCallbackOptions<A, E>,
|
|
||||||
) => Runtime.Cancel<A, E> {
|
|
||||||
const runtime = ReffuseRuntime.useRuntime()
|
|
||||||
const context = this.useContext()
|
|
||||||
|
|
||||||
return React.useCallback((effect, options) => effect.pipe(
|
|
||||||
Effect.provide(context),
|
|
||||||
effect => Runtime.runCallback(runtime)(effect, options),
|
|
||||||
), [runtime, context])
|
|
||||||
}
|
|
||||||
|
|
||||||
useScope<R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
deps: React.DependencyList = [],
|
|
||||||
options?: UseScopeOptions,
|
|
||||||
): readonly [scope: Scope.Scope, layer: Layer.Layer<Scope.Scope>] {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
const runFork = this.useRunFork()
|
|
||||||
|
|
||||||
const makeScope = React.useMemo(() => options?.scope
|
|
||||||
? Scope.fork(options.scope, options.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
|
||||||
[options?.scope])
|
|
||||||
|
|
||||||
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
|
|
||||||
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
|
|
||||||
Match.when("sync", () => { runSync(effect) }),
|
|
||||||
Match.when("fork", () => { runFork(effect) }),
|
|
||||||
Match.exhaustive,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const [isInitialRun, initialScope] = React.useMemo(() => runSync(
|
|
||||||
Effect.all([Ref.make(true), makeScope])
|
|
||||||
), [makeScope])
|
|
||||||
|
|
||||||
const [scope, setScope] = React.useState(initialScope)
|
|
||||||
|
|
||||||
React.useEffect(() => isInitialRun.pipe(
|
|
||||||
Effect.if({
|
|
||||||
onTrue: () => Effect.as(
|
|
||||||
Ref.set(isInitialRun, false),
|
|
||||||
() => closeScope(initialScope),
|
|
||||||
),
|
|
||||||
|
|
||||||
onFalse: () => makeScope.pipe(
|
|
||||||
Effect.tap(v => Effect.sync(() => setScope(v))),
|
|
||||||
Effect.map(v => () => closeScope(v)),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
runSync,
|
|
||||||
), [
|
|
||||||
makeScope,
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
|
|
||||||
return React.useMemo(() => [scope, Layer.succeed(Scope.Scope, scope)] as const, [scope])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reffuse equivalent to `React.useMemo`.
|
|
||||||
*
|
|
||||||
* `useMemo` will only recompute the memoized value by running the given synchronous effect when one of the deps has changed. \
|
|
||||||
* Trying to run an asynchronous effect will throw.
|
|
||||||
*
|
|
||||||
* Changes to the Reffuse runtime or context will recompute the value in addition to the deps.
|
|
||||||
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
|
|
||||||
*/
|
|
||||||
useMemo<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
effect: () => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: RenderOptions,
|
|
||||||
): A {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
|
|
||||||
return React.useMemo(() => runSync(effect()), [
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reffuse equivalent to `React.useEffect`.
|
|
||||||
*
|
|
||||||
* Executes a synchronous effect wrapped into a Scope when one of the deps has changed. Trying to run an asynchronous effect will throw.
|
|
||||||
*
|
|
||||||
* The Scope is closed on every cleanup, i.e. when one of the deps has changed and the effect needs to be re-executed. \
|
|
||||||
* Add finalizers to the Scope to handle cleanup logic.
|
|
||||||
*
|
|
||||||
* Changes to the Reffuse runtime or context will re-execute the effect in addition to the deps.
|
|
||||||
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
|
|
||||||
*
|
|
||||||
* ### Example
|
|
||||||
* ```
|
|
||||||
* useEffect(() => Effect.addFinalizer(() => Console.log("Component unmounted")).pipe(
|
|
||||||
* Effect.flatMap(() => Console.log("Component mounted"))
|
|
||||||
* ))
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Plain React equivalent:
|
|
||||||
* ```
|
|
||||||
* React.useEffect(() => {
|
|
||||||
* console.log("Component mounted")
|
|
||||||
* return () => { console.log("Component unmounted") }
|
|
||||||
* }, [])
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
useEffect<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: RenderOptions & ScopeOptions,
|
|
||||||
): void {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
|
|
||||||
Effect.tap(scope => Effect.provideService(effect(), Scope.Scope, scope)),
|
|
||||||
runSync,
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => { runSync(Scope.close(scope, Exit.void)) }
|
|
||||||
}, deps && [
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reffuse equivalent to `React.useLayoutEffect`.
|
|
||||||
*
|
|
||||||
* Executes a synchronous effect wrapped into a Scope when one of the deps has changed. Fires synchronously after all DOM mutations. \
|
|
||||||
* Trying to run an asynchronous effect will throw.
|
|
||||||
*
|
|
||||||
* The Scope is closed on every cleanup, i.e. when one of the deps has changed and the effect needs to be re-executed. \
|
|
||||||
* Add finalizers to the Scope to handle cleanup logic.
|
|
||||||
*
|
|
||||||
* Changes to the Reffuse runtime or context will re-execute the effect in addition to the deps.
|
|
||||||
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
|
|
||||||
*
|
|
||||||
* ### Example
|
|
||||||
* ```
|
|
||||||
* useLayoutEffect(() => Effect.addFinalizer(() => Console.log("Component unmounted")).pipe(
|
|
||||||
* Effect.flatMap(() => Console.log("Component mounted"))
|
|
||||||
* ))
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Plain React equivalent:
|
|
||||||
* ```
|
|
||||||
* React.useLayoutEffect(() => {
|
|
||||||
* console.log("Component mounted")
|
|
||||||
* return () => { console.log("Component unmounted") }
|
|
||||||
* }, [])
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
useLayoutEffect<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: RenderOptions & ScopeOptions,
|
|
||||||
): void {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
|
|
||||||
Effect.tap(scope => Effect.provideService(effect(), Scope.Scope, scope)),
|
|
||||||
runSync,
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => { runSync(Scope.close(scope, Exit.void)) }
|
|
||||||
}, deps && [
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An asynchronous and non-blocking alternative to `React.useEffect`.
|
|
||||||
*
|
|
||||||
* Forks an effect wrapped into a Scope in the background when one of the deps has changed.
|
|
||||||
*
|
|
||||||
* The Scope is closed on every cleanup, i.e. when one of the deps has changed and the effect needs to be re-executed. \
|
|
||||||
* Add finalizers to the Scope to handle cleanup logic.
|
|
||||||
*
|
|
||||||
* Changes to the Reffuse runtime or context will re-execute the effect in addition to the deps.
|
|
||||||
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
|
|
||||||
*
|
|
||||||
* ### Example
|
|
||||||
* ```
|
|
||||||
* const timeRef = useRefFromEffect(DateTime.now)
|
|
||||||
*
|
|
||||||
* useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
|
||||||
* Effect.map(() => Stream.repeatEffectWithSchedule(
|
|
||||||
* DateTime.now,
|
|
||||||
* Schedule.intersect(Schedule.forever, Schedule.spaced("1 second")),
|
|
||||||
* )),
|
|
||||||
*
|
|
||||||
* Effect.flatMap(Stream.runForEach(time => Ref.set(timeRef, time)),
|
|
||||||
* )), [timeRef])
|
|
||||||
*
|
|
||||||
* const [time] = useRefState(timeRef)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
useFork<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
|
||||||
): void {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
const runFork = this.useRunFork()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const scope = runSync(options?.scope
|
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(options?.finalizerExecutionStrategy)
|
|
||||||
)
|
|
||||||
runFork(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
|
||||||
|
|
||||||
return () => { runFork(Scope.close(scope, Exit.void)) }
|
|
||||||
}, deps && [
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
usePromise<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
|
||||||
deps?: React.DependencyList,
|
|
||||||
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
|
||||||
): Promise<A> {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
const runFork = this.useRunFork()
|
|
||||||
|
|
||||||
const [value, setValue] = React.useState(Promise.withResolvers<A>().promise)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const { promise, resolve, reject } = Promise.withResolvers<A>()
|
|
||||||
setValue(promise)
|
|
||||||
|
|
||||||
const scope = runSync(options?.scope
|
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
|
||||||
: Scope.make(options?.finalizerExecutionStrategy)
|
|
||||||
)
|
|
||||||
|
|
||||||
const cleanup = () => { runFork(Scope.close(scope, Exit.void)) }
|
|
||||||
if (options?.signal)
|
|
||||||
options.signal.addEventListener("abort", cleanup)
|
|
||||||
|
|
||||||
effect().pipe(
|
|
||||||
Effect.provideService(Scope.Scope, scope),
|
|
||||||
Effect.match({
|
|
||||||
onSuccess: resolve,
|
|
||||||
onFailure: reject,
|
|
||||||
}),
|
|
||||||
effect => runFork(effect, { ...options, scope }),
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (options?.signal)
|
|
||||||
options.signal.removeEventListener("abort", cleanup)
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
}, deps && [
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
useCallbackSync<Args extends unknown[], A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: RenderOptions,
|
|
||||||
): (...args: Args) => A {
|
|
||||||
const runSync = this.useRunSync()
|
|
||||||
|
|
||||||
return React.useCallback((...args) => runSync(callback(...args)), [
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
useCallbackPromise<Args extends unknown[], A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
|
||||||
deps: React.DependencyList,
|
|
||||||
options?: { readonly signal?: AbortSignal } & RenderOptions,
|
|
||||||
): (...args: Args) => Promise<A> {
|
|
||||||
const runPromise = this.useRunPromise()
|
|
||||||
|
|
||||||
return React.useCallback((...args) => runPromise(callback(...args), options), [
|
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise],
|
|
||||||
...deps,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
useRef<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
initialValue: () => Effect.Effect<A, E, R>,
|
|
||||||
): SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
return this.useMemo(
|
|
||||||
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
|
|
||||||
[],
|
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useRefFromReactiveValue<A, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
value: A,
|
|
||||||
): SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
const ref = this.useRef(() => Effect.succeed(value))
|
|
||||||
this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
|
|
||||||
return ref
|
|
||||||
}
|
|
||||||
|
|
||||||
useSubRefFromGetSet<A, B, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
getter: (parentValue: B) => A,
|
|
||||||
setter: (parentValue: B, value: A) => B,
|
|
||||||
): SubscriptionSubRef.SubscriptionSubRef<A, B> {
|
|
||||||
return React.useMemo(
|
|
||||||
() => SubscriptionSubRef.makeFromGetSet(parent, getter, setter),
|
|
||||||
[parent],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useSubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
path: P,
|
|
||||||
): SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> {
|
|
||||||
return React.useMemo(
|
|
||||||
() => SubscriptionSubRef.makeFromPath(parent, path),
|
|
||||||
[parent, ...path],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useSubscribeRefs<
|
|
||||||
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
|
||||||
R,
|
|
||||||
>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
...refs: Refs
|
|
||||||
): RefsA<Refs> {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
|
||||||
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
|
|
||||||
[],
|
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
|
||||||
) as RefsA<Refs>)
|
|
||||||
|
|
||||||
this.useFork(() => pipe(
|
|
||||||
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
|
||||||
streams => Stream.zipLatestAll(...streams),
|
|
||||||
Stream.runForEach(v =>
|
|
||||||
Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
|
|
||||||
),
|
|
||||||
), refs)
|
|
||||||
|
|
||||||
return reactStateValue
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds the state of a `SubscriptionRef` to the state of the React component.
|
|
||||||
*
|
|
||||||
* Returns a [value, setter] tuple just like `React.useState` and triggers a re-render everytime the value held by the ref changes.
|
|
||||||
*
|
|
||||||
* 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: ReffuseNamespace<R>,
|
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>,
|
|
||||||
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
|
||||||
() => ref,
|
|
||||||
[],
|
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
|
||||||
))
|
|
||||||
|
|
||||||
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 =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
|
||||||
),
|
|
||||||
[ref])
|
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
|
||||||
}
|
|
||||||
|
|
||||||
useStreamFromReactiveValues<const A extends React.DependencyList, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
values: A,
|
|
||||||
): Stream.Stream<A> {
|
|
||||||
const [, scopeLayer] = this.useScope([], { finalizerExecutionMode: "fork" })
|
|
||||||
|
|
||||||
const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
|
|
||||||
Effect.bind("latest", () => Ref.make(values)),
|
|
||||||
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
|
||||||
Effect.let("stream", ({ latest, pubsub }) => Ref.get(latest).pipe(
|
|
||||||
Effect.flatMap(a => Effect.map(
|
|
||||||
Stream.fromPubSub(pubsub, { scoped: true }),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrapScoped,
|
|
||||||
)),
|
|
||||||
Effect.provide(scopeLayer),
|
|
||||||
), [scopeLayer], { doNotReExecuteOnRuntimeOrContextChange: true })
|
|
||||||
|
|
||||||
this.useEffect(() => Ref.set(latest, values).pipe(
|
|
||||||
Effect.andThen(PubSub.publish(pubsub, values)),
|
|
||||||
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
|
||||||
), values, { doNotReExecuteOnRuntimeOrContextChange: true })
|
|
||||||
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
useSubscribeStream<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
): Option.Option<A>
|
|
||||||
useSubscribeStream<A, E, IE, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue: () => Effect.Effect<A, IE, R>,
|
|
||||||
): Option.Some<A>
|
|
||||||
useSubscribeStream<A, E, IE, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
stream: Stream.Stream<A, E, R>,
|
|
||||||
initialValue?: () => Effect.Effect<A, IE, R>,
|
|
||||||
): Option.Option<A> {
|
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
|
||||||
() => initialValue
|
|
||||||
? Effect.map(initialValue(), Option.some)
|
|
||||||
: Effect.succeed(Option.none()),
|
|
||||||
[],
|
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
|
||||||
))
|
|
||||||
|
|
||||||
this.useFork(() => Stream.runForEach(
|
|
||||||
Stream.changesWith(stream, (x, y) => x === y),
|
|
||||||
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
|
||||||
), [stream])
|
|
||||||
|
|
||||||
return reactStateValue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SubRefFromGetSet<A, B, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
props: {
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
readonly getter: (parentValue: B) => A,
|
|
||||||
readonly setter: (parentValue: B, value: A) => B,
|
|
||||||
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<A, B>) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode {
|
|
||||||
return props.children(this.useSubRefFromGetSet(props.parent, props.getter, props.setter))
|
|
||||||
}
|
|
||||||
|
|
||||||
SubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
props: {
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
readonly path: P,
|
|
||||||
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B>) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode {
|
|
||||||
return props.children(this.useSubRefFromPath(props.parent, props.path))
|
|
||||||
}
|
|
||||||
|
|
||||||
SubscribeRefs<
|
|
||||||
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
|
||||||
R,
|
|
||||||
>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
props: {
|
|
||||||
readonly refs: Refs
|
|
||||||
readonly children: (...args: RefsA<Refs>) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode {
|
|
||||||
return props.children(...this.useSubscribeRefs(...props.refs))
|
|
||||||
}
|
|
||||||
|
|
||||||
RefState<A, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
props: {
|
|
||||||
readonly ref: SubscriptionRef.SubscriptionRef<A>
|
|
||||||
readonly children: (state: [A, React.Dispatch<React.SetStateAction<A>>]) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode {
|
|
||||||
return props.children(this.useRefState(props.ref))
|
|
||||||
}
|
|
||||||
|
|
||||||
SubscribeStream<A, E, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
props: {
|
|
||||||
readonly stream: Stream.Stream<A, E, R>
|
|
||||||
readonly children: (latestValue: Option.Option<A>) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode
|
|
||||||
SubscribeStream<A, E, IE, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
props: {
|
|
||||||
readonly stream: Stream.Stream<A, E, R>
|
|
||||||
readonly initialValue: () => Effect.Effect<A, IE, R>
|
|
||||||
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode
|
|
||||||
SubscribeStream<A, E, IE, R>(
|
|
||||||
this: ReffuseNamespace<R>,
|
|
||||||
props: {
|
|
||||||
readonly stream: Stream.Stream<A, E, R>
|
|
||||||
readonly initialValue?: () => Effect.Effect<A, IE, R>
|
|
||||||
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
|
||||||
},
|
|
||||||
): React.ReactNode {
|
|
||||||
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReffuseNamespace<R> extends Pipeable.Pipeable {}
|
|
||||||
|
|
||||||
ReffuseNamespace.prototype.pipe = function pipe() {
|
|
||||||
return Pipeable.pipeArguments(this, arguments)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export interface ReffuseNamespaceClass<R> extends Pipeable.Pipeable {
|
|
||||||
new(): ReffuseNamespace<R>
|
|
||||||
make<Self>(this: new () => Self): Self
|
|
||||||
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
|
|
||||||
}
|
|
||||||
|
|
||||||
(ReffuseNamespace as ReffuseNamespaceClass<any>).make = function make() {
|
|
||||||
return new this()
|
|
||||||
};
|
|
||||||
|
|
||||||
(ReffuseNamespace as ReffuseNamespaceClass<any>).pipe = function pipe() {
|
|
||||||
return Pipeable.pipeArguments(this, arguments)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const makeClass = (): ReffuseNamespaceClass<never> => (
|
|
||||||
class extends (ReffuseNamespace<never> as ReffuseNamespaceClass<never>) {
|
|
||||||
static readonly contexts = []
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Runtime } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
|
|
||||||
|
|
||||||
export const Provider = function ReffuseRuntimeReactProvider(props: {
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return React.createElement(Context, {
|
|
||||||
...props,
|
|
||||||
value: Runtime.defaultRuntime,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useRuntime = () => React.useContext(Context)
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
export * as Reffuse from "./Reffuse.js"
|
export * as Reffuse from "./Reffuse.js"
|
||||||
export * as ReffuseContext from "./ReffuseContext.js"
|
|
||||||
export * as ReffuseExtension from "./ReffuseExtension.js"
|
|
||||||
export * as ReffuseNamespace from "./ReffuseNamespace.js"
|
|
||||||
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { Array, Function, Option, Predicate } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
||||||
|
|
||||||
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
|
||||||
D extends never ? [] :
|
|
||||||
T extends Seen ? [] :
|
|
||||||
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
|
||||||
T extends object ? ObjectPaths<T, D, Seen | T> :
|
|
||||||
never
|
|
||||||
)
|
|
||||||
|
|
||||||
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
|
||||||
[K in keyof T as K extends number ? K : never]:
|
|
||||||
| [K]
|
|
||||||
| [K, ...Paths<T[K], Prev[D], Seen>]
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
|
||||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
|
||||||
NonNullable<T[K]> extends infer V
|
|
||||||
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
|
||||||
: never
|
|
||||||
} extends infer O
|
|
||||||
? O[keyof O]
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
|
||||||
? Head extends keyof T
|
|
||||||
? ValueFromPath<T[Head], Tail>
|
|
||||||
: T extends readonly any[]
|
|
||||||
? Head extends number
|
|
||||||
? ValueFromPath<T[number], Tail>
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
: T
|
|
||||||
|
|
||||||
export type AnyKey = string | number | symbol
|
|
||||||
export type AnyPath = readonly AnyKey[]
|
|
||||||
|
|
||||||
|
|
||||||
export const unsafeGet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
|
||||||
path.reduce((acc: any, key: any) => acc?.[key], self)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const get: {
|
|
||||||
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
|
||||||
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
|
||||||
path.reduce(
|
|
||||||
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
|
||||||
? Predicate.hasProperty(acc.value, key)
|
|
||||||
? Option.some(acc.value[key])
|
|
||||||
: Option.none()
|
|
||||||
: acc,
|
|
||||||
|
|
||||||
Option.some(self),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const immutableSet: {
|
|
||||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
|
||||||
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
|
||||||
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
|
||||||
const key = Array.head(path as AnyPath)
|
|
||||||
if (Option.isNone(key))
|
|
||||||
return Option.some(value as T)
|
|
||||||
if (!Predicate.hasProperty(self, key.value))
|
|
||||||
return Option.none()
|
|
||||||
|
|
||||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
|
||||||
if (Option.isNone(child))
|
|
||||||
return child
|
|
||||||
|
|
||||||
if (Array.isArray(self))
|
|
||||||
return typeof key.value === "number"
|
|
||||||
? Option.some([
|
|
||||||
...self.slice(0, key.value),
|
|
||||||
child.value,
|
|
||||||
...self.slice(key.value + 1),
|
|
||||||
] as T)
|
|
||||||
: Option.none()
|
|
||||||
|
|
||||||
if (typeof self === "object")
|
|
||||||
return Option.some(
|
|
||||||
Object.assign(
|
|
||||||
Object.create(Object.getPrototypeOf(self)),
|
|
||||||
{ ...self, [key.value]: child.value },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Option.none()
|
|
||||||
})
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Function } from "effect"
|
|
||||||
import type * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const value: {
|
|
||||||
<S>(prevState: S): (self: React.SetStateAction<S>) => S
|
|
||||||
<S>(self: React.SetStateAction<S>, prevState: S): S
|
|
||||||
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
|
|
||||||
typeof self === "function"
|
|
||||||
? (self as (prevState: S) => S)(prevState)
|
|
||||||
: self
|
|
||||||
)
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
|
||||||
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
|
||||||
|
|
||||||
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
|
||||||
|
|
||||||
readonly [Unify.typeSymbol]?: unknown
|
|
||||||
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
|
||||||
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace SubscriptionSubRef {
|
|
||||||
export interface Variance<in out A, in out B> {
|
|
||||||
readonly [SubscriptionSubRefTypeId]: {
|
|
||||||
readonly _A: Types.Invariant<A>
|
|
||||||
readonly _B: Types.Invariant<B>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
|
||||||
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
|
||||||
SubscriptionRef?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const refVariance = { _A: (_: any) => _ }
|
|
||||||
const synchronizedRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionRefVariance = { _A: (_: any) => _ }
|
|
||||||
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
|
||||||
|
|
||||||
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
|
||||||
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
|
||||||
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
|
||||||
readonly [Ref.RefTypeId] = refVariance
|
|
||||||
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
|
||||||
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
|
||||||
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
|
||||||
|
|
||||||
readonly get: Effect.Effect<A>
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
readonly getter: (parentValue: B) => A,
|
|
||||||
readonly setter: (parentValue: B, value: A) => B,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
|
||||||
}
|
|
||||||
|
|
||||||
commit() {
|
|
||||||
return this.get
|
|
||||||
}
|
|
||||||
|
|
||||||
get changes(): Stream.Stream<A> {
|
|
||||||
return this.get.pipe(
|
|
||||||
Effect.map(a => this.parent.changes.pipe(
|
|
||||||
Stream.map(this.getter),
|
|
||||||
s => Stream.concat(Stream.make(a), s),
|
|
||||||
)),
|
|
||||||
Stream.unwrap,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
|
||||||
return this.modifyEffect(a => Effect.succeed(f(a)))
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
|
||||||
return Effect.Do.pipe(
|
|
||||||
Effect.bind("b", () => Ref.get(this.parent)),
|
|
||||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
|
||||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
|
||||||
Effect.map(({ ca: [c] }) => c),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const makeFromGetSet = <A, B>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
getter: (parentValue: B) => A,
|
|
||||||
setter: (parentValue: B, value: A) => B,
|
|
||||||
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
|
||||||
|
|
||||||
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
|
||||||
parent: SubscriptionRef.SubscriptionRef<B>,
|
|
||||||
path: P,
|
|
||||||
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
|
||||||
parent,
|
|
||||||
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
|
||||||
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * as PropertyPath from "./PropertyPath.js"
|
|
||||||
export * as SetStateAction from "./SetStateAction.js"
|
|
||||||
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Extracts the common keys between two types
|
|
||||||
*/
|
|
||||||
export type CommonKeys<A, B> = Extract<keyof A, keyof B>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtain the static members type of a constructor function type
|
|
||||||
*/
|
|
||||||
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
|
|
||||||
|
|
||||||
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self
|
|
||||||
11
turbo.json
11
turbo.json
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://turbo.build/schema.json",
|
|
||||||
"tasks": {
|
|
||||||
"build": {
|
|
||||||
"dependsOn": ["^build"],
|
|
||||||
"inputs": ["./src/**"],
|
|
||||||
"outputs": ["./dist/**"]
|
|
||||||
},
|
|
||||||
"pack": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user