Swiftpack.co -  teufelaudio/CombineWamp as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
teufelaudio/CombineWamp
WAMP protocol (https://wamp-proto.org) implemented using iOS 13 WebSocket and Combine
.package(url: "https://github.com/teufelaudio/CombineWamp.git", from: "v0.1.2")

CombineWamp

Implementation of WAMP protocol (https://wamp-proto.org/) using native Swift 5.3 and Combine (iOS >= 13, macOS >= 10.15, watchOS >= 6, tvOS >= 13).

Connection infrastructure

The connection infrastructure is composed by 3 main elements:

  • Serialization
  • Transport
  • Realm
  • Session

Serialization

WampSerializing defines how the messages will be serialized or deserialized from String. By default, WAMP uses JSON, so this serialization mode can be easily created by calling:

let serialization = WampSerializing.json(
    decoder: JSONDecoder.init,
    encoder: JSONEncoder.init
)

Other serialization protocols can be easily implemented by you.

Transport

WampTransport defines how the messages will be sent and received over the network. By default, WAMP uses WebSockets, so this transport mode can be easily created by calling:

let transport = WampTransport.webSocket(
    wsURL: URL(string: "ws://localhost:8080/ws")!, 
    urlSession: URLSession.shared,
    serializationFormat: serialization.serializationFormat
)

Other transport methods can be easily implemented by you.

Realm

A WAMP system may have one or more registered realms. They can even have different authentication or authorization rules. Realms are usually created by Routers, so to establish a session all you have to do is to provide a valid Realm URI:

let realm = URI("de.teufel.my_app.public_realm")!

Please notice that CombineWamp works with strict URI rules (https://wamp-proto.org/_static/gen/wamp_latest.html#strict-uris).

Session

A session glues all these things together. You can think of it as a client connection. The same session can be reused by certain client to perform different actions, such as publishing, subscribing, calling RPC procedures or responding RPC procedures. A client, if it wants, may also create more than a session, although this is not usually required.

let session = WampSession(transport: transport, serialization: serialization, realm: realm, roles: .allClientRoles)

Routers also have open sessions, but this is not implemented yet on CombineWamp.

Client

A WAMP Client is the peer responsible for the actual messaging handling.

It may implement one or more of the following roles:

  • Publisher: in the PubSub communication, this client will be able to publish events related to certain topic;
  • Subscriber: in the PubSub communication, this client will be able to subscribe for events related to certain topic;
  • Caller: in the RPC communication, this client will be able to call remote procedures registered by other clients;
  • Callee: in the RPC communication, this client will be able to register procedures and respond when they are called by other clients.

After you created the session as demonstrated earlier, you can now connect to it. This will make your client say HELLO to the router and receive a WELCOME message, or an error:

session.connect()
    .sink(
        receiveCompletion: { [weak self] completion in
            self?.handleCompletion(completion)
        },
        receiveValue: { [weak self] welcome in
            self?.onJoin()
        }
    ).store(in: &cancellables)

From that point, we can access the client property from the session object. With that, we can, for example, leave the session by saying GOODBYE.

session
    .client
    .sayGoodbye()
    .sink(
        receiveCompletion: { [weak self] completion in
            self?.handleCompletion(completion)
        },
        receiveValue: { [weak self] goodbye in
            self?.onLeave()
        }
    ).store(in: &cancellables)

More interesting commands are available when you lift your Client to a specific role, such as Subscriber or Caller. For that, please notice that your session must have been open with those roles set, or all client roles enabled (roles: .allClientRoles).

As Publisher

Lifts a Client to a Publisher and allows publishing to topics in the WAMP Realm.

session
    .client
    .asPublisher?
    .publish(
        topic: URI("de.teufel.my_app.hello_topic")!, 
        positionalArguments: [
            .string("Answer to the Ultimate Question of Life, The Universe, and Everything!"), 
            .integer(42)
        ]
    )
    .sink(
        receiveCompletion: { [weak self] completion in
            self?.handleCompletion(completion)
        },
        receiveValue: { published in
            self?.onPublishedSuccessfully()
        }
    ).store(in: &cancellables)

Please notice that session.client.asPublisher returns an Optional WampPublisher. This will be nil in case you didn't set .publisher role on creating the WampSession.

Instead of calling publish, you may also consider calling publishWithoutAck. This one won't receive the acknowledgement from the router that the message was received by it, however not all routers will support this acknowledgements and in case you don't get it, you must fallback to the option without ack.

Also, instead of positionalArguments you may also consider namedArguments, which will expect a dictionary such as:

namedArguments: [
    "Answer to the Ultimate Question of Life, The Universe, and Everything!": .integer(42)
]

For the possible element types, please check Element Types section below.

As Subscriber

Lifts a Client to a Subscriber and allows subscribing to topics in the WAMP Realm.

session
    .client
    .asSubscriber?
    .subscribe(topic: URI("de.teufel.my_app.hello_topic")!, onUnsubscribe: { unsubscribing in
        switch unsubscribing {
        case .success: break // Successfully unsubscribed
        case let .failure(error): break // Handle unsubscribe error
        }
    })
    .sink(
        receiveCompletion: { [weak self] completion in
            self?.handleCompletion(completion)
        },
        receiveValue: { [weak self] event in
            self?.handleHelloTopicEvent(
                positionalArguments: event.positionalArguments,
                namedArguments: event.namedArguments
            )
        }
    ).store(in: &cancellables)

Please notice that session.client.asSubscriber returns an Optional WampSubscriber. This will be nil in case you didn't set .subscriber role on creating the WampSession.

When this Combine subscription is cancelled, if the session is still active the client will send an UNSUBSCRIBE message to the router informing that we're no longer interested in this topic. You can optionally check if this message was properly delivered, by providing the closure onUnsubscribe.

For the possible element types, please check Element Types section below.

As Caller

Lifts a Client to a Caller and allows calling remote procedures (RPC) in the WAMP Realm.

session
    .client
    .asCaller?
    .call(procedure: URI("de.teufel.my_app.sum")!, positionalArguments: [.integer(11), .integer(31)])
    .sink(
        receiveCompletion: { [weak self] completion in
            self?.handleCompletion(completion)
        },
        receiveValue: { [weak self] result in
            guard let sumResult = result.positionalArguments?[safe: 0]?.integer else { return }
            self?.handleSumResult(sumResult)
        }
    ).store(in: &cancellables)

Please notice that session.client.asCaller returns an Optional WampCaller. This will be nil in case you didn't set .caller role on creating the WampSession.

Instead of positionalArguments you may also consider namedArguments, which will expect a dictionary such as:

namedArguments: [
    "sum_left_side": .integer(11),
    "sum_right_side": .integer(31)
]

For the possible element types, please check Element Types section below.

As Callee

Lifts a Client to a Callee and allows registering remote procedures (RPC) in the WAMP Realm and be called by other clients.

session
    .client
    .asCallee?
    .register(procedure: URI("de.teufel.my_app.sum")!, onUnregister: { unregistering in
        switch unregistering {
        case .success: break // Successfully unregistered
        case let .failure(error): break // Handle unregister error
        }
    })
    .sink(
        receiveCompletion: { [weak self] completion in
            self?.handleCompletion(completion)
        },
        receiveValue: { (invocation, responder) in
            let first = invocation.positionalArguments?[safe: 0]?.integer ?? 0
            let second = invocation.positionalArguments?[safe: 1]?.integer ?? 0
            let response = first - second
            responder([.integer(response)])
                .run(
                    onSuccess: { _ in
                    },
                    onFailure: { 
                        // Handle response error
                    }
                )
                .store(in: &cancellables)
        }
    ).store(in: &cancellables)

Please notice that session.client.asCallee returns an Optional WampCallee. This will be nil in case you didn't set .callee role on creating the WampSession.

When this Combine subscription is cancelled, if the session is still active the client will send an UNREGISTER message to the router informing that we're no longer offering in this procedure. You can optionally check if this message was properly delivered, by providing the closure onUnregister.

For the possible element types, please check Element Types section below.

Element Types

The possible element types when sending arguments are:

public enum ElementType: Equatable {
    case integer(Int)
    case string(String)
    case bool(Bool)
    case double(Double)
    indirect case dict([String: ElementType])
    indirect case list([ElementType])
}

Please notice that .double is not part of standard WAMP protocol and may not be understood by other peers and certain languages. Be sure to validate it works in your environment before using it.

Optionally you can implement ElementTypeConvertible protocol in your structs to easily converted from and to WAMP Element Types. Otherwise you can easily write this manually.

Extracting values from this enum is easier thanks to calculated properties for each of the enum cases. For example:

let integer = element.integer ?? 0
let string = element.string ?? ""
let thirdInteger = element.list?[safe: 2]?.integer ?? 0
let userName = element.dict?["name"]?.string ?? ""
let userAddressStreet = element.dict?["address"]?.dict?["street"]?.string ?? ""

Router

A WAMP Router is the peer responsible for coordinating, routing and proxying all clients communication

Not implemented yet

GitHub

link
Stars: 1
Last commit: 4 weeks ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Release Notes

More SPM product options
4 weeks ago

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API