Compare commits
373 Commits
618cee4028
...
reffuse-ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a78232ec7 | ||
|
|
19194d6677 | ||
|
|
40871b793d | ||
|
|
f079b90f28 | ||
|
|
28b6e9276e | ||
|
|
8025ec4a22 | ||
|
|
02ee2c10cc | ||
|
|
bb1a71f63b | ||
|
|
a9448f55cf | ||
|
|
c0f3073d20 | ||
|
|
8cfe186574 | ||
|
|
625cecda27 | ||
|
|
7cc0a68170 | ||
|
|
8be1295e2f | ||
|
|
a781be8f24 | ||
|
|
4913f5cc35 | ||
|
|
2a37f843ca | ||
|
|
78a3735038 | ||
|
|
37d9400ada | ||
|
|
2ef47bed70 | ||
|
|
2b78d4dc49 | ||
|
|
6fa73ee33f | ||
|
|
3ea4c81872 | ||
|
|
782629d5b3 | ||
|
|
8b2abbbd19 | ||
|
|
152657d97b | ||
|
|
faf1d4963c | ||
|
|
9ba36ebc04 | ||
|
|
f327728b3a | ||
|
|
8920674b26 | ||
|
|
b440503e50 | ||
|
|
4088d86652 | ||
|
|
79cf1e5eb7 | ||
|
|
8007c2693a | ||
|
|
1769c4074d | ||
|
|
8c5613aa62 | ||
|
|
7d220cb61a | ||
|
|
d81a9fcd91 | ||
|
|
45ce747ff0 | ||
|
|
e089bf9fee | ||
|
|
c2a1a7b212 | ||
|
|
4dc336fbf4 | ||
|
|
1fe2fec325 | ||
|
|
d8b40088cb | ||
|
|
38bf3f99ea | ||
|
|
30b72b5b52 | ||
|
|
3a8a1ed0c3 | ||
|
|
7013bed037 | ||
|
|
0b7a2dbe92 | ||
|
|
0d3e09354e | ||
|
|
dc46d03aab | ||
|
|
37ffc161d3 | ||
|
|
e8a267f4cb | ||
|
|
6dc0a548cd | ||
|
|
53c06e3dae | ||
|
|
82d154ac54 | ||
|
|
ed788af128 | ||
|
|
f4e380ddcb | ||
|
|
e58bd7ab5a | ||
|
|
21d011dd12 | ||
|
|
5b64d0d783 | ||
|
|
2a29f19ece | ||
|
|
919dad97ef | ||
|
|
2fb1a2b897 | ||
|
|
c9263c3d8a | ||
|
|
8444061de3 | ||
|
|
2f870e56cd | ||
|
|
f95c2596a3 | ||
|
|
5d85449fef | ||
|
|
3548ed5718 | ||
|
|
82f0f67ee6 | ||
|
|
8d52443b55 | ||
|
|
27f50db664 | ||
|
|
165b7bbeee | ||
|
|
58b2da373e | ||
|
|
f25fefb0f3 | ||
|
|
6edac19fa6 | ||
|
|
1c99d5c161 | ||
|
|
5044887d90 | ||
|
|
810e4bb9fd | ||
|
|
619dbe32ae | ||
|
|
28efea18f1 | ||
|
|
7e9a8a5fee | ||
|
|
16a7dec3fd | ||
|
|
2c467dc6ec | ||
|
|
ffba43c259 | ||
|
|
45ab641262 | ||
|
|
c7c2f8de62 | ||
|
|
dda868d444 | ||
|
|
b9e787f42b | ||
|
|
1af2a14b52 | ||
|
|
861e462ebd | ||
|
|
b6a127c8a7 | ||
|
|
497e9a34f2 | ||
|
|
9d0daaa87f | ||
|
|
557c4a1b97 | ||
|
|
b395644798 | ||
|
|
099a28ca0d | ||
|
|
27ca5e643a | ||
|
|
64943deaab | ||
|
|
a616e84079 | ||
|
|
c832c3f79a | ||
|
|
6f65574ebd | ||
|
|
9ccabbb627 | ||
|
|
5f4087aa40 | ||
|
|
85b41bda9f | ||
|
|
e5a7fe8ad6 | ||
|
|
59b7115d19 | ||
|
|
08af31f0b9 | ||
|
|
44fc6bbbc4 | ||
|
|
904b725753 | ||
|
|
73dd7bc160 | ||
|
|
6bc07d5b2a | ||
|
|
70e9b9218d | ||
|
|
31b07f842b | ||
|
|
10f23d4cb4 | ||
|
|
39765102db | ||
|
|
04e78e1ea3 | ||
|
|
606dd2c00f | ||
|
|
c13a8d549f | ||
|
|
4b9bfd0637 | ||
|
|
53fc1ef505 | ||
|
|
c8b675d93e | ||
|
|
882ec9591c | ||
|
|
5b3637afd8 | ||
|
|
d6256a7cfd | ||
|
|
cf6c84ff8e | ||
|
|
198a7cee03 | ||
|
|
032f283ac8 | ||
|
|
c34629e20d | ||
|
|
284a080f19 | ||
|
|
87d27dd48d | ||
|
|
24853561f1 | ||
|
|
1902ad373f | ||
|
|
aa6c4a8008 | ||
|
|
d5ac84b2cc | ||
|
|
3c604abcef | ||
|
|
ba99309877 | ||
|
|
db3cd05851 | ||
|
|
dce81be269 | ||
|
|
3980c10747 | ||
|
|
43a3793dbf | ||
|
|
da7044ee9f | ||
|
|
ff5503cfd1 | ||
|
|
dc2cfb35e0 | ||
|
|
bc8c96635c | ||
|
|
1228c51694 | ||
|
|
076007ec67 | ||
|
|
dd524e1aa5 | ||
|
|
1c7cef703b | ||
|
|
fa0f8c6b24 | ||
|
|
357e5aa56b | ||
|
|
ea374d7e0f | ||
|
|
148c98acbd | ||
|
|
39d2176c61 | ||
|
|
107ff1e794 | ||
|
|
a70ef27f75 | ||
|
|
04b2fad038 | ||
|
|
691b28427d | ||
|
|
1de976aaa8 | ||
|
|
df851cf9ee | ||
|
|
459f548c10 | ||
|
|
6156baec4d | ||
|
|
1163b83929 | ||
|
|
8917f84952 | ||
|
|
58752253b3 | ||
|
|
ba362baf04 | ||
|
|
33cf4fbcbd | ||
|
|
e8f92c88b8 | ||
|
|
6ae155de34 | ||
|
|
db783f174e | ||
|
|
2b48695e54 | ||
|
|
0fd3fe49a9 | ||
|
|
ab441fe982 | ||
|
|
eabcf9085b | ||
|
|
926482b154 | ||
|
|
110b0813f8 | ||
|
|
974af95a22 | ||
|
|
d6e1d445e8 | ||
|
|
d8d6e87a12 | ||
|
|
682e473bf7 | ||
|
|
31dd7b5fdb | ||
|
|
17686e68c3 | ||
|
|
49d4bd4d43 | ||
|
|
be88035936 | ||
|
|
3497d17046 | ||
|
|
8008e18221 | ||
|
|
1ca832e69d | ||
|
|
98bd72d1d7 | ||
|
|
f594f47793 | ||
|
|
4f9827720c | ||
|
|
0f761524fd | ||
|
|
574136e161 | ||
|
|
7a12abdbdf | ||
|
|
8fecb94292 | ||
|
|
4092da0f0c | ||
|
|
26a2111705 | ||
|
|
1cb02407c8 | ||
|
|
6e8ce84851 | ||
|
|
570fb93876 | ||
|
|
821fd18f8f | ||
|
|
b7ef95341b | ||
|
|
5f5ef5614b | ||
|
|
cbd39f893e | ||
|
|
529e3d3f9d | ||
|
|
9d47418a69 | ||
|
|
c1b6e73231 | ||
|
|
d1ba4148f2 | ||
|
|
ef13e87d12 | ||
|
|
8b141b907f | ||
|
|
52a36cb882 | ||
|
|
3b844f071b | ||
|
|
d7c648994d | ||
|
|
4e422a1901 | ||
|
|
a5c6b34dfe | ||
|
|
ab1f851428 | ||
|
|
3f091d55c2 | ||
|
|
76a33fccca | ||
|
|
c75bb10e6b | ||
|
|
3da4b2a318 | ||
|
|
9a24ecaf84 | ||
|
|
7b20df6c71 | ||
|
|
74fa30cf4f | ||
|
|
f40dae90fb | ||
|
|
46211638f5 | ||
|
|
a28d6c3d30 | ||
|
|
6b74b9a3b2 | ||
|
|
e17f945666 | ||
|
|
aa46ecc82d | ||
|
|
8ea9146dd9 | ||
|
|
0a4bb2856d | ||
|
|
b4cd7daa81 | ||
|
|
b5712d5433 | ||
|
|
57b7eac05c | ||
|
|
9a9bd78ec6 | ||
|
|
ddcd681ca4 | ||
|
|
66de517ab5 | ||
|
|
b50255ded2 | ||
|
|
03f0b623ed | ||
|
|
fb6d803723 | ||
|
|
972986241c | ||
|
|
9eb0904600 | ||
|
|
fc86c818e0 | ||
|
|
d01152bdcf | ||
|
|
5a12139602 | ||
|
|
a0928c718f | ||
|
|
49d9edd4b1 | ||
|
|
3552c25b5c | ||
|
|
516e0a465d | ||
|
|
7cf5367409 | ||
|
|
3b237c0588 | ||
|
|
d9aa42d23a | ||
|
|
fd3213c53f | ||
|
|
baa8c92221 | ||
|
|
d55b432846 | ||
|
|
6266c7506e | ||
|
|
043e966e45 | ||
|
|
88fab2c7d7 | ||
|
|
224ccd8e32 | ||
|
|
4cf70ada0b | ||
|
|
f9bd5d4d6b | ||
|
|
1ec1db0658 | ||
|
|
2d94e84941 | ||
|
|
aab83907ba | ||
|
|
8c0d6b4c8a | ||
|
|
d82d1d1c29 | ||
|
|
0f09573948 | ||
|
|
2b6b36713e | ||
|
|
5d0aecc9d5 | ||
|
|
f21d8b2d8a | ||
|
|
f85173fa68 | ||
|
|
65a124de1f | ||
|
|
16893761c6 | ||
|
|
3fdc2e31eb | ||
|
|
8636a28f2f | ||
|
|
d56578da8f | ||
|
|
299109d421 | ||
|
|
4995b2949f | ||
|
|
6e6e675709 | ||
|
|
b04860aa25 | ||
|
|
e9e17ac211 | ||
|
|
1f0ff725ff | ||
|
|
447d89982c | ||
|
|
778ee27795 | ||
|
|
077816efb6 | ||
|
|
e4bacd1ca7 | ||
|
|
0e2c0db28f | ||
|
|
c943d81702 | ||
|
|
c2bc406a5f | ||
|
|
4e778b6c95 | ||
|
|
0437fa5dcc | ||
|
|
5614b8df38 | ||
|
|
70b6c4434e | ||
|
|
2e8dfbc988 | ||
|
|
abc47c4647 | ||
|
|
eedd2a7f2a | ||
|
|
f4ab575a8d | ||
|
|
747e2c6056 | ||
|
|
68c68417d8 | ||
|
|
ed384a62a8 | ||
|
|
3a1748bb39 | ||
|
|
66b8fd2c2e | ||
|
|
bc81c443ab | ||
|
|
ee5dbe3766 | ||
|
|
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 |
@@ -12,5 +12,5 @@ jobs:
|
|||||||
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: bun run lint:tsc
|
run: bun run build
|
||||||
|
|||||||
@@ -11,22 +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"
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
- 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: |
|
run: bun run build
|
||||||
cd packages/reffuse
|
- name: Publish reffuse
|
||||||
bun run build
|
uses: JS-DevTools/npm-publish@v3
|
||||||
- name: Publish
|
with:
|
||||||
run: |
|
package: packages/reffuse
|
||||||
cd packages/reffuse
|
access: public
|
||||||
npm publish --access public
|
token: ${{ secrets.NPM_TOKEN }}
|
||||||
env:
|
registry: https://registry.npmjs.org
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
- 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,10 +18,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: bun run build
|
||||||
cd packages/reffuse
|
|
||||||
bun run build
|
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: |
|
run: bun run pack
|
||||||
cd packages/reffuse
|
|
||||||
npm pack --dry-run
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -130,3 +130,4 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
.turbo
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
17
README.md
17
README.md
@@ -1,12 +1,9 @@
|
|||||||
# Reffuse
|
# Reffuse Monorepo
|
||||||
|
|
||||||
[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.
|
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 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.
|
This monorepo contains:
|
||||||
|
- [The `reffuse` library](packages/reffuse)
|
||||||
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
- [`@reffuse/extension-lazyref`, a LazyRef integration for Reffuse](packages/extension-lazyref)
|
||||||
|
- [`@reffuse/extension-query`, TanStack Query style hooks for Reffuse](packages/extension-query)
|
||||||
## Dependencies
|
- [An example project](packges/example)
|
||||||
(needs to be manually installed)
|
|
||||||
- `effect`
|
|
||||||
- `react` 19+
|
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,17 +1,23 @@
|
|||||||
{
|
{
|
||||||
|
"name": "@reffuse/monorepo",
|
||||||
|
"packageManager": "bun@1.2.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./packages/*"
|
"./packages/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint:tsc": "bun run --filter '*' lint:tsc",
|
"build": "turbo build --filter=!@reffuse/example",
|
||||||
|
"lint:tsc": "turbo lint:tsc",
|
||||||
|
"pack": "turbo pack --filter=!@reffuse/example",
|
||||||
|
"publish": "turbo publish --filter=!@reffuse/example",
|
||||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-check-updates": "^17.1.14",
|
"npm-check-updates": "^18.0.1",
|
||||||
"npm-sort": "^0.0.4",
|
"npm-sort": "^0.0.4",
|
||||||
"typescript": "^5.7.3"
|
"turbo": "^2.5.3",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
packages/effect-components/README.md
Normal file
11
packages/effect-components/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+
|
||||||
44
packages/effect-components/package.json
Normal file
44
packages/effect-components/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "effect-components",
|
||||||
|
"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": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"default": "./dist/types/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"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"effect": "^3.15.0",
|
||||||
|
"react": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@effect/language-service": "^0.23.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
72
packages/effect-components/src/ReactComponent.ts
Normal file
72
packages/effect-components/src/ReactComponent.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
|
||||||
|
import type { Mutable } from "effect/Types"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ReactHook from "./ReactHook.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ReactComponent<E, R, P> {
|
||||||
|
(props: P): Effect.Effect<React.ReactNode, E, R>
|
||||||
|
readonly displayName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||||
|
|
||||||
|
export const withDisplayName: {
|
||||||
|
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
|
||||||
|
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
|
||||||
|
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
|
||||||
|
self: C,
|
||||||
|
displayName: string,
|
||||||
|
): C => {
|
||||||
|
(self as Mutable<C>).displayName = displayName
|
||||||
|
return self
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useFC: {
|
||||||
|
<E, R, P extends {} = {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
options?: ReactHook.ScopeOptions,
|
||||||
|
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R, P extends {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
options?: ReactHook.ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
return React.useMemo(() => function ScopeProvider(props: P) {
|
||||||
|
const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
|
||||||
|
|
||||||
|
const FC = React.useMemo(() => {
|
||||||
|
const f = (props: P) => Runtime.runSync(runtime)(
|
||||||
|
Effect.provideService(self(props), Scope.Scope, scope)
|
||||||
|
)
|
||||||
|
if (self.displayName) f.displayName = self.displayName
|
||||||
|
return f
|
||||||
|
}, [scope])
|
||||||
|
|
||||||
|
return React.createElement(FC, props)
|
||||||
|
}, Array.from(
|
||||||
|
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const use: {
|
||||||
|
<E, R, P extends {} = {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
fn: (Component: React.FC<P>) => React.ReactNode,
|
||||||
|
options?: ReactHook.ScopeOptions,
|
||||||
|
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function*(self, fn, options) {
|
||||||
|
return fn(yield* useFC(self, options))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const withRuntime: {
|
||||||
|
<E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
|
||||||
|
<E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
|
||||||
|
} = Function.dual(2, <E, R, P extends {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
|
): React.FC<P> => function WithRuntime(props) {
|
||||||
|
const runtime = React.useContext(context)
|
||||||
|
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
|
||||||
|
})
|
||||||
317
packages/effect-components/src/ReactHook.ts
Normal file
317
packages/effect-components/src/ReactHook.ts
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { SetStateAction } from "./types/index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ScopeOptions {
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useScope: {
|
||||||
|
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
|
||||||
|
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
||||||
|
Effect.all([Ref.make(true), makeScope(options)])
|
||||||
|
), [])
|
||||||
|
const [scope, setScope] = React.useState(initialScope)
|
||||||
|
|
||||||
|
React.useEffect(() => Runtime.runSync(runtime)(
|
||||||
|
Effect.if(isInitialRun, {
|
||||||
|
onTrue: () => Effect.as(
|
||||||
|
Ref.set(isInitialRun, false),
|
||||||
|
() => closeScope(scope, runtime, options),
|
||||||
|
),
|
||||||
|
|
||||||
|
onFalse: () => makeScope(options).pipe(
|
||||||
|
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||||
|
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
), [])
|
||||||
|
|
||||||
|
return scope
|
||||||
|
})
|
||||||
|
|
||||||
|
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
const closeScope = (
|
||||||
|
scope: Scope.CloseableScope,
|
||||||
|
runtime: Runtime.Runtime<never>,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useMemo: {
|
||||||
|
<A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useOnce: {
|
||||||
|
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>
|
||||||
|
) {
|
||||||
|
return yield* useMemo(factory, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useMemoLayer: {
|
||||||
|
<ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>
|
||||||
|
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
||||||
|
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>
|
||||||
|
) {
|
||||||
|
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useCallbackSync: {
|
||||||
|
<Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<(...args: Args) => A, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<R>()
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useCallbackPromise: {
|
||||||
|
<Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<R>()
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useEffect: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const { scope, exit } = Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useLayoutEffect: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const { scope, exit } = Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useFork: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const scope = Runtime.runSync(runtime)(options?.scope
|
||||||
|
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
: Scope.make(options?.finalizerExecutionStrategy)
|
||||||
|
)
|
||||||
|
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "fork") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useRefFromReactiveValue: {
|
||||||
|
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
||||||
|
} = Effect.fnUntraced(function*(value) {
|
||||||
|
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
||||||
|
yield* useEffect(() => Ref.set(ref, value), [value])
|
||||||
|
return ref
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useSubscribeRefs: {
|
||||||
|
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||||
|
...refs: Refs
|
||||||
|
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
||||||
|
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||||
|
...refs: Refs
|
||||||
|
) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
||||||
|
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* useFork(() => pipe(
|
||||||
|
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
||||||
|
streams => Stream.zipLatestAll(...streams),
|
||||||
|
Stream.runForEach(v =>
|
||||||
|
Effect.sync(() => setReactStateValue(v))
|
||||||
|
),
|
||||||
|
), refs)
|
||||||
|
|
||||||
|
return reactStateValue as any
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useRefState: {
|
||||||
|
<A>(
|
||||||
|
ref: SubscriptionRef.SubscriptionRef<A>
|
||||||
|
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
||||||
|
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||||
|
|
||||||
|
yield* useFork(() => Stream.runForEach(
|
||||||
|
Stream.changesWith(ref.changes, (x, y) => x === y),
|
||||||
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
), [ref])
|
||||||
|
|
||||||
|
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||||
|
Ref.update(ref, prevState =>
|
||||||
|
SetStateAction.value(setStateAction, prevState)
|
||||||
|
),
|
||||||
|
[ref])
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useStreamFromReactiveValues: {
|
||||||
|
<const A extends React.DependencyList>(
|
||||||
|
values: A
|
||||||
|
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
||||||
|
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
|
||||||
|
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
||||||
|
Effect.bind("latest", () => Ref.make(values)),
|
||||||
|
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||||
|
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
||||||
|
Effect.flatMap(a => Effect.map(
|
||||||
|
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||||
|
s => Stream.concat(Stream.make(a), s),
|
||||||
|
)),
|
||||||
|
Stream.unwrapScoped,
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* useEffect(() => Ref.set(latest, values).pipe(
|
||||||
|
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||||
|
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||||
|
), values)
|
||||||
|
|
||||||
|
return stream
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useSubscribeStream: {
|
||||||
|
<A, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>
|
||||||
|
): Effect.Effect<Option.Option<A>, never, R>
|
||||||
|
<A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue: A,
|
||||||
|
): Effect.Effect<Option.Some<A>, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue?: A,
|
||||||
|
) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(
|
||||||
|
React.useMemo(() => initialValue
|
||||||
|
? Option.some(initialValue)
|
||||||
|
: Option.none(),
|
||||||
|
[])
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* useFork(() => Stream.runForEach(
|
||||||
|
Stream.changesWith(stream, (x, y) => x === y),
|
||||||
|
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||||
|
), [stream])
|
||||||
|
|
||||||
|
return reactStateValue as Option.Some<A>
|
||||||
|
})
|
||||||
47
packages/effect-components/src/ReactManagedRuntime.ts
Normal file
47
packages/effect-components/src/ReactManagedRuntime.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ReactManagedRuntime<R, ER> {
|
||||||
|
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||||
|
readonly context: React.Context<Runtime.Runtime<R>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <R, ER>(
|
||||||
|
layer: Layer.Layer<R, ER>,
|
||||||
|
memoMap?: Layer.MemoMap,
|
||||||
|
): ReactManagedRuntime<R, ER> => ({
|
||||||
|
runtime: ManagedRuntime.make(layer, memoMap),
|
||||||
|
context: React.createContext<Runtime.Runtime<R>>(null!),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
|
||||||
|
readonly runtime: ReactManagedRuntime<R, ER>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncProvider<R, ER>(
|
||||||
|
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
|
||||||
|
): React.ReactNode {
|
||||||
|
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
React.Suspense,
|
||||||
|
suspenseProps,
|
||||||
|
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsyncProviderInnerProps<R, ER> {
|
||||||
|
readonly runtime: ReactManagedRuntime<R, ER>
|
||||||
|
readonly promise: Promise<Runtime.Runtime<R>>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function AsyncProviderInner<R, ER>(
|
||||||
|
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
|
||||||
|
): React.ReactNode {
|
||||||
|
const value = React.use(promise)
|
||||||
|
return React.createElement(runtime.context, { value }, children)
|
||||||
|
}
|
||||||
3
packages/effect-components/src/index.ts
Normal file
3
packages/effect-components/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * as ReactComponent from "./ReactComponent.js"
|
||||||
|
export * as ReactHook from "./ReactHook.js"
|
||||||
|
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
||||||
99
packages/effect-components/src/types/PropertyPath.ts
Normal file
99
packages/effect-components/src/types/PropertyPath.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Array, Function, Option, Predicate } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
|
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
||||||
|
D extends never ? [] :
|
||||||
|
T extends Seen ? [] :
|
||||||
|
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
||||||
|
T extends object ? ObjectPaths<T, D, Seen | T> :
|
||||||
|
never
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
||||||
|
[K in keyof T as K extends number ? K : never]:
|
||||||
|
| [K]
|
||||||
|
| [K, ...Paths<T[K], Prev[D], Seen>]
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
||||||
|
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||||
|
NonNullable<T[K]> extends infer V
|
||||||
|
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
||||||
|
: never
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
||||||
|
? Head extends keyof T
|
||||||
|
? ValueFromPath<T[Head], Tail>
|
||||||
|
: T extends readonly any[]
|
||||||
|
? Head extends number
|
||||||
|
? ValueFromPath<T[number], Tail>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: T
|
||||||
|
|
||||||
|
export type AnyKey = string | number | symbol
|
||||||
|
export type AnyPath = readonly AnyKey[]
|
||||||
|
|
||||||
|
|
||||||
|
export const unsafeGet: {
|
||||||
|
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
||||||
|
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
||||||
|
path.reduce((acc: any, key: any) => acc?.[key], self)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const get: {
|
||||||
|
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
||||||
|
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
||||||
|
path.reduce(
|
||||||
|
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
||||||
|
? Predicate.hasProperty(acc.value, key)
|
||||||
|
? Option.some(acc.value[key])
|
||||||
|
: Option.none()
|
||||||
|
: acc,
|
||||||
|
|
||||||
|
Option.some(self),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const immutableSet: {
|
||||||
|
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
||||||
|
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
||||||
|
const key = Array.head(path as AnyPath)
|
||||||
|
if (Option.isNone(key))
|
||||||
|
return Option.some(value as T)
|
||||||
|
if (!Predicate.hasProperty(self, key.value))
|
||||||
|
return Option.none()
|
||||||
|
|
||||||
|
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
||||||
|
if (Option.isNone(child))
|
||||||
|
return child
|
||||||
|
|
||||||
|
if (Array.isArray(self))
|
||||||
|
return typeof key.value === "number"
|
||||||
|
? Option.some([
|
||||||
|
...self.slice(0, key.value),
|
||||||
|
child.value,
|
||||||
|
...self.slice(key.value + 1),
|
||||||
|
] as T)
|
||||||
|
: Option.none()
|
||||||
|
|
||||||
|
if (typeof self === "object")
|
||||||
|
return Option.some(
|
||||||
|
Object.assign(
|
||||||
|
Object.create(Object.getPrototypeOf(self)),
|
||||||
|
{ ...self, [key.value]: child.value },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Option.none()
|
||||||
|
})
|
||||||
100
packages/effect-components/src/types/SubscriptionSubRef.ts
Normal file
100
packages/effect-components/src/types/SubscriptionSubRef.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||||
|
import * as PropertyPath from "./PropertyPath.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
||||||
|
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||||
|
|
||||||
|
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
||||||
|
|
||||||
|
readonly [Unify.typeSymbol]?: unknown
|
||||||
|
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
||||||
|
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace SubscriptionSubRef {
|
||||||
|
export interface Variance<in out A, in out B> {
|
||||||
|
readonly [SubscriptionSubRefTypeId]: {
|
||||||
|
readonly _A: Types.Invariant<A>
|
||||||
|
readonly _B: Types.Invariant<B>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
||||||
|
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
||||||
|
SubscriptionRef?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const refVariance = { _A: (_: any) => _ }
|
||||||
|
const synchronizedRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
||||||
|
|
||||||
|
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||||
|
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||||
|
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||||
|
readonly [Ref.RefTypeId] = refVariance
|
||||||
|
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
||||||
|
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
||||||
|
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
||||||
|
|
||||||
|
readonly get: Effect.Effect<A>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
readonly getter: (parentValue: B) => A,
|
||||||
|
readonly setter: (parentValue: B, value: A) => B,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
||||||
|
}
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
return this.get
|
||||||
|
}
|
||||||
|
|
||||||
|
get changes(): Stream.Stream<A> {
|
||||||
|
return this.get.pipe(
|
||||||
|
Effect.map(a => this.parent.changes.pipe(
|
||||||
|
Stream.map(this.getter),
|
||||||
|
s => Stream.concat(Stream.make(a), s),
|
||||||
|
)),
|
||||||
|
Stream.unwrap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
||||||
|
return this.modifyEffect(a => Effect.succeed(f(a)))
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
||||||
|
return Effect.Do.pipe(
|
||||||
|
Effect.bind("b", () => Ref.get(this.parent)),
|
||||||
|
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||||
|
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
||||||
|
Effect.map(({ ca: [c] }) => c),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const makeFromGetSet = <A, B>(
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
getter: (parentValue: B) => A,
|
||||||
|
setter: (parentValue: B, value: A) => B,
|
||||||
|
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
||||||
|
|
||||||
|
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
path: P,
|
||||||
|
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
||||||
|
parent,
|
||||||
|
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||||
|
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
||||||
|
)
|
||||||
3
packages/effect-components/src/types/index.ts
Normal file
3
packages/effect-components/src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * as PropertyPath from "./PropertyPath.js"
|
||||||
|
export * as SetStateAction from "./SetStateAction.js"
|
||||||
|
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||||
33
packages/effect-components/tsconfig.json
Normal file
33
packages/effect-components/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"]
|
||||||
|
}
|
||||||
@@ -11,36 +11,42 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@tanstack/react-router": "^1.111.7",
|
"@tanstack/react-router": "^1.120.3",
|
||||||
"@tanstack/router-devtools": "^1.111.7",
|
"@tanstack/react-router-devtools": "^1.120.3",
|
||||||
"@tanstack/router-plugin": "^1.111.7",
|
"@tanstack/router-plugin": "^1.120.3",
|
||||||
"@thilawyn/thilaschema": "^0.1.4",
|
"@thilawyn/thilaschema": "^0.1.4",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.1.4",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.1.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.1.0",
|
||||||
"typescript-eslint": "^8.25.0",
|
"typescript-eslint": "^8.32.1",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform": "^0.77.2",
|
"@effect/platform": "^0.82.1",
|
||||||
"@effect/platform-browser": "^0.56.2",
|
"@effect/platform-browser": "^0.62.1",
|
||||||
"@radix-ui/themes": "^3.2.0",
|
"@radix-ui/themes": "^3.2.1",
|
||||||
"@reffuse/extension-lazyref": "workspace:*",
|
"@reffuse/extension-lazyref": "workspace:*",
|
||||||
"@typed/id": "^0.17.1",
|
"@reffuse/extension-query": "workspace:*",
|
||||||
|
"@typed/async-data": "^0.13.1",
|
||||||
|
"@typed/id": "^0.17.2",
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
"@typed/lazy-ref": "^0.3.3",
|
||||||
"effect": "^3.13.2",
|
"effect": "^3.15.1",
|
||||||
"lucide-react": "^0.476.0",
|
"lucide-react": "^0.510.0",
|
||||||
"mobx": "^6.13.6",
|
"mobx": "^6.13.7",
|
||||||
"reffuse": "workspace:*"
|
"reffuse": "workspace:*"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"effect": "^3.13.2"
|
"effect": "^3.15.1",
|
||||||
|
"@effect/platform": "^0.82.1",
|
||||||
|
"@effect/platform-browser": "^0.62.1",
|
||||||
|
"@typed/lazy-ref": "^0.3.3",
|
||||||
|
"@typed/async-data": "^0.13.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
packages/example/src/VQueryErrorHandler.tsx
Normal file
57
packages/example/src/VQueryErrorHandler.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||||
|
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { R } from "./reffuse"
|
||||||
|
import { AppQueryErrorHandler } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export function VQueryErrorHandler() {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const error = R.useSubscribeStream(
|
||||||
|
R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
|
||||||
|
Effect.map(handler => handler.errors.pipe(
|
||||||
|
Stream.changes,
|
||||||
|
Stream.tap(Console.error),
|
||||||
|
Stream.tap(() => Effect.sync(() => setOpen(true))),
|
||||||
|
))
|
||||||
|
), [])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Option.isNone(error))
|
||||||
|
return <></>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog.Root open={open}>
|
||||||
|
<AlertDialog.Content maxWidth="450px">
|
||||||
|
<AlertDialog.Title>Error</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description size="2">
|
||||||
|
{Either.match(Cause.failureOrCause(error.value), {
|
||||||
|
onLeft: flow(
|
||||||
|
Match.value,
|
||||||
|
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
|
||||||
|
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
|
||||||
|
Match.exhaustive,
|
||||||
|
),
|
||||||
|
|
||||||
|
onRight: flow(
|
||||||
|
Cause.dieOption,
|
||||||
|
Option.match({
|
||||||
|
onSome: () => <Text>Unrecoverable defect</Text>,
|
||||||
|
onNone: () => <Text>Unknown error</Text>,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</AlertDialog.Description>
|
||||||
|
|
||||||
|
<Flex gap="3" mt="4" justify="end">
|
||||||
|
<AlertDialog.Action>
|
||||||
|
<Button variant="solid" color="red" onClick={() => setOpen(false)}>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</Flex>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ThSchema } from "@thilawyn/thilaschema"
|
import { ThSchema } from "@thilawyn/thilaschema"
|
||||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
import { Schema } from "effect"
|
||||||
import { Effect, Schema } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export class Todo extends Schema.Class<Todo>("Todo")({
|
export class Todo extends Schema.Class<Todo>("Todo")({
|
||||||
@@ -18,9 +17,4 @@ export const TodoFromJsonStruct = Schema.Struct({
|
|||||||
ThSchema.assertEncodedJsonifiable
|
ThSchema.assertEncodedJsonifiable
|
||||||
)
|
)
|
||||||
|
|
||||||
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
|
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
|
||||||
|
|
||||||
|
|
||||||
export const generateUniqueID = makeUuid4.pipe(
|
|
||||||
Effect.provide(GetRandomValues.CryptoRandom)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import { Layer } from "effect"
|
|||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { ReffuseRuntime } from "reffuse"
|
import { ReffuseRuntime } from "reffuse"
|
||||||
import { GlobalContext } from "./reffuse"
|
import { RootContext } from "./reffuse"
|
||||||
import { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||||
|
|
||||||
|
|
||||||
const layer = Layer.empty.pipe(
|
const layer = Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
|
||||||
|
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
|
||||||
Layer.provideMerge(Clipboard.layer),
|
Layer.provideMerge(Clipboard.layer),
|
||||||
Layer.provideMerge(Geolocation.layer),
|
Layer.provideMerge(Geolocation.layer),
|
||||||
Layer.provideMerge(Permissions.layer),
|
Layer.provideMerge(Permissions.layer),
|
||||||
@@ -28,9 +31,9 @@ declare module "@tanstack/react-router" {
|
|||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ReffuseRuntime.Provider>
|
<ReffuseRuntime.Provider>
|
||||||
<GlobalContext.Provider layer={layer}>
|
<RootContext.Provider layer={layer}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</GlobalContext.Provider>
|
</RootContext.Provider>
|
||||||
</ReffuseRuntime.Provider>
|
</ReffuseRuntime.Provider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
10
packages/example/src/query/reffuse.ts
Normal file
10
packages/example/src/query/reffuse.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { RootReffuse } from "@/reffuse"
|
||||||
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
|
import { Uuid4Query } from "./services"
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>()
|
||||||
|
|
||||||
|
export const R = new class QueryReffuse extends RootReffuse.pipe(
|
||||||
|
Reffuse.withContexts(QueryContext)
|
||||||
|
) {}
|
||||||
11
packages/example/src/query/services/Uuid4Query.ts
Normal file
11
packages/example/src/query/services/Uuid4Query.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { QueryRunner } from "@reffuse/extension-query"
|
||||||
|
import { ParseResult, Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export const Result = Schema.Array(Schema.String)
|
||||||
|
|
||||||
|
export class Uuid4Query extends QueryRunner.Tag("Uuid4Query")<Uuid4Query,
|
||||||
|
readonly ["uuid4", number],
|
||||||
|
typeof Result.Type,
|
||||||
|
ParseResult.ParseError
|
||||||
|
>() {}
|
||||||
1
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 runFork = R.useRunFork()
|
||||||
|
|
||||||
|
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||||
|
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Flex direction="column" align="center" gap="2">
|
||||||
|
<Text>
|
||||||
|
{AsyncData.match(state, {
|
||||||
|
NoData: () => "No data yet",
|
||||||
|
Loading: () => "Loading...",
|
||||||
|
Success: (value, { isRefreshing, isOptimistic }) =>
|
||||||
|
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
|
||||||
|
Failure: (cause, { isRefreshing }) =>
|
||||||
|
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
import { HttpClient } from "@effect/platform"
|
import { HttpClient } from "@effect/platform"
|
||||||
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
||||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||||
|
import { QueryExtension } from "@reffuse/extension-query"
|
||||||
import { Reffuse, ReffuseContext } from "reffuse"
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
|
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||||
|
|
||||||
|
|
||||||
export const GlobalContext = ReffuseContext.make<
|
export const RootContext = ReffuseContext.make<
|
||||||
|
| AppQueryClient.AppQueryClient
|
||||||
|
| AppQueryErrorHandler.AppQueryErrorHandler
|
||||||
| Clipboard.Clipboard
|
| Clipboard.Clipboard
|
||||||
| Geolocation.Geolocation
|
| Geolocation.Geolocation
|
||||||
| Permissions.Permissions
|
| Permissions.Permissions
|
||||||
| HttpClient.HttpClient
|
| HttpClient.HttpClient
|
||||||
>()
|
>()
|
||||||
|
|
||||||
export class GlobalReffuse extends Reffuse.Reffuse.pipe(
|
export class RootReffuse extends Reffuse.Reffuse.pipe(
|
||||||
Reffuse.withExtension(LazyRefExtension),
|
Reffuse.withExtension(LazyRefExtension),
|
||||||
Reffuse.withContexts(GlobalContext),
|
Reffuse.withExtension(QueryExtension),
|
||||||
|
Reffuse.withContexts(RootContext),
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
export const R = new GlobalReffuse()
|
export const R = new RootReffuse()
|
||||||
|
|||||||
@@ -11,16 +11,28 @@
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
|
import { Route as TodosImport } from './routes/todos'
|
||||||
import { Route as TimeImport } from './routes/time'
|
import { Route as TimeImport } from './routes/time'
|
||||||
import { Route as TestsImport } from './routes/tests'
|
import { Route as TestsImport } from './routes/tests'
|
||||||
import { Route as PromiseImport } from './routes/promise'
|
import { Route as PromiseImport } from './routes/promise'
|
||||||
import { Route as LazyrefImport } from './routes/lazyref'
|
import { Route as LazyrefImport } from './routes/lazyref'
|
||||||
|
import { Route as EffectComponentTestsImport } from './routes/effect-component-tests'
|
||||||
import { Route as CountImport } from './routes/count'
|
import { Route as CountImport } from './routes/count'
|
||||||
import { Route as BlankImport } from './routes/blank'
|
import { Route as BlankImport } from './routes/blank'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
|
import { Route as StreamsPullImport } from './routes/streams/pull'
|
||||||
|
import { Route as QueryUsequeryImport } from './routes/query/usequery'
|
||||||
|
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
|
||||||
|
import { Route as QueryServiceImport } from './routes/query/service'
|
||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const TodosRoute = TodosImport.update({
|
||||||
|
id: '/todos',
|
||||||
|
path: '/todos',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const TimeRoute = TimeImport.update({
|
const TimeRoute = TimeImport.update({
|
||||||
id: '/time',
|
id: '/time',
|
||||||
path: '/time',
|
path: '/time',
|
||||||
@@ -45,6 +57,12 @@ const LazyrefRoute = LazyrefImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const EffectComponentTestsRoute = EffectComponentTestsImport.update({
|
||||||
|
id: '/effect-component-tests',
|
||||||
|
path: '/effect-component-tests',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const CountRoute = CountImport.update({
|
const CountRoute = CountImport.update({
|
||||||
id: '/count',
|
id: '/count',
|
||||||
path: '/count',
|
path: '/count',
|
||||||
@@ -63,6 +81,30 @@ const IndexRoute = IndexImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const StreamsPullRoute = StreamsPullImport.update({
|
||||||
|
id: '/streams/pull',
|
||||||
|
path: '/streams/pull',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const QueryUsequeryRoute = QueryUsequeryImport.update({
|
||||||
|
id: '/query/usequery',
|
||||||
|
path: '/query/usequery',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const QueryUsemutationRoute = QueryUsemutationImport.update({
|
||||||
|
id: '/query/usemutation',
|
||||||
|
path: '/query/usemutation',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const QueryServiceRoute = QueryServiceImport.update({
|
||||||
|
id: '/query/service',
|
||||||
|
path: '/query/service',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
// Populate the FileRoutesByPath interface
|
// Populate the FileRoutesByPath interface
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -88,6 +130,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof CountImport
|
preLoaderRoute: typeof CountImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/effect-component-tests': {
|
||||||
|
id: '/effect-component-tests'
|
||||||
|
path: '/effect-component-tests'
|
||||||
|
fullPath: '/effect-component-tests'
|
||||||
|
preLoaderRoute: typeof EffectComponentTestsImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/lazyref': {
|
'/lazyref': {
|
||||||
id: '/lazyref'
|
id: '/lazyref'
|
||||||
path: '/lazyref'
|
path: '/lazyref'
|
||||||
@@ -116,6 +165,41 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof TimeImport
|
preLoaderRoute: typeof TimeImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/todos': {
|
||||||
|
id: '/todos'
|
||||||
|
path: '/todos'
|
||||||
|
fullPath: '/todos'
|
||||||
|
preLoaderRoute: typeof TodosImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/query/service': {
|
||||||
|
id: '/query/service'
|
||||||
|
path: '/query/service'
|
||||||
|
fullPath: '/query/service'
|
||||||
|
preLoaderRoute: typeof QueryServiceImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/query/usemutation': {
|
||||||
|
id: '/query/usemutation'
|
||||||
|
path: '/query/usemutation'
|
||||||
|
fullPath: '/query/usemutation'
|
||||||
|
preLoaderRoute: typeof QueryUsemutationImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/query/usequery': {
|
||||||
|
id: '/query/usequery'
|
||||||
|
path: '/query/usequery'
|
||||||
|
fullPath: '/query/usequery'
|
||||||
|
preLoaderRoute: typeof QueryUsequeryImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
|
'/streams/pull': {
|
||||||
|
id: '/streams/pull'
|
||||||
|
path: '/streams/pull'
|
||||||
|
fullPath: '/streams/pull'
|
||||||
|
preLoaderRoute: typeof StreamsPullImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,20 +209,32 @@ export interface FileRoutesByFullPath {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
|
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
|
'/todos': typeof TodosRoute
|
||||||
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
|
'/streams/pull': typeof StreamsPullRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
|
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
|
'/todos': typeof TodosRoute
|
||||||
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
|
'/streams/pull': typeof StreamsPullRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -146,10 +242,16 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
|
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
'/time': typeof TimeRoute
|
'/time': typeof TimeRoute
|
||||||
|
'/todos': typeof TodosRoute
|
||||||
|
'/query/service': typeof QueryServiceRoute
|
||||||
|
'/query/usemutation': typeof QueryUsemutationRoute
|
||||||
|
'/query/usequery': typeof QueryUsequeryRoute
|
||||||
|
'/streams/pull': typeof StreamsPullRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@@ -158,21 +260,46 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
|
| '/effect-component-tests'
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
| '/time'
|
| '/time'
|
||||||
|
| '/todos'
|
||||||
|
| '/query/service'
|
||||||
|
| '/query/usemutation'
|
||||||
|
| '/query/usequery'
|
||||||
|
| '/streams/pull'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/blank' | '/count' | '/lazyref' | '/promise' | '/tests' | '/time'
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/blank'
|
||||||
|
| '/count'
|
||||||
|
| '/effect-component-tests'
|
||||||
|
| '/lazyref'
|
||||||
|
| '/promise'
|
||||||
|
| '/tests'
|
||||||
|
| '/time'
|
||||||
|
| '/todos'
|
||||||
|
| '/query/service'
|
||||||
|
| '/query/usemutation'
|
||||||
|
| '/query/usequery'
|
||||||
|
| '/streams/pull'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
|
| '/effect-component-tests'
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
| '/time'
|
| '/time'
|
||||||
|
| '/todos'
|
||||||
|
| '/query/service'
|
||||||
|
| '/query/usemutation'
|
||||||
|
| '/query/usequery'
|
||||||
|
| '/streams/pull'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,20 +307,32 @@ export interface RootRouteChildren {
|
|||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BlankRoute: typeof BlankRoute
|
BlankRoute: typeof BlankRoute
|
||||||
CountRoute: typeof CountRoute
|
CountRoute: typeof CountRoute
|
||||||
|
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
|
||||||
LazyrefRoute: typeof LazyrefRoute
|
LazyrefRoute: typeof LazyrefRoute
|
||||||
PromiseRoute: typeof PromiseRoute
|
PromiseRoute: typeof PromiseRoute
|
||||||
TestsRoute: typeof TestsRoute
|
TestsRoute: typeof TestsRoute
|
||||||
TimeRoute: typeof TimeRoute
|
TimeRoute: typeof TimeRoute
|
||||||
|
TodosRoute: typeof TodosRoute
|
||||||
|
QueryServiceRoute: typeof QueryServiceRoute
|
||||||
|
QueryUsemutationRoute: typeof QueryUsemutationRoute
|
||||||
|
QueryUsequeryRoute: typeof QueryUsequeryRoute
|
||||||
|
StreamsPullRoute: typeof StreamsPullRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BlankRoute: BlankRoute,
|
BlankRoute: BlankRoute,
|
||||||
CountRoute: CountRoute,
|
CountRoute: CountRoute,
|
||||||
|
EffectComponentTestsRoute: EffectComponentTestsRoute,
|
||||||
LazyrefRoute: LazyrefRoute,
|
LazyrefRoute: LazyrefRoute,
|
||||||
PromiseRoute: PromiseRoute,
|
PromiseRoute: PromiseRoute,
|
||||||
TestsRoute: TestsRoute,
|
TestsRoute: TestsRoute,
|
||||||
TimeRoute: TimeRoute,
|
TimeRoute: TimeRoute,
|
||||||
|
TodosRoute: TodosRoute,
|
||||||
|
QueryServiceRoute: QueryServiceRoute,
|
||||||
|
QueryUsemutationRoute: QueryUsemutationRoute,
|
||||||
|
QueryUsequeryRoute: QueryUsequeryRoute,
|
||||||
|
StreamsPullRoute: StreamsPullRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const routeTree = rootRoute
|
export const routeTree = rootRoute
|
||||||
@@ -209,10 +348,16 @@ export const routeTree = rootRoute
|
|||||||
"/",
|
"/",
|
||||||
"/blank",
|
"/blank",
|
||||||
"/count",
|
"/count",
|
||||||
|
"/effect-component-tests",
|
||||||
"/lazyref",
|
"/lazyref",
|
||||||
"/promise",
|
"/promise",
|
||||||
"/tests",
|
"/tests",
|
||||||
"/time"
|
"/time",
|
||||||
|
"/todos",
|
||||||
|
"/query/service",
|
||||||
|
"/query/usemutation",
|
||||||
|
"/query/usequery",
|
||||||
|
"/streams/pull"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"/": {
|
"/": {
|
||||||
@@ -224,6 +369,9 @@ export const routeTree = rootRoute
|
|||||||
"/count": {
|
"/count": {
|
||||||
"filePath": "count.tsx"
|
"filePath": "count.tsx"
|
||||||
},
|
},
|
||||||
|
"/effect-component-tests": {
|
||||||
|
"filePath": "effect-component-tests.tsx"
|
||||||
|
},
|
||||||
"/lazyref": {
|
"/lazyref": {
|
||||||
"filePath": "lazyref.tsx"
|
"filePath": "lazyref.tsx"
|
||||||
},
|
},
|
||||||
@@ -235,6 +383,21 @@ export const routeTree = rootRoute
|
|||||||
},
|
},
|
||||||
"/time": {
|
"/time": {
|
||||||
"filePath": "time.tsx"
|
"filePath": "time.tsx"
|
||||||
|
},
|
||||||
|
"/todos": {
|
||||||
|
"filePath": "todos.tsx"
|
||||||
|
},
|
||||||
|
"/query/service": {
|
||||||
|
"filePath": "query/service.tsx"
|
||||||
|
},
|
||||||
|
"/query/usemutation": {
|
||||||
|
"filePath": "query/usemutation.tsx"
|
||||||
|
},
|
||||||
|
"/query/usequery": {
|
||||||
|
"filePath": "query/usequery.tsx"
|
||||||
|
},
|
||||||
|
"/streams/pull": {
|
||||||
|
"filePath": "streams/pull.tsx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { VQueryErrorHandler } from "@/VQueryErrorHandler"
|
||||||
import { Container, Flex, Theme } from "@radix-ui/themes"
|
import { Container, Flex, Theme } from "@radix-ui/themes"
|
||||||
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
|
||||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
|
||||||
|
|
||||||
import "@radix-ui/themes/styles.css"
|
import "@radix-ui/themes/styles.css"
|
||||||
import "../index.css"
|
import "../index.css"
|
||||||
@@ -20,11 +21,14 @@ function Root() {
|
|||||||
<Link to="/count">Count</Link>
|
<Link to="/count">Count</Link>
|
||||||
<Link to="/tests">Tests</Link>
|
<Link to="/tests">Tests</Link>
|
||||||
<Link to="/promise">Promise</Link>
|
<Link to="/promise">Promise</Link>
|
||||||
|
<Link to="/query/usequery">Query</Link>
|
||||||
<Link to="/blank">Blank</Link>
|
<Link to="/blank">Blank</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
||||||
|
<VQueryErrorHandler />
|
||||||
<TanStackRouterDevtools />
|
<TanStackRouterDevtools />
|
||||||
</Theme>
|
</Theme>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { R } from "@/reffuse"
|
import { R } from "@/reffuse"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Ref } from "effect"
|
import { Effect, Ref } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/count")({
|
export const Route = createFileRoute("/count")({
|
||||||
@@ -11,14 +11,13 @@ function Count() {
|
|||||||
|
|
||||||
const runSync = R.useRunSync()
|
const runSync = R.useRunSync()
|
||||||
|
|
||||||
const countRef = R.useRef(0)
|
const countRef = R.useRef(() => Effect.succeed(0))
|
||||||
const [count] = R.useRefState(countRef)
|
const [count] = R.useSubscribeRefs(countRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
{/* <button onClick={() => setCount((count) => count + 1)}> */}
|
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
|
||||||
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
|
|
||||||
count is {count}
|
count is {count}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
105
packages/example/src/routes/effect-component-tests.tsx
Normal file
105
packages/example/src/routes/effect-component-tests.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { Box, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
|
||||||
|
import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
|
||||||
|
|
||||||
|
|
||||||
|
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
|
||||||
|
Console.log("Runtime built."),
|
||||||
|
() => Console.log("Runtime destroyed."),
|
||||||
|
))
|
||||||
|
|
||||||
|
class TestService extends Effect.Service<TestService>()("TestService", {
|
||||||
|
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class SubService extends Effect.Service<SubService>()("SubService", {
|
||||||
|
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(LogLive),
|
||||||
|
Layer.provideMerge(TestService.Default),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/effect-component-tests")({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
|
||||||
|
<MyRoute />
|
||||||
|
</ReactManagedRuntime.AsyncProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyRoute = pipe(
|
||||||
|
Effect.fn(function*() {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const service = yield* TestService
|
||||||
|
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||||
|
|
||||||
|
// const MyTestComponentFC = yield* Effect.provide(
|
||||||
|
// ReactComponent.useFC(MyTestComponent),
|
||||||
|
// yield* ReactHook.useMemoLayer(SubService.Default),
|
||||||
|
// )
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Box>
|
||||||
|
<TextField.Root
|
||||||
|
value={value}
|
||||||
|
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
|
||||||
|
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{/* {Array.range(0, 3).map(k =>
|
||||||
|
<MyTestComponentFC key={k} />
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{yield* pipe(
|
||||||
|
Array.range(0, 3),
|
||||||
|
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
|
||||||
|
<FC key={k} />
|
||||||
|
)),
|
||||||
|
Effect.all,
|
||||||
|
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}),
|
||||||
|
|
||||||
|
ReactComponent.withDisplayName("MyRoute"),
|
||||||
|
ReactComponent.withRuntime(runtime.context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const MyTestComponent = pipe(
|
||||||
|
Effect.fn(function*() {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const service = yield* SubService
|
||||||
|
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||||
|
|
||||||
|
// yield* ReactHook.useMemo(() => Effect.andThen(
|
||||||
|
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
|
||||||
|
// Console.log("MyTestComponent mounted"),
|
||||||
|
// ), [])
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Box>
|
||||||
|
<TextField.Root
|
||||||
|
value={value}
|
||||||
|
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
}),
|
||||||
|
|
||||||
|
ReactComponent.withDisplayName("MyTestComponent"),
|
||||||
|
)
|
||||||
@@ -1,29 +1,10 @@
|
|||||||
import { TodosContext } from "@/todos/reffuse"
|
|
||||||
import { TodosState } from "@/todos/services"
|
|
||||||
import { VTodos } from "@/todos/views/VTodos"
|
|
||||||
import { Container } from "@radix-ui/themes"
|
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Layer } from "effect"
|
|
||||||
import { useMemo } from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute('/')({
|
||||||
component: Index
|
component: RouteComponent
|
||||||
})
|
})
|
||||||
|
|
||||||
function Index() {
|
function RouteComponent() {
|
||||||
|
return <div>Hello "/"!</div>
|
||||||
const todosLayer = useMemo(() => Layer.empty.pipe(
|
|
||||||
Layer.provideMerge(TodosState.make("todos"))
|
|
||||||
), [])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<TodosContext.Provider layer={todosLayer}>
|
|
||||||
<VTodos />
|
|
||||||
</TodosContext.Provider>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const Route = createFileRoute("/lazyref")({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const promise = R.usePromise(() => LazyRef.of(0))
|
const promise = R.usePromise(() => LazyRef.of(0), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Text>Loading...</Text>}>
|
<Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function RouteComponent() {
|
|||||||
HttpClient.withTracerPropagation(false),
|
HttpClient.withTracerPropagation(false),
|
||||||
Effect.flatMap(res => res.json),
|
Effect.flatMap(res => res.json),
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
))
|
), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Text>Loading...</Text>}>
|
<Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
|||||||
38
packages/example/src/routes/query/service.tsx
Normal file
38
packages/example/src/routes/query/service.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { QueryContext } from "@/query/reffuse"
|
||||||
|
import { Uuid4Query } from "@/query/services"
|
||||||
|
import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
|
||||||
|
import { R } from "@/reffuse"
|
||||||
|
import { HttpClient } from "@effect/platform"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, Effect, Layer, Schema } from "effect"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/query/service")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const query = R.useQuery({
|
||||||
|
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
||||||
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(Effect.map(
|
||||||
|
HttpClient.HttpClient,
|
||||||
|
HttpClient.withTracerPropagation(false),
|
||||||
|
)),
|
||||||
|
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
|
Effect.flatMap(res => res.json),
|
||||||
|
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
||||||
|
Effect.scoped,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryContext.Provider layer={layer}>
|
||||||
|
<Uuid4QueryService />
|
||||||
|
</QueryContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
84
packages/example/src/routes/query/usemutation.tsx
Normal file
84
packages/example/src/routes/query/usemutation.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { R } from "@/reffuse"
|
||||||
|
import { HttpClient } from "@effect/platform"
|
||||||
|
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
||||||
|
import { QueryProgress } from "@reffuse/extension-query"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/query/usemutation")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const Result = Schema.Array(Schema.String)
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const runFork = R.useRunFork()
|
||||||
|
|
||||||
|
const [count, setCount] = useState(1)
|
||||||
|
|
||||||
|
const mutation = R.useMutation({
|
||||||
|
mutation: ([count]: readonly [count: number]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
|
Effect.andThen(QueryProgress.QueryProgress.update(() =>
|
||||||
|
AsyncData.Progress.make({ loaded: 0, total: Option.some(100) })
|
||||||
|
)),
|
||||||
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
|
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
||||||
|
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
||||||
|
)),
|
||||||
|
Effect.andThen(Effect.map(
|
||||||
|
HttpClient.HttpClient,
|
||||||
|
HttpClient.withTracerPropagation(false),
|
||||||
|
)),
|
||||||
|
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
|
Effect.flatMap(res => res.json),
|
||||||
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
|
Effect.scoped,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const [state] = R.useSubscribeRefs(mutation.stateRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Flex direction="column" align="center" gap="2">
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={[count]}
|
||||||
|
onValueChange={flow(
|
||||||
|
Array.head,
|
||||||
|
Option.getOrThrow,
|
||||||
|
setCount,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
{AsyncData.match(state, {
|
||||||
|
NoData: () => "No data yet",
|
||||||
|
Loading: progress =>
|
||||||
|
`Loading...
|
||||||
|
${ Option.match(progress, {
|
||||||
|
onSome: ({ loaded, total }) => ` (${ loaded }/${ Option.getOrElse(total, () => "unknown") })`,
|
||||||
|
onNone: () => "",
|
||||||
|
}) }`,
|
||||||
|
Success: value => `Value: ${ value }`,
|
||||||
|
Failure: cause => `Error: ${ cause }`,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button onClick={() => mutation.forkMutate(count).pipe(
|
||||||
|
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
|
||||||
|
Effect.andThen(Console.log("Mutation done.")),
|
||||||
|
runFork,
|
||||||
|
)}>
|
||||||
|
Get
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
77
packages/example/src/routes/query/usequery.tsx
Normal file
77
packages/example/src/routes/query/usequery.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { R } from "@/reffuse"
|
||||||
|
import { HttpClient } from "@effect/platform"
|
||||||
|
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/query/usequery")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const Result = Schema.Array(Schema.String)
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const runFork = R.useRunFork()
|
||||||
|
|
||||||
|
const [count, setCount] = useState(1)
|
||||||
|
|
||||||
|
const query = R.useQuery({
|
||||||
|
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
||||||
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
|
Effect.andThen(Effect.map(
|
||||||
|
HttpClient.HttpClient,
|
||||||
|
HttpClient.withTracerPropagation(false),
|
||||||
|
)),
|
||||||
|
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
|
Effect.flatMap(res => res.json),
|
||||||
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
|
Effect.scoped,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Flex direction="column" align="center" gap="2">
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={[count]}
|
||||||
|
onValueChange={flow(
|
||||||
|
Array.head,
|
||||||
|
Option.getOrThrow,
|
||||||
|
setCount,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
{AsyncData.match(state, {
|
||||||
|
NoData: () => "No data yet",
|
||||||
|
Loading: () => "Loading...",
|
||||||
|
Success: (value, { isRefreshing, isOptimistic }) =>
|
||||||
|
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
|
||||||
|
Failure: (cause, { isRefreshing }) =>
|
||||||
|
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => query.forkRefresh.pipe(
|
||||||
|
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
|
||||||
|
Effect.andThen(Console.log("Refresh finished or stopped")),
|
||||||
|
runFork,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
34
packages/example/src/routes/streams/pull.tsx
Normal file
34
packages/example/src/routes/streams/pull.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { R } from "@/reffuse"
|
||||||
|
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Chunk, Effect, Exit, Option, Queue, Random, Scope, Stream } from "effect"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/streams/pull")({
|
||||||
|
component: RouteComponent
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const stream = useMemo(() => Stream.repeatEffect(Random.nextInt), [])
|
||||||
|
const streamScope = R.useScope([stream], { finalizerExecutionMode: "fork" })
|
||||||
|
|
||||||
|
const queue = R.useMemo(() => Effect.provideService(Stream.toQueueOfElements(stream), Scope.Scope, streamScope), [streamScope])
|
||||||
|
|
||||||
|
const [value, setValue] = useState(Option.none<number>())
|
||||||
|
const pullLatest = R.useCallbackSync(() => Queue.takeAll(queue).pipe(
|
||||||
|
Effect.flatMap(Chunk.last),
|
||||||
|
Effect.flatMap(Exit.matchEffect({
|
||||||
|
onSuccess: Effect.succeed,
|
||||||
|
onFailure: Effect.fail,
|
||||||
|
})),
|
||||||
|
Effect.tap(v => Effect.sync(() => setValue(Option.some(v)))),
|
||||||
|
), [queue])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" align="center" gap="2">
|
||||||
|
{Option.isSome(value) && <Text>{value.value}</Text>}
|
||||||
|
<Button onClick={pullLatest}>Pull latest</Button>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
import { R } from "@/reffuse"
|
import { R } from "@/reffuse"
|
||||||
import { Button } from "@radix-ui/themes"
|
import { Button, Flex, Text } from "@radix-ui/themes"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Console, Effect } from "effect"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Console, Effect, Option } from "effect"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
value: string
|
||||||
|
left?: Leaf
|
||||||
|
right?: Leaf
|
||||||
|
}
|
||||||
|
interface Leaf {
|
||||||
|
node: Node
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
|
||||||
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/tests")({
|
export const Route = createFileRoute("/tests")({
|
||||||
@@ -9,23 +24,39 @@ export const Route = createFileRoute("/tests")({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
const runSync = R.useRunSync()
|
||||||
// Effect.andThen(makeUuid4),
|
|
||||||
// Effect.provide(GetRandomValues.CryptoRandom),
|
|
||||||
// ), [])
|
|
||||||
// console.log(value)
|
|
||||||
|
|
||||||
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
|
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
|
||||||
Effect.andThen(Console.log("ouient")),
|
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
|
||||||
Effect.delay("1 second"),
|
Effect.tap(v => Effect.sync(() => setUuid(v)))
|
||||||
))
|
), [])
|
||||||
|
|
||||||
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
|
const uuidStream = R.useStreamFromReactiveValues([uuid])
|
||||||
yield* Effect.log(value)
|
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
|
||||||
}))
|
|
||||||
|
const [, scopeLayer] = R.useScope([uuid])
|
||||||
|
|
||||||
|
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
|
||||||
|
Effect.andThen(Console.log("Scope changed")),
|
||||||
|
Effect.provide(scopeLayer),
|
||||||
|
runSync,
|
||||||
|
), [scopeLayer, runSync])
|
||||||
|
|
||||||
|
|
||||||
|
const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
|
||||||
|
const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => logValue("test")}>Log value</Button>
|
<Flex direction="column" justify="center" align="center" gap="2">
|
||||||
|
<Text>{uuid}</Text>
|
||||||
|
<Button onClick={generateUuid}>Generate UUID</Button>
|
||||||
|
<Text>
|
||||||
|
{Option.match(uuidStreamLatestValue, {
|
||||||
|
onSome: ([v]) => v,
|
||||||
|
onNone: () => <></>,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/time")({
|
|||||||
|
|
||||||
function Time() {
|
function Time() {
|
||||||
|
|
||||||
const timeRef = R.useMemo(() => DateTime.now.pipe(Effect.flatMap(SubscriptionRef.make)))
|
const timeRef = R.useMemo(() => DateTime.now.pipe(Effect.flatMap(SubscriptionRef.make)), [])
|
||||||
|
|
||||||
R.useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
R.useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
|
||||||
Effect.andThen(Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v)))
|
Effect.andThen(Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v)))
|
||||||
|
|||||||
35
packages/example/src/routes/todos.tsx
Normal file
35
packages/example/src/routes/todos.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { TodosContext } from "@/todos/reffuse"
|
||||||
|
import { TodosState } from "@/todos/services"
|
||||||
|
import { VTodos } from "@/todos/views/VTodos"
|
||||||
|
import { Container } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, Effect, Layer } from "effect"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/todos")({
|
||||||
|
component: Todos
|
||||||
|
})
|
||||||
|
|
||||||
|
function Todos() {
|
||||||
|
|
||||||
|
const todosLayer = useMemo(() => Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(TodosState.make("todos")),
|
||||||
|
|
||||||
|
Layer.merge(Layer.effectDiscard(
|
||||||
|
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
|
||||||
|
Effect.andThen(Console.log("TodosContext constructed"))
|
||||||
|
)
|
||||||
|
)),
|
||||||
|
), [])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
|
||||||
|
<VTodos />
|
||||||
|
</TodosContext.Provider>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
7
packages/example/src/services/AppQueryClient.ts
Normal file
7
packages/example/src/services/AppQueryClient.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { QueryClient } from "@reffuse/extension-query"
|
||||||
|
import * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||||
|
|
||||||
|
|
||||||
|
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({
|
||||||
|
errorHandler: AppQueryErrorHandler.AppQueryErrorHandler
|
||||||
|
}) {}
|
||||||
13
packages/example/src/services/AppQueryErrorHandler.ts
Normal file
13
packages/example/src/services/AppQueryErrorHandler.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { HttpClientError } from "@effect/platform"
|
||||||
|
import { QueryErrorHandler } from "@reffuse/extension-query"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class AppQueryErrorHandler extends Effect.Service<AppQueryErrorHandler>()("AppQueryErrorHandler", {
|
||||||
|
effect: QueryErrorHandler.make<HttpClientError.HttpClientError>()(
|
||||||
|
(self, failure, defect) => self.pipe(
|
||||||
|
Effect.catchTag("RequestError", "ResponseError", failure),
|
||||||
|
Effect.catchAllDefect(defect),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}) {}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export {}
|
export * as AppQueryClient from "./AppQueryClient"
|
||||||
|
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { GlobalReffuse } from "@/reffuse"
|
import { RootReffuse } from "@/reffuse"
|
||||||
import { Reffuse, ReffuseContext } from "reffuse"
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
import { TodosState } from "./services"
|
import { TodosState } from "./services"
|
||||||
|
|
||||||
|
|
||||||
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
|
export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
|
||||||
|
|
||||||
export const R = new class TodosReffuse extends GlobalReffuse.pipe(
|
export const R = new class TodosReffuse extends RootReffuse.pipe(
|
||||||
Reffuse.withContexts(TodosContext)
|
Reffuse.withContexts(TodosContext)
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -2,68 +2,43 @@ import { Todo } from "@/domain"
|
|||||||
import { KeyValueStore } from "@effect/platform"
|
import { KeyValueStore } from "@effect/platform"
|
||||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
import { PlatformError } from "@effect/platform/Error"
|
import { PlatformError } from "@effect/platform/Error"
|
||||||
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
|
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
export class TodosState extends Context.Tag("TodosState")<TodosState, {
|
||||||
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
|
||||||
|
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
||||||
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
|
readonly save: 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*() {
|
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(
|
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
|
||||||
Effect.flatMap(kv => kv.get(key)),
|
Effect.flatMap(kv => kv.get(key)),
|
||||||
Effect.flatMap(identity),
|
Effect.flatMap(identity),
|
||||||
Effect.flatMap(Schema.parseJson().pipe(
|
Effect.flatMap(Schema.decode(
|
||||||
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||||
Schema.decode,
|
|
||||||
)),
|
)),
|
||||||
Effect.flatMap(v => Ref.set(todos, v)),
|
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
|
||||||
|
|
||||||
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
|
|
||||||
|
|
||||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
|
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
|
||||||
Effect.flatMap(([kv, values]) => values.pipe(
|
Effect.flatMap(kv => values.pipe(
|
||||||
Schema.parseJson().pipe(
|
Schema.encode(
|
||||||
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
|
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
|
||||||
Schema.encode,
|
|
||||||
),
|
),
|
||||||
Effect.flatMap(v => kv.set(key, v)),
|
Effect.flatMap(v => kv.set(key, v)),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
|
||||||
)
|
)
|
||||||
|
|
||||||
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
|
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||||
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
|
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
|
||||||
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
|
const save = Effect.flatMap(todos, writeToLocalStorage)
|
||||||
|
|
||||||
// const moveUp = (index: number) => Effect.gen(function*() {
|
// Sync changes with local storage
|
||||||
|
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
|
||||||
|
|
||||||
// })
|
return { todos, load, save }
|
||||||
|
|
||||||
yield* readFromLocalStorage
|
|
||||||
|
|
||||||
return {
|
|
||||||
todos,
|
|
||||||
readFromLocalStorage,
|
|
||||||
saveToLocalStorage,
|
|
||||||
prepend,
|
|
||||||
replace,
|
|
||||||
remove,
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import { Todo } from "@/domain"
|
import { Todo } from "@/domain"
|
||||||
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
|
||||||
import { Effect, Option, SubscriptionRef } from "effect"
|
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||||
|
import { Chunk, Effect, Option, Ref } from "effect"
|
||||||
import { R } from "../reffuse"
|
import { R } from "../reffuse"
|
||||||
import { TodosState } from "../services"
|
import { TodosState } from "../services"
|
||||||
|
|
||||||
|
|
||||||
const createEmptyTodo = Todo.generateUniqueID.pipe(
|
const createEmptyTodo = makeUuid4.pipe(
|
||||||
Effect.map(id => Todo.Todo.make({
|
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
|
||||||
id,
|
Effect.provide(GetRandomValues.CryptoRandom),
|
||||||
content: "",
|
|
||||||
completedAt: Option.none(),
|
|
||||||
}, true))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
export function VNewTodo() {
|
export function VNewTodo() {
|
||||||
|
|
||||||
const runSync = R.useRunSync()
|
const todoRef = R.useRef(() => createEmptyTodo)
|
||||||
|
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
|
||||||
|
|
||||||
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)))
|
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
|
||||||
const [todo, setTodo] = R.useRefState(todoRef)
|
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
|
||||||
|
Effect.andThen(createEmptyTodo),
|
||||||
|
Effect.flatMap(v => Ref.set(todoRef, v)),
|
||||||
|
), [todoRef])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,23 +29,12 @@ export function VNewTodo() {
|
|||||||
<Card>
|
<Card>
|
||||||
<Flex direction="column" align="stretch" gap="2">
|
<Flex direction="column" align="stretch" gap="2">
|
||||||
<TextArea
|
<TextArea
|
||||||
value={todo.content}
|
value={content}
|
||||||
onChange={e => setTodo(prev =>
|
onChange={e => setContent(e.target.value)}
|
||||||
Todo.Todo.make({ ...prev, content: e.target.value }, true)
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex direction="row" justify="center" align="center">
|
<Flex direction="row" justify="center" align="center">
|
||||||
<Button
|
<Button onClick={add}>Add</Button>
|
||||||
onClick={() => TodosState.TodosState.pipe(
|
|
||||||
Effect.flatMap(state => state.prepend(todo)),
|
|
||||||
Effect.flatMap(() => createEmptyTodo),
|
|
||||||
Effect.map(setTodo),
|
|
||||||
runSync,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
import { Todo } from "@/domain"
|
import { Todo } from "@/domain"
|
||||||
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
|
||||||
import { Effect } from "effect"
|
import { Effect, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
import { Delete } from "lucide-react"
|
import { Delete } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { R } from "../reffuse"
|
import { R } from "../reffuse"
|
||||||
import { TodosState } from "../services"
|
|
||||||
|
|
||||||
|
|
||||||
export interface VTodoProps {
|
export interface VTodoProps {
|
||||||
readonly index: number
|
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
|
||||||
readonly todo: Todo.Todo
|
readonly remove: Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VTodo({ index, todo }: VTodoProps) {
|
export function VTodo({ todoRef, remove }: VTodoProps) {
|
||||||
|
|
||||||
const runSync = R.useRunSync()
|
const runSync = R.useRunSync()
|
||||||
|
|
||||||
|
const localTodoRef = R.useRef(() => todoRef)
|
||||||
|
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
|
||||||
|
|
||||||
|
R.useFork(() => localTodoRef.changes.pipe(
|
||||||
|
Stream.debounce("250 millis"),
|
||||||
|
Stream.runForEach(v => Ref.set(todoRef, v)),
|
||||||
|
), [localTodoRef])
|
||||||
|
|
||||||
const editorMode = useState(false)
|
const editorMode = useState(false)
|
||||||
|
|
||||||
|
|
||||||
@@ -23,14 +31,8 @@ export function VTodo({ index, todo }: VTodoProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<Flex direction="column" align="stretch" gap="1">
|
<Flex direction="column" align="stretch" gap="1">
|
||||||
<TextArea
|
<TextArea
|
||||||
value={todo.content}
|
value={content}
|
||||||
onChange={e => TodosState.TodosState.pipe(
|
onChange={e => setContent(e.target.value)}
|
||||||
Effect.flatMap(state => state.replace(
|
|
||||||
index,
|
|
||||||
Todo.Todo.make({ ...todo, content: e.target.value }, true),
|
|
||||||
)),
|
|
||||||
runSync,
|
|
||||||
)}
|
|
||||||
disabled={!editorMode}
|
disabled={!editorMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -38,12 +40,7 @@ export function VTodo({ index, todo }: VTodoProps) {
|
|||||||
<Box></Box>
|
<Box></Box>
|
||||||
|
|
||||||
<Flex direction="row" align="center" gap="1">
|
<Flex direction="row" align="center" gap="1">
|
||||||
<IconButton
|
<IconButton onClick={() => runSync(remove)}>
|
||||||
onClick={() => TodosState.TodosState.pipe(
|
|
||||||
Effect.flatMap(state => state.remove(index)),
|
|
||||||
runSync,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Box, Flex } from "@radix-ui/themes"
|
import { Box, Flex } from "@radix-ui/themes"
|
||||||
import { Chunk, Effect, Stream } from "effect"
|
import { Chunk, Effect, Ref } from "effect"
|
||||||
import { R } from "../reffuse"
|
import { R } from "../reffuse"
|
||||||
import { TodosState } from "../services"
|
import { TodosState } from "../services"
|
||||||
import { VNewTodo } from "./VNewTodo"
|
import { VNewTodo } from "./VNewTodo"
|
||||||
@@ -8,15 +8,8 @@ import { VTodo } from "./VTodo"
|
|||||||
|
|
||||||
export function VTodos() {
|
export function VTodos() {
|
||||||
|
|
||||||
// Sync changes to the todos with the local storage
|
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
|
||||||
R.useFork(() => TodosState.TodosState.pipe(
|
const [todos] = R.useSubscribeRefs(todosRef)
|
||||||
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 (
|
return (
|
||||||
@@ -27,7 +20,16 @@ export function VTodos() {
|
|||||||
|
|
||||||
{Chunk.map(todos, (todo, index) => (
|
{Chunk.map(todos, (todo, index) => (
|
||||||
<Box key={todo.id} width="500px">
|
<Box key={todo.id} width="500px">
|
||||||
<VTodo index={index} todo={todo} />
|
<R.SubRefFromGetSet
|
||||||
|
parent={todosRef}
|
||||||
|
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
|
||||||
|
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
|
||||||
|
>
|
||||||
|
{ref => <VTodo
|
||||||
|
todoRef={ref}
|
||||||
|
remove={Ref.update(todosRef, Chunk.remove(index))}
|
||||||
|
/>}
|
||||||
|
</R.SubRefFromGetSet>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
# Reffuse
|
# LazyRef extension for 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.
|
Extension to integrate `@typed/lazy-ref` with Reffuse.
|
||||||
|
|
||||||
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.
|
## Peer dependencies
|
||||||
|
- `@typed/lazy-ref`
|
||||||
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
- `reffuse` 0.1.3+
|
||||||
|
- `effect` 3.13+
|
||||||
## Dependencies
|
- `react` & `@types/react` 19+
|
||||||
(needs to be manually installed)
|
|
||||||
- `effect`
|
|
||||||
- `react` 19+
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@reffuse/extension-lazyref",
|
"name": "@reffuse/extension-lazyref",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -24,15 +24,19 @@
|
|||||||
"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": {
|
||||||
|
"reffuse": "workspace:*"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typed/lazy-ref": "^0.3.3",
|
"@typed/lazy-ref": "^0.3.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"effect": "^3.13.0",
|
"effect": "^3.15.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"reffuse": "^0.1.1"
|
"reffuse": "^0.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,54 @@
|
|||||||
import * as LazyRef from "@typed/lazy-ref"
|
import * as LazyRef from "@typed/lazy-ref"
|
||||||
import { Effect, Stream } from "effect"
|
import { Effect, pipe, Stream } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse"
|
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||||
|
import { SetStateAction } from "reffuse/types"
|
||||||
|
|
||||||
|
|
||||||
export const LazyRefExtension = ReffuseExtension.make(() => ({
|
export const LazyRefExtension = ReffuseExtension.make(() => ({
|
||||||
|
useSubscribeLazyRefs<
|
||||||
|
const Refs extends readonly LazyRef.LazyRef<any>[],
|
||||||
|
R,
|
||||||
|
>(
|
||||||
|
this: ReffuseNamespace.ReffuseNamespace<R>,
|
||||||
|
...refs: Refs
|
||||||
|
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||||
|
() => Effect.all(refs as readonly LazyRef.LazyRef<any>[]),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||||
|
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
|
||||||
|
|
||||||
|
this.useFork(() => pipe(
|
||||||
|
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
||||||
|
streams => Stream.zipLatestAll(...streams),
|
||||||
|
Stream.runForEach(v =>
|
||||||
|
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
|
||||||
|
),
|
||||||
|
), refs)
|
||||||
|
|
||||||
|
return reactStateValue
|
||||||
|
},
|
||||||
|
|
||||||
useLazyRefState<A, E, R>(
|
useLazyRefState<A, E, R>(
|
||||||
this: ReffuseHelpers.ReffuseHelpers<R>,
|
this: ReffuseNamespace.ReffuseNamespace<R>,
|
||||||
ref: LazyRef.LazyRef<A, E, R>,
|
ref: LazyRef.LazyRef<A, E, R>,
|
||||||
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||||
const runSync = this.useRunSync()
|
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||||
|
() => ref,
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||||
|
))
|
||||||
|
|
||||||
const initialState = React.useMemo(() => runSync(ref), [])
|
this.useFork(() => Stream.runForEach(
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
Stream.changesWith(ref.changes, (x, y) => x === y),
|
||||||
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
), [ref])
|
||||||
|
|
||||||
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||||
setReactStateValue(v)
|
LazyRef.update(ref, prevState =>
|
||||||
)), [ref])
|
|
||||||
|
|
||||||
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
|
|
||||||
runSync(LazyRef.update(ref, prevState =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
SetStateAction.value(setStateAction, prevState)
|
||||||
)),
|
),
|
||||||
[ref])
|
[ref])
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
return [reactStateValue, setValue]
|
||||||
|
|||||||
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.5",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"./README.md",
|
||||||
|
"./dist"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"url": "git+https://github.com/Thiladev/reffuse.git"
|
||||||
|
},
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./dist/*.d.ts",
|
||||||
|
"default": "./dist/*.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint:tsc": "tsc --noEmit",
|
||||||
|
"pack": "npm pack",
|
||||||
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
|
"clean:dist": "rm -rf dist",
|
||||||
|
"clean:node": "rm -rf node_modules"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"reffuse": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@effect/platform": "^0.77.0",
|
||||||
|
"@effect/platform-browser": "^0.56.0",
|
||||||
|
"@typed/async-data": "^0.13.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"effect": "^3.15.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"reffuse": "^0.1.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
95
packages/extension-query/src/MutationRunner.ts
Normal file
95
packages/extension-query/src/MutationRunner.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
|
import type * as QueryClient from "./QueryClient.js"
|
||||||
|
import * as QueryProgress from "./QueryProgress.js"
|
||||||
|
import { QueryState } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface MutationRunner<K extends readonly unknown[], A, E> {
|
||||||
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
|
||||||
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
|
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never,
|
||||||
|
>() => Effect.Tag(id)<Self, MutationRunner<K, A, E>>()
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
||||||
|
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||||
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
||||||
|
{
|
||||||
|
QueryClient,
|
||||||
|
mutation,
|
||||||
|
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||||
|
): Effect.Effect<
|
||||||
|
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
||||||
|
never,
|
||||||
|
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||||
|
> => Effect.gen(function*() {
|
||||||
|
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
||||||
|
const globalStateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
||||||
|
|
||||||
|
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||||
|
|
||||||
|
const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
||||||
|
Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
|
||||||
|
Effect.andThen(mutation(key)),
|
||||||
|
client.errorHandler.handle,
|
||||||
|
Effect.matchCauseEffect({
|
||||||
|
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
||||||
|
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(context),
|
||||||
|
Effect.provide(QueryProgress.QueryProgress.Default),
|
||||||
|
)
|
||||||
|
|
||||||
|
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
globalStateRef,
|
||||||
|
value => Ref.set(globalStateRef, value),
|
||||||
|
))
|
||||||
|
|
||||||
|
const forkMutate = (...key: K) => Effect.all([
|
||||||
|
Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>()),
|
||||||
|
Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>(),
|
||||||
|
]).pipe(
|
||||||
|
Effect.flatMap(([stateRef, stateQueue]) =>
|
||||||
|
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
|
||||||
|
Effect.andThen(run(key)),
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
|
||||||
|
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Queue.offer(stateQueue, value).pipe(
|
||||||
|
Effect.andThen(Ref.set(stateRef, value)),
|
||||||
|
Effect.andThen(Ref.set(globalStateRef, value)),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
context,
|
||||||
|
stateRef: globalStateRef,
|
||||||
|
|
||||||
|
mutate,
|
||||||
|
forkMutate,
|
||||||
|
}
|
||||||
|
})
|
||||||
58
packages/extension-query/src/QueryClient.ts
Normal file
58
packages/extension-query/src/QueryClient.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Context, Effect, identity, Layer } from "effect"
|
||||||
|
import type { Mutable } from "effect/Types"
|
||||||
|
import * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryClient<FallbackA, HandledE> {
|
||||||
|
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<FallbackA, HandledE> {
|
||||||
|
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <FallbackA, HandledE>(
|
||||||
|
{ errorHandler }: MakeProps<FallbackA, HandledE>
|
||||||
|
): Effect.Effect<QueryClient<FallbackA, HandledE>> => Effect.Do.pipe(
|
||||||
|
Effect.let("errorHandler", () => errorHandler)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const id = "@reffuse/extension-query/QueryClient"
|
||||||
|
|
||||||
|
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
|
||||||
|
export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
|
||||||
|
TagClassShape<FallbackA, HandledE>,
|
||||||
|
typeof id,
|
||||||
|
QueryClient<FallbackA, HandledE>
|
||||||
|
>
|
||||||
|
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
|
||||||
|
|
||||||
|
|
||||||
|
export interface ServiceProps<FallbackA = never, HandledE = never, E = never, R = never> {
|
||||||
|
readonly errorHandler?: Effect.Effect<QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, E, R>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
|
||||||
|
Self,
|
||||||
|
typeof id,
|
||||||
|
QueryClient<FallbackA, HandledE>
|
||||||
|
> {
|
||||||
|
readonly Default: Layer.Layer<Self, E, R>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Service = <Self>() => (
|
||||||
|
<FallbackA = never, HandledE = never, E = never, R = never>(
|
||||||
|
props?: ServiceProps<FallbackA, HandledE, E, R>
|
||||||
|
): ServiceResult<Self, FallbackA, HandledE, E, R> => {
|
||||||
|
const TagClass = Context.Tag(id)() as ServiceResult<Self, FallbackA, HandledE, E, R>
|
||||||
|
|
||||||
|
(TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
|
||||||
|
props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
|
||||||
|
errorHandler => make({ errorHandler }),
|
||||||
|
))
|
||||||
|
|
||||||
|
return TagClass
|
||||||
|
}
|
||||||
|
)
|
||||||
40
packages/extension-query/src/QueryErrorHandler.ts
Normal file
40
packages/extension-query/src/QueryErrorHandler.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Cause, Effect, PubSub, Stream } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryErrorHandler<FallbackA, HandledE> {
|
||||||
|
readonly errors: Stream.Stream<Cause.Cause<HandledE>>
|
||||||
|
readonly handle: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Fallback<T> = T extends QueryErrorHandler<infer A, any> ? A : never
|
||||||
|
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
|
||||||
|
|
||||||
|
|
||||||
|
export const make = <HandledE = never>() => (
|
||||||
|
<FallbackA>(
|
||||||
|
f: (
|
||||||
|
self: Effect.Effect<never, HandledE>,
|
||||||
|
failure: (failure: HandledE) => Effect.Effect<never>,
|
||||||
|
defect: (defect: unknown) => Effect.Effect<never>,
|
||||||
|
) => Effect.Effect<FallbackA>
|
||||||
|
): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
|
||||||
|
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
|
||||||
|
const errors = Stream.fromPubSub(pubsub)
|
||||||
|
|
||||||
|
const handle = <A, E, R>(
|
||||||
|
self: Effect.Effect<A, E, R>
|
||||||
|
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
||||||
|
self as unknown as Effect.Effect<never, HandledE, never>,
|
||||||
|
(failure: HandledE) => Effect.andThen(
|
||||||
|
PubSub.publish(pubsub, Cause.fail(failure)),
|
||||||
|
Effect.failCause(Cause.empty),
|
||||||
|
),
|
||||||
|
(defect: unknown) => Effect.andThen(
|
||||||
|
PubSub.publish(pubsub, Cause.die(defect)),
|
||||||
|
Effect.failCause(Cause.empty),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return { errors, handle }
|
||||||
|
})
|
||||||
|
)
|
||||||
61
packages/extension-query/src/QueryExtension.ts
Normal file
61
packages/extension-query/src/QueryExtension.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { Effect, Stream } from "effect"
|
||||||
|
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||||
|
import * as MutationRunner from "./MutationRunner.js"
|
||||||
|
import * as QueryClient from "./QueryClient.js"
|
||||||
|
import type * as QueryProgress from "./QueryProgress.js"
|
||||||
|
import * as QueryRunner from "./QueryRunner.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
readonly options?: QueryRunner.RunOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
|
||||||
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const QueryExtension = ReffuseExtension.make(() => ({
|
||||||
|
useQuery<
|
||||||
|
QK extends readonly unknown[],
|
||||||
|
QA,
|
||||||
|
FallbackA,
|
||||||
|
QE,
|
||||||
|
HandledE,
|
||||||
|
QR extends R,
|
||||||
|
R,
|
||||||
|
>(
|
||||||
|
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||||
|
props: UseQueryProps<QK, QA, QE, QR>,
|
||||||
|
): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||||
|
const runner = this.useMemo(() => QueryRunner.make({
|
||||||
|
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||||
|
key: props.key,
|
||||||
|
query: props.query,
|
||||||
|
}), [props.key])
|
||||||
|
|
||||||
|
this.useFork(() => QueryRunner.run(runner, props.options), [runner])
|
||||||
|
|
||||||
|
return runner
|
||||||
|
},
|
||||||
|
|
||||||
|
useMutation<
|
||||||
|
QK extends readonly unknown[],
|
||||||
|
QA,
|
||||||
|
FallbackA,
|
||||||
|
QE,
|
||||||
|
HandledE,
|
||||||
|
QR extends R,
|
||||||
|
R,
|
||||||
|
>(
|
||||||
|
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||||
|
props: UseMutationProps<QK, QA, QE, QR>,
|
||||||
|
): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||||
|
return this.useMemo(() => MutationRunner.make({
|
||||||
|
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||||
|
mutation: props.mutation,
|
||||||
|
}), [])
|
||||||
|
},
|
||||||
|
}))
|
||||||
37
packages/extension-query/src/QueryProgress.ts
Normal file
37
packages/extension-query/src/QueryProgress.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { Effect, flow, Layer, Match, Option } from "effect"
|
||||||
|
import { QueryState } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryProgress")<QueryProgress, {
|
||||||
|
readonly get: Effect.Effect<Option.Option<AsyncData.Progress>>
|
||||||
|
|
||||||
|
readonly update: (
|
||||||
|
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
|
||||||
|
) => Effect.Effect<void>
|
||||||
|
}>() {
|
||||||
|
static readonly Default: Layer.Layer<
|
||||||
|
QueryProgress,
|
||||||
|
never,
|
||||||
|
QueryState.QueryState<any, any>
|
||||||
|
> = Layer.effect(this, Effect.gen(function*() {
|
||||||
|
const state = yield* QueryState.makeTag()
|
||||||
|
|
||||||
|
const get = state.get.pipe(
|
||||||
|
Effect.map(flow(Match.value,
|
||||||
|
Match.tag("Loading", v => v.progress),
|
||||||
|
Match.tag("Refreshing", v => v.progress),
|
||||||
|
Match.orElse(() => Option.none()),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const update = (f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress) => get.pipe(
|
||||||
|
Effect.map(f),
|
||||||
|
Effect.flatMap(progress => state.update(previous =>
|
||||||
|
AsyncData.updateProgress(previous, progress)
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return { get, update }
|
||||||
|
}))
|
||||||
|
}
|
||||||
193
packages/extension-query/src/QueryRunner.ts
Normal file
193
packages/extension-query/src/QueryRunner.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { BrowserStream } from "@effect/platform-browser"
|
||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Cause, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import type * as QueryClient from "./QueryClient.js"
|
||||||
|
import * as QueryProgress from "./QueryProgress.js"
|
||||||
|
import { QueryState } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryRunner<K extends readonly unknown[], A, E> {
|
||||||
|
readonly queryKey: Stream.Stream<K>
|
||||||
|
readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
|
||||||
|
AsyncData.Success<A> | AsyncData.Failure<E>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
>>>
|
||||||
|
|
||||||
|
readonly interrupt: Effect.Effect<void>
|
||||||
|
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void>>
|
||||||
|
readonly forkFetch: (keyValue: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
readonly forkRefresh: Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never
|
||||||
|
>() => Effect.Tag(id)<Self, QueryRunner<K, A, E>>()
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
||||||
|
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
||||||
|
{
|
||||||
|
QueryClient,
|
||||||
|
key,
|
||||||
|
query,
|
||||||
|
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||||
|
): Effect.Effect<
|
||||||
|
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
||||||
|
never,
|
||||||
|
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||||
|
> => Effect.gen(function*() {
|
||||||
|
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
||||||
|
|
||||||
|
const latestKeyValueRef = yield* SubscriptionRef.make(Option.none<K>())
|
||||||
|
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
||||||
|
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
|
||||||
|
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
>>())
|
||||||
|
|
||||||
|
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||||
|
|
||||||
|
const interrupt = Effect.flatMap(fiberRef, Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const forkInterrupt = Effect.flatMap(fiberRef, Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
||||||
|
Effect.asVoid,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.forkDaemon(Effect.void),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const run = (keyValue: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
||||||
|
Effect.flatMap(([client, state]) => Ref.set(latestKeyValueRef, Option.some(keyValue)).pipe(
|
||||||
|
Effect.andThen(query(keyValue)),
|
||||||
|
client.errorHandler.handle,
|
||||||
|
Effect.matchCauseEffect({
|
||||||
|
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
||||||
|
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(context),
|
||||||
|
Effect.provide(QueryProgress.QueryProgress.Default),
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkFetch = (keyValue: K) => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||||
|
Effect.flatMap(stateQueue => queryStateTag.pipe(
|
||||||
|
Effect.flatMap(state => interrupt.pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.addFinalizer(() => Effect.andThen(
|
||||||
|
Ref.set(fiberRef, Option.none()),
|
||||||
|
Queue.shutdown(stateQueue),
|
||||||
|
)).pipe(
|
||||||
|
Effect.andThen(state.set(AsyncData.loading())),
|
||||||
|
Effect.andThen(run(keyValue)),
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||||
|
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Effect.andThen(
|
||||||
|
Queue.offer(stateQueue, value),
|
||||||
|
Ref.set(stateRef, value),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const setInitialRefreshState = Effect.flatMap(queryStateTag, state => state.update(previous => {
|
||||||
|
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
||||||
|
return AsyncData.refreshing(previous)
|
||||||
|
if (AsyncData.isRefreshing(previous))
|
||||||
|
return AsyncData.refreshing(previous.previous)
|
||||||
|
return AsyncData.loading()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||||
|
Effect.flatMap(stateQueue => interrupt.pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.addFinalizer(() => Effect.andThen(
|
||||||
|
Ref.set(fiberRef, Option.none()),
|
||||||
|
Queue.shutdown(stateQueue),
|
||||||
|
)).pipe(
|
||||||
|
Effect.andThen(setInitialRefreshState),
|
||||||
|
Effect.andThen(latestKeyValueRef.pipe(
|
||||||
|
Effect.flatMap(identity),
|
||||||
|
Effect.flatMap(run),
|
||||||
|
)),
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||||
|
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Effect.andThen(
|
||||||
|
Queue.offer(stateQueue, value),
|
||||||
|
Ref.set(stateRef, value),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryKey: key,
|
||||||
|
latestKeyValueRef,
|
||||||
|
stateRef,
|
||||||
|
fiberRef,
|
||||||
|
|
||||||
|
interrupt,
|
||||||
|
forkInterrupt,
|
||||||
|
forkFetch,
|
||||||
|
forkRefresh,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export interface RunOptions {
|
||||||
|
readonly refreshOnWindowFocus?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const run = <K extends readonly unknown[], A, E>(
|
||||||
|
self: QueryRunner<K, A, E>,
|
||||||
|
options?: RunOptions,
|
||||||
|
): Effect.Effect<void, never, Scope.Scope> => Effect.gen(function*() {
|
||||||
|
if (typeof window !== "undefined" && (options?.refreshOnWindowFocus ?? true))
|
||||||
|
yield* Effect.forkScoped(
|
||||||
|
Stream.runForEach(BrowserStream.fromEventListenerWindow("focus"), () => self.forkRefresh)
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() => self.interrupt)
|
||||||
|
yield* Stream.runForEach(Stream.changes(self.queryKey), latestKey => self.forkFetch(latestKey))
|
||||||
|
})
|
||||||
6
packages/extension-query/src/index.ts
Normal file
6
packages/extension-query/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * as MutationRunner from "./MutationRunner.js"
|
||||||
|
export * as QueryClient from "./QueryClient.js"
|
||||||
|
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||||
|
export * from "./QueryExtension.js"
|
||||||
|
export * as QueryProgress from "./QueryProgress.js"
|
||||||
|
export * as QueryRunner from "./QueryRunner.js"
|
||||||
24
packages/extension-query/src/internal/QueryState.ts
Normal file
24
packages/extension-query/src/internal/QueryState.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type * as AsyncData from "@typed/async-data"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryState<A, E> {
|
||||||
|
readonly get: Effect.Effect<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>
|
||||||
|
readonly update: (f: (previous: AsyncData.AsyncData<A, E>) => AsyncData.AsyncData<A, E>) => Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeTag = <A, E>(): Context.Tag<QueryState<A, E>, QueryState<A, E>> => Context.GenericTag("@reffuse/query-extension/QueryState")
|
||||||
|
|
||||||
|
export const layer = <A, E>(
|
||||||
|
tag: Context.Tag<QueryState<A, E>, QueryState<A, E>>,
|
||||||
|
get: Effect.Effect<AsyncData.AsyncData<A, E>>,
|
||||||
|
set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>,
|
||||||
|
): Layer.Layer<QueryState<A, E>> => Layer.succeed(tag, {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
update: f => get.pipe(
|
||||||
|
Effect.map(f),
|
||||||
|
Effect.flatMap(set),
|
||||||
|
),
|
||||||
|
})
|
||||||
1
packages/extension-query/src/internal/index.ts
Normal file
1
packages/extension-query/src/internal/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * as QueryState from "./QueryState.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"]
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ This library is in early development. While it is (almost) feature complete and
|
|||||||
|
|
||||||
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
||||||
|
|
||||||
## Dependencies
|
## Peer dependencies
|
||||||
(needs to be manually installed)
|
- `effect` 3.13+
|
||||||
- `effect`
|
- `react` & `@types/react` 19+
|
||||||
- `react` 19+
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "reffuse",
|
"name": "reffuse",
|
||||||
"version": "0.1.1",
|
"version": "0.1.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -16,6 +16,10 @@
|
|||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
|
"./types": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"default": "./dist/types/index.js"
|
||||||
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./dist/*.d.ts",
|
"types": "./dist/*.d.ts",
|
||||||
"default": "./dist/*.js"
|
"default": "./dist/*.js"
|
||||||
@@ -24,13 +28,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint:tsc": "tsc --noEmit",
|
"lint:tsc": "tsc --noEmit",
|
||||||
|
"pack": "npm pack",
|
||||||
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node": "rm -rf node_modules"
|
"clean:node": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"effect": "^3.13.0",
|
"effect": "^3.15.0",
|
||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import type * as ReffuseContext from "./ReffuseContext.js"
|
import type * as ReffuseContext from "./ReffuseContext.js"
|
||||||
import type * as ReffuseExtension from "./ReffuseExtension.js"
|
import type * as ReffuseExtension from "./ReffuseExtension.js"
|
||||||
import * as ReffuseHelpers from "./ReffuseHelpers.js"
|
import * as ReffuseNamespace from "./ReffuseNamespace.js"
|
||||||
import type { Merge, StaticType } from "./types.js"
|
import type { Merge, StaticType } from "./utils.js"
|
||||||
|
|
||||||
|
|
||||||
export class Reffuse extends ReffuseHelpers.make() {}
|
export class Reffuse extends ReffuseNamespace.makeClass() {}
|
||||||
|
|
||||||
|
|
||||||
export const withContexts = <R2 extends Array<unknown>>(
|
export const withContexts = <R2 extends Array<unknown>>(
|
||||||
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
|
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
|
||||||
) =>
|
) => (
|
||||||
<
|
<
|
||||||
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R1>,
|
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R1>,
|
||||||
R1
|
R1
|
||||||
>(
|
>(
|
||||||
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R1>
|
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R1>
|
||||||
): (
|
): (
|
||||||
{
|
{
|
||||||
new(): Merge<
|
new(): Merge<
|
||||||
InstanceType<BaseClass>,
|
InstanceType<BaseClass>,
|
||||||
{ constructor: ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]> }
|
{ constructor: ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]> }
|
||||||
>
|
>
|
||||||
} &
|
} &
|
||||||
Merge<
|
Merge<
|
||||||
StaticType<BaseClass>,
|
StaticType<BaseClass>,
|
||||||
StaticType<ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]>>
|
StaticType<ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]>>
|
||||||
>
|
>
|
||||||
) => class extends self {
|
) => class extends self {
|
||||||
static readonly contexts = [...self.contexts, ...contexts]
|
static readonly contexts = [...self.contexts, ...contexts]
|
||||||
} as any
|
} as any
|
||||||
|
)
|
||||||
|
|
||||||
|
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) => (
|
||||||
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) =>
|
|
||||||
<
|
<
|
||||||
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R>,
|
BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R>,
|
||||||
R
|
R
|
||||||
>(
|
>(
|
||||||
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R>
|
self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R>
|
||||||
): (
|
): (
|
||||||
{ new(): Merge<InstanceType<BaseClass>, A> } &
|
{ new(): Merge<InstanceType<BaseClass>, A> } &
|
||||||
StaticType<BaseClass>
|
StaticType<BaseClass>
|
||||||
@@ -45,3 +45,4 @@ export const withExtension = <A extends object>(extension: ReffuseExtension.Reff
|
|||||||
Object.assign(class_.prototype, extension())
|
Object.assign(class_.prototype, extension())
|
||||||
return class_ as any
|
return class_ as any
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|||||||
178
packages/reffuse/src/ReffuseContext.ts
Normal file
178
packages/reffuse/src/ReffuseContext.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
|
|
||||||
|
|
||||||
|
export class ReffuseContext<R> {
|
||||||
|
readonly Context = React.createContext<Context.Context<R>>(null!)
|
||||||
|
readonly Provider = makeProvider(this.Context)
|
||||||
|
readonly AsyncProvider = makeAsyncProvider(this.Context)
|
||||||
|
|
||||||
|
|
||||||
|
useContext(): Context.Context<R> {
|
||||||
|
return React.useContext(this.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayer(): Layer.Layer<R> {
|
||||||
|
const context = this.useContext()
|
||||||
|
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type R<T> = T extends ReffuseContext<infer R> ? R : never
|
||||||
|
|
||||||
|
|
||||||
|
export type ReactProvider<R> = React.FC<{
|
||||||
|
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||||
|
readonly scope?: Scope.Scope
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}>
|
||||||
|
|
||||||
|
const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> => {
|
||||||
|
return function ReffuseContextReactProvider(props) {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
|
||||||
|
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
|
||||||
|
|
||||||
|
const makeScope = React.useMemo(() => props.scope
|
||||||
|
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||||
|
[props.scope])
|
||||||
|
|
||||||
|
const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
|
||||||
|
Effect.provide(props.layer),
|
||||||
|
Effect.provideService(Scope.Scope, scope),
|
||||||
|
)
|
||||||
|
|
||||||
|
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
|
||||||
|
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
|
||||||
|
Match.when("sync", () => { runSync(effect) }),
|
||||||
|
Match.when("fork", () => { runFork(effect) }),
|
||||||
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
|
||||||
|
Effect.bind("isInitialRun", () => Ref.make(true)),
|
||||||
|
Effect.bind("scope", () => makeScope),
|
||||||
|
Effect.bind("context", ({ scope }) => makeContext(scope)),
|
||||||
|
Effect.map(({ isInitialRun, scope, context }) => [isInitialRun, scope, context] as const),
|
||||||
|
runSync,
|
||||||
|
), [])
|
||||||
|
|
||||||
|
const [value, setValue] = React.useState(initialValue)
|
||||||
|
|
||||||
|
React.useEffect(() => isInitialRun.pipe(
|
||||||
|
Effect.if({
|
||||||
|
onTrue: () => Ref.set(isInitialRun, false).pipe(
|
||||||
|
Effect.map(() =>
|
||||||
|
() => closeScope(initialScope)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
onFalse: () => Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => makeScope),
|
||||||
|
Effect.bind("context", ({ scope }) => makeContext(scope)),
|
||||||
|
Effect.tap(({ context }) =>
|
||||||
|
Effect.sync(() => setValue(context))
|
||||||
|
),
|
||||||
|
Effect.map(({ scope }) =>
|
||||||
|
() => closeScope(scope)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
runSync,
|
||||||
|
), [makeScope, runSync, runFork])
|
||||||
|
|
||||||
|
return React.createElement(Context, { ...props, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AsyncReactProvider<R> = React.FC<{
|
||||||
|
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
|
||||||
|
readonly scope?: Scope.Scope
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
|
readonly fallback?: React.ReactNode
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}>
|
||||||
|
|
||||||
|
const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> => {
|
||||||
|
function ReffuseContextAsyncReactProviderInner({ promise, children }: {
|
||||||
|
readonly promise: Promise<Context.Context<R>>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return React.createElement(Context, {
|
||||||
|
value: React.use(promise),
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return function ReffuseContextAsyncReactProvider(props) {
|
||||||
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
|
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
|
||||||
|
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
|
||||||
|
|
||||||
|
const [promise, setPromise] = React.useState(Promise.withResolvers<Context.Context<R>>().promise)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const { promise, resolve, reject } = Promise.withResolvers<Context.Context<R>>()
|
||||||
|
setPromise(promise)
|
||||||
|
|
||||||
|
const scope = runSync(props.scope
|
||||||
|
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
)
|
||||||
|
|
||||||
|
Effect.context<R>().pipe(
|
||||||
|
Effect.match({
|
||||||
|
onSuccess: resolve,
|
||||||
|
onFailure: reject,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Effect.provide(props.layer),
|
||||||
|
Effect.provideService(Scope.Scope, scope),
|
||||||
|
effect => runFork(effect, { ...props, scope }),
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => Scope.close(scope, Exit.void).pipe(
|
||||||
|
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
|
||||||
|
Match.when("sync", () => { runSync(effect) }),
|
||||||
|
Match.when("fork", () => { runFork(effect) }),
|
||||||
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, [props.layer, runSync, runFork])
|
||||||
|
|
||||||
|
return React.createElement(React.Suspense, {
|
||||||
|
children: React.createElement(ReffuseContextAsyncReactProviderInner, { ...props, promise }),
|
||||||
|
fallback: props.fallback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const make = <R = never>() => new ReffuseContext<R>()
|
||||||
|
|
||||||
|
export const useMergeAll = <T extends Array<unknown>>(
|
||||||
|
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
|
||||||
|
): Context.Context<T[number]> => {
|
||||||
|
const values = contexts.map(v => React.use(v.Context))
|
||||||
|
return React.useMemo(() => Context.mergeAll(...values), values)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMergeAllLayers = <T extends Array<unknown>>(
|
||||||
|
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
|
||||||
|
): Layer.Layer<T[number]> => {
|
||||||
|
const values = contexts.map(v => React.use(v.Context))
|
||||||
|
|
||||||
|
return React.useMemo(() => Array.isNonEmptyArray(values)
|
||||||
|
? Layer.mergeAll(
|
||||||
|
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
|
||||||
|
)
|
||||||
|
: Layer.empty as Layer.Layer<T[number]>,
|
||||||
|
values)
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Array, Context, Effect, Layer, Runtime } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
|
||||||
|
|
||||||
|
|
||||||
export class ReffuseContext<R> {
|
|
||||||
|
|
||||||
readonly Context = React.createContext<Context.Context<R>>(null!)
|
|
||||||
readonly Provider: ReffuseContextReactProvider<R>
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// TODO: scope the layer creation
|
|
||||||
this.Provider = props => {
|
|
||||||
const runtime = ReffuseRuntime.useRuntime()
|
|
||||||
|
|
||||||
const value = React.useMemo(() => Effect.context<R>().pipe(
|
|
||||||
Effect.provide(props.layer),
|
|
||||||
Runtime.runSync(runtime),
|
|
||||||
), [props.layer, runtime])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<this.Context
|
|
||||||
{...props}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this.Provider.displayName = "ReffuseContextReactProvider"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 ReffuseContextReactProvider<R> = React.FC<{
|
|
||||||
readonly layer: Layer.Layer<R, unknown>
|
|
||||||
readonly children?: React.ReactNode
|
|
||||||
}>
|
|
||||||
|
|
||||||
export type R<T> = T extends ReffuseContext<infer R> ? R : never
|
|
||||||
|
|
||||||
|
|
||||||
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.NonEmptyArray<unknown>>(
|
|
||||||
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
|
|
||||||
): Layer.Layer<T[number]> {
|
|
||||||
const values = Array.map(
|
|
||||||
contexts as Array.NonEmptyArray<ReffuseContext<T[number]>>,
|
|
||||||
v => React.use(v.Context),
|
|
||||||
)
|
|
||||||
|
|
||||||
return React.useMemo(() => Layer.mergeAll(
|
|
||||||
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
|
|
||||||
), values)
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Pipeable, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ReffuseContext from "./ReffuseContext.js"
|
import * as ReffuseContext from "./ReffuseContext.js"
|
||||||
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
import * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
import * as SetStateAction from "./SetStateAction.js"
|
import { type PropertyPath, SetStateAction, SubscriptionSubRef } from "./types/index.js"
|
||||||
|
|
||||||
|
|
||||||
export interface RenderOptions {
|
export interface RenderOptions {
|
||||||
@@ -14,17 +14,38 @@ export interface ScopeOptions {
|
|||||||
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
|
||||||
|
readonly scope?: Scope.Scope
|
||||||
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class ReffuseHelpers<R> {
|
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
|
||||||
declare ["constructor"]: ReffuseHelpersClass<R>
|
[K in keyof T]: Effect.Effect.Success<T[K]>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> {
|
export abstract class ReffuseNamespace<R> {
|
||||||
return ReffuseContext.useMergeAll(...this.constructor.contexts)
|
declare ["constructor"]: ReffuseNamespaceClass<R>
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.SubRefFromGetSet = this.SubRefFromGetSet.bind(this as any) as any
|
||||||
|
this.SubRefFromPath = this.SubRefFromPath.bind(this as any) as any
|
||||||
|
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
|
||||||
|
this.RefState = this.RefState.bind(this as any) as any
|
||||||
|
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
useRunSync<R>(this: ReffuseHelpers<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
|
useContext<R>(this: ReffuseNamespace<R>): Context.Context<R> {
|
||||||
|
return ReffuseContext.useMergeAll(...this.constructor.contexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayer<R>(this: ReffuseNamespace<R>): Layer.Layer<R> {
|
||||||
|
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useRunSync<R>(this: ReffuseNamespace<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
|
||||||
const runtime = ReffuseRuntime.useRuntime()
|
const runtime = ReffuseRuntime.useRuntime()
|
||||||
const context = this.useContext()
|
const context = this.useContext()
|
||||||
|
|
||||||
@@ -34,7 +55,7 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
), [runtime, context])
|
), [runtime, context])
|
||||||
}
|
}
|
||||||
|
|
||||||
useRunPromise<R>(this: ReffuseHelpers<R>): <A, E>(
|
useRunPromise<R>(this: ReffuseNamespace<R>): <A, E>(
|
||||||
effect: Effect.Effect<A, E, R>,
|
effect: Effect.Effect<A, E, R>,
|
||||||
options?: { readonly signal?: AbortSignal },
|
options?: { readonly signal?: AbortSignal },
|
||||||
) => Promise<A> {
|
) => Promise<A> {
|
||||||
@@ -47,7 +68,7 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
), [runtime, context])
|
), [runtime, context])
|
||||||
}
|
}
|
||||||
|
|
||||||
useRunFork<R>(this: ReffuseHelpers<R>): <A, E>(
|
useRunFork<R>(this: ReffuseNamespace<R>): <A, E>(
|
||||||
effect: Effect.Effect<A, E, R>,
|
effect: Effect.Effect<A, E, R>,
|
||||||
options?: Runtime.RunForkOptions,
|
options?: Runtime.RunForkOptions,
|
||||||
) => Fiber.RuntimeFiber<A, E> {
|
) => Fiber.RuntimeFiber<A, E> {
|
||||||
@@ -60,7 +81,7 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
), [runtime, context])
|
), [runtime, context])
|
||||||
}
|
}
|
||||||
|
|
||||||
useRunCallback<R>(this: ReffuseHelpers<R>): <A, E>(
|
useRunCallback<R>(this: ReffuseNamespace<R>): <A, E>(
|
||||||
effect: Effect.Effect<A, E, R>,
|
effect: Effect.Effect<A, E, R>,
|
||||||
options?: Runtime.RunCallbackOptions<A, E>,
|
options?: Runtime.RunCallbackOptions<A, E>,
|
||||||
) => Runtime.Cancel<A, E> {
|
) => Runtime.Cancel<A, E> {
|
||||||
@@ -73,6 +94,56 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
), [runtime, context])
|
), [runtime, context])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useScope<R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
deps: React.DependencyList = [],
|
||||||
|
options?: UseScopeOptions,
|
||||||
|
): readonly [scope: Scope.Scope, layer: Layer.Layer<Scope.Scope>] {
|
||||||
|
const runSync = this.useRunSync()
|
||||||
|
const runFork = this.useRunFork()
|
||||||
|
|
||||||
|
const makeScope = React.useMemo(() => options?.scope
|
||||||
|
? Scope.fork(options.scope, options.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
|
||||||
|
[options?.scope])
|
||||||
|
|
||||||
|
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
|
||||||
|
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
|
||||||
|
Match.when("sync", () => { runSync(effect) }),
|
||||||
|
Match.when("fork", () => { runFork(effect) }),
|
||||||
|
Match.exhaustive,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const [isInitialRun, initialScope] = React.useMemo(() => runSync(
|
||||||
|
Effect.all([Ref.make(true), makeScope])
|
||||||
|
), [makeScope])
|
||||||
|
|
||||||
|
const [scope, setScope] = React.useState(initialScope)
|
||||||
|
|
||||||
|
React.useEffect(() => isInitialRun.pipe(
|
||||||
|
Effect.if({
|
||||||
|
onTrue: () => Effect.as(
|
||||||
|
Ref.set(isInitialRun, false),
|
||||||
|
() => closeScope(initialScope),
|
||||||
|
),
|
||||||
|
|
||||||
|
onFalse: () => makeScope.pipe(
|
||||||
|
Effect.tap(v => Effect.sync(() => setScope(v))),
|
||||||
|
Effect.map(v => () => closeScope(v)),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
|
||||||
|
runSync,
|
||||||
|
), [
|
||||||
|
makeScope,
|
||||||
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
|
||||||
|
...deps,
|
||||||
|
])
|
||||||
|
|
||||||
|
return React.useMemo(() => [scope, Layer.succeed(Scope.Scope, scope)] as const, [scope])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reffuse equivalent to `React.useMemo`.
|
* Reffuse equivalent to `React.useMemo`.
|
||||||
*
|
*
|
||||||
@@ -83,69 +154,19 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
|
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
|
||||||
*/
|
*/
|
||||||
useMemo<A, E, R>(
|
useMemo<A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
effect: () => Effect.Effect<A, E, R>,
|
effect: () => Effect.Effect<A, E, R>,
|
||||||
deps?: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
options?: RenderOptions,
|
options?: RenderOptions,
|
||||||
): A {
|
): A {
|
||||||
const runSync = this.useRunSync()
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
return React.useMemo(() => runSync(effect()), [
|
return React.useMemo(() => runSync(effect()), [
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
...(deps ?? []),
|
...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`.
|
* Reffuse equivalent to `React.useEffect`.
|
||||||
*
|
*
|
||||||
@@ -173,7 +194,7 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
useEffect<A, E, R>(
|
useEffect<A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: RenderOptions & ScopeOptions,
|
options?: RenderOptions & ScopeOptions,
|
||||||
@@ -187,9 +208,9 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return () => { runSync(Scope.close(scope, Exit.void)) }
|
return () => { runSync(Scope.close(scope, Exit.void)) }
|
||||||
}, [
|
}, deps && [
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
...(deps ?? []),
|
...deps,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,23 +242,23 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
useLayoutEffect<A, E, R>(
|
useLayoutEffect<A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: RenderOptions & ScopeOptions,
|
options?: RenderOptions & ScopeOptions,
|
||||||
): void {
|
): void {
|
||||||
const runSync = this.useRunSync()
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
return React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
|
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
|
||||||
Effect.tap(scope => Effect.provideService(effect(), Scope.Scope, scope)),
|
Effect.tap(scope => Effect.provideService(effect(), Scope.Scope, scope)),
|
||||||
runSync,
|
runSync,
|
||||||
)
|
)
|
||||||
|
|
||||||
return () => { runSync(Scope.close(scope, Exit.void)) }
|
return () => { runSync(Scope.close(scope, Exit.void)) }
|
||||||
}, [
|
}, deps && [
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
...(deps ?? []),
|
...deps,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +290,7 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
useFork<A, E, R>(
|
useFork<A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
||||||
@@ -277,7 +298,7 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
const runSync = this.useRunSync()
|
const runSync = this.useRunSync()
|
||||||
const runFork = this.useRunFork()
|
const runFork = this.useRunFork()
|
||||||
|
|
||||||
return React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const scope = runSync(options?.scope
|
const scope = runSync(options?.scope
|
||||||
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
: Scope.make(options?.finalizerExecutionStrategy)
|
: Scope.make(options?.finalizerExecutionStrategy)
|
||||||
@@ -285,14 +306,14 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
runFork(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
runFork(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||||
|
|
||||||
return () => { runFork(Scope.close(scope, Exit.void)) }
|
return () => { runFork(Scope.close(scope, Exit.void)) }
|
||||||
}, [
|
}, deps && [
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
...(deps ?? []),
|
...deps,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
usePromise<A, E, R>(
|
usePromise<A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
|
||||||
deps?: React.DependencyList,
|
deps?: React.DependencyList,
|
||||||
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
|
||||||
@@ -330,53 +351,109 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
|
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
}, [
|
}, deps && [
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
...(deps ?? []),
|
...deps,
|
||||||
])
|
])
|
||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
useCallbackSync<Args extends unknown[], A, E, R>(
|
useCallbackSync<Args extends unknown[], A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
deps?: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
options?: RenderOptions,
|
options?: RenderOptions,
|
||||||
): (...args: Args) => A {
|
): (...args: Args) => A {
|
||||||
const runSync = this.useRunSync()
|
const runSync = this.useRunSync()
|
||||||
|
|
||||||
return React.useCallback((...args) => runSync(callback(...args)), [
|
return React.useCallback((...args) => runSync(callback(...args)), [
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
|
||||||
...(deps ?? []),
|
...deps,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
useCallbackPromise<Args extends unknown[], A, E, R>(
|
useCallbackPromise<Args extends unknown[], A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
deps?: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
options?: { readonly signal?: AbortSignal } & RenderOptions,
|
options?: { readonly signal?: AbortSignal } & RenderOptions,
|
||||||
): (...args: Args) => Promise<A> {
|
): (...args: Args) => Promise<A> {
|
||||||
const runPromise = this.useRunPromise()
|
const runPromise = this.useRunPromise()
|
||||||
|
|
||||||
return React.useCallback((...args) => runPromise(callback(...args), options), [
|
return React.useCallback((...args) => runPromise(callback(...args), options), [
|
||||||
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise],
|
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise],
|
||||||
...(deps ?? []),
|
...deps,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
useRef<A, R>(
|
useRef<A, E, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
value: A,
|
initialValue: () => Effect.Effect<A, E, R>,
|
||||||
): SubscriptionRef.SubscriptionRef<A> {
|
): SubscriptionRef.SubscriptionRef<A> {
|
||||||
return this.useMemo(
|
return this.useMemo(
|
||||||
() => SubscriptionRef.make(value),
|
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
|
||||||
[],
|
[],
|
||||||
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useRefFromReactiveValue<A, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
value: A,
|
||||||
|
): SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
const ref = this.useRef(() => Effect.succeed(value))
|
||||||
|
this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
useSubRefFromGetSet<A, B, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
getter: (parentValue: B) => A,
|
||||||
|
setter: (parentValue: B, value: A) => B,
|
||||||
|
): SubscriptionSubRef.SubscriptionSubRef<A, B> {
|
||||||
|
return React.useMemo(
|
||||||
|
() => SubscriptionSubRef.makeFromGetSet(parent, getter, setter),
|
||||||
|
[parent],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useSubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
path: P,
|
||||||
|
): SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> {
|
||||||
|
return React.useMemo(
|
||||||
|
() => SubscriptionSubRef.makeFromPath(parent, path),
|
||||||
|
[parent, ...path],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useSubscribeRefs<
|
||||||
|
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
||||||
|
R,
|
||||||
|
>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
...refs: Refs
|
||||||
|
): RefsA<Refs> {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||||
|
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||||
|
) as RefsA<Refs>)
|
||||||
|
|
||||||
|
this.useFork(() => pipe(
|
||||||
|
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
||||||
|
streams => Stream.zipLatestAll(...streams),
|
||||||
|
Stream.runForEach(v =>
|
||||||
|
Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
|
||||||
|
),
|
||||||
|
), refs)
|
||||||
|
|
||||||
|
return reactStateValue
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds the state of a `SubscriptionRef` to the state of the React component.
|
* Binds the state of a `SubscriptionRef` to the state of the React component.
|
||||||
*
|
*
|
||||||
@@ -385,47 +462,185 @@ export abstract class ReffuseHelpers<R> {
|
|||||||
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
|
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
|
||||||
*/
|
*/
|
||||||
useRefState<A, R>(
|
useRefState<A, R>(
|
||||||
this: ReffuseHelpers<R>,
|
this: ReffuseNamespace<R>,
|
||||||
ref: SubscriptionRef.SubscriptionRef<A>,
|
ref: SubscriptionRef.SubscriptionRef<A>,
|
||||||
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
): [A, React.Dispatch<React.SetStateAction<A>>] {
|
||||||
const runSync = this.useRunSync()
|
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||||
|
() => ref,
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||||
|
))
|
||||||
|
|
||||||
const initialState = React.useMemo(() => runSync(ref), [])
|
this.useFork(() => Stream.runForEach(
|
||||||
const [reactStateValue, setReactStateValue] = React.useState(initialState)
|
Stream.changesWith(ref.changes, (x, y) => x === y),
|
||||||
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
), [ref])
|
||||||
|
|
||||||
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
|
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||||
setReactStateValue(v)
|
Ref.update(ref, prevState =>
|
||||||
)), [ref])
|
|
||||||
|
|
||||||
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
|
|
||||||
runSync(Ref.update(ref, prevState =>
|
|
||||||
SetStateAction.value(setStateAction, prevState)
|
SetStateAction.value(setStateAction, prevState)
|
||||||
)),
|
),
|
||||||
[ref])
|
[ref])
|
||||||
|
|
||||||
return [reactStateValue, setValue]
|
return [reactStateValue, setValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useStreamFromReactiveValues<const A extends React.DependencyList, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
values: A,
|
||||||
|
): Stream.Stream<A> {
|
||||||
|
const [, scopeLayer] = this.useScope([], { finalizerExecutionMode: "fork" })
|
||||||
|
|
||||||
|
const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
|
||||||
|
Effect.bind("latest", () => Ref.make(values)),
|
||||||
|
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||||
|
Effect.let("stream", ({ latest, pubsub }) => Ref.get(latest).pipe(
|
||||||
|
Effect.flatMap(a => Effect.map(
|
||||||
|
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||||
|
s => Stream.concat(Stream.make(a), s),
|
||||||
|
)),
|
||||||
|
Stream.unwrapScoped,
|
||||||
|
)),
|
||||||
|
Effect.provide(scopeLayer),
|
||||||
|
), [scopeLayer], { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||||
|
|
||||||
|
this.useEffect(() => Ref.set(latest, values).pipe(
|
||||||
|
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||||
|
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||||
|
), values, { doNotReExecuteOnRuntimeOrContextChange: true })
|
||||||
|
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
|
||||||
|
useSubscribeStream<A, E, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
): Option.Option<A>
|
||||||
|
useSubscribeStream<A, E, IE, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue: () => Effect.Effect<A, IE, R>,
|
||||||
|
): Option.Some<A>
|
||||||
|
useSubscribeStream<A, E, IE, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue?: () => Effect.Effect<A, IE, R>,
|
||||||
|
): Option.Option<A> {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
|
||||||
|
() => initialValue
|
||||||
|
? Effect.map(initialValue(), Option.some)
|
||||||
|
: Effect.succeed(Option.none()),
|
||||||
|
[],
|
||||||
|
{ doNotReExecuteOnRuntimeOrContextChange: true },
|
||||||
|
))
|
||||||
|
|
||||||
|
this.useFork(() => Stream.runForEach(
|
||||||
|
Stream.changesWith(stream, (x, y) => x === y),
|
||||||
|
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||||
|
), [stream])
|
||||||
|
|
||||||
|
return reactStateValue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SubRefFromGetSet<A, B, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
props: {
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
readonly getter: (parentValue: B) => A,
|
||||||
|
readonly setter: (parentValue: B, value: A) => B,
|
||||||
|
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<A, B>) => React.ReactNode
|
||||||
|
},
|
||||||
|
): React.ReactNode {
|
||||||
|
return props.children(this.useSubRefFromGetSet(props.parent, props.getter, props.setter))
|
||||||
|
}
|
||||||
|
|
||||||
|
SubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
props: {
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
readonly path: P,
|
||||||
|
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B>) => React.ReactNode
|
||||||
|
},
|
||||||
|
): React.ReactNode {
|
||||||
|
return props.children(this.useSubRefFromPath(props.parent, props.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscribeRefs<
|
||||||
|
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
|
||||||
|
R,
|
||||||
|
>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
props: {
|
||||||
|
readonly refs: Refs
|
||||||
|
readonly children: (...args: RefsA<Refs>) => React.ReactNode
|
||||||
|
},
|
||||||
|
): React.ReactNode {
|
||||||
|
return props.children(...this.useSubscribeRefs(...props.refs))
|
||||||
|
}
|
||||||
|
|
||||||
|
RefState<A, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
props: {
|
||||||
|
readonly ref: SubscriptionRef.SubscriptionRef<A>
|
||||||
|
readonly children: (state: [A, React.Dispatch<React.SetStateAction<A>>]) => React.ReactNode
|
||||||
|
},
|
||||||
|
): React.ReactNode {
|
||||||
|
return props.children(this.useRefState(props.ref))
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscribeStream<A, E, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
props: {
|
||||||
|
readonly stream: Stream.Stream<A, E, R>
|
||||||
|
readonly children: (latestValue: Option.Option<A>) => React.ReactNode
|
||||||
|
},
|
||||||
|
): React.ReactNode
|
||||||
|
SubscribeStream<A, E, IE, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
props: {
|
||||||
|
readonly stream: Stream.Stream<A, E, R>
|
||||||
|
readonly initialValue: () => Effect.Effect<A, IE, R>
|
||||||
|
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
||||||
|
},
|
||||||
|
): React.ReactNode
|
||||||
|
SubscribeStream<A, E, IE, R>(
|
||||||
|
this: ReffuseNamespace<R>,
|
||||||
|
props: {
|
||||||
|
readonly stream: Stream.Stream<A, E, R>
|
||||||
|
readonly initialValue?: () => Effect.Effect<A, IE, R>
|
||||||
|
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
|
||||||
|
},
|
||||||
|
): React.ReactNode {
|
||||||
|
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ReffuseHelpers<R> extends Pipeable.Pipeable {}
|
export interface ReffuseNamespace<R> extends Pipeable.Pipeable {}
|
||||||
|
|
||||||
ReffuseHelpers.prototype.pipe = function pipe() {
|
ReffuseNamespace.prototype.pipe = function pipe() {
|
||||||
return Pipeable.pipeArguments(this, arguments)
|
return Pipeable.pipeArguments(this, arguments)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable {
|
export interface ReffuseNamespaceClass<R> extends Pipeable.Pipeable {
|
||||||
new(): ReffuseHelpers<R>
|
new(): ReffuseNamespace<R>
|
||||||
|
make<Self>(this: new () => Self): Self
|
||||||
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
|
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
(ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() {
|
(ReffuseNamespace as ReffuseNamespaceClass<any>).make = function make() {
|
||||||
|
return new this()
|
||||||
|
};
|
||||||
|
|
||||||
|
(ReffuseNamespace as ReffuseNamespaceClass<any>).pipe = function pipe() {
|
||||||
return Pipeable.pipeArguments(this, arguments)
|
return Pipeable.pipeArguments(this, arguments)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export const make = (): ReffuseHelpersClass<never> =>
|
export const makeClass = (): ReffuseNamespaceClass<never> => (
|
||||||
class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) {
|
class extends (ReffuseNamespace<never> as ReffuseNamespaceClass<never>) {
|
||||||
static readonly contexts = []
|
static readonly contexts = []
|
||||||
}
|
}
|
||||||
|
)
|
||||||
16
packages/reffuse/src/ReffuseRuntime.ts
Normal file
16
packages/reffuse/src/ReffuseRuntime.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
|
||||||
|
|
||||||
|
export const Provider = function ReffuseRuntimeReactProvider(props: {
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return React.createElement(Context, {
|
||||||
|
...props,
|
||||||
|
value: Runtime.defaultRuntime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRuntime = () => React.useContext(Context)
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Runtime } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
|
|
||||||
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
|
|
||||||
|
|
||||||
export const Provider = (props: { readonly children?: React.ReactNode }) => (
|
|
||||||
<Context
|
|
||||||
{...props}
|
|
||||||
value={Runtime.defaultRuntime}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
Provider.displayName = "ReffuseRuntimeReactProvider"
|
|
||||||
|
|
||||||
export const useRuntime = () => React.useContext(Context)
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
export * as Reffuse from "./Reffuse.js"
|
export * as Reffuse from "./Reffuse.js"
|
||||||
export * as ReffuseContext from "./ReffuseContext.js"
|
export * as ReffuseContext from "./ReffuseContext.js"
|
||||||
export * as ReffuseExtension from "./ReffuseExtension.js"
|
export * as ReffuseExtension from "./ReffuseExtension.js"
|
||||||
export * as ReffuseHelpers from "./ReffuseHelpers.js"
|
export * as ReffuseNamespace from "./ReffuseNamespace.js"
|
||||||
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
export * as ReffuseRuntime from "./ReffuseRuntime.js"
|
||||||
export * as SetStateAction from "./SetStateAction.js"
|
|
||||||
|
|||||||
99
packages/reffuse/src/types/PropertyPath.ts
Normal file
99
packages/reffuse/src/types/PropertyPath.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Array, Function, Option, Predicate } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
|
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
||||||
|
D extends never ? [] :
|
||||||
|
T extends Seen ? [] :
|
||||||
|
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
||||||
|
T extends object ? ObjectPaths<T, D, Seen | T> :
|
||||||
|
never
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
||||||
|
[K in keyof T as K extends number ? K : never]:
|
||||||
|
| [K]
|
||||||
|
| [K, ...Paths<T[K], Prev[D], Seen>]
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
||||||
|
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||||
|
NonNullable<T[K]> extends infer V
|
||||||
|
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
||||||
|
: never
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
||||||
|
? Head extends keyof T
|
||||||
|
? ValueFromPath<T[Head], Tail>
|
||||||
|
: T extends readonly any[]
|
||||||
|
? Head extends number
|
||||||
|
? ValueFromPath<T[number], Tail>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: T
|
||||||
|
|
||||||
|
export type AnyKey = string | number | symbol
|
||||||
|
export type AnyPath = readonly AnyKey[]
|
||||||
|
|
||||||
|
|
||||||
|
export const unsafeGet: {
|
||||||
|
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
||||||
|
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
||||||
|
path.reduce((acc: any, key: any) => acc?.[key], self)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const get: {
|
||||||
|
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
||||||
|
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
||||||
|
path.reduce(
|
||||||
|
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
||||||
|
? Predicate.hasProperty(acc.value, key)
|
||||||
|
? Option.some(acc.value[key])
|
||||||
|
: Option.none()
|
||||||
|
: acc,
|
||||||
|
|
||||||
|
Option.some(self),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const immutableSet: {
|
||||||
|
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
||||||
|
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
||||||
|
const key = Array.head(path as AnyPath)
|
||||||
|
if (Option.isNone(key))
|
||||||
|
return Option.some(value as T)
|
||||||
|
if (!Predicate.hasProperty(self, key.value))
|
||||||
|
return Option.none()
|
||||||
|
|
||||||
|
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
||||||
|
if (Option.isNone(child))
|
||||||
|
return child
|
||||||
|
|
||||||
|
if (Array.isArray(self))
|
||||||
|
return typeof key.value === "number"
|
||||||
|
? Option.some([
|
||||||
|
...self.slice(0, key.value),
|
||||||
|
child.value,
|
||||||
|
...self.slice(key.value + 1),
|
||||||
|
] as T)
|
||||||
|
: Option.none()
|
||||||
|
|
||||||
|
if (typeof self === "object")
|
||||||
|
return Option.some(
|
||||||
|
Object.assign(
|
||||||
|
Object.create(Object.getPrototypeOf(self)),
|
||||||
|
{ ...self, [key.value]: child.value },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Option.none()
|
||||||
|
})
|
||||||
12
packages/reffuse/src/types/SetStateAction.ts
Normal file
12
packages/reffuse/src/types/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
|
||||||
|
)
|
||||||
100
packages/reffuse/src/types/SubscriptionSubRef.ts
Normal file
100
packages/reffuse/src/types/SubscriptionSubRef.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||||
|
import * as PropertyPath from "./PropertyPath.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
||||||
|
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||||
|
|
||||||
|
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
||||||
|
|
||||||
|
readonly [Unify.typeSymbol]?: unknown
|
||||||
|
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
||||||
|
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace SubscriptionSubRef {
|
||||||
|
export interface Variance<in out A, in out B> {
|
||||||
|
readonly [SubscriptionSubRefTypeId]: {
|
||||||
|
readonly _A: Types.Invariant<A>
|
||||||
|
readonly _B: Types.Invariant<B>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
||||||
|
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
||||||
|
SubscriptionRef?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const refVariance = { _A: (_: any) => _ }
|
||||||
|
const synchronizedRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
||||||
|
|
||||||
|
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||||
|
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||||
|
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||||
|
readonly [Ref.RefTypeId] = refVariance
|
||||||
|
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
||||||
|
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
||||||
|
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
||||||
|
|
||||||
|
readonly get: Effect.Effect<A>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
readonly getter: (parentValue: B) => A,
|
||||||
|
readonly setter: (parentValue: B, value: A) => B,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
||||||
|
}
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
return this.get
|
||||||
|
}
|
||||||
|
|
||||||
|
get changes(): Stream.Stream<A> {
|
||||||
|
return this.get.pipe(
|
||||||
|
Effect.map(a => this.parent.changes.pipe(
|
||||||
|
Stream.map(this.getter),
|
||||||
|
s => Stream.concat(Stream.make(a), s),
|
||||||
|
)),
|
||||||
|
Stream.unwrap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
||||||
|
return this.modifyEffect(a => Effect.succeed(f(a)))
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
||||||
|
return Effect.Do.pipe(
|
||||||
|
Effect.bind("b", () => Ref.get(this.parent)),
|
||||||
|
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||||
|
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
||||||
|
Effect.map(({ ca: [c] }) => c),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const makeFromGetSet = <A, B>(
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
getter: (parentValue: B) => A,
|
||||||
|
setter: (parentValue: B, value: A) => B,
|
||||||
|
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
||||||
|
|
||||||
|
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
path: P,
|
||||||
|
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
||||||
|
parent,
|
||||||
|
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||||
|
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
||||||
|
)
|
||||||
3
packages/reffuse/src/types/index.ts
Normal file
3
packages/reffuse/src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * as PropertyPath from "./PropertyPath.js"
|
||||||
|
export * as SetStateAction from "./SetStateAction.js"
|
||||||
|
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||||
@@ -8,14 +8,4 @@ export type CommonKeys<A, B> = Extract<keyof A, keyof B>
|
|||||||
*/
|
*/
|
||||||
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
|
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
|
||||||
|
|
||||||
export type Extend<Super, Self> =
|
|
||||||
Extendable<Super, Self> extends true
|
|
||||||
? Omit<Super, CommonKeys<Self, Super>> & Self
|
|
||||||
: never
|
|
||||||
|
|
||||||
export type Extendable<Super, Self> =
|
|
||||||
Pick<Self, CommonKeys<Self, Super>> extends Pick<Super, CommonKeys<Self, Super>>
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
|
|
||||||
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self
|
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self
|
||||||
@@ -26,7 +26,13 @@
|
|||||||
|
|
||||||
// Build
|
// Build
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "@effect/language-service"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["./src"]
|
"include": ["./src"]
|
||||||
|
|||||||
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