UNPKG

mudb

Version:

Real-time database for multiplayer games

296 lines (245 loc) 8.76 kB
import { allocBuffer, freeBuffer, MuBuffer } from '../../stream'; import { MuSocket, MuSocketSpec, MuSocketServer, MuSocketServerSpec, MuSessionId, MuData, MuSocketState, MuSocketServerState, MuMessageHandler, MuCloseHandler, MuConnectionHandler, } from '../socket'; import { MuScheduler } from '../../scheduler/scheduler'; import { MuSystemScheduler } from '../../scheduler/system'; import { MuLogger } from '../../logger'; function noop () {} class BufferWrapper { private _buffer:MuBuffer; public bytes:Uint8Array; constructor (data:Uint8Array) { this._buffer = allocBuffer(data.length); // make a copy of `data` this.bytes = this._buffer.uint8.subarray(0, data.length); this.bytes.set(data); } public free () { freeBuffer(this._buffer); } } type PendingMessage = string | BufferWrapper; export class MuLocalSocket implements MuSocket { public sessionId:MuSessionId; private _server:MuLocalSocketServer; // corresponding socket on the other end of the connection // should only be used inside the module public _duplex:MuLocalSocket = <any>null; private _onMessage:MuMessageHandler = noop; private _onClose:MuCloseHandler = noop; private _isClientSocket:boolean; private _state = MuSocketState.INIT; public state () { return this._state; } public scheduler:MuScheduler; constructor ( sessionId:string, server:MuLocalSocketServer, isClientSocket:boolean, scheduler:MuScheduler, ) { this.sessionId = sessionId; this._server = server; this._isClientSocket = isClientSocket; this.scheduler = scheduler; } public open (spec:MuSocketSpec) { this.scheduler.setTimeout( () => { if (this._state === MuSocketState.OPEN) { this._onClose('socket already open'); return; } if (this._state === MuSocketState.CLOSED) { this._onClose('cannot reopen closed socket'); return; } this._state = MuSocketState.OPEN; this._onMessage = spec.message; this._onClose = spec.close; if (this._isClientSocket) { this._server._handleConnection(this._duplex); } spec.ready(); // drain messages *after* ready handler this._drain(); while (this._pendingUnreliableMessages.length) { this._drainUnreliable(); } }, 0); } private _pendingUnreliableMessages:PendingMessage[] = []; private _drainUnreliable = () => { if (this._state !== MuSocketState.OPEN) { return; } const message = this._pendingUnreliableMessages.pop(); if (typeof message === 'string') { this._duplex._onMessage(message, true); } else if (message) { this._duplex._onMessage(message.bytes, true); message.free(); } } private _pendingMessages:PendingMessage[] = []; private _drainTimeout; private _drain = () => { // assuming timeout IDs always positive // indicate the draining task has been carried out this._drainTimeout = 0; if (this._state !== MuSocketState.OPEN) { return; } if (this._duplex._onMessage === noop) { this.scheduler.setTimeout(() => this._drain(), 0); return; } for (let i = 0; i < this._pendingMessages.length; ++i) { const message = this._pendingMessages[i]; if (typeof message === 'string') { this._duplex._onMessage(message, false); } else { this._duplex._onMessage(message.bytes, false); message.free(); } } this._pendingMessages.length = 0; } // draining reliable messages is scheduled only when no draining tasks are waiting, // to ensure messages are handled in correct order // while scheduling of draining unreliable message happen whenever one is "sent" // and they are "drained" one at a time, no handling order guaranteed public send (data_:MuData, unreliable?:boolean) { if (this._state === MuSocketState.CLOSED) { return; } const data = typeof data_ === 'string' ? data_ : new BufferWrapper(data_); if (unreliable) { this._pendingUnreliableMessages.push(data); this.scheduler.setTimeout(this._drainUnreliable, 0); } else { this._pendingMessages.push(data); // if no awaiting draining task if (!this._drainTimeout) { this._drainTimeout = this.scheduler.setTimeout(this._drain, 0); } } } public close () { if (this._state === MuSocketState.CLOSED) { return; } this._state = MuSocketState.CLOSED; this._server._removeSocket(this); this._onClose(); this._duplex.close(); } public reliableBufferedAmount() { return 0; } public unreliableBufferedAmount() { return 0; } } function removeIfExists (array, element) { const idx = array.indexOf(element); if (idx >= 0) { array[idx] = array[array.length - 1]; array.pop(); } } export class MuLocalSocketServer implements MuSocketServer { public clients:MuSocket[] = []; public _pendingSockets:MuSocket[] = []; private _state = MuSocketServerState.INIT; public state () { return this._state; } private _onConnection:MuConnectionHandler = noop; private _onClose:MuCloseHandler = noop; constructor (public scheduler:MuScheduler) {} // should only be used inside the module public _handleConnection (socket) { switch (this._state) { case MuSocketServerState.RUNNING: this.clients.push(socket); this._onConnection(socket); break; case MuSocketServerState.SHUTDOWN: socket.close(); break; default: this._pendingSockets.push(socket); } } // should only be used inside the module public _removeSocket (socket) { removeIfExists(this.clients, socket); removeIfExists(this._pendingSockets, socket); } public start (spec:MuSocketServerSpec) { this.scheduler.setTimeout( () => { if (this._state === MuSocketServerState.RUNNING) { this._onClose('local socket server already running'); return; } if (this._state === MuSocketServerState.SHUTDOWN) { this._onClose('local socket server already shut down, cannot restart'); return; } this._state = MuSocketServerState.RUNNING; this._onConnection = spec.connection; this._onClose = spec.close; // _pendingSockets -> clients while (this._pendingSockets.length > 0) { this._handleConnection(this._pendingSockets.pop()); } spec.ready(); }, 0); } public close () { if (this._state === MuSocketServerState.SHUTDOWN) { return; } if (this._state === MuSocketServerState.INIT) { this._state = MuSocketServerState.SHUTDOWN; return; } this._state = MuSocketServerState.SHUTDOWN; for (let i = this.clients.length - 1; i >= 0; --i) { this.clients[i].close(); } this._onClose(); } } export function createLocalSocketServer (spec?:{ scheduler?:MuScheduler, logger?:MuLogger, }) : MuLocalSocketServer { return new MuLocalSocketServer(spec && spec.scheduler || MuSystemScheduler); } export function createLocalSocket (spec:{ sessionId:MuSessionId; server:MuLocalSocketServer; scheduler?:MuScheduler; logger?:MuLogger; }) : MuLocalSocket { const scheduler = spec.scheduler || MuSystemScheduler; // manually spawn and relate sockets on both sides const clientSocket = new MuLocalSocket(spec.sessionId, spec.server, true, scheduler); const serverSocket = new MuLocalSocket(spec.sessionId, spec.server, false, scheduler); clientSocket._duplex = serverSocket; serverSocket._duplex = clientSocket; return clientSocket; }