Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
825
repos/effect/packages/platform-node/CHANGELOG.md
Normal file
825
repos/effect/packages/platform-node/CHANGELOG.md
Normal file
@@ -0,0 +1,825 @@
|
||||
# @effect/platform-node
|
||||
|
||||
## 4.0.0-beta.99
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`b330264`](https://github.com/Effect-TS/effect/commit/b3302645fc1c23f7d03a3693dabdcb88763d5cbd), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]:
|
||||
- effect@4.0.0-beta.99
|
||||
- @effect/platform-node-shared@4.0.0-beta.99
|
||||
|
||||
## 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
|
||||
21
repos/effect/packages/platform-node/LICENSE
Normal file
21
repos/effect/packages/platform-node/LICENSE
Normal 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.
|
||||
7
repos/effect/packages/platform-node/README.md
Normal file
7
repos/effect/packages/platform-node/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# `@effect/platform-node`
|
||||
|
||||
Provides Node.js-specific implementations for Effect's platform abstractions, 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).
|
||||
23
repos/effect/packages/platform-node/docgen.json
Normal file
23
repos/effect/packages/platform-node/docgen.json
Normal 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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
81
repos/effect/packages/platform-node/package.json
Normal file
81
repos/effect/packages/platform-node/package.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "@effect/platform-node",
|
||||
"type": "module",
|
||||
"version": "4.0.0-beta.99",
|
||||
"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.git",
|
||||
"directory": "packages/platform-node"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Effect-TS/effect/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:^"
|
||||
}
|
||||
}
|
||||
25
repos/effect/packages/platform-node/src/Mime.ts
Normal file
25
repos/effect/packages/platform-node/src/Mime.ts
Normal 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
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Node.js implementation of `ChildProcessSpawner`.
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeChildProcessSpawner"
|
||||
155
repos/effect/packages/platform-node/src/NodeClusterHttp.ts
Normal file
155
repos/effect/packages/platform-node/src/NodeClusterHttp.ts
Normal 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)
|
||||
176
repos/effect/packages/platform-node/src/NodeClusterSocket.ts
Normal file
176
repos/effect/packages/platform-node/src/NodeClusterSocket.ts
Normal 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)
|
||||
)
|
||||
24
repos/effect/packages/platform-node/src/NodeCrypto.ts
Normal file
24
repos/effect/packages/platform-node/src/NodeCrypto.ts
Normal 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
|
||||
21
repos/effect/packages/platform-node/src/NodeFileSystem.ts
Normal file
21
repos/effect/packages/platform-node/src/NodeFileSystem.ts
Normal 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
|
||||
664
repos/effect/packages/platform-node/src/NodeHttpClient.ts
Normal file
664
repos/effect/packages/platform-node/src/NodeHttpClient.ts
Normal 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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
70
repos/effect/packages/platform-node/src/NodeHttpPlatform.ts
Normal file
70
repos/effect/packages/platform-node/src/NodeHttpPlatform.ts
Normal 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)
|
||||
)
|
||||
647
repos/effect/packages/platform-node/src/NodeHttpServer.ts
Normal file
647
repos/effect/packages/platform-node/src/NodeHttpServer.ts
Normal 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
|
||||
)
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
183
repos/effect/packages/platform-node/src/NodeMultipart.ts
Normal file
183
repos/effect/packages/platform-node/src/NodeMultipart.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
40
repos/effect/packages/platform-node/src/NodePath.ts
Normal file
40
repos/effect/packages/platform-node/src/NodePath.ts
Normal 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
|
||||
92
repos/effect/packages/platform-node/src/NodeRedis.ts
Normal file
92
repos/effect/packages/platform-node/src/NodeRedis.ts
Normal 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)
|
||||
)
|
||||
)
|
||||
53
repos/effect/packages/platform-node/src/NodeRuntime.ts
Normal file
53
repos/effect/packages/platform-node/src/NodeRuntime.ts
Normal 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
|
||||
50
repos/effect/packages/platform-node/src/NodeServices.ts
Normal file
50
repos/effect/packages/platform-node/src/NodeServices.ts
Normal 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
|
||||
)
|
||||
)
|
||||
8
repos/effect/packages/platform-node/src/NodeSink.ts
Normal file
8
repos/effect/packages/platform-node/src/NodeSink.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeSink"
|
||||
72
repos/effect/packages/platform-node/src/NodeSocket.ts
Normal file
72
repos/effect/packages/platform-node/src/NodeSocket.ts
Normal 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)
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeSocketServer"
|
||||
22
repos/effect/packages/platform-node/src/NodeStdio.ts
Normal file
22
repos/effect/packages/platform-node/src/NodeStdio.ts
Normal 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
|
||||
8
repos/effect/packages/platform-node/src/NodeStream.ts
Normal file
8
repos/effect/packages/platform-node/src/NodeStream.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export * from "@effect/platform-node-shared/NodeStream"
|
||||
32
repos/effect/packages/platform-node/src/NodeTerminal.ts
Normal file
32
repos/effect/packages/platform-node/src/NodeTerminal.ts
Normal 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
|
||||
121
repos/effect/packages/platform-node/src/NodeWorker.ts
Normal file
121
repos/effect/packages/platform-node/src/NodeWorker.ts
Normal 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
|
||||
)
|
||||
110
repos/effect/packages/platform-node/src/NodeWorkerRunner.ts
Normal file
110
repos/effect/packages/platform-node/src/NodeWorkerRunner.ts
Normal 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 }
|
||||
})
|
||||
}
|
||||
})
|
||||
29
repos/effect/packages/platform-node/src/Undici.ts
Normal file
29
repos/effect/packages/platform-node/src/Undici.ts
Normal 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
|
||||
130
repos/effect/packages/platform-node/src/index.ts
Normal file
130
repos/effect/packages/platform-node/src/index.ts
Normal 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"
|
||||
1831
repos/effect/packages/platform-node/test/HttpApi.test.ts
Normal file
1831
repos/effect/packages/platform-node/test/HttpApi.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
59
repos/effect/packages/platform-node/test/NodeCrypto.test.ts
Normal file
59
repos/effect/packages/platform-node/test/NodeCrypto.test.ts
Normal 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)))
|
||||
})
|
||||
157
repos/effect/packages/platform-node/test/NodeHttpClient.test.ts
Normal file
157
repos/effect/packages/platform-node/test/NodeHttpClient.test.ts
Normal 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
|
||||
})
|
||||
)
|
||||
@@ -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)))
|
||||
})
|
||||
598
repos/effect/packages/platform-node/test/NodeHttpServer.test.ts
Normal file
598
repos/effect/packages/platform-node/test/NodeHttpServer.test.ts
Normal 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)))
|
||||
})
|
||||
84
repos/effect/packages/platform-node/test/NodeRedis.test.ts
Normal file
84
repos/effect/packages/platform-node/test/NodeRedis.test.ts
Normal 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
|
||||
})
|
||||
153
repos/effect/packages/platform-node/test/NodeSocket.test.ts
Normal file
153
repos/effect/packages/platform-node/test/NodeSocket.test.ts
Normal 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"])
|
||||
}))
|
||||
})
|
||||
})
|
||||
1573
repos/effect/packages/platform-node/test/OpenApi.test.ts
Normal file
1573
repos/effect/packages/platform-node/test/OpenApi.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
292
repos/effect/packages/platform-node/test/RpcServer.test.ts
Normal file
292
repos/effect/packages/platform-node/test/RpcServer.test.ts
Normal 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 }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
})
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server-outside.txt
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server-outside.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
outside root
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/conditional.txt
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/conditional.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
initial conditional body
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/custom/home.html
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/custom/home.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<html><body>custom home</body></html>
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/file.binx
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/file.binx
vendored
Normal file
@@ -0,0 +1 @@
|
||||
binary-ish
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/guide/index.html
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/guide/index.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<html><body>guide index</body></html>
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/hello.txt
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/hello.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hello static file
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/index.html
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/index.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<html><body>root index</body></html>
|
||||
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/range.txt
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/http-static-server/range.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
0123456789abcdefghijklmnopqrstuvwxyz
|
||||
64
repos/effect/packages/platform-node/test/fixtures/mysql2-utils.ts
vendored
Normal file
64
repos/effect/packages/platform-node/test/fixtures/mysql2-utils.ts
vendored
Normal 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))
|
||||
}
|
||||
39
repos/effect/packages/platform-node/test/fixtures/pg-utils.ts
vendored
Normal file
39
repos/effect/packages/platform-node/test/fixtures/pg-utils.ts
vendored
Normal 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))
|
||||
}
|
||||
149
repos/effect/packages/platform-node/test/fixtures/rpc-e2e.ts
vendored
Normal file
149
repos/effect/packages/platform-node/test/fixtures/rpc-e2e.ts
vendored
Normal 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)))
|
||||
})
|
||||
}
|
||||
175
repos/effect/packages/platform-node/test/fixtures/rpc-schemas.ts
vendored
Normal file
175
repos/effect/packages/platform-node/test/fixtures/rpc-schemas.ts
vendored
Normal 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")
|
||||
}))
|
||||
1
repos/effect/packages/platform-node/test/fixtures/text.txt
vendored
Normal file
1
repos/effect/packages/platform-node/test/fixtures/text.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
lorem ipsum dolar sit amet
|
||||
12
repos/effect/packages/platform-node/tsconfig.json
Normal file
12
repos/effect/packages/platform-node/tsconfig.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
6
repos/effect/packages/platform-node/vitest.config.ts
Normal file
6
repos/effect/packages/platform-node/vitest.config.ts
Normal 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)
|
||||
Reference in New Issue
Block a user