283 Commits

Author SHA1 Message Date
Julien Valverdé 6fa73ee33f Tests
Lint / lint (push) Successful in 15s
2025-06-29 19:11:16 +02:00
Julien Valverdé 3ea4c81872 React component refactoring 2025-06-29 18:52:42 +02:00
Julien Valverdé 782629d5b3 Refactoring
Lint / lint (push) Successful in 14s
2025-06-29 18:20:46 +02:00
Julien Valverdé 8b2abbbd19 Fix
Lint / lint (push) Successful in 14s
2025-06-29 17:07:05 +02:00
Julien Valverdé 152657d97b Effect LSP
Lint / lint (push) Failing after 13s
2025-06-29 15:27:49 +02:00
Julien Valverdé faf1d4963c Work
Lint / lint (push) Failing after 28s
2025-06-27 05:43:58 +02:00
Julien Valverdé 9ba36ebc04 Test
Lint / lint (push) Successful in 14s
2025-06-26 04:29:11 +02:00
Julien Valverdé f327728b3a Fix
Lint / lint (push) Successful in 13s
2025-06-26 04:26:58 +02:00
Julien Valverdé 8920674b26 Component scope
Lint / lint (push) Successful in 15s
2025-06-26 04:22:27 +02:00
Julien Valverdé b440503e50 Hook work
Lint / lint (push) Successful in 15s
2025-06-26 02:55:20 +02:00
Julien Valverdé 4088d86652 Work
Lint / lint (push) Successful in 15s
2025-06-26 01:26:25 +02:00
Julien Valverdé 79cf1e5eb7 API change
Lint / lint (push) Successful in 31s
2025-06-25 21:51:46 +02:00
Julien Valverdé 8007c2693a Tests
Lint / lint (push) Successful in 14s
2025-06-25 13:38:44 +02:00
Julien Valverdé 1769c4074d ReactComponent
Lint / lint (push) Failing after 7s
2025-06-25 13:17:27 +02:00
Julien Valverdé 8c5613aa62 Tests
Lint / lint (push) Successful in 14s
2025-06-25 04:07:22 +02:00
Julien Valverdé 7d220cb61a Tests
Lint / lint (push) Successful in 14s
2025-06-25 02:40:54 +02:00
Julien Valverdé d81a9fcd91 Fix
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
Lint / lint (push) Successful in 13s
2025-06-18 00:14:05 +02:00
Julien Valverdé e089bf9fee 0.1.13 (#18)
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
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
Lint / lint (push) Successful in 13s
2025-06-18 00:08:57 +02:00
Julien Valverdé 1fe2fec325 Fix
Lint / lint (push) Successful in 14s
2025-06-18 00:06:51 +02:00
Julien Valverdé d8b40088cb Fix
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
Lint / lint (push) Successful in 13s
2025-06-17 23:07:50 +02:00
Julien Valverdé 30b72b5b52 0.1.12 (#17)
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
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
Lint / lint (push) Successful in 14s
2025-06-17 23:01:26 +02:00
Julien Valverdé 0b7a2dbe92 Fix
Lint / lint (push) Successful in 14s
2025-06-17 22:24:02 +02:00
Julien Valverdé 0d3e09354e Fix
Lint / lint (push) Successful in 13s
2025-06-17 21:14:25 +02:00
Julien Valverdé dc46d03aab PropertyPath recursive fix
Lint / lint (push) Successful in 14s
2025-06-17 21:12:32 +02:00
Julien Valverdé 37ffc161d3 SubRef tests
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
Lint / lint (push) Successful in 13s
2025-06-01 05:30:26 +02:00
Julien Valverdé 6dc0a548cd @reffuse/extension-query 0.1.5 (#16)
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
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
Lint / lint (push) Successful in 13s
2025-06-01 05:03:41 +02:00
Julien Valverdé ed788af128 Fix
Lint / lint (push) Successful in 16s
2025-06-01 03:49:04 +02:00
Julien Valverdé f4e380ddcb QueryClient refactoring work
Lint / lint (push) Failing after 15s
2025-05-31 05:57:39 +02:00
Julien Valverdé e58bd7ab5a Fix
Lint / lint (push) Successful in 14s
2025-05-30 23:21:19 +02:00
Julien Valverdé 21d011dd12 QueryErrorHandler refactoring
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
Lint / lint (push) Successful in 13s
2025-05-29 22:12:37 +02:00
Julien Valverdé 2a29f19ece @reffuse/extension-query 0.1.4 (#15)
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
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
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
Lint / lint (push) Successful in 12s
2025-05-26 04:00:28 +02:00
Julien Valverdé 8444061de3 Fix
Lint / lint (push) Successful in 13s
2025-05-26 03:58:45 +02:00
Julien Valverdé 2f870e56cd Mutation refactoring
Lint / lint (push) Successful in 13s
2025-05-26 03:56:56 +02:00
Julien Valverdé f95c2596a3 Fix
Lint / lint (push) Successful in 13s
2025-05-26 00:56:29 +02:00
Julien Valverdé 5d85449fef QueryRunner refactoring
Lint / lint (push) Successful in 13s
2025-05-26 00:48:00 +02:00
Julien Valverdé 3548ed5718 Fix
Lint / lint (push) Successful in 14s
2025-05-25 19:17:53 +02:00
Julien Valverdé 82f0f67ee6 Fix
Lint / lint (push) Successful in 13s
2025-05-25 04:16:17 +02:00
Julien Valverdé 8d52443b55 Fix
Lint / lint (push) Successful in 13s
2025-05-25 03:54:14 +02:00
Julien Valverdé 27f50db664 Fix
Lint / lint (push) Successful in 14s
2025-05-25 03:52:15 +02:00
Julien Valverdé 165b7bbeee Query refactoring
Lint / lint (push) Successful in 14s
2025-05-24 21:31:56 +02:00
Julien Valverdé 58b2da373e Query refactoring
Lint / lint (push) Successful in 16s
2025-05-24 21:04:01 +02:00
Julien Valverdé f25fefb0f3 Cleanup
Lint / lint (push) Successful in 12s
2025-05-23 17:33:19 +02:00
Julien Valverdé 6edac19fa6 Service work
Lint / lint (push) Failing after 14s
2025-05-23 05:12:21 +02:00
Julien Valverdé 1c99d5c161 QueryService
Lint / lint (push) Failing after 13s
2025-05-23 03:34:02 +02:00
Julien Valverdé 5044887d90 Fix
Lint / lint (push) Successful in 13s
2025-05-22 20:22:13 +02:00
Julien Valverdé 810e4bb9fd Query refactoring
Lint / lint (push) Failing after 12s
2025-05-22 19:49:40 +02:00
Julien Valverdé 619dbe32ae Fix
Lint / lint (push) Failing after 12s
2025-05-22 16:18:03 +02:00
Julien Valverdé 28efea18f1 Fix
Lint / lint (push) Failing after 15s
2025-05-22 16:13:35 +02:00
Julien Valverdé 7e9a8a5fee QueryRunner work
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
Lint / lint (push) Successful in 13s
2025-05-19 14:03:52 +02:00
Julien Valverdé 2c467dc6ec 0.1.11 (#14)
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
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
Lint / lint (push) Successful in 13s
2025-05-19 13:03:14 +02:00
Julien Valverdé c7c2f8de62 Example fix
Lint / lint (push) Successful in 13s
2025-05-19 12:58:00 +02:00
Julien Valverdé dda868d444 Scope refactoring
Lint / lint (push) Successful in 14s
2025-05-19 12:49:37 +02:00
Julien Valverdé b9e787f42b Todos refactoring
Lint / lint (push) Successful in 13s
2025-05-18 15:45:31 +02:00
Julien Valverdé 1af2a14b52 Fix
Lint / lint (push) Successful in 13s
2025-05-18 13:57:59 +02:00
Julien Valverdé 861e462ebd Todos example work
Lint / lint (push) Successful in 13s
2025-05-18 13:52:39 +02:00
Julien Valverdé b6a127c8a7 SubRefFromGetSet
Lint / lint (push) Successful in 14s
2025-05-18 12:35:44 +02:00
Julien Valverdé 497e9a34f2 SubRefFromPath
Lint / lint (push) Successful in 14s
2025-05-18 12:16:25 +02:00
Julien Valverdé 9d0daaa87f Todos example refactoring
Lint / lint (push) Successful in 14s
2025-05-18 10:52:39 +02:00
Julien Valverdé 557c4a1b97 Update
Lint / lint (push) Successful in 13s
2025-05-14 06:29:37 +02:00
Julien Valverdé b395644798 Dependencies upgrade
Lint / lint (push) Failing after 14s
2025-05-14 06:19:39 +02:00
Julien Valverdé 099a28ca0d Fix
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
Lint / lint (push) Successful in 13s
2025-05-11 19:22:39 +02:00
Julien Valverdé 64943deaab 0.1.10 (#13)
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
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
Lint / lint (push) Successful in 18s
2025-05-11 19:13:30 +02:00
Julien Valverdé 6f65574ebd Cleanup
Lint / lint (push) Successful in 14s
2025-05-11 08:11:03 +02:00
Julien Valverdé 9ccabbb627 Tests
Lint / lint (push) Successful in 13s
2025-05-11 07:10:38 +02:00
Julien Valverdé 5f4087aa40 Cleanup
Lint / lint (push) Successful in 13s
2025-05-11 07:04:14 +02:00
Julien Valverdé 85b41bda9f Tests
Lint / lint (push) Successful in 13s
2025-05-11 06:48:58 +02:00
Julien Valverdé e5a7fe8ad6 useSubscribeStream
Lint / lint (push) Successful in 14s
2025-05-11 03:17:35 +02:00
Julien Valverdé 59b7115d19 Fix
Lint / lint (push) Successful in 13s
2025-05-10 05:33:14 +02:00
Julien Valverdé 08af31f0b9 Fix
Lint / lint (push) Successful in 13s
2025-05-10 05:30:13 +02:00
Julien Valverdé 44fc6bbbc4 Stream tests
Lint / lint (push) Successful in 15s
2025-05-10 05:26:15 +02:00
Julien Valverdé 904b725753 Dependencies upgrade
Lint / lint (push) Successful in 14s
2025-05-08 21:09:55 +02:00
Julien Valverdé 73dd7bc160 Cleanup
Lint / lint (push) Successful in 13s
2025-05-08 19:22:50 +02:00
Julien Valverdé 6bc07d5b2a Tests
Lint / lint (push) Successful in 12s
2025-05-08 05:59:36 +02:00
Julien Valverdé 70e9b9218d Queue -> PubSub
Lint / lint (push) Successful in 13s
2025-05-08 04:51:41 +02:00
Julien Valverdé 31b07f842b Fix
Lint / lint (push) Successful in 13s
2025-05-08 04:42:24 +02:00
Julien Valverdé 10f23d4cb4 Fix
Lint / lint (push) Successful in 13s
2025-05-08 01:28:41 +02:00
Julien Valverdé 39765102db useStreamFromReactiveValues
Lint / lint (push) Successful in 14s
2025-05-08 01:15:35 +02:00
Julien Valverdé 04e78e1ea3 Tests
Lint / lint (push) Successful in 13s
2025-05-07 04:53:40 +02:00
Julien Valverdé 606dd2c00f useStreamFromReactiveValues
Lint / lint (push) Successful in 12s
2025-05-07 02:15:46 +02:00
Julien Valverdé c13a8d549f useStreamFromReactiveValues
Lint / lint (push) Successful in 15s
2025-05-07 02:07:50 +02:00
Julien Valverdé 4b9bfd0637 Stream PubSub
Lint / lint (push) Successful in 14s
2025-05-05 21:52:55 +02:00
Julien Valverdé 53fc1ef505 Fix
Lint / lint (push) Successful in 13s
2025-05-05 20:38:39 +02:00
Julien Valverdé c8b675d93e Fix
Lint / lint (push) Successful in 14s
2025-05-05 03:33:50 +02:00
Julien Valverdé 882ec9591c Fix
Lint / lint (push) Successful in 14s
2025-05-05 03:09:17 +02:00
Julien Valverdé 5b3637afd8 useSubscribeStream work
Lint / lint (push) Successful in 13s
2025-05-05 02:39:12 +02:00
Julien Valverdé d6256a7cfd Stream hooks work
Lint / lint (push) Successful in 14s
2025-05-04 01:18:29 +02:00
Julien Valverdé cf6c84ff8e useScope fix
Lint / lint (push) Successful in 14s
2025-05-02 21:56:46 +02:00
Julien Valverdé 198a7cee03 Fix
Lint / lint (push) Successful in 13s
2025-05-01 21:59:51 +02:00
Julien Valverdé 032f283ac8 Pull example
Lint / lint (push) Successful in 13s
2025-05-01 21:56:58 +02:00
Julien Valverdé c34629e20d usePullStream
Lint / lint (push) Successful in 13s
2025-05-01 21:08:52 +02:00
Julien Valverdé 284a080f19 usePullStream
Lint / lint (push) Successful in 13s
2025-05-01 20:27:11 +02:00
Julien Valverdé 87d27dd48d useScope work
Lint / lint (push) Successful in 13s
2025-05-01 19:18:54 +02:00
Julien Valverdé 24853561f1 Working useScope
Lint / lint (push) Successful in 15s
2025-05-01 17:10:45 +02:00
Julien Valverdé 1902ad373f useScope tests
Lint / lint (push) Successful in 18s
2025-05-01 16:48:37 +02:00
Julien Valverdé aa6c4a8008 Fix
Lint / lint (push) Successful in 13s
2025-05-01 16:19:42 +02:00
Julien Valverdé d5ac84b2cc Fix
Lint / lint (push) Successful in 14s
2025-05-01 16:17:01 +02:00
Julien Valverdé 3c604abcef useScope
Lint / lint (push) Failing after 11s
2025-05-01 03:01:18 +02:00
Julien Valverdé ba99309877 useSubscribePullStream
Lint / lint (push) Failing after 12s
2025-04-30 23:41:03 +02:00
Julien Valverdé db3cd05851 Fix
Lint / lint (push) Successful in 14s
2025-04-30 23:00:37 +02:00
Julien Valverdé dce81be269 Fix
Lint / lint (push) Successful in 17s
2025-04-30 22:52:38 +02:00
Julien Valverdé 3980c10747 Fix
Lint / lint (push) Successful in 16s
2025-04-30 21:41:58 +02:00
Julien Valverdé 43a3793dbf Fix
Lint / lint (push) Successful in 14s
2025-04-30 16:27:08 +02:00
Julien Valverdé da7044ee9f useRefFromValue
Lint / lint (push) Successful in 13s
2025-04-30 13:33:21 +02:00
Julien Valverdé ff5503cfd1 Fix
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
Lint / lint (push) Successful in 13s
2025-04-27 19:13:26 +02:00
Julien Valverdé bc8c96635c 0.1.9 (#12)
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
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
Lint / lint (push) Successful in 13s
2025-04-27 18:52:08 +02:00
Julien Valverdé dd524e1aa5 Refactoring
Lint / lint (push) Successful in 14s
2025-04-27 18:46:33 +02:00
Julien Valverdé 1c7cef703b SubRef
Lint / lint (push) Successful in 17s
2025-04-25 13:50:54 +02:00
Julien Valverdé fa0f8c6b24 Refactoring
Lint / lint (push) Successful in 14s
2025-04-25 13:38:42 +02:00
Julien Valverdé 357e5aa56b useSubRef
Lint / lint (push) Successful in 13s
2025-04-25 10:16:04 +02:00
Julien Valverdé ea374d7e0f Fix
Lint / lint (push) Successful in 13s
2025-04-25 08:32:42 +02:00
Julien Valverdé 148c98acbd Fix
Lint / lint (push) Successful in 13s
2025-04-25 08:30:17 +02:00
Julien Valverdé 39d2176c61 Working subref from path
Lint / lint (push) Successful in 13s
2025-04-25 08:21:59 +02:00
Julien Valverdé 107ff1e794 SubscriptionSubRef.makeFromPath
Lint / lint (push) Successful in 14s
2025-04-25 08:12:34 +02:00
Julien Valverdé a70ef27f75 PropertyPath done
Lint / lint (push) Successful in 14s
2025-04-25 07:56:56 +02:00
Julien Valverdé 04b2fad038 PropertyPath
Lint / lint (push) Successful in 29s
2025-04-25 07:40:21 +02:00
Julien Valverdé 691b28427d SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-24 00:52:18 +02:00
Julien Valverdé 1de976aaa8 Fix
Lint / lint (push) Successful in 14s
2025-04-24 00:20:30 +02:00
Julien Valverdé df851cf9ee SearchPaths work
Lint / lint (push) Successful in 13s
2025-04-23 07:06:32 +02:00
Julien Valverdé 459f548c10 Fix
Lint / lint (push) Successful in 14s
2025-04-23 06:50:17 +02:00
Julien Valverdé 6156baec4d SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-23 06:47:11 +02:00
Julien Valverdé 1163b83929 SearchPaths work
Lint / lint (push) Failing after 13s
2025-04-23 05:53:19 +02:00
Julien Valverdé 8917f84952 SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-22 22:59:50 +02:00
Julien Valverdé 58752253b3 SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-22 22:36:17 +02:00
Julien Valverdé ba362baf04 SearchPaths work
Lint / lint (push) Successful in 15s
2025-04-22 21:55:59 +02:00
Julien Valverdé 33cf4fbcbd Tests
Lint / lint (push) Successful in 14s
2025-04-21 05:15:55 +02:00
Julien Valverdé e8f92c88b8 Fix
Lint / lint (push) Successful in 13s
2025-04-21 03:17:41 +02:00
Julien Valverdé 6ae155de34 Version bump
Lint / lint (push) Successful in 13s
2025-04-21 02:53:31 +02:00
Julien Valverdé db783f174e Fix
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
Lint / lint (push) Successful in 12s
2025-04-21 02:12:13 +02:00
Julien Valverdé 0fd3fe49a9 0.1.8 (#11)
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
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
Lint / lint (push) Successful in 14s
2025-04-21 01:21:59 +02:00
Julien Valverdé 926482b154 Fix
Lint / lint (push) Successful in 14s
2025-04-20 19:38:18 +02:00
Julien Valverdé 110b0813f8 Refactoring
Lint / lint (push) Successful in 29s
2025-04-20 06:14:58 +02:00
Julien Valverdé 974af95a22 Version bump
Lint / lint (push) Successful in 14s
2025-04-20 05:32:19 +02:00
Julien Valverdé d6e1d445e8 Fix
Lint / lint (push) Successful in 14s
2025-04-20 05:28:44 +02:00
Julien Valverdé d8d6e87a12 Refactoring
Lint / lint (push) Failing after 13s
2025-04-20 05:26:31 +02:00
Julien Valverdé 682e473bf7 Fix
Lint / lint (push) Successful in 13s
2025-04-20 05:10:51 +02:00
Julien Valverdé 31dd7b5fdb Working SubscriptionSubRef
Lint / lint (push) Successful in 13s
2025-04-20 05:06:48 +02:00
Julien Valverdé 17686e68c3 SubscriptionSubRef
Lint / lint (push) Successful in 18s
2025-04-20 04:34:01 +02:00
Julien Valverdé 49d4bd4d43 SubscriptionSubRef work
Lint / lint (push) Successful in 14s
2025-04-20 00:22:24 +02:00
Julien Valverdé be88035936 SubscriptionSubRef
Lint / lint (push) Failing after 10s
2025-04-19 03:42:48 +02:00
Julien Valverdé 3497d17046 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 13s
2025-04-14 00:58:05 +02:00
Julien Valverdé 8008e18221 0.1.7 (#10)
Publish / publish (push) Successful in 24s
Lint / lint (push) Successful in 18s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/10
2025-04-14 00:57:30 +02:00
Julien Valverdé 1ca832e69d Fix
Lint / lint (push) Successful in 14s
Test build / test-build (pull_request) Successful in 21s
2025-04-14 00:54:06 +02:00
Julien Valverdé 98bd72d1d7 Cleanup
Lint / lint (push) Successful in 13s
2025-04-13 18:29:00 +02:00
Julien Valverdé f594f47793 VQueryErrorHandler
Lint / lint (push) Successful in 13s
2025-04-13 17:39:54 +02:00
Julien Valverdé 4f9827720c Fix
Lint / lint (push) Successful in 14s
2025-04-13 17:18:06 +02:00
Julien Valverdé 0f761524fd Fix
Lint / lint (push) Successful in 14s
2025-04-13 17:06:20 +02:00
Julien Valverdé 574136e161 SubscribeStream
Lint / lint (push) Successful in 14s
2025-04-13 03:21:11 +02:00
Julien Valverdé 7a12abdbdf useSubscribeStream
Lint / lint (push) Successful in 13s
2025-04-13 02:30:29 +02:00
Julien Valverdé 8fecb94292 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 13s
2025-04-12 23:59:40 +02:00
Julien Valverdé 4092da0f0c 0.1.6 (#9)
Publish / publish (push) Successful in 29s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/9
2025-04-12 23:58:25 +02:00
Julien Valverdé 26a2111705 Version bump
Lint / lint (push) Successful in 14s
Test build / test-build (pull_request) Successful in 43s
2025-04-12 23:52:35 +02:00
Julien Valverdé 1cb02407c8 Dependencies fix
Lint / lint (push) Successful in 14s
2025-04-12 23:47:43 +02:00
Julien Valverdé 6e8ce84851 Cleanup
Lint / lint (push) Successful in 13s
2025-04-12 23:06:43 +02:00
Julien Valverdé 570fb93876 ReffuseHelpers -> ReffuseNamespace
Lint / lint (push) Successful in 14s
2025-04-12 23:03:17 +02:00
Julien Valverdé 821fd18f8f Fix
Lint / lint (push) Successful in 14s
2025-04-12 18:30:37 +02:00
Julien Valverdé b7ef95341b Tests
Lint / lint (push) Successful in 14s
2025-04-12 00:39:02 +02:00
Julien Valverdé 5f5ef5614b Working SubscribeRefs
Lint / lint (push) Successful in 14s
2025-04-12 00:16:04 +02:00
Julien Valverdé cbd39f893e Done useSubscribeRefs
Lint / lint (push) Successful in 16s
2025-04-11 23:40:06 +02:00
Julien Valverdé 529e3d3f9d useSubscribeRefs work
Lint / lint (push) Failing after 10s
2025-04-11 21:43:32 +02:00
Julien Valverdé 9d47418a69 useRefsState work
Lint / lint (push) Failing after 10s
2025-04-11 20:10:34 +02:00
Julien Valverdé c1b6e73231 useRefsState work
Lint / lint (push) Successful in 13s
2025-04-11 02:58:44 +02:00
Julien Valverdé d1ba4148f2 useRefsState work
Lint / lint (push) Failing after 11s
2025-04-11 02:10:21 +02:00
Julien Valverdé ef13e87d12 Fix
Lint / lint (push) Successful in 14s
2025-04-11 00:23:15 +02:00
Julien Valverdé 8b141b907f RefState tests
Lint / lint (push) Successful in 14s
2025-04-10 23:06:13 +02:00
Julien Valverdé 52a36cb882 RefState component
Lint / lint (push) Successful in 58s
2025-04-10 22:38:29 +02:00
Julien Valverdé 3b844f071b Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 12s
2025-03-31 21:43:29 +02:00
Julien Valverdé d7c648994d @reffuse/extension-query 0.1.2 (#8)
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 12s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/8
2025-03-31 21:42:24 +02:00
Julien Valverdé 4e422a1901 Version bump
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 17s
2025-03-31 21:40:55 +02:00
Julien Valverdé a5c6b34dfe Example fix
Lint / lint (push) Successful in 14s
2025-03-31 21:19:41 +02:00
Julien Valverdé ab1f851428 Refactoring
Lint / lint (push) Successful in 14s
2025-03-31 21:07:42 +02:00
Julien Valverdé 3f091d55c2 QueryClient refactoring
Lint / lint (push) Successful in 17s
2025-03-31 20:54:32 +02:00
Julien Valverdé 76a33fccca Query refactoring
Lint / lint (push) Successful in 14s
2025-03-31 20:38:21 +02:00
Julien Valverdé c75bb10e6b QueryClient work
Lint / lint (push) Failing after 17s
2025-03-31 18:22:18 +02:00
Julien Valverdé 3da4b2a318 QueryClient work
Lint / lint (push) Failing after 14s
2025-03-31 01:54:08 +02:00
Julien Valverdé 9a24ecaf84 QueryClient work
Lint / lint (push) Failing after 14s
2025-03-31 00:00:47 +02:00
Julien Valverdé 7b20df6c71 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 12s
2025-03-28 21:26:06 +01:00
Julien Valverdé 74fa30cf4f 0.1.5 (#7)
Publish / publish (push) Successful in 26s
Lint / lint (push) Successful in 12s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/7
2025-03-28 21:24:41 +01:00
Julien Valverdé f40dae90fb Version bump
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 44s
2025-03-28 21:22:32 +01:00
Julien Valverdé 46211638f5 Refactoring
Lint / lint (push) Successful in 13s
2025-03-28 21:19:17 +01:00
Julien Valverdé a28d6c3d30 Refactoring
Lint / lint (push) Successful in 13s
2025-03-28 21:12:42 +01:00
Julien Valverdé 6b74b9a3b2 useMemoScoped refactoring
Lint / lint (push) Successful in 13s
2025-03-28 21:09:27 +01:00
Julien Valverdé e17f945666 Fix
Lint / lint (push) Successful in 13s
2025-03-28 20:30:31 +01:00
Julien Valverdé aa46ecc82d Async provider refactoring
Lint / lint (push) Successful in 14s
2025-03-28 20:27:25 +01:00
Julien Valverdé 8ea9146dd9 Provider refactoring
Lint / lint (push) Successful in 13s
2025-03-28 19:18:46 +01:00
Julien Valverdé 0a4bb2856d Provider refactoring
Lint / lint (push) Successful in 13s
2025-03-28 18:48:49 +01:00
Julien Valverdé b4cd7daa81 Restore
Lint / lint (push) Failing after 26s
2025-03-28 18:26:04 +01:00
Julien Valverdé b5712d5433 Test
Lint / lint (push) Successful in 36s
2025-03-28 17:39:23 +01:00
Julien Valverdé 57b7eac05c Test 2025-03-28 17:37:28 +01:00
Julien Valverdé 9a9bd78ec6 Provider work
Lint / lint (push) Failing after 10s
2025-03-28 17:01:41 +01:00
Julien Valverdé ddcd681ca4 Provider refactoring
Lint / lint (push) Failing after 19m51s
2025-03-28 16:08:04 +01:00
Julien Valverdé 66de517ab5 Refactoring
Lint / lint (push) Failing after 15m36s
2025-03-26 20:24:53 +01:00
Julien Valverdé b50255ded2 Merge branch 'queryclient' of git.valverde.cloud:Thilawyn/reffuse into queryclient
Lint / lint (push) Successful in 13s
2025-03-26 19:34:59 +01:00
Julien Valverdé 03f0b623ed Removed JSX code 2025-03-26 19:34:21 +01:00
Julien Valverdé fb6d803723 Removed JSX code 2025-03-26 19:34:14 +01:00
Julien Valverdé 972986241c ReffuseHelpers.make()
Lint / lint (push) Has been cancelled
2025-03-25 19:29:17 +01:00
Julien Valverdé 9eb0904600 Refactoring
Lint / lint (push) Successful in 13s
2025-03-24 20:35:22 +01:00
Julien Valverdé fc86c818e0 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 13s
2025-03-24 19:41:21 +01:00
Julien Valverdé d01152bdcf 0.1.4 (#6)
Publish / publish (push) Successful in 30s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/6
2025-03-24 19:39:29 +01:00
Julien Valverdé 5a12139602 Regenerated lockfile
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 15s
2025-03-24 19:35:56 +01:00
Julien Valverdé a0928c718f Version bump
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 15s
2025-03-24 19:33:35 +01:00
Julien Valverdé 49d9edd4b1 Dependencies upgrade
Lint / lint (push) Successful in 14s
2025-03-24 19:29:12 +01:00
Julien Valverdé 3552c25b5c Mutation refactoring
Lint / lint (push) Successful in 13s
2025-03-24 19:07:11 +01:00
Julien Valverdé 516e0a465d Ref state fix
Lint / lint (push) Successful in 16s
2025-03-24 18:38:14 +01:00
Julien Valverdé 7cf5367409 Tests
Lint / lint (push) Successful in 12s
2025-03-24 17:34:50 +01:00
Julien Valverdé 3b237c0588 Query refactoring
Lint / lint (push) Successful in 13s
2025-03-24 17:30:41 +01:00
Julien Valverdé d9aa42d23a Fix
Lint / lint (push) Successful in 13s
2025-03-24 12:16:03 +01:00
Julien Valverdé fd3213c53f Fix
Lint / lint (push) Successful in 13s
2025-03-24 12:06:03 +01:00
Julien Valverdé baa8c92221 Query refactoring
Lint / lint (push) Successful in 14s
2025-03-24 12:03:55 +01:00
Julien Valverdé d55b432846 Refactoring
Lint / lint (push) Successful in 13s
2025-03-23 23:58:05 +01:00
Julien Valverdé 6266c7506e Example fix
Lint / lint (push) Successful in 13s
2025-03-23 07:32:11 +01:00
Julien Valverdé 043e966e45 ErrorHandler work
Lint / lint (push) Successful in 14s
2025-03-23 07:25:03 +01:00
Julien Valverdé 88fab2c7d7 ErrorHandler refactoring
Lint / lint (push) Failing after 15s
2025-03-23 06:08:35 +01:00
Julien Valverdé 224ccd8e32 Fix
Lint / lint (push) Successful in 14s
2025-03-21 04:55:38 +01:00
Julien Valverdé 4cf70ada0b Fix
Lint / lint (push) Successful in 16s
2025-03-21 04:49:44 +01:00
Julien Valverdé f9bd5d4d6b Query refactoring
Lint / lint (push) Successful in 14s
2025-03-21 04:37:32 +01:00
Julien Valverdé 1ec1db0658 Mutation progress
Lint / lint (push) Successful in 14s
2025-03-21 03:38:48 +01:00
Julien Valverdé 2d94e84941 Stream fix
Lint / lint (push) Successful in 13s
2025-03-21 03:24:55 +01:00
Julien Valverdé aab83907ba Working mutation progress
Lint / lint (push) Successful in 13s
2025-03-21 02:14:36 +01:00
Julien Valverdé 8c0d6b4c8a Cleanup
Lint / lint (push) Successful in 13s
2025-03-21 01:24:52 +01:00
Julien Valverdé d82d1d1c29 Refactoring
Lint / lint (push) Successful in 14s
2025-03-21 01:23:47 +01:00
Julien Valverdé 0f09573948 Mutation services
Lint / lint (push) Failing after 14s
2025-03-20 07:10:55 +01:00
Julien Valverdé 2b6b36713e MutationRunner work
Lint / lint (push) Successful in 14s
2025-03-20 04:31:38 +01:00
Julien Valverdé 5d0aecc9d5 QueryProgress
Lint / lint (push) Successful in 14s
2025-03-19 05:13:54 +01:00
Julien Valverdé f21d8b2d8a QueryProgress
Lint / lint (push) Successful in 14s
2025-03-18 03:11:39 +01:00
Julien Valverdé f85173fa68 Fix
Lint / lint (push) Successful in 30s
2025-03-18 02:46:41 +01:00
Julien Valverdé 65a124de1f Mutation tests
Lint / lint (push) Successful in 13s
2025-03-17 05:52:13 +01:00
Julien Valverdé 16893761c6 Mutation refactoring
Lint / lint (push) Successful in 13s
2025-03-17 05:34:19 +01:00
Julien Valverdé 3fdc2e31eb Mutation example
Lint / lint (push) Successful in 17s
2025-03-17 02:36:13 +01:00
Julien Valverdé 8636a28f2f Working mutations
Lint / lint (push) Successful in 14s
2025-03-17 02:15:27 +01:00
Julien Valverdé d56578da8f useMutation
Lint / lint (push) Successful in 13s
2025-03-16 06:50:20 +01:00
Julien Valverdé 299109d421 Mutation fix
Lint / lint (push) Successful in 14s
2025-03-16 06:25:02 +01:00
Julien Valverdé 4995b2949f MutationRunner
Lint / lint (push) Successful in 13s
2025-03-16 05:20:55 +01:00
Julien Valverdé 6e6e675709 MutationRunner 2025-03-16 05:20:37 +01:00
Julien Valverdé b04860aa25 Cleanup
Lint / lint (push) Successful in 13s
2025-03-16 04:32:51 +01:00
Julien Valverdé e9e17ac211 Fix
Lint / lint (push) Successful in 12s
2025-03-16 04:11:25 +01:00
Julien Valverdé 1f0ff725ff Fix
Lint / lint (push) Successful in 13s
2025-03-16 04:05:39 +01:00
Julien Valverdé 447d89982c Fix
Lint / lint (push) Successful in 13s
2025-03-16 03:34:54 +01:00
Julien Valverdé 778ee27795 ErrorHandler refactoring
Lint / lint (push) Successful in 16s
2025-03-16 03:33:01 +01:00
Julien Valverdé 077816efb6 Fix
Lint / lint (push) Successful in 13s
2025-03-16 03:23:12 +01:00
Julien Valverdé e4bacd1ca7 Working QueryClient refactoring
Lint / lint (push) Successful in 13s
2025-03-16 03:19:12 +01:00
Julien Valverdé 0e2c0db28f QueryClient refactoring
Lint / lint (push) Successful in 13s
2025-03-16 02:52:49 +01:00
Julien Valverdé c943d81702 QueryClient.make
Lint / lint (push) Successful in 12s
2025-03-15 22:27:15 +01:00
Julien Valverdé c2bc406a5f Fixed query error handler
Lint / lint (push) Successful in 13s
2025-03-15 06:43:47 +01:00
Julien Valverdé 4e778b6c95 VQueryErrorHandler
Lint / lint (push) Successful in 12s
2025-03-15 05:12:38 +01:00
Julien Valverdé 0437fa5dcc QueryErrorHandler work
Lint / lint (push) Successful in 16s
2025-03-15 02:30:37 +01:00
Julien Valverdé 5614b8df38 Fix
Lint / lint (push) Successful in 13s
2025-03-15 00:52:00 +01:00
Julien Valverdé 70b6c4434e Tests
Lint / lint (push) Successful in 16s
2025-03-14 22:07:53 +01:00
Julien Valverdé 2e8dfbc988 QueryClient
Lint / lint (push) Successful in 13s
2025-03-14 22:00:53 +01:00
Julien Valverdé abc47c4647 Fix
Lint / lint (push) Failing after 12s
2025-03-14 05:04:49 +01:00
Julien Valverdé eedd2a7f2a makeTag
Lint / lint (push) Failing after 12s
2025-03-14 04:57:07 +01:00
Julien Valverdé f4ab575a8d QueryExtension work
Lint / lint (push) Failing after 13s
2025-03-14 04:24:56 +01:00
Julien Valverdé 747e2c6056 Done QueryClient
Lint / lint (push) Successful in 14s
2025-03-14 04:13:14 +01:00
Julien Valverdé 68c68417d8 QueryClient work
Lint / lint (push) Successful in 12s
2025-03-14 03:56:54 +01:00
Julien Valverdé ed384a62a8 QueryClient work
Lint / lint (push) Failing after 15s
2025-03-14 03:26:28 +01:00
Julien Valverdé 3a1748bb39 QueryClient tests
Lint / lint (push) Successful in 18s
2025-03-13 22:31:50 +01:00
Julien Valverdé 66b8fd2c2e Fix
Lint / lint (push) Successful in 13s
2025-03-12 06:37:39 +01:00
Julien Valverdé bc81c443ab Query work
Lint / lint (push) Successful in 13s
2025-03-11 21:19:57 +01:00
Julien Valverdé ee5dbe3766 Query work
Lint / lint (push) Successful in 13s
2025-03-11 20:39:56 +01:00
63 changed files with 2477 additions and 999 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
+438 -226
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/monorepo", "name": "@reffuse/monorepo",
"packageManager": "bun@1.2.2", "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.14", "npm-check-updates": "^18.0.1",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
"turbo": "^2.4.4", "turbo": "^2.5.3",
"typescript": "^5.7.3" "typescript": "^5.8.3"
} }
} }
+11
View File
@@ -0,0 +1,11 @@
# Reffuse
[Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
## Peer dependencies
- `effect` 3.13+
- `react` & `@types/react` 19+
+40
View File
@@ -0,0 +1,40 @@
{
"name": "effect-components",
"version": "0.1.0",
"type": "module",
"files": [
"./README.md",
"./dist"
],
"license": "MIT",
"repository": {
"url": "git+https://github.com/Thiladev/reffuse.git"
},
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
},
"scripts": {
"build": "tsc",
"lint:tsc": "tsc --noEmit",
"pack": "npm pack",
"clean:cache": "rm -f tsconfig.tsbuildinfo",
"clean:dist": "rm -rf dist",
"clean:node": "rm -rf node_modules"
},
"peerDependencies": {
"@types/react": "^19.0.0",
"effect": "^3.15.0",
"react": "^19.0.0"
},
"devDependencies": {
"@effect/language-service": "^0.23.3"
}
}
@@ -0,0 +1,80 @@
import { Context, Effect, ExecutionStrategy, Exit, Ref, Runtime, Scope, Tracer } from "effect"
import * as React from "react"
import * as ReactHook from "./ReactHook.js"
export interface ReactComponent<P, E, R> {
(props: P): Effect.Effect<React.ReactNode, E, R>
}
export const nonReactiveTags = [Tracer.ParentSpan] as const
export const useFC: {
<P, E, R>(
self: ReactComponent<P, E, R>,
options?: ReactHook.ScopeOptions,
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* useFC<P, E, R>(
self: ReactComponent<P, E, R>,
options?: ReactHook.ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
return React.useCallback((props: P) => {
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
Effect.all([Ref.make(true), makeScope(options)])
), [])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => Runtime.runSync(runtime)(
Effect.if(isInitialRun, {
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => closeScope(scope, runtime, options),
),
onFalse: () => makeScope(options).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
), [])
return Runtime.runSync(runtime)(
Effect.provideService(self(props), Scope.Scope, scope)
)
}, Array.from(
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
))
})
const makeScope = (options?: ReactHook.ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
const closeScope = (
scope: Scope.CloseableScope,
runtime: Runtime.Runtime<never>,
options?: ReactHook.ScopeOptions,
) => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}
export const use: {
<P, E, R>(
self: ReactComponent<P, E, R>,
fn: (Component: React.FC<P>) => React.ReactNode,
options?: ReactHook.ScopeOptions,
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* use<P, E, R>(
self: ReactComponent<P, E, R>,
fn: (Component: React.FC<P>) => React.ReactNode,
options?: ReactHook.ScopeOptions,
) {
return fn(yield* useFC(self, options))
})
@@ -0,0 +1,96 @@
import { Effect, ExecutionStrategy, Runtime, Scope } from "effect"
import * as React from "react"
export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
}
export const useMemo: {
<A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<A, never, R>
} = Effect.fnUntraced(function* useMemo<A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime<R>()
return React.useMemo(() => Runtime.runSync(runtime)(factory()), deps)
})
export const useOnce: {
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, never, R>
} = Effect.fnUntraced(function* useOnce<A, E, R>(
factory: () => Effect.Effect<A, E, R>
) {
return yield* useMemo(factory, [])
})
export const useEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, R>
} = Effect.fnUntraced(function* useEffect<E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<R>()
React.useEffect(() => {
const { scope, exit } = Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Runtime.runSync(runtime),
)
return () => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, exit))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, exit))
break
}
}
}, deps)
})
export const useLayoutEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, R>
} = Effect.fnUntraced(function* useLayoutEffect<E, R>(
effect: () => Effect.Effect<void, E, R | Scope.Scope>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<R>()
React.useLayoutEffect(() => {
const { scope, exit } = Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Runtime.runSync(runtime),
)
return () => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, exit))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, exit))
break
}
}
}, deps)
})
+2
View File
@@ -0,0 +1,2 @@
export * as ReactComponent from "./ReactComponent.js"
export * as ReactHook from "./ReactHook.js"
+33
View File
@@ -0,0 +1,33 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "NodeNext",
"moduleDetection": "force",
"jsx": "react-jsx",
// "allowJs": true,
// Bundler mode
"moduleResolution": "NodeNext",
// "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
// "noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
// Build
"outDir": "./dist",
"declaration": true
},
"include": ["./src"]
}
+23 -23
View File
@@ -11,41 +11,41 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.26.0",
"@tanstack/react-router": "^1.112.7", "@tanstack/react-router": "^1.120.3",
"@tanstack/router-devtools": "^1.112.7", "@tanstack/react-router-devtools": "^1.120.3",
"@tanstack/router-plugin": "^1.112.7", "@tanstack/router-plugin": "^1.120.3",
"@thilawyn/thilaschema": "^0.1.4", "@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.0.10", "@types/react": "^19.1.4",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.21.0", "eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.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.0.0", "react": "^19.1.0",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"typescript-eslint": "^8.26.0", "typescript-eslint": "^8.32.1",
"vite": "^6.2.0" "vite": "^6.3.5"
}, },
"dependencies": { "dependencies": {
"@effect/platform": "^0.77.6", "@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.56.6", "@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.1", "@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3", "@typed/lazy-ref": "^0.3.3",
"effect": "^3.13.6", "effect": "^3.15.1",
"lucide-react": "^0.477.0", "lucide-react": "^0.510.0",
"mobx": "^6.13.6", "mobx": "^6.13.7",
"reffuse": "workspace:*" "reffuse": "workspace:*"
}, },
"overrides": { "overrides": {
"effect": "^3.13.6", "effect": "^3.15.1",
"@effect/platform": "^0.77.6", "@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.56.6", "@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"
} }
@@ -0,0 +1,57 @@
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
import { useState } from "react"
import { R } from "./reffuse"
import { AppQueryErrorHandler } from "./services"
export function VQueryErrorHandler() {
const [open, setOpen] = useState(false)
const error = R.useSubscribeStream(
R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
Effect.map(handler => handler.errors.pipe(
Stream.changes,
Stream.tap(Console.error),
Stream.tap(() => Effect.sync(() => setOpen(true))),
))
), [])
)
if (Option.isNone(error))
return <></>
return (
<AlertDialog.Root open={open}>
<AlertDialog.Content maxWidth="450px">
<AlertDialog.Title>Error</AlertDialog.Title>
<AlertDialog.Description size="2">
{Either.match(Cause.failureOrCause(error.value), {
onLeft: flow(
Match.value,
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
Match.exhaustive,
),
onRight: flow(
Cause.dieOption,
Option.match({
onSome: () => <Text>Unrecoverable defect</Text>,
onNone: () => <Text>Unknown error</Text>,
}),
),
})}
</AlertDialog.Description>
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={() => setOpen(false)}>
Ok
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
)
}
+2 -8
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)
)
+6 -3
View File
@@ -5,11 +5,14 @@ import { Layer } from "effect"
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { ReffuseRuntime } from "reffuse" import { ReffuseRuntime } from "reffuse"
import { GlobalContext } from "./reffuse" import { RootContext } from "./reffuse"
import { routeTree } from "./routeTree.gen" import { routeTree } from "./routeTree.gen"
import { AppQueryClient, AppQueryErrorHandler } from "./services"
const layer = Layer.empty.pipe( const layer = Layer.empty.pipe(
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
Layer.provideMerge(Clipboard.layer), Layer.provideMerge(Clipboard.layer),
Layer.provideMerge(Geolocation.layer), Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer), Layer.provideMerge(Permissions.layer),
@@ -28,9 +31,9 @@ declare module "@tanstack/react-router" {
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<ReffuseRuntime.Provider> <ReffuseRuntime.Provider>
<GlobalContext.Provider layer={layer}> <RootContext.Provider layer={layer}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</GlobalContext.Provider> </RootContext.Provider>
</ReffuseRuntime.Provider> </ReffuseRuntime.Provider>
</StrictMode> </StrictMode>
) )
+2 -2
View File
@@ -1,10 +1,10 @@
import { GlobalReffuse } from "@/reffuse" import { RootReffuse } from "@/reffuse"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { Uuid4Query } from "./services" import { Uuid4Query } from "./services"
export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>() export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>()
export const R = new class QueryReffuse extends GlobalReffuse.pipe( export const R = new class QueryReffuse extends RootReffuse.pipe(
Reffuse.withContexts(QueryContext) Reffuse.withContexts(QueryContext)
) {} ) {}
@@ -1,12 +1,11 @@
import { HttpClientError } from "@effect/platform" import { QueryRunner } from "@reffuse/extension-query"
import { QueryService } 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,
HttpClientError.HttpClientError | ParseResult.ParseError ParseResult.ParseError
>() {} >() {}
@@ -5,10 +5,10 @@ import { Uuid4Query } from "../services"
export function Uuid4QueryService() { export function Uuid4QueryService() {
const runSync = R.useRunSync() 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 (
@@ -25,7 +25,7 @@ export function Uuid4QueryService() {
})} })}
</Text> </Text>
<Button onClick={() => runSync(query.refresh)}>Refresh</Button> <Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button>
</Flex> </Flex>
</Container> </Container>
) )
+7 -4
View File
@@ -3,19 +3,22 @@ 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 "./services"
export const GlobalContext = ReffuseContext.make< export const RootContext = ReffuseContext.make<
| AppQueryClient.AppQueryClient
| AppQueryErrorHandler.AppQueryErrorHandler
| Clipboard.Clipboard | Clipboard.Clipboard
| Geolocation.Geolocation | Geolocation.Geolocation
| Permissions.Permissions | Permissions.Permissions
| HttpClient.HttpClient | HttpClient.HttpClient
>() >()
export class GlobalReffuse extends Reffuse.Reffuse.pipe( export class RootReffuse extends Reffuse.Reffuse.pipe(
Reffuse.withExtension(LazyRefExtension), Reffuse.withExtension(LazyRefExtension),
Reffuse.withExtension(QueryExtension), Reffuse.withExtension(QueryExtension),
Reffuse.withContexts(GlobalContext), Reffuse.withContexts(RootContext),
) {} ) {}
export const R = new GlobalReffuse() export const R = new RootReffuse()
+105 -1
View File
@@ -11,18 +11,28 @@
// Import Routes // Import Routes
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as TodosImport } from './routes/todos'
import { Route as TimeImport } from './routes/time' import { Route as TimeImport } from './routes/time'
import { Route as TestsImport } from './routes/tests' import { Route as TestsImport } from './routes/tests'
import { Route as PromiseImport } from './routes/promise' import { Route as PromiseImport } from './routes/promise'
import { Route as LazyrefImport } from './routes/lazyref' import { Route as LazyrefImport } from './routes/lazyref'
import { Route as EffectComponentTestsImport } from './routes/effect-component-tests'
import { Route as CountImport } from './routes/count' import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank' import { Route as BlankImport } from './routes/blank'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as StreamsPullImport } from './routes/streams/pull'
import { Route as QueryUsequeryImport } from './routes/query/usequery' import { Route as QueryUsequeryImport } from './routes/query/usequery'
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
import { Route as QueryServiceImport } from './routes/query/service' import { Route as QueryServiceImport } from './routes/query/service'
// Create/Update Routes // Create/Update Routes
const TodosRoute = TodosImport.update({
id: '/todos',
path: '/todos',
getParentRoute: () => rootRoute,
} as any)
const TimeRoute = TimeImport.update({ const TimeRoute = TimeImport.update({
id: '/time', id: '/time',
path: '/time', path: '/time',
@@ -47,6 +57,12 @@ const LazyrefRoute = LazyrefImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const EffectComponentTestsRoute = EffectComponentTestsImport.update({
id: '/effect-component-tests',
path: '/effect-component-tests',
getParentRoute: () => rootRoute,
} as any)
const CountRoute = CountImport.update({ const CountRoute = CountImport.update({
id: '/count', id: '/count',
path: '/count', path: '/count',
@@ -65,12 +81,24 @@ 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',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const QueryUsemutationRoute = QueryUsemutationImport.update({
id: '/query/usemutation',
path: '/query/usemutation',
getParentRoute: () => rootRoute,
} as any)
const QueryServiceRoute = QueryServiceImport.update({ const QueryServiceRoute = QueryServiceImport.update({
id: '/query/service', id: '/query/service',
path: '/query/service', path: '/query/service',
@@ -102,6 +130,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CountImport preLoaderRoute: typeof CountImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/effect-component-tests': {
id: '/effect-component-tests'
path: '/effect-component-tests'
fullPath: '/effect-component-tests'
preLoaderRoute: typeof EffectComponentTestsImport
parentRoute: typeof rootRoute
}
'/lazyref': { '/lazyref': {
id: '/lazyref' id: '/lazyref'
path: '/lazyref' path: '/lazyref'
@@ -130,6 +165,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TimeImport preLoaderRoute: typeof TimeImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/todos': {
id: '/todos'
path: '/todos'
fullPath: '/todos'
preLoaderRoute: typeof TodosImport
parentRoute: typeof rootRoute
}
'/query/service': { '/query/service': {
id: '/query/service' id: '/query/service'
path: '/query/service' path: '/query/service'
@@ -137,6 +179,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof QueryServiceImport preLoaderRoute: typeof QueryServiceImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/query/usemutation': {
id: '/query/usemutation'
path: '/query/usemutation'
fullPath: '/query/usemutation'
preLoaderRoute: typeof QueryUsemutationImport
parentRoute: typeof rootRoute
}
'/query/usequery': { '/query/usequery': {
id: '/query/usequery' id: '/query/usequery'
path: '/query/usequery' path: '/query/usequery'
@@ -144,6 +193,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof QueryUsequeryImport preLoaderRoute: typeof QueryUsequeryImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/streams/pull': {
id: '/streams/pull'
path: '/streams/pull'
fullPath: '/streams/pull'
preLoaderRoute: typeof StreamsPullImport
parentRoute: typeof rootRoute
}
} }
} }
@@ -153,24 +209,32 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/count': typeof CountRoute '/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute '/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/count': typeof CountRoute '/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute '/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
@@ -178,12 +242,16 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/blank': typeof BlankRoute '/blank': typeof BlankRoute
'/count': typeof CountRoute '/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute '/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
@@ -192,34 +260,46 @@ export interface FileRouteTypes {
| '/' | '/'
| '/blank' | '/blank'
| '/count' | '/count'
| '/effect-component-tests'
| '/lazyref' | '/lazyref'
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/blank' | '/blank'
| '/count' | '/count'
| '/effect-component-tests'
| '/lazyref' | '/lazyref'
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
id: id:
| '__root__' | '__root__'
| '/' | '/'
| '/blank' | '/blank'
| '/count' | '/count'
| '/effect-component-tests'
| '/lazyref' | '/lazyref'
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
| '/streams/pull'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -227,24 +307,32 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
BlankRoute: typeof BlankRoute BlankRoute: typeof BlankRoute
CountRoute: typeof CountRoute CountRoute: typeof CountRoute
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
LazyrefRoute: typeof LazyrefRoute LazyrefRoute: typeof LazyrefRoute
PromiseRoute: typeof PromiseRoute PromiseRoute: typeof PromiseRoute
TestsRoute: typeof TestsRoute TestsRoute: typeof TestsRoute
TimeRoute: typeof TimeRoute TimeRoute: typeof TimeRoute
TodosRoute: typeof TodosRoute
QueryServiceRoute: typeof QueryServiceRoute QueryServiceRoute: typeof QueryServiceRoute
QueryUsemutationRoute: typeof QueryUsemutationRoute
QueryUsequeryRoute: typeof QueryUsequeryRoute QueryUsequeryRoute: typeof QueryUsequeryRoute
StreamsPullRoute: typeof StreamsPullRoute
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
BlankRoute: BlankRoute, BlankRoute: BlankRoute,
CountRoute: CountRoute, CountRoute: CountRoute,
EffectComponentTestsRoute: EffectComponentTestsRoute,
LazyrefRoute: LazyrefRoute, LazyrefRoute: LazyrefRoute,
PromiseRoute: PromiseRoute, PromiseRoute: PromiseRoute,
TestsRoute: TestsRoute, TestsRoute: TestsRoute,
TimeRoute: TimeRoute, TimeRoute: TimeRoute,
TodosRoute: TodosRoute,
QueryServiceRoute: QueryServiceRoute, QueryServiceRoute: QueryServiceRoute,
QueryUsemutationRoute: QueryUsemutationRoute,
QueryUsequeryRoute: QueryUsequeryRoute, QueryUsequeryRoute: QueryUsequeryRoute,
StreamsPullRoute: StreamsPullRoute,
} }
export const routeTree = rootRoute export const routeTree = rootRoute
@@ -260,12 +348,16 @@ export const routeTree = rootRoute
"/", "/",
"/blank", "/blank",
"/count", "/count",
"/effect-component-tests",
"/lazyref", "/lazyref",
"/promise", "/promise",
"/tests", "/tests",
"/time", "/time",
"/todos",
"/query/service", "/query/service",
"/query/usequery" "/query/usemutation",
"/query/usequery",
"/streams/pull"
] ]
}, },
"/": { "/": {
@@ -277,6 +369,9 @@ export const routeTree = rootRoute
"/count": { "/count": {
"filePath": "count.tsx" "filePath": "count.tsx"
}, },
"/effect-component-tests": {
"filePath": "effect-component-tests.tsx"
},
"/lazyref": { "/lazyref": {
"filePath": "lazyref.tsx" "filePath": "lazyref.tsx"
}, },
@@ -289,11 +384,20 @@ export const routeTree = rootRoute
"/time": { "/time": {
"filePath": "time.tsx" "filePath": "time.tsx"
}, },
"/todos": {
"filePath": "todos.tsx"
},
"/query/service": { "/query/service": {
"filePath": "query/service.tsx" "filePath": "query/service.tsx"
}, },
"/query/usemutation": {
"filePath": "query/usemutation.tsx"
},
"/query/usequery": { "/query/usequery": {
"filePath": "query/usequery.tsx" "filePath": "query/usequery.tsx"
},
"/streams/pull": {
"filePath": "streams/pull.tsx"
} }
} }
} }
+4 -1
View File
@@ -1,6 +1,7 @@
import { VQueryErrorHandler } from "@/VQueryErrorHandler"
import { Container, Flex, Theme } from "@radix-ui/themes" import { Container, Flex, Theme } from "@radix-ui/themes"
import { createRootRoute, Link, Outlet } from "@tanstack/react-router" import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools" import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
import "@radix-ui/themes/styles.css" import "@radix-ui/themes/styles.css"
import "../index.css" import "../index.css"
@@ -26,6 +27,8 @@ function Root() {
</Container> </Container>
<Outlet /> <Outlet />
<VQueryErrorHandler />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</Theme> </Theme>
) )
+4 -5
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>
@@ -0,0 +1,43 @@
import { Box, Text, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Layer, ManagedRuntime, SubscriptionRef } from "effect"
import { ReactComponent, ReactHook } from "effect-components"
import * as React from "react"
export const Route = createFileRoute("/effect-component-tests")({
component: RouteComponent,
})
function RouteComponent() {
const runtime = React.useMemo(() => ManagedRuntime.make(Layer.empty), [])
return <>
{runtime.runSync(ReactComponent.use(MyTestComponent, Component => (
<Component />
)))}
</>
}
class TestService extends Effect.Service<TestService>()("TestService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
}) {}
const MyTestComponent = Effect.fn(function* MyTestComponent(props?: { readonly value?: string }) {
const [state, setState] = React.useState("value")
// yield* ReactHook.useMemo(() => Effect.andThen(
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
// Console.log("MyTestComponent mounted"),
// ), [])
return <>
<Box>
<TextField.Root
value={state}
onChange={e => setState(e.target.value)}
/>
</Box>
</>
})
+4 -23
View File
@@ -1,29 +1,10 @@
import { TodosContext } from "@/todos/reffuse"
import { TodosState } from "@/todos/services"
import { VTodos } from "@/todos/views/VTodos"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/")({ export const Route = createFileRoute('/')({
component: Index component: RouteComponent
}) })
function Index() { function RouteComponent() {
return <div>Hello "/"!</div>
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos"))
), [])
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</Container>
)
} }
@@ -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}>
@@ -0,0 +1,84 @@
import { R } from "@/reffuse"
import { HttpClient } from "@effect/platform"
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
import { QueryProgress } from "@reffuse/extension-query"
import { createFileRoute } from "@tanstack/react-router"
import * as AsyncData from "@typed/async-data"
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
import { useState } from "react"
export const Route = createFileRoute("/query/usemutation")({
component: RouteComponent
})
const Result = Schema.Array(Schema.String)
function RouteComponent() {
const runFork = R.useRunFork()
const [count, setCount] = useState(1)
const mutation = R.useMutation({
mutation: ([count]: readonly [count: number]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(QueryProgress.QueryProgress.update(() =>
AsyncData.Progress.make({ loaded: 0, total: Option.some(100) })
)),
Effect.andThen(Effect.sleep("500 millis")),
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
)),
Effect.andThen(Effect.map(
HttpClient.HttpClient,
HttpClient.withTracerPropagation(false),
)),
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)),
Effect.scoped,
)
})
const [state] = R.useSubscribeRefs(mutation.stateRef)
return (
<Container>
<Flex direction="column" align="center" gap="2">
<Slider
min={1}
max={100}
value={[count]}
onValueChange={flow(
Array.head,
Option.getOrThrow,
setCount,
)}
/>
<Text>
{AsyncData.match(state, {
NoData: () => "No data yet",
Loading: progress =>
`Loading...
${ Option.match(progress, {
onSome: ({ loaded, total }) => ` (${ loaded }/${ Option.getOrElse(total, () => "unknown") })`,
onNone: () => "",
}) }`,
Success: value => `Value: ${ value }`,
Failure: cause => `Error: ${ cause }`,
})}
</Text>
<Button onClick={() => mutation.forkMutate(count).pipe(
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
Effect.andThen(Console.log("Mutation done.")),
runFork,
)}>
Get
</Button>
</Flex>
</Container>
)
}
+18 -7
View File
@@ -3,7 +3,7 @@ import { HttpClient } from "@effect/platform"
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes" import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import * as AsyncData from "@typed/async-data" import * as AsyncData from "@typed/async-data"
import { Array, Console, Effect, flow, Option, Schema } from "effect" import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
import { useState } from "react" import { useState } from "react"
@@ -15,23 +15,26 @@ export const Route = createFileRoute("/query/usequery")({
const Result = Schema.Array(Schema.String) const Result = Schema.Array(Schema.String)
function RouteComponent() { function RouteComponent() {
const runSync = R.useRunSync() const runFork = R.useRunFork()
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.useRefState(query.state) const [state] = R.useSubscribeRefs(query.stateRef)
return ( return (
@@ -59,7 +62,15 @@ function RouteComponent() {
})} })}
</Text> </Text>
<Button onClick={() => runSync(query.refresh)}>Refresh</Button> <Button
onClick={() => query.forkRefresh.pipe(
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
Effect.andThen(Console.log("Refresh finished or stopped")),
runFork,
)}
>
Refresh
</Button>
</Flex> </Flex>
</Container> </Container>
) )
@@ -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>
)
}
+41 -25
View File
@@ -1,9 +1,22 @@
import { R } from "@/reffuse" import { R } from "@/reffuse"
import { Button, Flex } 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, Stream } from "effect" import { Console, Effect, Option } from "effect"
import { useState } from "react" 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")({
@@ -11,36 +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 [reactValue, setReactValue] = useState("initial") const uuidStream = R.useStreamFromReactiveValues([uuid])
const reactValueStream = R.useStreamFromValues([reactValue]) const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
R.useFork(() => Stream.runForEach(reactValueStream, Console.log), [reactValueStream])
const [, scopeLayer] = R.useScope([uuid])
useEffect(() => Effect.addFinalizer(() => Console.log("Scope cleanup!")).pipe(
Effect.andThen(Console.log("Scope changed")),
Effect.provide(scopeLayer),
runSync,
), [scopeLayer, runSync])
const 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.map(setReactValue),
), [])
return ( return (
<Flex direction="row" justify="center" align="center" gap="2"> <Flex direction="column" justify="center" align="center" gap="2">
<Button onClick={() => logValue("test")}>Log value</Button> <Text>{uuid}</Text>
<Button onClick={() => generateUuid()}>Generate UUID</Button> <Button onClick={generateUuid}>Generate UUID</Button>
<Text>
{Option.match(uuidStreamLatestValue, {
onSome: ([v]) => v,
onNone: () => <></>,
})}
</Text>
</Flex> </Flex>
) )
} }
+35
View File
@@ -0,0 +1,35 @@
import { TodosContext } from "@/todos/reffuse"
import { TodosState } from "@/todos/services"
import { VTodos } from "@/todos/views/VTodos"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/todos")({
component: Todos
})
function Todos() {
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos")),
Layer.merge(Layer.effectDiscard(
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
Effect.andThen(Console.log("TodosContext constructed"))
)
)),
), [])
return (
<Container>
<TodosContext.Provider layer={todosLayer} finalizerExecutionMode="fork">
<VTodos />
</TodosContext.Provider>
</Container>
)
}
@@ -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
}) {}
@@ -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),
)
)
}) {}
+2 -1
View File
@@ -1 +1,2 @@
export {} export * as AppQueryClient from "./AppQueryClient"
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"
+2 -2
View File
@@ -1,10 +1,10 @@
import { GlobalReffuse } from "@/reffuse" import { RootReffuse } from "@/reffuse"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { TodosState } from "./services" import { TodosState } from "./services"
export const TodosContext = ReffuseContext.make<TodosState.TodosState>() export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
export const R = new class TodosReffuse extends GlobalReffuse.pipe( export const R = new class TodosReffuse extends RootReffuse.pipe(
Reffuse.withContexts(TodosContext) Reffuse.withContexts(TodosContext)
) {} ) {}
@@ -2,68 +2,43 @@ import { Todo } from "@/domain"
import { KeyValueStore } from "@effect/platform" import { KeyValueStore } from "@effect/platform"
import { BrowserKeyValueStore } from "@effect/platform-browser" import { BrowserKeyValueStore } from "@effect/platform-browser"
import { PlatformError } from "@effect/platform/Error" import { PlatformError } from "@effect/platform/Error"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect" import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
export class TodosState extends Context.Tag("TodosState")<TodosState, { export class TodosState extends Context.Tag("TodosState")<TodosState, {
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>> readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError> readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
readonly remove: (index: number) => Effect.Effect<void>
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
}>() {} }>() {}
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() { export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe( const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => kv.get(key)), Effect.flatMap(kv => kv.get(key)),
Effect.flatMap(identity), Effect.flatMap(identity),
Effect.flatMap(Schema.parseJson().pipe( Effect.flatMap(Schema.decode(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)), Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
Schema.decode,
)), )),
Effect.flatMap(v => Ref.set(todos, v)), Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
Effect.provide(BrowserKeyValueStore.layerLocalStorage), Effect.provide(BrowserKeyValueStore.layerLocalStorage),
) )
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe( const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(([kv, values]) => values.pipe( Effect.flatMap(kv => values.pipe(
Schema.parseJson().pipe( Schema.encode(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)), Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
Schema.encode,
), ),
Effect.flatMap(v => kv.set(key, v)), Effect.flatMap(v => kv.set(key, v)),
)), )),
Effect.provide(BrowserKeyValueStore.layerLocalStorage), Effect.provide(BrowserKeyValueStore.layerLocalStorage),
) )
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo)) const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo)) const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
const remove = (index: number) => Ref.update(todos, Chunk.remove(index)) const save = Effect.flatMap(todos, writeToLocalStorage)
// const moveUp = (index: number) => Effect.gen(function*() { // Sync changes with local storage
yield* Effect.forkScoped(Stream.runForEach(todos.changes, writeToLocalStorage))
// }) return { todos, load, save }
yield* readFromLocalStorage
return {
todos,
readFromLocalStorage,
saveToLocalStorage,
prepend,
replace,
remove,
}
})) }))
+15 -24
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>
+16 -19
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>
+13 -11
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>
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/extension-lazyref", "name": "@reffuse/extension-lazyref",
"version": "0.1.0", "version": "0.1.4",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -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.3" "reffuse": "^0.1.8"
} }
} }
+37 -8
View File
@@ -1,20 +1,49 @@
import * as LazyRef from "@typed/lazy-ref" import * as LazyRef from "@typed/lazy-ref"
import { Effect, Stream } from "effect" import { Effect, pipe, Stream } from "effect"
import * as React from "react" import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse" import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import { SetStateAction } from "reffuse/types"
export const LazyRefExtension = ReffuseExtension.make(() => ({ export const LazyRefExtension = ReffuseExtension.make(() => ({
useSubscribeLazyRefs<
const Refs extends readonly LazyRef.LazyRef<any>[],
R,
>(
this: ReffuseNamespace.ReffuseNamespace<R>,
...refs: Refs
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly LazyRef.LazyRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
this.useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
),
), refs)
return reactStateValue
},
useLazyRefState<A, E, R>( useLazyRefState<A, E, R>(
this: ReffuseHelpers.ReffuseHelpers<R>, this: ReffuseNamespace.ReffuseNamespace<R>,
ref: LazyRef.LazyRef<A, E, R>, ref: LazyRef.LazyRef<A, E, R>,
): [A, React.Dispatch<React.SetStateAction<A>>] { ): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true }) const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
const [reactStateValue, setReactStateValue] = React.useState(initialState) () => ref,
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() => this.useFork(() => Stream.runForEach(
setReactStateValue(v) Stream.changesWith(ref.changes, (x, y) => x === y),
)), [ref]) v => Effect.sync(() => setReactStateValue(v)),
), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) => const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
LazyRef.update(ref, prevState => LazyRef.update(ref, prevState =>
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/extension-query", "name": "@reffuse/extension-query",
"version": "0.1.0", "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.3" "reffuse": "^0.1.11"
} }
} }
+78 -117
View File
@@ -1,134 +1,95 @@
// import { BrowserStream } from "@effect/platform-browser" import * as AsyncData from "@typed/async-data"
// import * as AsyncData from "@typed/async-data" import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
// import { type Cause, Effect, Fiber, identity, Option, 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 MutationRunner<K extends readonly unknown[], A, E, R> { export interface MutationRunner<K extends readonly unknown[], A, E> {
// readonly mutation: (...args: K) => Effect.Effect<A, E, 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 forkMutate: (...key: K) => Effect.Effect<readonly [
// readonly forkMutate: Effect.Effect<Fiber.RuntimeFiber<void>> fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
// } state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
}
// export interface MakeProps<K extends readonly unknown[], A, E, R> { export const Tag = <const Id extends string>(id: Id) => <
// readonly mutation: (...args: K) => Effect.Effect<A, E, R> Self, K extends readonly unknown[], A, E = never,
// } >() => Effect.Tag(id)<Self, MutationRunner<K, A, E>>()
// export const make = <K extends readonly unknown[], A, E, R>(
// { key, query }: MakeProps<K, A, E, R>
// ): Effect.Effect<MutationRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
// const context = yield* Effect.context<R>()
// const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>()) export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
}
// const interrupt = fiberRef.pipe( export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
// Effect.flatMap(Option.match({ {
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( QueryClient,
// Effect.andThen(Fiber.interrupt(fiber)) mutation,
// ), }: MakeProps<K, A, FallbackA, E, HandledE, R>
// onNone: () => Effect.void, ): Effect.Effect<
// })) MutationRunner<K, A | FallbackA, Exclude<E, HandledE>>,
// ) never,
R | QueryClient.TagClassShape<FallbackA, HandledE>
> => Effect.gen(function*() {
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
const globalStateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
// const forkInterrupt = fiberRef.pipe( const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
// Effect.flatMap(Option.match({
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
// Effect.andThen(Fiber.interrupt(fiber).pipe(
// Effect.asVoid,
// Effect.forkDaemon,
// ))
// ),
// onNone: () => Effect.forkDaemon(Effect.void),
// }))
// )
// const forkFetch = interrupt.pipe( const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe(
// Effect.andThen( Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
// Ref.set(stateRef, AsyncData.loading()).pipe( Effect.andThen(mutation(key)),
// Effect.andThen(latestKeyRef), client.errorHandler.handle,
// Effect.flatMap(identity), Effect.matchCauseEffect({
// Effect.flatMap(key => query(key).pipe( onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
// Effect.matchCauseEffect({ onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)), }),
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)), )),
// })
// )),
// Effect.provide(context), Effect.provide(context),
// Effect.fork, Effect.provide(QueryProgress.QueryProgress.Default),
// ) )
// ),
// Effect.flatMap(fiber => const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
// Ref.set(fiberRef, Option.some(fiber)).pipe( queryStateTag,
// Effect.andThen(Fiber.join(fiber)), globalStateRef,
// Effect.andThen(Ref.set(fiberRef, Option.none())), value => Ref.set(globalStateRef, value),
// ) ))
// ),
// Effect.forkDaemon, const forkMutate = (...key: K) => Effect.all([
// ) Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>()),
Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>(),
]).pipe(
Effect.flatMap(([stateRef, stateQueue]) =>
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
Effect.andThen(run(key)),
Effect.scoped,
Effect.forkDaemon,
// const forkRefresh = interrupt.pipe( Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
// Effect.andThen(
// Ref.update(stateRef, previous => {
// if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
// return AsyncData.refreshing(previous)
// if (AsyncData.isRefreshing(previous))
// return AsyncData.refreshing(previous.previous)
// return AsyncData.loading()
// }).pipe(
// Effect.andThen(latestKeyRef),
// Effect.flatMap(identity),
// Effect.flatMap(key => query(key).pipe(
// Effect.matchCauseEffect({
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
// })
// )),
// Effect.provide(context), Effect.provide(QueryState.layer(
// Effect.fork, queryStateTag,
// ) stateRef,
// ), value => Queue.offer(stateQueue, value).pipe(
Effect.andThen(Ref.set(stateRef, value)),
Effect.andThen(Ref.set(globalStateRef, value)),
),
)),
)
)
)
// Effect.flatMap(fiber => return {
// Ref.set(fiberRef, Option.some(fiber)).pipe( context,
// Effect.andThen(Fiber.join(fiber)), stateRef: globalStateRef,
// Effect.andThen(Ref.set(fiberRef, Option.none())),
// )
// ),
// Effect.forkDaemon, mutate,
// ) forkMutate,
}
// const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe( })
// Effect.andThen(Stream.runForEach(key, latestKey =>
// Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
// Effect.andThen(forkFetch)
// )
// ))
// )
// const refreshOnWindowFocus = Stream.runForEach(
// BrowserStream.fromEventListenerWindow("focus"),
// () => forkRefresh,
// )
// return {
// query,
// latestKeyRef,
// stateRef,
// fiberRef,
// forkInterrupt,
// forkFetch,
// forkRefresh,
// fetchOnKeyChange,
// refreshOnWindowFocus,
// }
// })
@@ -0,0 +1,58 @@
import { Context, Effect, identity, Layer } from "effect"
import type { Mutable } from "effect/Types"
import * as QueryErrorHandler from "./QueryErrorHandler.js"
export interface QueryClient<FallbackA, HandledE> {
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
}
export interface MakeProps<FallbackA, HandledE> {
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
}
export const make = <FallbackA, HandledE>(
{ errorHandler }: MakeProps<FallbackA, HandledE>
): Effect.Effect<QueryClient<FallbackA, HandledE>> => Effect.Do.pipe(
Effect.let("errorHandler", () => errorHandler)
)
const id = "@reffuse/extension-query/QueryClient"
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
TagClassShape<FallbackA, HandledE>,
typeof id,
QueryClient<FallbackA, HandledE>
>
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
export interface ServiceProps<FallbackA = never, HandledE = never, E = never, R = never> {
readonly errorHandler?: Effect.Effect<QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, E, R>
}
export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
Self,
typeof id,
QueryClient<FallbackA, HandledE>
> {
readonly Default: Layer.Layer<Self, E, R>
}
export const Service = <Self>() => (
<FallbackA = never, HandledE = never, E = never, R = never>(
props?: ServiceProps<FallbackA, HandledE, E, R>
): ServiceResult<Self, FallbackA, HandledE, E, R> => {
const TagClass = Context.Tag(id)() as ServiceResult<Self, FallbackA, HandledE, E, R>
(TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
errorHandler => make({ errorHandler }),
))
return TagClass
}
)
@@ -0,0 +1,40 @@
import { Cause, Effect, PubSub, Stream } from "effect"
export interface QueryErrorHandler<FallbackA, HandledE> {
readonly errors: Stream.Stream<Cause.Cause<HandledE>>
readonly handle: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R>
}
export type Fallback<T> = T extends QueryErrorHandler<infer A, any> ? A : never
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
export const make = <HandledE = never>() => (
<FallbackA>(
f: (
self: Effect.Effect<never, HandledE>,
failure: (failure: HandledE) => Effect.Effect<never>,
defect: (defect: unknown) => Effect.Effect<never>,
) => Effect.Effect<FallbackA>
): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
const errors = Stream.fromPubSub(pubsub)
const handle = <A, E, R>(
self: Effect.Effect<A, E, R>
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
self as unknown as Effect.Effect<never, HandledE, never>,
(failure: HandledE) => Effect.andThen(
PubSub.publish(pubsub, Cause.fail(failure)),
Effect.failCause(Cause.empty),
),
(defect: unknown) => Effect.andThen(
PubSub.publish(pubsub, Cause.die(defect)),
Effect.failCause(Cause.empty),
),
)
return { errors, handle }
})
)
+42 -36
View File
@@ -1,55 +1,61 @@
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 { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import * as React from "react" import * as MutationRunner from "./MutationRunner.js"
import { ReffuseExtension, type ReffuseHelpers } from "reffuse" import * as QueryClient from "./QueryClient.js"
import type * as QueryProgress from "./QueryProgress.js"
import * as QueryRunner from "./QueryRunner.js" import * as QueryRunner from "./QueryRunner.js"
import type * as QueryService from "./QueryService.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> 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> { export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>> readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
) => Layer.Layer<Self>
} }
export const QueryExtension = ReffuseExtension.make(() => ({ export const QueryExtension = ReffuseExtension.make(() => ({
useQuery<K extends readonly unknown[], A, E, R>( useQuery<
this: ReffuseHelpers.ReffuseHelpers<R>, QK extends readonly unknown[],
props: UseQueryProps<K, A, E, R>, QA,
): UseQueryResult<K, A, E> { FallbackA,
QE,
HandledE,
QR extends R,
R,
>(
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseQueryProps<QK, QA, QE, QR>,
): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => QueryRunner.make({ const runner = this.useMemo(() => QueryRunner.make({
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(() => ({ useMutation<
latestKey: runner.latestKeyRef, QK extends readonly unknown[],
state: runner.stateRef, QA,
refresh: runner.forkRefresh, FallbackA,
QE,
layer: tag => Layer.succeed(tag, { HandledE,
latestKey: runner.latestKeyRef, QR extends R,
state: runner.stateRef, R,
refresh: runner.forkRefresh, >(
}), this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
}), [runner]) props: UseMutationProps<QK, QA, QE, QR>,
} ): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
return this.useMemo(() => MutationRunner.make({
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
mutation: props.mutation,
}), [])
},
})) }))
@@ -0,0 +1,37 @@
import * as AsyncData from "@typed/async-data"
import { Effect, flow, Layer, Match, Option } from "effect"
import { QueryState } from "./internal/index.js"
export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryProgress")<QueryProgress, {
readonly get: Effect.Effect<Option.Option<AsyncData.Progress>>
readonly update: (
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
) => Effect.Effect<void>
}>() {
static readonly Default: Layer.Layer<
QueryProgress,
never,
QueryState.QueryState<any, any>
> = Layer.effect(this, Effect.gen(function*() {
const state = yield* QueryState.makeTag()
const get = state.get.pipe(
Effect.map(flow(Match.value,
Match.tag("Loading", v => v.progress),
Match.tag("Refreshing", v => v.progress),
Match.orElse(() => Option.none()),
))
)
const update = (f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress) => get.pipe(
Effect.map(f),
Effect.flatMap(progress => state.update(previous =>
AsyncData.updateProgress(previous, progress)
)),
)
return { get, update }
}))
}
+161 -112
View File
@@ -1,144 +1,193 @@
import { BrowserStream } from "@effect/platform-browser" import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data" import * as AsyncData from "@typed/async-data"
import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect" 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, R> { export interface QueryRunner<K extends readonly unknown[], A, E> {
readonly query: (key: K) => Effect.Effect<A, E, R> readonly queryKey: Stream.Stream<K>
readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>> readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>> 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 interrupt: Effect.Effect<void>
readonly forkFetch: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>> readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void>>
readonly forkRefresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>> readonly forkFetch: (keyValue: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope> state: Stream.Stream<AsyncData.AsyncData<A, E>>,
readonly refreshOnWindowFocus: Effect.Effect<void> ]>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
} }
export interface MakeProps<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, 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 key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R> readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
} }
export const make = <K extends readonly unknown[], A, E, R>( export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
{ key, query }: MakeProps<K, A, E, R> {
): Effect.Effect<QueryRunner<K, A, E, R>, never, R> => Effect.gen(function*() { QueryClient,
const context = yield* Effect.context<R>() 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 latestKeyRef = yield* SubscriptionRef.make(Option.none<K>()) const latestKeyValueRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>()) const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>()) const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
Cause.NoSuchElementException
>>())
const interrupt = fiberRef.pipe( const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
Effect.flatMap(Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( const interrupt = Effect.flatMap(fiberRef, Option.match({
Effect.andThen(Fiber.interrupt(fiber)) onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
), Effect.andThen(Fiber.interrupt(fiber))
onNone: () => Effect.void, ),
})) 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 forkInterrupt = fiberRef.pipe( const forkFetch = (keyValue: K) => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(Option.match({ Effect.flatMap(stateQueue => queryStateTag.pipe(
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe( Effect.flatMap(state => interrupt.pipe(
Effect.andThen(Fiber.interrupt(fiber).pipe( Effect.andThen(
Effect.asVoid, Effect.addFinalizer(() => Effect.andThen(
Effect.forkDaemon, Ref.set(fiberRef, Option.none()),
)) Queue.shutdown(stateQueue),
), )).pipe(
onNone: () => Effect.forkDaemon(Effect.void), Effect.andThen(state.set(AsyncData.loading())),
})) Effect.andThen(run(keyValue)),
) Effect.scoped,
Effect.forkDaemon,
)
),
const forkFetch = interrupt.pipe( Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.andThen( Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
Ref.set(stateRef, AsyncData.loading()).pipe( )),
Effect.andThen(latestKeyRef),
Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe(
Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
})
)),
Effect.provide(context), Effect.provide(QueryState.layer(
Effect.fork, queryStateTag,
) stateRef,
), value => Effect.andThen(
Queue.offer(stateQueue, value),
Effect.flatMap(fiber => Ref.set(stateRef, value),
Ref.set(fiberRef, Option.some(fiber)).pipe( ),
Effect.andThen(Fiber.join(fiber)), )),
Effect.andThen(Ref.set(fiberRef, Option.none())),
)
),
Effect.forkDaemon,
)
const forkRefresh = interrupt.pipe(
Effect.andThen(
Ref.update(stateRef, previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous)
return AsyncData.loading()
}).pipe(
Effect.andThen(latestKeyRef),
Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe(
Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
})
)),
Effect.provide(context),
Effect.fork,
)
),
Effect.flatMap(fiber =>
Ref.set(fiberRef, Option.some(fiber)).pipe(
Effect.andThen(Fiber.join(fiber)),
Effect.andThen(Ref.set(fiberRef, Option.none())),
)
),
Effect.forkDaemon,
)
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
Effect.andThen(Stream.runForEach(key, latestKey =>
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
Effect.andThen(forkFetch)
)
)) ))
) )
const refreshOnWindowFocus = Stream.runForEach( const setInitialRefreshState = Effect.flatMap(queryStateTag, state => state.update(previous => {
BrowserStream.fromEventListenerWindow("focus"), if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
() => forkRefresh, 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 { return {
query, queryKey: key,
latestKeyValueRef,
latestKeyRef,
stateRef, stateRef,
fiberRef, fiberRef,
interrupt,
forkInterrupt, forkInterrupt,
forkFetch, forkFetch,
forkRefresh, forkRefresh,
fetchOnKeyChange,
refreshOnWindowFocus,
} }
}) })
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))
})
@@ -1,32 +0,0 @@
import type * as AsyncData from "@typed/async-data"
import { type Cause, Effect, type Fiber, type Option, type SubscriptionRef } from "effect"
export interface QueryService<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
// export interface LayerProps<A, E, R> {
// readonly query: Effect.Effect<A, E, R>
// }
// export const layer = <Self, Id extends string, A, E, R>(
// tag: Context.TagClass<Self, Id, QueryService<A, E>>,
// props: LayerProps<A, E, R>,
// ): Layer.Layer<Self, never, R> => Layer.effect(tag, Effect.gen(function*() {
// const runner = yield* QueryRunner.make({
// query: props.query
// })
// return {
// state: runner.stateRef,
// refresh: runner.forkRefresh,
// }
// }))
+4 -1
View File
@@ -1,3 +1,6 @@
export * as MutationRunner from "./MutationRunner.js"
export * as QueryClient from "./QueryClient.js"
export * as QueryErrorHandler from "./QueryErrorHandler.js"
export * from "./QueryExtension.js" export * from "./QueryExtension.js"
export * as QueryProgress from "./QueryProgress.js"
export * as QueryRunner from "./QueryRunner.js" export * as QueryRunner from "./QueryRunner.js"
export * as QueryService from "./QueryService.js"
@@ -0,0 +1,24 @@
import type * as AsyncData from "@typed/async-data"
import { Context, Effect, Layer } from "effect"
export interface QueryState<A, E> {
readonly get: Effect.Effect<AsyncData.AsyncData<A, E>>
readonly set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>
readonly update: (f: (previous: AsyncData.AsyncData<A, E>) => AsyncData.AsyncData<A, E>) => Effect.Effect<void>
}
export const makeTag = <A, E>(): Context.Tag<QueryState<A, E>, QueryState<A, E>> => Context.GenericTag("@reffuse/query-extension/QueryState")
export const layer = <A, E>(
tag: Context.Tag<QueryState<A, E>, QueryState<A, E>>,
get: Effect.Effect<AsyncData.AsyncData<A, E>>,
set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>,
): Layer.Layer<QueryState<A, E>> => Layer.succeed(tag, {
get,
set,
update: f => get.pipe(
Effect.map(f),
Effect.flatMap(set),
),
})
@@ -0,0 +1 @@
export * as QueryState from "./QueryState.js"
+6 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "reffuse", "name": "reffuse",
"version": "0.1.3", "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"
} }
} }
+13 -12
View File
@@ -1,42 +1,42 @@
import type * as ReffuseContext from "./ReffuseContext.js" import type * as ReffuseContext from "./ReffuseContext.js"
import type * as ReffuseExtension from "./ReffuseExtension.js" import type * as ReffuseExtension from "./ReffuseExtension.js"
import * as ReffuseHelpers from "./ReffuseHelpers.js" import * as ReffuseNamespace from "./ReffuseNamespace.js"
import type { Merge, StaticType } from "./types.js" import type { Merge, StaticType } from "./utils.js"
export class Reffuse extends ReffuseHelpers.make() {} export class Reffuse extends ReffuseNamespace.makeClass() {}
export const withContexts = <R2 extends Array<unknown>>( export const withContexts = <R2 extends Array<unknown>>(
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }] ...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
) => ) => (
< <
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R1>, BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R1>,
R1 R1
>( >(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R1> self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R1>
): ( ): (
{ {
new(): Merge< new(): Merge<
InstanceType<BaseClass>, InstanceType<BaseClass>,
{ constructor: ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]> } { constructor: ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]> }
> >
} & } &
Merge< Merge<
StaticType<BaseClass>, StaticType<BaseClass>,
StaticType<ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]>> StaticType<ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]>>
> >
) => class extends self { ) => class extends self {
static readonly contexts = [...self.contexts, ...contexts] static readonly contexts = [...self.contexts, ...contexts]
} as any } as any
)
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) => (
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) =>
< <
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R>, BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R>,
R R
>( >(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R> self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R>
): ( ): (
{ new(): Merge<InstanceType<BaseClass>, A> } & { new(): Merge<InstanceType<BaseClass>, A> } &
StaticType<BaseClass> StaticType<BaseClass>
@@ -45,3 +45,4 @@ export const withExtension = <A extends object>(extension: ReffuseExtension.Reff
Object.assign(class_.prototype, extension()) Object.assign(class_.prototype, extension())
return class_ as any return class_ as any
} }
)
+178
View File
@@ -0,0 +1,178 @@
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider = makeProvider(this.Context)
readonly AsyncProvider = makeAsyncProvider(this.Context)
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly children?: React.ReactNode
}>
const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> => {
return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
const makeScope = React.useMemo(() => props.scope
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
[props.scope])
const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
)
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
Effect.bind("scope", () => makeScope),
Effect.bind("context", ({ scope }) => makeContext(scope)),
Effect.map(({ isInitialRun, scope, context }) => [isInitialRun, scope, context] as const),
runSync,
), [])
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => closeScope(initialScope)
)
),
onFalse: () => Effect.Do.pipe(
Effect.bind("scope", () => makeScope),
Effect.bind("context", ({ scope }) => makeContext(scope)),
Effect.tap(({ context }) =>
Effect.sync(() => setValue(context))
),
Effect.map(({ scope }) =>
() => closeScope(scope)
),
),
}),
runSync,
), [makeScope, runSync, runFork])
return React.createElement(Context, { ...props, value })
}
}
export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> => {
function ReffuseContextAsyncReactProviderInner({ promise, children }: {
readonly promise: Promise<Context.Context<R>>
readonly children?: React.ReactNode
}) {
return React.createElement(Context, {
value: React.use(promise),
children,
})
}
return function ReffuseContextAsyncReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
const [promise, setPromise] = React.useState(Promise.withResolvers<Context.Context<R>>().promise)
React.useEffect(() => {
const { promise, resolve, reject } = Promise.withResolvers<Context.Context<R>>()
setPromise(promise)
const scope = runSync(props.scope
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
)
Effect.context<R>().pipe(
Effect.match({
onSuccess: resolve,
onFailure: reject,
}),
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
effect => runFork(effect, { ...props, scope }),
)
return () => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
}, [props.layer, runSync, runFork])
return React.createElement(React.Suspense, {
children: React.createElement(ReffuseContextAsyncReactProviderInner, { ...props, promise }),
fallback: props.fallback,
})
}
}
export const make = <R = never>() => new ReffuseContext<R>()
export const useMergeAll = <T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Context.Context<T[number]> => {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export const useMergeAllLayers = <T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Layer.Layer<T[number]> => {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Array.isNonEmptyArray(values)
? Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
)
: Layer.empty as Layer.Layer<T[number]>,
values)
}
-111
View File
@@ -1,111 +0,0 @@
import { Array, Context, Effect, Layer, Runtime } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider = makeProvider(this.Context)
readonly AsyncProvider = makeAsyncProvider(this.Context)
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly children?: React.ReactNode
}>
function makeProvider<R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> {
return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const value = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runSync(runtime),
), [props.layer, runtime])
return (
<Context
{...props}
value={value}
/>
)
}
}
export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
function makeAsyncProvider<R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> {
function Inner({ promise, children }: {
readonly promise: Promise<Context.Context<R>>
readonly children?: React.ReactNode
}) {
const value = React.use(promise)
return (
<Context
value={value}
children={children}
/>
)
}
return function ReffuseContextAsyncReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const promise = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runPromise(runtime),
), [props.layer, runtime])
return (
<React.Suspense fallback={props.fallback}>
<Inner
{...props}
promise={promise}
/>
</React.Suspense>
)
}
}
export function make<R = never>() {
return new ReffuseContext<R>()
}
export function useMergeAll<T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Context.Context<T[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export function useMergeAllLayers<T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Layer.Layer<T[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Array.isNonEmptyArray(values)
? Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
)
: Layer.empty as Layer.Layer<T[number]>,
values)
}
@@ -1,8 +1,8 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, 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,21 +14,38 @@ export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
} }
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
readonly scope?: Scope.Scope
readonly finalizerExecutionMode?: "sync" | "fork"
}
export abstract class ReffuseHelpers<R> { export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
declare ["constructor"]: ReffuseHelpersClass<R> [K in keyof T]: Effect.Effect.Success<T[K]>
}
useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> { export abstract class ReffuseNamespace<R> {
declare ["constructor"]: ReffuseNamespaceClass<R>
constructor() {
this.SubRefFromGetSet = this.SubRefFromGetSet.bind(this as any) as any
this.SubRefFromPath = this.SubRefFromPath.bind(this as any) as any
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
this.RefState = this.RefState.bind(this as any) as any
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
}
useContext<R>(this: ReffuseNamespace<R>): Context.Context<R> {
return ReffuseContext.useMergeAll(...this.constructor.contexts) return ReffuseContext.useMergeAll(...this.constructor.contexts)
} }
useLayer<R>(this: ReffuseHelpers<R>): Layer.Layer<R> { useLayer<R>(this: ReffuseNamespace<R>): Layer.Layer<R> {
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts) return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
} }
useRunSync<R>(this: ReffuseHelpers<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A { useRunSync<R>(this: ReffuseNamespace<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
const runtime = ReffuseRuntime.useRuntime() const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext() const context = this.useContext()
@@ -38,7 +55,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunPromise<R>(this: ReffuseHelpers<R>): <A, E>( useRunPromise<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal }, options?: { readonly signal?: AbortSignal },
) => Promise<A> { ) => Promise<A> {
@@ -51,7 +68,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunFork<R>(this: ReffuseHelpers<R>): <A, E>( useRunFork<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions, options?: Runtime.RunForkOptions,
) => Fiber.RuntimeFiber<A, E> { ) => Fiber.RuntimeFiber<A, E> {
@@ -64,7 +81,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunCallback<R>(this: ReffuseHelpers<R>): <A, E>( useRunCallback<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: Runtime.RunCallbackOptions<A, E>, options?: Runtime.RunCallbackOptions<A, E>,
) => Runtime.Cancel<A, E> { ) => Runtime.Cancel<A, E> {
@@ -77,6 +94,56 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context]) ), [runtime, context])
} }
useScope<R>(
this: ReffuseNamespace<R>,
deps: React.DependencyList = [],
options?: UseScopeOptions,
): readonly [scope: Scope.Scope, layer: Layer.Layer<Scope.Scope>] {
const runSync = this.useRunSync()
const runFork = this.useRunFork()
const makeScope = React.useMemo(() => options?.scope
? Scope.fork(options.scope, options.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
[options?.scope])
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
effect => Match.value(options?.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
const [isInitialRun, initialScope] = React.useMemo(() => runSync(
Effect.all([Ref.make(true), makeScope])
), [makeScope])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => closeScope(initialScope),
),
onFalse: () => makeScope.pipe(
Effect.tap(v => Effect.sync(() => setScope(v))),
Effect.map(v => () => closeScope(v)),
),
}),
runSync,
), [
makeScope,
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync, runFork],
...deps,
])
return React.useMemo(() => [scope, Layer.succeed(Scope.Scope, scope)] as const, [scope])
}
/** /**
* Reffuse equivalent to `React.useMemo`. * Reffuse equivalent to `React.useMemo`.
* *
@@ -87,7 +154,7 @@ export abstract class ReffuseHelpers<R> {
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`. * You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*/ */
useMemo<A, E, R>( useMemo<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R>, effect: () => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions, options?: RenderOptions,
@@ -100,56 +167,6 @@ export abstract class ReffuseHelpers<R> {
]) ])
} }
useMemoScoped<A, E, R>(
this: ReffuseHelpers<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): A {
const runSync = this.useRunSync()
// Calculate an initial version of the value so that it can be accessed during the first render
const [initialScope, initialValue] = React.useMemo(() => Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.flatMap(scope => effect().pipe(
Effect.provideService(Scope.Scope, scope),
Effect.map(value => [scope, value] as const),
)),
runSync,
), [])
// Keep track of the state of the initial scope
const initialScopeClosed = React.useRef(false)
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => {
const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe(
Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })),
Effect.when(() => !initialScopeClosed.current),
)
const [scope, value] = closeInitialScopeIfNeeded.pipe(
Effect.andThen(Scope.make(options?.finalizerExecutionStrategy).pipe(
Effect.flatMap(scope => effect().pipe(
Effect.provideService(Scope.Scope, scope),
Effect.map(value => [scope, value] as const),
))
)),
runSync,
)
setValue(value)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
return value
}
/** /**
* Reffuse equivalent to `React.useEffect`. * Reffuse equivalent to `React.useEffect`.
* *
@@ -177,7 +194,7 @@ export abstract class ReffuseHelpers<R> {
* ``` * ```
*/ */
useEffect<A, E, R>( useEffect<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
@@ -225,7 +242,7 @@ export abstract class ReffuseHelpers<R> {
* ``` * ```
*/ */
useLayoutEffect<A, E, R>( useLayoutEffect<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
@@ -273,7 +290,7 @@ export abstract class ReffuseHelpers<R> {
* ``` * ```
*/ */
useFork<A, E, R>( useFork<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions, options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -296,7 +313,7 @@ export abstract class ReffuseHelpers<R> {
} }
usePromise<A, E, R>( usePromise<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions, options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -343,7 +360,7 @@ export abstract class ReffuseHelpers<R> {
} }
useCallbackSync<Args extends unknown[], A, E, R>( useCallbackSync<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>, callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions, options?: RenderOptions,
@@ -357,7 +374,7 @@ export abstract class ReffuseHelpers<R> {
} }
useCallbackPromise<Args extends unknown[], A, E, R>( useCallbackPromise<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>, callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: { readonly signal?: AbortSignal } & RenderOptions, options?: { readonly signal?: AbortSignal } & RenderOptions,
@@ -370,17 +387,73 @@ export abstract class ReffuseHelpers<R> {
]) ])
} }
useRef<A, R>( useRef<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
value: A, initialValue: () => Effect.Effect<A, E, R>,
): SubscriptionRef.SubscriptionRef<A> { ): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo( return this.useMemo(
() => SubscriptionRef.make(value), () => Effect.flatMap(initialValue(), SubscriptionRef.make),
[], [],
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes { doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
) )
} }
useRefFromReactiveValue<A, R>(
this: ReffuseNamespace<R>,
value: A,
): SubscriptionRef.SubscriptionRef<A> {
const ref = this.useRef(() => Effect.succeed(value))
this.useEffect(() => Ref.set(ref, value), [value], { doNotReExecuteOnRuntimeOrContextChange: true })
return ref
}
useSubRefFromGetSet<A, B, R>(
this: ReffuseNamespace<R>,
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef.SubscriptionSubRef<A, B> {
return React.useMemo(
() => SubscriptionSubRef.makeFromGetSet(parent, getter, setter),
[parent],
)
}
useSubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> {
return React.useMemo(
() => SubscriptionSubRef.makeFromPath(parent, path),
[parent, ...path],
)
}
useSubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
>(
this: ReffuseNamespace<R>,
...refs: Refs
): RefsA<Refs> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) as RefsA<Refs>)
this.useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
),
), refs)
return reactStateValue
}
/** /**
* Binds the state of a `SubscriptionRef` to the state of the React component. * Binds the state of a `SubscriptionRef` to the state of the React component.
* *
@@ -389,15 +462,19 @@ export abstract class ReffuseHelpers<R> {
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render. * Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
*/ */
useRefState<A, R>( useRefState<A, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
ref: SubscriptionRef.SubscriptionRef<A>, ref: SubscriptionRef.SubscriptionRef<A>,
): [A, React.Dispatch<React.SetStateAction<A>>] { ): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true }) const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
const [reactStateValue, setReactStateValue] = React.useState(initialState) () => ref,
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() => this.useFork(() => Stream.runForEach(
setReactStateValue(v) Stream.changesWith(ref.changes, (x, y) => x === y),
)), [ref]) v => Effect.sync(() => setReactStateValue(v)),
), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) => const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
Ref.update(ref, prevState => Ref.update(ref, prevState =>
@@ -408,39 +485,162 @@ export abstract class ReffuseHelpers<R> {
return [reactStateValue, setValue] return [reactStateValue, setValue]
} }
useStreamFromValues<const A extends React.DependencyList, R>( useStreamFromReactiveValues<const A extends React.DependencyList, R>(
this: ReffuseHelpers<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, E, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
): Option.Option<A>
useSubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue: () => Effect.Effect<A, IE, R>,
): Option.Some<A>
useSubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue?: () => Effect.Effect<A, IE, R>,
): Option.Option<A> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => initialValue
? Effect.map(initialValue(), Option.some)
: Effect.succeed(Option.none()),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach(
Stream.changesWith(stream, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream])
return reactStateValue
}
SubRefFromGetSet<A, B, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<A, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRefFromGetSet(props.parent, props.getter, props.setter))
}
SubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly path: P,
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRefFromPath(props.parent, props.path))
}
SubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
>(
this: ReffuseNamespace<R>,
props: {
readonly refs: Refs
readonly children: (...args: RefsA<Refs>) => React.ReactNode
},
): React.ReactNode {
return props.children(...this.useSubscribeRefs(...props.refs))
}
RefState<A, R>(
this: ReffuseNamespace<R>,
props: {
readonly ref: SubscriptionRef.SubscriptionRef<A>
readonly children: (state: [A, React.Dispatch<React.SetStateAction<A>>]) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useRefState(props.ref))
}
SubscribeStream<A, E, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly children: (latestValue: Option.Option<A>) => React.ReactNode
},
): React.ReactNode
SubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue: () => Effect.Effect<A, IE, R>
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
},
): React.ReactNode
SubscribeStream<A, E, IE, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue?: () => Effect.Effect<A, IE, R>
readonly children: (latestValue: Option.Some<A>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
}
} }
export interface ReffuseHelpers<R> extends Pipeable.Pipeable {} export interface ReffuseNamespace<R> extends Pipeable.Pipeable {}
ReffuseHelpers.prototype.pipe = function pipe() { ReffuseNamespace.prototype.pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments) return Pipeable.pipeArguments(this, arguments)
} };
export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable { export interface ReffuseNamespaceClass<R> extends Pipeable.Pipeable {
new(): ReffuseHelpers<R> new(): ReffuseNamespace<R>
make<Self>(this: new () => Self): Self
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[] readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
} }
(ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() { (ReffuseNamespace as ReffuseNamespaceClass<any>).make = function make() {
return new this()
};
(ReffuseNamespace as ReffuseNamespaceClass<any>).pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments) return Pipeable.pipeArguments(this, arguments)
} };
export const make = (): ReffuseHelpersClass<never> => export const makeClass = (): ReffuseNamespaceClass<never> => (
class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) { class extends (ReffuseNamespace<never> as ReffuseNamespaceClass<never>) {
static readonly contexts = [] static readonly contexts = []
} }
)
+16
View File
@@ -0,0 +1,16 @@
import { Runtime } from "effect"
import * as React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = function ReffuseRuntimeReactProvider(props: {
readonly children?: React.ReactNode
}) {
return React.createElement(Context, {
...props,
value: Runtime.defaultRuntime,
})
}
export const useRuntime = () => React.useContext(Context)
-15
View File
@@ -1,15 +0,0 @@
import { Runtime } from "effect"
import * as React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = (props: { readonly children?: React.ReactNode }) => (
<Context
{...props}
value={Runtime.defaultRuntime}
/>
)
Provider.displayName = "ReffuseRuntimeReactProvider"
export const useRuntime = () => React.useContext(Context)
+1 -2
View File
@@ -1,6 +1,5 @@
export * as Reffuse from "./Reffuse.js" export * as Reffuse from "./Reffuse.js"
export * as ReffuseContext from "./ReffuseContext.js" export * as ReffuseContext from "./ReffuseContext.js"
export * as ReffuseExtension from "./ReffuseExtension.js" export * as ReffuseExtension from "./ReffuseExtension.js"
export * as ReffuseHelpers from "./ReffuseHelpers.js" export * as ReffuseNamespace from "./ReffuseNamespace.js"
export * as ReffuseRuntime from "./ReffuseRuntime.js" export * as ReffuseRuntime from "./ReffuseRuntime.js"
export * as SetStateAction from "./SetStateAction.js"
@@ -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()
})
@@ -0,0 +1,100 @@
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
import * as PropertyPath from "./PropertyPath.js"
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
readonly parent: SubscriptionRef.SubscriptionRef<B>
readonly [Unify.typeSymbol]?: unknown
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
}
export declare namespace SubscriptionSubRef {
export interface Variance<in out A, in out B> {
readonly [SubscriptionSubRefTypeId]: {
readonly _A: Types.Invariant<A>
readonly _B: Types.Invariant<B>
}
}
}
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
}
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
SubscriptionRef?: true
}
const refVariance = { _A: (_: any) => _ }
const synchronizedRefVariance = { _A: (_: any) => _ }
const subscriptionRefVariance = { _A: (_: any) => _ }
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
readonly [Ref.RefTypeId] = refVariance
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
readonly get: Effect.Effect<A>
constructor(
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
) {
super()
this.get = Effect.map(Ref.get(this.parent), this.getter)
}
commit() {
return this.get
}
get changes(): Stream.Stream<A> {
return this.get.pipe(
Effect.map(a => this.parent.changes.pipe(
Stream.map(this.getter),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrap,
)
}
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
return this.modifyEffect(a => Effect.succeed(f(a)))
}
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
return Effect.Do.pipe(
Effect.bind("b", () => Ref.get(this.parent)),
Effect.bind("ca", ({ b }) => f(this.getter(b))),
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
Effect.map(({ ca: [c] }) => c),
)
}
}
export const makeFromGetSet = <A, B>(
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
parent,
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
)
+3
View File
@@ -0,0 +1,3 @@
export * as PropertyPath from "./PropertyPath.js"
export * as SetStateAction from "./SetStateAction.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
@@ -8,14 +8,4 @@ export type CommonKeys<A, B> = Extract<keyof A, keyof B>
*/ */
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype"> export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
export type Extend<Super, Self> =
Extendable<Super, Self> extends true
? Omit<Super, CommonKeys<Self, Super>> & Self
: never
export type Extendable<Super, Self> =
Pick<Self, CommonKeys<Self, Super>> extends Pick<Super, CommonKeys<Self, Super>>
? true
: false
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self
+7 -1
View File
@@ -26,7 +26,13 @@
// Build // Build
"outDir": "./dist", "outDir": "./dist",
"declaration": true "declaration": true,
"plugins": [
{
"name": "@effect/language-service"
}
]
}, },
"include": ["./src"] "include": ["./src"]