UNPKG

zeromq

Version:

Next-generation ZeroMQ bindings for Node.js

835 lines (764 loc) 22.9 kB
/* The API of the compatibility layer and parts of the implementation has been adapted from the original ZeroMQ.js version (up to 5.x) */ import {EventEmitter} from "events" import * as zmq from "." import {FullError} from "./errors" import * as longOptions from "./compat/long-options" import * as pollStates from "./compat/poll-states" import * as sendOptions from "./compat/send-options" type AnySocket = | zmq.Pair | zmq.Publisher | zmq.Subscriber | zmq.Request | zmq.Reply | zmq.Dealer | zmq.Router | zmq.Pull | zmq.Push | zmq.XPublisher | zmq.XSubscriber | zmq.Stream let count = 1 const shortOptions = { _fd: longOptions.ZMQ_FD, _ioevents: longOptions.ZMQ_EVENTS, _receiveMore: longOptions.ZMQ_RCVMORE, _subscribe: longOptions.ZMQ_SUBSCRIBE, _unsubscribe: longOptions.ZMQ_UNSUBSCRIBE, affinity: longOptions.ZMQ_AFFINITY, backlog: longOptions.ZMQ_BACKLOG, identity: longOptions.ZMQ_IDENTITY, linger: longOptions.ZMQ_LINGER, rate: longOptions.ZMQ_RATE, rcvbuf: longOptions.ZMQ_RCVBUF, last_endpoint: longOptions.ZMQ_LAST_ENDPOINT, reconnect_ivl: longOptions.ZMQ_RECONNECT_IVL, recovery_ivl: longOptions.ZMQ_RECOVERY_IVL, sndbuf: longOptions.ZMQ_SNDBUF, mechanism: longOptions.ZMQ_MECHANISM, plain_server: longOptions.ZMQ_PLAIN_SERVER, plain_username: longOptions.ZMQ_PLAIN_USERNAME, plain_password: longOptions.ZMQ_PLAIN_PASSWORD, curve_server: longOptions.ZMQ_CURVE_SERVER, curve_publickey: longOptions.ZMQ_CURVE_PUBLICKEY, curve_secretkey: longOptions.ZMQ_CURVE_SECRETKEY, curve_serverkey: longOptions.ZMQ_CURVE_SERVERKEY, zap_domain: longOptions.ZMQ_ZAP_DOMAIN, heartbeat_ivl: longOptions.ZMQ_HEARTBEAT_IVL, heartbeat_ttl: longOptions.ZMQ_HEARTBEAT_TTL, heartbeat_timeout: longOptions.ZMQ_HEARTBEAT_TIMEOUT, connect_timeout: longOptions.ZMQ_CONNECT_TIMEOUT, } class Context { static setMaxThreads(value: number) { zmq.context.ioThreads = value } static getMaxThreads() { return zmq.context.ioThreads } static setMaxSockets(value: number) { zmq.context.maxSockets = value } static getMaxSockets() { return zmq.context.maxSockets } constructor() { throw new Error("Context cannot be instantiated in compatibility mode") } } type SocketType = | "pair" | "req" | "rep" | "pub" | "sub" | "dealer" | "xreq" | "router" | "xrep" | "pull" | "push" | "xpub" | "xsub" | "stream" type Callback = (err?: Error) => void class Socket extends EventEmitter { [key: string]: any type: SocketType private _msg: zmq.MessageLike[] = [] private _recvQueue: zmq.Message[][] = [] private _sendQueue: Array<[zmq.MessageLike[], Callback | undefined]> = [] private _paused = false private _socket: AnySocket private _count = 0 constructor(type: SocketType) { super() this.type = type switch (type) { case "pair": this._socket = new zmq.Pair() break case "req": this._socket = new zmq.Request() break case "rep": this._socket = new zmq.Reply() break case "pub": this._socket = new zmq.Publisher() break case "sub": this._socket = new zmq.Subscriber() break case "dealer": case "xreq": this._socket = new zmq.Dealer() break case "router": case "xrep": this._socket = new zmq.Router() break case "pull": this._socket = new zmq.Pull() break case "push": this._socket = new zmq.Push() break case "xpub": this._socket = new zmq.XPublisher() break case "xsub": this._socket = new zmq.XSubscriber() break case "stream": this._socket = new zmq.Stream() break default: throw new Error(`Invalid socket type: ${type}`) } const recv = () => { this.once("_flushRecv", async () => { while (!this._socket.closed && !this._paused) { await this._recv() } if (!this._socket.closed) { recv() } }) } const send = () => { this.once("_flushSend", async () => { while ( !this._socket.closed && !this._paused && this._sendQueue.length ) { await this._send() } if (!this._socket.closed) { send() } }) } if (type !== "push" && type !== "pub") { recv() } send() this.emit("_flushRecv") } async _recv() { if ( this._socket instanceof zmq.Push || this._socket instanceof zmq.Publisher ) { throw new Error("Cannot receive on this socket type.") } try { if (this._recvQueue.length) { const msg = this._recvQueue.shift()! process.nextTick(() => this.emit("message", ...msg)) } { const msg = await this._socket.receive() if (this._paused) { this._recvQueue.push(msg) } else { process.nextTick(() => this.emit("message", ...msg)) } } } catch (err) { if (!this._socket.closed && (err as FullError).code !== "EBUSY") { process.nextTick(() => this.emit("error", err)) } } } async _send() { if ( this._socket instanceof zmq.Pull || this._socket instanceof zmq.Subscriber ) { throw new Error("Cannot send on this socket type.") } if (this._sendQueue.length) { const [msg, cb] = this._sendQueue.shift()! try { await (this._socket as zmq.Writable).send(msg) if (cb) { cb() } } catch (err) { if (cb) { cb(err as Error) } else { this.emit("error", err) } } } } bind(address: string, cb?: Callback) { this._socket .bind(address) .then(() => { process.nextTick(() => { this.emit("bind", address) if (cb) { cb() } }) }) .catch(err => { process.nextTick(() => { if (cb) { cb(err as Error) } else { this.emit("error", err) } }) }) return this } unbind(address: string, cb?: Callback) { this._socket .unbind(address) .then(() => { process.nextTick(() => { this.emit("unbind", address) if (cb) { cb() } }) }) .catch(err => { process.nextTick(() => { if (cb) { cb(err as Error) } else { this.emit("error", err) } }) }) return this } connect(address: string) { this._socket.connect(address) return this } disconnect(address: string) { this._socket.disconnect(address) return this } send( message: zmq.MessageLike[] | zmq.MessageLike, givenFlags: number | undefined | null = 0, cb: Callback | undefined = undefined, ) { const flags = (givenFlags ?? 0) | 0 this._msg = this._msg.concat(message) if ((flags & sendOptions.ZMQ_SNDMORE) === 0) { this._sendQueue.push([this._msg, cb]) this._msg = [] if (!this._paused) { this.emit("_flushSend") } } return this } read() { throw new Error( "read() has been removed from compatibility mode; " + "use on('message', ...) instead.", ) } bindSync(...args: Parameters<Socket["bind"]>) { try { Object.defineProperty(this, "bindSync", { value: require("deasync")(this.bind), }) } catch (err) { throw new Error( "bindSync() has been removed from compatibility mode; " + "use bind() instead, or add 'deasync' to your project dependencies", ) } this.bindSync(...args) } unbindSync(...args: Parameters<Socket["unbind"]>) { try { Object.defineProperty(this, "unbindSync", { value: require("deasync")(this.unbind), }) } catch (err) { throw new Error( "unbindSync() has been removed from compatibility mode; " + "use unbind() instead, or add 'deasync' to your project dependencies", ) } this.unbindSync(...args) } pause() { this._paused = true } resume() { this._paused = false this.emit("_flushRecv") this.emit("_flushSend") } close() { this._socket.close() return this } get closed() { return this._socket.closed } monitor(interval?: number, num?: number) { this._count = count++ /* eslint-disable-next-line no-unused-expressions */ this._count if (interval || num) { process.emitWarning( "Arguments to monitor() are ignored in compatibility mode; " + "all events are read automatically", ) } const events = this._socket.events const read = async () => { while (!events.closed) { try { const event = await events.receive() let type = event.type as string let value let error switch (event.type) { case "connect": break case "connect:delay": type = "connect_delay" break case "connect:retry": value = event.interval type = "connect_retry" break case "bind": type = "listen" break case "bind:error": error = event.error value = event.error ? event.error.errno : 0 type = "bind_error" break case "accept": break case "accept:error": error = event.error value = event.error ? event.error.errno : 0 type = "accept_error" break case "close": break case "close:error": error = event.error value = event.error ? event.error.errno : 0 type = "close_error" break case "disconnect": break case "end": return default: continue } this.emit(type, value, event.address, error) } catch (err) { if (!this._socket.closed) { this.emit("error", err) } } } } read() return this } unmonitor() { this._socket.events.close() return this } subscribe(filter: string) { if (this._socket instanceof zmq.Subscriber) { this._socket.subscribe(filter) return this } else { throw new Error("Subscriber socket required") } } unsubscribe(filter: string) { if (this._socket instanceof zmq.Subscriber) { this._socket.unsubscribe(filter) return this } else { throw new Error("Subscriber socket required") } } setsockopt(givenOption: number | keyof typeof shortOptions, value: any) { const option = typeof givenOption === "number" ? givenOption : shortOptions[givenOption] switch (option) { case longOptions.ZMQ_AFFINITY: this._socket.affinity = value break case longOptions.ZMQ_IDENTITY: ;(this._socket as zmq.Router).routingId = value break case longOptions.ZMQ_SUBSCRIBE: ;(this._socket as zmq.Subscriber).subscribe(value) break case longOptions.ZMQ_UNSUBSCRIBE: ;(this._socket as zmq.Subscriber).unsubscribe(value) break case longOptions.ZMQ_RATE: this._socket.rate = value break case longOptions.ZMQ_RECOVERY_IVL: this._socket.recoveryInterval = value break case longOptions.ZMQ_SNDBUF: ;(this._socket as zmq.Writable).sendBufferSize = value break case longOptions.ZMQ_RCVBUF: ;(this._socket as zmq.Readable).receiveBufferSize = value break case longOptions.ZMQ_LINGER: this._socket.linger = value break case longOptions.ZMQ_RECONNECT_IVL: this._socket.reconnectInterval = value break case longOptions.ZMQ_BACKLOG: this._socket.backlog = value break case longOptions.ZMQ_RECOVERY_IVL_MSEC: this._socket.recoveryInterval = value break case longOptions.ZMQ_RECONNECT_IVL_MAX: this._socket.reconnectMaxInterval = value break case longOptions.ZMQ_MAXMSGSIZE: this._socket.maxMessageSize = value break case longOptions.ZMQ_SNDHWM: ;(this._socket as zmq.Writable).sendHighWaterMark = value break case longOptions.ZMQ_RCVHWM: ;(this._socket as zmq.Readable).receiveHighWaterMark = value break case longOptions.ZMQ_MULTICAST_HOPS: ;(this._socket as zmq.Writable).multicastHops = value break case longOptions.ZMQ_RCVTIMEO: ;(this._socket as zmq.Readable).receiveTimeout = value break case longOptions.ZMQ_SNDTIMEO: ;(this._socket as zmq.Writable).sendTimeout = value break case longOptions.ZMQ_IPV4ONLY: this._socket.ipv6 = !value break case longOptions.ZMQ_ROUTER_MANDATORY: ;(this._socket as zmq.Router).mandatory = Boolean(value) break case longOptions.ZMQ_TCP_KEEPALIVE: this._socket.tcpKeepalive = value break case longOptions.ZMQ_TCP_KEEPALIVE_CNT: this._socket.tcpKeepaliveCount = value break case longOptions.ZMQ_TCP_KEEPALIVE_IDLE: this._socket.tcpKeepaliveIdle = value break case longOptions.ZMQ_TCP_KEEPALIVE_INTVL: this._socket.tcpKeepaliveInterval = value break case longOptions.ZMQ_TCP_ACCEPT_FILTER: this._socket.tcpAcceptFilter = value break case longOptions.ZMQ_DELAY_ATTACH_ON_CONNECT: this._socket.immediate = Boolean(value) break case longOptions.ZMQ_XPUB_VERBOSE: ;(this._socket as zmq.XPublisher).verbosity = value ? "allSubs" : null break case longOptions.ZMQ_ROUTER_RAW: throw new Error("ZMQ_ROUTER_RAW is not supported in compatibility mode") case longOptions.ZMQ_IPV6: this._socket.ipv6 = Boolean(value) break case longOptions.ZMQ_PLAIN_SERVER: this._socket.plainServer = Boolean(value) break case longOptions.ZMQ_PLAIN_USERNAME: this._socket.plainUsername = value break case longOptions.ZMQ_PLAIN_PASSWORD: this._socket.plainPassword = value break case longOptions.ZMQ_CURVE_SERVER: this._socket.curveServer = Boolean(value) break case longOptions.ZMQ_CURVE_PUBLICKEY: this._socket.curvePublicKey = value break case longOptions.ZMQ_CURVE_SECRETKEY: this._socket.curveSecretKey = value break case longOptions.ZMQ_CURVE_SERVERKEY: this._socket.curveServerKey = value break case longOptions.ZMQ_ZAP_DOMAIN: this._socket.zapDomain = value break case longOptions.ZMQ_HEARTBEAT_IVL: this._socket.heartbeatInterval = value break case longOptions.ZMQ_HEARTBEAT_TTL: this._socket.heartbeatTimeToLive = value break case longOptions.ZMQ_HEARTBEAT_TIMEOUT: this._socket.heartbeatTimeout = value break case longOptions.ZMQ_CONNECT_TIMEOUT: this._socket.connectTimeout = value break case longOptions.ZMQ_ROUTER_HANDOVER: ;(this._socket as zmq.Router).handover = Boolean(value) break default: throw new Error("Unknown option") } return this } getsockopt(givenOption: number | keyof typeof shortOptions) { const option = typeof givenOption !== "number" ? shortOptions[givenOption] : givenOption switch (option) { case longOptions.ZMQ_AFFINITY: return this._socket.affinity case longOptions.ZMQ_IDENTITY: return (this._socket as zmq.Router).routingId case longOptions.ZMQ_RATE: return this._socket.rate case longOptions.ZMQ_RECOVERY_IVL: return this._socket.recoveryInterval case longOptions.ZMQ_SNDBUF: return (this._socket as zmq.Writable).sendBufferSize case longOptions.ZMQ_RCVBUF: return (this._socket as zmq.Readable).receiveBufferSize case longOptions.ZMQ_RCVMORE: throw new Error("ZMQ_RCVMORE is not supported in compatibility mode") case longOptions.ZMQ_FD: throw new Error("ZMQ_FD is not supported in compatibility mode") case longOptions.ZMQ_EVENTS: return ( (this._socket.readable ? pollStates.ZMQ_POLLIN : 0) | (this._socket.writable ? pollStates.ZMQ_POLLOUT : 0) ) case longOptions.ZMQ_TYPE: return this._socket.type case longOptions.ZMQ_LINGER: return this._socket.linger case longOptions.ZMQ_RECONNECT_IVL: return this._socket.reconnectInterval case longOptions.ZMQ_BACKLOG: return this._socket.backlog case longOptions.ZMQ_RECOVERY_IVL_MSEC: return this._socket.recoveryInterval case longOptions.ZMQ_RECONNECT_IVL_MAX: return this._socket.reconnectMaxInterval case longOptions.ZMQ_MAXMSGSIZE: return this._socket.maxMessageSize case longOptions.ZMQ_SNDHWM: return (this._socket as zmq.Writable).sendHighWaterMark case longOptions.ZMQ_RCVHWM: return (this._socket as zmq.Readable).receiveHighWaterMark case longOptions.ZMQ_MULTICAST_HOPS: return (this._socket as zmq.Writable).multicastHops case longOptions.ZMQ_RCVTIMEO: return (this._socket as zmq.Readable).receiveTimeout case longOptions.ZMQ_SNDTIMEO: return (this._socket as zmq.Writable).sendTimeout case longOptions.ZMQ_IPV4ONLY: return !this._socket.ipv6 case longOptions.ZMQ_LAST_ENDPOINT: return this._socket.lastEndpoint case longOptions.ZMQ_ROUTER_MANDATORY: return (this._socket as zmq.Router).mandatory ? 1 : 0 case longOptions.ZMQ_TCP_KEEPALIVE: return this._socket.tcpKeepalive case longOptions.ZMQ_TCP_KEEPALIVE_CNT: return this._socket.tcpKeepaliveCount case longOptions.ZMQ_TCP_KEEPALIVE_IDLE: return this._socket.tcpKeepaliveIdle case longOptions.ZMQ_TCP_KEEPALIVE_INTVL: return this._socket.tcpKeepaliveInterval case longOptions.ZMQ_DELAY_ATTACH_ON_CONNECT: return this._socket.immediate ? 1 : 0 case longOptions.ZMQ_XPUB_VERBOSE: throw new Error("Reading ZMQ_XPUB_VERBOSE is not supported") case longOptions.ZMQ_ROUTER_RAW: throw new Error("ZMQ_ROUTER_RAW is not supported in compatibility mode") case longOptions.ZMQ_IPV6: return this._socket.ipv6 ? 1 : 0 case longOptions.ZMQ_MECHANISM: switch (this._socket.securityMechanism) { case "plain": return 1 case "curve": return 2 case "gssapi": return 3 default: return 0 } case longOptions.ZMQ_PLAIN_SERVER: return this._socket.plainServer ? 1 : 0 case longOptions.ZMQ_PLAIN_USERNAME: return this._socket.plainUsername case longOptions.ZMQ_PLAIN_PASSWORD: return this._socket.plainPassword case longOptions.ZMQ_CURVE_SERVER: return this._socket.curveServer ? 1 : 0 case longOptions.ZMQ_CURVE_PUBLICKEY: return this._socket.curvePublicKey case longOptions.ZMQ_CURVE_SECRETKEY: return this._socket.curveSecretKey case longOptions.ZMQ_CURVE_SERVERKEY: return this._socket.curveServerKey case longOptions.ZMQ_ZAP_DOMAIN: return this._socket.zapDomain case longOptions.ZMQ_HEARTBEAT_IVL: return this._socket.heartbeatInterval case longOptions.ZMQ_HEARTBEAT_TTL: return this._socket.heartbeatTimeToLive case longOptions.ZMQ_HEARTBEAT_TIMEOUT: return this._socket.heartbeatTimeout case longOptions.ZMQ_CONNECT_TIMEOUT: return this._socket.connectTimeout default: throw new Error("Unknown option") } } } for (const key in shortOptions) { if (!shortOptions.hasOwnProperty(key)) { continue } if (Socket.prototype.hasOwnProperty(key)) { continue } Object.defineProperty(Socket.prototype, key, { get(this: Socket) { return this.getsockopt(shortOptions[key as keyof typeof shortOptions]) }, set(this: Socket, givenVal: string | Buffer) { const val = typeof givenVal === "string" ? Buffer.from(givenVal, "utf8") : givenVal return this.setsockopt( shortOptions[key as keyof typeof shortOptions], val, ) }, }) } function createSocket(type: SocketType, options: {[key: string]: any} = {}) { const sock = new Socket(type) for (const key in options) { if (options.hasOwnProperty(key)) { sock[key] = options[key] } } return sock } function curveKeypair() { const {publicKey, secretKey} = zmq.curveKeyPair() return {public: publicKey, secret: secretKey} } function proxy(frontend: Socket, backend: Socket, capture?: Socket) { switch (`${frontend.type}/${backend.type}`) { case "push/pull": case "pull/push": case "xpub/xsub": frontend.on("message", (...args: zmq.MessageLike[]) => { backend.send(args) }) if (capture) { backend.on("message", (...args: zmq.MessageLike[]) => { frontend.send(args) capture.send(args) }) } else { backend.on("message", (...args: zmq.MessageLike[]) => { frontend.send(args) }) } break case "router/dealer": case "xrep/xreq": frontend.on("message", (...args: zmq.MessageLike[]) => { backend.send(args) }) if (capture) { backend.on("message", (...args: zmq.MessageLike[]) => { frontend.send(args) capture.send(args.slice(2)) }) } else { backend.on("message", (...args: zmq.MessageLike[]) => { frontend.send(args) }) } break default: throw new Error( "This socket type order is not supported in compatibility mode", ) } } const version = zmq.version export { version, Context, Socket, SocketType, createSocket as socket, createSocket, curveKeypair, proxy, shortOptions as options, } export * from "./compat/long-options" export * from "./compat/types" export * from "./compat/poll-states" export * from "./compat/send-options" export * from "./compat/capabilities" export * from "./compat/socket-states"