mudb
Version:
Real-time database for multiplayer games
144 lines (130 loc) • 5.27 kB
text/typescript
import { MuRDA, MuRDATypes } from '../rda/rda';
import { MuServer, MuServerProtocol } from '../server';
import { rdaProtocol, RDAProtocol } from './schema';
import { MuSessionId } from '../socket/socket';
import { MuScheduler } from '../scheduler/scheduler';
import { MuSystemScheduler } from '../scheduler/system';
import { MuLogger, MuDefaultLogger } from '../logger';
export class MuReplicaServer<RDA extends MuRDA<any, any, any, any>> {
public protocol:MuServerProtocol<RDAProtocol<RDA>>;
public rda:RDA;
public store:MuRDATypes<RDA>['store'];
public scheduler:MuScheduler;
private _logger:MuLogger;
constructor (spec:{
server:MuServer,
rda:RDA,
savedStore?:MuRDATypes<RDA>['serializedStore'],
initialState?:MuRDATypes<RDA>['state'],
scheduler?:MuScheduler,
logger?:MuLogger,
}) {
this._logger = spec.logger || MuDefaultLogger;
this.rda = spec.rda;
if ('savedStore' in spec) {
this.store = <MuRDATypes<RDA>['store']>this.rda.parse(spec.savedStore);
} else {
this.store = <MuRDATypes<RDA>['store']>this.rda.createStore(
'initialState' in spec
? spec.initialState
: this.rda.stateSchema.identity);
}
this.protocol = spec.server.protocol(rdaProtocol(spec.rda));
this.scheduler = spec.scheduler || MuSystemScheduler;
}
private _pendingChangeCallback:((state:MuRDATypes<RDA>['state']) => void)[] = [];
private _onChange = (state:MuRDATypes<RDA>['state']) => {};
private _changeTimeout:any = null;
private _handleChange = () => {
this._changeTimeout = null;
const state = this.state();
this._onChange(state);
for (let i = 0; i < this._pendingChangeCallback.length; ++i) {
this._pendingChangeCallback[i].call(null, state);
}
this._pendingChangeCallback.length = 0;
this.rda.stateSchema.free(state);
}
private _notifyChange () {
if (this._changeTimeout) {
return;
}
this._changeTimeout = this.scheduler.setTimeout(this._handleChange, 0);
}
public configure(spec:{
connect?:(sessionId:MuSessionId) => void;
disconnect?:(sessionId:MuSessionId) => void;
change?:(state:MuRDATypes<RDA>['state']) => void;
checkApply?:(action:MuRDATypes<RDA>['action'], sessionId:MuSessionId) => boolean;
checkUndo?:(action:MuRDATypes<RDA>['action'], sessionId:MuSessionId) => boolean;
}) {
if (spec.change) {
this._onChange = spec.change;
}
this.protocol.configure({
connect: (client) => {
const state = this.save();
client.message.init(state);
this.rda.storeSchema.free(state);
if (spec.connect) {
spec.connect(client.sessionId);
}
},
disconnect: (client) => {
if (spec.disconnect) {
spec.disconnect(client.sessionId);
}
},
message: {
apply: (client, action) => {
if (spec.checkApply && !spec.checkApply(action, client.sessionId)) {
this._logger.log(`invalid action ${JSON.stringify} from ${client.sessionId}`);
return;
}
this.dispatch(action);
},
},
});
}
// polls the current state
public state(out?:MuRDATypes<RDA>['state']) {
return this.store.state(this.rda, out || this.rda.stateSchema.alloc());
}
// dispatch an action
public dispatch (action:MuRDATypes<RDA>['action'], cb?:(state:MuRDATypes<RDA>['state']|null) => void) {
if (this.store.apply(this.rda, action)) {
this.protocol.broadcast.apply(action);
if (cb) {
this._pendingChangeCallback.push(cb);
}
this._notifyChange();
} else if (cb) {
this.scheduler.setTimeout(() => cb(null), 0);
}
}
public action () : RDA['actionMeta'] extends { type:'store' } ? ReturnType<RDA['action']> : RDA['action'] {
if (this.rda.actionMeta.type === 'store') {
return this.rda.action(this.store);
}
return this.rda.action;
}
// squash all history to current state. erase history and ability to undo previous actions
public reset (state?:MuRDATypes<RDA>['state']) {
const head = state || this.rda.stateSchema.identity;
this.store.free(this.rda);
this.store = <MuRDATypes<RDA>['store']>this.rda.createStore(head);
this.protocol.broadcast.squash(head);
this._notifyChange();
}
// save store
public save () : MuRDATypes<RDA>['serializedStore'] {
return this.store.serialize(this.rda, this.rda.storeSchema.alloc());
}
// load store
public load (saved:MuRDATypes<RDA>['serializedStore']) {
this.protocol.broadcast.init(saved);
this.store.free(this.rda);
this.store = <MuRDATypes<RDA>['store']>this.rda.parse(saved);
this._notifyChange();
}
}