Skip to content

Commit d19e63c

Browse files
author
Garrett Moseke
authored
Merge pull request #1 from PassiveLogic/feature/two-way-protocol
Allow `Next` messages from Client -> Server
2 parents 37de2c4 + ffaf80d commit d19e63c

15 files changed

+502
-166
lines changed

.swiftformat

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Default formatting rules for all PassiveLogic swift projects
2+
3+
--allman false
4+
--binarygrouping none
5+
--decimalgrouping none
6+
--disable redundantSelf,trailingClosures
7+
--elseposition next-line
8+
--exponentcase uppercase
9+
--guardelse same-line
10+
--hexliteralcase lowercase
11+
--indent 4
12+
--indentcase true
13+
--maxwidth 140
14+
--self insert
15+
--semicolons never
16+
--swiftversion 5
17+
--trimwhitespace always
18+
--varattributes prev-line
19+
--typeattributes prev-line
20+
--wrapparameters before-first
21+
--wrapcollections before-first
22+
--wraparguments before-first
23+
# By default we ignore anything in the "Packages" directory as this includes Third Party code.
24+
--exclude Packages
25+
26+
# Explicitly enable rules to prevent automatic opt-in
27+
# Generated from `swiftformat --rules` on v0.47.1
28+
--rules andOperator
29+
--rules anyObjectProtocol
30+
--rules blankLinesAroundMark
31+
--rules blankLinesAtEndOfScope
32+
--rules blankLinesAtStartOfScope
33+
--rules blankLinesBetweenScopes
34+
--rules braces
35+
--rules consecutiveBlankLines
36+
--rules consecutiveSpaces
37+
--rules duplicateImports
38+
--rules elseOnSameLine
39+
--rules emptyBraces
40+
--rules enumNamespaces
41+
--rules extensionAccessControl
42+
--rules fileHeader
43+
--rules hoistPatternLet
44+
--rules indent
45+
--rules initCoderUnavailable
46+
--rules leadingDelimiters
47+
--rules linebreakAtEndOfFile
48+
--rules linebreaks
49+
--rules modifierOrder
50+
--rules numberFormatting
51+
--rules preferKeyPath
52+
--rules redundantBackticks
53+
--rules redundantBreak
54+
--rules redundantExtensionACL
55+
--rules redundantFileprivate
56+
--rules redundantGet
57+
--rules redundantInit
58+
--rules redundantLet
59+
--rules redundantLetError
60+
--rules redundantNilInit
61+
--rules redundantObjc
62+
--rules redundantParens
63+
--rules redundantPattern
64+
--rules redundantRawValues
65+
--rules redundantReturn
66+
--rules redundantSelf
67+
--rules redundantType
68+
--disable redundantVoidReturnType # Disabled because this breaks some NIO closures
69+
--rules semicolons
70+
--rules sortedImports
71+
--rules spaceAroundBraces
72+
--rules spaceAroundBrackets
73+
--rules spaceAroundComments
74+
--rules spaceAroundGenerics
75+
--rules spaceAroundOperators
76+
--rules spaceAroundParens
77+
--rules spaceInsideBraces
78+
--rules spaceInsideBrackets
79+
--rules spaceInsideComments
80+
--rules spaceInsideGenerics
81+
--rules spaceInsideParens
82+
--rules strongOutlets
83+
--rules strongifiedSelf
84+
--rules todos
85+
--rules trailingClosures
86+
--rules trailingCommas
87+
--rules trailingSpace
88+
--rules unusedArguments
89+
--rules void
90+
--rules wrap
91+
--rules wrapArguments
92+
--rules wrapAttributes
93+
--rules wrapMultilineStatementBraces
94+
--rules yodaConditions

Package.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ let package = Package(
2525
.product(name: "GraphQLRxSwift", package: "GraphQLRxSwift"),
2626
.product(name: "GraphQL", package: "GraphQL"),
2727
.product(name: "NIO", package: "swift-nio"),
28-
.product(name: "RxSwift", package: "RxSwift")
29-
]),
28+
.product(name: "RxSwift", package: "RxSwift"),
29+
]
30+
),
3031
.testTarget(
3132
name: "GraphQLTransportWSTests",
3233
dependencies: ["GraphQLTransportWS"]

README.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1-
# GraphQLTransportWS
1+
# GraphQLTransportWS-DataSync
22

3-
This implements the [graphql-transport-ws WebSocket subprotocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md).
4-
It is mainly intended for server support, but there is a basic client implementation included.
3+
This implements the [graphql-transport-ws WebSocket subprotocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md), with the additional capability to send `GraphQLRequests` from the Client to the Server over the same websocket to support the PassiveLogic DataSync spec.
4+
5+
It is mainly intended for server support, but there is a client implementation included for swift projects.
56

67
Features:
78
- Server implementation that implements defined protocol conversations
89
- Client and Server types that wrap messengers
910
- Codable Server and Client message structures
1011
- Custom authentication support
1112

13+
## DataSync Additions
14+
This DataSync fork allows for `Next` messages to be handled by the Server with a custom `onNext` callback exposed. The server will detect incoming `Next` frames that contain `subscribe` requests and reject them with an error to avoid nesting subscriptions, but will allow handling logic for `query` and `mutation` requests.
15+
16+
The Client implemeneted here now also supports sending `Next` messages to the server via adding an `Observable` object, which will automatically wrap and send `Next` frames as it updates.
17+
18+
### Example
19+
*The client and the server has already gone through [successful connection initialisation.](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#successful-connection-initialisation)*
20+
1. *Client* generates a unique ID for the following operation
21+
2. *Client* dispatches the `Subscribe` message with the generated ID through the `id` field and the requested operation passed through the `payload` field
22+
*All future communication is linked through this unique ID*
23+
3. *Server* executes the streaming GraphQL operation
24+
4. *Server* checks if the generated ID is unique across active streaming subscriptions
25+
- If not unique, the server will close the socket with the event `4409: Subscriber for <generated-id> already exists`
26+
If unique, continue...
27+
5. *Server* optionally checks if the operation is valid before starting executing it, e.g. checking permissions
28+
- If not valid, the server sends an `Error` message and deems the operation complete.
29+
If valid, continue...
30+
6. *Server* & *Client* dispatch results over time with the `Next` message
31+
- *Server* & *Client* handle received `Next` messages with their respectively defined callbacks. All Context is assumed to be the same between the two and is established in step 3 when the subscription is created.
32+
7. The operation completes as described in [the `graphql-transport-ws` protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#streaming-operation)
33+
1234
## Usage
1335

1436
To use this package, include it in your `Package.swift` dependencies:
@@ -89,6 +111,28 @@ routes.webSocket(
89111
)
90112
```
91113

114+
You can also Create a client and add an observable callback to send data back to the server:
115+
```swift
116+
let messenger = WebSocketMessenger(websocket: websocket)
117+
let client = Client<EmptyInitPayload>(messenger: messenger)
118+
119+
// add an observable to the client that will automatically send `Next` frames to the server as it updates
120+
client.addObservable(observable:
121+
Observable.create { observer in
122+
// Some simple obsererver that fulfills every future with the same query. Your observer can be much more complex.
123+
observer.on(.next(loop.makeSucceededFuture(GraphQLRequest(
124+
query:
125+
"""
126+
query {
127+
hello
128+
}
129+
"""
130+
))))
131+
return Disposables.create()
132+
}
133+
)
134+
```
135+
92136
### Authentication
93137

94138
This package exposes authentication hooks on the `connection_init` message. To perform custom authentication,

Sources/GraphQLTransportWS/Client.swift

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
// Copyright (c) 2021 PassiveLogic, Inc.
2-
31
import Foundation
42
import GraphQL
3+
import GraphQLRxSwift
4+
import NIO
5+
import RxSwift
56

67
/// Client is an open-ended implementation of the client side of the protocol. It parses and adds callbacks for each type of server respose.
78
public class Client<InitPayload: Equatable & Codable> {
89
// We keep this weak because we strongly inject this object into the messenger callback
910
weak var messenger: Messenger?
10-
11+
1112
var onConnectionAck: (ConnectionAckResponse, Client) -> Void = { _, _ in }
1213
var onNext: (NextResponse, Client) -> Void = { _, _ in }
1314
var onError: (ErrorResponse, Client) -> Void = { _, _ in }
1415
var onComplete: (CompleteResponse, Client) -> Void = { _, _ in }
1516
var onMessage: (String, Client) -> Void = { _, _ in }
16-
17+
18+
let disposeBag = DisposeBag()
1719
let encoder = GraphQLJSONEncoder()
1820
let decoder = JSONDecoder()
19-
21+
2022
/// Create a new client.
2123
///
2224
/// - Parameters:
@@ -27,18 +29,18 @@ public class Client<InitPayload: Equatable & Codable> {
2729
self.messenger = messenger
2830
messenger.onReceive { message in
2931
self.onMessage(message, self)
30-
32+
3133
// Detect and ignore error responses.
3234
if message.starts(with: "44") {
3335
// TODO: Determine what to do with returned error messages
3436
return
3537
}
36-
37-
guard let json = message.data(using: .utf8) else {
38+
39+
guard let json = Data(message.utf8) else {
3840
self.error(.invalidEncoding())
3941
return
4042
}
41-
43+
4244
let response: Response
4345
do {
4446
response = try self.decoder.decode(Response.self, from: json)
@@ -47,7 +49,7 @@ public class Client<InitPayload: Equatable & Codable> {
4749
self.error(.noType())
4850
return
4951
}
50-
52+
5153
switch response.type {
5254
case .connectionAck:
5355
guard let connectionAckResponse = try? self.decoder.decode(ConnectionAckResponse.self, from: json) else {
@@ -78,37 +80,37 @@ public class Client<InitPayload: Equatable & Codable> {
7880
}
7981
}
8082
}
81-
83+
8284
/// Define the callback run on receipt of a `connection_ack` message
8385
/// - Parameter callback: The callback to assign
8486
public func onConnectionAck(_ callback: @escaping (ConnectionAckResponse, Client) -> Void) {
8587
self.onConnectionAck = callback
8688
}
87-
89+
8890
/// Define the callback run on receipt of a `next` message
8991
/// - Parameter callback: The callback to assign
9092
public func onNext(_ callback: @escaping (NextResponse, Client) -> Void) {
9193
self.onNext = callback
9294
}
93-
95+
9496
/// Define the callback run on receipt of an `error` message
9597
/// - Parameter callback: The callback to assign
9698
public func onError(_ callback: @escaping (ErrorResponse, Client) -> Void) {
9799
self.onError = callback
98100
}
99-
101+
100102
/// Define the callback run on receipt of a `complete` message
101103
/// - Parameter callback: The callback to assign
102104
public func onComplete(_ callback: @escaping (CompleteResponse, Client) -> Void) {
103105
self.onComplete = callback
104106
}
105-
107+
106108
/// Define the callback run on receipt of any message
107109
/// - Parameter callback: The callback to assign
108110
public func onMessage(_ callback: @escaping (String, Client) -> Void) {
109111
self.onMessage = callback
110112
}
111-
113+
112114
/// Send a `connection_init` request through the messenger
113115
public func sendConnectionInit(payload: InitPayload) {
114116
guard let messenger = messenger else { return }
@@ -118,9 +120,9 @@ public class Client<InitPayload: Equatable & Codable> {
118120
).toJSON(encoder)
119121
)
120122
}
121-
123+
122124
/// Send a `subscribe` request through the messenger
123-
public func sendStart(payload: GraphQLRequest, id: String) {
125+
public func sendSubscribe(payload: GraphQLRequest, id: String) {
124126
guard let messenger = messenger else { return }
125127
messenger.send(
126128
SubscribeRequest(
@@ -129,7 +131,7 @@ public class Client<InitPayload: Equatable & Codable> {
129131
).toJSON(encoder)
130132
)
131133
}
132-
134+
133135
/// Send a `complete` request through the messenger
134136
public func sendStop(id: String) {
135137
guard let messenger = messenger else { return }
@@ -139,7 +141,37 @@ public class Client<InitPayload: Equatable & Codable> {
139141
).toJSON(encoder)
140142
)
141143
}
142-
144+
145+
/// Add an observable object for this client that will fire off `Next` messages to the server as updates happen.
146+
/// - Parameter observable: `Observable<EventLoopFuture<GraphQLRequest>>` to subscribe to for changes.
147+
public func addObservableSubscription(observable: Observable<EventLoopFuture<GraphQLResult>>) {
148+
observable.subscribe(
149+
onNext: { [weak self] resultFuture in
150+
guard let self = self else { return }
151+
resultFuture.whenSuccess { result in
152+
self.sendNext(payload: result, id: UUID().uuidString)
153+
}
154+
resultFuture.whenFailure { error in
155+
self.error(.graphQLError(error))
156+
}
157+
}
158+
).disposed(by: disposeBag)
159+
}
160+
161+
/// Send a `next` message through the messenger.
162+
/// - Parameters:
163+
/// - payload: `GraphQLRequest` object for the server to handle
164+
/// - id: id of the message
165+
private func sendNext(payload: GraphQLResult, id: String) {
166+
guard let messenger = messenger else { return }
167+
messenger.send(
168+
NextResponse(
169+
payload,
170+
id: id
171+
).toJSON(encoder)
172+
)
173+
}
174+
143175
/// Send an error through the messenger and close the connection
144176
private func error(_ error: GraphQLTransportWSError) {
145177
guard let messenger = messenger else { return }

0 commit comments

Comments
 (0)