185 Commits

Author SHA1 Message Date
Julien Valverdé 2a78232ec7 Hooks
Lint / lint (push) Successful in 14s
2025-07-01 20:12:17 +02:00
Julien Valverdé 19194d6677 Hook
Lint / lint (push) Failing after 11s
2025-07-01 18:13:03 +02:00
Julien Valverdé 40871b793d Tests
Lint / lint (push) Successful in 14s
2025-07-01 16:54:50 +02:00
Julien Valverdé f079b90f28 Tests
Lint / lint (push) Successful in 14s
2025-07-01 16:48:53 +02:00
Julien Valverdé 28b6e9276e Fix
Lint / lint (push) Successful in 15s
2025-07-01 16:44:28 +02:00
Julien Valverdé 8025ec4a22 Fix
Lint / lint (push) Successful in 15s
2025-07-01 16:31:30 +02:00
Julien Valverdé 02ee2c10cc Fix
Lint / lint (push) Successful in 14s
2025-07-01 16:16:29 +02:00
Julien Valverdé bb1a71f63b Scope refactoring
Lint / lint (push) Successful in 15s
2025-07-01 15:59:58 +02:00
Julien Valverdé a9448f55cf Fix
Lint / lint (push) Successful in 13s
2025-07-01 13:32:25 +02:00
Julien Valverdé c0f3073d20 Hook work
Lint / lint (push) Successful in 15s
2025-07-01 13:30:50 +02:00
Julien Valverdé 8cfe186574 Fix
Lint / lint (push) Successful in 14s
2025-07-01 00:46:59 +02:00
Julien Valverdé 625cecda27 Fix
Lint / lint (push) Successful in 14s
2025-07-01 00:29:42 +02:00
Julien Valverdé 7cc0a68170 useMemoLayer
Lint / lint (push) Successful in 15s
2025-07-01 00:23:45 +02:00
Julien Valverdé 8be1295e2f Layer tests
Lint / lint (push) Successful in 15s
2025-07-01 00:11:34 +02:00
Julien Valverdé a781be8f24 Working ref
Lint / lint (push) Successful in 15s
2025-06-30 22:49:30 +02:00
Julien Valverdé 4913f5cc35 Tests
Lint / lint (push) Successful in 14s
2025-06-30 22:07:38 +02:00
Julien Valverdé 2a37f843ca AsyncProvider
Lint / lint (push) Successful in 15s
2025-06-30 22:04:03 +02:00
Julien Valverdé 78a3735038 Refactoring
Lint / lint (push) Successful in 16s
2025-06-30 21:44:29 +02:00
Julien Valverdé 37d9400ada Component displayName
Lint / lint (push) Successful in 14s
2025-06-30 00:37:16 +02:00
Julien Valverdé 2ef47bed70 Work
Lint / lint (push) Successful in 14s
2025-06-29 23:00:33 +02:00
Julien Valverdé 2b78d4dc49 Cleanup
Lint / lint (push) Successful in 15s
2025-06-29 19:35:43 +02:00
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
71 changed files with 2126 additions and 1094 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
+365 -207
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@reffuse/monorepo",
"packageManager": "bun@1.2.9",
"packageManager": "bun@1.2.13",
"private": true,
"workspaces": [
"./packages/*"
@@ -15,9 +15,9 @@
"clean:node": "rm -rf node_modules"
},
"devDependencies": {
"npm-check-updates": "^17.1.18",
"npm-check-updates": "^18.0.1",
"npm-sort": "^0.0.4",
"turbo": "^2.5.0",
"turbo": "^2.5.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+
@@ -1,5 +1,5 @@
{
"name": "@reffuse/extension-form",
"name": "effect-components",
"version": "0.1.0",
"type": "module",
"files": [
@@ -16,6 +16,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
@@ -29,12 +33,12 @@
"clean:dist": "rm -rf dist",
"clean:node": "rm -rf node_modules"
},
"devDependencies": {
"reffuse": "workspace:*"
},
"peerDependencies": {
"effect": "^3.13.0",
"react": "^19.0.0",
"reffuse": "^0.1.7"
"@types/react": "^19.0.0",
"effect": "^3.15.0",
"react": "^19.0.0"
},
"devDependencies": {
"@effect/language-service": "^0.23.3"
}
}
@@ -0,0 +1,72 @@
import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
import type { Mutable } from "effect/Types"
import * as React from "react"
import * as ReactHook from "./ReactHook.js"
export interface ReactComponent<E, R, P> {
(props: P): Effect.Effect<React.ReactNode, E, R>
readonly displayName?: string
}
export const nonReactiveTags = [Tracer.ParentSpan] as const
export const withDisplayName: {
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
self: C,
displayName: string,
): C => {
(self as Mutable<C>).displayName = displayName
return self
})
export const useFC: {
<E, R, P extends {} = {}>(
self: ReactComponent<E, R, P>,
options?: ReactHook.ScopeOptions,
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R, P extends {}>(
self: ReactComponent<E, R, P>,
options?: ReactHook.ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
return React.useMemo(() => function ScopeProvider(props: P) {
const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
const FC = React.useMemo(() => {
const f = (props: P) => Runtime.runSync(runtime)(
Effect.provideService(self(props), Scope.Scope, scope)
)
if (self.displayName) f.displayName = self.displayName
return f
}, [scope])
return React.createElement(FC, props)
}, Array.from(
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
))
})
export const use: {
<E, R, P extends {} = {}>(
self: ReactComponent<E, R, P>,
fn: (Component: React.FC<P>) => React.ReactNode,
options?: ReactHook.ScopeOptions,
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function*(self, fn, options) {
return fn(yield* useFC(self, options))
})
export const withRuntime: {
<E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
<E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
} = Function.dual(2, <E, R, P extends {}>(
self: ReactComponent<E, R, P>,
context: React.Context<Runtime.Runtime<R>>,
): React.FC<P> => function WithRuntime(props) {
const runtime = React.useContext(context)
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
})
+317
View File
@@ -0,0 +1,317 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import { SetStateAction } from "./types/index.js"
export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
}
export const useScope: {
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
const runtime = yield* Effect.runtime()
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
Effect.all([Ref.make(true), makeScope(options)])
), [])
const [scope, setScope] = React.useState(initialScope)
React.useEffect(() => Runtime.runSync(runtime)(
Effect.if(isInitialRun, {
onTrue: () => Effect.as(
Ref.set(isInitialRun, false),
() => closeScope(scope, runtime, options),
),
onFalse: () => makeScope(options).pipe(
Effect.tap(scope => Effect.sync(() => setScope(scope))),
Effect.map(scope => () => closeScope(scope, runtime, options)),
),
})
), [])
return scope
})
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
const closeScope = (
scope: Scope.CloseableScope,
runtime: Runtime.Runtime<never>,
options?: ScopeOptions,
) => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}
export const useMemo: {
<A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime()
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
})
export const useOnce: {
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
} = Effect.fnUntraced(function* <A, E, R>(
factory: () => Effect.Effect<A, E, R>
) {
return yield* useMemo(factory, [])
})
export const useMemoLayer: {
<ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>
): Effect.Effect<Context.Context<ROut>, E, RIn>
} = Effect.fnUntraced(function* <ROut, E, RIn>(
layer: Layer.Layer<ROut, E, RIn>
) {
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
})
export const useCallbackSync: {
<Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<(...args: Args) => A, never, R>
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
})
export const useCallbackPromise: {
<Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList,
) {
const runtime = yield* Effect.runtime<R>()
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
})
export const useEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => {
const { scope, exit } = Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Runtime.runSync(runtime),
)
return () => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, exit))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, exit))
break
}
}
}, deps)
})
export const useLayoutEffect: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useLayoutEffect(() => {
const { scope, exit } = Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
Runtime.runSync(runtime),
)
return () => {
switch (options?.finalizerExecutionMode ?? "sync") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, exit))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, exit))
break
}
}
}, deps)
})
export const useFork: {
<E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & ScopeOptions,
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
} = Effect.fnUntraced(function* <E, R>(
effect: () => Effect.Effect<void, E, R>,
deps?: React.DependencyList,
options?: Runtime.RunForkOptions & ScopeOptions,
) {
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
React.useEffect(() => {
const scope = Runtime.runSync(runtime)(options?.scope
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(options?.finalizerExecutionStrategy)
)
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
return () => {
switch (options?.finalizerExecutionMode ?? "fork") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
break
}
}
}, deps)
})
export const useRefFromReactiveValue: {
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
} = Effect.fnUntraced(function*(value) {
const ref = yield* useOnce(() => SubscriptionRef.make(value))
yield* useEffect(() => Ref.set(ref, value), [value])
return ref
})
export const useSubscribeRefs: {
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
...refs: Refs
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
...refs: Refs
) {
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
))
yield* useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v))
),
), refs)
return reactStateValue as any
})
export const useRefState: {
<A>(
ref: SubscriptionRef.SubscriptionRef<A>
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
yield* useFork(() => Stream.runForEach(
Stream.changesWith(ref.changes, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(v)),
), [ref])
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
Ref.update(ref, prevState =>
SetStateAction.value(setStateAction, prevState)
),
[ref])
return [reactStateValue, setValue]
})
export const useStreamFromReactiveValues: {
<const A extends React.DependencyList>(
values: A
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
Effect.bind("latest", () => Ref.make(values)),
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
Effect.flatMap(a => Effect.map(
Stream.fromPubSub(pubsub, { scoped: true }),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrapScoped,
)),
))
yield* useEffect(() => Ref.set(latest, values).pipe(
Effect.andThen(PubSub.publish(pubsub, values)),
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
), values)
return stream
})
export const useSubscribeStream: {
<A, E, R>(
stream: Stream.Stream<A, E, R>
): Effect.Effect<Option.Option<A>, never, R>
<A extends NonNullable<unknown>, E, R>(
stream: Stream.Stream<A, E, R>,
initialValue: A,
): Effect.Effect<Option.Some<A>, never, R>
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
stream: Stream.Stream<A, E, R>,
initialValue?: A,
) {
const [reactStateValue, setReactStateValue] = React.useState(
React.useMemo(() => initialValue
? Option.some(initialValue)
: Option.none(),
[])
)
yield* useFork(() => Stream.runForEach(
Stream.changesWith(stream, (x, y) => x === y),
v => Effect.sync(() => setReactStateValue(Option.some(v))),
), [stream])
return reactStateValue as Option.Some<A>
})
@@ -0,0 +1,47 @@
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
import * as React from "react"
export interface ReactManagedRuntime<R, ER> {
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
readonly context: React.Context<Runtime.Runtime<R>>
}
export const make = <R, ER>(
layer: Layer.Layer<R, ER>,
memoMap?: Layer.MemoMap,
): ReactManagedRuntime<R, ER> => ({
runtime: ManagedRuntime.make(layer, memoMap),
context: React.createContext<Runtime.Runtime<R>>(null!),
})
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
readonly runtime: ReactManagedRuntime<R, ER>
readonly children?: React.ReactNode
}
export function AsyncProvider<R, ER>(
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
): React.ReactNode {
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
return React.createElement(
React.Suspense,
suspenseProps,
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
)
}
interface AsyncProviderInnerProps<R, ER> {
readonly runtime: ReactManagedRuntime<R, ER>
readonly promise: Promise<Runtime.Runtime<R>>
readonly children?: React.ReactNode
}
function AsyncProviderInner<R, ER>(
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
): React.ReactNode {
const value = React.use(promise)
return React.createElement(runtime.context, { value }, children)
}
+3
View File
@@ -0,0 +1,3 @@
export * as ReactComponent from "./ReactComponent.js"
export * as ReactHook from "./ReactHook.js"
export * as ReactManagedRuntime from "./ReactManagedRuntime.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)),
)
@@ -0,0 +1,3 @@
export * as PropertyPath from "./PropertyPath.js"
export * as SetStateAction from "./SetStateAction.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
+19 -19
View File
@@ -11,41 +11,41 @@
"preview": "vite preview"
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@tanstack/react-router": "^1.115.3",
"@tanstack/react-router-devtools": "^1.115.3",
"@tanstack/router-plugin": "^1.115.3",
"@eslint/js": "^9.26.0",
"@tanstack/react-router": "^1.120.3",
"@tanstack/react-router-devtools": "^1.120.3",
"@tanstack/router-plugin": "^1.120.3",
"@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.1.1",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.24.0",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript-eslint": "^8.29.1",
"vite": "^6.2.6"
"typescript-eslint": "^8.32.1",
"vite": "^6.3.5"
},
"dependencies": {
"@effect/platform": "^0.80.8",
"@effect/platform-browser": "^0.59.8",
"@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.62.1",
"@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3",
"effect": "^3.14.8",
"lucide-react": "^0.487.0",
"effect": "^3.15.1",
"lucide-react": "^0.510.0",
"mobx": "^6.13.7",
"reffuse": "workspace:*"
},
"overrides": {
"effect": "^3.14.8",
"@effect/platform": "^0.80.8",
"@effect/platform-browser": "^0.59.8",
"effect": "^3.15.1",
"@effect/platform": "^0.82.1",
"@effect/platform-browser": "^0.62.1",
"@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1"
}
+2 -2
View File
@@ -1,15 +1,15 @@
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 { AppQueryErrorHandler } from "./query"
import { R } from "./reffuse"
import { AppQueryErrorHandler } from "./services"
export function VQueryErrorHandler() {
const [open, setOpen] = useState(false)
const error = R.useSubscribeStream(
R.useMemo(() => AppQueryErrorHandler.pipe(
R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
Effect.map(handler => handler.errors.pipe(
Stream.changes,
Stream.tap(Console.error),
+2 -8
View File
@@ -1,6 +1,5 @@
import { ThSchema } from "@thilawyn/thilaschema"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Effect, Schema } from "effect"
import { Schema } from "effect"
export class Todo extends Schema.Class<Todo>("Todo")({
@@ -18,9 +17,4 @@ export const TodoFromJsonStruct = Schema.Struct({
ThSchema.assertEncodedJsonifiable
)
export const TodoFromJson = TodoFromJsonStruct.pipe(Schema.compose(Todo))
export const generateUniqueID = makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom)
)
export const TodoFromJson = Schema.compose(TodoFromJsonStruct, Todo)
+3 -3
View File
@@ -5,14 +5,14 @@ import { Layer } from "effect"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { ReffuseRuntime } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
import { RootContext } from "./reffuse"
import { routeTree } from "./routeTree.gen"
import { AppQueryClient, AppQueryErrorHandler } from "./services"
const layer = Layer.empty.pipe(
Layer.provideMerge(AppQueryClient.Live),
Layer.provideMerge(AppQueryErrorHandler.Live),
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
Layer.provideMerge(Clipboard.layer),
Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer),
-21
View File
@@ -1,21 +0,0 @@
import { HttpClientError } from "@effect/platform"
import { QueryClient, QueryErrorHandler } from "@reffuse/extension-query"
import { Effect } from "effect"
export class AppQueryErrorHandler extends QueryErrorHandler.Service<AppQueryErrorHandler,
HttpClientError.HttpClientError
>()(
"AppQueryErrorHandler",
(self, failure, defect) => self.pipe(
Effect.catchTags({
RequestError: failure,
ResponseError: failure,
}),
Effect.catchAllDefect(defect),
),
) {}
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({ ErrorHandler: AppQueryErrorHandler }) {}
@@ -1,10 +1,10 @@
import { QueryService } from "@reffuse/extension-query"
import { QueryRunner } from "@reffuse/extension-query"
import { ParseResult, Schema } from "effect"
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],
typeof Result.Type,
ParseResult.ParseError
@@ -8,7 +8,7 @@ export function Uuid4QueryService() {
const runFork = R.useRunFork()
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
const [state] = R.useRefState(query.state)
const [state] = R.useSubscribeRefs(query.stateRef)
return (
+3 -3
View File
@@ -3,12 +3,12 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { LazyRefExtension } from "@reffuse/extension-lazyref"
import { QueryExtension } from "@reffuse/extension-query"
import { Reffuse, ReffuseContext } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
import { AppQueryClient, AppQueryErrorHandler } from "./services"
export const RootContext = ReffuseContext.make<
| AppQueryClient
| AppQueryErrorHandler
| AppQueryClient.AppQueryClient
| AppQueryErrorHandler.AppQueryErrorHandler
| Clipboard.Clipboard
| Geolocation.Geolocation
| Permissions.Permissions
+53 -1
View File
@@ -16,9 +16,11 @@ import { Route as TimeImport } from './routes/time'
import { Route as TestsImport } from './routes/tests'
import { Route as PromiseImport } from './routes/promise'
import { Route as LazyrefImport } from './routes/lazyref'
import { Route as EffectComponentTestsImport } from './routes/effect-component-tests'
import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank'
import { Route as IndexImport } from './routes/index'
import { Route as StreamsPullImport } from './routes/streams/pull'
import { Route as QueryUsequeryImport } from './routes/query/usequery'
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
import { Route as QueryServiceImport } from './routes/query/service'
@@ -55,6 +57,12 @@ const LazyrefRoute = LazyrefImport.update({
getParentRoute: () => rootRoute,
} as any)
const EffectComponentTestsRoute = EffectComponentTestsImport.update({
id: '/effect-component-tests',
path: '/effect-component-tests',
getParentRoute: () => rootRoute,
} as any)
const CountRoute = CountImport.update({
id: '/count',
path: '/count',
@@ -73,6 +81,12 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)
const StreamsPullRoute = StreamsPullImport.update({
id: '/streams/pull',
path: '/streams/pull',
getParentRoute: () => rootRoute,
} as any)
const QueryUsequeryRoute = QueryUsequeryImport.update({
id: '/query/usequery',
path: '/query/usequery',
@@ -116,6 +130,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CountImport
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': {
id: '/lazyref'
path: '/lazyref'
@@ -172,6 +193,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof QueryUsequeryImport
parentRoute: typeof rootRoute
}
'/streams/pull': {
id: '/streams/pull'
path: '/streams/pull'
fullPath: '/streams/pull'
preLoaderRoute: typeof StreamsPullImport
parentRoute: typeof rootRoute
}
}
}
@@ -181,6 +209,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute
'/tests': typeof TestsRoute
@@ -189,12 +218,14 @@ export interface FileRoutesByFullPath {
'/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute
'/tests': typeof TestsRoute
@@ -203,6 +234,7 @@ export interface FileRoutesByTo {
'/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
}
export interface FileRoutesById {
@@ -210,6 +242,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/blank': typeof BlankRoute
'/count': typeof CountRoute
'/effect-component-tests': typeof EffectComponentTestsRoute
'/lazyref': typeof LazyrefRoute
'/promise': typeof PromiseRoute
'/tests': typeof TestsRoute
@@ -218,6 +251,7 @@ export interface FileRoutesById {
'/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute
'/streams/pull': typeof StreamsPullRoute
}
export interface FileRouteTypes {
@@ -226,6 +260,7 @@ export interface FileRouteTypes {
| '/'
| '/blank'
| '/count'
| '/effect-component-tests'
| '/lazyref'
| '/promise'
| '/tests'
@@ -234,11 +269,13 @@ export interface FileRouteTypes {
| '/query/service'
| '/query/usemutation'
| '/query/usequery'
| '/streams/pull'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/blank'
| '/count'
| '/effect-component-tests'
| '/lazyref'
| '/promise'
| '/tests'
@@ -247,11 +284,13 @@ export interface FileRouteTypes {
| '/query/service'
| '/query/usemutation'
| '/query/usequery'
| '/streams/pull'
id:
| '__root__'
| '/'
| '/blank'
| '/count'
| '/effect-component-tests'
| '/lazyref'
| '/promise'
| '/tests'
@@ -260,6 +299,7 @@ export interface FileRouteTypes {
| '/query/service'
| '/query/usemutation'
| '/query/usequery'
| '/streams/pull'
fileRoutesById: FileRoutesById
}
@@ -267,6 +307,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
BlankRoute: typeof BlankRoute
CountRoute: typeof CountRoute
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
LazyrefRoute: typeof LazyrefRoute
PromiseRoute: typeof PromiseRoute
TestsRoute: typeof TestsRoute
@@ -275,12 +316,14 @@ export interface RootRouteChildren {
QueryServiceRoute: typeof QueryServiceRoute
QueryUsemutationRoute: typeof QueryUsemutationRoute
QueryUsequeryRoute: typeof QueryUsequeryRoute
StreamsPullRoute: typeof StreamsPullRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
BlankRoute: BlankRoute,
CountRoute: CountRoute,
EffectComponentTestsRoute: EffectComponentTestsRoute,
LazyrefRoute: LazyrefRoute,
PromiseRoute: PromiseRoute,
TestsRoute: TestsRoute,
@@ -289,6 +332,7 @@ const rootRouteChildren: RootRouteChildren = {
QueryServiceRoute: QueryServiceRoute,
QueryUsemutationRoute: QueryUsemutationRoute,
QueryUsequeryRoute: QueryUsequeryRoute,
StreamsPullRoute: StreamsPullRoute,
}
export const routeTree = rootRoute
@@ -304,6 +348,7 @@ export const routeTree = rootRoute
"/",
"/blank",
"/count",
"/effect-component-tests",
"/lazyref",
"/promise",
"/tests",
@@ -311,7 +356,8 @@ export const routeTree = rootRoute
"/todos",
"/query/service",
"/query/usemutation",
"/query/usequery"
"/query/usequery",
"/streams/pull"
]
},
"/": {
@@ -323,6 +369,9 @@ export const routeTree = rootRoute
"/count": {
"filePath": "count.tsx"
},
"/effect-component-tests": {
"filePath": "effect-component-tests.tsx"
},
"/lazyref": {
"filePath": "lazyref.tsx"
},
@@ -346,6 +395,9 @@ export const routeTree = rootRoute
},
"/query/usequery": {
"filePath": "query/usequery.tsx"
},
"/streams/pull": {
"filePath": "streams/pull.tsx"
}
}
}
+4 -5
View File
@@ -1,6 +1,6 @@
import { R } from "@/reffuse"
import { createFileRoute } from "@tanstack/react-router"
import { Ref } from "effect"
import { Effect, Ref } from "effect"
export const Route = createFileRoute("/count")({
@@ -11,14 +11,13 @@ function Count() {
const runSync = R.useRunSync()
const countRef = R.useRef(0)
const [count] = R.useRefState(countRef)
const countRef = R.useRef(() => Effect.succeed(0))
const [count] = R.useSubscribeRefs(countRef)
return (
<div className="container mx-auto">
{/* <button onClick={() => setCount((count) => count + 1)}> */}
<button onClick={() => Ref.update(countRef, count => count + 1).pipe(runSync)}>
<button onClick={() => runSync(Ref.update(countRef, count => count + 1))}>
count is {count}
</button>
</div>
@@ -0,0 +1,105 @@
import { Box, TextField } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
Console.log("Runtime built."),
() => Console.log("Runtime destroyed."),
))
class TestService extends Effect.Service<TestService>()("TestService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
}) {}
class SubService extends Effect.Service<SubService>()("SubService", {
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
}) {}
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
Layer.provideMerge(LogLive),
Layer.provideMerge(TestService.Default),
))
export const Route = createFileRoute("/effect-component-tests")({
component: RouteComponent,
})
function RouteComponent() {
return (
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
<MyRoute />
</ReactManagedRuntime.AsyncProvider>
)
}
const MyRoute = pipe(
Effect.fn(function*() {
const runtime = yield* Effect.runtime()
const service = yield* TestService
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
// const MyTestComponentFC = yield* Effect.provide(
// ReactComponent.useFC(MyTestComponent),
// yield* ReactHook.useMemoLayer(SubService.Default),
// )
return <>
<Box>
<TextField.Root
value={value}
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
/>
</Box>
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
)} */}
{/* {Array.range(0, 3).map(k =>
<MyTestComponentFC key={k} />
)} */}
{yield* pipe(
Array.range(0, 3),
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
<FC key={k} />
)),
Effect.all,
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
)}
</>
}),
ReactComponent.withDisplayName("MyRoute"),
ReactComponent.withRuntime(runtime.context),
)
const MyTestComponent = pipe(
Effect.fn(function*() {
const runtime = yield* Effect.runtime()
const service = yield* SubService
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
// yield* ReactHook.useMemo(() => Effect.andThen(
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
// Console.log("MyTestComponent mounted"),
// ), [])
return <>
<Box>
<TextField.Root
value={value}
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
/>
</Box>
</>
}),
ReactComponent.withDisplayName("MyTestComponent"),
)
@@ -4,7 +4,7 @@ import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
import { R } from "@/reffuse"
import { HttpClient } from "@effect/platform"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Schema } from "effect"
import { Console, Effect, Layer, Schema } from "effect"
import { useMemo } from "react"
@@ -14,18 +14,21 @@ export const Route = createFileRoute("/query/service")({
function RouteComponent() {
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(
Effect.andThen(Effect.sleep("500 millis")),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
HttpClient.withTracerPropagation(false),
Effect.andThen(Effect.map(
HttpClient.HttpClient,
HttpClient.withTracerPropagation(false),
)),
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
Effect.scoped,
),
})
const layer = useMemo(() => query.layer(Uuid4Query.Uuid4Query), [query])
const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query])
return (
<QueryContext.Provider layer={layer}>
@@ -29,15 +29,18 @@ function RouteComponent() {
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
)),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
HttpClient.withTracerPropagation(false),
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.state)
const [state] = R.useSubscribeRefs(mutation.stateRef)
return (
@@ -20,18 +20,21 @@ function RouteComponent() {
const [count, setCount] = useState(1)
const query = R.useQuery({
key: R.useStreamFromValues(["uuid4", count]),
key: R.useStreamFromReactiveValues(["uuid4", count]),
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(Effect.sleep("500 millis")),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
HttpClient.withTracerPropagation(false),
Effect.andThen(Effect.map(
HttpClient.HttpClient,
HttpClient.withTracerPropagation(false),
)),
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)),
Effect.scoped,
),
})
const [state] = R.useSubscribeRefs(query.state)
const [state] = R.useSubscribeRefs(query.stateRef)
return (
@@ -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>
)
}
+39 -26
View File
@@ -2,7 +2,21 @@ import { R } from "@/reffuse"
import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Ref } from "effect"
import { Console, Effect, Option } from "effect"
import { useEffect, useState } from "react"
interface Node {
value: string
left?: Leaf
right?: Leaf
}
interface Leaf {
node: Node
}
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
export const Route = createFileRoute("/tests")({
@@ -10,40 +24,39 @@ export const Route = createFileRoute("/tests")({
})
function RouteComponent() {
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
// Effect.andThen(makeUuid4),
// Effect.provide(GetRandomValues.CryptoRandom),
// ), [])
// console.log(value)
const runSync = R.useRunSync()
R.useFork(() => Effect.addFinalizer(() => Console.log("cleanup")).pipe(
Effect.andThen(Console.log("ouient")),
Effect.delay("1 second"),
const [uuid, setUuid] = useState(R.useMemo(() => makeUuid, []))
const generateUuid = R.useCallbackSync(() => makeUuid.pipe(
Effect.tap(v => Effect.sync(() => setUuid(v)))
), [])
const uuidStream = R.useStreamFromReactiveValues([uuid])
const uuidStreamLatestValue = R.useSubscribeStream(uuidStream)
const uuidRef = R.useRef("none")
const anotherRef = R.useRef(69)
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) {
yield* Effect.log(value)
}), [])
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom),
Effect.flatMap(v => Ref.set(uuidRef, v)),
), [])
const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
return (
<Flex direction="row" justify="center" align="center" gap="2">
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
</R.SubscribeRefs>
<Button onClick={() => logValue("test")}>Log value</Button>
<Button onClick={() => generateUuid()}>Generate UUID</Button>
<Flex direction="column" justify="center" align="center" gap="2">
<Text>{uuid}</Text>
<Button onClick={generateUuid}>Generate UUID</Button>
<Text>
{Option.match(uuidStreamLatestValue, {
onSome: ([v]) => v,
onNone: () => <></>,
})}
</Text>
</Flex>
)
}
+1 -1
View File
@@ -26,7 +26,7 @@ function Todos() {
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<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,68 +2,43 @@ import { Todo } from "@/domain"
import { KeyValueStore } from "@effect/platform"
import { BrowserKeyValueStore } from "@effect/platform-browser"
import { PlatformError } from "@effect/platform/Error"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, SubscriptionRef } from "effect"
import { Chunk, Context, Effect, identity, Layer, ParseResult, Ref, Schema, Stream, SubscriptionRef } from "effect"
export class TodosState extends Context.Tag("TodosState")<TodosState, {
readonly todos: SubscriptionRef.SubscriptionRef<Chunk.Chunk<Todo.Todo>>
readonly readFromLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly saveToLocalStorage: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly prepend: (todo: Todo.Todo) => Effect.Effect<void>
readonly replace: (index: number, todo: Todo.Todo) => Effect.Effect<void>
readonly remove: (index: number) => Effect.Effect<void>
// readonly moveUp: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
// readonly moveDown: (index: number) => Effect.Effect<void, Cause.NoSuchElementException>
readonly load: Effect.Effect<void, PlatformError | ParseResult.ParseError>
readonly save: Effect.Effect<void, PlatformError | ParseResult.ParseError>
}>() {}
export const make = (key: string) => Layer.effect(TodosState, Effect.gen(function*() {
const todos = yield* SubscriptionRef.make(Chunk.empty<Todo.Todo>())
const readFromLocalStorage = KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => kv.get(key)),
Effect.flatMap(identity),
Effect.flatMap(Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.decode,
Effect.flatMap(Schema.decode(
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
)),
Effect.flatMap(v => Ref.set(todos, v)),
Effect.catchTag("NoSuchElementException", () => Ref.set(todos, Chunk.empty())),
Effect.catchTag("NoSuchElementException", () => Effect.succeed(Chunk.empty<Todo.Todo>())),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const saveToLocalStorage = Effect.all([KeyValueStore.KeyValueStore, todos]).pipe(
Effect.flatMap(([kv, values]) => values.pipe(
Schema.parseJson().pipe(
Schema.compose(Schema.Chunk(Todo.TodoFromJson)),
Schema.encode,
const writeToLocalStorage = (values: Chunk.Chunk<Todo.Todo>) => KeyValueStore.KeyValueStore.pipe(
Effect.flatMap(kv => values.pipe(
Schema.encode(
Schema.compose(Schema.parseJson(), Schema.Chunk(Todo.TodoFromJson))
),
Effect.flatMap(v => kv.set(key, v)),
)),
Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)
const prepend = (todo: Todo.Todo) => Ref.update(todos, Chunk.prepend(todo))
const replace = (index: number, todo: Todo.Todo) => Ref.update(todos, Chunk.replace(index, todo))
const remove = (index: number) => Ref.update(todos, Chunk.remove(index))
const todos = yield* SubscriptionRef.make(yield* readFromLocalStorage)
const load = Effect.flatMap(readFromLocalStorage, v => Ref.set(todos, v))
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))
// })
yield* readFromLocalStorage
return {
todos,
readFromLocalStorage,
saveToLocalStorage,
prepend,
replace,
remove,
}
return { todos, load, save }
}))
+15 -24
View File
@@ -1,25 +1,27 @@
import { Todo } from "@/domain"
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 { TodosState } from "../services"
const createEmptyTodo = Todo.generateUniqueID.pipe(
Effect.map(id => Todo.Todo.make({
id,
content: "",
completedAt: Option.none(),
}, true))
const createEmptyTodo = makeUuid4.pipe(
Effect.map(id => Todo.Todo.make({ id, content: "", completedAt: Option.none()}, true)),
Effect.provide(GetRandomValues.CryptoRandom),
)
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 [todo, setTodo] = R.useRefState(todoRef)
const add = R.useCallbackSync(() => Effect.all([TodosState.TodosState, todoRef]).pipe(
Effect.flatMap(([state, todo]) => Ref.update(state.todos, Chunk.prepend(todo))),
Effect.andThen(createEmptyTodo),
Effect.flatMap(v => Ref.set(todoRef, v)),
), [todoRef])
return (
@@ -27,23 +29,12 @@ export function VNewTodo() {
<Card>
<Flex direction="column" align="stretch" gap="2">
<TextArea
value={todo.content}
onChange={e => setTodo(prev =>
Todo.Todo.make({ ...prev, content: e.target.value }, true)
)}
value={content}
onChange={e => setContent(e.target.value)}
/>
<Flex direction="row" justify="center" align="center">
<Button
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.prepend(todo)),
Effect.andThen(createEmptyTodo),
Effect.map(setTodo),
runSync,
)}
>
Add
</Button>
<Button onClick={add}>Add</Button>
</Flex>
</Flex>
</Card>
+16 -19
View File
@@ -1,20 +1,28 @@
import { Todo } from "@/domain"
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 { useState } from "react"
import { R } from "../reffuse"
import { TodosState } from "../services"
export interface VTodoProps {
readonly index: number
readonly todo: Todo.Todo
readonly todoRef: SubscriptionRef.SubscriptionRef<Todo.Todo>
readonly remove: Effect.Effect<void>
}
export function VTodo({ index, todo }: VTodoProps) {
export function VTodo({ todoRef, remove }: VTodoProps) {
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)
@@ -23,14 +31,8 @@ export function VTodo({ index, todo }: VTodoProps) {
<Card>
<Flex direction="column" align="stretch" gap="1">
<TextArea
value={todo.content}
onChange={e => TodosState.TodosState.pipe(
Effect.flatMap(state => state.replace(
index,
Todo.Todo.make({ ...todo, content: e.target.value }, true),
)),
runSync,
)}
value={content}
onChange={e => setContent(e.target.value)}
disabled={!editorMode}
/>
@@ -38,12 +40,7 @@ export function VTodo({ index, todo }: VTodoProps) {
<Box></Box>
<Flex direction="row" align="center" gap="1">
<IconButton
onClick={() => TodosState.TodosState.pipe(
Effect.flatMap(state => state.remove(index)),
runSync,
)}
>
<IconButton onClick={() => runSync(remove)}>
<Delete />
</IconButton>
</Flex>
+13 -11
View File
@@ -1,5 +1,5 @@
import { Box, Flex } from "@radix-ui/themes"
import { Chunk, Effect, Stream } from "effect"
import { Chunk, Effect, Ref } from "effect"
import { R } from "../reffuse"
import { TodosState } from "../services"
import { VNewTodo } from "./VNewTodo"
@@ -8,15 +8,8 @@ import { VTodo } from "./VTodo"
export function VTodos() {
// Sync changes to the todos with the local storage
R.useFork(() => TodosState.TodosState.pipe(
Effect.flatMap(state =>
Stream.runForEach(state.todos.changes, () => state.saveToLocalStorage)
)
), [])
const todosRef = R.useMemo(() => TodosState.TodosState.pipe(Effect.map(state => state.todos)), [])
const [todos] = R.useRefState(todosRef)
const todosRef = R.useMemo(() => Effect.map(TodosState.TodosState, state => state.todos), [])
const [todos] = R.useSubscribeRefs(todosRef)
return (
@@ -27,7 +20,16 @@ export function VTodos() {
{Chunk.map(todos, (todo, index) => (
<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>
))}
</Flex>
-9
View File
@@ -1,9 +0,0 @@
# LazyRef extension for Reffuse
Extension to integrate `@typed/lazy-ref` with Reffuse.
## Peer dependencies
- `@typed/lazy-ref`
- `reffuse` 0.1.3+
- `effect` 3.13+
- `react` & `@types/react` 19+
@@ -1,9 +0,0 @@
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
export const FormExtension = ReffuseExtension.make(() => ({
useForm<A, E, R>(
this: ReffuseNamespace.ReffuseNamespace<R>,
) {
},
}))
-1
View File
@@ -1 +0,0 @@
export * from "./FormExtension.js"
@@ -1,6 +0,0 @@
import { Schema } from "effect"
export interface Form<A, I, R> {
readonly schema: Schema.Schema<A, I, R>
}
@@ -1,69 +0,0 @@
import type { Effect, Schema } from "effect"
import type * as Formify from "./Formify.js"
export interface FormField<S extends Schema.Schema.Any> {
readonly schema: S
}
export const makeFormField = <S extends Schema.Schema.Any>(
schema: S,
get: Effect.Effect<S["Type"]>,
set: (value: S["Type"]) => Effect.Effect<void>,
): FormField<S> => {
}
export interface UnionFormField<
S extends Schema.Union<Members>,
Members extends ReadonlyArray<Schema.Schema.All>,
> extends FormField<S> {
readonly member: Formify.Formify<Members[number]>
}
export interface TupleFormField<
S extends Schema.TupleType<Elements, Rest>,
Elements extends Schema.TupleType.Elements,
Rest extends Schema.TupleType.Rest,
> extends FormField<S> {
readonly elements: [...{ readonly [K in keyof Elements]: Formify.Formify<Elements[K]> }]
}
export interface ArrayFormField<
S extends Schema.Array$<Value>,
Value extends Schema.Schema.Any,
> extends FormField<S> {
readonly elements: readonly Formify.Formify<Value>[]
}
export type StructFormField<
S extends Schema.Struct<Fields>,
Fields extends Schema.Struct.Fields,
> = (
& FormField<S>
& { readonly fields: { readonly [K in keyof Fields]: Formify.Formify<Fields[K]> } }
& {
[K in keyof Fields as Fields[K] extends
Schema.tag<infer _> ? K : never
]: Fields[K] extends
Schema.tag<infer Tag> ? Tag : never
}
)
export interface GenericFormField<S extends Schema.Schema.Any> extends FormField<S> {
}
export interface PropertySignatureFormField<
S extends Schema.PropertySignature<TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R>,
TypeToken extends Schema.PropertySignature.Token,
Type,
Key extends PropertyKey,
EncodedToken extends Schema.PropertySignature.Token,
Encoded,
HasDefault extends boolean = false,
R = never,
> {
readonly propertySignature: S
readonly value: Type
}
@@ -1,51 +0,0 @@
import { Schema } from "effect"
import type * as FormField from "./FormField.js"
export type Formify<S> = (
S extends Schema.Union<infer Members> ? FormField.UnionFormField<S, Members> :
S extends Schema.TupleType<infer Elements, infer Rest> ? FormField.TupleFormField<S, Elements, Rest> :
S extends Schema.Array$<infer Value> ? FormField.ArrayFormField<S, Value> :
S extends Schema.Struct<infer Fields> ? FormField.StructFormField<S, Fields> :
S extends Schema.Schema.Any ? FormField.GenericFormField<S> :
S extends Schema.PropertySignature<
infer TypeToken,
infer Type,
infer Key,
infer EncodedToken,
infer Encoded,
infer HasDefault,
infer R
> ? FormField.PropertySignatureFormField<S, TypeToken, Type, Key, EncodedToken, Encoded, HasDefault, R> :
never
)
const Login = Schema.Union(
Schema.Struct({
_tag: Schema.tag("ByEmail"),
email: Schema.String,
password: Schema.RedactedFromSelf(Schema.String),
}),
Schema.Struct({
_tag: Schema.tag("ByPhone"),
phone: Schema.String,
password: Schema.RedactedFromSelf(Schema.String),
}),
Schema.TaggedStruct("ByKey", {
id: Schema.String,
password: Schema.RedactedFromSelf(Schema.String),
}),
)
type LoginForm = Formify<typeof Login>
declare const loginForm: LoginForm
switch (loginForm.member._tag) {
case "ByEmail":
loginForm.member
break
case "ByPhone":
break
}
@@ -1,37 +0,0 @@
import { Array, Predicate, Record, Schema, Tuple } from "effect"
export const isTupleSchema = (u: unknown): u is Schema.Tuple<any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) &&
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest)
)
export const isArraySchema = (u: unknown): u is Schema.Array$<any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "elements") && Array.isArray(u.elements) && Array.isEmptyArray(u.elements) &&
Predicate.hasProperty(u, "rest") && Array.isArray(u.rest) && Tuple.isTupleOf(u.rest, 1) &&
Predicate.hasProperty(u, "value")
)
export const isStructSchema = (u: unknown): u is Schema.Struct<any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) &&
Predicate.hasProperty(u, "records") && Array.isArray(u.records) && Array.isEmptyArray(u.records)
)
export const isRecordSchema = (u: unknown): u is Schema.Record$<any, any> => (
Schema.isSchema(u) &&
Predicate.hasProperty(u, "fields") && Predicate.isObject(u.fields) && Record.isEmptyRecord(u.fields) &&
Predicate.hasProperty(u, "records") && Array.isArray(u.records) &&
Predicate.hasProperty(u, "key") &&
Predicate.hasProperty(u, "value")
)
const myTuple = Schema.Tuple(Schema.String)
const myArray = Schema.Array(Schema.String)
const myStruct = Schema.Struct({})
const myRecord = Schema.Record({ key: Schema.String, value: Schema.String })
console.log(isArraySchema(myTuple))
@@ -1 +0,0 @@
export * as Form from "./Form.js"
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@reffuse/extension-lazyref",
"version": "0.1.3",
"version": "0.1.4",
"type": "module",
"files": [
"./README.md",
@@ -35,8 +35,8 @@
"peerDependencies": {
"@typed/lazy-ref": "^0.3.0",
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"effect": "^3.15.0",
"react": "^19.0.0",
"reffuse": "^0.1.7"
"reffuse": "^0.1.8"
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
import * as LazyRef from "@typed/lazy-ref"
import { Effect, pipe, Stream } from "effect"
import * as React from "react"
import { ReffuseExtension, type ReffuseNamespace, SetStateAction } from "reffuse"
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import { SetStateAction } from "reffuse/types"
export const LazyRefExtension = ReffuseExtension.make(() => ({
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@reffuse/extension-query",
"version": "0.1.3",
"version": "0.1.5",
"type": "module",
"files": [
"./README.md",
@@ -37,8 +37,8 @@
"@effect/platform-browser": "^0.56.0",
"@typed/async-data": "^0.13.0",
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"effect": "^3.15.0",
"react": "^19.0.0",
"reffuse": "^0.1.6"
"reffuse": "^0.1.11"
}
}
@@ -1,12 +1,11 @@
import * as AsyncData from "@typed/async-data"
import { type Context, Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "../QueryClient.js"
import * as QueryProgress from "../QueryProgress.js"
import * as QueryState from "./QueryState.js"
import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "./QueryClient.js"
import * as QueryProgress from "./QueryProgress.js"
import { QueryState } from "./internal/index.js"
export interface MutationRunner<K extends readonly unknown[], A, E, R> {
readonly context: Context.Context<R>
export interface MutationRunner<K extends readonly unknown[], A, E> {
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
@@ -17,6 +16,11 @@ export interface MutationRunner<K extends readonly unknown[], A, E, R> {
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, MutationRunner<K, A, E>>()
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
@@ -28,7 +32,7 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
mutation,
}: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect<
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>>,
never,
R | QueryClient.TagClassShape<FallbackA, HandledE>
> => Effect.gen(function*() {
@@ -37,25 +41,18 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
const run = (key: K) => Effect.Do.pipe(
Effect.bind("state", () => queryStateTag),
Effect.bind("client", () => QueryClient),
Effect.flatMap(({ state, client }) => state.set(AsyncData.loading()).pipe(
const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe(
Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
Effect.andThen(mutation(key)),
client.errorHandler.handle,
Effect.matchCauseEffect({
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
Effect.tap(state.set)
),
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
Effect.tap(state.set)
),
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.Live),
Effect.provide(QueryProgress.QueryProgress.Default),
)
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
@@ -64,11 +61,11 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
value => Ref.set(globalStateRef, value),
))
const forkMutate = (...key: K) => Effect.Do.pipe(
Effect.bind("stateRef", () => Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())),
Effect.bind("stateQueue", () => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>()),
Effect.flatMap(({ stateRef, stateQueue }) =>
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,
@@ -1,16 +0,0 @@
import type * as AsyncData from "@typed/async-data"
import { Effect, type Fiber, type Stream, type SubscriptionRef } from "effect"
export interface MutationService<K extends readonly unknown[], A, E> {
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, MutationService<K, A, E>>()
+24 -31
View File
@@ -1,4 +1,4 @@
import { Context, Effect, Layer } from "effect"
import { Context, Effect, identity, Layer } from "effect"
import type { Mutable } from "effect/Types"
import * as QueryErrorHandler from "./QueryErrorHandler.js"
@@ -8,6 +8,17 @@ export interface QueryClient<FallbackA, HandledE> {
}
export interface MakeProps<FallbackA, HandledE> {
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
}
export const make = <FallbackA, HandledE>(
{ errorHandler }: MakeProps<FallbackA, HandledE>
): Effect.Effect<QueryClient<FallbackA, HandledE>> => Effect.Do.pipe(
Effect.let("errorHandler", () => errorHandler)
)
const id = "@reffuse/extension-query/QueryClient"
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
@@ -19,46 +30,28 @@ export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
export interface ServiceProps<EH, FallbackA, HandledE> {
readonly ErrorHandler?: Context.Tag<EH, QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>>
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, EH, FallbackA, HandledE> extends Context.TagClass<
export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
Self,
typeof id,
QueryClient<FallbackA, HandledE>
> {
readonly Live: Layer.Layer<
Self | (EH extends QueryErrorHandler.DefaultQueryErrorHandler ? EH : never),
never,
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
>
readonly Default: Layer.Layer<Self, E, R>
}
export const Service = <Self>() => (
<
EH = QueryErrorHandler.DefaultQueryErrorHandler,
FallbackA = QueryErrorHandler.Fallback<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>,
HandledE = QueryErrorHandler.Error<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>,
>(
props?: ServiceProps<EH, FallbackA, HandledE>
): ServiceResult<Self, EH, FallbackA, HandledE> => {
const TagClass = Context.Tag(id)() as ServiceResult<Self, EH, FallbackA, HandledE>
<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>).Live = Layer.effect(TagClass, Effect.Do.pipe(
Effect.bind("errorHandler", () =>
(props?.ErrorHandler ?? QueryErrorHandler.DefaultQueryErrorHandler) as Effect.Effect<
QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>,
never,
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
>
)
)).pipe(
Layer.provideMerge((props?.ErrorHandler
? Layer.empty
: QueryErrorHandler.DefaultQueryErrorHandler.Live
) as Layer.Layer<EH>)
)
(TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
errorHandler => make({ errorHandler }),
))
return TagClass
}
@@ -1,5 +1,4 @@
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect"
import type { Mutable } from "effect/Types"
import { Cause, Effect, PubSub, Stream } from "effect"
export interface QueryErrorHandler<FallbackA, HandledE> {
@@ -11,55 +10,31 @@ export type Fallback<T> = T extends QueryErrorHandler<infer A, any> ? A : never
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
export interface ServiceResult<
Self,
Id extends string,
FallbackA,
HandledE,
> extends Context.TagClass<
Self,
Id,
QueryErrorHandler<FallbackA, HandledE>
> {
readonly Live: Layer.Layer<Self>
}
export const Service = <Self, HandledE = never>() => (
<const Id extends string, FallbackA>(
id: Id,
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>,
): ServiceResult<Self, Id, FallbackA, HandledE> => {
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, FallbackA, HandledE>
) => Effect.Effect<FallbackA>
): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
const errors = Stream.fromPubSub(pubsub)
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.gen(function*() {
const queue = yield* Queue.unbounded<Cause.Cause<HandledE>>()
const errors = Stream.fromQueue(queue)
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),
),
)
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) => Queue.offer(queue, Cause.fail(failure)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
(defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
)
return { errors, handle }
}))
return TagClass
}
return { errors, handle }
})
)
export class DefaultQueryErrorHandler extends Service<DefaultQueryErrorHandler>()(
"@reffuse/extension-query/DefaultQueryErrorHandler",
identity,
) {}
+9 -70
View File
@@ -1,53 +1,21 @@
import type * as AsyncData from "@typed/async-data"
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
import * as React from "react"
import type { Effect, Stream } from "effect"
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import type * as MutationService from "./MutationService.js"
import * as MutationRunner from "./MutationRunner.js"
import * as QueryClient from "./QueryClient.js"
import type * as QueryProgress from "./QueryProgress.js"
import type * as QueryService from "./QueryService.js"
import { MutationRunner, QueryRunner } from "./internal/index.js"
import * as QueryRunner from "./QueryRunner.js"
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
readonly refreshOnWindowFocus?: boolean
readonly options?: QueryRunner.RunOptions
}
export interface UseQueryResult<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
) => Layer.Layer<Self>
}
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
}
export interface UseMutationResult<K extends readonly unknown[], A, E> {
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, MutationService.MutationService<K, A, E>>
) => Layer.Layer<Self>
}
export const QueryExtension = ReffuseExtension.make(() => ({
useQuery<
@@ -61,32 +29,16 @@ export const QueryExtension = ReffuseExtension.make(() => ({
>(
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseQueryProps<QK, QA, QE, QR>,
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => QueryRunner.make({
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
key: props.key,
query: props.query,
}), [props.key])
this.useFork(() => runner.fetchOnKeyChange, [runner])
this.useFork(() => QueryRunner.run(runner, props.options), [runner])
this.useFork(() => (props.refreshOnWindowFocus ?? true)
? runner.refreshOnWindowFocus
: Effect.void,
[props.refreshOnWindowFocus, runner])
return React.useMemo(() => ({
latestKey: runner.latestKeyRef,
state: runner.stateRef,
forkRefresh: runner.forkRefresh,
layer: tag => Layer.succeed(tag, {
latestKey: runner.latestKeyRef,
state: runner.stateRef,
forkRefresh: runner.forkRefresh,
}),
}), [runner])
return runner
},
useMutation<
@@ -100,23 +52,10 @@ export const QueryExtension = ReffuseExtension.make(() => ({
>(
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseMutationProps<QK, QA, QE, QR>,
): UseMutationResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => MutationRunner.make({
): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
return this.useMemo(() => MutationRunner.make({
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
mutation: props.mutation,
}), [])
return React.useMemo(() => ({
state: runner.stateRef,
mutate: runner.mutate,
forkMutate: runner.forkMutate,
layer: tag => Layer.succeed(tag, {
state: runner.stateRef,
mutate: runner.mutate,
forkMutate: runner.forkMutate,
}),
}), [runner])
},
}))
@@ -10,7 +10,7 @@ export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryPro
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
) => Effect.Effect<void>
}>() {
static readonly Live: Layer.Layer<
static readonly Default: Layer.Layer<
QueryProgress,
never,
QueryState.QueryState<any, any>
+193
View File
@@ -0,0 +1,193 @@
import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data"
import { type Cause, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "./QueryClient.js"
import * as QueryProgress from "./QueryProgress.js"
import { QueryState } from "./internal/index.js"
export interface QueryRunner<K extends readonly unknown[], A, E> {
readonly queryKey: Stream.Stream<K>
readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
AsyncData.Success<A> | AsyncData.Failure<E>,
Cause.NoSuchElementException
>>>
readonly interrupt: Effect.Effect<void>
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void>>
readonly forkFetch: (keyValue: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never
>() => Effect.Tag(id)<Self, QueryRunner<K, A, E>>()
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
}
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
{
QueryClient,
key,
query,
}: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect<
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>>,
never,
R | QueryClient.TagClassShape<FallbackA, HandledE>
> => Effect.gen(function*() {
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
const latestKeyValueRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
Cause.NoSuchElementException
>>())
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
const interrupt = Effect.flatMap(fiberRef, Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber))
),
onNone: () => Effect.void,
}))
const forkInterrupt = Effect.flatMap(fiberRef, Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber).pipe(
Effect.asVoid,
Effect.forkDaemon,
))
),
onNone: () => Effect.forkDaemon(Effect.void),
}))
const run = (keyValue: K) => Effect.all([QueryClient, queryStateTag]).pipe(
Effect.flatMap(([client, state]) => Ref.set(latestKeyValueRef, Option.some(keyValue)).pipe(
Effect.andThen(query(keyValue)),
client.errorHandler.handle,
Effect.matchCauseEffect({
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
}),
)),
Effect.provide(context),
Effect.provide(QueryProgress.QueryProgress.Default),
)
const forkFetch = (keyValue: K) => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => queryStateTag.pipe(
Effect.flatMap(state => interrupt.pipe(
Effect.andThen(
Effect.addFinalizer(() => Effect.andThen(
Ref.set(fiberRef, Option.none()),
Queue.shutdown(stateQueue),
)).pipe(
Effect.andThen(state.set(AsyncData.loading())),
Effect.andThen(run(keyValue)),
Effect.scoped,
Effect.forkDaemon,
)
),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
)),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Effect.andThen(
Queue.offer(stateQueue, value),
Ref.set(stateRef, value),
),
)),
))
)
const setInitialRefreshState = Effect.flatMap(queryStateTag, state => state.update(previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous)
return AsyncData.loading()
}))
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => interrupt.pipe(
Effect.andThen(
Effect.addFinalizer(() => Effect.andThen(
Ref.set(fiberRef, Option.none()),
Queue.shutdown(stateQueue),
)).pipe(
Effect.andThen(setInitialRefreshState),
Effect.andThen(latestKeyValueRef.pipe(
Effect.flatMap(identity),
Effect.flatMap(run),
)),
Effect.scoped,
Effect.forkDaemon,
)
),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Effect.andThen(
Queue.offer(stateQueue, value),
Ref.set(stateRef, value),
),
)),
))
)
return {
queryKey: key,
latestKeyValueRef,
stateRef,
fiberRef,
interrupt,
forkInterrupt,
forkFetch,
forkRefresh,
}
})
export interface RunOptions {
readonly refreshOnWindowFocus?: boolean
}
export const run = <K extends readonly unknown[], A, E>(
self: QueryRunner<K, A, E>,
options?: RunOptions,
): Effect.Effect<void, never, Scope.Scope> => Effect.gen(function*() {
if (typeof window !== "undefined" && (options?.refreshOnWindowFocus ?? true))
yield* Effect.forkScoped(
Stream.runForEach(BrowserStream.fromEventListenerWindow("focus"), () => self.forkRefresh)
)
yield* Effect.addFinalizer(() => self.interrupt)
yield* Stream.runForEach(Stream.changes(self.queryKey), latestKey => self.forkFetch(latestKey))
})
@@ -1,16 +0,0 @@
import type * as AsyncData from "@typed/async-data"
import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect"
export interface QueryService<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
}
export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
+2 -2
View File
@@ -1,6 +1,6 @@
export * as MutationService from "./MutationService.js"
export * as MutationRunner from "./MutationRunner.js"
export * as QueryClient from "./QueryClient.js"
export * as QueryErrorHandler from "./QueryErrorHandler.js"
export * from "./QueryExtension.js"
export * as QueryProgress from "./QueryProgress.js"
export * as QueryService from "./QueryService.js"
export * as QueryRunner from "./QueryRunner.js"
@@ -1,191 +0,0 @@
import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data"
import { type Cause, type Context, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
import type * as QueryClient from "../QueryClient.js"
import * as QueryProgress from "../QueryProgress.js"
import * as QueryState from "./QueryState.js"
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
readonly context: Context.Context<R>
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
AsyncData.Success<A> | AsyncData.Failure<E>,
Cause.NoSuchElementException
>>>
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly forkFetch: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly forkRefresh: Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
]>
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
readonly refreshOnWindowFocus: Effect.Effect<void>
}
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
}
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
{
QueryClient,
key,
query,
}: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect<
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
never,
R | QueryClient.TagClassShape<FallbackA, HandledE>
> => Effect.gen(function*() {
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
Cause.NoSuchElementException
>>())
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
const interrupt = fiberRef.pipe(
Effect.flatMap(Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber))
),
onNone: () => Effect.void,
}))
)
const forkInterrupt = fiberRef.pipe(
Effect.flatMap(Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber).pipe(
Effect.asVoid,
Effect.forkDaemon,
))
),
onNone: () => Effect.forkDaemon(Effect.void),
}))
)
const run = Effect.Do.pipe(
Effect.bind("state", () => queryStateTag),
Effect.bind("client", () => QueryClient),
Effect.bind("latestKey", () => latestKeyRef.pipe(Effect.flatMap(identity))),
Effect.flatMap(({ state, client, latestKey }) => query(latestKey).pipe(
client.errorHandler.handle,
Effect.matchCauseEffect({
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
Effect.tap(state.set)
),
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
Effect.tap(state.set)
),
}),
)),
Effect.provide(context),
Effect.provide(QueryProgress.QueryProgress.Live),
)
const forkFetch = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => queryStateTag.pipe(
Effect.flatMap(state => interrupt.pipe(
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Queue.shutdown(stateQueue))
)).pipe(
Effect.andThen(state.set(AsyncData.loading())),
Effect.andThen(run),
Effect.scoped,
Effect.forkDaemon,
)),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
)),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Queue.offer(stateQueue, value).pipe(
Effect.andThen(Ref.set(stateRef, value))
),
)),
))
)
const setInitialRefreshState = queryStateTag.pipe(
Effect.flatMap(state => state.update(previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous)
return AsyncData.loading()
}))
)
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
Effect.flatMap(stateQueue => interrupt.pipe(
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Queue.shutdown(stateQueue))
)).pipe(
Effect.andThen(setInitialRefreshState),
Effect.andThen(run),
Effect.scoped,
Effect.forkDaemon,
)),
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
Effect.provide(QueryState.layer(
queryStateTag,
stateRef,
value => Queue.offer(stateQueue, value).pipe(
Effect.andThen(Ref.set(stateRef, value))
),
)),
))
)
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
Effect.andThen(Stream.runForEach(Stream.changes(key), latestKey =>
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
Effect.andThen(forkFetch)
)
))
)
const refreshOnWindowFocus = Stream.runForEach(
BrowserStream.fromEventListenerWindow("focus"),
() => forkRefresh,
)
return {
context,
latestKeyRef,
stateRef,
fiberRef,
forkInterrupt,
forkFetch,
forkRefresh,
fetchOnKeyChange,
refreshOnWindowFocus,
}
})
@@ -1,3 +1 @@
export * as MutationRunner from "./MutationRunner.js"
export * as QueryRunner from "./QueryRunner.js"
export * as QueryState from "./QueryState.js"
+6 -2
View File
@@ -1,6 +1,6 @@
{
"name": "reffuse",
"version": "0.1.7",
"version": "0.1.13",
"type": "module",
"files": [
"./README.md",
@@ -16,6 +16,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
@@ -31,7 +35,7 @@
},
"peerDependencies": {
"@types/react": "^19.0.0",
"effect": "^3.13.0",
"effect": "^3.15.0",
"react": "^19.0.0"
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import type * as ReffuseContext from "./ReffuseContext.js"
import type * as ReffuseExtension from "./ReffuseExtension.js"
import * as ReffuseNamespace from "./ReffuseNamespace.js"
import type { Merge, StaticType } from "./types.js"
import type { Merge, StaticType } from "./utils.js"
export class Reffuse extends ReffuseNamespace.makeClass() {}
+28 -10
View File
@@ -1,4 +1,4 @@
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect"
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Match, Ref, Runtime, Scope } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
@@ -25,6 +25,8 @@ export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly children?: React.ReactNode
}>
@@ -32,16 +34,25 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
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, ExecutionStrategy.sequential)
: Scope.make(),
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential),
[props.scope])
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe(
const makeContext = (scope: Scope.CloseableScope) => Effect.context<R>().pipe(
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
), [props.layer])
)
const closeScope = (scope: Scope.CloseableScope) => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
@@ -57,7 +68,7 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
() => closeScope(initialScope)
)
),
@@ -68,13 +79,13 @@ const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvi
Effect.sync(() => setValue(context))
),
Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
() => closeScope(scope)
),
),
}),
runSync,
), [makeScope, makeContext, runSync])
), [makeScope, runSync, runFork])
return React.createElement(Context, { ...props, value })
}
@@ -84,6 +95,7 @@ export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly finalizerExecutionMode?: "sync" | "fork"
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
@@ -112,7 +124,7 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
const scope = runSync(props.scope
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy)
: Scope.make(props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
)
Effect.context<R>().pipe(
@@ -126,7 +138,13 @@ const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): Async
effect => runFork(effect, { ...props, scope }),
)
return () => { runFork(Scope.close(scope, Exit.void)) }
return () => Scope.close(scope, Exit.void).pipe(
effect => Match.value(props.finalizerExecutionMode ?? "sync").pipe(
Match.when("sync", () => { runSync(effect) }),
Match.when("fork", () => { runFork(effect) }),
Match.exhaustive,
)
)
}, [props.layer, runSync, runFork])
return React.createElement(React.Suspense, {
+185 -72
View File
@@ -1,8 +1,8 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, Layer, Match, Option, pipe, Pipeable, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react"
import * as ReffuseContext from "./ReffuseContext.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 {
@@ -14,11 +14,22 @@ export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
}
export interface UseScopeOptions extends RenderOptions, ScopeOptions {
readonly scope?: Scope.Scope
readonly finalizerExecutionMode?: "sync" | "fork"
}
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
[K in keyof T]: Effect.Effect.Success<T[K]>
}
export abstract class ReffuseNamespace<R> {
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
@@ -83,6 +94,56 @@ export abstract class ReffuseNamespace<R> {
), [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`.
*
@@ -106,53 +167,6 @@ export abstract class ReffuseNamespace<R> {
])
}
useMemoScoped<A, E, R>(
this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList,
options?: RenderOptions & ScopeOptions,
): A {
const runSync = this.useRunSync()
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const),
runSync,
), [])
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
)
),
onFalse: () => Effect.Do.pipe(
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.tap(({ value }) =>
Effect.sync(() => setValue(value))
),
Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
),
),
}),
runSync,
), [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps,
])
return value
}
/**
* Reffuse equivalent to `React.useEffect`.
*
@@ -373,14 +387,46 @@ export abstract class ReffuseNamespace<R> {
])
}
useRef<A, R>(
useRef<A, E, R>(
this: ReffuseNamespace<R>,
initialValue: () => Effect.Effect<A, E, R>,
): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
() => Effect.flatMap(initialValue(), SubscriptionRef.make),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
)
}
useRefFromReactiveValue<A, R>(
this: ReffuseNamespace<R>,
value: A,
): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo(
() => SubscriptionRef.make(value),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true }, // Do not recreate the ref when the context changes
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],
)
}
@@ -390,18 +436,18 @@ export abstract class ReffuseNamespace<R> {
>(
this: ReffuseNamespace<R>,
...refs: Refs
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
): RefsA<Refs> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
) 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 [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
),
), refs)
@@ -439,35 +485,87 @@ export abstract class ReffuseNamespace<R> {
return [reactStateValue, setValue]
}
useStreamFromValues<const A extends React.DependencyList, R>(
useStreamFromReactiveValues<const A extends React.DependencyList, R>(
this: ReffuseNamespace<R>,
values: A,
): Stream.Stream<A> {
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
Effect.map(queue => [queue, Stream.fromQueue(queue)] as const)
), [])
const [, scopeLayer] = this.useScope([], { finalizerExecutionMode: "fork" })
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
}
useSubscribeStream<A, InitialA extends A | undefined, E, R>(
useSubscribeStream<A, E, R>(
this: ReffuseNamespace<R>,
stream: Stream.Stream<A, E, R>,
initialValue?: InitialA,
): InitialA extends A ? Option.Some<A> : Option.Option<A> {
const [reactStateValue, setReactStateValue] = React.useState<Option.Option<A>>(Option.fromNullable(initialValue))
): 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 as InitialA extends A ? Option.Some<A> : Option.Option<A>
return reactStateValue
}
SubRefFromGetSet<A, B, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly getter: (parentValue: B) => A,
readonly setter: (parentValue: B, value: A) => B,
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<A, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRefFromGetSet(props.parent, props.getter, props.setter))
}
SubRefFromPath<B, const P extends PropertyPath.Paths<B>, R>(
this: ReffuseNamespace<R>,
props: {
readonly parent: SubscriptionRef.SubscriptionRef<B>,
readonly path: P,
readonly children: (subRef: SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B>) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useSubRefFromPath(props.parent, props.path))
}
SubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
@@ -475,7 +573,7 @@ export abstract class ReffuseNamespace<R> {
this: ReffuseNamespace<R>,
props: {
readonly refs: Refs
readonly children: (...args: [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]) => React.ReactNode
readonly children: (...args: RefsA<Refs>) => React.ReactNode
},
): React.ReactNode {
return props.children(...this.useSubscribeRefs(...props.refs))
@@ -491,15 +589,30 @@ export abstract class ReffuseNamespace<R> {
return props.children(this.useRefState(props.ref))
}
SubscribeStream<A, InitialA extends A | undefined, E, R>(
SubscribeStream<A, E, R>(
this: ReffuseNamespace<R>,
props: {
readonly stream: Stream.Stream<A, E, R>
readonly initialValue?: InitialA
readonly children: (latestValue: InitialA extends A ? Option.Some<A> : Option.Option<A>) => React.ReactNode
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))
return props.children(this.useSubscribeStream(props.stream, props.initialValue as () => Effect.Effect<A, IE, R>))
}
}
-1
View File
@@ -3,4 +3,3 @@ export * as ReffuseContext from "./ReffuseContext.js"
export * as ReffuseExtension from "./ReffuseExtension.js"
export * as ReffuseNamespace from "./ReffuseNamespace.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,12 @@
import { Function } from "effect"
import type * as React from "react"
export const value: {
<S>(prevState: S): (self: React.SetStateAction<S>) => S
<S>(self: React.SetStateAction<S>, prevState: S): S
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
typeof self === "function"
? (self as (prevState: S) => S)(prevState)
: self
)
@@ -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"
+7 -1
View File
@@ -26,7 +26,13 @@
// Build
"outDir": "./dist",
"declaration": true
"declaration": true,
"plugins": [
{
"name": "@effect/language-service"
}
]
},
"include": ["./src"]