Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'

This commit is contained in:
-Puter
2026-07-19 03:28:54 +05:30
parent 2daf979036
commit a37e0cc3c9
2163 changed files with 668421 additions and 0 deletions

View File

@@ -0,0 +1,817 @@
# @effect/platform-node
## 4.0.0-beta.98
### Patch Changes
- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]:
- effect@4.0.0-beta.98
- @effect/platform-node-shared@4.0.0-beta.98
## 4.0.0-beta.97
### Patch Changes
- Updated dependencies []:
- effect@4.0.0-beta.97
- @effect/platform-node-shared@4.0.0-beta.97
## 4.0.0-beta.96
### Patch Changes
- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]:
- effect@4.0.0-beta.96
- @effect/platform-node-shared@4.0.0-beta.96
## 4.0.0-beta.95
### Patch Changes
- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]:
- effect@4.0.0-beta.95
- @effect/platform-node-shared@4.0.0-beta.95
## 4.0.0-beta.94
### Patch Changes
- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]:
- effect@4.0.0-beta.94
- @effect/platform-node-shared@4.0.0-beta.94
## 4.0.0-beta.93
### Patch Changes
- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]:
- effect@4.0.0-beta.93
- @effect/platform-node-shared@4.0.0-beta.93
## 4.0.0-beta.92
### Patch Changes
- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]:
- effect@4.0.0-beta.92
- @effect/platform-node-shared@4.0.0-beta.92
## 4.0.0-beta.91
### Patch Changes
- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]:
- effect@4.0.0-beta.91
- @effect/platform-node-shared@4.0.0-beta.91
## 4.0.0-beta.90
### Patch Changes
- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]:
- effect@4.0.0-beta.90
- @effect/platform-node-shared@4.0.0-beta.90
## 4.0.0-beta.89
### Patch Changes
- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]:
- effect@4.0.0-beta.89
- @effect/platform-node-shared@4.0.0-beta.89
## 4.0.0-beta.88
### Patch Changes
- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]:
- effect@4.0.0-beta.88
- @effect/platform-node-shared@4.0.0-beta.88
## 4.0.0-beta.87
### Patch Changes
- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]:
- effect@4.0.0-beta.87
- @effect/platform-node-shared@4.0.0-beta.87
## 4.0.0-beta.86
### Patch Changes
- [#2463](https://github.com/Effect-TS/effect-smol/pull/2463) [`28b4196`](https://github.com/Effect-TS/effect-smol/commit/28b4196390d3ab83be1567b65440919a9061fcc3) Thanks @tim-smart! - Update `NodeHttpServer.layerConfig`'s type to report the same provided Node services as `NodeHttpServer.layer`.
- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]:
- effect@4.0.0-beta.86
- @effect/platform-node-shared@4.0.0-beta.86
## 4.0.0-beta.85
### Patch Changes
- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]:
- effect@4.0.0-beta.85
- @effect/platform-node-shared@4.0.0-beta.85
## 4.0.0-beta.84
### Patch Changes
- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]:
- effect@4.0.0-beta.84
- @effect/platform-node-shared@4.0.0-beta.84
## 4.0.0-beta.83
### Patch Changes
- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]:
- effect@4.0.0-beta.83
- @effect/platform-node-shared@4.0.0-beta.83
## 4.0.0-beta.82
### Patch Changes
- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]:
- effect@4.0.0-beta.82
- @effect/platform-node-shared@4.0.0-beta.82
## 4.0.0-beta.81
### Patch Changes
- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]:
- effect@4.0.0-beta.81
- @effect/platform-node-shared@4.0.0-beta.81
## 4.0.0-beta.80
### Patch Changes
- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]:
- effect@4.0.0-beta.80
- @effect/platform-node-shared@4.0.0-beta.80
## 4.0.0-beta.79
### Patch Changes
- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]:
- effect@4.0.0-beta.79
- @effect/platform-node-shared@4.0.0-beta.79
## 4.0.0-beta.78
### Patch Changes
- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]:
- effect@4.0.0-beta.78
- @effect/platform-node-shared@4.0.0-beta.78
## 4.0.0-beta.77
### Patch Changes
- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]:
- effect@4.0.0-beta.77
- @effect/platform-node-shared@4.0.0-beta.77
## 4.0.0-beta.76
### Patch Changes
- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]:
- effect@4.0.0-beta.76
- @effect/platform-node-shared@4.0.0-beta.76
## 4.0.0-beta.75
### Patch Changes
- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]:
- effect@4.0.0-beta.75
- @effect/platform-node-shared@4.0.0-beta.75
## 4.0.0-beta.74
### Patch Changes
- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]:
- effect@4.0.0-beta.74
- @effect/platform-node-shared@4.0.0-beta.74
## 4.0.0-beta.73
### Patch Changes
- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]:
- effect@4.0.0-beta.73
- @effect/platform-node-shared@4.0.0-beta.73
## 4.0.0-beta.72
### Patch Changes
- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]:
- effect@4.0.0-beta.72
- @effect/platform-node-shared@4.0.0-beta.72
## 4.0.0-beta.71
### Patch Changes
- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]:
- effect@4.0.0-beta.71
- @effect/platform-node-shared@4.0.0-beta.71
## 4.0.0-beta.70
### Patch Changes
- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`5805569`](https://github.com/Effect-TS/effect-smol/commit/5805569ef5ac47db49924fa34abf03fbdc1675f3), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]:
- effect@4.0.0-beta.70
- @effect/platform-node-shared@4.0.0-beta.70
## 4.0.0-beta.69
### Patch Changes
- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]:
- effect@4.0.0-beta.69
- @effect/platform-node-shared@4.0.0-beta.69
## 4.0.0-beta.68
### Patch Changes
- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure.
- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]:
- effect@4.0.0-beta.68
- @effect/platform-node-shared@4.0.0-beta.68
## 4.0.0-beta.67
### Patch Changes
- [#2185](https://github.com/Effect-TS/effect-smol/pull/2185) [`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f) Thanks @lloydrichards! - add rows to Terminal
- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]:
- effect@4.0.0-beta.67
- @effect/platform-node-shared@4.0.0-beta.67
## 4.0.0-beta.66
### Patch Changes
- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]:
- effect@4.0.0-beta.66
- @effect/platform-node-shared@4.0.0-beta.66
## 4.0.0-beta.65
### Patch Changes
- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]:
- effect@4.0.0-beta.65
- @effect/platform-node-shared@4.0.0-beta.65
## 4.0.0-beta.64
### Patch Changes
- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]:
- effect@4.0.0-beta.64
- @effect/platform-node-shared@4.0.0-beta.64
## 4.0.0-beta.63
### Patch Changes
- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]:
- effect@4.0.0-beta.63
- @effect/platform-node-shared@4.0.0-beta.63
## 4.0.0-beta.62
### Patch Changes
- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]:
- effect@4.0.0-beta.62
- @effect/platform-node-shared@4.0.0-beta.62
## 4.0.0-beta.61
### Patch Changes
- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]:
- effect@4.0.0-beta.61
- @effect/platform-node-shared@4.0.0-beta.61
## 4.0.0-beta.60
### Patch Changes
- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]:
- effect@4.0.0-beta.60
- @effect/platform-node-shared@4.0.0-beta.60
## 4.0.0-beta.59
### Patch Changes
- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]:
- effect@4.0.0-beta.59
- @effect/platform-node-shared@4.0.0-beta.59
## 4.0.0-beta.58
### Patch Changes
- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption
- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]:
- effect@4.0.0-beta.58
- @effect/platform-node-shared@4.0.0-beta.58
## 4.0.0-beta.57
### Patch Changes
- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]:
- effect@4.0.0-beta.57
- @effect/platform-node-shared@4.0.0-beta.57
## 4.0.0-beta.56
### Patch Changes
- Updated dependencies []:
- effect@4.0.0-beta.56
- @effect/platform-node-shared@4.0.0-beta.56
## 4.0.0-beta.55
### Patch Changes
- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]:
- effect@4.0.0-beta.55
- @effect/platform-node-shared@4.0.0-beta.55
## 4.0.0-beta.54
### Patch Changes
- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]:
- effect@4.0.0-beta.54
- @effect/platform-node-shared@4.0.0-beta.54
## 4.0.0-beta.53
### Patch Changes
- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]:
- effect@4.0.0-beta.53
- @effect/platform-node-shared@4.0.0-beta.53
## 4.0.0-beta.52
### Patch Changes
- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]:
- effect@4.0.0-beta.52
- @effect/platform-node-shared@4.0.0-beta.52
## 4.0.0-beta.51
### Patch Changes
- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]:
- effect@4.0.0-beta.51
- @effect/platform-node-shared@4.0.0-beta.51
## 4.0.0-beta.50
### Patch Changes
- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]:
- effect@4.0.0-beta.50
- @effect/platform-node-shared@4.0.0-beta.50
## 4.0.0-beta.49
### Patch Changes
- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]:
- effect@4.0.0-beta.49
- @effect/platform-node-shared@4.0.0-beta.49
## 4.0.0-beta.48
### Patch Changes
- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]:
- effect@4.0.0-beta.48
- @effect/platform-node-shared@4.0.0-beta.48
## 4.0.0-beta.47
### Patch Changes
- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]:
- effect@4.0.0-beta.47
- @effect/platform-node-shared@4.0.0-beta.47
## 4.0.0-beta.46
### Patch Changes
- Updated dependencies [[`a48d439`](https://github.com/Effect-TS/effect-smol/commit/a48d439efad075728bbaee45b77a8ab41f552505), [`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]:
- @effect/platform-node-shared@4.0.0-beta.46
- effect@4.0.0-beta.46
## 4.0.0-beta.45
### Patch Changes
- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]:
- effect@4.0.0-beta.45
- @effect/platform-node-shared@4.0.0-beta.45
## 4.0.0-beta.44
### Patch Changes
- [#1960](https://github.com/Effect-TS/effect-smol/pull/1960) [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf) Thanks @IMax153! - Add `ChildProcessHandle.unref`, returning an `Effect` that restores the child process reference when run.
- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests.
- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]:
- effect@4.0.0-beta.44
- @effect/platform-node-shared@4.0.0-beta.44
## 4.0.0-beta.43
### Patch Changes
- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]:
- effect@4.0.0-beta.43
- @effect/platform-node-shared@4.0.0-beta.43
## 4.0.0-beta.42
### Patch Changes
- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]:
- effect@4.0.0-beta.42
- @effect/platform-node-shared@4.0.0-beta.42
## 4.0.0-beta.41
### Patch Changes
- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]:
- effect@4.0.0-beta.41
- @effect/platform-node-shared@4.0.0-beta.41
## 4.0.0-beta.40
### Patch Changes
- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]:
- effect@4.0.0-beta.40
- @effect/platform-node-shared@4.0.0-beta.40
## 4.0.0-beta.39
### Patch Changes
- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]:
- effect@4.0.0-beta.39
- @effect/platform-node-shared@4.0.0-beta.39
## 4.0.0-beta.38
### Patch Changes
- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]:
- effect@4.0.0-beta.38
- @effect/platform-node-shared@4.0.0-beta.38
## 4.0.0-beta.37
### Patch Changes
- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]:
- effect@4.0.0-beta.37
- @effect/platform-node-shared@4.0.0-beta.37
## 4.0.0-beta.36
### Patch Changes
- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]:
- effect@4.0.0-beta.36
- @effect/platform-node-shared@4.0.0-beta.36
## 4.0.0-beta.35
### Patch Changes
- [#1779](https://github.com/Effect-TS/effect-smol/pull/1779) [`3015c2d`](https://github.com/Effect-TS/effect-smol/commit/3015c2dc25fb44694978b4ff921af9b24178fcc0) Thanks @aeterno-caspian! - bump undici versions
- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]:
- effect@4.0.0-beta.35
- @effect/platform-node-shared@4.0.0-beta.35
## 4.0.0-beta.34
### Patch Changes
- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]:
- effect@4.0.0-beta.34
- @effect/platform-node-shared@4.0.0-beta.34
## 4.0.0-beta.33
### Patch Changes
- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]:
- effect@4.0.0-beta.33
- @effect/platform-node-shared@4.0.0-beta.33
## 4.0.0-beta.32
### Patch Changes
- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]:
- effect@4.0.0-beta.32
- @effect/platform-node-shared@4.0.0-beta.32
## 4.0.0-beta.31
### Patch Changes
- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec<T, Json, RD, RE>` instead of `Codec<T, unknown, RD, RE>`.
Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect<Schema.Json, E>` instead of `Effect<unknown, E>`.
- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]:
- effect@4.0.0-beta.31
- @effect/platform-node-shared@4.0.0-beta.31
## 4.0.0-beta.30
### Patch Changes
- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]:
- effect@4.0.0-beta.30
- @effect/platform-node-shared@4.0.0-beta.30
## 4.0.0-beta.29
### Patch Changes
- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]:
- effect@4.0.0-beta.29
- @effect/platform-node-shared@4.0.0-beta.29
## 4.0.0-beta.28
### Patch Changes
- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]:
- effect@4.0.0-beta.28
- @effect/platform-node-shared@4.0.0-beta.28
## 4.0.0-beta.27
### Patch Changes
- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]:
- effect@4.0.0-beta.27
- @effect/platform-node-shared@4.0.0-beta.27
## 4.0.0-beta.26
### Patch Changes
- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]:
- effect@4.0.0-beta.26
- @effect/platform-node-shared@4.0.0-beta.26
## 4.0.0-beta.25
### Patch Changes
- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]:
- effect@4.0.0-beta.25
- @effect/platform-node-shared@4.0.0-beta.25
## 4.0.0-beta.24
### Patch Changes
- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]:
- effect@4.0.0-beta.24
- @effect/platform-node-shared@4.0.0-beta.24
## 4.0.0-beta.23
### Patch Changes
- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]:
- effect@4.0.0-beta.23
- @effect/platform-node-shared@4.0.0-beta.23
## 4.0.0-beta.22
### Patch Changes
- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]:
- effect@4.0.0-beta.22
- @effect/platform-node-shared@4.0.0-beta.22
## 4.0.0-beta.21
### Patch Changes
- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]:
- effect@4.0.0-beta.21
- @effect/platform-node-shared@4.0.0-beta.21
## 4.0.0-beta.20
### Patch Changes
- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]:
- effect@4.0.0-beta.20
- @effect/platform-node-shared@4.0.0-beta.20
## 4.0.0-beta.19
### Patch Changes
- Updated dependencies [[`b9951d6`](https://github.com/Effect-TS/effect-smol/commit/b9951d6b178b4c2173112fa7454b6abd8030585f)]:
- @effect/platform-node-shared@4.0.0-beta.19
- effect@4.0.0-beta.19
## 4.0.0-beta.18
### Patch Changes
- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]:
- effect@4.0.0-beta.18
- @effect/platform-node-shared@4.0.0-beta.18
## 4.0.0-beta.17
### Patch Changes
- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]:
- effect@4.0.0-beta.17
- @effect/platform-node-shared@4.0.0-beta.17
## 4.0.0-beta.16
### Patch Changes
- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]:
- effect@4.0.0-beta.16
- @effect/platform-node-shared@4.0.0-beta.16
## 4.0.0-beta.15
### Patch Changes
- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]:
- effect@4.0.0-beta.15
- @effect/platform-node-shared@4.0.0-beta.15
## 4.0.0-beta.14
### Patch Changes
- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]:
- effect@4.0.0-beta.14
- @effect/platform-node-shared@4.0.0-beta.14
## 4.0.0-beta.13
### Patch Changes
- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]:
- effect@4.0.0-beta.13
- @effect/platform-node-shared@4.0.0-beta.13
## 4.0.0-beta.12
### Patch Changes
- [#1450](https://github.com/Effect-TS/effect-smol/pull/1450) [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668) Thanks @tim-smart! - use cause annotations for detecting client aborts
- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]:
- effect@4.0.0-beta.12
- @effect/platform-node-shared@4.0.0-beta.12
## 4.0.0-beta.11
### Patch Changes
- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]:
- effect@4.0.0-beta.11
- @effect/platform-node-shared@4.0.0-beta.11
## 4.0.0-beta.10
### Patch Changes
- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]:
- effect@4.0.0-beta.10
- @effect/platform-node-shared@4.0.0-beta.10
## 4.0.0-beta.9
### Patch Changes
- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]:
- effect@4.0.0-beta.9
- @effect/platform-node-shared@4.0.0-beta.9
## 4.0.0-beta.8
### Patch Changes
- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]:
- effect@4.0.0-beta.8
- @effect/platform-node-shared@4.0.0-beta.8
## 4.0.0-beta.7
### Patch Changes
- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]:
- effect@4.0.0-beta.7
- @effect/platform-node-shared@4.0.0-beta.7
## 4.0.0-beta.6
### Patch Changes
- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`5207c20`](https://github.com/Effect-TS/effect-smol/commit/5207c2054a3f60c8fd795faa6cf3f179926b8878), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]:
- effect@4.0.0-beta.6
- @effect/platform-node-shared@4.0.0-beta.6
## 4.0.0-beta.5
### Patch Changes
- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]:
- effect@4.0.0-beta.5
- @effect/platform-node-shared@4.0.0-beta.5
## 4.0.0-beta.4
### Patch Changes
- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]:
- effect@4.0.0-beta.4
- @effect/platform-node-shared@4.0.0-beta.4
## 4.0.0-beta.3
### Patch Changes
- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]:
- effect@4.0.0-beta.3
- @effect/platform-node-shared@4.0.0-beta.3
## 4.0.0-beta.2
### Patch Changes
- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]:
- effect@4.0.0-beta.2
- @effect/platform-node-shared@4.0.0-beta.2
## 4.0.0-beta.1
### Patch Changes
- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]:
- effect@4.0.0-beta.1
- @effect/platform-node-shared@4.0.0-beta.1
## 4.0.0-beta.0
### Major Changes
- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta
### Patch Changes
- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]:
- @effect/platform-node-shared@4.0.0-beta.0
- effect@4.0.0-beta.0

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Effectful Technologies Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
# `@effect/platform-node`
Provides Node.js-specific implementations for the abstractions defined in [`@effect/platform`](https://github.com/Effect-TS/effect/tree/main/packages/platform), allowing you to write platform-independent code that integrates smoothly with Node.js.
## Documentation
- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/platform-node).

View File

@@ -0,0 +1,23 @@
{
"$schema": "../../node_modules/@effect/docgen/schema.json",
"srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-node/src/",
"exclude": ["src/internal/**/*.ts"],
"examplesCompilerOptions": {
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"moduleResolution": "Bundler",
"module": "ES2022",
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"rewriteRelativeImportExtensions": true,
"allowImportingTsExtensions": true,
"paths": {
"effect": ["../../../effect/src/index.js"],
"effect/*": ["../../../effect/src/*.js"]
},
"plugins": [
{ "name": "@effect/language-service", "includeSuggestionsInTsc": false }
]
}
}

View File

@@ -0,0 +1,81 @@
{
"name": "@effect/platform-node",
"type": "module",
"version": "4.0.0-beta.98",
"license": "MIT",
"description": "Platform specific implementations for the Node.js runtime",
"homepage": "https://effect.website",
"repository": {
"type": "git",
"url": "https://github.com/Effect-TS/effect-smol.git",
"directory": "packages/platform-node"
},
"bugs": {
"url": "https://github.com/Effect-TS/effect-smol/issues"
},
"tags": [
"node",
"typescript",
"algebraic-data-types",
"functional-programming"
],
"keywords": [
"node",
"typescript",
"algebraic-data-types",
"functional-programming"
],
"engines": {
"node": ">=18.0.0"
},
"sideEffects": [],
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts",
"./*": "./src/*.ts",
"./internal/*": null,
"./*/index": null
},
"files": [
"src/**/*.ts",
"dist/**/*.js",
"dist/**/*.js.map",
"dist/**/*.d.ts",
"dist/**/*.d.ts.map"
],
"publishConfig": {
"access": "public",
"provenance": true,
"exports": {
"./package.json": "./package.json",
".": "./dist/index.js",
"./*": "./dist/*.js",
"./internal/*": null,
"./*/index": null
}
},
"scripts": {
"codegen": "effect-utils codegen",
"build": "tsc -b tsconfig.json && pnpm babel",
"babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps",
"check": "tsc -b tsconfig.json",
"test": "vitest",
"coverage": "vitest --coverage"
},
"dependencies": {
"@effect/platform-node-shared": "workspace:^",
"mime": "^4.1.0",
"undici": "^8.7.0"
},
"peerDependencies": {
"effect": "workspace:^",
"ioredis": "^5.7.0"
},
"devDependencies": {
"@testcontainers/mysql": "^11.14.0",
"@testcontainers/postgresql": "^11.14.0",
"@testcontainers/redis": "^11.14.0",
"@types/node": "^26.1.1",
"effect": "workspace:^"
}
}

View File

@@ -0,0 +1,25 @@
/**
* Re-exports the `mime` package through the `@effect/platform-node/Mime`
* module and the `Mime` namespace in the package barrel.
*
* @since 4.0.0
*/
/* oxlint-disable no-named-as-default */
/**
* @since 4.0.0
*/
import Mime from "mime"
/**
* @category re-exports
* @since 4.0.0
*/
export * from "mime"
/**
* @category re-exports
* @since 4.0.0
*/
export default Mime

View File

@@ -0,0 +1,6 @@
/**
* Node.js implementation of `ChildProcessSpawner`.
*
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeChildProcessSpawner"

View File

@@ -0,0 +1,155 @@
/**
* Node.js HTTP and WebSocket layers for Effect Cluster runners.
*
* The main `layer` builds a sharding layer for HTTP or WebSocket transport,
* choosing serialization, runner health checks, runner storage, message
* storage, and optional client-only mode from the supplied options.
* `layerHttpServer` provides the Node HTTP server used by cluster runners, and
* this module re-exports the Kubernetes HTTP client layer used by runner health
* checks.
*
* @since 4.0.0
*/
import type * as Config from "effect/Config"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as HttpRunner from "effect/unstable/cluster/HttpRunner"
import * as MessageStorage from "effect/unstable/cluster/MessageStorage"
import * as RunnerHealth from "effect/unstable/cluster/RunnerHealth"
import * as Runners from "effect/unstable/cluster/Runners"
import * as RunnerStorage from "effect/unstable/cluster/RunnerStorage"
import type { Sharding } from "effect/unstable/cluster/Sharding"
import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig"
import * as SqlMessageStorage from "effect/unstable/cluster/SqlMessageStorage"
import * as SqlRunnerStorage from "effect/unstable/cluster/SqlRunnerStorage"
import type * as Etag from "effect/unstable/http/Etag"
import type { HttpPlatform } from "effect/unstable/http/HttpPlatform"
import type { HttpServer } from "effect/unstable/http/HttpServer"
import type { ServeError } from "effect/unstable/http/HttpServerError"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import type { SqlClient } from "effect/unstable/sql/SqlClient"
import { createServer } from "node:http"
import { layerK8sHttpClient } from "./NodeClusterSocket.ts"
import * as NodeHttpClient from "./NodeHttpClient.ts"
import * as NodeHttpServer from "./NodeHttpServer.ts"
import type { NodeServices } from "./NodeServices.ts"
import * as NodeSocket from "./NodeSocket.ts"
export {
/**
* Provides the Kubernetes HTTP client layer used by Kubernetes runner health checks.
*
* @category re-exports
* @since 4.0.0
*/
layerK8sHttpClient
} from "./NodeClusterSocket.ts"
/**
* Builds the Node cluster HTTP/WebSocket sharding layer, configuring runner
* transport, RPC serialization, message storage, runner health checks, and
* optional client-only mode.
*
* @category layers
* @since 4.0.0
*/
export const layer = <
const ClientOnly extends boolean = false,
const Storage extends "local" | "sql" | "byo" = never
>(options: {
readonly transport: "http" | "websocket"
readonly serialization?: "msgpack" | "ndjson" | undefined
readonly clientOnly?: ClientOnly | undefined
readonly storage?: Storage | undefined
readonly runnerHealth?: "ping" | "k8s" | undefined
readonly runnerHealthK8s?: {
readonly namespace?: string | undefined
readonly labelSelector?: string | undefined
} | undefined
readonly shardingConfig?: Partial<ShardingConfig.ShardingConfig["Service"]> | undefined
}): ClientOnly extends true ? Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
Config.ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> :
Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
ServeError | Config.ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> =>
{
const layer: Layer.Layer<any, any, any> = options.clientOnly
// client only
? options.transport === "http"
? Layer.provide(HttpRunner.layerHttpClientOnly, NodeHttpClient.layerUndici)
: Layer.provide(HttpRunner.layerWebsocketClientOnly, NodeSocket.layerWebSocketConstructor)
// with server
: options.transport === "http"
? Layer.provide(HttpRunner.layerHttp, [layerHttpServer, NodeHttpClient.layerUndici])
: Layer.provide(HttpRunner.layerWebsocket, [layerHttpServer, NodeSocket.layerWebSocketConstructor])
const runnerHealth: Layer.Layer<any, any, any> = options?.clientOnly
? Layer.empty as any
: options?.runnerHealth === "k8s"
? RunnerHealth.layerK8s(options.runnerHealthK8s).pipe(
Layer.provide(layerK8sHttpClient)
)
: RunnerHealth.layerPing.pipe(
Layer.provide(Runners.layerRpc),
Layer.provide(
options.transport === "http"
? HttpRunner.layerClientProtocolHttpDefault.pipe(Layer.provide(NodeHttpClient.layerUndici))
: HttpRunner.layerClientProtocolWebsocketDefault.pipe(Layer.provide(NodeSocket.layerWebSocketConstructor))
)
)
return layer.pipe(
Layer.provide(runnerHealth),
Layer.provideMerge(
options?.storage === "local"
? MessageStorage.layerNoop
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlMessageStorage.layer)
),
Layer.provide(
options?.storage === "local"
? RunnerStorage.layerMemory
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlRunnerStorage.layer)
),
Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)),
Layer.provide(
options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack
)
) as any
}
/**
* Provides the HTTP server and Node HTTP services used by cluster runners,
* listening on `ShardingConfig.runnerListenAddress` or `runnerAddress`.
*
* @category layers
* @since 4.0.0
*/
export const layerHttpServer: Layer.Layer<
| HttpPlatform
| Etag.Generator
| NodeServices
| HttpServer,
ServeError,
ShardingConfig.ShardingConfig
> = Effect.gen(function*() {
const config = yield* ShardingConfig.ShardingConfig
const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress)
if (Option.isNone(listenAddress)) {
return yield* Effect.die("NodeClusterHttp.layerHttpServer: ShardingConfig.runnerAddress is None")
}
return NodeHttpServer.layer(createServer, listenAddress.value)
}).pipe(Layer.unwrap)

View File

@@ -0,0 +1,176 @@
/**
* Node.js socket layers for Effect Cluster runners.
*
* The main `layer` builds a sharding layer for socket transport, choosing
* serialization, runner health checks, runner storage, message storage, and
* optional client-only mode from the supplied options. This module also
* re-exports the shared socket client and server protocol layers, and provides
* Kubernetes-aware Undici dispatcher and HTTP client layers for runner health
* checks.
*
* @since 4.0.0
*/
import { layerClientProtocol, layerSocketServer } from "@effect/platform-node-shared/NodeClusterSocket"
import type { ConfigError } from "effect/Config"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as K8sHttpClient from "effect/unstable/cluster/K8sHttpClient"
import * as MessageStorage from "effect/unstable/cluster/MessageStorage"
import * as RunnerHealth from "effect/unstable/cluster/RunnerHealth"
import * as Runners from "effect/unstable/cluster/Runners"
import * as RunnerStorage from "effect/unstable/cluster/RunnerStorage"
import type { Sharding } from "effect/unstable/cluster/Sharding"
import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig"
import * as SocketRunner from "effect/unstable/cluster/SocketRunner"
import * as SqlMessageStorage from "effect/unstable/cluster/SqlMessageStorage"
import * as SqlRunnerStorage from "effect/unstable/cluster/SqlRunnerStorage"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import type * as SocketServer from "effect/unstable/socket/SocketServer"
import type { SqlClient } from "effect/unstable/sql/SqlClient"
import * as NodeFileSystem from "./NodeFileSystem.ts"
import * as NodeHttpClient from "./NodeHttpClient.ts"
import * as Undici from "./Undici.ts"
export {
/**
* Provides the cluster `RpcClientProtocol` using the shared socket client
* implementation.
*
* @category re-exports
* @since 4.0.0
*/
layerClientProtocol,
/**
* Provides the socket server used by Node cluster runners through the shared
* socket server implementation.
*
* @category re-exports
* @since 4.0.0
*/
layerSocketServer
}
/**
* Builds the Node cluster socket sharding layer, configuring RPC
* serialization, message storage, runner health checks, and optional
* client-only mode.
*
* @category layers
* @since 4.0.0
*/
export const layer = <
const ClientOnly extends boolean = false,
const Storage extends "local" | "sql" | "byo" = never
>(
options?: {
readonly serialization?: "msgpack" | "ndjson" | undefined
readonly clientOnly?: ClientOnly | undefined
readonly storage?: Storage | undefined
readonly runnerHealth?: "ping" | "k8s" | undefined
readonly runnerHealthK8s?: {
readonly namespace?: string | undefined
readonly labelSelector?: string | undefined
} | undefined
readonly shardingConfig?: Partial<ShardingConfig.ShardingConfig["Service"]> | undefined
}
): ClientOnly extends true ? Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> :
Layer.Layer<
Sharding | Runners.Runners | ("byo" extends Storage ? never : MessageStorage.MessageStorage),
SocketServer.SocketServerError | ConfigError,
"local" extends Storage ? never
: "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage)
: SqlClient
> =>
{
const layer: Layer.Layer<any, any, any> = options?.clientOnly
// client only
? Layer.provide(SocketRunner.layerClientOnly, layerClientProtocol)
// with server
: Layer.provide(SocketRunner.layer, [layerSocketServer, layerClientProtocol])
const runnerHealth: Layer.Layer<any, any, any> = options?.clientOnly
? Layer.empty as any
: options?.runnerHealth === "k8s"
? RunnerHealth.layerK8s(options.runnerHealthK8s).pipe(
Layer.provide(layerK8sHttpClient)
)
: RunnerHealth.layerPing.pipe(
Layer.provide(Runners.layerRpc),
Layer.provide(layerClientProtocol)
)
return layer.pipe(
Layer.provide(runnerHealth),
Layer.provideMerge(
options?.storage === "local"
? MessageStorage.layerNoop
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlMessageStorage.layer)
),
Layer.provide(
options?.storage === "local"
? RunnerStorage.layerMemory
: options?.storage === "byo"
? Layer.empty
: Layer.orDie(SqlRunnerStorage.layer)
),
Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)),
Layer.provide(
options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack
)
) as any
}
/**
* Provides an Undici dispatcher for Kubernetes API calls, using the service
* account CA certificate when it is available and falling back to the default
* dispatcher otherwise.
*
* @category layers
* @since 4.0.0
*/
export const layerDispatcherK8s: Layer.Layer<NodeHttpClient.Dispatcher> = Layer.effect(NodeHttpClient.Dispatcher)(
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const caCertOption = yield* fs.readFileString("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt").pipe(
Effect.option
)
if (caCertOption._tag === "Some") {
return yield* Effect.acquireRelease(
Effect.sync(() =>
new Undici.Agent({
connect: {
ca: caCertOption.value
}
})
),
(agent) => Effect.promise(() => agent.destroy())
)
}
return yield* NodeHttpClient.makeDispatcher
})
).pipe(
Layer.provide(NodeFileSystem.layer)
)
/**
* Provides a `K8sHttpClient` backed by the Undici HTTP client and the
* Kubernetes-aware dispatcher.
*
* @category layers
* @since 4.0.0
*/
export const layerK8sHttpClient: Layer.Layer<K8sHttpClient.K8sHttpClient> = K8sHttpClient.layer.pipe(
Layer.provide(Layer.fresh(NodeHttpClient.layerUndiciNoDispatcher)),
Layer.provide(layerDispatcherK8s),
Layer.provide(NodeFileSystem.layer)
)

View File

@@ -0,0 +1,24 @@
/**
* The `NodeCrypto` module provides the Node.js `Crypto` service layer for
* Effect programs. Provide {@link layer} at the edge of a Node application,
* CLI, script, or test to satisfy `effect/Crypto` with Node's `node:crypto`
* implementation for secure random bytes, UUID generation, random values, and
* SHA digest operations.
*
* This module is the public Node adapter around the shared Node-compatible
* implementation. Digest failures are reported as platform errors, and SHA-1
* remains available only for interoperability with existing protocols.
*
* @since 1.0.0
*/
import * as NodeCrypto from "@effect/platform-node-shared/NodeCrypto"
import type * as Crypto from "effect/Crypto"
import type * as Layer from "effect/Layer"
/**
* Layer that provides the Node.js Crypto service implementation.
*
* @category layers
* @since 1.0.0
*/
export const layer: Layer.Layer<Crypto.Crypto> = NodeCrypto.layer

View File

@@ -0,0 +1,21 @@
/**
* Node.js `FileSystem` layer for programs that perform real filesystem I/O.
*
* The exported layer satisfies the platform-independent `FileSystem` service
* with Node-backed operations for files, directories, metadata, permissions,
* links, temporary paths, and path watching. Effects still call the service from
* `effect/FileSystem`; this module only chooses the Node implementation.
*
* @since 4.0.0
*/
import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem"
import type { FileSystem } from "effect/FileSystem"
import type * as Layer from "effect/Layer"
/**
* Provides the `FileSystem` service backed by Node filesystem APIs.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<FileSystem> = NodeFileSystem.layer

View File

@@ -0,0 +1,664 @@
/**
* Node.js implementations of the Effect `HttpClient`.
*
* This module supplies Node runtime backends for the platform-independent
* Effect HTTP client API. It re-exports the fetch-based `Fetch`, `RequestInit`,
* and `layerFetch` APIs, defines an Undici-backed client with dispatcher
* services and request options, and defines a lower-level `node:http` /
* `node:https` client with scoped HTTP agent layers.
*
* @since 4.0.0
*/
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import { flow } from "effect/Function"
import * as Inspectable from "effect/Inspectable"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import { type Pipeable, pipeArguments } from "effect/Pipeable"
import type * as Schema from "effect/Schema"
import type * as Scope from "effect/Scope"
import * as Stream from "effect/Stream"
import * as Cookies from "effect/unstable/http/Cookies"
import * as Headers from "effect/unstable/http/Headers"
import type * as Body from "effect/unstable/http/HttpBody"
import * as Client from "effect/unstable/http/HttpClient"
import * as Error from "effect/unstable/http/HttpClientError"
import type { HttpClientRequest } from "effect/unstable/http/HttpClientRequest"
import * as Response from "effect/unstable/http/HttpClientResponse"
import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"
import * as IncomingMessage from "effect/unstable/http/HttpIncomingMessage"
import * as UrlParams from "effect/unstable/http/UrlParams"
import * as Http from "node:http"
import * as Https from "node:https"
import { Readable } from "node:stream"
import { pipeline } from "node:stream/promises"
import { NodeHttpIncomingMessage } from "./NodeHttpIncomingMessage.ts"
import * as NodeSink from "./NodeSink.ts"
import * as NodeStream from "./NodeStream.ts"
import * as Undici from "./Undici.ts"
// -----------------------------------------------------------------------------
// Fetch
// -----------------------------------------------------------------------------
export {
/**
* Provides a fetch-based HTTP client implementation for Node.js.
*
* **When to use**
*
* Use to access or override the fetch implementation used by the Node
* fetch-based HTTP client.
*
* @category fetch
* @since 4.0.0
*/
Fetch,
/**
* Layer that provides the fetch-based HTTP client implementation.
*
* @category fetch
* @since 4.0.0
*/
layer as layerFetch,
/**
* Provides request initialization options accepted by the fetch-based HTTP client.
*
* **When to use**
*
* Use to provide default fetch request options for Node HTTP requests.
*
* @category fetch
* @since 4.0.0
*/
RequestInit
} from "effect/unstable/http/FetchHttpClient"
// -----------------------------------------------------------------------------
// Undici
// -----------------------------------------------------------------------------
/**
* Service tag for the Undici `Dispatcher` used by the Undici-backed HTTP
* client.
*
* @category Dispatcher
* @since 4.0.0
*/
export class Dispatcher extends Context.Service<Dispatcher, Undici.Dispatcher>()(
"@effect/platform-node/NodeHttpClient/Dispatcher"
) {}
/**
* Acquires a new Undici `Agent` dispatcher and destroys it when the enclosing
* scope is finalized.
*
* @category Dispatcher
* @since 4.0.0
*/
export const makeDispatcher: Effect.Effect<Undici.Dispatcher, never, Scope.Scope> = Effect.acquireRelease(
Effect.sync(() => new Undici.Agent()),
(dispatcher) => Effect.promise(() => dispatcher.destroy())
)
/**
* Provides the `Dispatcher` service using a scoped Undici `Agent`.
*
* @category Dispatcher
* @since 4.0.0
*/
export const layerDispatcher: Layer.Layer<Dispatcher> = Layer.effect(Dispatcher)(makeDispatcher)
/**
* Provides the `Dispatcher` service from Undici's process-global dispatcher,
* without creating or owning a new agent.
*
* @category Dispatcher
* @since 4.0.0
*/
export const dispatcherLayerGlobal: Layer.Layer<Dispatcher> = Layer.sync(Dispatcher)(() => Undici.getGlobalDispatcher())
/**
* Fiber reference containing default Undici request options applied to requests
* sent by `makeUndici`.
*
* @category Undici
* @since 4.0.0
*/
export const UndiciOptions = Context.Reference<Partial<Undici.Dispatcher.RequestOptions>>(
"@effect/platform-node/NodeHttpClient/UndiciOptions",
{ defaultValue: () => ({}) }
)
/**
* Creates an `HttpClient` that sends requests through the current Undici
* `Dispatcher`, converts Effect HTTP bodies to Undici bodies, and maps
* transport and decode failures to `HttpClientError`.
*
* @category Undici
* @since 4.0.0
*/
export const makeUndici = Effect.gen(function*() {
const dispatcher = yield* Dispatcher
return Client.make((request, url, signal, fiber) =>
convertBody(request.body).pipe(
Effect.flatMap((body) =>
Effect.tryPromise({
try: () =>
dispatcher.request({
...fiber.getRef(UndiciOptions),
signal,
method: request.method,
headers: request.headers,
origin: url.origin,
path: url.pathname + url.search + url.hash,
body,
// leave timeouts to Effect.timeout etc
headersTimeout: 60 * 60 * 1000,
bodyTimeout: 0
}),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
})
),
Effect.map((response) => new UndiciResponse(request, response))
)
)
})
function convertBody(
body: Body.HttpBody
): Effect.Effect<Exclude<Undici.Dispatcher.DispatchOptions["body"], undefined>> {
switch (body._tag) {
case "Empty": {
return Effect.succeed(null)
}
case "Uint8Array":
case "Raw": {
return Effect.succeed(body.body as Uint8Array)
}
case "FormData": {
return Effect.succeed(body.formData as any)
}
case "Stream": {
return NodeStream.toReadable(body.stream)
}
}
}
function noopErrorHandler(_: any) {}
class UndiciResponse extends Inspectable.Class implements HttpClientResponse, Pipeable {
readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId
readonly [Response.TypeId]: typeof Response.TypeId
readonly request: HttpClientRequest
readonly source: Undici.Dispatcher.ResponseData
constructor(
request: HttpClientRequest,
source: Undici.Dispatcher.ResponseData
) {
super()
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
this[Response.TypeId] = Response.TypeId
this.request = request
this.source = source
source.body.on("error", noopErrorHandler)
}
get status() {
return this.source.statusCode!
}
get statusText() {
return undefined
}
get headers(): Headers.Headers {
return Headers.fromInput(this.source.headers)
}
cachedCookies?: Cookies.Cookies
get cookies(): Cookies.Cookies {
if (this.cachedCookies !== undefined) {
return this.cachedCookies
}
const header = this.source.headers["set-cookie"]
return this.cachedCookies = header ? Cookies.fromSetCookie(header) : Cookies.empty
}
get remoteAddress(): Option.Option<string> {
return Option.none()
}
get stream(): Stream.Stream<Uint8Array, Error.HttpClientError> {
return NodeStream.fromReadable({
evaluate: () => this.source.body,
onError: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
})
}
get json(): Effect.Effect<Schema.Json, Error.HttpClientError> {
return Effect.flatMap(this.text, (text) =>
Effect.try({
try: () => text === "" ? null : JSON.parse(text),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}))
}
private textBody?: Effect.Effect<string, Error.HttpClientError>
get text(): Effect.Effect<string, Error.HttpClientError> {
if (this.textBody) {
return this.textBody
}
this.textBody = Effect.tryPromise({
try: () => this.source.body.text(),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}).pipe(Effect.cached, Effect.runSync)
this.arrayBufferBody = Effect.map(this.textBody, (_) => new TextEncoder().encode(_).buffer)
return this.textBody
}
get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, Error.HttpClientError> {
return Effect.flatMap(this.text, (_) =>
Effect.try({
try: () => UrlParams.fromInput(new URLSearchParams(_)),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}))
}
private formDataBody?: Effect.Effect<FormData, Error.HttpClientError>
get formData(): Effect.Effect<FormData, Error.HttpClientError> {
return this.formDataBody ??= Effect.tryPromise({
try: () => this.source.body.formData() as Promise<FormData>,
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}).pipe(Effect.cached, Effect.runSync)
}
private arrayBufferBody?: Effect.Effect<ArrayBuffer, Error.HttpClientError>
get arrayBuffer(): Effect.Effect<ArrayBuffer, Error.HttpClientError> {
if (this.arrayBufferBody) {
return this.arrayBufferBody
}
this.arrayBufferBody = Effect.tryPromise({
try: () => this.source.body.arrayBuffer(),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request: this.request,
response: this,
cause
})
})
}).pipe(Effect.cached, Effect.runSync)
this.textBody = Effect.map(this.arrayBufferBody, (_) => new TextDecoder().decode(_))
return this.arrayBufferBody
}
toJSON(): unknown {
return IncomingMessage.inspect(this, {
_id: "effect/http/HttpClientResponse",
request: this.request.toJSON(),
status: this.status
})
}
pipe() {
return pipeArguments(this, arguments)
}
}
/**
* Provides an Undici-backed `HttpClient` using the current `Dispatcher`
* service.
*
* @category Undici
* @since 4.0.0
*/
export const layerUndiciNoDispatcher: Layer.Layer<
Client.HttpClient,
never,
Dispatcher
> = Client.layerMergedContext(makeUndici)
/**
* Provides an Undici-backed `HttpClient` together with a scoped default
* Undici `Agent` dispatcher.
*
* @category Undici
* @since 4.0.0
*/
export const layerUndici: Layer.Layer<Client.HttpClient> = Layer.provide(layerUndiciNoDispatcher, layerDispatcher)
// -----------------------------------------------------------------------------
// node:http
// -----------------------------------------------------------------------------
/**
* Service tag for the paired Node `http` and `https` agents used by the
* node:http-backed HTTP client.
*
* @category HttpAgent
* @since 4.0.0
*/
export class HttpAgent extends Context.Service<HttpAgent, {
readonly http: Http.Agent
readonly https: Https.Agent
}>()("@effect/platform-node/NodeHttpClient/HttpAgent") {}
/**
* Acquires Node `http` and `https` agents with the supplied options and
* destroys both agents when the enclosing scope is finalized.
*
* @category HttpAgent
* @since 4.0.0
*/
export const makeAgent = (options?: Https.AgentOptions): Effect.Effect<HttpAgent["Service"], never, Scope.Scope> =>
Effect.zipWith(
Effect.acquireRelease(
Effect.sync(() => new Http.Agent(options)),
(agent) => Effect.sync(() => agent.destroy())
),
Effect.acquireRelease(
Effect.sync(() => new Https.Agent(options)),
(agent) => Effect.sync(() => agent.destroy())
),
(http, https) => ({ http, https })
)
/**
* Provides the `HttpAgent` service using scoped Node `http` and `https`
* agents configured with the supplied options.
*
* @category HttpAgent
* @since 4.0.0
*/
export const layerAgentOptions: (options?: Https.AgentOptions | undefined) => Layer.Layer<
HttpAgent
> = flow(makeAgent, Layer.effect(HttpAgent))
/**
* Provides the `HttpAgent` service using default scoped Node `http` and
* `https` agents.
*
* @category HttpAgent
* @since 4.0.0
*/
export const layerAgent: Layer.Layer<HttpAgent> = layerAgentOptions()
/**
* Creates an `HttpClient` backed by Node `http` and `https`, using the
* current `HttpAgent`, streaming request bodies, and wrapping Node responses
* as `HttpClientResponse` values.
*
* @category node:http
* @since 4.0.0
*/
export const makeNodeHttp = Effect.gen(function*() {
const agent = yield* HttpAgent
return Client.make((request, url, signal) => {
const nodeRequest = url.protocol === "https:" ?
Https.request(url, {
agent: agent.https,
method: request.method,
headers: request.headers,
signal
}) :
Http.request(url, {
agent: agent.http,
method: request.method,
headers: request.headers,
signal
})
return Effect.forkChild(sendBody(nodeRequest, request, request.body)).pipe(
Effect.flatMap(() => waitForResponse(nodeRequest, request)),
Effect.map((_) => new NodeHttpResponse(request, _))
)
})
})
const sendBody = (
nodeRequest: Http.ClientRequest,
request: HttpClientRequest,
body: Body.HttpBody
): Effect.Effect<void, Error.HttpClientError> =>
Effect.suspend((): Effect.Effect<void, Error.HttpClientError> => {
switch (body._tag) {
case "Empty": {
nodeRequest.end()
return waitForFinish(nodeRequest, request)
}
case "Uint8Array":
case "Raw": {
nodeRequest.end(body.body)
return waitForFinish(nodeRequest, request)
}
case "FormData": {
const response = new globalThis.Response(body.formData)
response.headers.forEach((value, key) => {
nodeRequest.setHeader(key, value)
})
return Effect.tryPromise({
try: () => pipeline(Readable.fromWeb(response.body! as any), nodeRequest),
catch: (cause) =>
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
})
}
case "Stream": {
return Stream.run(
Stream.mapError(body.stream, (cause) =>
new Error.HttpClientError({
reason: new Error.EncodeError({
request,
cause
})
})),
NodeSink.fromWritable({
evaluate: () => nodeRequest,
onError: (cause) =>
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
})
)
}
}
})
const waitForResponse = (nodeRequest: Http.ClientRequest, request: HttpClientRequest) =>
Effect.callback<Http.IncomingMessage, Error.HttpClientError>((resume) => {
function onError(cause: Error) {
resume(Effect.fail(
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
))
}
nodeRequest.on("error", onError)
function onResponse(response: Http.IncomingMessage) {
nodeRequest.off("error", onError)
resume(Effect.succeed(response))
}
nodeRequest.on("upgrade", onResponse)
nodeRequest.on("response", onResponse)
return Effect.sync(() => {
nodeRequest.off("error", onError)
nodeRequest.off("upgrade", onResponse)
nodeRequest.off("response", onResponse)
})
})
const waitForFinish = (nodeRequest: Http.ClientRequest, request: HttpClientRequest) =>
Effect.callback<void, Error.HttpClientError>((resume) => {
function onError(cause: Error) {
resume(Effect.fail(
new Error.HttpClientError({
reason: new Error.TransportError({
request,
cause
})
})
))
}
nodeRequest.once("error", onError)
function onFinish() {
nodeRequest.off("error", onError)
resume(Effect.void)
}
nodeRequest.once("finish", onFinish)
return Effect.sync(() => {
nodeRequest.off("error", onError)
nodeRequest.off("finish", onFinish)
})
})
class NodeHttpResponse extends NodeHttpIncomingMessage<Error.HttpClientError> implements HttpClientResponse, Pipeable {
readonly [Response.TypeId]: typeof Response.TypeId
readonly request: HttpClientRequest
constructor(
request: HttpClientRequest,
source: Http.IncomingMessage
) {
super(source, (cause) =>
new Error.HttpClientError({
reason: new Error.DecodeError({
request,
response: this,
cause
})
}))
this[Response.TypeId] = Response.TypeId
this.request = request
}
get status() {
return this.source.statusCode!
}
cachedCookies?: Cookies.Cookies
get cookies(): Cookies.Cookies {
if (this.cachedCookies !== undefined) {
return this.cachedCookies
}
const header = this.source.headers["set-cookie"]
return this.cachedCookies = header ? Cookies.fromSetCookie(header) : Cookies.empty
}
get formData(): Effect.Effect<FormData, Error.HttpClientError> {
return Effect.tryPromise({
try: () => {
const init: {
headers: HeadersInit
status?: number
statusText?: string
} = {
headers: new globalThis.Headers(this.source.headers as any)
}
if (this.source.statusCode) {
init.status = this.source.statusCode
}
if (this.source.statusMessage) {
init.statusText = this.source.statusMessage
}
return new globalThis.Response(Readable.toWeb(this.source) as any, init).formData()
},
catch: this.onError
})
}
toJSON(): unknown {
return IncomingMessage.inspect(this, {
_id: "effect/http/HttpClientResponse",
request: this.request.toJSON(),
status: this.status
})
}
pipe() {
return pipeArguments(this, arguments)
}
}
/**
* Provides a node:http-backed `HttpClient` using the current `HttpAgent`
* service.
*
* @category node:http
* @since 4.0.0
*/
export const layerNodeHttpNoAgent: Layer.Layer<
Client.HttpClient,
never,
HttpAgent
> = Client.layerMergedContext(makeNodeHttp)
/**
* Provides a node:http-backed `HttpClient` together with default scoped Node
* `http` and `https` agents.
*
* @category node:http
* @since 4.0.0
*/
export const layerNodeHttp: Layer.Layer<Client.HttpClient> = Layer.provide(layerNodeHttpNoAgent, layerAgent)

View File

@@ -0,0 +1,141 @@
/**
* Adapter base for exposing Node `http.IncomingMessage` values as Effect HTTP
* incoming messages.
*
* Server requests and Node client responses both arrive as Node readable
* streams with raw header objects, socket metadata, and one-shot body
* consumption. This module's `NodeHttpIncomingMessage` class keeps the original
* Node message available while presenting Effect's `HttpIncomingMessage` shape:
* typed headers, remote address lookup, stream access, and text, JSON,
* URL-encoded, and array-buffer body readers.
*
* @since 4.0.0
*/
import * as Effect from "effect/Effect"
import * as Inspectable from "effect/Inspectable"
import * as Option from "effect/Option"
import type * as Schema from "effect/Schema"
import type * as Stream from "effect/Stream"
import * as Headers from "effect/unstable/http/Headers"
import * as IncomingMessage from "effect/unstable/http/HttpIncomingMessage"
import * as UrlParams from "effect/unstable/http/UrlParams"
import type * as Http from "node:http"
import * as NodeStream from "./NodeStream.ts"
/**
* Adapts a Node `IncomingMessage` to Effect HTTP incoming messages.
*
* **When to use**
*
* Use to implement Node HTTP request or response adapters that expose the
* Effect HTTP incoming-message interface.
*
* **Details**
*
* The adapter exposes headers, remote address, stream access, and cached body
* decoders. Subclasses provide the error mapping for unknown Node errors.
*
* @category constructors
* @since 4.0.0
*/
export abstract class NodeHttpIncomingMessage<E> extends Inspectable.Class
implements IncomingMessage.HttpIncomingMessage<E>
{
/**
* Marks this value as an HTTP incoming message for runtime guards.
*
* @since 4.0.0
*/
readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId
readonly source: Http.IncomingMessage
readonly onError: (error: unknown) => E
readonly remoteAddressOverride?: Option.Option<string> | undefined
constructor(
source: Http.IncomingMessage,
onError: (error: unknown) => E,
remoteAddressOverride?: Option.Option<string>
) {
super()
this[IncomingMessage.TypeId] = IncomingMessage.TypeId
this.source = source
this.onError = onError
this.remoteAddressOverride = remoteAddressOverride
}
get headers() {
return Headers.fromInput(this.source.headers as any)
}
get remoteAddress() {
return this.remoteAddressOverride ?? Option.fromNullishOr(this.source.socket.remoteAddress)
}
private textEffect: Effect.Effect<string, E> | undefined
get text(): Effect.Effect<string, E> {
if (this.textEffect) {
return this.textEffect
}
this.textEffect = Effect.runSync(Effect.cached(
Effect.flatMap(
IncomingMessage.MaxBodySize,
(maxBodySize) =>
NodeStream.toString(() => this.source, {
onError: this.onError,
maxBytes: maxBodySize
})
)
))
this.arrayBufferEffect = Effect.map(this.textEffect, (_) => new TextEncoder().encode(_).buffer)
return this.textEffect
}
get textUnsafe(): string {
return Effect.runSync(this.text)
}
get json(): Effect.Effect<Schema.Json, E> {
return Effect.flatMap(this.text, (text) =>
Effect.try({
try: () => text === "" ? null : JSON.parse(text),
catch: this.onError
}))
}
get jsonUnsafe(): Schema.Json {
return Effect.runSync(this.json)
}
get urlParamsBody(): Effect.Effect<UrlParams.UrlParams, E> {
return Effect.flatMap(this.text, (_) =>
Effect.try({
try: () => UrlParams.fromInput(new URLSearchParams(_)),
catch: this.onError
}))
}
get stream(): Stream.Stream<Uint8Array, E> {
return NodeStream.fromReadable({
evaluate: () => this.source,
onError: this.onError
})
}
private arrayBufferEffect: Effect.Effect<ArrayBuffer, E> | undefined
get arrayBuffer(): Effect.Effect<ArrayBuffer, E> {
if (this.arrayBufferEffect) {
return this.arrayBufferEffect
}
this.arrayBufferEffect = Effect.withFiber((fiber) =>
NodeStream.toArrayBuffer(() => this.source, {
onError: this.onError,
maxBytes: fiber.getRef(IncomingMessage.MaxBodySize)
})
).pipe(
Effect.cached,
Effect.runSync
)
this.textEffect = Effect.map(this.arrayBufferEffect, (_) => new TextDecoder().decode(_))
return this.arrayBufferEffect
}
}

View File

@@ -0,0 +1,70 @@
/**
* Node.js implementation of the Effect HTTP platform service.
*
* This module connects the portable `HttpPlatform` file response helpers to
* Node runtime primitives. It serves local files through Node readable streams,
* supports byte ranges, converts Web `File` values to readable streams, and
* fills in content type and content length headers when needed.
*
* @since 4.0.0
*/
import { pipe } from "effect/Function"
import * as Layer from "effect/Layer"
import * as EtagImpl from "effect/unstable/http/Etag"
import * as Headers from "effect/unstable/http/Headers"
import * as Platform from "effect/unstable/http/HttpPlatform"
import * as ServerResponse from "effect/unstable/http/HttpServerResponse"
import * as Fs from "node:fs"
import { Readable } from "node:stream"
import Mime from "./Mime.ts"
import * as NodeFileSystem from "./NodeFileSystem.ts"
/**
* Creates the Node `HttpPlatform`, serving file responses from Node readable
* streams and adding MIME type and content-length headers when needed.
*
* @category constructors
* @since 4.0.0
*/
export const make = Platform.make({
fileResponse(path, status, statusText, headers, start, end, contentLength) {
const stream = contentLength === 0
? Readable.from([])
: Fs.createReadStream(path, { start, end: end === undefined ? undefined : end - 1 })
return ServerResponse.raw(stream, {
headers: {
...headers,
"content-type": headers["content-type"] ?? Mime.getType(path) ?? "application/octet-stream",
"content-length": contentLength.toString()
},
status,
statusText
})
},
fileWebResponse(file, status, statusText, headers, _options) {
return ServerResponse.raw(Readable.fromWeb(file.stream() as any), {
headers: Headers.merge(
headers,
Headers.fromRecordUnsafe({
"content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream",
"content-length": file.size.toString()
})
),
status,
statusText
})
}
})
/**
* Provides the Node `HttpPlatform` together with the filesystem and ETag
* services it needs for file responses.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Platform.HttpPlatform> = pipe(
Layer.effect(Platform.HttpPlatform)(make),
Layer.provide(NodeFileSystem.layer),
Layer.provide(EtagImpl.layer)
)

View File

@@ -0,0 +1,647 @@
/**
* Node.js implementation of the Effect `HttpServer`.
*
* This module adapts a supplied Node `http.Server` into Effect's
* platform-independent HTTP server service. It starts the server with Node
* `listen` options, converts `request` events into `HttpServerRequest` values,
* writes `HttpServerResponse` bodies through Node's `ServerResponse`, and
* handles `upgrade` events by exposing the upgraded socket through
* `HttpServerRequest.upgrade`. It also exports request and upgrade handler
* constructors plus layers for the server alone, HTTP support services, the
* combined server, configurable options, and tests.
*
* @since 4.0.0
*/
import * as Cause from "effect/Cause"
import * as Config from "effect/Config"
import * as Context from "effect/Context"
import * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Fiber from "effect/Fiber"
import type * as FileSystem from "effect/FileSystem"
import { flow, type LazyArg } from "effect/Function"
import * as Latch from "effect/Latch"
import * as Layer from "effect/Layer"
import type * as Option from "effect/Option"
import type * as Path from "effect/Path"
import type * as Record from "effect/Record"
import * as Scope from "effect/Scope"
import * as Stream from "effect/Stream"
import * as Cookies from "effect/unstable/http/Cookies"
import * as Etag from "effect/unstable/http/Etag"
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"
import type * as Headers from "effect/unstable/http/Headers"
import type { HttpClient } from "effect/unstable/http/HttpClient"
import * as HttpEffect from "effect/unstable/http/HttpEffect"
import * as HttpIncomingMessage from "effect/unstable/http/HttpIncomingMessage"
import type { HttpMethod } from "effect/unstable/http/HttpMethod"
import type * as Middleware from "effect/unstable/http/HttpMiddleware"
import type * as HttpPlatform from "effect/unstable/http/HttpPlatform"
import * as HttpServer from "effect/unstable/http/HttpServer"
import {
causeResponse,
ClientAbort,
HttpServerError,
RequestParseError,
ResponseError,
ServeError
} from "effect/unstable/http/HttpServerError"
import * as Request from "effect/unstable/http/HttpServerRequest"
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"
import type { HttpServerResponse } from "effect/unstable/http/HttpServerResponse"
import type * as Multipart from "effect/unstable/http/Multipart"
import * as Socket from "effect/unstable/socket/Socket"
import * as Http from "node:http"
import type * as Net from "node:net"
import type { Duplex } from "node:stream"
import { Readable } from "node:stream"
import { pipeline } from "node:stream/promises"
import { NodeHttpIncomingMessage } from "./NodeHttpIncomingMessage.ts"
import * as NodeHttpPlatform from "./NodeHttpPlatform.ts"
import * as NodeMultipart from "./NodeMultipart.ts"
import * as NodeServices from "./NodeServices.ts"
import { NodeWS } from "./NodeSocket.ts"
/**
* Creates a scoped `HttpServer` from a Node `http.Server`, starts listening
* with the supplied options, registers request and upgrade handling, and closes
* the server during scope finalization with optional graceful-shutdown control.
*
* @category constructors
* @since 4.0.0
*/
export const make = Effect.fnUntraced(function*(
evaluate: LazyArg<Http.Server>,
options: Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
) {
const scope = yield* Effect.scope
const server = evaluate()
const shutdown = yield* Effect.callback<void>((resume) => {
if (!server.listening) {
return resume(Effect.void)
}
server.close((error) => {
if (error) {
resume(Effect.die(error))
} else {
resume(Effect.void)
}
})
}).pipe(Effect.cached)
const preemptiveShutdown = options.disablePreemptiveShutdown ?
Effect.void :
Effect.timeoutOrElse(shutdown, {
duration: options.gracefulShutdownTimeout ?? Duration.seconds(20),
orElse: () => Effect.void
})
yield* Scope.addFinalizer(scope, shutdown)
yield* Effect.callback<void, ServeError>((resume) => {
function onError(cause: Error) {
resume(Effect.fail(new ServeError({ cause })))
}
server.on("error", onError)
server.listen(options, () => {
server.off("error", onError)
resume(Effect.void)
})
})
const address = server.address()!
const wss = yield* Effect.acquireRelease(
Effect.sync(() => new NodeWS.WebSocketServer({ noServer: true })),
(wss) =>
Effect.callback<void>((resume) => {
wss.close(() => resume(Effect.void))
})
).pipe(
Scope.provide(scope),
Effect.cached
)
return HttpServer.make({
address: typeof address === "string" ?
{
_tag: "UnixAddress",
path: address
} :
{
_tag: "TcpAddress",
hostname: address.address === "::" ? "0.0.0.0" : address.address,
port: address.port
},
serve: Effect.fnUntraced(function*(httpApp, middleware) {
const serveScope = yield* Effect.scope
const scope = Scope.forkUnsafe(serveScope, "parallel")
const handler = yield* (makeHandler(httpApp, {
middleware: middleware as any,
scope
}) as Effect.Effect<(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void>)
const upgradeHandler = yield* makeUpgradeHandler(wss, httpApp, {
middleware: middleware as any,
scope
})
yield* Scope.addFinalizerExit(serveScope, () => {
server.off("request", handler)
server.off("upgrade", upgradeHandler)
return preemptiveShutdown
})
server.on("request", handler)
server.on("upgrade", upgradeHandler)
})
})
})
/**
* Creates a Node `request` event handler for an Effect HTTP application,
* injecting a `HttpServerRequest` and interrupting the request fiber if the
* client closes the response before it finishes.
*
* @category handlers
* @since 4.0.0
*/
export const makeHandler = <
R,
E,
App extends Effect.Effect<HttpServerResponse, any, any> = Effect.Effect<HttpServerResponse, E, R>
>(
httpEffect: Effect.Effect<HttpServerResponse, E, R>,
options: {
readonly scope: Scope.Scope
readonly middleware?: Middleware.HttpMiddleware.Applied<App, E, R> | undefined
}
): Effect.Effect<
(nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse) => void,
never,
Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
> => {
const handled = HttpEffect.toHandled(httpEffect, handleResponse, options.middleware as any)
return Effect.withFiber((parent) => {
const services = parent.context
return Effect.succeed(function handler(
nodeRequest: Http.IncomingMessage,
nodeResponse: Http.ServerResponse
) {
const map = new Map(services.mapUnsafe)
map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse))
const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe<any>(map))(handled), options.scope)
nodeResponse.on("close", () => {
if (!nodeResponse.writableEnded) {
fiber.interruptUnsafe(parent.id, ClientAbort.annotation)
}
})
})
})
}
/**
* Creates a Node `upgrade` event handler for an Effect HTTP application,
* exposing the upgraded WebSocket as the request's `upgrade` effect and
* interrupting the request fiber when the socket closes early.
*
* @category handlers
* @since 4.0.0
*/
export const makeUpgradeHandler = <
R,
E,
App extends Effect.Effect<HttpServerResponse, any, any> = Effect.Effect<HttpServerResponse, E, R>
>(
lazyWss: Effect.Effect<NodeWS.WebSocketServer>,
httpEffect: Effect.Effect<HttpServerResponse, E, R>,
options: {
readonly scope: Scope.Scope
readonly middleware?: Middleware.HttpMiddleware.Applied<App, E, R> | undefined
}
): Effect.Effect<
(nodeRequest: Http.IncomingMessage, socket: Duplex, head: Buffer) => void,
never,
Exclude<Effect.Services<App>, HttpServerRequest | Scope.Scope>
> => {
const handledApp = HttpEffect.toHandled(httpEffect, handleResponse, options.middleware as any)
return Effect.withFiber((parent) => {
const services = parent.context
return Effect.succeed(function handler(
nodeRequest: Http.IncomingMessage,
socket: Duplex,
head: Buffer
) {
let nodeResponse_: Http.ServerResponse | undefined = undefined
const nodeResponse = () => {
if (nodeResponse_ === undefined) {
nodeResponse_ = new Http.ServerResponse(nodeRequest)
nodeResponse_.assignSocket(socket as any)
nodeResponse_.on("finish", () => {
socket.end()
})
}
return nodeResponse_
}
const upgradeEffect = Socket.fromWebSocket(Effect.flatMap(
lazyWss,
(wss) =>
Effect.acquireRelease(
Effect.callback<globalThis.WebSocket>((resume) =>
wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
resume(Effect.succeed(ws as any))
})
),
(ws) => Effect.sync(() => ws.close())
)
))
const map = new Map(services.mapUnsafe)
map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect))
const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe<any>(map))(handledApp), options.scope)
socket.on("close", () => {
if (!socket.writableEnded) {
fiber.interruptUnsafe(parent.id, ClientAbort.annotation)
}
})
})
})
}
class ServerRequestImpl extends NodeHttpIncomingMessage<HttpServerError> implements HttpServerRequest {
readonly [Request.TypeId]: typeof Request.TypeId
readonly response: Http.ServerResponse | LazyArg<Http.ServerResponse>
private upgradeEffect?: Effect.Effect<Socket.Socket, HttpServerError> | undefined
readonly url: string
private headersOverride?: Headers.Headers | undefined
constructor(
source: Http.IncomingMessage,
response: Http.ServerResponse | LazyArg<Http.ServerResponse>,
upgradeEffect?: Effect.Effect<Socket.Socket, HttpServerError>,
url = source.url!,
headersOverride?: Headers.Headers,
remoteAddressOverride?: Option.Option<string>
) {
super(source, (cause) =>
new HttpServerError({
reason: new RequestParseError({
request: this,
cause
})
}), remoteAddressOverride)
this[Request.TypeId] = Request.TypeId
this.response = response
this.upgradeEffect = upgradeEffect
this.url = url
this.headersOverride = headersOverride
}
private cachedCookies: Record.ReadonlyRecord<string, string> | undefined
get cookies() {
if (this.cachedCookies) {
return this.cachedCookies
}
return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "")
}
get resolvedResponse(): Http.ServerResponse {
return typeof this.response === "function" ? this.response() : this.response
}
modify(
options: {
readonly url?: string | undefined
readonly headers?: Headers.Headers | undefined
readonly remoteAddress?: Option.Option<string> | undefined
}
) {
return new ServerRequestImpl(
this.source,
this.response,
this.upgradeEffect,
options.url ?? this.url,
options.headers ?? this.headersOverride,
"remoteAddress" in options ? options.remoteAddress : this.remoteAddressOverride
)
}
get originalUrl(): string {
return this.source.url!
}
get method(): HttpMethod {
return this.source.method!.toUpperCase() as HttpMethod
}
override get headers(): Headers.Headers {
this.headersOverride ??= this.source.headers as Headers.Headers
return this.headersOverride
}
private multipartEffect:
| Effect.Effect<
Multipart.Persisted,
Multipart.MultipartError,
Scope.Scope | FileSystem.FileSystem | Path.Path
>
| undefined
get multipart(): Effect.Effect<
Multipart.Persisted,
Multipart.MultipartError,
Scope.Scope | FileSystem.FileSystem | Path.Path
> {
if (this.multipartEffect) {
return this.multipartEffect
}
this.multipartEffect = Effect.runSync(Effect.cached(
NodeMultipart.persisted(this.source, this.source.headers)
))
return this.multipartEffect
}
get multipartStream(): Stream.Stream<Multipart.Part, Multipart.MultipartError> {
return NodeMultipart.stream(this.source, this.source.headers)
}
get upgrade(): Effect.Effect<Socket.Socket, HttpServerError> {
return this.upgradeEffect ?? Effect.fail(
new HttpServerError({
reason: new RequestParseError({
request: this,
description: "not an upgradeable ServerRequest"
})
})
)
}
override toString(): string {
return `ServerRequest(${this.method} ${this.url})`
}
toJSON(): unknown {
return HttpIncomingMessage.inspect(this, {
_id: "HttpServerRequest",
method: this.method,
url: this.originalUrl
})
}
}
/**
* Provides an `HttpServer` by creating and managing a scoped Node
* `http.Server` with the supplied listen and shutdown options.
*
* @category layers
* @since 4.0.0
*/
export const layerServer: (
evaluate: LazyArg<Http.Server<typeof Http.IncomingMessage, typeof Http.ServerResponse>>,
options: Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
) => Layer.Layer<HttpServer.HttpServer, ServeError> = flow(make, Layer.effect(HttpServer.HttpServer))
/**
* Provides the Node HTTP support services used by `NodeHttpServer`, including
* the HTTP platform, ETag generator, and core Node platform services.
*
* @category layers
* @since 4.0.0
*/
export const layerHttpServices: Layer.Layer<
NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator
> = Layer.mergeAll(
NodeHttpPlatform.layer,
Etag.layerWeak,
NodeServices.layer
)
/**
* Provides a Node `HttpServer` together with the Node HTTP platform, ETag, and
* core platform services required to serve requests.
*
* @category layers
* @since 4.0.0
*/
export const layer = (
evaluate: LazyArg<Http.Server>,
options: Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
): Layer.Layer<
HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator,
ServeError
> =>
Layer.mergeAll(
layerServer(evaluate, options),
layerHttpServices
)
/**
* Provides a Node `HttpServer` together with the Node HTTP platform, ETag,
* and core Node platform services, reading the listen and shutdown options from
* a `Config` value.
*
* @category layers
* @since 4.0.0
*/
export const layerConfig = (
evaluate: LazyArg<Http.Server>,
options: Config.Wrap<
Net.ListenOptions & {
readonly disablePreemptiveShutdown?: boolean | undefined
readonly gracefulShutdownTimeout?: Duration.Input | undefined
}
>
): Layer.Layer<
HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator,
ServeError | Config.ConfigError
> =>
Layer.mergeAll(
Layer.effect(HttpServer.HttpServer)(
Effect.flatMap(Config.unwrap(options), (options) => make(evaluate, options))
),
layerHttpServices
)
/**
* Provides a test HTTP server listening on an ephemeral port together with a
* Fetch-backed `HttpClient` configured for server integration tests.
*
* @category testing
* @since 4.0.0
*/
export const layerTest: Layer.Layer<
| HttpServer.HttpServer
| FileSystem.FileSystem
| Path.Path
| HttpPlatform.HttpPlatform
| Etag.Generator
| HttpClient,
ServeError,
never
> = HttpServer.layerTestClient.pipe(
Layer.provide(
Layer.fresh(FetchHttpClient.layer).pipe(
Layer.provide(Layer.succeed(FetchHttpClient.RequestInit)({ keepalive: false }))
)
),
Layer.provideMerge(layer(Http.createServer, { port: 0 }))
)
// -----------------------------------------------------------------------------
// Internal
// -----------------------------------------------------------------------------
const handleResponse = (
request: HttpServerRequest,
response: HttpServerResponse
): Effect.Effect<void, HttpServerError> => {
const nodeResponse = (request as ServerRequestImpl).resolvedResponse
if (nodeResponse.writableEnded) {
return Effect.void
}
let headers: Record<string, string | Array<string>> = response.headers
if (!Cookies.isEmpty(response.cookies)) {
headers = { ...headers }
const toSet = Cookies.toSetCookieHeaders(response.cookies)
if (headers["set-cookie"] !== undefined) {
toSet.push(headers["set-cookie"] as string)
}
headers["set-cookie"] = toSet
}
if (request.method === "HEAD") {
nodeResponse.writeHead(response.status, headers)
return Effect.callback<void>((resume) => {
nodeResponse.end(() => resume(Effect.void))
})
}
const body = response.body
switch (body._tag) {
case "Empty": {
nodeResponse.writeHead(response.status, headers)
nodeResponse.end()
return Effect.void
}
case "Raw": {
nodeResponse.writeHead(response.status, headers)
if (
typeof body.body === "object" && body.body !== null && "pipe" in body.body &&
typeof body.body.pipe === "function"
) {
return Effect.tryPromise({
try: (signal) => pipeline(body.body as any, nodeResponse, { signal, end: true }),
catch: (cause) =>
new HttpServerError({
reason: new ResponseError({
request,
response,
description: "Error writing raw response",
cause
})
})
}).pipe(
Effect.interruptible,
Effect.tapCause(handleCause(nodeResponse, response))
)
}
return Effect.callback<void>((resume) => {
nodeResponse.end(body.body, () => resume(Effect.void))
})
}
case "Uint8Array": {
nodeResponse.writeHead(response.status, headers)
// If the body is less than 1MB, we skip the callback
if (body.body.length < 1024 * 1024) {
nodeResponse.end(body.body)
return Effect.void
}
return Effect.callback<void>((resume) => {
nodeResponse.end(body.body, () => resume(Effect.void))
})
}
case "FormData": {
return Effect.suspend(() => {
const r = new globalThis.Response(body.formData)
nodeResponse.writeHead(response.status, {
...headers,
...Object.fromEntries(r.headers)
})
return Effect.callback<void, HttpServerError>((resume, signal) => {
Readable.fromWeb(r.body as any, { signal })
.pipe(nodeResponse)
.on("error", (cause) => {
resume(Effect.fail(
new HttpServerError({
reason: new ResponseError({
request,
response,
description: "Error writing FormData response",
cause
})
})
))
})
.once("finish", () => {
resume(Effect.void)
})
}).pipe(
Effect.interruptible,
Effect.tapCause(handleCause(nodeResponse, response))
)
})
}
case "Stream": {
nodeResponse.writeHead(response.status, headers)
const drainLatch = Latch.makeUnsafe()
nodeResponse.on("drain", () => drainLatch.openUnsafe())
return body.stream.pipe(
Stream.orDie,
Stream.runForEachArray((array) => {
let needDrain = false
for (let i = 0; i < array.length; i++) {
const written = nodeResponse.write(array[i])
if (!written && !needDrain) {
needDrain = true
drainLatch.closeUnsafe()
} else if (written && needDrain) {
needDrain = false
}
}
if (!needDrain) return Effect.void
return drainLatch.await
}),
Effect.interruptible,
Effect.matchCauseEffect({
onSuccess: () => Effect.sync(() => nodeResponse.end()),
onFailure: handleCause(nodeResponse, response)
})
)
}
}
}
const handleCause = (
nodeResponse: Http.ServerResponse,
originalResponse: HttpServerResponse
) =>
<E>(originalCause: Cause.Cause<E>) =>
Effect.flatMap(causeResponse(originalCause), ([response, cause]) => {
const headersSent = nodeResponse.headersSent
if (!headersSent) {
nodeResponse.writeHead(response.status)
}
if (!nodeResponse.writableEnded) {
nodeResponse.end()
}
return Effect.failCause(
headersSent
? Cause.combine(originalCause, Cause.die(originalResponse))
: cause
)
})

View File

@@ -0,0 +1,33 @@
/**
* Accessors for the Node.js objects behind an Effect HTTP server request.
*
* `toIncomingMessage` returns the underlying Node `http.IncomingMessage`.
* `toServerResponse` returns the underlying Node `http.ServerResponse`,
* evaluating the stored response thunk when the response was created lazily.
*
* @since 4.0.0
*/
import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"
import type * as Http from "node:http"
/**
* Returns the underlying Node `IncomingMessage` for a platform Node
* `HttpServerRequest`.
*
* @category accessors
* @since 4.0.0
*/
export const toIncomingMessage = (self: HttpServerRequest): Http.IncomingMessage => self.source as any
/**
* Returns the underlying Node `ServerResponse` for a platform Node
* `HttpServerRequest`, evaluating the stored response thunk when the response
* was created lazily.
*
* @category accessors
* @since 4.0.0
*/
export const toServerResponse = (self: HttpServerRequest): Http.ServerResponse => {
const res = (self as any).response
return typeof res === "function" ? res() : res
}

View File

@@ -0,0 +1,183 @@
/**
* Node.js multipart parsing for HTTP `multipart/form-data` request bodies.
*
* `NodeMultipart` adapts a Node `Readable` plus incoming HTTP headers into
* Effect's shared multipart model. It can expose form parts as a stream or
* collect a complete persisted form by writing file uploads to scoped temporary
* files through the current `FileSystem` and `Path` services. `fileToReadable`
* returns the underlying Node readable stream for file parts produced by this
* parser.
*
* @since 4.0.0
*/
import * as Effect from "effect/Effect"
import type * as FileSystem from "effect/FileSystem"
import * as Inspectable from "effect/Inspectable"
import type * as Path from "effect/Path"
import type * as Scope from "effect/Scope"
import * as Stream from "effect/Stream"
import * as Multipart from "effect/unstable/http/Multipart"
import * as MP from "effect/unstable/http/Multipasta/Node"
import * as NFS from "node:fs"
import type { IncomingHttpHeaders } from "node:http"
import type { Readable } from "node:stream"
import * as NodeStreamP from "node:stream/promises"
import * as NodeStream from "./NodeStream.ts"
/**
* Parses multipart data from a Node readable request body and headers into a
* stream of `Multipart.Part` values, converting parser failures to
* `MultipartError`.
*
* @category constructors
* @since 4.0.0
*/
export const stream = (
source: Readable,
headers: IncomingHttpHeaders
): Stream.Stream<Multipart.Part, Multipart.MultipartError> =>
Multipart.makeConfig(headers as any).pipe(
Effect.map((config) =>
NodeStream.fromReadable<MP.Part, Multipart.MultipartError>({
evaluate() {
const parser = MP.make(config)
source.pipe(parser)
return parser as any
},
onError: (error) => convertError(error as any)
})
),
Stream.unwrap,
Stream.map(convertPart)
)
/**
* Parses multipart data from a Node readable request body and persists file
* parts using the current `FileSystem`, `Path`, and `Scope` services.
*
* @category constructors
* @since 4.0.0
*/
export const persisted = (
source: Readable,
headers: IncomingHttpHeaders
): Effect.Effect<
Multipart.Persisted,
Multipart.MultipartError,
Scope.Scope | FileSystem.FileSystem | Path.Path
> =>
Multipart.toPersisted(stream(source, headers), (path, file) =>
Effect.tryPromise({
try: (signal) => NodeStreamP.pipeline((file as FileImpl).file, NFS.createWriteStream(path), { signal }),
catch: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
}))
/**
* Returns the underlying Node readable stream for a multipart file produced by
* the Node multipart parser.
*
* @category converting
* @since 4.0.0
*/
export const fileToReadable = (file: Multipart.File): Readable => (file as FileImpl).file
// -----------------------------------------------------------------------------
// Internal
// -----------------------------------------------------------------------------
const convertPart = (part: MP.Part): Multipart.Part =>
part._tag === "Field" ? new FieldImpl(part.info, part.value) : new FileImpl(part)
abstract class PartBase extends Inspectable.Class {
readonly [Multipart.TypeId]: typeof Multipart.TypeId
constructor() {
super()
this[Multipart.TypeId] = Multipart.TypeId
}
}
class FieldImpl extends PartBase implements Multipart.Field {
readonly _tag = "Field"
readonly key: string
readonly contentType: string
readonly value: string
constructor(
info: MP.PartInfo,
value: Uint8Array
) {
super()
this.key = info.name
this.contentType = info.contentType
this.value = MP.decodeField(info, value)
}
toJSON(): unknown {
return {
_id: "@effect/platform/Multipart/Part",
_tag: "Field",
key: this.key,
value: this.value,
contentType: this.contentType
}
}
}
class FileImpl extends PartBase implements Multipart.File {
readonly _tag = "File"
readonly key: string
readonly name: string
readonly contentType: string
readonly content: Stream.Stream<Uint8Array, Multipart.MultipartError>
readonly contentEffect: Effect.Effect<Uint8Array, Multipart.MultipartError>
readonly file: MP.FileStream
constructor(file: MP.FileStream) {
super()
this.file = file
this.key = file.info.name
this.name = file.filename ?? file.info.name
this.contentType = file.info.contentType
this.content = NodeStream.fromReadable({
evaluate: () => file,
onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
})
this.contentEffect = NodeStream.toUint8Array(() => file, {
onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause)
})
}
toJSON(): unknown {
return {
_id: "@effect/platform/Multipart/Part",
_tag: "File",
key: this.key,
name: this.name,
contentType: this.contentType
}
}
}
function convertError(cause: MP.MultipartError): Multipart.MultipartError {
switch (cause._tag) {
case "ReachedLimit": {
switch (cause.limit) {
case "MaxParts": {
return Multipart.MultipartError.fromReason("TooManyParts", cause)
}
case "MaxFieldSize": {
return Multipart.MultipartError.fromReason("FieldTooLarge", cause)
}
case "MaxPartSize": {
return Multipart.MultipartError.fromReason("FileTooLarge", cause)
}
case "MaxTotalSize": {
return Multipart.MultipartError.fromReason("BodyTooLarge", cause)
}
}
}
default: {
return Multipart.MultipartError.fromReason("Parse", cause)
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* Node.js layers for Effect's `Path` service.
*
* This module provides the default, POSIX, and Windows variants of the
* platform-independent `Path` service by reusing the shared Node path
* implementation. The provided path services include Node file URL conversion
* behavior.
*
* @since 4.0.0
*/
import * as NodePath from "@effect/platform-node-shared/NodePath"
import type * as Layer from "effect/Layer"
import type { Path } from "effect/Path"
/**
* Provides the default Node `Path` service using the platform's `node:path`
* implementation.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Path> = NodePath.layer
/**
* Provides the `Path` service using Node's POSIX path implementation,
* regardless of the host platform.
*
* @category layers
* @since 4.0.0
*/
export const layerPosix: Layer.Layer<Path> = NodePath.layerPosix
/**
* Provides the `Path` service using Node's Windows path implementation,
* regardless of the host platform.
*
* @category layers
* @since 4.0.0
*/
export const layerWin32: Layer.Layer<Path> = NodePath.layerWin32

View File

@@ -0,0 +1,92 @@
/**
* Node.js Redis integration backed by `ioredis`.
*
* This module creates a scoped `ioredis` client and exposes it in two forms:
* the generic `Redis` service and the {@link NodeRedis} service for direct
* access to the underlying client. `layer` accepts ioredis options directly,
* while `layerConfig` reads them from Effect config. Both layers close the
* client when the layer scope ends.
*
* @since 4.0.0
*/
import * as Config from "effect/Config"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Fn from "effect/Function"
import * as Layer from "effect/Layer"
import * as Scope from "effect/Scope"
import * as Redis from "effect/unstable/persistence/Redis"
import * as IoRedis from "ioredis"
/**
* Service tag for the Node Redis integration, exposing the underlying
* `ioredis` client and a `use` helper that maps client failures to
* `RedisError`.
*
* @category services
* @since 4.0.0
*/
export class NodeRedis extends Context.Service<NodeRedis, {
readonly client: IoRedis.Redis
readonly use: <A>(f: (client: IoRedis.Redis) => Promise<A>) => Effect.Effect<A, Redis.RedisError>
}>()("@effect/platform-node/NodeRedis") {}
const make = Effect.fnUntraced(function*(
options?: IoRedis.RedisOptions
) {
const scope = yield* Effect.scope
yield* Scope.addFinalizer(scope, Effect.promise(() => client.quit()))
const client = new IoRedis.Redis(options ?? {})
const use = <A>(f: (client: IoRedis.Redis) => Promise<A>) =>
Effect.tryPromise({
try: () => f(client),
catch: (cause) => new Redis.RedisError({ cause })
})
const redis = yield* Redis.make({
send: <A = unknown>(command: string, ...args: ReadonlyArray<string>) =>
Effect.tryPromise({
try: () => client.call(command, ...args) as Promise<A>,
catch: (cause) => new Redis.RedisError({ cause })
})
})
const nodeRedis = Fn.identity<NodeRedis["Service"]>({
client,
use
})
return Context.make(NodeRedis, nodeRedis).pipe(
Context.add(Redis.Redis, redis)
)
})
/**
* Provides `Redis` and `NodeRedis` services backed by an `ioredis` client
* created with the supplied options and closed when the layer scope ends.
*
* @category layers
* @since 4.0.0
*/
export const layer = (
options?: IoRedis.RedisOptions | undefined
): Layer.Layer<Redis.Redis | NodeRedis> => Layer.effectContext(make(options))
/**
* Provides `Redis` and `NodeRedis` services from `Config`-backed ioredis
* options, closing the client when the layer scope ends.
*
* @category layers
* @since 4.0.0
*/
export const layerConfig: (
options: Config.Wrap<IoRedis.RedisOptions>
) => Layer.Layer<Redis.Redis | NodeRedis, Config.ConfigError> = (
options: Config.Wrap<IoRedis.RedisOptions>
): Layer.Layer<Redis.Redis | NodeRedis, Config.ConfigError> =>
Layer.effectContext(
Config.unwrap(options).pipe(
Effect.flatMap(make)
)
)

View File

@@ -0,0 +1,53 @@
/**
* Node.js process runner for Effect programs.
*
* This module exports `runMain`, which runs one Effect as the main process
* fiber in Node.js. It reuses the shared Node runtime runner, including its
* error reporting, `SIGINT` / `SIGTERM` interruption, and optional teardown
* behavior.
*
* @since 4.0.0
*/
import * as NodeRuntime from "@effect/platform-node-shared/NodeRuntime"
import type { Effect } from "effect/Effect"
import type * as Runtime from "effect/Runtime"
/**
* Helps you run a main effect with built-in error handling, logging, and signal management.
*
* **When to use**
*
* Use to run a Node.js application's main Effect with structured error
* handling, log management, interrupt support, or advanced teardown
* capabilities.
*
* **Details**
*
* This function launches an Effect as the main entry point, setting exit codes
* based on success or failure, handling interrupts (e.g., Ctrl+C), and optionally
* logging errors. By default, it logs errors and uses a "pretty" format, but both
* behaviors can be turned off. You can also provide custom teardown logic to
* finalize resources or produce different exit codes.
*
* The optional configuration object can include:
* - `disableErrorReporting`: Turn off automatic error logging.
* - `teardown`: Provide custom finalization logic.
*
* @category running
* @since 4.0.0
*/
export const runMain: {
(
options?: {
readonly disableErrorReporting?: boolean | undefined
readonly teardown?: Runtime.Teardown | undefined
}
): <E, A>(effect: Effect<A, E>) => void
<E, A>(
effect: Effect<A, E>,
options?: {
readonly disableErrorReporting?: boolean | undefined
readonly teardown?: Runtime.Teardown | undefined
}
): void
} = NodeRuntime.runMain

View File

@@ -0,0 +1,50 @@
/**
* Aggregate Node.js platform services layer.
*
* This module defines the `NodeServices` union and a single `layer` that
* provides Node-backed child process spawning, crypto, filesystem, path, stdio,
* and terminal services. Use the layer when a Node program wants the standard
* platform services from one place.
*
* @since 4.0.0
*/
import type { Crypto } from "effect/Crypto"
import type { FileSystem } from "effect/FileSystem"
import * as Layer from "effect/Layer"
import type { Path } from "effect/Path"
import type { Stdio } from "effect/Stdio"
import type { Terminal } from "effect/Terminal"
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts"
import * as NodeCrypto from "./NodeCrypto.ts"
import * as NodeFileSystem from "./NodeFileSystem.ts"
import * as NodePath from "./NodePath.ts"
import * as NodeStdio from "./NodeStdio.ts"
import * as NodeTerminal from "./NodeTerminal.ts"
/**
* The union of core services provided by the Node platform layer, including
* child process spawning, filesystem, path, stdio, and terminal services.
*
* @category models
* @since 4.0.0
*/
export type NodeServices = ChildProcessSpawner | Crypto | FileSystem | Path | Stdio | Terminal
/**
* Provides the default Node implementations for child process spawning,
* filesystem, path, stdio, and terminal services.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<NodeServices> = Layer.provideMerge(
NodeChildProcessSpawner.layer,
Layer.mergeAll(
NodeFileSystem.layer,
NodeCrypto.layer,
NodePath.layer,
NodeStdio.layer,
NodeTerminal.layer
)
)

View File

@@ -0,0 +1,8 @@
/**
* @since 4.0.0
*/
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeSink"

View File

@@ -0,0 +1,72 @@
/**
* Node.js socket constructors and layers for Effect sockets.
*
* This module re-exports the shared Node socket support for TCP connections,
* Unix domain socket connections, and Node `Duplex` streams. It also provides
* WebSocket constructor layers: one that uses `globalThis.WebSocket` when
* present and falls back to `ws`, one that always uses `ws`, and one that
* creates a `Socket.Socket` layer for a WebSocket URL.
*
* @since 4.0.0
*/
import { NodeWS as WS } from "@effect/platform-node-shared/NodeSocket"
import type * as Duration from "effect/Duration"
import type * as Effect from "effect/Effect"
import { flow } from "effect/Function"
import * as Layer from "effect/Layer"
import * as Socket from "effect/unstable/socket/Socket"
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeSocket"
/**
* Provides a `Socket.WebSocketConstructor`, using `globalThis.WebSocket` when
* available and falling back to the `ws` package otherwise.
*
* @category layers
* @since 4.0.0
*/
export const layerWebSocketConstructor: Layer.Layer<
Socket.WebSocketConstructor
> = Layer.sync(Socket.WebSocketConstructor)(() => {
if ("WebSocket" in globalThis) {
return (url, protocols) => new globalThis.WebSocket(url, protocols)
}
return (url, protocols) => new WS.WebSocket(url, protocols) as unknown as globalThis.WebSocket
})
/**
* Provides a `Socket.WebSocketConstructor` backed explicitly by the `ws`
* package.
*
* @category layers
* @since 4.0.0
*/
export const layerWebSocketConstructorWS: Layer.Layer<
Socket.WebSocketConstructor
> = Layer.succeed(Socket.WebSocketConstructor)(
(url, protocols) => new WS.WebSocket(url, protocols) as unknown as globalThis.WebSocket
)
/**
* Creates a `Socket.Socket` layer for a WebSocket URL using the Node WebSocket
* constructor layer, honoring protocol, open-timeout, and close-code error
* options.
*
* @category layers
* @since 4.0.0
*/
export const layerWebSocket: (
url: string | Effect.Effect<string>,
options?: {
readonly closeCodeIsError?: ((code: number) => boolean) | undefined
readonly openTimeout?: Duration.Input | undefined
readonly protocols?: string | Array<string> | undefined
} | undefined
) => Layer.Layer<Socket.Socket, never, never> = flow(
Socket.makeWebSocket,
Layer.effect(Socket.Socket),
Layer.provide(layerWebSocketConstructor)
)

View File

@@ -0,0 +1,8 @@
/**
* @since 4.0.0
*/
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeSocketServer"

View File

@@ -0,0 +1,22 @@
/**
* Node.js `Stdio` layer for the current process.
*
* The exported layer reuses the shared Node stdio implementation. It satisfies
* the platform-independent `Stdio` service by reading command-line arguments
* from `process.argv`, consuming input from `process.stdin`, and writing output
* streams to `process.stdout` and `process.stderr`.
*
* @since 4.0.0
*/
import * as NodeStdio from "@effect/platform-node-shared/NodeStdio"
import type * as Layer from "effect/Layer"
import type { Stdio } from "effect/Stdio"
/**
* Provides the `Stdio` service backed by the current process arguments,
* stdin, stdout, and stderr streams.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<Stdio> = NodeStdio.layer

View File

@@ -0,0 +1,8 @@
/**
* @since 4.0.0
*/
/**
* @since 4.0.0
*/
export * from "@effect/platform-node-shared/NodeStream"

View File

@@ -0,0 +1,32 @@
/**
* Node.js implementation of the Effect `Terminal` service.
*
* This module reuses the shared Node terminal implementation. `make` creates a
* scoped process-backed `Terminal` service, and `layer` provides the default
* service with the standard quit behavior for key input.
*
* @since 4.0.0
*/
import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal"
import type { Effect } from "effect/Effect"
import type { Layer } from "effect/Layer"
import type { Scope } from "effect/Scope"
import type { Terminal, UserInput } from "effect/Terminal"
/**
* Creates a scoped `Terminal` service backed by process stdin/stdout, using the
* optional predicate to decide when key input should end the input stream.
*
* @category constructors
* @since 4.0.0
*/
export const make: (shouldQuit?: (input: UserInput) => boolean) => Effect<Terminal, never, Scope> = NodeTerminal.make
/**
* Provides the default process-backed `Terminal` service, ending key input on
* the default quit keys.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer<Terminal> = NodeTerminal.layer

View File

@@ -0,0 +1,121 @@
/**
* Parent-side Node.js support for Effect workers.
*
* `layerPlatform` installs the `WorkerPlatform` used by a Node program that
* owns workers. It supports both `node:worker_threads` workers and IPC-enabled
* child processes, routing messages through Effect's worker protocol. `layer`
* combines that platform with a `Spawner` callback, and the platform asks
* workers to close on scope finalization before forcefully terminating them on
* timeout.
*
* @since 4.0.0
*/
import * as Deferred from "effect/Deferred"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Layer from "effect/Layer"
import * as Scope from "effect/Scope"
import * as Worker from "effect/unstable/workers/Worker"
import { WorkerError, WorkerReceiveError } from "effect/unstable/workers/WorkerError"
import type * as ChildProcess from "node:child_process"
import type * as WorkerThreads from "node:worker_threads"
/**
* Provides the Node `WorkerPlatform` for `worker_threads` workers and child
* process workers, wiring messages, errors, and exits into Effect workers and
* terminating the worker if graceful shutdown times out.
*
* @category layers
* @since 4.0.0
*/
export const layerPlatform: Layer.Layer<Worker.WorkerPlatform> = Layer.succeed(Worker.WorkerPlatform)(
Worker.makePlatform<WorkerThreads.Worker | ChildProcess.ChildProcess>()({
setup({ scope, worker }) {
const exitDeferred = Deferred.makeUnsafe<void, WorkerError>()
const thing = "postMessage" in worker ?
{
postMessage(msg: any, t?: any) {
worker.postMessage(msg, t)
},
kill: () => worker.terminate(),
worker
} :
{
postMessage(msg: any, _?: any) {
worker.send(msg)
},
kill: () => worker.kill("SIGKILL"),
worker
}
worker.on("exit", () => {
Deferred.doneUnsafe(exitDeferred, Exit.void)
})
return Effect.as(
Scope.addFinalizer(
scope,
Effect.suspend(() => {
thing.postMessage([1])
return Deferred.await(exitDeferred)
}).pipe(
Effect.timeout(5000),
Effect.catchCause(() => Effect.sync(() => thing.kill()))
)
),
thing
)
},
listen({ deferred, emit, port }) {
port.worker.on("message", (message) => {
emit(message)
})
port.worker.on("messageerror", (cause) => {
Deferred.doneUnsafe(
deferred,
new WorkerError({
reason: new WorkerReceiveError({
message: "An messageerror event was emitted",
cause
})
})
)
})
port.worker.on("error", (cause) => {
Deferred.doneUnsafe(
deferred,
new WorkerError({
reason: new WorkerReceiveError({
message: "An error event was emitted",
cause
})
})
)
})
port.worker.on("exit", (code) => {
Deferred.doneUnsafe(
deferred,
new WorkerError({
reason: new WorkerReceiveError({
message: "The worker has exited with code: " + code
})
})
)
})
return Effect.void
}
})
)
/**
* Provides the Node `WorkerPlatform` together with a `Worker.Spawner` created
* from the supplied worker or child-process spawning function.
*
* @category layers
* @since 4.0.0
*/
export const layer = (
spawn: (id: number) => WorkerThreads.Worker | ChildProcess.ChildProcess
): Layer.Layer<Worker.WorkerPlatform | Worker.Spawner> =>
Layer.merge(
Worker.layerSpawner(spawn),
layerPlatform
)

View File

@@ -0,0 +1,110 @@
/**
* Node.js runtime support for workers that serve Effect worker requests.
*
* `NodeWorkerRunner` supplies the Node implementation of the Effect worker
* runner platform. The exported `layer` runs inside a `node:worker_threads`
* worker through `parentPort`, or inside a child process through
* `process.send`. It listens for parent messages, runs handlers registered with
* `WorkerRunner`, sends replies over the same channel, and closes when the
* parent sends the close message.
*
* @since 4.0.0
*/
import * as Cause from "effect/Cause"
import * as Deferred from "effect/Deferred"
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Fiber from "effect/Fiber"
import * as Layer from "effect/Layer"
import { WorkerError, WorkerReceiveError, WorkerSpawnError } from "effect/unstable/workers/WorkerError"
import * as WorkerRunner from "effect/unstable/workers/WorkerRunner"
import * as WorkerThreads from "node:worker_threads"
/**
* Provides the `WorkerRunnerPlatform` for code running inside a Node worker
* thread or child process, routing parent messages to the registered handler
* and sending responses back through the parent channel.
*
* @category layers
* @since 4.0.0
*/
export const layer: Layer.Layer<WorkerRunner.WorkerRunnerPlatform> = Layer.succeed(WorkerRunner.WorkerRunnerPlatform)({
start<O = unknown, I = unknown>() {
return Effect.gen(function*() {
if (!WorkerThreads.parentPort && !process.send) {
return yield* new WorkerError({
reason: new WorkerSpawnError({ message: "not in a worker" })
})
}
const sendUnsafe = WorkerThreads.parentPort
? (_portId: number, message: any, transfers?: any) => WorkerThreads.parentPort!.postMessage(message, transfers)
: (_portId: number, message: any, _transfers?: any) => process.send!(message)
const send = (_portId: number, message: O, transfers?: ReadonlyArray<unknown>) =>
Effect.sync(() => sendUnsafe(_portId, [1, message], transfers as any))
const run = <A, E, R>(
handler: (portId: number, message: I) => Effect.Effect<A, E, R> | void
): Effect.Effect<void, WorkerError, R> =>
Effect.scopedWith(Effect.fnUntraced(function*(scope) {
const closeLatch = Deferred.makeUnsafe<void, WorkerError>()
const trackFiber = Fiber.runIn(scope)
const services = yield* Effect.context<R>()
const runFork = Effect.runForkWith(services)
const onExit = (exit: Exit.Exit<any, E>) => {
if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) {
runFork(Effect.logError("unhandled error in worker", exit.cause))
}
}
;(WorkerThreads.parentPort ?? process).on("message", (message: WorkerRunner.PlatformMessage<I>) => {
if (message[0] === 0) {
const result = handler(0, message[1])
if (Effect.isEffect(result)) {
const fiber = runFork(result)
fiber.addObserver(onExit)
trackFiber(fiber)
}
} else {
if (WorkerThreads.parentPort) {
WorkerThreads.parentPort.close()
} else {
process.channel?.unref()
}
Deferred.doneUnsafe(closeLatch, Exit.void)
}
})
if (WorkerThreads.parentPort) {
WorkerThreads.parentPort.on("messageerror", (cause) => {
Deferred.doneUnsafe(
closeLatch,
new WorkerError({
reason: new WorkerReceiveError({
message: "received messageerror event",
cause
})
})
)
})
WorkerThreads.parentPort.on("error", (cause) => {
Deferred.doneUnsafe(
closeLatch,
new WorkerError({
reason: new WorkerReceiveError({
message: "received messageerror event",
cause
})
})
)
})
}
sendUnsafe(0, [0])
return yield* Deferred.await(closeLatch)
}))
return { run, send, sendUnsafe }
})
}
})

View File

@@ -0,0 +1,29 @@
/**
* Re-export of the Undici HTTP client package used by the Node platform.
*
* This module gives Effect applications a package-local import for Undici
* primitives while working with `@effect/platform-node`. Import named Undici
* APIs from here when configuring Node HTTP client dispatchers, creating agents
* or mock agents, setting the process-global dispatcher, or sharing the same
* Undici types with integrations that use the platform HTTP client.
*
* The module does not wrap or reinterpret Undici behavior. It forwards the
* installed `undici` named exports and default export, so connection pooling,
* dispatcher lifetimes, mocking, aborts, and request options follow Undici's
* own semantics.
*
* @since 4.0.0
*/
import Undici from "undici"
/**
* @category Undici
* @since 4.0.0
*/
export * from "undici"
/**
* @category Undici
* @since 4.0.0
*/
export default Undici

View File

@@ -0,0 +1,130 @@
/**
* @since 4.0.0
*/
// @barrel: Auto-generated exports. Do not edit manually.
/**
* @since 4.0.0
*/
export * as Mime from "./Mime.ts"
/**
* @since 4.0.0
*/
export * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts"
/**
* @since 4.0.0
*/
export * as NodeClusterHttp from "./NodeClusterHttp.ts"
/**
* @since 4.0.0
*/
export * as NodeClusterSocket from "./NodeClusterSocket.ts"
/**
* @since 1.0.0
*/
export * as NodeCrypto from "./NodeCrypto.ts"
/**
* @since 4.0.0
*/
export * as NodeFileSystem from "./NodeFileSystem.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpClient from "./NodeHttpClient.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpIncomingMessage from "./NodeHttpIncomingMessage.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpPlatform from "./NodeHttpPlatform.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpServer from "./NodeHttpServer.ts"
/**
* @since 4.0.0
*/
export * as NodeHttpServerRequest from "./NodeHttpServerRequest.ts"
/**
* @since 4.0.0
*/
export * as NodeMultipart from "./NodeMultipart.ts"
/**
* @since 4.0.0
*/
export * as NodePath from "./NodePath.ts"
/**
* @since 4.0.0
*/
export * as NodeRedis from "./NodeRedis.ts"
/**
* @since 4.0.0
*/
export * as NodeRuntime from "./NodeRuntime.ts"
/**
* @since 4.0.0
*/
export * as NodeServices from "./NodeServices.ts"
/**
* @since 4.0.0
*/
export * as NodeSink from "./NodeSink.ts"
/**
* @since 4.0.0
*/
export * as NodeSocket from "./NodeSocket.ts"
/**
* @since 4.0.0
*/
export * as NodeSocketServer from "./NodeSocketServer.ts"
/**
* @since 4.0.0
*/
export * as NodeStdio from "./NodeStdio.ts"
/**
* @since 4.0.0
*/
export * as NodeStream from "./NodeStream.ts"
/**
* @since 4.0.0
*/
export * as NodeTerminal from "./NodeTerminal.ts"
/**
* @since 4.0.0
*/
export * as NodeWorker from "./NodeWorker.ts"
/**
* @since 4.0.0
*/
export * as NodeWorkerRunner from "./NodeWorkerRunner.ts"
/**
* @since 4.0.0
*/
export * as Undici from "./Undici.ts"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,253 @@
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"
import * as NodePathLayer from "@effect/platform-node/NodePath"
import { assert, describe, it } from "@effect/vitest"
import * as Layer from "effect/Layer"
import { HttpRouter, HttpStaticServer } from "effect/unstable/http"
import { copyFile, cp, mkdtemp, rm, utimes, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import * as NodePath from "node:path"
import { fileURLToPath } from "node:url"
const fixturesRoot = fileURLToPath(new URL("./fixtures/http-static-server", import.meta.url))
const fixturesOutsideFile = fileURLToPath(new URL("./fixtures/http-static-server-outside.txt", import.meta.url))
const staticFilesLayer = Layer.mergeAll(
NodePathLayer.layer,
NodeFileSystem.layer,
NodeHttpPlatform.layer
)
const makeHandler = (
root: string,
options: Omit<Parameters<typeof HttpStaticServer.make>[0], "root"> = {}
) =>
HttpRouter.toWebHandler(
HttpStaticServer.layer({
root,
...options
}).pipe(Layer.provideMerge(staticFilesLayer)),
{ disableLogger: true }
)
const withStaticFiles = async (
run: (context: {
readonly handler: (request: Request) => Promise<Response>
readonly root: string
readonly outsideFile: string
}) => Promise<void>,
options: Omit<Parameters<typeof HttpStaticServer.make>[0], "root"> = {}
) => {
const temporaryRoot = await mkdtemp(NodePath.join(tmpdir(), "effect-http-static-server-"))
const root = NodePath.join(temporaryRoot, "root")
const outsideFile = NodePath.join(temporaryRoot, NodePath.basename(fixturesOutsideFile))
await Promise.all([
cp(fixturesRoot, root, { recursive: true }),
copyFile(fixturesOutsideFile, outsideFile)
])
const { handler, dispose } = makeHandler(root, options)
try {
await run({ handler, root, outsideFile })
} finally {
await dispose()
await rm(temporaryRoot, { recursive: true, force: true })
}
}
describe("HttpStaticServer", () => {
it("serves files with expected content type and body", async () => {
await withStaticFiles(async ({ handler }) => {
const response = await handler(new Request("http://localhost/hello.txt"))
assert.strictEqual(response.status, 200)
assert.strictEqual(response.headers.get("content-type"), "text/plain; charset=utf-8")
assert.strictEqual((await response.text()).trimEnd(), "hello static file")
})
})
it("serves default index file for directory paths", async () => {
await withStaticFiles(async ({ handler }) => {
const response = await handler(new Request("http://localhost/guide"))
assert.strictEqual(response.status, 200)
assert.strictEqual(response.headers.get("content-type"), "text/html; charset=utf-8")
assert.strictEqual((await response.text()).trimEnd(), "<html><body>guide index</body></html>")
})
})
it("supports custom index file and disabled index fallback", async () => {
await withStaticFiles(async ({ handler }) => {
const response = await handler(new Request("http://localhost/custom"))
assert.strictEqual(response.status, 200)
assert.strictEqual((await response.text()).trimEnd(), "<html><body>custom home</body></html>")
}, { index: "home.html" })
await withStaticFiles(async ({ handler }) => {
const response = await handler(new Request("http://localhost/custom"))
assert.strictEqual(response.status, 404)
}, { index: undefined })
})
it("returns 304 for If-None-Match exact, weak, and wildcard", async () => {
await withStaticFiles(async ({ handler }) => {
const warmup = await handler(new Request("http://localhost/conditional.txt"))
const etag = warmup.headers.get("etag")
assert.notStrictEqual(etag, null)
if (etag === null) {
throw new Error("missing etag")
}
const weakComparable = etag.startsWith("W/") ? etag.slice(2) : `W/${etag}`
const exact = await handler(
new Request("http://localhost/conditional.txt", { headers: { "If-None-Match": etag } })
)
const weak = await handler(
new Request("http://localhost/conditional.txt", { headers: { "If-None-Match": weakComparable } })
)
const any = await handler(new Request("http://localhost/conditional.txt", { headers: { "If-None-Match": "*" } }))
assert.deepStrictEqual([exact.status, weak.status, any.status], [304, 304, 304])
assert.strictEqual(exact.headers.get("etag"), etag)
assert.strictEqual(exact.headers.get("cache-control"), "public, max-age=60")
assert.strictEqual(exact.headers.get("content-type"), null)
assert.strictEqual(exact.headers.get("content-length"), null)
}, { cacheControl: "public, max-age=60" })
})
it("returns 304 for If-Modified-Since and 200 when file changed", async () => {
await withStaticFiles(async ({ handler, root }) => {
const first = await handler(new Request("http://localhost/conditional.txt"))
const lastModified = first.headers.get("last-modified")
assert.notStrictEqual(lastModified, null)
if (lastModified === null) {
throw new Error("missing last-modified")
}
const notModified = await handler(
new Request("http://localhost/conditional.txt", {
headers: {
"If-Modified-Since": lastModified
}
})
)
assert.strictEqual(notModified.status, 304)
const conditionalFile = NodePath.join(root, "conditional.txt")
await writeFile(conditionalFile, "updated conditional body")
const updatedTime = new Date(Date.now() + 10_000)
await utimes(conditionalFile, updatedTime, updatedTime)
const modified = await handler(
new Request("http://localhost/conditional.txt", {
headers: {
"If-Modified-Since": lastModified
}
})
)
assert.strictEqual(modified.status, 200)
assert.strictEqual(await modified.text(), "updated conditional body")
})
})
it("handles range requests for valid, invalid, and malformed headers", async () => {
await withStaticFiles(async ({ handler }) => {
const fullBody = await handler(new Request("http://localhost/range.txt")).then((response) => response.text())
const fileSize = fullBody.length
const first = await handler(new Request("http://localhost/range.txt", { headers: { Range: "bytes=0-10" } }))
assert.strictEqual(first.status, 206)
assert.strictEqual(first.headers.get("content-range"), `bytes 0-10/${fileSize}`)
assert.strictEqual(await first.text(), "0123456789a")
const openEnded = await handler(new Request("http://localhost/range.txt", { headers: { Range: "bytes=5-" } }))
assert.strictEqual(openEnded.status, 206)
assert.strictEqual(openEnded.headers.get("content-range"), `bytes 5-${fileSize - 1}/${fileSize}`)
assert.strictEqual(await openEnded.text(), fullBody.slice(5))
const suffix = await handler(new Request("http://localhost/range.txt", { headers: { Range: "bytes=-10" } }))
assert.strictEqual(suffix.status, 206)
assert.strictEqual(suffix.headers.get("content-range"), `bytes ${fileSize - 10}-${fileSize - 1}/${fileSize}`)
assert.strictEqual(await suffix.text(), fullBody.slice(-10))
const invalid = await handler(new Request("http://localhost/range.txt", { headers: { Range: "bytes=100-200" } }))
assert.strictEqual(invalid.status, 416)
assert.strictEqual(invalid.headers.get("content-range"), `bytes */${fileSize}`)
const malformed = await handler(new Request("http://localhost/range.txt", { headers: { Range: "bytes=abc" } }))
assert.strictEqual(malformed.status, 200)
assert.strictEqual(await malformed.text(), fullBody)
})
})
it("handles SPA fallback for html accept and missing routes", async () => {
await withStaticFiles(async ({ handler }) => {
const htmlFallback = await handler(new Request("http://localhost/missing", { headers: { accept: "text/html" } }))
assert.strictEqual(htmlFallback.status, 200)
assert.strictEqual((await htmlFallback.text()).trimEnd(), "<html><body>root index</body></html>")
const withExtension = await handler(
new Request("http://localhost/missing.js", { headers: { accept: "text/html" } })
)
assert.strictEqual(withExtension.status, 404)
const withoutHtmlAccept = await handler(
new Request("http://localhost/missing", { headers: { accept: "application/json" } })
)
assert.strictEqual(withoutHtmlAccept.status, 404)
}, { spa: true, index: "index.html" })
})
it("rejects directory traversal attempts", async () => {
await withStaticFiles(async ({ handler, outsideFile }) => {
const plainTraversal = await handler(new Request("http://localhost/../../../etc/passwd"))
const encodedTraversal = await handler(new Request("http://localhost/..%2F..%2Fetc%2Fpasswd"))
const encodedExistingParentFileTraversal = await handler(
new Request(`http://localhost/..%2F${encodeURIComponent(NodePath.basename(outsideFile))}`)
)
assert.deepStrictEqual([
plainTraversal.status,
encodedTraversal.status,
encodedExistingParentFileTraversal.status
], [
404,
404,
404
])
})
})
it("rejects null bytes and malformed uri encoding", async () => {
await withStaticFiles(async ({ handler }) => {
const nullByte = await handler(new Request("http://localhost/null%00byte.txt"))
const malformed = await handler(new Request("http://localhost/%E0%A4%A"))
assert.deepStrictEqual([nullByte.status, malformed.status], [404, 404])
})
})
it("applies custom mime types and cache-control", async () => {
await withStaticFiles(async ({ handler }) => {
const response = await handler(new Request("http://localhost/hello.txt"))
assert.strictEqual(response.status, 200)
assert.strictEqual(response.headers.get("content-type"), "application/x-custom-text")
assert.strictEqual(response.headers.get("cache-control"), "public, max-age=120")
}, {
cacheControl: "public, max-age=120",
mimeTypes: {
txt: "application/x-custom-text"
}
})
})
it("uses application/octet-stream for unknown extension and 404 for missing file", async () => {
await withStaticFiles(async ({ handler }) => {
const unknown = await handler(new Request("http://localhost/file.binx"))
assert.strictEqual(unknown.status, 200)
assert.strictEqual(unknown.headers.get("content-type"), "application/octet-stream")
const missing = await handler(new Request("http://localhost/does-not-exist.txt"))
assert.strictEqual(missing.status, 404)
})
})
})

View File

@@ -0,0 +1,296 @@
import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import * as Layer from "effect/Layer"
import * as Option from "effect/Option"
import * as Path from "effect/Path"
import * as PlatformError from "effect/PlatformError"
import {
HttpEffect,
HttpPlatform,
HttpRouter,
HttpServerError,
HttpServerRequest,
HttpServerResponse,
HttpStaticServer
} from "effect/unstable/http"
const root = "/root"
const filePath = `${root}/file.txt`
const fileBody = "hello static"
const lastModified = "Wed, 01 Jan 2025 00:00:00 GMT"
const notFoundError = (path: string) =>
PlatformError.systemError({
_tag: "NotFound",
module: "FileSystem",
method: "stat",
description: "No such file or directory",
pathOrDescriptor: path
})
const permissionDeniedError = (path: string) =>
PlatformError.systemError({
_tag: "PermissionDenied",
module: "FileSystem",
method: "stat",
description: "Operation not permitted",
pathOrDescriptor: path
})
const fileInfo: FileSystem.File.Info = {
type: "File",
mtime: Option.some(new Date(lastModified)),
atime: Option.none(),
birthtime: Option.none(),
dev: 0,
ino: Option.none(),
mode: 0,
nlink: Option.none(),
uid: Option.none(),
gid: Option.none(),
rdev: Option.none(),
size: FileSystem.Size(fileBody.length),
blksize: Option.none(),
blocks: Option.none()
}
const makeHandler = async () => {
const fileSystem = FileSystem.makeNoop({
stat: (path) => path === filePath ? Effect.succeed(fileInfo) : Effect.fail(notFoundError(path))
})
const httpPlatform = HttpPlatform.HttpPlatform.of({
fileResponse: (_path, options) =>
Effect.succeed(HttpServerResponse.text(fileBody, {
status: options?.status,
headers: {
ETag: "\"etag-value\"",
"Last-Modified": lastModified
}
})),
fileWebResponse: () => Effect.die("not implemented")
})
const app = await Effect.runPromise(
HttpStaticServer.make({
root,
cacheControl: "public, max-age=60"
}).pipe(
Effect.provide(Path.layer),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(HttpPlatform.HttpPlatform, httpPlatform)
)
)
return HttpEffect.toWebHandler(app)
}
const makeFailingApp = async (options: {
readonly statError?: PlatformError.PlatformError
readonly fileResponseError?: PlatformError.PlatformError
}) => {
const fileSystem = FileSystem.makeNoop({
stat: (path) => {
if (options.statError !== undefined) {
return Effect.fail(options.statError)
}
return path === filePath ? Effect.succeed(fileInfo) : Effect.fail(notFoundError(path))
}
})
const httpPlatform = HttpPlatform.HttpPlatform.of({
fileResponse: (_path, fileOptions) => {
if (options.fileResponseError !== undefined) {
return Effect.fail(options.fileResponseError)
}
return Effect.succeed(HttpServerResponse.text(fileBody, {
status: fileOptions?.status,
headers: {
ETag: "\"etag-value\"",
"Last-Modified": lastModified
}
}))
},
fileWebResponse: () => Effect.die("not implemented")
})
return Effect.runPromise(
HttpStaticServer.make({ root }).pipe(
Effect.provide(Path.layer),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(HttpPlatform.HttpPlatform, httpPlatform)
)
)
}
const makeLayerHandler = (options: {
readonly statError?: PlatformError.PlatformError
readonly fileResponseError?: PlatformError.PlatformError
}) => {
const fileSystem = FileSystem.makeNoop({
stat: (path) => {
if (options.statError !== undefined) {
return Effect.fail(options.statError)
}
return path === filePath ? Effect.succeed(fileInfo) : Effect.fail(notFoundError(path))
}
})
const httpPlatform = HttpPlatform.HttpPlatform.of({
fileResponse: () =>
options.fileResponseError !== undefined
? Effect.fail(options.fileResponseError)
: Effect.succeed(HttpServerResponse.text(fileBody)),
fileWebResponse: () => Effect.die("not implemented")
})
const dependencies = Layer.mergeAll(
Path.layer,
Layer.succeed(FileSystem.FileSystem, fileSystem),
Layer.succeed(HttpPlatform.HttpPlatform, httpPlatform)
)
return HttpRouter.toWebHandler(
HttpStaticServer.layer({ root }).pipe(Layer.provideMerge(dependencies)),
{ disableLogger: true }
)
}
describe("HttpStaticServer", () => {
it("304 with If-None-Match exact, weak, and wildcard", async () => {
const handler = await makeHandler()
const warmupResponse = await handler(new Request("http://localhost/file.txt"))
const etag = warmupResponse.headers.get("etag")
assert.strictEqual(etag, "\"etag-value\"")
if (etag === null) {
throw new Error("expected ETag header")
}
const exact = await handler(new Request("http://localhost/file.txt", { headers: { "If-None-Match": etag } }))
const weak = await handler(new Request("http://localhost/file.txt", { headers: { "If-None-Match": `W/${etag}` } }))
const list = await handler(
new Request("http://localhost/file.txt", { headers: { "If-None-Match": `"other", W/${etag}` } })
)
const any = await handler(new Request("http://localhost/file.txt", { headers: { "If-None-Match": "*" } }))
assert.deepStrictEqual([exact.status, weak.status, list.status, any.status], [304, 304, 304, 304])
assert.strictEqual(exact.headers.get("etag"), "\"etag-value\"")
assert.strictEqual(exact.headers.get("cache-control"), "public, max-age=60")
assert.strictEqual(exact.headers.get("last-modified"), lastModified)
assert.strictEqual(exact.headers.get("content-type"), null)
assert.strictEqual(exact.headers.get("content-length"), null)
})
it("304 with If-Modified-Since and no If-None-Match", async () => {
const handler = await makeHandler()
const notModified = await handler(
new Request("http://localhost/file.txt", {
headers: {
"If-Modified-Since": lastModified
}
})
)
const modified = await handler(
new Request("http://localhost/file.txt", {
headers: {
"If-Modified-Since": "Tue, 31 Dec 2024 23:59:59 GMT"
}
})
)
const invalidDate = await handler(
new Request("http://localhost/file.txt", {
headers: {
"If-Modified-Since": "not-a-date"
}
})
)
assert.strictEqual(notModified.status, 304)
assert.strictEqual(modified.status, 200)
assert.strictEqual(invalidDate.status, 200)
})
it("If-None-Match takes precedence over If-Modified-Since", async () => {
const handler = await makeHandler()
const response = await handler(
new Request("http://localhost/file.txt", {
headers: {
"If-None-Match": "\"different\"",
"If-Modified-Since": "Thu, 02 Jan 2025 00:00:00 GMT"
}
})
)
assert.strictEqual(response.status, 200)
})
it("matched If-None-Match takes precedence over Range", async () => {
const handler = await makeHandler()
const response = await handler(
new Request("http://localhost/file.txt", {
headers: {
"If-None-Match": "\"etag-value\"",
Range: "bytes=1000-1001"
}
})
)
assert.strictEqual(response.status, 304)
assert.strictEqual(response.headers.get("etag"), "\"etag-value\"")
assert.strictEqual(response.headers.get("cache-control"), "public, max-age=60")
assert.strictEqual(response.headers.get("last-modified"), lastModified)
assert.strictEqual(response.headers.get("content-range"), null)
assert.strictEqual(response.headers.get("accept-ranges"), null)
assert.strictEqual(response.headers.get("content-type"), null)
assert.strictEqual(response.headers.get("content-length"), null)
})
it("wraps missing routes as HttpServerError RouteNotFound", async () => {
const app = await makeFailingApp({})
const error = await Effect.runPromise(
app.pipe(
Effect.provideService(
HttpServerRequest.HttpServerRequest,
HttpServerRequest.fromWeb(new Request("http://localhost/missing.txt"))
),
Effect.flip
)
)
assert.instanceOf(error, HttpServerError.HttpServerError)
assert.strictEqual(error.reason._tag, "RouteNotFound")
})
it("maps non-NotFound platform failures to HttpServerError InternalError", async () => {
const statError = permissionDeniedError(filePath)
const app = await makeFailingApp({ statError })
const error = await Effect.runPromise(
app.pipe(
Effect.provideService(
HttpServerRequest.HttpServerRequest,
HttpServerRequest.fromWeb(new Request("http://localhost/file.txt"))
),
Effect.flip
)
)
assert.instanceOf(error, HttpServerError.HttpServerError)
assert.strictEqual(error.reason._tag, "InternalError")
assert.strictEqual(error.reason.cause, statError)
})
it("layer renders InternalError responses as 500", async () => {
const { handler, dispose } = makeLayerHandler({
fileResponseError: permissionDeniedError(filePath)
})
try {
const response = await handler(new Request("http://localhost/file.txt"))
assert.strictEqual(response.status, 500)
} finally {
await dispose()
}
})
})

View File

@@ -0,0 +1,59 @@
import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
import { assert, describe, it } from "@effect/vitest"
import * as Crypto from "effect/Crypto"
import * as Effect from "effect/Effect"
import * as TestClock from "effect/testing/TestClock"
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
const uuidV7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
const hex = (bytes: Uint8Array): string => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")
describe("NodeCrypto", () => {
it.effect("generates empty random bytes", () =>
Effect.gen(function*() {
const crypto = yield* Crypto.Crypto
const bytes = yield* crypto.randomBytes(0)
assert.deepStrictEqual(bytes, new Uint8Array(0))
}).pipe(Effect.provide(NodeCrypto.layer)))
it.effect("generates random bytes with the requested size", () =>
Effect.gen(function*() {
const crypto = yield* Crypto.Crypto
const bytes = yield* crypto.randomBytes(32)
assert.strictEqual(bytes.length, 32)
}).pipe(Effect.provide(NodeCrypto.layer)))
it.effect("fails invalid random byte sizes", () =>
Effect.gen(function*() {
const crypto = yield* Crypto.Crypto
const error = yield* Effect.flip(crypto.randomBytes(-1))
assert.strictEqual(error._tag, "PlatformError")
}).pipe(Effect.provide(NodeCrypto.layer)))
it.effect("generates UUIDv4 values", () =>
Effect.gen(function*() {
const crypto = yield* Crypto.Crypto
const uuid1 = yield* crypto.randomUUIDv4
const uuid2 = yield* crypto.randomUUIDv4
assert.match(uuid1, uuidV4Regex)
assert.match(uuid2, uuidV4Regex)
assert.notStrictEqual(uuid1, uuid2)
}).pipe(Effect.provide(NodeCrypto.layer)))
it.effect("generates UUIDv7 values", () =>
Effect.gen(function*() {
yield* TestClock.setTime(0x0123456789ab)
const crypto = yield* Crypto.Crypto
const uuid = yield* crypto.randomUUIDv7
assert.match(uuid, uuidV7Regex)
assert.strictEqual(uuid.slice(0, 13), "01234567-89ab")
}).pipe(Effect.provide(NodeCrypto.layer)))
it.effect("computes SHA-256 digests", () =>
Effect.gen(function*() {
const crypto = yield* Crypto.Crypto
const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode("hello"))
assert.strictEqual(hex(digest), "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
}).pipe(Effect.provide(NodeCrypto.layer)))
})

View File

@@ -0,0 +1,157 @@
import * as NodeClient from "@effect/platform-node/NodeHttpClient"
import { describe, expect, it } from "@effect/vitest"
import { Struct } from "effect"
import * as Context from "effect/Context"
import * as Effect from "effect/Effect"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
const Todo = Schema.Struct({
userId: Schema.Number,
id: Schema.Number,
title: Schema.String,
completed: Schema.Boolean
})
const TodoWithoutId = Schema.Struct({
...Struct.omit(Todo.fields, ["id"])
})
const makeJsonPlaceholder = Effect.gen(function*() {
const defaultClient = yield* HttpClient.HttpClient
const client = defaultClient.pipe(
HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com"))
)
const createTodo = (todo: typeof TodoWithoutId.Type) =>
HttpClientRequest.post("/todos").pipe(
HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo),
Effect.flatMap(client.execute),
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
)
return {
client,
createTodo
} as const
})
interface JsonPlaceholder extends Effect.Success<typeof makeJsonPlaceholder> {}
const JsonPlaceholder = Context.Service<JsonPlaceholder>("test/JsonPlaceholder")
const JsonPlaceholderLive = Layer.effect(JsonPlaceholder)(makeJsonPlaceholder)
;[
{
name: "fetch",
layer: NodeClient.layerFetch
},
{
name: "node:http",
layer: NodeClient.layerNodeHttp
},
{
name: "undici",
layer: NodeClient.layerUndici
}
].forEach(({ layer, name }) => {
describe(`NodeHttpClient - ${name}`, () => {
it.effect("google", () =>
Effect.gen(function*() {
const response = yield* HttpClient.get("https://www.google.com/").pipe(
Effect.flatMap((_) => _.text)
)
expect(response).toContain("Google")
}).pipe(Effect.provide(layer), flaky))
it.effect("google followRedirects", () =>
flaky(
Effect.gen(function*() {
const client = (yield* HttpClient.HttpClient).pipe(
HttpClient.followRedirects()
)
const response = yield* client.get("http://google.com/").pipe(
Effect.flatMap((_) => _.text)
)
expect(response).toContain("Google")
}).pipe(Effect.provide(layer))
))
it.effect("google stream", () =>
flaky(
Effect.gen(function*() {
const client = yield* HttpClient.HttpClient
const response = yield* client.get("https://www.google.com/").pipe(
Effect.map((_) => _.stream),
Stream.unwrap,
Stream.decodeText(),
Stream.mkString
)
expect(response).toContain("Google")
}).pipe(Effect.provide(layer))
))
it.effect("jsonplaceholder", () =>
Effect.gen(function*() {
const jp = yield* JsonPlaceholder
const response = yield* jp.client.get("/todos/1").pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
)
expect(response.id).toBe(1)
}).pipe(
Effect.provide(JsonPlaceholderLive.pipe(
Layer.provide(layer)
)),
flaky
))
it.effect("jsonplaceholder schemaBodyJson", () =>
Effect.gen(function*() {
const jp = yield* JsonPlaceholder
const response = yield* jp.createTodo({
userId: 1,
title: "test",
completed: false
})
expect(response.title).toBe("test")
}).pipe(
Effect.provide(JsonPlaceholderLive.pipe(
Layer.provide(layer)
)),
flaky
))
it.effect("head request with schemaJson", () =>
Effect.gen(function*() {
const client = yield* HttpClient.HttpClient
const response = yield* client.head("https://jsonplaceholder.typicode.com/todos").pipe(
Effect.flatMap(
HttpClientResponse.schemaJson(Schema.Struct({ status: Schema.Literal(200) }))
)
)
expect(response).toEqual({ status: 200 })
}).pipe(Effect.provide(layer), flaky))
it.live("interrupt", () =>
Effect.gen(function*() {
const client = yield* HttpClient.HttpClient
const response = yield* client.get("https://www.google.com/").pipe(
Effect.flatMap((_) => _.text),
Effect.timeout(1),
Effect.asSome,
Effect.catchTag("TimeoutError", () => Effect.succeedNone)
)
expect(response._tag).toEqual("None")
}).pipe(Effect.provide(layer), flaky))
it.effect("close early", () =>
Effect.gen(function*() {
const response = yield* HttpClient.get("https://www.google.com/")
expect(response.status).toBe(200)
}).pipe(Effect.provide(layer), flaky))
})
})
const flaky = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
effect.pipe(
Effect.timeoutOrElse({
duration: "10 seconds",
orElse: () => Effect.void
})
)

View File

@@ -0,0 +1,51 @@
import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"
import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import type * as HttpBody from "effect/unstable/http/HttpBody"
import * as HttpPlatform from "effect/unstable/http/HttpPlatform"
import { Readable } from "node:stream"
const readStream = (stream: Readable) =>
Effect.promise(async () => {
let text = ""
for await (const chunk of stream) {
text += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")
}
return text
})
describe("NodeHttpPlatform", () => {
it.effect("fileResponse reads exact bytesToRead", () =>
Effect.gen(function*() {
const platform = yield* HttpPlatform.HttpPlatform
const response = yield* platform.fileResponse(`${__dirname}/fixtures/text.txt`, {
offset: 6,
bytesToRead: 5
})
assert.strictEqual(response.headers["content-length"], "5")
assert.strictEqual(response.body._tag, "Raw")
const body = (response.body as HttpBody.Raw).body
assert(body instanceof Readable)
const text = yield* readStream(body)
assert.strictEqual(text, "ipsum")
}).pipe(Effect.provide(NodeHttpPlatform.layer)))
it.effect("fileResponse supports zero bytesToRead", () =>
Effect.gen(function*() {
const platform = yield* HttpPlatform.HttpPlatform
const response = yield* platform.fileResponse(`${__dirname}/fixtures/text.txt`, {
offset: 6,
bytesToRead: 0
})
assert.strictEqual(response.headers["content-length"], "0")
assert.strictEqual(response.body._tag, "Raw")
const body = (response.body as HttpBody.Raw).body
assert(body instanceof Readable)
const text = yield* readStream(body)
assert.strictEqual(text, "")
}).pipe(Effect.provide(NodeHttpPlatform.layer)))
})

View File

@@ -0,0 +1,598 @@
/** @effect-diagnostics preferSchemaOverJson:skip-file */
import { NodeHttpServer } from "@effect/platform-node"
import { assert, describe, expect, it } from "@effect/vitest"
import { Effect } from "effect"
import * as Duration from "effect/Duration"
import * as Fiber from "effect/Fiber"
import * as Layer from "effect/Layer"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import * as Tracer from "effect/Tracer"
import {
Cookies,
HttpBody,
HttpClient,
HttpClientRequest,
HttpClientResponse,
HttpPlatform,
HttpRouter,
HttpServer,
HttpServerRequest,
HttpServerRespondable,
HttpServerResponse,
Multipart,
UrlParams
} from "effect/unstable/http"
import * as HttpApiError from "effect/unstable/httpapi/HttpApiError"
import * as Buffer from "node:buffer"
const Todo = Schema.Struct({
id: Schema.Number,
title: Schema.String
})
const IdParams = Schema.Struct({
id: Schema.FiniteFromString
})
const todoResponse = HttpServerResponse.schemaJson(Todo)
describe("HttpServer", () => {
it.effect("schema", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"GET",
"/todos/:id",
Effect.flatMap(
HttpRouter.schemaParams(IdParams),
({ id }) => todoResponse({ id, title: "test" })
)
).pipe(
HttpRouter.serve,
Layer.build
)
const todo = yield* HttpClient.get("/todos/1").pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
)
expect(todo).toEqual({ id: 1, title: "test" })
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("formData", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/upload",
Effect.gen(function*() {
const request = yield* HttpServerRequest.HttpServerRequest
const formData = yield* request.multipart
const part = formData.file
assert(typeof part !== "string")
const file = part[0]
assert(typeof file !== "string")
expect(file.path.endsWith("/test.txt")).toEqual(true)
expect(file.contentType).toEqual("text/plain")
return yield* HttpServerResponse.json({ ok: "file" in formData })
})
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const formData = new FormData()
formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt")
const result = yield* client.post("/upload", { body: HttpBody.formData(formData) }).pipe(
Effect.flatMap((r) => r.json)
)
expect(result).toEqual({ ok: true })
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("schemaBodyForm", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/upload",
Effect.gen(function*() {
const files = yield* HttpServerRequest.schemaBodyForm(Schema.Struct({
file: Multipart.FilesSchema,
test: Schema.String
}))
expect(files).toHaveProperty("file")
expect(files).toHaveProperty("test")
return HttpServerResponse.empty()
})
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const formData = new FormData()
formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt")
formData.append("test", "test")
const response = yield* client.post("/upload", { body: HttpBody.formData(formData) })
expect(response.status).toEqual(204)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("formData withMaxFileSize", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/upload",
Effect.gen(function*() {
const request = yield* HttpServerRequest.HttpServerRequest
yield* request.multipart
return HttpServerResponse.empty()
}).pipe(
Effect.catchTag("MultipartError", (error) =>
error.reason._tag === "FileTooLarge" ?
Effect.succeed(HttpServerResponse.empty({ status: 413 })) :
Effect.fail(error))
)
).pipe(
HttpRouter.serve,
Layer.build,
Effect.provideService(Multipart.MaxFileSize, 100)
)
const client = yield* HttpClient.HttpClient
const formData = new FormData()
const data = new Uint8Array(1000)
formData.append("file", new Blob([data], { type: "text/plain" }), "test.txt")
const response = yield* client.post("/upload", { body: HttpBody.formData(formData) })
expect(response.status).toEqual(413)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("formData withMaxFieldSize", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/upload",
Effect.gen(function*() {
const request = yield* HttpServerRequest.HttpServerRequest
yield* request.multipart
return HttpServerResponse.empty()
}).pipe(
Effect.catchTag("MultipartError", (error) =>
error.reason._tag === "FieldTooLarge" ?
Effect.succeed(HttpServerResponse.empty({ status: 413 })) :
Effect.fail(error))
)
).pipe(
HttpRouter.serve,
Layer.build,
Effect.provideService(Multipart.MaxFieldSize, 100)
)
const client = yield* HttpClient.HttpClient
const formData = new FormData()
const data = new Uint8Array(1000).fill(1)
formData.append("file", new TextDecoder().decode(data))
const response = yield* client.post("/upload", { body: HttpBody.formData(formData) })
expect(response.status).toEqual(413)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("mountApp", () =>
Effect.gen(function*() {
const child = Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))
yield* HttpRouter.use((router) => router.prefixed("/child").add("*", "*", child)).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const todo = yield* client.get("/child/1").pipe(Effect.flatMap((_) => _.text))
expect(todo).toEqual("/1")
const root = yield* client.get("/child").pipe(Effect.flatMap((_) => _.text))
expect(root).toEqual("/")
const rootSearch = yield* client.get("/child?foo=bar").pipe(Effect.flatMap((_) => _.text))
expect(rootSearch).toEqual("?foo=bar")
const rootSlash = yield* client.get("/child/").pipe(Effect.flatMap((_) => _.text))
expect(rootSlash).toEqual("/")
const invalid = yield* client.get("/child1/", {
urlParams: { foo: "bar" }
}).pipe(Effect.map((_) => _.status))
expect(invalid).toEqual(404)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("file", () =>
Effect.gen(function*() {
yield* (yield* HttpServerResponse.file(`${__dirname}/fixtures/text.txt`).pipe(
Effect.updateService(
HttpPlatform.HttpPlatform,
(_) => ({
..._,
fileResponse: (path, options) =>
Effect.map(
_.fileResponse(path, options),
(res) => {
;(res as any).headers.etag = "\"etag\""
return res
}
)
})
)
)).pipe(
Effect.succeed,
HttpServer.serveEffect()
)
const client = yield* HttpClient.HttpClient
const res = yield* client.get("/")
expect(res.status).toEqual(200)
expect(res.headers["content-type"]).toEqual("text/plain")
expect(res.headers["content-length"]).toEqual("27")
expect(res.headers.etag).toEqual("\"etag\"")
const text = yield* res.text
expect(text.trim()).toEqual("lorem ipsum dolar sit amet")
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("fileWeb", () =>
Effect.gen(function*() {
const now = new Date()
const file = new Buffer.File([new TextEncoder().encode("test")], "test.txt", {
type: "text/plain",
lastModified: now.getTime()
})
yield* HttpServerResponse.fileWeb(file).pipe(
Effect.updateService(
HttpPlatform.HttpPlatform,
(_) => ({
..._,
fileWebResponse: (path, options) =>
Effect.map(
_.fileWebResponse(path, options),
(res) => ({ ...res, headers: { ...res.headers, etag: "W/\"etag\"" } })
)
})
),
HttpServer.serveEffect()
)
const client = yield* HttpClient.HttpClient
const res = yield* client.get("/")
expect(res.status).toEqual(200)
expect(res.headers["content-type"]).toEqual("text/plain")
expect(res.headers["content-length"]).toEqual("4")
expect(res.headers["last-modified"]).toEqual(now.toUTCString())
expect(res.headers.etag).toEqual("W/\"etag\"")
const text = yield* res.text
expect(text.trim()).toEqual("test")
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("schemaBodyUrlParams", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/todos",
Effect.flatMap(
HttpServerRequest.schemaBodyUrlParams(Schema.Struct({
id: Schema.FiniteFromString,
title: Schema.String
})),
({ id, title }) => todoResponse({ id, title })
)
).pipe(
HttpRouter.serve,
Layer.build
)
const todo = yield* HttpClientRequest.post("/todos").pipe(
HttpClientRequest.bodyUrlParams({ id: "1", title: "test" }),
HttpClient.execute,
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
)
expect(todo).toEqual({ id: 1, title: "test" })
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("schemaBodyUrlParams error", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"GET",
"/todos",
Effect.flatMap(
HttpServerRequest.schemaBodyUrlParams(Schema.Struct({
id: Schema.FiniteFromString,
title: Schema.String
})),
({ id, title }) => todoResponse({ id, title })
).pipe(
Effect.catchTag("SchemaError", (error) =>
Effect.succeed(HttpServerResponse.jsonUnsafe({ error }, { status: 400 })))
)
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const response = yield* client.get("/todos")
expect(response.status).toEqual(400)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("schemaBodyFormJson", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/upload",
Effect.gen(function*() {
const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({
test: Schema.String
}))("json")
expect(result.test).toEqual("content")
return HttpServerResponse.empty()
})
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const formData = new FormData()
formData.append("json", JSON.stringify({ test: "content" }))
const response = yield* client.post("/upload", { body: HttpBody.formData(formData) })
expect(response.status).toEqual(204)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("schemaBodyFormJson file", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/upload",
Effect.gen(function*() {
const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({
test: Schema.String
}))("json")
expect(result.test).toEqual("content")
return HttpServerResponse.empty()
})
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const formData = new FormData()
formData.append(
"json",
new Blob([JSON.stringify({ test: "content" })], { type: "application/json" }),
"test.json"
)
const response = yield* client.post("/upload", { body: HttpBody.formData(formData) })
expect(response.status).toEqual(204)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("schemaBodyFormJson url encoded", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"POST",
"/upload",
Effect.gen(function*() {
const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({
test: Schema.String
}))("json")
expect(result.test).toEqual("content")
return HttpServerResponse.empty()
})
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const response = yield* client.post("/upload", {
body: HttpBody.urlParams(UrlParams.fromInput({
json: JSON.stringify({ test: "content" })
}))
})
expect(response.status).toEqual(204)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("tracing", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"GET",
"/",
Effect.flatMap(
Effect.currentSpan,
(_) => HttpServerResponse.json({ spanId: _.spanId, parent: _.parent })
)
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const requestSpan = yield* Effect.makeSpan("client request")
const body = yield* client.get("/").pipe(
Effect.flatMap((r) => r.json),
Effect.provideService(
Tracer.Tracer,
Tracer.make({
span(options) {
assert.strictEqual(options.name, "http.client GET")
assert.strictEqual(options.kind, "client")
assert(options.parent._tag === "Some")
if (options.parent.value._tag !== "Span") {
throw new Error("Expected span parent")
}
assert.strictEqual(options.parent.value.name, "request parent")
return requestSpan
}
})
),
Effect.withSpan("request parent"),
Effect.repeat({ times: 2 })
)
expect((body as any).parent._tag).toEqual("Some")
expect((body as any).parent.value.spanId).toEqual(requestSpan.spanId)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("html", () =>
Effect.gen(function*() {
yield* HttpRouter.addAll([
HttpRouter.route("GET", "/home", HttpServerResponse.html("<html />")),
HttpRouter.route(
"GET",
"/about",
HttpServerResponse.html`<html>${Effect.succeed("<body />")}</html>`
),
HttpRouter.route(
"GET",
"/stream",
HttpServerResponse.htmlStream`<html>${Stream.make("<body />", 123, "hello")}</html>`
)
]).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const home = yield* client.get("/home").pipe(Effect.flatMap((r) => r.text))
expect(home).toEqual("<html />")
const about = yield* client.get("/about").pipe(Effect.flatMap((r) => r.text))
expect(about).toEqual("<html><body /></html>")
const stream = yield* client.get("/stream").pipe(Effect.flatMap((r) => r.text))
expect(stream).toEqual("<html><body />123hello</html>")
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("setCookie", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"GET",
"/home",
HttpServerResponse.empty().pipe(
HttpServerResponse.setCookieUnsafe("test", "value"),
HttpServerResponse.setCookieUnsafe("test2", "value2", {
httpOnly: true,
secure: true,
sameSite: "lax",
partitioned: true,
path: "/",
domain: "example.com",
expires: new Date(2022, 1, 1, 0, 0, 0, 0),
maxAge: "5 minutes"
})
)
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const res = yield* client.get("/home")
assert.deepStrictEqual(
res.cookies.toJSON(),
Cookies.fromReadonlyRecord({
test: Cookies.makeCookieUnsafe("test", "value"),
test2: Cookies.makeCookieUnsafe("test2", "value2", {
httpOnly: true,
secure: true,
sameSite: "lax",
partitioned: true,
path: "/",
domain: "example.com",
expires: new Date(2022, 1, 1, 0, 0, 0, 0),
maxAge: Duration.minutes(5)
})
}).toJSON()
)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.live("uninterruptible routes", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"GET",
"/home",
Effect.gen(function*() {
const fiber = Fiber.getCurrent()!
setTimeout(() => fiber.interruptUnsafe(fiber.id), 10)
yield* Effect.sleep(50)
return HttpServerResponse.empty()
}),
{ uninterruptible: true }
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const res = yield* client.get("/home")
assert.strictEqual(res.status, 204)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
describe("HttpServerRespondable", () => {
it.effect("error/schema", () =>
Effect.gen(function*() {
class CustomError extends Schema.ErrorClass<CustomError>("CustomError")({
_tag: Schema.tag("CustomError"),
name: Schema.String
}) {
[HttpServerRespondable.symbol]() {
return HttpServerResponse.schemaJson(CustomError)(this, { status: 599 })
}
}
yield* HttpRouter.add(
"GET",
"/home",
new CustomError({ name: "test" })
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const res = yield* client.get("/home")
assert.strictEqual(res.status, 599)
const err = yield* HttpClientResponse.schemaBodyJson(CustomError)(res)
assert.deepStrictEqual(err, new CustomError({ name: "test" }))
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
it.effect("httpapi error", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"GET",
"/home",
new HttpApiError.BadRequest({})
).pipe(
HttpRouter.serve,
Layer.build
)
const client = yield* HttpClient.HttpClient
const res = yield* client.get("/home")
assert.strictEqual(res.status, 400)
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
})
it.effect("RouterConfig", () =>
Effect.gen(function*() {
yield* HttpRouter.add(
"GET",
"/:param",
Effect.succeed(HttpServerResponse.empty())
).pipe(
HttpRouter.serve,
Layer.build
)
let res = yield* HttpClient.get("/123456")
assert.strictEqual(res.status, 404)
res = yield* HttpClient.get("/12345")
assert.strictEqual(res.status, 204)
}).pipe(
Effect.provide([
NodeHttpServer.layerTest,
Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 5 })
])
))
it.effect("HttpRouter prefixed", () =>
Effect.gen(function*() {
const handler = HttpRouter.serve(HttpRouter.use(Effect.fnUntraced(function*(router_) {
const router = router_.prefixed("/todos")
yield* router.add(
"GET",
"/:id",
Effect.flatMap(
HttpRouter.schemaParams(IdParams),
({ id }) => todoResponse({ id, title: "test" })
)
)
yield* router.addAll([
HttpRouter.route("GET", "/", Effect.succeed(HttpServerResponse.text("root")))
])
})))
yield* Layer.build(handler)
const todo = yield* HttpClient.get("/todos/1").pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))
)
expect(todo).toEqual({ id: 1, title: "test" })
const root = yield* HttpClient.get("/todos").pipe(
Effect.flatMap((r) => r.text)
)
expect(root).toEqual("root")
}).pipe(Effect.provide(NodeHttpServer.layerTest)))
})

View File

@@ -0,0 +1,84 @@
import { NodeRedis } from "@effect/platform-node"
import { assert, it } from "@effect/vitest"
import { RedisContainer } from "@testcontainers/redis"
import { Effect, Layer, Schema } from "effect"
import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest"
import * as PersistedQueueTest from "effect-test/unstable/persistence/PersistedQueueTest"
import { PersistedQueue, Persistence } from "effect/unstable/persistence"
const RedisLayer = Layer.unwrap(
Effect.gen(function*() {
const container = yield* Effect.acquireRelease(
Effect.promise(() => new RedisContainer("redis:alpine").start()),
(container) => Effect.promise(() => container.stop())
)
return NodeRedis.layer({
host: container.getHost(),
port: container.getMappedPort(6379)
})
}).pipe(
Effect.catchCause(() => Effect.fail(new PersistedCacheTest.TransientError()))
)
)
PersistedCacheTest.suite(
"NodeRedis",
Persistence.layerRedis.pipe(Layer.provide(RedisLayer))
)
PersistedQueueTest.suite(
"NodeRedis",
// short intervals so the periodic reset runs while the suite's takes are
// in flight
PersistedQueue.layerStoreRedis({
pollInterval: "50 millis",
lockRefreshInterval: "100 millis"
}).pipe(Layer.provide(RedisLayer))
)
const PersistedQueueRedisLayer = Layer.mergeAll(
RedisLayer,
PersistedQueue.layer.pipe(
Layer.provideMerge(
PersistedQueue.layerStoreRedis().pipe(Layer.provide(RedisLayer))
)
)
)
it.layer(PersistedQueueRedisLayer, { timeout: "30 seconds" })(
"PersistedQueue (NodeRedis)",
(it) => {
// The shared PersistedQueue suite can only assert that exhausted elements
// are no longer delivered, which is also true if they are silently
// dropped. There is no public API for reading failed elements, so
// verifying they are preserved in the dead-letter list requires
// inspecting Redis directly.
it.effect("moves exhausted elements to the failed list", () =>
Effect.gen(function*() {
const redis = yield* NodeRedis.NodeRedis
const queueName = "test-redis-failed"
const queue = yield* PersistedQueue.make({
name: queueName,
schema: RedisItem
})
const id = yield* queue.offer({ n: 42 })
const error = yield* queue.take(() => Effect.fail("boom"), { maxAttempts: 1 }).pipe(Effect.flip)
assert.strictEqual(error, "boom")
const failed = yield* redis.use((client) => client.lrange(`effectq:${queueName}:failed`, 0, -1))
assert.strictEqual(failed.length, 1)
const failedItem = JSON.parse(failed[0])
assert.strictEqual(failedItem.id, id)
assert.deepStrictEqual(failedItem.element, { n: 42 })
assert.strictEqual(failedItem.attempts, 1)
const pending = yield* redis.use((client) => client.hlen(`effectq:${queueName}:pending`))
assert.strictEqual(pending, 0)
}))
}
)
const RedisItem = Schema.Struct({
n: Schema.Number
})

View File

@@ -0,0 +1,153 @@
import { NodeSocket, NodeSocketServer } from "@effect/platform-node"
import { assert, describe, expect, it } from "@effect/vitest"
import { Effect, Queue } from "effect"
import * as Fiber from "effect/Fiber"
import * as Stream from "effect/Stream"
import { Socket, type SocketServer } from "effect/unstable/socket"
import { WS } from "vitest-websocket-mock"
const makeServer = Effect.gen(function*() {
const server = yield* NodeSocketServer.make({ port: 0 })
yield* server.run(Effect.fnUntraced(function*(socket) {
const write = yield* socket.writer
yield* socket.run(write)
}, Effect.scoped)).pipe(Effect.forkScoped)
return server
})
describe("Socket", () => {
it.effect("open", () =>
Effect.gen(function*() {
const server = yield* makeServer
const channel = NodeSocket.makeNetChannel({ port: (server.address as SocketServer.TcpAddress).port })
const outputEffect = Stream.make("Hello", "World").pipe(
Stream.encodeText,
Stream.pipeThroughChannel(channel),
Stream.decodeText(),
Stream.mkString
)
const output = yield* outputEffect
assert.strictEqual(output, "HelloWorld")
}))
describe("WebSocket", () => {
const url = `ws://localhost:1234`
const makeServer = Effect.acquireRelease(
Effect.sync(() => new WS(url)),
(ws) =>
Effect.sync(() => {
ws.close()
WS.clean()
})
)
it.effect("messages", () =>
Effect.gen(function*() {
const server = yield* makeServer
const socket = yield* Socket.makeWebSocket(Effect.succeed(url), {
closeCodeIsError: () => false
})
const messages = yield* Queue.unbounded<Uint8Array>()
const fiber = yield* Effect.forkChild(socket.run((_) => Queue.offer(messages, _)))
yield* Effect.gen(function*() {
const write = yield* socket.writer
yield* write(new TextEncoder().encode("Hello"))
yield* write(new TextEncoder().encode("World"))
}).pipe(Effect.scoped)
yield* Effect.promise(async () => {
await expect(server).toReceiveMessage(new TextEncoder().encode("Hello"))
await expect(server).toReceiveMessage(new TextEncoder().encode("World"))
})
server.send("Right back at you!")
let message = yield* Queue.take(messages)
assert.deepStrictEqual(message, new TextEncoder().encode("Right back at you!"))
server.send(new Blob(["A Blob message"]))
message = yield* Queue.take(messages)
assert.deepStrictEqual(message, new TextEncoder().encode("A Blob message"))
server.close()
const exit = yield* Fiber.await(fiber)
assert.strictEqual(exit._tag, "Success")
}).pipe(
Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url))
))
it.effect("close codes are errors by default", () =>
Effect.gen(function*() {
const server = yield* makeServer
const socket = yield* Socket.makeWebSocket(Effect.succeed(url))
const fiber = yield* Effect.forkChild(socket.run(() => {}))
yield* Effect.promise(() => server.connected)
server.close({ code: 1000, reason: "done", wasClean: true })
const exit = yield* Effect.exit(Fiber.join(fiber))
assert.isTrue(exit._tag === "Failure")
if (exit._tag === "Failure") {
const failure = exit.cause.reasons[0]
if (failure._tag === "Fail") {
assert.isTrue(failure.error instanceof Socket.SocketError)
assert.strictEqual(failure.error.reason._tag, "SocketCloseError")
if (failure.error.reason._tag === "SocketCloseError") {
assert.strictEqual(failure.error.reason.code, 1000)
assert.strictEqual(failure.error.reason.closeReason, "done")
}
}
}
}).pipe(
Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url))
))
})
describe("TransformStream", () => {
it.effect("works", () =>
Effect.gen(function*() {
const readable = Stream.make("A", "B", "C").pipe(
Stream.tap(() => Effect.sleep(50)),
Stream.toReadableStream()
)
const decoder = new TextDecoder()
const chunks: Array<string> = []
const writable = new WritableStream<Uint8Array>({
write(chunk) {
chunks.push(decoder.decode(chunk))
}
})
const socket = yield* Socket.fromTransformStream(
Effect.succeed({
readable,
writable
}),
{
closeCodeIsError: () => false
}
)
yield* socket.writer.pipe(
Effect.tap((write) =>
write("Hello").pipe(
Effect.andThen(write("World"))
)
),
Effect.scoped,
Effect.forkChild
)
const received: Array<string> = []
yield* socket.run((chunk) =>
Effect.sync(() => {
received.push(decoder.decode(chunk))
})
).pipe(Effect.scoped)
assert.deepStrictEqual(chunks, ["Hello", "World"])
assert.deepStrictEqual(received, ["A", "B", "C"])
}))
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
import { NodeHttpServer, NodeSocket, NodeSocketServer } from "@effect/platform-node"
import { assert, describe, it } from "@effect/vitest"
import { Cause, Deferred, Effect, Fiber, Layer, Ref, Schedule, Schema, Stream } from "effect"
import { Entity, EntityProxy, EntityProxyServer, Sharding } from "effect/unstable/cluster"
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc"
import { SocketServer } from "effect/unstable/socket"
import { e2eSuite, UsersClient } from "./fixtures/rpc-e2e.ts"
import { RpcLive, User } from "./fixtures/rpc-schemas.ts"
describe("RpcServer", () => {
// http ndjson
const HttpProtocol = RpcServer.layerProtocolHttp({ path: "/rpc" }).pipe(
Layer.provide(HttpRouter.layer)
)
const HttpNdjsonServer = RpcLive.pipe(
Layer.provideMerge(HttpProtocol),
Layer.provide(HttpRouter.serve(HttpProtocol, { disableListenLog: true, disableLogger: true }))
)
const HttpNdjsonClient = UsersClient.layer.pipe(
Layer.provide(
RpcClient.layerProtocolHttp({
url: "",
transformClient: HttpClient.mapRequest(HttpClientRequest.appendUrl("/rpc"))
})
)
)
const CustomDefectLayer = HttpNdjsonClient.pipe(
Layer.provideMerge(HttpNdjsonServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson])
)
e2eSuite(
"e2e http ndjson",
HttpNdjsonClient.pipe(
Layer.provideMerge(HttpNdjsonServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson])
)
)
e2eSuite(
"e2e http msgpack",
HttpNdjsonClient.pipe(
Layer.provideMerge(HttpNdjsonServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerMsgPack])
)
)
e2eSuite(
"e2e http jsonrpc",
HttpNdjsonClient.pipe(
Layer.provideMerge(HttpNdjsonServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdJsonRpc()])
)
)
// websocket
const WsProtocol = RpcServer.layerProtocolWebsocket({ path: "/rpc" }).pipe(
Layer.provide(HttpRouter.layer)
)
const HttpWsServer = RpcLive.pipe(
Layer.provideMerge(WsProtocol),
Layer.provide(HttpRouter.serve(WsProtocol, { disableListenLog: true, disableLogger: true }))
)
const HttpWsClient = UsersClient.layer.pipe(
Layer.provide(RpcClient.layerProtocolSocket()),
Layer.provide(
Effect.gen(function*() {
const server = yield* HttpServer.HttpServer
const address = server.address as HttpServer.TcpAddress
return NodeSocket.layerWebSocket(`http://127.0.0.1:${address.port}/rpc`)
}).pipe(Layer.unwrap)
)
)
e2eSuite(
"e2e ws ndjson",
HttpWsClient.pipe(
Layer.provideMerge(HttpWsServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson])
)
)
e2eSuite(
"e2e ws json",
HttpWsClient.pipe(
Layer.provideMerge(HttpWsServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerJson])
)
)
e2eSuite(
"e2e ws msgpack",
HttpWsClient.pipe(
Layer.provideMerge(HttpWsServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerMsgPack])
)
)
e2eSuite(
"e2e ws jsonrpc",
HttpWsClient.pipe(
Layer.provideMerge(HttpWsServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerJsonRpc()])
)
)
// tcp
const TcpServer = RpcLive.pipe(
Layer.provideMerge(RpcServer.layerProtocolSocketServer),
Layer.provideMerge(NodeSocketServer.layer({ port: 0 }))
)
const TcpClient = UsersClient.layer.pipe(
Layer.provide(RpcClient.layerProtocolSocket()),
Layer.provide(
Effect.gen(function*() {
const server = yield* SocketServer.SocketServer
const address = server.address as SocketServer.TcpAddress
return NodeSocket.layerNet({ port: address.port })
}).pipe(Layer.unwrap)
)
)
e2eSuite(
"e2e tcp ndjson",
TcpClient.pipe(
Layer.provideMerge(TcpServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson])
)
)
e2eSuite(
"e2e tcp msgpack",
TcpClient.pipe(
Layer.provideMerge(TcpServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerMsgPack])
)
)
e2eSuite(
"e2e tcp jsonrpc",
TcpClient.pipe(
Layer.provideMerge(TcpServer),
Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdJsonRpc()])
)
)
// worker
// const WorkerClient = UsersClient.layer.pipe(
// Layer.provide(RpcClient.layerProtocolWorker({ size: 1 })),
// Layer.provide(
// NodeWorker.layerPlatform(() =>
// CP.fork(new URL("./fixtures/rpc-worker.ts", import.meta.url), {
// execPath: "node"
// })
// )
// ),
// Layer.merge(Layer.succeed(RpcServer.Protocol, {
// supportsAck: true
// } as any))
// )
// e2eSuite("e2e worker", WorkerClient)
describe("RpcTest", () => {
it.effect("works", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const user = yield* client.GetUser({ id: "1" })
assert.deepStrictEqual(user, new User({ id: "1", name: "Logged in user" }))
}).pipe(Effect.provide(UsersClient.layerTest)))
})
describe("custom defect schema", () => {
it.effect("preserves full defect with custom schema", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const cause = yield* client.ProduceDefectCustom().pipe(
Effect.sandbox,
Effect.flip
)
const defect = Cause.squash(cause)
assert.instanceOf(defect, Error)
assert.strictEqual(defect.name, "CustomDefect")
assert.strictEqual(defect.message, "detailed error")
assert.strictEqual(defect.stack, "Error: detailed error\n at handler.ts:1")
}).pipe(Effect.provide(CustomDefectLayer)))
})
describe("entity proxy", () => {
it.effect("provides handler context for generated rpc handlers", () =>
Effect.gen(function*() {
const TestEntity = Entity.make("TestEntity", [Rpc.make("NoPayload")])
const TestEntityRpcs = EntityProxy.toRpcGroup(TestEntity)
const called = yield* Deferred.make<void>()
const testClient = (entityId: string) => ({
NoPayload: (payload: void, options?: { readonly discard?: boolean }) =>
Effect.gen(function*() {
assert.strictEqual(entityId, "id")
assert.strictEqual(payload, undefined)
assert.strictEqual(options?.discard, true)
yield* Deferred.succeed(called, undefined)
})
})
const sharding = Sharding.Sharding.of({
...({} as Sharding.Sharding["Service"]),
isShutdown: Effect.succeed(false),
makeClient: () => Effect.succeed(testClient) as never,
pollStorage: Effect.void
})
const client = yield* RpcTest.makeClient(TestEntityRpcs).pipe(
Effect.provide(EntityProxyServer.layerRpcHandlers(TestEntity)),
Effect.provideService(Sharding.Sharding, sharding)
)
yield* client["TestEntity.NoPayloadDiscard"]({
entityId: "id",
payload: undefined
})
yield* Deferred.await(called)
}))
})
describe("unknown-tag isolation", () => {
const Ticker = Rpc.make("Ticker", {
success: Schema.Number,
stream: true
})
const Ghost = Rpc.make("Ghost", {
payload: { value: Schema.String },
success: Schema.String
})
const serverGroup = RpcGroup.make(Ticker)
const clientGroup = RpcGroup.make(Ticker, Ghost)
const TickerHandlers = serverGroup.toLayer({
Ticker: () => Stream.fromSchedule(Schedule.spaced("60 millis"))
})
const IsolationServer = RpcServer.layer(serverGroup).pipe(
Layer.provide(TickerHandlers),
Layer.provideMerge(RpcServer.layerProtocolSocketServer),
Layer.provideMerge(NodeSocketServer.layer({ port: 0 })),
Layer.provide(RpcSerialization.layerNdjson)
)
const IsolationClient = RpcClient.layerProtocolSocket().pipe(
Layer.provide(
Effect.gen(function*() {
const server = yield* SocketServer.SocketServer
const address = server.address as SocketServer.TcpAddress
return NodeSocket.layerNet({ port: address.port })
}).pipe(Layer.unwrap)
),
Layer.provide(RpcSerialization.layerNdjson)
)
it.live(
"an unknown request tag fails only its own request, not other in-flight streams on the same connection",
() =>
Effect.gen(function*() {
const client = yield* RpcClient.make(clientGroup)
const received = yield* Ref.make<Array<number>>([])
const tickerFiber = yield* client.Ticker().pipe(
Stream.runForEach((value) => Ref.update(received, (xs) => [...xs, value])),
Effect.forkChild
)
yield* Effect.retry(
Effect.flatMap(
Ref.get(received),
(xs) => xs.length >= 2 ? Effect.void : Effect.fail("not enough ticks yet")
),
{ schedule: Schedule.spaced("50 millis"), times: 200 }
)
const ticksBeforeGhost = (yield* Ref.get(received)).length
assert.isAtLeast(ticksBeforeGhost, 2)
const ghostExit = yield* client.Ghost({ value: "boo" }).pipe(Effect.exit)
assert.isTrue(ghostExit._tag === "Failure", "Ghost call should fail with the routing miss")
yield* Effect.sleep("300 millis")
const ticksAfterGhost = (yield* Ref.get(received)).length
const tickerStatus = tickerFiber.pollUnsafe()
yield* Fiber.interrupt(tickerFiber)
assert.isUndefined(tickerStatus, "Ticker stream must still be running after the unknown-tag failure")
assert.isAbove(
ticksAfterGhost,
ticksBeforeGhost,
"Ticker stream must keep emitting after the unknown-tag failure"
)
}).pipe(Effect.provide(IsolationClient.pipe(Layer.provideMerge(IsolationServer)))),
{ timeout: 30_000 }
)
})
})

View File

@@ -0,0 +1,715 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = `
{
"components": {
"schemas": {
"ComponentsSchema": {
"additionalProperties": false,
"properties": {
"contentType": {
"type": "string",
},
"length": {
"type": "integer",
},
},
"required": [
"contentType",
"length",
],
"type": "object",
},
"Group": {
"additionalProperties": false,
"properties": {
"id": {
"type": "integer",
},
"name": {
"type": "string",
},
},
"required": [
"id",
"name",
],
"type": "object",
},
"NoStatusError": {
"additionalProperties": false,
"properties": {
"_tag": {
"enum": [
"NoStatusError",
],
"type": "string",
},
},
"required": [
"_tag",
],
"type": "object",
},
"User": {
"additionalProperties": false,
"properties": {
"createdAt": {
"type": "string",
},
"id": {
"type": "integer",
},
"name": {
"type": "string",
},
"uuid": {
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
},
},
"required": [
"id",
"name",
"createdAt",
],
"type": "object",
},
"UserError": {
"additionalProperties": false,
"properties": {
"_tag": {
"enum": [
"UserError",
],
"type": "string",
},
},
"required": [
"_tag",
],
"type": "object",
},
},
"securitySchemes": {
"cookie": {
"in": "cookie",
"name": "token",
"type": "apiKey",
},
},
},
"info": {
"summary": "test api summary",
"title": "API",
"version": "0.0.1",
},
"openapi": "3.1.0",
"paths": {
"/groups": {
"post": {
"operationId": "groups.create",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
},
},
"application/x-www-form-urlencoded": {
"schema": {
"additionalProperties": false,
"properties": {
"foo": {
"type": "string",
},
},
"required": [
"foo",
],
"type": "object",
},
},
"multipart/form-data": {
"schema": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
},
},
},
"required": true,
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Group",
},
},
},
"description": "Group",
},
},
"security": [],
"tags": [
"groups",
],
},
},
"/groups/handle/{id}": {
"post": {
"operationId": "groups.handle",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string",
},
},
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
},
},
},
"required": true,
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "number",
},
"name": {
"type": "string",
},
},
"required": [
"id",
"name",
],
"type": "object",
},
},
},
"description": "Success",
},
},
"security": [],
"tags": [
"groups",
],
},
},
"/groups/handleraw/{id}": {
"post": {
"operationId": "groups.handleRaw",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string",
},
},
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
},
},
},
"required": true,
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"id": {
"type": "number",
},
"name": {
"type": "string",
},
},
"required": [
"id",
"name",
],
"type": "object",
},
},
},
"description": "Success",
},
},
"security": [],
"tags": [
"groups",
],
},
},
"/groups/{id}": {
"get": {
"operationId": "groups.findById",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string",
},
},
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Group",
},
},
},
"description": "Group",
},
"418": {
"description": "GroupError",
},
},
"security": [],
"tags": [
"groups",
],
},
},
"/healthz": {
"get": {
"operationId": "healthz",
"parameters": [],
"responses": {
"204": {
"description": "Empty",
},
},
"security": [],
"tags": [
"root",
],
},
},
"/users": {
"get": {
"deprecated": true,
"operationId": "listUsers",
"parameters": [
{
"in": "header",
"name": "page",
"required": false,
"schema": {
"allOf": [
{
"pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$",
},
],
"type": "string",
},
},
{
"in": "query",
"name": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
"description": "search query",
},
},
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/User",
},
"type": "array",
},
},
},
"description": "Success",
},
"500": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NoStatusError",
},
},
},
"description": "NoStatusError",
},
},
"security": [
{
"cookie": [],
},
],
"summary": "test summary",
"tags": [
"Users API",
],
},
"post": {
"operationId": "users.create",
"parameters": [
{
"in": "query",
"name": "id",
"required": true,
"schema": {
"allOf": [
{
"pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$",
},
],
"type": "string",
},
},
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
},
"uuid": {
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
},
},
"required": [
"name",
],
"type": "object",
},
},
},
"required": true,
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User",
},
},
},
"description": "Some description for User",
},
"400": {
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/UserError",
},
{
"$ref": "#/components/schemas/UserError",
},
],
},
},
},
"description": "UserError",
},
},
"security": [
{
"cookie": [],
},
],
"tags": [
"Users API",
],
},
},
"/users/upload/{0}": {
"post": {
"operationId": "users.upload",
"parameters": [
{
"in": "path",
"name": "0",
"required": true,
"schema": {
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
},
},
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"additionalProperties": false,
"properties": {
"file": {
"format": "binary",
"type": "string",
},
},
"required": [
"file",
],
"type": "object",
},
},
},
"required": true,
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"contentType": {
"type": "string",
},
"length": {
"type": "integer",
},
},
"required": [
"contentType",
"length",
],
"type": "object",
},
},
},
"description": "Success",
},
},
"security": [
{
"cookie": [],
},
],
"tags": [
"Users API",
],
},
},
"/users/uploadstream": {
"post": {
"operationId": "users.uploadStream",
"parameters": [],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"additionalProperties": false,
"properties": {
"file": {
"format": "binary",
"type": "string",
},
},
"required": [
"file",
],
"type": "object",
},
},
},
"required": true,
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"contentType": {
"type": "string",
},
"length": {
"type": "integer",
},
},
"required": [
"contentType",
"length",
],
"type": "object",
},
},
},
"description": "Success",
},
},
"security": [
{
"cookie": [],
},
],
"tags": [
"Users API",
],
},
},
"/users/{id}": {
"get": {
"operationId": "users.findById",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"allOf": [
{
"pattern": "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$",
},
],
"type": "string",
},
},
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User",
},
},
},
"description": "Some description for User",
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserError",
},
},
},
"description": "UserError",
},
},
"security": [
{
"cookie": [],
},
],
"tags": [
"Users API",
],
},
},
},
"security": [],
"tags": [
{
"name": "groups",
},
{
"name": "Users API",
},
{
"name": "root",
},
{
"name": "Tag from OpenApi.Transform annotation",
},
],
}
`;

View File

@@ -0,0 +1,196 @@
import { describe, expect, it } from "@effect/vitest"
import { Context, Effect, Exit, Fiber, Latch, Layer, Option, Schema } from "effect"
import { TestClock } from "effect/testing"
import {
EntityAddress,
EntityId,
EntityType,
Envelope,
Message,
MessageStorage,
Reply,
ShardId,
ShardingConfig,
Snowflake
} from "effect/unstable/cluster"
import { Headers } from "effect/unstable/http"
import { Rpc, RpcSchema } from "effect/unstable/rpc"
const MemoryLive = MessageStorage.layerMemory.pipe(
Layer.provideMerge(Snowflake.layerGenerator),
Layer.provide(ShardingConfig.layerDefaults)
)
describe("MessageStorage", () => {
describe("memory", () => {
it.effect("saves a request", () =>
Effect.gen(function*() {
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest()
const result = yield* storage.saveRequest(request)
expect(result._tag).toEqual("Success")
const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(1)
}).pipe(Effect.provide(MemoryLive)))
it.effect("detects duplicates", () =>
Effect.gen(function*() {
const storage = yield* MessageStorage.MessageStorage
yield* storage.saveRequest(
yield* makeRequest({
rpc: PrimaryKeyTest,
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
})
)
const result = yield* storage.saveRequest(
yield* makeRequest({
rpc: PrimaryKeyTest,
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
})
)
expect(result._tag).toEqual("Duplicate")
}).pipe(Effect.provide(MemoryLive)))
it.effect("unprocessedMessages excludes complete requests", () =>
Effect.gen(function*() {
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest()
yield* storage.saveRequest(request)
yield* storage.saveReply(yield* makeReply(request))
const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(0)
}).pipe(Effect.provide(MemoryLive)))
it.effect("repliesFor", () =>
Effect.gen(function*() {
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest()
yield* storage.saveRequest(request)
let replies = yield* storage.repliesFor([request])
expect(replies).toHaveLength(0)
yield* storage.saveReply(yield* makeReply(request))
replies = yield* storage.repliesFor([request])
expect(replies).toHaveLength(1)
expect(replies[0].requestId).toEqual(request.envelope.requestId)
}).pipe(Effect.provide(MemoryLive)))
it.effect("registerReplyHandler", () =>
Effect.gen(function*() {
const storage = yield* MessageStorage.MessageStorage
const latch = yield* Latch.make()
const request = yield* makeRequest()
yield* storage.saveRequest(request)
const fiber = yield* storage.registerReplyHandler(
new Message.OutgoingRequest({
...request,
respond: () => latch.open
})
).pipe(Effect.forkChild)
yield* TestClock.adjust(1)
yield* storage.saveReply(yield* makeReply(request))
yield* latch.await
yield* Fiber.await(fiber)
}).pipe(Effect.provide(MemoryLive)))
})
})
export const GetUserRpc = Rpc.make("GetUser", {
payload: { id: Schema.Number }
})
export const makeRequest = Effect.fnUntraced(function*(options?: {
readonly rpc?: Rpc.AnyWithProps
readonly payload?: any
}) {
const snowflake = yield* Snowflake.Generator
const rpc = options?.rpc ?? GetUserRpc
return new Message.OutgoingRequest({
envelope: Envelope.makeRequest<any>({
requestId: snowflake.nextUnsafe(),
address: EntityAddress.make({
shardId: ShardId.make("default", 1),
entityType: EntityType.make("test"),
entityId: EntityId.make("1")
}),
tag: rpc._tag,
payload: options?.payload ?? { id: 123 },
traceId: "noop",
spanId: "noop",
sampled: false,
headers: Headers.empty
}),
annotations: rpc.annotations,
context: Context.empty() as any,
rpc,
lastReceivedReply: Option.none(),
respond() {
return Effect.void
}
})
})
export class PrimaryKeyTest extends Rpc.make("PrimaryKeyTest", {
payload: {
id: Schema.Number
},
primaryKey: (value) => value.id.toString()
}) {}
export class StreamRpc extends Rpc.make("StreamTest", {
success: RpcSchema.Stream(Schema.Void, Schema.Never),
payload: {
id: Schema.Number
},
primaryKey: (value) => value.id.toString()
}) {}
export const makeReply = Effect.fnUntraced(function*(request: Message.OutgoingRequest<any>) {
const snowflake = yield* Snowflake.Generator
return new Reply.ReplyWithContext({
reply: new Reply.WithExit({
id: snowflake.nextUnsafe(),
requestId: request.envelope.requestId,
exit: Exit.void as any
}),
context: request.context,
rpc: request.rpc
})
})
export const makeAckChunk = Effect.fnUntraced(function*(
request: Message.OutgoingRequest<any>,
chunk: Reply.ReplyWithContext<any>
) {
const snowflake = yield* Snowflake.Generator
return new Message.OutgoingEnvelope({
envelope: new Envelope.AckChunk({
id: snowflake.nextUnsafe(),
address: request.envelope.address,
requestId: chunk.reply.requestId,
replyId: chunk.reply.id
}),
rpc: request.rpc
})
})
export const makeChunkReply = Effect.fnUntraced(function*(request: Message.OutgoingRequest<any>, sequence = 0) {
const snowflake = yield* Snowflake.Generator
return new Reply.ReplyWithContext({
reply: new Reply.Chunk({
id: snowflake.nextUnsafe(),
requestId: request.envelope.requestId,
sequence,
values: [undefined]
}),
context: request.context,
rpc: request.rpc
})
})
export const makeEmptyReply = (request: Message.OutgoingRequest<any>) => {
return new Reply.ReplyWithContext({
reply: Reply.Chunk.emptyFrom(request.envelope.requestId),
context: request.context,
rpc: request.rpc
})
}

View File

@@ -0,0 +1,120 @@
import { NodeClusterSocket } from "@effect/platform-node"
import { describe, it } from "@effect/vitest"
import { BigDecimal, Effect, Layer, Option, PrimaryKey, Schema } from "effect"
import {
ClusterSchema,
Entity,
MessageStorage,
RunnerAddress,
RunnerHealth,
RunnerStorage,
ShardingConfig,
SocketRunner
} from "effect/unstable/cluster"
import { Rpc, RpcSerialization } from "effect/unstable/rpc"
class TestPayload extends Schema.Class<TestPayload>("TestPayload")({
id: Schema.String,
amount: Schema.BigDecimal
}) {
[PrimaryKey.symbol]() {
return this.id
}
}
const TestEntity = Entity
.make("TestEntity", [
Rpc.make("Process", {
payload: TestPayload,
success: Schema.Void
})
])
.annotateRpcs(ClusterSchema.Persisted, true)
.annotateRpcs(ClusterSchema.Uninterruptible, true)
const TestEntityLayer = TestEntity.toLayer(
Effect.succeed({
Process: () => Effect.void
})
)
const RUNNER_PORT = 50_123
// Build shared storage instances once, so runner and client see the same state.
// MessageStorage.layerMemory requires ShardingConfig, so we provide a minimal one.
const SharedStorage = Layer.mergeAll(
RunnerStorage.layerMemory,
MessageStorage.layerMemory
).pipe(
Layer.provide(ShardingConfig.layerDefaults)
)
const makeRunnerLayer = (port: number) =>
TestEntityLayer.pipe(
Layer.provideMerge(SocketRunner.layer),
Layer.provide(RunnerHealth.layerNoop),
Layer.provide(NodeClusterSocket.layerSocketServer),
Layer.provide(NodeClusterSocket.layerClientProtocol),
Layer.provide(ShardingConfig.layer({
runnerAddress: Option.some(RunnerAddress.make("localhost", port)),
entityTerminationTimeout: 0,
entityMessagePollInterval: 5000,
sendRetryInterval: 100
})),
Layer.provide(RpcSerialization.layerMsgPack)
)
const makeClientLayer = (port: number) =>
SocketRunner.layerClientOnly.pipe(
Layer.provide(NodeClusterSocket.layerClientProtocol),
Layer.provide(ShardingConfig.layer({
runnerAddress: Option.some(RunnerAddress.make("localhost", port)),
runnerListenAddress: Option.some(RunnerAddress.make("localhost", port)),
entityTerminationTimeout: 0,
entityMessagePollInterval: 5000,
sendRetryInterval: 100
})),
Layer.provide(RpcSerialization.layerMsgPack)
)
// BigDecimal.normalize creates a circular `normalized` self-reference.
// When a persisted message is sent with discard: true, the notify path in Runners.makeRpc
// passes the raw envelope (with circular BigDecimal payload) to the runner via msgpack,
// causing RangeError: Maximum call stack size exceeded.
describe("SocketRunner", () => {
it.live(
"entity call with BigDecimal and discard should not stack overflow",
() =>
Effect.gen(function*() {
// Start the runner (with socket server and entity handler)
yield* Layer.launch(makeRunnerLayer(RUNNER_PORT)).pipe(Effect.forkScoped)
// Give the runner time to start and acquire shards
yield* Effect.sleep("2 seconds")
yield* Effect.log("Before starting the client")
// Send a message from the client with discard: true.
// The BigDecimal is normalized to trigger the circular `normalized` self-reference.
yield* Effect.gen(function*() {
yield* Effect.log("Starting the client")
yield* Effect.sleep("2 seconds")
const makeClient = yield* TestEntity.client
// Give the client time to discover the runner
yield* Effect.sleep("3 seconds")
const client = makeClient("entity-1")
const amount = BigDecimal.fromStringUnsafe("123.45")
yield* client.Process(
TestPayload.make({ id: "req-1", amount }),
{ discard: true }
)
}).pipe(
Effect.provide(makeClientLayer(RUNNER_PORT)),
Effect.scoped
)
}).pipe(Effect.provide(
SharedStorage
)),
30_000
)
})

View File

@@ -0,0 +1,222 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SqliteClient } from "@effect/sql-sqlite-node"
import { assert, describe, expect, it } from "@effect/vitest"
import { Effect, Fiber, FileSystem, Latch, Layer, Option } from "effect"
import { TestClock } from "effect/testing"
import { Message, MessageStorage, ShardingConfig, Snowflake, SqlMessageStorage } from "effect/unstable/cluster"
import { SqlClient } from "effect/unstable/sql"
import { MysqlContainer } from "../fixtures/mysql2-utils.ts"
import { PgContainer } from "../fixtures/pg-utils.ts"
import {
makeAckChunk,
makeChunkReply,
makeReply,
makeRequest,
PrimaryKeyTest,
StreamRpc
} from "./MessageStorageTest.ts"
const StorageLive = SqlMessageStorage.layer.pipe(
Layer.provideMerge(Snowflake.layerGenerator),
Layer.provide(ShardingConfig.layerDefaults)
)
const truncate = Effect.gen(function*() {
const sql = yield* SqlClient.SqlClient
yield* sql`DELETE FROM cluster_replies`
yield* sql`DELETE FROM cluster_messages`
})
describe("SqlMessageStorage", () => {
;([
["pg", Layer.orDie(PgContainer.layerClient)],
["mysql", Layer.orDie(MysqlContainer.layerClient)],
["sqlite", Layer.orDie(SqliteLayer)]
] as const).forEach(([label, layer]) => {
it.layer(StorageLive.pipe(Layer.provideMerge(layer)), {
timeout: 120000
})(label, (it) => {
it.effect("saveRequest", () =>
Effect.gen(function*() {
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest({ payload: { id: 1 } })
const result = yield* storage.saveRequest(request)
expect(result._tag).toEqual("Success")
for (let i = 2; i <= 5; i++) {
yield* storage.saveRequest(yield* makeRequest({ payload: { id: i } }))
}
yield* storage.saveReply(yield* makeReply(request))
let messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(4)
expect(messages.map((m: any) => m.envelope.payload.id)).toEqual([2, 3, 4, 5])
for (let i = 6; i <= 10; i++) {
yield* storage.saveRequest(yield* makeRequest({ payload: { id: i } }))
}
messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(5)
expect(messages.map((m: any) => m.envelope.payload.id)).toEqual([6, 7, 8, 9, 10])
}))
it.effect("saveReply + saveRequest duplicate", () =>
Effect.gen(function*() {
const sql = yield* SqlClient.SqlClient
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest({
rpc: StreamRpc,
payload: StreamRpc.payloadSchema.make({ id: 123 })
})
let result = yield* storage.saveRequest(request)
expect(result._tag).toEqual("Success")
let chunk = yield* makeChunkReply(request, 0)
yield* storage.saveReply(chunk)
const ackChunk = yield* makeAckChunk(request, chunk)
yield* storage.saveEnvelope(ackChunk)
chunk = yield* makeChunkReply(request, 1)
yield* storage.saveReply(chunk)
result = yield* storage.saveRequest(
yield* makeRequest({
rpc: StreamRpc,
payload: StreamRpc.payloadSchema.make({ id: 123 })
})
)
assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply))
expect(result.lastReceivedReply.value._tag).toEqual("Chunk")
// get the un-acked chunk
const replies = yield* storage.repliesFor([request])
expect(replies).toHaveLength(1)
yield* storage.saveReply(yield* makeReply(request))
result = yield* storage.saveRequest(
yield* makeRequest({
rpc: StreamRpc,
payload: StreamRpc.payloadSchema.make({ id: 123 })
})
)
assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply))
expect(result.lastReceivedReply.value._tag).toEqual("WithExit")
// duplicate WithExit
const fiber = yield* storage.saveReply(yield* makeReply(request)).pipe(Effect.forkChild)
yield* TestClock.adjust(1)
while (!fiber.pollUnsafe()) {
yield* sql`SELECT 1`
yield* TestClock.adjust(1000)
}
const error = yield* Effect.flip(Fiber.join(fiber))
expect(error._tag).toEqual("PersistenceError")
}))
it.effect("detects duplicates", () =>
Effect.gen(function*() {
yield* truncate
const storage = yield* MessageStorage.MessageStorage
yield* storage.saveRequest(
yield* makeRequest({
rpc: PrimaryKeyTest,
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
})
)
const result = yield* storage.saveRequest(
yield* makeRequest({
rpc: PrimaryKeyTest,
payload: PrimaryKeyTest.payloadSchema.make({ id: 123 })
})
)
expect(result._tag).toEqual("Duplicate")
}))
it.effect("unprocessedMessages", () =>
Effect.gen(function*() {
yield* truncate
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest()
yield* storage.saveRequest(request)
let messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(1)
messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(0)
yield* storage.saveRequest(yield* makeRequest())
messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(1)
}))
it.effect("unprocessedMessages excludes complete requests", () =>
Effect.gen(function*() {
yield* truncate
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest()
yield* storage.saveRequest(request)
yield* storage.saveReply(yield* makeReply(request))
const messages = yield* storage.unprocessedMessages([request.envelope.address.shardId])
expect(messages).toHaveLength(0)
}))
it.effect("repliesFor", () =>
Effect.gen(function*() {
yield* truncate
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest()
yield* storage.saveRequest(request)
let replies = yield* storage.repliesFor([request])
expect(replies).toHaveLength(0)
yield* storage.saveReply(yield* makeReply(request))
replies = yield* storage.repliesFor([request])
expect(replies).toHaveLength(1)
expect(replies[0].requestId).toEqual(request.envelope.requestId)
}))
it.effect("registerReplyHandler", () =>
Effect.gen(function*() {
const storage = yield* MessageStorage.MessageStorage
const latch = yield* Latch.make()
const request = yield* makeRequest()
yield* storage.saveRequest(request)
const fiber = yield* storage.registerReplyHandler(
new Message.OutgoingRequest({
...request,
respond: () => latch.open
})
).pipe(Effect.forkChild)
yield* TestClock.adjust(1)
yield* storage.saveReply(yield* makeReply(request))
yield* latch.await
yield* Fiber.await(fiber)
}))
it.effect("unprocessedMessagesById", () =>
Effect.gen(function*() {
yield* truncate
const storage = yield* MessageStorage.MessageStorage
const request = yield* makeRequest()
yield* storage.saveRequest(request)
let messages = yield* storage.unprocessedMessagesById([request.envelope.requestId])
expect(messages).toHaveLength(1)
yield* storage.saveReply(yield* makeReply(request))
messages = yield* storage.unprocessedMessagesById([request.envelope.requestId])
expect(messages).toHaveLength(0)
}))
})
})
})
const SqliteLayer = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
return SqliteClient.layer({
filename: dir + "/test.db"
})
}).pipe(Layer.unwrap, Layer.provide(NodeFileSystem.layer))

View File

@@ -0,0 +1,101 @@
import { NodeFileSystem } from "@effect/platform-node"
import { SqliteClient } from "@effect/sql-sqlite-node"
import { describe, expect, it } from "@effect/vitest"
import { Effect, FileSystem, Layer } from "effect"
import {
Runner,
RunnerAddress,
RunnerStorage,
ShardId,
ShardingConfig,
SqlRunnerStorage
} from "effect/unstable/cluster"
import { MysqlContainer } from "../fixtures/mysql2-utils.ts"
import { PgContainer } from "../fixtures/pg-utils.ts"
const StorageLive = SqlRunnerStorage.layer
describe("SqlRunnerStorage", () => {
;([
["pg", Layer.orDie(PgContainer.layerClient)],
["mysql", Layer.orDie(MysqlContainer.layerClient)],
["vitess", Layer.orDie(MysqlContainer.layerClientVitess)],
["sqlite", Layer.orDie(SqliteLayer)]
] as const).flatMap(([label, layer]) =>
[
[label, StorageLive.pipe(Layer.provideMerge(layer), Layer.provide(ShardingConfig.layer()))],
[
label + " (no advisory)",
StorageLive.pipe(
Layer.provideMerge(layer),
Layer.provide(ShardingConfig.layer({
shardLockDisableAdvisory: true
}))
)
]
] as const
).forEach(([label, layer]) => {
it.layer(layer, {
timeout: 60000
})(label, (it) => {
it.effect("getRunners", () =>
Effect.gen(function*() {
const storage = yield* RunnerStorage.RunnerStorage
const runner = Runner.make({
address: runnerAddress1,
groups: ["default"],
weight: 1
})
const machineId = yield* storage.register(runner, true)
yield* storage.register(runner, true)
expect(machineId).toEqual(1)
expect(yield* storage.getRunners).toEqual([[runner, true]])
yield* storage.setRunnerHealth(runnerAddress1, false)
expect(yield* storage.getRunners).toEqual([[runner, false]])
yield* storage.unregister(runnerAddress1)
expect(yield* storage.getRunners).toEqual([])
}), 30_000)
it.effect("acquireShards", () =>
Effect.gen(function*() {
const storage = yield* RunnerStorage.RunnerStorage
let acquired = yield* storage.acquire(runnerAddress1, [
ShardId.make("default", 1),
ShardId.make("default", 2),
ShardId.make("default", 3)
])
expect(acquired.map((_) => _.id)).toEqual([1, 2, 3])
acquired = yield* storage.acquire(runnerAddress1, [
ShardId.make("default", 1),
ShardId.make("default", 2),
ShardId.make("default", 3)
])
expect(acquired.map((_) => _.id)).toEqual([1, 2, 3])
const refreshed = yield* storage.refresh(runnerAddress1, [
ShardId.make("default", 1),
ShardId.make("default", 2),
ShardId.make("default", 3)
])
expect(refreshed.map((_) => _.id)).toEqual([1, 2, 3])
// smoke test release
yield* storage.release(runnerAddress1, ShardId.make("default", 2))
}))
})
})
})
const runnerAddress1 = RunnerAddress.make("localhost", 1234)
const SqliteLayer = Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
return SqliteClient.layer({
filename: dir + "/test.db"
})
}).pipe(Layer.unwrap, Layer.provide(NodeFileSystem.layer))

View File

@@ -0,0 +1 @@
outside root

View File

@@ -0,0 +1 @@
initial conditional body

View File

@@ -0,0 +1 @@
<html><body>custom home</body></html>

View File

@@ -0,0 +1 @@
binary-ish

View File

@@ -0,0 +1 @@
<html><body>guide index</body></html>

View File

@@ -0,0 +1 @@
hello static file

View File

@@ -0,0 +1 @@
<html><body>root index</body></html>

View File

@@ -0,0 +1 @@
0123456789abcdefghijklmnopqrstuvwxyz

View File

@@ -0,0 +1,64 @@
import { MysqlClient } from "@effect/sql-mysql2"
import type { StartedMySqlContainer } from "@testcontainers/mysql"
import { MySqlContainer } from "@testcontainers/mysql"
import { Context, Data, Effect, Layer, Redacted, String } from "effect"
export class ContainerError extends Data.TaggedError("ContainerError")<{
cause: unknown
}> {}
export class MysqlContainer extends Context.Service<
MysqlContainer,
StartedMySqlContainer
>()("test/MysqlContainer") {
static readonly layer = Layer.effect(this)(
Effect.acquireRelease(
Effect.tryPromise({
try: () => new MySqlContainer("mysql:lts").start(),
catch: (cause) => new ContainerError({ cause })
}),
(container) => Effect.promise(() => container.stop())
)
)
static client = Layer.unwrap(
Effect.gen(function*() {
const container = yield* MysqlContainer
return MysqlClient.layer({
url: Redacted.make(container.getConnectionUri())
})
})
)
static layerClient = this.client.pipe(Layer.provide(this.layer))
static layerClientWithTransforms = Layer.unwrap(
Effect.gen(function*() {
const container = yield* MysqlContainer
return MysqlClient.layer({
url: Redacted.make(container.getConnectionUri()),
transformQueryNames: String.camelToSnake,
transformResultNames: String.snakeToCamel
})
})
).pipe(Layer.provide(this.layer))
static layerVitest = Layer.effect(
this,
Effect.acquireRelease(
Effect.tryPromise({
try: () =>
new MySqlContainer("vitess/vttestserver:mysql80").withEnvironment({
KEYSPACES: "test,unsharded",
NUM_SHARDS: "1,1",
MYSQL_BIND_HOST: "0.0.0.0",
PORT: "3303"
}).start(),
catch: (cause) => new ContainerError({ cause })
}),
(container) => Effect.promise(() => container.stop())
)
)
static layerClientVitess = this.client.pipe(Layer.provide(this.layerVitest))
}

View File

@@ -0,0 +1,39 @@
import { PgClient } from "@effect/sql-pg"
import { PostgreSqlContainer } from "@testcontainers/postgresql"
import { Context, Data, Effect, Layer, Redacted, String } from "effect"
export class ContainerError extends Data.TaggedError("ContainerError")<{
cause: unknown
}> {}
export class PgContainer extends Context.Service<PgContainer>()("test/PgContainer", {
make: Effect.acquireRelease(
Effect.tryPromise({
try: () => new PostgreSqlContainer("postgres:alpine").start(),
catch: (cause) => new ContainerError({ cause })
}),
(container) => Effect.promise(() => container.stop())
)
}) {
static readonly layer = Layer.effect(this)(this.make)
static layerClient = Layer.unwrap(
Effect.gen(function*() {
const container = yield* PgContainer
return PgClient.layer({
url: Redacted.make(container.getConnectionUri())
})
})
).pipe(Layer.provide(this.layer))
static layerClientWithTransforms = Layer.unwrap(
Effect.gen(function*() {
const container = yield* PgContainer
return PgClient.layer({
url: Redacted.make(container.getConnectionUri()),
transformResultNames: String.snakeToCamel,
transformQueryNames: String.camelToSnake
})
})
).pipe(Layer.provide(this.layer))
}

View File

@@ -0,0 +1,149 @@
import { assert, describe, it } from "@effect/vitest"
import { Cause, Context, Effect, Fiber, Option, Stream } from "effect"
// @ts-ignore
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
import { NodeInspectSymbol } from "effect/Inspectable"
import * as Layer from "effect/Layer"
import * as RpcClient from "effect/unstable/rpc/RpcClient"
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"
import type * as RpcGroup from "effect/unstable/rpc/RpcGroup"
import * as RpcServer from "effect/unstable/rpc/RpcServer"
import * as RpcTest from "effect/unstable/rpc/RpcTest"
import { AuthClient, AuthLive, TimingLive, User, UserRpcs, UsersLive } from "./rpc-schemas.ts"
export class UsersClient extends Context.Service<
UsersClient,
RpcClient.RpcClient<RpcGroup.Rpcs<typeof UserRpcs>, RpcClientError>
>()("UsersClient") {
static readonly layer = Layer.effect(UsersClient)(RpcClient.make(UserRpcs)).pipe(
Layer.provide(AuthClient)
)
static layerTest = Layer.effect(UsersClient)(RpcTest.makeClient(UserRpcs)).pipe(
Layer.provide([UsersLive, AuthLive, TimingLive, AuthClient])
)
}
export const e2eSuite = <E>(
name: string,
layer: Layer.Layer<UsersClient | RpcServer.Protocol, E>,
concurrent = true
) => {
describe(name, { concurrent, timeout: 30_000 }, () => {
it.effect("should get user", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const user = yield* client.GetUser({ id: "1" })
assert.instanceOf(user, User)
assert.deepStrictEqual(user, new User({ id: "1", name: "Logged in user" }))
}).pipe(Effect.provide(layer)))
it.effect("should get deferred user", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const user = yield* client.GetUserDeferred({ id: "1" })
assert.instanceOf(user, User)
assert.deepStrictEqual(user, new User({ id: "1", name: "John" }))
}).pipe(Effect.provide(layer)))
it.effect("nested method", () =>
Effect.gen(function*() {
const client = yield* UsersClient
yield* client["nested.test"]()
}).pipe(Effect.provide(layer)))
it.effect("should not flatten Option", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const user = yield* client.GetUserOption({ id: "1" })
assert.deepStrictEqual(user, Option.some(new User({ id: "1", name: "John" })))
}).pipe(Effect.provide(layer)))
it.effect("headers", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const user = yield* client.GetUser({ id: "1" })
assert.instanceOf(user, User)
assert.deepStrictEqual(user, new User({ id: "123", name: "Logged in user" }))
}).pipe(
RpcClient.withHeaders({ userId: "123" }),
Effect.provide(layer)
))
it.live("Stream", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const users: Array<User> = []
yield* client.StreamUsers({ id: "1" }).pipe(
Stream.take(5),
Stream.runForEach((user) =>
Effect.sync(() => {
users.push(user)
})
),
Effect.forkChild
)
yield* Effect.sleep(2000)
assert.lengthOf(users, 5)
// test interrupts
const interrupts = yield* client.GetInterrupts()
assert.equal(interrupts, 1)
const { supportsAck } = yield* RpcServer.Protocol
// test backpressure
if (supportsAck) {
const emits = yield* client.GetEmits()
assert.equal(emits, 5)
}
}).pipe(Effect.provide(layer)), { timeout: 20000 })
it.effect("defect", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const cause = yield* client.ProduceDefect().pipe(
Effect.sandbox,
Effect.flip
)
assert.deepStrictEqual(cause, Cause.die("boom"))
}).pipe(
RpcClient.withHeaders({ userId: "123" }),
Effect.provide(layer)
))
it.live("never", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const fiber = yield* client.Never().pipe(
Effect.forkChild
)
yield* Effect.sleep(500)
assert.isUndefined(fiber.pollUnsafe())
yield* Fiber.interrupt(fiber)
yield* Effect.sleep(100)
const { supportsAck } = yield* RpcServer.Protocol
if (supportsAck) {
const interrupts = yield* client.GetInterrupts()
assert.equal(interrupts, 1)
}
}).pipe(
RpcClient.withHeaders({ userId: "123" }),
Effect.provide(layer)
))
it.effect("timing middleware", () =>
Effect.gen(function*() {
const client = yield* UsersClient
const result = yield* client.TimedMethod({ shouldFail: false })
assert.equal(result, 1)
yield* client.TimedMethod({ shouldFail: true }).pipe(Effect.exit)
const { count, defect, success } = yield* client.GetTimingMiddlewareMetrics()
assert.notEqual(count, 0)
assert.notEqual(defect, 0)
assert.notEqual(success, 0)
}).pipe(Effect.provide(layer)))
})
}

View File

@@ -0,0 +1,175 @@
import { Context, Deferred, Effect, Layer, Metric, Option, Queue, Schema } from "effect"
import { Headers } from "effect/unstable/http"
import * as Rpc from "effect/unstable/rpc/Rpc"
import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware"
import * as RpcServer from "effect/unstable/rpc/RpcServer"
export class User extends Schema.Class<User>("User")({
id: Schema.String,
name: Schema.String
}) {}
class StreamUsers extends Rpc.make("StreamUsers", {
success: User,
payload: {
id: Schema.String
},
stream: true
}) {}
class CurrentUser extends Context.Service<CurrentUser, User>()("CurrentUser") {}
class Unauthorized extends Schema.ErrorClass<Unauthorized>("Unauthorized")({
_tag: Schema.tag("Unauthorized")
}) {}
class AuthMiddleware extends RpcMiddleware.Service<AuthMiddleware, {
provides: CurrentUser
}>()("AuthMiddleware", {
error: Unauthorized,
requiredForClient: true
}) {}
class TimingMiddleware extends RpcMiddleware.Service<TimingMiddleware>()("TimingMiddleware") {}
class GetUser extends Rpc.make("GetUser", {
success: User,
payload: { id: Schema.String }
}) {}
export const UserRpcs = RpcGroup.make(
GetUser,
Rpc.make("GetUserDeferred", {
success: User,
payload: { id: Schema.String }
}),
Rpc.make("GetUserOption", {
success: Schema.Option(User),
payload: { id: Schema.String }
}),
StreamUsers,
Rpc.make("GetInterrupts", {
success: Schema.Number
}),
Rpc.make("GetEmits", {
success: Schema.Number
}),
Rpc.make("ProduceDefect"),
Rpc.make("ProduceDefectCustom", {
defect: Schema.Defect({ includeStack: true })
}),
Rpc.make("Never"),
Rpc.make("nested.test"),
Rpc.make("TimedMethod", {
payload: {
shouldFail: Schema.Boolean
},
success: Schema.Number
}).middleware(TimingMiddleware),
Rpc.make("GetTimingMiddlewareMetrics", {
success: Schema.Struct({
success: Schema.Number,
defect: Schema.Number,
count: Schema.Number
})
})
).middleware(AuthMiddleware)
export const AuthLive = Layer.succeed(AuthMiddleware)(
AuthMiddleware.of((effect, options) =>
Effect.provideService(
effect,
CurrentUser,
new User({ id: options.headers.userid ?? "1", name: options.headers.name ?? "Fallback name" })
)
)
)
const rpcSuccesses = Metric.counter("rpc_middleware_success")
const rpcDefects = Metric.counter("rpc_middleware_defects")
const rpcCount = Metric.counter("rpc_middleware_count")
export const TimingLive = Layer.succeed(TimingMiddleware)(
TimingMiddleware.of((effect) =>
effect.pipe(
Effect.tap(Metric.update(rpcSuccesses, 1)),
Effect.tapDefect(() => Metric.update(rpcDefects, 1)),
Effect.ensuring(Metric.update(rpcCount, 1))
)
)
)
export const UsersLive = UserRpcs.toLayer(Effect.gen(function*() {
let interrupts = 0
let emits = 0
return UserRpcs.of({
GetUser: (_) =>
CurrentUser.pipe(
Rpc.fork
),
GetUserDeferred(_) {
const deferred = Deferred.makeUnsafe<User>()
Deferred.doneUnsafe(deferred, Effect.succeed(new User({ id: "1", name: "John" })))
return Effect.succeed(deferred)
},
GetUserOption: Effect.fnUntraced(function*(req) {
return Option.some(new User({ id: req.id, name: "John" }))
}),
StreamUsers: Effect.fnUntraced(function*(req, _) {
const mailbox = yield* Queue.bounded<User>(0)
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
interrupts++
})
)
yield* Queue.offer(mailbox, new User({ id: req.id, name: "John" })).pipe(
Effect.tap(() =>
Effect.sync(() => {
emits++
})
),
Effect.delay(100),
Effect.forever,
Effect.forkScoped
)
return mailbox
}),
GetInterrupts: () => Effect.sync(() => interrupts),
GetEmits: () => Effect.sync(() => emits),
ProduceDefect: () => Effect.die("boom"),
ProduceDefectCustom: () =>
Effect.die({
message: "detailed error",
stack: "Error: detailed error\n at handler.ts:1",
name: "CustomDefect"
}),
Never: () => Effect.never.pipe(Effect.onInterrupt(() => Effect.sync(() => interrupts++))),
"nested.test": () => Effect.void,
TimedMethod: (_) => _.shouldFail ? Effect.die("boom") : Effect.succeed(1),
GetTimingMiddlewareMetrics: () =>
Effect.all({
defect: Metric.value(rpcDefects).pipe(Effect.map((_) => _.count)),
success: Metric.value(rpcSuccesses).pipe(Effect.map((_) => _.count)),
count: Metric.value(rpcCount).pipe(Effect.map((_) => _.count))
})
})
}))
export const RpcLive = RpcServer.layer(UserRpcs, {
disableFatalDefects: true
}).pipe(
Layer.provide([
UsersLive,
AuthLive,
TimingLive
])
)
export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) =>
next({
...request,
headers: Headers.set(request.headers, "name", "Logged in user")
}))

View File

@@ -0,0 +1 @@
lorem ipsum dolar sit amet

View File

@@ -0,0 +1,12 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"include": ["src"],
"references": [
{ "path": "../effect" },
{ "path": "../platform-node-shared" }
],
"compilerOptions": {
"types": ["node"]
}
}

View File

@@ -0,0 +1,6 @@
import { mergeConfig, type ViteUserConfig } from "vitest/config"
import shared from "../../vitest.shared.ts"
const config: ViteUserConfig = {}
export default mergeConfig(shared, config)