148 Commits
form ... next

Author SHA1 Message Date
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
59 changed files with 1293 additions and 1158 deletions

554
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

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

@@ -19,6 +19,7 @@ import { Route as LazyrefImport } from './routes/lazyref'
import { Route as CountImport } from './routes/count' import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank' import { Route as 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'
@@ -73,6 +74,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',
@@ -172,6 +179,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
}
} }
} }
@@ -189,6 +203,7 @@ 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 {
@@ -203,6 +218,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 {
@@ -218,6 +234,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 {
@@ -234,6 +251,7 @@ export interface FileRouteTypes {
| '/query/service' | '/query/service'
| '/query/usemutation' | '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
@@ -247,6 +265,7 @@ export interface FileRouteTypes {
| '/query/service' | '/query/service'
| '/query/usemutation' | '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@@ -260,6 +279,7 @@ export interface FileRouteTypes {
| '/query/service' | '/query/service'
| '/query/usemutation' | '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -275,6 +295,7 @@ 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 = {
@@ -289,6 +310,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
@@ -311,7 +333,8 @@ export const routeTree = rootRoute
"/todos", "/todos",
"/query/service", "/query/service",
"/query/usemutation", "/query/usemutation",
"/query/usequery" "/query/usequery",
"/streams/pull"
] ]
}, },
"/": { "/": {
@@ -346,6 +369,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

@@ -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,9 +0,0 @@
# LazyRef extension for Reffuse
Extension to integrate `@typed/lazy-ref` with Reffuse.
## Peer dependencies
- `@typed/lazy-ref`
- `reffuse` 0.1.3+
- `effect` 3.13+
- `react` & `@types/react` 19+

View File

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

View File

@@ -1,9 +0,0 @@
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
export const FormExtension = ReffuseExtension.make(() => ({
useForm<A, E, R>(
this: ReffuseNamespace.ReffuseNamespace<R>,
) {
},
}))

View File

@@ -1 +0,0 @@
export * from "./FormExtension.js"

View File

@@ -1,6 +0,0 @@
import { Schema } from "effect"
export interface Form<A, I, R> {
readonly schema: Schema.Schema<A, I, R>
}

View File

@@ -1,69 +0,0 @@
import type { Effect, Schema } from "effect"
import type * as Formify from "./Formify.js"
export interface FormField<S extends Schema.Schema.Any> {
readonly schema: S
}
export const makeFormField = <S extends Schema.Schema.Any>(
schema: S,
get: Effect.Effect<S["Type"]>,
set: (value: S["Type"]) => Effect.Effect<void>,
): FormField<S> => {
}
export interface UnionFormField<
S extends Schema.Union<Members>,
Members extends ReadonlyArray<Schema.Schema.All>,
> extends FormField<S> {
readonly member: Formify.Formify<Members[number]>
}
export interface TupleFormField<
S extends Schema.TupleType<Elements, Rest>,
Elements extends Schema.TupleType.Elements,
Rest extends Schema.TupleType.Rest,
> extends FormField<S> {
readonly elements: [...{ readonly [K in keyof Elements]: Formify.Formify<Elements[K]> }]
}
export interface ArrayFormField<
S extends Schema.Array$<Value>,
Value extends Schema.Schema.Any,
> extends FormField<S> {
readonly elements: readonly Formify.Formify<Value>[]
}
export type StructFormField<
S extends Schema.Struct<Fields>,
Fields extends Schema.Struct.Fields,
> = (
& FormField<S>
& { readonly fields: { readonly [K in keyof Fields]: Formify.Formify<Fields[K]> } }
& {
[K in keyof Fields as Fields[K] extends
Schema.tag<infer _> ? K : never
]: Fields[K] extends
Schema.tag<infer Tag> ? Tag : never
}
)
export interface GenericFormField<S extends Schema.Schema.Any> extends FormField<S> {
}
export interface PropertySignatureFormField<
S extends Schema.PropertySignature<TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R>,
TypeToken extends Schema.PropertySignature.Token,
Type,
Key extends PropertyKey,
EncodedToken extends Schema.PropertySignature.Token,
Encoded,
HasDefault extends boolean = false,
R = never,
> {
readonly propertySignature: S
readonly value: Type
}

View File

@@ -1,51 +0,0 @@
import { Schema } from "effect"
import type * as FormField from "./FormField.js"
export type Formify<S> = (
S extends Schema.Union<infer Members> ? FormField.UnionFormField<S, Members> :
S extends Schema.TupleType<infer Elements, infer Rest> ? FormField.TupleFormField<S, Elements, Rest> :
S extends Schema.Array$<infer Value> ? FormField.ArrayFormField<S, Value> :
S extends Schema.Struct<infer Fields> ? FormField.StructFormField<S, Fields> :
S extends Schema.Schema.Any ? FormField.GenericFormField<S> :
S extends Schema.PropertySignature<
infer TypeToken,
infer Type,
infer Key,
infer EncodedToken,
infer Encoded,
infer HasDefault,
infer R
> ? FormField.PropertySignatureFormField<S, TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R> :
never
)
const Login = Schema.Union(
Schema.Struct({
_tag: Schema.tag("ByEmail"),
email: Schema.String,
password: Schema.RedactedFromSelf(Schema.String),
}),
Schema.Struct({
_tag: Schema.tag("ByPhone"),
phone: Schema.String,
password: Schema.RedactedFromSelf(Schema.String),
}),
Schema.TaggedStruct("ByKey", {
id: Schema.String,
password: Schema.RedactedFromSelf(Schema.String),
}),
)
type LoginForm = Formify<typeof Login>
declare const loginForm: LoginForm
switch (loginForm.member._tag) {
case "ByEmail":
loginForm.member
break
case "ByPhone":
break
}

View File

@@ -1,37 +0,0 @@
import { Array, Predicate, Record, Schema, Tuple } from "effect"
export const isTupleSchema = (u: unknown): u is Schema.Tuple<any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) &&
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest)
)
export const isArraySchema = (u: unknown): u is Schema.Array$<any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) && Array.isEmptyArray(u.elements) &&
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest) && Tuple.isTupleOf(u.rest, 1) &&
Predicate.hasProperty(u, "value")
)
export const isStructSchema = (u: unknown): u is Schema.Struct<any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) &&
Predicate.hasProperty(u, "records") && Array.isArray(u.records) && Array.isEmptyArray(u.records)
)
export const isRecordSchema = (u: unknown): u is Schema.Record$<any, any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) && Record.isEmptyRecord(u.fields) &&
Predicate.hasProperty(u, "records") && Array.isArray(u.records) &&
Predicate.hasProperty(u, "key") &&
Predicate.hasProperty(u, "value")
)
const myTuple = Schema.Tuple(Schema.String)
const myArray = Schema.Array(Schema.String)
const myStruct = Schema.Struct({})
const myRecord = Schema.Record({ key: Schema.String, value: Schema.String })
console.log(isArraySchema(myTuple))

View File

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

View File

@@ -1,33 +0,0 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "NodeNext",
"moduleDetection": "force",
"jsx": "react-jsx",
// "allowJs": true,
// Bundler mode
"moduleResolution": "NodeNext",
// "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
// "noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
// Build
"outDir": "./dist",
"declaration": true
},
"include": ["./src"]
}

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,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"