diff --git a/3.0/docs/async/overview.md b/3.0/docs/async/promise-future.md similarity index 94% rename from 3.0/docs/async/overview.md rename to 3.0/docs/async/promise-future.md index f15bd9f3..671e4d38 100644 --- a/3.0/docs/async/overview.md +++ b/3.0/docs/async/promise-future.md @@ -1,8 +1,4 @@ -# Async - -Async provides the fundament for asynchronous libraries. It consists of Futures and Streams. - -## Promise and Future +# Promise and Future When working with asynchronous APIs, one of the problems you'll face is not knowing when a variable is set. @@ -29,7 +25,7 @@ You can imagine code becoming complex. Difficult to read and comprehend. Promises and futures are two types that this library introduces to solve this. -### Creating a promise +## Creating a promise Promises are important if you're implementing a function that returns a result in the future, such as the database shown above. @@ -69,7 +65,7 @@ func fetchUser(named name: String) -> Future { } ``` -### On future completion +## On future completion When a promise completes, you can chain the result/error into a closure: @@ -86,7 +82,7 @@ future.then { user in } ``` -### Catching specific errors +## Catching specific errors Sometimes you only care for specific errors, for example, for logging. @@ -106,7 +102,7 @@ future.then { user in } ``` -### Mapping results +## Mapping results Futures can be mapped to different results asynchronously. @@ -125,7 +121,7 @@ futureUsername.then { username in } ``` -### For synchronous APIs +## For synchronous APIs Sometimes, an API needs to be used synchronously in a synchronous envinronment. diff --git a/3.0/docs/async/stream.md b/3.0/docs/async/stream.md new file mode 100644 index 00000000..9f802253 --- /dev/null +++ b/3.0/docs/async/stream.md @@ -0,0 +1,125 @@ +# Stream + +Streams is a mechanism that you can implement on objects that process any information efficiently and asynchronously without bloat. + +There are three primary stream protocols: + +- InputStream +- OutputStream +- Stream + +Conforming to Stream means conformance to both InputStream and OutputStream. So `Stream` is both processing output and providing output. + +InputStream is a protocol that, when implemented, accepts streaming input. An example can be a TCP socket that, on input, writes data to the socket. + +OutputStream is a protocol that, when implement, can emit output. + +### Concept + +In Vapor 3 (related libraries), almost everything is a stream. TCP Server is a stream of clients. Each client is a stream of received binary data. For HTTP, each client has an HTTP Request Parser, and Response Serializer. A parser accepts the binary stream and outputs a request stream. And a responder accepts a response and outputs a binary stream (that you can send back to the client's TCP socket as input for the binary stream). + +## Implementing an example stream + +This example is a stream that deserializes `ByteBuffer` to `String` streaming/asynchronously. + +```swift +struct InvalidUTF8 : Error {} + +// Deserializes `ByteBuffer` (`Input`) to `String` (`Output`) using the provided encoding +class StringDeserializationStream : Async.Stream { + typealias Input = ByteBuffer + typealias Output = String + + // Used only by this specific stream to specify an encoding + let encoding: String.Encoding + + // MARK: Stream requirements + + // An error stream that can be listened to for errors in this stream + var errorStream: BaseStream.ErrorHandler? + + // A handler that can be set to handle output + var outputStream: OutputHandler? + + // Creates a new `StringDeserializationStream` + init(encoding: String.Encoding = .utf8) { + // Sets the String encoding + self.encoding = encoding + } + + // Receives `Input`/`ByteBuffer` from another stream or manual call + // + // Attempts to process it to a String using the specified encoding + func inputStream(_ input: Input) { + // Converts the `Input`/`ByteBuffer` to a String + guard let string = String(bytes: input, encoding: self.encoding) { + // Stream an error if string initialization failed + self.errorStream?(InvalidUTF8()) + return + } + + // On success, output the created string + self.outputStream?(string) + } +} +``` + +## Chaining Streams + +The above stream can now be called manually, but you still need to receive the output. + +In this example, we'll take a TCP Stream implementation that outputs `ByteBuffer` for incoming data. + +```swift +// TCP stream is a Stream of `ByteBuffer` +let tcpStream = ... +let stringStream = StringDeserializationStream() + +// tcpStream's output equals stringStream's input, we can drain tcpStream into stringStream +// +// This will call `stringStream.inputStream` for every tcpStream's output +tcpStream.drain(into: stringStream) + +// StringStream can then be drained to print the results +stringStream.drain { string in + // print the results + print(string) +} +``` + +## Transforming streams without an intermediate stream + +The above stream `StringDeserializationStream` is a very simple example of implementing a stream. + +Streams support two kinds of transforms. `flatMap` and `map`. Map transforms the output of the stream into a new stream with different output. And `flatMap` does the same, but allows returning `nil` and does not output it. + +```swift +// `flatMap`s the data into a `String?`. If the string results in `nil`, the resulting `stringStream` does not get called. +// `stringStream` is a stream outputting `String` +let stringStream = tcpStream.flatMap { bytes in + return String(bytes: bytes, encoding: self.encoding) +} + +// `map`s the data into a `String?`. If the string results in `nil`, the resulting `optionalStringStream` emits `nil`, too. +// `optionalStringStream` is a stream outputting `String?` +let optionalStringStream = tcpStream.flatMap { bytes in + return String(bytes: bytes, encoding: self.encoding) +} +``` + +As you see, you an provide a closure to do the mapping for you. If you want to reuse this code instead, you could make it a function for simplicity. This function can then be used instead of the closure. + +```swift +// Creates a `String` from `ByteBuffer`. This can return `nil` if the `ByteBuffer` doesn't contain valid UTF-8 +func utf8String(from bytes: ByteBuffer) -> String? { + return String(bytes: bytes, encoding: self.encoding) +} + +// `flatMap`s the data into a `String?`. If the string results in `nil`, the resulting `stringStream` does not get called. +// `stringStream` is a stream outputting `String` +let stringStream = tcpStream.flatMap(utf8String) + +// `map`s the data into a `String?`. If the string results in `nil`, the resulting `optionalStringStream` emits `nil`, too. +// `optionalStringStream` is a stream outputting `String?` +let optionalStringStream = tcpStream.flatMap(utf8String) +``` diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index b9a09d52..d8895f14 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -3,6 +3,9 @@ copyright: 'Copyright © 2017 Qutheory, LLC' pages: - Overview: 'index.md' +- Async: + - 'Promise & Future': 'async/promise-future.md' + - 'Stream': 'async/stream.md' markdown_extensions: - admonition @@ -28,7 +31,7 @@ extra: font: text: 'Roboto Slab' code: 'Source Code Pro' - + google_analytics: - 'UA-76177358-4' - 'auto'