492 lines
13 KiB
TypeScript
492 lines
13 KiB
TypeScript
import { EventEmitter } from 'events'
|
||
|
||
import { Packet } from './packet'
|
||
import { PacketTypes, SubPacketTypes } from './types'
|
||
import { Client } from './client'
|
||
import { Namespace } from './namespace'
|
||
import * as querystring from 'querystring'
|
||
import { ServerEvent } from './constants'
|
||
import { Adapter, BroadcastFlags, Room, SocketId } from './adapter'
|
||
import { Server } from 'index'
|
||
|
||
export const RESERVED_EVENTS = new Set([
|
||
"connect",
|
||
"connect_error",
|
||
"disconnect",
|
||
"disconnecting",
|
||
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
|
||
"newListener",
|
||
"removeListener"
|
||
])
|
||
|
||
/**
|
||
* The handshake details
|
||
*/
|
||
export interface Handshake {
|
||
/**
|
||
* The headers sent as part of the handshake
|
||
*/
|
||
headers: object
|
||
|
||
/**
|
||
* The date of creation (as string)
|
||
*/
|
||
time: string
|
||
|
||
/**
|
||
* The ip of the client
|
||
*/
|
||
address: string
|
||
|
||
/**
|
||
* Whether the connection is cross-domain
|
||
*/
|
||
xdomain: boolean
|
||
|
||
/**
|
||
* Whether the connection is secure
|
||
*/
|
||
secure: boolean
|
||
|
||
/**
|
||
* The date of creation (as unix timestamp)
|
||
*/
|
||
issued: number
|
||
|
||
/**
|
||
* The request URL string
|
||
*/
|
||
url: string
|
||
|
||
/**
|
||
* The query object
|
||
*/
|
||
query: any
|
||
|
||
/**
|
||
* The auth object
|
||
*/
|
||
auth: any
|
||
}
|
||
export class Socket extends EventEmitter {
|
||
nsp: Namespace
|
||
|
||
public readonly id: SocketId
|
||
public readonly handshake: Handshake
|
||
|
||
public connected: boolean
|
||
public disconnected: boolean
|
||
|
||
private readonly server: Server
|
||
private readonly adapter: Adapter
|
||
|
||
client: Client
|
||
private acks: Map<number, () => void>
|
||
|
||
fns: any[]
|
||
private flags: BroadcastFlags = {};
|
||
private _rooms: Set<Room> = new Set();
|
||
private _anyListeners: Array<(...args: any[]) => void>
|
||
|
||
constructor(nsp: Namespace, client: Client, auth = {}) {
|
||
super()
|
||
this.nsp = nsp
|
||
this.server = nsp.server
|
||
this.adapter = this.nsp.adapter
|
||
this.id = nsp.name !== '/' ? nsp.name + '#' + client.id : client.id
|
||
this.client = client
|
||
this.acks = new Map()
|
||
this.connected = true
|
||
this.disconnected = false
|
||
this.handshake = this.buildHandshake(auth)
|
||
|
||
this.fns = []
|
||
this.flags = {}
|
||
this._rooms = new Set()
|
||
}
|
||
emit(event: string, ...args: any[]): boolean {
|
||
let packet: Packet = {
|
||
type: PacketTypes.MESSAGE,
|
||
sub_type: (this.flags.binary !== undefined ? this.flags.binary : this.hasBin(args)) ? SubPacketTypes.BINARY_EVENT : SubPacketTypes.EVENT,
|
||
name: event,
|
||
data: args
|
||
}
|
||
|
||
// access last argument to see if it's an ACK callback
|
||
if (typeof args[args.length - 1] === "function") {
|
||
if (this._rooms.size || this.flags.broadcast) {
|
||
throw new Error("Callbacks are not supported when broadcasting")
|
||
}
|
||
|
||
// console.debug("emitting packet with ack id %d", this.nsp._ids)
|
||
this.acks.set(this.nsp._ids, args.pop())
|
||
packet.id = this.nsp._ids++
|
||
}
|
||
|
||
const rooms = new Set(this._rooms)
|
||
const flags = Object.assign({}, this.flags)
|
||
|
||
// reset flags
|
||
this._rooms.clear()
|
||
this.flags = {}
|
||
|
||
if (rooms.size || flags.broadcast) {
|
||
this.adapter.broadcast(packet, {
|
||
except: new Set([this.id]),
|
||
rooms: rooms,
|
||
flags: flags
|
||
})
|
||
} else {
|
||
// dispatch packet
|
||
this.packet(packet, flags)
|
||
}
|
||
return true
|
||
}
|
||
to(name: Room): Socket {
|
||
this._rooms.add(name)
|
||
return this
|
||
}
|
||
in(room: string): Socket {
|
||
return this.to(room)
|
||
}
|
||
use(fn: (packet: Packet, next: (err?: any) => void) => void): Socket {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
send(...args: any[]): Socket {
|
||
this.emit("message", ...args)
|
||
return this
|
||
}
|
||
write(...args: any[]): Socket {
|
||
return this.send(...args)
|
||
}
|
||
public join(rooms: Room | Array<Room>): Promise<void> | void {
|
||
console.debug(`join room ${rooms}`)
|
||
|
||
return this.adapter.addAll(
|
||
this.id,
|
||
new Set(Array.isArray(rooms) ? rooms : [rooms])
|
||
)
|
||
}
|
||
/**
|
||
* Leaves a room.
|
||
*
|
||
* @param {String} room
|
||
* @return a Promise or nothing, depending on the adapter
|
||
* @public
|
||
*/
|
||
public leave(room: string): Promise<void> | void {
|
||
console.debug(`leave room ${room}`)
|
||
|
||
return this.adapter.del(this.id, room)
|
||
}
|
||
|
||
/**
|
||
* Leave all rooms.
|
||
*
|
||
* @private
|
||
*/
|
||
private leaveAll(): void {
|
||
this.adapter.delAll(this.id)
|
||
}
|
||
|
||
/**
|
||
* Called by `Namespace` upon successful
|
||
* middleware execution (ie: authorization).
|
||
* Socket is added to namespace array before
|
||
* call to join, so adapters can access it.
|
||
*
|
||
* @private
|
||
*/
|
||
_onconnect(): void {
|
||
console.debug(`socket ${this.id} connected - writing packet`)
|
||
this.join(this.id)
|
||
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.CONNECT, data: { sid: this.id } })
|
||
}
|
||
_onpacket(packet: Packet) {
|
||
switch (packet.sub_type) {
|
||
// 2
|
||
case SubPacketTypes.EVENT:
|
||
this.onevent(packet)
|
||
break
|
||
// 5
|
||
case SubPacketTypes.BINARY_EVENT:
|
||
this.onevent(packet)
|
||
break
|
||
// 3
|
||
case SubPacketTypes.ACK:
|
||
this.onack(packet)
|
||
break
|
||
// 6
|
||
case SubPacketTypes.BINARY_ACK:
|
||
this.onack(packet)
|
||
break
|
||
// 1
|
||
case SubPacketTypes.DISCONNECT:
|
||
this.ondisconnect()
|
||
break
|
||
// 4
|
||
case SubPacketTypes.ERROR:
|
||
this._onerror(new Error(packet.data))
|
||
}
|
||
}
|
||
onevent(packet: Packet) {
|
||
if (null != packet.id) {
|
||
console.trace(`attaching ack ${packet.id} callback to client ${this.id} event`)
|
||
this.dispatch(packet, this.ack(packet.id))
|
||
} else {
|
||
this.dispatch(packet)
|
||
}
|
||
}
|
||
ack(id: number) {
|
||
let sent = false
|
||
return (...args: any[]) => {
|
||
if (sent) return
|
||
this.packet({
|
||
id: id,
|
||
type: PacketTypes.MESSAGE,
|
||
sub_type: this.hasBin(args) ? SubPacketTypes.BINARY_ACK : SubPacketTypes.ACK,
|
||
data: args
|
||
})
|
||
sent = true
|
||
}
|
||
}
|
||
onack(packet: Packet) {
|
||
let ack = this.acks.get(packet.id)
|
||
if ('function' == typeof ack) {
|
||
console.trace(`calling ack ${packet.id} on socket ${this.id} with ${packet.data}`)
|
||
ack.apply(this, packet.data)
|
||
this.acks.delete(packet.id)
|
||
} else {
|
||
console.trace(`bad ack ${packet.id} on socket ${this.id}`)
|
||
}
|
||
}
|
||
/**
|
||
* Called upon client disconnect packet.
|
||
*
|
||
* @private
|
||
*/
|
||
private ondisconnect(): void {
|
||
console.debug(`socket ${this.id} got disconnect packet`)
|
||
this._onclose("client namespace disconnect")
|
||
}
|
||
|
||
/**
|
||
* Handles a client error.
|
||
*
|
||
* @private
|
||
*/
|
||
_onerror(err): void {
|
||
if (this.listeners("error").length) {
|
||
super.emit("error", err)
|
||
} else {
|
||
console.error(`Missing error handler on 'socket(${this.id})'.`)
|
||
console.error(err.stack)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Called upon closing. Called by `Client`.
|
||
*
|
||
* @param {String} reason
|
||
* @throw {Error} optional error object
|
||
*
|
||
* @private
|
||
*/
|
||
_onclose(reason: string) {
|
||
if (!this.connected) return this
|
||
console.debug(`closing socket ${this.id} - reason: ${reason} connected: ${this.connected}`)
|
||
super.emit(ServerEvent.disconnecting, reason)
|
||
this.leaveAll()
|
||
this.nsp._remove(this)
|
||
this.client._remove(this)
|
||
this.connected = false
|
||
this.disconnected = true
|
||
super.emit(ServerEvent.disconnect, reason)
|
||
}
|
||
|
||
/**
|
||
* Produces an `error` packet.
|
||
*
|
||
* @param {Object} err - error object
|
||
*
|
||
* @private
|
||
*/
|
||
_error(err) {
|
||
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.ERROR, data: err })
|
||
}
|
||
disconnect(close?: boolean): Socket {
|
||
if (!this.connected) return this
|
||
if (close) {
|
||
this.client._disconnect()
|
||
} else {
|
||
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.DISCONNECT })
|
||
this._onclose('server namespace disconnect')
|
||
}
|
||
return this
|
||
}
|
||
|
||
compress(compress: boolean): Socket {
|
||
throw new Error("Method not implemented.")
|
||
}
|
||
|
||
/**
|
||
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
|
||
* receive messages (because of network slowness or other issues, or because they’re connected through long polling
|
||
* and is in the middle of a request-response cycle).
|
||
*
|
||
* @return {Socket} self
|
||
* @public
|
||
*/
|
||
public get volatile(): Socket {
|
||
this.flags.volatile = true
|
||
return this
|
||
}
|
||
|
||
/**
|
||
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
|
||
* sender.
|
||
*
|
||
* @return {Socket} self
|
||
* @public
|
||
*/
|
||
public get broadcast(): Socket {
|
||
this.flags.broadcast = true
|
||
return this
|
||
}
|
||
|
||
/**
|
||
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
|
||
*
|
||
* @return {Socket} self
|
||
* @public
|
||
*/
|
||
public get local(): Socket {
|
||
this.flags.local = true
|
||
return this
|
||
}
|
||
|
||
/**
|
||
* A reference to the request that originated the underlying Engine.IO Socket.
|
||
*
|
||
* @public
|
||
*/
|
||
public get request(): any {
|
||
return this.client.request
|
||
}
|
||
|
||
/**
|
||
* A reference to the underlying Client transport connection (Engine.IO Socket object).
|
||
*
|
||
* @public
|
||
*/
|
||
public get conn() {
|
||
return this.client.conn
|
||
}
|
||
|
||
/**
|
||
* @public
|
||
*/
|
||
public get rooms(): Set<Room> {
|
||
return this.adapter.socketRooms(this.id) || new Set()
|
||
}
|
||
|
||
/**
|
||
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
|
||
* callback.
|
||
*
|
||
* @param listener
|
||
* @public
|
||
*/
|
||
public onAny(listener: (...args: any[]) => void): Socket {
|
||
this._anyListeners = this._anyListeners || []
|
||
this._anyListeners.push(listener)
|
||
return this
|
||
}
|
||
|
||
/**
|
||
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
|
||
* callback. The listener is added to the beginning of the listeners array.
|
||
*
|
||
* @param listener
|
||
* @public
|
||
*/
|
||
public prependAny(listener: (...args: any[]) => void): Socket {
|
||
this._anyListeners = this._anyListeners || []
|
||
this._anyListeners.unshift(listener)
|
||
return this
|
||
}
|
||
|
||
/**
|
||
* Removes the listener that will be fired when any event is emitted.
|
||
*
|
||
* @param listener
|
||
* @public
|
||
*/
|
||
public offAny(listener?: (...args: any[]) => void): Socket {
|
||
if (!this._anyListeners) {
|
||
return this
|
||
}
|
||
if (listener) {
|
||
const listeners = this._anyListeners
|
||
for (let i = 0; i < listeners.length; i++) {
|
||
if (listener === listeners[i]) {
|
||
listeners.splice(i, 1)
|
||
return this
|
||
}
|
||
}
|
||
} else {
|
||
this._anyListeners = []
|
||
}
|
||
return this
|
||
}
|
||
|
||
/**
|
||
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
|
||
* e.g. to remove listeners.
|
||
*
|
||
* @public
|
||
*/
|
||
public listenersAny() {
|
||
return this._anyListeners || []
|
||
}
|
||
|
||
// ==========================================
|
||
buildHandshake(auth): Handshake {
|
||
let requestUri = this.request.uri()
|
||
let headers = {}
|
||
let nativeHeaders = this.request.headers()
|
||
nativeHeaders.forEach(function (header) {
|
||
headers[header.getKey()] = header.getValue()
|
||
})
|
||
return {
|
||
headers: headers,
|
||
time: new Date() + '',
|
||
address: this.conn.remoteAddress + '',
|
||
xdomain: !!headers['origin'],
|
||
secure: false,
|
||
issued: +new Date(),
|
||
url: requestUri,
|
||
query: querystring.parse(requestUri.indexOf('?') != -1 ? requestUri.split('?')[1] : ''),
|
||
auth
|
||
}
|
||
}
|
||
packet(packet: Packet, opts: any = { preEncoded: false }) {
|
||
if (!opts.preEncoded) {
|
||
packet.nsp = this.nsp.name
|
||
opts.compress = false !== opts.compress
|
||
}
|
||
try {
|
||
this.client._packet(packet, opts)
|
||
} catch (error) {
|
||
this._onerror(error)
|
||
}
|
||
}
|
||
dispatch(packet: Packet, ack?: () => void) {
|
||
if (ack) { this.acks.set(packet.id, ack) }
|
||
super.emit(packet.name, ...packet.data, ack)
|
||
}
|
||
private hasBin(obj: any) {
|
||
return false
|
||
}
|
||
}
|