Compare commits
119 Commits
b3ec1c4f49
...
ai-doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acce65c6a4 | ||
|
|
825de84cef | ||
|
|
d6011f7897 | ||
|
|
8d4bce9e53 | ||
|
|
a7b5a32071 | ||
|
|
f7dd4e51f5 | ||
|
|
8772e25ff5 | ||
|
|
94a0864132 | ||
|
|
be8098fb7d | ||
|
|
7021e604ed | ||
|
|
1fd2a9ffbe | ||
|
|
1ed73dc3ac | ||
|
|
c689778cea | ||
|
|
da2a32001c | ||
|
|
5ac3a932d9 | ||
|
|
7935293bc3 | ||
|
|
cabceaffcd | ||
|
|
d239a11cdc | ||
|
|
fad61afce7 | ||
|
|
11fd4941c0 | ||
|
|
7bebc39a87 | ||
|
|
3bc0cc6586 | ||
|
|
f99d18b846 | ||
|
|
d61339ea6a | ||
|
|
3659d3f342 | ||
|
|
1e8a5d412f | ||
|
|
86539f33f0 | ||
|
|
8fa24b1791 | ||
|
|
adaadf13b2 | ||
|
|
3af7c3bf7a | ||
|
|
00b7228073 | ||
|
|
c2b2b1b96e | ||
|
|
74cf37e3a3 | ||
|
|
98091d4598 | ||
|
|
b2f1626268 | ||
|
|
40e8bf6a1f | ||
|
|
9c96741c8e | ||
|
|
3fa9b7d821 | ||
|
|
6b0f2f33cb | ||
|
|
2e00db5778 | ||
|
|
660f32a171 | ||
|
|
3f2639fda1 | ||
|
|
f76b3f333a | ||
|
|
3b407c6b4f | ||
|
|
b01b95a9d5 | ||
|
|
91b95ea6af | ||
|
|
7c99d1ff3d | ||
|
|
ae815553f2 | ||
|
|
86a96cbcce | ||
|
|
538b3a415d | ||
|
|
5b023678f4 | ||
|
|
2aa0c64a7c | ||
|
|
52ff7edfa1 | ||
|
|
ccb65ec209 | ||
|
|
47905d86b6 | ||
|
|
9266697aa4 | ||
|
|
08f0610752 | ||
|
|
ad81bf9ed8 | ||
|
|
e92087e593 | ||
|
|
e182e6ab5c | ||
|
|
89175be558 | ||
|
|
4df90a0f1c | ||
|
|
693c7b2db8 | ||
|
|
5f60d03d83 | ||
|
|
ea768218a0 | ||
|
|
3b4eb750ed | ||
|
|
47aa130486 | ||
|
|
02da3df8eb | ||
|
|
8d276d2fbf | ||
|
|
af077d34aa | ||
|
|
618cee4028 | ||
|
|
8244c34d2a | ||
|
|
523d835d00 | ||
|
|
15e96b8fa9 | ||
|
|
44de864713 | ||
|
|
8e1f0a27cf | ||
|
|
8754020323 | ||
|
|
d9a01dae0f | ||
|
|
8873e81f7c | ||
|
|
38fcafb15c | ||
|
|
411397c7de | ||
|
|
85e7b54962 | ||
|
|
ce3989ab77 | ||
|
|
da0f6168f0 | ||
|
|
690dec1f1a | ||
|
|
60274266da | ||
|
|
28424b63cb | ||
|
|
e063eb06f7 | ||
|
|
fb5bb7fcef | ||
|
|
1f57f7d127 | ||
|
|
e8742e5aa6 | ||
|
|
be79d24d6e | ||
|
|
e1349e5e03 | ||
|
|
837dcbb1cb | ||
|
|
8252b6cbdf | ||
|
|
256638bc06 | ||
|
|
c0097bbe81 | ||
|
|
febeaa05d0 | ||
|
|
a71640d493 | ||
|
|
b636a709f3 | ||
|
|
fffbd01b5e | ||
|
|
36d5414d10 | ||
|
|
65810a6d79 | ||
|
|
9e7b30fbb4 | ||
|
|
6c843562ab | ||
|
|
809f512d11 | ||
|
|
e71239b903 | ||
|
|
bfcc097882 | ||
|
|
933b061b5d | ||
|
|
734c84824c | ||
|
|
e83e86f8f1 | ||
|
|
bebbc1d7de | ||
|
|
a7a0951b61 | ||
|
|
1b1a1961bc | ||
|
|
8a9f7ad4c2 | ||
|
|
030a032c67 | ||
|
|
13a60bfdf9 | ||
|
|
3a34a4f5c7 | ||
|
|
5430d8daa4 |
@@ -8,13 +8,9 @@ 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: Lint TypeScript
|
- name: Build
|
||||||
run: npm run lint:tsc
|
run: bun run build
|
||||||
|
|||||||
@@ -11,18 +11,30 @@ 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: npm run build
|
run: bun run build
|
||||||
- name: Publish
|
- name: Publish reffuse
|
||||||
run: |
|
uses: JS-DevTools/npm-publish@v3
|
||||||
npm config set @thilawyn:registry https://git.valverde.cloud/api/packages/thilawyn/npm/
|
with:
|
||||||
npm config set -- //git.valverde.cloud/api/packages/thilawyn/npm/:_authToken "${{ vars.NODE_AUTH_TOKEN }}"
|
package: packages/reffuse
|
||||||
npm publish
|
access: public
|
||||||
|
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: npm run build
|
run: bun run build
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: npm pack --dry-run
|
run: bun run pack
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -130,3 +130,4 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
.turbo
|
||||||
|
|||||||
1
.npmrc
Normal file
1
.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@thilawyn:registry=https://git.valverde.cloud/api/packages/thilawyn/npm/
|
||||||
10
README.md
10
README.md
@@ -1,3 +1,9 @@
|
|||||||
# Reffuse
|
# Reffuse Monorepo
|
||||||
|
|
||||||
Effect integration for React
|
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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[install.scopes]
|
||||||
|
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"
|
||||||
17
package.json
17
package.json
@@ -1,9 +1,24 @@
|
|||||||
{
|
{
|
||||||
|
"name": "@reffuse/monorepo",
|
||||||
|
"packageManager": "bun@1.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": ["./packages/*"],
|
"workspaces": [
|
||||||
|
"./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": {
|
||||||
|
"code-narrator": "^1.0.17",
|
||||||
|
"npm-check-updates": "^17.1.14",
|
||||||
|
"npm-sort": "^0.0.4",
|
||||||
|
"turbo": "^2.4.4",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
packages/example/.gitignore
vendored
Normal file
24
packages/example/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
50
packages/example/README.md
Normal file
50
packages/example/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
28
packages/example/eslint.config.js
Normal file
28
packages/example/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
13
packages/example/index.html
Normal file
13
packages/example/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
packages/example/package.json
Normal file
52
packages/example/package.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "@reffuse/example",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint:tsc": "tsc --noEmit",
|
||||||
|
"lint:eslint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.21.0",
|
||||||
|
"@tanstack/react-router": "^1.112.7",
|
||||||
|
"@tanstack/router-devtools": "^1.112.7",
|
||||||
|
"@tanstack/router-plugin": "^1.112.7",
|
||||||
|
"@thilawyn/thilaschema": "^0.1.4",
|
||||||
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.21.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"typescript-eslint": "^8.26.0",
|
||||||
|
"vite": "^6.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@effect/platform": "^0.77.6",
|
||||||
|
"@effect/platform-browser": "^0.56.6",
|
||||||
|
"@radix-ui/themes": "^3.2.1",
|
||||||
|
"@reffuse/extension-lazyref": "workspace:*",
|
||||||
|
"@reffuse/extension-query": "workspace:*",
|
||||||
|
"@typed/async-data": "^0.13.1",
|
||||||
|
"@typed/id": "^0.17.1",
|
||||||
|
"@typed/lazy-ref": "^0.3.3",
|
||||||
|
"effect": "^3.13.6",
|
||||||
|
"lucide-react": "^0.477.0",
|
||||||
|
"mobx": "^6.13.6",
|
||||||
|
"reffuse": "workspace:*"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"effect": "^3.13.6",
|
||||||
|
"@effect/platform": "^0.77.6",
|
||||||
|
"@effect/platform-browser": "^0.56.6",
|
||||||
|
"@typed/lazy-ref": "^0.3.3",
|
||||||
|
"@typed/async-data": "^0.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/example/public/vite.svg
Normal file
1
packages/example/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
26
packages/example/src/domain/Todo.ts
Normal file
26
packages/example/src/domain/Todo.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ThSchema } from "@thilawyn/thilaschema"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Effect, 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 = TodoFromJsonStruct.pipe(Schema.compose(Todo))
|
||||||
|
|
||||||
|
|
||||||
|
export const generateUniqueID = makeUuid4.pipe(
|
||||||
|
Effect.provide(GetRandomValues.CryptoRandom)
|
||||||
|
)
|
||||||
1
packages/example/src/domain/index.ts
Normal file
1
packages/example/src/domain/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as Todo from "./Todo"
|
||||||
0
packages/example/src/index.css
Normal file
0
packages/example/src/index.css
Normal file
36
packages/example/src/main.tsx
Normal file
36
packages/example/src/main.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { FetchHttpClient } from "@effect/platform"
|
||||||
|
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||||
|
import { createRouter, RouterProvider } from "@tanstack/react-router"
|
||||||
|
import { Layer } from "effect"
|
||||||
|
import { StrictMode } from "react"
|
||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
import { ReffuseRuntime } from "reffuse"
|
||||||
|
import { GlobalContext } from "./reffuse"
|
||||||
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
|
||||||
|
|
||||||
|
const layer = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(Clipboard.layer),
|
||||||
|
Layer.provideMerge(Geolocation.layer),
|
||||||
|
Layer.provideMerge(Permissions.layer),
|
||||||
|
Layer.provideMerge(FetchHttpClient.layer),
|
||||||
|
)
|
||||||
|
|
||||||
|
const router = createRouter({ routeTree })
|
||||||
|
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ReffuseRuntime.Provider>
|
||||||
|
<GlobalContext.Provider layer={layer}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</GlobalContext.Provider>
|
||||||
|
</ReffuseRuntime.Provider>
|
||||||
|
</StrictMode>
|
||||||
|
)
|
||||||
10
packages/example/src/query/reffuse.ts
Normal file
10
packages/example/src/query/reffuse.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { GlobalReffuse } from "@/reffuse"
|
||||||
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
|
import { Uuid4Query } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>()
|
||||||
|
|
||||||
|
export const R = new class QueryReffuse extends GlobalReffuse.pipe(
|
||||||
|
Reffuse.withContexts(QueryContext)
|
||||||
|
) {}
|
||||||
12
packages/example/src/query/services/Uuid4Query.ts
Normal file
12
packages/example/src/query/services/Uuid4Query.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { HttpClientError } from "@effect/platform"
|
||||||
|
import { QueryService } from "@reffuse/extension-query"
|
||||||
|
import { ParseResult, Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export const Result = Schema.Array(Schema.String)
|
||||||
|
|
||||||
|
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
|
||||||
|
readonly ["uuid4", number],
|
||||||
|
typeof Result.Type,
|
||||||
|
HttpClientError.HttpClientError | ParseResult.ParseError
|
||||||
|
>() {}
|
||||||
1
packages/example/src/query/services/index.ts
Normal file
1
packages/example/src/query/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as Uuid4Query from "./Uuid4Query"
|
||||||
32
packages/example/src/query/views/Uuid4QueryService.tsx
Normal file
32
packages/example/src/query/views/Uuid4QueryService.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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 runSync = R.useRunSync()
|
||||||
|
|
||||||
|
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||||
|
const [state] = R.useRefState(query.state)
|
||||||
|
|
||||||
|
|
||||||
|
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={() => runSync(query.refresh)}>Refresh</Button>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
packages/example/src/reffuse.ts
Normal file
21
packages/example/src/reffuse.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { HttpClient } from "@effect/platform"
|
||||||
|
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||||
|
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||||
|
import { QueryExtension } from "@reffuse/extension-query"
|
||||||
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
|
|
||||||
|
|
||||||
|
export const GlobalContext = ReffuseContext.make<
|
||||||
|
| Clipboard.Clipboard
|
||||||
|
| Geolocation.Geolocation
|
||||||
|
| Permissions.Permissions
|
||||||
|
| HttpClient.HttpClient
|
||||||
|
>()
|
||||||
|
|
||||||
|
export class GlobalReffuse extends Reffuse.Reffuse.pipe(
|
||||||
|
Reffuse.withExtension(LazyRefExtension),
|
||||||
|
Reffuse.withExtension(QueryExtension),
|
||||||
|
Reffuse.withContexts(GlobalContext),
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export const R = new GlobalReffuse()
|
||||||
300
packages/example/src/routeTree.gen.ts
Normal file
300
packages/example/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
|
// Import Routes
|
||||||
|
|
||||||
|
import { Route as rootRoute } from './routes/__root'
|
||||||
|
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 BlankImport } from './routes/blank'
|
||||||
|
import { Route as IndexImport } from './routes/index'
|
||||||
|
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||||
|
import { Route as QueryServiceImport } from './routes/query/service'
|
||||||
|
|
||||||
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const TimeRoute = TimeImport.update({
|
||||||
|
id: '/time',
|
||||||
|
path: '/time',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} 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({
|
||||||
|
id: '/count',
|
||||||
|
path: '/count',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const BlankRoute = BlankImport.update({
|
||||||
|
id: '/blank',
|
||||||
|
path: '/blank',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const IndexRoute = IndexImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||||
|
id: '/query/usequery',
|
||||||
|
path: '/query/usequery',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const QueryServiceRoute = QueryServiceImport.update({
|
||||||
|
id: '/query/service',
|
||||||
|
path: '/query/service',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface FileRoutesByPath {
|
||||||
|
'/': {
|
||||||
|
id: '/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/'
|
||||||
|
preLoaderRoute: typeof IndexImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/blank': {
|
||||||
|
id: '/blank'
|
||||||
|
path: '/blank'
|
||||||
|
fullPath: '/blank'
|
||||||
|
preLoaderRoute: typeof BlankImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/count': {
|
||||||
|
id: '/count'
|
||||||
|
path: '/count'
|
||||||
|
fullPath: '/count'
|
||||||
|
preLoaderRoute: typeof CountImport
|
||||||
|
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': {
|
||||||
|
id: '/time'
|
||||||
|
path: '/time'
|
||||||
|
fullPath: '/time'
|
||||||
|
preLoaderRoute: typeof TimeImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/query/service': {
|
||||||
|
id: '/query/service'
|
||||||
|
path: '/query/service'
|
||||||
|
fullPath: '/query/service'
|
||||||
|
preLoaderRoute: typeof QueryServiceImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/query/usequery': {
|
||||||
|
id: '/query/usequery'
|
||||||
|
path: '/query/usequery'
|
||||||
|
fullPath: '/query/usequery'
|
||||||
|
preLoaderRoute: typeof QueryUsequeryImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export the route tree
|
||||||
|
|
||||||
|
export interface FileRoutesByFullPath {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/blank': typeof BlankRoute
|
||||||
|
'/count': typeof CountRoute
|
||||||
|
'/lazyref': typeof LazyrefRoute
|
||||||
|
'/promise': typeof PromiseRoute
|
||||||
|
'/tests': typeof TestsRoute
|
||||||
|
'/time': typeof TimeRoute
|
||||||
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRoutesByTo {
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/blank': typeof BlankRoute
|
||||||
|
'/count': typeof CountRoute
|
||||||
|
'/lazyref': typeof LazyrefRoute
|
||||||
|
'/promise': typeof PromiseRoute
|
||||||
|
'/tests': typeof TestsRoute
|
||||||
|
'/time': typeof TimeRoute
|
||||||
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRoutesById {
|
||||||
|
__root__: typeof rootRoute
|
||||||
|
'/': typeof IndexRoute
|
||||||
|
'/blank': typeof BlankRoute
|
||||||
|
'/count': typeof CountRoute
|
||||||
|
'/lazyref': typeof LazyrefRoute
|
||||||
|
'/promise': typeof PromiseRoute
|
||||||
|
'/tests': typeof TestsRoute
|
||||||
|
'/time': typeof TimeRoute
|
||||||
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRouteTypes {
|
||||||
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/blank'
|
||||||
|
| '/count'
|
||||||
|
| '/lazyref'
|
||||||
|
| '/promise'
|
||||||
|
| '/tests'
|
||||||
|
| '/time'
|
||||||
|
| '/query/service'
|
||||||
|
| '/query/usequery'
|
||||||
|
fileRoutesByTo: FileRoutesByTo
|
||||||
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/blank'
|
||||||
|
| '/count'
|
||||||
|
| '/lazyref'
|
||||||
|
| '/promise'
|
||||||
|
| '/tests'
|
||||||
|
| '/time'
|
||||||
|
| '/query/service'
|
||||||
|
| '/query/usequery'
|
||||||
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/blank'
|
||||||
|
| '/count'
|
||||||
|
| '/lazyref'
|
||||||
|
| '/promise'
|
||||||
|
| '/tests'
|
||||||
|
| '/time'
|
||||||
|
| '/query/service'
|
||||||
|
| '/query/usequery'
|
||||||
|
fileRoutesById: FileRoutesById
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RootRouteChildren {
|
||||||
|
IndexRoute: typeof IndexRoute
|
||||||
|
BlankRoute: typeof BlankRoute
|
||||||
|
CountRoute: typeof CountRoute
|
||||||
|
LazyrefRoute: typeof LazyrefRoute
|
||||||
|
PromiseRoute: typeof PromiseRoute
|
||||||
|
TestsRoute: typeof TestsRoute
|
||||||
|
TimeRoute: typeof TimeRoute
|
||||||
|
QueryServiceRoute: typeof QueryServiceRoute
|
||||||
|
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
|
IndexRoute: IndexRoute,
|
||||||
|
BlankRoute: BlankRoute,
|
||||||
|
CountRoute: CountRoute,
|
||||||
|
LazyrefRoute: LazyrefRoute,
|
||||||
|
PromiseRoute: PromiseRoute,
|
||||||
|
TestsRoute: TestsRoute,
|
||||||
|
TimeRoute: TimeRoute,
|
||||||
|
QueryServiceRoute: QueryServiceRoute,
|
||||||
|
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeTree = rootRoute
|
||||||
|
._addFileChildren(rootRouteChildren)
|
||||||
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
|
/* ROUTE_MANIFEST_START
|
||||||
|
{
|
||||||
|
"routes": {
|
||||||
|
"__root__": {
|
||||||
|
"filePath": "__root.tsx",
|
||||||
|
"children": [
|
||||||
|
"/",
|
||||||
|
"/blank",
|
||||||
|
"/count",
|
||||||
|
"/lazyref",
|
||||||
|
"/promise",
|
||||||
|
"/tests",
|
||||||
|
"/time",
|
||||||
|
"/query/service",
|
||||||
|
"/query/usequery"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"/": {
|
||||||
|
"filePath": "index.tsx"
|
||||||
|
},
|
||||||
|
"/blank": {
|
||||||
|
"filePath": "blank.tsx"
|
||||||
|
},
|
||||||
|
"/count": {
|
||||||
|
"filePath": "count.tsx"
|
||||||
|
},
|
||||||
|
"/lazyref": {
|
||||||
|
"filePath": "lazyref.tsx"
|
||||||
|
},
|
||||||
|
"/promise": {
|
||||||
|
"filePath": "promise.tsx"
|
||||||
|
},
|
||||||
|
"/tests": {
|
||||||
|
"filePath": "tests.tsx"
|
||||||
|
},
|
||||||
|
"/time": {
|
||||||
|
"filePath": "time.tsx"
|
||||||
|
},
|
||||||
|
"/query/service": {
|
||||||
|
"filePath": "query/service.tsx"
|
||||||
|
},
|
||||||
|
"/query/usequery": {
|
||||||
|
"filePath": "query/usequery.tsx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ROUTE_MANIFEST_END */
|
||||||
32
packages/example/src/routes/__root.tsx
Normal file
32
packages/example/src/routes/__root.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Container, Flex, Theme } from "@radix-ui/themes"
|
||||||
|
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||||
|
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
||||||
|
|
||||||
|
import "@radix-ui/themes/styles.css"
|
||||||
|
import "../index.css"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createRootRoute({
|
||||||
|
component: Root
|
||||||
|
})
|
||||||
|
|
||||||
|
function Root() {
|
||||||
|
return (
|
||||||
|
<Theme>
|
||||||
|
<Container>
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
|
<Link to="/">Index</Link>
|
||||||
|
<Link to="/time">Time</Link>
|
||||||
|
<Link to="/count">Count</Link>
|
||||||
|
<Link to="/tests">Tests</Link>
|
||||||
|
<Link to="/promise">Promise</Link>
|
||||||
|
<Link to="/query/usequery">Query</Link>
|
||||||
|
<Link to="/blank">Blank</Link>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
<TanStackRouterDevtools />
|
||||||
|
</Theme>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
packages/example/src/routes/blank.tsx
Normal file
9
packages/example/src/routes/blank.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/blank')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <div>Hello "/blank"!</div>
|
||||||
|
}
|
||||||
27
packages/example/src/routes/count.tsx
Normal file
27
packages/example/src/routes/count.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { R } from "@/reffuse"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Ref } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/count")({
|
||||||
|
component: Count
|
||||||
|
})
|
||||||
|
|
||||||
|
function Count() {
|
||||||
|
|
||||||
|
const runSync = R.useRunSync()
|
||||||
|
|
||||||
|
const countRef = R.useRef(0)
|
||||||
|
const [count] = R.useRefState(countRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto">
|
||||||
|
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
||||||
|
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
|
||||||
|
count is {count}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
29
packages/example/src/routes/index.tsx
Normal file
29
packages/example/src/routes/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { TodosContext } from "@/todos/reffuse"
|
||||||
|
import { TodosState } from "@/todos/services"
|
||||||
|
import { VTodos } from "@/todos/views/VTodos"
|
||||||
|
import { Container } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Layer } from "effect"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: Index
|
||||||
|
})
|
||||||
|
|
||||||
|
function Index() {
|
||||||
|
|
||||||
|
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(TodosState.make("todos"))
|
||||||
|
), [])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<TodosContext.Provider layer={todosLayer}>
|
||||||
|
<VTodos />
|
||||||
|
</TodosContext.Provider>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
31
packages/example/src/routes/lazyref.tsx
Normal file
31
packages/example/src/routes/lazyref.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
packages/example/src/routes/promise.tsx
Normal file
35
packages/example/src/routes/promise.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
35
packages/example/src/routes/query/service.tsx
Normal file
35
packages/example/src/routes/query/service.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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, Schema } from "effect"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/query/service")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const query = R.useQuery({
|
||||||
|
key: R.useStreamFromValues(["uuid4", 10 as number]),
|
||||||
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
|
HttpClient.withTracerPropagation(false),
|
||||||
|
Effect.flatMap(res => res.json),
|
||||||
|
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
||||||
|
Effect.scoped,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const layer = useMemo(() => query.layer(Uuid4Query.Uuid4Query), [query])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryContext.Provider layer={layer}>
|
||||||
|
<Uuid4QueryService />
|
||||||
|
</QueryContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
66
packages/example/src/routes/query/usequery.tsx
Normal file
66
packages/example/src/routes/query/usequery.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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 } from "effect"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/query/usequery")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const Result = Schema.Array(Schema.String)
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const runSync = R.useRunSync()
|
||||||
|
|
||||||
|
const [count, setCount] = useState(1)
|
||||||
|
|
||||||
|
const query = R.useQuery({
|
||||||
|
key: R.useStreamFromValues(["uuid4", count]),
|
||||||
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
|
HttpClient.withTracerPropagation(false),
|
||||||
|
Effect.flatMap(res => res.json),
|
||||||
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
|
Effect.scoped,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const [state] = R.useRefState(query.state)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Flex direction="column" align="center" gap="2">
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={[count]}
|
||||||
|
onValueChange={flow(
|
||||||
|
Array.head,
|
||||||
|
Option.getOrThrow,
|
||||||
|
setCount,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
{AsyncData.match(state, {
|
||||||
|
NoData: () => "No data yet",
|
||||||
|
Loading: () => "Loading...",
|
||||||
|
Success: (value, { isRefreshing, isOptimistic }) =>
|
||||||
|
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
|
||||||
|
Failure: (cause, { isRefreshing }) =>
|
||||||
|
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button onClick={() => runSync(query.refresh)}>Refresh</Button>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
46
packages/example/src/routes/tests.tsx
Normal file
46
packages/example/src/routes/tests.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { R } from "@/reffuse"
|
||||||
|
import { Button, Flex } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Console, Effect, Stream } from "effect"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/tests")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||||
|
// Effect.andThen(makeUuid4),
|
||||||
|
// Effect.provide(GetRandomValues.CryptoRandom),
|
||||||
|
// ), [])
|
||||||
|
// console.log(value)
|
||||||
|
|
||||||
|
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
||||||
|
Effect.andThen(Console.log("ouient")),
|
||||||
|
Effect.delay("1 second"),
|
||||||
|
), [])
|
||||||
|
|
||||||
|
const [reactValue, setReactValue] = useState("initial")
|
||||||
|
const reactValueStream = R.useStreamFromValues([reactValue])
|
||||||
|
R.useFork(() => Stream.runForEach(reactValueStream, Console.log), [reactValueStream])
|
||||||
|
|
||||||
|
|
||||||
|
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
|
||||||
|
yield* Effect.log(value)
|
||||||
|
}), [])
|
||||||
|
|
||||||
|
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
|
||||||
|
Effect.provide(GetRandomValues.CryptoRandom),
|
||||||
|
Effect.map(setReactValue),
|
||||||
|
), [])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="row" justify="center" align="center" gap="2">
|
||||||
|
<Button onClick={() => logValue("test")}>Log value</Button>
|
||||||
|
<Button onClick={() => generateUuid()}>Generate UUID</Button>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
packages/example/src/routes/time.tsx
Normal file
39
packages/example/src/routes/time.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { R } from "@/reffuse"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, DateTime, Effect, Ref, Schedule, Stream, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
const timeEverySecond = Stream.repeatEffectWithSchedule(
|
||||||
|
DateTime.now,
|
||||||
|
Schedule.intersect(Schedule.forever, Schedule.spaced("1 second")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/time")({
|
||||||
|
component: Time
|
||||||
|
})
|
||||||
|
|
||||||
|
function Time() {
|
||||||
|
|
||||||
|
const timeRef = R.useMemo(() => DateTime.now.pipe(Effect.flatMap(SubscriptionRef.make)), [])
|
||||||
|
|
||||||
|
R.useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||||
|
Effect.andThen(Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v)))
|
||||||
|
), [timeRef])
|
||||||
|
|
||||||
|
const [time] = R.useRefState(timeRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<p className="text-center">
|
||||||
|
{DateTime.format(time, {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
second: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
1
packages/example/src/services/index.ts
Normal file
1
packages/example/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {}
|
||||||
10
packages/example/src/todos/reffuse.ts
Normal file
10
packages/example/src/todos/reffuse.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { GlobalReffuse } from "@/reffuse"
|
||||||
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
|
import { TodosState } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
|
||||||
|
|
||||||
|
export const R = new class TodosReffuse extends GlobalReffuse.pipe(
|
||||||
|
Reffuse.withContexts(TodosContext)
|
||||||
|
) {}
|
||||||
69
packages/example/src/todos/services/TodosState.ts
Normal file
69
packages/example/src/todos/services/TodosState.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Todo } from "@/domain"
|
||||||
|
import { KeyValueStore } from "@effect/platform"
|
||||||
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
|
import { PlatformError } from "@effect/platform/Error"
|
||||||
|
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
||||||
|
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
||||||
|
|
||||||
|
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||||
|
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||||
|
|
||||||
|
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
|
||||||
|
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
|
||||||
|
readonly remove: (index: number) => Effect.Effect<void>
|
||||||
|
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
|
||||||
|
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
|
||||||
|
}>() {}
|
||||||
|
|
||||||
|
|
||||||
|
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
|
||||||
|
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
|
||||||
|
|
||||||
|
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
||||||
|
Effect.flatMap(kv => kv.get(key)),
|
||||||
|
Effect.flatMap(identity),
|
||||||
|
Effect.flatMap(Schema.parseJson().pipe(
|
||||||
|
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||||
|
Schema.decode,
|
||||||
|
)),
|
||||||
|
Effect.flatMap(v => Ref.set(todos, v)),
|
||||||
|
|
||||||
|
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
|
||||||
|
|
||||||
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
|
||||||
|
Effect.flatMap(([kv, values]) => values.pipe(
|
||||||
|
Schema.parseJson().pipe(
|
||||||
|
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
||||||
|
Schema.encode,
|
||||||
|
),
|
||||||
|
Effect.flatMap(v => kv.set(key, v)),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
|
)
|
||||||
|
|
||||||
|
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
|
||||||
|
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
|
||||||
|
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
|
||||||
|
|
||||||
|
// const moveUp = (index: number) => Effect.gen(function*() {
|
||||||
|
|
||||||
|
// })
|
||||||
|
|
||||||
|
yield* readFromLocalStorage
|
||||||
|
|
||||||
|
return {
|
||||||
|
todos,
|
||||||
|
readFromLocalStorage,
|
||||||
|
saveToLocalStorage,
|
||||||
|
prepend,
|
||||||
|
replace,
|
||||||
|
remove,
|
||||||
|
}
|
||||||
|
}))
|
||||||
1
packages/example/src/todos/services/index.ts
Normal file
1
packages/example/src/todos/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as TodosState from "./TodosState"
|
||||||
53
packages/example/src/todos/views/VNewTodo.tsx
Normal file
53
packages/example/src/todos/views/VNewTodo.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Todo } from "@/domain"
|
||||||
|
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
||||||
|
import { Effect, Option, SubscriptionRef } from "effect"
|
||||||
|
import { R } from "../reffuse"
|
||||||
|
import { TodosState } from "../services"
|
||||||
|
|
||||||
|
|
||||||
|
const createEmptyTodo = Todo.generateUniqueID.pipe(
|
||||||
|
Effect.map(id => Todo.Todo.make({
|
||||||
|
id,
|
||||||
|
content: "",
|
||||||
|
completedAt: Option.none(),
|
||||||
|
}, true))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export function VNewTodo() {
|
||||||
|
|
||||||
|
const runSync = R.useRunSync()
|
||||||
|
|
||||||
|
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)), [])
|
||||||
|
const [todo, setTodo] = R.useRefState(todoRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Card>
|
||||||
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
|
<TextArea
|
||||||
|
value={todo.content}
|
||||||
|
onChange={e => setTodo(prev =>
|
||||||
|
Todo.Todo.make({ ...prev, content: e.target.value }, true)
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="center" align="center">
|
||||||
|
<Button
|
||||||
|
onClick={() => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.prepend(todo)),
|
||||||
|
Effect.andThen(createEmptyTodo),
|
||||||
|
Effect.map(setTodo),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
56
packages/example/src/todos/views/VTodo.tsx
Normal file
56
packages/example/src/todos/views/VTodo.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Todo } from "@/domain"
|
||||||
|
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Delete } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { R } from "../reffuse"
|
||||||
|
import { TodosState } from "../services"
|
||||||
|
|
||||||
|
|
||||||
|
export interface VTodoProps {
|
||||||
|
readonly index: number
|
||||||
|
readonly todo: Todo.Todo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VTodo({ index, todo }: VTodoProps) {
|
||||||
|
|
||||||
|
const runSync = R.useRunSync()
|
||||||
|
const editorMode = useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Card>
|
||||||
|
<Flex direction="column" align="stretch" gap="1">
|
||||||
|
<TextArea
|
||||||
|
value={todo.content}
|
||||||
|
onChange={e => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.replace(
|
||||||
|
index,
|
||||||
|
Todo.Todo.make({ ...todo, content: e.target.value }, true),
|
||||||
|
)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
disabled={!editorMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Flex direction="row" justify="between" align="center">
|
||||||
|
<Box></Box>
|
||||||
|
|
||||||
|
<Flex direction="row" align="center" gap="1">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state => state.remove(index)),
|
||||||
|
runSync,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
36
packages/example/src/todos/views/VTodos.tsx
Normal file
36
packages/example/src/todos/views/VTodos.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Box, Flex } from "@radix-ui/themes"
|
||||||
|
import { Chunk, Effect, Stream } from "effect"
|
||||||
|
import { R } from "../reffuse"
|
||||||
|
import { TodosState } from "../services"
|
||||||
|
import { VNewTodo } from "./VNewTodo"
|
||||||
|
import { VTodo } from "./VTodo"
|
||||||
|
|
||||||
|
|
||||||
|
export function VTodos() {
|
||||||
|
|
||||||
|
// Sync changes to the todos with the local storage
|
||||||
|
R.useFork(() => TodosState.TodosState.pipe(
|
||||||
|
Effect.flatMap(state =>
|
||||||
|
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
|
||||||
|
)
|
||||||
|
), [])
|
||||||
|
|
||||||
|
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
|
||||||
|
const [todos] = R.useRefState(todosRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="center" gap="3">
|
||||||
|
<Box width="500px">
|
||||||
|
<VNewTodo />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{Chunk.map(todos, (todo, index) => (
|
||||||
|
<Box key={todo.id} width="500px">
|
||||||
|
<VTodo index={index} todo={todo} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
1
packages/example/src/vite-env.d.ts
vendored
Normal file
1
packages/example/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
30
packages/example/tsconfig.app.json
Normal file
30
packages/example/tsconfig.app.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
packages/example/tsconfig.json
Normal file
7
packages/example/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
24
packages/example/tsconfig.node.json
Normal file
24
packages/example/tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
19
packages/example/vite.config.ts
Normal file
19
packages/example/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"
|
||||||
|
import react from "@vitejs/plugin-react"
|
||||||
|
import path from "node:path"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
TanStackRouterVite(),
|
||||||
|
react(),
|
||||||
|
],
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
9
packages/extension-lazyref/README.md
Normal file
9
packages/extension-lazyref/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# LazyRef extension for Reffuse
|
||||||
|
|
||||||
|
Extension to integrate `@typed/lazy-ref` with Reffuse.
|
||||||
|
|
||||||
|
## Peer dependencies
|
||||||
|
- `@typed/lazy-ref`
|
||||||
|
- `reffuse` 0.1.3+
|
||||||
|
- `effect` 3.13+
|
||||||
|
- `react` & `@types/react` 19+
|
||||||
42
packages/extension-lazyref/package.json
Normal file
42
packages/extension-lazyref/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "@reffuse/extension-lazyref",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"./README.md",
|
||||||
|
"./dist"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"url": "git+https://github.com/Thiladev/reffuse.git"
|
||||||
|
},
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./dist/*.d.ts",
|
||||||
|
"default": "./dist/*.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint:tsc": "tsc --noEmit",
|
||||||
|
"pack": "npm pack",
|
||||||
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
|
"clean:dist": "rm -rf dist",
|
||||||
|
"clean:node": "rm -rf node_modules"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"reffuse": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@typed/lazy-ref": "^0.3.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"effect": "^3.13.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"reffuse": "^0.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/extension-lazyref/src/index.ts
Normal file
27
packages/extension-lazyref/src/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as LazyRef from "@typed/lazy-ref"
|
||||||
|
import { Effect, Stream } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse"
|
||||||
|
|
||||||
|
|
||||||
|
export const LazyRefExtension = ReffuseExtension.make(() => ({
|
||||||
|
useLazyRefState<A, E, R>(
|
||||||
|
this: ReffuseHelpers.ReffuseHelpers<R>,
|
||||||
|
ref: LazyRef.LazyRef<A, E, R>,
|
||||||
|
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||||
|
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||||
|
|
||||||
|
this.useFork(() => Stream.runForEach(ref.changes, 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]
|
||||||
|
},
|
||||||
|
}))
|
||||||
33
packages/extension-lazyref/tsconfig.json
Normal file
33
packages/extension-lazyref/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
// "allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
// "allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
// "noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
// Build
|
||||||
|
"outDir": "./dist",
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"include": ["./src"]
|
||||||
|
}
|
||||||
10
packages/extension-query/README.md
Normal file
10
packages/extension-query/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 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`
|
||||||
44
packages/extension-query/package.json
Normal file
44
packages/extension-query/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "@reffuse/extension-query",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"./README.md",
|
||||||
|
"./dist"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"url": "git+https://github.com/Thiladev/reffuse.git"
|
||||||
|
},
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./dist/*.d.ts",
|
||||||
|
"default": "./dist/*.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint:tsc": "tsc --noEmit",
|
||||||
|
"pack": "npm pack",
|
||||||
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
|
"clean:dist": "rm -rf dist",
|
||||||
|
"clean:node": "rm -rf node_modules"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"reffuse": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@effect/platform": "^0.77.0",
|
||||||
|
"@effect/platform-browser": "^0.56.0",
|
||||||
|
"@typed/async-data": "^0.13.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"effect": "^3.13.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"reffuse": "^0.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
134
packages/extension-query/src/MutationRunner.ts
Normal file
134
packages/extension-query/src/MutationRunner.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// import { BrowserStream } from "@effect/platform-browser"
|
||||||
|
// import * as AsyncData from "@typed/async-data"
|
||||||
|
// import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
// export interface MutationRunner<K extends readonly unknown[], A, E, R> {
|
||||||
|
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
|
||||||
|
|
||||||
|
// readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
|
||||||
|
// readonly forkMutate: Effect.Effect<Fiber.RuntimeFiber<void>>
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// export interface MakeProps<K extends readonly unknown[], A, E, R> {
|
||||||
|
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export const make = <K extends readonly unknown[], A, E, R>(
|
||||||
|
// { key, query }: MakeProps<K, A, E, R>
|
||||||
|
// ): Effect.Effect<MutationRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
|
||||||
|
// const context = yield* Effect.context<R>()
|
||||||
|
|
||||||
|
// const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>())
|
||||||
|
|
||||||
|
// const interrupt = fiberRef.pipe(
|
||||||
|
// Effect.flatMap(Option.match({
|
||||||
|
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
// Effect.andThen(Fiber.interrupt(fiber))
|
||||||
|
// ),
|
||||||
|
// onNone: () => Effect.void,
|
||||||
|
// }))
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const forkInterrupt = fiberRef.pipe(
|
||||||
|
// Effect.flatMap(Option.match({
|
||||||
|
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
// Effect.andThen(Fiber.interrupt(fiber).pipe(
|
||||||
|
// Effect.asVoid,
|
||||||
|
// Effect.forkDaemon,
|
||||||
|
// ))
|
||||||
|
// ),
|
||||||
|
// onNone: () => Effect.forkDaemon(Effect.void),
|
||||||
|
// }))
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const forkFetch = interrupt.pipe(
|
||||||
|
// Effect.andThen(
|
||||||
|
// Ref.set(stateRef, AsyncData.loading()).pipe(
|
||||||
|
// Effect.andThen(latestKeyRef),
|
||||||
|
// Effect.flatMap(identity),
|
||||||
|
// Effect.flatMap(key => query(key).pipe(
|
||||||
|
// Effect.matchCauseEffect({
|
||||||
|
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
|
||||||
|
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
|
||||||
|
// })
|
||||||
|
// )),
|
||||||
|
|
||||||
|
// Effect.provide(context),
|
||||||
|
// Effect.fork,
|
||||||
|
// )
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// Effect.flatMap(fiber =>
|
||||||
|
// Ref.set(fiberRef, Option.some(fiber)).pipe(
|
||||||
|
// Effect.andThen(Fiber.join(fiber)),
|
||||||
|
// Effect.andThen(Ref.set(fiberRef, Option.none())),
|
||||||
|
// )
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// Effect.forkDaemon,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const forkRefresh = interrupt.pipe(
|
||||||
|
// Effect.andThen(
|
||||||
|
// Ref.update(stateRef, previous => {
|
||||||
|
// if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
||||||
|
// return AsyncData.refreshing(previous)
|
||||||
|
// if (AsyncData.isRefreshing(previous))
|
||||||
|
// return AsyncData.refreshing(previous.previous)
|
||||||
|
// return AsyncData.loading()
|
||||||
|
// }).pipe(
|
||||||
|
// Effect.andThen(latestKeyRef),
|
||||||
|
// Effect.flatMap(identity),
|
||||||
|
// Effect.flatMap(key => query(key).pipe(
|
||||||
|
// Effect.matchCauseEffect({
|
||||||
|
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
|
||||||
|
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
|
||||||
|
// })
|
||||||
|
// )),
|
||||||
|
|
||||||
|
// Effect.provide(context),
|
||||||
|
// Effect.fork,
|
||||||
|
// )
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// Effect.flatMap(fiber =>
|
||||||
|
// Ref.set(fiberRef, Option.some(fiber)).pipe(
|
||||||
|
// Effect.andThen(Fiber.join(fiber)),
|
||||||
|
// Effect.andThen(Ref.set(fiberRef, Option.none())),
|
||||||
|
// )
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// Effect.forkDaemon,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
||||||
|
// Effect.andThen(Stream.runForEach(key, latestKey =>
|
||||||
|
// Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
||||||
|
// Effect.andThen(forkFetch)
|
||||||
|
// )
|
||||||
|
// ))
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const refreshOnWindowFocus = Stream.runForEach(
|
||||||
|
// BrowserStream.fromEventListenerWindow("focus"),
|
||||||
|
// () => forkRefresh,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// query,
|
||||||
|
|
||||||
|
// latestKeyRef,
|
||||||
|
// stateRef,
|
||||||
|
// fiberRef,
|
||||||
|
|
||||||
|
// forkInterrupt,
|
||||||
|
// forkFetch,
|
||||||
|
// forkRefresh,
|
||||||
|
|
||||||
|
// fetchOnKeyChange,
|
||||||
|
// refreshOnWindowFocus,
|
||||||
|
// }
|
||||||
|
// })
|
||||||
55
packages/extension-query/src/QueryExtension.ts
Normal file
55
packages/extension-query/src/QueryExtension.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
|
||||||
|
import * as QueryRunner from "./QueryRunner.js"
|
||||||
|
import type * as QueryService from "./QueryService.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly query: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
readonly refreshOnWindowFocus?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseQueryResult<K extends readonly unknown[], A, E> {
|
||||||
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
||||||
|
|
||||||
|
readonly layer: <Self, Id extends string>(
|
||||||
|
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
|
||||||
|
) => Layer.Layer<Self>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryExtension = ReffuseExtension.make(() => ({
|
||||||
|
useQuery<K extends readonly unknown[], A, E, R>(
|
||||||
|
this: ReffuseHelpers.ReffuseHelpers<R>,
|
||||||
|
props: UseQueryProps<K, A, E, R>,
|
||||||
|
): UseQueryResult<K, A, E> {
|
||||||
|
const runner = this.useMemo(() => QueryRunner.make({
|
||||||
|
key: props.key,
|
||||||
|
query: props.query,
|
||||||
|
}), [props.key])
|
||||||
|
|
||||||
|
this.useFork(() => runner.fetchOnKeyChange, [runner])
|
||||||
|
|
||||||
|
this.useFork(() => (props.refreshOnWindowFocus ?? true)
|
||||||
|
? runner.refreshOnWindowFocus
|
||||||
|
: Effect.void,
|
||||||
|
[props.refreshOnWindowFocus, runner])
|
||||||
|
|
||||||
|
return React.useMemo(() => ({
|
||||||
|
latestKey: runner.latestKeyRef,
|
||||||
|
state: runner.stateRef,
|
||||||
|
refresh: runner.forkRefresh,
|
||||||
|
|
||||||
|
layer: tag => Layer.succeed(tag, {
|
||||||
|
latestKey: runner.latestKeyRef,
|
||||||
|
state: runner.stateRef,
|
||||||
|
refresh: runner.forkRefresh,
|
||||||
|
}),
|
||||||
|
}), [runner])
|
||||||
|
}
|
||||||
|
}))
|
||||||
144
packages/extension-query/src/QueryRunner.ts
Normal file
144
packages/extension-query/src/QueryRunner.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { BrowserStream } from "@effect/platform-browser"
|
||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly query: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
|
||||||
|
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>>
|
||||||
|
|
||||||
|
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
||||||
|
readonly forkFetch: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
||||||
|
readonly forkRefresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
||||||
|
|
||||||
|
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
|
||||||
|
readonly refreshOnWindowFocus: Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly query: (key: K) => Effect.Effect<A, E, R>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <K extends readonly unknown[], A, E, R>(
|
||||||
|
{ key, query }: MakeProps<K, A, E, R>
|
||||||
|
): Effect.Effect<QueryRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
|
||||||
|
const context = yield* Effect.context<R>()
|
||||||
|
|
||||||
|
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
|
||||||
|
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>())
|
||||||
|
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>())
|
||||||
|
|
||||||
|
const interrupt = fiberRef.pipe(
|
||||||
|
Effect.flatMap(Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkInterrupt = fiberRef.pipe(
|
||||||
|
Effect.flatMap(Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
||||||
|
Effect.asVoid,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.forkDaemon(Effect.void),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkFetch = interrupt.pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
Ref.set(stateRef, AsyncData.loading()).pipe(
|
||||||
|
Effect.andThen(latestKeyRef),
|
||||||
|
Effect.flatMap(identity),
|
||||||
|
Effect.flatMap(key => query(key).pipe(
|
||||||
|
Effect.matchCauseEffect({
|
||||||
|
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
|
||||||
|
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
|
||||||
|
})
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(context),
|
||||||
|
Effect.fork,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.flatMap(fiber =>
|
||||||
|
Ref.set(fiberRef, Option.some(fiber)).pipe(
|
||||||
|
Effect.andThen(Fiber.join(fiber)),
|
||||||
|
Effect.andThen(Ref.set(fiberRef, Option.none())),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkRefresh = interrupt.pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
Ref.update(stateRef, previous => {
|
||||||
|
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
||||||
|
return AsyncData.refreshing(previous)
|
||||||
|
if (AsyncData.isRefreshing(previous))
|
||||||
|
return AsyncData.refreshing(previous.previous)
|
||||||
|
return AsyncData.loading()
|
||||||
|
}).pipe(
|
||||||
|
Effect.andThen(latestKeyRef),
|
||||||
|
Effect.flatMap(identity),
|
||||||
|
Effect.flatMap(key => query(key).pipe(
|
||||||
|
Effect.matchCauseEffect({
|
||||||
|
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
|
||||||
|
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
|
||||||
|
})
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(context),
|
||||||
|
Effect.fork,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.flatMap(fiber =>
|
||||||
|
Ref.set(fiberRef, Option.some(fiber)).pipe(
|
||||||
|
Effect.andThen(Fiber.join(fiber)),
|
||||||
|
Effect.andThen(Ref.set(fiberRef, Option.none())),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
||||||
|
Effect.andThen(Stream.runForEach(key, latestKey =>
|
||||||
|
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
||||||
|
Effect.andThen(forkFetch)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const refreshOnWindowFocus = Stream.runForEach(
|
||||||
|
BrowserStream.fromEventListenerWindow("focus"),
|
||||||
|
() => forkRefresh,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
|
||||||
|
latestKeyRef,
|
||||||
|
stateRef,
|
||||||
|
fiberRef,
|
||||||
|
|
||||||
|
forkInterrupt,
|
||||||
|
forkFetch,
|
||||||
|
forkRefresh,
|
||||||
|
|
||||||
|
fetchOnKeyChange,
|
||||||
|
refreshOnWindowFocus,
|
||||||
|
}
|
||||||
|
})
|
||||||
32
packages/extension-query/src/QueryService.ts
Normal file
32
packages/extension-query/src/QueryService.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Cause, Effect, type Fiber, type Option, type SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryService<K extends readonly unknown[], A, E> {
|
||||||
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never,
|
||||||
|
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
|
||||||
|
|
||||||
|
|
||||||
|
// export interface LayerProps<A, E, R> {
|
||||||
|
// readonly query: Effect.Effect<A, E, R>
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export const layer = <Self, Id extends string, A, E, R>(
|
||||||
|
// tag: Context.TagClass<Self, Id, QueryService<A, E>>,
|
||||||
|
// props: LayerProps<A, E, R>,
|
||||||
|
// ): Layer.Layer<Self, never, R> => Layer.effect(tag, Effect.gen(function*() {
|
||||||
|
// const runner = yield* QueryRunner.make({
|
||||||
|
// query: props.query
|
||||||
|
// })
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// state: runner.stateRef,
|
||||||
|
// refresh: runner.forkRefresh,
|
||||||
|
// }
|
||||||
|
// }))
|
||||||
3
packages/extension-query/src/index.ts
Normal file
3
packages/extension-query/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./QueryExtension.js"
|
||||||
|
export * as QueryRunner from "./QueryRunner.js"
|
||||||
|
export * as QueryService from "./QueryService.js"
|
||||||
33
packages/extension-query/tsconfig.json
Normal file
33
packages/extension-query/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
// "allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
// "allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
// "noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
// Build
|
||||||
|
"outDir": "./dist",
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"include": ["./src"]
|
||||||
|
}
|
||||||
14
packages/reffuse/.code-narrator/gpt_questions/README.liquid
Normal file
14
packages/reffuse/.code-narrator/gpt_questions/README.liquid
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Create a "ReadMe" guide for the project, named "{{ projectName }}".
|
||||||
|
Include the following:
|
||||||
|
Title, Description,
|
||||||
|
Getting Started by installing npm package, how to run it with npx
|
||||||
|
Configuration is optional and will be generated on first run
|
||||||
|
Reporting bugs, repository and homepage
|
||||||
|
Versioning
|
||||||
|
Authors
|
||||||
|
License
|
||||||
|
|
||||||
|
This is the entry file:
|
||||||
|
###
|
||||||
|
{{ entryFileContent }}
|
||||||
|
###
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Show how developer would add HowTo in config file,
|
||||||
|
args property is used to inject properties into liquid template, any property set in args can be access in liquid template with {{ keyName }}
|
||||||
|
file property appends extracted content of a file to liquid template, using JSONPath or the extract property that uses LLM to extract content from file
|
||||||
|
Developers MUST create a liquid template in .code-narrator/gpt_questions, this template file is used to ask GPT question
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Give title and short description that this is an overview file for files located in directory
|
||||||
|
Give short description of each file that is provided
|
||||||
|
Add link to each file, link should be the filename
|
||||||
|
|
||||||
11
packages/reffuse/README.md
Normal file
11
packages/reffuse/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 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+
|
||||||
101
packages/reffuse/code-narrator.config.js
Normal file
101
packages/reffuse/code-narrator.config.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
const ConfigurationBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Configuration/ConfigurationBuilder");
|
||||||
|
const FilesBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Files/FilesBuilder");
|
||||||
|
const FoldersBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Folders/FoldersBuilder");
|
||||||
|
const UserDefinedBuilder = require("code-narrator/dist/src/documentation/plugins/builders/UserDefined/UserDefinedBuilder");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can find the documentation about code-narrator.config.js at
|
||||||
|
* https://github.com/ingig/code-narrator/blob/master/docs/Configuration/code-narrator.config.js.md
|
||||||
|
*
|
||||||
|
* @type {ICodeNarratorConfig}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
// App specific configuration files. This could be something like project_name.json
|
||||||
|
config_files: [
|
||||||
|
|
||||||
|
],
|
||||||
|
project_file: "package.json",
|
||||||
|
entry_file: "./dist/index.js",
|
||||||
|
cli_file: "",
|
||||||
|
project_path: "./",
|
||||||
|
source_path: "src",
|
||||||
|
documentation_path: "./docs",
|
||||||
|
test_path: "test",
|
||||||
|
exclude: [
|
||||||
|
"/node_modules",
|
||||||
|
".env",
|
||||||
|
"/.idea",
|
||||||
|
"/.git",
|
||||||
|
".gitignore",
|
||||||
|
"/.code-narrator",
|
||||||
|
"/dist",
|
||||||
|
"/build",
|
||||||
|
"package-lock.json",
|
||||||
|
],
|
||||||
|
// Indicates if the documentation should create a README file in root of project
|
||||||
|
readmeRoot: true,
|
||||||
|
// Url to the repository, code-narrator tries to extract this from project file
|
||||||
|
repository_url: "git+https://github.com/Thiladev/reffuse.git",
|
||||||
|
// These are the plugins used when building documentation. You can create your own plugin. Checkout the code-narrator docs HowTo create a builder plugin
|
||||||
|
builderPlugins: [
|
||||||
|
ConfigurationBuilder,
|
||||||
|
FilesBuilder,
|
||||||
|
FoldersBuilder,
|
||||||
|
UserDefinedBuilder,
|
||||||
|
],
|
||||||
|
// These are system commends send to GPT with every query
|
||||||
|
gptSystemCommands: [
|
||||||
|
"Act as a documentation expert for software",
|
||||||
|
"If there is :::note, :::info, :::caution, :::tip, :::danger in the text, extract that from its location and format it correctly",
|
||||||
|
"Return your answer in {DocumentationType} format",
|
||||||
|
"If you notice any secret information, replace it with ***** in your response",
|
||||||
|
],
|
||||||
|
documentation_type: "md",
|
||||||
|
document_file_extension: ".md",
|
||||||
|
folderRootFileName: "README",
|
||||||
|
cache_file: ".code-narrator/cache.json",
|
||||||
|
gptModel: "gpt-4",
|
||||||
|
aiService: undefined,
|
||||||
|
project_name: "reffuse",
|
||||||
|
include: [
|
||||||
|
"src/**/*",
|
||||||
|
],
|
||||||
|
// Array of user defined documentations. See code-narrator How to create a user defined builder
|
||||||
|
builders: [
|
||||||
|
{
|
||||||
|
name: "README",
|
||||||
|
type: "README",
|
||||||
|
template: "README",
|
||||||
|
sidebarPosition: 1,
|
||||||
|
args: {
|
||||||
|
entryFileContent: "content(package.json)",
|
||||||
|
aiService: undefined,
|
||||||
|
},
|
||||||
|
aiService: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HowTo Overview",
|
||||||
|
type: "README",
|
||||||
|
template: "overview_readme",
|
||||||
|
path: "howto",
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "howto/*.md",
|
||||||
|
aiService: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
name: "HowTo Example",
|
||||||
|
type: "howto",
|
||||||
|
template: "howto_create_howto",
|
||||||
|
aiService: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
aiService: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
}
|
||||||
|
module.exports = config;
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@thilawyn/reffuse",
|
"name": "reffuse",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"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": {
|
||||||
".": {
|
".": {
|
||||||
@@ -19,10 +24,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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"peerDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"effect": "^3.13.0",
|
||||||
|
"react": "^19.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
packages/reffuse/src/Reffuse.ts
Normal file
47
packages/reffuse/src/Reffuse.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type * as ReffuseContext from "./ReffuseContext.js"
|
||||||
|
import type * as ReffuseExtension from "./ReffuseExtension.js"
|
||||||
|
import * as ReffuseHelpers from "./ReffuseHelpers.js"
|
||||||
|
import type { Merge, StaticType } from "./types.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class Reffuse extends ReffuseHelpers.make() {}
|
||||||
|
|
||||||
|
|
||||||
|
export const withContexts = <R2 extends Array<unknown>>(
|
||||||
|
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
|
||||||
|
) =>
|
||||||
|
<
|
||||||
|
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R1>,
|
||||||
|
R1
|
||||||
|
>(
|
||||||
|
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R1>
|
||||||
|
): (
|
||||||
|
{
|
||||||
|
new(): Merge<
|
||||||
|
InstanceType<BaseClass>,
|
||||||
|
{ constructor: ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]> }
|
||||||
|
>
|
||||||
|
} &
|
||||||
|
Merge<
|
||||||
|
StaticType<BaseClass>,
|
||||||
|
StaticType<ReffuseHelpers.ReffuseHelpersClass<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 ReffuseHelpers.ReffuseHelpersClass<R>,
|
||||||
|
R
|
||||||
|
>(
|
||||||
|
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R>
|
||||||
|
): (
|
||||||
|
{ new(): Merge<InstanceType<BaseClass>, A> } &
|
||||||
|
StaticType<BaseClass>
|
||||||
|
) => {
|
||||||
|
const class_ = class extends self {}
|
||||||
|
Object.assign(class_.prototype, extension())
|
||||||
|
return class_ as any
|
||||||
|
}
|
||||||
111
packages/reffuse/src/ReffuseContext.tsx
Normal file
111
packages/reffuse/src/ReffuseContext.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { Array, Context, Effect, Layer, Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class ReffuseContext<R> {
|
||||||
|
readonly Context = React.createContext<Context.Context<R>>(null!)
|
||||||
|
readonly Provider = makeProvider(this.Context)
|
||||||
|
readonly AsyncProvider = makeAsyncProvider(this.Context)
|
||||||
|
|
||||||
|
|
||||||
|
useContext(): Context.Context<R> {
|
||||||
|
return React.useContext(this.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayer(): Layer.Layer<R> {
|
||||||
|
const context = this.useContext()
|
||||||
|
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type R<T> = T extends ReffuseContext<infer R> ? R : never
|
||||||
|
|
||||||
|
|
||||||
|
export type ReactProvider<R> = React.FC<{
|
||||||
|
readonly layer: Layer.Layer<R, unknown>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}>
|
||||||
|
|
||||||
|
function makeProvider<R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> {
|
||||||
|
return function ReffuseContextReactProvider(props) {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
|
||||||
|
const value = React.useMemo(() => Effect.context<R>().pipe(
|
||||||
|
Effect.provide(props.layer),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
), [props.layer, runtime])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AsyncReactProvider<R> = React.FC<{
|
||||||
|
readonly layer: Layer.Layer<R, unknown>
|
||||||
|
readonly fallback?: React.ReactNode
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}>
|
||||||
|
|
||||||
|
function makeAsyncProvider<R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> {
|
||||||
|
function Inner({ promise, children }: {
|
||||||
|
readonly promise: Promise<Context.Context<R>>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const value = React.use(promise)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context
|
||||||
|
value={value}
|
||||||
|
children={children}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return function ReffuseContextAsyncReactProvider(props) {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
|
||||||
|
const promise = React.useMemo(() => Effect.context<R>().pipe(
|
||||||
|
Effect.provide(props.layer),
|
||||||
|
Runtime.runPromise(runtime),
|
||||||
|
), [props.layer, runtime])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={props.fallback}>
|
||||||
|
<Inner
|
||||||
|
{...props}
|
||||||
|
promise={promise}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function make<R = never>() {
|
||||||
|
return new ReffuseContext<R>()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMergeAll<T extends Array<unknown>>(
|
||||||
|
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
|
||||||
|
): Context.Context<T[number]> {
|
||||||
|
const values = contexts.map(v => React.use(v.Context))
|
||||||
|
return React.useMemo(() => Context.mergeAll(...values), values)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMergeAllLayers<T extends Array<unknown>>(
|
||||||
|
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
|
||||||
|
): Layer.Layer<T[number]> {
|
||||||
|
const values = contexts.map(v => React.use(v.Context))
|
||||||
|
|
||||||
|
return React.useMemo(() => Array.isNonEmptyArray(values)
|
||||||
|
? Layer.mergeAll(
|
||||||
|
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
|
||||||
|
)
|
||||||
|
: Layer.empty as Layer.Layer<T[number]>,
|
||||||
|
values)
|
||||||
|
}
|
||||||
7
packages/reffuse/src/ReffuseExtension.ts
Normal file
7
packages/reffuse/src/ReffuseExtension.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface ReffuseExtension<A extends object> {
|
||||||
|
(): A
|
||||||
|
readonly Type: A
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <A extends object>(extension: () => A): ReffuseExtension<A> =>
|
||||||
|
extension as ReffuseExtension<A>
|
||||||
446
packages/reffuse/src/ReffuseHelpers.ts
Normal file
446
packages/reffuse/src/ReffuseHelpers.ts
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ReffuseContext from "./ReffuseContext.js"
|
||||||
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
import * as SetStateAction from "./SetStateAction.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 abstract class ReffuseHelpers<R> {
|
||||||
|
declare ["constructor"]: ReffuseHelpersClass<R>
|
||||||
|
|
||||||
|
|
||||||
|
useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> {
|
||||||
|
return ReffuseContext.useMergeAll(...this.constructor.contexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayer<R>(this: ReffuseHelpers<R>): Layer.Layer<R> {
|
||||||
|
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRunSync<R>(this: ReffuseHelpers<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: ReffuseHelpers<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: ReffuseHelpers<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: ReffuseHelpers<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])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: ReffuseHelpers<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,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
useMemoScoped<A, E, R>(
|
||||||
|
this: ReffuseHelpers<R>,
|
||||||
|
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
options?: RenderOptions & ScopeOptions,
|
||||||
|
): A {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
|
// Calculate an initial version of the value so that it can be accessed during the first render
|
||||||
|
const [initialScope, initialValue] = React.useMemo(() => Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||||
|
Effect.flatMap(scope => effect().pipe(
|
||||||
|
Effect.provideService(Scope.Scope, scope),
|
||||||
|
Effect.map(value => [scope, value] as const),
|
||||||
|
)),
|
||||||
|
|
||||||
|
runSync,
|
||||||
|
), [])
|
||||||
|
|
||||||
|
// Keep track of the state of the initial scope
|
||||||
|
const initialScopeClosed = React.useRef(false)
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState(initialValue)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe(
|
||||||
|
Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })),
|
||||||
|
Effect.when(() => !initialScopeClosed.current),
|
||||||
|
)
|
||||||
|
|
||||||
|
const [scope, value] = closeInitialScopeIfNeeded.pipe(
|
||||||
|
Effect.andThen(Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||||
|
Effect.flatMap(scope => effect().pipe(
|
||||||
|
Effect.provideService(Scope.Scope, scope),
|
||||||
|
Effect.map(value => [scope, value] as const),
|
||||||
|
))
|
||||||
|
)),
|
||||||
|
|
||||||
|
runSync,
|
||||||
|
)
|
||||||
|
|
||||||
|
setValue(value)
|
||||||
|
return () => { runSync(Scope.close(scope, Exit.void)) }
|
||||||
|
}, [
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
|
...deps,
|
||||||
|
])
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: ReffuseHelpers<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: ReffuseHelpers<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: ReffuseHelpers<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: ReffuseHelpers<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: ReffuseHelpers<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: ReffuseHelpers<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, R>(
|
||||||
|
this: ReffuseHelpers<R>,
|
||||||
|
value: A,
|
||||||
|
): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
return this.useMemo(
|
||||||
|
() => SubscriptionRef.make(value),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: ReffuseHelpers<R>,
|
||||||
|
ref: SubscriptionRef.SubscriptionRef<A>,
|
||||||
|
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||||
|
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
||||||
|
|
||||||
|
this.useFork(() => Stream.runForEach(ref.changes, 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]
|
||||||
|
}
|
||||||
|
|
||||||
|
useStreamFromValues<const A extends React.DependencyList, R>(
|
||||||
|
this: ReffuseHelpers<R>,
|
||||||
|
values: A,
|
||||||
|
): Stream.Stream<A> {
|
||||||
|
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
|
||||||
|
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
|
||||||
|
), [])
|
||||||
|
|
||||||
|
this.useEffect(() => Queue.offer(queue, values), values)
|
||||||
|
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ReffuseHelpers<R> extends Pipeable.Pipeable {}
|
||||||
|
|
||||||
|
ReffuseHelpers.prototype.pipe = function pipe() {
|
||||||
|
return Pipeable.pipeArguments(this, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable {
|
||||||
|
new(): ReffuseHelpers<R>
|
||||||
|
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
(ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() {
|
||||||
|
return Pipeable.pipeArguments(this, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const make = (): ReffuseHelpersClass<never> =>
|
||||||
|
class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) {
|
||||||
|
static readonly contexts = []
|
||||||
|
}
|
||||||
15
packages/reffuse/src/ReffuseRuntime.tsx
Normal file
15
packages/reffuse/src/ReffuseRuntime.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
|
||||||
|
|
||||||
|
export const Provider = (props: { readonly children?: React.ReactNode }) => (
|
||||||
|
<Context
|
||||||
|
{...props}
|
||||||
|
value={Runtime.defaultRuntime}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
Provider.displayName = "ReffuseRuntimeReactProvider"
|
||||||
|
|
||||||
|
export const useRuntime = () => React.useContext(Context)
|
||||||
12
packages/reffuse/src/SetStateAction.ts
Normal file
12
packages/reffuse/src/SetStateAction.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * as Reffuse from "./Reffuse.js"
|
||||||
|
export * as ReffuseContext from "./ReffuseContext.js"
|
||||||
|
export * as ReffuseExtension from "./ReffuseExtension.js"
|
||||||
|
export * as ReffuseHelpers from "./ReffuseHelpers.js"
|
||||||
|
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
export * as SetStateAction from "./SetStateAction.js"
|
||||||
|
|||||||
21
packages/reffuse/src/types.ts
Normal file
21
packages/reffuse/src/types.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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 Extend<Super, Self> =
|
||||||
|
Extendable<Super, Self> extends true
|
||||||
|
? Omit<Super, CommonKeys<Self, Super>> & Self
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type Extendable<Super, Self> =
|
||||||
|
Pick<Self, CommonKeys<Self, Super>> extends Pick<Super, CommonKeys<Self, Super>>
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
|
||||||
|
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self
|
||||||
11
turbo.json
Normal file
11
turbo.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"tasks": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": ["./src/**"],
|
||||||
|
"outputs": ["./dist/**"]
|
||||||
|
},
|
||||||
|
"pack": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user