Skip to content

Commit 6034f8e

Browse files
WIP single channel
Switch to new channel for messages TODO mention that this needs to come after typing has been changed to single channel JS stuff based on 688d5147e3928f6c40664006eb626d2afae3d33c in ably/ably-chat-js#505 I'm assuming that this needs to be accompanied by the v3 chat API, and am assuming that we need v3 for occupancy, too. Switch reactions to use single channel TODO update failing tests Use ephemeral publishing for reactions Remove feature-specific error codes TODO next we can remove the code that supports this remove the RoomFeature on contributor not needed now we've done 4ea9148 have not updated any wording in tests - waiting for new spec points Put typing indicators on same channel TODO revert - this is just so that I can proceed with single-channel stuff, but this will break typing indicators _and_ presence (functionality / integration tests) change Room code to only create a single channel TODO tidy up some of this contributor / channel difference - and I think there's still just too much complexity left here too Move discontinuities to room level and remove EmitsDiscontinuities, same as JS TODO tests move channel to room level per JS API changes didn't bother with an Implementation class for `Room` since we barely touch the channel inside it; have just written to make sure you use `internalChannel` WIP Make lifecycle manager single-contributor This only contains the internal API changes, to allow me to do some further refactors TODO fill in the behaviour once the spec is done Also worst case we could ship like this, it's massively overengineered for a single contributor but could work WIP remove FeatureChannel and RoomLifecycleContributor TODO implement the changes to manager wip wip TODO check that the non-tested spec poitns (i.e. code comments) are udpated too Update room options API TODO if we have any tests that pass a room option to turn a feature on (e.g. the "merges channel options") one, then remove that Based on [1] at 9103d78. - there's no concept of a feature being turned "on" any more, and correspondingly no error when you try and use a feature that's not turned on - remove `allFeaturesEnabled`, since the equivalent has been removed from JS - you can now fetch a room without passing a room options; this uses the default options [1] ably/ably-chat-js#505 wip update for spec 09ba2c TODO mention that we've resolved 133 (setting of presence-related channel modes)
1 parent f466cdb commit 6034f8e

37 files changed

+2745
-3273
lines changed

Example/AblyChatExample/ContentView.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,7 @@ struct ContentView: View {
7070
}
7171

7272
private func room() async throws -> Room {
73-
try await chatClient.rooms.get(
74-
roomID: roomID,
75-
options: .allFeaturesEnabled
76-
)
73+
try await chatClient.rooms.get(roomID: roomID)
7774
}
7875

7976
private var sendTitle: String {

Example/AblyChatExample/Mocks/MockClients.swift

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ actor MockRoom: Room {
4747
nonisolated let roomID: String
4848
nonisolated let options: RoomOptions
4949

50+
let channel: any RealtimeChannelProtocol = MockRealtime.Channel()
51+
5052
init(roomID: String, options: RoomOptions) {
5153
self.roomID = roomID
5254
self.options = options
@@ -83,19 +85,21 @@ actor MockRoom: Room {
8385
func onStatusChange(bufferingPolicy _: BufferingPolicy) async -> Subscription<RoomStatusChange> {
8486
.init(mockAsyncSequence: createSubscription())
8587
}
88+
89+
func onDiscontinuity(bufferingPolicy _: BufferingPolicy) -> Subscription<DiscontinuityEvent> {
90+
fatalError("Not yet implemented")
91+
}
8692
}
8793

8894
actor MockMessages: Messages {
8995
let clientID: String
9096
let roomID: String
91-
let channel: any RealtimeChannelProtocol
9297

9398
private let mockSubscriptions = MockSubscriptionStorage<Message>()
9499

95100
init(clientID: String, roomID: String) {
96101
self.clientID = clientID
97102
self.roomID = roomID
98-
channel = MockRealtime.Channel()
99103
}
100104

101105
private func createSubscription() -> MockSubscription<Message> {
@@ -179,23 +183,17 @@ actor MockMessages: Messages {
179183
mockSubscriptions.emit(message)
180184
return message
181185
}
182-
183-
func onDiscontinuity(bufferingPolicy _: BufferingPolicy) -> Subscription<DiscontinuityEvent> {
184-
fatalError("Not yet implemented")
185-
}
186186
}
187187

188188
actor MockRoomReactions: RoomReactions {
189189
let clientID: String
190190
let roomID: String
191-
let channel: any RealtimeChannelProtocol
192191

193192
private let mockSubscriptions = MockSubscriptionStorage<Reaction>()
194193

195194
init(clientID: String, roomID: String) {
196195
self.clientID = clientID
197196
self.roomID = roomID
198-
channel = MockRealtime.Channel()
199197
}
200198

201199
private func createSubscription() -> MockSubscription<Reaction> {
@@ -226,23 +224,17 @@ actor MockRoomReactions: RoomReactions {
226224
func subscribe(bufferingPolicy _: BufferingPolicy) -> Subscription<Reaction> {
227225
.init(mockAsyncSequence: createSubscription())
228226
}
229-
230-
func onDiscontinuity(bufferingPolicy _: BufferingPolicy) -> Subscription<DiscontinuityEvent> {
231-
fatalError("Not yet implemented")
232-
}
233227
}
234228

235229
actor MockTyping: Typing {
236230
let clientID: String
237231
let roomID: String
238-
let channel: any RealtimeChannelProtocol
239232

240233
private let mockSubscriptions = MockSubscriptionStorage<TypingEvent>()
241234

242235
init(clientID: String, roomID: String) {
243236
self.clientID = clientID
244237
self.roomID = roomID
245-
channel = MockRealtime.Channel()
246238
}
247239

248240
private func createSubscription() -> MockSubscription<TypingEvent> {
@@ -269,10 +261,6 @@ actor MockTyping: Typing {
269261
func stop() async throws(ARTErrorInfo) {
270262
mockSubscriptions.emit(TypingEvent(currentlyTyping: []))
271263
}
272-
273-
func onDiscontinuity(bufferingPolicy _: BufferingPolicy) -> Subscription<DiscontinuityEvent> {
274-
fatalError("Not yet implemented")
275-
}
276264
}
277265

278266
actor MockPresence: Presence {
@@ -389,23 +377,17 @@ actor MockPresence: Presence {
389377
func subscribe(events _: [PresenceEventType], bufferingPolicy _: BufferingPolicy) -> Subscription<PresenceEvent> {
390378
.init(mockAsyncSequence: createSubscription())
391379
}
392-
393-
func onDiscontinuity(bufferingPolicy _: BufferingPolicy) -> Subscription<DiscontinuityEvent> {
394-
fatalError("Not yet implemented")
395-
}
396380
}
397381

398382
actor MockOccupancy: Occupancy {
399383
let clientID: String
400384
let roomID: String
401-
let channel: any RealtimeChannelProtocol
402385

403386
private let mockSubscriptions = MockSubscriptionStorage<OccupancyEvent>()
404387

405388
init(clientID: String, roomID: String) {
406389
self.clientID = clientID
407390
self.roomID = roomID
408-
channel = MockRealtime.Channel()
409391
}
410392

411393
private func createSubscription() -> MockSubscription<OccupancyEvent> {
@@ -422,10 +404,6 @@ actor MockOccupancy: Occupancy {
422404
func get() async throws(ARTErrorInfo) -> OccupancyEvent {
423405
OccupancyEvent(connections: 10, presenceMembers: 5)
424406
}
425-
426-
func onDiscontinuity(bufferingPolicy _: BufferingPolicy) -> Subscription<DiscontinuityEvent> {
427-
fatalError("Not yet implemented")
428-
}
429407
}
430408

431409
actor MockConnection: Connection {

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ Task {
109109
}
110110
}
111111

112-
// Get a chat room for the tutorial - using the defaults to enable all features in the chat room
112+
// Get a chat room for the tutorial
113113
let room = try await chatClient.rooms.get(
114-
roomID: "readme-getting-started", options: RoomOptions.allFeaturesEnabled)
114+
roomID: "readme-getting-started")
115115

116116
// Add a listener to observe changes to the chat rooms status
117117
let statusSubscription = await room.onStatusChange()

Sources/AblyChat/ChatAPI.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@ import Ably
22

33
internal final class ChatAPI: Sendable {
44
private let realtime: any InternalRealtimeClientProtocol
5-
private let apiVersion = "/chat/v1"
6-
private let apiVersionV2 = "/chat/v2" // TODO: remove v1 after full transition to v2
5+
private let apiVersionV3 = "/chat/v3"
76

87
public init(realtime: any InternalRealtimeClientProtocol) {
98
self.realtime = realtime
109
}
1110

1211
// (CHA-M6) Messages should be queryable from a paginated REST API.
1312
internal func getMessages(roomId: String, params: QueryOptions) async throws(InternalError) -> any PaginatedResult<Message> {
14-
let endpoint = "\(apiVersionV2)/rooms/\(roomId)/messages"
13+
let endpoint = "\(apiVersionV3)/rooms/\(roomId)/messages"
1514
return try await makePaginatedRequest(endpoint, params: params.asQueryItems())
1615
}
1716

@@ -45,7 +44,7 @@ internal final class ChatAPI: Sendable {
4544
throw ARTErrorInfo.create(withCode: 40000, message: "Ensure your Realtime instance is initialized with a clientId.").toInternalError()
4645
}
4746

48-
let endpoint = "\(apiVersionV2)/rooms/\(roomId)/messages"
47+
let endpoint = "\(apiVersionV3)/rooms/\(roomId)/messages"
4948
var body: [String: JSONValue] = ["text": .string(params.text)]
5049

5150
// (CHA-M3b) A message may be sent without metadata or headers. When these are not specified by the user, they must be omitted from the REST payload.
@@ -84,7 +83,7 @@ internal final class ChatAPI: Sendable {
8483
throw ARTErrorInfo.create(withCode: 40000, message: "Ensure your Realtime instance is initialized with a clientId.").toInternalError()
8584
}
8685

87-
let endpoint = "\(apiVersionV2)/rooms/\(modifiedMessage.roomID)/messages/\(modifiedMessage.serial)"
86+
let endpoint = "\(apiVersionV3)/rooms/\(modifiedMessage.roomID)/messages/\(modifiedMessage.serial)"
8887
var body: [String: JSONValue] = [:]
8988
let messageObject: [String: JSONValue] = [
9089
"text": .string(modifiedMessage.text),
@@ -132,7 +131,7 @@ internal final class ChatAPI: Sendable {
132131
// (CHA-M9) A client must be able to delete a message in a room.
133132
// (CHA-M9a) A client may delete a message via the Chat REST API by calling the delete method.
134133
internal func deleteMessage(message: Message, params: DeleteMessageParams) async throws(InternalError) -> Message {
135-
let endpoint = "\(apiVersionV2)/rooms/\(message.roomID)/messages/\(message.serial)/delete"
134+
let endpoint = "\(apiVersionV3)/rooms/\(message.roomID)/messages/\(message.serial)/delete"
136135
var body: [String: JSONValue] = [:]
137136

138137
if let description = params.description {
@@ -170,7 +169,7 @@ internal final class ChatAPI: Sendable {
170169
}
171170

172171
internal func getOccupancy(roomId: String) async throws(InternalError) -> OccupancyEvent {
173-
let endpoint = "\(apiVersion)/rooms/\(roomId)/occupancy"
172+
let endpoint = "\(apiVersionV3)/rooms/\(roomId)/occupancy"
174173
return try await makeRequest(endpoint, method: "GET")
175174
}
176175

Sources/AblyChat/DefaultMessages.swift

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,15 @@ private struct MessageSubscriptionWrapper {
88

99
// TODO: Don't have a strong understanding of why @MainActor is needed here. Revisit as part of https://github.com/ably-labs/ably-chat-swift/issues/83
1010
@MainActor
11-
internal final class DefaultMessages: Messages, EmitsDiscontinuities {
12-
public nonisolated let featureChannel: FeatureChannel
11+
internal final class DefaultMessages: Messages {
12+
public nonisolated let channel: any InternalRealtimeChannelProtocol
1313
private let implementation: Implementation
1414

15-
internal nonisolated init(featureChannel: FeatureChannel, chatAPI: ChatAPI, roomID: String, clientID: String, logger: InternalLogger) async {
16-
self.featureChannel = featureChannel
17-
implementation = await .init(featureChannel: featureChannel, chatAPI: chatAPI, roomID: roomID, clientID: clientID, logger: logger)
15+
internal nonisolated init(channel: any InternalRealtimeChannelProtocol, chatAPI: ChatAPI, roomID: String, clientID: String, logger: InternalLogger) async {
16+
self.channel = channel
17+
implementation = await .init(channel: channel, chatAPI: chatAPI, roomID: roomID, clientID: clientID, logger: logger)
1818
}
1919

20-
internal nonisolated var channel: any RealtimeChannelProtocol {
21-
featureChannel.channel.underlying
22-
}
23-
24-
#if DEBUG
25-
internal nonisolated var testsOnly_internalChannel: any InternalRealtimeChannelProtocol {
26-
featureChannel.channel
27-
}
28-
#endif
29-
3020
internal func subscribe(bufferingPolicy: BufferingPolicy) async throws(ARTErrorInfo) -> MessageSubscription {
3121
try await implementation.subscribe(bufferingPolicy: bufferingPolicy)
3222
}
@@ -47,24 +37,20 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
4737
try await implementation.delete(message: message, params: params)
4838
}
4939

50-
internal func onDiscontinuity(bufferingPolicy: BufferingPolicy) async -> Subscription<DiscontinuityEvent> {
51-
await implementation.onDiscontinuity(bufferingPolicy: bufferingPolicy)
52-
}
53-
5440
/// 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 `Messages` 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.
5541
@MainActor
5642
private final class Implementation: Sendable {
5743
private let roomID: String
58-
public nonisolated let featureChannel: FeatureChannel
44+
public nonisolated let channel: any InternalRealtimeChannelProtocol
5945
private let chatAPI: ChatAPI
6046
private let clientID: String
6147
private let logger: InternalLogger
6248

6349
// UUID acts as a unique identifier for each listener/subscription. MessageSubscriptionWrapper houses the subscription and the serial of when it was attached or resumed.
6450
private var subscriptionPoints: [UUID: MessageSubscriptionWrapper] = [:]
6551

66-
internal nonisolated init(featureChannel: FeatureChannel, chatAPI: ChatAPI, roomID: String, clientID: String, logger: InternalLogger) async {
67-
self.featureChannel = featureChannel
52+
internal nonisolated init(channel: any InternalRealtimeChannelProtocol, chatAPI: ChatAPI, roomID: String, clientID: String, logger: InternalLogger) async {
53+
self.channel = channel
6854
self.chatAPI = chatAPI
6955
self.roomID = roomID
7056
self.clientID = clientID
@@ -94,7 +80,7 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
9480
// (CHA-M4c) When a realtime message with name set to message.created is received, it is translated into a message event, which contains a type field with the event type as well as a message field containing the Message Struct. This event is then broadcast to all subscribers.
9581
// (CHA-M4d) If a realtime message with an unknown name is received, the SDK shall silently discard the message, though it may log at DEBUG or TRACE level.
9682
// (CHA-M5k) Incoming realtime events that are malformed (unknown field should be ignored) shall not be emitted to subscribers.
97-
let eventListener = featureChannel.channel.subscribe(RealtimeMessageName.chatMessage.rawValue) { message in
83+
let eventListener = channel.subscribe(RealtimeMessageName.chatMessage.rawValue) { message in
9884
Task {
9985
// TODO: Revisit errors thrown as part of https://github.com/ably-labs/ably-chat-swift/issues/32
10086
guard let ablyCocoaData = message.data,
@@ -159,7 +145,7 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
159145
guard let self else {
160146
return
161147
}
162-
featureChannel.channel.unsubscribe(eventListener)
148+
channel.unsubscribe(eventListener)
163149
subscriptionPoints.removeValue(forKey: uuid)
164150
}
165151
}
@@ -204,11 +190,6 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
204190
}
205191
}
206192

207-
// (CHA-M7) Users may subscribe to discontinuity events to know when there’s been a break in messages that they need to resolve. Their listener will be called when a discontinuity event is triggered from the room lifecycle.
208-
internal func onDiscontinuity(bufferingPolicy: BufferingPolicy) async -> Subscription<DiscontinuityEvent> {
209-
await featureChannel.onDiscontinuity(bufferingPolicy: bufferingPolicy)
210-
}
211-
212193
private func getBeforeSubscriptionStart(_ uuid: UUID, params: QueryOptions) async throws -> any PaginatedResult<Message> {
213194
guard let subscriptionPoint = subscriptionPoints[uuid]?.serial else {
214195
throw ARTErrorInfo.create(
@@ -230,7 +211,7 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
230211

231212
private func handleChannelEvents(roomId _: String) {
232213
// (CHA-M5c) If a channel leaves the ATTACHED state and then re-enters ATTACHED with resumed=false, then it must be assumed that messages have been missed. The subscription point of any subscribers must be reset to the attachSerial.
233-
_ = featureChannel.channel.on(.attached) { [weak self] stateChange in
214+
_ = channel.on(.attached) { [weak self] stateChange in
234215
Task {
235216
do {
236217
try await self?.handleAttach(fromResume: stateChange.resumed)
@@ -241,7 +222,7 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
241222
}
242223

243224
// (CHA-M4d) If a channel UPDATE event is received and resumed=false, then it must be assumed that messages have been missed. The subscription point of any subscribers must be reset to the attachSerial.
244-
_ = featureChannel.channel.on(.update) { [weak self] stateChange in
225+
_ = channel.on(.update) { [weak self] stateChange in
245226
Task {
246227
do {
247228
try await self?.handleAttach(fromResume: stateChange.resumed)
@@ -276,8 +257,8 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
276257
private func resolveSubscriptionStart() async throws(InternalError) -> String {
277258
logger.log(message: "Resolving subscription start", level: .debug)
278259
// (CHA-M5a) If a subscription is added when the underlying realtime channel is ATTACHED, then the subscription point is the current channelSerial of the realtime channel.
279-
if await featureChannel.channel.state == .attached {
280-
if let channelSerial = featureChannel.channel.properties.channelSerial {
260+
if await channel.state == .attached {
261+
if let channelSerial = channel.properties.channelSerial {
281262
logger.log(message: "Channel is attached, returning channelSerial: \(channelSerial)", level: .debug)
282263
return channelSerial
283264
} else {
@@ -295,8 +276,8 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
295276
private func serialOnChannelAttach() async throws(InternalError) -> String {
296277
logger.log(message: "Resolving serial on channel attach", level: .debug)
297278
// If the state is already 'attached', return the attachSerial immediately
298-
if await featureChannel.channel.state == .attached {
299-
if let attachSerial = featureChannel.channel.properties.attachSerial {
279+
if await channel.state == .attached {
280+
if let attachSerial = channel.properties.attachSerial {
300281
logger.log(message: "Channel is attached, returning attachSerial: \(attachSerial)", level: .debug)
301282
return attachSerial
302283
} else {
@@ -311,15 +292,15 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
311292
// avoids multiple invocations of the continuation
312293
var nillableContinuation: CheckedContinuation<Result<String, InternalError>, Never>? = continuation
313294

314-
_ = featureChannel.channel.on { [weak self] stateChange in
295+
_ = channel.on { [weak self] stateChange in
315296
guard let self else {
316297
return
317298
}
318299

319300
switch stateChange.current {
320301
case .attached:
321302
// Handle successful attachment
322-
if let attachSerial = featureChannel.channel.properties.attachSerial {
303+
if let attachSerial = channel.properties.attachSerial {
323304
logger.log(message: "Channel is attached, returning attachSerial: \(attachSerial)", level: .debug)
324305
nillableContinuation?.resume(returning: .success(attachSerial))
325306
} else {
@@ -330,7 +311,7 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities {
330311
case .failed, .suspended:
331312
// TODO: Revisit as part of https://github.com/ably-labs/ably-chat-swift/issues/32
332313
logger.log(message: "Channel failed to attach", level: .error)
333-
let errorCodeCase = ErrorCode.CaseThatImpliesFixedStatusCode.messagesAttachmentFailed
314+
let errorCodeCase = ErrorCode.CaseThatImpliesFixedStatusCode.roomAttachmentFailed
334315
nillableContinuation?.resume(
335316
returning: .failure(
336317
ARTErrorInfo.create(

0 commit comments

Comments
 (0)