├── .gitignore ├── .github └── workflows │ └── test.yml ├── gleam.toml ├── package.json ├── README.md ├── src ├── httpp_ffi.erl └── httpp │ ├── send.gleam │ ├── async.gleam │ ├── sse.gleam │ ├── hackney.gleam │ └── streaming.gleam ├── test └── httpp_test.gleam ├── manifest.toml ├── test-server.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | node_modules 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: erlef/setup-beam@v1 16 | with: 17 | otp-version: "26.0.2" 18 | gleam-version: "1.2.1" 19 | rebar3-version: "3" 20 | # elixir-version: "1.15.4" 21 | - run: gleam deps download 22 | - run: gleam test 23 | - run: gleam format --check src test 24 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "httpp" 2 | version = "1.1.0" 3 | 4 | # Fill out these fields if you intend to generate HTML documentation or publish 5 | # your project to the Hex package manager. 6 | # 7 | description = "an http client for gleam which supports streaming, based on hackney" 8 | licences = ["Apache-2.0"] 9 | repository = { type = "github", user = "VioletBuse", repo = "httpp" } 10 | links = [{ title = "Website", href = "https://gleam.run" }] 11 | # 12 | # For a full reference of all the available options, you can have a look at 13 | # https://gleam.run/writing-gleam/gleam-toml/. 14 | 15 | [dependencies] 16 | gleam_stdlib = "~> 0.32" 17 | gleam_http = ">= 3.6.0 and < 4.0.0" 18 | hackney = "~> 1.18" 19 | gleam_otp = ">= 0.10.0 and < 1.0.0" 20 | gleam_erlang = ">= 0.25.0 and < 1.0.0" 21 | 22 | [dev-dependencies] 23 | gleeunit = ">= 1.0.0 and < 2.0.0" 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "httpp", 3 | "version": "1.0.0", 4 | "description": "[![Package Version](https://img.shields.io/hexpm/v/http_stream)](https://hex.pm/packages/http_stream) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/http_stream/)", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "npm run test-server & npm run gleam-test", 11 | "gleam-test": "wait-on http://localhost:1337 && gleam test", 12 | "test-server": "ts-node-dev test-server.ts" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@types/express": "^4.17.21", 18 | "@types/node": "^20.14.2", 19 | "concurrently": "^8.2.2", 20 | "express": "^4.19.2", 21 | "ts-node": "^10.9.2", 22 | "ts-node-dev": "^2.0.0", 23 | "typescript": "^5.4.5", 24 | "wait-on": "^7.2.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpp 2 | 3 | [![Package Version](https://img.shields.io/hexpm/v/httpp)](https://hex.pm/packages/httpp) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/httpp/) 5 | 6 | ```sh 7 | gleam add httpp 8 | ``` 9 | ```gleam 10 | import httpp/async 11 | import gleam/http/request 12 | import gleam/uri 13 | import gleam/bytes_builder 14 | import gleam/erlang/process 15 | 16 | pub fn main() { 17 | let response_subject = uri.parse("https://example.com") 18 | |> request.from_uri 19 | |> request.map(bytes_builder.from_string) 20 | |> async.send 21 | 22 | // asynchronously send an http request and receive it later 23 | process.receive(response_subject, 2000) 24 | } 25 | ``` 26 | 27 | Further documentation can be found at . 28 | 29 | ## Development 30 | 31 | ```sh 32 | gleam run # Run the project 33 | gleam test # Run the tests 34 | gleam shell # Run an Erlang shell 35 | ``` 36 | -------------------------------------------------------------------------------- /src/httpp_ffi.erl: -------------------------------------------------------------------------------- 1 | -module(httpp_ffi). 2 | 3 | -export([send/5, insert_selector_handler/3]). 4 | 5 | send(Method, Url, Headers, Body, Options) -> 6 | case hackney:request(Method, Url, Headers, Body, Options) of 7 | {ok, Status, ResponseHeaders, <>} -> 8 | {ok, {binary_response, Status, ResponseHeaders, Binary}}; 9 | 10 | {ok, Status, ResponseHeaders, ClientRef} -> 11 | {ok, {client_ref_response, Status, ResponseHeaders, ClientRef}}; 12 | 13 | {ok, Status, ResponseHeaders} -> 14 | {ok, {empty_response, Status, ResponseHeaders}}; 15 | 16 | {ok, ClientRef} -> 17 | {ok, {async_response, ClientRef}}; 18 | 19 | {error, {closed, PartialBody}} -> 20 | {error, {connection_closed, PartialBody}}; 21 | 22 | {error, Error} -> 23 | {error, {other, Error}} 24 | end. 25 | 26 | insert_selector_handler({selector, Handlers}, Tag, Fn) -> 27 | {selector, Handlers#{Tag => Fn}}. 28 | -------------------------------------------------------------------------------- /test/httpp_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/bytes_builder 2 | import gleam/erlang/process 3 | import gleam/http/request 4 | import gleam/list 5 | import gleam/option 6 | import gleam/uri 7 | import gleeunit 8 | import gleeunit/should 9 | import httpp/sse 10 | 11 | pub fn main() { 12 | gleeunit.main() 13 | } 14 | 15 | fn receive_all( 16 | subject: process.Subject(sse.SSEEvent), 17 | rest, 18 | ) -> Result(List(sse.SSEEvent), Nil) { 19 | case process.receive(subject, 5000) { 20 | Ok(sse.Closed) -> Ok(list.concat([rest, []])) 21 | Ok(sse.Event(..) as event) -> 22 | receive_all(subject, list.concat([rest, [event]])) 23 | _ -> Error(Nil) 24 | } 25 | } 26 | 27 | pub fn sse_mixture_test() { 28 | let assert Ok(uri) = uri.parse("http://localhost:1773/sse/with-mixture") 29 | let assert Ok(request) = request.from_uri(uri) 30 | 31 | let req = 32 | request.set_header(request, "connection", "keep-alive") 33 | |> request.map(bytes_builder.from_string) 34 | 35 | let subject = process.new_subject() 36 | let _ = sse.event_source(req, subject) 37 | 38 | let events = receive_all(subject, []) 39 | 40 | should.equal( 41 | events, 42 | Ok([ 43 | sse.Event(option.Some("event-1"), "0"), 44 | sse.Event( 45 | option.None, 46 | "line one of data\nline two of data\nline three of data", 47 | ), 48 | sse.Event(option.Some("event-3"), "hello\nworld"), 49 | ]), 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/httpp/send.gleam: -------------------------------------------------------------------------------- 1 | /// This is a regular send request module 2 | import gleam/bit_array 3 | import gleam/bytes_builder.{type BytesBuilder} 4 | import gleam/http.{type Header} 5 | import gleam/http/request.{type Request} 6 | import gleam/http/response.{type Response, Response} 7 | import gleam/io 8 | import gleam/list 9 | import gleam/result 10 | import gleam/string 11 | import gleam/uri 12 | import httpp/hackney 13 | 14 | fn process_headers(list: List(Header)) -> List(Header) { 15 | list.map(list, fn(header) { #(string.lowercase(header.0), header.1) }) 16 | } 17 | 18 | pub fn send_bits( 19 | req: Request(BytesBuilder), 20 | ) -> Result(Response(BitArray), hackney.Error) { 21 | use response <- result.then( 22 | req 23 | |> request.to_uri 24 | |> uri.to_string 25 | |> hackney.send( 26 | req.method, 27 | _, 28 | req.headers, 29 | req.body, 30 | [hackney.WithBody(True)], 31 | ), 32 | ) 33 | 34 | let result = case response { 35 | hackney.ClientRefResponse(status, headers, client_ref) -> { 36 | case hackney.body(client_ref) { 37 | Ok(body_bits) -> Ok(#(status, process_headers(headers), body_bits)) 38 | Error(err) -> Error(err) 39 | } 40 | } 41 | hackney.BinaryResponse(status, headers, binary) -> 42 | Ok(#(status, process_headers(headers), binary)) 43 | hackney.EmptyResponse(status, headers) -> 44 | Ok(#(status, process_headers(headers), <<>>)) 45 | received -> { 46 | io.debug(received) 47 | panic as "received invalid response from hackney with {with_body, true}" 48 | } 49 | } 50 | 51 | use #(status, headers, binary) <- result.try(result) 52 | 53 | Ok(Response(status, headers, binary)) 54 | } 55 | 56 | pub fn send(req: Request(String)) -> Result(Response(String), hackney.Error) { 57 | use response <- result.then( 58 | req 59 | |> request.map(bytes_builder.from_string) 60 | |> send_bits, 61 | ) 62 | 63 | case bit_array.to_string(response.body) { 64 | Ok(body) -> Ok(response.set_body(response, body)) 65 | Error(_) -> Error(hackney.InvalidUtf8Response) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "certifi", version = "2.12.0", build_tools = ["rebar3"], requirements = [], otp_app = "certifi", source = "hex", outer_checksum = "EE68D85DF22E554040CDB4BE100F33873AC6051387BAF6A8F6CE82272340FF1C" }, 6 | { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 7 | { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, 8 | { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, 9 | { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, 10 | { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 11 | { name = "hackney", version = "1.20.1", build_tools = ["rebar3"], requirements = ["certifi", "idna", "metrics", "mimerl", "parse_trans", "ssl_verify_fun", "unicode_util_compat"], otp_app = "hackney", source = "hex", outer_checksum = "FE9094E5F1A2A2C0A7D10918FEE36BFEC0EC2A979994CFF8CFE8058CD9AF38E3" }, 12 | { name = "idna", version = "6.1.1", build_tools = ["rebar3"], requirements = ["unicode_util_compat"], otp_app = "idna", source = "hex", outer_checksum = "92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA" }, 13 | { name = "metrics", version = "1.0.1", build_tools = ["rebar3"], requirements = [], otp_app = "metrics", source = "hex", outer_checksum = "69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16" }, 14 | { name = "mimerl", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "mimerl", source = "hex", outer_checksum = "A1E15A50D1887217DE95F0B9B0793E32853F7C258A5CD227650889B38839FE9D" }, 15 | { name = "parse_trans", version = "3.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "parse_trans", source = "hex", outer_checksum = "620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A" }, 16 | { name = "ssl_verify_fun", version = "1.1.7", build_tools = ["mix", "rebar3", "make"], requirements = [], otp_app = "ssl_verify_fun", source = "hex", outer_checksum = "FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8" }, 17 | { name = "unicode_util_compat", version = "0.7.0", build_tools = ["rebar3"], requirements = [], otp_app = "unicode_util_compat", source = "hex", outer_checksum = "25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521" }, 18 | ] 19 | 20 | [requirements] 21 | gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" } 22 | gleam_http = { version = ">= 3.6.0 and < 4.0.0" } 23 | gleam_otp = { version = ">= 0.10.0 and < 1.0.0" } 24 | gleam_stdlib = { version = "~> 0.32" } 25 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 26 | hackney = { version = "~> 1.18" } 27 | -------------------------------------------------------------------------------- /test-server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const app = express(); 4 | 5 | app.head("/", async (_, res) => { 6 | res.status(200).end(); 7 | }); 8 | 9 | app.get("/", async (_, res) => { 10 | res.status(200).send("hello world"); 11 | }); 12 | 13 | app.get("/json", async (_, res) => { 14 | res.status(200).send(JSON.stringify({ message: "hello world" })); 15 | }); 16 | 17 | app.get("/stream", async (_, res) => { 18 | res.status(200); 19 | res.setHeader("Connection", "keep-alive"); 20 | res.flushHeaders(); 21 | 22 | let counter = 0; 23 | const interval = setInterval(() => { 24 | if (counter >= 10) { 25 | clearInterval(interval); 26 | res.write("done"); 27 | res.end(); 28 | return; 29 | } 30 | 31 | res.write(counter.toString() + "\n"); 32 | counter++; 33 | }, 200); 34 | 35 | res.on("close", () => { 36 | clearInterval(interval); 37 | res.end(); 38 | }); 39 | }); 40 | 41 | app.get("/stream/json", async (_, res) => { 42 | res.status(200); 43 | res.setHeader("Connection", "keep-alive"); 44 | res.flushHeaders(); 45 | 46 | let counter = 0; 47 | const interval = setInterval(() => { 48 | if (counter >= 10) { 49 | clearInterval(interval); 50 | res.write(JSON.stringify({ done: true }) + "\n"); 51 | res.end(); 52 | return; 53 | } 54 | 55 | res.write(JSON.stringify({ counter }) + "\n"); 56 | counter++; 57 | }, 200); 58 | 59 | res.on("close", () => { 60 | clearInterval(interval); 61 | res.end(); 62 | }); 63 | }); 64 | 65 | app.get("/sse", async (_, res) => { 66 | res.status(200); 67 | res.setHeader("Connection", "keep-alive"); 68 | res.flushHeaders(); 69 | 70 | let counter = 0; 71 | const interval = setInterval(() => { 72 | if (counter >= 10) { 73 | clearInterval(interval); 74 | res.end(); 75 | return; 76 | } 77 | 78 | res.write(`data: ${counter.toString()}\n\n`); 79 | counter++; 80 | }, 200); 81 | 82 | res.on("close", () => { 83 | clearInterval(interval); 84 | res.end(); 85 | }); 86 | }); 87 | 88 | app.get("/sse/with-event-type", async (_, res) => { 89 | res.status(200); 90 | res.setHeader("Connection", "keep-alive"); 91 | res.flushHeaders(); 92 | 93 | let counter = 0; 94 | const interval = setInterval(() => { 95 | if (counter >= 10) { 96 | clearInterval(interval); 97 | res.end(); 98 | return; 99 | } 100 | 101 | res.write(`event: counter-event\ndata: ${counter.toString()}\n\n`); 102 | counter++; 103 | }, 200); 104 | 105 | res.on("close", () => { 106 | clearInterval(interval); 107 | res.end(); 108 | }); 109 | }); 110 | 111 | app.get("/sse/with-multiline-data", async (_, res) => { 112 | res.status(200); 113 | res.setHeader("Connection", "keep-alive"); 114 | res.flushHeaders(); 115 | 116 | let counter = 0; 117 | const interval = setInterval(() => { 118 | if (counter >= 10) { 119 | clearInterval(interval); 120 | res.end(); 121 | return; 122 | } 123 | 124 | res.write( 125 | `event: counter-event\ndata: ${counter.toString()}\ndata: line_1\ndata: line_2\n\n`, 126 | ); 127 | counter++; 128 | }, 200); 129 | 130 | res.on("close", () => { 131 | clearInterval(interval); 132 | res.end(); 133 | }); 134 | }); 135 | 136 | app.get("/sse/with-mixture", async (_, res) => { 137 | res.status(200); 138 | res.setHeader("Connection", "keep-alive"); 139 | res.flushHeaders(); 140 | 141 | res.write(": a silly lil comment\n\n"); 142 | 143 | res.write(":comment-1\n:comment-2\n\n"); 144 | 145 | res.write(`event: event-1\ndata: 0\n\n`); 146 | res.write( 147 | `data: line one of data\ndata: line two of data\ndata: line three of data\n\n`, 148 | ); 149 | res.write(`event: event-3\n: comment-1\ndata: hello\ndata: world\n\n`); 150 | 151 | res.end(); 152 | }); 153 | 154 | app.listen(1773); 155 | -------------------------------------------------------------------------------- /src/httpp/async.gleam: -------------------------------------------------------------------------------- 1 | /// Use the async module if you want to fetch data asynchronously 2 | /// 3 | /// ```gleam 4 | /// 5 | /// fn fetch() { 6 | /// let subject = uri.from_string("https://example.com") 7 | /// |> request.from_uri() 8 | /// |> async.send_async 9 | /// 10 | /// let response = process.receive(subject, 1000) 11 | /// 12 | /// } 13 | /// 14 | /// ``` 15 | /// 16 | import gleam/bool 17 | import gleam/bytes_builder.{type BytesBuilder} 18 | import gleam/erlang/process.{type Subject} 19 | import gleam/http.{type Header} 20 | import gleam/http/request.{type Request} 21 | import gleam/http/response.{type Response, Response} 22 | import gleam/list 23 | import gleam/result 24 | import gleam/string 25 | import gleam/uri 26 | import httpp/hackney 27 | 28 | fn process_headers(list: List(Header)) -> List(Header) { 29 | list.map(list, fn(header) { #(string.lowercase(header.0), header.1) }) 30 | } 31 | 32 | fn loop( 33 | response_subject: Subject(Response(BitArray)), 34 | messages: List(hackney.HttppMessage), 35 | ) { 36 | process.new_selector() 37 | |> hackney.selecting_http_message(mapping: fn(_, message) { 38 | case message { 39 | hackney.DoneStreaming -> { 40 | let response: Response(BitArray) = { 41 | let see_other = 42 | list.find(messages, fn(message) { 43 | case message { 44 | hackney.SeeOther(..) -> True 45 | _ -> False 46 | } 47 | }) 48 | 49 | use <- bool.lazy_guard(when: result.is_ok(see_other), return: fn() { 50 | let assert Ok(hackney.SeeOther(location, headers)) = see_other 51 | let headers = 52 | list.concat([process_headers(headers), [#("location", location)]]) 53 | Response(303, headers, <<>>) 54 | }) 55 | 56 | let redirect = 57 | list.find(messages, fn(message) { 58 | case message { 59 | hackney.Redirect(..) -> True 60 | _ -> False 61 | } 62 | }) 63 | 64 | use <- bool.lazy_guard(when: result.is_ok(redirect), return: fn() { 65 | let assert Ok(hackney.Redirect(location, headers)) = redirect 66 | let headers = 67 | list.concat([process_headers(headers), [#("location", location)]]) 68 | Response(302, headers, <<>>) 69 | }) 70 | 71 | let #(status, headers, body_builder) = 72 | list.fold( 73 | messages, 74 | #(404, [], bytes_builder.new()), 75 | fn(acc, message) { 76 | case message { 77 | hackney.Status(new_status) -> #(new_status, acc.1, acc.2) 78 | hackney.Headers(headers) -> #(acc.0, headers, acc.2) 79 | hackney.Binary(bin) -> #( 80 | acc.0, 81 | acc.1, 82 | bytes_builder.append(acc.2, bin), 83 | ) 84 | _ -> acc 85 | } 86 | }, 87 | ) 88 | 89 | Response( 90 | status, 91 | process_headers(headers), 92 | bytes_builder.to_bit_array(body_builder), 93 | ) 94 | } 95 | 96 | process.send(response_subject, response) 97 | } 98 | other_message -> 99 | loop(response_subject, list.concat([messages, [other_message]])) 100 | } 101 | }) 102 | |> process.select_forever 103 | } 104 | 105 | /// Send a request and receive a subject which you can receive to get the response 106 | pub fn send( 107 | req: Request(BytesBuilder), 108 | ) -> Result(Subject(Response(BitArray)), hackney.Error) { 109 | let subject = process.new_subject() 110 | 111 | let receiving_process = process.start(fn() { loop(subject, []) }, False) 112 | 113 | use _ <- result.then( 114 | req 115 | |> request.to_uri 116 | |> uri.to_string 117 | |> hackney.send( 118 | req.method, 119 | _, 120 | req.headers, 121 | req.body, 122 | [hackney.Async, hackney.StreamTo(receiving_process)], 123 | ), 124 | ) 125 | 126 | Ok(subject) 127 | } 128 | -------------------------------------------------------------------------------- /src/httpp/sse.gleam: -------------------------------------------------------------------------------- 1 | import gleam/bit_array 2 | import gleam/bytes_builder.{type BytesBuilder} 3 | import gleam/erlang/process.{type ExitReason, type Subject} 4 | import gleam/http/request 5 | import gleam/http/response.{type Response} 6 | import gleam/list 7 | import gleam/option.{type Option, None, Some} 8 | import gleam/string 9 | import httpp/hackney 10 | import httpp/streaming 11 | 12 | pub type SSEEvent { 13 | Event(event_type: Option(String), data: String) 14 | Closed 15 | } 16 | 17 | type InternalState { 18 | InternalState(current: String) 19 | } 20 | 21 | pub type SSEManagerMessage { 22 | Shutdown 23 | } 24 | 25 | fn create_on_data( 26 | event_subject: Subject(SSEEvent), 27 | ) -> fn(streaming.Message, Response(Nil), InternalState) -> 28 | Result(InternalState, ExitReason) { 29 | fn(message, response, state) { 30 | case message { 31 | streaming.Bits(bits) -> handle_bits(event_subject, bits, response, state) 32 | streaming.Done -> { 33 | process.send(event_subject, Closed) 34 | Error(process.Normal) 35 | } 36 | } 37 | } 38 | } 39 | 40 | type EventComponents { 41 | Data(String) 42 | EventType(String) 43 | Comment(String) 44 | Invalid 45 | } 46 | 47 | // fn process_string( 48 | // input: String, 49 | // current: Option(#(Option(String), String)), 50 | // ) -> #(List(SSEEvent), String) { 51 | // case input 52 | // } 53 | 54 | fn handle_bits( 55 | event_subject: Subject(SSEEvent), 56 | bits: BitArray, 57 | _response: Response(Nil), 58 | state: InternalState, 59 | ) -> Result(InternalState, ExitReason) { 60 | case bit_array.to_string(bits) { 61 | Error(_) -> 62 | Error(process.Abnormal("Server sent bits could not be read as string")) 63 | Ok(stringified) -> { 64 | let full_str = state.current <> stringified 65 | let split_vals = string.split(full_str, "\n\n") 66 | 67 | let event_candidates = list.take(split_vals, list.length(split_vals) - 1) 68 | let assert Ok(new_current) = list.last(split_vals) 69 | 70 | let events = 71 | event_candidates 72 | |> list.map(string.split(_, "\n")) 73 | |> list.map(list.map(_, fn(line) { 74 | case line { 75 | ":" <> comment -> Comment(comment) 76 | "data: " <> data -> Data(data) 77 | "event: " <> event_type -> EventType(event_type) 78 | _ -> Invalid 79 | } 80 | })) 81 | |> list.filter(list.any(_, fn(component) { 82 | case component { 83 | Comment(..) -> False 84 | _ -> True 85 | } 86 | })) 87 | |> list.map(list.fold( 88 | _, 89 | #(None, ""), 90 | fn(acc, component) { 91 | case component { 92 | Invalid | Comment(..) -> acc 93 | EventType(event_type) -> #(Some(event_type), acc.1) 94 | Data(data) -> 95 | case acc.1 { 96 | "" -> #(acc.0, data) 97 | prefix -> #(acc.0, prefix <> "\n" <> data) 98 | } 99 | } 100 | }, 101 | )) 102 | |> list.map(fn(tuple) { Event(tuple.0, tuple.1) }) 103 | 104 | list.each(events, fn(event) { process.send(event_subject, event) }) 105 | 106 | Ok(InternalState(new_current)) 107 | } 108 | } 109 | } 110 | 111 | fn create_on_message( 112 | _event_subject: Subject(SSEEvent), 113 | ) -> fn(SSEManagerMessage, Response(Nil), InternalState) -> 114 | Result(InternalState, ExitReason) { 115 | fn(_, _, state) { Ok(state) } 116 | } 117 | 118 | fn create_on_error( 119 | _event_subject: Subject(SSEEvent), 120 | ) -> fn(hackney.Error, Option(Response(Nil)), InternalState) -> 121 | Result(InternalState, ExitReason) { 122 | fn(_, _, _) { Error(process.Abnormal("sse handler received an error")) } 123 | } 124 | 125 | /// Send a request to a server-sent events endpoint, and receive events 126 | /// back on a subject you provide 127 | pub fn event_source( 128 | req: request.Request(BytesBuilder), 129 | subject: Subject(SSEEvent), 130 | ) { 131 | let new_request = 132 | req 133 | |> request.set_header("connection", "keep-alive") 134 | 135 | streaming.start(streaming.StreamingRequestHandler( 136 | req: new_request, 137 | initial_state: InternalState(""), 138 | on_data: create_on_data(subject), 139 | on_message: create_on_message(subject), 140 | on_error: create_on_error(subject), 141 | initial_response_timeout: 2000, 142 | )) 143 | } 144 | -------------------------------------------------------------------------------- /src/httpp/hackney.gleam: -------------------------------------------------------------------------------- 1 | //// This module contains the base code to interact with hackney in a low-level manner 2 | 3 | import gleam/bit_array 4 | import gleam/bool 5 | import gleam/bytes_builder.{type BytesBuilder} 6 | import gleam/dynamic.{type Dynamic} 7 | import gleam/erlang/atom 8 | import gleam/erlang/process.{type Selector} 9 | import gleam/http.{type Header, type Method} 10 | import gleam/result 11 | 12 | pub type Error { 13 | /// Hackney Error Type 14 | Other(Dynamic) 15 | /// Error returned when the connection is unexpectedly closed 16 | ConnectionClosed(partial_body: BitArray) 17 | TimedOut 18 | NoStatusOrHeaders 19 | /// could not decode BitArray to string 20 | InvalidUtf8Response 21 | /// when expecting a client ref, we did not get one back 22 | NoClientRefReturned 23 | /// when the client has already received a message and doesn't expect the message 24 | UnexpectedServerMessage(HttppMessage) 25 | /// when the client receives a message that it can't decode 26 | MessageNotDecoded(Dynamic) 27 | } 28 | 29 | /// A hackney client_ref 30 | pub type ClientRef 31 | 32 | /// Response of the hackney http client 33 | pub type HackneyResponse { 34 | /// Received on response when neither `WithBody` or `Async` are used 35 | ClientRefResponse(status: Int, headers: List(Header), client_ref: ClientRef) 36 | /// Received when you use the `WithBody(True)` Option 37 | BinaryResponse(status: Int, headers: List(Header), body: BitArray) 38 | /// This is received on a HEAD request when response succeeded 39 | EmptyResponse(status: Int, headers: List(Header)) 40 | /// This is received when used with the option Async 41 | /// You can use the passed in client ref to disambiguate messages received 42 | AsyncResponse(client_ref: ClientRef) 43 | } 44 | 45 | pub type Options { 46 | /// Receive a binary response 47 | WithBody(Bool) 48 | /// If using `WithBody(True)`, set maximum body size 49 | MaxBody(Int) 50 | /// Receive a `ClientRef` back 51 | Async 52 | /// Receive the response as message, use the function `selecting_http_message` 53 | StreamTo(process.Pid) 54 | /// Follow redirects, this enables the messages `Redirect` and `SeeOther` 55 | FollowRedirect(Bool) 56 | /// Max number of redirects 57 | MaxRedirect(Int) 58 | /// Basic auth username/password 59 | BasicAuth(BitArray, BitArray) 60 | } 61 | 62 | /// Send hackney a request, this is basically the direct 63 | @external(erlang, "httpp_ffi", "send") 64 | pub fn send( 65 | a: Method, 66 | b: String, 67 | c: List(http.Header), 68 | d: BytesBuilder, 69 | e: List(Options), 70 | ) -> Result(HackneyResponse, Error) 71 | 72 | @external(erlang, "hackney", "body") 73 | fn body_ffi(a: ClientRef) -> Result(BitArray, Error) 74 | 75 | /// retrieve the full body from a client_ref 76 | pub fn body(ref client_ref: ClientRef) -> Result(BitArray, Error) { 77 | body_ffi(client_ref) 78 | } 79 | 80 | /// retrieve the full body from a client ref as a string 81 | pub fn body_string(ref client_ref: ClientRef) -> Result(String, Error) { 82 | use bits <- result.try(body_ffi(client_ref)) 83 | bit_array.to_string(bits) |> result.map_error(fn(_) { InvalidUtf8Response }) 84 | } 85 | 86 | pub type HttppMessage { 87 | Status(Int) 88 | Headers(List(Header)) 89 | Binary(BitArray) 90 | Redirect(String, List(Header)) 91 | SeeOther(String, List(Header)) 92 | DoneStreaming 93 | /// In case we couldn't decode the message, you'll get the dynamic version 94 | NotDecoded(Dynamic) 95 | } 96 | 97 | @external(erlang, "httpp_ffi", "insert_selector_handler") 98 | fn insert_selector_handler( 99 | a: Selector(payload), 100 | for for: tag, 101 | mapping mapping: fn(message) -> payload, 102 | ) -> Selector(payload) 103 | 104 | fn decode_on_atom_disc( 105 | atom: atom.Atom, 106 | decoder: fn(Dynamic) -> Result(a, dynamic.DecodeErrors), 107 | ) -> fn(Dynamic) -> Result(a, dynamic.DecodeErrors) { 108 | fn(dyn) { 109 | use decoded_atom <- result.try(dynamic.element(0, atom.from_dynamic)(dyn)) 110 | use <- bool.guard( 111 | when: decoded_atom != atom, 112 | return: Error([ 113 | dynamic.DecodeError( 114 | found: "atom: " <> atom.to_string(decoded_atom), 115 | expected: "atom: " <> atom.to_string(atom), 116 | path: [], 117 | ), 118 | ]), 119 | ) 120 | 121 | decoder(dyn) 122 | } 123 | } 124 | 125 | /// if sending with async, put this in your selector to receive messages related to your request 126 | pub fn selecting_http_message( 127 | selector: Selector(a), 128 | mapping transform: fn(ClientRef, HttppMessage) -> a, 129 | ) -> Selector(a) { 130 | let handler = fn(message: #(atom.Atom, ClientRef, Dynamic)) { 131 | let headers_decoder = 132 | dynamic.list(dynamic.tuple2(dynamic.string, dynamic.string)) 133 | 134 | let decoder = 135 | dynamic.any([ 136 | decode_on_atom_disc( 137 | atom.create_from_string("status"), 138 | dynamic.decode1(Status, dynamic.element(1, dynamic.int)), 139 | ), 140 | decode_on_atom_disc( 141 | atom.create_from_string("headers"), 142 | dynamic.decode1(Headers, dynamic.element(1, headers_decoder)), 143 | ), 144 | dynamic.decode1(Binary, dynamic.bit_array), 145 | decode_on_atom_disc( 146 | atom.create_from_string("redirect"), 147 | dynamic.decode2( 148 | Redirect, 149 | dynamic.element(1, dynamic.string), 150 | dynamic.element(2, headers_decoder), 151 | ), 152 | ), 153 | decode_on_atom_disc( 154 | atom.create_from_string("see_other"), 155 | dynamic.decode2( 156 | SeeOther, 157 | dynamic.element(1, dynamic.string), 158 | dynamic.element(2, headers_decoder), 159 | ), 160 | ), 161 | fn(a) { 162 | use atom <- result.try(atom.from_dynamic(a)) 163 | let atom_str = atom.to_string(atom) 164 | case atom_str { 165 | "done" -> Ok(DoneStreaming) 166 | found -> 167 | Error([ 168 | dynamic.DecodeError(found: found, expected: "done", path: []), 169 | ]) 170 | } 171 | }, 172 | fn(b) { Ok(NotDecoded(b)) }, 173 | ]) 174 | 175 | let assert Ok(http_message) = decoder(message.2) 176 | 177 | transform(message.1, http_message) 178 | } 179 | 180 | let tag = atom.create_from_string("hackney_response") 181 | 182 | insert_selector_handler(selector, #(tag, 3), handler) 183 | } 184 | -------------------------------------------------------------------------------- /src/httpp/streaming.gleam: -------------------------------------------------------------------------------- 1 | //// This module allows you to make an http request to a server and receive a streamed response back which 2 | //// is sent to a process managing it. You can also define a custom message type and send messages and state 3 | //// to this response handler. 4 | 5 | import gleam/bool 6 | import gleam/bytes_builder.{type BytesBuilder} 7 | import gleam/erlang 8 | import gleam/erlang/process.{type ExitReason, type Subject} 9 | import gleam/http.{type Header} 10 | import gleam/http/request.{type Request} 11 | import gleam/http/response.{type Response, Response} 12 | import gleam/list 13 | import gleam/option.{type Option, None, Some} 14 | import gleam/otp/actor 15 | import gleam/result 16 | import gleam/string 17 | import gleam/uri 18 | import httpp/hackney 19 | 20 | pub type Message { 21 | Bits(BitArray) 22 | Done 23 | } 24 | 25 | /// This defines the behaviour of the request handler 26 | /// *NOTE*: if the on_error handler does not receive a response, the handler will be shut down regardless of if it returns an Ok() result 27 | /// if the initial response does not arrive within the timeout, the on_error handler will receive the `TimedOut` error 28 | pub type StreamingRequestHandler(state, message_type) { 29 | StreamingRequestHandler( 30 | initial_state: state, 31 | req: Request(BytesBuilder), 32 | on_data: fn(Message, Response(Nil), state) -> Result(state, ExitReason), 33 | on_message: fn(message_type, Response(Nil), state) -> 34 | Result(state, ExitReason), 35 | on_error: fn(hackney.Error, Option(Response(Nil)), state) -> 36 | Result(state, ExitReason), 37 | initial_response_timeout: Int, 38 | ) 39 | } 40 | 41 | fn initial_request_loop( 42 | request_state: #(Option(Int), Option(List(Header))), 43 | when_to_time_out: Int, 44 | ) -> Result(Response(Nil), hackney.Error) { 45 | case request_state { 46 | #(Some(status), Some(headers)) -> Ok(Response(status, headers, Nil)) 47 | request_state -> { 48 | let next_selector = 49 | process.new_selector() 50 | |> hackney.selecting_http_message(mapping: fn(_, message) { 51 | case message { 52 | hackney.Status(status_code) -> 53 | initial_request_loop( 54 | #(Some(status_code), request_state.1), 55 | when_to_time_out, 56 | ) 57 | hackney.Headers(headers) -> 58 | initial_request_loop( 59 | #( 60 | request_state.0, 61 | Some( 62 | list.map(headers, fn(header) { 63 | #(string.lowercase(header.0), header.1) 64 | }), 65 | ), 66 | ), 67 | when_to_time_out, 68 | ) 69 | _ -> Error(hackney.NoStatusOrHeaders) 70 | } 71 | }) 72 | 73 | case process.select(next_selector, 500) { 74 | Ok(inner) -> inner 75 | Error(_) -> { 76 | let system_time = erlang.system_time(erlang.Millisecond) 77 | use <- bool.guard( 78 | when: system_time > when_to_time_out, 79 | return: Error(hackney.TimedOut), 80 | ) 81 | initial_request_loop(request_state, when_to_time_out) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | fn loop( 89 | message_subject: Subject(message_type), 90 | response: Response(Nil), 91 | state: state, 92 | handler: StreamingRequestHandler(state, message_type), 93 | ) { 94 | let next_state = 95 | process.new_selector() 96 | |> process.selecting(message_subject, handler.on_message(_, response, state)) 97 | |> hackney.selecting_http_message(fn(_, http_message) { 98 | case http_message { 99 | hackney.Status(..) 100 | | hackney.Headers(..) 101 | | hackney.SeeOther(..) 102 | | hackney.Redirect(..) -> 103 | handler.on_error( 104 | hackney.UnexpectedServerMessage(http_message), 105 | Some(response), 106 | state, 107 | ) 108 | hackney.Binary(bits) -> handler.on_data(Bits(bits), response, state) 109 | hackney.DoneStreaming -> handler.on_data(Done, response, state) 110 | hackney.NotDecoded(dyn) -> 111 | handler.on_error( 112 | hackney.MessageNotDecoded(dyn), 113 | Some(response), 114 | state, 115 | ) 116 | } 117 | }) 118 | |> process.select_forever 119 | 120 | result.try(next_state, loop(message_subject, response, _, handler)) 121 | } 122 | 123 | /// Starts a streaming request manager, based on the spec 124 | pub fn start( 125 | handler: StreamingRequestHandler(state, message_type), 126 | ) -> Result(Subject(message_type), actor.StartError) { 127 | let parent_subject: Subject(Subject(message_type)) = process.new_subject() 128 | 129 | process.start( 130 | fn() { 131 | let message_subject = process.new_subject() 132 | process.send(parent_subject, message_subject) 133 | 134 | let request_result = 135 | handler.req 136 | |> request.to_uri 137 | |> uri.to_string 138 | |> hackney.send( 139 | handler.req.method, 140 | _, 141 | handler.req.headers, 142 | handler.req.body, 143 | [hackney.Async], 144 | ) 145 | 146 | use <- bool.guard(when: result.is_error(request_result), return: Nil) 147 | 148 | let timeout_time = 149 | erlang.system_time(erlang.Millisecond) 150 | + handler.initial_response_timeout 151 | 152 | let response = initial_request_loop(#(None, None), timeout_time) 153 | 154 | let exit_reason = case response { 155 | Ok(response) -> 156 | case loop(message_subject, response, handler.initial_state, handler) { 157 | Ok(_) -> process.Normal 158 | Error(reason) -> reason 159 | } 160 | Error(err) -> { 161 | let exit_reason = case 162 | handler.on_error(err, None, handler.initial_state) 163 | { 164 | Ok(_) -> 165 | process.Abnormal( 166 | "streaming request manager could not get initial request data", 167 | ) 168 | Error(reason) -> reason 169 | } 170 | } 171 | } 172 | 173 | case exit_reason { 174 | process.Normal -> process.send_exit(process.self()) 175 | process.Abnormal(reason) -> 176 | process.send_abnormal_exit(process.self(), reason) 177 | process.Killed -> process.kill(process.self()) 178 | } 179 | }, 180 | False, 181 | ) 182 | 183 | case process.receive(parent_subject, 10_000) { 184 | Ok(subject) -> Ok(subject) 185 | Error(_) -> Error(actor.InitTimeout) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | --------------------------------------------------------------------------------