185 Commits

Author SHA1 Message Date
Julien Valverdé
2a78232ec7 Hooks
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 20:12:17 +02:00
Julien Valverdé
19194d6677 Hook
Some checks failed
Lint / lint (push) Failing after 11s
2025-07-01 18:13:03 +02:00
Julien Valverdé
40871b793d Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 16:54:50 +02:00
Julien Valverdé
f079b90f28 Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 16:48:53 +02:00
Julien Valverdé
28b6e9276e Fix
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 16:44:28 +02:00
Julien Valverdé
8025ec4a22 Fix
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 16:31:30 +02:00
Julien Valverdé
02ee2c10cc Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 16:16:29 +02:00
Julien Valverdé
bb1a71f63b Scope refactoring
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 15:59:58 +02:00
Julien Valverdé
a9448f55cf Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-07-01 13:32:25 +02:00
Julien Valverdé
c0f3073d20 Hook work
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 13:30:50 +02:00
Julien Valverdé
8cfe186574 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 00:46:59 +02:00
Julien Valverdé
625cecda27 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-07-01 00:29:42 +02:00
Julien Valverdé
7cc0a68170 useMemoLayer
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 00:23:45 +02:00
Julien Valverdé
8be1295e2f Layer tests
All checks were successful
Lint / lint (push) Successful in 15s
2025-07-01 00:11:34 +02:00
Julien Valverdé
a781be8f24 Working ref
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-30 22:49:30 +02:00
Julien Valverdé
4913f5cc35 Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-30 22:07:38 +02:00
Julien Valverdé
2a37f843ca AsyncProvider
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-30 22:04:03 +02:00
Julien Valverdé
78a3735038 Refactoring
All checks were successful
Lint / lint (push) Successful in 16s
2025-06-30 21:44:29 +02:00
Julien Valverdé
37d9400ada Component displayName
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-30 00:37:16 +02:00
Julien Valverdé
2ef47bed70 Work
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-29 23:00:33 +02:00
Julien Valverdé
2b78d4dc49 Cleanup
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-29 19:35:43 +02:00
Julien Valverdé
6fa73ee33f Tests
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-29 19:11:16 +02:00
Julien Valverdé
3ea4c81872 React component refactoring 2025-06-29 18:52:42 +02:00
Julien Valverdé
782629d5b3 Refactoring
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-29 18:20:46 +02:00
Julien Valverdé
8b2abbbd19 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-29 17:07:05 +02:00
Julien Valverdé
152657d97b Effect LSP
Some checks failed
Lint / lint (push) Failing after 13s
2025-06-29 15:27:49 +02:00
Julien Valverdé
faf1d4963c Work
Some checks failed
Lint / lint (push) Failing after 28s
2025-06-27 05:43:58 +02:00
Julien Valverdé
9ba36ebc04 Test
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-26 04:29:11 +02:00
Julien Valverdé
f327728b3a Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-06-26 04:26:58 +02:00
Julien Valverdé
8920674b26 Component scope
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-26 04:22:27 +02:00
Julien Valverdé
b440503e50 Hook work
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-26 02:55:20 +02:00
Julien Valverdé
4088d86652 Work
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-26 01:26:25 +02:00
Julien Valverdé
79cf1e5eb7 API change
All checks were successful
Lint / lint (push) Successful in 31s
2025-06-25 21:51:46 +02:00
Julien Valverdé
8007c2693a Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-25 13:38:44 +02:00
Julien Valverdé
1769c4074d ReactComponent
Some checks failed
Lint / lint (push) Failing after 7s
2025-06-25 13:17:27 +02:00
Julien Valverdé
8c5613aa62 Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-25 04:07:22 +02:00
Julien Valverdé
7d220cb61a Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-25 02:40:54 +02:00
Julien Valverdé
d81a9fcd91 Fix
All checks were successful
Lint / lint (push) Successful in 15s
2025-06-23 08:32:48 +02:00
Julien Valverdé
45ce747ff0 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 13s
2025-06-18 00:14:05 +02:00
Julien Valverdé
e089bf9fee 0.1.13 (#18)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 15s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/18
2025-06-18 00:12:19 +02:00
Julien Valverdé
c2a1a7b212 Version bump
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 16s
2025-06-18 00:09:34 +02:00
Julien Valverdé
4dc336fbf4 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-06-18 00:08:57 +02:00
Julien Valverdé
1fe2fec325 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-18 00:06:51 +02:00
Julien Valverdé
d8b40088cb Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-17 23:22:39 +02:00
Julien Valverdé
38bf3f99ea Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 13s
2025-06-17 23:07:50 +02:00
Julien Valverdé
30b72b5b52 0.1.12 (#17)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 14s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/17
2025-06-17 23:06:08 +02:00
Julien Valverdé
3a8a1ed0c3 Version bump
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 16s
2025-06-17 23:03:20 +02:00
Julien Valverdé
7013bed037 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-17 23:01:26 +02:00
Julien Valverdé
0b7a2dbe92 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-17 22:24:02 +02:00
Julien Valverdé
0d3e09354e Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-06-17 21:14:25 +02:00
Julien Valverdé
dc46d03aab PropertyPath recursive fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-06-17 21:12:32 +02:00
Julien Valverdé
37ffc161d3 SubRef tests
All checks were successful
Lint / lint (push) Successful in 16s
2025-06-17 20:34:23 +02:00
Julien Valverdé
e8a267f4cb Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 13s
2025-06-01 05:30:26 +02:00
Julien Valverdé
6dc0a548cd @reffuse/extension-query 0.1.5 (#16)
All checks were successful
Publish / publish (push) Successful in 25s
Lint / lint (push) Successful in 14s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/16
2025-06-01 05:28:46 +02:00
Julien Valverdé
53c06e3dae Version bump
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 1m29s
2025-06-01 05:05:44 +02:00
Julien Valverdé
82d154ac54 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-06-01 05:03:41 +02:00
Julien Valverdé
ed788af128 Fix
All checks were successful
Lint / lint (push) Successful in 16s
2025-06-01 03:49:04 +02:00
Julien Valverdé
f4e380ddcb QueryClient refactoring work
Some checks failed
Lint / lint (push) Failing after 15s
2025-05-31 05:57:39 +02:00
Julien Valverdé
e58bd7ab5a Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-30 23:21:19 +02:00
Julien Valverdé
21d011dd12 QueryErrorHandler refactoring
Some checks failed
Lint / lint (push) Failing after 15s
2025-05-30 01:11:10 +02:00
Julien Valverdé
5b64d0d783 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-29 22:12:37 +02:00
Julien Valverdé
2a29f19ece @reffuse/extension-query 0.1.4 (#15)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 1m1s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/15
2025-05-26 04:15:01 +02:00
Julien Valverdé
919dad97ef Refactoring
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 15s
2025-05-26 04:11:57 +02:00
Julien Valverdé
2fb1a2b897 Fix
All checks were successful
Lint / lint (push) Successful in 12s
Test build / test-build (pull_request) Successful in 15s
2025-05-26 04:01:19 +02:00
Julien Valverdé
c9263c3d8a Version bump
All checks were successful
Lint / lint (push) Successful in 12s
2025-05-26 04:00:28 +02:00
Julien Valverdé
8444061de3 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-26 03:58:45 +02:00
Julien Valverdé
2f870e56cd Mutation refactoring
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-26 03:56:56 +02:00
Julien Valverdé
f95c2596a3 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-26 00:56:29 +02:00
Julien Valverdé
5d85449fef QueryRunner refactoring
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-26 00:48:00 +02:00
Julien Valverdé
3548ed5718 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-25 19:17:53 +02:00
Julien Valverdé
82f0f67ee6 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-25 04:16:17 +02:00
Julien Valverdé
8d52443b55 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-25 03:54:14 +02:00
Julien Valverdé
27f50db664 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-25 03:52:15 +02:00
Julien Valverdé
165b7bbeee Query refactoring
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-24 21:31:56 +02:00
Julien Valverdé
58b2da373e Query refactoring
All checks were successful
Lint / lint (push) Successful in 16s
2025-05-24 21:04:01 +02:00
Julien Valverdé
f25fefb0f3 Cleanup
All checks were successful
Lint / lint (push) Successful in 12s
2025-05-23 17:33:19 +02:00
Julien Valverdé
6edac19fa6 Service work
Some checks failed
Lint / lint (push) Failing after 14s
2025-05-23 05:12:21 +02:00
Julien Valverdé
1c99d5c161 QueryService
Some checks failed
Lint / lint (push) Failing after 13s
2025-05-23 03:34:02 +02:00
Julien Valverdé
5044887d90 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-22 20:22:13 +02:00
Julien Valverdé
810e4bb9fd Query refactoring
Some checks failed
Lint / lint (push) Failing after 12s
2025-05-22 19:49:40 +02:00
Julien Valverdé
619dbe32ae Fix
Some checks failed
Lint / lint (push) Failing after 12s
2025-05-22 16:18:03 +02:00
Julien Valverdé
28efea18f1 Fix
Some checks failed
Lint / lint (push) Failing after 15s
2025-05-22 16:13:35 +02:00
Julien Valverdé
7e9a8a5fee QueryRunner work
Some checks failed
Lint / lint (push) Failing after 17s
2025-05-20 17:38:44 +02:00
Julien Valverdé
16a7dec3fd Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-19 14:03:52 +02:00
Julien Valverdé
2c467dc6ec 0.1.11 (#14)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/14
2025-05-19 14:01:41 +02:00
Julien Valverdé
ffba43c259 useScope refactoring
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 16s
2025-05-19 13:58:12 +02:00
Julien Valverdé
45ab641262 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-19 13:03:14 +02:00
Julien Valverdé
c7c2f8de62 Example fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-19 12:58:00 +02:00
Julien Valverdé
dda868d444 Scope refactoring
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-19 12:49:37 +02:00
Julien Valverdé
b9e787f42b Todos refactoring
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-18 15:45:31 +02:00
Julien Valverdé
1af2a14b52 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-18 13:57:59 +02:00
Julien Valverdé
861e462ebd Todos example work
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-18 13:52:39 +02:00
Julien Valverdé
b6a127c8a7 SubRefFromGetSet
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-18 12:35:44 +02:00
Julien Valverdé
497e9a34f2 SubRefFromPath
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-18 12:16:25 +02:00
Julien Valverdé
9d0daaa87f Todos example refactoring
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-18 10:52:39 +02:00
Julien Valverdé
557c4a1b97 Update
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-14 06:29:37 +02:00
Julien Valverdé
b395644798 Dependencies upgrade
Some checks failed
Lint / lint (push) Failing after 14s
2025-05-14 06:19:39 +02:00
Julien Valverdé
099a28ca0d Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-11 19:44:46 +02:00
Julien Valverdé
27ca5e643a Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-11 19:22:39 +02:00
Julien Valverdé
64943deaab 0.1.10 (#13)
All checks were successful
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/13
2025-05-11 19:21:06 +02:00
Julien Valverdé
a616e84079 Version bump
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 16s
2025-05-11 19:19:02 +02:00
Julien Valverdé
c832c3f79a Fix
All checks were successful
Lint / lint (push) Successful in 18s
2025-05-11 19:13:30 +02:00
Julien Valverdé
6f65574ebd Cleanup
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-11 08:11:03 +02:00
Julien Valverdé
9ccabbb627 Tests
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-11 07:10:38 +02:00
Julien Valverdé
5f4087aa40 Cleanup
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-11 07:04:14 +02:00
Julien Valverdé
85b41bda9f Tests
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-11 06:48:58 +02:00
Julien Valverdé
e5a7fe8ad6 useSubscribeStream
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-11 03:17:35 +02:00
Julien Valverdé
59b7115d19 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-10 05:33:14 +02:00
Julien Valverdé
08af31f0b9 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-10 05:30:13 +02:00
Julien Valverdé
44fc6bbbc4 Stream tests
All checks were successful
Lint / lint (push) Successful in 15s
2025-05-10 05:26:15 +02:00
Julien Valverdé
904b725753 Dependencies upgrade
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-08 21:09:55 +02:00
Julien Valverdé
73dd7bc160 Cleanup
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-08 19:22:50 +02:00
Julien Valverdé
6bc07d5b2a Tests
All checks were successful
Lint / lint (push) Successful in 12s
2025-05-08 05:59:36 +02:00
Julien Valverdé
70e9b9218d Queue -> PubSub
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-08 04:51:41 +02:00
Julien Valverdé
31b07f842b Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-08 04:42:24 +02:00
Julien Valverdé
10f23d4cb4 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-08 01:28:41 +02:00
Julien Valverdé
39765102db useStreamFromReactiveValues
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-08 01:15:35 +02:00
Julien Valverdé
04e78e1ea3 Tests
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-07 04:53:40 +02:00
Julien Valverdé
606dd2c00f useStreamFromReactiveValues
All checks were successful
Lint / lint (push) Successful in 12s
2025-05-07 02:15:46 +02:00
Julien Valverdé
c13a8d549f useStreamFromReactiveValues
All checks were successful
Lint / lint (push) Successful in 15s
2025-05-07 02:07:50 +02:00
Julien Valverdé
4b9bfd0637 Stream PubSub
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-05 21:52:55 +02:00
Julien Valverdé
53fc1ef505 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-05 20:38:39 +02:00
Julien Valverdé
c8b675d93e Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-05 03:33:50 +02:00
Julien Valverdé
882ec9591c Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-05 03:09:17 +02:00
Julien Valverdé
5b3637afd8 useSubscribeStream work
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-05 02:39:12 +02:00
Julien Valverdé
d6256a7cfd Stream hooks work
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-04 01:18:29 +02:00
Julien Valverdé
cf6c84ff8e useScope fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-02 21:56:46 +02:00
Julien Valverdé
198a7cee03 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-01 21:59:51 +02:00
Julien Valverdé
032f283ac8 Pull example
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-01 21:56:58 +02:00
Julien Valverdé
c34629e20d usePullStream
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-01 21:08:52 +02:00
Julien Valverdé
284a080f19 usePullStream
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-01 20:27:11 +02:00
Julien Valverdé
87d27dd48d useScope work
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-01 19:18:54 +02:00
Julien Valverdé
24853561f1 Working useScope
All checks were successful
Lint / lint (push) Successful in 15s
2025-05-01 17:10:45 +02:00
Julien Valverdé
1902ad373f useScope tests
All checks were successful
Lint / lint (push) Successful in 18s
2025-05-01 16:48:37 +02:00
Julien Valverdé
aa6c4a8008 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-05-01 16:19:42 +02:00
Julien Valverdé
d5ac84b2cc Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-05-01 16:17:01 +02:00
Julien Valverdé
3c604abcef useScope
Some checks failed
Lint / lint (push) Failing after 11s
2025-05-01 03:01:18 +02:00
Julien Valverdé
ba99309877 useSubscribePullStream
Some checks failed
Lint / lint (push) Failing after 12s
2025-04-30 23:41:03 +02:00
Julien Valverdé
db3cd05851 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-30 23:00:37 +02:00
Julien Valverdé
dce81be269 Fix
All checks were successful
Lint / lint (push) Successful in 17s
2025-04-30 22:52:38 +02:00
Julien Valverdé
3980c10747 Fix
All checks were successful
Lint / lint (push) Successful in 16s
2025-04-30 21:41:58 +02:00
Julien Valverdé
43a3793dbf Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-30 16:27:08 +02:00
Julien Valverdé
da7044ee9f useRefFromValue
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-30 13:33:21 +02:00
Julien Valverdé
ff5503cfd1 Fix
All checks were successful
Lint / lint (push) Successful in 15s
2025-04-29 19:47:09 +02:00
Julien Valverdé
dc2cfb35e0 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-27 19:13:26 +02:00
Julien Valverdé
bc8c96635c 0.1.9 (#12)
All checks were successful
Publish / publish (push) Successful in 25s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/12
2025-04-27 19:12:09 +02:00
Julien Valverdé
1228c51694 Dependencies upgrade
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 43s
2025-04-27 19:09:30 +02:00
Julien Valverdé
076007ec67 Refactoring
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-27 18:52:08 +02:00
Julien Valverdé
dd524e1aa5 Refactoring
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-27 18:46:33 +02:00
Julien Valverdé
1c7cef703b SubRef
All checks were successful
Lint / lint (push) Successful in 17s
2025-04-25 13:50:54 +02:00
Julien Valverdé
fa0f8c6b24 Refactoring
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-25 13:38:42 +02:00
Julien Valverdé
357e5aa56b useSubRef
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-25 10:16:04 +02:00
Julien Valverdé
ea374d7e0f Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-25 08:32:42 +02:00
Julien Valverdé
148c98acbd Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-25 08:30:17 +02:00
Julien Valverdé
39d2176c61 Working subref from path
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-25 08:21:59 +02:00
Julien Valverdé
107ff1e794 SubscriptionSubRef.makeFromPath
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-25 08:12:34 +02:00
Julien Valverdé
a70ef27f75 PropertyPath done
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-25 07:56:56 +02:00
Julien Valverdé
04b2fad038 PropertyPath
All checks were successful
Lint / lint (push) Successful in 29s
2025-04-25 07:40:21 +02:00
Julien Valverdé
691b28427d SearchPaths work
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-24 00:52:18 +02:00
Julien Valverdé
1de976aaa8 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-24 00:20:30 +02:00
Julien Valverdé
df851cf9ee SearchPaths work
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-23 07:06:32 +02:00
Julien Valverdé
459f548c10 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-23 06:50:17 +02:00
Julien Valverdé
6156baec4d SearchPaths work
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-23 06:47:11 +02:00
Julien Valverdé
1163b83929 SearchPaths work
Some checks failed
Lint / lint (push) Failing after 13s
2025-04-23 05:53:19 +02:00
Julien Valverdé
8917f84952 SearchPaths work
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-22 22:59:50 +02:00
Julien Valverdé
58752253b3 SearchPaths work
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-22 22:36:17 +02:00
Julien Valverdé
ba362baf04 SearchPaths work
All checks were successful
Lint / lint (push) Successful in 15s
2025-04-22 21:55:59 +02:00
Julien Valverdé
33cf4fbcbd Tests
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-21 05:15:55 +02:00
Julien Valverdé
e8f92c88b8 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-21 03:17:41 +02:00
Julien Valverdé
6ae155de34 Version bump
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-21 02:53:31 +02:00
Julien Valverdé
db783f174e Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-21 02:52:50 +02:00
Julien Valverdé
2b48695e54 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
All checks were successful
Lint / lint (push) Successful in 12s
2025-04-21 02:12:13 +02:00
Julien Valverdé
0fd3fe49a9 0.1.8 (#11)
All checks were successful
Publish / publish (push) Successful in 27s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/11
2025-04-21 02:08:14 +02:00
Julien Valverdé
ab441fe982 Fix
All checks were successful
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 44s
2025-04-21 02:01:24 +02:00
Julien Valverdé
eabcf9085b useSubRefFromGetSet
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-21 01:21:59 +02:00
Julien Valverdé
926482b154 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-20 19:38:18 +02:00
Julien Valverdé
110b0813f8 Refactoring
All checks were successful
Lint / lint (push) Successful in 29s
2025-04-20 06:14:58 +02:00
Julien Valverdé
974af95a22 Version bump
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-20 05:32:19 +02:00
Julien Valverdé
d6e1d445e8 Fix
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-20 05:28:44 +02:00
Julien Valverdé
d8d6e87a12 Refactoring
Some checks failed
Lint / lint (push) Failing after 13s
2025-04-20 05:26:31 +02:00
Julien Valverdé
682e473bf7 Fix
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-20 05:10:51 +02:00
Julien Valverdé
31dd7b5fdb Working SubscriptionSubRef
All checks were successful
Lint / lint (push) Successful in 13s
2025-04-20 05:06:48 +02:00
Julien Valverdé
17686e68c3 SubscriptionSubRef
All checks were successful
Lint / lint (push) Successful in 18s
2025-04-20 04:34:01 +02:00
Julien Valverdé
49d4bd4d43 SubscriptionSubRef work
All checks were successful
Lint / lint (push) Successful in 14s
2025-04-20 00:22:24 +02:00
Julien Valverdé
be88035936 SubscriptionSubRef
Some checks failed
Lint / lint (push) Failing after 10s
2025-04-19 03:42:48 +02:00
63 changed files with 2193 additions and 891 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

560
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/monorepo", "name": "@reffuse/monorepo",
"packageManager": "bun@1.2.9", "packageManager": "bun@1.2.13",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"./packages/*" "./packages/*"
@@ -15,9 +15,9 @@
"clean:node": "rm -rf node_modules" "clean:node": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"npm-check-updates": "^17.1.18", "npm-check-updates": "^18.0.1",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
"turbo": "^2.5.0", "turbo": "^2.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

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,44 @@
{
"name": "effect-components",
"version": "0.1.0",
"type": "module",
"files": [
"./README.md",
"./dist"
],
"license": "MIT",
"repository": {
"url": "git+https://github.com/Thiladev/reffuse.git"
},
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
},
"scripts": {
"build": "tsc",
"lint:tsc": "tsc --noEmit",
"pack": "npm pack",
"clean:cache": "rm -f tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist",
"clean:node": "rm -rf node_modules"
},
"peerDependencies": {
"@types/react": "^19.0.0",
"effect": "^3.15.0",
"react": "^19.0.0"
},
"devDependencies": {
"@effect/language-service": "^0.23.3"
}
}

View File

@@ -0,0 +1,72 @@
import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
import type { Mutable } from "effect/Types"
import * as React from "react"
import * as ReactHook from "./ReactHook.js"
export interface ReactComponent<E, R, P> {
(props: P): Effect.Effect<React.ReactNode, E, R>
readonly displayName?: string
}
export const nonReactiveTags = [Tracer.ParentSpan] as const
export const withDisplayName: {
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
self: C,
displayName: string,
): C => {
(self as Mutable<C>).displayName = displayName
return self
})
export const useFC: {
<E, R, P extends {} = {}>(
self: ReactComponent<E, R, P>,
options?: ReactHook.ScopeOptions,
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R, P extends {}>(
self: ReactComponent<E, R, P>,
options?: ReactHook.ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
return React.useMemo(() => function ScopeProvider(props: P) {
const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
const FC = React.useMemo(() => {
const f = (props: P) => Runtime.runSync(runtime)(
Effect.provideService(self(props), Scope.Scope, scope)
)
if (self.displayName) f.displayName = self.displayName
return f
}, [scope])
return React.createElement(FC, props)
}, Array.from(
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
))
})
export const use: {
<E, R, P extends {} = {}>(
self: ReactComponent<E, R, P>,
fn: (Component: React.FC<P>) => React.ReactNode,
options?: ReactHook.ScopeOptions,
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function*(self, fn, options) {
return fn(yield* useFC(self, options))
})
export const withRuntime: {
<E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
<E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
} = Function.dual(2, <E, R, P extends {}>(
self: ReactComponent<E, R, P>,
context: React.Context<Runtime.Runtime<R>>,
): React.FC<P> => function WithRuntime(props) {
const runtime = React.useContext(context)
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
})

View File

@@ -0,0 +1,317 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import { SetStateAction } from "./types/index.js"
export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
}
export const useScope: {
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
const runtime = yield* Effect.runtime()
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
Effect.all([Ref.make(true), makeScope(options)])
), [])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => Runtime.runSync(runtime)(
Effect.if(isInitialRun, {
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => closeScope(scope, runtime, options),
),
onFalse: () => makeScope(options).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
), [])
return scope
})
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
const closeScope = (
scope: Scope.CloseableScope,
runtime: Runtime.Runtime<never>,
options?: ScopeOptions,
) => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}
export const useMemo: {
<A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime()
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
})
export const useOnce: {
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>
) {
return yield* useMemo(factory, [])
})
export const useMemoLayer: {
<ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>
): Effect.Effect<Context.Context<ROut>, E, RIn>
} = Effect.fnUntraced(function* <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>
) {
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
})
export const useCallbackSync: {
<Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<(...args: Args) => A, never, R>
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
})
export const useCallbackPromise: {
<Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
})
export const useEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => {
const { scope, exit } = Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Runtime.runSync(runtime),
)
return () => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, exit))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, exit))
break
}
}
}, deps)
})
export const useLayoutEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useLayoutEffect(() => {
const { scope, exit } = Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Runtime.runSync(runtime),
)
return () => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, exit))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, exit))
break
}
}
}, deps)
})
export const useFork: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => {
const scope = Runtime.runSync(runtime)(options?.scope
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy)
)
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
return () => {
switch (options?.finalizerExecutionMode ?? "fork") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}
}, deps)
})
export const useRefFromReactiveValue: {
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
} = Effect.fnUntraced(function*(value) {
const ref = yield* useOnce(() => SubscriptionRef.make(value))
yield* useEffect(() => Ref.set(ref, value), [value])
return ref
})
export const useSubscribeRefs: {
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
...refs: Refs
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
...refs: Refs
) {
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
))
yield* useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v))
),
), refs)
return reactStateValue as any
})
export const useRefState: {
<A>(
ref: SubscriptionRef.SubscriptionRef<A>
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
yield* useFork(() => Stream.runForEach(
Stream.changesWith(ref.changes, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(v)),
), [ref])
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
Ref.update(ref, prevState =>
SetStateAction.value(setStateAction, prevState)
),
[ref])
return [reactStateValue, setValue]
})
export const useStreamFromReactiveValues: {
<const A extends React.DependencyList>(
values: A
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
Effect.bind("latest", () => Ref.make(values)),
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
Effect.flatMap(a => Effect.map(
Stream.fromPubSub(pubsub, { scoped: true }),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrapScoped,
)),
))
yield* useEffect(() => Ref.set(latest, values).pipe(
Effect.andThen(PubSub.publish(pubsub, values)),
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
), values)
return stream
})
export const useSubscribeStream: {
<A, E, R>(
stream: Stream.Stream<A, E, R>
): Effect.Effect<Option.Option<A>, never, R>
<A extends NonNullable<unknown>, E, R>(
stream: Stream.Stream<A, E, R>,
initialValue: A,
): Effect.Effect<Option.Some<A>, never, R>
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
stream: Stream.Stream<A, E, R>,
initialValue?: A,
) {
const [reactStateValue, setReactStateValue] = React.useState(
React.useMemo(() => initialValue
? Option.some(initialValue)
: Option.none(),
[])
)
yield* useFork(() => Stream.runForEach(
Stream.changesWith(stream, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream])
return reactStateValue as Option.Some<A>
})

View File

@@ -0,0 +1,47 @@
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
import * as React from "react"
export interface ReactManagedRuntime<R, ER> {
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
readonly context: React.Context<Runtime.Runtime<R>>
}
export const make = <R, ER>(
layer: Layer.Layer<R, ER>,
memoMap?: Layer.MemoMap,
): ReactManagedRuntime<R, ER> => ({
runtime: ManagedRuntime.make(layer, memoMap),
context: React.createContext<Runtime.Runtime<R>>(null!),
})
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
readonly runtime: ReactManagedRuntime<R, ER>
readonly children?: React.ReactNode
}
export function AsyncProvider<R, ER>(
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
): React.ReactNode {
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
return React.createElement(
React.Suspense,
suspenseProps,
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
)
}
interface AsyncProviderInnerProps<R, ER> {
readonly runtime: ReactManagedRuntime<R, ER>
readonly promise: Promise<Runtime.Runtime<R>>
readonly children?: React.ReactNode
}
function AsyncProviderInner<R, ER>(
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
): React.ReactNode {
const value = React.use(promise)
return React.createElement(runtime.context, { value }, children)
}

View File

@@ -0,0 +1,3 @@
export * as ReactComponent from "./ReactComponent.js"
export * as ReactHook from "./ReactHook.js"
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"

View File

@@ -0,0 +1,99 @@
import { Array, Function, Option, Predicate } from "effect"
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
export type Paths<T, D extends number = 5, Seen = never> = [] | (
D extends never ? [] :
T extends Seen ? [] :
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
T extends object ? ObjectPaths<T, D, Seen | T> :
never
)
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
[K in keyof T as K extends number ? K : never]:
| [K]
| [K, ...Paths<T[K], Prev[D], Seen>]
} extends infer O
? O[keyof O]
: never
export type ObjectPaths<T extends object, D extends number, Seen> = {
[K in keyof T as K extends string | number | symbol ? K : never]-?:
NonNullable<T[K]> extends infer V
? [K] | [K, ...Paths<V, Prev[D], Seen>]
: never
} extends infer O
? O[keyof O]
: never
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
? Head extends keyof T
? ValueFromPath<T[Head], Tail>
: T extends readonly any[]
? Head extends number
? ValueFromPath<T[number], Tail>
: never
: never
: T
export type AnyKey = string | number | symbol
export type AnyPath = readonly AnyKey[]
export const unsafeGet: {
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
path.reduce((acc: any, key: any) => acc?.[key], self)
)
export const get: {
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
path.reduce(
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
? Predicate.hasProperty(acc.value, key)
? Option.some(acc.value[key])
: Option.none()
: acc,
Option.some(self),
)
)
export const immutableSet: {
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
const key = Array.head(path as AnyPath)
if (Option.isNone(key))
return Option.some(value as T)
if (!Predicate.hasProperty(self, key.value))
return Option.none()
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
if (Option.isNone(child))
return child
if (Array.isArray(self))
return typeof key.value === "number"
? Option.some([
...self.slice(0, key.value),
child.value,
...self.slice(key.value + 1),
] as T)
: Option.none()
if (typeof self === "object")
return Option.some(
Object.assign(
Object.create(Object.getPrototypeOf(self)),
{ ...self, [key.value]: child.value },
)
)
return Option.none()
})

View File

@@ -0,0 +1,100 @@
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
import * as PropertyPath from "./PropertyPath.js"
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
readonly parent: SubscriptionRef.SubscriptionRef<B>
readonly [Unify.typeSymbol]?: unknown
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
}
export declare namespace SubscriptionSubRef {
export interface Variance<in out A, in out B> {
readonly [SubscriptionSubRefTypeId]: {
readonly _A: Types.Invariant<A>
readonly _B: Types.Invariant<B>
}
}
}
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
}
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
SubscriptionRef?: true
}
const refVariance = { _A: (_: any) => _ }
const synchronizedRefVariance = { _A: (_: any) => _ }
const subscriptionRefVariance = { _A: (_: any) => _ }
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
readonly [Ref.RefTypeId] = refVariance
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
readonly get: Effect.Effect<A>
constructor(
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
) {
super()
this.get = Effect.map(Ref.get(this.parent), this.getter)
}
commit() {
return this.get
}
get changes(): Stream.Stream<A> {
return this.get.pipe(
Effect.map(a => this.parent.changes.pipe(
Stream.map(this.getter),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrap,
)
}
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
return this.modifyEffect(a => Effect.succeed(f(a)))
}
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
return Effect.Do.pipe(
Effect.bind("b", () => Ref.get(this.parent)),
Effect.bind("ca", ({ b }) => f(this.getter(b))),
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
Effect.map(({ ca: [c] }) => c),
)
}
}
export const makeFromGetSet = <A, B>(
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
parent,
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
)

View File

@@ -0,0 +1,3 @@
export * as PropertyPath from "./PropertyPath.js"
export * as SetStateAction from "./SetStateAction.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.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

@@ -11,41 +11,41 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.24.0", "@eslint/js": "^9.26.0",
"@tanstack/react-router": "^1.115.3", "@tanstack/react-router": "^1.120.3",
"@tanstack/react-router-devtools": "^1.115.3", "@tanstack/react-router-devtools": "^1.120.3",
"@tanstack/router-plugin": "^1.115.3", "@tanstack/router-plugin": "^1.120.3",
"@thilawyn/thilaschema": "^0.1.4", "@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.1.1", "@types/react": "^19.1.4",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.24.0", "eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0", "globals": "^16.1.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"typescript-eslint": "^8.29.1", "typescript-eslint": "^8.32.1",
"vite": "^6.2.6" "vite": "^6.3.5"
}, },
"dependencies": { "dependencies": {
"@effect/platform": "^0.80.8", "@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.59.8", "@effect/platform-browser": "^0.62.1",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*", "@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*", "@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1", "@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.2", "@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3", "@typed/lazy-ref": "^0.3.3",
"effect": "^3.14.8", "effect": "^3.15.1",
"lucide-react": "^0.487.0", "lucide-react": "^0.510.0",
"mobx": "^6.13.7", "mobx": "^6.13.7",
"reffuse": "workspace:*" "reffuse": "workspace:*"
}, },
"overrides": { "overrides": {
"effect": "^3.14.8", "effect": "^3.15.1",
"@effect/platform": "^0.80.8", "@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.59.8", "@effect/platform-browser": "^0.62.1",
"@typed/lazy-ref": "^0.3.3", "@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1" "@typed/async-data": "^0.13.1"
} }

View File

@@ -1,15 +1,15 @@
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes" import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect" import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
import { useState } from "react" import { useState } from "react"
import { AppQueryErrorHandler } from "./query"
import { R } from "./reffuse" import { R } from "./reffuse"
import { AppQueryErrorHandler } from "./services"
export function VQueryErrorHandler() { export function VQueryErrorHandler() {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const error = R.useSubscribeStream( const error = R.useSubscribeStream(
R.useMemo(() => AppQueryErrorHandler.pipe( R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
Effect.map(handler => handler.errors.pipe( Effect.map(handler => handler.errors.pipe(
Stream.changes, Stream.changes,
Stream.tap(Console.error), Stream.tap(Console.error),

View File

@@ -1,6 +1,5 @@
import { ThSchema } from "@thilawyn/thilaschema" import { ThSchema } from "@thilawyn/thilaschema"
import { GetRandomValues, makeUuid4 } from "@typed/id" import { Schema } from "effect"
import { Effect, Schema } from "effect"
export class Todo extends Schema.Class<Todo>("Todo")({ export class Todo extends Schema.Class<Todo>("Todo")({
@@ -18,9 +17,4 @@ export const TodoFromJsonStruct = Schema.Struct({
ThSchema.assertEncodedJsonifiable ThSchema.assertEncodedJsonifiable
) )
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo)) export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
export const generateUniqueID = makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom)
)

View File

@@ -5,14 +5,14 @@ import { Layer } from "effect"
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { ReffuseRuntime } from "reffuse" import { ReffuseRuntime } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
import { RootContext } from "./reffuse" import { RootContext } from "./reffuse"
import { routeTree } from "./routeTree.gen" import { routeTree } from "./routeTree.gen"
import { AppQueryClient, AppQueryErrorHandler } from "./services"
const layer = Layer.empty.pipe( const layer = Layer.empty.pipe(
Layer.provideMerge(AppQueryClient.Live), Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
Layer.provideMerge(AppQueryErrorHandler.Live), Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
Layer.provideMerge(Clipboard.layer), Layer.provideMerge(Clipboard.layer),
Layer.provideMerge(Geolocation.layer), Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer), Layer.provideMerge(Permissions.layer),

View File

@@ -1,21 +0,0 @@
import { HttpClientError } from "@effect/platform"
import { QueryClient, QueryErrorHandler } from "@reffuse/extension-query"
import { Effect } from "effect"
export class AppQueryErrorHandler extends QueryErrorHandler.Service<AppQueryErrorHandler,
HttpClientError.HttpClientError
>()(
"AppQueryErrorHandler",
(self, failure, defect) => self.pipe(
Effect.catchTags({
RequestError: failure,
ResponseError: failure,
}),
Effect.catchAllDefect(defect),
),
) {}
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({ ErrorHandler: AppQueryErrorHandler }) {}

View File

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

View File

@@ -8,7 +8,7 @@ export function Uuid4QueryService() {
const runFork = R.useRunFork() const runFork = R.useRunFork()
const query = R.useMemo(() => Uuid4Query.Uuid4Query, []) const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
const [state] = R.useRefState(query.state) const [state] = R.useSubscribeRefs(query.stateRef)
return ( return (

View File

@@ -3,12 +3,12 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { LazyRefExtension } from "@reffuse/extension-lazyref" import { LazyRefExtension } from "@reffuse/extension-lazyref"
import { QueryExtension } from "@reffuse/extension-query" import { QueryExtension } from "@reffuse/extension-query"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query" import { AppQueryClient, AppQueryErrorHandler } from "./services"
export const RootContext = ReffuseContext.make< export const RootContext = ReffuseContext.make<
| AppQueryClient | AppQueryClient.AppQueryClient
| AppQueryErrorHandler | AppQueryErrorHandler.AppQueryErrorHandler
| Clipboard.Clipboard | Clipboard.Clipboard
| Geolocation.Geolocation | Geolocation.Geolocation
| Permissions.Permissions | Permissions.Permissions

View File

@@ -16,9 +16,11 @@ import { Route as TimeImport } from './routes/time'
import { Route as TestsImport } from './routes/tests' import { Route as TestsImport } from './routes/tests'
import { Route as PromiseImport } from './routes/promise' import { Route as PromiseImport } from './routes/promise'
import { Route as LazyrefImport } from './routes/lazyref' import { Route as LazyrefImport } from './routes/lazyref'
import { Route as EffectComponentTestsImport } from './routes/effect-component-tests'
import { Route as CountImport } from './routes/count' import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank' import { Route as BlankImport } from './routes/blank'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as StreamsPullImport } from './routes/streams/pull'
import { Route as QueryUsequeryImport } from './routes/query/usequery' import { Route as QueryUsequeryImport } from './routes/query/usequery'
import { Route as QueryUsemutationImport } from './routes/query/usemutation' import { Route as QueryUsemutationImport } from './routes/query/usemutation'
import { Route as QueryServiceImport } from './routes/query/service' import { Route as QueryServiceImport } from './routes/query/service'
@@ -55,6 +57,12 @@ const LazyrefRoute = LazyrefImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const EffectComponentTestsRoute = EffectComponentTestsImport.update({
id: '/effect-component-tests',
path: '/effect-component-tests',
getParentRoute: () => rootRoute,
} as any)
const CountRoute = CountImport.update({ const CountRoute = CountImport.update({
id: '/count', id: '/count',
path: '/count', path: '/count',
@@ -73,6 +81,12 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const StreamsPullRoute = StreamsPullImport.update({
id: '/streams/pull',
path: '/streams/pull',
getParentRoute: () => rootRoute,
} as any)
const QueryUsequeryRoute = QueryUsequeryImport.update({ const QueryUsequeryRoute = QueryUsequeryImport.update({
id: '/query/usequery', id: '/query/usequery',
path: '/query/usequery', path: '/query/usequery',
@@ -116,6 +130,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CountImport preLoaderRoute: typeof CountImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/effect-component-tests': {
id: '/effect-component-tests'
path: '/effect-component-tests'
fullPath: '/effect-component-tests'
preLoaderRoute: typeof EffectComponentTestsImport
parentRoute: typeof rootRoute
}
'/lazyref': { '/lazyref': {
id: '/lazyref' id: '/lazyref'
path: '/lazyref' path: '/lazyref'
@@ -172,6 +193,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof QueryUsequeryImport preLoaderRoute: typeof QueryUsequeryImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/streams/pull': {
id: '/streams/pull'
path: '/streams/pull'
fullPath: '/streams/pull'
preLoaderRoute: typeof StreamsPullImport
parentRoute: typeof rootRoute
}
} }
} }
@@ -181,6 +209,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/count': typeof CountRoute '/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute '/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
@@ -189,12 +218,14 @@ export interface FileRoutesByFullPath {
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute '/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/count': typeof CountRoute '/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute '/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
@@ -203,6 +234,7 @@ export interface FileRoutesByTo {
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute '/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -210,6 +242,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/count': typeof CountRoute '/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute '/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
@@ -218,6 +251,7 @@ export interface FileRoutesById {
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute '/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
@@ -226,6 +260,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/blank' | '/blank'
| '/count' | '/count'
| '/effect-component-tests'
| '/lazyref' | '/lazyref'
| '/promise' | '/promise'
| '/tests' | '/tests'
@@ -234,11 +269,13 @@ export interface FileRouteTypes {
| '/query/service' | '/query/service'
| '/query/usemutation' | '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/blank' | '/blank'
| '/count' | '/count'
| '/effect-component-tests'
| '/lazyref' | '/lazyref'
| '/promise' | '/promise'
| '/tests' | '/tests'
@@ -247,11 +284,13 @@ export interface FileRouteTypes {
| '/query/service' | '/query/service'
| '/query/usemutation' | '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/blank' | '/blank'
| '/count' | '/count'
| '/effect-component-tests'
| '/lazyref' | '/lazyref'
| '/promise' | '/promise'
| '/tests' | '/tests'
@@ -260,6 +299,7 @@ export interface FileRouteTypes {
| '/query/service' | '/query/service'
| '/query/usemutation' | '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -267,6 +307,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
BlankRoute: typeof BlankRoute BlankRoute: typeof BlankRoute
CountRoute: typeof CountRoute CountRoute: typeof CountRoute
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
LazyrefRoute: typeof LazyrefRoute LazyrefRoute: typeof LazyrefRoute
PromiseRoute: typeof PromiseRoute PromiseRoute: typeof PromiseRoute
TestsRoute: typeof TestsRoute TestsRoute: typeof TestsRoute
@@ -275,12 +316,14 @@ export interface RootRouteChildren {
QueryServiceRoute: typeof QueryServiceRoute QueryServiceRoute: typeof QueryServiceRoute
QueryUsemutationRoute: typeof QueryUsemutationRoute QueryUsemutationRoute: typeof QueryUsemutationRoute
QueryUsequeryRoute: typeof QueryUsequeryRoute QueryUsequeryRoute: typeof QueryUsequeryRoute
StreamsPullRoute: typeof StreamsPullRoute
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
BlankRoute: BlankRoute, BlankRoute: BlankRoute,
CountRoute: CountRoute, CountRoute: CountRoute,
EffectComponentTestsRoute: EffectComponentTestsRoute,
LazyrefRoute: LazyrefRoute, LazyrefRoute: LazyrefRoute,
PromiseRoute: PromiseRoute, PromiseRoute: PromiseRoute,
TestsRoute: TestsRoute, TestsRoute: TestsRoute,
@@ -289,6 +332,7 @@ const rootRouteChildren: RootRouteChildren = {
QueryServiceRoute: QueryServiceRoute, QueryServiceRoute: QueryServiceRoute,
QueryUsemutationRoute: QueryUsemutationRoute, QueryUsemutationRoute: QueryUsemutationRoute,
QueryUsequeryRoute: QueryUsequeryRoute, QueryUsequeryRoute: QueryUsequeryRoute,
StreamsPullRoute: StreamsPullRoute,
} }
export const routeTree = rootRoute export const routeTree = rootRoute
@@ -304,6 +348,7 @@ export const routeTree = rootRoute
"/", "/",
"/blank", "/blank",
"/count", "/count",
"/effect-component-tests",
"/lazyref", "/lazyref",
"/promise", "/promise",
"/tests", "/tests",
@@ -311,7 +356,8 @@ export const routeTree = rootRoute
"/todos", "/todos",
"/query/service", "/query/service",
"/query/usemutation", "/query/usemutation",
"/query/usequery" "/query/usequery",
"/streams/pull"
] ]
}, },
"/": { "/": {
@@ -323,6 +369,9 @@ export const routeTree = rootRoute
"/count": { "/count": {
"filePath": "count.tsx" "filePath": "count.tsx"
}, },
"/effect-component-tests": {
"filePath": "effect-component-tests.tsx"
},
"/lazyref": { "/lazyref": {
"filePath": "lazyref.tsx" "filePath": "lazyref.tsx"
}, },
@@ -346,6 +395,9 @@ export const routeTree = rootRoute
}, },
"/query/usequery": { "/query/usequery": {
"filePath": "query/usequery.tsx" "filePath": "query/usequery.tsx"
},
"/streams/pull": {
"filePath": "streams/pull.tsx"
} }
} }
} }

View File

@@ -1,6 +1,6 @@
import { R } from "@/reffuse" import { R } from "@/reffuse"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Ref } from "effect" import { Effect, Ref } from "effect"
export const Route = createFileRoute("/count")({ export const Route = createFileRoute("/count")({
@@ -11,14 +11,13 @@ function Count() {
const runSync = R.useRunSync() const runSync = R.useRunSync()
const countRef = R.useRef(0) const countRef = R.useRef(() => Effect.succeed(0))
const [count] = R.useRefState(countRef) const [count] = R.useSubscribeRefs(countRef)
return ( return (
<div className="container mx-auto"> <div className="container mx-auto">
{/* <button onClick={() => setCount((count) => count + 1)}> */} <button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
count is {count} count is {count}
</button> </button>
</div> </div>

View File

@@ -0,0 +1,105 @@
import { Box, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
Console.log("Runtime built."),
() => Console.log("Runtime destroyed."),
))
class TestService extends Effect.Service<TestService>()("TestService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
}) {}
class SubService extends Effect.Service<SubService>()("SubService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
}) {}
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
Layer.provideMerge(LogLive),
Layer.provideMerge(TestService.Default),
))
export const Route = createFileRoute("/effect-component-tests")({
component: RouteComponent,
})
function RouteComponent() {
return (
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
<MyRoute />
</ReactManagedRuntime.AsyncProvider>
)
}
const MyRoute = pipe(
Effect.fn(function*() {
const runtime = yield* Effect.runtime()
const service = yield* TestService
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
// const MyTestComponentFC = yield* Effect.provide(
// ReactComponent.useFC(MyTestComponent),
// yield* ReactHook.useMemoLayer(SubService.Default),
// )
return <>
<Box>
<TextField.Root
value={value}
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
/>
</Box>
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
)} */}
{/* {Array.range(0, 3).map(k =>
<MyTestComponentFC key={k} />
)} */}
{yield* pipe(
Array.range(0, 3),
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
<FC key={k} />
)),
Effect.all,
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
)}
</>
}),
ReactComponent.withDisplayName("MyRoute"),
ReactComponent.withRuntime(runtime.context),
)
const MyTestComponent = pipe(
Effect.fn(function*() {
const runtime = yield* Effect.runtime()
const service = yield* SubService
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
// yield* ReactHook.useMemo(() => Effect.andThen(
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
// Console.log("MyTestComponent mounted"),
// ), [])
return <>
<Box>
<TextField.Root
value={value}
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
/>
</Box>
</>
}),
ReactComponent.withDisplayName("MyTestComponent"),
)

View File

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

View File

@@ -29,15 +29,18 @@ function RouteComponent() {
Effect.tap(() => QueryProgress.QueryProgress.update(() => Effect.tap(() => QueryProgress.QueryProgress.update(() =>
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) }) AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
)), )),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), Effect.andThen(Effect.map(
HttpClient.withTracerPropagation(false), HttpClient.HttpClient,
HttpClient.withTracerPropagation(false),
)),
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
Effect.flatMap(res => res.json), Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)), Effect.flatMap(Schema.decodeUnknown(Result)),
Effect.scoped, Effect.scoped,
) )
}) })
const [state] = R.useSubscribeRefs(mutation.state) const [state] = R.useSubscribeRefs(mutation.stateRef)
return ( return (

View File

@@ -20,18 +20,21 @@ function RouteComponent() {
const [count, setCount] = useState(1) const [count, setCount] = useState(1)
const query = R.useQuery({ const query = R.useQuery({
key: R.useStreamFromValues(["uuid4", count]), key: R.useStreamFromReactiveValues(["uuid4", count]),
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe( query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(Effect.sleep("500 millis")), Effect.andThen(Effect.sleep("500 millis")),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)), Effect.andThen(Effect.map(
HttpClient.withTracerPropagation(false), HttpClient.HttpClient,
HttpClient.withTracerPropagation(false),
)),
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
Effect.flatMap(res => res.json), Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)), Effect.flatMap(Schema.decodeUnknown(Result)),
Effect.scoped, Effect.scoped,
), ),
}) })
const [state] = R.useSubscribeRefs(query.state) const [state] = R.useSubscribeRefs(query.stateRef)
return ( return (

View File

@@ -0,0 +1,34 @@
import { R } from "@/reffuse"
import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Chunk, Effect, Exit, Option, Queue, Random, Scope, Stream } from "effect"
import { useMemo, useState } from "react"
export const Route = createFileRoute("/streams/pull")({
component: RouteComponent
})
function RouteComponent() {
const stream = useMemo(() => Stream.repeatEffect(Random.nextInt), [])
const streamScope = R.useScope([stream], { finalizerExecutionMode: "fork" })
const queue = R.useMemo(() => Effect.provideService(Stream.toQueueOfElements(stream), Scope.Scope, streamScope), [streamScope])
const [value, setValue] = useState(Option.none<number>())
const pullLatest = R.useCallbackSync(() => Queue.takeAll(queue).pipe(
Effect.flatMap(Chunk.last),
Effect.flatMap(Exit.matchEffect({
onSuccess: Effect.succeed,
onFailure: Effect.fail,
})),
Effect.tap(v => Effect.sync(() => setValue(Option.some(v)))),
), [queue])
return (
<Flex direction="column" align="center" gap="2">
{Option.isSome(value) && <Text>{value.value}</Text>}
<Button onClick={pullLatest}>Pull latest</Button>
</Flex>
)
}

View File

@@ -2,7 +2,21 @@ import { R } from "@/reffuse"
import { Button, Flex, Text } from "@radix-ui/themes" import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id" import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Ref } from "effect" import { Console, Effect, Option } from "effect"
import { useEffect, useState } from "react"
interface Node {
value: string
left?: Leaf
right?: Leaf
}
interface Leaf {
node: Node
}
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
export const Route = createFileRoute("/tests")({ export const Route = createFileRoute("/tests")({
@@ -10,40 +24,39 @@ export const Route = createFileRoute("/tests")({
}) })
function RouteComponent() { function RouteComponent() {
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe( const runSync = R.useRunSync()
// Effect.andThen(makeUuid4),
// Effect.provide(GetRandomValues.CryptoRandom),
// ), [])
// console.log(value)
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe( const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
Effect.andThen(Console.log("ouient")), const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
Effect.delay("1 second"), Effect.tap(v => Effect.sync(() => setUuid(v)))
), []) ), [])
const uuidStream = R.useStreamFromReactiveValues([uuid])
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
const uuidRef = R.useRef("none") const [, scopeLayer] = R.useScope([uuid])
const anotherRef = R.useRef(69)
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
Effect.andThen(Console.log("Scope changed")),
Effect.provide(scopeLayer),
runSync,
), [scopeLayer, runSync])
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) { const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
yield* Effect.log(value) const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
}), [])
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom),
Effect.flatMap(v => Ref.set(uuidRef, v)),
), [])
return ( return (
<Flex direction="row" justify="center" align="center" gap="2"> <Flex direction="column" justify="center" align="center" gap="2">
<R.SubscribeRefs refs={[uuidRef, anotherRef]}> <Text>{uuid}</Text>
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>} <Button onClick={generateUuid}>Generate UUID</Button>
</R.SubscribeRefs> <Text>
{Option.match(uuidStreamLatestValue, {
<Button onClick={() => logValue("test")}>Log value</Button> onSome: ([v]) => v,
<Button onClick={() => generateUuid()}>Generate UUID</Button> onNone: () => <></>,
})}
</Text>
</Flex> </Flex>
) )
} }

View File

@@ -26,7 +26,7 @@ function Todos() {
return ( return (
<Container> <Container>
<TodosContext.Provider layer={todosLayer}> <TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
<VTodos /> <VTodos />
</TodosContext.Provider> </TodosContext.Provider>
</Container> </Container>

View File

@@ -0,0 +1,7 @@
import { QueryClient } from "@reffuse/extension-query"
import * as AppQueryErrorHandler from "./AppQueryErrorHandler"
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({
errorHandler: AppQueryErrorHandler.AppQueryErrorHandler
}) {}

View File

@@ -0,0 +1,13 @@
import { HttpClientError } from "@effect/platform"
import { QueryErrorHandler } from "@reffuse/extension-query"
import { Effect } from "effect"
export class AppQueryErrorHandler extends Effect.Service<AppQueryErrorHandler>()("AppQueryErrorHandler", {
effect: QueryErrorHandler.make<HttpClientError.HttpClientError>()(
(self, failure, defect) => self.pipe(
Effect.catchTag("RequestError", "ResponseError", failure),
Effect.catchAllDefect(defect),
)
)
}) {}

View File

@@ -1 +1,2 @@
export {} export * as AppQueryClient from "./AppQueryClient"
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"

View File

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

View File

@@ -1,25 +1,27 @@
import { Todo } from "@/domain" import { Todo } from "@/domain"
import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes" import { Box, Button, Card, Flex, TextArea } from "@radix-ui/themes"
import { Effect, Option, SubscriptionRef } from "effect" import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Chunk, Effect, Option, Ref } from "effect"
import { R } from "../reffuse" import { R } from "../reffuse"
import { TodosState } from "../services" import { TodosState } from "../services"
const createEmptyTodo = Todo.generateUniqueID.pipe( const createEmptyTodo = makeUuid4.pipe(
Effect.map(id => Todo.Todo.make({ Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
id, Effect.provide(GetRandomValues.CryptoRandom),
content: "",
completedAt: Option.none(),
}, true))
) )
export function VNewTodo() { export function VNewTodo() {
const runSync = R.useRunSync() const todoRef = R.useRef(() => createEmptyTodo)
const [content, setContent] = R.useRefState(R.useSubRefFromPath(todoRef, ["content"]))
const todoRef = R.useMemo(() => createEmptyTodo.pipe(Effect.flatMap(SubscriptionRef.make)), []) const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
const [todo, setTodo] = R.useRefState(todoRef) Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
Effect.andThen(createEmptyTodo),
Effect.flatMap(v => Ref.set(todoRef, v)),
), [todoRef])
return ( return (
@@ -27,23 +29,12 @@ export function VNewTodo() {
<Card> <Card>
<Flex direction="column" align="stretch" gap="2"> <Flex direction="column" align="stretch" gap="2">
<TextArea <TextArea
value={todo.content} value={content}
onChange={e => setTodo(prev => onChange={e => setContent(e.target.value)}
Todo.Todo.make({ ...prev, content: e.target.value }, true)
)}
/> />
<Flex direction="row" justify="center" align="center"> <Flex direction="row" justify="center" align="center">
<Button <Button onClick={add}>Add</Button>
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.prepend(todo)),
Effect.andThen(createEmptyTodo),
Effect.map(setTodo),
runSync,
)}
>
Add
</Button>
</Flex> </Flex>
</Flex> </Flex>
</Card> </Card>

View File

@@ -1,20 +1,28 @@
import { Todo } from "@/domain" import { Todo } from "@/domain"
import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes" import { Box, Card, Flex, IconButton, TextArea } from "@radix-ui/themes"
import { Effect } from "effect" import { Effect, Ref, Stream, SubscriptionRef } from "effect"
import { Delete } from "lucide-react" import { Delete } from "lucide-react"
import { useState } from "react" import { useState } from "react"
import { R } from "../reffuse" import { R } from "../reffuse"
import { TodosState } from "../services"
export interface VTodoProps { export interface VTodoProps {
readonly index: number readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
readonly todo: Todo.Todo readonly remove: Effect.Effect<void>
} }
export function VTodo({ index, todo }: VTodoProps) { export function VTodo({ todoRef, remove }: VTodoProps) {
const runSync = R.useRunSync() const runSync = R.useRunSync()
const localTodoRef = R.useRef(() => todoRef)
const [content, setContent] = R.useRefState(R.useSubRefFromPath(localTodoRef, ["content"]))
R.useFork(() => localTodoRef.changes.pipe(
Stream.debounce("250 millis"),
Stream.runForEach(v => Ref.set(todoRef, v)),
), [localTodoRef])
const editorMode = useState(false) const editorMode = useState(false)
@@ -23,14 +31,8 @@ export function VTodo({ index, todo }: VTodoProps) {
<Card> <Card>
<Flex direction="column" align="stretch" gap="1"> <Flex direction="column" align="stretch" gap="1">
<TextArea <TextArea
value={todo.content} value={content}
onChange={e => TodosState.TodosState.pipe( onChange={e => setContent(e.target.value)}
Effect.flatMap(state => state.replace(
index,
Todo.Todo.make({ ...todo, content: e.target.value }, true),
)),
runSync,
)}
disabled={!editorMode} disabled={!editorMode}
/> />
@@ -38,12 +40,7 @@ export function VTodo({ index, todo }: VTodoProps) {
<Box></Box> <Box></Box>
<Flex direction="row" align="center" gap="1"> <Flex direction="row" align="center" gap="1">
<IconButton <IconButton onClick={() => runSync(remove)}>
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
<Delete /> <Delete />
</IconButton> </IconButton>
</Flex> </Flex>

View File

@@ -1,5 +1,5 @@
import { Box, Flex } from "@radix-ui/themes" import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect" import { Chunk, Effect, Ref } from "effect"
import { R } from "../reffuse" import { R } from "../reffuse"
import { TodosState } from "../services" import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo" import { VNewTodo } from "./VNewTodo"
@@ -8,15 +8,8 @@ import { VTodo } from "./VTodo"
export function VTodos() { export function VTodos() {
// Sync changes to the todos with the local storage const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
R.useFork(() => TodosState.TodosState.pipe( const [todos] = R.useSubscribeRefs(todosRef)
Effect.flatMap(state =>
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
)
), [])
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
const [todos] = R.useRefState(todosRef)
return ( return (
@@ -27,7 +20,16 @@ export function VTodos() {
{Chunk.map(todos, (todo, index) => ( {Chunk.map(todos, (todo, index) => (
<Box key={todo.id} width="500px"> <Box key={todo.id} width="500px">
<VTodo index={index} todo={todo} /> <R.SubRefFromGetSet
parent={todosRef}
getter={parentValue => Chunk.unsafeGet(parentValue, index)}
setter={(parentValue, value) => Chunk.replace(parentValue, index, value)}
>
{ref => <VTodo
todoRef={ref}
remove={Ref.update(todosRef, Chunk.remove(index))}
/>}
</R.SubRefFromGetSet>
</Box> </Box>
))} ))}
</Flex> </Flex>

View File

@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/extension-lazyref", "name": "@reffuse/extension-lazyref",
"version": "0.1.3", "version": "0.1.4",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -35,8 +35,8 @@
"peerDependencies": { "peerDependencies": {
"@typed/lazy-ref": "^0.3.0", "@typed/lazy-ref": "^0.3.0",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"effect": "^3.13.0", "effect": "^3.15.0",
"react": "^19.0.0", "react": "^19.0.0",
"reffuse": "^0.1.7" "reffuse": "^0.1.8"
} }
} }

View File

@@ -1,7 +1,8 @@
import * as LazyRef from "@typed/lazy-ref" import * as LazyRef from "@typed/lazy-ref"
import { Effect, pipe, Stream } from "effect" import { Effect, pipe, Stream } from "effect"
import * as React from "react" import * as React from "react"
import { ReffuseExtension, type ReffuseNamespace, SetStateAction } from "reffuse" import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import { SetStateAction } from "reffuse/types"
export const LazyRefExtension = ReffuseExtension.make(() => ({ export const LazyRefExtension = ReffuseExtension.make(() => ({

View File

@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/extension-query", "name": "@reffuse/extension-query",
"version": "0.1.3", "version": "0.1.5",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -37,8 +37,8 @@
"@effect/platform-browser": "^0.56.0", "@effect/platform-browser": "^0.56.0",
"@typed/async-data": "^0.13.0", "@typed/async-data": "^0.13.0",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"effect": "^3.13.0", "effect": "^3.15.0",
"react": "^19.0.0", "react": "^19.0.0",
"reffuse": "^0.1.6" "reffuse": "^0.1.11"
} }
} }

View File

@@ -1,12 +1,11 @@
import * as AsyncData from "@typed/async-data" import * as AsyncData from "@typed/async-data"
import { type Context, Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect" import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "../QueryClient.js" import type * as QueryClient from "./QueryClient.js"
import * as QueryProgress from "../QueryProgress.js" import * as QueryProgress from "./QueryProgress.js"
import * as QueryState from "./QueryState.js" import { QueryState } from "./internal/index.js"
export interface MutationRunner<K extends readonly unknown[], A, E, R> { export interface MutationRunner<K extends readonly unknown[], A, E> {
readonly context: Context.Context<R>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>> readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>> readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
@@ -17,6 +16,11 @@ export interface MutationRunner<K extends readonly unknown[], A, E, R> {
} }
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, MutationRunner<K, A, E>>()
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> { export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE> readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress> readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
@@ -28,7 +32,7 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
mutation, mutation,
}: MakeProps<K, A, FallbackA, E, HandledE, R> }: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect< ): Effect.Effect<
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>, R>, MutationRunner<K, A | FallbackA, Exclude<E, HandledE>>,
never, never,
R | QueryClient.TagClassShape<FallbackA, HandledE> R | QueryClient.TagClassShape<FallbackA, HandledE>
> => Effect.gen(function*() { > => Effect.gen(function*() {
@@ -37,25 +41,18 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>() const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
const run = (key: K) => Effect.Do.pipe( const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe(
Effect.bind("state", () => queryStateTag), Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
Effect.bind("client", () => QueryClient),
Effect.flatMap(({ state, client }) => state.set(AsyncData.loading()).pipe(
Effect.andThen(mutation(key)), Effect.andThen(mutation(key)),
client.errorHandler.handle, client.errorHandler.handle,
Effect.matchCauseEffect({ Effect.matchCauseEffect({
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe( onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
Effect.tap(state.set) onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
),
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
Effect.tap(state.set)
),
}), }),
)), )),
Effect.provide(context), Effect.provide(context),
Effect.provide(QueryProgress.QueryProgress.Live), Effect.provide(QueryProgress.QueryProgress.Default),
) )
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer( const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
@@ -64,11 +61,11 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
value => Ref.set(globalStateRef, value), value => Ref.set(globalStateRef, value),
)) ))
const forkMutate = (...key: K) => Effect.Do.pipe( const forkMutate = (...key: K) => Effect.all([
Effect.bind("stateRef", () => Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())), Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>()),
Effect.bind("stateQueue", () => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>()), Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>(),
]).pipe(
Effect.flatMap(({ stateRef, stateQueue }) => Effect.flatMap(([stateRef, stateQueue]) =>
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe( Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
Effect.andThen(run(key)), Effect.andThen(run(key)),
Effect.scoped, Effect.scoped,

View File

@@ -1,16 +0,0 @@
import type * as AsyncData from "@typed/async-data"
import { Effect, type Fiber, type Stream, type SubscriptionRef } from "effect"
export interface MutationService<K extends readonly unknown[], A, E> {
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, MutationService<K, A, E>>()

View File

@@ -1,4 +1,4 @@
import { Context, Effect, Layer } from "effect" import { Context, Effect, identity, Layer } from "effect"
import type { Mutable } from "effect/Types" import type { Mutable } from "effect/Types"
import * as QueryErrorHandler from "./QueryErrorHandler.js" import * as QueryErrorHandler from "./QueryErrorHandler.js"
@@ -8,6 +8,17 @@ export interface QueryClient<FallbackA, HandledE> {
} }
export interface MakeProps<FallbackA, HandledE> {
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
}
export const make = <FallbackA, HandledE>(
{ errorHandler }: MakeProps<FallbackA, HandledE>
): Effect.Effect<QueryClient<FallbackA, HandledE>> => Effect.Do.pipe(
Effect.let("errorHandler", () => errorHandler)
)
const id = "@reffuse/extension-query/QueryClient" const id = "@reffuse/extension-query/QueryClient"
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>> export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
@@ -19,46 +30,28 @@ export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)() export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
export interface ServiceProps<EH, FallbackA, HandledE> { export interface ServiceProps<FallbackA = never, HandledE = never, E = never, R = never> {
readonly ErrorHandler?: Context.Tag<EH, QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>> readonly errorHandler?: Effect.Effect<QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, E, R>
} }
export interface ServiceResult<Self, EH, FallbackA, HandledE> extends Context.TagClass< export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
Self, Self,
typeof id, typeof id,
QueryClient<FallbackA, HandledE> QueryClient<FallbackA, HandledE>
> { > {
readonly Live: Layer.Layer< readonly Default: Layer.Layer<Self, E, R>
Self | (EH extends QueryErrorHandler.DefaultQueryErrorHandler ? EH : never),
never,
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
>
} }
export const Service = <Self>() => ( export const Service = <Self>() => (
< <FallbackA = never, HandledE = never, E = never, R = never>(
EH = QueryErrorHandler.DefaultQueryErrorHandler, props?: ServiceProps<FallbackA, HandledE, E, R>
FallbackA = QueryErrorHandler.Fallback<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>, ): ServiceResult<Self, FallbackA, HandledE, E, R> => {
HandledE = QueryErrorHandler.Error<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>, const TagClass = Context.Tag(id)() as ServiceResult<Self, FallbackA, HandledE, E, R>
>(
props?: ServiceProps<EH, FallbackA, HandledE>
): ServiceResult<Self, EH, FallbackA, HandledE> => {
const TagClass = Context.Tag(id)() as ServiceResult<Self, EH, FallbackA, HandledE>
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.Do.pipe( (TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
Effect.bind("errorHandler", () => props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
(props?.ErrorHandler ?? QueryErrorHandler.DefaultQueryErrorHandler) as Effect.Effect< errorHandler => make({ errorHandler }),
QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, ))
never,
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
>
)
)).pipe(
Layer.provideMerge((props?.ErrorHandler
? Layer.empty
: QueryErrorHandler.DefaultQueryErrorHandler.Live
) as Layer.Layer<EH>)
)
return TagClass return TagClass
} }

View File

@@ -1,5 +1,4 @@
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect" import { Cause, Effect, PubSub, Stream } from "effect"
import type { Mutable } from "effect/Types"
export interface QueryErrorHandler<FallbackA, HandledE> { export interface QueryErrorHandler<FallbackA, HandledE> {
@@ -11,55 +10,31 @@ export type Fallback<T> = T extends QueryErrorHandler<infer A, any> ? A : never
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
export interface ServiceResult< export const make = <HandledE = never>() => (
Self, <FallbackA>(
Id extends string,
FallbackA,
HandledE,
> extends Context.TagClass<
Self,
Id,
QueryErrorHandler<FallbackA, HandledE>
> {
readonly Live: Layer.Layer<Self>
}
export const Service = <Self, HandledE = never>() => (
<const Id extends string, FallbackA>(
id: Id,
f: ( f: (
self: Effect.Effect<never, HandledE>, self: Effect.Effect<never, HandledE>,
failure: (failure: HandledE) => Effect.Effect<never>, failure: (failure: HandledE) => Effect.Effect<never>,
defect: (defect: unknown) => Effect.Effect<never>, defect: (defect: unknown) => Effect.Effect<never>,
) => Effect.Effect<FallbackA>, ) => Effect.Effect<FallbackA>
): ServiceResult<Self, Id, FallbackA, HandledE> => { ): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, FallbackA, HandledE> const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
const errors = Stream.fromPubSub(pubsub)
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.gen(function*() { const handle = <A, E, R>(
const queue = yield* Queue.unbounded<Cause.Cause<HandledE>>() self: Effect.Effect<A, E, R>
const errors = Stream.fromQueue(queue) ): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
self as unknown as Effect.Effect<never, HandledE, never>,
(failure: HandledE) => Effect.andThen(
PubSub.publish(pubsub, Cause.fail(failure)),
Effect.failCause(Cause.empty),
),
(defect: unknown) => Effect.andThen(
PubSub.publish(pubsub, Cause.die(defect)),
Effect.failCause(Cause.empty),
),
)
const handle = <A, E, R>( return { errors, handle }
self: Effect.Effect<A, E, R> })
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
self as unknown as Effect.Effect<never, HandledE, never>,
(failure: HandledE) => Queue.offer(queue, Cause.fail(failure)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
(defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
)
return { errors, handle }
}))
return TagClass
}
) )
export class DefaultQueryErrorHandler extends Service<DefaultQueryErrorHandler>()(
"@reffuse/extension-query/DefaultQueryErrorHandler",
identity,
) {}

View File

@@ -1,53 +1,21 @@
import type * as AsyncData from "@typed/async-data" import type { Effect, Stream } from "effect"
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 ReffuseNamespace } from "reffuse" import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import type * as MutationService from "./MutationService.js" import * as MutationRunner from "./MutationRunner.js"
import * as QueryClient from "./QueryClient.js" import * as QueryClient from "./QueryClient.js"
import type * as QueryProgress from "./QueryProgress.js" import type * as QueryProgress from "./QueryProgress.js"
import type * as QueryService from "./QueryService.js" import * as QueryRunner from "./QueryRunner.js"
import { MutationRunner, QueryRunner } from "./internal/index.js"
export interface UseQueryProps<K extends readonly unknown[], A, E, R> { export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
readonly key: Stream.Stream<K> readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress> readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
readonly refreshOnWindowFocus?: boolean readonly options?: QueryRunner.RunOptions
} }
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 forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
) => Layer.Layer<Self>
}
export interface UseMutationProps<K extends readonly unknown[], A, E, R> { export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress> readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
} }
export interface UseMutationResult<K extends readonly unknown[], A, E> {
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, MutationService.MutationService<K, A, E>>
) => Layer.Layer<Self>
}
export const QueryExtension = ReffuseExtension.make(() => ({ export const QueryExtension = ReffuseExtension.make(() => ({
useQuery< useQuery<
@@ -61,32 +29,16 @@ export const QueryExtension = ReffuseExtension.make(() => ({
>( >(
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>, this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseQueryProps<QK, QA, QE, QR>, props: UseQueryProps<QK, QA, QE, QR>,
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> { ): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => QueryRunner.make({ const runner = this.useMemo(() => QueryRunner.make({
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(), QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
key: props.key, key: props.key,
query: props.query, query: props.query,
}), [props.key]) }), [props.key])
this.useFork(() => runner.fetchOnKeyChange, [runner]) this.useFork(() => QueryRunner.run(runner, props.options), [runner])
this.useFork(() => (props.refreshOnWindowFocus ?? true) return runner
? runner.refreshOnWindowFocus
: Effect.void,
[props.refreshOnWindowFocus, runner])
return React.useMemo(() => ({
latestKey: runner.latestKeyRef,
state: runner.stateRef,
forkRefresh: runner.forkRefresh,
layer: tag => Layer.succeed(tag, {
latestKey: runner.latestKeyRef,
state: runner.stateRef,
forkRefresh: runner.forkRefresh,
}),
}), [runner])
}, },
useMutation< useMutation<
@@ -100,23 +52,10 @@ export const QueryExtension = ReffuseExtension.make(() => ({
>( >(
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>, this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseMutationProps<QK, QA, QE, QR>, props: UseMutationProps<QK, QA, QE, QR>,
): UseMutationResult<QK, QA | FallbackA, Exclude<QE, HandledE>> { ): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => MutationRunner.make({ return this.useMemo(() => MutationRunner.make({
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(), QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
mutation: props.mutation, mutation: props.mutation,
}), []) }), [])
return React.useMemo(() => ({
state: runner.stateRef,
mutate: runner.mutate,
forkMutate: runner.forkMutate,
layer: tag => Layer.succeed(tag, {
state: runner.stateRef,
mutate: runner.mutate,
forkMutate: runner.forkMutate,
}),
}), [runner])
}, },
})) }))

View File

@@ -10,7 +10,7 @@ export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryPro
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
) => Effect.Effect<void> ) => Effect.Effect<void>
}>() { }>() {
static readonly Live: Layer.Layer< static readonly Default: Layer.Layer<
QueryProgress, QueryProgress,
never, never,
QueryState.QueryState<any, any> QueryState.QueryState<any, any>

View File

@@ -0,0 +1,193 @@
import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data"
import { type Cause, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "./QueryClient.js"
import * as QueryProgress from "./QueryProgress.js"
import { QueryState } from "./internal/index.js"
export interface QueryRunner<K extends readonly unknown[], A, E> {
readonly queryKey: Stream.Stream<K>
readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
AsyncData.Success<A> | AsyncData.Failure<E>,
Cause.NoSuchElementException
>>>
readonly interrupt: Effect.Effect<void>
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void>>
readonly forkFetch: (keyValue: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never
>() => Effect.Tag(id)<Self, QueryRunner<K, A, E>>()
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
}
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
{
QueryClient,
key,
query,
}: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect<
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>>,
never,
R | QueryClient.TagClassShape<FallbackA, HandledE>
> => Effect.gen(function*() {
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
const latestKeyValueRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
Cause.NoSuchElementException
>>())
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
const interrupt = Effect.flatMap(fiberRef, Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber))
),
onNone: () => Effect.void,
}))
const forkInterrupt = Effect.flatMap(fiberRef, Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber).pipe(
Effect.asVoid,
Effect.forkDaemon,
))
),
onNone: () => Effect.forkDaemon(Effect.void),
}))
const run = (keyValue: K) => Effect.all([QueryClient, queryStateTag]).pipe(
Effect.flatMap(([client, state]) => Ref.set(latestKeyValueRef, Option.some(keyValue)).pipe(
Effect.andThen(query(keyValue)),
client.errorHandler.handle,
Effect.matchCauseEffect({
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
}),
)),
Effect.provide(context),
Effect.provide(QueryProgress.QueryProgress.Default),
)
const forkFetch = (keyValue: K) => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => queryStateTag.pipe(
Effect.flatMap(state => interrupt.pipe(
Effect.andThen(
Effect.addFinalizer(() => Effect.andThen(
Ref.set(fiberRef, Option.none()),
Queue.shutdown(stateQueue),
)).pipe(
Effect.andThen(state.set(AsyncData.loading())),
Effect.andThen(run(keyValue)),
Effect.scoped,
Effect.forkDaemon,
)
),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
)),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Effect.andThen(
Queue.offer(stateQueue, value),
Ref.set(stateRef, value),
),
)),
))
)
const setInitialRefreshState = Effect.flatMap(queryStateTag, state => state.update(previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous)
return AsyncData.loading()
}))
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => interrupt.pipe(
Effect.andThen(
Effect.addFinalizer(() => Effect.andThen(
Ref.set(fiberRef, Option.none()),
Queue.shutdown(stateQueue),
)).pipe(
Effect.andThen(setInitialRefreshState),
Effect.andThen(latestKeyValueRef.pipe(
Effect.flatMap(identity),
Effect.flatMap(run),
)),
Effect.scoped,
Effect.forkDaemon,
)
),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Effect.andThen(
Queue.offer(stateQueue, value),
Ref.set(stateRef, value),
),
)),
))
)
return {
queryKey: key,
latestKeyValueRef,
stateRef,
fiberRef,
interrupt,
forkInterrupt,
forkFetch,
forkRefresh,
}
})
export interface RunOptions {
readonly refreshOnWindowFocus?: boolean
}
export const run = <K extends readonly unknown[], A, E>(
self: QueryRunner<K, A, E>,
options?: RunOptions,
): Effect.Effect<void, never, Scope.Scope> => Effect.gen(function*() {
if (typeof window !== "undefined" && (options?.refreshOnWindowFocus ?? true))
yield* Effect.forkScoped(
Stream.runForEach(BrowserStream.fromEventListenerWindow("focus"), () => self.forkRefresh)
)
yield* Effect.addFinalizer(() => self.interrupt)
yield* Stream.runForEach(Stream.changes(self.queryKey), latestKey => self.forkFetch(latestKey))
})

View File

@@ -1,16 +0,0 @@
import type * as AsyncData from "@typed/async-data"
import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect"
export interface QueryService<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()

View File

@@ -1,6 +1,6 @@
export * as MutationService from "./MutationService.js" export * as MutationRunner from "./MutationRunner.js"
export * as QueryClient from "./QueryClient.js" export * as QueryClient from "./QueryClient.js"
export * as QueryErrorHandler from "./QueryErrorHandler.js" export * as QueryErrorHandler from "./QueryErrorHandler.js"
export * from "./QueryExtension.js" export * from "./QueryExtension.js"
export * as QueryProgress from "./QueryProgress.js" export * as QueryProgress from "./QueryProgress.js"
export * as QueryService from "./QueryService.js" export * as QueryRunner from "./QueryRunner.js"

View File

@@ -1,191 +0,0 @@
import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data"
import { type Cause, type Context, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "../QueryClient.js"
import * as QueryProgress from "../QueryProgress.js"
import * as QueryState from "./QueryState.js"
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
readonly context: Context.Context<R>
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
AsyncData.Success<A> | AsyncData.Failure<E>,
Cause.NoSuchElementException
>>>
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly forkFetch: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
readonly refreshOnWindowFocus: Effect.Effect<void>
}
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
}
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
{
QueryClient,
key,
query,
}: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect<
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
never,
R | QueryClient.TagClassShape<FallbackA, HandledE>
> => Effect.gen(function*() {
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
Cause.NoSuchElementException
>>())
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
const interrupt = fiberRef.pipe(
Effect.flatMap(Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber))
),
onNone: () => Effect.void,
}))
)
const forkInterrupt = fiberRef.pipe(
Effect.flatMap(Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber).pipe(
Effect.asVoid,
Effect.forkDaemon,
))
),
onNone: () => Effect.forkDaemon(Effect.void),
}))
)
const run = Effect.Do.pipe(
Effect.bind("state", () => queryStateTag),
Effect.bind("client", () => QueryClient),
Effect.bind("latestKey", () => latestKeyRef.pipe(Effect.flatMap(identity))),
Effect.flatMap(({ state, client, latestKey }) => query(latestKey).pipe(
client.errorHandler.handle,
Effect.matchCauseEffect({
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
Effect.tap(state.set)
),
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
Effect.tap(state.set)
),
}),
)),
Effect.provide(context),
Effect.provide(QueryProgress.QueryProgress.Live),
)
const forkFetch = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => queryStateTag.pipe(
Effect.flatMap(state => interrupt.pipe(
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Queue.shutdown(stateQueue))
)).pipe(
Effect.andThen(state.set(AsyncData.loading())),
Effect.andThen(run),
Effect.scoped,
Effect.forkDaemon,
)),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
)),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Queue.offer(stateQueue, value).pipe(
Effect.andThen(Ref.set(stateRef, value))
),
)),
))
)
const setInitialRefreshState = queryStateTag.pipe(
Effect.flatMap(state => state.update(previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous)
return AsyncData.loading()
}))
)
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => interrupt.pipe(
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Queue.shutdown(stateQueue))
)).pipe(
Effect.andThen(setInitialRefreshState),
Effect.andThen(run),
Effect.scoped,
Effect.forkDaemon,
)),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Queue.offer(stateQueue, value).pipe(
Effect.andThen(Ref.set(stateRef, value))
),
)),
))
)
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
Effect.andThen(Stream.runForEach(Stream.changes(key), latestKey =>
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
Effect.andThen(forkFetch)
)
))
)
const refreshOnWindowFocus = Stream.runForEach(
BrowserStream.fromEventListenerWindow("focus"),
() => forkRefresh,
)
return {
context,
latestKeyRef,
stateRef,
fiberRef,
forkInterrupt,
forkFetch,
forkRefresh,
fetchOnKeyChange,
refreshOnWindowFocus,
}
})

View File

@@ -1,3 +1 @@
export * as MutationRunner from "./MutationRunner.js"
export * as QueryRunner from "./QueryRunner.js"
export * as QueryState from "./QueryState.js" export * as QueryState from "./QueryState.js"

View File

@@ -1,6 +1,6 @@
{ {
"name": "reffuse", "name": "reffuse",
"version": "0.1.7", "version": "0.1.13",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -16,6 +16,10 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"default": "./dist/index.js" "default": "./dist/index.js"
}, },
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./*": { "./*": {
"types": "./dist/*.d.ts", "types": "./dist/*.d.ts",
"default": "./dist/*.js" "default": "./dist/*.js"
@@ -31,7 +35,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"effect": "^3.13.0", "effect": "^3.15.0",
"react": "^19.0.0" "react": "^19.0.0"
} }
} }

View File

@@ -1,7 +1,7 @@
import type * as ReffuseContext from "./ReffuseContext.js" import type * as ReffuseContext from "./ReffuseContext.js"
import type * as ReffuseExtension from "./ReffuseExtension.js" import type * as ReffuseExtension from "./ReffuseExtension.js"
import * as ReffuseNamespace from "./ReffuseNamespace.js" import * as ReffuseNamespace from "./ReffuseNamespace.js"
import type { Merge, StaticType } from "./types.js" import type { Merge, StaticType } from "./utils.js"
export class Reffuse extends ReffuseNamespace.makeClass() {} export class Reffuse extends ReffuseNamespace.makeClass() {}

View File

@@ -1,4 +1,4 @@
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect" import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect"
import * as React from "react" import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js" import * as ReffuseRuntime from "./ReffuseRuntime.js"
@@ -25,6 +25,8 @@ export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{ export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope> readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly children?: React.ReactNode readonly children?: React.ReactNode
}> }>
@@ -32,16 +34,25 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
return function ReffuseContextReactProvider(props) { return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime() const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime]) const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
const makeScope = React.useMemo(() => props.scope const makeScope = React.useMemo(() => props.scope
? Scope.fork(props.scope, ExecutionStrategy.sequential) ? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(), : Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
[props.scope]) [props.scope])
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe( const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
Effect.provide(props.layer), Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope), Effect.provideService(Scope.Scope, scope),
), [props.layer]) )
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe( const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)), Effect.bind("isInitialRun", () => Ref.make(true)),
@@ -57,7 +68,7 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
Effect.if({ Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe( onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() => Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void)) () => closeScope(initialScope)
) )
), ),
@@ -68,13 +79,13 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
Effect.sync(() => setValue(context)) Effect.sync(() => setValue(context))
), ),
Effect.map(({ scope }) => Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void)) () => closeScope(scope)
), ),
), ),
}), }),
runSync, runSync,
), [makeScope, makeContext, runSync]) ), [makeScope, runSync, runFork])
return React.createElement(Context, { ...props, value }) return React.createElement(Context, { ...props, value })
} }
@@ -84,6 +95,7 @@ export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope> readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly fallback?: React.ReactNode readonly fallback?: React.ReactNode
readonly children?: React.ReactNode readonly children?: React.ReactNode
}> }>
@@ -112,7 +124,7 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
const scope = runSync(props.scope const scope = runSync(props.scope
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential) ? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy) : Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
) )
Effect.context<R>().pipe( Effect.context<R>().pipe(
@@ -126,7 +138,13 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
effect => runFork(effect, { ...props, scope }), effect => runFork(effect, { ...props, scope }),
) )
return () => { runFork(Scope.close(scope, Exit.void)) } return () => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
}, [props.layer, runSync, runFork]) }, [props.layer, runSync, runFork])
return React.createElement(React.Suspense, { return React.createElement(React.Suspense, {

View File

@@ -1,8 +1,8 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import * as ReffuseContext from "./ReffuseContext.js" import * as ReffuseContext from "./ReffuseContext.js"
import * as ReffuseRuntime from "./ReffuseRuntime.js" import * as ReffuseRuntime from "./ReffuseRuntime.js"
import * as SetStateAction from "./SetStateAction.js" import { type PropertyPath, SetStateAction, SubscriptionSubRef } from "./types/index.js"
export interface RenderOptions { export interface RenderOptions {
@@ -14,11 +14,22 @@ export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
} }
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
readonly scope?: Scope.Scope
readonly finalizerExecutionMode?: "sync" | "fork"
}
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
[K in keyof T]: Effect.Effect.Success<T[K]>
}
export abstract class ReffuseNamespace<R> { export abstract class ReffuseNamespace<R> {
declare ["constructor"]: ReffuseNamespaceClass<R> declare ["constructor"]: ReffuseNamespaceClass<R>
constructor() { constructor() {
this.SubRefFromGetSet = this.SubRefFromGetSet.bind(this as any) as any
this.SubRefFromPath = this.SubRefFromPath.bind(this as any) as any
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
this.RefState = this.RefState.bind(this as any) as any this.RefState = this.RefState.bind(this as any) as any
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
@@ -83,6 +94,56 @@ export abstract class ReffuseNamespace<R> {
), [runtime, context]) ), [runtime, context])
} }
useScope<R>(
this: ReffuseNamespace<R>,
deps: React.DependencyList = [],
options?: UseScopeOptions,
): readonly [scope: Scope.Scope, layer: Layer.Layer<Scope.Scope>] {
const runSync = this.useRunSync()
const runFork = this.useRunFork()
const makeScope = React.useMemo(() => options?.scope
? Scope.fork(options.scope, options.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
[options?.scope])
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
const [isInitialRun, initialScope] = React.useMemo(() => runSync(
Effect.all([Ref.make(true), makeScope])
), [makeScope])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => closeScope(initialScope),
),
onFalse: () => makeScope.pipe(
Effect.tap(v => Effect.sync(() => setScope(v))),
Effect.map(v => () => closeScope(v)),
),
}),
runSync,
), [
makeScope,
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
...deps,
])
return React.useMemo(() => [scope, Layer.succeed(Scope.Scope, scope)] as const, [scope])
}
/** /**
* Reffuse equivalent to `React.useMemo`. * Reffuse equivalent to `React.useMemo`.
* *
@@ -106,53 +167,6 @@ export abstract class ReffuseNamespace<R> {
]) ])
} }
useMemoScoped<A, E, R>(
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): A {
const runSync = this.useRunSync()
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const),
runSync,
), [])
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
)
),
onFalse: () => Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.tap(({ value }) =>
Effect.sync(() => setValue(value))
),
Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
),
),
}),
runSync,
), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
return value
}
/** /**
* Reffuse equivalent to `React.useEffect`. * Reffuse equivalent to `React.useEffect`.
* *
@@ -373,14 +387,46 @@ export abstract class ReffuseNamespace<R> {
]) ])
} }
useRef<A, R>( useRef<A, E, R>(
this: ReffuseNamespace<R>,
initialValue: () => Effect.Effect<A, E, R>,
): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
)
}
useRefFromReactiveValue<A, R>(
this: ReffuseNamespace<R>, this: ReffuseNamespace<R>,
value: A, value: A,
): SubscriptionRef.SubscriptionRef<A> { ): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo( const ref = this.useRef(() => Effect.succeed(value))
() => SubscriptionRef.make(value), this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
[], return ref
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes }
useSubRefFromGetSet<A, B, R>(
this: ReffuseNamespace<R>,
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef.SubscriptionSubRef<A, B> {
return React.useMemo(
() => SubscriptionSubRef.makeFromGetSet(parent, getter, setter),
[parent],
)
}
useSubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> {
return React.useMemo(
() => SubscriptionSubRef.makeFromPath(parent, path),
[parent, ...path],
) )
} }
@@ -390,18 +436,18 @@ export abstract class ReffuseNamespace<R> {
>( >(
this: ReffuseNamespace<R>, this: ReffuseNamespace<R>,
...refs: Refs ...refs: Refs
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] { ): RefsA<Refs> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo( const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]), () => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
[], [],
{ doNotReExecuteOnRuntimeOrContextChange: true }, { doNotReExecuteOnRuntimeOrContextChange: true },
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]) ) as RefsA<Refs>)
this.useFork(() => pipe( this.useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)), refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams), streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v => Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])) Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
), ),
), refs) ), refs)
@@ -439,35 +485,87 @@ export abstract class ReffuseNamespace<R> {
return [reactStateValue, setValue] return [reactStateValue, setValue]
} }
useStreamFromValues<const A extends React.DependencyList, R>( useStreamFromReactiveValues<const A extends React.DependencyList, R>(
this: ReffuseNamespace<R>, this: ReffuseNamespace<R>,
values: A, values: A,
): Stream.Stream<A> { ): Stream.Stream<A> {
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe( const [, scopeLayer] = this.useScope([], { finalizerExecutionMode: "fork" })
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
), [])
this.useEffect(() => Queue.offer(queue, values), values) const { latest, pubsub, stream } = this.useMemo(() => Effect.Do.pipe(
Effect.bind("latest", () => Ref.make(values)),
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
Effect.let("stream", ({ latest, pubsub }) => Ref.get(latest).pipe(
Effect.flatMap(a => Effect.map(
Stream.fromPubSub(pubsub, { scoped: true }),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrapScoped,
)),
Effect.provide(scopeLayer),
), [scopeLayer], { doNotReExecuteOnRuntimeOrContextChange: true })
this.useEffect(() => Ref.set(latest, values).pipe(
Effect.andThen(PubSub.publish(pubsub, values)),
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
), values, { doNotReExecuteOnRuntimeOrContextChange: true })
return stream return stream
} }
useSubscribeStream<A, InitialA extends A | undefined, E, R>( useSubscribeStream<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>, stream: Stream.Stream<A, E, R>,
initialValue?: InitialA, ): Option.Option<A>
): InitialA extends A ? Option.Some<A> : Option.Option<A> { useSubscribeStream<A, E, IE, R>(
const [reactStateValue, setReactStateValue] = React.useState<Option.Option<A>>(Option.fromNullable(initialValue)) this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue: () => Effect.Effect<A, IE, R>,
): Option.Some<A>
useSubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue?: () => Effect.Effect<A, IE, R>,
): Option.Option<A> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => initialValue
? Effect.map(initialValue(), Option.some)
: Effect.succeed(Option.none()),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach( this.useFork(() => Stream.runForEach(
Stream.changesWith(stream, (x, y) => x === y), Stream.changesWith(stream, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(Option.some(v))), v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream]) ), [stream])
return reactStateValue as InitialA extends A ? Option.Some<A> : Option.Option<A> return reactStateValue
} }
SubRefFromGetSet<A, B, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<A, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRefFromGetSet(props.parent, props.getter, props.setter))
}
SubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly path: P,
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRefFromPath(props.parent, props.path))
}
SubscribeRefs< SubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[], const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R, R,
@@ -475,7 +573,7 @@ export abstract class ReffuseNamespace<R> {
this: ReffuseNamespace<R>, this: ReffuseNamespace<R>,
props: { props: {
readonly refs: Refs readonly refs: Refs
readonly children: (...args: [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]) => React.ReactNode readonly children: (...args: RefsA<Refs>) => React.ReactNode
}, },
): React.ReactNode { ): React.ReactNode {
return props.children(...this.useSubscribeRefs(...props.refs)) return props.children(...this.useSubscribeRefs(...props.refs))
@@ -491,15 +589,30 @@ export abstract class ReffuseNamespace<R> {
return props.children(this.useRefState(props.ref)) return props.children(this.useRefState(props.ref))
} }
SubscribeStream<A, InitialA extends A | undefined, E, R>( SubscribeStream<A, E, R>(
this: ReffuseNamespace<R>, this: ReffuseNamespace<R>,
props: { props: {
readonly stream: Stream.Stream<A, E, R> readonly stream: Stream.Stream<A, E, R>
readonly initialValue?: InitialA readonly children: (latestValue: Option.Option<A>) => React.ReactNode
readonly children: (latestValue: InitialA extends A ? Option.Some<A> : Option.Option<A>) => React.ReactNode },
): React.ReactNode
SubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue: () => Effect.Effect<A, IE, R>
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
},
): React.ReactNode
SubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue?: () => Effect.Effect<A, IE, R>
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
}, },
): React.ReactNode { ): React.ReactNode {
return props.children(this.useSubscribeStream(props.stream, props.initialValue)) return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
} }
} }

View File

@@ -3,4 +3,3 @@ export * as ReffuseContext from "./ReffuseContext.js"
export * as ReffuseExtension from "./ReffuseExtension.js" export * as ReffuseExtension from "./ReffuseExtension.js"
export * as ReffuseNamespace from "./ReffuseNamespace.js" export * as ReffuseNamespace from "./ReffuseNamespace.js"
export * as ReffuseRuntime from "./ReffuseRuntime.js" export * as ReffuseRuntime from "./ReffuseRuntime.js"
export * as SetStateAction from "./SetStateAction.js"

View File

@@ -0,0 +1,99 @@
import { Array, Function, Option, Predicate } from "effect"
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
export type Paths<T, D extends number = 5, Seen = never> = [] | (
D extends never ? [] :
T extends Seen ? [] :
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
T extends object ? ObjectPaths<T, D, Seen | T> :
never
)
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
[K in keyof T as K extends number ? K : never]:
| [K]
| [K, ...Paths<T[K], Prev[D], Seen>]
} extends infer O
? O[keyof O]
: never
export type ObjectPaths<T extends object, D extends number, Seen> = {
[K in keyof T as K extends string | number | symbol ? K : never]-?:
NonNullable<T[K]> extends infer V
? [K] | [K, ...Paths<V, Prev[D], Seen>]
: never
} extends infer O
? O[keyof O]
: never
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
? Head extends keyof T
? ValueFromPath<T[Head], Tail>
: T extends readonly any[]
? Head extends number
? ValueFromPath<T[number], Tail>
: never
: never
: T
export type AnyKey = string | number | symbol
export type AnyPath = readonly AnyKey[]
export const unsafeGet: {
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
path.reduce((acc: any, key: any) => acc?.[key], self)
)
export const get: {
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
path.reduce(
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
? Predicate.hasProperty(acc.value, key)
? Option.some(acc.value[key])
: Option.none()
: acc,
Option.some(self),
)
)
export const immutableSet: {
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
const key = Array.head(path as AnyPath)
if (Option.isNone(key))
return Option.some(value as T)
if (!Predicate.hasProperty(self, key.value))
return Option.none()
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
if (Option.isNone(child))
return child
if (Array.isArray(self))
return typeof key.value === "number"
? Option.some([
...self.slice(0, key.value),
child.value,
...self.slice(key.value + 1),
] as T)
: Option.none()
if (typeof self === "object")
return Option.some(
Object.assign(
Object.create(Object.getPrototypeOf(self)),
{ ...self, [key.value]: child.value },
)
)
return Option.none()
})

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

@@ -0,0 +1,100 @@
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
import * as PropertyPath from "./PropertyPath.js"
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
readonly parent: SubscriptionRef.SubscriptionRef<B>
readonly [Unify.typeSymbol]?: unknown
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
}
export declare namespace SubscriptionSubRef {
export interface Variance<in out A, in out B> {
readonly [SubscriptionSubRefTypeId]: {
readonly _A: Types.Invariant<A>
readonly _B: Types.Invariant<B>
}
}
}
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
}
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
SubscriptionRef?: true
}
const refVariance = { _A: (_: any) => _ }
const synchronizedRefVariance = { _A: (_: any) => _ }
const subscriptionRefVariance = { _A: (_: any) => _ }
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
readonly [Ref.RefTypeId] = refVariance
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
readonly get: Effect.Effect<A>
constructor(
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
) {
super()
this.get = Effect.map(Ref.get(this.parent), this.getter)
}
commit() {
return this.get
}
get changes(): Stream.Stream<A> {
return this.get.pipe(
Effect.map(a => this.parent.changes.pipe(
Stream.map(this.getter),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrap,
)
}
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
return this.modifyEffect(a => Effect.succeed(f(a)))
}
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
return Effect.Do.pipe(
Effect.bind("b", () => Ref.get(this.parent)),
Effect.bind("ca", ({ b }) => f(this.getter(b))),
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
Effect.map(({ ca: [c] }) => c),
)
}
}
export const makeFromGetSet = <A, B>(
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
parent,
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
)

View File

@@ -0,0 +1,3 @@
export * as PropertyPath from "./PropertyPath.js"
export * as SetStateAction from "./SetStateAction.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"

View File

@@ -26,7 +26,13 @@
// Build // Build
"outDir": "./dist", "outDir": "./dist",
"declaration": true "declaration": true,
"plugins": [
{
"name": "@effect/language-service"
}
]
}, },
"include": ["./src"] "include": ["./src"]