-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathDefaultRoomReactions.swift
More file actions
109 lines (91 loc) · 5.69 KB
/
DefaultRoomReactions.swift
File metadata and controls
109 lines (91 loc) · 5.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import Ably
internal final class DefaultRoomReactions: RoomReactions {
private let implementation: Implementation
internal init(channel: any InternalRealtimeChannelProtocol, clientID: String, roomName: String, logger: InternalLogger) {
implementation = .init(channel: channel, clientID: clientID, roomName: roomName, logger: logger)
}
internal func send(params: SendReactionParams) async throws(ARTErrorInfo) {
try await implementation.send(params: params)
}
@discardableResult
internal func subscribe(_ callback: @escaping @MainActor (RoomReactionEvent) -> Void) -> SubscriptionProtocol {
implementation.subscribe(callback)
}
/// This class exists to make sure that the internals of the SDK only access ably-cocoa via the `InternalRealtimeChannelProtocol` interface. It does this by removing access to the `channel` property that exists as part of the public API of the `RoomReactions` protocol, making it unlikely that we accidentally try to call the `ARTRealtimeChannelProtocol` interface. We can remove this `Implementation` class when we remove the feature-level `channel` property in https://github.com/ably/ably-chat-swift/issues/242.
@MainActor
private final class Implementation: Sendable {
private let channel: any InternalRealtimeChannelProtocol
private let roomName: String
private let logger: InternalLogger
private let clientID: String
internal init(channel: any InternalRealtimeChannelProtocol, clientID: String, roomName: String, logger: InternalLogger) {
self.roomName = roomName
self.channel = channel
self.logger = logger
self.clientID = clientID
}
// (CHA-ER3) Ephemeral room reactions are sent to Ably via the Realtime connection via a send method.
// (CHA-ER3d) Reactions are sent on the channel using a message in a particular format - see spec for format.
internal func send(params: SendReactionParams) async throws(ARTErrorInfo) {
do {
logger.log(message: "Sending reaction with params: \(params)", level: .debug)
let dto = RoomReactionDTO(name: params.name, metadata: params.metadata, headers: params.headers)
try await channel.publish(
RoomReactionEvents.reaction.rawValue,
data: dto.data.toJSONValue,
extras: dto.extras.toJSONObject
)
} catch {
throw error.toARTErrorInfo()
}
}
// (CHA-ER4) A user may subscribe to reaction events in Realtime.
// (CHA-ER4a) A user may provide a listener to subscribe to reaction events. This operation must have no side-effects in relation to room or underlying status. When a realtime message with name roomReaction is received, this message is converted into a reaction object and emitted to subscribers.
@discardableResult
internal func subscribe(_ callback: @escaping @MainActor (RoomReactionEvent) -> Void) -> SubscriptionProtocol {
logger.log(message: "Subscribing to reaction events", level: .debug)
// (CHA-ER4c) Realtime events with an unknown name shall be silently discarded.
let eventListener = channel.subscribe(RoomReactionEvents.reaction.rawValue) { [clientID, logger] message in
logger.log(message: "Received roomReaction message: \(message)", level: .debug)
do {
guard let ablyCocoaData = message.data else {
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without data")
}
guard let messageClientID = message.clientId else {
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without clientId")
}
guard let timestamp = message.timestamp else {
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without timestamp")
}
guard let ablyCocoaExtras = message.extras else {
throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without extras")
}
let dto = try RoomReactionDTO(
data: .init(jsonValue: .init(ablyCocoaData: ablyCocoaData)),
extras: .init(jsonObject: JSONValue.objectFromAblyCocoaExtras(ablyCocoaExtras))
)
// (CHA-ER4d) Realtime events that are malformed (unknown fields should be ignored) shall not be emitted to listeners.
let reaction = RoomReaction(
name: dto.name,
metadata: dto.metadata ?? [:],
headers: dto.headers ?? [:],
createdAt: timestamp,
clientID: messageClientID,
isSelf: messageClientID == clientID
)
let event = RoomReactionEvent(type: .reaction, reaction: reaction)
logger.log(message: "Emitting room reaction: \(reaction)", level: .debug)
callback(event)
} catch {
logger.log(message: "Error processing incoming reaction message: \(error)", level: .error)
}
}
return Subscription { [weak self] in
self?.channel.unsubscribe(eventListener)
}
}
private enum RoomReactionsError: Error {
case noReferenceToSelf
}
}
}