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