Skip to content

Commit 93f4ff5

Browse files
authored
feat(realtime): send broadcast events through HTTP (#476)
1 parent 9f15d2d commit 93f4ff5

File tree

15 files changed

+386
-184
lines changed

15 files changed

+386
-184
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"pins" : [
3+
{
4+
"identity" : "swift-concurrency-extras",
5+
"kind" : "remoteSourceControl",
6+
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
7+
"state" : {
8+
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
9+
"version" : "1.1.0"
10+
}
11+
},
12+
{
13+
"identity" : "swift-crypto",
14+
"kind" : "remoteSourceControl",
15+
"location" : "https://github.com/apple/swift-crypto.git",
16+
"state" : {
17+
"revision" : "bc1c29221f6dfeb0ebbfbc98eb95cd3d4967868e",
18+
"version" : "3.4.0"
19+
}
20+
},
21+
{
22+
"identity" : "swift-custom-dump",
23+
"kind" : "remoteSourceControl",
24+
"location" : "https://github.com/pointfreeco/swift-custom-dump",
25+
"state" : {
26+
"revision" : "aec6a73f5c1dc1f1be4f61888094b95cf995d973",
27+
"version" : "1.3.2"
28+
}
29+
},
30+
{
31+
"identity" : "swift-snapshot-testing",
32+
"kind" : "remoteSourceControl",
33+
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
34+
"state" : {
35+
"revision" : "c097f955b4e724690f0fc8ffb7a6d4b881c9c4e3",
36+
"version" : "1.17.2"
37+
}
38+
},
39+
{
40+
"identity" : "swift-syntax",
41+
"kind" : "remoteSourceControl",
42+
"location" : "https://github.com/swiftlang/swift-syntax",
43+
"state" : {
44+
"revision" : "303e5c5c36d6a558407d364878df131c3546fad8",
45+
"version" : "510.0.2"
46+
}
47+
},
48+
{
49+
"identity" : "xctest-dynamic-overlay",
50+
"kind" : "remoteSourceControl",
51+
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
52+
"state" : {
53+
"revision" : "357ca1e5dd31f613a1d43320870ebc219386a495",
54+
"version" : "1.2.2"
55+
}
56+
}
57+
],
58+
"version" : 2
59+
}

.swiftpm/xcode/xcshareddata/xcschemes/Supabase.xcscheme

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
<TestPlanReference
3636
reference = "container:TestPlans/Integration.xctestplan">
3737
</TestPlanReference>
38+
<TestPlanReference
39+
reference = "container:TestPlans/AllTests.xctestplan">
40+
</TestPlanReference>
3841
</TestPlans>
3942
</TestAction>
4043
<LaunchAction

Package.resolved

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@
2323
"kind" : "remoteSourceControl",
2424
"location" : "https://github.com/pointfreeco/swift-custom-dump",
2525
"state" : {
26-
"revision" : "d237304f42af07f22563aa4cc2d7e2cfb25da82e",
27-
"version" : "1.3.1"
26+
"revision" : "aec6a73f5c1dc1f1be4f61888094b95cf995d973",
27+
"version" : "1.3.2"
2828
}
2929
},
3030
{
3131
"identity" : "swift-issue-reporting",
3232
"kind" : "remoteSourceControl",
3333
"location" : "https://github.com/pointfreeco/swift-issue-reporting",
3434
"state" : {
35-
"revision" : "c85092304cda8cb38d2d68454b29609a8013620b",
36-
"version" : "1.2.1"
35+
"revision" : "357ca1e5dd31f613a1d43320870ebc219386a495",
36+
"version" : "1.2.2"
3737
}
3838
},
3939
{
@@ -50,8 +50,17 @@
5050
"kind" : "remoteSourceControl",
5151
"location" : "https://github.com/swiftlang/swift-syntax",
5252
"state" : {
53-
"revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c",
54-
"version" : "600.0.0-prerelease-2024-06-12"
53+
"revision" : "82a453c2dfa335c7e778695762438dfe72b328d2",
54+
"version" : "600.0.0-prerelease-2024-07-24"
55+
}
56+
},
57+
{
58+
"identity" : "xctest-dynamic-overlay",
59+
"kind" : "remoteSourceControl",
60+
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
61+
"state" : {
62+
"revision" : "357ca1e5dd31f613a1d43320870ebc219386a495",
63+
"version" : "1.2.2"
5564
}
5665
}
5766
],

Package.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ let package = Package(
2323
],
2424
dependencies: [
2525
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"),
26-
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"),
27-
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.0"),
28-
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.8.1"),
29-
.package(url: "https://github.com/pointfreeco/swift-issue-reporting", from: "1.2.0"),
26+
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.1.0"),
27+
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
28+
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.2"),
29+
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
3030
],
3131
targets: [
3232
.target(
@@ -55,7 +55,7 @@ let package = Package(
5555
dependencies: [
5656
.product(name: "CustomDump", package: "swift-custom-dump"),
5757
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
58-
.product(name: "IssueReporting", package: "swift-issue-reporting"),
58+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
5959
"Helpers",
6060
"Auth",
6161
"TestHelpers",
@@ -71,7 +71,7 @@ let package = Package(
7171
dependencies: [
7272
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
7373
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
74-
.product(name: "IssueReporting", package: "swift-issue-reporting"),
74+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
7575
"Functions",
7676
"TestHelpers",
7777
],
@@ -82,7 +82,7 @@ let package = Package(
8282
dependencies: [
8383
.product(name: "CustomDump", package: "swift-custom-dump"),
8484
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
85-
.product(name: "IssueReporting", package: "swift-issue-reporting"),
85+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
8686
"Helpers",
8787
"Auth",
8888
"PostgREST",
@@ -129,7 +129,7 @@ let package = Package(
129129
name: "StorageTests",
130130
dependencies: [
131131
.product(name: "CustomDump", package: "swift-custom-dump"),
132-
.product(name: "IssueReporting", package: "swift-issue-reporting"),
132+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
133133
"Storage",
134134
]
135135
),
@@ -155,7 +155,7 @@ let package = Package(
155155
name: "TestHelpers",
156156
dependencies: [
157157
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
158-
.product(name: "IssueReporting", package: "swift-issue-reporting"),
158+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
159159
"Auth",
160160
]
161161
),

Sources/Realtime/RealtimeChannel.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,6 @@ public struct RealtimeChannelOptions {
105105
}
106106
}
107107

108-
/// Represents the different status of a push
109-
public enum PushStatus: String, Sendable {
110-
case ok
111-
case error
112-
case timeout
113-
}
114-
115108
public enum RealtimeSubscribeStates {
116109
case subscribed
117110
case timedOut

Sources/Realtime/V2/PushV2.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
import Foundation
99
import Helpers
1010

11+
/// Represents the different status of a push
12+
public enum PushStatus: String, Sendable {
13+
case ok
14+
case error
15+
case timeout
16+
}
17+
1118
actor PushV2 {
1219
private weak var channel: RealtimeChannelV2?
1320
let message: RealtimeMessageV2

Sources/Realtime/V2/RealtimeChannelV2.swift

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,56 @@ import ConcurrencyExtras
99
import Foundation
1010
import Helpers
1111

12+
#if canImport(FoundationNetworking)
13+
import FoundationNetworking
14+
15+
extension HTTPURLResponse {
16+
convenience init() {
17+
self.init(
18+
url: URL(string: "http://127.0.0.1")!,
19+
statusCode: 200,
20+
httpVersion: nil,
21+
headerFields: nil
22+
)!
23+
}
24+
}
25+
#endif
26+
1227
public struct RealtimeChannelConfig: Sendable {
1328
public var broadcast: BroadcastJoinConfig
1429
public var presence: PresenceJoinConfig
1530
public var isPrivate: Bool
1631
}
1732

1833
struct Socket: Sendable {
34+
var broadcastURL: @Sendable () -> URL
1935
var status: @Sendable () -> RealtimeClientV2.Status
2036
var options: @Sendable () -> RealtimeClientOptions
2137
var accessToken: @Sendable () -> String?
38+
var apiKey: @Sendable () -> String?
2239
var makeRef: @Sendable () -> Int
2340

2441
var connect: @Sendable () async -> Void
2542
var addChannel: @Sendable (_ channel: RealtimeChannelV2) -> Void
2643
var removeChannel: @Sendable (_ channel: RealtimeChannelV2) async -> Void
2744
var push: @Sendable (_ message: RealtimeMessageV2) async -> Void
45+
var httpSend: @Sendable (_ request: HTTPRequest) async throws -> HTTPResponse
2846
}
2947

3048
extension Socket {
3149
init(client: RealtimeClientV2) {
3250
self.init(
51+
broadcastURL: { [weak client] in client?.broadcastURL ?? URL(string: "http://localhost")! },
3352
status: { [weak client] in client?.status ?? .disconnected },
3453
options: { [weak client] in client?.options ?? .init() },
3554
accessToken: { [weak client] in client?.mutableState.accessToken },
55+
apiKey: { [weak client] in client?.apikey },
3656
makeRef: { [weak client] in client?.makeRef() ?? 0 },
3757
connect: { [weak client] in await client?.connect() },
3858
addChannel: { [weak client] in client?.addChannel($0) },
3959
removeChannel: { [weak client] in await client?.removeChannel($0) },
40-
push: { [weak client] in await client?.push($0) }
60+
push: { [weak client] in await client?.push($0) },
61+
httpSend: { [weak client] in try await client?.http.send($0) ?? .init(data: Data(), response: HTTPURLResponse()) }
4162
)
4263
}
4364
}
@@ -202,24 +223,64 @@ public final class RealtimeChannelV2: Sendable {
202223
/// - event: Broadcast message event.
203224
/// - message: Message payload.
204225
public func broadcast(event: String, message: JSONObject) async {
205-
assert(
206-
status == .subscribed,
207-
"You can only broadcast after subscribing to the channel. Did you forget to call `channel.subscribe()`?"
208-
)
226+
if status != .subscribed {
227+
struct Message: Encodable {
228+
let topic: String
229+
let event: String
230+
let payload: JSONObject
231+
let `private`: Bool
232+
}
209233

210-
await push(
211-
RealtimeMessageV2(
212-
joinRef: mutableState.joinRef,
213-
ref: socket.makeRef().description,
214-
topic: topic,
215-
event: ChannelEvent.broadcast,
216-
payload: [
217-
"type": "broadcast",
218-
"event": .string(event),
219-
"payload": .object(message),
220-
]
234+
var headers = HTTPHeaders(["content-type": "application/json"])
235+
if let apiKey = socket.apiKey() {
236+
headers["apikey"] = apiKey
237+
}
238+
if let accessToken = socket.accessToken() {
239+
headers["authorization"] = "Bearer \(accessToken)"
240+
}
241+
242+
let task = Task { [headers] in
243+
_ = try? await socket.httpSend(
244+
HTTPRequest(
245+
url: socket.broadcastURL(),
246+
method: .post,
247+
headers: headers,
248+
body: JSONEncoder().encode(
249+
[
250+
"messages": [
251+
Message(
252+
topic: topic,
253+
event: event,
254+
payload: message,
255+
private: config.isPrivate
256+
),
257+
],
258+
]
259+
)
260+
)
261+
)
262+
}
263+
264+
if config.broadcast.acknowledgeBroadcasts {
265+
try? await withTimeout(interval: socket.options().timeoutInterval) {
266+
await task.value
267+
}
268+
}
269+
} else {
270+
await push(
271+
RealtimeMessageV2(
272+
joinRef: mutableState.joinRef,
273+
ref: socket.makeRef().description,
274+
topic: topic,
275+
event: ChannelEvent.broadcast,
276+
payload: [
277+
"type": "broadcast",
278+
"event": .string(event),
279+
"payload": .object(message),
280+
]
281+
)
221282
)
222-
)
283+
}
223284
}
224285

225286
public func track(_ state: some Codable) async throws {

Sources/Realtime/V2/RealtimeClientV2.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public final class RealtimeClientV2: Sendable {
7979
let options: RealtimeClientOptions
8080
let ws: any WebSocketClient
8181
let mutableState = LockIsolated(MutableState())
82+
let http: any HTTPClientType
8283
let apikey: String?
8384

8485
public var subscriptions: [String: RealtimeChannelV2] {
@@ -128,6 +129,12 @@ public final class RealtimeClientV2: Sendable {
128129
}
129130

130131
public convenience init(url: URL, options: RealtimeClientOptions) {
132+
var interceptors: [any HTTPClientInterceptor] = []
133+
134+
if let logger = options.logger {
135+
interceptors.append(LoggerInterceptor(logger: logger))
136+
}
137+
131138
self.init(
132139
url: url,
133140
options: options,
@@ -137,14 +144,24 @@ public final class RealtimeClientV2: Sendable {
137144
apikey: options.apikey
138145
),
139146
options: options
147+
),
148+
http: HTTPClient(
149+
fetch: options.fetch ?? { try await URLSession.shared.data(for: $0) },
150+
interceptors: interceptors
140151
)
141152
)
142153
}
143154

144-
init(url: URL, options: RealtimeClientOptions, ws: any WebSocketClient) {
155+
init(
156+
url: URL,
157+
options: RealtimeClientOptions,
158+
ws: any WebSocketClient,
159+
http: any HTTPClientType
160+
) {
145161
self.url = url
146162
self.options = options
147163
self.ws = ws
164+
self.http = http
148165
apikey = options.apikey
149166

150167
mutableState.withValue {
@@ -471,7 +488,7 @@ public final class RealtimeClientV2: Sendable {
471488
return url
472489
}
473490

474-
private var broadcastURL: URL {
491+
var broadcastURL: URL {
475492
url.appendingPathComponent("api/broadcast")
476493
}
477494
}

0 commit comments

Comments
 (0)