All Projects → kuon → java-phoenix-channel

kuon / java-phoenix-channel

Licence: Apache-2.0, MIT licenses found Licenses found Apache-2.0 LICENSE-APACHE MIT LICENSE-MIT
Phoenix Channel Java Client written in Kotlin

Programming Languages

kotlin
9241 projects
elixir
2628 projects

Projects that are alternatives of or similar to java-phoenix-channel

Realtime
Listen to your to PostgreSQL database in realtime via websockets. Built with Elixir.
Stars: ✭ 4,278 (+21290%)
Mutual labels:  phoenix-framework
zero-to-graphql-using-elixir
The purpose of this example is to provide details as to how one would go about using GraphQL with the Elixir Language.
Stars: ✭ 20 (+0%)
Mutual labels:  phoenix-framework
distillery packager
Elixir lib for creating Debian and RPM packages with Distillery
Stars: ✭ 25 (+25%)
Mutual labels:  phoenix-framework
phoenix-cms
Headless CMS fun with Phoenix LiveView and Airtable
Stars: ✭ 72 (+260%)
Mutual labels:  phoenix-framework
pokerex client
Elm client for PokerEx project
Stars: ✭ 39 (+95%)
Mutual labels:  phoenix-framework
live dj
💿 Join or create video playlists to share a real-time experience with others! 🎧
Stars: ✭ 19 (-5%)
Mutual labels:  phoenix-framework
Phoenix Toggl
Toggl tribute done with Elixir, Phoenix Framework, React and Redux.
Stars: ✭ 176 (+780%)
Mutual labels:  phoenix-framework
tutorials as code
so that stuff read/seen don't get muddled up with time
Stars: ✭ 42 (+110%)
Mutual labels:  phoenix-framework
rent-bot
My personal Facebook Messenger chat bot that helped find an apartment to rent.
Stars: ✭ 91 (+355%)
Mutual labels:  phoenix-framework
sublime-phoenix-beagle
Sublime Text plugin to make development with Phoenix Framework better!
Stars: ✭ 22 (+10%)
Mutual labels:  phoenix-framework
admissions
Admissions is the gateway to Elixir School's private Slack
Stars: ✭ 18 (-10%)
Mutual labels:  phoenix-framework
phxsockets.io
Phoenix sockets and channels GUI client
Stars: ✭ 16 (-20%)
Mutual labels:  phoenix-framework
gringotts payment
Demo Phoenix app showing gringotts payment library integrations.
Stars: ✭ 24 (+20%)
Mutual labels:  phoenix-framework
Phoenix Trello
Trello tribute done in Elixir, Phoenix Framework, React and Redux.
Stars: ✭ 2,492 (+12360%)
Mutual labels:  phoenix-framework
phoenix-liveview-15m.twitter
Based on the "real-time Twitter clone in 15 minutes with LiveView and Phoenix", from Chris McCord
Stars: ✭ 40 (+100%)
Mutual labels:  phoenix-framework
Phoenix And Elm
Example application using Elixir, Phoenix and Elm
Stars: ✭ 188 (+840%)
Mutual labels:  phoenix-framework
phoenix-jsroutes
Javascript routing for the phoenix framework
Stars: ✭ 25 (+25%)
Mutual labels:  phoenix-framework
phoenix live view tablefilter
Table Filter with Phoenix LiveView
Stars: ✭ 15 (-25%)
Mutual labels:  phoenix-framework
coophub
Cooperatives repos over the world! 🌈🌎
Stars: ✭ 53 (+165%)
Mutual labels:  phoenix-framework
phoenix-channel-client
A Phoenix Channels client library for Elixir.
Stars: ✭ 20 (+0%)
Mutual labels:  phoenix-framework

Build CI Version License Apache 2.0/MIT

Phoenix Channel Java Client written in Kotlin

A simple client for Phoenix channels.

Features:

  • Written in Kotlin.
  • Support Socket, Channels and Presence.
  • Uses Socket serialization protocol V2 which is more compact that V1.
  • Includes a phoenix mock server for testing.
  • Code is modeled closely to the  reference JavaScript implementation, this ensure an easy maintainability.
  • Based on nv-websocket-client.

API documentation

https://kuon.github.io/java-phoenix-channel/ch.kuon.phoenix/index.html

Getting started

When using gradle (Kotlin DSL):

Add the repository:

repositories {
    maven {
        setUrl("https://maven.goyman.com/")
    }
}

Add the following dependencies:

dependencies {
    // ...
    // Web Socket Client
    implementation("com.neovisionaries:nv-websocket-client:2.9")

    // JSON handling
    implementation("com.github.openjson:openjson:1.0.11")

    // Phoenix Channel Client
    implementation("ch.kuon.phoenix:channel:0.1.9")
    // ...
}

You should be able run this minimal example:

import ch.kuon.phoenix.Socket
import ch.kuon.phoenix.Channel
import ch.kuon.phoenix.Presence


fun doSomething() {
    val url = "ws://localhost:4444/socket"
    val sd = Socket(url)

    sd.connect()

    val chan = sd.channel("auth:login")

    chan
    .join()
    .receive("ok") { msg ->
        // channel is connected
    }
    .receive("error") { msg ->
        // channel did not connected
    }
    .receive("timeout") { msg ->
        // connection timeout
    }

    chan
    .push("hello")
    .receive("ok") { msg ->
        // sent hello and got msg back
    }

}

Important Notes

  • API should be thread safe, but they use a naive locking mechanism (over the socket object).
  • API can be used from main UI thread as minimal work is done on the calling thread.
  • Callbacks can be called on any thread, be sure to take this into account.
  • Callbacks mut be thread safe.
  • Be sure to disconnect the socket in your cleanup code, this is not done automatically.

Sockets

Connection

A single websocket connection is established to the server and channels are multiplexed over the single connection.

Connect to the server using the Socket class:

val opts = Socket.Options()
opts.timeout = 5_000 // socket timeout in milliseconds
opts.heartbeatIntervalMs = 10_000 // heartbeat intervall in milliseconds
opts.rejoinAfterMs = {tries -> tries * 500} // rejoin timer function
opts.reconnectAfterMs = {tries -> tries * 500} // reconnect timer function
opts.logger = {tag, msg -> com.android.Log.d(tag, msg)} // log message
opts.params = hashMap("user_token" to "supersecret") // params

// opts can be omitted for most uses
val socket = Socket("ws://myapp.com/socket", opts)
socket.connect()

Hooks

Lifecycle events of the multiplexed connection can be hooked into via socket.onError() and socket.onClose() events, ie:

socket.onError { System.out.println("There was an error with the connection!") }
socket.onClose { System.out.println("The connection closed!") }

Channels

Channels are isolated, concurrent processes on the server that subscribe to topics and broker events between the client and server.

To join a channel, you must provide the topic and channel params for authorization. Here's an example chat room example where "new_msg" events are listened for, messages are pushed to the server, and the channel is joined with ok/error/timeout matches:

val channel = socket.channel(
    "room:123",
    JSONObject(hashMap("token" to roomToken))
)
channel.on("new_msg") { msg ->
    System.out.println("Got a message: " + msg.response.toString())
}
someTextInput.onClick {
    channel
    .push("new_msg", JSONObject(hashMap("data" to "somedata")))
    .receive("ok") { _ ->
        System.out.println("Created msg")
    }
    .receive("error") { reason ->
        System.out.println("Got an error: " + reason)
    }
    .receive("timeout") {
        System.out.println("Timeout!")
    }
}
channel
.join()
.receive("ok") { msg ->
    System.out.println("Join success, got messages: " + msg.toString())
}
.receive("error") { reason ->
    System.out.println("Failed to join because: " + reason)
}
.receive("timeout") {
    System.out.println("Join timeout!")
}

Joining

When channels are created with socket.channel(topic, params), params is bound to the channel and sent on join().

join() can only be called once, but channel might rejoin on timeout or other error.

Successful joins receive an "ok" status, while unsuccessful joins receive "error".

Duplicate Join Subscriptions

While the client may join any number of topics on any number of channels, the client may only hold a single subscription for each unique topic at any given time. When attempting to create a duplicate subscription, the server will close the existing channel, log a warning, and spawn a new channel for the topic. The client will have their channel.onClose() callbacks fired for the existing channel, and the new channel join will have its receive hooks processed as normal.

Pushing Messages

From the previous example, we can see that pushing messages to the server can be done with channel.push(eventName, payload) and we can optionally receive responses from the push. Additionally, we can use receive("timeout", callback) to abort waiting for our other receive hooks and take action after some period of waiting. The default timeout is 10000ms.

Hooks

For each joined channel, you can bind to onError and onClose events to monitor the channel lifecycle, ie:

channel.onError { System.out.println("There was an error!") }
channel.onClose { System.out.println("The was closed gracefully!") }

onError hooks

onError hooks are invoked if the socket connection drops, or the channel crashes on the server. In either case, a channel rejoin is attempted automatically in an exponential backoff manner (the timer can be altered with socket option rejoinAfterMs).

onClose hooks

onClose hooks are invoked only in two cases:

  1. The channel explicitly closed on the server.
  2. The client explicitly closed, by calling channel.leave()

Presence

The Presence object provides features for syncing presence information from the server with the client and handling presences joining and leaving.

Syncing state from the server

To sync presence state from the server, first instantiate an object and pass your channel in to track lifecycle events:

val channel = socket.channel("some:topic")
val presence = Presence(channel)

Next, use the presence.onSync callback to react to state changes from the server. For example, to render the list of users every time the list changes, you could write:

presence.onSync {
  myRenderUsersFunction(presence.list())
}

Listing Presences

presence.list() is used to return a list of presence information based on the local state of metadata. By default, all presence metadata is returned, but a listBy function can be supplied to allow the client to select which metadata to use for a given presence.

For example, you may have a user online from different devices with a metadata status of "online", but they have set themselves to "away" on another device. In this case, the app may choose to use the "away" status for what appears on the UI. The example below defines a listBy function which prioritizes the first metadata which was registered for each user. This could be the first tab they opened, or the first device they came online from:

val listBy = { id, metas ->
    return metas.get(0)
}
val onlineUsers = presence.list(listBy)

Handling individual presence join and leave events

The presence.onJoin and presence.onLeave callbacks can be used to react to individual presences joining and leaving the app. For example:

val presence = Presence(channel)

// detect if user has joined for the 1st time or from another tab/device
presence.onJoin { id, current, newPres ->
  if(current != null) {
    log("user has entered for the first time", newPres)
  } else {
    log("user additional presence", newPres)
  }
}

// detect if user has left from all tabs/devices, or is still present
presence.onLeave { id, current, leftPres ->
  if(current.getMetas().length() === 0){
    log("user has left from all devices", leftPres)
  } else {
    log("user left from a device", leftPres)
  }
}

// receive presence data from server
presence.onSync {
  displayUsers(presence.list())
}

Promises

The library itself do not include promises support, but you can easily integrate a library like kovenant, like so:

fun connect(url: String): Promise<Socket, Exception> {
    val deferred = deferred<Socket,Exception>()

    val opt = Socket.Options()
    opt.logger = { tag, msg ->
        Log.d(tag, msg)
    }
    val socket = Socket(url, opt)

    val refs = mutableListOf<Int>()

    val openRef = socket.onOpen {
        deferred.resolve(socket)
        socket.off(refs)
    }
    val errRef = socket.onError { err ->
        deferred.reject(Exception(err))
        socket.off(refs)
    }
    refs.add(openRef)
    refs.add(errRef)

    socket.connect()

    return deferred.promise
}

fun join(
    socket: Socket,
    topic: String
): Promise<Pair<Channel, JSONObject>, Exception> {
    val deferred = deferred<Pair<Channel, JSONObject>,Exception>()
    val channel = socket.channel(topic)

    val refs = mutableListOf<Int>()

    val errRef = channel.onError { err ->
        channel.off(refs)
        deferred.reject(Exception(err))
    }

    refs.add(errRef)

    channel
    .join()
    .receive("ok") { msg ->
        channel.off(refs)
        deferred.resolve(Pair(channel, msg))
    }
    .receive("error") { msg ->
        channel.off(refs)
        deferred.reject(Exception(msg.toString()))
    }

    return deferred.promise
}

fun push(
    channel: Channel?,
    event: String,
    payload: JSONObject = JSONObject()
): Promise<Pair<Channel, JSONObject>, Exception> {

    if (channel == null) {
        return Promise.ofFail(Exception("Channel cannot be null"))
    }

    val deferred = deferred<Pair<Channel, JSONObject>,Exception>()

    val refs = mutableListOf<Int>()

    val errRef = channel.onError { err ->
        channel.off(refs)
        deferred.reject(Exception(err))
    }

    refs.add(errRef)

    channel
    .push(event, payload)
    .receive("ok") { msg ->
        channel.off(refs)
        deferred.resolve(Pair(channel, msg))
    }
    .receive("error") { msg ->
        channel.off(refs)
        deferred.reject(Exception(msg.toString()))
    }

    return deferred.promise
}

// Then it can be used like so

fun example() {
    connect(getString(R.string.endpoint_url)) bind { socket ->
        this.socket = socket
        join(socket, "some:channel")
    } bind { (channel, _) ->
        this.channel = channel
        push(channel, "somemessage")
    } bind { (channel, msg) ->
    } success {
        // ok
    } fail { err ->
        // not ok
    }
}

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Sponsoring

You can sponsor this project.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].