├── .gitignore ├── .prettierrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── lerna.json ├── package.json ├── packages ├── bun │ ├── .changeset │ │ ├── README.md │ │ ├── config.json │ │ └── flat-dancers-notice.md │ ├── .vscode │ │ └── settings.json │ ├── CHANGELOG.md │ ├── Makefile │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── tsplus.config.json ├── client │ ├── .changeset │ │ ├── README.md │ │ └── config.json │ ├── .vscode │ │ └── settings.json │ ├── CHANGELOG.md │ ├── Makefile │ ├── README.md │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── Error.ts │ │ ├── Request.ts │ │ ├── Request │ │ │ ├── Body.ts │ │ │ ├── Executor.ts │ │ │ └── FetchExecutor.ts │ │ ├── Response.ts │ │ ├── _common.ts │ │ ├── _global.ts │ │ ├── index.ts │ │ └── util │ │ │ └── stream.ts │ ├── tsconfig.json │ └── tsplus.config.json ├── core │ ├── .changeset │ │ ├── README.md │ │ └── config.json │ ├── .vscode │ │ └── settings.json │ ├── CHANGELOG.md │ ├── Makefile │ ├── package.json │ ├── src │ │ ├── HttpApp.ts │ │ ├── Request.ts │ │ ├── Response.ts │ │ ├── _common.ts │ │ ├── _global.ts │ │ ├── definitions.ts │ │ ├── index.ts │ │ ├── internal │ │ │ └── HttpFs.ts │ │ ├── multipart.ts │ │ ├── router.ts │ │ ├── schema.ts │ │ └── serveDirectory.ts │ ├── tsconfig.json │ └── tsplus.config.json └── node │ ├── .changeset │ ├── README.md │ ├── config.json │ └── fresh-ducks-explain.md │ ├── .vscode │ └── settings.json │ ├── CHANGELOG.md │ ├── Makefile │ ├── package.json │ ├── src │ ├── Client.ts │ ├── Server.ts │ ├── _common.ts │ ├── index.ts │ └── internal │ │ ├── Agent.ts │ │ ├── HttpFs.ts │ │ ├── Request.ts │ │ ├── body.ts │ │ ├── fs.ts │ │ ├── multipart.ts │ │ └── stream.ts │ ├── tsconfig.json │ └── tsplus.config.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | dist/ 4 | tsconfig.tsbuildinfo 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": false, 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.40.1](https://github.com/tim-smart/effect-http/compare/v0.40.0...v0.40.1) (2023-08-04) 7 | 8 | **Note:** Version bump only for package effect-http 9 | 10 | 11 | 12 | 13 | 14 | # [0.40.0](https://github.com/tim-smart/effect-http/compare/v0.39.0...v0.40.0) (2023-08-04) 15 | 16 | **Note:** Version bump only for package effect-http 17 | 18 | 19 | 20 | 21 | 22 | # [0.39.0](https://github.com/tim-smart/effect-http/compare/v0.38.0...v0.39.0) (2023-07-31) 23 | 24 | **Note:** Version bump only for package effect-http 25 | 26 | 27 | 28 | 29 | 30 | # [0.38.0](https://github.com/tim-smart/effect-http/compare/v0.37.0...v0.38.0) (2023-07-27) 31 | 32 | **Note:** Version bump only for package effect-http 33 | 34 | 35 | 36 | 37 | 38 | # [0.37.0](https://github.com/tim-smart/effect-http/compare/v0.36.0...v0.37.0) (2023-07-25) 39 | 40 | **Note:** Version bump only for package effect-http 41 | 42 | 43 | 44 | 45 | 46 | # [0.36.0](https://github.com/tim-smart/effect-http/compare/v0.35.0...v0.36.0) (2023-07-25) 47 | 48 | **Note:** Version bump only for package effect-http 49 | 50 | 51 | 52 | 53 | 54 | # [0.35.0](https://github.com/tim-smart/effect-http/compare/v0.34.0...v0.35.0) (2023-07-20) 55 | 56 | **Note:** Version bump only for package effect-http 57 | 58 | 59 | 60 | 61 | 62 | # [0.34.0](https://github.com/tim-smart/effect-http/compare/v0.33.0...v0.34.0) (2023-07-19) 63 | 64 | **Note:** Version bump only for package effect-http 65 | 66 | 67 | 68 | 69 | 70 | # [0.33.0](https://github.com/tim-smart/effect-http/compare/v0.32.0...v0.33.0) (2023-07-18) 71 | 72 | **Note:** Version bump only for package effect-http 73 | 74 | 75 | 76 | 77 | 78 | # [0.32.0](https://github.com/tim-smart/effect-http/compare/v0.31.1...v0.32.0) (2023-07-17) 79 | 80 | **Note:** Version bump only for package effect-http 81 | 82 | 83 | 84 | 85 | 86 | ## [0.31.1](https://github.com/tim-smart/effect-http/compare/v0.31.0...v0.31.1) (2023-07-12) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * don't build handler env twice ([dd6add5](https://github.com/tim-smart/effect-http/commit/dd6add56a3df3b7d2a828782be02c96c50023698)) 92 | 93 | 94 | 95 | 96 | 97 | # [0.31.0](https://github.com/tim-smart/effect-http/compare/v0.30.0...v0.31.0) (2023-07-12) 98 | 99 | **Note:** Version bump only for package effect-http 100 | 101 | 102 | 103 | 104 | 105 | # [0.30.0](https://github.com/tim-smart/effect-http/compare/v0.29.1...v0.30.0) (2023-07-11) 106 | 107 | **Note:** Version bump only for package effect-http 108 | 109 | 110 | 111 | 112 | 113 | ## [0.29.1](https://github.com/tim-smart/effect-http/compare/v0.29.0...v0.29.1) (2023-07-10) 114 | 115 | **Note:** Version bump only for package effect-http 116 | 117 | 118 | 119 | 120 | 121 | # [0.29.0](https://github.com/tim-smart/effect-http/compare/v0.28.0...v0.29.0) (2023-07-08) 122 | 123 | **Note:** Version bump only for package effect-http 124 | 125 | 126 | 127 | 128 | 129 | # [0.28.0](https://github.com/tim-smart/effect-http/compare/v0.27.5...v0.28.0) (2023-06-23) 130 | 131 | 132 | ### Features 133 | 134 | * add preprocess to router ([7016e0f](https://github.com/tim-smart/effect-http/commit/7016e0fd301ef0b6b75563df4b5b54a73145abf1)) 135 | 136 | 137 | 138 | 139 | 140 | ## [0.27.5](https://github.com/tim-smart/effect-http/compare/v0.27.4...v0.27.5) (2023-06-23) 141 | 142 | 143 | ### Bug Fixes 144 | 145 | * node request text ([6f33e21](https://github.com/tim-smart/effect-http/commit/6f33e216f48aa272d7373f3ac3a0734e390a897b)) 146 | 147 | 148 | 149 | 150 | 151 | ## [0.27.4](https://github.com/tim-smart/effect-http/compare/v0.27.3...v0.27.4) (2023-06-23) 152 | 153 | 154 | ### Reverts 155 | 156 | * Revert "chore: update deps" ([d600d64](https://github.com/tim-smart/effect-http/commit/d600d640d1817822c0f83527e1b097058def367d)) 157 | 158 | 159 | 160 | 161 | 162 | ## [0.27.3](https://github.com/tim-smart/effect-http/compare/v0.27.2...v0.27.3) (2023-06-22) 163 | 164 | **Note:** Version bump only for package effect-http 165 | 166 | 167 | 168 | 169 | 170 | ## [0.27.2](https://github.com/tim-smart/effect-http/compare/v0.27.1...v0.27.2) (2023-06-18) 171 | 172 | **Note:** Version bump only for package effect-http 173 | 174 | 175 | 176 | 177 | 178 | ## [0.27.1](https://github.com/tim-smart/effect-http/compare/v0.27.0...v0.27.1) (2023-06-18) 179 | 180 | **Note:** Version bump only for package effect-http 181 | 182 | 183 | 184 | 185 | 186 | # [0.27.0](https://github.com/tim-smart/effect-http/compare/v0.26.1...v0.27.0) (2023-06-02) 187 | 188 | **Note:** Version bump only for package effect-http 189 | 190 | 191 | 192 | 193 | 194 | ## [0.26.1](https://github.com/tim-smart/effect-http/compare/v0.26.0...v0.26.1) (2023-05-22) 195 | 196 | **Note:** Version bump only for package effect-http 197 | 198 | 199 | 200 | 201 | 202 | # [0.26.0](https://github.com/tim-smart/effect-http/compare/v0.25.2...v0.26.0) (2023-05-15) 203 | 204 | 205 | ### Features 206 | 207 | * tap to client Executor ([664b1cc](https://github.com/tim-smart/effect-http/commit/664b1cc6a7c3b676b8748eae430fedef3c97e7c8)) 208 | 209 | 210 | 211 | 212 | 213 | ## [0.25.2](https://github.com/tim-smart/effect-http/compare/v0.25.1...v0.25.2) (2023-05-15) 214 | 215 | 216 | ### Bug Fixes 217 | 218 | * catchTag ([c591756](https://github.com/tim-smart/effect-http/commit/c5917564ad2f7c72546c65e1a91a6d3a95bb3be9)) 219 | 220 | 221 | 222 | 223 | 224 | ## [0.25.1](https://github.com/tim-smart/effect-http/compare/v0.25.0...v0.25.1) (2023-05-12) 225 | 226 | 227 | ### Bug Fixes 228 | 229 | * bun runPromise ([7e15baf](https://github.com/tim-smart/effect-http/commit/7e15baf87221e504b8c604f0b58463da1548a2fc)) 230 | 231 | 232 | 233 | 234 | 235 | # [0.25.0](https://github.com/tim-smart/effect-http/compare/v0.24.2...v0.25.0) (2023-05-10) 236 | 237 | **Note:** Version bump only for package effect-http 238 | 239 | 240 | 241 | 242 | 243 | ## [0.24.2](https://github.com/tim-smart/effect-http/compare/v0.24.1...v0.24.2) (2023-05-01) 244 | 245 | **Note:** Version bump only for package effect-http 246 | 247 | 248 | 249 | 250 | 251 | ## [0.24.1](https://github.com/tim-smart/effect-http/compare/v0.24.0...v0.24.1) (2023-04-30) 252 | 253 | 254 | ### Features 255 | 256 | * appendUrl ([229ad75](https://github.com/tim-smart/effect-http/commit/229ad75d9f9154521121bc71182965196868a5b1)) 257 | 258 | 259 | 260 | 261 | 262 | # [0.24.0](https://github.com/tim-smart/effect-http/compare/v0.23.0...v0.24.0) (2023-04-30) 263 | 264 | 265 | ### Features 266 | 267 | * Request modify ([d1301cf](https://github.com/tim-smart/effect-http/commit/d1301cf505b1357178a2ba0e731f8c14788e04bc)) 268 | 269 | 270 | 271 | 272 | 273 | # [0.23.0](https://github.com/tim-smart/effect-http/compare/v0.22.4...v0.23.0) (2023-04-30) 274 | 275 | 276 | ### Features 277 | 278 | * setMethod & setUrl ([7057a67](https://github.com/tim-smart/effect-http/commit/7057a67ceb5cf85baf4856fd99cd60070eeb8505)) 279 | 280 | 281 | 282 | 283 | 284 | ## [0.22.4](https://github.com/tim-smart/effect-http/compare/v0.22.3...v0.22.4) (2023-04-27) 285 | 286 | 287 | ### Bug Fixes 288 | 289 | * retry return type ([29fc101](https://github.com/tim-smart/effect-http/commit/29fc1011baf754c82076471432e44ca66463a628)) 290 | 291 | 292 | 293 | 294 | 295 | ## [0.22.3](https://github.com/tim-smart/effect-http/compare/v0.22.2...v0.22.3) (2023-04-27) 296 | 297 | 298 | ### Bug Fixes 299 | 300 | * realign retry signature with Effect.retry ([7005552](https://github.com/tim-smart/effect-http/commit/7005552fdba3662d059c2dc5e4cef6f259d5d033)) 301 | 302 | 303 | 304 | 305 | 306 | ## [0.22.2](https://github.com/tim-smart/effect-http/compare/v0.22.1...v0.22.2) (2023-04-26) 307 | 308 | **Note:** Version bump only for package effect-http 309 | 310 | 311 | 312 | 313 | 314 | ## [0.22.1](https://github.com/tim-smart/effect-http/compare/v0.22.0...v0.22.1) (2023-04-26) 315 | 316 | 317 | ### Features 318 | 319 | * basicAuth for Request ([0074df1](https://github.com/tim-smart/effect-http/commit/0074df170b545d90a91e4cd207b94eea0f537da3)) 320 | 321 | 322 | 323 | 324 | 325 | # 0.22.0 (2023-04-24) 326 | 327 | 328 | ### Bug Fixes 329 | 330 | * actually pipe request to busboy ([de941a0](https://github.com/tim-smart/effect-http/commit/de941a00b2c6d41185619d2bd0ca1f1b19c0976d)) 331 | * add baseUrl option to node runtime ([1b2eb17](https://github.com/tim-smart/effect-http/commit/1b2eb1774831bfcc3bbb53c0b0de6cedb5f0720d)) 332 | * bad imports ([1b693b4](https://github.com/tim-smart/effect-http/commit/1b693b477fb1d35dac3721be06a0748cc0d2adbb)) 333 | * better url extraction ([a189e6b](https://github.com/tim-smart/effect-http/commit/a189e6b303e05dc753e39e8308b9efb19351b2a6)) 334 | * catch EarlyResponse ([74969cf](https://github.com/tim-smart/effect-http/commit/74969cfa9f194850ae25d63227d7dafef5163298)) 335 | * catchTag extraction ([bab5c5a](https://github.com/tim-smart/effect-http/commit/bab5c5a130d6a69e73cb346ac414fe30f1f1c2a6)) 336 | * **client:** encode typing ([a578ab7](https://github.com/tim-smart/effect-http/commit/a578ab7b8a59b17c52c20f1f2fa9ed6a4e418059)) 337 | * **client:** exports ([469f095](https://github.com/tim-smart/effect-http/commit/469f095ce9517189649991219654fdd16046664e)) 338 | * **client:** schema from checks ([8a5770c](https://github.com/tim-smart/effect-http/commit/8a5770c72d7dffa8c493990b286751dab7a9edb1)) 339 | * handle failures ([bf85b53](https://github.com/tim-smart/effect-http/commit/bf85b532239aaa389fa63f1f6706510ffd789257)) 340 | * HttpClientError symbol ([5516530](https://github.com/tim-smart/effect-http/commit/551653069edd2cbdd4f42395f631df5ddf97e7ce)) 341 | * make MultipartOptions partial ([8f101c3](https://github.com/tim-smart/effect-http/commit/8f101c31e0614b06e4b9f58c3b0e428422b71489)) 342 | * missing import ([6763564](https://github.com/tim-smart/effect-http/commit/6763564fbdeceaf42f17abb1419a4f34095ac08f)) 343 | * **node:** client ParseOptions ([d7dd159](https://github.com/tim-smart/effect-http/commit/d7dd1593b2e89bb93c27275833ba6917a5894308)) 344 | * **node:** client schema ([472040b](https://github.com/tim-smart/effect-http/commit/472040b6d444e7e82a06a52abf106d1f6363a8b3)) 345 | * prevent path escalation in serveDirectory ([e78abc0](https://github.com/tim-smart/effect-http/commit/e78abc01983660a8c1da83720501d4b3aa1c7b74)) 346 | * put tsplus static on Ops ([c87c880](https://github.com/tim-smart/effect-http/commit/c87c880d8bb07187ecaab62dea5d6d5de643f3d5)) 347 | * return types for request combinators ([d8ea0bf](https://github.com/tim-smart/effect-http/commit/d8ea0bfeb0b056617450d5820b67916b4c34d0c0)) 348 | * set empty status code to 204 ([bd84f71](https://github.com/tim-smart/effect-http/commit/bd84f71e9d7281ab702ce9f69d6f3559cc86aae1)) 349 | * tsplus for fetch() ([fe0036f](https://github.com/tim-smart/effect-http/commit/fe0036f69e378c22d26d4a2fe176717651d82e14)) 350 | * use pipeable context add ([9ddd8c1](https://github.com/tim-smart/effect-http/commit/9ddd8c12cf41a0186abb881c7208b7e9d9f46516)) 351 | * workspace deps ([1d44981](https://github.com/tim-smart/effect-http/commit/1d44981d7b6124418fd2ab2d3e36e6523616f630)) 352 | 353 | 354 | ### Features 355 | 356 | * add cause functions to HttpApp ([dd3f7c7](https://github.com/tim-smart/effect-http/commit/dd3f7c787bc6691b1f5d60a79c301451b0134c11)) 357 | * add convenience accessors for request ([f9bc2db](https://github.com/tim-smart/effect-http/commit/f9bc2db066c62d215203cbe9c6deb09356e4904d)) 358 | * add empty response constructor ([be21256](https://github.com/tim-smart/effect-http/commit/be212564d88578f09a2bd8d6dade81fcc25ff038)) 359 | * add FileResponse ([f70fec6](https://github.com/tim-smart/effect-http/commit/f70fec65bc4e43670641209d1239deecb700cd63)) 360 | * add formData to HttpRequest ([e0b9a1b](https://github.com/tim-smart/effect-http/commit/e0b9a1bc31466803e560e7904126b9337ab9236e)) 361 | * add FormDataResponse ([c58eded](https://github.com/tim-smart/effect-http/commit/c58eded1938552f7f9e57d18913b0e5f87e7052a)) 362 | * add HttpRequestExecutor service ([524ffc1](https://github.com/tim-smart/effect-http/commit/524ffc1938174d8cf1de098c4157ded9e55c4f9a)) 363 | * add more request body helpers ([1ac952c](https://github.com/tim-smart/effect-http/commit/1ac952c02bdafa05844e5fd18b0583083cc61309)) 364 | * add node runtime ([0bf8d9e](https://github.com/tim-smart/effect-http/commit/0bf8d9ec73c7471b895b11491d9ce8b41a0efe20)) 365 | * add some helpful fetch executors ([4059526](https://github.com/tim-smart/effect-http/commit/40595266737f45643009b6fd61789992942c1861)) 366 | * allow custom env and error in executors ([96d2b37](https://github.com/tim-smart/effect-http/commit/96d2b3766c5bdf01cf535e33888a22ae3e7725c7)) 367 | * better body apis ([4dbad4e](https://github.com/tim-smart/effect-http/commit/4dbad4e945ad443ff01f331551e8d7c3e52b14e0)) 368 | * better streaming support ([8f1c625](https://github.com/tim-smart/effect-http/commit/8f1c625e5c613f544887dd0f64ef812e8899f8b7)) 369 | * **client:** executor filterStatusOk ([739f033](https://github.com/tim-smart/effect-http/commit/739f0332743ee41d6f528a0daf5368907cf6a3b3)) 370 | * **client:** retry on RequestExecutor ([b71be8c](https://github.com/tim-smart/effect-http/commit/b71be8cda4cf40b679ac8cedb4b8b28d8dfe21a3)) 371 | * **client:** setParams(s) ([6f1d48c](https://github.com/tim-smart/effect-http/commit/6f1d48c9ce72309a4cfae92dab30852f8ce234fe)) 372 | * convert application/json parts to fields ([d79f80e](https://github.com/tim-smart/effect-http/commit/d79f80ec5f79e03e187e4dcd9b32f0e1508aa7d2)) 373 | * decodeFormData ([2925676](https://github.com/tim-smart/effect-http/commit/2925676e12043cd7a44e6c10ee2e6958ab61995e)) 374 | * executor combinators ([63b28d9](https://github.com/tim-smart/effect-http/commit/63b28d9d577b503deb42f88ead20fbae69258fcb)) 375 | * Executor contramap ([5e3b5e2](https://github.com/tim-smart/effect-http/commit/5e3b5e2af186846f6559ec3df52a9b38df5c1e87)) 376 | * FormDataResponse in node runtime ([9315a25](https://github.com/tim-smart/effect-http/commit/9315a25a1199d4556c68fedd56219f954d0c0c42)) 377 | * html response method ([4b7eb1a](https://github.com/tim-smart/effect-http/commit/4b7eb1abdd0cd4493b4fd59478b5e3b3ca6173e6)) 378 | * make request lazy ([328e20d](https://github.com/tim-smart/effect-http/commit/328e20dafef29964abd713c07d69712d9bc69c82)) 379 | * mount api ([64c76d7](https://github.com/tim-smart/effect-http/commit/64c76d7995938fdce92ef58971d9b509dd510163)) 380 | * move some client exports to root level ([84613fc](https://github.com/tim-smart/effect-http/commit/84613fc61ff3df2e7e15ad811e1b81ded3e5063f)) 381 | * **node:** add client executor ([9318776](https://github.com/tim-smart/effect-http/commit/93187762b1c75cf7d15ad282f37b8c76655a4127)) 382 | * **node:** FileResponse ([cd7f61d](https://github.com/tim-smart/effect-http/commit/cd7f61d10cf58cbd8e19266be4e0db2c7658bb9b)) 383 | * **node:** LiveNodeRequestExecutor* ([9af2dfa](https://github.com/tim-smart/effect-http/commit/9af2dfab5af1e3d6ec814cfecdf12bdcbf2822e9)) 384 | * **node:** make server creation lazy ([1121e54](https://github.com/tim-smart/effect-http/commit/1121e54c5810acc4cbb82409c8f8533cd267d55c)) 385 | * sandbox uploads per request ([776af26](https://github.com/tim-smart/effect-http/commit/776af266522c12f68beeabf6ff3e31b3876ebd64)) 386 | * schema & body parsing ([d338f0e](https://github.com/tim-smart/effect-http/commit/d338f0e92032a3cf3b148bcde6f292d2830a4696)) 387 | * start streaming multipart for node runtime ([b23856a](https://github.com/tim-smart/effect-http/commit/b23856a625615fdc2afb1995095e47aa103f21a5)) 388 | * streamBody Request ([30d4dd3](https://github.com/tim-smart/effect-http/commit/30d4dd3fb0b494418ce6ddfd93c13d489647f7d4)) 389 | * unsafeRunSyncOrPromise ([3b4fa7c](https://github.com/tim-smart/effect-http/commit/3b4fa7cd7cc1ba0d007524e15deace5edb0add9d)) 390 | * webStream ([0ca0411](https://github.com/tim-smart/effect-http/commit/0ca04117c9b7fdaa2c6155902994ae18ca56ab14)) 391 | * withSchema ([7397cdc](https://github.com/tim-smart/effect-http/commit/7397cdcc0ed4cb1de3d7c30b0ef5fc6a96c0e712)) 392 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # effect-http 2 | 3 | A runtime agnostic http library for effect-ts 4 | 5 | ## node.js example 6 | 7 | ```ts 8 | import * as Http from "@effect-http/core" 9 | import * as HttpNode from "@effect-http/node" 10 | import { Effect, pipe } from "effect" 11 | import { createServer } from "node:http" 12 | 13 | const app = pipe( 14 | Http.router 15 | .route("GET", "/", Effect.succeed(Http.response.text("Hello world!"))) 16 | .toHttpApp(), 17 | 18 | Http.httpApp.catchTag("RouteNotFound", () => 19 | Effect.succeed(response.text("Not found", { status: 404 })), 20 | ), 21 | ) 22 | 23 | pipe( 24 | app, 25 | HttpNode.serve(() => createServer(), { port: 3000 }), 26 | Effect.runFork, 27 | ) 28 | ``` 29 | 30 | ## bun example 31 | 32 | ```ts 33 | import * as Http from "@effect-http/core" 34 | import * as HttpBun from "@effect-http/bun" 35 | import { Effect, pipe } from "effect" 36 | 37 | const app = pipe( 38 | Http.router 39 | .route("GET", "/", Effect.succeed(Http.response.text("Hello world!"))) 40 | .toHttpApp(), 41 | 42 | Http.httpApp.catchTag("RouteNotFound", () => 43 | Effect.succeed(Http.response.text("Not found", { status: 404 })), 44 | ), 45 | ) 46 | 47 | pipe(app, HttpBun.serve({ port: 3000 }), Effect.runFork) 48 | ``` 49 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "0.40.1", 7 | "npmClient": "pnpm", 8 | "command": { 9 | "version": { 10 | "allowBranch": "main", 11 | "conventionalCommits": true, 12 | "createRelease": "github" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effect-http", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "lerna": "^7.1.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/bun/.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/bun/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/bun/.changeset/flat-dancers-notice.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@effect-http/bun": patch 3 | --- 4 | 5 | update deps 6 | -------------------------------------------------------------------------------- /packages/bun/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/bun/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.40.1](https://github.com/tim-smart/effect-http/compare/v0.40.0...v0.40.1) (2023-08-04) 7 | 8 | **Note:** Version bump only for package @effect-http/bun 9 | 10 | 11 | 12 | 13 | 14 | # [0.40.0](https://github.com/tim-smart/effect-http/compare/v0.39.0...v0.40.0) (2023-08-04) 15 | 16 | **Note:** Version bump only for package @effect-http/bun 17 | 18 | 19 | 20 | 21 | 22 | # [0.39.0](https://github.com/tim-smart/effect-http/compare/v0.38.0...v0.39.0) (2023-07-31) 23 | 24 | **Note:** Version bump only for package @effect-http/bun 25 | 26 | 27 | 28 | 29 | 30 | # [0.38.0](https://github.com/tim-smart/effect-http/compare/v0.37.0...v0.38.0) (2023-07-27) 31 | 32 | **Note:** Version bump only for package @effect-http/bun 33 | 34 | 35 | 36 | 37 | 38 | # [0.37.0](https://github.com/tim-smart/effect-http/compare/v0.36.0...v0.37.0) (2023-07-25) 39 | 40 | **Note:** Version bump only for package @effect-http/bun 41 | 42 | 43 | 44 | 45 | 46 | # [0.36.0](https://github.com/tim-smart/effect-http/compare/v0.35.0...v0.36.0) (2023-07-25) 47 | 48 | **Note:** Version bump only for package @effect-http/bun 49 | 50 | 51 | 52 | 53 | 54 | # [0.35.0](https://github.com/tim-smart/effect-http/compare/v0.34.0...v0.35.0) (2023-07-20) 55 | 56 | **Note:** Version bump only for package @effect-http/bun 57 | 58 | 59 | 60 | 61 | 62 | # [0.34.0](https://github.com/tim-smart/effect-http/compare/v0.33.0...v0.34.0) (2023-07-19) 63 | 64 | **Note:** Version bump only for package @effect-http/bun 65 | 66 | 67 | 68 | 69 | 70 | # [0.33.0](https://github.com/tim-smart/effect-http/compare/v0.32.0...v0.33.0) (2023-07-18) 71 | 72 | **Note:** Version bump only for package @effect-http/bun 73 | 74 | 75 | 76 | 77 | 78 | # [0.32.0](https://github.com/tim-smart/effect-http/compare/v0.31.1...v0.32.0) (2023-07-17) 79 | 80 | **Note:** Version bump only for package @effect-http/bun 81 | 82 | 83 | 84 | 85 | 86 | ## [0.31.1](https://github.com/tim-smart/effect-http/compare/v0.31.0...v0.31.1) (2023-07-12) 87 | 88 | **Note:** Version bump only for package @effect-http/bun 89 | 90 | 91 | 92 | 93 | 94 | # [0.31.0](https://github.com/tim-smart/effect-http/compare/v0.30.0...v0.31.0) (2023-07-12) 95 | 96 | **Note:** Version bump only for package @effect-http/bun 97 | 98 | 99 | 100 | 101 | 102 | # [0.30.0](https://github.com/tim-smart/effect-http/compare/v0.29.1...v0.30.0) (2023-07-11) 103 | 104 | **Note:** Version bump only for package @effect-http/bun 105 | 106 | 107 | 108 | 109 | 110 | ## [0.29.1](https://github.com/tim-smart/effect-http/compare/v0.29.0...v0.29.1) (2023-07-10) 111 | 112 | **Note:** Version bump only for package @effect-http/bun 113 | 114 | 115 | 116 | 117 | 118 | # [0.29.0](https://github.com/tim-smart/effect-http/compare/v0.28.0...v0.29.0) (2023-07-08) 119 | 120 | **Note:** Version bump only for package @effect-http/bun 121 | 122 | 123 | 124 | 125 | 126 | # [0.28.0](https://github.com/tim-smart/effect-http/compare/v0.27.5...v0.28.0) (2023-06-23) 127 | 128 | **Note:** Version bump only for package @effect-http/bun 129 | 130 | 131 | 132 | 133 | 134 | ## [0.27.3](https://github.com/tim-smart/effect-http/compare/v0.27.2...v0.27.3) (2023-06-22) 135 | 136 | **Note:** Version bump only for package @effect-http/bun 137 | 138 | 139 | 140 | 141 | 142 | ## [0.27.2](https://github.com/tim-smart/effect-http/compare/v0.27.1...v0.27.2) (2023-06-18) 143 | 144 | **Note:** Version bump only for package @effect-http/bun 145 | 146 | 147 | 148 | 149 | 150 | ## [0.27.1](https://github.com/tim-smart/effect-http/compare/v0.27.0...v0.27.1) (2023-06-18) 151 | 152 | **Note:** Version bump only for package @effect-http/bun 153 | 154 | 155 | 156 | 157 | 158 | # [0.27.0](https://github.com/tim-smart/effect-http/compare/v0.26.1...v0.27.0) (2023-06-02) 159 | 160 | **Note:** Version bump only for package @effect-http/bun 161 | 162 | 163 | 164 | 165 | 166 | ## [0.26.1](https://github.com/tim-smart/effect-http/compare/v0.26.0...v0.26.1) (2023-05-22) 167 | 168 | **Note:** Version bump only for package @effect-http/bun 169 | 170 | 171 | 172 | 173 | 174 | # [0.26.0](https://github.com/tim-smart/effect-http/compare/v0.25.2...v0.26.0) (2023-05-15) 175 | 176 | **Note:** Version bump only for package @effect-http/bun 177 | 178 | 179 | 180 | 181 | 182 | ## [0.25.2](https://github.com/tim-smart/effect-http/compare/v0.25.1...v0.25.2) (2023-05-15) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * catchTag ([c591756](https://github.com/tim-smart/effect-http/commit/c5917564ad2f7c72546c65e1a91a6d3a95bb3be9)) 188 | 189 | 190 | 191 | 192 | 193 | ## [0.25.1](https://github.com/tim-smart/effect-http/compare/v0.25.0...v0.25.1) (2023-05-12) 194 | 195 | 196 | ### Bug Fixes 197 | 198 | * bun runPromise ([7e15baf](https://github.com/tim-smart/effect-http/commit/7e15baf87221e504b8c604f0b58463da1548a2fc)) 199 | 200 | 201 | 202 | 203 | 204 | # [0.25.0](https://github.com/tim-smart/effect-http/compare/v0.24.2...v0.25.0) (2023-05-10) 205 | 206 | **Note:** Version bump only for package @effect-http/bun 207 | 208 | 209 | 210 | 211 | 212 | # [0.24.0](https://github.com/tim-smart/effect-http/compare/v0.23.0...v0.24.0) (2023-04-30) 213 | 214 | **Note:** Version bump only for package @effect-http/bun 215 | 216 | 217 | 218 | 219 | 220 | # [0.23.0](https://github.com/tim-smart/effect-http/compare/v0.22.4...v0.23.0) (2023-04-30) 221 | 222 | **Note:** Version bump only for package @effect-http/bun 223 | 224 | 225 | 226 | 227 | 228 | # 0.22.0 (2023-04-24) 229 | 230 | 231 | ### Bug Fixes 232 | 233 | * catch EarlyResponse ([74969cf](https://github.com/tim-smart/effect-http/commit/74969cfa9f194850ae25d63227d7dafef5163298)) 234 | * workspace deps ([1d44981](https://github.com/tim-smart/effect-http/commit/1d44981d7b6124418fd2ab2d3e36e6523616f630)) 235 | 236 | 237 | ### Features 238 | 239 | * unsafeRunSyncOrPromise ([3b4fa7c](https://github.com/tim-smart/effect-http/commit/3b4fa7cd7cc1ba0d007524e15deace5edb0add9d)) 240 | 241 | 242 | 243 | 244 | 245 | # [0.20.0](https://github.com/tim-smart/effect-http/compare/@effect-http/bun@0.19.0...@effect-http/bun@0.20.0) (2023-04-18) 246 | 247 | **Note:** Version bump only for package @effect-http/bun 248 | 249 | 250 | 251 | 252 | 253 | # [0.19.0](https://github.com/tim-smart/effect-http/compare/@effect-http/bun@0.3.1...@effect-http/bun@0.19.0) (2023-04-11) 254 | 255 | 256 | ### Bug Fixes 257 | 258 | * workspace deps ([1d44981](https://github.com/tim-smart/effect-http/commit/1d44981d7b6124418fd2ab2d3e36e6523616f630)) 259 | 260 | 261 | 262 | 263 | 264 | # @effect-http/bun 265 | 266 | ## 0.3.1 267 | 268 | ### Patch Changes 269 | 270 | - update deps 271 | -------------------------------------------------------------------------------- /packages/bun/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tsc 2 | tsc: clean 3 | ./node_modules/.bin/tsc 4 | sed 's#/dist/#/#' package.json > dist/package.json 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf dist tsconfig.tsbuildinfo 9 | 10 | -------------------------------------------------------------------------------- /packages/bun/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@effect-http/bun", 3 | "version": "0.40.1", 4 | "publishConfig": { 5 | "access": "public", 6 | "directory": "dist" 7 | }, 8 | "exports": { 9 | ".": "./dist/index.js", 10 | "./*": "./dist/*.js" 11 | }, 12 | "scripts": { 13 | "clean": "make clean", 14 | "postinstall": "tsplus-install || true", 15 | "prepublishOnly": "make tsc", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "@effect-http/core": "workspace:*", 23 | "@effect/data": "^0.17.1", 24 | "@effect/io": "^0.38.0", 25 | "@tsplus-types/effect__data": "0.17.1-11739c0", 26 | "@tsplus-types/effect__io": "0.38.0-11739c0", 27 | "@tsplus/installer": "^0.0.178", 28 | "bun-types": "^0.7.2", 29 | "prettier": "^3.0.1", 30 | "typescript": "5.1.6" 31 | }, 32 | "sideEffects": false, 33 | "gitHead": "f285647e065f4b904e4d2165df54dd797a9c51fc", 34 | "peerDependencies": { 35 | "@effect-http/core": "workspace:*", 36 | "@effect/data": "^0.17.1", 37 | "@effect/io": "^0.38.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/bun/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { HttpApp } from "@effect-http/core" 2 | import type { HttpRequest } from "@effect-http/core/Request" 3 | import { EarlyResponse, HttpResponse } from "@effect-http/core/Response" 4 | import { 5 | HttpFs, 6 | HttpFsError, 7 | HttpFsNotFound, 8 | } from "@effect-http/core/internal/HttpFs" 9 | import type { Effect } from "@effect/io/Effect" 10 | import type { Layer } from "@effect/io/Layer" 11 | import { GenericServeOptions, SystemError } from "bun" 12 | import * as Fs from "node:fs" 13 | 14 | /** 15 | * @tsplus pipeable effect-http/HttpApp serve 16 | */ 17 | export const serve = 18 | (options: Exclude = {}) => 19 | ( 20 | httpApp: HttpApp, 21 | ): Effect, never, void> => 22 | Effect.runtime() 23 | .flatMap(rt => { 24 | const run = rt.runPromise 25 | return Effect.async(() => { 26 | const server = Bun.serve({ 27 | ...options, 28 | fetch(request) { 29 | return run( 30 | httpApp( 31 | HttpRequest.fromStandard( 32 | request, 33 | request.method, 34 | request.url, 35 | ), 36 | ) 37 | .catchTag("EarlyResponse", e => Effect.succeed(e.response)) 38 | .map(HttpResponse.toStandard), 39 | ) 40 | }, 41 | }) 42 | 43 | return Effect(() => { 44 | server.stop() 45 | }) 46 | }) 47 | }) 48 | .provideService(HttpFs, bunHttpFsImpl) 49 | 50 | const bunHttpFsImpl: HttpFs = { 51 | toResponse(path, { status, contentType, range }) { 52 | return Effect.async(resume => { 53 | Fs.stat(path, (err, stats) => { 54 | if (err) { 55 | resume(Effect.fail(err)) 56 | } else { 57 | resume(Effect.succeed(stats)) 58 | } 59 | }) 60 | }) 61 | .map(() => { 62 | let file = Bun.file(path, { 63 | type: contentType, 64 | }) 65 | 66 | if (range) { 67 | file = file.slice(range[0], range[1]) 68 | } 69 | 70 | return HttpResponse.raw(file, { status }) 71 | }) 72 | .mapError(_ => 73 | _.code === "ENOENT" ? new HttpFsNotFound(path, _) : new HttpFsError(_), 74 | ) 75 | }, 76 | } 77 | 78 | export const BunHttpFsLive = Layer.succeed(HttpFs, bunHttpFsImpl) 79 | -------------------------------------------------------------------------------- /packages/bun/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Node16", 10 | "module": "Node16", 11 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 12 | "sourceMap": true, 13 | "declaration": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "target": "ES2022", 17 | "incremental": true, 18 | "types": ["bun-types"], 19 | "tsPlusConfig": "./tsplus.config.json", 20 | "paths": { 21 | "@effect-http/bun": ["./src/index.js"], 22 | "@effect-http/bun/*": ["./src/*.js"] 23 | }, 24 | "tsBuildInfoFile": "./tsconfig.tsbuildinfo" 25 | }, 26 | "include": ["src/**/*"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/bun/tsplus.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": { 3 | "^(.*)/src/(.*)\\.(ts|js)$": "@effect-http/bun/$2" 4 | }, 5 | "traceMap": { 6 | "^(.*)/src/(.*)\\.(ts|js)$": "(@effect-http/bun) $2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/client/.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/client/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/client/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.40.0](https://github.com/tim-smart/effect-http/compare/v0.39.0...v0.40.0) (2023-08-04) 7 | 8 | **Note:** Version bump only for package @effect-http/client 9 | 10 | 11 | 12 | 13 | 14 | # [0.39.0](https://github.com/tim-smart/effect-http/compare/v0.38.0...v0.39.0) (2023-07-31) 15 | 16 | **Note:** Version bump only for package @effect-http/client 17 | 18 | 19 | 20 | 21 | 22 | # [0.38.0](https://github.com/tim-smart/effect-http/compare/v0.37.0...v0.38.0) (2023-07-27) 23 | 24 | **Note:** Version bump only for package @effect-http/client 25 | 26 | 27 | 28 | 29 | 30 | # [0.37.0](https://github.com/tim-smart/effect-http/compare/v0.36.0...v0.37.0) (2023-07-25) 31 | 32 | **Note:** Version bump only for package @effect-http/client 33 | 34 | 35 | 36 | 37 | 38 | # [0.36.0](https://github.com/tim-smart/effect-http/compare/v0.35.0...v0.36.0) (2023-07-25) 39 | 40 | **Note:** Version bump only for package @effect-http/client 41 | 42 | 43 | 44 | 45 | 46 | # [0.35.0](https://github.com/tim-smart/effect-http/compare/v0.34.0...v0.35.0) (2023-07-20) 47 | 48 | **Note:** Version bump only for package @effect-http/client 49 | 50 | 51 | 52 | 53 | 54 | # [0.34.0](https://github.com/tim-smart/effect-http/compare/v0.33.0...v0.34.0) (2023-07-19) 55 | 56 | **Note:** Version bump only for package @effect-http/client 57 | 58 | 59 | 60 | 61 | 62 | # [0.33.0](https://github.com/tim-smart/effect-http/compare/v0.32.0...v0.33.0) (2023-07-18) 63 | 64 | **Note:** Version bump only for package @effect-http/client 65 | 66 | 67 | 68 | 69 | 70 | # [0.32.0](https://github.com/tim-smart/effect-http/compare/v0.31.1...v0.32.0) (2023-07-17) 71 | 72 | **Note:** Version bump only for package @effect-http/client 73 | 74 | 75 | 76 | 77 | 78 | # [0.31.0](https://github.com/tim-smart/effect-http/compare/v0.30.0...v0.31.0) (2023-07-12) 79 | 80 | **Note:** Version bump only for package @effect-http/client 81 | 82 | 83 | 84 | 85 | 86 | # [0.30.0](https://github.com/tim-smart/effect-http/compare/v0.29.1...v0.30.0) (2023-07-11) 87 | 88 | **Note:** Version bump only for package @effect-http/client 89 | 90 | 91 | 92 | 93 | 94 | ## [0.29.1](https://github.com/tim-smart/effect-http/compare/v0.29.0...v0.29.1) (2023-07-10) 95 | 96 | **Note:** Version bump only for package @effect-http/client 97 | 98 | 99 | 100 | 101 | 102 | # [0.29.0](https://github.com/tim-smart/effect-http/compare/v0.28.0...v0.29.0) (2023-07-08) 103 | 104 | **Note:** Version bump only for package @effect-http/client 105 | 106 | 107 | 108 | 109 | 110 | # [0.28.0](https://github.com/tim-smart/effect-http/compare/v0.27.5...v0.28.0) (2023-06-23) 111 | 112 | **Note:** Version bump only for package @effect-http/client 113 | 114 | 115 | 116 | 117 | 118 | ## [0.27.3](https://github.com/tim-smart/effect-http/compare/v0.27.2...v0.27.3) (2023-06-22) 119 | 120 | **Note:** Version bump only for package @effect-http/client 121 | 122 | 123 | 124 | 125 | 126 | ## [0.27.2](https://github.com/tim-smart/effect-http/compare/v0.27.1...v0.27.2) (2023-06-18) 127 | 128 | **Note:** Version bump only for package @effect-http/client 129 | 130 | 131 | 132 | 133 | 134 | ## [0.27.1](https://github.com/tim-smart/effect-http/compare/v0.27.0...v0.27.1) (2023-06-18) 135 | 136 | **Note:** Version bump only for package @effect-http/client 137 | 138 | 139 | 140 | 141 | 142 | # [0.27.0](https://github.com/tim-smart/effect-http/compare/v0.26.1...v0.27.0) (2023-06-02) 143 | 144 | **Note:** Version bump only for package @effect-http/client 145 | 146 | 147 | 148 | 149 | 150 | ## [0.26.1](https://github.com/tim-smart/effect-http/compare/v0.26.0...v0.26.1) (2023-05-22) 151 | 152 | **Note:** Version bump only for package @effect-http/client 153 | 154 | 155 | 156 | 157 | 158 | # [0.26.0](https://github.com/tim-smart/effect-http/compare/v0.25.2...v0.26.0) (2023-05-15) 159 | 160 | 161 | ### Features 162 | 163 | * tap to client Executor ([664b1cc](https://github.com/tim-smart/effect-http/commit/664b1cc6a7c3b676b8748eae430fedef3c97e7c8)) 164 | 165 | 166 | 167 | 168 | 169 | ## [0.25.2](https://github.com/tim-smart/effect-http/compare/v0.25.1...v0.25.2) (2023-05-15) 170 | 171 | 172 | ### Bug Fixes 173 | 174 | * catchTag ([c591756](https://github.com/tim-smart/effect-http/commit/c5917564ad2f7c72546c65e1a91a6d3a95bb3be9)) 175 | 176 | 177 | 178 | 179 | 180 | ## [0.25.1](https://github.com/tim-smart/effect-http/compare/v0.25.0...v0.25.1) (2023-05-12) 181 | 182 | **Note:** Version bump only for package @effect-http/client 183 | 184 | 185 | 186 | 187 | 188 | # [0.25.0](https://github.com/tim-smart/effect-http/compare/v0.24.2...v0.25.0) (2023-05-10) 189 | 190 | **Note:** Version bump only for package @effect-http/client 191 | 192 | 193 | 194 | 195 | 196 | ## [0.24.2](https://github.com/tim-smart/effect-http/compare/v0.24.1...v0.24.2) (2023-05-01) 197 | 198 | **Note:** Version bump only for package @effect-http/client 199 | 200 | 201 | 202 | 203 | 204 | ## [0.24.1](https://github.com/tim-smart/effect-http/compare/v0.24.0...v0.24.1) (2023-04-30) 205 | 206 | 207 | ### Features 208 | 209 | * appendUrl ([229ad75](https://github.com/tim-smart/effect-http/commit/229ad75d9f9154521121bc71182965196868a5b1)) 210 | 211 | 212 | 213 | 214 | 215 | # [0.24.0](https://github.com/tim-smart/effect-http/compare/v0.23.0...v0.24.0) (2023-04-30) 216 | 217 | 218 | ### Features 219 | 220 | * Request modify ([d1301cf](https://github.com/tim-smart/effect-http/commit/d1301cf505b1357178a2ba0e731f8c14788e04bc)) 221 | 222 | 223 | 224 | 225 | 226 | # [0.23.0](https://github.com/tim-smart/effect-http/compare/v0.22.4...v0.23.0) (2023-04-30) 227 | 228 | 229 | ### Features 230 | 231 | * setMethod & setUrl ([7057a67](https://github.com/tim-smart/effect-http/commit/7057a67ceb5cf85baf4856fd99cd60070eeb8505)) 232 | 233 | 234 | 235 | 236 | 237 | ## [0.22.4](https://github.com/tim-smart/effect-http/compare/v0.22.3...v0.22.4) (2023-04-27) 238 | 239 | 240 | ### Bug Fixes 241 | 242 | * retry return type ([29fc101](https://github.com/tim-smart/effect-http/commit/29fc1011baf754c82076471432e44ca66463a628)) 243 | 244 | 245 | 246 | 247 | 248 | ## [0.22.3](https://github.com/tim-smart/effect-http/compare/v0.22.2...v0.22.3) (2023-04-27) 249 | 250 | 251 | ### Bug Fixes 252 | 253 | * realign retry signature with Effect.retry ([7005552](https://github.com/tim-smart/effect-http/commit/7005552fdba3662d059c2dc5e4cef6f259d5d033)) 254 | 255 | 256 | 257 | 258 | 259 | ## [0.22.2](https://github.com/tim-smart/effect-http/compare/v0.22.1...v0.22.2) (2023-04-26) 260 | 261 | **Note:** Version bump only for package @effect-http/client 262 | 263 | 264 | 265 | 266 | 267 | ## [0.22.1](https://github.com/tim-smart/effect-http/compare/v0.22.0...v0.22.1) (2023-04-26) 268 | 269 | 270 | ### Features 271 | 272 | * basicAuth for Request ([0074df1](https://github.com/tim-smart/effect-http/commit/0074df170b545d90a91e4cd207b94eea0f537da3)) 273 | 274 | 275 | 276 | 277 | 278 | # 0.22.0 (2023-04-24) 279 | 280 | 281 | ### Bug Fixes 282 | 283 | * catchTag extraction ([bab5c5a](https://github.com/tim-smart/effect-http/commit/bab5c5a130d6a69e73cb346ac414fe30f1f1c2a6)) 284 | * **client:** encode typing ([a578ab7](https://github.com/tim-smart/effect-http/commit/a578ab7b8a59b17c52c20f1f2fa9ed6a4e418059)) 285 | * **client:** exports ([469f095](https://github.com/tim-smart/effect-http/commit/469f095ce9517189649991219654fdd16046664e)) 286 | * **client:** schema from checks ([8a5770c](https://github.com/tim-smart/effect-http/commit/8a5770c72d7dffa8c493990b286751dab7a9edb1)) 287 | * HttpClientError symbol ([5516530](https://github.com/tim-smart/effect-http/commit/551653069edd2cbdd4f42395f631df5ddf97e7ce)) 288 | * put tsplus static on Ops ([c87c880](https://github.com/tim-smart/effect-http/commit/c87c880d8bb07187ecaab62dea5d6d5de643f3d5)) 289 | * return types for request combinators ([d8ea0bf](https://github.com/tim-smart/effect-http/commit/d8ea0bfeb0b056617450d5820b67916b4c34d0c0)) 290 | * tsplus for fetch() ([fe0036f](https://github.com/tim-smart/effect-http/commit/fe0036f69e378c22d26d4a2fe176717651d82e14)) 291 | 292 | 293 | ### Features 294 | 295 | * add HttpRequestExecutor service ([524ffc1](https://github.com/tim-smart/effect-http/commit/524ffc1938174d8cf1de098c4157ded9e55c4f9a)) 296 | * add more request body helpers ([1ac952c](https://github.com/tim-smart/effect-http/commit/1ac952c02bdafa05844e5fd18b0583083cc61309)) 297 | * add some helpful fetch executors ([4059526](https://github.com/tim-smart/effect-http/commit/40595266737f45643009b6fd61789992942c1861)) 298 | * allow custom env and error in executors ([96d2b37](https://github.com/tim-smart/effect-http/commit/96d2b3766c5bdf01cf535e33888a22ae3e7725c7)) 299 | * better body apis ([4dbad4e](https://github.com/tim-smart/effect-http/commit/4dbad4e945ad443ff01f331551e8d7c3e52b14e0)) 300 | * **client:** executor filterStatusOk ([739f033](https://github.com/tim-smart/effect-http/commit/739f0332743ee41d6f528a0daf5368907cf6a3b3)) 301 | * **client:** retry on RequestExecutor ([b71be8c](https://github.com/tim-smart/effect-http/commit/b71be8cda4cf40b679ac8cedb4b8b28d8dfe21a3)) 302 | * **client:** setParams(s) ([6f1d48c](https://github.com/tim-smart/effect-http/commit/6f1d48c9ce72309a4cfae92dab30852f8ce234fe)) 303 | * executor combinators ([63b28d9](https://github.com/tim-smart/effect-http/commit/63b28d9d577b503deb42f88ead20fbae69258fcb)) 304 | * Executor contramap ([5e3b5e2](https://github.com/tim-smart/effect-http/commit/5e3b5e2af186846f6559ec3df52a9b38df5c1e87)) 305 | * move some client exports to root level ([84613fc](https://github.com/tim-smart/effect-http/commit/84613fc61ff3df2e7e15ad811e1b81ded3e5063f)) 306 | * streamBody Request ([30d4dd3](https://github.com/tim-smart/effect-http/commit/30d4dd3fb0b494418ce6ddfd93c13d489647f7d4)) 307 | * withSchema ([7397cdc](https://github.com/tim-smart/effect-http/commit/7397cdcc0ed4cb1de3d7c30b0ef5fc6a96c0e712)) 308 | 309 | 310 | 311 | 312 | 313 | # [0.18.0](https://github.com/tim-smart/effect-http/compare/@effect-http/client@0.17.0...@effect-http/client@0.18.0) (2023-04-18) 314 | 315 | **Note:** Version bump only for package @effect-http/client 316 | 317 | 318 | 319 | 320 | 321 | # 0.17.0 (2023-04-11) 322 | 323 | 324 | ### Bug Fixes 325 | 326 | * catchTag extraction ([bab5c5a](https://github.com/tim-smart/effect-http/commit/bab5c5a130d6a69e73cb346ac414fe30f1f1c2a6)) 327 | * **client:** encode typing ([a578ab7](https://github.com/tim-smart/effect-http/commit/a578ab7b8a59b17c52c20f1f2fa9ed6a4e418059)) 328 | * **client:** exports ([469f095](https://github.com/tim-smart/effect-http/commit/469f095ce9517189649991219654fdd16046664e)) 329 | * **client:** schema from checks ([8a5770c](https://github.com/tim-smart/effect-http/commit/8a5770c72d7dffa8c493990b286751dab7a9edb1)) 330 | * HttpClientError symbol ([5516530](https://github.com/tim-smart/effect-http/commit/551653069edd2cbdd4f42395f631df5ddf97e7ce)) 331 | * put tsplus static on Ops ([c87c880](https://github.com/tim-smart/effect-http/commit/c87c880d8bb07187ecaab62dea5d6d5de643f3d5)) 332 | * return types for request combinators ([d8ea0bf](https://github.com/tim-smart/effect-http/commit/d8ea0bfeb0b056617450d5820b67916b4c34d0c0)) 333 | * tsplus for fetch() ([fe0036f](https://github.com/tim-smart/effect-http/commit/fe0036f69e378c22d26d4a2fe176717651d82e14)) 334 | 335 | 336 | ### Features 337 | 338 | * add HttpRequestExecutor service ([524ffc1](https://github.com/tim-smart/effect-http/commit/524ffc1938174d8cf1de098c4157ded9e55c4f9a)) 339 | * add more request body helpers ([1ac952c](https://github.com/tim-smart/effect-http/commit/1ac952c02bdafa05844e5fd18b0583083cc61309)) 340 | * add some helpful fetch executors ([4059526](https://github.com/tim-smart/effect-http/commit/40595266737f45643009b6fd61789992942c1861)) 341 | * allow custom env and error in executors ([96d2b37](https://github.com/tim-smart/effect-http/commit/96d2b3766c5bdf01cf535e33888a22ae3e7725c7)) 342 | * better body apis ([4dbad4e](https://github.com/tim-smart/effect-http/commit/4dbad4e945ad443ff01f331551e8d7c3e52b14e0)) 343 | * **client:** executor filterStatusOk ([739f033](https://github.com/tim-smart/effect-http/commit/739f0332743ee41d6f528a0daf5368907cf6a3b3)) 344 | * **client:** retry on RequestExecutor ([b71be8c](https://github.com/tim-smart/effect-http/commit/b71be8cda4cf40b679ac8cedb4b8b28d8dfe21a3)) 345 | * **client:** setParams(s) ([6f1d48c](https://github.com/tim-smart/effect-http/commit/6f1d48c9ce72309a4cfae92dab30852f8ce234fe)) 346 | * executor combinators ([63b28d9](https://github.com/tim-smart/effect-http/commit/63b28d9d577b503deb42f88ead20fbae69258fcb)) 347 | * Executor contramap ([5e3b5e2](https://github.com/tim-smart/effect-http/commit/5e3b5e2af186846f6559ec3df52a9b38df5c1e87)) 348 | * move some client exports to root level ([84613fc](https://github.com/tim-smart/effect-http/commit/84613fc61ff3df2e7e15ad811e1b81ded3e5063f)) 349 | * streamBody Request ([30d4dd3](https://github.com/tim-smart/effect-http/commit/30d4dd3fb0b494418ce6ddfd93c13d489647f7d4)) 350 | * withSchema ([7397cdc](https://github.com/tim-smart/effect-http/commit/7397cdcc0ed4cb1de3d7c30b0ef5fc6a96c0e712)) 351 | -------------------------------------------------------------------------------- /packages/client/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tsc 2 | tsc: clean 3 | ./node_modules/.bin/tsc 4 | sed 's#/dist/#/#' package.json > dist/package.json 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf dist tsconfig.tsbuildinfo 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # @effect-http/client 2 | 3 | An implementation agnostic http client for Effect-TS. 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import * as Http from "@effect-http/client" 9 | import * as Effect from "@effect/io/Effect" 10 | import { pipe } from "@fp-ts/core/Function" 11 | 12 | pipe( 13 | Http.get("https://google.com", { params: { q: "hello" } }), 14 | Http.fetch(), // <- uses `fetch` to execute the request 15 | Effect.flatMap(response => response.text), 16 | Effect.tap(text => 17 | Effect.sync(() => { 18 | console.log(text) 19 | }), 20 | ), 21 | Effect.runPromise, 22 | ) 23 | ``` 24 | 25 | Example using fp-ts/schema: 26 | 27 | ```ts 28 | import * as S from "@fp-ts/schema" 29 | import * as Http from "@effect-http/client" 30 | import * as Effect from "@effect/io/Effect" 31 | import { pipe } from "@fp-ts/core/Function" 32 | 33 | const User_ = S.struct({ 34 | id: S.number, 35 | name: S.string, 36 | age: S.number, 37 | }) 38 | 39 | export interface User extends S.Infer {} 40 | export const User: S.Schema = User_ 41 | 42 | const baseUrlExecutor = pipe( 43 | Http.fetch(), 44 | Http.executor.contramap(Http.updateUrl(_ => `https://example.com/api${_}`)), 45 | ) 46 | 47 | const userResponseExecutor = pipe( 48 | baseUrlExecutor, 49 | Http.executor.mapEffect(_ => _.decode(User)), 50 | ) 51 | 52 | export const createUser = pipe( 53 | Http.post("/users"), 54 | Http.withSchema(User, userResponseExecutor), 55 | ) 56 | 57 | export const updateUser = (user: User) => 58 | pipe( 59 | Http.patch(`/users/${user.id}`), 60 | Http.withSchema(User, userResponseExecutor), 61 | )(user) 62 | ``` 63 | 64 | Here is an example using tsplus: 65 | 66 | ```ts 67 | import * as Http from "@effect-http/client" 68 | import type { HttpClientError } from "@effect-http/client" 69 | import * as S from "@fp-ts/schema" 70 | 71 | const Post_ = S.struct({ 72 | id: S.number, 73 | title: S.string, 74 | body: S.string, 75 | userId: S.number, 76 | }) 77 | interface Post extends S.Infer {} 78 | const Post: S.Schema = Post_ 79 | 80 | const Posts = S.array(Post) 81 | const CreatePost = pipe(Post, S.omit("id")) 82 | 83 | /** 84 | * Here is a jsonplaceholder request executor, which adds a base url and an 85 | * Accept header. 86 | */ 87 | const jsonplaceholder = Http.fetch().contramap( 88 | _ => _.updateUrl(_ => `https://jsonplaceholder.typicode.com${_}`).acceptJson, 89 | ) 90 | 91 | /** 92 | * We further refine the jsonplaceholder request executor by adding a Post 93 | * decoder. 94 | * 95 | * @tsplus getter effect-http/client/Request fetchPost 96 | */ 97 | export const jsonplaceholderPost = jsonplaceholder.mapEffect(_ => 98 | _.decode(Post), 99 | ) 100 | 101 | /** 102 | * @tsplus getter effect-http/client/Request fetchPosts 103 | */ 104 | export const jsonplaceholderPosts = jsonplaceholder.mapEffect(_ => 105 | _.decode(Posts), 106 | ) 107 | 108 | /** 109 | * We can now use the jsonplaceholderPost executor to send a POST request that 110 | * creates a new post. 111 | */ 112 | export const createPost: (post: { 113 | readonly title: string 114 | readonly body: string 115 | readonly userId: number 116 | }) => Effect = Http.post("/posts").withSchema( 117 | CreatePost, 118 | jsonplaceholderPost, 119 | ) 120 | 121 | /** 122 | * Here we use the fetchPosts tsplus getter to create a listPosts effect. 123 | */ 124 | export const listPosts: Effect = 125 | Http.get("/posts").fetchPosts 126 | 127 | /** 128 | * Here we use the fetchPost tsplus getter. 129 | */ 130 | export const getPost = (id: number) => Http.get(`/posts/${id}`).fetchPost 131 | ``` 132 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@effect-http/client", 3 | "version": "0.40.0", 4 | "publishConfig": { 5 | "access": "public", 6 | "directory": "dist" 7 | }, 8 | "exports": { 9 | ".": "./dist/index.js", 10 | "./*": "./dist/*.js" 11 | }, 12 | "scripts": { 13 | "clean": "make clean", 14 | "postinstall": "tsplus-install || true", 15 | "prepublishOnly": "make tsc", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@effect/data": "^0.17.1", 23 | "@effect/io": "^0.38.0", 24 | "@effect/schema": "^0.33.0", 25 | "@effect/stream": "^0.34.0", 26 | "@tsplus-types/effect__data": "0.17.1-11739c0", 27 | "@tsplus-types/effect__io": "0.38.0-11739c0", 28 | "@tsplus-types/effect__schema": "0.32.0-11739c0", 29 | "@tsplus-types/effect__stream": "0.33.0-11739c0", 30 | "@tsplus/installer": "^0.0.178", 31 | "prettier": "^3.0.1", 32 | "typescript": "5.1.6" 33 | }, 34 | "sideEffects": false, 35 | "gitHead": "f285647e065f4b904e4d2165df54dd797a9c51fc", 36 | "peerDependencies": { 37 | "@effect/data": "^0.17.1", 38 | "@effect/io": "^0.38.0", 39 | "@effect/schema": "^0.33.0", 40 | "@effect/stream": "^0.34.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/client/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@effect/io': ~0.0.64 5 | '@fp-ts/data': ~0.0.39 6 | '@fp-ts/schema': ^0.0.8 7 | '@tsplus-types/effect__io': 0.0.64-44b6108 8 | '@tsplus-types/fp-ts__data': 0.0.39-44b6108 9 | '@tsplus-types/fp-ts__schema': 0.0.8-44b6108 10 | '@types/route-parser': ^0.1.4 11 | find-my-way: ^7.4.0 12 | prettier: ^2.8.2 13 | typescript: https://cdn.jsdelivr.net/npm/@tsplus/installer@0.0.164/compiler/typescript.tgz 14 | 15 | dependencies: 16 | '@effect/io': 0.0.64 17 | '@fp-ts/data': 0.0.39 18 | '@fp-ts/schema': 0.0.8 19 | '@types/route-parser': 0.1.4 20 | find-my-way: 7.4.0 21 | 22 | devDependencies: 23 | '@tsplus-types/effect__io': 0.0.64-44b6108_@effect+io@0.0.64 24 | '@tsplus-types/fp-ts__data': 0.0.39-44b6108_@fp-ts+data@0.0.39 25 | '@tsplus-types/fp-ts__schema': 0.0.8-44b6108_@fp-ts+schema@0.0.8 26 | prettier: 2.8.2 27 | typescript: '@cdn.jsdelivr.net/npm/@tsplus/installer@0.0.164/compiler/typescript.tgz' 28 | 29 | packages: 30 | 31 | /@effect/io/0.0.64: 32 | resolution: {integrity: sha512-E3dQSlHVY2D3m7tUOmDxmPmBaQJ3mFl3YRndgm+Pl0Zog8DVlWH0lEuZyUEa8XiKlAHqIMq6c0VN82C2xuyNOQ==} 33 | dependencies: 34 | '@fp-ts/core': 0.0.11 35 | '@fp-ts/data': 0.0.39 36 | 37 | /@fp-ts/core/0.0.11: 38 | resolution: {integrity: sha512-BCAJBYzghwoJpcUOARJ1tui50HoYJFlV2pJlVMlsEkDFhD8MTtq8xQVpZCRF66RmtkxtGBYINCQ+5H1lRaL35Q==} 39 | 40 | /@fp-ts/data/0.0.39: 41 | resolution: {integrity: sha512-dNw1LWjQmChmdenrDuxPct+ycucgKQQboVtWqov8rkAGY3DULznoSDJZam6l2kaLdq1ScPv810N4mzLLT+cBBw==} 42 | dependencies: 43 | '@fp-ts/core': 0.0.11 44 | 45 | /@fp-ts/schema/0.0.8: 46 | resolution: {integrity: sha512-+osZGEnfVO4jaoBnjMRmZT/yoYhu+DoN3eCn0HjWMnDF6ZDUuWahtSTp4HpBwd2lL0+FkulAywvM8WRpy4lZiw==} 47 | dependencies: 48 | '@fp-ts/core': 0.0.11 49 | '@fp-ts/data': 0.0.39 50 | fast-check: 3.5.0 51 | 52 | /@tsplus-types/effect__io/0.0.64-44b6108_@effect+io@0.0.64: 53 | resolution: {integrity: sha512-wG0g/7eiZpR3MVOG/ETAbaikiFXYgfeHH5HVGBm79/08VNvKWlBJHGGpxBxFa+au35nYPq0Yl1/JYTTin8vuvw==} 54 | peerDependencies: 55 | '@effect/io': 0.0.64 56 | dependencies: 57 | '@effect/io': 0.0.64 58 | dev: true 59 | 60 | /@tsplus-types/fp-ts__data/0.0.39-44b6108_@fp-ts+data@0.0.39: 61 | resolution: {integrity: sha512-nw6e9oje1eWUANSFcdCmzQSOdBoG/lFeS4JaZqrQbdzAMAddlF2l/T9rhbDo8nkixbB7fIYmLZ6J1RG1flqmvg==} 62 | peerDependencies: 63 | '@fp-ts/data': 0.0.39 64 | dependencies: 65 | '@fp-ts/data': 0.0.39 66 | dev: true 67 | 68 | /@tsplus-types/fp-ts__schema/0.0.8-44b6108_@fp-ts+schema@0.0.8: 69 | resolution: {integrity: sha512-MOL1gpXGUnefNTdWnXSwU7mdyX/1Qo8U4xR72HR5lNBBHubElmhxmKvD/9/lpL8+ch8slhqXkbLefKZeHrYD9w==} 70 | peerDependencies: 71 | '@fp-ts/schema': 0.0.8 72 | dependencies: 73 | '@fp-ts/schema': 0.0.8 74 | dev: true 75 | 76 | /@types/route-parser/0.1.4: 77 | resolution: {integrity: sha512-lwH3SeyKwCAwP7oUoJNryPDdbW3Bx5lrB6mhV5iebqzOJHIut6wlaSxpQR4Lsk6j7wC08pGenr/xE8I/A4J3Fg==} 78 | dev: false 79 | 80 | /fast-check/3.5.0: 81 | resolution: {integrity: sha512-5Yk9kfPy1DR8+9nl+wgRbXGM+p7e25nDRuZ/4b3TjIaWu53p/KkwS4uyKFc+Ek2Ft5nF40KRCbPYGrWYp0SjPw==} 82 | engines: {node: '>=8.0.0'} 83 | dependencies: 84 | pure-rand: 5.0.5 85 | 86 | /fast-decode-uri-component/1.0.1: 87 | resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} 88 | dev: false 89 | 90 | /fast-deep-equal/3.1.3: 91 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 92 | dev: false 93 | 94 | /fast-querystring/1.1.0: 95 | resolution: {integrity: sha512-LWkjBCZlxjnSanuPpZ6mHswjy8hQv3VcPJsQB3ltUF2zjvrycr0leP3TSTEEfvQ1WEMSRl5YNsGqaft9bjLqEw==} 96 | dependencies: 97 | fast-decode-uri-component: 1.0.1 98 | dev: false 99 | 100 | /find-my-way/7.4.0: 101 | resolution: {integrity: sha512-JFT7eURLU5FumlZ3VBGnveId82cZz7UR7OUu+THQJOwdQXxmS/g8v0KLoFhv97HreycOrmAbqjXD/4VG2j0uMQ==} 102 | engines: {node: '>=14'} 103 | dependencies: 104 | fast-deep-equal: 3.1.3 105 | fast-querystring: 1.1.0 106 | safe-regex2: 2.0.0 107 | dev: false 108 | 109 | /prettier/2.8.2: 110 | resolution: {integrity: sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==} 111 | engines: {node: '>=10.13.0'} 112 | hasBin: true 113 | dev: true 114 | 115 | /pure-rand/5.0.5: 116 | resolution: {integrity: sha512-BwQpbqxSCBJVpamI6ydzcKqyFmnd5msMWUGvzXLm1aXvusbbgkbOto/EUPM00hjveJEaJtdbhUjKSzWRhQVkaw==} 117 | 118 | /ret/0.2.2: 119 | resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} 120 | engines: {node: '>=4'} 121 | dev: false 122 | 123 | /safe-regex2/2.0.0: 124 | resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} 125 | dependencies: 126 | ret: 0.2.2 127 | dev: false 128 | 129 | '@cdn.jsdelivr.net/npm/@tsplus/installer@0.0.164/compiler/typescript.tgz': 130 | resolution: {tarball: https://cdn.jsdelivr.net/npm/@tsplus/installer@0.0.164/compiler/typescript.tgz} 131 | name: typescript 132 | version: 5.0.0-tsplus.20230113 133 | engines: {node: '>=4.2.0'} 134 | hasBin: true 135 | dev: true 136 | -------------------------------------------------------------------------------- /packages/client/src/Error.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "./Request.js" 2 | import { Response } from "./Response.js" 3 | 4 | export const HttpClientErrorTypeId = Symbol.for( 5 | "@effect-http/client/HttpClientError", 6 | ) 7 | export type HttpClientErrorTypeId = typeof HttpClientErrorTypeId 8 | 9 | export type HttpClientError = 10 | | RequestError 11 | | StatusCodeError 12 | | ResponseDecodeError 13 | | SchemaDecodeError 14 | | SchemaEncodeError 15 | 16 | export abstract class BaseHttpError { 17 | readonly [HttpClientErrorTypeId]: (_: HttpClientErrorTypeId) => unknown = 18 | identity 19 | } 20 | 21 | export class RequestError extends BaseHttpError { 22 | readonly _tag = "RequestError" 23 | constructor(readonly request: Request, readonly error: unknown) { 24 | super() 25 | } 26 | } 27 | 28 | export class StatusCodeError extends BaseHttpError { 29 | readonly _tag = "StatusCodeError" 30 | readonly status: number 31 | constructor(readonly response: Response) { 32 | super() 33 | this.status = response.status 34 | } 35 | } 36 | 37 | export class ResponseDecodeError extends BaseHttpError { 38 | readonly _tag = "ResponseDecodeError" 39 | constructor( 40 | readonly error: unknown, 41 | readonly source: Response, 42 | readonly kind: 43 | | "json" 44 | | "text" 45 | | "blob" 46 | | "arrayBuffer" 47 | | "formData" 48 | | "stream", 49 | ) { 50 | super() 51 | } 52 | } 53 | 54 | export class SchemaEncodeError extends BaseHttpError { 55 | readonly _tag = "SchemaEncodeError" 56 | constructor(readonly error: ParseError, readonly request: Request) { 57 | super() 58 | } 59 | } 60 | 61 | export class SchemaDecodeError extends BaseHttpError { 62 | readonly _tag = "SchemaDecodeError" 63 | constructor(readonly error: ParseError, readonly response: Response) { 64 | super() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/client/src/Request.ts: -------------------------------------------------------------------------------- 1 | import { ParseOptions } from "@effect/schema/AST" 2 | import { SchemaEncodeError } from "./Error.js" 3 | import type { RequestBody } from "./Request/Body.js" 4 | import * as body from "./Request/Body.js" 5 | import { RequestExecutor } from "./Request/Executor.js" 6 | import { Schema } from "@effect/schema/Schema" 7 | import { dual } from "@effect/data/Function" 8 | 9 | export type HttpMethod = 10 | | "GET" 11 | | "POST" 12 | | "PUT" 13 | | "DELETE" 14 | | "PATCH" 15 | | "HEAD" 16 | | "OPTIONS" 17 | 18 | /** 19 | * @tsplus type effect-http/client/Request 20 | * @tsplus companion effect-http/client/Request.Ops 21 | */ 22 | export interface Request { 23 | readonly _tag: "Request" 24 | readonly url: string 25 | readonly urlParams: Chunk<[string, string]> 26 | readonly method: HttpMethod 27 | readonly headers: HashMap 28 | readonly body: Maybe 29 | } 30 | 31 | export const empty: Request = { 32 | _tag: "Request", 33 | method: "GET", 34 | url: "", 35 | urlParams: Chunk.empty(), 36 | headers: HashMap.empty(), 37 | body: Maybe.none(), 38 | } 39 | 40 | export type MakeOptions = Omit 41 | 42 | /** 43 | * @tsplus static effect-http/client/Request.Ops make 44 | */ 45 | export const make = 46 | (method: HttpMethod) => 47 | (url: string, options: Partial = {}): Request => 48 | modify(empty, { ...options, method, url }) 49 | 50 | export interface ModifyOptions { 51 | readonly method: HttpMethod 52 | readonly url: string 53 | readonly params: Record 54 | readonly headers: Record 55 | readonly body: RequestBody 56 | readonly accept: string 57 | readonly acceptJson: boolean 58 | } 59 | 60 | /** 61 | * @tsplus fluent effect-http/client/Request modify 62 | */ 63 | export const modify: { 64 | (options: Partial): (self: Request) => Request 65 | (self: Request, options: Partial): Request 66 | } = dual(2, (self: Request, options: ModifyOptions) => { 67 | if (options.method) { 68 | self = setMethod(options.method)(self) 69 | } 70 | 71 | if (options.url) { 72 | self = setUrl(options.url)(self) 73 | } 74 | 75 | if (options.body) { 76 | self = setBody(options.body)(self) 77 | } 78 | 79 | if (options.acceptJson) { 80 | self = acceptJson(self) 81 | } 82 | 83 | if (options.accept) { 84 | self = accept(options.accept)(self) 85 | } 86 | 87 | if (options.headers) { 88 | self = setHeaders(options.headers)(self) 89 | } 90 | 91 | if (options.params) { 92 | self = appendParams(options.params)(self) 93 | } 94 | 95 | if (options.body) { 96 | self = setBody(options.body)(self) 97 | } 98 | 99 | return self 100 | }) 101 | 102 | /** 103 | * @tsplus static effect-http/client/Request.Ops get 104 | */ 105 | export const get = make("GET") 106 | 107 | /** 108 | * @tsplus static effect-http/client/Request.Ops post 109 | */ 110 | export const post = make("POST") 111 | 112 | /** 113 | * @tsplus static effect-http/client/Request.Ops put 114 | */ 115 | export const put = make("PUT") 116 | 117 | /** 118 | * @tsplus static effect-http/client/Request.Ops del 119 | */ 120 | export const del = make("DELETE") 121 | 122 | /** 123 | * @tsplus static effect-http/client/Request.Ops patch 124 | */ 125 | export const patch = make("PATCH") 126 | 127 | /** 128 | * @tsplus static effect-http/client/Request.Ops head 129 | */ 130 | export const head = make("HEAD") 131 | 132 | /** 133 | * @tsplus static effect-http/client/Request.Ops options 134 | */ 135 | export const options = make("OPTIONS") 136 | 137 | /** 138 | * @tsplus fluent effect-http/client/Request setHeader 139 | */ 140 | export const setHeader: { 141 | (name: string, value: string): (self: Request) => Request 142 | (self: Request, name: string, value: string): Request 143 | } = dual(3, (self: Request, name: string, value: string) => ({ 144 | ...self, 145 | headers: self.headers.set(name.toLowerCase(), value), 146 | })) 147 | 148 | /** 149 | * @tsplus fluent effect-http/client/Request basicAuth 150 | */ 151 | export const basicAuth: { 152 | (username: string, password: string): (self: Request) => Request 153 | (self: Request, username: string, password: string): Request 154 | } = dual(3, (self: Request, username: string, password: string) => 155 | setHeader(self, "Authorization", `Basic ${btoa(`${username}:${password}`)}`), 156 | ) 157 | 158 | /** 159 | * @tsplus fluent effect-http/client/Request setHeaders 160 | */ 161 | export const setHeaders: { 162 | (headers: Record): (self: Request) => Request 163 | (self: Request, headers: Record): Request 164 | } = dual(2, (self: Request, headers: Record) => 165 | Object.entries(headers).reduce( 166 | (acc, [key, value]) => setHeader(acc, key, value), 167 | self, 168 | ), 169 | ) 170 | 171 | /** 172 | * @tsplus fluent effect-http/client/Request setMethod 173 | */ 174 | export const setMethod: { 175 | (method: HttpMethod): (self: Request) => Request 176 | (self: Request, method: HttpMethod): Request 177 | } = dual(2, (self: Request, method: HttpMethod) => ({ ...self, method })) 178 | 179 | /** 180 | * @tsplus fluent effect-http/client/Request setUrl 181 | */ 182 | export const setUrl: { 183 | (url: string): (self: Request) => Request 184 | (self: Request, url: string): Request 185 | } = dual(2, (self: Request, url: string) => ({ ...self, url })) 186 | 187 | /** 188 | * @tsplus fluent effect-http/client/Request appendUrl 189 | */ 190 | export const appendUrl: { 191 | (path: string): (self: Request) => Request 192 | (self: Request, path: string): Request 193 | } = dual(2, (self: Request, path: string) => ({ 194 | ...self, 195 | url: `${self.url}${path}`, 196 | })) 197 | 198 | /** 199 | * @tsplus fluent effect-http/client/Request updateUrl 200 | */ 201 | export const updateUrl: { 202 | (f: (url: string) => string): (self: Request) => Request 203 | (self: Request, f: (url: string) => string): Request 204 | } = dual(2, (self: Request, f: (url: string) => string) => ({ 205 | ...self, 206 | url: f(self.url), 207 | })) 208 | 209 | /** 210 | * @tsplus fluent effect-http/client/Request accept 211 | */ 212 | export const accept: { 213 | (value: string): (self: Request) => Request 214 | (self: Request, value: string): Request 215 | } = dual(2, (self: Request, value: string) => setHeader(self, "Accept", value)) 216 | 217 | /** 218 | * @tsplus getter effect-http/client/Request acceptJson 219 | */ 220 | export const acceptJson = accept("application/json") 221 | 222 | /** 223 | * @tsplus fluent effect-http/client/Request appendParam 224 | */ 225 | export const appendParam: { 226 | (name: string, value: any): (self: Request) => Request 227 | (self: Request, name: string, value: any): Request 228 | } = dual(3, (self: Request, name: string, value: any) => { 229 | if (Array.isArray(value)) { 230 | return { 231 | ...self, 232 | urlParams: self.urlParams.appendAll( 233 | Chunk.fromIterable(value.map(_ => [name, _])), 234 | ), 235 | } 236 | } else if (typeof value === "string") { 237 | return { 238 | ...self, 239 | urlParams: self.urlParams.append([name, value]), 240 | } 241 | } 242 | 243 | return { 244 | ...self, 245 | urlParams: self.urlParams.append([name, JSON.stringify(value)]), 246 | } 247 | }) 248 | 249 | /** 250 | * @tsplus fluent effect-http/client/Request appendParams 251 | */ 252 | export const appendParams: { 253 | (params: Record): (self: Request) => Request 254 | (self: Request, params: Record): Request 255 | } = dual(2, (self: Request, params: Record) => 256 | Object.entries(params).reduce( 257 | (acc, [key, value]) => appendParam(key, value)(acc), 258 | self, 259 | ), 260 | ) 261 | 262 | /** 263 | * @tsplus fluent effect-http/client/Request setParam 264 | */ 265 | export const setParam: { 266 | (name: string, value: any): (self: Request) => Request 267 | (self: Request, name: string, value: any): Request 268 | } = dual(3, (self: Request, name: string, value: any) => 269 | appendParam( 270 | { 271 | ...self, 272 | urlParams: self.urlParams.filter(([key]) => key !== name), 273 | }, 274 | name, 275 | value, 276 | ), 277 | ) 278 | 279 | /** 280 | * @tsplus fluent effect-http/client/Request setParams 281 | */ 282 | export const setParams: { 283 | (params: Record): (self: Request) => Request 284 | (self: Request, params: Record): Request 285 | } = dual(2, (self: Request, params: Record) => 286 | Object.entries(params).reduce( 287 | (acc, [key, value]) => setParam(acc, key, value), 288 | self, 289 | ), 290 | ) 291 | 292 | /** 293 | * @tsplus fluent effect-http/client/Request setBody 294 | */ 295 | export const setBody: { 296 | (body: RequestBody): (self: Request) => Request 297 | (self: Request, body: RequestBody): Request 298 | } = dual(2, (self: Request, body: RequestBody) => { 299 | let request: Request = { 300 | ...self, 301 | body: Maybe.some(body), 302 | } 303 | 304 | if (body._tag === "FormDataBody") { 305 | return request 306 | } 307 | 308 | request = body.contentType.match({ 309 | onNone: () => request, 310 | onSome: contentType => setHeader("content-type", contentType)(request), 311 | }) 312 | 313 | request = body.contentLength.match({ 314 | onNone: () => request, 315 | onSome: contentLength => 316 | setHeader("content-length", contentLength.toString())(request), 317 | }) 318 | 319 | return request 320 | }) 321 | 322 | /** 323 | * @tsplus fluent effect-http/client/Request textBody 324 | */ 325 | export const textBody: { 326 | (value: string, contentType: string): (self: Request) => Request 327 | (self: Request, value: string, contentType: string): Request 328 | } = dual(3, (self: Request, value: string, contentType: string) => 329 | self.setBody(body.text(value, contentType)), 330 | ) 331 | 332 | /** 333 | * @tsplus fluent effect-http/client/Request jsonBody 334 | */ 335 | export const jsonBody: { 336 | (value: unknown): (self: Request) => Request 337 | (self: Request, value: unknown): Request 338 | } = dual( 339 | 2, 340 | (self: Request, value: unknown) => self.setBody(body.json(value)).acceptJson, 341 | ) 342 | 343 | /** 344 | * @tsplus fluent effect-http/client/Request searchParamsBody 345 | */ 346 | export const searchParamsBody: { 347 | (value: URLSearchParams): (self: Request) => Request 348 | (self: Request, value: URLSearchParams): Request 349 | } = dual(2, (self: Request, value: URLSearchParams) => 350 | self.setBody(body.searchParams(value)), 351 | ) 352 | 353 | /** 354 | * @tsplus fluent effect-http/client/Request formDataBody 355 | */ 356 | export const formDataBody: { 357 | (value: FormData): (self: Request) => Request 358 | (self: Request, value: FormData): Request 359 | } = dual(2, (self: Request, value: FormData) => 360 | self.setBody(body.formData(value)), 361 | ) 362 | 363 | /** 364 | * @tsplus pipeable effect-http/client/Request streamBody 365 | */ 366 | export const streamBody = 367 | ( 368 | value: Stream, 369 | { 370 | contentType, 371 | contentLength, 372 | }: { contentType?: string; contentLength?: number } = {}, 373 | ) => 374 | (self: Request): Request => 375 | self.setBody(body.stream(value, contentType, contentLength)) 376 | 377 | /** 378 | * @tsplus pipeable effect-http/client/Request withSchema 379 | */ 380 | export const withSchema = ( 381 | schema: Schema, 382 | run: RequestExecutor, 383 | options?: ParseOptions, 384 | ) => { 385 | const encode = schema.encode 386 | 387 | return (self: Request) => 388 | (input: O): Effect => 389 | encode(input, options) 390 | .mapError(_ => new SchemaEncodeError(_, self)) 391 | .flatMap(_ => run(self.jsonBody(_))) 392 | } 393 | -------------------------------------------------------------------------------- /packages/client/src/Request/Body.ts: -------------------------------------------------------------------------------- 1 | export type RequestBody = RawBody | FormDataBody | StreamBody 2 | 3 | export class RawBody { 4 | readonly _tag = "RawBody" 5 | constructor( 6 | readonly contentType: Maybe, 7 | readonly contentLength: Maybe, 8 | readonly value: unknown, 9 | ) {} 10 | } 11 | 12 | export class FormDataBody { 13 | readonly _tag = "FormDataBody" 14 | constructor(readonly value: FormData) {} 15 | } 16 | 17 | export class StreamBody { 18 | readonly _tag = "StreamBody" 19 | constructor( 20 | readonly contentType: Maybe, 21 | readonly contentLength: Maybe, 22 | readonly value: Stream, 23 | ) {} 24 | } 25 | 26 | const rawFromString = (contentType: string, value: string): RawBody => { 27 | const body = new TextEncoder().encode(value) 28 | return new RawBody(Maybe.some(contentType), Maybe.some(body.length), body) 29 | } 30 | 31 | export const text = (value: string, contentType = "text/plain"): RequestBody => 32 | rawFromString(contentType, value) 33 | 34 | export const json = (value: unknown): RequestBody => 35 | rawFromString("application/json", JSON.stringify(value)) 36 | 37 | export const searchParams = (value: URLSearchParams): RequestBody => 38 | rawFromString("application/x-www-form-urlencoded", value.toString()) 39 | 40 | export const formData = (value: FormData): RequestBody => 41 | new FormDataBody(value) 42 | 43 | export const stream = ( 44 | value: Stream, 45 | contentType = "application/octet-stream", 46 | contentLength?: number, 47 | ): RequestBody => 48 | new StreamBody( 49 | Maybe.some(contentType), 50 | Maybe.fromNullable(contentLength), 51 | value, 52 | ) 53 | -------------------------------------------------------------------------------- /packages/client/src/Request/Executor.ts: -------------------------------------------------------------------------------- 1 | import { Predicate } from "@effect/data/Predicate" 2 | import { RequestError, StatusCodeError } from "../Error.js" 3 | import { Request } from "../Request.js" 4 | import { Response } from "../Response.js" 5 | import { dual } from "@effect/data/Function" 6 | 7 | /** 8 | * Represents a function that can execute a request. 9 | * 10 | * It takes a `Request` and returns an Effect that returns the result. 11 | * 12 | * @tsplus type effect-http/client/RequestExecutor 13 | * @since 1.0.0 14 | */ 15 | export interface RequestExecutor { 16 | (request: Request): Effect 17 | } 18 | 19 | /** 20 | * Represents a service that can execute a request. 21 | * 22 | * Can be used for embedding a RequestExecutor into a Layer. 23 | * 24 | * @since 1.0.0 25 | */ 26 | export interface HttpRequestExecutor { 27 | readonly execute: RequestExecutor 28 | } 29 | 30 | /** 31 | * A tag for the HttpRequestExecutor service. 32 | * 33 | * @since 1.0.0 34 | */ 35 | export const HttpRequestExecutor = Tag() 36 | 37 | /** 38 | * @tsplus fluent effect-http/client/RequestExecutor contramap 39 | */ 40 | export const contramap: { 41 | (f: (a: Request) => Request): ( 42 | self: RequestExecutor, 43 | ) => RequestExecutor 44 | ( 45 | self: RequestExecutor, 46 | f: (a: Request) => Request, 47 | ): RequestExecutor 48 | } = dual( 49 | 2, 50 | ( 51 | self: RequestExecutor, 52 | f: (a: Request) => Request, 53 | ): RequestExecutor => 54 | request => 55 | self(f(request)), 56 | ) 57 | 58 | /** 59 | * @tsplus fluent effect-http/client/RequestExecutor contramapEffect 60 | */ 61 | export const contramapEffect: { 62 | (f: (a: Request) => Effect): ( 63 | self: RequestExecutor, 64 | ) => RequestExecutor 65 | ( 66 | self: RequestExecutor, 67 | f: (a: Request) => Effect, 68 | ): RequestExecutor 69 | } = dual( 70 | 2, 71 | ( 72 | self: RequestExecutor, 73 | f: (a: Request) => Effect, 74 | ): RequestExecutor => 75 | request => 76 | f(request).flatMap(self), 77 | ) 78 | 79 | /** 80 | * @tsplus fluent effect-http/client/RequestExecutor contratapEffect 81 | */ 82 | export const contratapEffect: { 83 | (f: (a: Request) => Effect): ( 84 | self: RequestExecutor, 85 | ) => RequestExecutor 86 | ( 87 | self: RequestExecutor, 88 | f: (a: Request) => Effect, 89 | ): RequestExecutor 90 | } = dual( 91 | 2, 92 | ( 93 | self: RequestExecutor, 94 | f: (a: Request) => Effect, 95 | ): RequestExecutor => 96 | request => 97 | f(request).zipRight(self(request)), 98 | ) 99 | 100 | /** 101 | * @tsplus fluent effect-http/client/RequestExecutor map 102 | */ 103 | export const map: { 104 | (f: (a: A) => B): ( 105 | self: RequestExecutor, 106 | ) => RequestExecutor 107 | (self: RequestExecutor, f: (a: A) => B): RequestExecutor< 108 | R, 109 | E, 110 | B 111 | > 112 | } = dual( 113 | 2, 114 | ( 115 | self: RequestExecutor, 116 | f: (a: A) => B, 117 | ): RequestExecutor => 118 | request => 119 | self(request).map(f), 120 | ) 121 | 122 | /** 123 | * @tsplus fluent effect-http/client/RequestExecutor mapEffect 124 | */ 125 | export const mapEffect: { 126 | (f: (a: A) => Effect): ( 127 | self: RequestExecutor, 128 | ) => RequestExecutor 129 | ( 130 | self: RequestExecutor, 131 | f: (a: A) => Effect, 132 | ): RequestExecutor 133 | } = dual( 134 | 2, 135 | ( 136 | self: RequestExecutor, 137 | f: (a: A) => Effect, 138 | ): RequestExecutor => 139 | request => 140 | self(request).flatMap(f), 141 | ) 142 | 143 | /** 144 | * @tsplus fluent effect-http/client/RequestExecutor tapEffect 145 | */ 146 | export const tapEffect: { 147 | (f: (a: A) => Effect): ( 148 | self: RequestExecutor, 149 | ) => RequestExecutor 150 | ( 151 | self: RequestExecutor, 152 | f: (a: A) => Effect, 153 | ): RequestExecutor 154 | } = dual( 155 | 2, 156 | ( 157 | self: RequestExecutor, 158 | f: (a: A) => Effect, 159 | ): RequestExecutor => 160 | request => 161 | self(request).tap(f), 162 | ) 163 | 164 | /** 165 | * @tsplus fluent effect-http/client/RequestExecutor filterStatus 166 | */ 167 | export const filterStatus: { 168 | (f: (status: number) => boolean): ( 169 | self: RequestExecutor, 170 | ) => RequestExecutor 171 | ( 172 | self: RequestExecutor, 173 | f: (status: number) => boolean, 174 | ): RequestExecutor 175 | } = dual( 176 | 2, 177 | ( 178 | self: RequestExecutor, 179 | f: (status: number) => boolean, 180 | ): RequestExecutor => 181 | request => 182 | self(request).filterOrFail( 183 | _ => f(_.status), 184 | _ => new StatusCodeError(_), 185 | ), 186 | ) 187 | 188 | /** 189 | * @tsplus getter effect-http/client/RequestExecutor filterStatusOk 190 | */ 191 | export const filterStatusOk = filterStatus(_ => _ >= 200 && _ < 300) 192 | 193 | /** 194 | * @tsplus fluent effect-http/client/RequestExecutor filterOrElse 195 | */ 196 | export const filterOrElse: { 197 | (f: Predicate, orElse: (a: A) => Effect): ( 198 | self: RequestExecutor, 199 | ) => RequestExecutor 200 | ( 201 | self: RequestExecutor, 202 | f: Predicate, 203 | orElse: (a: A) => Effect, 204 | ): RequestExecutor 205 | } = dual( 206 | 3, 207 | ( 208 | self: RequestExecutor, 209 | f: Predicate, 210 | orElse: (a: A) => Effect, 211 | ): RequestExecutor => 212 | request => 213 | self(request).filterOrElse(f, orElse), 214 | ) 215 | 216 | /** 217 | * @tsplus fluent effect-http/client/RequestExecutor retry 218 | */ 219 | export const retry: { 220 | (policy: Schedule): ( 221 | self: RequestExecutor, 222 | ) => RequestExecutor 223 | ( 224 | self: RequestExecutor, 225 | policy: Schedule, 226 | ): RequestExecutor 227 | } = dual( 228 | 2, 229 | ( 230 | self: RequestExecutor, 231 | policy: Schedule, 232 | ): RequestExecutor => 233 | request => 234 | self(request).retry(policy), 235 | ) 236 | 237 | /** 238 | * @tsplus fluent effect-http/client/RequestExecutor catchTag 239 | */ 240 | export const catchTag: { 241 | ( 242 | tag: K, 243 | f: (e: Extract) => Effect, 244 | ): ( 245 | self: RequestExecutor, 246 | ) => RequestExecutor, A1 | A> 247 | ( 248 | self: RequestExecutor, 249 | tag: K, 250 | f: (e: Extract) => Effect, 251 | ): RequestExecutor, A1 | A> 252 | } = dual( 253 | 3, 254 | ( 255 | self: RequestExecutor, 256 | tag: K, 257 | f: (e: Extract) => Effect, 258 | ): RequestExecutor, A1 | A> => 259 | request => 260 | self(request).catchTag(tag, f), 261 | ) 262 | 263 | /** 264 | * @tsplus fluent effect-http/client/RequestExecutor catchTags 265 | */ 266 | export const catchTags: { 267 | < 268 | E extends { _tag: string }, 269 | Cases extends { 270 | [K in E["_tag"]]+?: 271 | | ((error: Extract) => Effect) 272 | | undefined 273 | }, 274 | >( 275 | cases: Cases, 276 | ): ( 277 | self: RequestExecutor, 278 | ) => RequestExecutor< 279 | | R 280 | | { 281 | [K in keyof Cases]: Cases[K] extends ( 282 | ...args: Array 283 | ) => Effect 284 | ? R 285 | : never 286 | }[keyof Cases], 287 | | Exclude 288 | | { 289 | [K in keyof Cases]: Cases[K] extends ( 290 | ...args: Array 291 | ) => Effect 292 | ? E 293 | : never 294 | }[keyof Cases], 295 | | A 296 | | { 297 | [K in keyof Cases]: Cases[K] extends ( 298 | ...args: Array 299 | ) => Effect 300 | ? A 301 | : never 302 | }[keyof Cases] 303 | > 304 | < 305 | R, 306 | E extends { _tag: string }, 307 | A, 308 | Cases extends { 309 | [K in E["_tag"]]+?: 310 | | ((error: Extract) => Effect) 311 | | undefined 312 | }, 313 | >( 314 | self: RequestExecutor, 315 | cases: Cases, 316 | ): RequestExecutor< 317 | | R 318 | | { 319 | [K in keyof Cases]: Cases[K] extends ( 320 | ...args: Array 321 | ) => Effect 322 | ? R 323 | : never 324 | }[keyof Cases], 325 | | Exclude 326 | | { 327 | [K in keyof Cases]: Cases[K] extends ( 328 | ...args: Array 329 | ) => Effect 330 | ? E 331 | : never 332 | }[keyof Cases], 333 | | A 334 | | { 335 | [K in keyof Cases]: Cases[K] extends ( 336 | ...args: Array 337 | ) => Effect 338 | ? A 339 | : never 340 | }[keyof Cases] 341 | > 342 | } = dual( 343 | 2, 344 | < 345 | R, 346 | E extends { _tag: string }, 347 | A, 348 | Cases extends { 349 | [K in E["_tag"]]+?: 350 | | ((error: Extract) => Effect) 351 | | undefined 352 | }, 353 | >( 354 | self: RequestExecutor, 355 | cases: Cases, 356 | ): RequestExecutor< 357 | | R 358 | | { 359 | [K in keyof Cases]: Cases[K] extends ( 360 | ...args: Array 361 | ) => Effect 362 | ? R 363 | : never 364 | }[keyof Cases], 365 | | Exclude 366 | | { 367 | [K in keyof Cases]: Cases[K] extends ( 368 | ...args: Array 369 | ) => Effect 370 | ? E 371 | : never 372 | }[keyof Cases], 373 | | A 374 | | { 375 | [K in keyof Cases]: Cases[K] extends ( 376 | ...args: Array 377 | ) => Effect 378 | ? A 379 | : never 380 | }[keyof Cases] 381 | > => 382 | request => 383 | self(request).catchTags(cases), 384 | ) 385 | 386 | /** 387 | * @tsplus fluent effect-http/client/RequestExecutor catchAll 388 | */ 389 | export const catchAll: { 390 | (f: (e: E) => Effect): ( 391 | self: RequestExecutor, 392 | ) => RequestExecutor 393 | ( 394 | self: RequestExecutor, 395 | f: (e: E) => Effect, 396 | ): RequestExecutor 397 | } = dual( 398 | 2, 399 | 400 | ( 401 | self: RequestExecutor, 402 | f: (e: E) => Effect, 403 | ): RequestExecutor => 404 | request => 405 | self(request).catchAll(f), 406 | ) 407 | -------------------------------------------------------------------------------- /packages/client/src/Request/FetchExecutor.ts: -------------------------------------------------------------------------------- 1 | import type { ParseOptions } from "@effect/schema/AST" 2 | import { Schema } from "@effect/schema/Schema" 3 | import { 4 | RequestError, 5 | ResponseDecodeError, 6 | SchemaDecodeError, 7 | StatusCodeError, 8 | } from "../Error.js" 9 | import { Request } from "../Request.js" 10 | import * as response from "../Response.js" 11 | import { toReadableStream } from "../util/stream.js" 12 | import { RequestBody } from "./Body.js" 13 | import type { RequestExecutor } from "./Executor.js" 14 | import * as executor from "./Executor.js" 15 | import * as ReadonlyArray from "@effect/data/ReadonlyArray" 16 | import * as Effect from "@effect/io/Effect" 17 | 18 | /** 19 | * A request executor that uses the global fetch function. 20 | * 21 | * It performs no validation on the response status code. 22 | * 23 | * @since 1.0.0 24 | */ 25 | export const fetch = 26 | ( 27 | options: RequestInit = {}, 28 | ): RequestExecutor => 29 | request => 30 | Do($ => { 31 | const url = $( 32 | Effect.try({ 33 | try: () => new URL(request.url), 34 | catch: _ => new RequestError(request, _), 35 | }), 36 | ) 37 | 38 | ReadonlyArray.forEach(request.urlParams, ([key, value]) => { 39 | if (value === undefined) return 40 | url.searchParams.append(key, value) 41 | }) 42 | 43 | const headers = new Headers([...request.headers] as any) 44 | const body = request.body.map(convertBody).getOrUndefined 45 | 46 | return $( 47 | Effect.tryPromise({ 48 | try: signal => 49 | globalThis.fetch(url, { 50 | ...options, 51 | method: request.method, 52 | headers, 53 | body, 54 | signal, 55 | }), 56 | catch: _ => new RequestError(request, _), 57 | }).map(response.fromWeb), 58 | ) 59 | }) 60 | 61 | /** 62 | * A request executor that uses the global fetch function. 63 | * 64 | * It filters out responses with a status code outside the range 200-299. 65 | * 66 | * @since 1.0.0 67 | */ 68 | export const fetchOk = (options?: RequestInit) => fetch(options).filterStatusOk 69 | 70 | /** 71 | * @since 1.0.0 72 | * @tsplus pipeable effect-http/client/Request fetch 73 | */ 74 | export const fetch_: ( 75 | options?: RequestInit, 76 | ) => ( 77 | request: Request, 78 | ) => Effect.Effect = 79 | fetchOk 80 | /** 81 | * A request executor that uses the global fetch function. 82 | * 83 | * It sets the Accept header to "application/json" and decodes the response 84 | * body. 85 | * 86 | * @since 1.0.0 87 | */ 88 | export const fetchJson = (options?: RequestInit) => 89 | fetchOk(options) 90 | .contramap(_ => _.acceptJson) 91 | .mapEffect(_ => _.json) 92 | 93 | /** 94 | * @tsplus pipeable effect-http/client/Request fetchJson 95 | */ 96 | export const fetchJson_: ( 97 | options?: RequestInit, 98 | ) => ( 99 | request: Request, 100 | ) => Effect.Effect< 101 | never, 102 | RequestError | StatusCodeError | ResponseDecodeError, 103 | unknown 104 | > = fetchJson 105 | 106 | /** 107 | * A request executor that uses the global fetch function. 108 | * 109 | * It decodes the response body using the given schema. 110 | * 111 | * @since 1.0.0 112 | */ 113 | export const fetchDecode = ( 114 | schema: Schema, 115 | options?: ParseOptions, 116 | requestInit?: RequestInit, 117 | ): RequestExecutor< 118 | never, 119 | RequestError | StatusCodeError | ResponseDecodeError | SchemaDecodeError, 120 | O 121 | > => 122 | fetchOk(requestInit) 123 | .contramap(_ => _.acceptJson) 124 | .mapEffect(_ => _.decode(schema, options)) 125 | 126 | /** 127 | * @tsplus pipeable effect-http/client/Request fetchDecode 128 | */ 129 | export const fetchDecode_: ( 130 | schema: Schema, 131 | options?: ParseOptions, 132 | requestInit?: RequestInit, 133 | ) => ( 134 | request: Request, 135 | ) => Effect.Effect< 136 | never, 137 | RequestError | StatusCodeError | ResponseDecodeError | SchemaDecodeError, 138 | O 139 | > = fetchDecode 140 | 141 | const convertBody = (body: RequestBody): BodyInit => { 142 | switch (body._tag) { 143 | case "FormDataBody": 144 | case "RawBody": 145 | return body.value as any 146 | 147 | case "StreamBody": 148 | return toReadableStream(body.value) 149 | } 150 | } 151 | 152 | export const LiveFetchRequestExecutor = Layer.succeed( 153 | executor.HttpRequestExecutor, 154 | { execute: fetch() }, 155 | ) 156 | -------------------------------------------------------------------------------- /packages/client/src/Response.ts: -------------------------------------------------------------------------------- 1 | import type { Effect } from "@effect/io/Effect" 2 | import { ResponseDecodeError, SchemaDecodeError } from "./Error.js" 3 | import { fromReadableStream } from "./util/stream.js" 4 | import type { ParseOptions } from "@effect/schema/AST" 5 | import { Schema } from "@effect/schema/Schema" 6 | 7 | export interface Response { 8 | readonly status: number 9 | readonly headers: Headers 10 | readonly stream: Stream 11 | readonly json: Effect 12 | readonly text: Effect 13 | readonly formData: Effect 14 | readonly blob: Effect 15 | readonly decode: ( 16 | schema: Schema, 17 | options?: ParseOptions, 18 | ) => Effect 19 | } 20 | 21 | class ResponseImpl implements Response { 22 | constructor(private readonly source: globalThis.Response) {} 23 | 24 | get status(): number { 25 | return this.source.status 26 | } 27 | 28 | get headers(): Headers { 29 | return this.source.headers 30 | } 31 | 32 | get stream(): Stream { 33 | return this.source.body 34 | ? fromReadableStream(this.source.body).mapError( 35 | _ => new ResponseDecodeError(_, this, "stream"), 36 | ) 37 | : Stream.fail(new ResponseDecodeError("no body", this, "stream")) 38 | } 39 | 40 | get json(): Effect { 41 | return Effect.tryPromise({ 42 | try: () => this.source.json(), 43 | catch: _ => new ResponseDecodeError(_, this, "json"), 44 | }) 45 | } 46 | 47 | get text(): Effect { 48 | return Effect.tryPromise({ 49 | try: () => this.source.text(), 50 | catch: _ => new ResponseDecodeError(_, this, "text"), 51 | }) 52 | } 53 | 54 | get formData(): Effect { 55 | return Effect.tryPromise({ 56 | try: () => this.source.formData(), 57 | catch: _ => new ResponseDecodeError(_, this, "text"), 58 | }) 59 | } 60 | 61 | get blob(): Effect { 62 | return Effect.tryPromise({ 63 | try: () => this.source.blob(), 64 | catch: _ => new ResponseDecodeError(_, this, "blob"), 65 | }) 66 | } 67 | 68 | decode( 69 | schema: Schema, 70 | options?: ParseOptions, 71 | ): Effect { 72 | const parse = schema.parse 73 | return this.json.flatMap(_ => 74 | parse(_, options).mapError(_ => new SchemaDecodeError(_, this)), 75 | ) 76 | } 77 | } 78 | 79 | export const fromWeb = (_: globalThis.Response): Response => new ResponseImpl(_) 80 | -------------------------------------------------------------------------------- /packages/client/src/_common.ts: -------------------------------------------------------------------------------- 1 | export type { Cause } from "@effect/io/Cause" 2 | export type { Effect } from "@effect/io/Effect" 3 | export type { Layer } from "@effect/io/Layer" 4 | export type { Schedule } from "@effect/io/Schedule" 5 | export type { Stream } from "@effect/stream/Stream" 6 | export { Context, Tag } from "@effect/data/Context" 7 | export type { LazyArg } from "@effect/data/Function" 8 | export type { Option as Maybe } from "@effect/data/Option" 9 | export type { Schema } from "@effect/schema/Schema" 10 | -------------------------------------------------------------------------------- /packages/client/src/_global.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @tsplus global 3 | */ 4 | import type { Cause } from "@effect/io/Cause" 5 | 6 | /** 7 | * @tsplus global 8 | */ 9 | import type { Effect } from "@effect/io/Effect" 10 | 11 | /** 12 | * @tsplus global 13 | */ 14 | import type { Exit } from "@effect/io/Exit" 15 | 16 | /** 17 | * @tsplus global 18 | */ 19 | import type { Layer } from "@effect/io/Layer" 20 | 21 | /** 22 | * @tsplus global 23 | */ 24 | import type { Schedule } from "@effect/io/Schedule" 25 | 26 | /** 27 | * @tsplus global 28 | */ 29 | import type { Scope, CloseableScope } from "@effect/io/Scope" 30 | 31 | /** 32 | * @tsplus global 33 | */ 34 | import type { Stream } from "@effect/stream/Stream" 35 | 36 | /** 37 | * @tsplus global 38 | */ 39 | import type { Maybe } from "@effect-http/client/_common" 40 | 41 | /** 42 | * @tsplus global 43 | */ 44 | import type { NonEmptyReadonlyArray } from "@effect/data/ReadonlyArray" 45 | 46 | /** 47 | * @tsplus global 48 | */ 49 | import type { ParseError } from "@effect/schema/ParseResult" 50 | 51 | /** 52 | * @tsplus global 53 | */ 54 | import type { Schema } from "@effect/schema/Schema" 55 | 56 | /** 57 | * @tsplus global 58 | */ 59 | import { Chunk } from "@effect/data/Chunk" 60 | 61 | /** 62 | * @tsplus global 63 | */ 64 | import { Context, Tag } from "@effect/data/Context" 65 | 66 | /** 67 | * @tsplus global 68 | */ 69 | import { Either } from "@effect/data/Either" 70 | 71 | /** 72 | * @tsplus global 73 | */ 74 | import type { HashMap } from "@effect/data/HashMap" 75 | 76 | /** 77 | * @tsplus global 78 | */ 79 | import { pipe, identity, LazyArg } from "@effect/data/Function" 80 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Request.js" 2 | 3 | export { 4 | fetch, 5 | fetchOk, 6 | fetchJson, 7 | fetchDecode, 8 | LiveFetchRequestExecutor, 9 | } from "./Request/FetchExecutor.js" 10 | 11 | export * from "./Error.js" 12 | 13 | export { RequestBody } from "./Request/Body.js" 14 | export * as body from "./Request/Body.js" 15 | 16 | export { RequestExecutor, HttpRequestExecutor } from "./Request/Executor.js" 17 | export * as executor from "./Request/Executor.js" 18 | 19 | export { Response } from "./Response.js" 20 | export * as response from "./Response.js" 21 | -------------------------------------------------------------------------------- /packages/client/src/util/stream.ts: -------------------------------------------------------------------------------- 1 | import * as Scope from "@effect/io/Scope" 2 | import * as ReadonlyArray from "@effect/data/ReadonlyArray" 3 | 4 | export const fromReadableStream = ( 5 | evaluate: LazyArg>, 6 | ) => 7 | Stream.unwrapScoped( 8 | Effect(() => evaluate().getReader()) 9 | .acquireRelease(reader => Effect.promise(() => reader.cancel())) 10 | .map(reader => 11 | Stream.repeatEffectOption( 12 | Effect.tryPromise({ 13 | try: () => reader.read(), 14 | catch: _ => Maybe.some(_), 15 | }).flatMap(({ value, done }) => 16 | done ? Effect.fail(Maybe.none()) : Effect.succeed(value), 17 | ), 18 | ), 19 | ), 20 | ) 21 | 22 | export const toReadableStream = (source: Stream) => { 23 | let pull: Effect 24 | let scope: CloseableScope 25 | 26 | return new ReadableStream({ 27 | start(controller) { 28 | scope = Scope.make().runSync 29 | pull = source.toPull 30 | .use(scope) 31 | .runSync.tap(_ => 32 | Effect(() => { 33 | ReadonlyArray.forEach(_, _ => { 34 | controller.enqueue(_) 35 | }) 36 | }), 37 | ) 38 | .tapErrorCause(() => scope.close(Exit.unit)) 39 | .catchTag("None", () => 40 | Effect(() => { 41 | controller.close() 42 | }), 43 | ) 44 | .catchTag("Some", e => 45 | Effect(() => { 46 | controller.error(e.value) 47 | }), 48 | ).asUnit 49 | }, 50 | pull() { 51 | return pull.runPromise 52 | }, 53 | cancel() { 54 | return scope.close(Exit.unit).runPromise 55 | }, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "resolveJsonModule": true, 7 | "skipLibCheck": true, 8 | "moduleResolution": "Node16", 9 | "module": "Node16", 10 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 11 | "sourceMap": true, 12 | "declaration": true, 13 | "strict": true, 14 | "strictNullChecks": true, 15 | "target": "ES2022", 16 | "incremental": true, 17 | "tsPlusConfig": "./tsplus.config.json", 18 | "paths": { 19 | "@effect-http/client": ["./src/index.js"], 20 | "@effect-http/client/*": ["./src/*.js"] 21 | }, 22 | "tsBuildInfoFile": "./tsconfig.tsbuildinfo" 23 | }, 24 | "include": ["src/**/*"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/client/tsplus.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": { 3 | "^(.*)/src/(.*)\\.(ts|js)$": "@effect-http/client/$2" 4 | }, 5 | "traceMap": { 6 | "^(.*)/src/(.*)\\.(ts|js)$": "(@effect-http/client) $2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/core/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.40.0](https://github.com/tim-smart/effect-http/compare/v0.39.0...v0.40.0) (2023-08-04) 7 | 8 | **Note:** Version bump only for package @effect-http/core 9 | 10 | 11 | 12 | 13 | 14 | # [0.39.0](https://github.com/tim-smart/effect-http/compare/v0.38.0...v0.39.0) (2023-07-31) 15 | 16 | **Note:** Version bump only for package @effect-http/core 17 | 18 | 19 | 20 | 21 | 22 | # [0.38.0](https://github.com/tim-smart/effect-http/compare/v0.37.0...v0.38.0) (2023-07-27) 23 | 24 | **Note:** Version bump only for package @effect-http/core 25 | 26 | 27 | 28 | 29 | 30 | # [0.37.0](https://github.com/tim-smart/effect-http/compare/v0.36.0...v0.37.0) (2023-07-25) 31 | 32 | **Note:** Version bump only for package @effect-http/core 33 | 34 | 35 | 36 | 37 | 38 | # [0.36.0](https://github.com/tim-smart/effect-http/compare/v0.35.0...v0.36.0) (2023-07-25) 39 | 40 | **Note:** Version bump only for package @effect-http/core 41 | 42 | 43 | 44 | 45 | 46 | # [0.35.0](https://github.com/tim-smart/effect-http/compare/v0.34.0...v0.35.0) (2023-07-20) 47 | 48 | **Note:** Version bump only for package @effect-http/core 49 | 50 | 51 | 52 | 53 | 54 | # [0.34.0](https://github.com/tim-smart/effect-http/compare/v0.33.0...v0.34.0) (2023-07-19) 55 | 56 | **Note:** Version bump only for package @effect-http/core 57 | 58 | 59 | 60 | 61 | 62 | # [0.33.0](https://github.com/tim-smart/effect-http/compare/v0.32.0...v0.33.0) (2023-07-18) 63 | 64 | **Note:** Version bump only for package @effect-http/core 65 | 66 | 67 | 68 | 69 | 70 | # [0.32.0](https://github.com/tim-smart/effect-http/compare/v0.31.1...v0.32.0) (2023-07-17) 71 | 72 | **Note:** Version bump only for package @effect-http/core 73 | 74 | 75 | 76 | 77 | 78 | ## [0.31.1](https://github.com/tim-smart/effect-http/compare/v0.31.0...v0.31.1) (2023-07-12) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * don't build handler env twice ([dd6add5](https://github.com/tim-smart/effect-http/commit/dd6add56a3df3b7d2a828782be02c96c50023698)) 84 | 85 | 86 | 87 | 88 | 89 | # [0.31.0](https://github.com/tim-smart/effect-http/compare/v0.30.0...v0.31.0) (2023-07-12) 90 | 91 | **Note:** Version bump only for package @effect-http/core 92 | 93 | 94 | 95 | 96 | 97 | # [0.30.0](https://github.com/tim-smart/effect-http/compare/v0.29.1...v0.30.0) (2023-07-11) 98 | 99 | **Note:** Version bump only for package @effect-http/core 100 | 101 | 102 | 103 | 104 | 105 | ## [0.29.1](https://github.com/tim-smart/effect-http/compare/v0.29.0...v0.29.1) (2023-07-10) 106 | 107 | **Note:** Version bump only for package @effect-http/core 108 | 109 | 110 | 111 | 112 | 113 | # [0.29.0](https://github.com/tim-smart/effect-http/compare/v0.28.0...v0.29.0) (2023-07-08) 114 | 115 | **Note:** Version bump only for package @effect-http/core 116 | 117 | 118 | 119 | 120 | 121 | # [0.28.0](https://github.com/tim-smart/effect-http/compare/v0.27.5...v0.28.0) (2023-06-23) 122 | 123 | 124 | ### Features 125 | 126 | * add preprocess to router ([7016e0f](https://github.com/tim-smart/effect-http/commit/7016e0fd301ef0b6b75563df4b5b54a73145abf1)) 127 | 128 | 129 | 130 | 131 | 132 | ## [0.27.3](https://github.com/tim-smart/effect-http/compare/v0.27.2...v0.27.3) (2023-06-22) 133 | 134 | **Note:** Version bump only for package @effect-http/core 135 | 136 | 137 | 138 | 139 | 140 | ## [0.27.2](https://github.com/tim-smart/effect-http/compare/v0.27.1...v0.27.2) (2023-06-18) 141 | 142 | **Note:** Version bump only for package @effect-http/core 143 | 144 | 145 | 146 | 147 | 148 | ## [0.27.1](https://github.com/tim-smart/effect-http/compare/v0.27.0...v0.27.1) (2023-06-18) 149 | 150 | **Note:** Version bump only for package @effect-http/core 151 | 152 | 153 | 154 | 155 | 156 | # [0.27.0](https://github.com/tim-smart/effect-http/compare/v0.26.1...v0.27.0) (2023-06-02) 157 | 158 | **Note:** Version bump only for package @effect-http/core 159 | 160 | 161 | 162 | 163 | 164 | ## [0.26.1](https://github.com/tim-smart/effect-http/compare/v0.26.0...v0.26.1) (2023-05-22) 165 | 166 | **Note:** Version bump only for package @effect-http/core 167 | 168 | 169 | 170 | 171 | 172 | # [0.26.0](https://github.com/tim-smart/effect-http/compare/v0.25.2...v0.26.0) (2023-05-15) 173 | 174 | **Note:** Version bump only for package @effect-http/core 175 | 176 | 177 | 178 | 179 | 180 | ## [0.25.2](https://github.com/tim-smart/effect-http/compare/v0.25.1...v0.25.2) (2023-05-15) 181 | 182 | 183 | ### Bug Fixes 184 | 185 | * catchTag ([c591756](https://github.com/tim-smart/effect-http/commit/c5917564ad2f7c72546c65e1a91a6d3a95bb3be9)) 186 | 187 | 188 | 189 | 190 | 191 | ## [0.25.1](https://github.com/tim-smart/effect-http/compare/v0.25.0...v0.25.1) (2023-05-12) 192 | 193 | **Note:** Version bump only for package @effect-http/core 194 | 195 | 196 | 197 | 198 | 199 | # [0.25.0](https://github.com/tim-smart/effect-http/compare/v0.24.2...v0.25.0) (2023-05-10) 200 | 201 | **Note:** Version bump only for package @effect-http/core 202 | 203 | 204 | 205 | 206 | 207 | # [0.24.0](https://github.com/tim-smart/effect-http/compare/v0.23.0...v0.24.0) (2023-04-30) 208 | 209 | **Note:** Version bump only for package @effect-http/core 210 | 211 | 212 | 213 | 214 | 215 | # [0.23.0](https://github.com/tim-smart/effect-http/compare/v0.22.4...v0.23.0) (2023-04-30) 216 | 217 | **Note:** Version bump only for package @effect-http/core 218 | 219 | 220 | 221 | 222 | 223 | # 0.22.0 (2023-04-24) 224 | 225 | 226 | ### Bug Fixes 227 | 228 | * missing import ([6763564](https://github.com/tim-smart/effect-http/commit/6763564fbdeceaf42f17abb1419a4f34095ac08f)) 229 | * prevent path escalation in serveDirectory ([e78abc0](https://github.com/tim-smart/effect-http/commit/e78abc01983660a8c1da83720501d4b3aa1c7b74)) 230 | * set empty status code to 204 ([bd84f71](https://github.com/tim-smart/effect-http/commit/bd84f71e9d7281ab702ce9f69d6f3559cc86aae1)) 231 | * use pipeable context add ([9ddd8c1](https://github.com/tim-smart/effect-http/commit/9ddd8c12cf41a0186abb881c7208b7e9d9f46516)) 232 | 233 | 234 | ### Features 235 | 236 | * add cause functions to HttpApp ([dd3f7c7](https://github.com/tim-smart/effect-http/commit/dd3f7c787bc6691b1f5d60a79c301451b0134c11)) 237 | * add convenience accessors for request ([f9bc2db](https://github.com/tim-smart/effect-http/commit/f9bc2db066c62d215203cbe9c6deb09356e4904d)) 238 | * add empty response constructor ([be21256](https://github.com/tim-smart/effect-http/commit/be212564d88578f09a2bd8d6dade81fcc25ff038)) 239 | * add FileResponse ([f70fec6](https://github.com/tim-smart/effect-http/commit/f70fec65bc4e43670641209d1239deecb700cd63)) 240 | * add formData to HttpRequest ([e0b9a1b](https://github.com/tim-smart/effect-http/commit/e0b9a1bc31466803e560e7904126b9337ab9236e)) 241 | * add FormDataResponse ([c58eded](https://github.com/tim-smart/effect-http/commit/c58eded1938552f7f9e57d18913b0e5f87e7052a)) 242 | * add node runtime ([0bf8d9e](https://github.com/tim-smart/effect-http/commit/0bf8d9ec73c7471b895b11491d9ce8b41a0efe20)) 243 | * better streaming support ([8f1c625](https://github.com/tim-smart/effect-http/commit/8f1c625e5c613f544887dd0f64ef812e8899f8b7)) 244 | * decodeFormData ([2925676](https://github.com/tim-smart/effect-http/commit/2925676e12043cd7a44e6c10ee2e6958ab61995e)) 245 | * html response method ([4b7eb1a](https://github.com/tim-smart/effect-http/commit/4b7eb1abdd0cd4493b4fd59478b5e3b3ca6173e6)) 246 | * make request lazy ([328e20d](https://github.com/tim-smart/effect-http/commit/328e20dafef29964abd713c07d69712d9bc69c82)) 247 | * **node:** add client executor ([9318776](https://github.com/tim-smart/effect-http/commit/93187762b1c75cf7d15ad282f37b8c76655a4127)) 248 | * webStream ([0ca0411](https://github.com/tim-smart/effect-http/commit/0ca04117c9b7fdaa2c6155902994ae18ca56ab14)) 249 | 250 | 251 | 252 | 253 | 254 | # [0.21.0](https://github.com/tim-smart/effect-http/compare/@effect-http/core@0.20.0...@effect-http/core@0.21.0) (2023-04-18) 255 | 256 | **Note:** Version bump only for package @effect-http/core 257 | 258 | 259 | 260 | 261 | 262 | # 0.20.0 (2023-04-11) 263 | 264 | 265 | ### Bug Fixes 266 | 267 | * missing import ([6763564](https://github.com/tim-smart/effect-http/commit/6763564fbdeceaf42f17abb1419a4f34095ac08f)) 268 | * prevent path escalation in serveDirectory ([e78abc0](https://github.com/tim-smart/effect-http/commit/e78abc01983660a8c1da83720501d4b3aa1c7b74)) 269 | * set empty status code to 204 ([bd84f71](https://github.com/tim-smart/effect-http/commit/bd84f71e9d7281ab702ce9f69d6f3559cc86aae1)) 270 | * use pipeable context add ([9ddd8c1](https://github.com/tim-smart/effect-http/commit/9ddd8c12cf41a0186abb881c7208b7e9d9f46516)) 271 | 272 | 273 | ### Features 274 | 275 | * add cause functions to HttpApp ([dd3f7c7](https://github.com/tim-smart/effect-http/commit/dd3f7c787bc6691b1f5d60a79c301451b0134c11)) 276 | * add convenience accessors for request ([f9bc2db](https://github.com/tim-smart/effect-http/commit/f9bc2db066c62d215203cbe9c6deb09356e4904d)) 277 | * add empty response constructor ([be21256](https://github.com/tim-smart/effect-http/commit/be212564d88578f09a2bd8d6dade81fcc25ff038)) 278 | * add FileResponse ([f70fec6](https://github.com/tim-smart/effect-http/commit/f70fec65bc4e43670641209d1239deecb700cd63)) 279 | * add formData to HttpRequest ([e0b9a1b](https://github.com/tim-smart/effect-http/commit/e0b9a1bc31466803e560e7904126b9337ab9236e)) 280 | * add FormDataResponse ([c58eded](https://github.com/tim-smart/effect-http/commit/c58eded1938552f7f9e57d18913b0e5f87e7052a)) 281 | * add node runtime ([0bf8d9e](https://github.com/tim-smart/effect-http/commit/0bf8d9ec73c7471b895b11491d9ce8b41a0efe20)) 282 | * better streaming support ([8f1c625](https://github.com/tim-smart/effect-http/commit/8f1c625e5c613f544887dd0f64ef812e8899f8b7)) 283 | * decodeFormData ([2925676](https://github.com/tim-smart/effect-http/commit/2925676e12043cd7a44e6c10ee2e6958ab61995e)) 284 | * html response method ([4b7eb1a](https://github.com/tim-smart/effect-http/commit/4b7eb1abdd0cd4493b4fd59478b5e3b3ca6173e6)) 285 | * make request lazy ([328e20d](https://github.com/tim-smart/effect-http/commit/328e20dafef29964abd713c07d69712d9bc69c82)) 286 | * **node:** add client executor ([9318776](https://github.com/tim-smart/effect-http/commit/93187762b1c75cf7d15ad282f37b8c76655a4127)) 287 | * webStream ([0ca0411](https://github.com/tim-smart/effect-http/commit/0ca04117c9b7fdaa2c6155902994ae18ca56ab14)) 288 | -------------------------------------------------------------------------------- /packages/core/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tsc 2 | tsc: clean 3 | ./node_modules/.bin/tsc 4 | sed 's#/dist/#/#' package.json > dist/package.json 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf dist tsconfig.tsbuildinfo 9 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@effect-http/core", 3 | "version": "0.40.0", 4 | "publishConfig": { 5 | "access": "public", 6 | "directory": "dist" 7 | }, 8 | "exports": { 9 | ".": "./dist/index.js", 10 | "./HttpApp": "./dist/HttpApp.js", 11 | "./Request": "./dist/Request.js", 12 | "./Response": "./dist/Response.js", 13 | "./*": "./dist/*.js" 14 | }, 15 | "scripts": { 16 | "clean": "make clean", 17 | "postinstall": "tsplus-install || true", 18 | "prepublishOnly": "make tsc", 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "find-my-way": "^7.6.2", 26 | "mime-types": "^2.1.35", 27 | "range-parser": "^1.2.1" 28 | }, 29 | "peerDependencies": { 30 | "@effect/data": "^0.17.1", 31 | "@effect/io": "^0.38.0", 32 | "@effect/schema": "^0.33.0", 33 | "@effect/stream": "^0.34.0" 34 | }, 35 | "devDependencies": { 36 | "@changesets/cli": "^2.26.2", 37 | "@effect/data": "^0.17.1", 38 | "@effect/io": "^0.38.0", 39 | "@effect/schema": "^0.33.0", 40 | "@effect/stream": "^0.34.0", 41 | "@tsplus-types/effect__data": "0.17.1-11739c0", 42 | "@tsplus-types/effect__io": "0.38.0-11739c0", 43 | "@tsplus-types/effect__schema": "0.32.0-11739c0", 44 | "@tsplus-types/effect__stream": "0.33.0-11739c0", 45 | "@tsplus/installer": "^0.0.178", 46 | "@types/mime-types": "^2.1.1", 47 | "@types/node": "^20.4.7", 48 | "@types/range-parser": "^1.2.4", 49 | "prettier": "^3.0.1", 50 | "typescript": "5.1.6" 51 | }, 52 | "sideEffects": false, 53 | "gitHead": "f285647e065f4b904e4d2165df54dd797a9c51fc" 54 | } 55 | -------------------------------------------------------------------------------- /packages/core/src/HttpApp.ts: -------------------------------------------------------------------------------- 1 | import type { HttpResponse } from "./Response.js" 2 | 3 | /** 4 | * @tsplus pipeable effect-http/HttpApp map 5 | */ 6 | export const map = 7 | (f: (r: HttpResponse) => HttpResponse) => 8 | (self: HttpApp): HttpApp => 9 | request => 10 | self(request).map(f) 11 | 12 | /** 13 | * @tsplus pipeable effect-http/HttpApp map 14 | */ 15 | export const mapEffect = 16 | (f: (r: HttpResponse) => Effect) => 17 | (self: HttpApp): HttpApp => 18 | request => 19 | self(request).flatMap(f) 20 | 21 | /** 22 | * @tsplus pipeable effect-http/HttpApp catchTag 23 | */ 24 | export const catchTag = 25 | ( 26 | tag: K, 27 | onError: (e: Extract) => Effect, 28 | ) => 29 | (self: HttpApp): HttpApp> => 30 | request => 31 | self(request).catchTag(tag, onError) 32 | 33 | /** 34 | * @tsplus pipeable effect-http/HttpApp catchTags 35 | */ 36 | export const catchTags = 37 | < 38 | E extends { _tag: string }, 39 | Cases extends { 40 | [K in E["_tag"]]?: ( 41 | error: Extract, 42 | ) => Effect 43 | }, 44 | >( 45 | cases: Cases, 46 | ) => 47 | ( 48 | self: HttpApp, 49 | ): HttpApp< 50 | | R 51 | | { 52 | [K in keyof Cases]: Cases[K] extends ( 53 | ...args: Array 54 | ) => Effect 55 | ? R 56 | : never 57 | }[keyof Cases], 58 | | Exclude 59 | | { 60 | [K in keyof Cases]: Cases[K] extends ( 61 | ...args: Array 62 | ) => Effect 63 | ? E 64 | : never 65 | }[keyof Cases] 66 | > => 67 | request => 68 | self(request).catchTags(cases as any) as any 69 | 70 | /** 71 | * @tsplus pipeable effect-http/HttpApp catchAll 72 | */ 73 | export const catchAll = 74 | (onError: (e: E) => Effect) => 75 | (self: HttpApp): HttpApp => 76 | request => 77 | self(request).catchAll(onError) 78 | 79 | /** 80 | * @tsplus pipeable effect-http/HttpApp tapErrorCause 81 | */ 82 | export const tapErrorCause = 83 | (onError: (e: Cause) => Effect) => 84 | (self: HttpApp): HttpApp => 85 | request => 86 | self(request).tapErrorCause(onError) 87 | 88 | /** 89 | * @tsplus pipeable effect-http/HttpApp catchAllCause 90 | */ 91 | export const catchAllCause = 92 | (onError: (e: Cause) => Effect) => 93 | (self: HttpApp): HttpApp => 94 | request => 95 | self(request).catchAllCause(onError) 96 | 97 | /** 98 | * @tsplus pipeable effect-http/HttpApp applyMiddleware 99 | * @tsplus pipeable-operator effect-http/HttpApp >> 100 | */ 101 | export const applyMiddleware = 102 | (fa: Middleware) => 103 | (self: HttpApp): HttpApp => 104 | fa(self) 105 | -------------------------------------------------------------------------------- /packages/core/src/Request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @tsplus type effect-http/Request 3 | * @tsplus companion effect-http/Request.Ops 4 | */ 5 | export interface HttpRequest { 6 | readonly source: unknown 7 | readonly method: string 8 | 9 | readonly url: string 10 | readonly originalUrl: string 11 | setUrl(url: string): HttpRequest 12 | 13 | readonly headers: Headers 14 | readonly json: Effect 15 | readonly text: Effect 16 | readonly formData: Effect 17 | readonly formDataStream: Stream 18 | readonly stream: Stream 19 | readonly webStream: Effect< 20 | never, 21 | RequestBodyError, 22 | ReadableStream 23 | > 24 | } 25 | 26 | export class RequestBodyError { 27 | readonly _tag = "RequestBodyError" 28 | constructor(readonly reason: unknown) {} 29 | } 30 | 31 | export class ReadableStreamError { 32 | readonly _tag = "ReadableStreamError" 33 | constructor(readonly reason: unknown) {} 34 | } 35 | 36 | class HttpRequestImpl implements HttpRequest { 37 | constructor( 38 | private _build: Request | LazyArg, 39 | readonly method: string, 40 | readonly url: string, 41 | ) {} 42 | 43 | get source() { 44 | if (typeof this._build !== "function") { 45 | return this._build 46 | } 47 | 48 | this._build = this._build() 49 | return this._build 50 | } 51 | 52 | get originalUrl() { 53 | return this.source.url 54 | } 55 | 56 | setUrl(url: string): HttpRequest { 57 | return new HttpRequestImpl(this.source, this.method, url) 58 | } 59 | 60 | get headers() { 61 | return this.source.headers 62 | } 63 | 64 | get json() { 65 | return Effect.tryPromise({ 66 | try: () => this.source.json(), 67 | catch: reason => new RequestBodyError(reason), 68 | }) 69 | } 70 | 71 | get text() { 72 | return Effect.tryPromise({ 73 | try: () => this.source.text(), 74 | catch: reason => new RequestBodyError(reason), 75 | }) 76 | } 77 | 78 | get formData() { 79 | return Effect.tryPromise({ 80 | try: () => this.source.formData(), 81 | catch: reason => new RequestBodyError(reason), 82 | }) 83 | } 84 | 85 | get formDataStream(): any { 86 | throw "unimplemented" 87 | } 88 | 89 | get stream() { 90 | return this.source.body 91 | ? Stream.fromReadableStream( 92 | this.source.body, 93 | _ => new ReadableStreamError(_), 94 | ).mapError(_ => new RequestBodyError(_)) 95 | : Stream.fail(new RequestBodyError("no body")) 96 | } 97 | 98 | get webStream() { 99 | return this.source.body 100 | ? Effect.succeed(this.source.body) 101 | : Effect.fail(new RequestBodyError("no body")) 102 | } 103 | } 104 | 105 | /** 106 | * @tsplus static effect-http/Request.Ops fromStandard 107 | */ 108 | export const fromStandard = ( 109 | source: LazyArg | Request, 110 | method: string, 111 | url: string, 112 | ): HttpRequest => new HttpRequestImpl(source, method, url) 113 | 114 | /** 115 | * @tsplus static effect-http/Request.Ops params 116 | */ 117 | export const params = RouteContext.accessWith( 118 | (_): Readonly> => ({ 119 | ..._.searchParams, 120 | ...(_.params || {}), 121 | }), 122 | ) 123 | 124 | /** 125 | * @tsplus static effect-http/Request.Ops json 126 | */ 127 | export const json = RouteContext.accessWithEffect(_ => _.request.json) 128 | 129 | /** 130 | * @tsplus static effect-http/Request.Ops text 131 | */ 132 | export const text = RouteContext.accessWithEffect(_ => _.request.text) 133 | 134 | /** 135 | * @tsplus static effect-http/Request.Ops formData 136 | */ 137 | export const formData = RouteContext.accessWithEffect(_ => _.request.formData) 138 | 139 | /** 140 | * @tsplus static effect-http/Request.Ops formDataStream 141 | */ 142 | export const formDataStream = Stream.fromEffect(RouteContext).flatMap( 143 | _ => _.request.formDataStream, 144 | ) 145 | 146 | /** 147 | * @tsplus static effect-http/Request.Ops stream 148 | */ 149 | export const stream = Stream.fromEffect(RouteContext).flatMap( 150 | _ => _.request.stream, 151 | ) 152 | -------------------------------------------------------------------------------- /packages/core/src/Response.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "@effect/schema/Schema" 2 | import { ToResponseOptions } from "./internal/HttpFs.js" 3 | import * as Mime from "mime-types" 4 | 5 | export class EncodeSchemaError { 6 | readonly _tag = "EncodeSchemaError" 7 | constructor( 8 | readonly error: ParseError, 9 | readonly value: unknown, 10 | ) {} 11 | } 12 | 13 | /** 14 | * @tsplus type effect-http/Response 15 | * @tsplus companion effect-http/Response.Ops 16 | */ 17 | export type HttpResponse = 18 | | EmptyResponse 19 | | FormDataResponse 20 | | StreamResponse 21 | | RawResponse 22 | 23 | export class HttpStreamError { 24 | readonly _tag = "HttpStreamError" 25 | constructor(readonly error: unknown) {} 26 | } 27 | 28 | export class EmptyResponse { 29 | readonly _tag = "EmptyResponse" 30 | constructor( 31 | readonly status: number, 32 | readonly headers: Headers | undefined, 33 | ) {} 34 | } 35 | 36 | export class FormDataResponse { 37 | readonly _tag = "FormDataResponse" 38 | constructor( 39 | readonly status: number, 40 | readonly headers: Headers | undefined, 41 | readonly body: FormData, 42 | ) {} 43 | } 44 | 45 | export class StreamResponse { 46 | readonly _tag = "StreamResponse" 47 | constructor( 48 | readonly status: number, 49 | readonly headers: Headers, 50 | readonly body: Stream, 51 | ) {} 52 | } 53 | 54 | export class RawResponse { 55 | readonly _tag = "RawResponse" 56 | constructor( 57 | readonly status: number, 58 | readonly headers: Headers | undefined, 59 | readonly body: unknown, 60 | ) {} 61 | } 62 | 63 | /** 64 | * @tsplus static effect-http/Response.Ops empty 65 | */ 66 | export const empty = ({ 67 | headers, 68 | status = 204, 69 | }: { 70 | status?: number 71 | headers?: Headers 72 | } = {}): HttpResponse => new EmptyResponse(status, headers) 73 | 74 | /** 75 | * @tsplus static effect-http/Response.Ops json 76 | */ 77 | export const json = ( 78 | value: unknown, 79 | { 80 | headers = new Headers(), 81 | status = 200, 82 | }: { 83 | status?: number 84 | headers?: Headers 85 | } = {}, 86 | ): HttpResponse => { 87 | headers.set("content-type", "application/json") 88 | return new RawResponse(status, headers, JSON.stringify(value)) 89 | } 90 | 91 | /** 92 | * @tsplus static effect-http/Response.Ops encodeJson 93 | */ 94 | export const encodeJson = (schema: Schema) => { 95 | const encode = schema.encode 96 | return ( 97 | value: A, 98 | opts: { 99 | status?: number 100 | headers?: Headers 101 | } = {}, 102 | ): Effect => 103 | encode(value) 104 | .mapError(_ => new EncodeSchemaError(_, value)) 105 | .map(_ => json(_, opts)) 106 | } 107 | 108 | /** 109 | * @tsplus static effect-http/Response.Ops text 110 | */ 111 | export const text = ( 112 | value: string, 113 | { 114 | headers = new Headers(), 115 | status = 200, 116 | contentType = "text/plain", 117 | }: { 118 | status?: number 119 | contentType?: string 120 | headers?: Headers 121 | } = {}, 122 | ): HttpResponse => { 123 | headers.set("content-type", contentType) 124 | return new RawResponse(status, headers, value) 125 | } 126 | 127 | /** 128 | * @tsplus static effect-http/Response.Ops html 129 | */ 130 | export const html = ( 131 | value: string, 132 | { 133 | headers = new Headers(), 134 | status = 200, 135 | }: { 136 | status?: number 137 | headers?: Headers 138 | } = {}, 139 | ): HttpResponse => { 140 | headers.set("content-type", "text/html") 141 | return new RawResponse(status, headers, value) 142 | } 143 | 144 | /** 145 | * @tsplus static effect-http/Response.Ops searchParams 146 | */ 147 | export const searchParams = ( 148 | value: URLSearchParams, 149 | { 150 | headers = new Headers(), 151 | status = 200, 152 | }: { 153 | status?: number 154 | headers?: Headers 155 | } = {}, 156 | ): HttpResponse => { 157 | headers.set("content-type", "application/x-www-form-urlencoded") 158 | return new RawResponse(status, headers, value.toString()) 159 | } 160 | 161 | /** 162 | * @tsplus static effect-http/Response.Ops stream 163 | */ 164 | export const stream = ( 165 | value: Stream, 166 | { 167 | headers = new Headers(), 168 | status = 200, 169 | contentType = "application/octet-stream", 170 | contentLength, 171 | }: { 172 | status?: number 173 | headers?: Headers 174 | contentType?: string 175 | contentLength?: number 176 | } = {}, 177 | ): HttpResponse => { 178 | headers.set("content-type", contentType) 179 | 180 | if (contentLength) { 181 | headers.set("content-length", contentLength.toString()) 182 | } 183 | 184 | return new StreamResponse(status, headers, value) 185 | } 186 | 187 | /** 188 | * @tsplus static effect-http/Response.Ops formData 189 | */ 190 | export const formData = ( 191 | value: FormData, 192 | { 193 | headers, 194 | status = 200, 195 | }: { 196 | status?: number 197 | headers?: Headers 198 | } = {}, 199 | ): HttpResponse => new FormDataResponse(status, headers, value) 200 | 201 | /** 202 | * @tsplus static effect-http/Response.Ops raw 203 | */ 204 | export const raw = ( 205 | body: unknown, 206 | { 207 | headers, 208 | status = 200, 209 | }: { 210 | status?: number 211 | headers?: Headers 212 | } = {}, 213 | ): HttpResponse => new RawResponse(status, headers, body) 214 | 215 | /** 216 | * @tsplus static effect-http/Response.Ops file 217 | */ 218 | export const file = (path: string, opts: Partial = {}) => { 219 | const options: ToResponseOptions = { 220 | ...opts, 221 | status: opts.status || 200, 222 | contentType: Mime.lookup(path) || "application/octet-stream", 223 | } 224 | 225 | return HttpFs.accessWithEffect(_ => _.toResponse(path, options)) 226 | } 227 | 228 | export class EarlyResponse { 229 | readonly _tag = "EarlyResponse" 230 | constructor(readonly response: HttpResponse) {} 231 | } 232 | 233 | /** 234 | * @tsplus getter effect-http/Response.Ops early 235 | */ 236 | export const early = ( 237 | response: HttpResponse, 238 | ): Effect => 239 | Effect.fail(new EarlyResponse(response)) 240 | 241 | /** 242 | * @tsplus fluent effect-http/Response toStandard 243 | * @tsplus static effect-http/Response.Ops toStandard 244 | */ 245 | export const toStandard = (self: HttpResponse): Response => { 246 | switch (self._tag) { 247 | case "EmptyResponse": 248 | return new Response(null, { 249 | status: self.status, 250 | headers: self.headers, 251 | }) 252 | 253 | case "RawResponse": 254 | return new Response(self.body as any, { 255 | status: self.status, 256 | headers: self.headers, 257 | }) 258 | 259 | case "FormDataResponse": 260 | return new Response(self.body, { 261 | status: self.status, 262 | headers: self.headers, 263 | }) 264 | 265 | case "StreamResponse": 266 | return new Response(self.body.toReadableStream, { 267 | status: self.status, 268 | headers: self.headers, 269 | }) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /packages/core/src/_common.ts: -------------------------------------------------------------------------------- 1 | export type { Cause } from "@effect/io/Cause" 2 | export type { Effect } from "@effect/io/Effect" 3 | export type { Stream } from "@effect/stream/Stream" 4 | export { Context, Tag } from "@effect/data/Context" 5 | export type { LazyArg } from "@effect/data/Function" 6 | export type { Option as Maybe } from "@effect/data/Option" 7 | export type { Schema } from "@effect/schema/Schema" 8 | -------------------------------------------------------------------------------- /packages/core/src/_global.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @tsplus global 3 | */ 4 | import type { Cause } from "@effect/io/Cause" 5 | 6 | /** 7 | * @tsplus global 8 | */ 9 | import type { Effect } from "@effect/io/Effect" 10 | 11 | /** 12 | * @tsplus global 13 | */ 14 | import type { Exit } from "@effect/io/Exit" 15 | 16 | /** 17 | * @tsplus global 18 | */ 19 | import type { Scope, CloseableScope } from "@effect/io/Scope" 20 | 21 | /** 22 | * @tsplus global 23 | */ 24 | import type { Stream } from "@effect/stream/Stream" 25 | 26 | /** 27 | * @tsplus global 28 | */ 29 | import type { Maybe } from "@effect-http/core/_common" 30 | 31 | /** 32 | * @tsplus global 33 | */ 34 | import type { NonEmptyReadonlyArray } from "@effect/data/ReadonlyArray" 35 | 36 | /** 37 | * @tsplus global 38 | */ 39 | import type { ParseError } from "@effect/schema/ParseResult" 40 | 41 | /** 42 | * @tsplus global 43 | */ 44 | import type { Schema } from "@effect/schema/Schema" 45 | 46 | /** 47 | * @tsplus global 48 | */ 49 | import { Chunk } from "@effect/data/Chunk" 50 | 51 | /** 52 | * @tsplus global 53 | */ 54 | import { Context, Tag } from "@effect/data/Context" 55 | 56 | /** 57 | * @tsplus global 58 | */ 59 | import { Either } from "@effect/data/Either" 60 | 61 | /** 62 | * @tsplus global 63 | */ 64 | import type { HashMap } from "@effect/data/HashMap" 65 | 66 | /** 67 | * @tsplus global 68 | */ 69 | import { pipe, identity, LazyArg } from "@effect/data/Function" 70 | 71 | /** 72 | * @tsplus global 73 | */ 74 | import { 75 | HttpApp, 76 | Middleware, 77 | Route, 78 | Concat, 79 | ConcatWithPrefix, 80 | RouteContext, 81 | } from "@effect-http/core/definitions" 82 | 83 | /** 84 | * @tsplus global 85 | */ 86 | import type { HttpResponse, HttpStreamError } from "@effect-http/core/Response" 87 | 88 | /** 89 | * @tsplus global 90 | */ 91 | import { HttpRequest, RequestBodyError } from "@effect-http/core/Request" 92 | 93 | /** 94 | * @tsplus global 95 | */ 96 | import { Router, RouteNotFound } from "@effect-http/core/router" 97 | 98 | /** 99 | * @tsplus global 100 | */ 101 | import { FormDataPart } from "@effect-http/core/multipart" 102 | 103 | /** 104 | * @tsplus global 105 | */ 106 | import { 107 | HttpFs, 108 | HttpFsError, 109 | HttpFsNotFound, 110 | } from "@effect-http/core/internal/HttpFs" 111 | -------------------------------------------------------------------------------- /packages/core/src/definitions.ts: -------------------------------------------------------------------------------- 1 | import type { HTTPMethod } from "find-my-way" 2 | import type { HttpRequest } from "./Request.js" 3 | import type { HttpResponse } from "./Response.js" 4 | 5 | /** 6 | * @tsplus type effect-http/HttpApp 7 | */ 8 | export interface HttpApp { 9 | (request: HttpRequest): Effect 10 | } 11 | 12 | /** 13 | * @tsplus type effect-http/Middleware 14 | * @tsplus companion effect-http/Middleware.Ops 15 | */ 16 | export interface Middleware { 17 | (self: HttpApp): HttpApp 18 | } 19 | 20 | /** 21 | * @tsplus static effect-http/Middleware.Ops make 22 | */ 23 | export const middleware = ( 24 | f: Middleware, 25 | ): Middleware => f 26 | 27 | export class Route { 28 | readonly _tag = "Route" 29 | 30 | constructor( 31 | readonly method: HTTPMethod, 32 | readonly path: string, 33 | readonly handler: Effect, 34 | ) {} 35 | } 36 | 37 | export class Concat { 38 | readonly _tag = "Concat" 39 | constructor(readonly router: Router) {} 40 | 41 | get routes() { 42 | return this.router.routes 43 | } 44 | 45 | get env() { 46 | return this.router.env 47 | } 48 | } 49 | 50 | export class ConcatWithPrefix { 51 | readonly _tag = "ConcatWithPrefix" 52 | constructor( 53 | readonly router: Router, 54 | readonly prefix: string, 55 | ) {} 56 | 57 | get routes() { 58 | return this.router.routes 59 | } 60 | 61 | get env() { 62 | return this.router.env 63 | } 64 | } 65 | 66 | export interface RouteContext { 67 | readonly request: HttpRequest 68 | readonly params: Readonly> 69 | readonly searchParams: Readonly> 70 | } 71 | export const RouteContext = Tag() 72 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./definitions.js" 2 | 3 | export { early as respondEarly } from "./Response.js" 4 | export { RouteNotFound } from "./router.js" 5 | export * from "./serveDirectory.js" 6 | 7 | export * from "./schema.js" 8 | 9 | export * as httpApp from "./HttpApp.js" 10 | export * as request from "./Request.js" 11 | export * as response from "./Response.js" 12 | 13 | export const router = new Router() 14 | -------------------------------------------------------------------------------- /packages/core/src/internal/HttpFs.ts: -------------------------------------------------------------------------------- 1 | export interface ToResponseOptions { 2 | readonly status: number 3 | readonly contentType: string 4 | readonly range?: readonly [start: number, end: number] 5 | readonly headers?: Headers 6 | } 7 | 8 | export class HttpFsError { 9 | readonly _tag = "HttpFsError" 10 | constructor(readonly error: unknown) {} 11 | } 12 | 13 | export class HttpFsNotFound { 14 | readonly _tag = "HttpFsNotFound" 15 | constructor(readonly path: string, readonly error: unknown) {} 16 | } 17 | 18 | export interface HttpFs { 19 | toResponse: ( 20 | path: string, 21 | opts: ToResponseOptions, 22 | ) => Effect 23 | } 24 | 25 | export const HttpFs = Tag() 26 | -------------------------------------------------------------------------------- /packages/core/src/multipart.ts: -------------------------------------------------------------------------------- 1 | export type FormDataPart = FormDataField | FormDataFile 2 | 3 | export class FormDataField { 4 | readonly _tag = "FormDataField" 5 | 6 | constructor( 7 | readonly key: string, 8 | readonly contentType: string, 9 | readonly value: string, 10 | ) {} 11 | } 12 | 13 | export class FormDataFileError { 14 | readonly _tag = "FormDataFileError" 15 | 16 | constructor( 17 | readonly key: string, 18 | readonly name: string, 19 | readonly contentType: string, 20 | readonly error: unknown, 21 | ) {} 22 | } 23 | 24 | export class FormDataFile { 25 | readonly _tag = "FormDataFile" 26 | 27 | constructor( 28 | readonly key: string, 29 | readonly name: string, 30 | readonly contentType: string, 31 | readonly content: Stream, 32 | readonly source?: unknown, 33 | ) {} 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/router.ts: -------------------------------------------------------------------------------- 1 | import { Context, Tag, add } from "@effect/data/Context" 2 | import FindMyWay, { HTTPMethod } from "find-my-way" 3 | import type { HttpRequest } from "./Request.js" 4 | import type { HttpResponse } from "./Response.js" 5 | 6 | export class Router { 7 | constructor( 8 | readonly routes: ReadonlyArray< 9 | Route | Concat | ConcatWithPrefix 10 | > = [], 11 | readonly env: Maybe>> = Maybe.none(), 12 | readonly mounts: HashMap> = HashMap.empty(), 13 | ) {} 14 | 15 | combineWith( 16 | router: Router, 17 | ): Router, E | E2, EnvR, ReqR> { 18 | return new Router( 19 | [...this.routes, new Concat(router)] as any, 20 | this.env as any, 21 | this.mounts, 22 | ) 23 | } 24 | 25 | route( 26 | method: HTTPMethod, 27 | path: string, 28 | handler: Effect, 29 | ) { 30 | return new Router, E | E2, EnvR, ReqR>( 31 | [...this.routes, new Route(method, path, handler)] as any, 32 | this.env as any, 33 | this.mounts, 34 | ) 35 | } 36 | 37 | preprocess(effect: Effect) { 38 | return new Router< 39 | R | Exclude, 40 | E | E2, 41 | EnvR | Exclude, 42 | ReqR 43 | >( 44 | this.routes, 45 | this.env 46 | .map(prevEnv => prevEnv.zipLeft(effect)) 47 | .orElse(() => Maybe.some(effect.as(Context.empty()))) as any, 48 | this.mounts, 49 | ) 50 | } 51 | 52 | provideService>(tag: T, service: Tag.Service) { 53 | return this.provideServiceEffect(tag, Effect.succeed(service)) 54 | } 55 | 56 | provideServiceEffect, R2, E2>( 57 | tag: T, 58 | service: Effect>, 59 | ) { 60 | return new Router< 61 | Exclude> | Exclude, 62 | E | E2, 63 | EnvR | Exclude, 64 | ReqR | Tag.Service 65 | >( 66 | this.routes as any, 67 | this.env 68 | .map(prevEnv => 69 | prevEnv.flatMap(ctx => 70 | service 71 | .mapInputContext(a => a.merge(ctx)) 72 | .map(a => add(tag, a)(ctx)), 73 | ), 74 | ) 75 | .orElse(() => 76 | Maybe.some(service.map(a => Context.make(tag, a))), 77 | ) as any, 78 | this.mounts as any, 79 | ) 80 | } 81 | 82 | mount(path: string, handler: HttpApp) { 83 | path = path.endsWith("/") ? path.slice(0, -1) : path 84 | 85 | return new Router, EnvR | R2, ReqR>( 86 | this.routes, 87 | this.env, 88 | this.mounts.set(path, handler as any), 89 | ) 90 | } 91 | 92 | mountRouter( 93 | path: string, 94 | router: Router | Router, 95 | ): Router, E | E2, EnvR, ReqR> { 96 | return new Router( 97 | [...this.routes, new ConcatWithPrefix(router as any, path)], 98 | this.env as any, 99 | this.mounts, 100 | ) 101 | } 102 | 103 | routesWithEnv( 104 | prefix?: string, 105 | ): (readonly [Route, Maybe>>])[] { 106 | let allRoutes = this.routes.flatMap(a => 107 | a._tag === "Route" 108 | ? [[a, Maybe.none()] as const] 109 | : a._tag === "ConcatWithPrefix" 110 | ? a.router.routesWithEnv(prefix ? `${a.prefix}${prefix}` : a.prefix) 111 | : a.router.routesWithEnv(prefix), 112 | ) 113 | 114 | if (prefix) { 115 | allRoutes = allRoutes.map(([a, env]) => [ 116 | new Route( 117 | a.method, 118 | a.path === "/" ? prefix : prefix + a.path, 119 | a.handler, 120 | ), 121 | env, 122 | ]) 123 | } 124 | 125 | return allRoutes.map(([route, env]) => [ 126 | route, 127 | this.env.match({ 128 | onNone: () => env, 129 | onSome: selfEnv => 130 | env.match({ 131 | onNone: () => Maybe.some(selfEnv), 132 | onSome: prevEnv => 133 | Maybe.some( 134 | selfEnv.flatMap(selfCtx => 135 | prevEnv 136 | .mapInputContext((a: Context) => a.merge(selfCtx)) 137 | .map(prevCtx => selfCtx.merge(prevCtx)), 138 | ), 139 | ), 140 | }), 141 | }), 142 | ]) 143 | } 144 | 145 | toHttpApp(): HttpApp { 146 | const routes = this.routesWithEnv() 147 | const router = FindMyWay() 148 | 149 | for (const [route, env] of routes) { 150 | const handler = env.match({ 151 | onNone: () => route.handler, 152 | onSome: env => 153 | env.flatMap(ctx => route.handler.mapInputContext(a => a.merge(ctx))), 154 | }) 155 | router.on(route.method, route.path, () => handler) 156 | } 157 | 158 | const hasMounts = !this.mounts.isEmpty() 159 | const mounts = [...this.mounts] 160 | const mountsLength = mounts.length 161 | 162 | return request => { 163 | if (hasMounts) { 164 | const urlObj = new URL(request.url) 165 | 166 | for (var i = 0; i < mountsLength; i++) { 167 | const [path, handler] = mounts[i] 168 | if ( 169 | !( 170 | urlObj.pathname === path || urlObj.pathname.startsWith(`${path}/`) 171 | ) 172 | ) { 173 | continue 174 | } 175 | 176 | urlObj.pathname = urlObj.pathname.slice(path.length) 177 | 178 | return handler(request.setUrl(urlObj.toString())) 179 | } 180 | } 181 | 182 | const findResult = router.find(request.method as HTTPMethod, request.url) 183 | 184 | if (!findResult) { 185 | return Effect.fail(new RouteNotFound(request)) 186 | } 187 | 188 | const handler = findResult!.handler as any 189 | const routeHandler = handler() as Effect< 190 | R | RouteContext, 191 | E, 192 | HttpResponse 193 | > 194 | 195 | return routeHandler.provideService(RouteContext, { 196 | request, 197 | params: findResult.params, 198 | searchParams: findResult.searchParams, 199 | }) 200 | } 201 | } 202 | } 203 | 204 | export class RouteNotFound { 205 | readonly _tag = "RouteNotFound" 206 | readonly method: string 207 | readonly url: string 208 | 209 | constructor(readonly request: HttpRequest) { 210 | this.method = request.method 211 | this.url = request.url 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /packages/core/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "@effect/schema/Schema" 2 | import type { HttpRequest } from "./Request.js" 3 | import * as Effect from "@effect/io/Effect" 4 | 5 | export class DecodeSchemaError { 6 | readonly _tag = "DecodeSchemaError" 7 | constructor( 8 | readonly error: ParseError, 9 | readonly request: HttpRequest, 10 | readonly body: unknown, 11 | ) {} 12 | } 13 | 14 | const decodeEither = 15 | () => 16 | (schema: Schema) => { 17 | const decode = schema.parseEither 18 | 19 | return ( 20 | input: unknown, 21 | request: HttpRequest, 22 | ): Either => 23 | decode(input).mapLeft(_ => new DecodeSchemaError(_, request, input)) 24 | } 25 | 26 | const decodeEffect = 27 | () => 28 | (schema: Schema) => { 29 | const decode = schema.parse 30 | return (input: unknown, request: HttpRequest) => 31 | decode(input).mapError(_ => new DecodeSchemaError(_, request, input)) 32 | } 33 | 34 | export const decode = (schema: Schema) => { 35 | const decode = decodeEffect()(schema) 36 | 37 | return Do($ => { 38 | const ctx = $(Effect.map(RouteContext, identity)) 39 | const params = $(parseBodyWithParams(ctx)) 40 | 41 | return $(decode(params, ctx.request)) 42 | }) 43 | } 44 | 45 | export const decodeHeaders = , A>( 46 | schema: Schema, 47 | ) => { 48 | const decode = decodeEffect>()(schema) 49 | return Do($ => { 50 | const ctx = $(Effect.map(RouteContext, identity)) 51 | return $(decode(Object.fromEntries(ctx.request.headers), ctx.request)) 52 | }) 53 | } 54 | 55 | export const decodeParams = , A>( 56 | schema: Schema, 57 | ) => { 58 | const decode = decodeEffect>()(schema) 59 | 60 | return Do($ => { 61 | const { request, params, searchParams } = $( 62 | Effect.map(RouteContext, identity), 63 | ) 64 | return $(decode({ ...searchParams, ...params }, request)) 65 | }) 66 | } 67 | 68 | export class FormDataKeyNotFound { 69 | readonly _tag = "FormDataKeyNotFound" 70 | constructor(readonly key: string) {} 71 | } 72 | 73 | const jsonParse = (_: string) => 74 | Effect.try({ 75 | try: () => JSON.parse(_) as unknown, 76 | catch: _ => new RequestBodyError(_), 77 | }) 78 | 79 | export const decodeJsonFromFormData = 80 | (schema: Schema) => 81 | (key: string, formData?: FormData) => { 82 | const decode = decodeEither()(schema) 83 | 84 | return Do($ => { 85 | const { request } = $(Effect.map(RouteContext, identity)) 86 | const data = $(formData ? Effect.succeed(formData) : request.formData) 87 | 88 | const result = Effect.mapError( 89 | Maybe.fromNullable(data.get(key)), 90 | () => new RequestBodyError(new FormDataKeyNotFound(key)), 91 | ) 92 | .flatMap(_ => jsonParse(_.toString())) 93 | .flatMap(_ => decode(_, request)) 94 | .map(value => [value, data] as const) 95 | 96 | return $(result) 97 | }) 98 | } 99 | 100 | export const parseBodyWithParams = ({ 101 | request, 102 | params, 103 | searchParams, 104 | }: RouteContext) => 105 | Do(($): unknown => { 106 | const allParams = { ...searchParams, ...params } 107 | const body = $(parseBody(request)) 108 | 109 | return body._tag === "Some" 110 | ? { 111 | ...allParams, 112 | ...(body.value as any), 113 | } 114 | : allParams 115 | }) 116 | 117 | export const parseBody = (request: HttpRequest) => { 118 | const contentType = request.headers.get("content-type")?.toLowerCase() 119 | 120 | if (contentType?.includes("application/json")) { 121 | return request.json.asSome 122 | } else if (contentType?.includes("application/x-www-form-urlencoded")) { 123 | return queryStringBody(request).asSome 124 | } 125 | 126 | return Effect.succeed(Maybe.none()) 127 | } 128 | 129 | export const queryStringBody = (request: HttpRequest) => 130 | request.text.flatMap(_ => 131 | Effect.try({ 132 | try: () => Object.fromEntries(new URLSearchParams(_).entries()), 133 | catch: reason => new RequestBodyError(reason), 134 | }), 135 | ) 136 | -------------------------------------------------------------------------------- /packages/core/src/serveDirectory.ts: -------------------------------------------------------------------------------- 1 | import parseRange from "range-parser" 2 | import * as Path from "node:path" 3 | 4 | export const serveDirectory = 5 | (directory: string): HttpApp => 6 | request => { 7 | const url = new URL(request.url) 8 | const path = Path.join(directory, Path.join("/", url.pathname)) 9 | 10 | const range = request.headers.get("range") 11 | const parsedRange = range ? parseRange(Infinity, range) : undefined 12 | const validRange = Array.isArray(parsedRange) 13 | 14 | return HttpResponse.file(path, { 15 | range: validRange 16 | ? [parsedRange[0].start, parsedRange[1].end] 17 | : undefined, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "resolveJsonModule": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "Node16", 10 | "module": "Node16", 11 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 12 | "sourceMap": true, 13 | "declaration": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "target": "ES2022", 17 | "incremental": true, 18 | "tsPlusConfig": "./tsplus.config.json", 19 | "paths": { 20 | "@effect-http/core": ["./src/index.js"], 21 | "@effect-http/core/*": ["./src/*.js"] 22 | }, 23 | "tsBuildInfoFile": "./tsconfig.tsbuildinfo" 24 | }, 25 | "include": ["src/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/tsplus.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": { 3 | "^(.*)/src/(.*)\\.(ts|js)$": "@effect-http/core/$2" 4 | }, 5 | "traceMap": { 6 | "^(.*)/src/(.*)\\.(ts|js)$": "(@effect-http/core) $2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/node/.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/node/.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/node/.changeset/fresh-ducks-explain.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@effect-http/node": patch 3 | --- 4 | 5 | update deps 6 | -------------------------------------------------------------------------------- /packages/node/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /packages/node/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.40.1](https://github.com/tim-smart/effect-http/compare/v0.40.0...v0.40.1) (2023-08-04) 7 | 8 | **Note:** Version bump only for package @effect-http/node 9 | 10 | 11 | 12 | 13 | 14 | # [0.40.0](https://github.com/tim-smart/effect-http/compare/v0.39.0...v0.40.0) (2023-08-04) 15 | 16 | **Note:** Version bump only for package @effect-http/node 17 | 18 | 19 | 20 | 21 | 22 | # [0.39.0](https://github.com/tim-smart/effect-http/compare/v0.38.0...v0.39.0) (2023-07-31) 23 | 24 | **Note:** Version bump only for package @effect-http/node 25 | 26 | 27 | 28 | 29 | 30 | # [0.38.0](https://github.com/tim-smart/effect-http/compare/v0.37.0...v0.38.0) (2023-07-27) 31 | 32 | **Note:** Version bump only for package @effect-http/node 33 | 34 | 35 | 36 | 37 | 38 | # [0.37.0](https://github.com/tim-smart/effect-http/compare/v0.36.0...v0.37.0) (2023-07-25) 39 | 40 | **Note:** Version bump only for package @effect-http/node 41 | 42 | 43 | 44 | 45 | 46 | # [0.36.0](https://github.com/tim-smart/effect-http/compare/v0.35.0...v0.36.0) (2023-07-25) 47 | 48 | **Note:** Version bump only for package @effect-http/node 49 | 50 | 51 | 52 | 53 | 54 | # [0.35.0](https://github.com/tim-smart/effect-http/compare/v0.34.0...v0.35.0) (2023-07-20) 55 | 56 | **Note:** Version bump only for package @effect-http/node 57 | 58 | 59 | 60 | 61 | 62 | # [0.34.0](https://github.com/tim-smart/effect-http/compare/v0.33.0...v0.34.0) (2023-07-19) 63 | 64 | **Note:** Version bump only for package @effect-http/node 65 | 66 | 67 | 68 | 69 | 70 | # [0.33.0](https://github.com/tim-smart/effect-http/compare/v0.32.0...v0.33.0) (2023-07-18) 71 | 72 | **Note:** Version bump only for package @effect-http/node 73 | 74 | 75 | 76 | 77 | 78 | # [0.32.0](https://github.com/tim-smart/effect-http/compare/v0.31.1...v0.32.0) (2023-07-17) 79 | 80 | **Note:** Version bump only for package @effect-http/node 81 | 82 | 83 | 84 | 85 | 86 | ## [0.31.1](https://github.com/tim-smart/effect-http/compare/v0.31.0...v0.31.1) (2023-07-12) 87 | 88 | **Note:** Version bump only for package @effect-http/node 89 | 90 | 91 | 92 | 93 | 94 | # [0.31.0](https://github.com/tim-smart/effect-http/compare/v0.30.0...v0.31.0) (2023-07-12) 95 | 96 | **Note:** Version bump only for package @effect-http/node 97 | 98 | 99 | 100 | 101 | 102 | # [0.30.0](https://github.com/tim-smart/effect-http/compare/v0.29.1...v0.30.0) (2023-07-11) 103 | 104 | **Note:** Version bump only for package @effect-http/node 105 | 106 | 107 | 108 | 109 | 110 | ## [0.29.1](https://github.com/tim-smart/effect-http/compare/v0.29.0...v0.29.1) (2023-07-10) 111 | 112 | **Note:** Version bump only for package @effect-http/node 113 | 114 | 115 | 116 | 117 | 118 | # [0.29.0](https://github.com/tim-smart/effect-http/compare/v0.28.0...v0.29.0) (2023-07-08) 119 | 120 | **Note:** Version bump only for package @effect-http/node 121 | 122 | 123 | 124 | 125 | 126 | # [0.28.0](https://github.com/tim-smart/effect-http/compare/v0.27.5...v0.28.0) (2023-06-23) 127 | 128 | **Note:** Version bump only for package @effect-http/node 129 | 130 | 131 | 132 | 133 | 134 | ## [0.27.5](https://github.com/tim-smart/effect-http/compare/v0.27.4...v0.27.5) (2023-06-23) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * node request text ([6f33e21](https://github.com/tim-smart/effect-http/commit/6f33e216f48aa272d7373f3ac3a0734e390a897b)) 140 | 141 | 142 | 143 | 144 | 145 | ## [0.27.4](https://github.com/tim-smart/effect-http/compare/v0.27.3...v0.27.4) (2023-06-23) 146 | 147 | 148 | ### Reverts 149 | 150 | * Revert "chore: update deps" ([d600d64](https://github.com/tim-smart/effect-http/commit/d600d640d1817822c0f83527e1b097058def367d)) 151 | 152 | 153 | 154 | 155 | 156 | ## [0.27.3](https://github.com/tim-smart/effect-http/compare/v0.27.2...v0.27.3) (2023-06-22) 157 | 158 | **Note:** Version bump only for package @effect-http/node 159 | 160 | 161 | 162 | 163 | 164 | ## [0.27.2](https://github.com/tim-smart/effect-http/compare/v0.27.1...v0.27.2) (2023-06-18) 165 | 166 | **Note:** Version bump only for package @effect-http/node 167 | 168 | 169 | 170 | 171 | 172 | ## [0.27.1](https://github.com/tim-smart/effect-http/compare/v0.27.0...v0.27.1) (2023-06-18) 173 | 174 | **Note:** Version bump only for package @effect-http/node 175 | 176 | 177 | 178 | 179 | 180 | # [0.27.0](https://github.com/tim-smart/effect-http/compare/v0.26.1...v0.27.0) (2023-06-02) 181 | 182 | **Note:** Version bump only for package @effect-http/node 183 | 184 | 185 | 186 | 187 | 188 | ## [0.26.1](https://github.com/tim-smart/effect-http/compare/v0.26.0...v0.26.1) (2023-05-22) 189 | 190 | **Note:** Version bump only for package @effect-http/node 191 | 192 | 193 | 194 | 195 | 196 | # [0.26.0](https://github.com/tim-smart/effect-http/compare/v0.25.2...v0.26.0) (2023-05-15) 197 | 198 | **Note:** Version bump only for package @effect-http/node 199 | 200 | 201 | 202 | 203 | 204 | ## [0.25.2](https://github.com/tim-smart/effect-http/compare/v0.25.1...v0.25.2) (2023-05-15) 205 | 206 | 207 | ### Bug Fixes 208 | 209 | * catchTag ([c591756](https://github.com/tim-smart/effect-http/commit/c5917564ad2f7c72546c65e1a91a6d3a95bb3be9)) 210 | 211 | 212 | 213 | 214 | 215 | ## [0.25.1](https://github.com/tim-smart/effect-http/compare/v0.25.0...v0.25.1) (2023-05-12) 216 | 217 | **Note:** Version bump only for package @effect-http/node 218 | 219 | 220 | 221 | 222 | 223 | # [0.25.0](https://github.com/tim-smart/effect-http/compare/v0.24.2...v0.25.0) (2023-05-10) 224 | 225 | **Note:** Version bump only for package @effect-http/node 226 | 227 | 228 | 229 | 230 | 231 | ## [0.24.2](https://github.com/tim-smart/effect-http/compare/v0.24.1...v0.24.2) (2023-05-01) 232 | 233 | **Note:** Version bump only for package @effect-http/node 234 | 235 | 236 | 237 | 238 | 239 | ## [0.24.1](https://github.com/tim-smart/effect-http/compare/v0.24.0...v0.24.1) (2023-04-30) 240 | 241 | **Note:** Version bump only for package @effect-http/node 242 | 243 | 244 | 245 | 246 | 247 | # [0.24.0](https://github.com/tim-smart/effect-http/compare/v0.23.0...v0.24.0) (2023-04-30) 248 | 249 | **Note:** Version bump only for package @effect-http/node 250 | 251 | 252 | 253 | 254 | 255 | # [0.23.0](https://github.com/tim-smart/effect-http/compare/v0.22.4...v0.23.0) (2023-04-30) 256 | 257 | **Note:** Version bump only for package @effect-http/node 258 | 259 | 260 | 261 | 262 | 263 | ## [0.22.4](https://github.com/tim-smart/effect-http/compare/v0.22.3...v0.22.4) (2023-04-27) 264 | 265 | **Note:** Version bump only for package @effect-http/node 266 | 267 | 268 | 269 | 270 | 271 | ## [0.22.3](https://github.com/tim-smart/effect-http/compare/v0.22.2...v0.22.3) (2023-04-27) 272 | 273 | **Note:** Version bump only for package @effect-http/node 274 | 275 | 276 | 277 | 278 | 279 | ## [0.22.2](https://github.com/tim-smart/effect-http/compare/v0.22.1...v0.22.2) (2023-04-26) 280 | 281 | **Note:** Version bump only for package @effect-http/node 282 | 283 | 284 | 285 | 286 | 287 | ## [0.22.1](https://github.com/tim-smart/effect-http/compare/v0.22.0...v0.22.1) (2023-04-26) 288 | 289 | **Note:** Version bump only for package @effect-http/node 290 | 291 | 292 | 293 | 294 | 295 | # 0.22.0 (2023-04-24) 296 | 297 | 298 | ### Bug Fixes 299 | 300 | * actually pipe request to busboy ([de941a0](https://github.com/tim-smart/effect-http/commit/de941a00b2c6d41185619d2bd0ca1f1b19c0976d)) 301 | * add baseUrl option to node runtime ([1b2eb17](https://github.com/tim-smart/effect-http/commit/1b2eb1774831bfcc3bbb53c0b0de6cedb5f0720d)) 302 | * bad imports ([1b693b4](https://github.com/tim-smart/effect-http/commit/1b693b477fb1d35dac3721be06a0748cc0d2adbb)) 303 | * better url extraction ([a189e6b](https://github.com/tim-smart/effect-http/commit/a189e6b303e05dc753e39e8308b9efb19351b2a6)) 304 | * handle failures ([bf85b53](https://github.com/tim-smart/effect-http/commit/bf85b532239aaa389fa63f1f6706510ffd789257)) 305 | * make MultipartOptions partial ([8f101c3](https://github.com/tim-smart/effect-http/commit/8f101c31e0614b06e4b9f58c3b0e428422b71489)) 306 | * missing import ([6763564](https://github.com/tim-smart/effect-http/commit/6763564fbdeceaf42f17abb1419a4f34095ac08f)) 307 | * **node:** client ParseOptions ([d7dd159](https://github.com/tim-smart/effect-http/commit/d7dd1593b2e89bb93c27275833ba6917a5894308)) 308 | * **node:** client schema ([472040b](https://github.com/tim-smart/effect-http/commit/472040b6d444e7e82a06a52abf106d1f6363a8b3)) 309 | * workspace deps ([1d44981](https://github.com/tim-smart/effect-http/commit/1d44981d7b6124418fd2ab2d3e36e6523616f630)) 310 | 311 | 312 | ### Features 313 | 314 | * add node runtime ([0bf8d9e](https://github.com/tim-smart/effect-http/commit/0bf8d9ec73c7471b895b11491d9ce8b41a0efe20)) 315 | * convert application/json parts to fields ([d79f80e](https://github.com/tim-smart/effect-http/commit/d79f80ec5f79e03e187e4dcd9b32f0e1508aa7d2)) 316 | * FormDataResponse in node runtime ([9315a25](https://github.com/tim-smart/effect-http/commit/9315a25a1199d4556c68fedd56219f954d0c0c42)) 317 | * html response method ([4b7eb1a](https://github.com/tim-smart/effect-http/commit/4b7eb1abdd0cd4493b4fd59478b5e3b3ca6173e6)) 318 | * **node:** add client executor ([9318776](https://github.com/tim-smart/effect-http/commit/93187762b1c75cf7d15ad282f37b8c76655a4127)) 319 | * **node:** FileResponse ([cd7f61d](https://github.com/tim-smart/effect-http/commit/cd7f61d10cf58cbd8e19266be4e0db2c7658bb9b)) 320 | * **node:** LiveNodeRequestExecutor* ([9af2dfa](https://github.com/tim-smart/effect-http/commit/9af2dfab5af1e3d6ec814cfecdf12bdcbf2822e9)) 321 | * **node:** make server creation lazy ([1121e54](https://github.com/tim-smart/effect-http/commit/1121e54c5810acc4cbb82409c8f8533cd267d55c)) 322 | * sandbox uploads per request ([776af26](https://github.com/tim-smart/effect-http/commit/776af266522c12f68beeabf6ff3e31b3876ebd64)) 323 | * start streaming multipart for node runtime ([b23856a](https://github.com/tim-smart/effect-http/commit/b23856a625615fdc2afb1995095e47aa103f21a5)) 324 | 325 | 326 | 327 | 328 | 329 | # [0.21.0](https://github.com/tim-smart/effect-http/compare/@effect-http/node@0.20.0...@effect-http/node@0.21.0) (2023-04-18) 330 | 331 | **Note:** Version bump only for package @effect-http/node 332 | 333 | 334 | 335 | 336 | 337 | # [0.20.0](https://github.com/tim-smart/effect-http/compare/@effect-http/node@0.3.2...@effect-http/node@0.20.0) (2023-04-11) 338 | 339 | 340 | ### Bug Fixes 341 | 342 | * **node:** client ParseOptions ([d7dd159](https://github.com/tim-smart/effect-http/commit/d7dd1593b2e89bb93c27275833ba6917a5894308)) 343 | * **node:** client schema ([472040b](https://github.com/tim-smart/effect-http/commit/472040b6d444e7e82a06a52abf106d1f6363a8b3)) 344 | * workspace deps ([1d44981](https://github.com/tim-smart/effect-http/commit/1d44981d7b6124418fd2ab2d3e36e6523616f630)) 345 | 346 | 347 | ### Features 348 | 349 | * **node:** add client executor ([9318776](https://github.com/tim-smart/effect-http/commit/93187762b1c75cf7d15ad282f37b8c76655a4127)) 350 | * **node:** LiveNodeRequestExecutor* ([9af2dfa](https://github.com/tim-smart/effect-http/commit/9af2dfab5af1e3d6ec814cfecdf12bdcbf2822e9)) 351 | -------------------------------------------------------------------------------- /packages/node/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tsc 2 | tsc: clean 3 | ./node_modules/.bin/tsc 4 | sed 's#/dist/#/#' package.json > dist/package.json 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf dist *.tsbuildinfo 9 | -------------------------------------------------------------------------------- /packages/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@effect-http/node", 3 | "version": "0.40.1", 4 | "publishConfig": { 5 | "access": "public", 6 | "directory": "dist" 7 | }, 8 | "exports": { 9 | ".": "./dist/index.js", 10 | "./*": "./dist/*.js" 11 | }, 12 | "scripts": { 13 | "clean": "make clean", 14 | "postinstall": "tsplus-install || true", 15 | "prepublishOnly": "make tsc", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "busboy": "^1.6.0", 23 | "raw-body": "^2.5.2" 24 | }, 25 | "devDependencies": { 26 | "@changesets/cli": "^2.26.2", 27 | "@effect-http/client": "workspace:*", 28 | "@effect-http/core": "workspace:*", 29 | "@effect/data": "^0.17.1", 30 | "@effect/io": "^0.38.0", 31 | "@effect/schema": "^0.33.0", 32 | "@effect/stream": "^0.34.0", 33 | "@tsplus-types/effect__data": "0.17.1-11739c0", 34 | "@tsplus-types/effect__io": "0.38.0-11739c0", 35 | "@tsplus-types/effect__schema": "0.32.0-11739c0", 36 | "@tsplus-types/effect__stream": "0.33.0-11739c0", 37 | "@tsplus/installer": "^0.0.178", 38 | "@types/busboy": "^1.5.0", 39 | "@types/node": "^20.4.7", 40 | "prettier": "^3.0.1", 41 | "typescript": "5.1.6" 42 | }, 43 | "sideEffects": false, 44 | "gitHead": "f285647e065f4b904e4d2165df54dd797a9c51fc", 45 | "peerDependencies": { 46 | "@effect-http/client": "workspace:*", 47 | "@effect-http/core": "workspace:*", 48 | "@effect/data": "^0.17.1", 49 | "@effect/io": "^0.38.0", 50 | "@effect/schema": "^0.33.0", 51 | "@effect/stream": "^0.34.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/node/src/Client.ts: -------------------------------------------------------------------------------- 1 | import * as Http from "@effect-http/client" 2 | import type { Option } from "@effect/data/Option" 3 | import type { Layer } from "@effect/io/Layer" 4 | import { ParseOptions } from "@effect/schema/AST" 5 | import * as S from "@effect/schema/Schema" 6 | import type { Stream } from "@effect/stream/Stream" 7 | import { IncomingMessage } from "http" 8 | import * as NodeHttp from "node:http" 9 | import * as NodeHttps from "node:https" 10 | import { Readable } from "node:stream" 11 | import { pipeline } from "node:stream/promises" 12 | import { LiveNodeAgent, NodeAgent } from "./internal/Agent.js" 13 | import * as IS from "./internal/stream.js" 14 | import * as Effect from "@effect/io/Effect" 15 | import { identity } from "@effect/data/Function" 16 | import * as ReadonlyArray from "@effect/data/ReadonlyArray" 17 | 18 | export const executeRaw: Http.executor.RequestExecutor< 19 | NodeAgent, 20 | Http.RequestError, 21 | Http.response.Response 22 | > = request => 23 | Do($ => { 24 | const agent = $(Effect.map(NodeAgent, identity)) 25 | 26 | const url = $( 27 | Effect.try({ 28 | try: () => new URL(request.url), 29 | catch: _ => new Http.RequestError(request, _), 30 | }), 31 | ) 32 | 33 | ReadonlyArray.forEach(request.urlParams, ([key, value]) => { 34 | url.searchParams.append(key, value) 35 | }) 36 | 37 | const response = $( 38 | executeRequest(agent, url, request.body, { 39 | method: request.method, 40 | headers: Object.fromEntries(request.headers), 41 | }).mapError(_ => new Http.RequestError(request, _)), 42 | ) 43 | 44 | return new ResponseImpl(response) 45 | }) 46 | 47 | /** 48 | * @tsplus getter effect-http/client/Request execute 49 | */ 50 | export const execute = executeRaw.filterStatus(_ => _ >= 200 && _ < 300) 51 | 52 | /** 53 | * @tsplus getter effect-http/client/Request executeJson 54 | */ 55 | export const executeJson = execute 56 | .contramap(_ => _.acceptJson) 57 | .mapEffect(_ => _.json) 58 | 59 | export const executeDecode = (schema: S.Schema) => 60 | execute.contramap(_ => _.acceptJson).mapEffect(_ => _.decode(schema)) 61 | 62 | /** 63 | * @tsplus pipeable effect-http/client/Request executeDecode 64 | */ 65 | export const executeDecode_: ( 66 | schema: S.Schema, 67 | ) => ( 68 | request: Http.Request, 69 | ) => Effect.Effect< 70 | NodeAgent, 71 | | Http.RequestError 72 | | Http.StatusCodeError 73 | | Http.ResponseDecodeError 74 | | Http.SchemaDecodeError, 75 | A 76 | > = executeDecode 77 | 78 | export const LiveNodeRequestExecutor = Layer.effect( 79 | Http.executor.HttpRequestExecutor, 80 | Do($ => { 81 | const agent = $(Effect.map(NodeAgent, identity)) 82 | 83 | return { 84 | execute: (request: Http.Request) => 85 | executeRaw(request).provideService(NodeAgent, agent), 86 | } 87 | }), 88 | ) 89 | 90 | export const LiveNodeRequestExecutorWithAgent = 91 | LiveNodeAgent >> LiveNodeRequestExecutor 92 | 93 | const executeRequest = ( 94 | { httpAgent, httpsAgent }: NodeAgent, 95 | url: URL, 96 | body: Option, 97 | options: NodeHttp.RequestOptions, 98 | ) => { 99 | const controller = new AbortController() 100 | 101 | const request = url.protocol.startsWith("https") 102 | ? NodeHttps.request(url, { 103 | ...options, 104 | agent: httpsAgent, 105 | signal: controller.signal, 106 | }) 107 | : NodeHttp.request(url, { 108 | ...options, 109 | agent: httpAgent, 110 | signal: controller.signal, 111 | }) 112 | 113 | const requestEffect = handleRequest(request) 114 | const bodyEffect = sendBody(request, body) 115 | 116 | return bodyEffect 117 | .zipRight(requestEffect, { concurrent: true }) 118 | .onInterrupt(() => 119 | Effect.sync(() => { 120 | controller.abort() 121 | }), 122 | ) 123 | } 124 | 125 | const handleRequest = (request: NodeHttp.ClientRequest) => 126 | Effect.async(resume => { 127 | request.on("response", response => { 128 | resume(Effect.succeed(response)) 129 | }) 130 | }) 131 | 132 | const sendBody = ( 133 | request: NodeHttp.ClientRequest, 134 | body: Option, 135 | ): Effect.Effect => { 136 | if (body._tag === "None") { 137 | request.end() 138 | return waitForFinish(request) 139 | } 140 | 141 | switch (body.value._tag) { 142 | case "RawBody": 143 | request.end(body.value.value) 144 | return waitForFinish(request) 145 | 146 | case "FormDataBody": 147 | return Do($ => { 148 | const response = new Response(body.value.value as FormData) 149 | 150 | response.headers.forEach((value, key) => { 151 | request.setHeader(key, value) 152 | }) 153 | 154 | return $( 155 | Effect.tryPromise({ 156 | try: () => 157 | pipeline(Readable.fromWeb(response.body! as any), request), 158 | catch: _ => _, 159 | }), 160 | ) 161 | }) 162 | 163 | case "StreamBody": 164 | return body.value.value.run(IS.sink(request)) 165 | } 166 | } 167 | 168 | const waitForFinish = (request: NodeHttp.ClientRequest) => 169 | Effect.async(resume => { 170 | request.on("error", error => { 171 | resume(Effect.fail(error)) 172 | }) 173 | 174 | request.on("finish", () => { 175 | resume(Effect.unit) 176 | }) 177 | }) 178 | 179 | export class ResponseImpl implements Http.response.Response { 180 | constructor(private readonly source: IncomingMessage) {} 181 | 182 | get status() { 183 | return this.source.statusCode! 184 | } 185 | 186 | get headers() { 187 | return new Headers(this.source.headers as any) 188 | } 189 | 190 | get text(): Effect.Effect { 191 | return IS.readableToString(this.source).mapError( 192 | _ => new Http.ResponseDecodeError(_.reason, this, "text"), 193 | ) 194 | } 195 | 196 | get json(): Effect.Effect { 197 | return IS.readableToString(this.source) 198 | .mapError(_ => new Http.ResponseDecodeError(_.reason, this, "json")) 199 | .flatMap(_ => 200 | Effect.try({ 201 | try: () => JSON.parse(_) as unknown, 202 | catch: _ => new Http.ResponseDecodeError(_, this, "json"), 203 | }), 204 | ) 205 | } 206 | 207 | get formData(): Effect.Effect { 208 | return Effect.fail( 209 | new Http.ResponseDecodeError("Not implemented", this, "formData"), 210 | ) 211 | } 212 | 213 | get stream(): Stream { 214 | return IS.fromReadable(this.source).mapError( 215 | _ => new Http.ResponseDecodeError(_, this, "stream"), 216 | ) 217 | } 218 | 219 | get blob(): Effect.Effect { 220 | return IS.readableToBuffer(this.source) 221 | .map(_ => new Blob([_])) 222 | .mapError(_ => new Http.ResponseDecodeError(_, this, "blob")) 223 | } 224 | 225 | decode( 226 | schema: S.Schema, 227 | options?: ParseOptions, 228 | ): Effect.Effect< 229 | never, 230 | Http.ResponseDecodeError | Http.SchemaDecodeError, 231 | A 232 | > { 233 | const parse = schema.parse 234 | return this.json.flatMap(_ => 235 | parse(_, options).mapError(_ => new Http.SchemaDecodeError(_, this)), 236 | ) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /packages/node/src/Server.ts: -------------------------------------------------------------------------------- 1 | import type { HttpApp } from "@effect-http/core" 2 | import { 3 | EarlyResponse, 4 | HttpResponse, 5 | HttpStreamError, 6 | } from "@effect-http/core/Response" 7 | import type { Effect } from "@effect/io/Effect" 8 | import * as Runtime from "@effect/io/Runtime" 9 | import { LazyArg } from "@effect/data/Function" 10 | import * as Http from "http" 11 | import type { ListenOptions } from "net" 12 | import { Readable } from "stream" 13 | import { NodeHttpRequest } from "./internal/Request.js" 14 | import { MultipartOptions } from "./internal/multipart.js" 15 | import * as S from "./internal/stream.js" 16 | import { HttpFs } from "@effect-http/core/internal/HttpFs" 17 | import { nodeHttpFsImpl } from "./internal/HttpFs.js" 18 | 19 | export type ServeOptions = ListenOptions & { 20 | port: number 21 | } & Partial 22 | 23 | export class NodeHttpError { 24 | readonly _tag = "NodeHttpError" 25 | constructor(readonly error: Error) {} 26 | } 27 | 28 | /** 29 | * @tsplus pipeable effect-http/HttpApp serve 30 | */ 31 | export const serve = 32 | (makeServer: LazyArg, options: ServeOptions) => 33 | (httpApp: HttpApp) => 34 | Effect.runtime() 35 | .flatMap(runtime => 36 | Effect.async(resume => { 37 | const server = makeServer() 38 | 39 | server.once("error", err => { 40 | resume(Effect.fail(new NodeHttpError(err))) 41 | }) 42 | 43 | server.on("request", (request, response) => { 44 | Runtime.runCallback(runtime)( 45 | httpApp(convertRequest(request, options)) 46 | .catchTag("EarlyResponse", _ => Effect.succeed(_.response)) 47 | .flatMap(_ => handleResponse(_, response)), 48 | 49 | exit => { 50 | if (exit.isFailure()) { 51 | if (!response.headersSent) { 52 | response.writeHead(500) 53 | } 54 | if (!response.writableEnded) { 55 | response.end() 56 | } 57 | 58 | Effect.logError(exit.cause).runFork 59 | } 60 | }, 61 | ) 62 | }) 63 | 64 | server.listen(options) 65 | 66 | return Effect.async(resume => { 67 | server.close(() => resume(Effect.unit)) 68 | }) 69 | }), 70 | ) 71 | .provideService(HttpFs, nodeHttpFsImpl) 72 | 73 | const convertRequest = ( 74 | source: Http.IncomingMessage, 75 | { 76 | port, 77 | limits = {}, 78 | multipartFieldTypes = ["application/json"], 79 | }: { port: number } & Partial, 80 | ) => { 81 | const url = requestUrl(source, port) 82 | return new NodeHttpRequest(source, url, url, { 83 | limits, 84 | multipartFieldTypes, 85 | }) 86 | } 87 | 88 | const handleResponse = ( 89 | source: HttpResponse, 90 | dest: Http.ServerResponse, 91 | ): Effect => { 92 | switch (source._tag) { 93 | case "EmptyResponse": 94 | return Effect(() => { 95 | dest.writeHead( 96 | source.status, 97 | source.headers 98 | ? Object.fromEntries(source.headers.entries()) 99 | : undefined, 100 | ) 101 | dest.end() 102 | }) 103 | 104 | case "RawResponse": 105 | return Effect(() => { 106 | if (source.headers) { 107 | dest.writeHead( 108 | source.status, 109 | Object.fromEntries(source.headers.entries()), 110 | ) 111 | } else { 112 | dest.writeHead(source.status) 113 | } 114 | dest.end(source.body) 115 | }) 116 | 117 | case "FormDataResponse": 118 | return Effect.async(resume => { 119 | const r = new Response(source.body) 120 | const headers = source.headers 121 | ? Object.fromEntries(source.headers.entries()) 122 | : {} 123 | headers["content-type"] = r.headers.get("content-type")! 124 | dest.writeHead(source.status, headers) 125 | Readable.fromWeb(r.body as any) 126 | .pipe(dest) 127 | .once("finish", () => { 128 | resume(Effect.unit) 129 | }) 130 | }) 131 | 132 | case "StreamResponse": 133 | return Effect(() => { 134 | dest.writeHead( 135 | source.status, 136 | Object.fromEntries(source.headers.entries()), 137 | ) 138 | }) 139 | .tap(() => source.body.run(S.sink(dest))) 140 | .catchTag("WritableError", _ => Effect.fail(new HttpStreamError(_))) 141 | } 142 | } 143 | 144 | const requestUrl = (source: Http.IncomingMessage, port: number) => { 145 | const proto = requestProtocol(source) 146 | const host = requestHost(source, port) 147 | 148 | return `${proto}://${host}${source.url}` 149 | } 150 | 151 | const requestProtocol = (source: Http.IncomingMessage) => { 152 | if ((source.socket as any).encrypted) { 153 | return "https" 154 | } else if (typeof source.headers["x-forwarded-proto"] === "string") { 155 | return source.headers["x-forwarded-proto"].trim() 156 | } 157 | 158 | return "http" 159 | } 160 | 161 | const requestHost = (source: Http.IncomingMessage, port: number) => { 162 | if (typeof source.headers["x-forwarded-host"] === "string") { 163 | return source.headers["x-forwarded-host"].trim() 164 | } else if (typeof source.headers["host"] === "string") { 165 | return source.headers["host"].trim() 166 | } 167 | 168 | return `localhost:${port}` 169 | } 170 | -------------------------------------------------------------------------------- /packages/node/src/_common.ts: -------------------------------------------------------------------------------- 1 | export type { Scope } from "@effect/io/Scope" 2 | export type { LazyArg } from "@effect/data/Function" 3 | -------------------------------------------------------------------------------- /packages/node/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./internal/HttpFs.js" 2 | export * from "./Server.js" 3 | 4 | export * as client from "./Client.js" 5 | export * from "./internal/Agent.js" 6 | -------------------------------------------------------------------------------- /packages/node/src/internal/Agent.ts: -------------------------------------------------------------------------------- 1 | import * as Http from "node:http" 2 | import * as Https from "node:https" 3 | import type { AgentOptions } from "node:https" 4 | import type { Effect } from "@effect/io/Effect" 5 | import type { Layer } from "@effect/io/Layer" 6 | import { Tag } from "@effect/data/Context" 7 | 8 | const makeAgent = (opts?: AgentOptions) => 9 | Effect.all({ 10 | httpAgent: Effect(() => new Http.Agent(opts)).acquireRelease(_ => 11 | Effect(() => _.destroy()), 12 | ), 13 | httpsAgent: Effect(() => new Https.Agent(opts)).acquireRelease(_ => 14 | Effect(() => _.destroy()), 15 | ), 16 | }) 17 | 18 | export interface NodeAgent 19 | extends Effect.Success> {} 20 | export const NodeAgent = Tag() 21 | 22 | export const makeAgentLayer = ( 23 | opts?: AgentOptions, 24 | ): Layer => Layer.scoped(NodeAgent, makeAgent(opts)) 25 | export const LiveNodeAgent = makeAgentLayer() 26 | -------------------------------------------------------------------------------- /packages/node/src/internal/HttpFs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpFs, 3 | HttpFsError, 4 | HttpFsNotFound, 5 | } from "@effect-http/core/internal/HttpFs" 6 | import { HttpResponse, HttpStreamError } from "@effect-http/core/Response" 7 | import * as Fs from "./fs.js" 8 | import type { Layer } from "@effect/io/Layer" 9 | 10 | export const nodeHttpFsImpl: HttpFs = { 11 | toResponse: (path, { status, range, contentType }) => 12 | Do($ => { 13 | const headers = new Headers() 14 | const stats = $(Fs.stat(path)) 15 | 16 | if (range) { 17 | const [start, end] = range 18 | headers.set("content-length", `${end - start}`) 19 | } else { 20 | headers.set("content-length", stats.size.toString()) 21 | } 22 | 23 | const stream = Fs.stream(path, { 24 | offset: range ? range[0] : undefined, 25 | bytesToRead: range ? range[1] - range[0] : undefined, 26 | }).mapError(e => new HttpStreamError(e)) 27 | 28 | return HttpResponse.stream(stream, { 29 | status, 30 | headers, 31 | contentType, 32 | }) 33 | }).mapError(e => 34 | e.error.code === "ENOENT" 35 | ? new HttpFsNotFound(path, e) 36 | : new HttpFsError(e), 37 | ), 38 | } 39 | 40 | export const NodeHttpFsLive = Layer.succeed(HttpFs, nodeHttpFsImpl) 41 | -------------------------------------------------------------------------------- /packages/node/src/internal/Request.ts: -------------------------------------------------------------------------------- 1 | import * as Effect from "@effect/io/Effect" 2 | import { HttpRequest, RequestBodyError } from "@effect-http/core/Request" 3 | import { IncomingMessage } from "http" 4 | import * as Body from "./body.js" 5 | import { fromReadable } from "./stream.js" 6 | import { Readable } from "stream" 7 | import * as MP from "./multipart.js" 8 | 9 | export class NodeHttpRequest implements HttpRequest { 10 | readonly text: Effect.Effect 11 | 12 | constructor( 13 | readonly source: IncomingMessage, 14 | readonly originalUrl: string, 15 | readonly url: string, 16 | readonly options: MP.MultipartOptions, 17 | ) { 18 | this.text = Body.utf8String(source, options.limits.fieldSize).mapError( 19 | e => new RequestBodyError(e), 20 | ).cached.runSync 21 | } 22 | 23 | get method() { 24 | return this.source.method! 25 | } 26 | 27 | get headers() { 28 | return new Headers(this.source.headers as any) 29 | } 30 | 31 | setUrl(url: string): HttpRequest { 32 | return new NodeHttpRequest(this.source, this.originalUrl, url, this.options) 33 | } 34 | 35 | get json() { 36 | return this.text.flatMap(_ => 37 | Effect.try({ 38 | try: () => JSON.parse(_) as unknown, 39 | catch: reason => new RequestBodyError(reason), 40 | }), 41 | ) 42 | } 43 | 44 | get formData(): any { 45 | return MP.formData(this.source, this.options) 46 | } 47 | 48 | get formDataStream(): any { 49 | return MP.fromRequest(this.source, this.options) 50 | } 51 | 52 | get stream() { 53 | return fromReadable(this.source).mapError( 54 | _ => new RequestBodyError(_), 55 | ) 56 | } 57 | 58 | get webStream() { 59 | return Readable.toWeb(this.source) as any 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/node/src/internal/body.ts: -------------------------------------------------------------------------------- 1 | import type { Effect } from "@effect/io/Effect" 2 | import * as Http from "http" 3 | import Body from "raw-body" 4 | 5 | export const utf8String = ( 6 | request: Http.IncomingMessage, 7 | limit = 1024 * 1024, 8 | ) => 9 | Effect.async((resume) => { 10 | Body( 11 | request, 12 | { 13 | encoding: "utf-8", 14 | limit, 15 | }, 16 | (err, body) => { 17 | if (err) { 18 | resume(Effect.fail(err)) 19 | } else { 20 | resume(Effect.succeed(body)) 21 | } 22 | }, 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/node/src/internal/fs.ts: -------------------------------------------------------------------------------- 1 | import * as Effect from "@effect/io/Effect" 2 | import * as Sink from "@effect/stream/Sink" 3 | import * as Stream from "@effect/stream/Stream" 4 | import { pipe } from "@effect/data/Function" 5 | import * as Option from "@effect/data/Option" 6 | import * as NFS from "fs" 7 | 8 | export class ErrnoError { 9 | readonly _tag = "ErrnoError" 10 | constructor(readonly error: NodeJS.ErrnoException) {} 11 | } 12 | 13 | export const DEFAULT_CHUNK_SIZE = 512 * 1024 14 | 15 | export class OpenError { 16 | readonly _tag = "OpenError" 17 | constructor(readonly error: unknown) {} 18 | } 19 | 20 | const unsafeOpen = (path: string, flags?: NFS.OpenMode, mode?: NFS.Mode) => 21 | Effect.async(resume => { 22 | try { 23 | NFS.open(path, flags, mode, (err, fd) => { 24 | if (err) { 25 | resume(Effect.fail(new ErrnoError(err))) 26 | } else { 27 | resume(Effect.succeed(fd)) 28 | } 29 | }) 30 | } catch (err) { 31 | resume(Effect.fail(new OpenError(err))) 32 | } 33 | }) 34 | 35 | const close = (fd: number) => 36 | Effect.async(resume => { 37 | NFS.close(fd, err => { 38 | if (err) { 39 | resume(Effect.fail(new ErrnoError(err))) 40 | } else { 41 | resume(Effect.unit) 42 | } 43 | }) 44 | }) 45 | 46 | export const open = (path: string, flags?: NFS.OpenMode, mode?: NFS.Mode) => 47 | Effect.acquireRelease(unsafeOpen(path, flags, mode), fd => 48 | Effect.ignoreLogged(close(fd)), 49 | ) 50 | 51 | export const stat = (path: string) => 52 | Effect.async(resume => { 53 | NFS.stat(path, (err, stats) => { 54 | if (err) { 55 | resume(Effect.fail(new ErrnoError(err))) 56 | } else { 57 | resume(Effect.succeed(stats)) 58 | } 59 | }) 60 | }) 61 | 62 | export const read = ( 63 | fd: number, 64 | buf: Uint8Array, 65 | offset: number, 66 | length: number, 67 | position: NFS.ReadPosition | null, 68 | ) => 69 | Effect.async(resume => { 70 | NFS.read(fd, buf, offset, length, position, (err, bytesRead) => { 71 | if (err) { 72 | resume(Effect.fail(new ErrnoError(err))) 73 | } else { 74 | resume(Effect.succeed(bytesRead)) 75 | } 76 | }) 77 | }) 78 | 79 | export const allocAndRead = ( 80 | fd: number, 81 | size: number, 82 | position: NFS.ReadPosition | null, 83 | ) => 84 | pipe( 85 | Effect.sync(() => Buffer.allocUnsafeSlow(size)), 86 | Effect.flatMap(buf => 87 | pipe( 88 | read(fd, buf, 0, size, position), 89 | Effect.map(bytesRead => { 90 | if (bytesRead === 0) { 91 | return Option.none() 92 | } 93 | 94 | if (bytesRead === size) { 95 | return Option.some([buf, bytesRead] as const) 96 | } 97 | 98 | const dst = Buffer.allocUnsafeSlow(bytesRead) 99 | buf.copy(dst, 0, 0, bytesRead) 100 | return Option.some([dst, bytesRead] as const) 101 | }), 102 | ), 103 | ), 104 | ) 105 | 106 | export interface StreamOptions { 107 | bufferSize?: number 108 | chunkSize?: number 109 | offset?: number 110 | bytesToRead?: number 111 | } 112 | 113 | export const stream = ( 114 | path: string, 115 | { 116 | bufferSize = 4, 117 | chunkSize = DEFAULT_CHUNK_SIZE, 118 | offset = 0, 119 | bytesToRead, 120 | }: StreamOptions = {}, 121 | ) => 122 | pipe( 123 | open(path, "r"), 124 | Effect.map(fd => 125 | Stream.unfoldEffect(offset, position => { 126 | if (bytesToRead !== undefined && bytesToRead <= position - offset) { 127 | return Effect.succeedNone 128 | } 129 | 130 | const toRead = 131 | bytesToRead !== undefined && 132 | bytesToRead - (position - offset) < chunkSize 133 | ? bytesToRead - (position - offset) 134 | : chunkSize 135 | 136 | return pipe( 137 | allocAndRead(fd, toRead, position), 138 | Effect.map( 139 | Option.map( 140 | ([buf, bytesRead]) => [buf, position + bytesRead] as const, 141 | ), 142 | ), 143 | ) 144 | }), 145 | ), 146 | Stream.unwrapScoped, 147 | Stream.bufferChunks({ capacity: bufferSize }), 148 | ) 149 | 150 | export const write = (fd: number, data: Uint8Array, offset?: number) => 151 | Effect.async(resume => { 152 | NFS.write(fd, data, offset, (err, written) => { 153 | if (err) { 154 | resume(Effect.fail(new ErrnoError(err))) 155 | } else { 156 | resume(Effect.succeed(written)) 157 | } 158 | }) 159 | }) 160 | 161 | export const writeAll = ( 162 | fd: number, 163 | data: Uint8Array, 164 | offset = 0, 165 | ): Effect.Effect => 166 | pipe( 167 | write(fd, data, offset), 168 | Effect.flatMap(bytesWritten => { 169 | const newOffset = offset + bytesWritten 170 | 171 | if (newOffset >= data.byteLength) { 172 | return Effect.unit 173 | } 174 | 175 | return writeAll(fd, data, newOffset) 176 | }), 177 | ) 178 | 179 | export const sink = ( 180 | path: string, 181 | flags: NFS.OpenMode = "w", 182 | mode?: NFS.Mode, 183 | ) => 184 | pipe( 185 | open(path, flags, mode), 186 | Effect.map(fd => Sink.forEach((_: Uint8Array) => writeAll(fd, _))), 187 | Sink.unwrapScoped, 188 | ) 189 | -------------------------------------------------------------------------------- /packages/node/src/internal/multipart.ts: -------------------------------------------------------------------------------- 1 | import BB from "busboy" 2 | import type { Effect } from "@effect/io/Effect" 3 | import { RequestBodyError } from "@effect-http/core/Request" 4 | import { IncomingMessage } from "http" 5 | import * as Stream from "@effect/stream/Stream" 6 | import { 7 | FormDataField, 8 | FormDataFile, 9 | FormDataFileError, 10 | FormDataPart, 11 | } from "@effect-http/core/multipart" 12 | import * as OS from "os" 13 | import * as NS from "stream/promises" 14 | import * as Path from "path" 15 | import * as NFS from "fs" 16 | import * as Crypto from "crypto" 17 | import { fromReadable, readableToString } from "./stream.js" 18 | 19 | export interface MultipartOptions { 20 | limits: BB.Limits 21 | /** 22 | * Convert files with this MIME type to FormData fields instead of File's 23 | */ 24 | multipartFieldTypes: string[] 25 | } 26 | 27 | export const fromRequest = ( 28 | source: IncomingMessage, 29 | { limits, multipartFieldTypes }: MultipartOptions, 30 | ) => { 31 | const make = Effect(() => BB({ headers: source.headers, limits })) 32 | .acquireRelease(_ => 33 | Effect(() => { 34 | _.removeAllListeners() 35 | 36 | if (!_.closed) { 37 | _.destroy() 38 | } 39 | }), 40 | ) 41 | .map(bb => 42 | Stream.async(emit => { 43 | bb.on("field", (name, value, info) => { 44 | emit.single(new FormDataField(name, info.mimeType, value)) 45 | }) 46 | 47 | bb.on("file", (name, stream, info) => { 48 | emit.single( 49 | new FormDataFile( 50 | name, 51 | info.filename, 52 | info.mimeType, 53 | fromReadable(() => stream).mapError( 54 | _ => 55 | new FormDataFileError(name, info.filename, info.mimeType, _), 56 | ), 57 | stream, 58 | ), 59 | ) 60 | }) 61 | 62 | bb.on("error", _ => { 63 | emit.fail(new RequestBodyError(_)) 64 | }) 65 | 66 | bb.on("finish", () => { 67 | emit.end() 68 | }) 69 | 70 | source.pipe(bb) 71 | }).mapEffect(part => 72 | part._tag === "FormDataFile" && 73 | multipartFieldTypes.some(_ => part.contentType.includes(_)) 74 | ? readableToString(part.source as any) 75 | .map(body => new FormDataField(part.key, part.contentType, body)) 76 | .mapError(_ => new RequestBodyError(_)) 77 | : Effect.succeed(part), 78 | ), 79 | ) 80 | 81 | return Stream.unwrapScoped(make) 82 | } 83 | 84 | export const formData = (source: IncomingMessage, opts: MultipartOptions) => 85 | fromRequest(source, opts).runFoldEffect(new FormData(), (formData, part) => { 86 | if (part._tag === "FormDataField") { 87 | formData.append(part.key, part.value) 88 | return Effect.succeed(formData) 89 | } 90 | 91 | return Do($ => { 92 | const dir = $(randomTmpDir.mapError(e => new RequestBodyError(e))) 93 | const path = Path.join(dir, part.name) 94 | 95 | formData.append(part.key, new Blob(), path) 96 | 97 | return $( 98 | Effect.tryPromise({ 99 | try: () => 100 | NS.pipeline(part.source as any, NFS.createWriteStream(path)), 101 | catch: reason => new RequestBodyError(reason), 102 | }).as(formData), 103 | ) 104 | }) 105 | }) 106 | 107 | const randomTmpDir = Effect.async( 108 | resume => { 109 | const random = Crypto.randomBytes(10).toString("hex") 110 | const dir = Path.join(OS.tmpdir(), random) 111 | 112 | NFS.mkdir(dir, err => { 113 | if (err) { 114 | resume(Effect.fail(err)) 115 | } else { 116 | resume(Effect.succeed(dir)) 117 | } 118 | }) 119 | }, 120 | ) 121 | -------------------------------------------------------------------------------- /packages/node/src/internal/stream.ts: -------------------------------------------------------------------------------- 1 | import type { Effect } from "@effect/io/Effect" 2 | import * as Sink from "@effect/stream/Sink" 3 | import * as Stream from "@effect/stream/Stream" 4 | import { LazyArg, pipe } from "@effect/data/Function" 5 | import * as Option from "@effect/data/Option" 6 | import { Readable, Writable } from "stream" 7 | 8 | export class ReadableError { 9 | readonly _tag = "ReadableError" 10 | constructor(readonly reason: Error) {} 11 | } 12 | 13 | export const fromReadable = (evaluate: LazyArg) => 14 | Effect(evaluate) 15 | .acquireRelease(stream => 16 | Effect(() => { 17 | stream.removeAllListeners() 18 | 19 | if (!stream.closed) { 20 | stream.destroy() 21 | } 22 | }), 23 | ) 24 | .map(stream => 25 | Stream.async(emit => { 26 | stream.once("error", err => { 27 | emit.fail(new ReadableError(err)) 28 | }) 29 | 30 | stream.once("end", () => { 31 | emit.end() 32 | }) 33 | 34 | stream.on("readable", () => { 35 | emit.single(stream) 36 | }) 37 | 38 | if (stream.readable) { 39 | emit.single(stream) 40 | } 41 | }, 0), 42 | ) 43 | .unwrapStreamScoped.flatMap(_ => Stream.repeatEffectOption(readChunk(_))) 44 | 45 | const readChunk = ( 46 | stream: Readable, 47 | ): Effect, A> => 48 | Effect(() => stream.read() as A | null).flatMap(_ => 49 | _ ? Effect.succeed(_) : Effect.fail(Option.none()), 50 | ) 51 | 52 | export const fromReadableEager = (evaluate: LazyArg) => 53 | Effect(evaluate) 54 | .acquireRelease(stream => 55 | Effect(() => { 56 | stream.removeAllListeners() 57 | 58 | if (!stream.closed) { 59 | stream.destroy() 60 | } 61 | }), 62 | ) 63 | .map(stream => 64 | Stream.async(emit => { 65 | stream.once("error", err => { 66 | emit.fail(new ReadableError(err)) 67 | }) 68 | 69 | stream.once("end", () => { 70 | emit.end() 71 | }) 72 | 73 | stream.on("data", _ => { 74 | emit.single(_) 75 | }) 76 | }), 77 | ).unwrapStreamScoped 78 | 79 | export type WritableSink = Sink.Sink 80 | 81 | export class WritableError { 82 | readonly _tag = "WritableError" 83 | constructor(readonly error: Error) {} 84 | } 85 | 86 | export interface SinkOptions { 87 | endOnExit?: boolean 88 | encoding?: BufferEncoding 89 | } 90 | 91 | export const sink = ( 92 | evaluate: LazyArg, 93 | { endOnExit = true, encoding = "binary" }: SinkOptions = {}, 94 | ): WritableSink => 95 | pipe( 96 | Effect.sync(evaluate) 97 | .acquireRelease(endOnExit ? end : () => Effect.unit) 98 | .map(_ => makeSink(_, encoding)), 99 | Sink.unwrapScoped, 100 | ) 101 | 102 | const end = (stream: Writable) => 103 | Effect.async(resume => { 104 | if (stream.closed) { 105 | resume(Effect.unit) 106 | return 107 | } 108 | 109 | stream.end(() => resume(Effect.unit)) 110 | }) 111 | 112 | const makeSink = (stream: Writable, encoding: BufferEncoding) => 113 | Sink.forEach(write(stream, encoding)) 114 | 115 | const write = 116 | (stream: Writable, encoding: BufferEncoding) => 117 | (_: A) => 118 | Effect.async(resume => { 119 | stream.write(_, encoding, err => { 120 | if (err) { 121 | resume(Effect.fail(new WritableError(err))) 122 | } else { 123 | resume(Effect.unit) 124 | } 125 | }) 126 | }) 127 | 128 | export const readableToString = (stream: Readable) => { 129 | stream.setEncoding("utf-8") 130 | return fromReadableEager(stream).runFold("", (a, b) => `${a}${b}`) 131 | } 132 | 133 | export const readableToBuffer = (stream: Readable) => { 134 | return fromReadableEager(stream).runCollect.map(_ => 135 | Buffer.concat(_.toReadonlyArray), 136 | ) 137 | } 138 | -------------------------------------------------------------------------------- /packages/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "moduleResolution": "Node16", 10 | "module": "Node16", 11 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 12 | "sourceMap": true, 13 | "declaration": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "target": "ES2022", 17 | "incremental": true, 18 | "tsPlusConfig": "./tsplus.config.json", 19 | "paths": { 20 | "@effect-http/node": ["./src/index.js"], 21 | "@effect-http/node/*": ["./src/*.js"] 22 | }, 23 | "tsBuildInfoFile": "./tsconfig.tsbuildinfo" 24 | }, 25 | "include": ["src/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/node/tsplus.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": { 3 | "^(.*)/src/(.*)\\.(ts|js)$": "@effect-http/node/$2" 4 | }, 5 | "traceMap": { 6 | "^(.*)/src/(.*)\\.(ts|js)$": "(@effect-http/node) $2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | --------------------------------------------------------------------------------