@feathersjs/transport-commons
Version:
Shared functionality for websocket providers
188 lines (153 loc) • 5.54 kB
text/typescript
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { convert } from '@feathersjs/errors'
import { createDebug } from '@feathersjs/commons'
import { Id, NullableId, Params, ServiceInterface } from '@feathersjs/feathers'
const debug = createDebug('@feathersjs/transport-commons/client')
const namespacedEmitterMethods = [
'addListener',
'addEventListener',
'emit',
'listenerCount',
'listeners',
'on',
'once',
'prependListener',
'prependOnceListener',
'removeAllListeners',
'removeEventListener',
'removeListener'
]
const otherEmitterMethods = ['eventNames', 'getMaxListeners', 'setMaxListeners']
const addEmitterMethods = (service: any) => {
otherEmitterMethods.forEach((method) => {
service[method] = function (...args: any[]) {
if (typeof this.connection[method] !== 'function') {
throw new Error(`Can not call '${method}' on the client service connection`)
}
return this.connection[method](...args)
}
})
// Methods that should add the namespace (service path)
namespacedEmitterMethods.forEach((method) => {
service[method] = function (name: string, ...args: any[]) {
if (typeof this.connection[method] !== 'function') {
throw new Error(`Can not call '${method}' on the client service connection`)
}
const eventName = `${this.path} ${name}`
debug(`Calling emitter method ${method} with ` + `namespaced event '${eventName}'`)
const result = this.connection[method](eventName, ...args)
return result === this.connection ? this : result
}
})
}
interface ServiceOptions {
name: string
connection: any
method: string
events?: string[]
}
export type SocketService<T = any, D = Partial<any>, P extends Params = Params> = Service<T, D, P>
export class Service<T = any, D = Partial<T>, P extends Params = Params>
implements ServiceInterface<T, D, P>
{
events: string[]
path: string
connection: any
method: string
constructor(options: ServiceOptions) {
this.events = options.events
this.path = options.name
this.connection = options.connection
this.method = options.method
addEmitterMethods(this)
}
send<X = any>(method: string, ...args: any[]) {
return new Promise<X>((resolve, reject) => {
const route: Record<string, any> = args.pop()
let path = this.path
if (route) {
Object.keys(route).forEach((key) => {
path = path.replace(`:${key}`, route[key])
})
}
args.unshift(method, path)
const socketTimeout = this.connection.flags?.timeout || this.connection._opts?.ackTimeout
if (socketTimeout !== undefined) {
args.push(function (timeoutError: any, error: any, data: any) {
return timeoutError || error ? reject(convert(timeoutError || error)) : resolve(data)
})
} else {
args.push(function (error: any, data: any) {
return error ? reject(convert(error)) : resolve(data)
})
}
debug(`Sending socket.${this.method}`, args)
this.connection[this.method](...args)
})
}
methods(this: any, ...names: string[]) {
names.forEach((method) => {
const _method = `_${method}`
this[_method] = function (data: any, params: Params = {}) {
return this.send(method, data, params.query || {}, params.route || {})
}
this[method] = function (data: any, params: Params = {}) {
return this[_method](data, params, params.route || {})
}
})
return this
}
_find(params: Params = {}) {
return this.send<T | T[]>('find', params.query || {}, params.route || {})
}
find(params: Params = {}) {
return this._find(params)
}
_get(id: Id, params: Params = {}) {
return this.send<T>('get', id, params.query || {}, params.route || {})
}
get(id: Id, params: Params = {}) {
return this._get(id, params)
}
_create(data: D, params: Params = {}) {
return this.send<T>('create', data, params.query || {}, params.route || {})
}
create(data: D, params: Params = {}) {
return this._create(data, params)
}
_update(id: NullableId, data: D, params: Params = {}) {
if (typeof id === 'undefined') {
return Promise.reject(new Error("id for 'update' can not be undefined"))
}
return this.send<T>('update', id, data, params.query || {}, params.route || {})
}
update(id: NullableId, data: D, params: Params = {}) {
return this._update(id, data, params)
}
_patch(id: NullableId, data: D, params: Params = {}) {
return this.send<T | T[]>('patch', id, data, params.query || {}, params.route || {})
}
patch(id: NullableId, data: D, params: Params = {}) {
return this._patch(id, data, params)
}
_remove(id: NullableId, params: Params = {}) {
return this.send<T | T[]>('remove', id, params.query || {}, params.route || {})
}
remove(id: NullableId, params: Params = {}) {
return this._remove(id, params)
}
// `off` is actually not part of the Node event emitter spec
// but we are adding it since everybody is expecting it because
// of the emitter-component Socket.io is using
off(name: string, ...args: any[]) {
if (typeof this.connection.off === 'function') {
const result = this.connection.off(`${this.path} ${name}`, ...args)
return result === this.connection ? this : result
} else if (args.length === 0) {
// @ts-ignore
return this.removeAllListeners(name)
}
// @ts-ignore
return this.removeListener(name, ...args)
}
}