Skip to content

Commit

Permalink
Allow a lifecycle operation to await another one
Browse files Browse the repository at this point in the history
We’ll use this mechanism to implement CHA-RL1d and CHA-RL3c for #52.
  • Loading branch information
lawrence-forooghian committed Oct 15, 2024
1 parent 8203f81 commit 60a6ccc
Showing 1 changed file with 88 additions and 3 deletions.
91 changes: 88 additions & 3 deletions Sources/AblyChat/RoomLifecycleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ internal actor RoomLifecycleManager<Contributor: RoomLifecycleContributor> {
private var listenForStateChangesTask: Task<Void, Never>!
// TODO: clean up old subscriptions (https://github.com/ably-labs/ably-chat-swift/issues/36)
private var subscriptions: [Subscription<RoomStatusChange>] = []
private var operationResultContinuations = OperationResultContinuations()

// MARK: - Initializers and `deinit`

Expand Down Expand Up @@ -376,12 +377,88 @@ internal actor RoomLifecycleManager<Contributor: RoomLifecycleContributor> {
status.operationID != nil
}

/// Stores bookkeeping information needed for allowing one operation to await the result of another.
private struct OperationResultContinuations {
typealias Continuation = CheckedContinuation<Void, Error>

private var operationResultContinuationsByOperationID: [UUID: [Continuation]] = [:]

mutating func addContinuation(_ continuation: Continuation, forResultOfOperationWithID operationID: UUID) {
operationResultContinuationsByOperationID[operationID, default: []].append(continuation)
}

mutating func removeContinuationsForResultOfOperationWithID(_ waitedOperationID: UUID) -> [Continuation] {
operationResultContinuationsByOperationID.removeValue(forKey: waitedOperationID) ?? []
}
}

/// Waits for the operation with ID `waitedOperationID` to complete, re-throwing any error thrown by that operation.
///
/// Note that this method currently treats all waited operations as throwing. If you wish to wait for an operation that you _know_ to be non-throwing (which the RELEASE operation currently is) then you’ll need to call this method with `try!` or equivalent. (It might be possible to improve this in the future, but I didn’t want to put much time into figuring it out.)
///
/// - Parameters:
/// - waitedOperationID: The ID of the operation whose completion will be awaited.
/// - waitingOperationID: The ID of the operation which is awaiting this result. Only used for logging.
private func waitForCompletionOfOperationWithID(
_ waitedOperationID: UUID,
waitingOperationID: UUID
) async throws {
logger.log(message: "Operation \(waitingOperationID) started waiting for result of operation \(waitedOperationID)", level: .debug)

do {
try await withCheckedThrowingContinuation { (continuation: OperationResultContinuations.Continuation) in
// TODO: how can we be sure that these don't result in actor hopping? i.e. that the synchronous part of `resultOfOperationWithID` happens immediately
operationResultContinuations.addContinuation(continuation, forResultOfOperationWithID: waitedOperationID)
}

logger.log(message: "Operation \(waitingOperationID) completed waiting for result of operation \(waitedOperationID), which completed successfully", level: .debug)
} catch {
logger.log(message: "Operation \(waitingOperationID) completed waiting for result of operation \(waitedOperationID), which threw error \(error)", level: .debug)
}
}

// TODO: who calls this? I don't think the `defer` can? but we don't want to have to call it each time an operation completes. ok, that's what i'm implementing with performAnOperation
private func operationWithID(_ operationID: UUID, didCompleteWithResult result: Result<Void, Error>) {
logger.log(message: "Operation \(operationID) completed with result \(result)", level: .debug)
let continuationsToResume = operationResultContinuations.removeContinuationsForResultOfOperationWithID(operationID)

for continuation in continuationsToResume {
continuation.resume(with: result)
}
}

// TODO: make it clear that what we have _not_ implemented is a queue — each operation has its own logic for whether it should proceed in relation to other operations

// TODO: what is this
// TODO: use this for all operations
// TODO: note that the operation is isolated to the actor (or is it? not sure — find out)
private func performAnOperation<Failure: Error>(_ body: (UUID) async throws(Failure) -> Void) async throws(Failure) {
let operationID = UUID()
logger.log(message: "Performing operation \(operationID)", level: .debug)
let result: Result<Void, Failure>
do {
try await body(operationID)
result = .success(())
} catch {
result = .failure(error)
}

// TODO: I guess the fact that the operation hasn't returned doesn't matter?
operationWithID(operationID, didCompleteWithResult: result.mapError { $0 })

try result.get()
}

// MARK: - ATTACH operation

/// Implements CHA-RL1’s `ATTACH` operation.
internal func performAttachOperation() async throws {
let operationID = UUID()
try await performAnOperation { operationID in
try await bodyOfAttachOperation(operationID: operationID)
}
}

private func bodyOfAttachOperation(operationID: UUID) async throws {
switch status {
case .attached:
// CHA-RL1a
Expand Down Expand Up @@ -476,8 +553,12 @@ internal actor RoomLifecycleManager<Contributor: RoomLifecycleContributor> {

/// Implements CHA-RL2’s DETACH operation.
internal func performDetachOperation() async throws {
let operationID = UUID()
try await performAnOperation { operationID in
try await bodyOfDetachOperation(operationID: operationID)
}
}

private func bodyOfDetachOperation(operationID: UUID) async throws {
switch status {
case .detached:
// CHA-RL2a
Expand Down Expand Up @@ -559,8 +640,12 @@ internal actor RoomLifecycleManager<Contributor: RoomLifecycleContributor> {

/// Implements CHA-RL3’s RELEASE operation.
internal func performReleaseOperation() async {
let operationID = UUID()
await performAnOperation { operationID in
await bodyOfReleaseOperation(operationID: operationID)
}
}

private func bodyOfReleaseOperation(operationID: UUID) async {
switch status {
case .released:
// CHA-RL3a
Expand Down

0 comments on commit 60a6ccc

Please sign in to comment.