Skip to content

Bidirectional stream events #583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ The following packages show working with various content types, such as JSON, UR
- [various-content-types-server-example](./various-content-types-server-example) - A server showing how to handle and provide the various content types.
- [event-streams-client-example](./event-streams-client-example) - A client showing how to provide and handle event streams.
- [event-streams-server-example](./event-streams-server-example) - A server showing how to handle and provide event streams.
- [bidirectional-event-streams-client-example](./bidirectional-event-streams-client-example) - A client showing how to provide and handle bidirectional event streams.
- [bidirectional-event-streams-server-example](./bidirectional-event-streams-server-example) - A server showing how to handle and provide bidirectional event streams.

## Integrations

Expand Down
11 changes: 11 additions & 0 deletions Examples/bidirectional-event-streams-client-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.vscode
/Package.resolved
.ci/
.docc-build/
35 changes: 35 additions & 0 deletions Examples/bidirectional-event-streams-client-example/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// swift-tools-version:5.9
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import PackageDescription

let package = Package(
name: "bidirectional-event-streams-client-example",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)],
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.2.0"),
.package(url: "https://github.com/swift-server/swift-openapi-async-http-client", from: "1.0.0"),
],
targets: [
.executableTarget(
name: "BidirectionalEventStreamsClient",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
],
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
)
]
)
23 changes: 23 additions & 0 deletions Examples/bidirectional-event-streams-client-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Client handling bidirectional event streams

An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator).

> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only.

## Overview

A command-line tool that uses a generated client to show how to work with bidirectional event streams.

Instead of [URLSession](https://developer.apple.com/documentation/foundation/urlsession), which will return stream only until at least “some” bytes of the body have also been received (see [comment](https://github.com/apple/swift-openapi-urlsession/blob/main/Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/URLSessionBidirectionalStreamingTests.swift#L193-L206)), tool uses the [AsyncHTTPClient](https://github.com/swift-server/async-http-client) API to perform the HTTP call, wrapped in the [AsyncHTTPClient Transport for Swift OpenAPI Generator](https://github.com/swift-server/swift-openapi-async-http-client). A workaround for URLSession could be sending an `empty`, `.joined` or some kind of hearbeat message from server first when initialising a stream.

The server can be started by running `bidirectional-event-streams-server-example` locally.

## Usage

Build and run the client CLI using:

```console
% swift run
Sending and fetching back greetings using JSON Lines
...
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import OpenAPIRuntime
import OpenAPIAsyncHTTPClient
import Foundation

@main struct BidirectionalEventStreamsClient {
private static let templates: [String] = [
"Hello, %@!", "Good morning, %@!", "Hi, %@!", "Greetings, %@!", "Hey, %@!", "Hi there, %@!",
"Good evening, %@!",
]
static func main() async throws {
let client = Client(serverURL: URL(string: "http://localhost:8080/api")!, transport: AsyncHTTPClientTransport())
do {
print("Sending and fetching back greetings using JSON Lines")
let (stream, continuation) = AsyncStream<Components.Schemas.Greeting>.makeStream()
/// To keep it simple, using JSON Lines, as it most straightforward and easy way to have streams.
/// For SSE and JSON Sequences cases please check `event-streams-client-example`.
let requestBody: Operations.getGreetingsStream.Input.Body = .application_jsonl(
.init(stream.asEncodedJSONLines(), length: .unknown, iterationBehavior: .single)
)
let response = try await client.getGreetingsStream(query: .init(name: "Example"), body: requestBody)
let greetingStream = try response.ok.body.application_jsonl.asDecodedJSONLines(
of: Components.Schemas.Greeting.self
)
try await withThrowingTaskGroup(of: Void.self) { group in
// Listen for upcoming messages
group.addTask {
for try await greeting in greetingStream {
try Task.checkCancellation()
print("Got greeting: \(greeting.message)")
}
}
// Send messages
group.addTask {
for template in Self.templates {
try Task.checkCancellation()
continuation.yield(.init(message: template))
try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
}
continuation.finish()
}
return try await group.waitForAll()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
generate:
- types
- client
accessModifier: internal
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
openapi: '3.1.0'
info:
title: EventStreamsService
version: 1.0.0
servers:
- url: https://example.com/api
description: Example service deployment.
paths:
/greetings:
post:
operationId: getGreetingsStream
parameters:
- name: name
required: false
in: query
description: The name used in the returned greeting.
schema:
type: string
requestBody:
description: A body with a greetings stream.
required: true
content:
application/jsonl: {}
responses:
'200':
description: A success response with a greetings stream.
content:
application/jsonl: {}
components:
schemas:
Greeting:
type: object
description: A value with the greeting contents.
properties:
message:
type: string
description: The string representation of the greeting.
required:
- message
12 changes: 12 additions & 0 deletions Examples/bidirectional-event-streams-server-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.DS_Store
.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.vscode
/Package.resolved
.ci/
.docc-build/

37 changes: 37 additions & 0 deletions Examples/bidirectional-event-streams-server-example/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// swift-tools-version:5.9
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import PackageDescription

let package = Package(
name: "bidirectional-event-streams-server-example",
platforms: [.macOS(.v10_15)],
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.2.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-rc.1"),
.package(url: "https://github.com/swift-server/swift-openapi-hummingbird.git", from: "2.0.0-beta.4"),
],
targets: [
.executableTarget(
name: "BidirectionalEventStreamsServer",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIHummingbird", package: "swift-openapi-hummingbird"),
.product(name: "Hummingbird", package: "hummingbird"),
],
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
)
]
)
23 changes: 23 additions & 0 deletions Examples/bidirectional-event-streams-server-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Server supporting bidirectional event streams

An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator).

> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only.

## Overview

A server that uses generated server stubs to show how to work with bidirectional event streams.

The tool uses the [Hummingbird](https://github.com/hummingbird-project/hummingbird) server framework to handle HTTP requests, wrapped in the [Swift OpenAPI Hummingbird](https://github.com/swift-server/swift-openapi-hummingbird).

The CLI starts the server on `http://localhost:8080` and can be invoked by running `bidirectional-event-streams-client-example`.

## Usage

Build and run the server CLI using:

```console
% swift run
2024-07-04T08:56:23+0200 info Hummingbird : [HummingbirdCore] Server started and listening on 127.0.0.1:8080
...
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import OpenAPIRuntime
import OpenAPIHummingbird
import Hummingbird
import Foundation

struct Handler: APIProtocol {
private let storage: StreamStorage = .init()
func getGreetingsStream(_ input: Operations.getGreetingsStream.Input) async throws
-> Operations.getGreetingsStream.Output
{
let eventStream = await self.storage.makeStream(input: input)
/// To keep it simple, using JSON Lines, as it most straightforward and easy way to have streams.
/// For SSE and JSON Sequences cases please check `event-streams-server-example`.
let responseBody = Operations.getGreetingsStream.Output.Ok.Body.application_jsonl(
.init(eventStream.asEncodedJSONLines(), length: .unknown, iterationBehavior: .single)
)
return .ok(.init(body: responseBody))
}
}

@main struct BidirectionalEventStreamsServer {
static func main() async throws {
let router = Router()
let handler = Handler()
try handler.registerHandlers(on: router, serverURL: URL(string: "/api")!)
let app = Application(router: router, configuration: .init())
try await app.run()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation

actor StreamStorage: Sendable {
private typealias StreamType = AsyncStream<Components.Schemas.Greeting>
private var streams: [String: Task<Void, any Error>] = [:]
init() {}
private func finishedStream(id: String) {
guard self.streams[id] != nil else { return }
self.streams.removeValue(forKey: id)
}
private func cancelStream(id: String) {
guard let task = self.streams[id] else { return }
self.streams.removeValue(forKey: id)
task.cancel()
print("Canceled stream \(id)")
}
func makeStream(input: Operations.getGreetingsStream.Input) -> AsyncStream<Components.Schemas.Greeting> {
let name = input.query.name ?? "Stranger"
let id = UUID().uuidString
print("Creating stream \(id) for name: \(name)")
let (stream, continuation) = StreamType.makeStream()
continuation.onTermination = { termination in
Task { [weak self] in
switch termination {
case .cancelled: await self?.cancelStream(id: id)
case .finished: await self?.finishedStream(id: id)
@unknown default: await self?.finishedStream(id: id)
}
}
}
let inputStream =
switch input.body {
case .application_jsonl(let body): body.asDecodedJSONLines(of: Components.Schemas.Greeting.self)
}
let task = Task<Void, any Error> {
for try await message in inputStream {
try Task.checkCancellation()
print("Recieved a message \(message)")
print("Sending greeting back for \(id)")
let greetingText = String(format: message.message, name)
continuation.yield(.init(message: greetingText))
}
continuation.finish()
}
self.streams[id] = task
return stream
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
generate:
- types
- server
accessModifier: internal
Loading