119 Commits

Author SHA1 Message Date
Julien Valverdé
acce65c6a4 Code Narrator test
All checks were successful
Lint / lint (push) Successful in 17s
2025-03-11 03:28:00 +01:00
Julien Valverdé
825de84cef Cleanup
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-11 03:12:36 +01:00
Julien Valverdé
d6011f7897 MutationRunner
Some checks failed
Lint / lint (push) Failing after 18s
2025-03-11 02:17:50 +01:00
Julien Valverdé
8d4bce9e53 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-11 01:45:49 +01:00
Julien Valverdé
a7b5a32071 0.1.3 (#5)
All checks were successful
Publish / publish (push) Successful in 24s
Lint / lint (push) Successful in 16s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/5
2025-03-11 01:44:37 +01:00
Julien Valverdé
f7dd4e51f5 Doc update
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 16s
2025-03-11 01:36:13 +01:00
Julien Valverdé
8772e25ff5 CI update
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-10 18:44:08 +01:00
Julien Valverdé
94a0864132 Query refactoring
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-10 18:37:45 +01:00
Julien Valverdé
be8098fb7d Query work
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-10 01:56:11 +01:00
Julien Valverdé
7021e604ed Cleanup
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-09 19:14:20 +01:00
Julien Valverdé
1fd2a9ffbe Cleanup
All checks were successful
Lint / lint (push) Successful in 17s
2025-03-09 18:35:48 +01:00
Julien Valverdé
1ed73dc3ac Cleanup
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-09 18:21:35 +01:00
Julien Valverdé
c689778cea Working query
All checks were successful
Lint / lint (push) Successful in 15s
2025-03-09 18:08:52 +01:00
Julien Valverdé
da2a32001c Query work
Some checks failed
Lint / lint (push) Failing after 13s
2025-03-08 01:56:50 +01:00
Julien Valverdé
5ac3a932d9 Query work
Some checks failed
Lint / lint (push) Failing after 12s
2025-03-07 23:17:32 +01:00
Julien Valverdé
7935293bc3 Query work
All checks were successful
Lint / lint (push) Successful in 15s
2025-03-07 22:23:44 +01:00
Julien Valverdé
cabceaffcd Key work
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-07 04:26:39 +01:00
Julien Valverdé
d239a11cdc Service query
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-06 20:00:40 +01:00
Julien Valverdé
fad61afce7 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-06 17:32:30 +01:00
Julien Valverdé
11fd4941c0 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-06 17:26:08 +01:00
Julien Valverdé
7bebc39a87 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-06 17:22:27 +01:00
Julien Valverdé
3bc0cc6586 Cleanup
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-06 03:31:21 +01:00
Julien Valverdé
f99d18b846 Cleanup fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-06 03:15:43 +01:00
Julien Valverdé
d61339ea6a Query work
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-05 02:23:43 +01:00
Julien Valverdé
3659d3f342 Version bump
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-05 01:50:08 +01:00
Julien Valverdé
1e8a5d412f Refresh on window focus
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-05 00:44:13 +01:00
Julien Valverdé
86539f33f0 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-05 00:24:38 +01:00
Julien Valverdé
8fa24b1791 Query work
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-05 00:16:13 +01:00
Julien Valverdé
adaadf13b2 Working service query
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-04 23:18:35 +01:00
Julien Valverdé
3af7c3bf7a Query service work
Some checks failed
Lint / lint (push) Failing after 11s
2025-03-04 22:44:40 +01:00
Julien Valverdé
00b7228073 Refetch on focus
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-04 02:15:31 +01:00
Julien Valverdé
c2b2b1b96e Dependencies fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-04 02:04:29 +01:00
Julien Valverdé
74cf37e3a3 Query example
All checks were successful
Lint / lint (push) Successful in 14s
2025-03-04 01:35:52 +01:00
Julien Valverdé
98091d4598 Refactoring
All checks were successful
Lint / lint (push) Successful in 12s
2025-03-04 01:22:51 +01:00
Julien Valverdé
b2f1626268 Working query
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-04 01:19:42 +01:00
Julien Valverdé
40e8bf6a1f Query work
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-03 19:42:33 +01:00
Julien Valverdé
9c96741c8e Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-03 03:37:39 +01:00
Julien Valverdé
3fa9b7d821 Working query
All checks were successful
Lint / lint (push) Successful in 13s
2025-03-02 20:14:45 +01:00
Julien Valverdé
6b0f2f33cb Query work
Some checks failed
Lint / lint (push) Failing after 13s
2025-03-02 02:48:19 +01:00
Julien Valverdé
2e00db5778 Query work
Some checks failed
Lint / lint (push) Failing after 43s
2025-03-02 01:11:18 +01:00
Julien Valverdé
660f32a171 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-02-28 17:24:40 +01:00
Julien Valverdé
3f2639fda1 Query work
All checks were successful
Lint / lint (push) Successful in 13s
2025-02-28 16:08:08 +01:00
Julien Valverdé
f76b3f333a Query work
All checks were successful
Lint / lint (push) Successful in 13s
2025-02-28 02:13:23 +01:00
Julien Valverdé
3b407c6b4f Query work
All checks were successful
Lint / lint (push) Successful in 17s
2025-02-28 01:06:11 +01:00
Julien Valverdé
b01b95a9d5 Query work
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-28 00:27:22 +01:00
Julien Valverdé
91b95ea6af useRefState refactpro
All checks were successful
Lint / lint (push) Successful in 13s
2025-02-27 18:32:57 +01:00
Julien Valverdé
7c99d1ff3d Working useQuery
All checks were successful
Lint / lint (push) Successful in 13s
2025-02-27 01:19:09 +01:00
Julien Valverdé
ae815553f2 Query work
All checks were successful
Lint / lint (push) Successful in 13s
2025-02-26 23:57:03 +01:00
Julien Valverdé
86a96cbcce extension-query
Some checks failed
Lint / lint (push) Failing after 7s
2025-02-26 21:17:09 +01:00
Julien Valverdé
538b3a415d Cleanup
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-26 20:50:05 +01:00
Julien Valverdé
5b023678f4 Merge branch 'master' into next
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-26 20:47:00 +01:00
Julien Valverdé
2aa0c64a7c CI fix
All checks were successful
Lint / lint (push) Successful in 12s
Publish / publish (push) Successful in 18s
2025-02-26 20:45:17 +01:00
Julien Valverdé
52ff7edfa1 CI publish
Some checks failed
Lint / lint (push) Successful in 12s
Publish / publish (push) Failing after 21s
2025-02-26 20:06:33 +01:00
Julien Valverdé
ccb65ec209 Turbo fix
Some checks failed
Lint / lint (push) Successful in 15s
Publish / publish (push) Failing after 13s
2025-02-26 19:49:19 +01:00
Julien Valverdé
47905d86b6 Turbo fix
Some checks failed
Lint / lint (push) Successful in 12s
Publish / publish (push) Failing after 13s
2025-02-26 19:35:22 +01:00
Julien Valverdé
9266697aa4 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-26 19:29:48 +01:00
Julien Valverdé
08f0610752 0.1.2 (#4)
Some checks failed
Publish / publish (push) Failing after 13s
Lint / lint (push) Successful in 12s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/4
2025-02-26 19:27:38 +01:00
Julien Valverdé
ad81bf9ed8 Cleanup
All checks were successful
Lint / lint (push) Successful in 11s
Test build / test-build (pull_request) Successful in 13s
2025-02-26 19:25:21 +01:00
Julien Valverdé
e92087e593 Turbo fix
All checks were successful
Lint / lint (push) Successful in 12s
Test build / test-build (pull_request) Successful in 13s
2025-02-26 19:23:49 +01:00
Julien Valverdé
e182e6ab5c README update
Some checks failed
Lint / lint (push) Successful in 12s
Test build / test-build (pull_request) Failing after 15s
2025-02-26 19:13:49 +01:00
Julien Valverdé
89175be558 README work
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-26 14:30:59 +01:00
Julien Valverdé
4df90a0f1c README work
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-26 14:05:12 +01:00
Julien Valverdé
693c7b2db8 Reffuse context refactoring
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-26 13:40:52 +01:00
Julien Valverdé
5f60d03d83 Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-25 23:19:44 +01:00
Julien Valverdé
ea768218a0 Deps API change
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-25 23:11:58 +01:00
Julien Valverdé
3b4eb750ed Version bump
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-25 22:55:45 +01:00
Julien Valverdé
47aa130486 CI fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-25 22:53:07 +01:00
Julien Valverdé
02da3df8eb CI fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-25 22:45:55 +01:00
Julien Valverdé
8d276d2fbf Dependencies
Some checks failed
Lint / lint (push) Failing after 8s
2025-02-25 22:16:53 +01:00
Julien Valverdé
af077d34aa Turbo setup
Some checks failed
Lint / lint (push) Failing after 13s
2025-02-25 22:07:18 +01:00
Julien Valverdé
618cee4028 Callback tests
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-25 18:39:19 +01:00
Julien Valverdé
8244c34d2a Callback helpers
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-25 18:29:00 +01:00
Julien Valverdé
523d835d00 Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-25 17:14:07 +01:00
Julien Valverdé
15e96b8fa9 Merge branch 'plugins' of git.valverde.cloud:Thilawyn/reffuse into plugins
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-25 14:49:22 +01:00
Julien Valverdé
44de864713 API update 2025-02-25 14:48:58 +01:00
Julien Valverdé
8e1f0a27cf Lockfile
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-25 13:45:09 +01:00
Julien Valverdé
8754020323 Working lazyref extension
Some checks failed
Lint / lint (push) Failing after 12s
2025-02-25 12:17:45 +01:00
Julien Valverdé
d9a01dae0f withLazyRef
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-25 11:22:49 +01:00
Julien Valverdé
8873e81f7c Dependencies
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-25 10:31:43 +01:00
Julien Valverdé
38fcafb15c Dependencies
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-25 10:21:34 +01:00
Julien Valverdé
411397c7de Fix
All checks were successful
Lint / lint (push) Successful in 12s
2025-02-24 21:30:13 +01:00
Julien Valverdé
85e7b54962 extension-lazyref
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-24 21:24:38 +01:00
Julien Valverdé
ce3989ab77 Extension fix
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-24 21:09:44 +01:00
Julien Valverdé
da0f6168f0 Fix
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-24 20:47:49 +01:00
Julien Valverdé
690dec1f1a Finalized
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-24 20:18:56 +01:00
Julien Valverdé
60274266da Extension work
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-24 20:00:02 +01:00
Julien Valverdé
28424b63cb Working extension
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-24 13:47:29 +01:00
Julien Valverdé
e063eb06f7 Extension work
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-24 13:17:10 +01:00
Julien Valverdé
fb5bb7fcef Cleanup
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-24 02:21:37 +01:00
Julien Valverdé
1f57f7d127 Tests
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-24 01:55:47 +01:00
Julien Valverdé
e8742e5aa6 Fix
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-23 23:38:24 +01:00
Julien Valverdé
be79d24d6e Tests
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-22 01:03:15 +01:00
Julien Valverdé
e1349e5e03 Tests
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-21 15:44:28 +01:00
Julien Valverdé
837dcbb1cb Extension work
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-21 15:27:11 +01:00
Julien Valverdé
8252b6cbdf Extension work
Some checks failed
Lint / lint (push) Failing after 10s
2025-02-21 05:22:19 +01:00
Julien Valverdé
256638bc06 ReffuseHelper
Some checks failed
Lint / lint (push) Failing after 11s
2025-02-21 04:22:48 +01:00
Julien Valverdé
c0097bbe81 Extension tests
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-20 14:57:46 +01:00
Julien Valverdé
febeaa05d0 ReffuseExtension
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-20 14:12:56 +01:00
Julien Valverdé
a71640d493 Cleanup
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-20 00:41:37 +01:00
Julien Valverdé
b636a709f3 Tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-20 00:39:15 +01:00
Julien Valverdé
fffbd01b5e Pipeable API tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-20 00:21:43 +01:00
Julien Valverdé
36d5414d10 Fix
All checks were successful
Lint / lint (push) Successful in 44s
2025-02-19 23:59:34 +01:00
Julien Valverdé
65810a6d79 usePromise
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-19 23:44:02 +01:00
Julien Valverdé
9e7b30fbb4 useFork refactoring
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-19 23:24:15 +01:00
Julien Valverdé
6c843562ab usePromiseScoped fork implementation
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-18 23:47:32 +01:00
Julien Valverdé
809f512d11 Fix
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-18 22:33:49 +01:00
Julien Valverdé
e71239b903 usePromiseScoped
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-18 22:28:49 +01:00
Julien Valverdé
bfcc097882 usePromise
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-18 15:25:46 +01:00
Julien Valverdé
933b061b5d Promise tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-18 05:18:34 +01:00
Julien Valverdé
734c84824c Implement Pipeable
All checks were successful
Lint / lint (push) Successful in 10s
2025-02-18 04:30:10 +01:00
Julien Valverdé
e83e86f8f1 Promise tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-18 02:56:05 +01:00
Julien Valverdé
bebbc1d7de Promise tests
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-18 02:23:40 +01:00
Julien Valverdé
a7a0951b61 Dependencies fix
All checks were successful
Lint / lint (push) Successful in 11s
2025-02-18 01:08:26 +01:00
Julien Valverdé
1b1a1961bc Dependencies upgrade
Some checks failed
Lint / lint (push) Failing after 13s
2025-02-17 00:16:41 +01:00
Julien Valverdé
8a9f7ad4c2 0.1.1 (#3)
All checks were successful
Publish / publish (push) Successful in 13s
Lint / lint (push) Successful in 9s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/3
2025-01-22 03:42:21 +01:00
Julien Valverdé
030a032c67 Fix
All checks were successful
Publish / publish (push) Successful in 15s
Lint / lint (push) Successful in 11s
2025-01-18 01:13:38 +01:00
Julien Valverdé
13a60bfdf9 Fix
Some checks failed
Lint / lint (push) Successful in 11s
Publish / publish (push) Failing after 12s
2025-01-18 01:12:08 +01:00
Julien Valverdé
3a34a4f5c7 CI fix (#2)
Some checks failed
Lint / lint (push) Successful in 11s
Publish / publish (push) Failing after 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/2
2025-01-18 01:09:10 +01:00
Julien Valverdé
5430d8daa4 0.1.0 (#1)
Some checks failed
Publish / publish (push) Failing after 13s
Lint / lint (push) Successful in 11s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/1
2025-01-18 00:54:42 +01:00
77 changed files with 3269 additions and 546 deletions

View File

@@ -8,13 +8,9 @@ jobs:
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Clone repo
uses: actions/checkout@v4
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint TypeScript
run: npm run lint:tsc
- name: Build
run: bun run build

View File

@@ -11,18 +11,30 @@ jobs:
steps:
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Clone repo
uses: actions/checkout@v4
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build
run: npm run build
- name: Publish
run: |
npm config set @thilawyn:registry https://git.valverde.cloud/api/packages/thilawyn/npm/
npm config set -- //git.valverde.cloud/api/packages/thilawyn/npm/:_authToken "${{ vars.NODE_AUTH_TOKEN }}"
npm publish
run: bun run build
- name: Publish reffuse
uses: JS-DevTools/npm-publish@v3
with:
package: packages/reffuse
access: public
token: ${{ secrets.NPM_TOKEN }}
registry: https://registry.npmjs.org
- name: Publish @reffuse/extension-lazyref
uses: JS-DevTools/npm-publish@v3
with:
package: packages/extension-lazyref
access: public
token: ${{ secrets.NPM_TOKEN }}
registry: https://registry.npmjs.org
- name: Publish @reffuse/extension-query
uses: JS-DevTools/npm-publish@v3
with:
package: packages/extension-query
access: public
token: ${{ secrets.NPM_TOKEN }}
registry: https://registry.npmjs.org

View File

@@ -18,6 +18,6 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build
run: npm run build
run: bun run build
- name: Pack
run: npm pack --dry-run
run: bun run pack

1
.gitignore vendored
View File

@@ -130,3 +130,4 @@ dist
.yarn/install-state.gz
.pnp.*
.turbo

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
@thilawyn:registry=https://git.valverde.cloud/api/packages/thilawyn/npm/

View File

@@ -1,3 +1,9 @@
# Reffuse
# Reffuse Monorepo
Effect integration for React
Reffuse is a [Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
This monorepo contains:
- [The `reffuse` library](packages/reffuse)
- [`@reffuse/extension-lazyref`, a LazyRef integration for Reffuse](packages/extension-lazyref)
- [`@reffuse/extension-query`, TanStack Query style hooks for Reffuse](packages/extension-query)
- [An example project](packges/example)

1021
bun.lock Normal file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb

Binary file not shown.

2
bunfig.toml Normal file
View File

@@ -0,0 +1,2 @@
[install.scopes]
"@thilawyn" = "https://git.valverde.cloud/api/packages/thilawyn/npm/"

View File

@@ -1,12 +1,24 @@
{
"name": "@reffuse/monorepo",
"packageManager": "bun@1.2.2",
"private": true,
"workspaces": ["./packages/*"],
"workspaces": [
"./packages/*"
],
"scripts": {
"build": "turbo build --filter=!@reffuse/example",
"lint:tsc": "turbo lint:tsc",
"pack": "turbo pack --filter=!@reffuse/example",
"publish": "turbo publish --filter=!@reffuse/example",
"clean:cache": "rm -f tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist",
"clean:node": "rm -rf node_modules"
},
"devDependencies": {
"npm-check-updates": "^17.1.13"
"code-narrator": "^1.0.17",
"npm-check-updates": "^17.1.14",
"npm-sort": "^0.0.4",
"turbo": "^2.4.4",
"typescript": "^5.7.3"
}
}

View File

@@ -1,34 +1,52 @@
{
"name": "example",
"name": "@reffuse/example",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint:tsc": "tsc --noEmit",
"lint:eslint": "eslint .",
"preview": "vite preview"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tanstack/react-router": "^1.95.3",
"@tanstack/router-devtools": "^1.95.3",
"@tanstack/router-plugin": "^1.95.3",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@eslint/js": "^9.21.0",
"@tanstack/react-router": "^1.112.7",
"@tanstack/router-devtools": "^1.112.7",
"@tanstack/router-plugin": "^1.112.7",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"effect": "^3.12.1",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"postcss": "^8.4.49",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
"typescript-eslint": "^8.26.0",
"vite": "^6.2.0"
},
"dependencies": {
"@effect/platform": "^0.77.6",
"@effect/platform-browser": "^0.56.6",
"@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.1",
"@typed/lazy-ref": "^0.3.3",
"effect": "^3.13.6",
"lucide-react": "^0.477.0",
"mobx": "^6.13.6",
"reffuse": "workspace:*"
},
"overrides": {
"effect": "^3.13.6",
"@effect/platform": "^0.77.6",
"@effect/platform-browser": "^0.56.6",
"@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}

View File

@@ -1,8 +0,0 @@
import { Schema } from "effect"
export class Post extends Schema.Class<Post>("Post")({
id: Schema.String,
title: Schema.String,
content: Schema.String,
}) {}

View File

@@ -0,0 +1,26 @@
import { ThSchema } from "@thilawyn/thilaschema"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect, Schema } from "effect"
export class Todo extends Schema.Class<Todo>("Todo")({
_tag: Schema.tag("Todo"),
id: Schema.String,
content: Schema.String,
completedAt: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
}) {}
export const TodoFromJsonStruct = Schema.Struct({
...Todo.fields,
completedAt: Schema.Option(Schema.DateTimeUtc),
}).pipe(
ThSchema.assertEncodedJsonifiable
)
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
export const generateUniqueID = makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom)
)

View File

@@ -1 +1 @@
export * as Post from "./Post"
export * as Todo from "./Todo"

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,15 +1,19 @@
import { FetchHttpClient } from "@effect/platform"
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { createRouter, RouterProvider } from "@tanstack/react-router"
import { Layer } from "effect"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import "./index.css"
import { Reffuse } from "./reffuse"
import { ReffuseRuntime } from "reffuse"
import { GlobalContext } from "./reffuse"
import { routeTree } from "./routeTree.gen"
import { FetchData } from "./services"
const layer = Layer.empty.pipe(
Layer.provideMerge(FetchData.mockLayer)
Layer.provideMerge(Clipboard.layer),
Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer),
Layer.provideMerge(FetchHttpClient.layer),
)
const router = createRouter({ routeTree })
@@ -23,8 +27,10 @@ declare module "@tanstack/react-router" {
createRoot(document.getElementById("root")!).render(
<StrictMode>
<Reffuse.Provider layer={layer}>
<RouterProvider router={router} />
</Reffuse.Provider>
<ReffuseRuntime.Provider>
<GlobalContext.Provider layer={layer}>
<RouterProvider router={router} />
</GlobalContext.Provider>
</ReffuseRuntime.Provider>
</StrictMode>
)

View File

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

View File

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

View File

@@ -0,0 +1 @@
export * as Uuid4Query from "./Uuid4Query"

View File

@@ -0,0 +1,32 @@
import { Button, Container, Flex, Text } from "@radix-ui/themes"
import * as AsyncData from "@typed/async-data"
import { R } from "../reffuse"
import { Uuid4Query } from "../services"
export function Uuid4QueryService() {
const runSync = R.useRunSync()
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
const [state] = R.useRefState(query.state)
return (
<Container>
<Flex direction="column" align="center" gap="2">
<Text>
{AsyncData.match(state, {
NoData: () => "No data yet",
Loading: () => "Loading...",
Success: (value, { isRefreshing, isOptimistic }) =>
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
Failure: (cause, { isRefreshing }) =>
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
})}
</Text>
<Button onClick={() => runSync(query.refresh)}>Refresh</Button>
</Flex>
</Container>
)
}

View File

@@ -1,5 +1,21 @@
import { make } from "@thilawyn/reffuse/Reffuse"
import { FetchData } from "./services"
import { HttpClient } from "@effect/platform"
import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { LazyRefExtension } from "@reffuse/extension-lazyref"
import { QueryExtension } from "@reffuse/extension-query"
import { Reffuse, ReffuseContext } from "reffuse"
export const Reffuse = make<FetchData.FetchData>()
export const GlobalContext = ReffuseContext.make<
| Clipboard.Clipboard
| Geolocation.Geolocation
| Permissions.Permissions
| HttpClient.HttpClient
>()
export class GlobalReffuse extends Reffuse.Reffuse.pipe(
Reffuse.withExtension(LazyRefExtension),
Reffuse.withExtension(QueryExtension),
Reffuse.withContexts(GlobalContext),
) {}
export const R = new GlobalReffuse()

View File

@@ -12,8 +12,14 @@
import { Route as rootRoute } from './routes/__root'
import { Route as TimeImport } from './routes/time'
import { Route as TestsImport } from './routes/tests'
import { Route as PromiseImport } from './routes/promise'
import { Route as LazyrefImport } from './routes/lazyref'
import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank'
import { Route as IndexImport } from './routes/index'
import { Route as QueryUsequeryImport } from './routes/query/usequery'
import { Route as QueryServiceImport } from './routes/query/service'
// Create/Update Routes
@@ -23,18 +29,54 @@ const TimeRoute = TimeImport.update({
getParentRoute: () => rootRoute,
} as any)
const TestsRoute = TestsImport.update({
id: '/tests',
path: '/tests',
getParentRoute: () => rootRoute,
} as any)
const PromiseRoute = PromiseImport.update({
id: '/promise',
path: '/promise',
getParentRoute: () => rootRoute,
} as any)
const LazyrefRoute = LazyrefImport.update({
id: '/lazyref',
path: '/lazyref',
getParentRoute: () => rootRoute,
} as any)
const CountRoute = CountImport.update({
id: '/count',
path: '/count',
getParentRoute: () => rootRoute,
} as any)
const BlankRoute = BlankImport.update({
id: '/blank',
path: '/blank',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
const QueryUsequeryRoute = QueryUsequeryImport.update({
id: '/query/usequery',
path: '/query/usequery',
getParentRoute: () => rootRoute,
} as any)
const QueryServiceRoute = QueryServiceImport.update({
id: '/query/service',
path: '/query/service',
getParentRoute: () => rootRoute,
} as any)
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@@ -46,6 +88,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/blank': {
id: '/blank'
path: '/blank'
fullPath: '/blank'
preLoaderRoute: typeof BlankImport
parentRoute: typeof rootRoute
}
'/count': {
id: '/count'
path: '/count'
@@ -53,6 +102,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CountImport
parentRoute: typeof rootRoute
}
'/lazyref': {
id: '/lazyref'
path: '/lazyref'
fullPath: '/lazyref'
preLoaderRoute: typeof LazyrefImport
parentRoute: typeof rootRoute
}
'/promise': {
id: '/promise'
path: '/promise'
fullPath: '/promise'
preLoaderRoute: typeof PromiseImport
parentRoute: typeof rootRoute
}
'/tests': {
id: '/tests'
path: '/tests'
fullPath: '/tests'
preLoaderRoute: typeof TestsImport
parentRoute: typeof rootRoute
}
'/time': {
id: '/time'
path: '/time'
@@ -60,6 +130,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TimeImport
parentRoute: typeof rootRoute
}
'/query/service': {
id: '/query/service'
path: '/query/service'
fullPath: '/query/service'
preLoaderRoute: typeof QueryServiceImport
parentRoute: typeof rootRoute
}
'/query/usequery': {
id: '/query/usequery'
path: '/query/usequery'
fullPath: '/query/usequery'
preLoaderRoute: typeof QueryUsequeryImport
parentRoute: typeof rootRoute
}
}
}
@@ -67,42 +151,100 @@ declare module '@tanstack/react-router' {
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/count': typeof CountRoute
'/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute
'/tests': typeof TestsRoute
'/time': typeof TimeRoute
'/query/service': typeof QueryServiceRoute
'/query/usequery': typeof QueryUsequeryRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/count': typeof CountRoute
'/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute
'/tests': typeof TestsRoute
'/time': typeof TimeRoute
'/query/service': typeof QueryServiceRoute
'/query/usequery': typeof QueryUsequeryRoute
}
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/count': typeof CountRoute
'/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute
'/tests': typeof TestsRoute
'/time': typeof TimeRoute
'/query/service': typeof QueryServiceRoute
'/query/usequery': typeof QueryUsequeryRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/count' | '/time'
fullPaths:
| '/'
| '/blank'
| '/count'
| '/lazyref'
| '/promise'
| '/tests'
| '/time'
| '/query/service'
| '/query/usequery'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/count' | '/time'
id: '__root__' | '/' | '/count' | '/time'
to:
| '/'
| '/blank'
| '/count'
| '/lazyref'
| '/promise'
| '/tests'
| '/time'
| '/query/service'
| '/query/usequery'
id:
| '__root__'
| '/'
| '/blank'
| '/count'
| '/lazyref'
| '/promise'
| '/tests'
| '/time'
| '/query/service'
| '/query/usequery'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
BlankRoute: typeof BlankRoute
CountRoute: typeof CountRoute
LazyrefRoute: typeof LazyrefRoute
PromiseRoute: typeof PromiseRoute
TestsRoute: typeof TestsRoute
TimeRoute: typeof TimeRoute
QueryServiceRoute: typeof QueryServiceRoute
QueryUsequeryRoute: typeof QueryUsequeryRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BlankRoute: BlankRoute,
CountRoute: CountRoute,
LazyrefRoute: LazyrefRoute,
PromiseRoute: PromiseRoute,
TestsRoute: TestsRoute,
TimeRoute: TimeRoute,
QueryServiceRoute: QueryServiceRoute,
QueryUsequeryRoute: QueryUsequeryRoute,
}
export const routeTree = rootRoute
@@ -116,18 +258,42 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
"/blank",
"/count",
"/time"
"/lazyref",
"/promise",
"/tests",
"/time",
"/query/service",
"/query/usequery"
]
},
"/": {
"filePath": "index.tsx"
},
"/blank": {
"filePath": "blank.tsx"
},
"/count": {
"filePath": "count.tsx"
},
"/lazyref": {
"filePath": "lazyref.tsx"
},
"/promise": {
"filePath": "promise.tsx"
},
"/tests": {
"filePath": "tests.tsx"
},
"/time": {
"filePath": "time.tsx"
},
"/query/service": {
"filePath": "query/service.tsx"
},
"/query/usequery": {
"filePath": "query/usequery.tsx"
}
}
}

View File

@@ -1,20 +1,32 @@
import { Container, Flex, Theme } from "@radix-ui/themes"
import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools"
import "@radix-ui/themes/styles.css"
import "../index.css"
export const Route = createRootRoute({
component: Root
})
function Root() {
return <>
<div className="container flex-row gap-2 justify-center items-center mx-auto mb-4">
<Link to="/">Index</Link>
<Link to="/time">Time</Link>
<Link to="/count">Count</Link>
</div>
return (
<Theme>
<Container>
<Flex direction="row" justify="center" align="center" gap="2">
<Link to="/">Index</Link>
<Link to="/time">Time</Link>
<Link to="/count">Count</Link>
<Link to="/tests">Tests</Link>
<Link to="/promise">Promise</Link>
<Link to="/query/usequery">Query</Link>
<Link to="/blank">Blank</Link>
</Flex>
</Container>
<Outlet />
<TanStackRouterDevtools />
</>
<Outlet />
<TanStackRouterDevtools />
</Theme>
)
}

View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/blank')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/blank"!</div>
}

View File

@@ -1,4 +1,4 @@
import { Reffuse } from "@/reffuse"
import { R } from "@/reffuse"
import { createFileRoute } from "@tanstack/react-router"
import { Ref } from "effect"
@@ -9,10 +9,10 @@ export const Route = createFileRoute("/count")({
function Count() {
const runSync = Reffuse.useRunSync()
const runSync = R.useRunSync()
const countRef = Reffuse.useRef(0)
const [count] = Reffuse.useRefState(countRef)
const countRef = R.useRef(0)
const [count] = R.useRefState(countRef)
return (

View File

@@ -1,10 +1,10 @@
import { Reffuse } from "@/reffuse"
import { FetchData } from "@/services"
import { Reffuse as PostsReffuse } from "@/views/posts/reffuse"
import { PostsState } from "@/views/posts/services"
import { VPosts } from "@/views/posts/VPosts"
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 { Effect } from "effect"
import { Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/")({
@@ -13,18 +13,17 @@ export const Route = createFileRoute("/")({
function Index() {
const postsLayer = Reffuse.useMemo(FetchData.FetchData.pipe(
Effect.flatMap(({ fetchPosts }) => fetchPosts),
Effect.map(PostsState.make),
))
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos"))
), [])
return (
<div className="container mx-auto">
<PostsReffuse.Provider layer={postsLayer}>
<VPosts />
</PostsReffuse.Provider>
</div>
<Container>
<TodosContext.Provider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</Container>
)
}

View File

@@ -0,0 +1,31 @@
import { R } from "@/reffuse"
import { Button, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import * as LazyRef from "@typed/lazy-ref"
import { Suspense, use } from "react"
export const Route = createFileRoute("/lazyref")({
component: RouteComponent
})
function RouteComponent() {
const promise = R.usePromise(() => LazyRef.of(0), [])
return (
<Suspense fallback={<Text>Loading...</Text>}>
<LazyRefComponent promise={promise} />
</Suspense>
)
}
function LazyRefComponent({ promise }: { readonly promise: Promise<LazyRef.LazyRef<number>> }) {
const ref = use(promise)
const [value, setValue] = R.useLazyRefState(ref)
return (
<Button onClick={() => setValue(prev => prev + 1)}>
{value}
</Button>
)
}

View File

@@ -0,0 +1,35 @@
import { R } from "@/reffuse"
import { HttpClient } from "@effect/platform"
import { Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Schema } from "effect"
import { Suspense, use } from "react"
export const Route = createFileRoute("/promise")({
component: RouteComponent
})
const Result = Schema.Tuple(Schema.String)
type Result = typeof Result.Type
function RouteComponent() {
const promise = R.usePromise(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
Effect.andThen(HttpClient.get("https://www.uuidtools.com/api/generate/v4")),
HttpClient.withTracerPropagation(false),
Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)),
), [])
return (
<Suspense fallback={<Text>Loading...</Text>}>
<AsyncComponent promise={promise} />
</Suspense>
)
}
function AsyncComponent({ promise }: { readonly promise: Promise<Result> }) {
const [uuid] = use(promise)
return <Text>{uuid}</Text>
}

View File

@@ -0,0 +1,35 @@
import { QueryContext } from "@/query/reffuse"
import { Uuid4Query } from "@/query/services"
import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
import { R } from "@/reffuse"
import { HttpClient } from "@effect/platform"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Schema } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/query/service")({
component: RouteComponent
})
function RouteComponent() {
const query = R.useQuery({
key: R.useStreamFromValues(["uuid4", 10 as number]),
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(Effect.sleep("500 millis")),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
HttpClient.withTracerPropagation(false),
Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
Effect.scoped,
),
})
const layer = useMemo(() => query.layer(Uuid4Query.Uuid4Query), [query])
return (
<QueryContext.Provider layer={layer}>
<Uuid4QueryService />
</QueryContext.Provider>
)
}

View File

@@ -0,0 +1,66 @@
import { R } from "@/reffuse"
import { HttpClient } from "@effect/platform"
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import * as AsyncData from "@typed/async-data"
import { Array, Console, Effect, flow, Option, Schema } from "effect"
import { useState } from "react"
export const Route = createFileRoute("/query/usequery")({
component: RouteComponent
})
const Result = Schema.Array(Schema.String)
function RouteComponent() {
const runSync = R.useRunSync()
const [count, setCount] = useState(1)
const query = R.useQuery({
key: R.useStreamFromValues(["uuid4", count]),
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(Effect.sleep("500 millis")),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
HttpClient.withTracerPropagation(false),
Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)),
Effect.scoped,
),
})
const [state] = R.useRefState(query.state)
return (
<Container>
<Flex direction="column" align="center" gap="2">
<Slider
min={1}
max={100}
value={[count]}
onValueChange={flow(
Array.head,
Option.getOrThrow,
setCount,
)}
/>
<Text>
{AsyncData.match(state, {
NoData: () => "No data yet",
Loading: () => "Loading...",
Success: (value, { isRefreshing, isOptimistic }) =>
`Value: ${value} ${isRefreshing ? "(refreshing)" : ""} ${isOptimistic ? "(optimistic)" : ""}`,
Failure: (cause, { isRefreshing }) =>
`Error: ${cause} ${isRefreshing ? "(refreshing)" : ""}`,
})}
</Text>
<Button onClick={() => runSync(query.refresh)}>Refresh</Button>
</Flex>
</Container>
)
}

View File

@@ -0,0 +1,46 @@
import { R } from "@/reffuse"
import { Button, Flex } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Stream } from "effect"
import { useState } from "react"
export const Route = createFileRoute("/tests")({
component: RouteComponent
})
function RouteComponent() {
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
// Effect.andThen(makeUuid4),
// Effect.provide(GetRandomValues.CryptoRandom),
// ), [])
// console.log(value)
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
Effect.andThen(Console.log("ouient")),
Effect.delay("1 second"),
), [])
const [reactValue, setReactValue] = useState("initial")
const reactValueStream = R.useStreamFromValues([reactValue])
R.useFork(() => Stream.runForEach(reactValueStream, Console.log), [reactValueStream])
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
yield* Effect.log(value)
}), [])
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom),
Effect.map(setReactValue),
), [])
return (
<Flex direction="row" justify="center" align="center" gap="2">
<Button onClick={() => logValue("test")}>Log value</Button>
<Button onClick={() => generateUuid()}>Generate UUID</Button>
</Flex>
)
}

View File

@@ -1,6 +1,6 @@
import { Reffuse } from "@/reffuse"
import { R } from "@/reffuse"
import { createFileRoute } from "@tanstack/react-router"
import { Console, DateTime, Effect, Ref, Schedule, Stream } from "effect"
import { Console, DateTime, Effect, Ref, Schedule, Stream, SubscriptionRef } from "effect"
const timeEverySecond = Stream.repeatEffectWithSchedule(
@@ -15,23 +15,13 @@ export const Route = createFileRoute("/time")({
function Time() {
const timeRef = Reffuse.useRefFromEffect(DateTime.now)
const timeRef = R.useMemo(() => DateTime.now.pipe(Effect.flatMap(SubscriptionRef.make)), [])
Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
Effect.flatMap(() =>
Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v))
)
R.useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
Effect.andThen(Stream.runForEach(timeEverySecond, v => Ref.set(timeRef, v)))
), [timeRef])
// Reffuse.useFork(Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
// Effect.flatMap(() => DateTime.now),
// Effect.flatMap(v => Ref.set(timeRef, v)),
// Effect.repeat(Schedule.intersect(
// Schedule.forever,
// Schedule.spaced("1 second"),
// )),
// ), [timeRef])
const [time] = Reffuse.useRefState(timeRef)
const [time] = R.useRefState(timeRef)
return (

View File

@@ -1,24 +0,0 @@
import { Post } from "@/domain"
import { Chunk, Context, Effect, Layer } from "effect"
export class FetchData extends Context.Tag("FetchData")<FetchData, {
readonly fetchPosts: Effect.Effect<Chunk.Chunk<Post.Post>>
}>() {}
export const mockLayer = Layer.succeed(FetchData, {
fetchPosts: Effect.succeed(Chunk.make(
Post.Post.make({
id: "1",
title: "Lorem ipsum dolor sit amet",
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget lacus sit amet diam suscipit porttitor non at felis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla risus ligula, elementum nec scelerisque eget, volutpat vel sapien. Phasellus aliquam ac neque vitae sodales. Nunc sodales congue odio. Nulla eget nisl cursus, convallis lorem at, varius lectus. Aliquam vitae mauris vel mi dignissim condimentum. Proin sed dignissim sapien, ut cursus ex. Donec eget sapien sagittis, auctor metus vitae, fringilla lacus. Donec ut elit a quam aliquet consectetur interdum eu nisl. Etiam nec convallis purus, eu venenatis nulla. Phasellus non metus id mauris tincidunt cursus. Cras varius aliquet diam eu blandit. In hac habitasse platea dictumst.",
}),
Post.Post.make({
id: "2",
title: "Vestibulum non bibendum ligula",
content: "Vestibulum non bibendum ligula. Integer pellentesque, diam ac faucibus volutpat, nulla libero porttitor nunc, ac pulvinar tortor diam id ipsum. Sed id enim at odio euismod imperdiet et ac purus. Etiam tempus ipsum semper scelerisque mollis. Integer auctor, magna et tristique tempus, nisi mi euismod est, nec finibus quam nunc nec libero. Maecenas aliquet viverra magna, vitae blandit ligula pharetra id. Vestibulum vel lacus at nibh placerat tincidunt. Sed suscipit tellus vel felis euismod, et sollicitudin neque cursus. Curabitur dapibus eros vitae ligula suscipit, at facilisis risus venenatis. Sed pharetra blandit pulvinar. Vivamus vestibulum at ligula pulvinar fringilla. Suspendisse vel mattis libero, eget vulputate massa. Vivamus vehicula, lectus id tempor maximus, erat tortor blandit purus, at scelerisque nunc urna faucibus sapien.",
}),
))
})

View File

@@ -1 +1 @@
export * as FetchData from "./FetchData"
export {}

View File

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

View File

@@ -0,0 +1,69 @@
import { Todo } from "@/domain"
import { KeyValueStore } from "@effect/platform"
import { BrowserKeyValueStore } from "@effect/platform-browser"
import { PlatformError } from "@effect/platform/Error"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
export class TodosState extends Context.Tag("TodosState")<TodosState, {
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
readonly remove: (index: number) => Effect.Effect<void>
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
}>() {}
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => kv.get(key)),
Effect.flatMap(identity),
Effect.flatMap(Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.decode,
)),
Effect.flatMap(v => Ref.set(todos, v)),
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
Effect.flatMap(([kv, values]) => values.pipe(
Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.encode,
),
Effect.flatMap(v => kv.set(key, v)),
)),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
// const moveUp = (index: number) => Effect.gen(function*() {
// })
yield* readFromLocalStorage
return {
todos,
readFromLocalStorage,
saveToLocalStorage,
prepend,
replace,
remove,
}
}))

View File

@@ -0,0 +1 @@
export * as TodosState from "./TodosState"

View File

@@ -0,0 +1,53 @@
import { Todo } from "@/domain"
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
import { Effect, Option, SubscriptionRef } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
const createEmptyTodo = Todo.generateUniqueID.pipe(
Effect.map(id => Todo.Todo.make({
id,
content: "",
completedAt: Option.none(),
}, true))
)
export function VNewTodo() {
const runSync = R.useRunSync()
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)), [])
const [todo, setTodo] = R.useRefState(todoRef)
return (
<Box>
<Card>
<Flex direction="column" align="stretch" gap="2">
<TextArea
value={todo.content}
onChange={e => setTodo(prev =>
Todo.Todo.make({ ...prev, content: e.target.value }, true)
)}
/>
<Flex direction="row" justify="center" align="center">
<Button
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.prepend(todo)),
Effect.andThen(createEmptyTodo),
Effect.map(setTodo),
runSync,
)}
>
Add
</Button>
</Flex>
</Flex>
</Card>
</Box>
)
}

View File

@@ -0,0 +1,56 @@
import { Todo } from "@/domain"
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
import { Effect } from "effect"
import { Delete } from "lucide-react"
import { useState } from "react"
import { R } from "../reffuse"
import { TodosState } from "../services"
export interface VTodoProps {
readonly index: number
readonly todo: Todo.Todo
}
export function VTodo({ index, todo }: VTodoProps) {
const runSync = R.useRunSync()
const editorMode = useState(false)
return (
<Box>
<Card>
<Flex direction="column" align="stretch" gap="1">
<TextArea
value={todo.content}
onChange={e => TodosState.TodosState.pipe(
Effect.flatMap(state => state.replace(
index,
Todo.Todo.make({ ...todo, content: e.target.value }, true),
)),
runSync,
)}
disabled={!editorMode}
/>
<Flex direction="row" justify="between" align="center">
<Box></Box>
<Flex direction="row" align="center" gap="1">
<IconButton
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
<Delete />
</IconButton>
</Flex>
</Flex>
</Flex>
</Card>
</Box>
)
}

View File

@@ -0,0 +1,36 @@
import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo"
import { VTodo } from "./VTodo"
export function VTodos() {
// Sync changes to the todos with the local storage
R.useFork(() => TodosState.TodosState.pipe(
Effect.flatMap(state =>
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
)
), [])
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
const [todos] = R.useRefState(todosRef)
return (
<Flex direction="column" align="center" gap="3">
<Box width="500px">
<VNewTodo />
</Box>
{Chunk.map(todos, (todo, index) => (
<Box key={todo.id} width="500px">
<VTodo index={index} todo={todo} />
</Box>
))}
</Flex>
)
}

View File

@@ -1,34 +0,0 @@
import { Post } from "@/domain"
import { Effect } from "effect"
import { PostsState } from "../posts/services"
import { Reffuse } from "./reffuse"
export interface VPostProps {
readonly index: number
readonly post: Post.Post
}
export function VPost({ post, index }: VPostProps) {
const runSync = Reffuse.useRunSync()
return (
<div className="flex-col gap-1 items-stretch">
<p>{post.title}</p>
<p>{post.content}</p>
<button
onClick={() => PostsState.PostsState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
X
</button>
</div>
)
}

View File

@@ -1 +0,0 @@
export { Reffuse } from "../posts/reffuse"

View File

@@ -1,25 +0,0 @@
import { Chunk } from "effect"
import { VPost } from "../post/VPost"
import { Reffuse } from "./reffuse"
import { PostsState } from "./services"
export function VPosts() {
const state = Reffuse.useMemo(PostsState.PostsState)
const [posts] = Reffuse.useRefState(state.posts)
return (
<div className="flex-col gap-2 items-stretch">
{Chunk.map(posts, (post, index) => (
<VPost
key={`${ index }-${ post.id }`}
index={index}
post={post}
/>
))}
</div>
)
}

View File

@@ -1,5 +0,0 @@
import { Reffuse as RootReffuse } from "@/reffuse"
import { PostsState } from "./services"
export const Reffuse = RootReffuse.extend<PostsState.PostsState>()

View File

@@ -1,15 +0,0 @@
import { Post } from "@/domain"
import { Chunk, Context, Effect, Layer, Ref, SubscriptionRef } from "effect"
export class PostsState extends Context.Tag("PostsState")<PostsState, {
readonly posts: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Post.Post>>
readonly remove: (index: number) => Effect.Effect<void>
}>() {}
export const make = (posts: Chunk.Chunk<Post.Post>) => Layer.effect(PostsState, SubscriptionRef.make(posts).pipe(
Effect.map(posts => ({
posts,
remove: (index: number) => Ref.update(posts, Chunk.remove(index)),
}))
))

View File

@@ -1 +0,0 @@
export * as PostsState from "./PostsState"

View File

@@ -1,11 +0,0 @@
/** @type {import("tailwindcss").Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,9 @@
# LazyRef extension for Reffuse
Extension to integrate `@typed/lazy-ref` with Reffuse.
## Peer dependencies
- `@typed/lazy-ref`
- `reffuse` 0.1.3+
- `effect` 3.13+
- `react` & `@types/react` 19+

View File

@@ -0,0 +1,42 @@
{
"name": "@reffuse/extension-lazyref",
"version": "0.1.0",
"type": "module",
"files": [
"./README.md",
"./dist"
],
"license": "MIT",
"repository": {
"url": "git+https://github.com/Thiladev/reffuse.git"
},
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
},
"scripts": {
"build": "tsc",
"lint:tsc": "tsc --noEmit",
"pack": "npm pack",
"clean:cache": "rm -f tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist",
"clean:node": "rm -rf node_modules"
},
"devDependencies": {
"reffuse": "workspace:*"
},
"peerDependencies": {
"@typed/lazy-ref": "^0.3.0",
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"react": "^19.0.0",
"reffuse": "^0.1.3"
}
}

View File

@@ -0,0 +1,27 @@
import * as LazyRef from "@typed/lazy-ref"
import { Effect, Stream } from "effect"
import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse"
export const LazyRefExtension = ReffuseExtension.make(() => ({
useLazyRefState<A, E, R>(
this: ReffuseHelpers.ReffuseHelpers<R>,
ref: LazyRef.LazyRef<A, E, R>,
): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
const [reactStateValue, setReactStateValue] = React.useState(initialState)
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
setReactStateValue(v)
)), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
LazyRef.update(ref, prevState =>
SetStateAction.value(setStateAction, prevState)
),
[ref])
return [reactStateValue, setValue]
},
}))

View 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"]
}

View 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`

View File

@@ -0,0 +1,44 @@
{
"name": "@reffuse/extension-query",
"version": "0.1.0",
"type": "module",
"files": [
"./README.md",
"./dist"
],
"license": "MIT",
"repository": {
"url": "git+https://github.com/Thiladev/reffuse.git"
},
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
},
"scripts": {
"build": "tsc",
"lint:tsc": "tsc --noEmit",
"pack": "npm pack",
"clean:cache": "rm -f tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist",
"clean:node": "rm -rf node_modules"
},
"devDependencies": {
"reffuse": "workspace:*"
},
"peerDependencies": {
"@effect/platform": "^0.77.0",
"@effect/platform-browser": "^0.56.0",
"@typed/async-data": "^0.13.0",
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"react": "^19.0.0",
"reffuse": "^0.1.3"
}
}

View File

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

View File

@@ -0,0 +1,55 @@
import type * as AsyncData from "@typed/async-data"
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers } from "reffuse"
import * as QueryRunner from "./QueryRunner.js"
import type * as QueryService from "./QueryService.js"
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R>
readonly refreshOnWindowFocus?: boolean
}
export interface UseQueryResult<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
) => Layer.Layer<Self>
}
export const QueryExtension = ReffuseExtension.make(() => ({
useQuery<K extends readonly unknown[], A, E, R>(
this: ReffuseHelpers.ReffuseHelpers<R>,
props: UseQueryProps<K, A, E, R>,
): UseQueryResult<K, A, E> {
const runner = this.useMemo(() => QueryRunner.make({
key: props.key,
query: props.query,
}), [props.key])
this.useFork(() => runner.fetchOnKeyChange, [runner])
this.useFork(() => (props.refreshOnWindowFocus ?? true)
? runner.refreshOnWindowFocus
: Effect.void,
[props.refreshOnWindowFocus, runner])
return React.useMemo(() => ({
latestKey: runner.latestKeyRef,
state: runner.stateRef,
refresh: runner.forkRefresh,
layer: tag => Layer.succeed(tag, {
latestKey: runner.latestKeyRef,
state: runner.stateRef,
refresh: runner.forkRefresh,
}),
}), [runner])
}
}))

View File

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

View File

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

View File

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

View 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"]
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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+

View File

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

View File

@@ -1,10 +1,15 @@
{
"name": "@thilawyn/reffuse",
"version": "0.1.0",
"name": "reffuse",
"version": "0.1.3",
"type": "module",
"files": [
"./README.md",
"./dist"
],
"license": "MIT",
"repository": {
"url": "git+https://github.com/Thiladev/reffuse.git"
},
"types": "./dist/index.d.ts",
"exports": {
".": {
@@ -19,14 +24,14 @@
"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": {
"@types/react": "^19.0.4",
"effect": "^3.12.1",
"react": "^19.0.0",
"typescript": "^5.7.3"
"peerDependencies": {
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"react": "^19.0.0"
}
}

View File

@@ -1,182 +1,47 @@
import { Context, Effect, Fiber, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import React from "react"
import * as ReffuseReactContext from "./ReffuseReactContext.js"
import type * as ReffuseContext from "./ReffuseContext.js"
import type * as ReffuseExtension from "./ReffuseExtension.js"
import * as ReffuseHelpers from "./ReffuseHelpers.js"
import type { Merge, StaticType } from "./types.js"
export class Reffuse<
RuntimeR,
ContextR extends ParentContextR | OwnContextR,
OwnContextR,
ParentContextR = never,
> {
export class Reffuse extends ReffuseHelpers.make() {}
readonly Context = React.createContext<ReffuseReactContext.Value<RuntimeR, ContextR>>(null!)
readonly Provider: ReffuseReactContext.Provider<RuntimeR, OwnContextR, ParentContextR>
constructor(
private readonly runtime: Runtime.Runtime<RuntimeR>,
parent?: Reffuse<RuntimeR, ParentContextR, unknown, unknown>,
) {
this.Provider = parent
? ReffuseReactContext.makeNestedProvider(runtime, this.Context, parent)
: ReffuseReactContext.makeRootProvider(runtime, this.Context)
export const withContexts = <R2 extends Array<unknown>>(
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
) =>
<
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R1>,
R1
>(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R1>
): (
{
new(): Merge<
InstanceType<BaseClass>,
{ constructor: ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]> }
>
} &
Merge<
StaticType<BaseClass>,
StaticType<ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]>>
>
) => class extends self {
static readonly contexts = [...self.contexts, ...contexts]
} as any
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) =>
<
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R>,
R
>(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R>
): (
{ new(): Merge<InstanceType<BaseClass>, A> } &
StaticType<BaseClass>
) => {
const class_ = class extends self {}
Object.assign(class_.prototype, extension())
return class_ as any
}
extend<OwnContextR = never>() {
return new Reffuse<
RuntimeR,
ContextR | OwnContextR,
OwnContextR,
ContextR
>(this.runtime, this)
}
useRuntime(): Runtime.Runtime<RuntimeR> {
return React.useContext(this.Context).runtime
}
useContext(): Context.Context<ContextR> {
return React.useContext(this.Context).context
}
useRunSync() {
const { runtime, context } = React.useContext(this.Context)
return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>
): A => effect.pipe(
Effect.provide(context),
Runtime.runSync(runtime),
), [runtime, context])
}
useRunPromise() {
const { runtime, context } = React.useContext(this.Context)
return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
options?: { readonly signal?: AbortSignal },
): Promise<A> => effect.pipe(
Effect.provide(context),
effect => Runtime.runPromise(runtime)(effect, options),
), [runtime, context])
}
useRunFork() {
const { runtime, context } = React.useContext(this.Context)
return React.useCallback(<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
options?: Runtime.RunForkOptions,
): Fiber.RuntimeFiber<A, E> => effect.pipe(
Effect.provide(context),
effect => Runtime.runFork(runtime)(effect, options),
), [runtime, context])
}
useMemo<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
deps?: React.DependencyList,
options?: RenderOptions,
): A {
const runSync = this.useRunSync()
return React.useMemo(() => runSync(effect), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...(deps ?? []),
])
}
// useEffect<A, E>(
// effect: Effect.Effect<A, E, RuntimeR | ContextR | Scope.Scope>,
// deps?: React.DependencyList,
// options?: RenderOptions,
// ): void {
// const runSync = this.useRunSync()
// return React.useEffect(() => { runSync(effect) }, [
// ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
// ...(deps ?? []),
// ])
// }
useSuspense<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR>,
options?: { readonly signal?: AbortSignal },
): A {
const runPromise = this.useRunPromise()
return React.use(runPromise(effect, options))
}
useFork<A, E>(
effect: Effect.Effect<A, E, RuntimeR | ContextR | Scope.Scope>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions,
): void {
const runFork = this.useRunFork()
return React.useEffect(() => {
const fiber = runFork(Effect.scoped(effect), options)
return () => { runFork(Fiber.interrupt(fiber)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runFork],
...(deps ?? []),
])
}
useRef<A>(value: A): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
SubscriptionRef.make(value),
[],
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
)
}
useRefFromEffect<A, E>(effect: Effect.Effect<A, E, RuntimeR | ContextR>): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
effect.pipe(Effect.flatMap(SubscriptionRef.make)),
[],
{ doNotReExecuteOnRuntimeOrContextChange: false }, // Do not recreate the ref when the context changes
)
}
useRefState<A>(ref: SubscriptionRef.SubscriptionRef<A>): [A, React.Dispatch<React.SetStateAction<A>>] {
const runSync = this.useRunSync()
const initialState = React.useMemo(() => runSync(ref), [ref])
const [reactStateValue, setReactStateValue] = React.useState(initialState)
this.useFork(Stream.runForEach(ref.changes, v => Effect.sync(() =>
setReactStateValue(v)
)), [ref])
const setValue = React.useCallback((setStateAction: React.SetStateAction<A>) =>
runSync(Ref.update(ref, previousState =>
typeof setStateAction === "function"
? (setStateAction as (prevState: A) => A)(previousState)
: setStateAction
)),
[ref])
return [reactStateValue, setValue]
}
}
export interface RenderOptions {
/** Prevents re-executing the effect when the Effect runtime or context changes. Defaults to `false`. */
readonly doNotReExecuteOnRuntimeOrContextChange?: boolean
}
export const make = <R = never>(): Reffuse<never, R, R> =>
new Reffuse(Runtime.defaultRuntime)
export const makeWithRuntime = <R = never>() =>
<RuntimeR>(runtime: Runtime.Runtime<RuntimeR>): Reffuse<RuntimeR, R, R> =>
new Reffuse(runtime)

View File

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

View File

@@ -0,0 +1,7 @@
export interface ReffuseExtension<A extends object> {
(): A
readonly Type: A
}
export const make = <A extends object>(extension: () => A): ReffuseExtension<A> =>
extension as ReffuseExtension<A>

View File

@@ -0,0 +1,446 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import * as ReffuseContext from "./ReffuseContext.js"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
import * as SetStateAction from "./SetStateAction.js"
export interface RenderOptions {
/** Prevents re-executing the effect when the Effect runtime or context changes. Defaults to `false`. */
readonly doNotReExecuteOnRuntimeOrContextChange?: boolean
}
export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
}
export abstract class ReffuseHelpers<R> {
declare ["constructor"]: ReffuseHelpersClass<R>
useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> {
return ReffuseContext.useMergeAll(...this.constructor.contexts)
}
useLayer<R>(this: ReffuseHelpers<R>): Layer.Layer<R> {
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
}
useRunSync<R>(this: ReffuseHelpers<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback(effect => effect.pipe(
Effect.provide(context),
Runtime.runSync(runtime),
), [runtime, context])
}
useRunPromise<R>(this: ReffuseHelpers<R>): <A, E>(
effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal },
) => Promise<A> {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback((effect, options) => effect.pipe(
Effect.provide(context),
effect => Runtime.runPromise(runtime)(effect, options),
), [runtime, context])
}
useRunFork<R>(this: ReffuseHelpers<R>): <A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions,
) => Fiber.RuntimeFiber<A, E> {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback((effect, options) => effect.pipe(
Effect.provide(context),
effect => Runtime.runFork(runtime)(effect, options),
), [runtime, context])
}
useRunCallback<R>(this: ReffuseHelpers<R>): <A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunCallbackOptions<A, E>,
) => Runtime.Cancel<A, E> {
const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext()
return React.useCallback((effect, options) => effect.pipe(
Effect.provide(context),
effect => Runtime.runCallback(runtime)(effect, options),
), [runtime, context])
}
/**
* Reffuse equivalent to `React.useMemo`.
*
* `useMemo` will only recompute the memoized value by running the given synchronous effect when one of the deps has changed. \
* Trying to run an asynchronous effect will throw.
*
* Changes to the Reffuse runtime or context will recompute the value in addition to the deps.
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*/
useMemo<A, E, R>(
this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
options?: RenderOptions,
): A {
const runSync = this.useRunSync()
return React.useMemo(() => runSync(effect()), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
}
useMemoScoped<A, E, R>(
this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): A {
const runSync = this.useRunSync()
// Calculate an initial version of the value so that it can be accessed during the first render
const [initialScope, initialValue] = React.useMemo(() => Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.flatMap(scope => effect().pipe(
Effect.provideService(Scope.Scope, scope),
Effect.map(value => [scope, value] as const),
)),
runSync,
), [])
// Keep track of the state of the initial scope
const initialScopeClosed = React.useRef(false)
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => {
const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe(
Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })),
Effect.when(() => !initialScopeClosed.current),
)
const [scope, value] = closeInitialScopeIfNeeded.pipe(
Effect.andThen(Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.flatMap(scope => effect().pipe(
Effect.provideService(Scope.Scope, scope),
Effect.map(value => [scope, value] as const),
))
)),
runSync,
)
setValue(value)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
return value
}
/**
* Reffuse equivalent to `React.useEffect`.
*
* Executes a synchronous effect wrapped into a Scope when one of the deps has changed. Trying to run an asynchronous effect will throw.
*
* The Scope is closed on every cleanup, i.e. when one of the deps has changed and the effect needs to be re-executed. \
* Add finalizers to the Scope to handle cleanup logic.
*
* Changes to the Reffuse runtime or context will re-execute the effect in addition to the deps.
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*
* ### Example
* ```
* useEffect(() => Effect.addFinalizer(() => Console.log("Component unmounted")).pipe(
* Effect.flatMap(() => Console.log("Component mounted"))
* ))
* ```
*
* Plain React equivalent:
* ```
* React.useEffect(() => {
* console.log("Component mounted")
* return () => { console.log("Component unmounted") }
* }, [])
* ```
*/
useEffect<A, E, R>(
this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): void {
const runSync = this.useRunSync()
React.useEffect(() => {
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.tap(scope => Effect.provideService(effect(), Scope.Scope, scope)),
runSync,
)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, deps && [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
}
/**
* Reffuse equivalent to `React.useLayoutEffect`.
*
* Executes a synchronous effect wrapped into a Scope when one of the deps has changed. Fires synchronously after all DOM mutations. \
* Trying to run an asynchronous effect will throw.
*
* The Scope is closed on every cleanup, i.e. when one of the deps has changed and the effect needs to be re-executed. \
* Add finalizers to the Scope to handle cleanup logic.
*
* Changes to the Reffuse runtime or context will re-execute the effect in addition to the deps.
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*
* ### Example
* ```
* useLayoutEffect(() => Effect.addFinalizer(() => Console.log("Component unmounted")).pipe(
* Effect.flatMap(() => Console.log("Component mounted"))
* ))
* ```
*
* Plain React equivalent:
* ```
* React.useLayoutEffect(() => {
* console.log("Component mounted")
* return () => { console.log("Component unmounted") }
* }, [])
* ```
*/
useLayoutEffect<A, E, R>(
this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): void {
const runSync = this.useRunSync()
React.useLayoutEffect(() => {
const scope = Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.tap(scope => Effect.provideService(effect(), Scope.Scope, scope)),
runSync,
)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, deps && [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
}
/**
* An asynchronous and non-blocking alternative to `React.useEffect`.
*
* Forks an effect wrapped into a Scope in the background when one of the deps has changed.
*
* The Scope is closed on every cleanup, i.e. when one of the deps has changed and the effect needs to be re-executed. \
* Add finalizers to the Scope to handle cleanup logic.
*
* Changes to the Reffuse runtime or context will re-execute the effect in addition to the deps.
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*
* ### Example
* ```
* const timeRef = useRefFromEffect(DateTime.now)
*
* useFork(() => Effect.addFinalizer(() => Console.log("Cleanup")).pipe(
* Effect.map(() => Stream.repeatEffectWithSchedule(
* DateTime.now,
* Schedule.intersect(Schedule.forever, Schedule.spaced("1 second")),
* )),
*
* Effect.flatMap(Stream.runForEach(time => Ref.set(timeRef, time)),
* )), [timeRef])
*
* const [time] = useRefState(timeRef)
* ```
*/
useFork<A, E, R>(
this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
): void {
const runSync = this.useRunSync()
const runFork = this.useRunFork()
React.useEffect(() => {
const scope = runSync(options?.scope
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy)
)
runFork(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
return () => { runFork(Scope.close(scope, Exit.void)) }
}, deps && [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
}
usePromise<A, E, R>(
this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
): Promise<A> {
const runSync = this.useRunSync()
const runFork = this.useRunFork()
const [value, setValue] = React.useState(Promise.withResolvers<A>().promise)
React.useEffect(() => {
const { promise, resolve, reject } = Promise.withResolvers<A>()
setValue(promise)
const scope = runSync(options?.scope
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy)
)
const cleanup = () => { runFork(Scope.close(scope, Exit.void)) }
if (options?.signal)
options.signal.addEventListener("abort", cleanup)
effect().pipe(
Effect.provideService(Scope.Scope, scope),
Effect.match({
onSuccess: resolve,
onFailure: reject,
}),
effect => runFork(effect, { ...options, scope }),
)
return () => {
if (options?.signal)
options.signal.removeEventListener("abort", cleanup)
cleanup()
}
}, deps && [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
return value
}
useCallbackSync<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
options?: RenderOptions,
): (...args: Args) => A {
const runSync = this.useRunSync()
return React.useCallback((...args) => runSync(callback(...args)), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
}
useCallbackPromise<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
options?: { readonly signal?: AbortSignal } & RenderOptions,
): (...args: Args) => Promise<A> {
const runPromise = this.useRunPromise()
return React.useCallback((...args) => runPromise(callback(...args), options), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runPromise],
...deps,
])
}
useRef<A, R>(
this: ReffuseHelpers<R>,
value: A,
): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
() => SubscriptionRef.make(value),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
)
}
/**
* Binds the state of a `SubscriptionRef` to the state of the React component.
*
* Returns a [value, setter] tuple just like `React.useState` and triggers a re-render everytime the value held by the ref changes.
*
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
*/
useRefState<A, R>(
this: ReffuseHelpers<R>,
ref: SubscriptionRef.SubscriptionRef<A>,
): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true })
const [reactStateValue, setReactStateValue] = React.useState(initialState)
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() =>
setReactStateValue(v)
)), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
Ref.update(ref, prevState =>
SetStateAction.value(setStateAction, prevState)
),
[ref])
return [reactStateValue, setValue]
}
useStreamFromValues<const A extends React.DependencyList, R>(
this: ReffuseHelpers<R>,
values: A,
): Stream.Stream<A> {
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
), [])
this.useEffect(() => Queue.offer(queue, values), values)
return stream
}
}
export interface ReffuseHelpers<R> extends Pipeable.Pipeable {}
ReffuseHelpers.prototype.pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments)
}
export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable {
new(): ReffuseHelpers<R>
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
}
(ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments)
}
export const make = (): ReffuseHelpersClass<never> =>
class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) {
static readonly contexts = []
}

View File

@@ -1,84 +0,0 @@
import { Context, Effect, Runtime, type Layer } from "effect"
import React from "react"
import type * as Reffuse from "./Reffuse.js"
export interface Value<RuntimeR, ContextR> {
readonly runtime: Runtime.Runtime<RuntimeR>
readonly context: Context.Context<ContextR>
}
export type Provider<
RuntimeR,
OwnContextR,
ParentContextR,
> = React.FC<ProviderProps<RuntimeR, OwnContextR, ParentContextR>>
export interface ProviderProps<
RuntimeR,
OwnContextR,
ParentContextR,
> {
readonly layer: Layer.Layer<OwnContextR, unknown, RuntimeR | ParentContextR>
readonly children?: React.ReactNode
}
export function makeRootProvider<
RuntimeR,
ContextR extends ParentContextR | OwnContextR,
OwnContextR,
ParentContextR,
>(
runtime: Runtime.Runtime<RuntimeR>,
ReactContext: React.Context<Value<RuntimeR, ContextR>>,
): Provider<RuntimeR, OwnContextR, ParentContextR> {
return function ReffuseRootReactContextProvider(props) {
const value = React.useMemo(() => ({
runtime,
context: Effect.context<ContextR>().pipe(
Effect.provide(props.layer),
Effect.provide(Context.empty() as Context.Context<ParentContextR>), // Just there for type safety. ParentContextR is always never here anyway
Runtime.runSync(runtime),
),
}), [props.layer])
return (
<ReactContext
{...props}
value={value}
/>
)
}
}
export function makeNestedProvider<
RuntimeR,
ContextR extends ParentContextR | OwnContextR,
OwnContextR,
ParentContextR,
>(
runtime: Runtime.Runtime<RuntimeR>,
ReactContext: React.Context<Value<RuntimeR, ContextR>>,
parent: Reffuse.Reffuse<RuntimeR, ParentContextR, unknown, unknown>,
): Provider<RuntimeR, OwnContextR, ParentContextR> {
return function ReffuseNestedReactContextProvider(props) {
const parentContext = parent.useContext()
const value = React.useMemo(() => ({
runtime,
context: Effect.context<ContextR>().pipe(
Effect.provide(props.layer),
Effect.provide(parentContext),
Runtime.runSync(runtime),
),
}), [props.layer, parentContext])
return (
<ReactContext
{...props}
value={value}
/>
)
}
}

View File

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

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
import { Context, Effect, FiberRefs, Layer, Ref, Runtime, RuntimeFlags } from "effect"
const runtime = Runtime.make({
context: Context.empty(),
runtimeFlags: RuntimeFlags.make(),
fiberRefs: FiberRefs.empty(),
})
const createRunSync = <R1, R2>(runtime: Runtime.Runtime<R1>, layer: Layer.Layer<R2>) => {
const context = Effect.context<R1 | R2>().pipe(
Effect.provide(layer),
Runtime.runSync(runtime),
)
return <A, E>(effect: Effect.Effect<A, E, R1 | R2>) =>
Runtime.runSync(runtime)(effect.pipe(Effect.provide(context)))
}
class MyService extends Effect.Service<MyService>()("MyServer", {
effect: Effect.gen(function*() {
return {
ref: yield* Ref.make("initial value")
} as const
})
}) {}
const MyLayer = Layer.empty.pipe(
Layer.provideMerge(MyService.Default)
)
const runSync = createRunSync(runtime, MyLayer)
const setMyServiceValue = (value: string) => Effect.gen(function*() {
console.log("previous value: ", yield* (yield* MyService).ref)
yield* Ref.set((yield* MyService).ref, value)
console.log("new value: ", yield* (yield* MyService).ref)
})
runSync(setMyServiceValue("1"))
runSync(setMyServiceValue("2"))

View File

@@ -0,0 +1,21 @@
/**
* Extracts the common keys between two types
*/
export type CommonKeys<A, B> = Extract<keyof A, keyof B>
/**
* Obtain the static members type of a constructor function type
*/
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
export type Extend<Super, Self> =
Extendable<Super, Self> extends true
? Omit<Super, CommonKeys<Self, Super>> & Self
: never
export type Extendable<Super, Self> =
Pick<Self, CommonKeys<Self, Super>> extends Pick<Super, CommonKeys<Self, Super>>
? true
: false
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self

11
turbo.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["./src/**"],
"outputs": ["./dist/**"]
},
"pack": {}
}
}