├── .gitignore ├── jasmine.json ├── src ├── socketOptions.ts ├── pub.ts ├── pull.ts ├── types.ts ├── utils │ ├── loadBalancer.ts │ ├── array.ts │ ├── distribution.ts │ ├── trie.ts │ └── multiTrie.ts ├── push.ts ├── dealer.ts ├── sub.ts ├── pair.ts ├── req.ts ├── index.ts ├── xsub.ts ├── rep.ts ├── router.ts ├── xpub.ts ├── webSocketListener.ts ├── socketBase.ts └── webSocketEndpoint.ts ├── tsconfig-test.json ├── test ├── dealer_router_test.ts ├── pubsub_test.ts └── reqrep_test.ts ├── .github └── workflows │ └── node.js.yml ├── tsconfig.json ├── package.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | .idea/ 4 | .npmrc 5 | .js 6 | .nyc_output/ 7 | GL/ 8 | coverage 9 | -------------------------------------------------------------------------------- /jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "lib/test", 3 | "spec_files": ["**/*[tT]est.js"], 4 | "stopSpecOnExpectationFailure": false, 5 | "random": false 6 | } 7 | -------------------------------------------------------------------------------- /src/socketOptions.ts: -------------------------------------------------------------------------------- 1 | export default class SocketOptions { 2 | immediate = false 3 | recvRoutingId = false 4 | routingId = "" 5 | reconnectInterval = 100 6 | xpubVerbose = false 7 | } -------------------------------------------------------------------------------- /tsconfig-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDirs": ["./src", "./test"] 5 | }, 6 | "include": ["./test"], 7 | "exclude": ["**/*.d.ts", "./lib", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /src/pub.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from "buffer" 2 | import XPub from './xpub' 3 | import {IEndpoint} from './types' 4 | 5 | export default class Pub extends XPub { 6 | protected xxrecv(endpoint: IEndpoint, ...frames: Buffer[]) { 7 | // Drop any message sent to pub socket 8 | } 9 | 10 | protected sendUnsubscription() { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pull.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | import SocketBase from './socketBase' 3 | import {IEndpoint} from './types' 4 | 5 | export default class Pull extends SocketBase { 6 | protected attachEndpoint(endpoint: IEndpoint) { 7 | 8 | } 9 | 10 | protected endpointTerminated(endpoint: IEndpoint) { 11 | } 12 | 13 | protected xrecv(endpoint: IEndpoint, ...frames: Buffer[]) { 14 | this.emit('message', ...frames) 15 | } 16 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | 3 | export type Frame = Buffer | string 4 | export type Msg = Frame[] 5 | 6 | export interface IEndpoint { 7 | send(msg: Msg):boolean 8 | 9 | close(): void; 10 | readonly address: string; 11 | routingKey: Buffer; 12 | routingKeyString: string; 13 | removeListener(event: string | symbol, listener: (...args: any[]) => void): this; 14 | on(event: string | symbol, listener: (...args: any[]) => void): this; 15 | } 16 | 17 | export interface IListener { 18 | readonly address: string; 19 | removeListener(event: string | symbol, listener: (...args: any[]) => void): this; 20 | on(event: string | symbol, listener: (...args: any[]) => void): this; 21 | close(): void; 22 | } -------------------------------------------------------------------------------- /test/dealer_router_test.ts: -------------------------------------------------------------------------------- 1 | import 'jasmine' 2 | import * as jsmq from '../src' 3 | 4 | describe('dealer-router', function () { 5 | it('ping-pong', function (done) { 6 | const router = new jsmq.Router() 7 | const dealer = new jsmq.Dealer() 8 | router.bind('ws://localhost:3002/dealer-router') 9 | dealer.connect('ws://localhost:3002/dealer-router') 10 | 11 | dealer.send('hello') 12 | router.once('message', (routingId, message) => { 13 | expect(message.toString()).toBe('hello') 14 | router.send([routingId, 'world']) 15 | dealer.once('message', reply => { 16 | expect(reply.toString()).toBe('world') 17 | done() 18 | }) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/utils/loadBalancer.ts: -------------------------------------------------------------------------------- 1 | import {Msg, IEndpoint} from '../types' 2 | 3 | export default class LoadBalancer { 4 | endpoints:IEndpoint[] = [] 5 | current = 0 6 | 7 | attach(endpoint:IEndpoint) { 8 | this.endpoints.push(endpoint) 9 | } 10 | 11 | terminated(endpoint:IEndpoint) { 12 | const index = this.endpoints.indexOf(endpoint) 13 | 14 | if (this.current === this.endpoints.length - 1) { 15 | this.current = 0 16 | } 17 | 18 | this.endpoints.splice(index, 1) 19 | } 20 | 21 | send(msg:Msg) { 22 | if (this.endpoints.length === 0) 23 | return false 24 | 25 | const result = this.endpoints[this.current].send(msg) 26 | this.current = (this.current + 1) % this.endpoints.length 27 | 28 | return result 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /src/push.ts: -------------------------------------------------------------------------------- 1 | import SocketBase from './socketBase' 2 | import LoadBalancer from './utils/loadBalancer' 3 | import {IEndpoint, Msg} from './types' 4 | 5 | export default class Push extends SocketBase { 6 | private loadBalancer = new LoadBalancer() 7 | private pending: Msg[] = [] 8 | 9 | protected attachEndpoint(endpoint: IEndpoint) { 10 | this.loadBalancer.attach(endpoint) 11 | 12 | while(true) { 13 | const msg = this.pending.shift() 14 | if (!msg) 15 | break 16 | 17 | if (!this.loadBalancer.send(msg)) 18 | break 19 | } 20 | } 21 | 22 | protected endpointTerminated(endpoint: IEndpoint) { 23 | this.loadBalancer.terminated(endpoint) 24 | } 25 | 26 | protected xsend(msg: Msg) { 27 | if (!this.loadBalancer.send(msg)) 28 | this.pending.push(msg) 29 | } 30 | } -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | export function copy(src:Array, srcOffset:number, dest:Array, destOffset:number, length:number) { 2 | for (let i = 0; i < length; i++) 3 | dest[i + destOffset] = src[i + srcOffset] 4 | } 5 | 6 | export function resize(src:Array, size:number,ended:boolean) 7 | { 8 | if (size > src.length) 9 | { 10 | const dest = new Array(size).fill(null) 11 | if (ended) 12 | copy(src, 0, dest, 0, src.length); 13 | else 14 | copy(src, 0, dest, size - src.length, src.length); 15 | return dest 16 | } 17 | else if (size < src.length) 18 | { 19 | const dest = new Array(size).fill(null) 20 | if (ended) 21 | copy(src, 0, dest, 0, size); 22 | else 23 | copy(src, src.length - size, dest, 0, size); 24 | return dest 25 | } 26 | 27 | return src 28 | } -------------------------------------------------------------------------------- /src/dealer.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | import SocketBase from './socketBase' 3 | import LoadBalancer from './utils/loadBalancer' 4 | import {IEndpoint, Msg} from './types' 5 | 6 | export default class Dealer extends SocketBase { 7 | private loadBalancer = new LoadBalancer() 8 | private pending: Msg[] = [] 9 | 10 | protected attachEndpoint(endpoint: IEndpoint) { 11 | this.loadBalancer.attach(endpoint) 12 | 13 | while(true) { 14 | const msg = this.pending.shift() 15 | if (!msg) 16 | break 17 | 18 | if (!this.loadBalancer.send(msg)) 19 | break 20 | } 21 | } 22 | 23 | protected endpointTerminated(endpoint: IEndpoint) { 24 | this.loadBalancer.terminated(endpoint) 25 | } 26 | 27 | protected xrecv(endpoint: IEndpoint, ...frames: Buffer[]) { 28 | this.emit('message', ...frames) 29 | } 30 | 31 | protected xsend(msg: Msg) { 32 | if (!this.loadBalancer.send(msg)) 33 | this.pending.push(msg) 34 | } 35 | } -------------------------------------------------------------------------------- /src/sub.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | import {isString} from 'lodash' 3 | import XSub from './xsub' 4 | import {Frame, Msg} from './types' 5 | 6 | export default class Sub extends XSub { 7 | subscribe(topic: Frame) { 8 | if (isString(topic)) { 9 | const frame = Buffer.concat([Buffer.from([1]), Buffer.from(topic)]) 10 | super.xsend([frame]) 11 | } else if (Buffer.isBuffer(topic)) { 12 | const frame = Buffer.concat([Buffer.from([1]), topic]) 13 | super.xsend([frame]) 14 | } else 15 | throw new Error('unsupported topic type') 16 | } 17 | 18 | unsubscribe(topic: Frame) { 19 | if (isString(topic)) { 20 | const frame = Buffer.concat([Buffer.from([0]), Buffer.from(topic)]) 21 | super.xsend([frame]) 22 | } else if (Buffer.isBuffer(topic)) { 23 | const frame = Buffer.concat([Buffer.from([0]), topic]) 24 | super.xsend([frame]) 25 | } else 26 | throw new Error('unsupported topic type') 27 | } 28 | 29 | xsend(msg: Msg) { 30 | throw new Error('not supported') 31 | } 32 | } -------------------------------------------------------------------------------- /src/pair.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | import SocketBase from './socketBase' 3 | import {IEndpoint, Msg} from './types' 4 | 5 | export default class Pair extends SocketBase { 6 | private endpoint?:IEndpoint 7 | private pending: Msg[] = [] 8 | 9 | protected attachEndpoint(endpoint: IEndpoint) { 10 | if (this.endpoint) { 11 | endpoint.close() 12 | return 13 | } 14 | 15 | this.endpoint = endpoint 16 | 17 | while(true) { 18 | const msg = this.pending.shift() 19 | if (!msg) 20 | break 21 | 22 | if (!endpoint.send(msg)) 23 | break 24 | } 25 | } 26 | 27 | protected endpointTerminated(endpoint: IEndpoint) { 28 | if (endpoint === this.endpoint) 29 | this.endpoint = undefined 30 | } 31 | 32 | protected xrecv(endpoint: IEndpoint, ...frames: Buffer[]) { 33 | if (endpoint === this.endpoint) 34 | this.emit('message', ...frames) 35 | } 36 | 37 | protected xsend(msg: Msg) { 38 | if (this.endpoint) 39 | this.endpoint.send(msg) 40 | else 41 | this.pending.push(msg) 42 | } 43 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "pretty" : true, 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": ["es5"], /* Specify library files to be included in the compilation. */ 7 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 8 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 9 | "sourceMap": true, /* Generates corresponding '.map' file. */ 10 | "outDir": "./lib", /* Redirect output structure to the directory. */ 11 | "rootDirs": ["./src"], /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 12 | "strict": true, /* Enable all strict type-checking options. */ 13 | "allowSyntheticDefaultImports": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | }, 15 | "exclude": ["./test", "**/*.d.ts", "./lib", "node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /src/req.ts: -------------------------------------------------------------------------------- 1 | import Dealer from './dealer' 2 | import {IEndpoint, Msg} from './types' 3 | import {Buffer} from "buffer" 4 | 5 | export default class Req extends Dealer { 6 | private static bottom = Buffer.alloc(0) 7 | 8 | // If true, request was already sent and reply wasn't received yet or 9 | // was received partially. 10 | receivingReply: boolean 11 | 12 | constructor() { 13 | super() 14 | this.receivingReply = false 15 | } 16 | 17 | protected xsend(msg: Msg) { 18 | // If we've sent a request and we still haven't got the reply, 19 | // we can't send another request. 20 | if (this.receivingReply) 21 | throw new Error("cannot send another request") 22 | 23 | const withBottom = [Req.bottom, ...msg] 24 | super.xsend(withBottom) 25 | 26 | this.receivingReply = true 27 | } 28 | 29 | protected xrecv(endpoint: IEndpoint, bottom:Buffer, ...frames: Buffer[]) { 30 | // If request wasn't send, we can't process reply, drop. 31 | if (!this.receivingReply) 32 | return 33 | 34 | // Skip messages until one with the right first frames is found. 35 | if (frames.length === 0 || bottom.length !== 0) 36 | return 37 | 38 | this.receivingReply = false 39 | 40 | super.xrecv(endpoint, ...frames) 41 | } 42 | } -------------------------------------------------------------------------------- /test/pubsub_test.ts: -------------------------------------------------------------------------------- 1 | import * as jsmq from '../src' 2 | 3 | describe('pubsub', function() { 4 | it('subscribe', function(done) { 5 | const pub = new jsmq.XPub() 6 | const sub = new jsmq.Sub() 7 | 8 | pub.bind('ws://localhost:55556') 9 | sub.subscribe('A') 10 | sub.connect('ws://localhost:55556') 11 | 12 | // Waiting for subscriptions before publishing 13 | pub.once('message', () => { 14 | pub.send('B') 15 | pub.send('AAA') 16 | 17 | sub.once('message', topic => { 18 | expect(topic.toString()).toBe('AAA') 19 | pub.close() 20 | sub.close() 21 | done() 22 | }) 23 | }) 24 | }) 25 | 26 | it('unsubscribe', function (done) { 27 | const pub = new jsmq.XPub() 28 | const sub = new jsmq.Sub() 29 | 30 | pub.bind('ws://localhost:55556') 31 | sub.subscribe('A') 32 | sub.subscribe('B') 33 | sub.connect('ws://localhost:55556') 34 | 35 | // Waiting for subscriptions before publishing 36 | pub.once('message', () => { 37 | pub.send('A') 38 | sub.once('message', topic => { 39 | sub.unsubscribe('A') 40 | pub.send('A') 41 | pub.send('B') 42 | 43 | sub.once('message', topic2 => { 44 | expect(topic2.toString()).toBe('B') 45 | pub.close() 46 | sub.close() 47 | done() 48 | }) 49 | }) 50 | }) 51 | }) 52 | }) -------------------------------------------------------------------------------- /test/reqrep_test.ts: -------------------------------------------------------------------------------- 1 | import * as jsmq from '../src' 2 | 3 | describe('reqrep', function() { 4 | it('simple request response', function(done) { 5 | const req = new jsmq.Req() 6 | const rep = new jsmq.Rep() 7 | 8 | rep.bind('ws://localhost:55556') 9 | req.connect('ws://localhost:55556') 10 | 11 | req.send('Hello') 12 | rep.once('message', msg => { 13 | expect(msg.toString()).toEqual('Hello') 14 | rep.send('World') 15 | }) 16 | req.once('message', msg => { 17 | expect(msg.toString()).toEqual('World') 18 | req.close() 19 | rep.close() 20 | done() 21 | }) 22 | }) 23 | 24 | it('multiple requests', function (done) { 25 | const rep = new jsmq.Rep() 26 | const reqs:jsmq.Req[] = [] 27 | const last = new jsmq.Req() 28 | 29 | rep.bind('ws://localhost:55556') 30 | 31 | for (let i = 0; i < 100; i++) { 32 | reqs[i] = new jsmq.Req() 33 | reqs[i].connect('ws://localhost:55556') 34 | } 35 | last.connect('ws://localhost:55556') 36 | 37 | rep.on('message', msg => rep.send(msg)) 38 | for (let i = 0; i < 100; i++) { 39 | reqs[i].send(i.toString()) 40 | reqs[i].once('message', reply => expect(reply.toString()).toEqual(i.toString())) 41 | } 42 | last.send('done') 43 | last.once('message', reply => { 44 | expect(reply.toString()).toEqual('done') 45 | 46 | for (let i = 0; i < 100; i++) 47 | reqs[i].close() 48 | last.close() 49 | rep.close() 50 | 51 | done() 52 | }) 53 | }) 54 | }) -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Dealer from './dealer' 2 | import Router from './router' 3 | import Sub from './sub' 4 | import XSub from './xsub' 5 | import Pub from './pub' 6 | import XPub from './xpub' 7 | import Pull from './pull' 8 | import Push from './push' 9 | import Pair from './pair' 10 | import Req from './req' 11 | import Rep from './rep' 12 | 13 | export function socket(type:'dealer'|'router'|'pub'|'sub'|'xsub'|'xpub'|'pull'|'push'|'pair'|'req'|'rep') { 14 | 15 | switch (type) { 16 | case 'dealer': 17 | return new Dealer() 18 | case 'router': 19 | return new Router() 20 | case 'pub': 21 | return new Pub() 22 | case 'sub': 23 | return new Sub() 24 | case 'xsub': 25 | return new XSub() 26 | case 'xpub': 27 | return new XPub() 28 | case 'pull': 29 | return new Pull() 30 | case 'push': 31 | return new Push() 32 | case 'pair': 33 | return new Pair() 34 | case 'req': 35 | return new Req() 36 | case 'rep': 37 | return new Rep() 38 | default: 39 | throw new Error('Unsupported socket type') 40 | } 41 | } 42 | 43 | export {default as Sub} from './sub' 44 | export {default as XSub} from './xsub' 45 | export {default as Router} from './router' 46 | export {default as Dealer} from './dealer' 47 | export {default as XPub} from './xpub' 48 | export {default as Pub} from './pub' 49 | export {default as Push} from './push' 50 | export {default as Pull} from './pull' 51 | export {default as Pair} from './pair' 52 | export {default as Req} from './req' 53 | export {default as Rep} from './rep' 54 | export {Buffer} from 'buffer' 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jszmq", 3 | "version": "0.1.3", 4 | "description": "Port of zeromq to Javascript over Web Socket transport", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.json", 9 | "check": "tsc -p tsconfig.json --noEmit", 10 | "clear": "rm -rf lib/ dist/ coverage/ .nyc_output/", 11 | "clear:all": "rm -rf node_modules/ npm-debug.log && npm run clear", 12 | "build:test": "npm run clear && tsc -p tsconfig-test.json", 13 | "test": "npm run build:test && jasmine --reporter=jasmine-console-reporter --config=jasmine.json", 14 | "coverage": "nyc npm run test && nyc report --reporter=html", 15 | "coveralls": "nyc npm run test && nyc report --reporter=text-lcov | coveralls -v" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/zeromq/jszmq.git" 20 | }, 21 | "author": "", 22 | "license": "MPL-2.0", 23 | "bugs": { 24 | "url": "https://github.com/zeromq/jszmq/issues" 25 | }, 26 | "homepage": "https://github.com/zeromq/jszmq#readme", 27 | "dependencies": { 28 | "@types/events": "^3.0.0", 29 | "@types/lodash": "^4.14.158", 30 | "@types/node": "^14.0.26", 31 | "@types/setasap": "^2.0.0", 32 | "@types/ws": "^7.2.6", 33 | "assert": "^2.0.0", 34 | "buffer": "^5.6.0", 35 | "events": "^3.2.0", 36 | "isomorphic-ws": "^4.0.1", 37 | "lodash": "^4.17.19", 38 | "setasap": "^2.0.1", 39 | "ws": "^7.3.1" 40 | }, 41 | "devDependencies": { 42 | "@types/jasmine": "^3.5.11", 43 | "coveralls": "^3.1.0", 44 | "jasmine": "^3.6.1", 45 | "jasmine-console-reporter": "^3.1.0", 46 | "nyc": "^15.1.0", 47 | "ts-node": "^8.10.2", 48 | "typescript": "^3.9.7" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/xsub.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | import SocketBase from './socketBase' 3 | import {IEndpoint, Msg} from './types' 4 | import Trie from './utils/trie' 5 | import Distribution from './utils/distribution' 6 | 7 | export default class XSub extends SocketBase { 8 | subscriptions: Trie 9 | distribution:Distribution 10 | 11 | constructor() { 12 | super() 13 | this.subscriptions = new Trie() 14 | this.distribution = new Distribution() 15 | } 16 | 17 | protected attachEndpoint(endpoint:IEndpoint) { 18 | this.distribution.attach(endpoint) 19 | 20 | this.subscriptions.forEach(s => endpoint.send([Buffer.concat([Buffer.from([1]), s])])) 21 | } 22 | 23 | protected hiccuped(endpoint: IEndpoint) { 24 | this.subscriptions.forEach(s => endpoint.send([Buffer.concat([Buffer.from([1]), s])])) 25 | } 26 | 27 | protected endpointTerminated(endpoint:IEndpoint) { 28 | this.distribution.terminated(endpoint) 29 | } 30 | 31 | protected xrecv(endpoint:IEndpoint, ...frames: Buffer[]) { 32 | const topic = frames[0] 33 | 34 | const subscribed = this.subscriptions.check(topic, 0, topic.length) 35 | if (subscribed) 36 | this.emit('message', ...frames) 37 | } 38 | 39 | protected xsend(msg:Msg) { 40 | const frame = msg[0] 41 | 42 | if (!Buffer.isBuffer(frame)) 43 | throw new Error("subscription must be a buffer") 44 | 45 | if (frame.length > 0 && frame.readUInt8(0) === 1) { 46 | this.subscriptions.add(frame, 1, frame.length - 1) 47 | this.distribution.sendToAll(msg) 48 | } else if (frame.length > 0 && frame.readUInt8(0) === 0) { 49 | // Removing only one subscriptions 50 | const removed = this.subscriptions.remove(frame, 1, frame.length - 1) 51 | if (removed) 52 | this.distribution.sendToAll(msg) 53 | } else { 54 | // upstream message unrelated to sub/unsub 55 | this.distribution.sendToAll(msg) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/rep.ts: -------------------------------------------------------------------------------- 1 | import Router from './router' 2 | import {IEndpoint, Msg} from './types' 3 | import {Buffer} from "buffer" 4 | import setAsap from 'setasap' 5 | 6 | type PendingMsg = [IEndpoint, Buffer[]] 7 | 8 | export default class Rep extends Router { 9 | private static bottom = Buffer.alloc(0) 10 | 11 | sendingReply: boolean 12 | ids:Buffer[] 13 | pending:PendingMsg[] 14 | 15 | constructor() { 16 | super() 17 | this.sendingReply = false 18 | this.ids = [] 19 | this.pending = [] 20 | } 21 | 22 | protected xsend(msg: Msg) { 23 | // If we are in the middle of receiving a request, we cannot send reply. 24 | if (!this.sendingReply) 25 | throw new Error("cannot send another reply") 26 | 27 | const withIds = [...this.ids, Rep.bottom, ...msg] 28 | super.xsend(withIds) 29 | 30 | this.ids = [] 31 | 32 | // We are ready to handle next message 33 | const nextMsg = this.pending.shift() 34 | if (nextMsg) 35 | setAsap(() => this.recvInternal(nextMsg[0], nextMsg[1])) 36 | else 37 | this.sendingReply = false 38 | } 39 | 40 | private recvInternal(endpoint: IEndpoint, frames: Buffer[]) { 41 | while(true) { 42 | const frame = frames.shift() 43 | 44 | // Invalid msg, dropping current msg 45 | if (!frame) { 46 | this.ids = [] 47 | 48 | const nextMsg = this.pending.shift() 49 | if (nextMsg) 50 | this.recvInternal(nextMsg[0], nextMsg[1]) 51 | 52 | return 53 | } 54 | 55 | // Reached bottom, enqueue msg 56 | if (frame.length === 0) { 57 | this.sendingReply = true 58 | this.emit('message', ...frames) 59 | return 60 | } 61 | 62 | this.ids.push(frame) 63 | } 64 | } 65 | 66 | protected xxrecv(endpoint: IEndpoint, ...frames: Buffer[]) { 67 | // If we are in middle of sending a reply, we cannot receive next request yet, add to pending 68 | if (this.sendingReply) 69 | this.pending.push([endpoint, frames]) 70 | else 71 | this.recvInternal(endpoint, frames) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | import SocketBase from './socketBase' 3 | import {includes, pull} from 'lodash' 4 | import {IEndpoint, Msg} from './types' 5 | 6 | export default class Router extends SocketBase { 7 | anonymousPipes: IEndpoint[] = [] 8 | pipes: Map = new Map() 9 | nextId: number = 0 10 | 11 | constructor() { 12 | super() 13 | this.options.recvRoutingId = true 14 | } 15 | 16 | protected attachEndpoint(endpoint: IEndpoint) { 17 | this.anonymousPipes.push(endpoint) 18 | } 19 | 20 | protected endpointTerminated(endpoint: IEndpoint) { 21 | this.pipes.delete(endpoint.routingKeyString) 22 | pull(this.anonymousPipes, endpoint) 23 | } 24 | 25 | protected xrecv(endpoint: IEndpoint, ...msg: Buffer[]) { 26 | // For anonymous pipe, the first message is the identity 27 | if (includes(this.anonymousPipes, endpoint)) { 28 | pull(this.anonymousPipes, endpoint) 29 | 30 | let routingKey = msg[0] 31 | if (routingKey.length > 0) 32 | endpoint.routingKey = Buffer.concat([new Uint8Array([0]), routingKey]) 33 | else { 34 | const buffer = Buffer.alloc(5); 35 | buffer.writeUInt8(1, 0) 36 | buffer.writeInt32BE(this.nextId, 1) 37 | endpoint.routingKey = buffer 38 | this.nextId++ 39 | } 40 | 41 | endpoint.routingKeyString = endpoint.routingKey.toString('hex') 42 | this.pipes.set(endpoint.routingKeyString, endpoint) 43 | 44 | return 45 | } 46 | 47 | this.xxrecv(endpoint, endpoint.routingKey, ...msg) 48 | } 49 | 50 | protected xxrecv(endpoint: IEndpoint, ...msg: Buffer[]) { 51 | this.emit('message', ...msg) 52 | } 53 | 54 | protected xsend(msg: Msg) { 55 | if (msg.length <= 1) 56 | throw new Error('router message must include a routing key') 57 | 58 | const routingKey = msg.shift() 59 | if (!Buffer.isBuffer(routingKey)) 60 | throw new Error('routing key must be a buffer') 61 | 62 | const endpoint = this.pipes.get(routingKey.toString('hex')) 63 | if (!endpoint) 64 | return; // TODO: use mandatory option, if true throw exception here 65 | 66 | endpoint.send(msg) 67 | } 68 | } -------------------------------------------------------------------------------- /src/xpub.ts: -------------------------------------------------------------------------------- 1 | import {Buffer} from 'buffer' 2 | import SocketBase from './socketBase' 3 | import {IEndpoint, Msg} from './types' 4 | import MultiTrie from './utils/multiTrie' 5 | import Distribution from './utils/distribution' 6 | 7 | export default class XPub extends SocketBase { 8 | subscriptions = new MultiTrie() 9 | distribution = new Distribution() 10 | 11 | constructor() { 12 | super() 13 | 14 | this.markAsMatching = this.markAsMatching.bind(this) 15 | this.sendUnsubscription = this.sendUnsubscription.bind(this) 16 | } 17 | 18 | private markAsMatching(endpoint: IEndpoint) { 19 | this.distribution.match(endpoint) 20 | } 21 | 22 | protected sendUnsubscription(endpoint: IEndpoint, data: Buffer, size: number) { 23 | const unsubscription = Buffer.concat([Buffer.from([0]), data.slice(0, size)]) 24 | endpoint.send([unsubscription]) 25 | } 26 | 27 | protected attachEndpoint(endpoint: IEndpoint) { 28 | this.distribution.attach(endpoint) 29 | } 30 | 31 | protected endpointTerminated(endpoint: IEndpoint) { 32 | this.subscriptions.removeEndpoint(endpoint, this.sendUnsubscription) 33 | this.distribution.terminated(endpoint) 34 | } 35 | 36 | protected xsend(msg: Msg) { 37 | let topic: Buffer 38 | 39 | if (Buffer.isBuffer(msg[0])) { 40 | // @ts-ignore 41 | topic = msg[0] 42 | } else { 43 | // @ts-ignore 44 | topic = Buffer.from(msg[0], 'utf8') 45 | } 46 | 47 | this.subscriptions.match(topic, 0, topic.length, this.markAsMatching) 48 | this.distribution.sendToMatching(msg) 49 | } 50 | 51 | protected xrecv(endpoint: IEndpoint, subscription:Buffer, ...frames: Buffer[]) { 52 | if (subscription.length > 0) { 53 | const type = subscription.readUInt8(0) 54 | if (type === 0 || type === 1) { 55 | let unique 56 | 57 | if (type === 0) 58 | unique = this.subscriptions.remove(subscription, 1, subscription.length - 1, endpoint) 59 | else 60 | unique = this.subscriptions.add(subscription, 1, subscription.length - 1, endpoint) 61 | 62 | if (unique || this.options.xpubVerbose) 63 | this.xxrecv(endpoint, subscription, ...frames) 64 | 65 | return 66 | } 67 | } 68 | 69 | this.xxrecv(endpoint, subscription, ...frames) 70 | } 71 | 72 | protected xxrecv(endpoint: IEndpoint, ...frames: Buffer[]) { 73 | this.emit('message', ...frames) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/distribution.ts: -------------------------------------------------------------------------------- 1 | import {pull} from 'lodash' 2 | import {IEndpoint, Msg} from '../types' 3 | 4 | function swap(items: Array, index1: number, index2: number) { 5 | if (index1 === index2) 6 | return; 7 | 8 | let item1 = items[index1] 9 | let item2 = items[index2] 10 | if (item1 !== null) 11 | items[index2] = item1 12 | if (item2 !== null) 13 | items[index1] = item2 14 | } 15 | 16 | export default class Distribution { 17 | endpoints: IEndpoint[] = [] 18 | matching = 0 19 | active = 0 20 | 21 | attach(endpoint: IEndpoint) { 22 | this.endpoints.push(endpoint) 23 | swap(this.endpoints, this.active, this.endpoints.length - 1) 24 | this.active++ 25 | } 26 | 27 | match(endpoint: IEndpoint) { 28 | const index = this.endpoints.indexOf(endpoint) 29 | 30 | // If pipe is already matching do nothing. 31 | if (index < this.matching) 32 | return; 33 | 34 | // If the pipe isn't active, ignore it. 35 | if (index >= this.active) 36 | return; 37 | 38 | // Mark the pipe as matching. 39 | swap(this.endpoints, index, this.matching) 40 | this.matching++ 41 | } 42 | 43 | unmatch() { 44 | this.matching = 0; 45 | } 46 | 47 | terminated(endpoint: IEndpoint) { 48 | // Remove the endpoint from the list; adjust number of matching, active and/or 49 | // eligible endpoint accordingly. 50 | if (this.endpoints.indexOf(endpoint) < this.matching) 51 | this.matching-- 52 | if (this.endpoints.indexOf(endpoint) < this.active) 53 | this.active-- 54 | 55 | pull(this.endpoints, endpoint) 56 | } 57 | 58 | activated(endpoint: IEndpoint) { 59 | // Move the pipe from passive to active state. 60 | swap(this.endpoints, this.endpoints.indexOf(endpoint), this.active) 61 | this.active++ 62 | } 63 | 64 | sendToAll(msg: Msg) { 65 | this.matching = this.active 66 | this.sendToMatching(msg) 67 | } 68 | 69 | sendToMatching(msg: Msg) { 70 | // If there are no matching pipes available, simply drop the message. 71 | if (this.matching === 0) 72 | return; 73 | 74 | for (let i = 0; i < this.matching; i++) { 75 | if (!this.write(this.endpoints[i], msg)) 76 | --i; // Retry last write because index will have been swapped 77 | } 78 | } 79 | 80 | write(endpoint: IEndpoint, msg: Msg) { 81 | if (!endpoint.send(msg)) { 82 | swap(this.endpoints, this.endpoints.indexOf(endpoint), this.matching - 1) 83 | this.matching-- 84 | swap(this.endpoints, this.endpoints.indexOf(endpoint), this.active - 1) 85 | this.active-- 86 | return false; 87 | } 88 | 89 | return true; 90 | } 91 | } -------------------------------------------------------------------------------- /src/webSocketListener.ts: -------------------------------------------------------------------------------- 1 | import * as WebSocket from 'isomorphic-ws' 2 | import {URL} from 'url' 3 | import {toNumber} from 'lodash' 4 | import { EventEmitter } from 'events' 5 | import SocketOptions from './socketOptions' 6 | import Endpoint from './webSocketEndpoint' 7 | import * as http from 'http' 8 | import * as https from 'https' 9 | import * as net from "net" 10 | import * as url from 'url' 11 | import {IListener} from './types' 12 | 13 | type HttpServer = http.Server | https.Server 14 | 15 | class HttpServerListener { 16 | servers = new Map() 17 | 18 | constructor(private server:HttpServer) { 19 | server.on('upgrade', this.onUpgrade.bind(this)) 20 | } 21 | 22 | onUpgrade(request:http.IncomingMessage, socket: net.Socket, head: Buffer) { 23 | let wsServer: WebSocket.Server 24 | 25 | if (request.url) { 26 | const path = url.parse(request.url).pathname 27 | 28 | if (path) { 29 | const wsServer = this.servers.get(path) 30 | 31 | if (wsServer) { 32 | wsServer.handleUpgrade(request, socket, head, function done(ws) { 33 | wsServer.emit('connection', ws, request) 34 | }) 35 | return 36 | } 37 | } 38 | } 39 | 40 | socket.destroy() 41 | } 42 | 43 | add(path:string, wsServer: WebSocket.Server) { 44 | this.servers.set(path, wsServer) 45 | } 46 | 47 | remove(path:string) { 48 | this.servers.delete(path) 49 | 50 | if (this.servers.size === 0) 51 | listeners.delete(this.server) 52 | } 53 | } 54 | 55 | const listeners = new Map() 56 | 57 | function getHttpServerListener(httpServer:HttpServer) { 58 | let listener = listeners.get(httpServer) 59 | 60 | if (listener) 61 | return listener 62 | 63 | listener = new HttpServerListener(httpServer) 64 | listeners.set(httpServer, listener) 65 | 66 | return listener 67 | } 68 | 69 | export default class WebSocketListener extends EventEmitter implements IListener { 70 | server:WebSocket.Server 71 | path:string|undefined 72 | 73 | constructor(public address:string, private httpServer: HttpServer | undefined, private options:SocketOptions) { 74 | super() 75 | this.onConnection = this.onConnection.bind(this) 76 | 77 | if (!WebSocket.Server) 78 | throw 'binding websocket is not supported on browser' 79 | 80 | const url = new URL(address) 81 | 82 | let port 83 | 84 | if (url.port) 85 | port = toNumber(url.port) 86 | else if (url.protocol === 'wss') 87 | port = 443 88 | else if (url.protocol == 'ws') 89 | port = 80 90 | else 91 | throw new Error('not a websocket address') 92 | 93 | if (httpServer) { 94 | this.server = new WebSocket.Server({noServer: true}) 95 | const listener = getHttpServerListener(httpServer) 96 | this.path = url.pathname 97 | listener.add(url.pathname, this.server) 98 | } else { 99 | this.server = new WebSocket.Server({ 100 | port: port, 101 | path: url.pathname 102 | }) 103 | } 104 | 105 | 106 | this.server.on('connection', this.onConnection) 107 | } 108 | 109 | onConnection(connection:WebSocket) { 110 | const endpoint = new Endpoint(connection, this.options) 111 | this.emit('attach', endpoint) 112 | } 113 | 114 | close(): void { 115 | if (this.path && this.httpServer) 116 | getHttpServerListener(this.httpServer).remove(this.path) 117 | 118 | this.server.close() 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/socketBase.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import SocketOptions from './socketOptions' 3 | import {find, pull} from 'lodash' 4 | import {Frame, IEndpoint, IListener, Msg} from './types' 5 | import WebSocketListener from './webSocketListener' 6 | import * as http from 'http' 7 | import * as https from 'https' 8 | import WebSocketEndpoint from './webSocketEndpoint' 9 | 10 | class SocketBase extends EventEmitter { 11 | private endpoints: IEndpoint[] = [] 12 | private binds: IListener[] = [] 13 | public readonly options = new SocketOptions() 14 | 15 | constructor() { 16 | super() 17 | this.bindAttachEndpoint = this.bindAttachEndpoint.bind(this) 18 | this.bindEndpointTerminated = this.bindEndpointTerminated.bind(this) 19 | this.attachEndpoint = this.attachEndpoint.bind(this) 20 | this.endpointTerminated = this.endpointTerminated.bind(this) 21 | this.xrecv = this.xrecv.bind(this) 22 | this.hiccuped = this.hiccuped.bind(this) 23 | } 24 | 25 | connect(address: string) { 26 | if (address.startsWith("ws://") || address.startsWith("wss://")) { 27 | const endpoint = new WebSocketEndpoint(address, this.options) 28 | endpoint.on('attach', this.attachEndpoint) 29 | endpoint.on('terminated', this.endpointTerminated) 30 | endpoint.on('message', this.xrecv) 31 | endpoint.on('hiccuped', this.hiccuped) 32 | this.endpoints.push(endpoint) 33 | 34 | if (!this.options.immediate) 35 | this.attachEndpoint(endpoint) 36 | } else { 37 | throw new Error('unsupported transport') 38 | } 39 | } 40 | 41 | disconnect(address: string) { 42 | const endpoint = find(this.endpoints, e => e.address === address) 43 | 44 | if (endpoint) { 45 | endpoint.removeListener('attach', this.attachEndpoint) 46 | endpoint.removeListener('terminated', this.endpointTerminated) 47 | endpoint.removeListener('message', this.xrecv) 48 | endpoint.removeListener('hiccuped', this.hiccuped) 49 | endpoint.close() 50 | pull(this.endpoints, endpoint) 51 | this.endpointTerminated(endpoint) 52 | } 53 | } 54 | 55 | bind(address: string, server?: http.Server | https.Server) { 56 | if (address.startsWith("ws://") || address.startsWith("wss://")) { 57 | const listener = new WebSocketListener(address, server, this.options) 58 | listener.on('attach', this.bindAttachEndpoint) 59 | this.binds.push(listener) 60 | } else { 61 | throw new Error('unsupported transport') 62 | } 63 | } 64 | 65 | bindSync(address: string, server?: http.Server | https.Server) { 66 | return this.bind(address, server) 67 | } 68 | 69 | unbind(address: string) { 70 | const listener = find(this.binds, b => b.address === address) 71 | 72 | if (listener) { 73 | listener.removeListener('attach', this.attachEndpoint) 74 | listener.close() 75 | pull(this.binds, listener) 76 | } 77 | } 78 | 79 | close() { 80 | this.binds.forEach(listener => { 81 | listener.removeListener('attach', this.attachEndpoint) 82 | listener.close() 83 | }) 84 | 85 | this.binds = [] 86 | 87 | this.endpoints.forEach(endpoint => { 88 | endpoint.removeListener('attach', this.attachEndpoint) 89 | endpoint.removeListener('terminated', this.endpointTerminated) 90 | endpoint.removeListener('message', this.xrecv) 91 | endpoint.removeListener('hiccuped', this.hiccuped) 92 | endpoint.close() 93 | pull(this.endpoints, endpoint) 94 | this.endpointTerminated(endpoint) 95 | }) 96 | } 97 | 98 | subscribe(topic: Frame) { 99 | throw new Error('not supported') 100 | } 101 | 102 | unsubscribe(topic: Frame) { 103 | throw new Error('not supported') 104 | } 105 | 106 | private bindAttachEndpoint(endpoint: IEndpoint) { 107 | endpoint.on('terminated', this.bindEndpointTerminated) 108 | endpoint.on('message', this.xrecv) 109 | 110 | this.attachEndpoint(endpoint) 111 | } 112 | 113 | private bindEndpointTerminated(endpoint: IEndpoint) { 114 | endpoint.removeListener('terminated', this.bindEndpointTerminated) 115 | endpoint.removeListener('message', this.xrecv) 116 | 117 | this.endpointTerminated(endpoint) 118 | } 119 | 120 | protected attachEndpoint(endpoint: IEndpoint) { 121 | } 122 | 123 | protected endpointTerminated(endpoint: IEndpoint) { 124 | 125 | } 126 | 127 | protected hiccuped(endpoint: IEndpoint) { 128 | 129 | } 130 | 131 | protected xrecv(endpoint: IEndpoint, ...frames: Buffer[]) { 132 | } 133 | 134 | protected xsend(msg: Msg) { 135 | 136 | } 137 | 138 | send(msg: Msg | Frame) { 139 | if (Array.isArray(msg)) 140 | this.xsend(msg) 141 | else 142 | this.xsend([msg]) 143 | } 144 | } 145 | 146 | export default SocketBase 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jszmq 2 | ====== 3 | 4 | jszmq is port of zeromq to Javascript, supporting both browsers and NodeJS. 5 | The library only support the WebSocket transport ([ZWS 2.0](https://rfc.zeromq.org/spec:45/ZWS/)). 6 | 7 | The API of the library is similar to that of [zeromq.js](https://github.com/zeromq/zeromq.js). 8 | 9 | ## Compatibility with ZeroMQ 10 | 11 | WebSocket transport added to [zeromq](https://github.com/zeromq/libzmq) recently, and it is only available when compiling from source. 12 | 13 | Other ports of zeromq, like NetMQ (C#) and JeroMQ (Java) don't yet support the WebSocket transport. 14 | 15 | ## Compatibility with ZWS 1.0, zwssock, JSMQ and NetMQ.WebSockets 16 | 17 | The library is currently not compatible with ZWS 1.0 and the implementation of it. 18 | 19 | ## Installation 20 | 21 | ``` 22 | npm install --save jszmq 23 | ``` 24 | 25 | ## Supported socket types 26 | 27 | Following socket types are currently supported: 28 | * Pub 29 | * Sub 30 | * XPub 31 | * XSub 32 | * Dealer 33 | * Router 34 | * Req 35 | * Rep 36 | * Push 37 | * Pull 38 | 39 | ## How to use 40 | 41 | Import jszmq with one of the following: 42 | 43 | ```js 44 | import * as zmq from 'jszmq'; 45 | ``` 46 | 47 | ```js 48 | const zmq = require('jszmq'); 49 | ``` 50 | 51 | ### Creating a socket 52 | 53 | To create a socket you can either use the `socket` function, which is compatible with zeromq.js or use the socket type class. 54 | 55 | Socket type class: 56 | 57 | ```js 58 | const dealer = new zmq.Dealer(); 59 | ``` 60 | 61 | with socket function: 62 | ```js 63 | const dealer = zmq.socket('dealer'); 64 | ``` 65 | 66 | ### Bind 67 | 68 | To bind call the `bind` function: 69 | 70 | ```js 71 | const zmq = require('jszmq'); 72 | 73 | const router = new zmq.Router(); 74 | router.bind('ws://localhost:80'); 75 | ``` 76 | 77 | You can also provide an http server and bind multiple sockets on the same port: 78 | 79 | ```js 80 | const http = require('http'); 81 | const zmq = require('jszmq'); 82 | 83 | const server = http.createServer(); 84 | 85 | const rep = new zmq.Rep(); 86 | const pub = new zmq.Pub(); 87 | 88 | rep.bind('ws://localhost:80/reqrep', server); 89 | pub.bind('ws://localhost:80/pubsub', server); 90 | 91 | server.listen(80); 92 | ``` 93 | 94 | `bindSync` function is an alias for bind in order to be compatible with zeromq.js. 95 | 96 | ### Sending 97 | 98 | To send call the send method and provide with either array or a single frame. 99 | Frame can either be Buffer of string, in case of string it would be converted to Buffer with utf8 encoding. 100 | 101 | ```js 102 | socket.send('Hello'); // Single frame 103 | socket.send(['Hello', 'World']); // Multiple frames 104 | socket.send([Buffer.from('Hello', 'utf8')]); // Using Buffer 105 | ``` 106 | 107 | ### Receiving 108 | 109 | Socket emit messages through the on (and once) methods which listen to `message` event. 110 | Each frame is a parameter to the callback function, all frames are always instances of Buffer. 111 | 112 | ```js 113 | socket.on('message', msg => console.log(msg.toString())); // One frame 114 | socket.on('message', (frame1, frame2) => console.log(frame1.toString(), frame2.toString())); // Multiple frames 115 | socket.on('message', (...frames) => frames.forEach(f => console.log(f.toString()))); // All frames as array 116 | ``` 117 | 118 | ## Examples 119 | 120 | ### Push/Pull 121 | 122 | This example demonstrates how a producer pushes information onto a 123 | socket and how a worker pulls information from the socket. 124 | 125 | **producer.js** 126 | 127 | ```js 128 | // producer.js 129 | const zmq = require('jszmq'); // OR import * as zmq form 'jszmq' 130 | const sock = zmq.socket('push'); // OR const sock = new zmq.Push(); 131 | 132 | sock.bind('tcp://127.0.0.1:3000'); 133 | console.log('Producer bound to port 3000'); 134 | 135 | setInterval(function(){ 136 | console.log('sending work'); 137 | sock.send('some work'); 138 | }, 500); 139 | ``` 140 | 141 | **worker.js** 142 | 143 | ```js 144 | // worker.js 145 | const zmq = require('jszmq'); // OR import * as zmq form 'jszmq' 146 | const sock = zmq.socket('pull'); // OR const sock = new zmq.Pull(); 147 | 148 | sock.connect('tcp://127.0.0.1:3000'); 149 | console.log('Worker connected to port 3000'); 150 | 151 | sock.on('message', function(msg) { 152 | console.log('work: %s', msg.toString()); 153 | }); 154 | ``` 155 | 156 | ### Pub/Sub 157 | 158 | This example demonstrates using `jszmq` in a classic Pub/Sub, 159 | Publisher/Subscriber, application. 160 | 161 | **Publisher: pubber.js** 162 | 163 | ```js 164 | // pubber.js 165 | const zmq = require('jszmq'); // OR import * as zmq form 'jszmq' 166 | const sock = zmq.socket('pub'); // OR const sock = new zmq.Pub(); 167 | 168 | sock.bind('tcp://127.0.0.1:3000'); 169 | console.log('Publisher bound to port 3000'); 170 | 171 | setInterval(function() { 172 | console.log('sending a multipart message envelope'); 173 | sock.send(['kitty cats', 'meow!']); 174 | }, 500); 175 | ``` 176 | 177 | **Subscriber: subber.js** 178 | 179 | ```js 180 | // subber.js 181 | const zmq = require('jszmq'); // OR import * as zmq form 'jszmq' 182 | const sock = zmq.socket('sub'); // OR const sock = new zmq.Sub(); 183 | 184 | sock.connect('tcp://127.0.0.1:3000'); 185 | sock.subscribe('kitty cats'); 186 | console.log('Subscriber connected to port 3000'); 187 | 188 | sock.on('message', function(topic, message) { 189 | console.log('received a message related to:', topic.toString(), 'containing message:', message.toString()); 190 | }); 191 | ``` 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/webSocketEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import * as WebSocket from 'isomorphic-ws' 3 | import {Buffer} from 'buffer' 4 | import SocketOptions from './socketOptions' 5 | import {isString} from 'lodash' 6 | import {IEndpoint, Msg} from './types' 7 | 8 | enum State { 9 | Closed, 10 | Connecting, 11 | Reconnecting, 12 | Active 13 | } 14 | 15 | export default class WebSocketEndpoint extends EventEmitter implements IEndpoint { 16 | socket!: WebSocket; 17 | state: State 18 | frames:Buffer[] = [] 19 | queue:Buffer[] = [] 20 | options:SocketOptions 21 | routingIdReceived = false 22 | accepted:boolean 23 | public routingKey:Buffer = Buffer.alloc(0) 24 | public routingKeyString = '' 25 | public address:string 26 | 27 | constructor(address:string|WebSocket, options:SocketOptions) { 28 | super() 29 | this.options = options 30 | this.connect = this.connect.bind(this) 31 | 32 | if (isString(address)) { 33 | this.address = address 34 | this.state = State.Connecting 35 | this.accepted = false 36 | 37 | this.connect() 38 | } else { 39 | this.routingIdReceived = false 40 | this.address = '' 41 | this.socket = address 42 | this.accepted = true 43 | this.state = State.Active 44 | this.socket.binaryType = "arraybuffer" 45 | this.socket.onclose = this.onClose.bind(this) 46 | this.socket.onmessage = this.onMessage.bind(this) 47 | this.send([this.options.routingId]) 48 | } 49 | } 50 | 51 | private connect() { 52 | if (this.state === State.Closed) 53 | return // The socket was already closed, abort 54 | 55 | this.routingIdReceived = false 56 | this.socket = new WebSocket(this.address, ['ZWS2.0']) 57 | this.socket.binaryType = "arraybuffer" 58 | this.socket.onopen = this.onOpen.bind(this) 59 | this.socket.onclose = this.onClose.bind(this) 60 | this.socket.onmessage = this.onMessage.bind(this) 61 | } 62 | 63 | onOpen() { 64 | const oldState = this.state 65 | this.state = State.Active 66 | 67 | this.send([this.options.routingId]) 68 | this.queue.forEach(frame => this.socket.send(frame)) 69 | this.queue = [] 70 | 71 | if (this.options.immediate) 72 | this.emit('attach', this) 73 | else if (oldState === State.Reconnecting) 74 | this.emit('hiccuped', this) 75 | } 76 | 77 | onClose() { 78 | if (this.accepted) { 79 | this.state = State.Closed 80 | this.emit('terminated', this) 81 | } 82 | else if (this.state !== State.Closed) { 83 | if ((this.state === State.Active || this.state === State.Connecting) && this.options.immediate) 84 | this.emit('terminated', this) 85 | 86 | if (this.state === State.Active) 87 | this.state = State.Reconnecting 88 | 89 | setTimeout(this.connect, this.options.reconnectInterval) 90 | } 91 | } 92 | 93 | error() { 94 | this.socket.close() 95 | } 96 | 97 | onMessage(message:ArrayBuffer|any) { 98 | if (!this.routingIdReceived) { 99 | this.routingIdReceived = true 100 | 101 | if (!this.options.recvRoutingId) 102 | return 103 | } 104 | 105 | if (message.data instanceof ArrayBuffer) { 106 | const buffer = Buffer.from(message.data) 107 | 108 | if (buffer.length > 0) { 109 | const more = buffer.readUInt8(0) === 1 110 | const msg = buffer.slice(1) 111 | 112 | this.frames.push(msg) 113 | 114 | if (!more) { 115 | this.emit("message", this, ...this.frames) 116 | this.frames = [] 117 | } 118 | } 119 | else 120 | this.error() 121 | } 122 | else 123 | this.error() 124 | } 125 | 126 | close() { 127 | if (this.state !== State.Closed) { 128 | this.state = State.Closed 129 | 130 | if (this.socket.readyState === this.socket.CONNECTING || this.socket.readyState === this.socket.OPEN) 131 | this.socket.close() 132 | 133 | this.emit('terminated', this) 134 | } 135 | } 136 | 137 | send(msg:Msg) { 138 | if (this.state === State.Closed) 139 | return false 140 | 141 | for (let i = 0, len = msg.length; i < len; i++) { 142 | const isLast = i === len - 1 143 | const flags = isLast ? 0 : 1 144 | 145 | let frame = msg[i] 146 | 147 | if (isString(frame)) 148 | frame = Buffer.from(frame, 'utf8') 149 | else if (frame instanceof ArrayBuffer || frame instanceof Buffer) { 150 | // Nothing to do, use as is 151 | } else { 152 | throw new Error('invalid message type') 153 | } 154 | 155 | const flagsArray = Buffer.alloc(1) 156 | flagsArray.writeUInt8(flags, 0) 157 | const buffer = Buffer.concat([flagsArray, frame]) 158 | 159 | if (this.state === State.Active) 160 | this.socket.send(buffer) 161 | else 162 | this.queue.push(buffer) 163 | } 164 | 165 | return true 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/utils/trie.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import {Buffer} from 'buffer' 3 | import {resize} from './array' 4 | 5 | type ForeachCallback = (buffer:Buffer) => void 6 | 7 | export default class Trie { 8 | referenceCount:number 9 | minCharacter:number 10 | count:number 11 | liveNodes:number 12 | next:Array 13 | 14 | constructor() { 15 | this.referenceCount = 0 16 | this.minCharacter = 0 17 | this.count = 0 18 | this.liveNodes = 0 19 | this.next = [] 20 | } 21 | 22 | get isRedundant() : boolean { 23 | return this.referenceCount === 0 && this.liveNodes === 0 24 | } 25 | 26 | add(prefix:Buffer, start:number, size:number) : boolean { 27 | // We are at the node corresponding to the prefix. We are done. 28 | if (size === 0) { 29 | this.referenceCount++ 30 | return this.referenceCount === 1 31 | } 32 | 33 | const currentCharacter = prefix.readUInt8(start) 34 | if (currentCharacter < this.minCharacter || currentCharacter >= this.minCharacter + this.count) { 35 | // The character is out of range of currently handled 36 | // characters. We have to extend the table. 37 | if (this.count === 0) { 38 | this.minCharacter = currentCharacter 39 | this.count = 1 40 | this.next = Array(1).fill(null) 41 | } else if (this.count === 1) { 42 | const oldc = this.minCharacter 43 | const oldp = this.next[0] 44 | 45 | this.count = (this.minCharacter < currentCharacter ? currentCharacter - this.minCharacter : this.minCharacter - currentCharacter) + 1 46 | this.next = Array(this.count).fill(null) 47 | this.minCharacter = Math.min(this.minCharacter, currentCharacter) 48 | this.next[oldc - this.minCharacter] = oldp 49 | } else if (this.minCharacter < currentCharacter) { 50 | // The new character is above the current character range. 51 | this.count = currentCharacter - this.minCharacter + 1 52 | this.next = resize(this.next, this.count, true) 53 | } else { 54 | // The new character is below the current character range. 55 | this.count = (this.minCharacter + this.count) - currentCharacter 56 | this.next = resize(this.next, this.count, false) 57 | this.minCharacter = currentCharacter 58 | } 59 | } 60 | 61 | if (this.next[currentCharacter - this.minCharacter] === null) { 62 | this.next[currentCharacter - this.minCharacter] = new Trie() 63 | this.liveNodes++ 64 | } 65 | 66 | // @ts-ignore 67 | return this.next[currentCharacter - this.minCharacter].add(prefix, start + 1, size - 1) 68 | } 69 | 70 | 71 | remove(prefix:Buffer, start:number, size:number) : boolean { 72 | if (size === 0) { 73 | if (this.referenceCount === 0) 74 | return false 75 | this.referenceCount-- 76 | return this.referenceCount === 0 77 | } 78 | 79 | const currentCharacter = prefix.readUInt8(start) 80 | if (this.count == 0 || currentCharacter < this.minCharacter || currentCharacter >= this.minCharacter + this.count) 81 | return false 82 | 83 | const nextNode = this.count == 1 ? this.next[0] : this.next[currentCharacter - this.minCharacter] 84 | 85 | if (nextNode === null) 86 | return false; 87 | 88 | const wasRemoved = nextNode.remove(prefix, start + 1, size - 1) 89 | 90 | if (nextNode.isRedundant) { 91 | if (this.count === 1) { 92 | this.next = [] 93 | this.count = 0 94 | this.liveNodes-- 95 | assert(this.liveNodes == 0) 96 | } else { 97 | this.next[currentCharacter - this.minCharacter] = null 98 | assert(this.liveNodes > 1) 99 | this.liveNodes-- 100 | 101 | if (currentCharacter == this.minCharacter) { 102 | // We can compact the table "from the left" 103 | let newMin = this.minCharacter 104 | for (let i = 1; i < this.count; ++i) { 105 | if (this.next[i] !== null) 106 | { 107 | newMin = i + this.minCharacter 108 | break 109 | } 110 | } 111 | assert(newMin != this.minCharacter) 112 | assert(newMin > this.minCharacter) 113 | assert(this.count > newMin - this.minCharacter) 114 | 115 | this.count = this.count - (newMin - this.minCharacter) 116 | this.next = resize(this.next, this.count, false); 117 | 118 | this.minCharacter = newMin 119 | } else if (currentCharacter == this.minCharacter + this.count - 1) { 120 | // We can compact the table "from the right" 121 | let newCount = this.count 122 | for (let i = 1; i < this.count; i++) { 123 | if (this.next[this.count - 1 - i] != null) { 124 | newCount = this.count - i 125 | break 126 | } 127 | } 128 | assert(newCount != this.count) 129 | this.count = newCount 130 | 131 | this.next = resize(this.next, this.count, true) 132 | } 133 | } 134 | } 135 | 136 | return wasRemoved 137 | } 138 | 139 | public check(data:Buffer, offset:number, size:number) : boolean { 140 | // This function is on critical path. It deliberately doesn't use 141 | // recursion to get a bit better performance. 142 | let current = this 143 | let start = offset 144 | while (true) { 145 | // We've found a corresponding subscription! 146 | if (current.referenceCount > 0) 147 | return true 148 | 149 | // We've checked all the data and haven't found matching subscription. 150 | if (size === 0) 151 | return false 152 | 153 | // If there's no corresponding slot for the first character 154 | // of the prefix, the message does not match. 155 | const character = data.readUInt8(start) 156 | if (character < current.minCharacter || character >= current.minCharacter + current.count) 157 | return false 158 | 159 | // Move to the next character. 160 | if (current.count === 1) { 161 | // @ts-ignore 162 | current = current.next[0] 163 | } else { 164 | // @ts-ignore 165 | current = current.next[character - current.minCharacter] 166 | 167 | if (current === null) 168 | return false 169 | } 170 | start++ 171 | size-- 172 | } 173 | } 174 | 175 | // Apply the function supplied to each subscription in the trie. 176 | forEach(func: ForeachCallback) { 177 | this.forEachHelper(Buffer.alloc(0), 0, 0, func); 178 | } 179 | 180 | forEachHelper(buffer:Buffer, bufferSize:number, maxBufferSize:number, func:ForeachCallback) { 181 | // If this node is a subscription, apply the function. 182 | if (this.referenceCount > 0) 183 | func(buffer.slice(0, bufferSize)) 184 | 185 | // Adjust the buffer. 186 | if (bufferSize >= maxBufferSize) { 187 | maxBufferSize = bufferSize + 256 188 | const newBuffer = Buffer.alloc(maxBufferSize, 0) 189 | buffer.copy(newBuffer) 190 | buffer = newBuffer 191 | } 192 | 193 | // If there are no subnodes in the trie, return. 194 | if (this.count === 0) 195 | return; 196 | 197 | // If there's one subnode (optimisation). 198 | if (this.count === 1) { 199 | buffer[bufferSize] = this.minCharacter 200 | bufferSize++ 201 | 202 | // @ts-ignore 203 | this.next[0].forEachHelper(buffer, bufferSize, maxBufferSize, func) 204 | return 205 | } 206 | 207 | // If there are multiple subnodes. 208 | for (let c = 0; c != this.count; c++) { 209 | buffer.writeUInt8(this.minCharacter + c, bufferSize) 210 | if (this.next[c] != null) { 211 | // @ts-ignore 212 | this.next[c].forEachHelper(buffer, bufferSize + 1, maxBufferSize, func) 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/utils/multiTrie.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import {IEndpoint} from '../types' 3 | import {Buffer} from 'buffer' 4 | import {isUndefined} from 'lodash' 5 | import {copy, resize} from './array' 6 | 7 | type RemovedCallback = (endpoint:IEndpoint, buffer:Buffer, bufferSize:number) => void 8 | type MatchCallback = (endpoint:IEndpoint) => void 9 | 10 | export default class MultiTrie { 11 | endpoints?: Set 12 | minCharacter: number 13 | count:number 14 | liveNodes: number 15 | next: Array 16 | 17 | constructor() { 18 | this.minCharacter = 0 19 | this.count = 0 20 | this.liveNodes = 0 21 | this.next = [] 22 | } 23 | 24 | get isRedundant() : boolean { 25 | return isUndefined(this.endpoints) && this.liveNodes === 0 26 | } 27 | 28 | add(prefix:Buffer, start:number, size:number, endpoint:IEndpoint) : boolean { 29 | return this.addHelper(prefix, start, size, endpoint) 30 | } 31 | 32 | private addHelper(prefix:Buffer, start:number, size:number, endpoint:IEndpoint) : boolean { 33 | // We are at the node corresponding to the prefix. We are done. 34 | if (size === 0) { 35 | let result = isUndefined(this.endpoints) 36 | 37 | if (isUndefined(this.endpoints)) 38 | this.endpoints = new Set() 39 | 40 | this.endpoints.add(endpoint) 41 | 42 | return result 43 | } 44 | 45 | const currentCharacter = prefix.readUInt8(start) 46 | 47 | if (currentCharacter < this.minCharacter || currentCharacter >= this.minCharacter + this.count) { 48 | // The character is out of range of currently handled 49 | // characters. We have to extend the table. 50 | if (this.count === 0) { 51 | this.minCharacter = currentCharacter 52 | this.count = 1 53 | this.next = Array(1).fill(null) 54 | } 55 | else if (this.count === 1) { 56 | let oldc = this.minCharacter 57 | const oldp = this.next[0] 58 | this.count = (this.minCharacter < currentCharacter ? currentCharacter - this.minCharacter : this.minCharacter - currentCharacter) + 1 59 | this.next = Array(this.count).fill(null) 60 | this.minCharacter = Math.min(this.minCharacter, currentCharacter) 61 | this.next[oldc - this.minCharacter] = oldp 62 | } 63 | else if (this.minCharacter < currentCharacter) { 64 | // The new character is above the current character range. 65 | this.count = currentCharacter - this.minCharacter + 1 66 | this.next = resize(this.next, this.count, true) 67 | } 68 | else { 69 | // The new character is below the current character range. 70 | this.count = (this.minCharacter + this.count) - currentCharacter 71 | this.next = resize(this.next, this.count, false) 72 | this.minCharacter = currentCharacter 73 | } 74 | } 75 | 76 | if (this.next[currentCharacter - this.minCharacter] === null) { 77 | this.next[currentCharacter - this.minCharacter] = new MultiTrie() 78 | this.liveNodes++ 79 | } 80 | 81 | // @ts-ignore 82 | return this.next[currentCharacter - this.minCharacter].addHelper(prefix, start + 1, size - 1, endpoint) 83 | } 84 | 85 | public removeEndpoint(endpoint:IEndpoint, func:RemovedCallback) { 86 | return this.removeEndpointHelper(endpoint, Buffer.alloc(0), 0, 0, func) 87 | } 88 | 89 | private removeEndpointHelper(endpoint:IEndpoint, buffer:Buffer, bufferSize:number, maxBufferSize:number, func:RemovedCallback) : boolean { 90 | // Remove the subscription from this node. 91 | if (this.endpoints && this.endpoints.delete(endpoint) && this.endpoints.size === 0) { 92 | func(endpoint, buffer, bufferSize) 93 | this.endpoints = undefined 94 | } 95 | 96 | // Adjust the buffer. 97 | if (bufferSize >= maxBufferSize) { 98 | maxBufferSize = bufferSize + 256 99 | const newBuffer = Buffer.alloc(maxBufferSize, 0) 100 | buffer.copy(newBuffer) 101 | buffer = newBuffer 102 | } 103 | 104 | // If there are no subnodes in the trie, return. 105 | if (this.count === 0) 106 | return true 107 | 108 | // If there's one subnode (optimisation). 109 | if (this.count === 1) { 110 | buffer.writeUInt8(this.minCharacter, bufferSize) 111 | bufferSize++ 112 | // @ts-ignore 113 | this.next[0].removeEndpointHelper(endpoint, buffer, bufferSize, maxBufferSize, func); 114 | 115 | // Prune the node if it was made redundant by the removal 116 | // @ts-ignore 117 | if (this.next[0].isRedundant) { 118 | this.next = [] 119 | this.count = 0 120 | this.liveNodes-- 121 | } 122 | 123 | return true 124 | } 125 | 126 | // If there are multiple subnodes. 127 | 128 | // New min non-null character in the node table after the removal 129 | let newMin = this.minCharacter + this.count - 1 130 | 131 | // New max non-null character in the node table after the removal 132 | let newMax = this.minCharacter 133 | 134 | for (let currentCharacter = 0; currentCharacter != this.count; currentCharacter++) { 135 | buffer.writeUInt8(this.minCharacter + currentCharacter, bufferSize) 136 | 137 | const next = this.next[currentCharacter] 138 | if (next) { 139 | next.removeEndpointHelper(endpoint, buffer, bufferSize + 1, maxBufferSize, func) 140 | 141 | // Prune redundant nodes from the mtrie 142 | if (next.isRedundant) { 143 | this.next[currentCharacter] = null 144 | this.liveNodes-- 145 | } 146 | else { 147 | // The node is not redundant, so it's a candidate for being 148 | // the new min/max node. 149 | // 150 | // We loop through the node array from left to right, so the 151 | // first non-null, non-redundant node encountered is the new 152 | // minimum index. Conversely, the last non-redundant, non-null 153 | // node encountered is the new maximum index. 154 | if (currentCharacter + this.minCharacter < newMin) 155 | newMin = currentCharacter + this.minCharacter 156 | 157 | if (currentCharacter + this.minCharacter > newMax) 158 | newMax = currentCharacter + this.minCharacter 159 | } 160 | } 161 | } 162 | 163 | // Free the node table if it's no longer used. 164 | if (this.liveNodes === 0) { 165 | this.next = [] 166 | this.count = 0 167 | } 168 | // Compact the node table if possible 169 | else if (this.liveNodes === 1) { 170 | // If there's only one live node in the table we can 171 | // switch to using the more compact single-node 172 | // representation 173 | const node = this.next[newMin - this.minCharacter] 174 | assert(node) 175 | this.next = [ node ] 176 | this.count = 1 177 | this.minCharacter = newMin 178 | } 179 | else if (this.liveNodes > 1 && (newMin > this.minCharacter || newMax < this.minCharacter + this.count - 1)) 180 | { 181 | let oldTable = this.next 182 | this.count = newMax - newMin + 1 183 | this.next = Array(this.count).fill(null) 184 | 185 | copy(oldTable, (newMin - this.minCharacter), this.next, 0, this.count) 186 | 187 | this.minCharacter = newMin 188 | } 189 | return true; 190 | } 191 | 192 | public remove(prefix:Buffer, start:number, size:number, endpoint:IEndpoint) : boolean { 193 | if (size === 0) { 194 | if (this.endpoints) { 195 | const erased = this.endpoints.delete(endpoint) 196 | assert(erased) 197 | if (this.endpoints.size === 0) 198 | this.endpoints = undefined 199 | } 200 | return !this.endpoints 201 | } 202 | 203 | const currentCharacter = prefix.readUInt8(start) 204 | if (this.count == 0 || currentCharacter < this.minCharacter || currentCharacter >= this.minCharacter + this.count) 205 | return false; 206 | 207 | const nextNode = this.count == 1 ? this.next[0] : this.next[currentCharacter - this.minCharacter] 208 | if (nextNode === null) 209 | return false; 210 | 211 | let ret = nextNode.remove(prefix, start + 1, size - 1, endpoint) 212 | if (nextNode.isRedundant) 213 | { 214 | assert(this.count > 0) 215 | 216 | if (this.count === 1) { 217 | this.next = [] 218 | this.count = 0 219 | this.liveNodes-- 220 | } 221 | else 222 | { 223 | this.next[currentCharacter - this.minCharacter] = null 224 | this.liveNodes-- 225 | 226 | // Compact the table if possible 227 | if (this.liveNodes === 1) { 228 | // If there's only one live node in the table we can 229 | // switch to using the more compact single-node 230 | // representation 231 | let i = 0 232 | for (; i < this.count; i++) { 233 | if (this.next[i] !== null) 234 | break; 235 | } 236 | 237 | this.minCharacter += i 238 | this.count = 1 239 | const old = this.next[i] 240 | this.next = [ old ] 241 | } else if (currentCharacter == this.minCharacter) { 242 | // We can compact the table "from the left" 243 | let i = 1; 244 | for (;i < this.count; i++) { 245 | if (this.next[i] !== null) 246 | break; 247 | } 248 | 249 | this.minCharacter += i 250 | this.count -= i 251 | this.next = resize(this.next, this.count, false) 252 | } else if (currentCharacter === this.minCharacter + this.count - 1) { 253 | // We can compact the table "from the right" 254 | let i = 1; 255 | for (;i < this.count; i++) { 256 | if (this.next[this.count - 1 - i] !== null) 257 | break; 258 | } 259 | 260 | this.count -= i 261 | this.next = resize(this.next, this.count, true) 262 | } 263 | } 264 | } 265 | 266 | return ret; 267 | } 268 | 269 | match(data:Buffer, offset:number, size:number, func:MatchCallback) 270 | { 271 | let current = this 272 | let index = offset 273 | 274 | while (true) { 275 | // Signal the pipes attached to this node. 276 | if (current.endpoints) 277 | current.endpoints.forEach(e => func(e)) 278 | 279 | // If we are at the end of the message, there's nothing more to match. 280 | if (size === 0) 281 | break; 282 | 283 | // If there are no subnodes in the trie, return. 284 | if (current.count === 0) 285 | break; 286 | 287 | const c = data.readUInt8(index) 288 | // If there's one subnode (optimisation). 289 | if (current.count === 1) { 290 | if (c != current.minCharacter) 291 | break; 292 | // @ts-ignore 293 | current = current.next[0] 294 | index++ 295 | size-- 296 | continue 297 | } 298 | 299 | // If there are multiple subnodes. 300 | if (c < current.minCharacter || c >= current.minCharacter + current.count) 301 | break; 302 | if (current.next[c - current.minCharacter] === null) 303 | break; 304 | // @ts-ignore 305 | current = current.next[c - current.minCharacter] 306 | index++ 307 | size-- 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------