Skip to content
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

[ECO-4948] Spec complete for Presence + Occupancy (excluding room status behaviour) #113

Merged
merged 1 commit into from
Nov 18, 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
4 changes: 2 additions & 2 deletions AblyChat.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"originHash" : "a296396707b7685153f4cf548f6281f483d562002fe11235f1fc3bb053be91d7",
"originHash" : "1ad2d7338668d15feccbf564582941161acd47349bfca8f34374e11c69677ae8",
"pins" : [
{
"identity" : "ably-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ably/ably-cocoa",
"state" : {
"branch" : "main",
"revision" : "4856ba6a423788902a6ef680793e7f404ceb4a51"
"revision" : "f7bff4b1c941b4c7b952b9224a33674e2302e19f"
}
},
{
Expand Down
23 changes: 18 additions & 5 deletions Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ struct ContentView: View {

private func room() async throws -> Room {
let chosenChatClient: ChatClient = (mode == .mock) ? mockChatClient : liveChatClient
return try await chosenChatClient.rooms.get(roomID: roomID, options: .init(reactions: .init()))
return try await chosenChatClient.rooms.get(
roomID: roomID,
options: .init(
presence: .init(),
reactions: .init(),
occupancy: .init()
)
)
}

private var sendTitle: String {
Expand Down Expand Up @@ -127,12 +134,12 @@ struct ContentView: View {
.tryTask { try await attachRoom() }
.tryTask { try await showMessages() }
.tryTask { try await showReactions() }
.tryTask { try await showPresence() }
.tryTask { try await showOccupancy() }
.tryTask {
// NOTE: As we implement more features, move them out of the `if mode == .mock` block and into the main block just above.
if mode == .mock {
try await showPresence()
try await showTypings()
try await showOccupancy()
try await showRoomStatus()
}
}
Expand Down Expand Up @@ -185,9 +192,15 @@ struct ContentView: View {
}

func showPresence() async throws {
for await event in try await room().presence.subscribe(events: [.enter, .leave]) {
try await room().presence.enter(data: .init(userCustomData: ["status": .string("📱 Online")]))

umair-ably marked this conversation as resolved.
Show resolved Hide resolved
for await event in try await room().presence.subscribe(events: [.enter, .leave, .update]) {
withAnimation {
messages.insert(BasicListItem(id: UUID().uuidString, title: "System", text: event.clientID + " \(event.action.displayedText)"), at: 0)
let status = event.data?.userCustomData?["status"]?.value as? String
let clientPresenceChangeMessage = "\(event.clientID) \(event.action.displayedText)"
let presenceMessage = status != nil ? "\(clientPresenceChangeMessage) with status: \(status!)" : clientPresenceChangeMessage

messages.insert(BasicListItem(id: UUID().uuidString, title: "System", text: presenceMessage), at: 0)
}
}
}
Expand Down
37 changes: 8 additions & 29 deletions Example/AblyChatExample/Mocks/MockClients.swift
Original file line number Diff line number Diff line change
Expand Up @@ -277,19 +277,19 @@ actor MockPresence: Presence {
MockStrings.names.shuffled().map { name in
PresenceMember(
clientID: name,
data: ["foo": "bar"],
data: PresenceData(userCustomData: nil),
action: .present,
extras: nil,
updatedAt: Date()
)
}
}

func get(params _: PresenceQuery?) async throws -> [PresenceMember] {
func get(params _: PresenceQuery) async throws -> [PresenceMember] {
MockStrings.names.shuffled().map { name in
PresenceMember(
clientID: name,
data: ["foo": "bar"],
data: PresenceData(userCustomData: nil),
action: .present,
extras: nil,
updatedAt: Date()
Expand All @@ -301,20 +301,7 @@ actor MockPresence: Presence {
fatalError("Not yet implemented")
}

func enter() async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
action: .enter,
clientID: clientID,
timestamp: Date(),
data: nil
)
)
}
}

func enter(data: PresenceData) async throws {
func enter(data: PresenceData? = nil) async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
Expand All @@ -327,28 +314,20 @@ actor MockPresence: Presence {
}
}

func update() async throws {
fatalError("Not yet implemented")
}

func update(data _: PresenceData) async throws {
fatalError("Not yet implemented")
}

func leave() async throws {
func update(data: PresenceData? = nil) async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
action: .leave,
action: .update,
clientID: clientID,
timestamp: Date(),
data: nil
data: data
)
)
}
}

func leave(data: PresenceData) async throws {
func leave(data: PresenceData? = nil) async throws {
for subscription in mockSubscriptions {
subscription.emit(
PresenceEvent(
Expand Down
4 changes: 4 additions & 0 deletions Example/AblyChatExample/Mocks/MockRealtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ final class MockRealtime: NSObject, RealtimeClientProtocol, Sendable {
fatalError("Not implemented")
}

var presence: ARTRealtimePresenceProtocol {
fatalError("Not implemented")
}

var errorReason: ARTErrorInfo? {
fatalError("Not implemented")
}
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"originHash" : "f00ee2e8c80adfe8d72deb089738cdb967aeae43e71837f90d99fc602728fe45",
"originHash" : "b6d25f160b01b473629481d68d4fe734b3981fcd87079531f784c2ade3afdc4d",
"pins" : [
{
"identity" : "ably-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ably/ably-cocoa",
"state" : {
"branch" : "main",
"revision" : "4856ba6a423788902a6ef680793e7f404ceb4a51"
"revision" : "f7bff4b1c941b4c7b952b9224a33674e2302e19f"
}
},
{
Expand Down
56 changes: 56 additions & 0 deletions Sources/AblyChat/DefaultOccupancy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Ably

internal final class DefaultOccupancy: Occupancy, EmitsDiscontinuities {
private let chatAPI: ChatAPI
private let roomID: String
private let logger: InternalLogger
public nonisolated let featureChannel: FeatureChannel

internal nonisolated var channel: any RealtimeChannelProtocol {
featureChannel.channel
}

internal init(featureChannel: FeatureChannel, chatAPI: ChatAPI, roomID: String, logger: InternalLogger) {
self.featureChannel = featureChannel
self.chatAPI = chatAPI
self.roomID = roomID
self.logger = logger
}

// (CHA-04a) Users may register a listener that receives occupancy events in realtime.
// (CHA-04c) When a regular occupancy event is received on the channel (a standard PubSub occupancy event per the docs), the SDK will convert it into occupancy event format and broadcast it to subscribers.
// (CHA-04d) If an invalid occupancy event is received on the channel, it shall be dropped.
internal func subscribe(bufferingPolicy: BufferingPolicy) async -> Subscription<OccupancyEvent> {
logger.log(message: "Subscribing to occupancy events", level: .debug)
let subscription = Subscription<OccupancyEvent>(bufferingPolicy: bufferingPolicy)
channel.subscribe(OccupancyEvents.meta.rawValue) { [logger] message in
logger.log(message: "Received occupancy message: \(message)", level: .debug)
guard let data = message.data as? [String: Any],
let metrics = data["metrics"] as? [String: Any]
else {
let error = ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without data or metrics")
logger.log(message: "Error parsing occupancy message: \(error)", level: .error)
return // (CHA-04d) implies we don't throw an error
}

let connections = metrics["connections"] as? Int ?? 0
let presenceMembers = metrics["presenceMembers"] as? Int ?? 0

let occupancyEvent = OccupancyEvent(connections: connections, presenceMembers: presenceMembers)
logger.log(message: "Emitting occupancy event: \(occupancyEvent)", level: .debug)
subscription.emit(occupancyEvent)
}
return subscription
}

// (CHA-O3) Users can request an instantaneous occupancy check via the REST API. The request is detailed here (https://sdk.ably.com/builds/ably/specification/main/chat-features/#rest-occupancy-request), with the response format being a simple occupancy event
internal func get() async throws -> OccupancyEvent {
logger.log(message: "Getting occupancy for room: \(roomID)", level: .debug)
return try await chatAPI.getOccupancy(roomId: roomID)
}

// (CHA-O5) Users may subscribe to discontinuity events to know when there’s been a break in occupancy. Their listener will be called when a discontinuity event is triggered from the room lifecycle. For occupancy, there shouldn’t need to be user action as most channels will send occupancy updates regularly as clients churn.
internal func subscribeToDiscontinuities() async -> Subscription<ARTErrorInfo> {
await featureChannel.subscribeToDiscontinuities()
}
}
Loading