Skip to content

Commit

Permalink
EventEngine
Browse files Browse the repository at this point in the history
* Introduced "isEmpty" variable in PresenceInput and SubscribeInput
* Unique identifier for Invocation relies on "id" variable instead of RawRepresentable<String>
* Implemented state transitions for Presence EE with unit tests
* HeartbeatEffect and DelayedHeartbeatEffect
* Added separate directories for Subscribe and Presence in tests target
  • Loading branch information
jguz-pubnub committed Jul 26, 2023
1 parent 8397907 commit cdd8c78
Show file tree
Hide file tree
Showing 22 changed files with 1,182 additions and 94 deletions.
116 changes: 92 additions & 24 deletions PubNub.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Sources/PubNub/EventEngine/Core/Dispatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class EffectDispatcher<Invocation: AnyEffectInvocation, Event, Input>: Dispatche
}

func hasPendingInvocation(_ invocation: Invocation) -> Bool {
effectsCache.hasPendingEffect(with: invocation.rawValue)
effectsCache.hasPendingEffect(with: invocation.id)
}

func dispatch(
Expand All @@ -69,9 +69,9 @@ class EffectDispatcher<Invocation: AnyEffectInvocation, Event, Input>: Dispatche
let effectsToRun = invocations.compactMap {
switch $0 {
case .managed(let invocation):
return EffectWrapper(id: invocation.rawValue, effect: factory.effect(for: invocation, with: customInput))
return EffectWrapper(id: invocation.id, effect: factory.effect(for: invocation, with: customInput))
case .cancel(let cancelInvocation):
effectsCache.getEffect(with: cancelInvocation.rawValue)?.cancelTask(); return nil
effectsCache.getEffect(with: cancelInvocation.id)?.cancelTask(); return nil
}
}

Expand Down
20 changes: 10 additions & 10 deletions Sources/PubNub/EventEngine/Core/EventEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@

import Foundation

protocol AnyCancellableInvocation: RawRepresentable<String>, Equatable {
protocol AnyIdentifiableInvocation {
var id: String { get }
}

protocol AnyCancellableInvocation: AnyIdentifiableInvocation, Equatable {

}

protocol AnyEffectInvocation: Equatable, RawRepresentable<String> {
protocol AnyEffectInvocation: AnyIdentifiableInvocation, Equatable {
associatedtype Cancellable: AnyCancellableInvocation
}

Expand All @@ -47,7 +51,7 @@ struct EventEngineCustomInput<Value> {
class EventEngine<State, Event, Invocation: AnyEffectInvocation, Input> {
private let transition: any TransitionProtocol<State, Event, Invocation>
private let dispatcher: any Dispatcher<Invocation, Event, Input>
private var currentState: State
private(set) var state: State

var customInput: EventEngineCustomInput<Input>
// A delegate that's notified when the State object is replaced
Expand All @@ -59,24 +63,20 @@ class EventEngine<State, Event, Invocation: AnyEffectInvocation, Input> {
dispatcher: some Dispatcher<Invocation, Event, Input>,
customInput: EventEngineCustomInput<Input>
) {
self.currentState = state
self.state = state
self.transition = transition
self.dispatcher = dispatcher
self.customInput = customInput
}

var state: State {
currentState
}

func send(event: Event) {
objc_sync_enter(self)
defer { objc_sync_exit(self) }

let transitionResult = transition.transition(from: currentState, event: event)
let transitionResult = transition.transition(from: state, event: event)
let invocations = transitionResult.invocations

currentState = transitionResult.state
state = transitionResult.state

let listener = DispatcherListener<Event>(
onAnyInvocationCompleted: { [weak self] results in
Expand Down
2 changes: 1 addition & 1 deletion Sources/PubNub/EventEngine/Core/TransitionProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ enum EffectInvocation<Invocation: AnyEffectInvocation>: Equatable {
case (let .managed(lhsInvocation), let .managed(rhsInvocation)):
return lhsInvocation == rhsInvocation
case (let .cancel(lhsId), let .cancel(rhsId)):
return lhsId.rawValue == rhsId.rawValue
return lhsId.id == rhsId.id
default:
return false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// DelayedHeartbeatEffect.swift
//
// PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks
// Copyright © 2023 PubNub Inc.
// https://www.pubnub.com/
// https://www.pubnub.com/terms
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

class DelayedHeartbeatEffect: EffectHandler {
private let request: HeartbeatRequest
private let configuration: PubNubConfiguration
private let currentAttempt: Int
private let reason: PubNubError
private var completionBlock: (([Presence.Event]) -> Void)?

init(
request: HeartbeatRequest,
currentAttempt: Int,
reason: PubNubError,
configuration: PubNubConfiguration
) {
self.request = request
self.currentAttempt = currentAttempt
self.reason = reason
self.configuration = configuration
}

func performTask(completionBlock: @escaping ([Presence.Event]) -> Void) {
if currentAttempt <= 2 {
self.completionBlock = completionBlock
self.scheduleHeartbeatCall()
} else {
completionBlock([.heartbeatGiveUp(error: reason)])
}
}

private func scheduleHeartbeatCall() {
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + computeDelay()) { [weak self] in
self?.request.execute() { result in
switch result {
case .success(_):
self?.completionBlock?([.heartbeatSuccess])
case .failure(let error):
self?.completionBlock?([.heartbeatFailed(error: error)])
}
}
}
}

private func computeDelay() -> TimeInterval {
if currentAttempt == 0 {
return 0
} else if currentAttempt == 1 {
return 0.5 * Double(configuration.heartbeatInterval)
} else {
return Double(configuration.heartbeatInterval) - 1.0
}
}

func cancelTask() {
completionBlock = nil
}
}
43 changes: 43 additions & 0 deletions Sources/PubNub/EventEngine/Presence/Effects/HeartbeatEffect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// HeartbeatEffect.swift
//
// PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks
// Copyright © 2023 PubNub Inc.
// https://www.pubnub.com/
// https://www.pubnub.com/terms
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

struct HeartbeatEffect: EffectHandler {
private let request: HeartbeatRequest

func performTask(completionBlock: @escaping ([Presence.Event]) -> Void) {
request.execute() { result in
switch result {
case .success(_):
completionBlock([.heartbeatSuccess])
case .failure(let error):
completionBlock([.heartbeatFailed(error: error)])
}
}
}
}
67 changes: 67 additions & 0 deletions Sources/PubNub/EventEngine/Presence/Helpers/HeartbeatRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// PresenceRequest.swift
//
// PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks
// Copyright © 2023 PubNub Inc.
// https://www.pubnub.com/
// https://www.pubnub.com/terms
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

class HeartbeatRequest {
let channels: [String]
let groups: [String]
let configuration: PubNubConfiguration

private let session: SessionReplaceable
private let sessionResponseQueue: DispatchQueue
private var request: RequestReplaceable?

init(
channels: [String],
groups: [String],
configuration: PubNubConfiguration,
session: SessionReplaceable,
sessionResponseQueue: DispatchQueue
) {
self.channels = channels
self.groups = groups
self.configuration = configuration
self.session = session
self.sessionResponseQueue = sessionResponseQueue
}

func execute(completionBlock: @escaping (Result<Void, PubNubError>) -> Void) {
request = session.request(with: PresenceRouter(
.heartbeat(channels: channels, groups: groups, presenceTimeout: configuration.heartbeatInterval),
configuration: configuration), requestOperator: nil
)
request?.validate().response(on: sessionResponseQueue, decoder: GenericServiceResponseDecoder()) { result in
switch result {
case .success(_):
completionBlock(.success(()))
case .failure(let error):
completionBlock(.failure(error as? PubNubError ?? PubNubError(.unknown, underlying: error)))
}
}
}
}
79 changes: 79 additions & 0 deletions Sources/PubNub/EventEngine/Presence/Helpers/PresenceInput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// PresenceInput.swift
//
// PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks
// Copyright © 2023 PubNub Inc.
// https://www.pubnub.com/
// https://www.pubnub.com/terms
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

struct PresenceInput: Equatable {
fileprivate let channelsSet: Set<String>
fileprivate let groupsSet: Set<String>

var channels: [String] {
channelsSet.allObjects.sorted(by: <)
}

var groups: [String] {
groupsSet.allObjects.sorted(by: <)
}

var isEmpty: Bool {
channelsSet.isEmpty && groupsSet.isEmpty
}

init(channels: [String] = [], groups: [String] = []) {
channelsSet = Set(channels)
groupsSet = Set(groups)
}

fileprivate init(channels: Set<String>, groups: Set<String>) {
channelsSet = channels
groupsSet = groups
}

static func +(lhs: PresenceInput, rhs: PresenceInput) -> PresenceInput {
var uniqueChannels = lhs.channelsSet
var uniqueGroups = lhs.groupsSet

rhs.channelsSet.forEach { uniqueChannels.insert($0) }
rhs.groupsSet.forEach { uniqueGroups.insert($0) }

return PresenceInput(channels: uniqueChannels, groups: uniqueGroups)
}

static func -(lhs: PresenceInput, rhs: PresenceInput) -> PresenceInput {
var uniqueChannels = lhs.channelsSet
var uniqueGroups = lhs.groupsSet

rhs.channelsSet.forEach { uniqueChannels.remove($0) }
rhs.groupsSet.forEach { uniqueGroups.remove($0) }

return PresenceInput(channels: uniqueChannels, groups: uniqueGroups)
}

static func ==(lhs: PresenceInput, rhs: PresenceInput) -> Bool {
lhs.channels == rhs.channels && lhs.groups == rhs.groups
}
}
Loading

0 comments on commit cdd8c78

Please sign in to comment.