diff --git a/.swiftlint.yml b/.swiftlint.yml index 0dc0e03..cd28ee9 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -11,5 +11,6 @@ identifier_name: excluded: - id - to + - i disabled_rules: - trailing_comma diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..7e575fc --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,106 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.1) + activesupport (4.2.11.1) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + algoliasearch (1.27.1) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + claide (1.0.3) + cocoapods (1.8.4) + activesupport (>= 4.0.2, < 5) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.8.4) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 1.2.2, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-stats (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.6.6) + nap (~> 1.0) + ruby-macho (~> 1.4) + xcodeproj (>= 1.11.1, < 2.0) + cocoapods-core (1.8.4) + activesupport (>= 4.0.2, < 6) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + cocoapods-deintegrate (1.0.4) + cocoapods-downloader (1.2.2) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.0) + cocoapods-stats (1.1.0) + cocoapods-trunk (1.4.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.1.0) + colored2 (3.1.2) + concurrent-ruby (1.1.6) + escape (0.0.4) + ffi (1.12.2) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + httpclient (2.8.3) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jazzy (0.11.1) + cocoapods (~> 1.5) + mustache (~> 1.1) + open4 + redcarpet (~> 3.4) + rouge (>= 2.0.6, < 4.0) + sassc (~> 2.1) + sqlite3 (~> 1.3) + xcinvoke (~> 0.3.0) + json (2.2.0) + liferaft (0.0.6) + minitest (5.14.0) + molinillo (0.6.6) + mustache (1.1.0) + nanaimo (0.2.6) + nap (1.1.0) + netrc (0.11.0) + open4 (1.3.4) + redcarpet (3.5.0) + rouge (2.0.7) + ruby-macho (1.4.0) + sassc (2.2.1) + ffi (~> 1.9) + sqlite3 (1.4.1) + thread_safe (0.3.6) + tzinfo (1.2.6) + thread_safe (~> 0.1) + xcinvoke (0.3.0) + liferaft (~> 0.0.6) + xcodeproj (1.12.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) + xcpretty (0.3.0) + rouge (~> 2.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + jazzy (= 0.11.1) + xcpretty + +BUNDLED WITH + 2.1.4 diff --git a/Makefile b/Makefile index f5c3fbf..5ade8ff 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: docs format test lint xcode linuxmain autocorrect clean test build +.PHONY: docs format test lint xcode linuxmain autocorrect clean test build protobuf APP="UB" @@ -47,3 +47,6 @@ linuxmain: format: swiftformat . + +protobuf: + protoc --swift_out=Sources/UB/Protobuf/ --proto_path=../protobufs/ Packet.proto diff --git a/Sources/UB/Extensions/Array.swift b/Sources/UB/Extensions/Array.swift new file mode 100644 index 0000000..dbd5854 --- /dev/null +++ b/Sources/UB/Extensions/Array.swift @@ -0,0 +1,9 @@ +import Foundation + +extension Array where Element == Addr { + func closest(to: Addr) -> Addr? { + return self.min { a, b in + a.distance(to: to) < b.distance(to: to) + } + } +} diff --git a/Sources/UB/Extensions/Packet.swift b/Sources/UB/Extensions/Packet.swift new file mode 100644 index 0000000..51fab88 --- /dev/null +++ b/Sources/UB/Extensions/Packet.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Packet { + static func new(topic: Data, type: TypeEnum, body: Data) -> Packet { + return Packet.with { + $0.topic = topic + $0.type = type + $0.body = body + } + } +} diff --git a/Sources/UB/Message.swift b/Sources/UB/Message.swift deleted file mode 100644 index 5f0ac73..0000000 --- a/Sources/UB/Message.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -/// Message represents the message sent between nodes. -public struct Message: Equatable { - /// The message service. - public let service: UBID - - /// The recipient of the message. - public let recipient: Addr - - /// The sender of the message. - public let from: Addr - - /// The origin of the message, or the original sender. - /// Differs from the `sender` as that changes on every hop. - public let origin: Addr - - /// The raw message data. - public let message: Data - - /// Initializes a message with the passed data. - /// - /// - Parameters: - /// - service: The message service. - /// - recipient: The recipient of the message. - /// - from: The previous sender of the message. - /// - origin: The origin of the message, or the original sender. - /// Differs from the `sender` as that changes on every hop. - /// - message: The raw message data. - public init(service: UBID, recipient: Addr, from: Addr, origin: Addr, message: Data) { - self.service = service - self.recipient = recipient - self.from = from - self.origin = origin - self.message = message - } - - /// Initializes a Message with a packet and a from addr. - /// - /// - Parameters - /// - protobuf: The protocol buffer. - /// - from: The from address. - init(protobuf: Packet, from: Addr) { - service = UBID(protobuf.service) - recipient = Addr(protobuf.recipient) - self.from = from - origin = Addr(protobuf.origin) - message = protobuf.body - } - - func toProto() -> Packet { - return Packet.with { - $0.service = Data(service) - $0.recipient = Data(recipient) - $0.origin = Data(origin) - $0.body = message - } - } -} - -// @todo encoding and decoding diff --git a/Sources/UB/Node.swift b/Sources/UB/Node.swift index e72c4b6..bbcae95 100644 --- a/Sources/UB/Node.swift +++ b/Sources/UB/Node.swift @@ -11,6 +11,15 @@ public class Node { /// The nodes delegate. public weak var delegate: NodeDelegate? + /// The current subscribed to topic. + public private(set) var topics = [UBID]() + + /// The parent for a specific topic for the given peer. + public private(set) var parents = [UBID: Addr]() + + /// The children for a specific topic for the given peer. + public private(set) var children = [UBID: [Addr]]() + /// Initializes a node. public init() {} @@ -42,59 +51,104 @@ public class Node { transports.removeValue(forKey: transport) } - /// Sends a message through the current transports. + /// Sends data through the current transports. /// /// - Parameters: - /// - message: The message to send. - public func send(_ message: Message) { - if message.recipient.count == 0, message.service.count == 0 { + /// - topic: The topic to send the data to. + /// - data: The data to send. + public func send(topic: UBID, data: Data) { + if topic.count == 0 { return } - guard let data = try? message.toProto().serializedData() else { + let packet = Packet.new(topic: Data(topic), type: .message, body: data) + guard let data = try? packet.serializedData() else { + // @todo error return } - transports.forEach { _, transport in - let peers = transport.peers - - // @todo ensure that messages are delivered? - // what this does is try to send a message to an exact target or broadcast it to all peers - if message.recipient.count != 0 { - if peers.contains(where: { $0.id == message.recipient }) { - return transport.send(message: data, to: message.recipient) - } - } + send(topic: topic, message: data, except: nil) + } - // what this does is send a message to anyone that implements a specific service - if message.service.count != 0 { - let filtered = peers.filter { $0.services.contains { $0 == message.service } } - if filtered.count > 0 { - let sends = flood(message, data: data, transport: transport, peers: filtered) - if sends > 0 { - return - } - } - } - _ = flood(message, data: data, transport: transport, peers: peers) + /// Subscribes a to a specific topic. + /// + /// - Parameter + /// - topic: The topic to subscribe to. + public func subscribe(_ topic: UBID) { + if topics.contains(topic) { + return } + + topics.append(topic) + subscribeTo(topic) } - private func flood(_ message: Message, data: Data, transport: Transport, peers: [Peer]) -> Int { - var sends = 0 - peers.forEach { - if $0.id == message.from || $0.id == message.origin { - return - } + /// Unsubscribe from a specific topic. + /// + /// - Parameter + /// - topic: The topic to unsubscribe from. + public func unsubscribe(_ topic: UBID) { + topics.removeAll(where: { $0 == topic }) + unsubscribeFrom(topic) + } + + private func subscribeTo(_ topic: UBID) { + if parents[topic] != nil { + return + } - sends += 1 - transport.send(message: data, to: $0.id) + let packet = Packet.new(topic: Data(topic), type: .subscribe, body: Data(count: 0)) + guard let data = try? packet.serializedData() else { + return // @todo error } - return sends + let topicAddr = Addr(topic) + let potential = closest(toTopic: topic) + let sort = potential.sorted { + $0.key.distance(to: topicAddr) < $1.key.distance(to: topicAddr) + } + + guard let closest = sort.first else { + return + } + +// if closest.key.distance(to: topicAddr) > self.id.distance(to: topicAddr) { +// return +// } + + transports[closest.value]?.send(message: data, to: closest.key) + parents[topic] = closest.key } - // @todo create a message send loop with retransmissions and shit + private func unsubscribeFrom(_ topic: UBID) { + if children[topic] != nil, children[topic]!.count > 0 { + return + } + + let packet = Packet.new(topic: Data(topic), type: .unsubscribe, body: Data(count: 0)) + guard let data = try? packet.serializedData() else { + // @todo error + return + } + + guard let parent = parents[topic] else { return } + transports.forEach { _, transport in + if transport.peers.contains(parent) { + transport.send(message: data, to: parent) + } + } + } + + private func closest(toTopic to: UBID) -> [Addr: String] { + var potential = [Addr: String]() + transports.forEach { label, transport in + if let close = transport.peers.closest(to: Addr(to)) { + potential[close] = label + } + } + + return potential + } } /// :nodoc: @@ -111,6 +165,88 @@ extension Node: TransportDelegate { return } - delegate?.node(self, didReceiveMessage: Message(protobuf: packet, from: from)) + let topic = UBID(packet.topic) + + switch packet.type { + case .subscribe: + return didReceiveSubscribe(from: from, topic: topic) + case .unsubscribe: + return didReceiveUnsubscribe(from: from, topic: topic) + default: + break + } + + send(topic: topic, message: packet.body, except: from) + + if !topics.contains(topic) { + return + } + + delegate?.node(self, didReceiveData: packet.body) + } + + public func transport(_: Transport, peerDidDisconnect peer: Addr) { + let childTopics = children.filter { $0.value.contains(peer) } + childTopics.forEach { topic, _ in + didReceiveUnsubscribe(from: peer, topic: topic) + } + + let topics = parents.filter { $0.value == peer } + topics.forEach { topic, _ in + parents.removeValue(forKey: topic) + subscribeTo(topic) + } + } + + /// Send a message to the parent and children excluding a specific address. + /// + /// - Parameters: + /// - topic: The topic to send to. + /// - message: The message to send. + /// - except: The address to exclude. (Optional) + func send(topic: UBID, message: Data, except: Addr?) { + var forwarding = [Addr]() + if let parent = parents[topic] { + forwarding.append(parent) + } + + if let child = children[topic] { + forwarding.append(contentsOf: child) + } + + if except != nil { + forwarding = forwarding.filter { $0 != except } + } + + let peers = Set(forwarding) + transports.forEach { _, transport in + peers.intersection(Set(transport.peers)).forEach { + transport.send(message: message, to: $0) + } + } + } + + private func didReceiveSubscribe(from: Addr, topic: UBID) { + if children[topic] == nil { + children[topic] = [Addr]() + } else if children[topic]!.contains(from) { + return + } + + if !topics.contains(topic) { + subscribeTo(topic) + } + + children[topic]!.append(from) + } + + private func didReceiveUnsubscribe(from: Addr, topic: UBID) { + guard children[topic] != nil else { + return + } + + children[topic]!.removeAll(where: { $0 == from }) + + unsubscribeFrom(topic) } } diff --git a/Sources/UB/NodeDelegate.swift b/Sources/UB/NodeDelegate.swift index 346d7e3..4ca886d 100644 --- a/Sources/UB/NodeDelegate.swift +++ b/Sources/UB/NodeDelegate.swift @@ -2,10 +2,10 @@ import Foundation /// An interface used to handle events on the Node. public protocol NodeDelegate: AnyObject { - /// This method is called when a node receives a message. + /// This method is called when a node receives a data. /// /// - Parameters: - /// - node: The node that received the message. - /// - message: The received message. - func node(_ node: Node, didReceiveMessage message: Message) // @todo return something? + /// - node: The node that received the data. + /// - data: The received data. + func node(_ node: Node, didReceiveData data: Data) // @todo return something? } diff --git a/Sources/UB/Peer.swift b/Sources/UB/Peer.swift deleted file mode 100644 index 8c94bb9..0000000 --- a/Sources/UB/Peer.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -// @todo clean this up properly, currently very rough for testing purposes. - -/// Represents the nodes a transport can communicate with. -public class Peer { - /// The peers id. - public let id: Addr - - /// The services a peer knows. - public let services: [UBID] - - /// Initializes a peer with a specified id and list of known services. - /// - /// - Parameters: - /// - id: The peer id. - /// - services: The services a peer can knows. - init(id: Addr, services: [UBID]) { - self.id = id - self.services = services - } -} diff --git a/Sources/UB/Protobuf/Packet.pb.swift b/Sources/UB/Protobuf/Packet.pb.swift index b13b7de..a9b0c1b 100644 --- a/Sources/UB/Protobuf/Packet.pb.swift +++ b/Sources/UB/Protobuf/Packet.pb.swift @@ -24,64 +24,108 @@ struct Packet { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - var service: Data = SwiftProtobuf.Internal.emptyData + var type: Packet.TypeEnum = .message - var origin: Data = SwiftProtobuf.Internal.emptyData - - var recipient: Data = SwiftProtobuf.Internal.emptyData + var topic: Data = SwiftProtobuf.Internal.emptyData var body: Data = SwiftProtobuf.Internal.emptyData var unknownFields = SwiftProtobuf.UnknownStorage() + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case message // = 0 + case subscribe // = 1 + case unsubscribe // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .message + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .message + case 1: self = .subscribe + case 2: self = .unsubscribe + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .message: return 0 + case .subscribe: return 1 + case .unsubscribe: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + } + init() {} } +#if swift(>=4.2) + +extension Packet.TypeEnum: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [Packet.TypeEnum] = [ + .message, + .subscribe, + .unsubscribe, + ] +} + +#endif // swift(>=4.2) + // MARK: - Code below here is support for the SwiftProtobuf runtime. extension Packet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "Packet" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "service"), - 2: .same(proto: "origin"), - 3: .same(proto: "recipient"), - 4: .same(proto: "body"), + 1: .same(proto: "type"), + 2: .same(proto: "topic"), + 3: .same(proto: "body"), ] mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { switch fieldNumber { - case 1: try decoder.decodeSingularBytesField(value: &self.service) - case 2: try decoder.decodeSingularBytesField(value: &self.origin) - case 3: try decoder.decodeSingularBytesField(value: &self.recipient) - case 4: try decoder.decodeSingularBytesField(value: &self.body) + case 1: try decoder.decodeSingularEnumField(value: &self.type) + case 2: try decoder.decodeSingularBytesField(value: &self.topic) + case 3: try decoder.decodeSingularBytesField(value: &self.body) default: break } } } func traverse(visitor: inout V) throws { - if !self.service.isEmpty { - try visitor.visitSingularBytesField(value: self.service, fieldNumber: 1) + if self.type != .message { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) } - if !self.origin.isEmpty { - try visitor.visitSingularBytesField(value: self.origin, fieldNumber: 2) - } - if !self.recipient.isEmpty { - try visitor.visitSingularBytesField(value: self.recipient, fieldNumber: 3) + if !self.topic.isEmpty { + try visitor.visitSingularBytesField(value: self.topic, fieldNumber: 2) } if !self.body.isEmpty { - try visitor.visitSingularBytesField(value: self.body, fieldNumber: 4) + try visitor.visitSingularBytesField(value: self.body, fieldNumber: 3) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Packet, rhs: Packet) -> Bool { - if lhs.service != rhs.service {return false} - if lhs.origin != rhs.origin {return false} - if lhs.recipient != rhs.recipient {return false} + if lhs.type != rhs.type {return false} + if lhs.topic != rhs.topic {return false} if lhs.body != rhs.body {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } + +extension Packet.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "MESSAGE"), + 1: .same(proto: "SUBSCRIBE"), + 2: .same(proto: "UNSUBSCRIBE"), + ] +} diff --git a/Sources/UB/Transports/CoreBluetoothTransport.swift b/Sources/UB/Transports/CoreBluetoothTransport.swift index e31d8a9..a873c2a 100644 --- a/Sources/UB/Transports/CoreBluetoothTransport.swift +++ b/Sources/UB/Transports/CoreBluetoothTransport.swift @@ -7,7 +7,7 @@ public class CoreBluetoothTransport: NSObject { public weak var delegate: TransportDelegate? /// :nodoc: - public fileprivate(set) var peers = [Peer]() + public fileprivate(set) var peers = [Addr]() private let centralManager: CBCentralManager private let peripheralManager: CBPeripheralManager @@ -59,7 +59,7 @@ public class CoreBluetoothTransport: NSObject { private func remove(peer: Addr) { peripherals.removeValue(forKey: peer) - peers.removeAll(where: { $0.id == peer }) + peers.removeAll(where: { $0 == peer }) } private func add(central: CBCentral) { @@ -71,11 +71,11 @@ public class CoreBluetoothTransport: NSObject { centrals[id] = central - if peers.filter({ $0.id == id }).count != 0 { + if peers.filter({ $0 == id }).count != 0 { return } - peers.append(Peer(id: id, services: [UBID]())) + peers.append(id) } } @@ -147,7 +147,7 @@ extension CoreBluetoothTransport: CBPeripheralManagerDelegate { // @todo check that this is the characteristic let id = Addr(central.identifier.bytes) centrals.removeValue(forKey: id) - peers.removeAll(where: { $0.id == id }) + peers.removeAll(where: { $0 == id }) } } @@ -205,11 +205,11 @@ extension CoreBluetoothTransport: CBPeripheralDelegate { peripherals[id]?.peripheral.setNotifyValue(true, for: char) // @todo we may need to do some handshake to obtain services from a peer. - if peers.filter({ $0.id == id }).count != 0 { + if peers.filter({ $0 == id }).count != 0 { return } - peers.append(Peer(id: id, services: [UBID]())) + peers.append(id) } } diff --git a/Sources/UB/Transports/Transport.swift b/Sources/UB/Transports/Transport.swift index 6950007..753ceb0 100644 --- a/Sources/UB/Transports/Transport.swift +++ b/Sources/UB/Transports/Transport.swift @@ -6,7 +6,7 @@ public protocol Transport { var delegate: TransportDelegate? { get set } /// The peers a specific transport can send messages to. - var peers: [Peer] { get } + var peers: [Addr] { get } /// Send implements a function to send messages between nodes using the transport. /// diff --git a/Sources/UB/Transports/TransportDelegate.swift b/Sources/UB/Transports/TransportDelegate.swift index 7880d63..b6d5d76 100644 --- a/Sources/UB/Transports/TransportDelegate.swift +++ b/Sources/UB/Transports/TransportDelegate.swift @@ -9,4 +9,11 @@ public protocol TransportDelegate: AnyObject { /// - data: The received data. /// - from: The peer from which the data was received. func transport(_ transport: Transport, didReceiveData data: Data, from: Addr) + + /// This method is called when a peer disconnected. + /// + /// - Parameters: + /// - transport: The transport with which a peer disconnected. + /// - peer: The peer which disconnected. + func transport(_ transport: Transport, peerDidDisconnect peer: Addr) } diff --git a/Sources/UB/Types.swift b/Sources/UB/Types.swift index e6e6d7b..1d860fd 100644 --- a/Sources/UB/Types.swift +++ b/Sources/UB/Types.swift @@ -5,3 +5,40 @@ public typealias Addr = [UInt8] // @todo use yeeth multiaddr /// Ultralight Beam specific IDs represented as byte arrays. public typealias UBID = [UInt8] // @todo might be data? + +extension Addr { + func distance(to: Addr) -> Int { + let value = self ^ to + return Int(value.reversed().reduce(0) { $0 << 8 + UInt64($1) }) + } + + static func ^ (left: Addr, right: UBID) -> [UInt8] { + var temp = left + + for i in 0 ..< left.count { + temp[i] ^= right[i % right.count] + } + + return temp + } + + static func < (left: Addr, right: Addr) -> Bool { + for i in 0 ... left.count { + if left[i] > right[i] { + return false + } + } + + return true + } + + static func > (left: Addr, right: Addr) -> Bool { + for i in 0 ... left.count { + if left[i] < right[i] { + return false + } + } + + return true + } +} diff --git a/Tests/UBTests/NodeTests.swift b/Tests/UBTests/NodeTests.swift index 9908ac9..6d3e883 100644 --- a/Tests/UBTests/NodeTests.swift +++ b/Tests/UBTests/NodeTests.swift @@ -27,143 +27,73 @@ final class NodeTests: XCTestCase { XCTAssert(node.transports.values.isEmpty) } - func testSendToSinglePeer() { - let transport = Transport() + func testUnsubscribeWorks() { let node = UB.Node() - node.add(transport: transport) - - let id = Addr(repeating: 1, count: 3) - let peer = Peer(id: id, services: []) - transport.peers.append(peer) - - let message = Message( - service: UBID(repeating: 1, count: 1), - recipient: id, - from: Addr(repeating: 2, count: 3), - origin: Addr(repeating: 2, count: 3), - message: Data(repeating: 0, count: 3) - ) - - node.send(message) - - let sent = transport.sent.first! - - guard let encoded = try? message.toProto().serializedData() else { - XCTFail("failed to encode message") - return - } + let topic = UBID(repeating: 1, count: 10) + node.subscribe(topic) - if sent.0 != encoded { - XCTFail("sent message did not match") - } + XCTAssertTrue(node.topics.contains(topic)) - if sent.1 != id { - XCTFail("send target did not match") - } + node.unsubscribe(topic) + XCTAssertFalse(node.topics.contains(topic)) } - func testDoesNotSendWhenNoPeerOrServiceInMessage() { - let transport = Transport() + func testChildIsAddedWhenSubscribing() { let node = UB.Node() + let transport = Transport() - node.add(transport: transport) - - let peer = Peer(id: Addr(repeating: 1, count: 3), services: []) - transport.peers.append(peer) - - let message = Message( - service: UBID(repeating: 0, count: 0), - recipient: Addr(repeating: 1, count: 0), - from: Addr(repeating: 2, count: 3), - origin: Addr(repeating: 2, count: 3), - message: Data(repeating: 0, count: 3) - ) + let addr = Addr(repeating: 2, count: 3) + let topic = UBID(repeating: 3, count: 3) - node.send(message) + let packet = Packet.new(topic: Data(topic), type: .subscribe, body: Data(count: 0)) - XCTAssertEqual(0, transport.sent.count) + let data = try! packet.serializedData() + node.transport(transport, didReceiveData: data, from: addr) + XCTAssertTrue(node.children[topic]!.contains(addr)) } - func testSendsToAllPeersExceptOriginWhenExactMatchNotFound() { - let transport = Transport() + func testChildIsRemovedOnUnsubscribe() { let node = UB.Node() + let transport = Transport() - node.add(transport: transport) + let addr = Addr(repeating: 2, count: 3) + let topic = UBID(repeating: 3, count: 3) - transport.peers.append(Peer(id: Addr(repeating: 2, count: 3), services: [])) - transport.peers.append(Peer(id: Addr(repeating: 3, count: 3), services: [])) - transport.peers.append(Peer(id: Addr(repeating: 4, count: 3), services: [])) - transport.peers.append(Peer(id: Addr(repeating: 5, count: 3), services: [])) + let subscribe = Packet.new(topic: Data(topic), type: .subscribe, body: Data(count: 0)) - let message = Message( - service: UBID(repeating: 1, count: 1), - recipient: Addr(repeating: 1, count: 3), - from: Addr(repeating: 4, count: 3), - origin: Addr(repeating: 3, count: 3), - message: Data(repeating: 0, count: 3) - ) + let subscription = try! subscribe.serializedData() + node.transport(transport, didReceiveData: subscription, from: addr) + XCTAssertTrue(node.children[topic]!.contains(addr)) - node.send(message) + let unsubscribe = Packet.new(topic: Data(topic), type: .unsubscribe, body: Data(count: 0)) - XCTAssertEqual(2, transport.sent.count) + let data = try! unsubscribe.serializedData() + node.transport(transport, didReceiveData: data, from: addr) + XCTAssertFalse(node.children[topic]!.contains(addr)) } - func testSendsToAllPeersWithSameServiceId() { - let transport = Transport() + func testMessageIsSentToChildren() { let node = UB.Node() - + let transport = Transport() node.add(transport: transport) - let id = UBID(repeating: 3, count: 2) + let addr = Addr(repeating: 2, count: 3) + let topic = UBID(repeating: 3, count: 3) - transport.peers.append(Peer(id: Addr(repeating: 2, count: 3), services: [id])) - transport.peers.append(Peer(id: Addr(repeating: 3, count: 3), services: [id])) - transport.peers.append(Peer(id: Addr(repeating: 4, count: 3), services: [id])) - transport.peers.append(Peer(id: Addr(repeating: 5, count: 3), services: [])) + transport.peers.append(addr) - let message = Message( - service: id, - recipient: Addr(repeating: 1, count: 3), - from: Addr(repeating: 4, count: 3), - origin: Addr(repeating: 5, count: 3), - message: Data(repeating: 0, count: 3) - ) + let subscribe = Packet.new(topic: Data(topic), type: .subscribe, body: Data(count: 0)) - node.send(message) + let subscription = try! subscribe.serializedData() + node.transport(transport, didReceiveData: subscription, from: addr) - XCTAssertEqual(2, transport.sent.count) - XCTAssertEqual(transport.sent[0].1, transport.peers[0].id) - XCTAssertEqual(transport.sent[1].1, transport.peers[1].id) - } + node.send(topic: topic, data: Data(repeating: 3, count: 1)) - func testSendsToAllPeersWhenServiceIsOnlyProvidedByOrigin() { - let transport = Transport() - let node = UB.Node() - - node.add(transport: transport) - - let id = UBID(repeating: 3, count: 2) - let origin = Addr(repeating: 2, count: 3) - - transport.peers.append(Peer(id: origin, services: [id])) - transport.peers.append(Peer(id: Addr(repeating: 3, count: 3), services: [])) - transport.peers.append(Peer(id: Addr(repeating: 4, count: 3), services: [])) - transport.peers.append(Peer(id: Addr(repeating: 5, count: 3), services: [])) - - let message = Message( - service: id, - recipient: Addr(repeating: 1, count: 3), - from: origin, - origin: origin, - message: Data(repeating: 0, count: 3) - ) - - node.send(message) + guard let sent = transport.sent.first else { + return XCTFail("no messages sent") + } - XCTAssertEqual(3, transport.sent.count) - XCTAssertEqual(transport.sent[0].1, transport.peers[1].id) - XCTAssertEqual(transport.sent[1].1, transport.peers[2].id) - XCTAssertEqual(transport.sent[2].1, transport.peers[3].id) + XCTAssert(sent.1 == addr) } } diff --git a/Tests/UBTests/stubs/Transport.swift b/Tests/UBTests/stubs/Transport.swift index cf569c1..9c7294f 100644 --- a/Tests/UBTests/stubs/Transport.swift +++ b/Tests/UBTests/stubs/Transport.swift @@ -6,7 +6,7 @@ class Transport: UB.Transport { private(set) var sent: [(Data, Addr)] = [] - var peers: [Peer] = [] + var peers: [Addr] = [] func send(message: Data, to: Addr) { sent.append((message, to)) diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 8ad5468..0000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -swift.ultralightbeam.io diff --git a/docs/Classes.html b/docs/Classes.html index df45d5f..f5e3ce1 100644 --- a/docs/Classes.html +++ b/docs/Classes.html @@ -14,7 +14,7 @@
-

UB 0.2.0 Docs (100% documented)

+

UB 0.2.0 Docs (93% documented)

View on GitHub

@@ -37,9 +37,6 @@ - - - - - - - - - - - - - - - - - - - - - - -