157 Commits

Author SHA1 Message Date
Julien Valverdé 076007ec67 Refactoring
Lint / lint (push) Successful in 13s
2025-04-27 18:52:08 +02:00
Julien Valverdé dd524e1aa5 Refactoring
Lint / lint (push) Successful in 14s
2025-04-27 18:46:33 +02:00
Julien Valverdé 1c7cef703b SubRef
Lint / lint (push) Successful in 17s
2025-04-25 13:50:54 +02:00
Julien Valverdé fa0f8c6b24 Refactoring
Lint / lint (push) Successful in 14s
2025-04-25 13:38:42 +02:00
Julien Valverdé 357e5aa56b useSubRef
Lint / lint (push) Successful in 13s
2025-04-25 10:16:04 +02:00
Julien Valverdé ea374d7e0f Fix
Lint / lint (push) Successful in 13s
2025-04-25 08:32:42 +02:00
Julien Valverdé 148c98acbd Fix
Lint / lint (push) Successful in 13s
2025-04-25 08:30:17 +02:00
Julien Valverdé 39d2176c61 Working subref from path
Lint / lint (push) Successful in 13s
2025-04-25 08:21:59 +02:00
Julien Valverdé 107ff1e794 SubscriptionSubRef.makeFromPath
Lint / lint (push) Successful in 14s
2025-04-25 08:12:34 +02:00
Julien Valverdé a70ef27f75 PropertyPath done
Lint / lint (push) Successful in 14s
2025-04-25 07:56:56 +02:00
Julien Valverdé 04b2fad038 PropertyPath
Lint / lint (push) Successful in 29s
2025-04-25 07:40:21 +02:00
Julien Valverdé 691b28427d SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-24 00:52:18 +02:00
Julien Valverdé 1de976aaa8 Fix
Lint / lint (push) Successful in 14s
2025-04-24 00:20:30 +02:00
Julien Valverdé df851cf9ee SearchPaths work
Lint / lint (push) Successful in 13s
2025-04-23 07:06:32 +02:00
Julien Valverdé 459f548c10 Fix
Lint / lint (push) Successful in 14s
2025-04-23 06:50:17 +02:00
Julien Valverdé 6156baec4d SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-23 06:47:11 +02:00
Julien Valverdé 1163b83929 SearchPaths work
Lint / lint (push) Failing after 13s
2025-04-23 05:53:19 +02:00
Julien Valverdé 8917f84952 SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-22 22:59:50 +02:00
Julien Valverdé 58752253b3 SearchPaths work
Lint / lint (push) Successful in 14s
2025-04-22 22:36:17 +02:00
Julien Valverdé ba362baf04 SearchPaths work
Lint / lint (push) Successful in 15s
2025-04-22 21:55:59 +02:00
Julien Valverdé 33cf4fbcbd Tests
Lint / lint (push) Successful in 14s
2025-04-21 05:15:55 +02:00
Julien Valverdé e8f92c88b8 Fix
Lint / lint (push) Successful in 13s
2025-04-21 03:17:41 +02:00
Julien Valverdé 6ae155de34 Version bump
Lint / lint (push) Successful in 13s
2025-04-21 02:53:31 +02:00
Julien Valverdé db783f174e Fix
Lint / lint (push) Successful in 13s
2025-04-21 02:52:50 +02:00
Julien Valverdé 2b48695e54 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 12s
2025-04-21 02:12:13 +02:00
Julien Valverdé 0fd3fe49a9 0.1.8 (#11)
Publish / publish (push) Successful in 27s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/11
2025-04-21 02:08:14 +02:00
Julien Valverdé ab441fe982 Fix
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 44s
2025-04-21 02:01:24 +02:00
Julien Valverdé eabcf9085b useSubRefFromGetSet
Lint / lint (push) Successful in 14s
2025-04-21 01:21:59 +02:00
Julien Valverdé 926482b154 Fix
Lint / lint (push) Successful in 14s
2025-04-20 19:38:18 +02:00
Julien Valverdé 110b0813f8 Refactoring
Lint / lint (push) Successful in 29s
2025-04-20 06:14:58 +02:00
Julien Valverdé 974af95a22 Version bump
Lint / lint (push) Successful in 14s
2025-04-20 05:32:19 +02:00
Julien Valverdé d6e1d445e8 Fix
Lint / lint (push) Successful in 14s
2025-04-20 05:28:44 +02:00
Julien Valverdé d8d6e87a12 Refactoring
Lint / lint (push) Failing after 13s
2025-04-20 05:26:31 +02:00
Julien Valverdé 682e473bf7 Fix
Lint / lint (push) Successful in 13s
2025-04-20 05:10:51 +02:00
Julien Valverdé 31dd7b5fdb Working SubscriptionSubRef
Lint / lint (push) Successful in 13s
2025-04-20 05:06:48 +02:00
Julien Valverdé 17686e68c3 SubscriptionSubRef
Lint / lint (push) Successful in 18s
2025-04-20 04:34:01 +02:00
Julien Valverdé 49d4bd4d43 SubscriptionSubRef work
Lint / lint (push) Successful in 14s
2025-04-20 00:22:24 +02:00
Julien Valverdé be88035936 SubscriptionSubRef
Lint / lint (push) Failing after 10s
2025-04-19 03:42:48 +02:00
Julien Valverdé 3497d17046 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 13s
2025-04-14 00:58:05 +02:00
Julien Valverdé 8008e18221 0.1.7 (#10)
Publish / publish (push) Successful in 24s
Lint / lint (push) Successful in 18s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/10
2025-04-14 00:57:30 +02:00
Julien Valverdé 1ca832e69d Fix
Lint / lint (push) Successful in 14s
Test build / test-build (pull_request) Successful in 21s
2025-04-14 00:54:06 +02:00
Julien Valverdé 98bd72d1d7 Cleanup
Lint / lint (push) Successful in 13s
2025-04-13 18:29:00 +02:00
Julien Valverdé f594f47793 VQueryErrorHandler
Lint / lint (push) Successful in 13s
2025-04-13 17:39:54 +02:00
Julien Valverdé 4f9827720c Fix
Lint / lint (push) Successful in 14s
2025-04-13 17:18:06 +02:00
Julien Valverdé 0f761524fd Fix
Lint / lint (push) Successful in 14s
2025-04-13 17:06:20 +02:00
Julien Valverdé 574136e161 SubscribeStream
Lint / lint (push) Successful in 14s
2025-04-13 03:21:11 +02:00
Julien Valverdé 7a12abdbdf useSubscribeStream
Lint / lint (push) Successful in 13s
2025-04-13 02:30:29 +02:00
Julien Valverdé 8fecb94292 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 13s
2025-04-12 23:59:40 +02:00
Julien Valverdé 4092da0f0c 0.1.6 (#9)
Publish / publish (push) Successful in 29s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/9
2025-04-12 23:58:25 +02:00
Julien Valverdé 26a2111705 Version bump
Lint / lint (push) Successful in 14s
Test build / test-build (pull_request) Successful in 43s
2025-04-12 23:52:35 +02:00
Julien Valverdé 1cb02407c8 Dependencies fix
Lint / lint (push) Successful in 14s
2025-04-12 23:47:43 +02:00
Julien Valverdé 6e8ce84851 Cleanup
Lint / lint (push) Successful in 13s
2025-04-12 23:06:43 +02:00
Julien Valverdé 570fb93876 ReffuseHelpers -> ReffuseNamespace
Lint / lint (push) Successful in 14s
2025-04-12 23:03:17 +02:00
Julien Valverdé 821fd18f8f Fix
Lint / lint (push) Successful in 14s
2025-04-12 18:30:37 +02:00
Julien Valverdé b7ef95341b Tests
Lint / lint (push) Successful in 14s
2025-04-12 00:39:02 +02:00
Julien Valverdé 5f5ef5614b Working SubscribeRefs
Lint / lint (push) Successful in 14s
2025-04-12 00:16:04 +02:00
Julien Valverdé cbd39f893e Done useSubscribeRefs
Lint / lint (push) Successful in 16s
2025-04-11 23:40:06 +02:00
Julien Valverdé 529e3d3f9d useSubscribeRefs work
Lint / lint (push) Failing after 10s
2025-04-11 21:43:32 +02:00
Julien Valverdé 9d47418a69 useRefsState work
Lint / lint (push) Failing after 10s
2025-04-11 20:10:34 +02:00
Julien Valverdé c1b6e73231 useRefsState work
Lint / lint (push) Successful in 13s
2025-04-11 02:58:44 +02:00
Julien Valverdé d1ba4148f2 useRefsState work
Lint / lint (push) Failing after 11s
2025-04-11 02:10:21 +02:00
Julien Valverdé ef13e87d12 Fix
Lint / lint (push) Successful in 14s
2025-04-11 00:23:15 +02:00
Julien Valverdé 8b141b907f RefState tests
Lint / lint (push) Successful in 14s
2025-04-10 23:06:13 +02:00
Julien Valverdé 52a36cb882 RefState component
Lint / lint (push) Successful in 58s
2025-04-10 22:38:29 +02:00
Julien Valverdé 3b844f071b Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 12s
2025-03-31 21:43:29 +02:00
Julien Valverdé d7c648994d @reffuse/extension-query 0.1.2 (#8)
Publish / publish (push) Successful in 22s
Lint / lint (push) Successful in 12s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/8
2025-03-31 21:42:24 +02:00
Julien Valverdé 4e422a1901 Version bump
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 17s
2025-03-31 21:40:55 +02:00
Julien Valverdé a5c6b34dfe Example fix
Lint / lint (push) Successful in 14s
2025-03-31 21:19:41 +02:00
Julien Valverdé ab1f851428 Refactoring
Lint / lint (push) Successful in 14s
2025-03-31 21:07:42 +02:00
Julien Valverdé 3f091d55c2 QueryClient refactoring
Lint / lint (push) Successful in 17s
2025-03-31 20:54:32 +02:00
Julien Valverdé 76a33fccca Query refactoring
Lint / lint (push) Successful in 14s
2025-03-31 20:38:21 +02:00
Julien Valverdé c75bb10e6b QueryClient work
Lint / lint (push) Failing after 17s
2025-03-31 18:22:18 +02:00
Julien Valverdé 3da4b2a318 QueryClient work
Lint / lint (push) Failing after 14s
2025-03-31 01:54:08 +02:00
Julien Valverdé 9a24ecaf84 QueryClient work
Lint / lint (push) Failing after 14s
2025-03-31 00:00:47 +02:00
Julien Valverdé 7b20df6c71 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 12s
2025-03-28 21:26:06 +01:00
Julien Valverdé 74fa30cf4f 0.1.5 (#7)
Publish / publish (push) Successful in 26s
Lint / lint (push) Successful in 12s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/7
2025-03-28 21:24:41 +01:00
Julien Valverdé f40dae90fb Version bump
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 44s
2025-03-28 21:22:32 +01:00
Julien Valverdé 46211638f5 Refactoring
Lint / lint (push) Successful in 13s
2025-03-28 21:19:17 +01:00
Julien Valverdé a28d6c3d30 Refactoring
Lint / lint (push) Successful in 13s
2025-03-28 21:12:42 +01:00
Julien Valverdé 6b74b9a3b2 useMemoScoped refactoring
Lint / lint (push) Successful in 13s
2025-03-28 21:09:27 +01:00
Julien Valverdé e17f945666 Fix
Lint / lint (push) Successful in 13s
2025-03-28 20:30:31 +01:00
Julien Valverdé aa46ecc82d Async provider refactoring
Lint / lint (push) Successful in 14s
2025-03-28 20:27:25 +01:00
Julien Valverdé 8ea9146dd9 Provider refactoring
Lint / lint (push) Successful in 13s
2025-03-28 19:18:46 +01:00
Julien Valverdé 0a4bb2856d Provider refactoring
Lint / lint (push) Successful in 13s
2025-03-28 18:48:49 +01:00
Julien Valverdé b4cd7daa81 Restore
Lint / lint (push) Failing after 26s
2025-03-28 18:26:04 +01:00
Julien Valverdé b5712d5433 Test
Lint / lint (push) Successful in 36s
2025-03-28 17:39:23 +01:00
Julien Valverdé 57b7eac05c Test 2025-03-28 17:37:28 +01:00
Julien Valverdé 9a9bd78ec6 Provider work
Lint / lint (push) Failing after 10s
2025-03-28 17:01:41 +01:00
Julien Valverdé ddcd681ca4 Provider refactoring
Lint / lint (push) Failing after 19m51s
2025-03-28 16:08:04 +01:00
Julien Valverdé 66de517ab5 Refactoring
Lint / lint (push) Failing after 15m36s
2025-03-26 20:24:53 +01:00
Julien Valverdé b50255ded2 Merge branch 'queryclient' of git.valverde.cloud:Thilawyn/reffuse into queryclient
Lint / lint (push) Successful in 13s
2025-03-26 19:34:59 +01:00
Julien Valverdé 03f0b623ed Removed JSX code 2025-03-26 19:34:21 +01:00
Julien Valverdé fb6d803723 Removed JSX code 2025-03-26 19:34:14 +01:00
Julien Valverdé 972986241c ReffuseHelpers.make()
Lint / lint (push) Has been cancelled
2025-03-25 19:29:17 +01:00
Julien Valverdé 9eb0904600 Refactoring
Lint / lint (push) Successful in 13s
2025-03-24 20:35:22 +01:00
Julien Valverdé fc86c818e0 Merge branch 'next' of git.valverde.cloud:Thilawyn/reffuse into next
Lint / lint (push) Successful in 13s
2025-03-24 19:41:21 +01:00
Julien Valverdé d01152bdcf 0.1.4 (#6)
Publish / publish (push) Successful in 30s
Lint / lint (push) Successful in 13s
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: https://gitea:3000/Thilawyn/reffuse/pulls/6
2025-03-24 19:39:29 +01:00
Julien Valverdé 5a12139602 Regenerated lockfile
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 15s
2025-03-24 19:35:56 +01:00
Julien Valverdé a0928c718f Version bump
Lint / lint (push) Successful in 13s
Test build / test-build (pull_request) Successful in 15s
2025-03-24 19:33:35 +01:00
Julien Valverdé 49d9edd4b1 Dependencies upgrade
Lint / lint (push) Successful in 14s
2025-03-24 19:29:12 +01:00
Julien Valverdé 3552c25b5c Mutation refactoring
Lint / lint (push) Successful in 13s
2025-03-24 19:07:11 +01:00
Julien Valverdé 516e0a465d Ref state fix
Lint / lint (push) Successful in 16s
2025-03-24 18:38:14 +01:00
Julien Valverdé 7cf5367409 Tests
Lint / lint (push) Successful in 12s
2025-03-24 17:34:50 +01:00
Julien Valverdé 3b237c0588 Query refactoring
Lint / lint (push) Successful in 13s
2025-03-24 17:30:41 +01:00
Julien Valverdé d9aa42d23a Fix
Lint / lint (push) Successful in 13s
2025-03-24 12:16:03 +01:00
Julien Valverdé fd3213c53f Fix
Lint / lint (push) Successful in 13s
2025-03-24 12:06:03 +01:00
Julien Valverdé baa8c92221 Query refactoring
Lint / lint (push) Successful in 14s
2025-03-24 12:03:55 +01:00
Julien Valverdé d55b432846 Refactoring
Lint / lint (push) Successful in 13s
2025-03-23 23:58:05 +01:00
Julien Valverdé 6266c7506e Example fix
Lint / lint (push) Successful in 13s
2025-03-23 07:32:11 +01:00
Julien Valverdé 043e966e45 ErrorHandler work
Lint / lint (push) Successful in 14s
2025-03-23 07:25:03 +01:00
Julien Valverdé 88fab2c7d7 ErrorHandler refactoring
Lint / lint (push) Failing after 15s
2025-03-23 06:08:35 +01:00
Julien Valverdé 224ccd8e32 Fix
Lint / lint (push) Successful in 14s
2025-03-21 04:55:38 +01:00
Julien Valverdé 4cf70ada0b Fix
Lint / lint (push) Successful in 16s
2025-03-21 04:49:44 +01:00
Julien Valverdé f9bd5d4d6b Query refactoring
Lint / lint (push) Successful in 14s
2025-03-21 04:37:32 +01:00
Julien Valverdé 1ec1db0658 Mutation progress
Lint / lint (push) Successful in 14s
2025-03-21 03:38:48 +01:00
Julien Valverdé 2d94e84941 Stream fix
Lint / lint (push) Successful in 13s
2025-03-21 03:24:55 +01:00
Julien Valverdé aab83907ba Working mutation progress
Lint / lint (push) Successful in 13s
2025-03-21 02:14:36 +01:00
Julien Valverdé 8c0d6b4c8a Cleanup
Lint / lint (push) Successful in 13s
2025-03-21 01:24:52 +01:00
Julien Valverdé d82d1d1c29 Refactoring
Lint / lint (push) Successful in 14s
2025-03-21 01:23:47 +01:00
Julien Valverdé 0f09573948 Mutation services
Lint / lint (push) Failing after 14s
2025-03-20 07:10:55 +01:00
Julien Valverdé 2b6b36713e MutationRunner work
Lint / lint (push) Successful in 14s
2025-03-20 04:31:38 +01:00
Julien Valverdé 5d0aecc9d5 QueryProgress
Lint / lint (push) Successful in 14s
2025-03-19 05:13:54 +01:00
Julien Valverdé f21d8b2d8a QueryProgress
Lint / lint (push) Successful in 14s
2025-03-18 03:11:39 +01:00
Julien Valverdé f85173fa68 Fix
Lint / lint (push) Successful in 30s
2025-03-18 02:46:41 +01:00
Julien Valverdé 65a124de1f Mutation tests
Lint / lint (push) Successful in 13s
2025-03-17 05:52:13 +01:00
Julien Valverdé 16893761c6 Mutation refactoring
Lint / lint (push) Successful in 13s
2025-03-17 05:34:19 +01:00
Julien Valverdé 3fdc2e31eb Mutation example
Lint / lint (push) Successful in 17s
2025-03-17 02:36:13 +01:00
Julien Valverdé 8636a28f2f Working mutations
Lint / lint (push) Successful in 14s
2025-03-17 02:15:27 +01:00
Julien Valverdé d56578da8f useMutation
Lint / lint (push) Successful in 13s
2025-03-16 06:50:20 +01:00
Julien Valverdé 299109d421 Mutation fix
Lint / lint (push) Successful in 14s
2025-03-16 06:25:02 +01:00
Julien Valverdé 4995b2949f MutationRunner
Lint / lint (push) Successful in 13s
2025-03-16 05:20:55 +01:00
Julien Valverdé 6e6e675709 MutationRunner 2025-03-16 05:20:37 +01:00
Julien Valverdé b04860aa25 Cleanup
Lint / lint (push) Successful in 13s
2025-03-16 04:32:51 +01:00
Julien Valverdé e9e17ac211 Fix
Lint / lint (push) Successful in 12s
2025-03-16 04:11:25 +01:00
Julien Valverdé 1f0ff725ff Fix
Lint / lint (push) Successful in 13s
2025-03-16 04:05:39 +01:00
Julien Valverdé 447d89982c Fix
Lint / lint (push) Successful in 13s
2025-03-16 03:34:54 +01:00
Julien Valverdé 778ee27795 ErrorHandler refactoring
Lint / lint (push) Successful in 16s
2025-03-16 03:33:01 +01:00
Julien Valverdé 077816efb6 Fix
Lint / lint (push) Successful in 13s
2025-03-16 03:23:12 +01:00
Julien Valverdé e4bacd1ca7 Working QueryClient refactoring
Lint / lint (push) Successful in 13s
2025-03-16 03:19:12 +01:00
Julien Valverdé 0e2c0db28f QueryClient refactoring
Lint / lint (push) Successful in 13s
2025-03-16 02:52:49 +01:00
Julien Valverdé c943d81702 QueryClient.make
Lint / lint (push) Successful in 12s
2025-03-15 22:27:15 +01:00
Julien Valverdé c2bc406a5f Fixed query error handler
Lint / lint (push) Successful in 13s
2025-03-15 06:43:47 +01:00
Julien Valverdé 4e778b6c95 VQueryErrorHandler
Lint / lint (push) Successful in 12s
2025-03-15 05:12:38 +01:00
Julien Valverdé 0437fa5dcc QueryErrorHandler work
Lint / lint (push) Successful in 16s
2025-03-15 02:30:37 +01:00
Julien Valverdé 5614b8df38 Fix
Lint / lint (push) Successful in 13s
2025-03-15 00:52:00 +01:00
Julien Valverdé 70b6c4434e Tests
Lint / lint (push) Successful in 16s
2025-03-14 22:07:53 +01:00
Julien Valverdé 2e8dfbc988 QueryClient
Lint / lint (push) Successful in 13s
2025-03-14 22:00:53 +01:00
Julien Valverdé abc47c4647 Fix
Lint / lint (push) Failing after 12s
2025-03-14 05:04:49 +01:00
Julien Valverdé eedd2a7f2a makeTag
Lint / lint (push) Failing after 12s
2025-03-14 04:57:07 +01:00
Julien Valverdé f4ab575a8d QueryExtension work
Lint / lint (push) Failing after 13s
2025-03-14 04:24:56 +01:00
Julien Valverdé 747e2c6056 Done QueryClient
Lint / lint (push) Successful in 14s
2025-03-14 04:13:14 +01:00
Julien Valverdé 68c68417d8 QueryClient work
Lint / lint (push) Successful in 12s
2025-03-14 03:56:54 +01:00
Julien Valverdé ed384a62a8 QueryClient work
Lint / lint (push) Failing after 15s
2025-03-14 03:26:28 +01:00
Julien Valverdé 3a1748bb39 QueryClient tests
Lint / lint (push) Successful in 18s
2025-03-13 22:31:50 +01:00
Julien Valverdé 66b8fd2c2e Fix
Lint / lint (push) Successful in 13s
2025-03-12 06:37:39 +01:00
Julien Valverdé bc81c443ab Query work
Lint / lint (push) Successful in 13s
2025-03-11 21:19:57 +01:00
Julien Valverdé ee5dbe3766 Query work
Lint / lint (push) Successful in 13s
2025-03-11 20:39:56 +01:00
51 changed files with 1754 additions and 1140 deletions
+229 -404
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/monorepo", "name": "@reffuse/monorepo",
"packageManager": "bun@1.2.2", "packageManager": "bun@1.2.9",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"./packages/*" "./packages/*"
@@ -15,10 +15,9 @@
"clean:node": "rm -rf node_modules" "clean:node": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"code-narrator": "^1.0.17", "npm-check-updates": "^17.1.18",
"npm-check-updates": "^17.1.14",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
"turbo": "^2.4.4", "turbo": "^2.5.0",
"typescript": "^5.7.3" "typescript": "^5.8.3"
} }
} }
+20 -20
View File
@@ -11,41 +11,41 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.24.0",
"@tanstack/react-router": "^1.112.7", "@tanstack/react-router": "^1.115.3",
"@tanstack/router-devtools": "^1.112.7", "@tanstack/react-router-devtools": "^1.115.3",
"@tanstack/router-plugin": "^1.112.7", "@tanstack/router-plugin": "^1.115.3",
"@thilawyn/thilaschema": "^0.1.4", "@thilawyn/thilaschema": "^0.1.4",
"@types/react": "^19.0.10", "@types/react": "^19.1.1",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0", "eslint": "^9.24.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0", "globals": "^16.0.0",
"react": "^19.0.0", "react": "^19.1.0",
"react-dom": "^19.0.0", "react-dom": "^19.1.0",
"typescript-eslint": "^8.26.0", "typescript-eslint": "^8.29.1",
"vite": "^6.2.0" "vite": "^6.2.6"
}, },
"dependencies": { "dependencies": {
"@effect/platform": "^0.77.6", "@effect/platform": "^0.80.8",
"@effect/platform-browser": "^0.56.6", "@effect/platform-browser": "^0.59.8",
"@radix-ui/themes": "^3.2.1", "@radix-ui/themes": "^3.2.1",
"@reffuse/extension-lazyref": "workspace:*", "@reffuse/extension-lazyref": "workspace:*",
"@reffuse/extension-query": "workspace:*", "@reffuse/extension-query": "workspace:*",
"@typed/async-data": "^0.13.1", "@typed/async-data": "^0.13.1",
"@typed/id": "^0.17.1", "@typed/id": "^0.17.2",
"@typed/lazy-ref": "^0.3.3", "@typed/lazy-ref": "^0.3.3",
"effect": "^3.13.6", "effect": "^3.14.8",
"lucide-react": "^0.477.0", "lucide-react": "^0.487.0",
"mobx": "^6.13.6", "mobx": "^6.13.7",
"reffuse": "workspace:*" "reffuse": "workspace:*"
}, },
"overrides": { "overrides": {
"effect": "^3.13.6", "effect": "^3.14.8",
"@effect/platform": "^0.77.6", "@effect/platform": "^0.80.8",
"@effect/platform-browser": "^0.56.6", "@effect/platform-browser": "^0.59.8",
"@typed/lazy-ref": "^0.3.3", "@typed/lazy-ref": "^0.3.3",
"@typed/async-data": "^0.13.1" "@typed/async-data": "^0.13.1"
} }
@@ -0,0 +1,57 @@
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
import { useState } from "react"
import { AppQueryErrorHandler } from "./query"
import { R } from "./reffuse"
export function VQueryErrorHandler() {
const [open, setOpen] = useState(false)
const error = R.useSubscribeStream(
R.useMemo(() => AppQueryErrorHandler.pipe(
Effect.map(handler => handler.errors.pipe(
Stream.changes,
Stream.tap(Console.error),
Stream.tap(() => Effect.sync(() => setOpen(true))),
))
), [])
)
if (Option.isNone(error))
return <></>
return (
<AlertDialog.Root open={open}>
<AlertDialog.Content maxWidth="450px">
<AlertDialog.Title>Error</AlertDialog.Title>
<AlertDialog.Description size="2">
{Either.match(Cause.failureOrCause(error.value), {
onLeft: flow(
Match.value,
Match.tag("RequestError", () => <Text>HTTP request error</Text>),
Match.tag("ResponseError", () => <Text>HTTP response error</Text>),
Match.exhaustive,
),
onRight: flow(
Cause.dieOption,
Option.match({
onSome: () => <Text>Unrecoverable defect</Text>,
onNone: () => <Text>Unknown error</Text>,
}),
),
})}
</AlertDialog.Description>
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Action>
<Button variant="solid" color="red" onClick={() => setOpen(false)}>
Ok
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
)
}
+6 -3
View File
@@ -5,11 +5,14 @@ import { Layer } from "effect"
import { StrictMode } from "react" import { StrictMode } from "react"
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { ReffuseRuntime } from "reffuse" import { ReffuseRuntime } from "reffuse"
import { GlobalContext } from "./reffuse" import { AppQueryClient, AppQueryErrorHandler } from "./query"
import { RootContext } from "./reffuse"
import { routeTree } from "./routeTree.gen" import { routeTree } from "./routeTree.gen"
const layer = Layer.empty.pipe( const layer = Layer.empty.pipe(
Layer.provideMerge(AppQueryClient.Live),
Layer.provideMerge(AppQueryErrorHandler.Live),
Layer.provideMerge(Clipboard.layer), Layer.provideMerge(Clipboard.layer),
Layer.provideMerge(Geolocation.layer), Layer.provideMerge(Geolocation.layer),
Layer.provideMerge(Permissions.layer), Layer.provideMerge(Permissions.layer),
@@ -28,9 +31,9 @@ declare module "@tanstack/react-router" {
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<ReffuseRuntime.Provider> <ReffuseRuntime.Provider>
<GlobalContext.Provider layer={layer}> <RootContext.Provider layer={layer}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</GlobalContext.Provider> </RootContext.Provider>
</ReffuseRuntime.Provider> </ReffuseRuntime.Provider>
</StrictMode> </StrictMode>
) )
+21
View File
@@ -0,0 +1,21 @@
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 }) {}
+2 -2
View File
@@ -1,10 +1,10 @@
import { GlobalReffuse } from "@/reffuse" import { RootReffuse } from "@/reffuse"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { Uuid4Query } from "./services" import { Uuid4Query } from "./services"
export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>() export const QueryContext = ReffuseContext.make<Uuid4Query.Uuid4Query>()
export const R = new class QueryReffuse extends GlobalReffuse.pipe( export const R = new class QueryReffuse extends RootReffuse.pipe(
Reffuse.withContexts(QueryContext) Reffuse.withContexts(QueryContext)
) {} ) {}
@@ -1,4 +1,3 @@
import { HttpClientError } from "@effect/platform"
import { QueryService } from "@reffuse/extension-query" import { QueryService } from "@reffuse/extension-query"
import { ParseResult, Schema } from "effect" import { ParseResult, Schema } from "effect"
@@ -8,5 +7,5 @@ export const Result = Schema.Array(Schema.String)
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query, export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
readonly ["uuid4", number], readonly ["uuid4", number],
typeof Result.Type, typeof Result.Type,
HttpClientError.HttpClientError | ParseResult.ParseError ParseResult.ParseError
>() {} >() {}
@@ -5,7 +5,7 @@ import { Uuid4Query } from "../services"
export function Uuid4QueryService() { export function Uuid4QueryService() {
const runSync = R.useRunSync() const runFork = R.useRunFork()
const query = R.useMemo(() => Uuid4Query.Uuid4Query, []) const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
const [state] = R.useRefState(query.state) const [state] = R.useRefState(query.state)
@@ -25,7 +25,7 @@ export function Uuid4QueryService() {
})} })}
</Text> </Text>
<Button onClick={() => runSync(query.refresh)}>Refresh</Button> <Button onClick={() => runFork(query.forkRefresh)}>Refresh</Button>
</Flex> </Flex>
</Container> </Container>
) )
+7 -4
View File
@@ -3,19 +3,22 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
import { LazyRefExtension } from "@reffuse/extension-lazyref" import { LazyRefExtension } from "@reffuse/extension-lazyref"
import { QueryExtension } from "@reffuse/extension-query" import { QueryExtension } from "@reffuse/extension-query"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { AppQueryClient, AppQueryErrorHandler } from "./query"
export const GlobalContext = ReffuseContext.make< export const RootContext = ReffuseContext.make<
| AppQueryClient
| AppQueryErrorHandler
| Clipboard.Clipboard | Clipboard.Clipboard
| Geolocation.Geolocation | Geolocation.Geolocation
| Permissions.Permissions | Permissions.Permissions
| HttpClient.HttpClient | HttpClient.HttpClient
>() >()
export class GlobalReffuse extends Reffuse.Reffuse.pipe( export class RootReffuse extends Reffuse.Reffuse.pipe(
Reffuse.withExtension(LazyRefExtension), Reffuse.withExtension(LazyRefExtension),
Reffuse.withExtension(QueryExtension), Reffuse.withExtension(QueryExtension),
Reffuse.withContexts(GlobalContext), Reffuse.withContexts(RootContext),
) {} ) {}
export const R = new GlobalReffuse() export const R = new RootReffuse()
+52
View File
@@ -11,6 +11,7 @@
// Import Routes // Import Routes
import { Route as rootRoute } from './routes/__root' import { Route as rootRoute } from './routes/__root'
import { Route as TodosImport } from './routes/todos'
import { Route as TimeImport } from './routes/time' import { Route as TimeImport } from './routes/time'
import { Route as TestsImport } from './routes/tests' import { Route as TestsImport } from './routes/tests'
import { Route as PromiseImport } from './routes/promise' import { Route as PromiseImport } from './routes/promise'
@@ -19,10 +20,17 @@ import { Route as CountImport } from './routes/count'
import { Route as BlankImport } from './routes/blank' import { Route as BlankImport } from './routes/blank'
import { Route as IndexImport } from './routes/index' import { Route as IndexImport } from './routes/index'
import { Route as QueryUsequeryImport } from './routes/query/usequery' import { Route as QueryUsequeryImport } from './routes/query/usequery'
import { Route as QueryUsemutationImport } from './routes/query/usemutation'
import { Route as QueryServiceImport } from './routes/query/service' import { Route as QueryServiceImport } from './routes/query/service'
// Create/Update Routes // Create/Update Routes
const TodosRoute = TodosImport.update({
id: '/todos',
path: '/todos',
getParentRoute: () => rootRoute,
} as any)
const TimeRoute = TimeImport.update({ const TimeRoute = TimeImport.update({
id: '/time', id: '/time',
path: '/time', path: '/time',
@@ -71,6 +79,12 @@ const QueryUsequeryRoute = QueryUsequeryImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const QueryUsemutationRoute = QueryUsemutationImport.update({
id: '/query/usemutation',
path: '/query/usemutation',
getParentRoute: () => rootRoute,
} as any)
const QueryServiceRoute = QueryServiceImport.update({ const QueryServiceRoute = QueryServiceImport.update({
id: '/query/service', id: '/query/service',
path: '/query/service', path: '/query/service',
@@ -130,6 +144,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof TimeImport preLoaderRoute: typeof TimeImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/todos': {
id: '/todos'
path: '/todos'
fullPath: '/todos'
preLoaderRoute: typeof TodosImport
parentRoute: typeof rootRoute
}
'/query/service': { '/query/service': {
id: '/query/service' id: '/query/service'
path: '/query/service' path: '/query/service'
@@ -137,6 +158,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof QueryServiceImport preLoaderRoute: typeof QueryServiceImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/query/usemutation': {
id: '/query/usemutation'
path: '/query/usemutation'
fullPath: '/query/usemutation'
preLoaderRoute: typeof QueryUsemutationImport
parentRoute: typeof rootRoute
}
'/query/usequery': { '/query/usequery': {
id: '/query/usequery' id: '/query/usequery'
path: '/query/usequery' path: '/query/usequery'
@@ -157,7 +185,9 @@ export interface FileRoutesByFullPath {
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
} }
@@ -169,7 +199,9 @@ export interface FileRoutesByTo {
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
} }
@@ -182,7 +214,9 @@ export interface FileRoutesById {
'/promise': typeof PromiseRoute '/promise': typeof PromiseRoute
'/tests': typeof TestsRoute '/tests': typeof TestsRoute
'/time': typeof TimeRoute '/time': typeof TimeRoute
'/todos': typeof TodosRoute
'/query/service': typeof QueryServiceRoute '/query/service': typeof QueryServiceRoute
'/query/usemutation': typeof QueryUsemutationRoute
'/query/usequery': typeof QueryUsequeryRoute '/query/usequery': typeof QueryUsequeryRoute
} }
@@ -196,7 +230,9 @@ export interface FileRouteTypes {
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
@@ -207,7 +243,9 @@ export interface FileRouteTypes {
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
id: id:
| '__root__' | '__root__'
@@ -218,7 +256,9 @@ export interface FileRouteTypes {
| '/promise' | '/promise'
| '/tests' | '/tests'
| '/time' | '/time'
| '/todos'
| '/query/service' | '/query/service'
| '/query/usemutation'
| '/query/usequery' | '/query/usequery'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
@@ -231,7 +271,9 @@ export interface RootRouteChildren {
PromiseRoute: typeof PromiseRoute PromiseRoute: typeof PromiseRoute
TestsRoute: typeof TestsRoute TestsRoute: typeof TestsRoute
TimeRoute: typeof TimeRoute TimeRoute: typeof TimeRoute
TodosRoute: typeof TodosRoute
QueryServiceRoute: typeof QueryServiceRoute QueryServiceRoute: typeof QueryServiceRoute
QueryUsemutationRoute: typeof QueryUsemutationRoute
QueryUsequeryRoute: typeof QueryUsequeryRoute QueryUsequeryRoute: typeof QueryUsequeryRoute
} }
@@ -243,7 +285,9 @@ const rootRouteChildren: RootRouteChildren = {
PromiseRoute: PromiseRoute, PromiseRoute: PromiseRoute,
TestsRoute: TestsRoute, TestsRoute: TestsRoute,
TimeRoute: TimeRoute, TimeRoute: TimeRoute,
TodosRoute: TodosRoute,
QueryServiceRoute: QueryServiceRoute, QueryServiceRoute: QueryServiceRoute,
QueryUsemutationRoute: QueryUsemutationRoute,
QueryUsequeryRoute: QueryUsequeryRoute, QueryUsequeryRoute: QueryUsequeryRoute,
} }
@@ -264,7 +308,9 @@ export const routeTree = rootRoute
"/promise", "/promise",
"/tests", "/tests",
"/time", "/time",
"/todos",
"/query/service", "/query/service",
"/query/usemutation",
"/query/usequery" "/query/usequery"
] ]
}, },
@@ -289,9 +335,15 @@ export const routeTree = rootRoute
"/time": { "/time": {
"filePath": "time.tsx" "filePath": "time.tsx"
}, },
"/todos": {
"filePath": "todos.tsx"
},
"/query/service": { "/query/service": {
"filePath": "query/service.tsx" "filePath": "query/service.tsx"
}, },
"/query/usemutation": {
"filePath": "query/usemutation.tsx"
},
"/query/usequery": { "/query/usequery": {
"filePath": "query/usequery.tsx" "filePath": "query/usequery.tsx"
} }
+4 -1
View File
@@ -1,6 +1,7 @@
import { VQueryErrorHandler } from "@/VQueryErrorHandler"
import { Container, Flex, Theme } from "@radix-ui/themes" import { Container, Flex, Theme } from "@radix-ui/themes"
import { createRootRoute, Link, Outlet } from "@tanstack/react-router" import { createRootRoute, Link, Outlet } from "@tanstack/react-router"
import { TanStackRouterDevtools } from "@tanstack/router-devtools" import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"
import "@radix-ui/themes/styles.css" import "@radix-ui/themes/styles.css"
import "../index.css" import "../index.css"
@@ -26,6 +27,8 @@ function Root() {
</Container> </Container>
<Outlet /> <Outlet />
<VQueryErrorHandler />
<TanStackRouterDevtools /> <TanStackRouterDevtools />
</Theme> </Theme>
) )
+4 -23
View File
@@ -1,29 +1,10 @@
import { TodosContext } from "@/todos/reffuse"
import { TodosState } from "@/todos/services"
import { VTodos } from "@/todos/views/VTodos"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/")({ export const Route = createFileRoute('/')({
component: Index component: RouteComponent
}) })
function Index() { function RouteComponent() {
return <div>Hello "/"!</div>
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos"))
), [])
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</Container>
)
} }
@@ -0,0 +1,81 @@
import { R } from "@/reffuse"
import { HttpClient } from "@effect/platform"
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
import { QueryProgress } from "@reffuse/extension-query"
import { createFileRoute } from "@tanstack/react-router"
import * as AsyncData from "@typed/async-data"
import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
import { useState } from "react"
export const Route = createFileRoute("/query/usemutation")({
component: RouteComponent
})
const Result = Schema.Array(Schema.String)
function RouteComponent() {
const runFork = R.useRunFork()
const [count, setCount] = useState(1)
const mutation = R.useMutation({
mutation: ([count]: readonly [count: number]) => Console.log(`Querying ${ count } IDs...`).pipe(
Effect.andThen(QueryProgress.QueryProgress.update(() =>
AsyncData.Progress.make({ loaded: 0, total: Option.some(100) })
)),
Effect.andThen(Effect.sleep("500 millis")),
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
)),
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
HttpClient.withTracerPropagation(false),
Effect.flatMap(res => res.json),
Effect.flatMap(Schema.decodeUnknown(Result)),
Effect.scoped,
)
})
const [state] = R.useSubscribeRefs(mutation.state)
return (
<Container>
<Flex direction="column" align="center" gap="2">
<Slider
min={1}
max={100}
value={[count]}
onValueChange={flow(
Array.head,
Option.getOrThrow,
setCount,
)}
/>
<Text>
{AsyncData.match(state, {
NoData: () => "No data yet",
Loading: progress =>
`Loading...
${ Option.match(progress, {
onSome: ({ loaded, total }) => ` (${ loaded }/${ Option.getOrElse(total, () => "unknown") })`,
onNone: () => "",
}) }`,
Success: value => `Value: ${ value }`,
Failure: cause => `Error: ${ cause }`,
})}
</Text>
<Button onClick={() => mutation.forkMutate(count).pipe(
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
Effect.andThen(Console.log("Mutation done.")),
runFork,
)}>
Get
</Button>
</Flex>
</Container>
)
}
+12 -4
View File
@@ -3,7 +3,7 @@ import { HttpClient } from "@effect/platform"
import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes" import { Button, Container, Flex, Slider, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import * as AsyncData from "@typed/async-data" import * as AsyncData from "@typed/async-data"
import { Array, Console, Effect, flow, Option, Schema } from "effect" import { Array, Console, Effect, flow, Option, Schema, Stream } from "effect"
import { useState } from "react" import { useState } from "react"
@@ -15,7 +15,7 @@ export const Route = createFileRoute("/query/usequery")({
const Result = Schema.Array(Schema.String) const Result = Schema.Array(Schema.String)
function RouteComponent() { function RouteComponent() {
const runSync = R.useRunSync() const runFork = R.useRunFork()
const [count, setCount] = useState(1) const [count, setCount] = useState(1)
@@ -31,7 +31,7 @@ function RouteComponent() {
), ),
}) })
const [state] = R.useRefState(query.state) const [state] = R.useSubscribeRefs(query.state)
return ( return (
@@ -59,7 +59,15 @@ function RouteComponent() {
})} })}
</Text> </Text>
<Button onClick={() => runSync(query.refresh)}>Refresh</Button> <Button
onClick={() => query.forkRefresh.pipe(
Effect.flatMap(([, state]) => Stream.runForEach(state, Console.log)),
Effect.andThen(Console.log("Refresh finished or stopped")),
runFork,
)}
>
Refresh
</Button>
</Flex> </Flex>
</Container> </Container>
) )
+18 -7
View File
@@ -1,9 +1,8 @@
import { R } from "@/reffuse" import { R } from "@/reffuse"
import { Button, Flex } from "@radix-ui/themes" import { Button, Flex, Text } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { GetRandomValues, makeUuid4 } from "@typed/id" import { GetRandomValues, makeUuid4 } from "@typed/id"
import { Console, Effect, Stream } from "effect" import { Console, Effect, Ref } from "effect"
import { useState } from "react"
export const Route = createFileRoute("/tests")({ export const Route = createFileRoute("/tests")({
@@ -11,6 +10,9 @@ export const Route = createFileRoute("/tests")({
}) })
function RouteComponent() { function RouteComponent() {
const deepRef = R.useRef({ value: "poulet" })
const deepValueRef = R.useSubRef(deepRef, ["value"])
// const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe( // const value = R.useMemoScoped(Effect.addFinalizer(() => Console.log("cleanup")).pipe(
// Effect.andThen(makeUuid4), // Effect.andThen(makeUuid4),
// Effect.provide(GetRandomValues.CryptoRandom), // Effect.provide(GetRandomValues.CryptoRandom),
@@ -22,9 +24,9 @@ function RouteComponent() {
Effect.delay("1 second"), Effect.delay("1 second"),
), []) ), [])
const [reactValue, setReactValue] = useState("initial")
const reactValueStream = R.useStreamFromValues([reactValue]) const uuidRef = R.useRef("none")
R.useFork(() => Stream.runForEach(reactValueStream, Console.log), [reactValueStream]) const anotherRef = R.useRef(69)
const logValue = R.useCallbackSync(Effect.fn(function*(value: string) { const logValue = R.useCallbackSync(Effect.fn(function*(value: string) {
@@ -33,12 +35,21 @@ function RouteComponent() {
const generateUuid = R.useCallbackSync(() => makeUuid4.pipe( const generateUuid = R.useCallbackSync(() => makeUuid4.pipe(
Effect.provide(GetRandomValues.CryptoRandom), Effect.provide(GetRandomValues.CryptoRandom),
Effect.map(setReactValue), Effect.tap(v => Ref.set(uuidRef, v)),
Effect.tap(v => Ref.set(deepValueRef, v)),
), []) ), [])
return ( return (
<Flex direction="row" justify="center" align="center" gap="2"> <Flex direction="row" justify="center" align="center" gap="2">
<R.SubscribeRefs refs={[uuidRef, anotherRef]}>
{(uuid, anotherRef) => <Text>{uuid} / {anotherRef}</Text>}
</R.SubscribeRefs>
<R.SubscribeRefs refs={[deepRef, deepValueRef]}>
{(deep, deepValue) => <Text>{JSON.stringify(deep)} / {deepValue}</Text>}
</R.SubscribeRefs>
<Button onClick={() => logValue("test")}>Log value</Button> <Button onClick={() => logValue("test")}>Log value</Button>
<Button onClick={() => generateUuid()}>Generate UUID</Button> <Button onClick={() => generateUuid()}>Generate UUID</Button>
</Flex> </Flex>
+35
View File
@@ -0,0 +1,35 @@
import { TodosContext } from "@/todos/reffuse"
import { TodosState } from "@/todos/services"
import { VTodos } from "@/todos/views/VTodos"
import { Container } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router"
import { Console, Effect, Layer } from "effect"
import { useMemo } from "react"
export const Route = createFileRoute("/todos")({
component: Todos
})
function Todos() {
const todosLayer = useMemo(() => Layer.empty.pipe(
Layer.provideMerge(TodosState.make("todos")),
Layer.merge(Layer.effectDiscard(
Effect.addFinalizer(() => Console.log("TodosContext cleaned up")).pipe(
Effect.andThen(Console.log("TodosContext constructed"))
)
)),
), [])
return (
<Container>
<TodosContext.Provider layer={todosLayer}>
<VTodos />
</TodosContext.Provider>
</Container>
)
}
+2 -2
View File
@@ -1,10 +1,10 @@
import { GlobalReffuse } from "@/reffuse" import { RootReffuse } from "@/reffuse"
import { Reffuse, ReffuseContext } from "reffuse" import { Reffuse, ReffuseContext } from "reffuse"
import { TodosState } from "./services" import { TodosState } from "./services"
export const TodosContext = ReffuseContext.make<TodosState.TodosState>() export const TodosContext = ReffuseContext.make<TodosState.TodosState>()
export const R = new class TodosReffuse extends GlobalReffuse.pipe( export const R = new class TodosReffuse extends RootReffuse.pipe(
Reffuse.withContexts(TodosContext) Reffuse.withContexts(TodosContext)
) {} ) {}
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/extension-lazyref", "name": "@reffuse/extension-lazyref",
"version": "0.1.0", "version": "0.1.4",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -37,6 +37,6 @@
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"effect": "^3.13.0", "effect": "^3.13.0",
"react": "^19.0.0", "react": "^19.0.0",
"reffuse": "^0.1.3" "reffuse": "^0.1.8"
} }
} }
+37 -8
View File
@@ -1,20 +1,49 @@
import * as LazyRef from "@typed/lazy-ref" import * as LazyRef from "@typed/lazy-ref"
import { Effect, Stream } from "effect" import { Effect, pipe, Stream } from "effect"
import * as React from "react" import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers, SetStateAction } from "reffuse" import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import { SetStateAction } from "reffuse/types"
export const LazyRefExtension = ReffuseExtension.make(() => ({ export const LazyRefExtension = ReffuseExtension.make(() => ({
useSubscribeLazyRefs<
const Refs extends readonly LazyRef.LazyRef<any>[],
R,
>(
this: ReffuseNamespace.ReffuseNamespace<R>,
...refs: Refs
): [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }] {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly LazyRef.LazyRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }])
this.useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v as [...{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }]))
),
), refs)
return reactStateValue
},
useLazyRefState<A, E, R>( useLazyRefState<A, E, R>(
this: ReffuseHelpers.ReffuseHelpers<R>, this: ReffuseNamespace.ReffuseNamespace<R>,
ref: LazyRef.LazyRef<A, E, R>, ref: LazyRef.LazyRef<A, E, R>,
): [A, React.Dispatch<React.SetStateAction<A>>] { ): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true }) const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
const [reactStateValue, setReactStateValue] = React.useState(initialState) () => ref,
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() => this.useFork(() => Stream.runForEach(
setReactStateValue(v) Stream.changesWith(ref.changes, (x, y) => x === y),
)), [ref]) v => Effect.sync(() => setReactStateValue(v)),
), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) => const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
LazyRef.update(ref, prevState => LazyRef.update(ref, prevState =>
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@reffuse/extension-query", "name": "@reffuse/extension-query",
"version": "0.1.0", "version": "0.1.3",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -39,6 +39,6 @@
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"effect": "^3.13.0", "effect": "^3.13.0",
"react": "^19.0.0", "react": "^19.0.0",
"reffuse": "^0.1.3" "reffuse": "^0.1.6"
} }
} }
@@ -1,134 +0,0 @@
// import { BrowserStream } from "@effect/platform-browser"
// import * as AsyncData from "@typed/async-data"
// import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
// export interface MutationRunner<K extends readonly unknown[], A, E, R> {
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
// readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
// readonly forkMutate: Effect.Effect<Fiber.RuntimeFiber<void>>
// }
// export interface MakeProps<K extends readonly unknown[], A, E, R> {
// readonly mutation: (...args: K) => Effect.Effect<A, E, R>
// }
// export const make = <K extends readonly unknown[], A, E, R>(
// { key, query }: MakeProps<K, A, E, R>
// ): Effect.Effect<MutationRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
// const context = yield* Effect.context<R>()
// const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>())
// const interrupt = fiberRef.pipe(
// Effect.flatMap(Option.match({
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
// Effect.andThen(Fiber.interrupt(fiber))
// ),
// onNone: () => Effect.void,
// }))
// )
// const forkInterrupt = fiberRef.pipe(
// Effect.flatMap(Option.match({
// onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
// Effect.andThen(Fiber.interrupt(fiber).pipe(
// Effect.asVoid,
// Effect.forkDaemon,
// ))
// ),
// onNone: () => Effect.forkDaemon(Effect.void),
// }))
// )
// const forkFetch = interrupt.pipe(
// Effect.andThen(
// Ref.set(stateRef, AsyncData.loading()).pipe(
// Effect.andThen(latestKeyRef),
// Effect.flatMap(identity),
// Effect.flatMap(key => query(key).pipe(
// Effect.matchCauseEffect({
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
// })
// )),
// Effect.provide(context),
// Effect.fork,
// )
// ),
// Effect.flatMap(fiber =>
// Ref.set(fiberRef, Option.some(fiber)).pipe(
// Effect.andThen(Fiber.join(fiber)),
// Effect.andThen(Ref.set(fiberRef, Option.none())),
// )
// ),
// Effect.forkDaemon,
// )
// const forkRefresh = interrupt.pipe(
// Effect.andThen(
// Ref.update(stateRef, previous => {
// if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
// return AsyncData.refreshing(previous)
// if (AsyncData.isRefreshing(previous))
// return AsyncData.refreshing(previous.previous)
// return AsyncData.loading()
// }).pipe(
// Effect.andThen(latestKeyRef),
// Effect.flatMap(identity),
// Effect.flatMap(key => query(key).pipe(
// Effect.matchCauseEffect({
// onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
// onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
// })
// )),
// Effect.provide(context),
// Effect.fork,
// )
// ),
// Effect.flatMap(fiber =>
// Ref.set(fiberRef, Option.some(fiber)).pipe(
// Effect.andThen(Fiber.join(fiber)),
// Effect.andThen(Ref.set(fiberRef, Option.none())),
// )
// ),
// Effect.forkDaemon,
// )
// const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
// Effect.andThen(Stream.runForEach(key, latestKey =>
// Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
// Effect.andThen(forkFetch)
// )
// ))
// )
// const refreshOnWindowFocus = Stream.runForEach(
// BrowserStream.fromEventListenerWindow("focus"),
// () => forkRefresh,
// )
// return {
// query,
// latestKeyRef,
// stateRef,
// fiberRef,
// forkInterrupt,
// forkFetch,
// forkRefresh,
// fetchOnKeyChange,
// refreshOnWindowFocus,
// }
// })
@@ -0,0 +1,16 @@
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>>()
@@ -0,0 +1,65 @@
import { Context, Effect, Layer } from "effect"
import type { Mutable } from "effect/Types"
import * as QueryErrorHandler from "./QueryErrorHandler.js"
export interface QueryClient<FallbackA, HandledE> {
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
}
const id = "@reffuse/extension-query/QueryClient"
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
TagClassShape<FallbackA, HandledE>,
typeof id,
QueryClient<FallbackA, HandledE>
>
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
export interface ServiceProps<EH, FallbackA, HandledE> {
readonly ErrorHandler?: Context.Tag<EH, QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>>
}
export interface ServiceResult<Self, EH, FallbackA, HandledE> 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
>
}
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>
(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>)
)
return TagClass
}
)
@@ -0,0 +1,65 @@
import { Cause, Context, Effect, identity, Layer, Queue, Stream } from "effect"
import type { Mutable } from "effect/Types"
export interface QueryErrorHandler<FallbackA, HandledE> {
readonly errors: Stream.Stream<Cause.Cause<HandledE>>
readonly handle: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R>
}
export type Fallback<T> = T extends QueryErrorHandler<infer A, any> ? A : never
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
export 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,
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>
(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) => Queue.offer(queue, Cause.fail(failure)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
(defect: unknown) => Queue.offer(queue, Cause.die(defect)).pipe(
Effect.andThen(Effect.failCause(Cause.empty))
),
)
return { errors, handle }
}))
return TagClass
}
)
export class DefaultQueryErrorHandler extends Service<DefaultQueryErrorHandler>()(
"@reffuse/extension-query/DefaultQueryErrorHandler",
identity,
) {}
+78 -11
View File
@@ -1,21 +1,28 @@
import type * as AsyncData from "@typed/async-data" 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 { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import { ReffuseExtension, type ReffuseHelpers } from "reffuse" import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
import * as QueryRunner from "./QueryRunner.js" import type * as MutationService from "./MutationService.js"
import * as QueryClient from "./QueryClient.js"
import type * as QueryProgress from "./QueryProgress.js"
import type * as QueryService from "./QueryService.js" import type * as QueryService from "./QueryService.js"
import { MutationRunner, QueryRunner } from "./internal/index.js"
export interface UseQueryProps<K extends readonly unknown[], A, E, R> { export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
readonly key: Stream.Stream<K> readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R> readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
readonly refreshOnWindowFocus?: boolean readonly refreshOnWindowFocus?: boolean
} }
export interface UseQueryResult<K extends readonly unknown[], A, E> { export interface UseQueryResult<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>> readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>> readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
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>( readonly layer: <Self, Id extends string>(
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>> tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
@@ -23,12 +30,40 @@ export interface UseQueryResult<K extends readonly unknown[], A, E> {
} }
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(() => ({ export const QueryExtension = ReffuseExtension.make(() => ({
useQuery<K extends readonly unknown[], A, E, R>( useQuery<
this: ReffuseHelpers.ReffuseHelpers<R>, QK extends readonly unknown[],
props: UseQueryProps<K, A, E, R>, QA,
): UseQueryResult<K, A, E> { FallbackA,
QE,
HandledE,
QR extends R,
R,
>(
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
props: UseQueryProps<QK, QA, QE, QR>,
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
const runner = this.useMemo(() => QueryRunner.make({ const runner = this.useMemo(() => QueryRunner.make({
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
key: props.key, key: props.key,
query: props.query, query: props.query,
}), [props.key]) }), [props.key])
@@ -43,13 +78,45 @@ export const QueryExtension = ReffuseExtension.make(() => ({
return React.useMemo(() => ({ return React.useMemo(() => ({
latestKey: runner.latestKeyRef, latestKey: runner.latestKeyRef,
state: runner.stateRef, state: runner.stateRef,
refresh: runner.forkRefresh,
forkRefresh: runner.forkRefresh,
layer: tag => Layer.succeed(tag, { layer: tag => Layer.succeed(tag, {
latestKey: runner.latestKeyRef, latestKey: runner.latestKeyRef,
state: runner.stateRef, state: runner.stateRef,
refresh: runner.forkRefresh, forkRefresh: runner.forkRefresh,
}), }),
}), [runner]) }), [runner])
} },
useMutation<
QK extends readonly unknown[],
QA,
FallbackA,
QE,
HandledE,
QR extends R,
R,
>(
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({
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])
},
})) }))
@@ -0,0 +1,37 @@
import * as AsyncData from "@typed/async-data"
import { Effect, flow, Layer, Match, Option } from "effect"
import { QueryState } from "./internal/index.js"
export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryProgress")<QueryProgress, {
readonly get: Effect.Effect<Option.Option<AsyncData.Progress>>
readonly update: (
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
) => Effect.Effect<void>
}>() {
static readonly Live: Layer.Layer<
QueryProgress,
never,
QueryState.QueryState<any, any>
> = Layer.effect(this, Effect.gen(function*() {
const state = yield* QueryState.makeTag()
const get = state.get.pipe(
Effect.map(flow(Match.value,
Match.tag("Loading", v => v.progress),
Match.tag("Refreshing", v => v.progress),
Match.orElse(() => Option.none()),
))
)
const update = (f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress) => get.pipe(
Effect.map(f),
Effect.flatMap(progress => state.update(previous =>
AsyncData.updateProgress(previous, progress)
)),
)
return { get, update }
}))
}
-144
View File
@@ -1,144 +0,0 @@
import { BrowserStream } from "@effect/platform-browser"
import * as AsyncData from "@typed/async-data"
import { type Cause, Effect, Fiber, identity, Option, Ref, type Scope, Stream, SubscriptionRef } from "effect"
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
readonly query: (key: K) => Effect.Effect<A, E, R>
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>>
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly forkFetch: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly forkRefresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
readonly refreshOnWindowFocus: Effect.Effect<void>
}
export interface MakeProps<K extends readonly unknown[], A, E, R> {
readonly key: Stream.Stream<K>
readonly query: (key: K) => Effect.Effect<A, E, R>
}
export const make = <K extends readonly unknown[], A, E, R>(
{ key, query }: MakeProps<K, A, E, R>
): Effect.Effect<QueryRunner<K, A, E, R>, never, R> => Effect.gen(function*() {
const context = yield* Effect.context<R>()
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A, E>())
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>())
const interrupt = fiberRef.pipe(
Effect.flatMap(Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber))
),
onNone: () => Effect.void,
}))
)
const forkInterrupt = fiberRef.pipe(
Effect.flatMap(Option.match({
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
Effect.andThen(Fiber.interrupt(fiber).pipe(
Effect.asVoid,
Effect.forkDaemon,
))
),
onNone: () => Effect.forkDaemon(Effect.void),
}))
)
const forkFetch = interrupt.pipe(
Effect.andThen(
Ref.set(stateRef, AsyncData.loading()).pipe(
Effect.andThen(latestKeyRef),
Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe(
Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
})
)),
Effect.provide(context),
Effect.fork,
)
),
Effect.flatMap(fiber =>
Ref.set(fiberRef, Option.some(fiber)).pipe(
Effect.andThen(Fiber.join(fiber)),
Effect.andThen(Ref.set(fiberRef, Option.none())),
)
),
Effect.forkDaemon,
)
const forkRefresh = interrupt.pipe(
Effect.andThen(
Ref.update(stateRef, previous => {
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
return AsyncData.refreshing(previous)
if (AsyncData.isRefreshing(previous))
return AsyncData.refreshing(previous.previous)
return AsyncData.loading()
}).pipe(
Effect.andThen(latestKeyRef),
Effect.flatMap(identity),
Effect.flatMap(key => query(key).pipe(
Effect.matchCauseEffect({
onSuccess: v => Ref.set(stateRef, AsyncData.success(v)),
onFailure: c => Ref.set(stateRef, AsyncData.failure(c)),
})
)),
Effect.provide(context),
Effect.fork,
)
),
Effect.flatMap(fiber =>
Ref.set(fiberRef, Option.some(fiber)).pipe(
Effect.andThen(Fiber.join(fiber)),
Effect.andThen(Ref.set(fiberRef, Option.none())),
)
),
Effect.forkDaemon,
)
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
Effect.andThen(Stream.runForEach(key, latestKey =>
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
Effect.andThen(forkFetch)
)
))
)
const refreshOnWindowFocus = Stream.runForEach(
BrowserStream.fromEventListenerWindow("focus"),
() => forkRefresh,
)
return {
query,
latestKeyRef,
stateRef,
fiberRef,
forkInterrupt,
forkFetch,
forkRefresh,
fetchOnKeyChange,
refreshOnWindowFocus,
}
})
+5 -21
View File
@@ -1,32 +1,16 @@
import type * as AsyncData from "@typed/async-data" import type * as AsyncData from "@typed/async-data"
import { type Cause, Effect, type Fiber, type Option, type SubscriptionRef } from "effect" import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect"
export interface QueryService<K extends readonly unknown[], A, E> { export interface QueryService<K extends readonly unknown[], A, E> {
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>> readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>> readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly refresh: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>> 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) => < export const Tag = <const Id extends string>(id: Id) => <
Self, K extends readonly unknown[], A, E = never, Self, K extends readonly unknown[], A, E = never,
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>() >() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
// export interface LayerProps<A, E, R> {
// readonly query: Effect.Effect<A, E, R>
// }
// export const layer = <Self, Id extends string, A, E, R>(
// tag: Context.TagClass<Self, Id, QueryService<A, E>>,
// props: LayerProps<A, E, R>,
// ): Layer.Layer<Self, never, R> => Layer.effect(tag, Effect.gen(function*() {
// const runner = yield* QueryRunner.make({
// query: props.query
// })
// return {
// state: runner.stateRef,
// refresh: runner.forkRefresh,
// }
// }))
+4 -1
View File
@@ -1,3 +1,6 @@
export * as MutationService from "./MutationService.js"
export * as QueryClient from "./QueryClient.js"
export * as QueryErrorHandler from "./QueryErrorHandler.js"
export * from "./QueryExtension.js" export * from "./QueryExtension.js"
export * as QueryRunner from "./QueryRunner.js" export * as QueryProgress from "./QueryProgress.js"
export * as QueryService from "./QueryService.js" export * as QueryService from "./QueryService.js"
@@ -0,0 +1,98 @@
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"
export interface MutationRunner<K extends readonly unknown[], A, E, R> {
readonly context: Context.Context<R>
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
state: Stream.Stream<AsyncData.AsyncData<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>
}
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
{
QueryClient,
mutation,
}: MakeProps<K, A, FallbackA, E, HandledE, R>
): Effect.Effect<
MutationRunner<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 globalStateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
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(
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)
),
}),
)),
Effect.provide(context),
Effect.provide(QueryProgress.QueryProgress.Live),
)
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
queryStateTag,
globalStateRef,
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 }) =>
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
Effect.andThen(run(key)),
Effect.scoped,
Effect.forkDaemon,
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)),
Effect.andThen(Ref.set(globalStateRef, value)),
),
)),
)
)
)
return {
context,
stateRef: globalStateRef,
mutate,
forkMutate,
}
})
@@ -0,0 +1,191 @@
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,
}
})
@@ -0,0 +1,24 @@
import type * as AsyncData from "@typed/async-data"
import { Context, Effect, Layer } from "effect"
export interface QueryState<A, E> {
readonly get: Effect.Effect<AsyncData.AsyncData<A, E>>
readonly set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>
readonly update: (f: (previous: AsyncData.AsyncData<A, E>) => AsyncData.AsyncData<A, E>) => Effect.Effect<void>
}
export const makeTag = <A, E>(): Context.Tag<QueryState<A, E>, QueryState<A, E>> => Context.GenericTag("@reffuse/query-extension/QueryState")
export const layer = <A, E>(
tag: Context.Tag<QueryState<A, E>, QueryState<A, E>>,
get: Effect.Effect<AsyncData.AsyncData<A, E>>,
set: (value: AsyncData.AsyncData<A, E>) => Effect.Effect<void>,
): Layer.Layer<QueryState<A, E>> => Layer.succeed(tag, {
get,
set,
update: f => get.pipe(
Effect.map(f),
Effect.flatMap(set),
),
})
@@ -0,0 +1,3 @@
export * as MutationRunner from "./MutationRunner.js"
export * as QueryRunner from "./QueryRunner.js"
export * as QueryState from "./QueryState.js"
@@ -1,14 +0,0 @@
Create a "ReadMe" guide for the project, named "{{ projectName }}".
Include the following:
Title, Description,
Getting Started by installing npm package, how to run it with npx
Configuration is optional and will be generated on first run
Reporting bugs, repository and homepage
Versioning
Authors
License
This is the entry file:
###
{{ entryFileContent }}
###
@@ -1,4 +0,0 @@
Show how developer would add HowTo in config file,
args property is used to inject properties into liquid template, any property set in args can be access in liquid template with {{ keyName }}
file property appends extracted content of a file to liquid template, using JSONPath or the extract property that uses LLM to extract content from file
Developers MUST create a liquid template in .code-narrator/gpt_questions, this template file is used to ask GPT question
@@ -1,4 +0,0 @@
Give title and short description that this is an overview file for files located in directory
Give short description of each file that is provided
Add link to each file, link should be the filename
-101
View File
@@ -1,101 +0,0 @@
const ConfigurationBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Configuration/ConfigurationBuilder");
const FilesBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Files/FilesBuilder");
const FoldersBuilder = require("code-narrator/dist/src/documentation/plugins/builders/Folders/FoldersBuilder");
const UserDefinedBuilder = require("code-narrator/dist/src/documentation/plugins/builders/UserDefined/UserDefinedBuilder");
/**
* You can find the documentation about code-narrator.config.js at
* https://github.com/ingig/code-narrator/blob/master/docs/Configuration/code-narrator.config.js.md
*
* @type {ICodeNarratorConfig}
*/
const config = {
// App specific configuration files. This could be something like project_name.json
config_files: [
],
project_file: "package.json",
entry_file: "./dist/index.js",
cli_file: "",
project_path: "./",
source_path: "src",
documentation_path: "./docs",
test_path: "test",
exclude: [
"/node_modules",
".env",
"/.idea",
"/.git",
".gitignore",
"/.code-narrator",
"/dist",
"/build",
"package-lock.json",
],
// Indicates if the documentation should create a README file in root of project
readmeRoot: true,
// Url to the repository, code-narrator tries to extract this from project file
repository_url: "git+https://github.com/Thiladev/reffuse.git",
// These are the plugins used when building documentation. You can create your own plugin. Checkout the code-narrator docs HowTo create a builder plugin
builderPlugins: [
ConfigurationBuilder,
FilesBuilder,
FoldersBuilder,
UserDefinedBuilder,
],
// These are system commends send to GPT with every query
gptSystemCommands: [
"Act as a documentation expert for software",
"If there is :::note, :::info, :::caution, :::tip, :::danger in the text, extract that from its location and format it correctly",
"Return your answer in {DocumentationType} format",
"If you notice any secret information, replace it with ***** in your response",
],
documentation_type: "md",
document_file_extension: ".md",
folderRootFileName: "README",
cache_file: ".code-narrator/cache.json",
gptModel: "gpt-4",
aiService: undefined,
project_name: "reffuse",
include: [
"src/**/*",
],
// Array of user defined documentations. See code-narrator How to create a user defined builder
builders: [
{
name: "README",
type: "README",
template: "README",
sidebarPosition: 1,
args: {
entryFileContent: "content(package.json)",
aiService: undefined,
},
aiService: undefined,
},
{
name: "HowTo Overview",
type: "README",
template: "overview_readme",
path: "howto",
files: [
{
path: "howto/*.md",
aiService: undefined,
},
],
pages: [
{
name: "HowTo Example",
type: "howto",
template: "howto_create_howto",
aiService: undefined,
},
],
aiService: undefined,
},
],
}
module.exports = config;
+5 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "reffuse", "name": "reffuse",
"version": "0.1.3", "version": "0.1.9",
"type": "module", "type": "module",
"files": [ "files": [
"./README.md", "./README.md",
@@ -16,6 +16,10 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"default": "./dist/index.js" "default": "./dist/index.js"
}, },
"./types": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"./*": { "./*": {
"types": "./dist/*.d.ts", "types": "./dist/*.d.ts",
"default": "./dist/*.js" "default": "./dist/*.js"
+13 -12
View File
@@ -1,42 +1,42 @@
import type * as ReffuseContext from "./ReffuseContext.js" import type * as ReffuseContext from "./ReffuseContext.js"
import type * as ReffuseExtension from "./ReffuseExtension.js" import type * as ReffuseExtension from "./ReffuseExtension.js"
import * as ReffuseHelpers from "./ReffuseHelpers.js" import * as ReffuseNamespace from "./ReffuseNamespace.js"
import type { Merge, StaticType } from "./types.js" import type { Merge, StaticType } from "./utils.js"
export class Reffuse extends ReffuseHelpers.make() {} export class Reffuse extends ReffuseNamespace.makeClass() {}
export const withContexts = <R2 extends Array<unknown>>( export const withContexts = <R2 extends Array<unknown>>(
...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }] ...contexts: [...{ [K in keyof R2]: ReffuseContext.ReffuseContext<R2[K]> }]
) => ) => (
< <
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R1>, BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R1>,
R1 R1
>( >(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R1> self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R1>
): ( ): (
{ {
new(): Merge< new(): Merge<
InstanceType<BaseClass>, InstanceType<BaseClass>,
{ constructor: ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]> } { constructor: ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]> }
> >
} & } &
Merge< Merge<
StaticType<BaseClass>, StaticType<BaseClass>,
StaticType<ReffuseHelpers.ReffuseHelpersClass<R1 | R2[number]>> StaticType<ReffuseNamespace.ReffuseNamespaceClass<R1 | R2[number]>>
> >
) => class extends self { ) => class extends self {
static readonly contexts = [...self.contexts, ...contexts] static readonly contexts = [...self.contexts, ...contexts]
} as any } as any
)
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) => (
export const withExtension = <A extends object>(extension: ReffuseExtension.ReffuseExtension<A>) =>
< <
BaseClass extends ReffuseHelpers.ReffuseHelpersClass<R>, BaseClass extends ReffuseNamespace.ReffuseNamespaceClass<R>,
R R
>( >(
self: BaseClass & ReffuseHelpers.ReffuseHelpersClass<R> self: BaseClass & ReffuseNamespace.ReffuseNamespaceClass<R>
): ( ): (
{ new(): Merge<InstanceType<BaseClass>, A> } & { new(): Merge<InstanceType<BaseClass>, A> } &
StaticType<BaseClass> StaticType<BaseClass>
@@ -45,3 +45,4 @@ export const withExtension = <A extends object>(extension: ReffuseExtension.Reff
Object.assign(class_.prototype, extension()) Object.assign(class_.prototype, extension())
return class_ as any return class_ as any
} }
)
+160
View File
@@ -0,0 +1,160 @@
import { Array, Context, Effect, ExecutionStrategy, Exit, Layer, Ref, Runtime, Scope } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider = makeProvider(this.Context)
readonly AsyncProvider = makeAsyncProvider(this.Context)
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly children?: React.ReactNode
}>
const makeProvider = <R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> => {
return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const makeScope = React.useMemo(() => props.scope
? Scope.fork(props.scope, ExecutionStrategy.sequential)
: Scope.make(),
[props.scope])
const makeContext = React.useCallback((scope: Scope.CloseableScope) => Effect.context<R>().pipe(
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
), [props.layer])
const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
Effect.bind("isInitialRun", () => Ref.make(true)),
Effect.bind("scope", () => makeScope),
Effect.bind("context", ({ scope }) => makeContext(scope)),
Effect.map(({ isInitialRun, scope, context }) => [isInitialRun, scope, context] as const),
runSync,
), [])
const [value, setValue] = React.useState(initialValue)
React.useEffect(() => isInitialRun.pipe(
Effect.if({
onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
)
),
onFalse: () => Effect.Do.pipe(
Effect.bind("scope", () => makeScope),
Effect.bind("context", ({ scope }) => makeContext(scope)),
Effect.tap(({ context }) =>
Effect.sync(() => setValue(context))
),
Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
),
),
}),
runSync,
), [makeScope, makeContext, runSync])
return React.createElement(Context, { ...props, value })
}
}
export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown, Scope.Scope>
readonly scope?: Scope.Scope
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
const makeAsyncProvider = <R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> => {
function ReffuseContextAsyncReactProviderInner({ promise, children }: {
readonly promise: Promise<Context.Context<R>>
readonly children?: React.ReactNode
}) {
return React.createElement(Context, {
value: React.use(promise),
children,
})
}
return function ReffuseContextAsyncReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const runSync = React.useMemo(() => Runtime.runSync(runtime), [runtime])
const runFork = React.useMemo(() => Runtime.runFork(runtime), [runtime])
const [promise, setPromise] = React.useState(Promise.withResolvers<Context.Context<R>>().promise)
React.useEffect(() => {
const { promise, resolve, reject } = Promise.withResolvers<Context.Context<R>>()
setPromise(promise)
const scope = runSync(props.scope
? Scope.fork(props.scope, props.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
: Scope.make(props.finalizerExecutionStrategy)
)
Effect.context<R>().pipe(
Effect.match({
onSuccess: resolve,
onFailure: reject,
}),
Effect.provide(props.layer),
Effect.provideService(Scope.Scope, scope),
effect => runFork(effect, { ...props, scope }),
)
return () => { runFork(Scope.close(scope, Exit.void)) }
}, [props.layer, runSync, runFork])
return React.createElement(React.Suspense, {
children: React.createElement(ReffuseContextAsyncReactProviderInner, { ...props, promise }),
fallback: props.fallback,
})
}
}
export const make = <R = never>() => new ReffuseContext<R>()
export const useMergeAll = <T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Context.Context<T[number]> => {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export const useMergeAllLayers = <T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Layer.Layer<T[number]> => {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Array.isNonEmptyArray(values)
? Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
)
: Layer.empty as Layer.Layer<T[number]>,
values)
}
-111
View File
@@ -1,111 +0,0 @@
import { Array, Context, Effect, Layer, Runtime } from "effect"
import * as React from "react"
import * as ReffuseRuntime from "./ReffuseRuntime.js"
export class ReffuseContext<R> {
readonly Context = React.createContext<Context.Context<R>>(null!)
readonly Provider = makeProvider(this.Context)
readonly AsyncProvider = makeAsyncProvider(this.Context)
useContext(): Context.Context<R> {
return React.useContext(this.Context)
}
useLayer(): Layer.Layer<R> {
const context = this.useContext()
return React.useMemo(() => Layer.effectContext(Effect.succeed(context)), [context])
}
}
export type R<T> = T extends ReffuseContext<infer R> ? R : never
export type ReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly children?: React.ReactNode
}>
function makeProvider<R>(Context: React.Context<Context.Context<R>>): ReactProvider<R> {
return function ReffuseContextReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const value = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runSync(runtime),
), [props.layer, runtime])
return (
<Context
{...props}
value={value}
/>
)
}
}
export type AsyncReactProvider<R> = React.FC<{
readonly layer: Layer.Layer<R, unknown>
readonly fallback?: React.ReactNode
readonly children?: React.ReactNode
}>
function makeAsyncProvider<R>(Context: React.Context<Context.Context<R>>): AsyncReactProvider<R> {
function Inner({ promise, children }: {
readonly promise: Promise<Context.Context<R>>
readonly children?: React.ReactNode
}) {
const value = React.use(promise)
return (
<Context
value={value}
children={children}
/>
)
}
return function ReffuseContextAsyncReactProvider(props) {
const runtime = ReffuseRuntime.useRuntime()
const promise = React.useMemo(() => Effect.context<R>().pipe(
Effect.provide(props.layer),
Runtime.runPromise(runtime),
), [props.layer, runtime])
return (
<React.Suspense fallback={props.fallback}>
<Inner
{...props}
promise={promise}
/>
</React.Suspense>
)
}
}
export function make<R = never>() {
return new ReffuseContext<R>()
}
export function useMergeAll<T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Context.Context<T[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Context.mergeAll(...values), values)
}
export function useMergeAllLayers<T extends Array<unknown>>(
...contexts: [...{ [K in keyof T]: ReffuseContext<T[K]> }]
): Layer.Layer<T[number]> {
const values = contexts.map(v => React.use(v.Context))
return React.useMemo(() => Array.isNonEmptyArray(values)
? Layer.mergeAll(
...Array.map(values, context => Layer.effectContext(Effect.succeed(context)))
)
: Layer.empty as Layer.Layer<T[number]>,
values)
}
@@ -1,8 +1,8 @@
import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect" import { type Context, Effect, ExecutionStrategy, Exit, type Fiber, type Layer, Option, pipe, Pipeable, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
import * as React from "react" import * as React from "react"
import * as ReffuseContext from "./ReffuseContext.js" import * as ReffuseContext from "./ReffuseContext.js"
import * as ReffuseRuntime from "./ReffuseRuntime.js" import * as ReffuseRuntime from "./ReffuseRuntime.js"
import * as SetStateAction from "./SetStateAction.js" import { type PropertyPath, SetStateAction, SubscriptionSubRef } from "./types/index.js"
export interface RenderOptions { export interface RenderOptions {
@@ -14,21 +14,32 @@ export interface ScopeOptions {
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
} }
export type RefsA<T extends readonly SubscriptionRef.SubscriptionRef<any>[]> = {
export abstract class ReffuseHelpers<R> { [K in keyof T]: Effect.Effect.Success<T[K]>
declare ["constructor"]: ReffuseHelpersClass<R> }
useContext<R>(this: ReffuseHelpers<R>): Context.Context<R> { export abstract class ReffuseNamespace<R> {
declare ["constructor"]: ReffuseNamespaceClass<R>
constructor() {
this.SubRef = this.SubRef.bind(this as any) as any
this.SubscribeRefs = this.SubscribeRefs.bind(this as any) as any
this.RefState = this.RefState.bind(this as any) as any
this.SubscribeStream = this.SubscribeStream.bind(this as any) as any
}
useContext<R>(this: ReffuseNamespace<R>): Context.Context<R> {
return ReffuseContext.useMergeAll(...this.constructor.contexts) return ReffuseContext.useMergeAll(...this.constructor.contexts)
} }
useLayer<R>(this: ReffuseHelpers<R>): Layer.Layer<R> { useLayer<R>(this: ReffuseNamespace<R>): Layer.Layer<R> {
return ReffuseContext.useMergeAllLayers(...this.constructor.contexts) return ReffuseContext.useMergeAllLayers(...this.constructor.contexts)
} }
useRunSync<R>(this: ReffuseHelpers<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A { useRunSync<R>(this: ReffuseNamespace<R>): <A, E>(effect: Effect.Effect<A, E, R>) => A {
const runtime = ReffuseRuntime.useRuntime() const runtime = ReffuseRuntime.useRuntime()
const context = this.useContext() const context = this.useContext()
@@ -38,7 +49,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunPromise<R>(this: ReffuseHelpers<R>): <A, E>( useRunPromise<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: { readonly signal?: AbortSignal }, options?: { readonly signal?: AbortSignal },
) => Promise<A> { ) => Promise<A> {
@@ -51,7 +62,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunFork<R>(this: ReffuseHelpers<R>): <A, E>( useRunFork<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions, options?: Runtime.RunForkOptions,
) => Fiber.RuntimeFiber<A, E> { ) => Fiber.RuntimeFiber<A, E> {
@@ -64,7 +75,7 @@ export abstract class ReffuseHelpers<R> {
), [runtime, context]) ), [runtime, context])
} }
useRunCallback<R>(this: ReffuseHelpers<R>): <A, E>( useRunCallback<R>(this: ReffuseNamespace<R>): <A, E>(
effect: Effect.Effect<A, E, R>, effect: Effect.Effect<A, E, R>,
options?: Runtime.RunCallbackOptions<A, E>, options?: Runtime.RunCallbackOptions<A, E>,
) => Runtime.Cancel<A, E> { ) => Runtime.Cancel<A, E> {
@@ -87,7 +98,7 @@ export abstract class ReffuseHelpers<R> {
* You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`. * You can disable this behavior by setting `doNotReExecuteOnRuntimeOrContextChange` to `true` in `options`.
*/ */
useMemo<A, E, R>( useMemo<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R>, effect: () => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions, options?: RenderOptions,
@@ -101,48 +112,45 @@ export abstract class ReffuseHelpers<R> {
} }
useMemoScoped<A, E, R>( useMemoScoped<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
): A { ): A {
const runSync = this.useRunSync() const runSync = this.useRunSync()
// Calculate an initial version of the value so that it can be accessed during the first render const [isInitialRun, initialScope, initialValue] = React.useMemo(() => Effect.Do.pipe(
const [initialScope, initialValue] = React.useMemo(() => Scope.make(options?.finalizerExecutionStrategy).pipe( Effect.bind("isInitialRun", () => Ref.make(true)),
Effect.flatMap(scope => effect().pipe( Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.provideService(Scope.Scope, scope), Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.map(value => [scope, value] as const), Effect.map(({ isInitialRun, scope, value }) => [isInitialRun, scope, value] as const),
)),
runSync, runSync,
), []) ), [])
// Keep track of the state of the initial scope
const initialScopeClosed = React.useRef(false)
const [value, setValue] = React.useState(initialValue) const [value, setValue] = React.useState(initialValue)
React.useEffect(() => { React.useEffect(() => isInitialRun.pipe(
const closeInitialScopeIfNeeded = Scope.close(initialScope, Exit.void).pipe( Effect.if({
Effect.andThen(Effect.sync(() => { initialScopeClosed.current = true })), onTrue: () => Ref.set(isInitialRun, false).pipe(
Effect.when(() => !initialScopeClosed.current), Effect.map(() =>
() => runSync(Scope.close(initialScope, Exit.void))
) )
),
const [scope, value] = closeInitialScopeIfNeeded.pipe( onFalse: () => Effect.Do.pipe(
Effect.andThen(Scope.make(options?.finalizerExecutionStrategy).pipe( Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy)),
Effect.flatMap(scope => effect().pipe( Effect.bind("value", ({ scope }) => Effect.provideService(effect(), Scope.Scope, scope)),
Effect.provideService(Scope.Scope, scope), Effect.tap(({ value }) =>
Effect.map(value => [scope, value] as const), Effect.sync(() => setValue(value))
)) ),
)), Effect.map(({ scope }) =>
() => runSync(Scope.close(scope, Exit.void))
),
),
}),
runSync, runSync,
) ), [
setValue(value)
return () => { runSync(Scope.close(scope, Exit.void)) }
}, [
...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync], ...options?.doNotReExecuteOnRuntimeOrContextChange ? [] : [runSync],
...deps, ...deps,
]) ])
@@ -177,7 +185,7 @@ export abstract class ReffuseHelpers<R> {
* ``` * ```
*/ */
useEffect<A, E, R>( useEffect<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
@@ -225,7 +233,7 @@ export abstract class ReffuseHelpers<R> {
* ``` * ```
*/ */
useLayoutEffect<A, E, R>( useLayoutEffect<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: RenderOptions & ScopeOptions, options?: RenderOptions & ScopeOptions,
@@ -273,7 +281,7 @@ export abstract class ReffuseHelpers<R> {
* ``` * ```
*/ */
useFork<A, E, R>( useFork<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions, options?: Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -296,7 +304,7 @@ export abstract class ReffuseHelpers<R> {
} }
usePromise<A, E, R>( usePromise<A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
effect: () => Effect.Effect<A, E, R | Scope.Scope>, effect: () => Effect.Effect<A, E, R | Scope.Scope>,
deps?: React.DependencyList, deps?: React.DependencyList,
options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions, options?: { readonly signal?: AbortSignal } & Runtime.RunForkOptions & RenderOptions & ScopeOptions,
@@ -343,7 +351,7 @@ export abstract class ReffuseHelpers<R> {
} }
useCallbackSync<Args extends unknown[], A, E, R>( useCallbackSync<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>, callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: RenderOptions, options?: RenderOptions,
@@ -357,7 +365,7 @@ export abstract class ReffuseHelpers<R> {
} }
useCallbackPromise<Args extends unknown[], A, E, R>( useCallbackPromise<Args extends unknown[], A, E, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
callback: (...args: Args) => Effect.Effect<A, E, R>, callback: (...args: Args) => Effect.Effect<A, E, R>,
deps: React.DependencyList, deps: React.DependencyList,
options?: { readonly signal?: AbortSignal } & RenderOptions, options?: { readonly signal?: AbortSignal } & RenderOptions,
@@ -371,7 +379,7 @@ export abstract class ReffuseHelpers<R> {
} }
useRef<A, R>( useRef<A, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
value: A, value: A,
): SubscriptionRef.SubscriptionRef<A> { ): SubscriptionRef.SubscriptionRef<A> {
return this.useMemo( return this.useMemo(
@@ -381,6 +389,41 @@ export abstract class ReffuseHelpers<R> {
) )
} }
useSubRef<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],
)
}
useSubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
>(
this: ReffuseNamespace<R>,
...refs: Refs
): RefsA<Refs> {
const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
() => Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[]),
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
) as RefsA<Refs>)
this.useFork(() => pipe(
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
streams => Stream.zipLatestAll(...streams),
Stream.runForEach(v =>
Effect.sync(() => setReactStateValue(v as RefsA<Refs>))
),
), refs)
return reactStateValue
}
/** /**
* Binds the state of a `SubscriptionRef` to the state of the React component. * Binds the state of a `SubscriptionRef` to the state of the React component.
* *
@@ -389,15 +432,19 @@ export abstract class ReffuseHelpers<R> {
* Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render. * Note that the rules of React's immutable state still apply: updating a ref with the same value will not trigger a re-render.
*/ */
useRefState<A, R>( useRefState<A, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
ref: SubscriptionRef.SubscriptionRef<A>, ref: SubscriptionRef.SubscriptionRef<A>,
): [A, React.Dispatch<React.SetStateAction<A>>] { ): [A, React.Dispatch<React.SetStateAction<A>>] {
const initialState = this.useMemo(() => ref, [], { doNotReExecuteOnRuntimeOrContextChange: true }) const [reactStateValue, setReactStateValue] = React.useState(this.useMemo(
const [reactStateValue, setReactStateValue] = React.useState(initialState) () => ref,
[],
{ doNotReExecuteOnRuntimeOrContextChange: true },
))
this.useFork(() => Stream.runForEach(ref.changes, v => Effect.sync(() => this.useFork(() => Stream.runForEach(
setReactStateValue(v) Stream.changesWith(ref.changes, (x, y) => x === y),
)), [ref]) v => Effect.sync(() => setReactStateValue(v)),
), [ref])
const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) => const setValue = this.useCallbackSync((setStateAction: React.SetStateAction<A>) =>
Ref.update(ref, prevState => Ref.update(ref, prevState =>
@@ -409,7 +456,7 @@ export abstract class ReffuseHelpers<R> {
} }
useStreamFromValues<const A extends React.DependencyList, R>( useStreamFromValues<const A extends React.DependencyList, R>(
this: ReffuseHelpers<R>, this: ReffuseNamespace<R>,
values: A, values: A,
): Stream.Stream<A> { ): Stream.Stream<A> {
const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe( const [queue, stream] = this.useMemo(() => Queue.unbounded<A>().pipe(
@@ -420,27 +467,94 @@ export abstract class ReffuseHelpers<R> {
return stream return stream
} }
useSubscribeStream<A, InitialA extends A | undefined, 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))
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>
}
SubRef<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.useSubRef(props.parent, props.path))
}
SubscribeRefs<
const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[],
R,
>(
this: ReffuseNamespace<R>,
props: {
readonly refs: Refs
readonly children: (...args: RefsA<Refs>) => React.ReactNode
},
): React.ReactNode {
return props.children(...this.useSubscribeRefs(...props.refs))
}
RefState<A, R>(
this: ReffuseNamespace<R>,
props: {
readonly ref: SubscriptionRef.SubscriptionRef<A>
readonly children: (state: [A, React.Dispatch<React.SetStateAction<A>>]) => React.ReactNode
},
): React.ReactNode {
return props.children(this.useRefState(props.ref))
}
SubscribeStream<A, InitialA extends A | undefined, 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
},
): React.ReactNode {
return props.children(this.useSubscribeStream(props.stream, props.initialValue))
}
} }
export interface ReffuseHelpers<R> extends Pipeable.Pipeable {} export interface ReffuseNamespace<R> extends Pipeable.Pipeable {}
ReffuseHelpers.prototype.pipe = function pipe() { ReffuseNamespace.prototype.pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments) return Pipeable.pipeArguments(this, arguments)
} };
export interface ReffuseHelpersClass<R> extends Pipeable.Pipeable { export interface ReffuseNamespaceClass<R> extends Pipeable.Pipeable {
new(): ReffuseHelpers<R> new(): ReffuseNamespace<R>
make<Self>(this: new () => Self): Self
readonly contexts: readonly ReffuseContext.ReffuseContext<R>[] readonly contexts: readonly ReffuseContext.ReffuseContext<R>[]
} }
(ReffuseHelpers as ReffuseHelpersClass<any>).pipe = function pipe() { (ReffuseNamespace as ReffuseNamespaceClass<any>).make = function make() {
return new this()
};
(ReffuseNamespace as ReffuseNamespaceClass<any>).pipe = function pipe() {
return Pipeable.pipeArguments(this, arguments) return Pipeable.pipeArguments(this, arguments)
} };
export const make = (): ReffuseHelpersClass<never> => export const makeClass = (): ReffuseNamespaceClass<never> => (
class extends (ReffuseHelpers<never> as ReffuseHelpersClass<never>) { class extends (ReffuseNamespace<never> as ReffuseNamespaceClass<never>) {
static readonly contexts = [] static readonly contexts = []
} }
)
+16
View File
@@ -0,0 +1,16 @@
import { Runtime } from "effect"
import * as React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = function ReffuseRuntimeReactProvider(props: {
readonly children?: React.ReactNode
}) {
return React.createElement(Context, {
...props,
value: Runtime.defaultRuntime,
})
}
export const useRuntime = () => React.useContext(Context)
-15
View File
@@ -1,15 +0,0 @@
import { Runtime } from "effect"
import * as React from "react"
export const Context = React.createContext<Runtime.Runtime<never>>(null!)
export const Provider = (props: { readonly children?: React.ReactNode }) => (
<Context
{...props}
value={Runtime.defaultRuntime}
/>
)
Provider.displayName = "ReffuseRuntimeReactProvider"
export const useRuntime = () => React.useContext(Context)
+1 -2
View File
@@ -1,6 +1,5 @@
export * as Reffuse from "./Reffuse.js" export * as Reffuse from "./Reffuse.js"
export * as ReffuseContext from "./ReffuseContext.js" export * as ReffuseContext from "./ReffuseContext.js"
export * as ReffuseExtension from "./ReffuseExtension.js" export * as ReffuseExtension from "./ReffuseExtension.js"
export * as ReffuseHelpers from "./ReffuseHelpers.js" export * as ReffuseNamespace from "./ReffuseNamespace.js"
export * as ReffuseRuntime from "./ReffuseRuntime.js" export * as ReffuseRuntime from "./ReffuseRuntime.js"
export * as SetStateAction from "./SetStateAction.js"
@@ -0,0 +1,94 @@
import { Array, Function, Option, Predicate } from "effect"
export type Paths<T> = [] | (
T extends readonly any[] ? ArrayPaths<T> :
T extends object ? ObjectPaths<T> :
never
)
export type ArrayPaths<T extends readonly any[]> = {
[K in keyof T as K extends number ? K : never]:
| [K]
| [K, ...Paths<T[K]>]
} extends infer O
? O[keyof O]
: never
export type ObjectPaths<T extends object> = {
[K in keyof T as K extends string | number | symbol ? K : never]:
| [K]
| [K, ...Paths<T[K]>]
} 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 = Ref.get(this.parent).pipe(Effect.map(this.getter))
}
commit() {
return this.get
}
get changes(): Stream.Stream<A> {
return this.get.pipe(
Effect.map(a => this.parent.changes.pipe(
Stream.map(this.getter),
s => Stream.concat(Stream.make(a), s),
)),
Stream.unwrap,
)
}
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
return this.modifyEffect(a => Effect.succeed(f(a)))
}
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
return Effect.Do.pipe(
Effect.bind("b", () => Ref.get(this.parent)),
Effect.bind("ca", ({ b }) => f(this.getter(b))),
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
Effect.map(({ ca: [c] }) => c),
)
}
}
export const makeFromGetSet = <A, B>(
parent: SubscriptionRef.SubscriptionRef<B>,
getter: (parentValue: B) => A,
setter: (parentValue: B, value: A) => B,
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
parent: SubscriptionRef.SubscriptionRef<B>,
path: P,
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
parent,
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
)
+3
View File
@@ -0,0 +1,3 @@
export * as PropertyPath from "./PropertyPath.js"
export * as SetStateAction from "./SetStateAction.js"
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
@@ -8,14 +8,4 @@ export type CommonKeys<A, B> = Extract<keyof A, keyof B>
*/ */
export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype"> export type StaticType<T extends abstract new (...args: any) => any> = Omit<T, "prototype">
export type Extend<Super, Self> =
Extendable<Super, Self> extends true
? Omit<Super, CommonKeys<Self, Super>> & Self
: never
export type Extendable<Super, Self> =
Pick<Self, CommonKeys<Self, Super>> extends Pick<Super, CommonKeys<Self, Super>>
? true
: false
export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self export type Merge<Super, Self> = Omit<Super, CommonKeys<Self, Super>> & Self