Skip to content

Commit

Permalink
Merge pull request #13 from j12n/phoenix-presence
Browse files Browse the repository at this point in the history
Implementation presence functionality
  • Loading branch information
mfeckie authored Apr 28, 2019
2 parents 8f1e025 + 4450320 commit 14c2730
Show file tree
Hide file tree
Showing 4 changed files with 566 additions and 2 deletions.
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [0.0.1] - TODO: Add release date.
## [0.2.0]
* Fix a bug with socket options params vsn value being overridden
* Remove dependency on Flutter
* Move to TravisCI
* Upgrade dependencies
* Add the `PhoenixPresence` service which is a port of the Phoenix JavaScript client

* TODO: Describe initial release.
## [0.1.3]

## [0.1.2]
1 change: 1 addition & 0 deletions lib/phoenix_wings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ library phoenix_wings;
export 'src/phoenix_channel.dart';
export 'src/phoenix_io_connection.dart';
export 'src/phoenix_message.dart';
export 'src/phoenix_presence.dart';
export 'src/phoenix_push.dart';
export 'src/phoenix_serializer.dart';
export 'src/phoenix_socket.dart';
Expand Down
171 changes: 171 additions & 0 deletions lib/src/phoenix_presence.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import 'dart:convert';
import 'package:phoenix_wings/src/phoenix_channel.dart';

class PhoenixPresence {
PhoenixChannel channel;
Map<String, dynamic> opts;
PresenceEvents events;
Map<String, Map<String, dynamic>> state;
List pendingDiffs = [];
String joinRef;
_PresenceCallers caller;

static void _noop(key, currentPresence, newPresence) {
}

PhoenixPresence(this.channel, {this.opts}) {
opts ??= {};
events = opts['events'] ?? PresenceEvents(PhoenixPresenceEvents.presenceState, PhoenixPresenceEvents.presenceDiff);
state = {};
joinRef = null;
caller = _PresenceCallers(onJoin: _noop, onLeave: _noop, onSync: () {});

this.channel.on(events.state, (newState, _ref, _joinRef) {
this.joinRef = this.channel.joinRef;
this.state = Map<String, Map<String, dynamic>>.from(syncState(this.state, newState, caller.onJoin, caller.onLeave));

this.pendingDiffs.forEach((diff) => this.state = Map<String, Map<String, dynamic>>.from(syncDiff(this.state, diff, caller.onJoin, caller.onLeave)));

this.pendingDiffs = [];
caller.onSync();
});

this.channel.on(events.diff, (diff, _ref, _joinRef) {
if (this.inPendingSyncState) {
this.pendingDiffs.add(diff);
} else {
this.state = Map<String, Map<String, dynamic>>.from(syncDiff(this.state, diff, caller.onJoin, caller.onLeave));
caller.onSync();
}
});
}

onJoin(Function(dynamic key, dynamic currentPresence, dynamic newPresence) callback) => this.caller.onJoin = callback;

onLeave(Function(dynamic key, dynamic currentPresence, dynamic newPresence) callback) => this.caller.onLeave = callback;

onSync(Function() callback) => this.caller.onSync = callback;

list({Function by = null}) => _list(this.state, by);

get inPendingSyncState => this.joinRef == null || (this.joinRef != this.channel.joinRef);

static syncState(currentState, newState, onJoinCallback, onLeaveCallback){
var state = clone(currentState);
var joins = {};
var leaves = {};

state.forEach((key, presence) {
if (newState[key] == null) {
leaves[key] = presence;
}
});

newState.forEach((key, newPresence) {
var currentPresence = state[key];
if (currentPresence == null) {
joins[key] = newPresence;
return;
}

var newRefs = List<Map<String, dynamic>>.from(newPresence['metas'])
.where((meta) => meta.containsKey('phx_ref'))
.map((meta) => meta['phx_ref']);

var curRefs = List<Map<String, dynamic>>.from(currentPresence['metas'])
.where((meta) => meta.containsKey('phx_ref'))
.map((meta) => meta['phx_ref']);

var joinedMetas = newPresence['metas'].where((meta) => !curRefs.contains(meta['phx_ref']));
var leftMetas = currentPresence['metas'].where((meta) => !newRefs.contains(meta['phx_ref']));

if (joinedMetas.length > 0) {
joins[key] = clone(newPresence);
joins[key]['metas'] = List<Map<String, dynamic>>.from(joinedMetas);
}

if (leftMetas.length > 0) {
leaves[key] = clone(currentPresence);
leaves[key]['metas'] = List<Map<String, dynamic>>.from(leftMetas);
}
});

return syncDiff(state, {'joins': joins, 'leaves': leaves}, onJoinCallback, onLeaveCallback);
}

static syncDiff(currentState, diffs, onJoinCallback, onLeaveCallback){
var joins = diffs['joins'] ?? {};
var leaves = diffs['leaves'] ?? {};
var state = clone(currentState);
if (onJoinCallback == null) { onJoinCallback = _noop; }
if (onLeaveCallback == null) { onLeaveCallback = _noop; }

joins.forEach((key, newPresence) {
var currentPresence = state[key];
state[key] = newPresence;
if (currentPresence != null) {
var joinedRefs = state[key]['metas']
.where((Map<String, dynamic> meta) => meta.containsKey('phx_ref'))
.map((Map<String, dynamic> meta) => meta['phx_ref']);

var curMetas = List<Map<String, dynamic>>.from(currentPresence['metas'].where((meta) => !joinedRefs.contains(meta['phx_ref'])));
state[key]['metas'].insertAll(0, curMetas);
}
onJoinCallback(key, currentPresence, newPresence);
});

leaves.forEach((key, leftPresence) {
var currentPresence = state[key];
if (currentPresence == null) {
return;
}

var refsToRemove = List.from(leftPresence['metas'])
.where((meta) => meta.containsKey('phx_ref'))
.map((meta) => meta['phx_ref']);

var currentPresenceMetas = List.from(currentPresence['metas']);
currentPresenceMetas.removeWhere((meta) => refsToRemove.contains(meta['phx_ref']));
currentPresence['metas'] = currentPresenceMetas;

onLeaveCallback(key, currentPresence, leftPresence);

if (currentPresence['metas'] == null || currentPresence['metas'].length == 0) {
state.remove(key);
}
});

return state;
}

static _list(presences, chooser){
if (chooser == null) {
chooser = (key, pres) => pres;
}

return List.from(presences.entries)
.map((entry) => chooser(entry.key, entry.value));
}

static clone(object){
return jsonDecode(jsonEncode(object));
}
}

class PhoenixPresenceEvents {
static const presenceState = "presence_state";
static const presenceDiff = "presence_diff";
}

class PresenceEvents {
String state;
String diff;
PresenceEvents(this.state, this.diff);
}

class _PresenceCallers {
Function(dynamic key, dynamic currentPresence, dynamic newPresence) onJoin;
Function(dynamic key, dynamic currentPresence, dynamic newPresence) onLeave;
Function() onSync;
_PresenceCallers({this.onJoin, this.onLeave, this.onSync});
}
Loading

0 comments on commit 14c2730

Please sign in to comment.