Skip to content

Instantly share code, notes, and snippets.

@diericx
Created March 17, 2018 17:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save diericx/adfc3ff53910804f79b815a96091ad14 to your computer and use it in GitHub Desktop.
Save diericx/adfc3ff53910804f79b815a96091ad14 to your computer and use it in GitHub Desktop.
import * as msgpack from "notepack.io";
import * as fossilDelta from "fossil-delta";
import * as shortid from "shortid";
import { Client } from "./index";
import { Protocol, send } from "./Protocol";
import { logError, spliceOne } from "./Utils";
import { debugPatch, debugPatchData } from "./Debug";
import {Room} from "./Room";
import * as jsonPatch from "fast-json-patch"; // this is only used for debugging patches
export abstract class RoomAll<T=any> extends Room {
public clients: Client[] = [];
public state: T;
// when a new user connects, it receives the '_previousState', which holds
// the last binary snapshot other users already have, therefore the patches
// that follow will be the same for all clients.
protected _previousState: any;
protected _previousStateEncoded: any;
constructor () {
super();
if (arguments.length > 0) {
console.warn("DEPRECATION WARNING: use 'onInit(options)' instead of 'constructor(options)' to initialize the room.");
}
this.setPatchRate(this.patchRate);
}
public getNumClients(clients) {
return clients.length;
}
public mapClients(clients, fun) {
for (let i = 0; i < clients.length; i++) {
fun(clients[i])
}
}
protected addClient(client: Client) {
this.clients.push( client );
}
protected removeClient(client: Client): boolean {
return spliceOne(this.clients, this.clients.indexOf(client))
}
public setState (newState) {
this.clock.start();
this._previousState = newState;
// ensure state is populated for `sendState()` method.
this._previousStateEncoded = msgpack.encode( this._previousState );
this.state = newState;
if ( this.timeline ) {
this.timeline.takeSnapshot( this.state );
}
}
protected sendState (client: Client): void {
send(client, [
Protocol.ROOM_STATE,
this.roomId,
this._previousStateEncoded,
this.clock.currentTime,
this.clock.elapsedTime,
]);
}
protected broadcast (data: any): boolean {
// no data given, try to broadcast patched state
if (!data) {
throw new Error("Room#broadcast: 'data' is required to broadcast.");
}
// encode all messages with msgpack
if (!(data instanceof Buffer)) {
data = msgpack.encode([Protocol.ROOM_DATA, this.roomId, data]);
}
let numClients = this.clients.length;
while (numClients--) {
this.clients[ numClients ].send(data, { binary: true }, logError.bind(this) );
}
return true;
}
protected broadcastPatch (): boolean {
if ( !this._previousState ) {
debugPatch('trying to broadcast null state. you should call #setState on constructor or during user connection.');
return false;
}
let currentState = this.state;
let currentStateEncoded = msgpack.encode( currentState );
// skip if state has not changed.
if ( currentStateEncoded.equals( this._previousStateEncoded ) ) {
return false;
}
let patches = fossilDelta.create( this._previousStateEncoded, currentStateEncoded );
// take a snapshot of the current state
if (this.timeline) {
this.timeline.takeSnapshot( this.state, this.clock.elapsedTime );
}
//
// debugging
//
if (debugPatch.enabled) {
debugPatch(`"%s" (roomId: "%s") is sending %d bytes:`, this.roomName, this.roomId, patches.length);
}
if (debugPatchData.enabled) {
debugPatchData(jsonPatch.compare(this._previousState, currentState));
}
this._previousState = currentState;
this._previousStateEncoded = currentStateEncoded;
// broadcast patches (diff state) to all clients,
// even if nothing has changed in order to calculate PING on client-side
return this.broadcast( msgpack.encode([ Protocol.ROOM_STATE_PATCH, this.roomId, patches ]) );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment