Skip to content

alvinlindstam/phoenix-presence-immutable

Repository files navigation

npm version Build Status

phoenix-presence-immutable

Helper library for managing Phoenix Presence state using Immutable.js.

Usage

npm install phoenix-presence-immutable

import ImmutablePresence from 'phoenix-presence-immutable'


// Assuming we're using a React component
this.setState({presenceState: ImmutablePresence.emptyState()})

const updatePresence = newData => {
  this.setState((oldState) => {
    const newPresenceState = ImmutablePresence.sync(oldState.presenceState, newData)
    return {presenceState: newPresenceState}
  })
}

// receive initial presence data from server, sent after join
myChannel.on("presence_state", updatePresence)

// receive "presence_diff" from server, containing join/leave events
myChannel.on("presence_diff", updatePresence)

Documentation

emptyState()

Returns an empty state object. Use this as your first state and before letting syncState and syncDiff use it for their updates.

syncState(oldState, newState, onChanged)

ImmutablePresence.syncState is used to sync the list of presences on the server with the client's state. oldState is the the current presence state, and newState should be the new state provided by the server (in normal JS format, not as Immutable.js data structures)

onChanged is an optional callback

syncDiff(oldState, diff, onChanged)

ImmutablePresence.syncDiff is used to sync a diff of presence join and leave events from the server, as they happen. oldState is the current presence state, and diff is the diff data provided by the server.

sync(oldState, newData, onChanged)

ImmutablePresence.sync can be called with either a new state or presence diff data. It will inspect newData and detect if it should call syncState or syncDiff.

onChanged is an optional callback

list(presenceState, listBy)

ImmutablePresence.list is used to return a sequence 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, they 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:

// Extracting user data. The chooser function will pick data from the presence metas. 
const listBy = (id, presence) => {
  const metas = presence.get('metas')
  return metas.first().set('id', id).set('count', metas.size)
}
// onlineUsers will be an Immutable.Seq
let onlineUsers = ImmutablePresence.list(presenceState, listBy)

Note that this will always return new Immutable objects, so any algorithms using object identity to identify changes will see each users data as new. A better option might be to aggregate user data on changes using the onChanged callback, which will only recalculate the data on presence changes.

onChanged(key, newPresence, oldPresence) callback

The onChanged callback can be used to react to changes in the client's local presences across disconnects and reconnects with the server.

newPresence and oldPresence is the old and new presence state for the given key. One of them may be undefined. The callback is called once for each changed presence object.

The function may return a modified version of newPresence, if it wishes to set additional data for the given key. The presence metas may not be changed.

Example onChanged usage: aggregated data

const onChanged = (key, newPresence) => {
  return newPresence.set(
    'isTyping', newPresence.get('metas').some((meta) => meta.get('isTyping'))
  ).set(
    'connectionCount', newPresence.get('metas').size
  ).set(
    'userId', key
  )
}   

Example onChanged usage: notifications

// detect if user has joined for the 1st time or from another tab/device
const onChanged = (id, newPresence, oldPresence) => {
  if(!oldPresence){
    console.log("user has entered for the first time", newPresence)
  }
  
  if(!newPresence){
    console.log("user has left from all devices", oldPresence)
  } 
}   

Sorting

The iteration order of an Immutable.Map is undefined, so iterating over the state will be different from when using the built in Phoenix js client.

To iterate over the presence state in a well defined order, use presenceState.sortBy which will return an Immutable.OrderedMap.

It is perfectly ok to save the ordered map and pass it as the existing state into sync, syncState or syncDiff. You should re-sort the state afterwards if you wish to preserve you sort order (Immutable uses insertion order for OrderedMap, so any new keys will be added to the end).

Data structure

The state maintained is a simple mapping from Phoenix's standard presence format and Immutable.js.

Top level map

The top level data structure is an Immutable.Map, where the keys are the key used for Phoenix.Presence.track on the server. The values are the presence objects.

To get the total number of presences, use presenceState.size.

Presence objects

For each key, there is a single presence object. It is an Immutable.Map.

It always has the key "metas" which is an Immutable.Collection.

It may also contain any additional metadata aggregated through the onChanged callback.

Metas

For each present key, there will be at least one but possible many meta objects. The meta objects each represent a single presence for the given key. A common pattern is using user ids as keys, where each user may be present on multiple devices and tabs with different metadata on each.

Each meta object is an Immutable.Map, and will at the very least have the key phx_ref which is used internally. It may also contain any other keys defined as metadata on the tracked presence.