Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
#34: Squashed commit to fix presence.
  • Loading branch information
Simon Bergström committed Oct 5, 2018
commit 13b998ab34f3e295777ad4bc54520b3ec93f3bb5
4 changes: 2 additions & 2 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
github "Quick/Nimble" "v7.1.3"
github "Quick/Quick" "v1.3.1"
github "Quick/Nimble" "v7.3.1"
github "Quick/Quick" "v1.3.2"
github "daltoniam/Starscream" "3.0.5"
4 changes: 2 additions & 2 deletions Sources/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,8 @@ public class Channel {
}

/// The Ref send during the join message.
var joinRef: String {
return self.joinPush.ref ?? ""
var joinRef: String? {
return self.joinPush.ref
}

/// - return: True if the Channel can push messages, meaning the socket
Expand Down
280 changes: 187 additions & 93 deletions Sources/Presence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,128 +13,222 @@
import Foundation

public final class Presence {
// MARK: - Convenience typealiases
static let phx_ref = "phx_ref"
static let diff_joins = "joins"
static let diff_leaves = "leaves"

static public let defaultOptions = PresenceOptions(events: [PresenceEventType.state: "presence_state", PresenceEventType.diff: "presence_diff"])

// MARK: - Enum declarations and classes
public enum PresenceEventType: String {
case state = "state", diff = "diff"
}

public typealias PresenceState = [String: [Meta]]
public typealias Diff = [String: [String: [Meta]]]
public typealias Meta = [String: AnyObject]
public struct PresenceOptions {
let events: [PresenceEventType: String]
}

// MARK: - Convenience typealiases
public typealias PresenceMap = [String: Array<Meta>]
public typealias PresenceState = [String: PresenceMap]
// Diff has keys "joins" and "leaves", pointing to a PresenceState each
// containing the users that joined and left, respectively...
public typealias Diff = [String: PresenceState]
public typealias Meta = [String: Any]
public typealias OnJoinCallback = ((_ key: String, _ currentPresence: PresenceMap?, _ newPresence: PresenceMap) -> Void)?
public typealias OnLeaveCallback = ((_ key: String, _ currentPresence: PresenceMap, _ leftPresence: PresenceMap) -> Void)?
public typealias OnSync = (() -> ())?

public typealias ListBy = (_ key: String, _ presence: PresenceMap) -> Any

// MARK: - Properties
public let channel: Channel

private(set) public var joinRef: String? = nil
private(set) public var pendingDiffs: Array<Diff> = []
private(set) public var state: PresenceState
private(set) public var options: PresenceOptions

// MARK: - Callbacks
public var isPendingSyncState: Bool { return joinRef == nil || joinRef != self.channel.joinRef }

public var onJoin: ((_ id: String, _ meta: Meta) -> ())?
public var onLeave: ((_ id: String, _ meta: Meta) -> ())?
public var onStateChange: ((_ state: PresenceState) -> ())?
// MARK: - Callbacks
public var onJoin: OnJoinCallback
public var onLeave: OnLeaveCallback
public var onSync: OnSync

// MARK: - Initialisation

init(state: PresenceState) {
self.state = state
public convenience init(channel: Channel) {
self.init(channel: channel, options: Presence.defaultOptions)
}

// MARK: - Syncing

func sync(diff: Message) {
// Initial state event
if diff.event == "presence_state" {
diff.payload.forEach{ id, entry in
if let entry = entry as? [String: [Meta]] {
state[id] = entry["metas"]
public init(channel: Channel, options: PresenceOptions) {
self.channel = channel
self.state = [:]
self.options = options
if let state = self.options.events[PresenceEventType.state] {
channel.on(state) { (message: Message) in
if let newState = message.payload as? PresenceState {
self.joinRef = channel.joinRef
self.state = Presence.syncState(self.state, newState: newState, onJoin: self.onJoin, onLeave: self.onLeave)

for diff in self.pendingDiffs {
self.state = Presence.syncDiff(self.state, diff: diff, onJoin: self.onJoin, onLeave: self.onLeave)
}
self.pendingDiffs = []
if let onSync = self.onSync {
onSync()
}
}
}
}
else if diff.event == "presence_diff" {
if let leaves = diff.payload["leaves"] as? Diff {
syncLeaves(diff: leaves)
}
if let joins = diff.payload["joins"] as? Diff {
syncJoins(diff: joins)
if let diff = self.options.events[PresenceEventType.diff] {
channel.on(diff) { (message: Message) in
if let diff = message.payload as? Diff {
if self.isPendingSyncState {
self.pendingDiffs.append(diff)
} else {
self.state = Presence.syncDiff(self.state, diff: diff, onJoin: self.onJoin, onLeave: self.onLeave)
if let onSync = self.onSync {
onSync()
}
}
}
}
}

onStateChange?(state)
}

func syncLeaves(diff: Diff) {
defer {
diff.forEach { id, entry in
if let metas = entry["metas"] {
metas.forEach { onLeave?(id, $0) }
}
}
// MARK: - Syncing
/**
Used to sync the list of presences on the server
with the client's state. An optional `onJoin` and `onLeave` callback can
be provided to react to changes in the client's local presences across
disconnects and reconnects with the server.

- Parameter pState: the current PresenceState

- Parameter pNewState: the new PresenceState sent from the server

- Parameter pOnJoin: an optional callback for the client to react to new users joining

- Parameter pOnLeave: an optional callback for the client to react to users leaving

- Returns: A new PresenceState
*/
static public func syncState(_ pState: PresenceState, newState pNewState: PresenceState,
onJoin pOnJoin: OnJoinCallback, onLeave pOnLeave: OnLeaveCallback) -> PresenceState {
var state = pState
var leaves = pState.filter { (key, value) -> Bool in
!pNewState.contains(where: { $0.key == key })
}
var joins = pNewState.filter { (key, value) -> Bool in
!pState.contains(where: { $0.key == key })
}

for (id, entry) in diff where state[id] != nil {
guard var existing = state[id] else {
continue
}

// If there's only one entry for the id, just remove it.
if existing.count == 1 {
state.removeValue(forKey: id)
continue
pNewState.forEach { (key: String, newPresence: PresenceMap) in
// Looking for differences in metadata of already present users.
if let currentPresence = state[key] {
let curRefs = currentPresence["metas"]!.map { $0[phx_ref] as! String }
let newMetas = newPresence["metas"]!.filter({ (meta: Meta) -> Bool in
curRefs.contains { $0 == meta[phx_ref] as! String }
})
if newMetas.count > 0 {
joins[key] = ["metas": newMetas]
}

let newRefs = newPresence["metas"]!.map { $0[phx_ref] as! String }
let leftMetas = currentPresence["metas"]!.filter({ (meta: Meta) -> Bool in
newRefs.contains { $0 == meta[phx_ref] as! String }
})
if leftMetas.count > 0 {
leaves[key] = ["metas": leftMetas]
}
}

// Otherwise, we need to find the phx_ref keys to delete.
let refsToDelete = entry["metas"]?.map { $0["phx_ref"] as! String }
existing = existing.filter { !refsToDelete!.contains($0["phx_ref"]! as! String) }
state[id] = existing
}

return Presence.syncDiff(state, diff: [diff_joins: joins, diff_leaves: leaves], onJoin: pOnJoin, onLeave: pOnLeave)
}

func syncJoins(diff: Diff) {
diff.forEach { id, entry in
let metas = entry["metas"]

if var existing = state[id] {
existing += metas!
}
static public func syncDiff(_ pState: PresenceState, diff: Diff,
onJoin pOnJoin: OnJoinCallback, onLeave pOnLeave: OnLeaveCallback) -> PresenceState {
var state = pState
guard let joins = diff[diff_joins],
let leaves = diff[diff_leaves]
else {
state[id] = metas
}

metas?.forEach { onJoin?(id, $0) }
}
}

// MARK: - Presence access convenience

public func metas(id: String) -> [Meta]? {
return state[id]
}

public func firstMeta(id: String) -> Meta? {
return state[id]?.first
}

public func firstMetas() -> [String: Meta] {
var result = [String: Meta]()
state.forEach { id, metas in
result[id] = metas.first
// TODO: Do something about this or just force cast instead of guard?
return [:]
}

return result
}

public func firstMetaValue<T>(id: String, key: String) -> T? {
guard let meta = state[id]?.first, let value = meta[key] as? T else {
return nil

for (key, newPresence) in joins {
let currentPresence: PresenceMap? = state[key]
state[key] = newPresence
if currentPresence != nil {
let joinedRefs = state[key]!["metas"]!.map { $0[phx_ref] as! String }
let curMetas = currentPresence!["metas"]!.filter { (meta: Meta) -> Bool in
joinedRefs.contains { $0 == meta[phx_ref] as! String }
}
state[key]!["metas"]!.append(contentsOf: curMetas)
}
if let onJoin = pOnJoin {
onJoin(key, currentPresence, newPresence)
}
}

return value
}

public func firstMetaValues<T>(key: String) -> [T] {
var result = [T]()
state.forEach { id, metas in
if let meta = metas.first, let value = meta[key] as? T {
result.append(value)
for (key, leftPresence) in leaves {
if let currentPresence = state[key] {
let refsToRemove = leftPresence["metas"]!.map { $0[phx_ref] as! String }
let keepMetas = currentPresence["metas"]!.filter { (meta: Meta) -> Bool in
!refsToRemove.contains { $0 == meta[phx_ref] as! String }
}

if let onLeave = pOnLeave {
onLeave(key, currentPresence, leftPresence)
}
if keepMetas.count > 0 {
state[key]!["metas"] = keepMetas
} else {
state.removeValue(forKey: key)
}
}
}
return result

return state
}

// MARK: - Presence access convenience

// public func metas(id: String) -> PresenceMap? {
// return state[id]
// }
//
// public func firstMeta(id: String) -> PresenceMap? {
// return state[id]?.first
// }
//
// public func firstMetas() -> [String: Meta] {
// var result = [String: Meta]()
// state.forEach { id, metas in
// result[id] = metas.first
// }
//
// return result
// }
//
// public func firstMetaValue<T>(id: String, key: String) -> T? {
// guard let meta = state[id]?.first, let value = meta[key] as? T else {
// return nil
// }
//
// return value
// }
//
// public func firstMetaValues<T>(key: String) -> [T] {
// var result = [T]()
// state.forEach { id, metas in
// if let meta = metas.first, let value = meta[key] as? T {
// result.append(value)
// }
// }
//
// return result
// }
}
14 changes: 13 additions & 1 deletion SwiftPhoenixClient.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
12F879272097A3FC00161C02 /* PhxTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12F879262097A3FC00161C02 /* PhxTimer.swift */; };
63CD642E1DB11E8400C406A6 /* Presence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD642D1DB11E8400C406A6 /* Presence.swift */; };
63CD64321DB1272400C406A6 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD64311DB1272400C406A6 /* Message.swift */; };
B425205C2165203200700CBD /* PresenceStateSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B425205B2165203200700CBD /* PresenceStateSpec.swift */; };
B425205E2165D48900700CBD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B425205D2165D48900700CBD /* Helpers.swift */; };
B48458A82164E3D300466E32 /* PresenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48458A62164E28500466E32 /* PresenceSpec.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -54,8 +57,11 @@
12C140DA20AF41A900184725 /* Mocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocks.swift; sourceTree = "<group>"; };
12EAFF27202F654300685575 /* SocketSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketSpec.swift; sourceTree = "<group>"; };
12F879262097A3FC00161C02 /* PhxTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhxTimer.swift; sourceTree = "<group>"; };
63CD642D1DB11E8400C406A6 /* Presence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Presence.swift; sourceTree = "<group>"; };
63CD642D1DB11E8400C406A6 /* Presence.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = Presence.swift; sourceTree = "<group>"; tabWidth = 2; };
63CD64311DB1272400C406A6 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
B425205B2165203200700CBD /* PresenceStateSpec.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = PresenceStateSpec.swift; sourceTree = "<group>"; tabWidth = 2; };
B425205D2165D48900700CBD /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
B48458A62164E28500466E32 /* PresenceSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresenceSpec.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -124,6 +130,9 @@
124B95C120AF41190012006C /* PushSpec.swift */,
12C140D720AF415900184725 /* ChannelSpec.swift */,
12C140DA20AF41A900184725 /* Mocks.swift */,
B48458A62164E28500466E32 /* PresenceSpec.swift */,
B425205B2165203200700CBD /* PresenceStateSpec.swift */,
B425205D2165D48900700CBD /* Helpers.swift */,
);
path = Tests;
sourceTree = "<group>";
Expand Down Expand Up @@ -265,9 +274,12 @@
buildActionMask = 2147483647;
files = (
12A3983D20D00050003203DF /* PushSpec.swift in Sources */,
B425205E2165D48900700CBD /* Helpers.swift in Sources */,
B425205C2165203200700CBD /* PresenceStateSpec.swift in Sources */,
12C140D920AF416600184725 /* ChannelSpec.swift in Sources */,
12EAFF29202F656300685575 /* SocketSpec.swift in Sources */,
12C140DB20AF41A900184725 /* Mocks.swift in Sources */,
B48458A82164E3D300466E32 /* PresenceSpec.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
2 changes: 1 addition & 1 deletion Tests/ChannelSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class ChannelSpec: QuickSpec {
}

describe(".push(event:, payload:, timeout:)") {
it("should send the push if the channel canp ush", closure: {
it("should send the push if the channel can push", closure: {
channel.joinedOnce = true
channel.state = ChannelState.joined
mockSocket.isConnected = true
Expand Down
Loading