diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..545b4a9e --- /dev/null +++ b/.clang-format @@ -0,0 +1,68 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -4 +AlignAfterOpenBracket: false +AlignConsecutiveAssignments: false +AlignEscapedNewlinesLeft: false +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +ColumnLimit: 90 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +IndentCaseLabels: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 4 +UseTab: Never +... + diff --git a/.editorconfig b/.editorconfig index fd99cd74..998f8d5a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,14 @@ root = true [*] -end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true - -[*.js] +end_of_line = lf indent_style = space -indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true -[*.cc] -indent_style = space +[*.{js,ts}] indent_size = 2 +[*.{h,cc}] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..fcadb2cf --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/.gitignore b/.gitignore index 0ee05d7f..80f9ba04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,13 @@ -*.swp -*.swo -*.o build -*.lock* -binding.node -examples/stress-test-client -node_modules -Makefile.gyp -binding.Makefile -binding.target.gyp.mk -npm-debug.log -gyp-mac-tool -out/ -zmq docs - -npm-debug.log -prebuilds -zmq-build.log -windows/lib/libzmq.lib - -# Coverage directory used by tools like istanbul -coverage -.nyc_output -coverage.lcov - +lib +tmp +node_modules +yarn.lock +yarn-error.log package-lock.json +lib/binary/*.node +lib/binary/napi-v*/*.node +build-tmp-napi-v* +prebuilds +test.js diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 43c97e71..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/.travis.yml b/.travis.yml index 0a5eae2f..ee2c3924 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,68 +1,157 @@ language: node_js -node_js: -- "6" -- "8" -- "10" -- "11" -sudo: required - -matrix: +cache: yarn +dist: bionic + +jobs: include: - - os: linux - dist: trusty - env: - - DEPLOY="true" - - MAIN="true" - - os: linux - dist: trusty - env: - - DEPLOY="true" - - ARCH="armv7" - - os: linux - dist: trusty - env: - - DEPLOY="true" - - ARCH="armv8" - - os: linux - dist: trusty - env: - - ELECTRON="3.0.0" - - os: osx - env: - - DEPLOY="true" + ## TEST STAGE -env: - global: - - GH_REF: github.com/nteract/zmq-prebuilt.git - - secure: AMY9chDNyNawxPhxAksCREhbQoOmphQKo2hDuYIPXBCUNF6Zik969QlB07TVjo+Bo0KVEbG4ANTdI0FkFIhg4MuJIWUkW30ZDyCSovUx8zyWB/7IN23A4Cx4p4ZnKjezTHUQpZ9InRIZzy9kOR+215IIBP6GfxbUY89eDA6ARElcv1rwDjZqSnLAxfie6v80vIbhWxNK4kQjiEyIez5eWBjRg4//dmb63TvJjc78GFTaqmh6lRIApAyA0uKka14XR/PJw5IpDR+0Q2omHl4t4zbu9MdVNYMSD+c6iNxOq3fqZJwnyJ1ELErbtR8oeKgZX1SOew5QTCUZWQsa+ySYTCzUnhWjhCgN6W0ciVtT2PslS/ZcvnrGGIxJAOmTZwINw8sAp8yYdY9QCJOGdB5ah2oUBLGKElcFGqSblohXUEM1tXeRchmpXiiAxnPNtxc6lkZzz6NxDl2OFXSmjMS4Cr8cA//Bn6gM04xRJyETy86/8KDoL+ENc7gkeiuYUOFqkAn5X8PrqPdsPmF7yA61IF1ru/OgtiuCh+eqnzZKBPImJo2KfUIqVuRi/6eUaDfWLT7d/J84Vbe3p2nPxoFd2pSw60f/lIkzFSRZx3vBjQiW2MVUN7ZTdawMOzE4TpIV7rDOkKe7qJJobquJEjd2X9Z+8JiTYXK2pPN2GceiqAM= + # Test main OSes on Node 10.x branch. + - os: linux + node_js: "10.16" + env: ZMQ_DRAFT=true INCLUDE_COMPAT_TESTS=true + + - os: linux + env: ALPINE_CHROOT=3.10 ZMQ_DRAFT=true INCLUDE_COMPAT_TESTS=true + sudo: required + + - os: osx + env: ZMQ_DRAFT=true + node_js: "10.16" + + - os: windows + node_js: "10.16" + # https://travis-ci.community/t/build-doesnt-finish-after-completing-tests/288 + env: ZMQ_DRAFT=true YARN_GPG=no + + - os: windows + node_js: "10.16/x86" + # https://travis-ci.community/t/build-doesnt-finish-after-completing-tests/288 + env: ZMQ_DRAFT=true YARN_GPG=no + + # Test shared libraries on Linux and macOS. + - os: linux + node_js: "10.16" + env: ZMQ_SHARED=true + addons: {apt: {packages: libzmq3-dev}} + + - os: osx + node_js: "10.16" + env: ZMQ_SHARED=true + addons: {homebrew: {packages: zeromq, update: true}} + + # Test older versions of ZMQ. + - os: linux + node_js: "10.16" + env: ZMQ_VERSION=4.2.4 + + # Test recent Node versions. + - os: linux + node_js: "12" + env: ZMQ_DRAFT=true INCLUDE_COMPAT_TESTS=true + + - os: linux + node_js: "13" + env: ZMQ_DRAFT=true + + ## PREBUILD STAGE + + - stage: prebuild + os: linux + env: ARCHIVE_SUFFIX=-x64 + node_js: "10.16" + script: script/ci/prebuild.sh + + - stage: prebuild + os: linux + env: ALPINE_CHROOT=3.10 ARCHIVE_SUFFIX=-x64-musl + sudo: required + script: script/ci/prebuild.sh -before_install: - - export DISPLAY=':99.0' - - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - - if [ -n "$ELECTRON" ]; then npm install electron@${ELECTRON}; fi + - stage: prebuild + os: linux + node_js: "10.16" + env: ARCH=arm TRIPLE=arm-linux-gnueabihf GCC=8 ARCHIVE_SUFFIX=-armv7 + addons: {apt: {packages: [gcc-8-arm-linux-gnueabihf, g++-8-arm-linux-gnueabihf]}} + script: script/ci/prebuild.sh + + - stage: prebuild + os: linux + node_js: "10.16" + env: ARCH=arm64 TRIPLE=aarch64-linux-gnu GCC=8 ARCHIVE_SUFFIX=-armv8 + addons: {apt: {packages: [gcc-8-aarch64-linux-gnu, g++-8-aarch64-linux-gnu]}} + script: script/ci/prebuild.sh + + - stage: prebuild + os: osx + node_js: "10.16" + script: script/ci/prebuild.sh + + - stage: prebuild + os: windows + node_js: "10.16" + # https://travis-ci.community/t/build-doesnt-finish-after-completing-tests/288 + env: YARN_GPG=no ARCHIVE_SUFFIX=-x64 + script: script/ci/prebuild.sh + + - stage: prebuild + os: windows + node_js: "10.16/x86" + # https://travis-ci.community/t/build-doesnt-finish-after-completing-tests/288 + env: YARN_GPG=no ARCHIVE_SUFFIX=-x86 + script: script/ci/prebuild.sh + + ## PUBLISH STAGE + + - stage: publish + os: linux + node_js: "10.16" + env: IGNORE_SCRIPTS=true + script: script/ci/package.sh + + fast_finish: true + allow_failures: + - os: linux + node_js: "13" + +stages: +- name: test +- name: prebuild + if: tag IS present +- name: publish + if: tag IS present install: - - if [ -n "$ARCH" ]; then bash ./scripts/cross_compile.sh ${ARCH}; fi - - if [ -n "$ELECTRON" ]; then npm install --runtime=electron --target=${ELECTRON} --disturl=https://atom.io/download/electron; fi - - if [ -z "$ARCH" ] && [ -z "$ELECTRON" ]; then npm install; fi +- travis_retry script/ci/install.sh script: - - if [ -n "$ELECTRON" ]; then travis_retry npm run test:electron; fi - - if [ -z "$ARCH" ] && [ -z "$ELECTRON" ]; then travis_retry npm run coverage; fi - -after_success: - - if [ "$MAIN" = "true" ]; then bash <(curl -s https://codecov.io/bash); fi - - if [ "$MAIN" = "true" ]; then bash ./scripts/publish_docs.sh; fi +- travis_retry script/ci/test.sh deploy: - provider: script +# Deploy prebuilds to Github releases. +- provider: releases + draft: false + file: "${TRAVIS_TAG:-latest}-${TRAVIS_OS_NAME}${ARCHIVE_SUFFIX}.tar.gz" skip_cleanup: true - script: - - if [ -z "$ARCH" ]; then ./node_modules/prebuild/bin.js --all --strip -u ${GH_TOKEN}; else bash ./scripts/cross_compile.sh ${ARCH} ${GH_TOKEN}; fi on: - condition: $DEPLOY = "true" tags: true + condition: "$TRAVIS_BUILD_STAGE_NAME = Prebuild" + api_key: + # Github API token for @zeromqjs-integration + secure: LGumSW+o+hjoYeYTR0HPSkeonjipv5jdflV0AjzIkgQpZAYQv7tOtKBZDbHTdn1kK/P6Kgu3MpzmOsOD/Wr8tdUEK23u1IgL4oUi+sWxqIz0qpOXRnNo6flxZ71LWOtdRJsx9tM+sGKIcHwgcAzvKLeaILheBFxRyYWajluRvCzGfzpcgM5fBxl/lyhJ6KC5xQYbMVZJSHH0frUeSzs3FIlW2NPYBeRk0BWpsmxcLaNGZt2rPEg2RZszRSk7me+i8Z3QmhcbFwugT1LmnRqnHFG3xt4kndwEMUPmncdmyCO//V+cBHCjkt1T3Y06o6BoGC5hSjZvu5E2msAOlfx3iB9ZBxuVE2av2MEwdGh16sOg7wUYJN/OBHNIn8aevvDyWBfCwxqXCUV+Y4UzDqL01JePjYHTpaUN5v4dD+Pw6g6rUj2B4ImMb54Uum8NikcqODjii1iJqF08wZdrentTOz85eLLclRY3qu5I0dR50DPfZt1sXVgIzBt/jCDADLUlIFmvwFCRn3/wp+//xNFZMFy0LyaxDuVfKnje4lcHSo3/ueIBYvyKh4UwWs2WZwt34rzaABAKzSy+/B2d8cFRo+ov0jiwdIdxIexYAI0GjoA3+gOq6GnscSIxEwtH440Lzu+Deq5A602OZnXY1rnlaxcVG+V2ldmxY6EGOwMdZxc= -notifications: - slack: - secure: nklWvK1Mvoj5zly8PO0WmgQdakaSa5Ixwbxw4+1yyIKgyEUbyHzu0oySMAuyMPPV3nLaTaB/bwFxvrsUP6lxoO1CvQMUTrucLm3H5FXDDuTBBw5/tREFyuUy6XPeVsTPdL9ltrvCn8mTBYNZOM/3BWq6mV0XDHpxAIgE3FA2AUTrNoAxLqXC/V7ZmTnDYuH2T/cdeyP+Ri0UDuszUn/dzTM/fOSVpJ/J74JylJ5FH7x5z9lnD6T/916t8//i4tzoEaFkS4V7QgAj0Bta8BEMP0BN40Wap9UgXfrzafCnHDjcbKRoyVEs09kWMbrdY572EGiow2B+QOlYELLYCcD00VH71zDCpw1Ji6di3v+CxCzxFcNyZz8i/Hb8PIfYTyHV2C43X/doVzC3MVllw+d3IVTR0smn2jwjhr+szm9MDQcnUxq5zuOCiAMzE51hbhLQATUNQIiqsD0X+NK3o5urb/Fjb6I6q3MO7GJkuvsda1ni5OEPeFAlYKjvgBiwUZnc32uBHfd3iGzbdwzihSKreuIdVTCtmlnhVmM0ztg5E+BQ8SxWJPxLw5c+9ZaEja5bVCkZF4L3P/EcsdBQ1YmorsROgf/p+PsrnqKAy9zU/NynQQYODx28IPcuQgJbhzeTUEINnFej513BRnkREgjLY5bjXY6iLjKdlRo8kPVqrj8= +# Deploy package including all generated prebuilds to NPM. +- provider: npm + email: r.w.timmermans@gmail.com + skip_cleanup: true + on: + tags: true + condition: "$TRAVIS_BUILD_STAGE_NAME = Publish" + api_key: + # NPM API token for @zeromqjs-integration + secure: sJLxdZXXP0pbUEt2PQkJfraPHqG0jrpoesdVDw189KQvOyeLxRMzuF+BpxGhm6wkrOVF+H4AMVbpTC52yZoHZBLMLUNcHhXjMeztxVsuCe54NLDWSZQ9xGzrZh8npZZCykjdkb+Ax0Xf8wNRcBPZy7KNCoA/JD/qkRyzMjIOQLefxdxAQUcFdYh4sWWc6GqJzxVjX7/Q6jh9l+5Ky+ayLypGaZRirSrsVCsrGhhebPCx9RVQrzopips5fC8UJRF+t3oWRmsrqlsh4FAlo6PPRVzraJL8b92krHrea1wtIPJbTH66sWxzvCw3O8TyiQIkTQwCKVSTmJ4UOVsKchIwWABdno/7DS/5kQyvP/f8yuQ+/kburuRxeJWkTuOz08EAQBviGR5VxVbffPBQyakfEhO0Aw9BC4yGZxTx4mXv2JqSAZRuDS4tntAfpVbkBUZvIDMqGAviMlM9JgTyvoEYmRJAUA33vjaKTa2kJ/bls/lSej6Hl/f5BJ7PHHzofmqi4PPdqRVHX61zrm/TaG5OfIO8wS2cTGFwTWOHcn9BCYzkEm8z+xJ9Kz4dRpKCQAKDCr/FXRAGSyWtA7Wk6T9hdx67wKHOXTRaoiT+V1Ci9xPAUFURJKl/Cef7VOMT1imnx0o3VKlX2md/Up9oSi0tuXHrz+Vol/D+3hMEgVsbub8= + +env: + global: + # GITHUB_TOKEN= + secure: C4S4sfX6e3VubfxkeM1LA3qbKM1BnhnCLVOgCHiHYeRpnVROMQmXqxo9S0cy+M0oj35jdaGWaCjJjbdpE2ofc/9MMEGeBsT91Gz3cE9HAmwDsp4XD12fxjrK0tTr6Bu/G1gxmUYxNw3Vb0EeaRAMx19MO/vRQTbxlSGf38zNLQccioqPq1ihIHAg9g6K9gtoE/WZnCTvydvOQYnBpBaEsiQJBVmmqZnO6Rxg1t8dxRjyUnzEufdKzPT4Qf1sn6zIC7mM362ndIuE4V483BiK5xt/ZDzkb6QPdgWSnTS2dBc2gCrNMnIAdqY1tbQ9PdDr8phY/X5tYqjZwwL8sRcab5gZm5qTzK1fdz3sYOYDU1j8uUyh7W9H10ePHkwhQgQxZc6g8kkwEq8mA/35dMO+x/VaIYYDUoFW8rU+suVFThuSGZhjCzEwM4+b/BkL26Ux1MrXJdsaqpRqBJNJKO9SyaDBrdNHZ4aYLhvnTikE82PR98mQTO7bPdZbrmMPVa1mE9dl1TOdPCk22ykzJuIMYSaFDDm5fUF3smkBqmkvVE3EO4FoVYyVL5tM4PoZzIQVO9H6w8nli49Ry0zZ0mEuUuEKG+uH4ZY6wL49m/f7WJ3ndnUF8FUHoW1Z43V2SZ+MHtIZX8srQxWmXN38x2hHc9HePN629SHEqx7OV8zFoJU= diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..27413b84 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +### v6.0.0-beta.1 + +* BREAKING: Complete rewrite of ZeroMQ.js with a modern and safe API. This version is based on ZeroMQ-NG version v5.0.0-beta.26. A compatibility layer for existing users of versions 5.x or earlier of ZeroMQ.js is available as "zeromq/v5-compat". + +### Previous changes + +* See https://github.com/rolftimmermans/zeromq-ng/blob/master/CHANGELOG.md for the changelog of the next generation API before the merge back into ZeroMQ.js. + +* See https://github.com/zeromq/zeromq.js/blob/5.x/History.md for the changelog of the previous API before the merge of ZeroMQ-NG. diff --git a/History.md b/History.md deleted file mode 100644 index ac8deb10..00000000 --- a/History.md +++ /dev/null @@ -1,181 +0,0 @@ -2.15.3 / 2016-6-3 -================= - - * 2.15.0 introduced a bug where request sockets could no longer batch up requests. - This release should fix that [ronkorving, jaleigh] - -2.15.2 / 2016-5-22 -================== - - * 2.15.0 introduced a bug where some messages would not be received. This release - should fix that [ronkorving] - -2.15.1 / 2016-5-8 -================= - - * Node.js 6 compatibility (NAN 2.3) [kkoopa] - -2.15.0 / 2016-4-27 -================== - - * Dropped support for Node 0.8 [reqshark] - * Added unref/ref APIs to detach/attach sockets from/to the event loop [Joongi Kim] - * Improved message throughput 3-fold on ZMQ 4 [ronkorving] - * When bind or unbind failed, you could never try again (fixed) [ronkorving] - * Various travis configuration improvements [reqshark] - * Bumped NAN to 2.2.x [JanStevens] - -2.14.0 / 2015-11-20 -=================== - - * A socket.read() method was added to retrieve messages while paused [sshutovskyi] - * socket.send() now takes a callback as 3rd argument which is called once the message is sent [ronkorving] - * Now tested on Node.js 0.8, 0.10, 0.12, 4 and 5 [ronkorving] - -2.13.0 / 2015-08-26 -=================== - - * io.js 3.x compatible [kkoopa] - * corrections to type casting operations [kkoopa] - * "make clean" now also removes node_modules [reqshark] - -2.12.0 / 2015-07-10 -=================== - - * Massive improvements to monitoring code, with new documentation and tests [ValYouW] - * Improved documentation [reqshark] - * Updated bindings from ~1.1.1 to ~1.2.1 [reqshark] - * Test suite improvements [reqshark] - * Updated the Windows bundle to ZeroMQ 4.0.4 [kkoopa] - * License attribute added to package.json [pdehaan] - -2.11.1 / 2015-05-21 -=================== - - * io.js 2.x compatible [transcranial] - * replaced asserts with proper exceptions [reqshark] - -2.11.0 / 2015-03-31 -=================== - - * Added pause() and resume() APIs on sockets to allow backpressure [philip1986] - * Elegant handling of EINTR return codes [hurricaneLTG] - * Small performance improvements in send() and internal flush methods [ronkorving] - * Updated test suite to cover io.js and Node 0.12 (removed 0.11) [ronkorving] - * Added "make perf" for easy benchmarking [ronkorving] - -2.10.0 / 2015-01-22 -=================== - - * Added ZMQ_STREAM socket type [reqshark] - * Update NAN to io.js compatible 1.5.0 [kkoopa] - * Hitting open file descriptor limit now throws an error during zmq.socket() [briansorahan] - * More reliable benchmarking [maxired] - -2.9.0 / 2015-01-05 -================== - - * More unit tests [bluebery and reqshark] - * More reliable testing [f34rdotcom and kkoopa] - * Improved ReadMe [dminkovsky and skibz] - * Support for zmq_proxy sockets [reqshark] - * Removed "docs" and related deps in favor of ReadMe [reqshark] - -2.8.0 / 2014-08-27 -================== - - * Fixed: monitor API would keep CPU busy at 100% [f34rdotcom] - * Fixed: an exception during flush could render a socket unusable [ronkorving] - * Fixed: Travis changed behavior and broke our tests [ronkorving] - * Code cleanup [kkoopa and ronkorving] - * Removed legacy nextTick event emission during flush [utvara and ronkorving] - * Context API added: setMaxThreads, getMaxThreads, setMaxSockets, getMaxSockets [yoneal] - * Changed unit test suite to Mocha [skeggse and yoneal] - * NAN updated to ~1.3.0 [kkoopa] - -2.7.0 / 2014-04-24 -================== - - * Fixed memory leak when closing socket [rasky] - * Fixed high water mark [soplwang, kkoopa] - * Added socket opts for zeromq 4.x security mechanisms [msealand] - * Use MakeCallback [kkoopa] - * Remove useless setImmediate [kkoopa] - * Use `zmq_msg_send` for ZMQ >= 4.0 [kkoopa] - * Expose the Socket class as zmq.Socket [tcr] - -2.6.0 / 2014-01-23 -================== - - * Monitor support [f34rdotcom, dr-fozzy] - * Unbind support [kkoopa] - * Node 0.11.9 compatibility [kkoopa] - * Support for ZMQ 4 [atrniv] - * Fixed memory leak [utvara] - * OSX Homebrew support [jwalton] - * Fix unit tests [ryanlelek] - -2.5.1 / 2013-08-28 -================== - - * Regression fix for IPC socket bind failure [christopherobin] - -2.5.0 / 2013-08-20 -================== - - * Added testing against Node.js v0.11 [AlexeyKupershtokh] - * Add support for Joyent SmartMachines [JonGretar] - * Use pkg-config on OS X too [blalor] - * Patch for Node 0.11.3 [kkoopa] - * Fix for bind / connect / send problem [kkoopa] - * Fixed multiple bugs in perf tests and changed them to push/pull [ronkorving] - * Add definitions for building on openbsd & freebsd [Minjung] - -2.4.0 / 2013-04-09 -================== - - * added: Windows support [mscdex] - * added: support for all options ever [AlexeyKupershtokh] - * fixed: prevent zeromq sockets from being destroyed by GC [AlexeyKupershtokh] - -2.3.0 / 2013-03-15 -================== - - * added: xpub/xsub socket types [xla] - * added: support for zmq_disconnect [matehat] - * added: LAST_ENDPOINT socket option [ronkorving] - * added: local/remote_lat local/remote_thr perf test [wavded] - * fixed: tests improved [qubyte, jeremybarnes, ronkorving] - * fixed: Node v0.9.4+ compatibility [mscdex] - * fixed: SNDHWM and RCVHWM options were given the wrong type [freehaha] - * removed: waf support [mscdex] - -2.2.0 / 2012-10-17 -================== - - * add support for pkg-config - * add libzmq 3.x support [aaudis] - * fix: prevent GC happening too soon for connect/bindSync - -2.1.0 / 2012-06-29 -================== - - * fix require() for 0.8.0 - * change: use uv_poll in place of IOWatcher - * remove stupid engines field - -2.0.3 / 2012-03-14 -================== - - * Removed -Wall (libuv unused vars caused the build to fail...) - -2.0.2 / 2012-02-16 -================== - - * Added back `.createSocket()` for BC. Closes #86 - -2.0.1 / 2012-01-26 -================== - - * Added `.zmqVersion` [patricklucas] - * Fixed multipart support [joshrtay] diff --git a/LICENSE b/LICENSE index ee35089d..9da36d45 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,18 @@ -Copyright (c) 2011 TJ Holowaychuk -Copyright (c) 2010, 2011 Justin Tulloss +Copyright 2017-2019 Rolf Timmermans -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 30c6765e..78710980 100644 --- a/README.md +++ b/README.md @@ -1,233 +1,277 @@ -# zeromq.js +# ZeroMQ.js Next Generation -[![codecov](https://codecov.io/gh/zeromq/zeromq.js/branch/master/graph/badge.svg)](https://codecov.io/gh/zeromq/zeromq.js) -[![Greenkeeper badge](https://badges.greenkeeper.io/zeromq/zeromq.js.svg)](https://greenkeeper.io/) -[![](https://img.shields.io/badge/version-latest-blue.svg)](https://github.com/zeromq/zeromq.js) -[![Build Status](https://travis-ci.org/zeromq/zeromq.js.svg?branch=master)](https://travis-ci.org/zeromq/zeromq.js) -[![Build status](https://ci.appveyor.com/api/projects/status/6u7saauir2msxpou?svg=true)](https://ci.appveyor.com/project/zeromq/zeromq-js/branch/master) -[![](https://img.shields.io/badge/version-stable-blue.svg)](https://github.com/zeromq/zeromq.js/releases) -[![Build Status](https://travis-ci.org/zeromq/zeromq.js.svg?branch=prebuilt-testing)](https://travis-ci.org/zeromq/zeromq.js) -[![Build status](https://ci.appveyor.com/api/projects/status/w189dgubmg9darun/branch/master?svg=true)](https://ci.appveyor.com/project/zeromq/zeromq-js/branch/prebuilt-testing) +[![Greenkeeper monitoring](https://img.shields.io/badge/dependencies-monitored-brightgreen.svg)](https://greenkeeper.io/) [![Travis build status](https://img.shields.io/travis/zeromq/zeromq.js.svg)](https://travis-ci.org/zeromq/zeromq.js) -[**Users**](#installation---users) | [**From Source**](#installation---from-source) | [**Contributors and Development**](#installation---contributors-and-development) | [**Maintainers**](#for-maintainers-creating-a-release) +## ⚠️⚠️⚠️ This is work in progress and published only as a beta version. For the current stable version see the [5.x branch](https://github.com/zeromq/zeromq.js/tree/5.x) ⚠️⚠️⚠️ -**`zeromq`**: Your ready to use, prebuilt [ØMQ](http://www.zeromq.org/) -bindings for [Node.js](https://nodejs.org/en/). +[ØMQ](http://zeromq.org) bindings for Node.js. The goals of this library are: +* Semantically similar to the [native](https://github.com/zeromq/libzmq) ØMQ library, while sticking to JavaScript idioms. +* Use modern JavaScript and Node.js features such as `async`/`await` and async iterators. +* High performance. +* Fully usable with TypeScript. -ØMQ provides handy functionality when working with sockets. Yet, -installing dependencies on your operating system or building ØMQ from -source can lead to developer frustration. +# Table of contents -**zeromq** simplifies creating communications for a Node.js -application by providing well-tested, ready to use ØMQ bindings. -zeromq supports all major operating systems, including: +* [Installation](#installation) +* [Examples](#examples) + * [Push/Pull](#pushpull) + * [Pub/Sub](#pubsub) + * [Compatibility layer for version 4/5](#compatibility-layer-for-version-45) +* [Contribution](#contribution) +* [History](#history) -* OS X/Darwin (x64) -* Linux (x64, ARMv7 and ARMv8) -* Windows (x64 and x86) -Use **zeromq** and take advantage of the *elegant simplicity of binaries*. +# Installation +Install ZeroMQ.js with prebuilt binaries: -## Installation - Users +```sh +npm install zeromq@6.0.0-beta.1 +``` -We rely on [`prebuild`](https://github.com/mafintosh/prebuild). +Requirements for prebuilt binaries: -Install `zeromq` with the following: +* Node.js 10+ or Electron 3+ (requires a [N-API](https://nodejs.org/api/n-api.html) version 3+) -```bash -npm install zeromq -``` -windows users: -do not forget to set msvs_version according to your visual studio version 2013,2015,2017 - `npm config set msvs_version 2015` -Now, prepare to be amazed by the wonders of binaries. +The following platforms have a prebuilt binary available: -To use your system's libzmq (if it has been installed and development headers -are available): +* Linux on x86-64/armv7/armv8 with glibc +* Linux on x86-64 with musl (e.g. Alpine) +* MacOS on x86-64 +* Windows on x86 or x86-64 -```bash -npm install zeromq --zmq-external -``` +If a prebuilt binary is not available for your platform, installing will attempt to start a build from source. +If you want to link against a shared ZeroMQ library, you can build and link with the shared library as follows: -### Rebuilding for Electron +```sh +npm install zeromq@6.0.0-beta.1 --zmq-shared +``` -If you want to use `zeromq` inside your [Electron](http://electron.atom.io/) application -it needs to be rebuild against Electron headers. We ship prebuilt binaries for Electron so you won't need to build `zeromq` from source. +If you wish to use any DRAFT sockets then it is also necessary to compile the library from source: -You can rebuild `zeromq` manually by running: -```bash -npm rebuild zeromq --runtime=electron --target=1.4.5 +```sh +npm install zeromq@6.0.0-beta.1 --zmq-draft ``` -Where `target` is your desired Electron version. This will download the correct binary for usage in Electron. -For packaging your Electron application we recommend using [`electron-builder`](https://github.com/electron-userland/electron-builder) which handles rebuilding automatically. Enable the `npmSkipBuildFromSource` option to make use of the prebuilt binaries. For a real world example take a look at [nteract](https://github.com/nteract/nteract/blob/master/applications/desktop/package.json). +Make sure you have the following installed before attempting to build from source: + +* Node.js 10+ or Electron 3+ +* A working C/C++ compiler toolchain with make +* Python 2 (2.7 recommended, 3+ does not work) +* ZeroMQ 4.0+ with development headers + +# Examples -## Installation - From Source +More examples can be found in the [examples directory](examples). -If you are working on a Linux 32-bit system or want to install a development version, you have to build `zeromq` from source. +## Push/Pull -### Prerequisites +This example demonstrates how a producer pushes information onto a +socket and how a worker pulls information from the socket. -**Linux** -- `python` (`v2.7` recommended, `v3.x.x` is not supported) -- `make` -- A proper C/C++ compiler toolchain, like [GCC](https://gcc.gnu.org/) +### producer.js -Use your distribution's package manager to install. +```js +const zmq = require("zeromq") -**macOS** +async function run() { + const sock = new zmq.Push -- `python` (`v2.7` recommended, `v3.x.x` is not supported): already installed on Mac OS X -- `Xcode Command Line Tools`: Can be installed with `xcode-select --install` + await sock.bind("tcp://127.0.0.1:3000") + console.log("Producer bound to port 3000") -**Windows** + while (!sock.closed) { + await sock.send("some work") + await new Promise(resolve => setTimeout(resolve, 500)) + } +} -- **Option 1:** Install all the required tools and configurations using Microsoft's [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) by running `npm install -g windows-build-tools` from an elevated PowerShell (run as Administrator). -- **Option 2:** Install dependencies and configuration manually - 1. Visual C++ Build Environment: - * **Option 1:** Install [Visual C++ Build Tools](http://go.microsoft.com/fwlink/?LinkId=691126) using the *Default Install* option. - * **Option 2:** Install [Visual Studio 2015](https://www.visualstudio.com/products/visual-studio-community-vs) (or modify an existing installation) and select *Common Tools for Visual C++* during setup. +run() +``` - > :bulb: [Windows Vista / 7 only] requires [.NET Framework 4.5.1](http://www.microsoft.com/en-us/download/details.aspx?id=40773) +### worker.js - 2. Install [Python 2.7](https://www.python.org/downloads/) or [Miniconda 2.7](http://conda.pydata.org/miniconda.html) (`v3.x.x` is not supported), and run `npm config set python python2.7` - 3. Launch cmd, and set msvs_version according to your visual studio version 2013,2015,2017 - `npm config set msvs_version 2015` +```js +const zmq = require("zeromq") +async function run() { + const sock = new zmq.Pull -### Installation + sock.connect("tcp://127.0.0.1:3000") + console.log("Worker connected to port 3000") -Now you can install `zeromq` with the following: + while (!sock.closed) { + const [msg] = await sock.receive() + console.log("work: %s", msg.toString()) + } +} -```bash -npm install zeromq +run() ``` -## Installation - Contributors and Development -To set up `zeromq` for development, fork this repository and -clone your fork to your system. +## Pub/Sub + +This example demonstrates using `zeromq` in a classic Pub/Sub, +Publisher/Subscriber, application. -Make sure you have the required [dependencies for building `zeromq` from source](#installation---from-source) installed. +### publisher.js -Install a development version of `zeromq` with the following: +```js +const zmq = require("zeromq") -```bash -npm install -``` +async function run() { + const sock = new zmq.Publisher -## Testing + await sock.bind("tcp://127.0.0.1:3000") + console.log("Publisher bound to port 3000") -Run the test suite using: + while (!sock.closed) { + console.log("sending a multipart message envelope") + await sock.send(["kitty cats", "meow!"]) + await new Promise(resolve => setTimeout(resolve, 500)) + } +} -```bash -npm test +run() ``` -## Running an example application +### subscriber.js -Several example applications are found in the `examples` directory. Use -`node` to run an example. To run the 'subber' application, enter the -following: +```js +const zmq = require("zeromq") -```bash -node examples/subber.js -``` +async function run() { + const sock = new zmq.Subscriber + + sock.connect("tcp://127.0.0.1:3000") + sock.subscribe("kitty cats") + console.log("Subscriber connected to port 3000") + while (!sock.closed) { + const [topic, msg] = await sock.receive() + console.log("received a message related to:", topic, "containing message:", msg) + } +} -## Examples using zeromq +run() +``` -### Push/Pull -This example demonstrates how a producer pushes information onto a -socket and how a worker pulls information from the socket. +## Compatibility layer for version 4/5 -**producer.js** +The next generation version of the library features a compatibility layer for ZeroMQ.js versions 4 and 5. This is recommended for users upgrading from previous versions. + +Example: ```js -// producer.js -var zmq = require('zeromq') - , sock = zmq.socket('push'); +const zmq = require("zeromq/v5-compat") + +const pub = zmq.socket("pub") +const sub = zmq.socket("sub") + +pub.bind("tcp://*:3456", err => { + if (err) throw err -sock.bindSync('tcp://127.0.0.1:3000'); -console.log('Producer bound to port 3000'); + sub.connect("tcp://127.0.0.1:3456") -setInterval(function(){ - console.log('sending work'); - sock.send('some work'); -}, 500); + pub.send("message") + + sub.on("message", msg => { + // Handle received message... + }) +}) ``` -**worker.js** -```js -// worker.js -var zmq = require('zeromq') - , sock = zmq.socket('pull'); +# Contribution -sock.connect('tcp://127.0.0.1:3000'); -console.log('Worker connected to port 3000'); -sock.on('message', function(msg){ - console.log('work: %s', msg.toString()); -}); -``` +## Dependencies -### Pub/Sub +In order to develop and test the library, you'll need the following: -This example demonstrates using `zeromq` in a classic Pub/Sub, -Publisher/Subscriber, application. +* A working C/C++ compiler toolchain with make +* Python 2.7 +* Node.js 10+ +* CMake 2.8+ +* curl +* clang-format is strongly recommended -**Publisher: pubber.js** -```js -// pubber.js -var zmq = require('zeromq') - , sock = zmq.socket('pub'); +## Defining new options -sock.bindSync('tcp://127.0.0.1:3000'); -console.log('Publisher bound to port 3000'); +Socket and context options can be set at runtime, even if they are not implemented by this library. By design, this requires no recompilation if the built version of ZeroMQ has support for them. This allows library users to test and use options that have been introduced in recent versions of ZeroMQ without having to modify this library. Of course we'd love to include support for new options in an idiomatic way. -setInterval(function(){ - console.log('sending a multipart message envelope'); - sock.send(['kitty cats', 'meow!']); -}, 500); +Options can be set as follows: + +```js +const {Dealer} = require("zeromq") + +/* This defines an accessor named 'sendHighWaterMark', which corresponds to + the constant ZMQ_SNDHWM, which is defined as '23' in zmq.h. The option takes + integers. The accessor name has been converted to idiomatic JavaScript. + Of course, this particular option already exists in this library. */ +class MyDealer extends Dealer { + get sendHighWaterMark(): number { + return this.getInt32Option(23) + } + + set sendHighWaterMark(value: number) { + this.setInt32Option(23, value) + } +} + +const sock = new MyDealer({sendHighWaterMark: 456}) ``` -**Subscriber: subber.js** +When submitting pull requests for new socket/context options, please consider the following: -```js -// subber.js -var zmq = require('zeromq') - , sock = zmq.socket('sub'); +* The option is documented in the TypeScript interface. +* The option is only added to relevant socket types, and if the ZMQ_ constant has a prefix indicating which type it applies to, it is stripped from the name as it is exposed in JavaScript. +* The name as exposed in this library is idiomatic for JavaScript, spelling out any abbreviations and using proper `camelCase` naming conventions. +* The option is a value that can be set on a socket, and you don't think it should actually be a method. -sock.connect('tcp://127.0.0.1:3000'); -sock.subscribe('kitty cats'); -console.log('Subscriber connected to port 3000'); -sock.on('message', function(topic, message) { - console.log('received a message related to:', topic, 'containing message:', message); -}); -``` +## Testing +The test suite can be run with: -## For maintainers: Creating a release +```sh +npm install +npm run dev:test +``` -When making a release, do the following: +Or, if you prefer: -```bash -npm version minor && git push && git push --tags +```sh +yarn +yarn run dev:test ``` -Then, wait for the prebuilds to get uploaded for each OS. After the -prebuilds are uploaded, run the following to publish the release: +The test suite will validate and fix the coding style, run all unit tests and verify the validity of the included TypeScript type definitions. + +Some tests are not enabled by default: + +* API Compatibility tests from ZeroMQ 5.x have been disabled by default. You can include the tests with `INCLUDE_COMPAT_TESTS=1 npm run dev:test` +* Some transports are not reliable on some older versions of ZeroMQ, the relevant tests will be skipped for those versions automatically. -```bash -npm publish + +## Publishing + +To publish a new version, run: + +```sh +npm version +git push && git push --tags ``` -## Background +Wait for continuous integration to finish. Prebuilds will be generated for all supported platforms and attached to a Github release. Documentation is automatically generated and committed to `gh-pages`. Finally, a new NPM package version will be automatically released. + + +# History + +Version 6+ is a complete rewrite of previous versions of ZeroMQ.js in order to be more reliable, correct, and usable in modern JavaScript & TypeScript code as first outlined in [this issue](https://github.com/zeromq/zeromq.js/issues/189). Previous versions of ZeroMQ.js were based on `zmq` and a fork that included prebuilt binaries. -This codebase largely came from the npm module `zmq` and was, at one point, named `nteract/zmq-prebuilt`. It started as a community run fork of `zmq` that fixed up the build process and automated prebuilt binaries. In the process of setting up a way to do statically compiled binaries of zeromq for node, `zmq-static` was created. Eventually `zmq-prebuilt` was able to do the job of `zmq-static` and it was deprecated. Once `zmq-prebuilt` was shipping for a while, allowed building from source, and suggesting people use it for electron + node.js, the repository moved to the zeromq org and it became official. +See detailed changes in the [CHANGELOG](CHANGELOG.md). diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 5ac93545..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -environment: - GITHUB_TOKEN: - secure: E1HpiZf9OJuc8XPGA57hJbCQlMWVCPVBePHiWF/BgmJ/+e/2OplyifiS/x8CJtcw - matrix: - - nodejs_version: "10" - deploy: "true" - - nodejs_version: "10" - electron: "3.0.0" - -platform: - - x64 - - x86 - -build: off - -install: - - ps: Install-Product node $env:nodejs_version $env:platform - - IF DEFINED ELECTRON (npm install electron@%ELECTRON%) - - IF DEFINED ELECTRON (npm install --runtime=electron --target=%ELECTRON% --disturl=https://atom.io/download/electron) ELSE (npm install) - - -test_script: - - IF DEFINED ELECTRON (appveyor-retry call npm run test:electron) ELSE (appveyor-retry call npm test) - -deploy_script: - - IF "%deploy%;%appveyor_repo_tag%"=="true;true" (node_modules\.bin\prebuild --all --strip -u %GITHUB_TOKEN%) - -notifications: - - provider: Slack - auth_token: - secure: q6/gXUGl67pHWThEVtLaKpkxP2vIr5pzHT0FuUCRN5ZI9gBZdROM+OXUnBiyNodJLAC9tg8w4uccXFFoMgQYok4/8nJ9QKjq92mhEuShDEA= - channel: '#zmq-prebuilt' diff --git a/binding.cc b/binding.cc deleted file mode 100644 index c5a63c0c..00000000 --- a/binding.cc +++ /dev/null @@ -1,1558 +0,0 @@ -/* - * Copyright (c) 2011 Justin Tulloss - * Copyright (c) 2010 Justin Tulloss - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#include -#include -#include -#include -#include -#if (ZMQ_VERSION < 40200) -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include "nan.h" - -#ifdef _WIN32 -# define snprintf _snprintf_s -#endif - -#define ZMQ_CAN_DISCONNECT (ZMQ_VERSION_MAJOR == 3 && ZMQ_VERSION_MINOR >= 2) || ZMQ_VERSION_MAJOR > 3 -#define ZMQ_CAN_UNBIND (ZMQ_VERSION_MAJOR == 3 && ZMQ_VERSION_MINOR >= 2) || ZMQ_VERSION_MAJOR > 3 -#define ZMQ_CAN_MONITOR (ZMQ_VERSION > 30201) -#define ZMQ_CAN_SET_CTX (ZMQ_VERSION_MAJOR == 3 && ZMQ_VERSION_MINOR >= 2) || ZMQ_VERSION_MAJOR > 3 - -#define ZERO_COPY_MESSAGE_SEND 1 - -using namespace v8; -using namespace node; - -enum { - STATE_READY - , STATE_BUSY - , STATE_CLOSED -}; - -namespace zmq { - - std::set opts_int; - std::set opts_uint32; - std::set opts_int64; - std::set opts_uint64; - std::set opts_binary; - - class Socket; - - class Context : public Nan::ObjectWrap { - friend class Socket; - public: - static NAN_MODULE_INIT(Initialize); - virtual ~Context(); - - private: - Context(int io_threads); - static NAN_METHOD(New); - static Context *GetContext(const Nan::FunctionCallbackInfo&); - void Close(); - static NAN_METHOD(Close); -#if ZMQ_CAN_SET_CTX - static NAN_METHOD(GetOpt); - static NAN_METHOD(SetOpt); -#endif - - void* context_; - }; - - class Socket : public Nan::ObjectWrap { - public: - static NAN_MODULE_INIT(Initialize); - virtual ~Socket(); - void NotifyReadReady(); - void NotifySendReady(); - void CallbackIfReady(); - -#if ZMQ_CAN_MONITOR - void MonitorEvent(uint16_t event_id, int32_t event_value, char *endpoint); - void MonitorError(const char *error_msg); -#endif - - private: - static NAN_METHOD(New); - Socket(Context *context, int type); - - static Socket* GetSocket(const Nan::FunctionCallbackInfo&); - static NAN_GETTER(GetState); - - static NAN_GETTER(GetPending); - static NAN_SETTER(SetPending); - - template - Local GetSockOpt(int option); - template - Local SetSockOpt(int option, Local wrappedValue); - static NAN_METHOD(GetSockOpt); - static NAN_METHOD(SetSockOpt); - - void _AttachToEventLoop(); - void _DetachFromEventLoop(); - static NAN_METHOD(AttachToEventLoop); - static NAN_METHOD(DetachFromEventLoop); - - struct BindState; - static NAN_METHOD(Bind); - - static void UV_BindAsync(uv_work_t* req); - static void UV_BindAsyncAfter(uv_work_t* req); - - static NAN_METHOD(BindSync); -#if ZMQ_CAN_UNBIND - static NAN_METHOD(Unbind); - - static void UV_UnbindAsync(uv_work_t* req); - static void UV_UnbindAsyncAfter(uv_work_t* req); - - static NAN_METHOD(UnbindSync); -#endif - static NAN_METHOD(Connect); -#if ZMQ_CAN_DISCONNECT - static NAN_METHOD(Disconnect); -#endif - - class IncomingMessage; - class OutgoingMessage; - static NAN_METHOD(Recv); - static NAN_METHOD(Readv); - static NAN_METHOD(Sendv); - void Close(); - static NAN_METHOD(Close); - - Nan::Persistent context_; - void *socket_; - bool pending_; - uint8_t state_; - int32_t endpoints; -#if ZMQ_CAN_MONITOR - void *monitor_socket_; - uv_timer_t *monitor_handle_; - int64_t timer_interval_; - int64_t num_of_events_; - static void UV_MonitorCallback(uv_timer_t* handle, int status); - static NAN_METHOD(Monitor); - void Unmonitor(); - static NAN_METHOD(Unmonitor); -#endif - - short PollForEvents(); - uv_poll_t *poll_handle_; - static void UV_PollCallback(uv_poll_t* handle, int status, int events); - }; - - Nan::Persistent send_callback_symbol; - Nan::Persistent read_callback_symbol; - -#if ZMQ_CAN_MONITOR - Nan::Persistent monitor_symbol; - Nan::Persistent monitor_error; - int monitors_count = 0; -#endif - - static NAN_MODULE_INIT(Initialize); - - static void - on_uv_close(uv_handle_t *handle) - { - delete handle; - } - - /* - * Helpers for dealing with ØMQ errors. - */ - - static inline const char* - ErrorMessage() { - return zmq_strerror(zmq_errno()); - } - - static inline Local - ExceptionFromError() { - return Nan::Error(ErrorMessage()); - } - - /* - * Context methods. - */ - - NAN_MODULE_INIT(Context::Initialize) { - Nan::HandleScope scope; - Local t = Nan::New(New); - t->InstanceTemplate()->SetInternalFieldCount(1); - - Nan::SetPrototypeMethod(t, "close", Close); -#if ZMQ_CAN_SET_CTX - Nan::SetPrototypeMethod(t, "setOpt", SetOpt); - Nan::SetPrototypeMethod(t, "getOpt", GetOpt); -#endif - - Nan::Set(target, Nan::New("Context").ToLocalChecked(), Nan::GetFunction(t).ToLocalChecked()); - } - - - Context::~Context() { - Close(); - } - - NAN_METHOD(Context::New) { - assert(info.IsConstructCall()); - int io_threads = 1; - if (info.Length() == 1) { - if (!info[0]->IsNumber()) { - return Nan::ThrowTypeError("io_threads must be an integer"); - } - io_threads = Nan::To(info[0]).FromJust(); - if (io_threads < 1) { - return Nan::ThrowRangeError("io_threads must be a positive number"); - } - } - Context *context = new Context(io_threads); - context->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); - } - - Context::Context(int io_threads) : Nan::ObjectWrap() { - context_ = zmq_init(io_threads); - if (!context_) Nan::ThrowError(ErrorMessage()); - } - - Context * - Context::GetContext(const Nan::FunctionCallbackInfo& info) { - return Nan::ObjectWrap::Unwrap(info.This()); - } - - void - Context::Close() { - if (context_ != NULL) { - if (zmq_term(context_) < 0) { - Nan::ThrowError(ErrorMessage()); - return; - } - context_ = NULL; - } - } - - NAN_METHOD(Context::Close) { - GetContext(info)->Close(); - return; - } - -#if ZMQ_CAN_SET_CTX - NAN_METHOD(Context::SetOpt) { - if (info.Length() != 2) - return Nan::ThrowError("Must pass an option and a value"); - if (!info[0]->IsNumber() || !info[1]->IsNumber()) - return Nan::ThrowTypeError("Arguments must be numbers"); - int option = Nan::To(info[0]).FromJust(); - int value = Nan::To(info[1]).FromJust(); - - Context *context = GetContext(info); - if (zmq_ctx_set(context->context_, option, value) < 0) - return Nan::ThrowError(ExceptionFromError()); - return; - } - - NAN_METHOD(Context::GetOpt) { - if (info.Length() != 1) - return Nan::ThrowError("Must pass an option"); - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("Option must be an integer"); - int option = Nan::To(info[0]).FromJust(); - - Context *context = GetContext(info); - int value = zmq_ctx_get(context->context_, option); - info.GetReturnValue().Set(Nan::New(value)); - } -#endif - /* - * Socket methods. - */ - - NAN_MODULE_INIT(Socket::Initialize) { - Nan::HandleScope scope; - - Local t = Nan::New(New); - t->InstanceTemplate()->SetInternalFieldCount(1); - Nan::SetAccessor(t->InstanceTemplate(), - Nan::New("state").ToLocalChecked(), Socket::GetState); - Nan::SetAccessor(t->InstanceTemplate(), - Nan::New("pending").ToLocalChecked(), GetPending, SetPending); - - Nan::SetPrototypeMethod(t, "bind", Bind); - Nan::SetPrototypeMethod(t, "bindSync", BindSync); -#if ZMQ_CAN_UNBIND - Nan::SetPrototypeMethod(t, "unbind", Unbind); - Nan::SetPrototypeMethod(t, "unbindSync", UnbindSync); -#endif - Nan::SetPrototypeMethod(t, "connect", Connect); - Nan::SetPrototypeMethod(t, "getsockopt", GetSockOpt); - Nan::SetPrototypeMethod(t, "setsockopt", SetSockOpt); - Nan::SetPrototypeMethod(t, "ref", AttachToEventLoop); - Nan::SetPrototypeMethod(t, "unref", DetachFromEventLoop); - Nan::SetPrototypeMethod(t, "recv", Recv); - Nan::SetPrototypeMethod(t, "readv", Readv); - Nan::SetPrototypeMethod(t, "sendv", Sendv); - Nan::SetPrototypeMethod(t, "close", Close); - -#if ZMQ_CAN_DISCONNECT - Nan::SetPrototypeMethod(t, "disconnect", Disconnect); -#endif - -#if ZMQ_CAN_MONITOR - Nan::SetPrototypeMethod(t, "monitor", Monitor); - Nan::SetPrototypeMethod(t, "unmonitor", Unmonitor); - monitor_symbol.Reset(Nan::New("onMonitorEvent").ToLocalChecked()); - monitor_error.Reset(Nan::New("onMonitorError").ToLocalChecked()); -#endif - - Nan::Set(target, Nan::New("SocketBinding").ToLocalChecked(), Nan::GetFunction(t).ToLocalChecked()); - - read_callback_symbol.Reset(Nan::New("onReadReady").ToLocalChecked()); - send_callback_symbol.Reset(Nan::New("onSendReady").ToLocalChecked()); - } - - Socket::~Socket() { - Unmonitor(); - Close(); - } - - NAN_METHOD(Socket::New) { - assert(info.IsConstructCall()); - - if (info.Length() != 2) { - return Nan::ThrowError("Must pass a context and a type to constructor"); - } - - Context *context = Nan::ObjectWrap::Unwrap(info[0].As()); - - if (!info[1]->IsNumber()) { - return Nan::ThrowTypeError("Type must be an integer"); - } - - int type = Nan::To(info[1]).FromJust(); - - Socket *socket = new Socket(context, type); - socket->Wrap(info.This()); - info.GetReturnValue().Set(info.This()); - } - - short - Socket::PollForEvents() { - zmq_pollitem_t item = { socket_, 0, ZMQ_POLLIN, 0 }; - if (pending_) - item.events |= ZMQ_POLLOUT; - - while (true) { - int rc = zmq_poll(&item, 1, 0); - if (rc < 0) { - if (zmq_errno()==EINTR) { - continue; - } else { - Nan::ThrowError(ErrorMessage()); - return -1; - } - } else { - break; - } - } - return item.revents & item.events; - } - - void - Socket::NotifyReadReady() { - Nan::HandleScope scope; - Local callback_v = Nan::Get(this->handle(), Nan::New(read_callback_symbol)).ToLocalChecked(); - - Nan::MakeCallback(this->handle(), callback_v.As(), 0, NULL); - } - - void - Socket::NotifySendReady() { - Nan::HandleScope scope; - Local callback_v = Nan::Get(this->handle(), Nan::New(send_callback_symbol)).ToLocalChecked(); - - Nan::MakeCallback(this->handle(), callback_v.As(), 0, NULL); - } - - void - Socket::CallbackIfReady() { - short events = PollForEvents(); - - if ((events & ZMQ_POLLIN) != 0) { - NotifyReadReady(); - } - - if ((events & ZMQ_POLLOUT) != 0) { - NotifySendReady(); - } - } - - void - Socket::UV_PollCallback(uv_poll_t* handle, int status, int events) { - if (status != 0) { - Nan::ThrowError("I/O status: socket not ready !=0 "); - return; - } - Socket* s = static_cast(handle->data); - s->CallbackIfReady(); - } - -#if ZMQ_CAN_MONITOR - void - Socket::MonitorEvent(uint16_t event_id, int32_t event_value, char *event_endpoint) { - Nan::HandleScope scope; - - Local callback_v = Nan::Get(this->handle(), Nan::New(monitor_symbol)).ToLocalChecked(); - if (!callback_v->IsFunction()) { - return; - } - - Local argv[4]; - argv[0] = Nan::New(event_id); - argv[1] = Nan::New(event_value); - argv[2] = Nan::New(event_endpoint).ToLocalChecked(); - switch (event_id) { - case ZMQ_EVENT_BIND_FAILED: - case ZMQ_EVENT_ACCEPT_FAILED: - case ZMQ_EVENT_CLOSE_FAILED: - argv[3] = ExceptionFromError(); - break; - default: - argv[3] = Nan::Undefined(); - break; - } - - Nan::MakeCallback(this->handle(), callback_v.As(), 4, argv); - } - - void - Socket::MonitorError(const char *error_msg) { - Nan::HandleScope scope; - - Local callback_v = Nan::Get(this->handle(), Nan::New(monitor_error)).ToLocalChecked(); - if (!callback_v->IsFunction()) { - return; - } - - Local argv[1]; - argv[0] = Nan::New(error_msg).ToLocalChecked(); - - Nan::MakeCallback(this->handle(), callback_v.As(), 1, argv); - } - - void - Socket::UV_MonitorCallback(uv_timer_t* handle, int status) { - Nan::HandleScope scope; - Socket* s = static_cast(handle->data); - zmq_msg_t msg1; /* 3.x has 1 message per event */ - - if (s->state_ == STATE_CLOSED) - return; - - zmq_pollitem_t item; - item.socket = s->monitor_socket_; - item.events = ZMQ_POLLIN; - - const char* error = NULL; - int64_t ittr = 0; - while ((s->num_of_events_ == 0 || s->num_of_events_ > ittr++) && zmq_poll(&item, 1, 0)) { - zmq_msg_init (&msg1); - if (zmq_recvmsg (s->monitor_socket_, &msg1, ZMQ_DONTWAIT) > 0) { - char event_endpoint[1025]; - uint16_t event_id; - int32_t event_value; - -#if ZMQ_VERSION_MAJOR >= 4 - uint8_t *data = static_cast(zmq_msg_data(&msg1)); - event_id = *reinterpret_cast(data); - event_value = *reinterpret_cast(data + 2); - - zmq_msg_t msg2; /* 4.x has 2 messages per event */ - - // get our next frame it may have the target address and safely copy to our buffer - zmq_msg_init (&msg2); - if (zmq_msg_more(&msg1) == 0 || zmq_recvmsg (s->monitor_socket_, &msg2, 0) == -1) { - error = ErrorMessage(); - zmq_msg_close(&msg2); - break; - } - - // protect from overflow - size_t len = zmq_msg_size(&msg2); - // MIN message size and buffer size with null padding - len = len < sizeof(event_endpoint)-1 ? len : sizeof(event_endpoint)-1; - memcpy(event_endpoint, zmq_msg_data(&msg2), len); - zmq_msg_close(&msg2); - - // null terminate our string - event_endpoint[len]=0; -#else - // monitoring on zmq < 4 used zmq_event_t - zmq_event_t event; - memcpy (&event, zmq_msg_data (&msg1), sizeof (zmq_event_t)); - event_id = event.event; - - // Bit of a hack, but all events in the zmq_event_t union have the same layout so this will work for all event types. - event_value = event.data.connected.fd; - snprintf(event_endpoint, sizeof(event_endpoint), "%s", event.data.connected.addr); -#endif - - s->MonitorEvent(event_id, event_value, event_endpoint); - zmq_msg_close(&msg1); - } - else { - error = ErrorMessage(); - zmq_msg_close(&msg1); - break; - } - } - - // If there was no error and we still monitor we reset the monitor timer - if (error == NULL && s->monitor_handle_ != NULL) { - uv_timer_start(s->monitor_handle_, reinterpret_cast(Socket::UV_MonitorCallback), s->timer_interval_, 0); - } - // If error raise the monitor error event and stop the monitor - else if (error != NULL) { - s->Unmonitor(); - s->MonitorError(error); - } - } -#endif - - Socket::Socket(Context *context, int type) : Nan::ObjectWrap() { - context_.Reset(context->handle()); - socket_ = zmq_socket(context->context_, type); - pending_ = false; - state_ = STATE_READY; - - if (NULL == socket_) { - Nan::ThrowError(ErrorMessage()); - return; - } - - endpoints = 0; - - poll_handle_ = new uv_poll_t; - - poll_handle_->data = this; - - uv_os_sock_t socket; - size_t len = sizeof(uv_os_sock_t); - - if (zmq_getsockopt(socket_, ZMQ_FD, &socket, &len)) { - Nan::ThrowError(ErrorMessage()); - return; - } - - #if ZMQ_CAN_MONITOR - this->monitor_socket_ = NULL; - #endif - - uv_poll_init_socket(uv_default_loop(), poll_handle_, socket); - uv_poll_start(poll_handle_, UV_READABLE, Socket::UV_PollCallback); - } - - Socket * - Socket::GetSocket(const Nan::FunctionCallbackInfo &info) { - return Nan::ObjectWrap::Unwrap(info.This()); - } - - /* - * This macro makes a call to GetSocket and checks the socket state. These two - * things go hand in hand everywhere in our code. - */ - #define GET_SOCKET(info) \ - Socket* socket = GetSocket(info); \ - if (socket->state_ == STATE_CLOSED) \ - return Nan::ThrowTypeError("Socket is closed"); \ - if (socket->state_ == STATE_BUSY) \ - return Nan::ThrowTypeError("Socket is busy"); - - NAN_GETTER(Socket::GetState) { - Socket* socket = Nan::ObjectWrap::Unwrap(info.Holder()); - info.GetReturnValue().Set(Nan::New(socket->state_)); - } - - NAN_GETTER(Socket::GetPending) { - Socket* socket = Nan::ObjectWrap::Unwrap(info.Holder()); - info.GetReturnValue().Set(socket->pending_); - } - - NAN_SETTER(Socket::SetPending) { - if (!value->IsBoolean()) - return Nan::ThrowTypeError("Pending must be a boolean"); - - Socket* socket = Nan::ObjectWrap::Unwrap(info.Holder()); - socket->pending_ = Nan::To(value).FromJust(); - } - - template - Local Socket::GetSockOpt(int option) { - T value = 0; - size_t len = sizeof(T); - while (true) { - int rc = zmq_getsockopt(socket_, option, &value, &len); - if (rc < 0) { - if(zmq_errno()==EINTR) { - continue; - } - Nan::ThrowError(ExceptionFromError()); - return Nan::Undefined(); - } - break; - } - return Nan::New(value); - } - - template - Local Socket::SetSockOpt(int option, Local wrappedValue) { - if (!wrappedValue->IsNumber()) { - Nan::ThrowError("Value must be an integer"); - return Nan::Undefined(); - } - T value = Nan::To(wrappedValue).FromJust(); - if (zmq_setsockopt(socket_, option, &value, sizeof(T)) < 0) - Nan::ThrowError(ExceptionFromError()); - return Nan::Undefined(); - } - - template<> Local - Socket::GetSockOpt(int option) { - char value[1024]; - size_t len = sizeof(value) - 1; - if (zmq_getsockopt(socket_, option, value, &len) < 0) { - Nan::ThrowError(ExceptionFromError()); - return Nan::Undefined(); - } - value[len] = '\0'; - return Nan::New(value).ToLocalChecked(); - } - - template<> Local - Socket::SetSockOpt(int option, Local wrappedValue) { - if (!Buffer::HasInstance(wrappedValue)) { - Nan::ThrowTypeError("Value must be a buffer"); - return Nan::Undefined(); - } - Local buf = wrappedValue.As(); - size_t length = Buffer::Length(buf); - if (zmq_setsockopt(socket_, option, Buffer::Data(buf), length) < 0) - Nan::ThrowError(ExceptionFromError()); - return Nan::Undefined(); - } - - NAN_METHOD(Socket::GetSockOpt) { - if (info.Length() != 1) - return Nan::ThrowError("Must pass an option"); - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("Option must be an integer"); - int option = Nan::To(info[0]).FromJust(); - - GET_SOCKET(info); - - if (opts_int.count(option)) { - info.GetReturnValue().Set(socket->GetSockOpt(option)); - } else if (opts_uint32.count(option)) { - info.GetReturnValue().Set(socket->GetSockOpt(option)); - } else if (opts_int64.count(option)) { - info.GetReturnValue().Set(socket->GetSockOpt(option)); - } else if (opts_uint64.count(option)) { - info.GetReturnValue().Set(socket->GetSockOpt(option)); - } else if (opts_binary.count(option)) { - info.GetReturnValue().Set(socket->GetSockOpt(option)); - } else { - return Nan::ThrowError(zmq_strerror(EINVAL)); - } - } - - NAN_METHOD(Socket::SetSockOpt) { - if (info.Length() != 2) - return Nan::ThrowError("Must pass an option and a value"); - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("Option must be an integer"); - int option = Nan::To(info[0]).FromJust(); - GET_SOCKET(info); - - if (opts_int.count(option)) { - info.GetReturnValue().Set(socket->SetSockOpt(option, info[1])); - } else if (opts_uint32.count(option)) { - info.GetReturnValue().Set(socket->SetSockOpt(option, info[1])); - } else if (opts_int64.count(option)) { - info.GetReturnValue().Set(socket->SetSockOpt(option, info[1])); - } else if (opts_uint64.count(option)) { - info.GetReturnValue().Set(socket->SetSockOpt(option, info[1])); - } else if (opts_binary.count(option)) { - info.GetReturnValue().Set(socket->SetSockOpt(option, info[1])); - } else { - return Nan::ThrowError(zmq_strerror(EINVAL)); - } - } - - void Socket::_AttachToEventLoop() { - uv_ref(reinterpret_cast(this->poll_handle_)); - } - - NAN_METHOD(Socket::AttachToEventLoop) { - GET_SOCKET(info); - socket->_AttachToEventLoop(); - } - - void Socket::_DetachFromEventLoop() { - uv_unref(reinterpret_cast(this->poll_handle_)); - } - - NAN_METHOD(Socket::DetachFromEventLoop) { - GET_SOCKET(info); - socket->_DetachFromEventLoop(); - } - - struct Socket::BindState { - BindState(Socket* sock_, Local cb_, Local addr_) - : addr(addr_) { - sock_obj.Reset(sock_->handle()); - sock = sock_->socket_; - cb.Reset(cb_); - error = 0; - } - - ~BindState() { - sock_obj.Reset(); - cb.Reset(); - } - - Nan::Persistent sock_obj; - void* sock; - Nan::Persistent cb; - Nan::Utf8String addr; - int error; - }; - - NAN_METHOD(Socket::Bind) { - if (!info[0]->IsString()) - return Nan::ThrowTypeError("Address must be a string!"); - Local addr = info[0].As(); - if (info.Length() > 1 && !info[1]->IsFunction()) - return Nan::ThrowTypeError("Provided callback must be a function"); - Local cb = Local::Cast(info[1]); - - GET_SOCKET(info); - - BindState* state = new BindState(socket, cb, addr); - uv_work_t* req = new uv_work_t; - req->data = state; - uv_queue_work(uv_default_loop(), - req, - UV_BindAsync, - (uv_after_work_cb)UV_BindAsyncAfter); - socket->state_ = STATE_BUSY; - - return; - } - - void Socket::UV_BindAsync(uv_work_t* req) { - BindState* state = static_cast(req->data); - if (zmq_bind(state->sock, *state->addr) < 0) - state->error = zmq_errno(); - } - - void Socket::UV_BindAsyncAfter(uv_work_t* req) { - BindState* state = static_cast(req->data); - Nan::HandleScope scope; - - Local argv[1]; - - if (state->error) { - argv[0] = Nan::Error(zmq_strerror(state->error)); - } else { - argv[0] = Nan::Undefined(); - } - - Local cb = Nan::New(state->cb); - - Socket *socket = Nan::ObjectWrap::Unwrap(Nan::New(state->sock_obj)); - socket->state_ = STATE_READY; - - if (socket->endpoints == 0) { - socket->Ref(); - socket->_AttachToEventLoop(); - } - socket->endpoints += 1; - - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), cb, 1, argv); - - delete state; - delete req; - } - - NAN_METHOD(Socket::BindSync) { - if (!info[0]->IsString()) - return Nan::ThrowTypeError("Address must be a string!"); - Nan::Utf8String addr(info[0].As()); - GET_SOCKET(info); - socket->state_ = STATE_BUSY; - if (zmq_bind(socket->socket_, *addr) < 0) { - socket->state_ = STATE_READY; - return Nan::ThrowError(ErrorMessage()); - } - - socket->state_ = STATE_READY; - - if (socket->endpoints == 0) { - socket->Ref(); - socket->_AttachToEventLoop(); - } - - socket->endpoints += 1; - - return; - } - -#if ZMQ_CAN_UNBIND - NAN_METHOD(Socket::Unbind) { - if (!info[0]->IsString()) - return Nan::ThrowTypeError("Address must be a string!"); - Local addr = info[0].As(); - if (info.Length() > 1 && !info[1]->IsFunction()) - return Nan::ThrowTypeError("Provided callback must be a function"); - Local cb = Local::Cast(info[1]); - - GET_SOCKET(info); - - BindState* state = new BindState(socket, cb, addr); - uv_work_t* req = new uv_work_t; - req->data = state; - uv_queue_work(uv_default_loop(), - req, - UV_UnbindAsync, - (uv_after_work_cb)UV_UnbindAsyncAfter); - socket->state_ = STATE_BUSY; - return; - } - - void Socket::UV_UnbindAsync(uv_work_t* req) { - BindState* state = static_cast(req->data); - if (zmq_unbind(state->sock, *state->addr) < 0) - state->error = zmq_errno(); - } - - void Socket::UV_UnbindAsyncAfter(uv_work_t* req) { - BindState* state = static_cast(req->data); - Nan::HandleScope scope; - - Local argv[1]; - - if (state->error) { - argv[0] = Nan::Error(zmq_strerror(state->error)); - } else { - argv[0] = Nan::Undefined(); - } - - Local cb = Nan::New(state->cb); - - Socket *socket = Nan::ObjectWrap::Unwrap(Nan::New(state->sock_obj)); - socket->state_ = STATE_READY; - - if (--socket->endpoints == 0) { - socket->Unref(); - socket->_DetachFromEventLoop(); - } - - Nan::MakeCallback(Nan::GetCurrentContext()->Global(), cb, 1, argv); - - delete state; - delete req; - } - - NAN_METHOD(Socket::UnbindSync) { - if (!info[0]->IsString()) - return Nan::ThrowTypeError("Address must be a string!"); - Nan::Utf8String addr(info[0].As()); - GET_SOCKET(info); - socket->state_ = STATE_BUSY; - if (zmq_unbind(socket->socket_, *addr) < 0) { - socket->state_ = STATE_READY; - return Nan::ThrowError(ErrorMessage()); - } - - socket->state_ = STATE_READY; - - if (--socket->endpoints == 0) { - socket->Unref(); - socket->_DetachFromEventLoop(); - } - - return; - } -#endif - - NAN_METHOD(Socket::Connect) { - if (!info[0]->IsString()) { - return Nan::ThrowTypeError("Address must be a string!"); - } - - GET_SOCKET(info); - - Nan::Utf8String address(info[0].As()); - if (zmq_connect(socket->socket_, *address)) - return Nan::ThrowError(ErrorMessage()); - - if (socket->endpoints++ == 0) { - socket->Ref(); - socket->_AttachToEventLoop(); - } - - return; - } - -#if ZMQ_CAN_DISCONNECT - NAN_METHOD(Socket::Disconnect) { - - if (!info[0]->IsString()) { - return Nan::ThrowTypeError("Address must be a string!"); - } - - GET_SOCKET(info); - - Nan::Utf8String address(info[0].As()); - if (zmq_disconnect(socket->socket_, *address)) - return Nan::ThrowError(ErrorMessage()); - if (--socket->endpoints == 0) { - socket->Unref(); - socket->_DetachFromEventLoop(); - } - - return; - } -#endif - - /* - * An object that creates an empty ØMQ message, which can be used for - * zmq_recv. After the receive call, a Buffer object wrapping the ØMQ - * message can be requested. The reference for the ØMQ message will - * remain while the data is in use by the Buffer. - */ - - class Socket::IncomingMessage { - public: - inline IncomingMessage() { - msgref_ = new MessageReference(); - }; - - inline ~IncomingMessage() { - if (buf_.IsEmpty() && msgref_) { - delete msgref_; - msgref_ = NULL; - } else { - buf_.Reset(); - } - }; - - inline operator zmq_msg_t*() { - return *msgref_; - } - - inline Local GetBuffer() { - if (buf_.IsEmpty()) { - Local buf_obj = Nan::NewBuffer((char*)zmq_msg_data(*msgref_), zmq_msg_size(*msgref_), FreeCallback, msgref_).ToLocalChecked(); - if (buf_obj.IsEmpty()) { - return Local(); - } - buf_.Reset(buf_obj); - } - return Nan::New(buf_); - } - - private: - static void FreeCallback(char* data, void* message) { - delete static_cast(message); - } - - class MessageReference { - public: - inline MessageReference() { - if (zmq_msg_init(&msg_) < 0) - Nan::ThrowError(ErrorMessage()); - } - - inline ~MessageReference() { - if (zmq_msg_close(&msg_) < 0) - Nan::ThrowError(ErrorMessage()); - } - - inline operator zmq_msg_t*() { - return &msg_; - } - - private: - zmq_msg_t msg_; - }; - - Nan::Persistent buf_; - MessageReference* msgref_; - }; - - class Socket::OutgoingMessage { - public: - inline OutgoingMessage(Local buf) - : bufref_(new BufferReference(buf)) { - if (zmq_msg_init_data(&msg_, Buffer::Data(buf), Buffer::Length(buf), - BufferReference::FreeCallback, bufref_) < 0) { - delete bufref_; - Nan::ThrowError(ErrorMessage()); - } - }; - - inline ~OutgoingMessage() { - if (zmq_msg_close(&msg_) < 0) - Nan::ThrowError(ErrorMessage()); - }; - - inline operator zmq_msg_t*() { - return &msg_; - } - - private: - class BufferReference { - public: - inline BufferReference(Local buf) - : persistent_(buf), async_(new uv_async_t) { - if (uv_async_init(uv_default_loop(), async_, Destroy) < 0) { - delete async_; - delete this; - Nan::ThrowError("Async initialization failed"); - } else { - async_->data = this; - } - } - - inline ~BufferReference() { - persistent_.Reset(); - } - - // Called by zmq when the message has been sent. - // NOTE: May be called from a worker thread. Do not modify V8/Node. - static void FreeCallback(void*, void* bufref) { - int result = uv_async_send(static_cast(bufref)->async_); - assert(result == 0); - } - - static void Destroy(uv_async_t* async) { - uv_close(reinterpret_cast(async), on_uv_close); - BufferReference* bufref = static_cast(async->data); - delete bufref; - } - private: - Nan::Persistent persistent_; - uv_async_t* async_; - }; - - zmq_msg_t msg_; - BufferReference* bufref_; - }; - - -#if ZMQ_CAN_MONITOR - NAN_METHOD(Socket::Monitor) { - int64_t timer_interval = 10; // default to 10ms interval - int64_t num_of_events = 1; // default is 1 event per interval - - if (info.Length() > 0 && !info[0]->IsUndefined()) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("Option must be an integer"); - timer_interval = Nan::To(info[0]).FromJust(); - if (timer_interval <= 0) - return Nan::ThrowTypeError("Option must be a positive integer"); - } - - if (info.Length() > 1 && !info[1]->IsUndefined()) { - if (!info[1]->IsNumber()) - return Nan::ThrowTypeError("numOfEvents must be an integer"); - num_of_events = Nan::To(info[1]).FromJust(); - if (num_of_events < 0) - return Nan::ThrowTypeError("numOfEvents should be no less than zero"); - } - - GET_SOCKET(info); - char addr[255]; - Context *context = Nan::ObjectWrap::Unwrap(Nan::New(socket->context_)); - sprintf(addr, "%s%d", "inproc://monitor.req.", monitors_count++); - - if(zmq_socket_monitor(socket->socket_, addr, ZMQ_EVENT_ALL) != -1) { - socket->monitor_socket_ = zmq_socket (context->context_, ZMQ_PAIR); - zmq_connect (socket->monitor_socket_, addr); - socket->timer_interval_ = timer_interval; - socket->num_of_events_ = num_of_events; - socket->monitor_handle_ = new uv_timer_t; - socket->monitor_handle_->data = socket; - - uv_timer_init(uv_default_loop(), socket->monitor_handle_); - uv_timer_start(socket->monitor_handle_, reinterpret_cast(Socket::UV_MonitorCallback), timer_interval, 0); - } - - return; - } - - void - Socket::Unmonitor() { - // Make sure we are monitoring - if (this->monitor_socket_ == NULL) { - return; - } - - // Passing NULL as addr will tell zmq to stop monitor - zmq_socket_monitor(this->socket_, NULL, ZMQ_EVENT_ALL); - - // Close the monitor socket and stop timer - if (zmq_close(this->monitor_socket_) < 0) { - Nan::ThrowError(ErrorMessage()); - return; - } - uv_timer_stop(this->monitor_handle_); - uv_close(reinterpret_cast(this->monitor_handle_), on_uv_close); - this->monitor_handle_ = NULL; - this->monitor_socket_ = NULL; - } - - NAN_METHOD(Socket::Unmonitor) { - // We can't use the GET_SOCKET macro here as it requries the socket to be open, - // which might not always be the case - Socket* socket = GetSocket(info); - socket->Unmonitor(); - return; - } - -#endif - - NAN_METHOD(Socket::Readv) { - Socket* socket = GetSocket(info); - if (socket->state_ != STATE_READY) - return; - - int events; - size_t events_size = sizeof(events); - bool checkPollIn = true; - - int rc = 0; - int flags = 0; - int64_t more = 1; - size_t more_size = sizeof(more); - uint32_t index = 0; - - Local result = Nan::New(); - - while (more == 1) { - if (checkPollIn) { - while (zmq_getsockopt(socket->socket_, ZMQ_EVENTS, &events, &events_size)) { - if (zmq_errno() != EINTR) - return Nan::ThrowError(ErrorMessage()); - } - - if ((events & ZMQ_POLLIN) == 0) - return; - } - - IncomingMessage part; - - while (true) { - rc = zmq_msg_init(part); - if (rc != 0) { - if (zmq_errno()==EINTR) { - continue; - } - return Nan::ThrowError(ErrorMessage()); - } - break; - } - - while (true) { - #if ZMQ_VERSION_MAJOR == 2 - rc = zmq_recv(socket->socket_, part, flags); - #elif ZMQ_VERSION_MAJOR == 3 - rc = zmq_recvmsg(socket->socket_, part, flags); - #else - rc = zmq_msg_recv(part, socket->socket_, flags); - checkPollIn = false; - #endif - - if (rc < 0) { - if (zmq_errno() == EINTR) - continue; - return Nan::ThrowError(ErrorMessage()); - } - - Nan::Set(result, index++, part.GetBuffer()); - break; - } - - while (zmq_getsockopt(socket->socket_, ZMQ_RCVMORE, &more, &more_size)) { - if (zmq_errno() != EINTR) - return Nan::ThrowError(ErrorMessage()); - } - } - - info.GetReturnValue().Set(result); - } - - NAN_METHOD(Socket::Recv) { - int flags = 0; - int argc = info.Length(); - if (argc == 1) { - if (!info[0]->IsNumber()) - return Nan::ThrowTypeError("Argument should be an integer"); - flags = Nan::To(info[0]).FromJust(); - } else if (argc != 0) { - return Nan::ThrowTypeError("Only one argument at most was expected"); - } - - GET_SOCKET(info); - - IncomingMessage msg; - while (true) { - int rc; - #if ZMQ_VERSION_MAJOR == 2 - rc = zmq_recv(socket->socket_, msg, flags); - #else - rc = zmq_recvmsg(socket->socket_, msg, flags); - #endif - if (rc < 0) { - if (zmq_errno()==EINTR) { - continue; - } - return Nan::ThrowError(ErrorMessage()); - } else { - break; - } - } - info.GetReturnValue().Set(msg.GetBuffer()); - } - - NAN_METHOD(Socket::Sendv) { - Socket* socket = GetSocket(info); - if (socket->state_ != STATE_READY) - return info.GetReturnValue().Set(false); - - int events; - size_t events_size = sizeof(events); - bool checkPollOut = true; - bool readsReady = false; - - Local batch = info[0].As(); - size_t len = batch->Length(); - - if (len == 0) - return info.GetReturnValue().Set(true); - - if (len % 2 != 0) - return Nan::ThrowTypeError("Batch length must be even!"); - - for (uint32_t i = 0; i < len; i += 2) { - if (checkPollOut) { - while (zmq_getsockopt(socket->socket_, ZMQ_EVENTS, &events, &events_size)) { - if (zmq_errno() != EINTR) - return Nan::ThrowError(ErrorMessage()); - } - - if ((events & ZMQ_POLLIN) != 0) { - readsReady = true; - } - - if ((events & ZMQ_POLLOUT) == 0) { - if (readsReady) { - socket->NotifyReadReady(); - } - return info.GetReturnValue().Set(false); - } - } - - Local buf = batch->Get(i).As(); - Local flagsObj = batch->Get(i + 1).As(); - - int flags = Nan::To(flagsObj).FromJust(); - -#if ZERO_COPY_MESSAGE_SEND - /* Non-copying implementation. */ - OutgoingMessage msg_p(buf); -#else - /* Copying implementation. */ - zmq_msg_t msg; - int rc; - size_t len = Buffer::Length(buf); - rc = zmq_msg_init_size(&msg, len); - if (rc != 0) - return Nan::ThrowError(ErrorMessage()); - - char* cp = static_cast(zmq_msg_data(&msg)); - const char* dat = Buffer::Data(buf); - std::copy(dat, dat + len, cp); - zmq_msg_t* msg_p = &msg; -#endif - - while (true) { - int rc; - #if ZMQ_VERSION_MAJOR == 2 - rc = zmq_send(socket->socket_, msg_p, flags); - #elif ZMQ_VERSION_MAJOR == 3 - rc = zmq_sendmsg(socket->socket_, msg_p, flags); - #else - rc = zmq_msg_send(msg_p, socket->socket_, flags); - checkPollOut = false; - #endif - if (rc < 0){ - if (zmq_errno() == EINTR) { - continue; - } - return Nan::ThrowError(ErrorMessage()); - } - break; - } - } - - while (zmq_getsockopt(socket->socket_, ZMQ_EVENTS, &events, &events_size)) { - if (zmq_errno() != EINTR) - return Nan::ThrowError(ErrorMessage()); - } - - if ((events & ZMQ_POLLIN) != 0) { - readsReady = true; - } - - if (readsReady) { - socket->NotifyReadReady(); - } - - return info.GetReturnValue().Set(true); - } - - void - Socket::Close() { - if (socket_) { - if (zmq_close(socket_) < 0) { - Nan::ThrowError(ErrorMessage()); - return; - } - socket_ = NULL; - state_ = STATE_CLOSED; - context_.Reset(); - - if (this->endpoints > 0) - this->Unref(); - this->endpoints = 0; - - uv_poll_stop(poll_handle_); - uv_close(reinterpret_cast(poll_handle_), on_uv_close); - } - } - - NAN_METHOD(Socket::Close) { - GET_SOCKET(info); - socket->Close(); - return; - } - - // Make zeromq versions less than 2.1.3 work by defining - // the new constants if they don't already exist - #if (ZMQ_VERSION < 20103) - # define ZMQ_DEALER ZMQ_XREQ - # define ZMQ_ROUTER ZMQ_XREP - #endif - - /* - * Module functions. - */ - - static NAN_METHOD(ZmqVersion) { - int major, minor, patch; - zmq_version(&major, &minor, &patch); - - char version_info[16]; - snprintf(version_info, 16, "%d.%d.%d", major, minor, patch); - - info.GetReturnValue().Set(Nan::New(version_info).ToLocalChecked()); - } - -#if ZMQ_VERSION_MAJOR >= 4 - static NAN_METHOD(ZmqCurveKeypair) { - - char public_key [41]; - char secret_key [41]; - - int rc = zmq_curve_keypair( public_key, secret_key); - if (rc < 0) { - return Nan::ThrowError("zmq_curve_keypair operation failed. Method support in libzmq v4+ -with-libsodium."); - } - - Local obj = Nan::New(); - Nan::Set(obj, Nan::New("public").ToLocalChecked(), Nan::New(public_key).ToLocalChecked()); - Nan::Set(obj, Nan::New("secret").ToLocalChecked(), Nan::New(secret_key).ToLocalChecked()); - - info.GetReturnValue().Set(obj); - } -#endif - - static NAN_MODULE_INIT(Initialize) { - Nan::HandleScope scope; - - opts_int.insert(14); // ZMQ_FD - opts_int.insert(16); // ZMQ_TYPE - opts_int.insert(17); // ZMQ_LINGER - opts_int.insert(18); // ZMQ_RECONNECT_IVL - opts_int.insert(19); // ZMQ_BACKLOG - opts_int.insert(21); // ZMQ_RECONNECT_IVL_MAX - opts_int.insert(23); // ZMQ_SNDHWM - opts_int.insert(24); // ZMQ_RCVHWM - opts_int.insert(25); // ZMQ_MULTICAST_HOPS - opts_int.insert(27); // ZMQ_RCVTIMEO - opts_int.insert(28); // ZMQ_SNDTIMEO - opts_int.insert(29); // ZMQ_RCVLABEL - opts_int.insert(30); // ZMQ_RCVCMD - opts_int.insert(31); // ZMQ_IPV4ONLY - opts_int.insert(33); // ZMQ_ROUTER_MANDATORY - opts_int.insert(34); // ZMQ_TCP_KEEPALIVE - opts_int.insert(35); // ZMQ_TCP_KEEPALIVE_CNT - opts_int.insert(36); // ZMQ_TCP_KEEPALIVE_IDLE - opts_int.insert(37); // ZMQ_TCP_KEEPALIVE_INTVL - opts_int.insert(39); // ZMQ_DELAY_ATTACH_ON_CONNECT - opts_int.insert(40); // ZMQ_XPUB_VERBOSE - opts_int.insert(41); // ZMQ_ROUTER_RAW - opts_int.insert(42); // ZMQ_IPV6 - - opts_int64.insert(3); // ZMQ_SWAP - opts_int64.insert(8); // ZMQ_RATE - opts_int64.insert(10); // ZMQ_MCAST_LOOP - opts_int64.insert(20); // ZMQ_RECOVERY_IVL_MSEC - opts_int64.insert(22); // ZMQ_MAXMSGSIZE - - opts_uint64.insert(1); // ZMQ_HWM - opts_uint64.insert(4); // ZMQ_AFFINITY - - opts_binary.insert(5); // ZMQ_IDENTITY - opts_binary.insert(6); // ZMQ_SUBSCRIBE - opts_binary.insert(7); // ZMQ_UNSUBSCRIBE - opts_binary.insert(32); // ZMQ_LAST_ENDPOINT - opts_binary.insert(38); // ZMQ_TCP_ACCEPT_FILTER - - // transition types - #if ZMQ_VERSION_MAJOR >= 3 - opts_int.insert(15); // ZMQ_EVENTS 3.x int - opts_int.insert(8); // ZMQ_RATE 3.x int - opts_int.insert(9); // ZMQ_RECOVERY_IVL 3.x int - opts_int.insert(13); // ZMQ_RCVMORE 3.x int - opts_int.insert(11); // ZMQ_SNDBUF 3.x int - opts_int.insert(12); // ZMQ_RCVBUF 3.x int - #else - opts_uint32.insert(15); // ZMQ_EVENTS 2.x uint32_t - opts_int64.insert(8); // ZMQ_RATE 2.x int64_t - opts_int64.insert(9); // ZMQ_RECOVERY_IVL 2.x int64_t - opts_int64.insert(13); // ZMQ_RCVMORE 2.x int64_t - opts_uint64.insert(11); // ZMQ_SNDBUF 2.x uint64_t - opts_uint64.insert(12); // ZMQ_RCVBUF 2.x uint64_t - #endif - - #if ZMQ_VERSION_MAJOR >= 4 - opts_int.insert(43); // ZMQ_MECHANISM - opts_int.insert(44); // ZMQ_PLAIN_SERVER - opts_binary.insert(45); // ZMQ_PLAIN_USERNAME - opts_binary.insert(46); // ZMQ_PLAIN_PASSWORD - opts_int.insert(47); // ZMQ_CURVE_SERVER - opts_binary.insert(48); // ZMQ_CURVE_PUBLICKEY - opts_binary.insert(49); // ZMQ_CURVE_SECRETKEY - opts_binary.insert(50); // ZMQ_CURVE_SERVERKEY - opts_int.insert(51); //ZMQ_PROBE_ROUTER - opts_binary.insert(55); // ZMQ_ZAP_DOMAIN - opts_int.insert(56); // ZMQ_ROUTER_HANDOVER - opts_int.insert(66); //ZMQ_HANDSHAKE_IVL - #if ZMQ_VERSION_MINOR >= 2 - opts_int.insert(75); //ZMQ_HEARTBEAT_IVL - opts_int.insert(76); //ZMQ_HEARTBEAT_TTL - opts_int.insert(77); //ZMQ_HEARTBEAT_TIMEOUT - opts_int.insert(79); //ZMQ_CONNECT_TIMEOUT - #endif - #endif - - NODE_DEFINE_CONSTANT(target, ZMQ_CAN_DISCONNECT); - NODE_DEFINE_CONSTANT(target, ZMQ_CAN_UNBIND); - NODE_DEFINE_CONSTANT(target, ZMQ_CAN_MONITOR); - NODE_DEFINE_CONSTANT(target, ZMQ_CAN_SET_CTX); - NODE_DEFINE_CONSTANT(target, ZMQ_PUB); - NODE_DEFINE_CONSTANT(target, ZMQ_SUB); - #if ZMQ_VERSION_MAJOR >= 3 - NODE_DEFINE_CONSTANT(target, ZMQ_XPUB); - NODE_DEFINE_CONSTANT(target, ZMQ_XSUB); - #endif - NODE_DEFINE_CONSTANT(target, ZMQ_REQ); - NODE_DEFINE_CONSTANT(target, ZMQ_XREQ); - NODE_DEFINE_CONSTANT(target, ZMQ_REP); - NODE_DEFINE_CONSTANT(target, ZMQ_XREP); - NODE_DEFINE_CONSTANT(target, ZMQ_DEALER); - NODE_DEFINE_CONSTANT(target, ZMQ_ROUTER); - NODE_DEFINE_CONSTANT(target, ZMQ_PUSH); - NODE_DEFINE_CONSTANT(target, ZMQ_PULL); - NODE_DEFINE_CONSTANT(target, ZMQ_PAIR); - #if ZMQ_VERSION_MAJOR >= 4 - NODE_DEFINE_CONSTANT(target, ZMQ_STREAM); - #endif - - NODE_DEFINE_CONSTANT(target, ZMQ_POLLIN); - NODE_DEFINE_CONSTANT(target, ZMQ_POLLOUT); - NODE_DEFINE_CONSTANT(target, ZMQ_POLLERR); - - NODE_DEFINE_CONSTANT(target, ZMQ_SNDMORE); - #if ZMQ_VERSION_MAJOR == 2 - NODE_DEFINE_CONSTANT(target, ZMQ_NOBLOCK); - #endif - - NODE_DEFINE_CONSTANT(target, STATE_READY); - NODE_DEFINE_CONSTANT(target, STATE_BUSY); - NODE_DEFINE_CONSTANT(target, STATE_CLOSED); - - Nan::SetMethod(target, "zmqVersion", ZmqVersion); - #if ZMQ_VERSION_MAJOR >= 4 - Nan::SetMethod(target, "zmqCurveKeypair", ZmqCurveKeypair); - #endif - - Context::Initialize(target); - Socket::Initialize(target); - } -} // namespace zmq - - -// module - -extern "C" NAN_MODULE_INIT(init) { - zmq::Initialize(target); -} - -NODE_MODULE(zmq, init) diff --git a/binding.gyp b/binding.gyp index 77e12f08..896eae41 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,48 +1,168 @@ { 'variables': { - 'zmq_external%': 'false', + 'zmq_shared%': 'false', + 'zmq_draft%': 'false', }, + 'targets': [ { - 'target_name': 'zmq', - 'sources': ['binding.cc'], - 'include_dirs' : [" yarn install +> yarn majordomo +``` + +## Expected behaviour + +The example will start a broker and some workers, then do some requests. The output will be similar to this: + +``` +starting broker on tcp://127.0.0.1:5555 +requesting 'oolong' from 'tea' +requesting 'sencha' from 'tea' +requesting 'earl grey, with milk' from 'tea' +requesting 'jasmine' from 'tea' +requesting 'cappuccino' from 'coffee' +requesting 'latte, with soy milk' from 'coffee' +requesting 'espresso' from 'coffee' +requesting 'irish coffee' from 'coffee' +registered worker 00800041af for 'coffee' +dispatching 'coffee' 00800041ab req -> 00800041af +registered worker 00800041b0 for 'tea' +dispatching 'tea' 00800041a7 req -> 00800041b0 +registered worker 00800041b1 for 'tea' +dispatching 'tea' 00800041a8 req -> 00800041b1 +dispatching 'tea' 00800041a7 <- rep 00800041b0 +dispatching 'tea' 00800041a9 req -> 00800041b0 +received 'oolong' from 'tea' +dispatching 'coffee' 00800041ab <- rep 00800041af +dispatching 'coffee' 00800041ac req -> 00800041af +received 'cappuccino' from 'coffee' +dispatching 'tea' 00800041a9 <- rep 00800041b0 +dispatching 'tea' 00800041aa req -> 00800041b0 +received 'earl grey, with milk' from 'tea' +dispatching 'tea' 00800041a8 <- rep 00800041b1 +received 'sencha' from 'tea' +dispatching 'tea' 00800041aa <- rep 00800041b0 +received 'jasmine' from 'tea' +dispatching 'coffee' 00800041ac <- rep 00800041af +dispatching 'coffee' 00800041ad req -> 00800041af +received 'latte, with soy milk' from 'coffee' +dispatching 'coffee' 00800041ad <- rep 00800041af +dispatching 'coffee' 00800041ae req -> 00800041af +received 'espresso' from 'coffee' +dispatching 'coffee' 00800041ae <- rep 00800041af +received 'irish coffee' from 'coffee' +timeout expired waiting for 'soda' +deregistered worker 00800041b1 for 'tea' +``` diff --git a/examples/majordomo/broker.ts b/examples/majordomo/broker.ts new file mode 100644 index 00000000..6b8c8cc9 --- /dev/null +++ b/examples/majordomo/broker.ts @@ -0,0 +1,109 @@ +/* tslint:disable: no-console */ +import {Router} from "zeromq" + +import {Service} from "./service" +import {Header, Message} from "./types" + +export class Broker { + address: string + socket: Router = new Router({sendHighWaterMark: 1, sendTimeout: 1}) + services: Map = new Map() + workers: Map = new Map() + + constructor(address: string = "tcp://127.0.0.1:5555") { + this.address = address + } + + async start() { + console.log(`starting broker on ${this.address}`) + await this.socket.bind(this.address) + + const loop = async () => { + for await (const [sender, blank, header, ...rest] of this.socket) { + switch (header.toString()) { + case Header.Client: + this.handleClient(sender, ...rest) + break + case Header.Worker: + this.handleWorker(sender, ...rest) + break + default: + console.error(`invalid message header: ${header}`) + } + } + } + + loop() + } + + async stop() { + if (!this.socket.closed) { + this.socket.close() + } + } + + handleClient(client: Buffer, service?: Buffer, ...req: Buffer[]) { + if (service) { + this.dispatchRequest(client, service, ...req) + } + } + + handleWorker(worker: Buffer, type?: Buffer, ...rest: Buffer[]) { + switch (type && type.toString()) { + case Message.Ready: + const [service] = rest + this.register(worker, service) + break + case Message.Reply: + const [client, blank, ...rep] = rest + this.dispatchReply(worker, client, ...rep) + break + case Message.Heartbeat: + /* Heartbeats not implemented yet. */ + break + case Message.Disconnect: + this.deregister(worker) + break + default: + console.error(`invalid worker message type: ${type}`) + } + } + + register(worker: Buffer, service: Buffer) { + this.setWorkerService(worker, service) + this.getService(service).register(worker) + } + + dispatchRequest(client: Buffer, service: Buffer, ...req: Buffer[]) { + this.getService(service).dispatchRequest(client, ...req) + } + + dispatchReply(worker: Buffer, client: Buffer, ...rep: Buffer[]) { + const service = this.getWorkerService(worker) + this.getService(service).dispatchReply(worker, client, ...rep) + } + + deregister(worker: Buffer) { + const service = this.getWorkerService(worker) + this.getService(service).deregister(worker) + } + + getService(name: Buffer): Service { + const key = name.toString() + if (this.services.has(key)) { + return this.services.get(key)! + } else { + const service = new Service(this.socket, key) + this.services.set(key, service) + return service + } + } + + getWorkerService(worker: Buffer): Buffer { + return this.workers.get(worker.toString("hex"))! + } + + setWorkerService(worker: Buffer, service: Buffer) { + this.workers.set(worker.toString("hex"), service) + } +} diff --git a/examples/majordomo/index.ts b/examples/majordomo/index.ts new file mode 100644 index 00000000..b4f7f273 --- /dev/null +++ b/examples/majordomo/index.ts @@ -0,0 +1,77 @@ +/* tslint:disable: no-console */ +import {Request} from "zeromq" + +import {Broker} from "./broker" +import {Worker} from "./worker" + +async function sleep(msec: number) { + return new Promise((resolve) => setTimeout(resolve, msec)) +} + +class TeaWorker extends Worker { + service = "tea" + + async process(...msgs: Buffer[]): Promise { + await sleep(Math.random() * 500) + return msgs + } +} + +class CoffeeWorker extends Worker { + service = "coffee" + + async process(...msgs: Buffer[]): Promise { + await sleep(Math.random() * 200) + return msgs + } +} + +const broker = new Broker() + +const workers = [ + new TeaWorker(), + new CoffeeWorker(), + new TeaWorker(), +] + +async function request(service: string, ...req: string[]): Promise { + const socket = new Request({receiveTimeout: 2000}) + socket.connect(broker.address) + + console.log(`requesting '${req.join(", ")}' from '${service}'`) + await socket.send(["MDPC01", service, ...req]) + + try { + const [blank, header, ...res] = await socket.receive() + console.log(`received '${res.join(", ")}' from '${service}'`) + return res + } catch (err) { + console.log(`timeout expired waiting for '${service}'`) + } +} + +async function main() { + for (const worker of workers) worker.start() + broker.start() + + /* Requests are issued in parallel. */ + await Promise.all([ + request("soda", "cola"), + request("tea", "oolong"), + request("tea", "sencha"), + request("tea", "earl grey", "with milk"), + request("tea", "jasmine"), + request("coffee", "cappuccino"), + request("coffee", "latte", "with soy milk"), + request("coffee", "espresso"), + request("coffee", "irish coffee"), + ]) + + for (const worker of workers) worker.stop() + broker.stop() +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/examples/majordomo/service.ts b/examples/majordomo/service.ts new file mode 100644 index 00000000..97256859 --- /dev/null +++ b/examples/majordomo/service.ts @@ -0,0 +1,75 @@ +/* tslint:disable: no-console */ +import {Router} from "zeromq" + +import {Header, Message} from "./types" + +export class Service { + name: string + socket: Router + workers: Map = new Map() + requests: Array<[Buffer, Buffer[]]> = [] + + constructor(socket: Router, name: string) { + this.socket = socket + this.name = name + } + + dispatchRequest(client: Buffer, ...req: Buffer[]) { + this.requests.push([client, req]) + this.dispatchPending() + } + + async dispatchReply(worker: Buffer, client: Buffer, ...rep: Buffer[]) { + this.workers.set(worker.toString("hex"), worker) + + console.log( + `dispatching '${this.name}' ` + + `${client.toString("hex")} <- rep ${worker.toString("hex")}`, + ) + + await this.socket.send([ + client, + null, + Header.Client, + this.name, + ...rep, + ]) + + this.dispatchPending() + } + + async dispatchPending() { + while (this.workers.size && this.requests.length) { + const [key, worker] = this.workers.entries().next().value! + this.workers.delete(key) + const [client, req] = this.requests.shift()! + + console.log( + `dispatching '${this.name}' ` + + `${client.toString("hex")} req -> ${worker.toString("hex")}`, + ) + + await this.socket.send([ + worker, + null, + Header.Worker, + Message.Request, + client, + null, + ...req, + ]) + } + } + + register(worker: Buffer) { + console.log(`registered worker ${worker.toString("hex")} for '${this.name}'`) + this.workers.set(worker.toString("hex"), worker) + this.dispatchPending() + } + + deregister(worker: Buffer) { + console.log(`deregistered worker ${worker.toString("hex")} for '${this.name}'`) + this.workers.delete(worker.toString("hex")) + this.dispatchPending() + } +} diff --git a/examples/majordomo/types.ts b/examples/majordomo/types.ts new file mode 100644 index 00000000..48280074 --- /dev/null +++ b/examples/majordomo/types.ts @@ -0,0 +1,12 @@ +export enum Header { + Client = "MDPC01", + Worker = "MDPW01", +} + +export enum Message { + Ready = "\x01", + Request = "\x02", + Reply = "\x03", + Heartbeat = "\x04", + Disconnect = "\x05", +} diff --git a/examples/majordomo/worker.ts b/examples/majordomo/worker.ts new file mode 100644 index 00000000..3f31daee --- /dev/null +++ b/examples/majordomo/worker.ts @@ -0,0 +1,45 @@ +/* tslint:disable: no-console */ +import {Dealer} from "zeromq" + +import {Header, Message} from "./types" + +export class Worker { + address: string + service: string = "" + socket: Dealer = new Dealer() + + constructor(address: string = "tcp://127.0.0.1:5555") { + this.address = address + this.socket.connect(address) + } + + async start() { + await this.socket.send([null, Header.Worker, Message.Ready, this.service]) + + const loop = async () => { + for await (const [blank1, header, type, client, blank2, ...req] of this.socket) { + const rep = await this.process(...req) + try { + await this.socket.send([ + null, Header.Worker, Message.Reply, client, null, ...rep, + ]) + } catch (err) { + console.error(`unable to send reply for ${this.address}`) + } + } + } + + loop() + } + + async stop() { + if (!this.socket.closed) { + await this.socket.send([null, Header.Worker, Message.Disconnect, this.service]) + this.socket.close() + } + } + + async process(...req: Buffer[]): Promise { + return req + } +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 00000000..31e35cf8 --- /dev/null +++ b/examples/package.json @@ -0,0 +1,14 @@ +{ + "name": "zeromq-examples", + "license": "MIT", + "dependencies": { + "zeromq": "file:.." + }, + "devDependencies": { + "ts-node": ">= 0" + }, + "scripts": { + "majordomo": "ts-node majordomo", + "queue": "ts-node queue" + } +} diff --git a/examples/producer.js b/examples/producer.js deleted file mode 100755 index 10a875f6..00000000 --- a/examples/producer.js +++ /dev/null @@ -1,10 +0,0 @@ -var zmq = require('../'); -var sock = zmq.socket('push'); - -sock.bindSync('tcp://127.0.0.1:3000'); -console.log('Producer bound to port 3000'); - -setInterval(function(){ - console.log('sending work'); - sock.send('some work'); -}, 500); diff --git a/examples/pubber.js b/examples/pubber.js deleted file mode 100755 index 2e578a19..00000000 --- a/examples/pubber.js +++ /dev/null @@ -1,10 +0,0 @@ -var zmq = require('../'); -var sock = zmq.socket('pub'); - -sock.bindSync('tcp://127.0.0.1:3000'); -console.log('Publisher bound to port 3000'); - -setInterval(function(){ - console.log('sending a multipart message envelope'); - sock.send(['kitty cats', 'meow!']); -}, 500); diff --git a/examples/queue/README.md b/examples/queue/README.md new file mode 100644 index 00000000..b2588832 --- /dev/null +++ b/examples/queue/README.md @@ -0,0 +1,24 @@ +# Simple send queue + +This example implements a simple outgoing queue that will queue messages before sending them. If sending is possible, messages from the queue will be forwarded to the socket. If sending is not possible because the socket blocks, queueing will continue until the queue is full. + +This example can serve as the basis for a queue that can be used in a broker to temporarily queue messages while there are no worker processes available, for example. + +## Running this example + +To run this example, install the example project depedencies and run the majordomo example script with `yarn`: + +``` +> yarn install +> yarn queue +``` + +## Expected behaviour + +The example will start a queue, send messages onto it, and only afterwards connect a worker socket. The output will be similar to this: + +``` +received: hello +received: world! +received: +``` diff --git a/examples/queue/index.ts b/examples/queue/index.ts new file mode 100644 index 00000000..cfa94c08 --- /dev/null +++ b/examples/queue/index.ts @@ -0,0 +1,31 @@ +/* tslint:disable: no-console */ +import {Dealer} from "zeromq" + +import {Queue} from "./queue" + +async function main() { + const sender = new Dealer() + await sender.bind("tcp://127.0.0.1:5555") + + const queue = new Queue(sender) + queue.send("hello") + queue.send("world!") + queue.send(null) + + const receiver = new Dealer() + receiver.connect("tcp://127.0.0.1:5555") + + for await (const [msg] of receiver) { + if (msg.length === 0) { + receiver.close() + console.log("received: ") + } else { + console.log(`received: ${msg}`) + } + } +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/examples/queue/queue.ts b/examples/queue/queue.ts new file mode 100644 index 00000000..73e3fb5b --- /dev/null +++ b/examples/queue/queue.ts @@ -0,0 +1,32 @@ +import {Socket} from "zeromq" + +export class Queue { + queue: any[] = [] + socket: Socket + max: number + sending: boolean = false + + constructor(socket: Socket, max: number = 100) { + this.socket = socket + this.max = max + } + + send(msg: any) { + if (this.queue.length > this.max) { + throw new Error("Queue is full") + } + this.queue.push(msg) + this.trySend() + } + + async trySend() { + if (this.sending) return + this.sending = true + + while (this.queue.length) { + await this.socket.send(this.queue.shift()) + } + + this.sending = false + } +} diff --git a/examples/smoketest.js b/examples/smoketest.js deleted file mode 100644 index 3408d2e8..00000000 --- a/examples/smoketest.js +++ /dev/null @@ -1,10 +0,0 @@ -// minimal smoke test for appveyor -// while mocha tests don't run -var zmq = require('..'); - -var s = new zmq.socket('pub'); -s.bind('tcp://127.0.0.1:0'); -s.send('test'); -setTimeout(function () { - s.close(); -}, 1000); diff --git a/examples/subber.js b/examples/subber.js deleted file mode 100755 index 155ba18d..00000000 --- a/examples/subber.js +++ /dev/null @@ -1,10 +0,0 @@ -var zmq = require('../'); -var sock = zmq.socket('sub'); - -sock.connect('tcp://127.0.0.1:3000'); -sock.subscribe('kitty cats'); -console.log('Subscriber connected to port 3000'); - -sock.on('message', function(topic, message) { - console.log('received a message related to:', topic.toString(), 'containing message:', message.toString()); -}); diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 00000000..33e4bdc6 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,9 @@ +{ + "include": ["*"], + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "lib": ["es2017", "esnext.asynciterable"], + "strict": true, + } +} diff --git a/examples/worker.js b/examples/worker.js deleted file mode 100755 index c66b641d..00000000 --- a/examples/worker.js +++ /dev/null @@ -1,9 +0,0 @@ -var zmq = require('../'); -var sock = zmq.socket('pull'); - -sock.connect('tcp://127.0.0.1:3000'); -console.log('Worker connected to port 3000'); - -sock.on('message', function(msg){ - console.log('work: %s', msg.toString()); -}); diff --git a/index.js b/index.js deleted file mode 100644 index 13a03e22..00000000 --- a/index.js +++ /dev/null @@ -1,2 +0,0 @@ - -module.exports = require('./lib'); \ No newline at end of file diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index ad7e7d67..00000000 --- a/lib/index.js +++ /dev/null @@ -1,855 +0,0 @@ -/** - * Module dependencies. - */ - -var EventEmitter = require('events').EventEmitter - , zmq = require('../build/Release/zmq.node') - , util = require('util'); - -/** - * Expose bindings as the module. - */ - -exports = module.exports = zmq; - -/** - * Expose zmq version. - */ - -exports.version = zmq.zmqVersion(); - -/** - * Expose zmq_curve_keypair - */ - -exports.curveKeypair = zmq.zmqCurveKeypair; - -/** - * Map of socket types. - */ - -var types = exports.types = { - pub: zmq.ZMQ_PUB - , xpub: zmq.ZMQ_XPUB - , sub: zmq.ZMQ_SUB - , xsub: zmq.ZMQ_XSUB - , req: zmq.ZMQ_REQ - , xreq: zmq.ZMQ_XREQ - , rep: zmq.ZMQ_REP - , xrep: zmq.ZMQ_XREP - , push: zmq.ZMQ_PUSH - , pull: zmq.ZMQ_PULL - , dealer: zmq.ZMQ_DEALER - , router: zmq.ZMQ_ROUTER - , pair: zmq.ZMQ_PAIR - , stream: zmq.ZMQ_STREAM -}; - -var longOptions = { - ZMQ_HWM: 1 - , ZMQ_SWAP: 3 - , ZMQ_AFFINITY: 4 - , ZMQ_IDENTITY: 5 - , ZMQ_SUBSCRIBE: 6 - , ZMQ_UNSUBSCRIBE: 7 - , ZMQ_RATE: 8 - , ZMQ_RECOVERY_IVL: 9 - , ZMQ_MCAST_LOOP: 10 - , ZMQ_SNDBUF: 11 - , ZMQ_RCVBUF: 12 - , ZMQ_RCVMORE: 13 - , ZMQ_FD: 14 - , ZMQ_EVENTS: 15 - , ZMQ_TYPE: 16 - , ZMQ_LINGER: 17 - , ZMQ_RECONNECT_IVL: 18 - , ZMQ_BACKLOG: 19 - , ZMQ_RECOVERY_IVL_MSEC: 20 - , ZMQ_RECONNECT_IVL_MAX: 21 - , ZMQ_MAXMSGSIZE: 22 - , ZMQ_SNDHWM: 23 - , ZMQ_RCVHWM: 24 - , ZMQ_MULTICAST_HOPS: 25 - , ZMQ_RCVTIMEO: 27 - , ZMQ_SNDTIMEO: 28 - , ZMQ_IPV4ONLY: 31 - , ZMQ_LAST_ENDPOINT: 32 - , ZMQ_ROUTER_MANDATORY: 33 - , ZMQ_TCP_KEEPALIVE: 34 - , ZMQ_TCP_KEEPALIVE_CNT: 35 - , ZMQ_TCP_KEEPALIVE_IDLE: 36 - , ZMQ_TCP_KEEPALIVE_INTVL: 37 - , ZMQ_TCP_ACCEPT_FILTER: 38 - , ZMQ_DELAY_ATTACH_ON_CONNECT: 39 - , ZMQ_XPUB_VERBOSE: 40 - , ZMQ_ROUTER_RAW: 41 - , ZMQ_IPV6: 42 - , ZMQ_MECHANISM: 43 - , ZMQ_PLAIN_SERVER: 44 - , ZMQ_PLAIN_USERNAME: 45 - , ZMQ_PLAIN_PASSWORD: 46 - , ZMQ_CURVE_SERVER: 47 - , ZMQ_CURVE_PUBLICKEY: 48 - , ZMQ_CURVE_SECRETKEY: 49 - , ZMQ_CURVE_SERVERKEY: 50 - , ZMQ_ZAP_DOMAIN: 55 - , ZMQ_HEARTBEAT_IVL: 75 - , ZMQ_HEARTBEAT_TTL: 76 - , ZMQ_HEARTBEAT_TIMEOUT: 77 - , ZMQ_CONNECT_TIMEOUT: 79 - , ZMQ_IO_THREADS: 1 - , ZMQ_MAX_SOCKETS: 2 - , ZMQ_ROUTER_HANDOVER: 56 -}; - -Object.keys(longOptions).forEach(function(name){ - Object.defineProperty(zmq, name, { - enumerable: true, - configurable: false, - writable: false, - value: longOptions[name] - }); -}); - -/** - * Map of socket options. - */ - -var opts = exports.options = { - _fd: zmq.ZMQ_FD - , _ioevents: zmq.ZMQ_EVENTS - , _receiveMore: zmq.ZMQ_RCVMORE - , _subscribe: zmq.ZMQ_SUBSCRIBE - , _unsubscribe: zmq.ZMQ_UNSUBSCRIBE - , affinity: zmq.ZMQ_AFFINITY - , backlog: zmq.ZMQ_BACKLOG - , hwm: zmq.ZMQ_HWM - , identity: zmq.ZMQ_IDENTITY - , linger: zmq.ZMQ_LINGER - , mcast_loop: zmq.ZMQ_MCAST_LOOP - , rate: zmq.ZMQ_RATE - , rcvbuf: zmq.ZMQ_RCVBUF - , last_endpoint: zmq.ZMQ_LAST_ENDPOINT - , reconnect_ivl: zmq.ZMQ_RECONNECT_IVL - , recovery_ivl: zmq.ZMQ_RECOVERY_IVL - , sndbuf: zmq.ZMQ_SNDBUF - , swap: zmq.ZMQ_SWAP - , mechanism: zmq.ZMQ_MECHANISM - , plain_server: zmq.ZMQ_PLAIN_SERVER - , plain_username: zmq.ZMQ_PLAIN_USERNAME - , plain_password: zmq.ZMQ_PLAIN_PASSWORD - , curve_server: zmq.ZMQ_CURVE_SERVER - , curve_publickey: zmq.ZMQ_CURVE_PUBLICKEY - , curve_secretkey: zmq.ZMQ_CURVE_SECRETKEY - , curve_serverkey: zmq.ZMQ_CURVE_SERVERKEY - , zap_domain: zmq.ZMQ_ZAP_DOMAIN - , heartbeat_ivl: zmq.ZMQ_HEARTBEAT_IVL - , heartbeat_ttl: zmq.ZMQ_HEARTBEAT_TTL - , heartbeat_timeout: zmq.ZMQ_HEARTBEAT_TIMEOUT - , connect_timeout: zmq.ZMQ_CONNECT_TIMEOUT -}; - -/** - * Monitor events - */ -var events = exports.events = { - 1: "connect" // zmq.ZMQ_EVENT_CONNECTED - , 2: "connect_delay" // zmq.ZMQ_EVENT_CONNECT_DELAYED - , 4: "connect_retry" // zmq.ZMQ_EVENT_CONNECT_RETRIED - , 8: "listen" // zmq.ZMQ_EVENT_LISTENING - , 16: "bind_error" // zmq.ZMQ_EVENT_BIND_FAILED - , 32: "accept" // zmq.ZMQ_EVENT_ACCEPTED - , 64: "accept_error" // zmq.ZMQ_EVENT_ACCEPT_FAILED - , 128: "close" // zmq.ZMQ_EVENT_CLOSED - , 256: "close_error" // zmq.ZMQ_EVENT_CLOSE_FAILED - , 512: "disconnect" // zmq.ZMQ_EVENT_DISCONNECTED -} - -// Context management happens here. We lazily initialize a default context, -// and use that everywhere. Also cleans up on exit. -var ctx; -function defaultContext() { - if (ctx) return ctx; - - var io_threads = 1; - if (process.env.ZMQ_IO_THREADS) { - io_threads = parseInt(process.env.ZMQ_IO_THREADS, 10); - if (!io_threads || io_threads < 1) { - console.warn('Invalid number in ZMQ_IO_THREADS, using 1 IO thread.'); - io_threads = 1; - } - } - - ctx = new zmq.Context(io_threads); - process.on('exit', function(){ - // ctx.close(); - ctx = null; - }); - - return ctx; -}; - -/** - * A batch consists of 1 or more message parts with their flags that need to be sent as one unit - */ - -function OutBatch() { - this.content = []; // buf, flags, buf, flags, ... - this.cbs = []; // callbacks - this.isClosed = false; // true if the last message does not have SNDMORE in its flags, false otherwise - this.next = null; // next batch (for linked list of batches) -} - -OutBatch.prototype.append = function (buf, flags, cb) { - if (!Buffer.isBuffer(buf)) { - buf = Buffer.from(String(buf), 'utf8'); - } - - this.content.push(buf, flags); - - if (cb) { - this.cbs.push(cb); - } - - if ((flags & zmq.ZMQ_SNDMORE) === 0) { - this.isClosed = true; - } -}; - -OutBatch.prototype.invokeError = function (socket, error) { - var returned = false; - for (var i = 0; i < this.cbs.length; i += 1) { - this.cbs[i].call(socket, error); - returned = true; - } - - if (!returned) { - throw error; - } -}; - -OutBatch.prototype.invokeSent = function (socket) { - for (var i = 0; i < this.cbs.length; i += 1) { - this.cbs[i].call(socket); - } -}; - - -function BatchList() { - this.firstBatch = null; - this.lastBatch = null; - this.length = 0; -} - -BatchList.prototype.canSend = function () { - return this.firstBatch ? this.firstBatch.isClosed : false; -}; - -BatchList.prototype.append = function (buf, flags, cb) { - var batch = this.lastBatch; - - if (!batch || batch.isClosed) { - batch = new OutBatch(); - - if (this.lastBatch) { - this.lastBatch.next = batch; - } - - this.lastBatch = batch; - - if (!this.firstBatch) { - this.firstBatch = batch; - } - - this.length += 1; - } - - batch.append(buf, flags, cb); -}; - -BatchList.prototype.fetch = function () { - var batch = this.firstBatch; - if (batch && batch.isClosed) { - this.firstBatch = batch.next; - this.length -= 1; - return batch; - } - return undefined; -}; - -BatchList.prototype.restore = function (batch) { - this.firstBatch = batch; - this.length += 1; -}; - - -/** - * Create a new socket of the given `type`. - * - * @constructor - * @param {String|Number} type - * @api public - */ - -var Socket = -exports.Socket = function (type) { - var self = this; - EventEmitter.call(this); - this.type = type; - this._zmq = new zmq.SocketBinding(defaultContext(), types[type]); - this._paused = false; - this._isFlushingReads = false; - this._isFlushingWrites = false; - this._outgoing = new BatchList(); - - this._zmq.onReadReady = function () { - setImmediate(function(){ - self._flushReads(); - }); - }; - - this._zmq.onSendReady = function () { - self._flushWrites(); - }; -}; - -/** - * Inherit from `EventEmitter.prototype`. - */ - -util.inherits(Socket, EventEmitter); - -/** - * Set socket to pause mode - * no data will be emit until resume() is called - * all send() calls will be queued - * - * @api public - */ - -Socket.prototype.pause = function() { - this._paused = true; -} - -/** - * Set a socket back to normal work mode - * - * @api public - */ - -Socket.prototype.resume = function() { - this._paused = false; - this._flushReads(); - this._flushWrites(); -} - -Socket.prototype.ref = function() { - this._zmq.ref(); -} - -Socket.prototype.unref = function() { - this._zmq.unref(); -} - -Socket.prototype.read = function() { - var message = [], flags; - - if (this._zmq.state !== zmq.STATE_READY) { - return null; - } - - flags = this._zmq.getsockopt(zmq.ZMQ_EVENTS); - - if (flags & zmq.ZMQ_POLLIN) { - do { - message.push(this._zmq.recv()); - } while (this._zmq.getsockopt(zmq.ZMQ_RCVMORE)); - - return message; - } - - return null; -} - - -/** - * Set `opt` to `val`. - * - * @param {String|Number} opt - * @param {Mixed} val - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.setsockopt = function(opt, val){ - this._zmq.setsockopt(opts[opt] || opt, val); - return this; -}; - -/** - * Get socket `opt`. - * - * @param {String|Number} opt - * @return {Mixed} - * @api public - */ - -Socket.prototype.getsockopt = function(opt){ - return this._zmq.getsockopt(opts[opt] || opt); -}; - -/** - * Socket opt accessors allowing `sock.backlog = val` - * instead of `sock.setsockopt('backlog', val)`. - */ - -Object.keys(opts).forEach(function(name){ - Socket.prototype.__defineGetter__(name, function() { - return this._zmq.getsockopt(opts[name]); - }); - - Socket.prototype.__defineSetter__(name, function(val) { - if ('string' == typeof val) val = Buffer.from(val, 'utf8'); - return this._zmq.setsockopt(opts[name], val); - }); -}); - -/** - * Return true if socket state is closed. - */ -Socket.prototype.__defineGetter__("closed", function() { - return this._zmq.state === zmq.STATE_CLOSED; -}); - -/** - * Async bind. - * - * Emits the "bind" event. - * - * @param {String} addr - * @param {Function} cb - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.bind = function(addr, cb) { - var self = this; - this._zmq.bind(addr, function(err) { - if (err) { - return cb && cb(err); - } - - self._flushReads(); - self._flushWrites(); - - self.emit('bind', addr); - cb && cb(); - }); - return this; -}; - -/** - * Sync bind. - * - * @param {String} addr - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.bindSync = function(addr) { - this._zmq.bindSync(addr); - - return this; -}; - -/** - * Async unbind. - * - * Emits the "unbind" event. - * - * @param {String} addr - * @param {Function} cb - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.unbind = function(addr, cb) { - if (zmq.ZMQ_CAN_UNBIND) { - var self = this; - this._zmq.unbind(addr, function(err) { - if (err) { - return cb && cb(err); - } - self.emit('unbind', addr); - - self._flushReads(); - self._flushWrites(); - cb && cb(); - }); - } else { - cb && cb(); - } - return this; -}; - -/** - * Sync unbind. - * - * @param {String} addr - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.unbindSync = function(addr) { - if (zmq.ZMQ_CAN_UNBIND) { - this._zmq.unbindSync(addr); - } - return this; -} - -/** - * Connect to `addr`. - * - * @param {String} addr - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.connect = function(addr) { - this._zmq.connect(addr); - return this; -}; - -/** - * Disconnect from `addr`. - * - * @param {String} addr - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.disconnect = function(addr) { - if (zmq.ZMQ_CAN_DISCONNECT) { - this._zmq.disconnect(addr); - } - return this; -}; - -/** - * Enable monitoring of a Socket - * - * @param {Number} timer interval in ms > 0 or Undefined for default - * @param {Number} The maximum number of events to read on each interval, default is 1, use 0 for reading all events - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.monitor = function(interval, numOfEvents) { - if (zmq.ZMQ_CAN_MONITOR) { - var self = this; - - self._zmq.onMonitorEvent = function(event_id, event_value, event_endpoint_addr, ex) { - self.emit(events[event_id], event_value, event_endpoint_addr, ex); - } - - self._zmq.onMonitorError = function(error) { - self.emit('monitor_error', error); - } - - this._zmq.monitor(interval, numOfEvents); - } else { - throw new Error('Monitoring support disabled check zmq version is > 3.2.1 and recompile this addon'); - } - return this; -}; - -/** - * Disable monitoring of a Socket release idle handler - * and close the socket - * - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.unmonitor = function() { - if (zmq.ZMQ_CAN_MONITOR) { - this._zmq.unmonitor(); - } - return this; -}; - - -/** - * Subscribe with the given `filter`. - * - * @param {String} filter - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.subscribe = function(filter) { - this._subscribe = filter; - return this; -}; - -/** - * Unsubscribe with the given `filter`. - * - * @param {String} filter - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.unsubscribe = function(filter) { - this._unsubscribe = filter; - return this; -}; - - -/** - * Send the given `msg`. - * - * @param {String|Buffer|Array} msg - * @param {Number} [flags] - * @param {Function} [cb] - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.send = function(msg, flags, cb) { - flags = flags | 0; - - if (Array.isArray(msg)) { - for (var i = 0, len = msg.length; i < len; i++) { - var isLast = i === len - 1; - var msgFlags = isLast ? flags : flags | zmq.ZMQ_SNDMORE; - var callback = isLast ? cb : undefined; - - this._outgoing.append(msg[i], msgFlags, callback); - } - } else { - this._outgoing.append(msg, flags, cb); - } - - if (this._outgoing.canSend()) { - this._zmq.pending = true; - this._flushWrites(); - } else { - this._zmq.pending = false; - } - - return this; -}; - -Socket.prototype._emitMessage = function (message) { - if (message.length === 1) { - // hot path - this.emit('message', message[0]); - } else { - this.emit.apply(this, ['message'].concat(message)); - } -} - -Socket.prototype._flushRead = function () { - try { - var message = this._zmq.readv(); // can throw - if (!message) { - return false; - } - // Handle received message immediately to prevent memory leak in driver - this._emitMessage(message) - } catch (error) { - this.emit('error', error); // can throw - } - return true; -}; - -Socket.prototype._flushWrite = function () { - var batch = this._outgoing.fetch(); - if (!batch) { - this._zmq.pending = false; - return false; - } - - try { - if (this._zmq.sendv(batch.content)) { - this._zmq.pending = this._outgoing.canSend(); - batch.invokeSent(this); - return true; - } - - this._outgoing.restore(batch); - return false; - } catch (sendError) { - this._zmq.pending = this._outgoing.canSend(); - batch.invokeError(this, sendError); // can throw - return false; - } -}; - - -Socket.prototype._flushReads = function() { - if (this._paused || this._isFlushingReads) return; - - this._isFlushingReads = true; - - while (this._flushRead()); - - this._isFlushingReads = false; - - // if many sends happened, but ended up in the queue (eg. in a req/rep scenario where each send must be followed by a - // response), we can try to send again now - - this._flushWrites(); -}; - -Socket.prototype._flushWrites = function() { - if (this._paused || this._isFlushingWrites) return; - - this._isFlushingWrites = true; - - var sent; - - do { - try { - sent = this._flushWrite(); - } catch (error) { - this._isFlushingWrites = false; - this.emit('error', error); // can throw - return; - } - } while (sent); - - this._isFlushingWrites = false; -}; - -/** - * Close the socket. - * - * @return {Socket} for chaining - * @api public - */ - -Socket.prototype.close = function() { - this._zmq.close(); - return this; -}; - -/** - * Create a `type` socket with the given `options`. - * - * @param {String} type - * @param {Object} options - * @return {Socket} - * @api public - */ - -exports.socket = -exports.createSocket = function(type, options) { - var sock = new Socket(type); - for (var key in options) sock[key] = options[key]; - return sock; -}; - -exports.Context.setMaxThreads = function(value) { - if (!zmq.ZMQ_CAN_SET_CTX) { - throw new Error('Setting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon'); - } - var defaultCtx = defaultContext(); - defaultCtx.setOpt(zmq.ZMQ_IO_THREADS, value); -}; - -exports.Context.getMaxThreads = function() { - if (!zmq.ZMQ_CAN_SET_CTX) { - throw new Error('Getting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon'); - } - var defaultCtx = defaultContext(); - return defaultCtx.getOpt(zmq.ZMQ_IO_THREADS); -}; - -exports.Context.setMaxSockets = function(value) { - if (!zmq.ZMQ_CAN_SET_CTX) { - throw new Error('Setting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon'); - } - var defaultCtx = defaultContext(); - defaultCtx.setOpt(zmq.ZMQ_MAX_SOCKETS, value); -}; - -exports.Context.getMaxSockets = function() { - if (!zmq.ZMQ_CAN_SET_CTX) { - throw new Error('Getting of context options disabled, check zmq version is >= 3.2.1 and recompile this addon'); - } - var defaultCtx = defaultContext(); - return defaultCtx.getOpt(zmq.ZMQ_MAX_SOCKETS); -}; - -/** - * JS based on API characteristics of the native zmq_proxy() - */ - -function proxy (frontend, backend, capture){ - switch(frontend.type+'/'+backend.type){ - case 'push/pull': - case 'pull/push': - case 'xpub/xsub': - if(capture){ - - frontend.on('message',function (){ - backend.send([].slice.call(arguments)); - }); - - backend.on('message',function (){ - frontend.send([].slice.call(arguments)); - - //forwarding messages over capture socket - capture.send([].slice.call(arguments)); - }); - - } else { - - //no capture socket provided, just forwarding msgs to respective sockets - frontend.on('message',function (){ - backend.send([].slice.call(arguments)); - }); - - backend.on('message',function (){ - frontend.send([].slice.call(arguments)); - }); - - } - break; - case 'router/dealer': - case 'xrep/xreq': - if(capture){ - - //forwarding router/dealer pack signature: id, delimiter, msg - frontend.on('message',function (id,delimiter,msg){ - backend.send([].slice.call(arguments)); - }); - - backend.on('message',function (id,delimiter,msg){ - frontend.send([].slice.call(arguments)); - - //forwarding message to the capture socket - capture.send([].slice.call(arguments, 2)); - }); - - } else { - - //forwarding router/dealer signatures without capture - frontend.on('message',function (id,delimiter,msg){ - backend.send([].slice.call(arguments)); - }); - - backend.on('message',function (id,delimiter,msg){ - frontend.send([].slice.call(arguments)); - }); - - } - break; - default: - throw new Error('wrong socket order to proxy'); - } -} - -exports.proxy = proxy; diff --git a/package.json b/package.json index e78bc32f..3bf21730 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,65 @@ { "name": "zeromq", - "version": "5.1.0", - "description": "ZeroMQ for node.js", - "main": "index", + "version": "6.0.0-beta.1", + "description": "Next-generation ZeroMQ bindings for Node.js", + "main": "lib/index.js", + "types": "lib/index.d.ts", "gypfile": true, "repository": { "type": "git", "url": "https://github.com/zeromq/zeromq.js.git" }, "dependencies": { - "nan": "^2.10.0", - "prebuild-install": "5.2.1" + "node-addon-api": "^1.7.1", + "node-gyp-build": "^4.1.0" }, "devDependencies": { - "electron-mocha": "^6.0.0", - "jsdoc": "^3.5.4", - "mocha": "^5.0.0", - "nyc": "^12.0.2", - "prebuild": "^8.1.0", - "semver": "^5.4.1", - "should": "^13.0.0" + "@gnd/typedoc": "^0.15.0-0", + "@types/chai": ">= 4.1", + "@types/mocha": ">= 5.2", + "@types/node": ">= 8.0", + "@types/semver": ">= 0", + "benchmark": ">= 0", + "chai": ">= 4.1", + "choma": ">= 1.2", + "gunzip-maybe": "^1.4.1", + "mocha": ">= 4.0", + "node-fetch": "^2.6.0", + "prebuildify": "^3.0", + "semver": ">= 0", + "tar-fs": "^2.0.0", + "ts-node": ">= 7", + "tslint": "^5.20.0", + "typescript": ">= 3", + "weak-napi": ">= 1.0" }, "engines": { - "node": ">=6.0" + "node": ">= 10" }, + "files": [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "binding.gyp", + "compat.js", + "compat.d.ts", + "draft.js", + "draft.d.ts", + "lib/*.d.ts", + "lib/*.js", + "src/util/*.h", + "src/*.cc", + "src/*.h", + "script/build.sh", + "prebuilds" + ], "scripts": { - "build:libzmq": "node scripts/preinstall.js", - "install": "node scripts/prebuild-install.js || (node scripts/preinstall.js && node-gyp rebuild)", - "prebuild": "prebuild --all --strip", - "build:docs": "jsdoc -R README.md -d docs lib/*.js", - "test": "mocha --expose-gc --slow 300", - "test:electron": "electron-mocha --slow 300", - "precoverage": "nyc npm run test", - "coverage": "nyc report --reporter=text-lcov > coverage.lcov" + "install": "node-gyp-build", + "ci:compile": "tsc --project tsconfig-build.json", + "ci:doc": "typedoc --out docs --name zeromq.js --excludeProtected --excludePrivate --excludeNotExported --excludeExternals --externalPattern 'src/+(draft|native|compat).ts' --tsconfig tsconfig-build.json --mode file", + "ci:prebuild": "prebuildify --napi --strip", + "dev:test": "tsc --project tsconfig-build.json && mocha && tslint -t stylish --project . && script/format.sh && rm -f tmp/*", + "dev:bench": "node --expose-gc test/bench" }, "keywords": [ "zeromq", @@ -40,10 +67,17 @@ "0mq", "ømq", "libzmq", + "zmtp", + "message", + "messaging", + "queue", + "async", + "sockets", "native", "binding", - "addon" + "addon", + "napi" ], "license": "MIT", - "author": "Justin Tulloss (http://justin.harmonize.fm)" + "author": "Rolf Timmermans " } diff --git a/script/build.sh b/script/build.sh new file mode 100755 index 00000000..002953f9 --- /dev/null +++ b/script/build.sh @@ -0,0 +1,68 @@ +#!/bin/sh +set -e + +ZMQ_VERSION=${ZMQ_VERSION:-"4.3.2"} + +SRC_URL="https://github.com/zeromq/libzmq/releases/download/v${ZMQ_VERSION}/zeromq-${ZMQ_VERSION}.tar.gz" +SRC_DIR="zeromq-${ZMQ_VERSION}" +TARBALL="zeromq-${ZMQ_VERSION}.tar.gz" +BUILD_OPTIONS="" + +if [ -n "${WINDIR}" ]; then + # Working directory is NAPI temporary build directory. + PATH_PREFIX="${PWD}/libzmq" + ARTIFACT="${PATH_PREFIX}/lib/libzmq.lib" + CMAKE_GENERATOR="Visual Studio 15 2017" + TOOLSET_VERSION="141" + + # In Travis CI, Node paths are: + # - C:\ProgramData\nvs\node\\x64\node.exe + # - C:\ProgramData\nvs\node\\x86\node.exe + if [[ "${NODE}" != *"x86"* ]]; then + # Target Windows x64 platform. + CMAKE_GENERATOR="${CMAKE_GENERATOR} Win64" + fi +else + # Working directory is project root. + PATH_PREFIX="${PWD}/build/libzmq" + ARTIFACT="${PATH_PREFIX}/lib/libzmq.a" + CMAKE_GENERATOR="Unix Makefiles" + + export MACOSX_DEPLOYMENT_TARGET=10.9 +fi + +mkdir -p "${PATH_PREFIX}" && cd "${PATH_PREFIX}" + +if [ -f "${ARTIFACT}" ]; then + echo "Found previously built libzmq; skipping rebuild..." +else + if [ -f "${TARBALL}" ]; then + echo "Found libzmq source; skipping download..." + else + echo "Downloading libzmq source..." + curl "${SRC_URL}" -fsSL -o "${TARBALL}" + fi + + test -d "${SRC_DIR}" || tar xzf "${TARBALL}" + + if [ "${npm_config_zmq_draft}" = "true" ]; then + echo "Building libzmq (with draft support)..." + BUILD_OPTIONS="-DENABLE_DRAFTS=ON ${BUILD_OPTIONS}" + else + echo "Building libzmq..." + fi + + # ClangFormat include causes issues but is not required to build. + if [ -f "${SRC_DIR}/builds/cmake/Modules/ClangFormat.cmake" ]; then + echo > "${SRC_DIR}/builds/cmake/Modules/ClangFormat.cmake" + fi + + cmake -G "${CMAKE_GENERATOR}" "${BUILD_OPTIONS}" -DCMAKE_INSTALL_PREFIX="${PATH_PREFIX}" -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_STATIC=ON -DBUILD_TESTS=OFF -DBUILD_SHARED=OFF "${SRC_DIR}" + + if [ -n "${WINDIR}" ]; then + cmake --build . --config Release --target install -- -verbosity:Minimal -maxcpucount + mv "${PATH_PREFIX}/lib/libzmq-v${TOOLSET_VERSION}-mt-s-${ZMQ_VERSION//./_}.lib" "${PATH_PREFIX}/lib/libzmq.lib" + else + cmake --build . --config Release --target install -- -j5 + fi +fi diff --git a/script/ci/alpine-chroot-install.sh b/script/ci/alpine-chroot-install.sh new file mode 100755 index 00000000..ab444010 --- /dev/null +++ b/script/ci/alpine-chroot-install.sh @@ -0,0 +1,392 @@ +#!/bin/sh +# https://github.com/alpinelinux/alpine-chroot-install/blob/master/alpine-chroot-install +# vim: set ts=4: +#---help--- +# Usage: alpine-chroot-install [options] +# +# This script installs Alpine Linux into a chroot and optionally sets up +# qemu-user and binfmt to emulate different architecture (e.g. armhf). +# +# If qemu-user and binfmt is needed, the script checks if both are available +# and qemu-user has version >= 2.6. If not, it tries to install them using +# apt-get. Beside this the script should work on any Linux system. +# +# It also creates script "enter-chroot" inside the chroot directory, that may +# be used to enter the chroot environment. That script do the following: +# +# 1. saves environment variables specified by $CHROOT_KEEP_VARS and PWD, +# 2. chroots into $CHROOT_DIR, +# 3. starts clean environment using "env -i", +# 4. switches user and simulates full login using "su -l", +# 5. loads saved environment variables and changes directory to saved PWD, +# 6. executes specified command or "sh" if not provided. +# +# Example: +# sudo alpine-chroot-install -d /alpine -p build-base -p cmake +# /alpine/enter-chroot -u $USER ./build +# +# Options and environment variables: +# -a ARCH CPU architecture for the chroot. If not set, then it's +# the same as the host's architecture. If it's different +# from the host's architecture, then it will be emulated +# using qemu-user. Options: x86_64, x86, aarch64, armhf, +# ppc64le, s390x. +# +# -b ALPINE_BRANCH Alpine branch to install (default is v3.8). +# +# -d CHROOT_DIR Absolute path to the directory where Alpine chroot +# should be installed (default is /alpine). +# +# -i BIND_DIR Absolute path to the directory on the host system that +# should be mounted on the same path inside the chroot +# (default is PWD, if it's under /home, or none). +# +# -k CHROOT_KEEP_VARS... Names of the environment variables to pass from the +# host environment into chroot by the enter-chroot +# script. Name may be an extended regular expression. +# Default: ARCH CI QEMU_EMULATOR TRAVIS_.*. +# +# -m ALPINE_MIRROR... URI of the Aports mirror to fetch packages from +# (default is https://nl.alpinelinux.org/alpine). +# +# -p ALPINE_PACKAGES... Alpine packages to install into the chroot (default is +# build-base ca-certificates ssl_client). +# +# -r EXTRA_REPOS... Alpine repositories to be added to +# /etc/apk/repositories (main and community from +# $ALPINE_MIRROR are always added). +# +# -t TEMP_DIR Absolute path to the directory where to store temporary +# files (default is /tmp/alpine). +# +# -h Show this help message and exit. +# +# -v Print version and exit. +# +# APK_TOOLS_URI URL of static apk-tools tarball to download. +# Default is x86_64 apk-tools from +# https://github.com/alpinelinux/apk-tools/releases. +# +# APK_TOOLS_SHA256 SHA-256 checksum of $APK_TOOLS_URI. +# +# Each option can be also provided by environment variable. If both option and +# variable is specified and the option accepts only one argument, then the +# option takes precedence. +# +# https://github.com/alpinelinux/alpine-chroot-install +#---help--- +set -eu + +#======================= C o n s t a n t s =======================# + +: ${APK_TOOLS_URI:="https://github.com/alpinelinux/apk-tools/releases/download/v2.10.0/apk-tools-2.10.0-x86_64-linux.tar.gz"} +: ${APK_TOOLS_SHA256:="77f2d256fcd5d6fdafadf43bb6a9c85c3da7bb471ee842dcd729175235cb9fed"} + +# Alpine APK keys for packages verification. +ALPINE_KEYS=' +4a6a0840:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1yHJxQgsHQREclQu4Ohe\nqxTxd1tHcNnvnQTu/UrTky8wWvgXT+jpveroeWWnzmsYlDI93eLI2ORakxb3gA2O\nQ0Ry4ws8vhaxLQGC74uQR5+/yYrLuTKydFzuPaS1dK19qJPXB8GMdmFOijnXX4SA\njixuHLe1WW7kZVtjL7nufvpXkWBGjsfrvskdNA/5MfxAeBbqPgaq0QMEfxMAn6/R\nL5kNepi/Vr4S39Xvf2DzWkTLEK8pcnjNkt9/aafhWqFVW7m3HCAII6h/qlQNQKSo\nGuH34Q8GsFG30izUENV9avY7hSLq7nggsvknlNBZtFUcmGoQrtx3FmyYsIC8/R+B\nywIDAQAB +5243ef4b:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvNijDxJ8kloskKQpJdx+\nmTMVFFUGDoDCbulnhZMJoKNkSuZOzBoFC94omYPtxnIcBdWBGnrm6ncbKRlR+6oy\nDO0W7c44uHKCFGFqBhDasdI4RCYP+fcIX/lyMh6MLbOxqS22TwSLhCVjTyJeeH7K\naA7vqk+QSsF4TGbYzQDDpg7+6aAcNzg6InNePaywA6hbT0JXbxnDWsB+2/LLSF2G\nmnhJlJrWB1WGjkz23ONIWk85W4S0XB/ewDefd4Ly/zyIciastA7Zqnh7p3Ody6Q0\nsS2MJzo7p3os1smGjUF158s6m/JbVh4DN6YIsxwl2OjDOz9R0OycfJSDaBVIGZzg\ncQIDAQAB +524d27bb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr8s1q88XpuJWLCZALdKj\nlN8wg2ePB2T9aIcaxryYE/Jkmtu+ZQ5zKq6BT3y/udt5jAsMrhHTwroOjIsF9DeG\ne8Y3vjz+Hh4L8a7hZDaw8jy3CPag47L7nsZFwQOIo2Cl1SnzUc6/owoyjRU7ab0p\niWG5HK8IfiybRbZxnEbNAfT4R53hyI6z5FhyXGS2Ld8zCoU/R4E1P0CUuXKEN4p0\n64dyeUoOLXEWHjgKiU1mElIQj3k/IF02W89gDj285YgwqA49deLUM7QOd53QLnx+\nxrIrPv3A+eyXMFgexNwCKQU9ZdmWa00MjjHlegSGK8Y2NPnRoXhzqSP9T9i2HiXL\nVQIDAQAB +5261cecb:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwlzMkl7b5PBdfMzGdCT0\ncGloRr5xGgVmsdq5EtJvFkFAiN8Ac9MCFy/vAFmS8/7ZaGOXoCDWbYVLTLOO2qtX\nyHRl+7fJVh2N6qrDDFPmdgCi8NaE+3rITWXGrrQ1spJ0B6HIzTDNEjRKnD4xyg4j\ng01FMcJTU6E+V2JBY45CKN9dWr1JDM/nei/Pf0byBJlMp/mSSfjodykmz4Oe13xB\nCa1WTwgFykKYthoLGYrmo+LKIGpMoeEbY1kuUe04UiDe47l6Oggwnl+8XD1MeRWY\nsWgj8sF4dTcSfCMavK4zHRFFQbGp/YFJ/Ww6U9lA3Vq0wyEI6MCMQnoSMFwrbgZw\nwwIDAQAB +58199dcc:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3v8/ye/V/t5xf4JiXLXa\nhWFRozsnmn3hobON20GdmkrzKzO/eUqPOKTpg2GtvBhK30fu5oY5uN2ORiv2Y2ht\neLiZ9HVz3XP8Fm9frha60B7KNu66FO5P2o3i+E+DWTPqqPcCG6t4Znk2BypILcit\nwiPKTsgbBQR2qo/cO01eLLdt6oOzAaF94NH0656kvRewdo6HG4urbO46tCAizvCR\nCA7KGFMyad8WdKkTjxh8YLDLoOCtoZmXmQAiwfRe9pKXRH/XXGop8SYptLqyVVQ+\ntegOD9wRs2tOlgcLx4F/uMzHN7uoho6okBPiifRX+Pf38Vx+ozXh056tjmdZkCaV\naQIDAQAB +58cbb476:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoSPnuAGKtRIS5fEgYPXD\n8pSGvKAmIv3A08LBViDUe+YwhilSHbYXUEAcSH1KZvOo1WT1x2FNEPBEFEFU1Eyc\n+qGzbA03UFgBNvArurHQ5Z/GngGqE7IarSQFSoqewYRtFSfp+TL9CUNBvM0rT7vz\n2eMu3/wWG+CBmb92lkmyWwC1WSWFKO3x8w+Br2IFWvAZqHRt8oiG5QtYvcZL6jym\nY8T6sgdDlj+Y+wWaLHs9Fc+7vBuyK9C4O1ORdMPW15qVSl4Lc2Wu1QVwRiKnmA+c\nDsH/m7kDNRHM7TjWnuj+nrBOKAHzYquiu5iB3Qmx+0gwnrSVf27Arc3ozUmmJbLj\nzQIDAQAB +58e4f17d:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvBxJN9ErBgdRcPr5g4hV\nqyUSGZEKuvQliq2Z9SRHLh2J43+EdB6A+yzVvLnzcHVpBJ+BZ9RV30EM9guck9sh\nr+bryZcRHyjG2wiIEoduxF2a8KeWeQH7QlpwGhuobo1+gA8L0AGImiA6UP3LOirl\nI0G2+iaKZowME8/tydww4jx5vG132JCOScMjTalRsYZYJcjFbebQQolpqRaGB4iG\nWqhytWQGWuKiB1A22wjmIYf3t96l1Mp+FmM2URPxD1gk/BIBnX7ew+2gWppXOK9j\n1BJpo0/HaX5XoZ/uMqISAAtgHZAqq+g3IUPouxTphgYQRTRYpz2COw3NF43VYQrR\nbQIDAQAB' + +# Minimal required version of QEMU emulator. +QEMU_MIN_VER='2.6' + +# Name of Ubuntu release to install qemu-user-static from if package for +# the host system is older than $QEMU_MIN_VER. +QEMU_UBUNTU_REL='artful' + +# Version of alpine-chroot-install script. +VERSION='0.9.0' + + +#======================= F u n c t i o n s =======================# + +die() { + printf '\033[1;31mERROR:\033[0m %s\n' "$@" >&2 # bold red + exit 1 +} + +einfo() { + printf '\n\033[1;36m> %s\033[0m\n' "$@" >&2 # bold cyan +} + +ewarn() { + printf '\033[1;33m> %s\033[0m\n' "$@" >&2 # bold yellow +} + +# Writes Alpine APK keys embedded in this script into directory $1. +dump_alpine_keys() { + local dest_dir="$1" + local content id line + + mkdir -p "$dest_dir" + for line in $ALPINE_KEYS; do + id=${line%%:*} + content=${line#*:} + + printf -- "-----BEGIN PUBLIC KEY-----\n$content\n-----END PUBLIC KEY-----\n" \ + > "$dest_dir/alpine-devel@lists.alpinelinux.org-$id.rsa.pub" + done +} + +normalize_arch() { + case "$1" in + x86 | i[3456]86) echo 'i386';; + armhf | armv[4-9]) echo 'arm';; + *) echo "$1";; + esac +} + +wgets() ( + local url="$1" + local sha256="$2" + local dest="${3:-.}" + + mkdir -p "$dest" \ + && cd "$dest" \ + && rm -f "${url##*/}" \ + && wget -T 10 --no-verbose "$url" \ + && echo "$sha256 ${url##*/}" | sha256sum -c +) + +usage() { + sed -En '/^#---help---/,/^#---help---/p' "$0" | sed -E 's/^# ?//; 1d;$d;' +} + +gen_chroot_script() { + cat <<-EOF + #!/bin/sh + set -e + + ENV_FILTER_REGEX='($(echo "$CHROOT_KEEP_VARS" | tr -s ' ' '|'))' + EOF + if [ -n "$QEMU_EMULATOR" ]; then + printf 'export QEMU_EMULATOR="%s"' "$QEMU_EMULATOR" + fi + cat <<-'EOF' + + user='root' + if [ $# -ge 2 ] && [ "$1" = '-u' ]; then + user="$2"; shift 2 + fi + oldpwd="$(pwd)" + [ "$(id -u)" -eq 0 ] || _sudo='sudo' + + tmpfile="$(mktemp)" + chmod 644 "$tmpfile" + export | sed -En "s/^([^=]+ ${ENV_FILTER_REGEX}=)('.*'|\".*\")$/\1\3/p" > "$tmpfile" || true + + cd "$(dirname "$0")" + $_sudo mv "$tmpfile" env.sh + $_sudo chroot . /usr/bin/env -i su -l "$user" \ + sh -c ". /etc/profile; . /env.sh; cd '$oldpwd' 2>/dev/null; \"\$@\"" \ + -- "${@:-sh}" + EOF + # NOTE: ash does not load login profile when run with QEMU user-mode + # emulation (I have no clue why), that's why /etc/profile is sourced here. +} + +#------------------------- Debian/Ubuntu ---------------------------# + +alias cmp_versions='dpkg --compare-versions' +alias apt_install='apt-get install -y --no-install-recommends' + +# Adds repository of the specified Ubuntu release to the sources list +# and pins it with priority -1. +# $1: ubuntu release name +add_ubuntu_repo() { + local release="$1" + local source_list="/etc/apt/sources.list.d/ubuntu-$release.list" + + echo "deb http://archive.ubuntu.com/ubuntu $release main universe" >> $source_list + cat >> /etc/apt/preferences.d/ubuntu-$release <<-EOF + Package: * + Pin: release n=$release + Pin-Priority: -1 + EOF + + apt-get -q update -o Dir::Etc::sourcelist="$source_list" +} + +# Prints version of the specified APT package that would be installed. +# $1: package name +apt_pkgver() { + apt-cache policy "$1" | sed -En 's/^\s*Candidate:( [0-9]+:| )(\S+).*/\2/p' +} + +# Prints version of the given QEMU binary. +# $1: qemu binary +qemu_version() { + "$1" --version | sed -En 's/^.*version ([0-9.]+).*/\1/p' +} + +# Installs and enables binfmt-support on Debian/Ubuntu host. +install_binfmt_support() { + apt_install binfmt-support \ + || die 'Failed to install binfmt-support using apt-get!' + + update-binfmts --enable \ + || die 'Failed to enable binfmt!' +} + +# Installs QEMU user mode emulation binaries on Debian/Ubuntu host. +install_qemu_user() { + local target_rel='' + local qemu_ver=$(apt_pkgver qemu-user-static) + + if ! cmp_versions "$qemu_ver" ge "$QEMU_MIN_VER"; then + ewarn "Package qemu-user-static available for your system is too old ($qemu_ver)" + ewarn "Installing newer version from repository of ubuntu $QEMU_UBUNTU_REL..." + + add_ubuntu_repo $QEMU_UBUNTU_REL \ + || die "Failed to add repository of ubuntu $QEMU_UBUNTU_REL!" + + target_rel="--target-release $QEMU_UBUNTU_REL" + fi + + apt_install $target_rel qemu-user-static \ + || die 'Failed to install qemu-user-static using apt-get!' +} + + +#============================ M a i n ============================# + +while getopts 'a:b:d:i:k:m:p:r:t:hv' OPTION; do + case "$OPTION" in + a) ARCH="$OPTARG";; + b) ALPINE_BRANCH="$OPTARG";; + d) CHROOT_DIR="$OPTARG";; + i) BIND_DIR="$OPTARG";; + k) CHROOT_KEEP_VARS="${CHROOT_KEEP_VARS:-} $OPTARG";; + m) ALPINE_MIRROR="$OPTARG";; + p) ALPINE_PACKAGES="${ALPINE_PACKAGES:-} $OPTARG";; + r) EXTRA_REPOS="${EXTRA_REPOS:-} $OPTARG";; + t) TEMP_DIR="$OPTARG";; + h) usage; exit 0;; + v) echo "alpine-chroot-install $VERSION"; exit 0;; + esac +done + +: ${ALPINE_BRANCH:="v3.8"} +: ${ALPINE_MIRROR:="https://nl.alpinelinux.org/alpine"} +: ${ALPINE_PACKAGES:="build-base ca-certificates ssl_client"} +: ${ARCH:=} +: ${BIND_DIR:=} +: ${CHROOT_DIR:="/alpine"} +: ${CHROOT_KEEP_VARS:="ARCH CI QEMU_EMULATOR TRAVIS_.*"} +: ${EXTRA_REPOS:=} +: ${TEMP_DIR:="/tmp/alpine"} + +# Note: Binding $PWD into chroot as default was a bad idea. It's convenient +# on Travis, but dangerous in general. However, all existing .travis.yml relies +# on this behaviour, so we can't (shouldn't) remove it completely. +[ "$BIND_DIR" ] || case "$(pwd)" in + /home/*) BIND_DIR="$(pwd)";; +esac + + +if [ "$(id -u)" -ne 0 ]; then + die 'This script must be run as root!' +fi + +mkdir -p "$CHROOT_DIR" +cd "$CHROOT_DIR" + + +# Install QEMU user mode emulation if needed + +QEMU_EMULATOR='' +if [ -n "$ARCH" ] && [ $(normalize_arch $ARCH) != $(normalize_arch $(uname -m)) ]; then + qemu_arch="$(normalize_arch $ARCH)" + QEMU_EMULATOR="/usr/bin/qemu-$qemu_arch-static" + + if [ ! -x "$QEMU_EMULATOR" ]; then + einfo 'Installing qemu-user-static on host system...' + install_qemu_user + + elif ! cmp_versions "$(qemu_version $QEMU_EMULATOR)" ge $QEMU_MIN_VER; then + ver=$(qemu_version $QEMU_EMULATOR) + ewarn "${QEMU_EMULATOR##*/} on host system is too old ($ver), minimal required version is $QEMU_MIN_VER" + ewarn 'Installing newer version...' + install_qemu_user + fi + + if [ ! -e /proc/sys/fs/binfmt_misc/qemu-$qemu_arch ]; then + einfo 'Installing and enabling binfmt-support on host system...' + install_binfmt_support + fi + + mkdir -p usr/bin + cp -v "$QEMU_EMULATOR" usr/bin/ +fi + + +einfo 'Downloading static apk-tools' + +wgets "$APK_TOOLS_URI" "$APK_TOOLS_SHA256" "$TEMP_DIR" +tar -C "$TEMP_DIR" -xzf "$TEMP_DIR/${APK_TOOLS_URI##*/}" +mv "$TEMP_DIR"/apk-tools-*/apk "$TEMP_DIR"/ + + +einfo "Installing Alpine Linux $ALPINE_BRANCH ($ARCH) into chroot" + +mkdir -p "$CHROOT_DIR"/etc/apk +cd "$CHROOT_DIR" + +printf '%s\n' \ + "$ALPINE_MIRROR/$ALPINE_BRANCH/main" \ + "$ALPINE_MIRROR/$ALPINE_BRANCH/community" \ + $EXTRA_REPOS \ + > etc/apk/repositories + +dump_alpine_keys etc/apk/keys/ + +cp /etc/resolv.conf etc/resolv.conf + +"$TEMP_DIR"/apk \ + --root . --update-cache --initdb --no-progress \ + ${ARCH:+--arch $ARCH} \ + add alpine-base + +gen_chroot_script > enter-chroot +chmod +x enter-chroot + + +einfo 'Binding filesystems into chroot' + +mount -v -t proc none proc +mount -v --rbind /sys sys +mount -v --rbind /dev dev +mount -v --rbind /run run + +if [ "$BIND_DIR" ]; then + mkdir -p "${CHROOT_DIR}${BIND_DIR}" + mount -v --bind "$BIND_DIR" "${CHROOT_DIR}${BIND_DIR}" +fi + + +einfo 'Setting up Alpine' + +./enter-chroot <<-EOF + set -e + apk update + apk add $ALPINE_PACKAGES + + if [ -d /etc/sudoers.d ] && [ ! -e /etc/sudoers.d/wheel ]; then + echo '%wheel ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/wheel + fi + + if [ -n "${SUDO_USER:-}" ]; then + adduser -u "${SUDO_UID:-1000}" -G users -s /bin/sh -D "${SUDO_USER:-}" || true + fi +EOF + +cat >&2 <<-EOF + --- + Alpine installation is complete + Run $CHROOT_DIR/enter-chroot [-u ] [command] to enter the chroot. +EOF \ No newline at end of file diff --git a/script/ci/download.js b/script/ci/download.js new file mode 100644 index 00000000..75531b2c --- /dev/null +++ b/script/ci/download.js @@ -0,0 +1,34 @@ +const path = require("path") +const fetch = require("node-fetch") +const gunzip = require("gunzip-maybe") +const tar = require("tar-fs") + +async function download() { + const {repository: {url}, version} = require(path.resolve("./package.json")) + + if (process.env.TRAVIS_TAG && process.env.TRAVIS_TAG != `v${version}`) { + throw new Error(`Version mismatch (TRAVIS_TAG=${process.env.TRAVIS_TAG}, version=${version}`) + } + + const [, user, repo] = url.match(/\/([a-z0-9_-]+)\/([a-z0-9_-]+)\.git$/i) + + const res = await fetch(`https://api.github.com/repos/${user}/${repo}/releases/tags/v${version}`) + if (!res.ok) { + throw new Error(`Github release v${version} not found (${res.status})`) + } + + const {assets} = await res.json() + + await Promise.all(assets.map(async ({browser_download_url: url}) => { + console.log(`Downloading prebuilds from ${url}`) + const res = await fetch(url) + return new Promise((resolve, reject) => { + res.body.pipe(gunzip()).pipe(tar.extract(".")).on("error", reject).on("finish", resolve) + }) + })) +} + +download().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/script/ci/install.sh b/script/ci/install.sh new file mode 100755 index 00000000..60be430d --- /dev/null +++ b/script/ci/install.sh @@ -0,0 +1,48 @@ +#!/bin/sh +set -e + +echo "Installing dependencies..." + +if [ -n "${TRIPLE}" ]; then + export CC="${TRIPLE}-gcc-${GCC}" + export CXX="${TRIPLE}-g++-${GCC}" + export STRIP="${TRIPLE}-strip" + export ZMQ_BUILD_OPTIONS="--host=${TRIPLE}" + + export npm_config_arch=${ARCH} + export npm_config_target_arch=${ARCH} +fi + +if [ -n "${ALPINE_CHROOT}" ]; then + sudo script/ci/alpine-chroot-install.sh -b v${ALPINE_CHROOT} -p 'nodejs-dev yarn build-base git cmake curl python2 coreutils' -k 'CI TRAVIS_.* ZMQ_.* NODE_.* npm_.*' +fi + +if [ -n "${ZMQ_SHARED}" ]; then + export npm_config_zmq_shared=true +fi + +if [ -n "${ZMQ_DRAFT}" ]; then + export npm_config_zmq_draft=true +fi + +export npm_config_build_from_source=true + +# Installing node-gyp globally facilitates calling it in various ways, not just +# via yarn but also via bin stubs in node_modules (even on Windows). +if [ -n "${ALPINE_CHROOT}" ]; then + /alpine/enter-chroot yarn global add node-gyp + + if [ -n "${IGNORE_SCRIPTS}" ]; then + /alpine/enter-chroot yarn install --ignore-scripts + else + /alpine/enter-chroot yarn install + fi +else + yarn global add node-gyp + + if [ -n "${IGNORE_SCRIPTS}" ]; then + yarn install --ignore-scripts + else + yarn install + fi +fi diff --git a/script/ci/package.sh b/script/ci/package.sh new file mode 100755 index 00000000..5bb09ec8 --- /dev/null +++ b/script/ci/package.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -e + +# Prepare for packaging. +yarn ci:compile +node script/ci/download.js + +# Generate & publish documentation. +yarn ci:doc +cd docs +git init +git add . +git commit -m "Deploy documentation for ${TRAVIS_TAG:-latest}." +git push --force --quiet "https://${GITHUB_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git" master:gh-pages diff --git a/script/ci/prebuild.sh b/script/ci/prebuild.sh new file mode 100755 index 00000000..244c16c4 --- /dev/null +++ b/script/ci/prebuild.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +echo "Building distribution binary..." + +if [ -n "${TRIPLE}" ]; then + export CC="${TRIPLE}-gcc-${GCC}" + export CXX="${TRIPLE}-g++-${GCC}" + export STRIP="${TRIPLE}-strip" + export ZMQ_BUILD_OPTIONS="--host=${TRIPLE}" + + export npm_config_arch=${ARCH} + export npm_config_target_arch=${ARCH} + + export PREBUILD_ARCH="${ARCH}" + export PREBUILD_STRIP_BIN="${STRIP}" +fi + +if [ -n "${ALPINE_CHROOT}" ]; then + /alpine/enter-chroot yarn ci:prebuild --tag-libc +else + if [ "${TRAVIS_OS_NAME}" = "linux" ]; then + yarn ci:prebuild --tag-libc + else + yarn ci:prebuild + fi +fi + +ARCHIVE_NAME="${TRAVIS_TAG:-latest}-${TRAVIS_OS_NAME}${ARCHIVE_SUFFIX}.tar.gz" +tar -zcvf "${ARCHIVE_NAME}" -C "${TRAVIS_BUILD_DIR}" prebuilds diff --git a/script/ci/test.sh b/script/ci/test.sh new file mode 100755 index 00000000..66eecd81 --- /dev/null +++ b/script/ci/test.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +if [ -n "${ALPINE_CHROOT}" ]; then + /alpine/enter-chroot yarn dev:test +else + yarn dev:test +fi diff --git a/script/format.sh b/script/format.sh new file mode 100755 index 00000000..77706d37 --- /dev/null +++ b/script/format.sh @@ -0,0 +1,9 @@ +#!/bin/sh +if [ -z "$CI" ]; then + if command -v clang-format >/dev/null; then + echo "Formatting source files..." + clang-format -i -style=file src/*.{cc,h} src/*/*.h + fi +else + echo "Skipping source formatting..." +fi diff --git a/scripts/build_libzmq.sh b/scripts/build_libzmq.sh deleted file mode 100755 index c7e3b68e..00000000 --- a/scripts/build_libzmq.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh -set -e - -if [ "$1" != "" ]; then - ZMQ=$1 -else - echo "No ZMQ version given" - exit 1 -fi - -if [ "$2" = "ia32" ]; then - export CFLAGS="-fPIC -m32" - export CXXFLAGS="-fPIC -m32" -else - export CFLAGS=-fPIC - export CXXFLAGS=-fPIC -fi - -export MACOSX_DEPLOYMENT_TARGET=10.9 -export BASE=$(dirname "$0") -export ZMQ_PREFIX="${BASE}/../zmq" -export ZMQ_SRC_DIR=zeromq-$ZMQ -cd "${ZMQ_PREFIX}" - -export PKG_CONFIG_PATH="${ZMQ_PREFIX}/lib/pkgconfig" - -test -d "${ZMQ_SRC_DIR}" || tar xzf zeromq-$ZMQ.tar.gz -cd "${ZMQ_SRC_DIR}" - -test -f configure || ./autogen.sh -if [ "$ZMQ" = "4.1.6" ]; then - ./configure "--prefix=${ZMQ_PREFIX}" --with-relaxed --enable-static --disable-shared --without-documentation ${ZMQ_BUILD_OPTIONS} -else - ./configure "--prefix=${ZMQ_PREFIX}" --disable-pedantic --enable-static --disable-shared --without-docs ${ZMQ_BUILD_OPTIONS} -fi -make -j 2 -make install - -cd "${ZMQ_PREFIX}" -rm -rf "${ZMQ_SRC_DIR}" -rm -f zeromq-$ZMQ.tar.gz diff --git a/scripts/cross_compile.sh b/scripts/cross_compile.sh deleted file mode 100755 index c4e1597e..00000000 --- a/scripts/cross_compile.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -ARCH=$1 -GH_TOKEN=$2 - -if [[ "${ARCH}" == "armv7" ]]; then - TRIPLE="arm-linux-gnueabihf" - GCC="4.8" -elif [[ "${ARCH}" == "armv8" ]]; then - TRIPLE="aarch64-linux-gnu" - GCC="4.8" -else - exit 1 -fi - -PACKAGES="gcc-${GCC}-${TRIPLE} g++-${GCC}-${TRIPLE}" -export CC="${TRIPLE}-gcc-${GCC}" -export CXX="${TRIPLE}-g++-${GCC}" -export STRIP="${TRIPLE}-strip" -export ZMQ_BUILD_OPTIONS="--host=${TRIPLE}" - -echo "Building zeromq.js for ${ARCH}" - -if [[ -z $GH_TOKEN ]]; then - sudo apt-get -qq update - sudo apt-get install -y ${PACKAGES} - npm install "--arch=${TRIPLE}" -else - ./node_modules/prebuild/bin.js "--arch=${ARCH}" --all --strip -u "${GH_TOKEN}" -fi diff --git a/scripts/download.js b/scripts/download.js deleted file mode 100644 index acd89008..00000000 --- a/scripts/download.js +++ /dev/null @@ -1,44 +0,0 @@ -var https = require("https"); -var fs = require("fs"); -var url = require("url"); - -function writeToFile(filename, response, callback) { - response.pipe(fs.createWriteStream(filename)); - response.on("end", callback); -} - -function download(fileUrl, filename, callback) { - https - .get(fileUrl, function(response) { - if ( - response.statusCode > 300 && - response.statusCode < 400 && - response.headers.location - ) { - if (url.parse(response.headers.location).hostname) { - https.get(response.headers.location, function(res) { - writeToFile(filename, res, callback); - }); - } else { - https - .get( - url.resolve( - url.parse(fileUrl).hostname, - response.headers.location - ), - function(res) { - writeToFile(filename, res, callback); - } - ) - .on("error", callback); - } - } else { - writeToFile(filename, response, callback); - } - }) - .on("error", callback); -} - -module.exports = { - download: download -}; diff --git a/scripts/prebuild-install.js b/scripts/prebuild-install.js deleted file mode 100644 index 7b6aeab9..00000000 --- a/scripts/prebuild-install.js +++ /dev/null @@ -1,24 +0,0 @@ -var exec = require('child_process').exec; - -var pbi = 'prebuild-install'; -var platform = process.platform; -var arch = process.arch; - -if (process.env.npm_config_zmq_external == "true") { - console.log('Requested to use external libzmq. Skipping download of prebuilt binaries.'); - process.exit(1); -} - -if ( - platform === 'linux' && - (arch === 'arm' || arch === 'arm64') -) { - var armv = (arch === 'arm64') ? '8' : process.config.variables.arm_version; - pbi += ' --arch=armv' + armv; -} - -exec(pbi, function(err, stdout, stderr) { - console.log(stdout); - console.log(stderr); - if (err) process.exit(1); -}); diff --git a/scripts/preinstall.js b/scripts/preinstall.js deleted file mode 100644 index 9539b561..00000000 --- a/scripts/preinstall.js +++ /dev/null @@ -1,109 +0,0 @@ -var download = require("./download").download; -var spawn = require("child_process").spawn; -var path = require("path"); -var fs = require("fs"); - -var ARCH = process.arch; -var ZMQ = "4.2.2"; -var ZMQ_REPO = "libzmq"; - -if (process.env.npm_config_zmq_external == "true") { - console.log("Requested to use external libzmq. Skipping libzmq build"); - process.exit(0); -} - -function buildZMQ(scriptPath, zmqDir) { - console.log("Building libzmq for " + process.platform); - - var child = spawn(scriptPath, [ZMQ, ARCH]); - - child.stdout.pipe(process.stdout); - child.stderr.pipe(process.stderr); - child.on("error", function(err) { - console.error("Failed to start child process."); - }); - child.on("close", function(code) { - if (code !== 0) { - return console.error("child process exited with code " + code); - } - var message = "Succesfully build libzmq on " + Date(); - fs.writeFile(path.join(zmqDir, "BUILD_SUCCESS"), message, function(err) { - if (err) { - return console.error(err.message); - } - console.log(message); - }); - }); -} - -function handleError(err) { - if (!err) { - return; - } - console.error(err); - if (err.code === "ECONNRESET") { - console.error("\n** Your connection was reset. **"); - console.error( - "\n** Are you behind a proxy or a firewall that is preventing a connection? **" - ); - } - process.exit(1); -} - -if (process.platform === "win32") { - var LIB_URL = - "https://github.com/nteract/libzmq-win/releases/download/v2.1.0/libzmq-" + - ZMQ + - "-" + - process.arch + - ".lib"; - var DIR_NAME = path.join(__dirname, "..", "windows", "lib"); - var FILE_NAME = path.join(DIR_NAME, "libzmq.lib"); - - if (!fs.existsSync(DIR_NAME)) { - fs.mkdirSync(DIR_NAME); - } - - if (!fs.existsSync(FILE_NAME)) { - console.log("Downloading libzmq for Windows"); - download(LIB_URL, FILE_NAME, function(err) { - if (err) { - handleError(err); - } - console.log("Download finished"); - }); - } -} else { - var SCRIPT_PATH = path.join(__dirname, "build_libzmq.sh"); - var TAR_URL = - "https://github.com/zeromq/" + - ZMQ_REPO + - "/releases/download/v" + - ZMQ + - "/zeromq-" + - ZMQ + - ".tar.gz"; - var DIR_NAME = path.join(__dirname, "..", "zmq"); - var FILE_NAME = path.join(DIR_NAME, "zeromq-" + ZMQ + ".tar.gz"); - - if (!fs.existsSync(DIR_NAME)) { - fs.mkdirSync(DIR_NAME); - } - - if (fs.existsSync(path.join(DIR_NAME, "BUILD_SUCCESS"))) { - console.log("Libzmq found, skipping rebuild."); - process.exit(0); - } - - if (fs.existsSync(FILE_NAME)) { - buildZMQ(SCRIPT_PATH, DIR_NAME); - process.exit(0); - } - - download(TAR_URL, FILE_NAME, function(err) { - if (err) { - handleError(err); - } - buildZMQ(SCRIPT_PATH, DIR_NAME); - }); -} diff --git a/scripts/publish_docs.sh b/scripts/publish_docs.sh deleted file mode 100644 index 4996e940..00000000 --- a/scripts/publish_docs.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -if [[ $TRAVIS_PULL_REQUEST == false && $TRAVIS_REPO_SLUG == 'zeromq/zeromq.js' && $TRAVIS_BRANCH == "master" && $DEPLOY == "true" && $TRAVIS_OS_NAME == "linux" ]] -then - npm run build:docs - - ( cd docs - git init - git config user.email "travis@travis-ci.com" - git config user.name "Travis Bot" - - git add . - git commit -m "Publish docs from $TRAVIS_BUILD_NUMBER" - git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1 - echo "Documentation has been published!" - ) -else - echo "Documentation has not been published because not on master!" -fi diff --git a/src/binding.cc b/src/binding.cc new file mode 100644 index 00000000..855108d7 --- /dev/null +++ b/src/binding.cc @@ -0,0 +1,86 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#include "context.h" +#include "observer.h" +#include "outgoing_msg.h" +#include "proxy.h" +#include "socket.h" + +namespace zmq { +static inline Napi::String Version(Napi::Env& env) { + int32_t major, minor, patch; + zmq_version(&major, &minor, &patch); + + return Napi::String::New( + env, to_string(major) + "." + to_string(minor) + "." + to_string(patch)); +} + +static inline Napi::Object Capabilities(Napi::Env& env) { + auto result = Napi::Object::New(env); + +#ifdef ZMQ_HAS_CAPABILITIES + static auto options = {"ipc", "pgm", "tipc", "norm", "curve", "gssapi", "draft"}; + for (auto& option : options) { + result.Set(option, static_cast(zmq_has(option))); + } + + /* Disable DRAFT sockets if there is no way to poll them (< 4.3.2), even + if libzmq was built with DRAFT support. */ +#ifndef ZMQ_HAS_POLLABLE_THREAD_SAFE + result.Set("draft", false); +#endif + +#else +#if !defined(ZMQ_HAVE_WINDOWS) && !defined(ZMQ_HAVE_OPENVMS) + result.Set("ipc", true); +#endif +#if defined(ZMQ_HAVE_OPENPGM) + result.Set("pgm", true); +#endif +#if defined(ZMQ_HAVE_TIPC) + result.Set("tipc", true); +#endif +#if defined(ZMQ_HAVE_NORM) + result.Set("norm", true); +#endif +#if defined(ZMQ_HAVE_CURVE) + result.Set("curve", true); +#endif +#endif + + return result; +} + +static inline Napi::Value CurveKeyPair(const Napi::CallbackInfo& info) { + char public_key[41]; + char secret_key[41]; + if (zmq_curve_keypair(public_key, secret_key) < 0) { + ErrnoException(info.Env(), zmq_errno()).ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } + + auto result = Napi::Object::New(info.Env()); + result["publicKey"] = Napi::String::New(info.Env(), public_key); + result["secretKey"] = Napi::String::New(info.Env(), secret_key); + return result; +} +} + +Napi::Object init(Napi::Env env, Napi::Object exports) { + exports.Set("version", zmq::Version(env)); + exports.Set("capability", zmq::Capabilities(env)); + exports.Set("curveKeyPair", Napi::Function::New(env, zmq::CurveKeyPair)); + + zmq::OutgoingMsg::Initialize(env); + + zmq::Context::Initialize(env, exports); + zmq::Socket::Initialize(env, exports); + zmq::Observer::Initialize(env, exports); + +#ifdef ZMQ_HAS_STEERABLE_PROXY + zmq::Proxy::Initialize(env, exports); +#endif + + return exports; +} + +NODE_API_MODULE(zmq, init) diff --git a/src/binding.h b/src/binding.h new file mode 100644 index 00000000..95f56cae --- /dev/null +++ b/src/binding.h @@ -0,0 +1,48 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include +#define NAPI_BUILD_VERSION NAPI_VERSION + +#include +#if ZMQ_VERSION < ZMQ_MAKE_VERSION(4, 1, 0) +#include +#endif + +#include + +#include + +#include "util/arguments.h" +#include "util/error.h" +#include "util/object.h" +#include "util/to_string.h" + +#ifdef _MSC_VER +#define force_inline inline __forceinline +#else +#define force_inline inline __attribute__((always_inline)) +#endif + +#ifdef _MSC_VER +#pragma warning(disable : 4146) +#ifndef _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES +#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1 +#endif +#endif + +/* Fix errors with numeric_limits::max. */ +#ifdef max +#undef max +#endif + +#if ZMQ_VERSION >= ZMQ_MAKE_VERSION(4, 0, 5) +#define ZMQ_HAS_STEERABLE_PROXY 1 +#endif + +/* Threadsafe sockets can only be used if zmq_poller_fd() is available. */ +#if ZMQ_VERSION >= ZMQ_MAKE_VERSION(4, 3, 2) +#ifdef ZMQ_BUILD_DRAFT_API +#define ZMQ_HAS_POLLABLE_THREAD_SAFE 1 +#endif +#endif diff --git a/src/compat.ts b/src/compat.ts new file mode 100644 index 00000000..a79132b3 --- /dev/null +++ b/src/compat.ts @@ -0,0 +1,816 @@ +/* tslint:disable: variable-name */ + +/* The API of the compatibility layer and parts of the implementation has been + adapted from the original ZeroMQ.js version (up to 5.x) for which the license + and copyright notice is reproduced below. + +Copyright (c) 2017-2019 Rolf Timmermans +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2010, 2011 Justin Tulloss + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import {EventEmitter} from "events" +import * as zmq from "." + + +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 types = { + ZMQ_PAIR: 0, + ZMQ_PUB: 1, + ZMQ_SUB: 2, + ZMQ_REQ: 3, + ZMQ_REP: 4, + ZMQ_DEALER: 5, + ZMQ_XREQ: 5, + ZMQ_ROUTER: 6, + ZMQ_XREP: 6, + ZMQ_PULL: 7, + ZMQ_PUSH: 8, + ZMQ_XPUB: 9, + ZMQ_XSUB: 10, + ZMQ_STREAM: 11, +} + +const longOptions = { + ZMQ_AFFINITY: 4, + ZMQ_IDENTITY: 5, + ZMQ_SUBSCRIBE: 6, + ZMQ_UNSUBSCRIBE: 7, + ZMQ_RATE: 8, + ZMQ_RECOVERY_IVL: 9, + ZMQ_RECOVERY_IVL_MSEC: 9, + ZMQ_SNDBUF: 11, + ZMQ_RCVBUF: 12, + ZMQ_RCVMORE: 13, + ZMQ_FD: 14, + ZMQ_EVENTS: 15, + ZMQ_TYPE: 16, + ZMQ_LINGER: 17, + ZMQ_RECONNECT_IVL: 18, + ZMQ_BACKLOG: 19, + ZMQ_RECONNECT_IVL_MAX: 21, + ZMQ_MAXMSGSIZE: 22, + ZMQ_SNDHWM: 23, + ZMQ_RCVHWM: 24, + ZMQ_MULTICAST_HOPS: 25, + ZMQ_RCVTIMEO: 27, + ZMQ_SNDTIMEO: 28, + ZMQ_IPV4ONLY: 31, + ZMQ_LAST_ENDPOINT: 32, + ZMQ_ROUTER_MANDATORY: 33, + ZMQ_TCP_KEEPALIVE: 34, + ZMQ_TCP_KEEPALIVE_CNT: 35, + ZMQ_TCP_KEEPALIVE_IDLE: 36, + ZMQ_TCP_KEEPALIVE_INTVL: 37, + ZMQ_TCP_ACCEPT_FILTER: 38, + ZMQ_DELAY_ATTACH_ON_CONNECT: 39, + ZMQ_XPUB_VERBOSE: 40, + ZMQ_ROUTER_RAW: 41, + ZMQ_IPV6: 42, + ZMQ_MECHANISM: 43, + ZMQ_PLAIN_SERVER: 44, + ZMQ_PLAIN_USERNAME: 45, + ZMQ_PLAIN_PASSWORD: 46, + ZMQ_CURVE_SERVER: 47, + ZMQ_CURVE_PUBLICKEY: 48, + ZMQ_CURVE_SECRETKEY: 49, + ZMQ_CURVE_SERVERKEY: 50, + ZMQ_ZAP_DOMAIN: 55, + ZMQ_HEARTBEAT_IVL: 75, + ZMQ_HEARTBEAT_TTL: 76, + ZMQ_HEARTBEAT_TIMEOUT: 77, + ZMQ_CONNECT_TIMEOUT: 79, + ZMQ_IO_THREADS: 1, + ZMQ_MAX_SOCKETS: 2, + ZMQ_ROUTER_HANDOVER: 56, +} + +const pollStates = { + ZMQ_POLLIN: 1, + ZMQ_POLLOUT: 2, + ZMQ_POLLERR: 4, +} + +const sendOptions = { + ZMQ_SNDMORE: 2, +} + +const capabilities = { + ZMQ_CAN_MONITOR: 1, + ZMQ_CAN_DISCONNECT: 1, + ZMQ_CAN_UNBIND: 1, + ZMQ_CAN_SET_CTX: 1, +} + +const socketStates = { + STATE_READY: 0, + STATE_BUSY: 1, + STATE_CLOSED: 2, +} + +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: number = 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 + } + + const recv = async () => { + 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.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) + } 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) + } 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) + } 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[], flags: number = 0, cb?: Callback) { + flags = flags | 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() { + throw new Error( + "bindSync() has been removed from compatibility mode; " + + "use bind() instead", + ) + } + + unbindSync() { + throw new Error( + "unbindSync() has been removed from compatibility mode; " + + "use unbind() instead", + ) + } + + 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++ + + /* tslint:disable-next-line: no-unused-expression */ + (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(option: number | keyof typeof shortOptions, value: any) { + option = typeof option !== "number" ? shortOptions[option] : option + + 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 = !!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 = !!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 = !!value; break + case longOptions.ZMQ_PLAIN_SERVER: + this._socket.plainServer = !!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 = !!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 = !!value; break + default: + throw new Error("Unknown option") + } + + return this + } + + getsockopt(option: number | keyof typeof shortOptions) { + option = typeof option !== "number" ? shortOptions[option] : option + + 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)) { + Object.defineProperty(Socket.prototype, key, { + get(this: Socket) { + return this.getsockopt(shortOptions[key as keyof typeof shortOptions]) + }, + set(this: Socket, val: string | Buffer) { + if ("string" === typeof val) val = Buffer.from(val, "utf8") + 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, + createSocket as socket, + createSocket, + curveKeypair, + proxy, + shortOptions as options, +} + +/* Unfortunately there is no easy way to include these in the resulting + TS definitions. */ +Object.assign(module.exports, longOptions) +Object.assign(module.exports, types) +Object.assign(module.exports, pollStates) +Object.assign(module.exports, sendOptions) +Object.assign(module.exports, socketStates) +Object.assign(module.exports, capabilities) diff --git a/src/context.cc b/src/context.cc new file mode 100644 index 00000000..10a6f57d --- /dev/null +++ b/src/context.cc @@ -0,0 +1,239 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#include "context.h" +#include "socket.h" + +#include "util/uvwork.h" + +#include + +namespace zmq { +/* Create a reference to a single global context that is automatically + closed on process exit. This is the default context. */ +Napi::ObjectReference GlobalContext; + +Napi::FunctionReference Context::Constructor; + +std::unordered_set Context::ActivePtrs; + +Context::Context(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { + auto args = { + Argument{"Options must be an object", &Napi::Value::IsObject, + &Napi::Value::IsUndefined}, + }; + + if (!ValidateArguments(info, args)) return; + + context = zmq_ctx_new(); + if (context != nullptr) { + ActivePtrs.insert(context); + } else { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + + /* Sealing causes setting/getting invalid options to throw an error. + Otherwise they would fail silently, which is very confusing. */ + Seal(info.This().As()); + + if (info[0].IsObject()) { + Assign(info.This().As(), info[0].As()); + } +} + +Context::~Context() { + if (context != nullptr) { + /* Messages may still be in the pipeline, so we only shutdown + and do not terminate the context just yet. */ + auto err = zmq_ctx_shutdown(context); + assert(err == 0); + + context = nullptr; + } +} + +template <> +Napi::Value Context::GetCtxOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + + uint32_t option = info[0].As(); + + int32_t value = zmq_ctx_get(context, option); + if (value < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return Napi::Boolean::New(Env(), value); +} + +template <> +void Context::SetCtxOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + Argument{"Option value must be a boolean", &Napi::Value::IsBoolean}, + }; + + if (!ValidateArguments(info, args)) return; + + uint32_t option = info[0].As(); + + int32_t value = info[1].As(); + if (zmq_ctx_set(context, option, value) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } +} + +#ifdef ZMQ_BUILD_DRAFT_API + +// template <> +// Napi::Value Context::GetCtxOpt(const Napi::CallbackInfo& info) { +// auto args = { +// Argument{"Identifier must be a number", &Napi::Value::IsNumber}, +// }; +// +// if (!ValidateArguments(info, args)) return Env().Undefined(); +// +// uint32_t option = info[0].As(); +// +// char value[1024]; +// size_t length = sizeof(value) - 1; +// if (zmq_ctx_get_ext(context, option, value, &length) < 0) { +// ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); +// return Env().Undefined(); +// } +// +// if (length == 0 || (length == 1 && value[0] == 0)) { +// return Env().Null(); +// } else { +// value[length] = '\0'; +// return Napi::String::New(Env(), value); +// } +// } +// +// template <> +// void Context::SetCtxOpt(const Napi::CallbackInfo& info) { +// auto args = { +// Argument{"Identifier must be a number", &Napi::Value::IsNumber}, +// Argument{"Option value must be a string or buffer", &Napi::Value::IsString, +// &Napi::Value::IsBuffer, &Napi::Value::IsNull}, +// }; +// +// if (!ValidateArguments(info, args)) return; +// +// int32_t option = info[0].As(); +// WarnUnlessImmediateOption(option); +// +// int32_t err; +// if (info[1].IsBuffer()) { +// Napi::Object buf = info[1].As(); +// auto length = buf.As>().Length(); +// auto value = buf.As>().Data(); +// err = zmq_ctx_set_ext(context, option, value, length); +// } else if (info[1].IsString()) { +// std::string str = info[1].As(); +// auto length = str.length(); +// auto value = str.data(); +// err = zmq_ctx_set_ext(context, option, value, length); +// } else { +// auto length = 0u; +// auto value = nullptr; +// err = zmq_ctx_set_ext(context, option, value, length); +// } +// +// if (err < 0) { +// ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); +// return; +// } +// } + +#endif + +template +Napi::Value Context::GetCtxOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + + uint32_t option = info[0].As(); + + T value = zmq_ctx_get(context, option); + if (value < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return Napi::Number::New(Env(), value); +} + +template +void Context::SetCtxOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + Argument{"Option value must be a number", &Napi::Value::IsNumber}, + }; + + if (!ValidateArguments(info, args)) return; + + uint32_t option = info[0].As(); + + T value = info[1].As(); + if (zmq_ctx_set(context, option, value) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } +} + +void TerminateAll(void*) { + OutgoingMsg::Terminate(); + + /* Close all currently open sockets. */ + for (auto socket : Socket::ActivePtrs) { + auto err = zmq_close(socket); + assert(err == 0); + } + + /* Terminate all remaining contexts on process exit. */ + for (auto context : Context::ActivePtrs) { + auto err = zmq_ctx_term(context); + assert(err == 0); + } +} + +void Context::Initialize(Napi::Env& env, Napi::Object& exports) { + auto proto = { + InstanceMethod("getBoolOption", &Context::GetCtxOpt), + InstanceMethod("setBoolOption", &Context::SetCtxOpt), + InstanceMethod("getInt32Option", &Context::GetCtxOpt), + InstanceMethod("setInt32Option", &Context::SetCtxOpt), +#ifdef ZMQ_BUILD_DRAFT_API + // InstanceMethod("getStringOption", &Context::GetCtxOpt), + // InstanceMethod("setStringOption", &Context::SetCtxOpt), +#endif + }; + + auto constructor = DefineClass(env, "Context", proto); + + /* Create global context that is closed on process exit. */ + auto context = constructor.New({}); + + GlobalContext = Napi::Persistent(context); + GlobalContext.SuppressDestruct(); + + exports.Set("context", context); + + Constructor = Napi::Persistent(constructor); + Constructor.SuppressDestruct(); + + exports.Set("Context", constructor); + + auto status = napi_add_env_cleanup_hook(env, TerminateAll, nullptr); + assert(status == napi_ok); +} +} diff --git a/src/context.h b/src/context.h new file mode 100644 index 00000000..a22d4b09 --- /dev/null +++ b/src/context.h @@ -0,0 +1,40 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "binding.h" + +#include + +namespace zmq { +extern Napi::ObjectReference GlobalContext; + +class Context : public Napi::ObjectWrap { +public: + static Napi::FunctionReference Constructor; + static std::unordered_set ActivePtrs; + static void Initialize(Napi::Env& env, Napi::Object& exports); + + explicit Context(const Napi::CallbackInfo& info); + ~Context(); + + Context(Context&&) = delete; + Context& operator=(Context&&) = delete; + +protected: + template + inline Napi::Value GetCtxOpt(const Napi::CallbackInfo& info); + + template + inline void SetCtxOpt(const Napi::CallbackInfo& info); + +private: + void* context = nullptr; + + friend class Socket; + friend class Observer; + friend class Proxy; +}; +} + +static_assert(!std::is_copy_constructible::value, "not copyable"); +static_assert(!std::is_move_constructible::value, "not movable"); diff --git a/src/draft.ts b/src/draft.ts new file mode 100644 index 00000000..8d22ea29 --- /dev/null +++ b/src/draft.ts @@ -0,0 +1,118 @@ +import { + methods, + Socket, + SocketType, +} from "./native" + +import { + Message, + MessageLike, + Readable, + SocketOptions, + Writable, +} from "." + + +const {send, receive, join, leave} = methods + + +export class Server extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Server, options) + } +} + +interface ServerRoutingOptions { + routingId: number +} + +export interface Server extends + Readable<[Message, ServerRoutingOptions]>, + Writable {} +Object.assign(Server.prototype, {send, receive}) + + +export class Client extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Client, options) + } +} + +export interface Client extends Readable<[Message]>, Writable {} +Object.assign(Client.prototype, {send, receive}) + + +export class Radio extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Radio, options) + } +} + +interface RadioGroupOptions { + group: Buffer | string +} + +export interface Radio extends Writable {} +Object.assign(Radio.prototype, {send}) + + +export class Dish extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Dish, options) + } + + /* TODO: These methods might accept arrays in their C++ implementation for + the sake of simplicity. */ + + join(...values: Array): void { + for (const value of values) join.call(this, value) + } + + leave(...values: Array): void { + for (const value of values) leave.call(this, value) + } +} + +interface DishGroupOptions { + group: Buffer +} + +export interface Dish extends Readable<[Message, DishGroupOptions]> {} +Object.assign(Dish.prototype, {receive}) + + +export class Gather extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Gather, options) + } +} + +export interface Gather extends Readable<[Message]> { + conflate: boolean +} + +Object.assign(Gather.prototype, {receive}) + + +export class Scatter extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Scatter, options) + } +} + +export interface Scatter extends Writable { + conflate: boolean +} + +Object.assign(Scatter.prototype, {send}) + + +export class Datagram extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Datagram, options) + } +} + +export interface Datagram extends + Readable<[Message, Message]>, Writable<[MessageLike, MessageLike]> {} +Object.assign(Datagram.prototype, {send, receive}) diff --git a/src/incoming_msg.cc b/src/incoming_msg.cc new file mode 100644 index 00000000..f3dbb567 --- /dev/null +++ b/src/incoming_msg.cc @@ -0,0 +1,52 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#include "incoming_msg.h" + +namespace zmq { +IncomingMsg::IncomingMsg() : ref(new Reference()) {} + +IncomingMsg::~IncomingMsg() { + if (!moved && ref != nullptr) { + delete ref; + ref = nullptr; + } +} + +Napi::Value IncomingMsg::IntoBuffer(const Napi::Env& env) { + if (moved) { + /* If ownership has been transferred, do not attempt to read the buffer + again in any case. This should not happen of course. */ + ErrnoException(env, EINVAL).ThrowAsJavaScriptException(); + return env.Undefined(); + } + + static auto constexpr zero_copy_threshold = 32; + + auto data = reinterpret_cast(zmq_msg_data(*ref)); + auto length = zmq_msg_size(*ref); + + if (length > zero_copy_threshold) { + /* Reuse existing buffer for external storage. This avoids copying but + does include an overhead in having to call a finalizer when the + buffer is GC'ed. For very small messages it is faster to copy. */ + moved = true; + return Napi::Buffer::New(env, data, length, + [](const Napi::Env& env, uint8_t*, Reference* ref) { delete ref; }, ref); + } + + if (length > 0) { + return Napi::Buffer::Copy(env, data, length); + } + + return Napi::Buffer::New(env, 0); +} + +IncomingMsg::Reference::Reference() { + auto err = zmq_msg_init(&msg); + assert(err == 0); +} + +IncomingMsg::Reference::~Reference() { + auto err = zmq_msg_close(&msg); + assert(err == 0); +} +} diff --git a/src/incoming_msg.h b/src/incoming_msg.h new file mode 100644 index 00000000..08e061cc --- /dev/null +++ b/src/incoming_msg.h @@ -0,0 +1,40 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "binding.h" + +namespace zmq { +class IncomingMsg { +public: + IncomingMsg(); + ~IncomingMsg(); + + IncomingMsg(const IncomingMsg&) = delete; + IncomingMsg& operator=(const IncomingMsg&) = delete; + + Napi::Value IntoBuffer(const Napi::Env& env); + + inline operator zmq_msg_t*() { + return *ref; + } + +private: + class Reference { + zmq_msg_t msg; + + public: + Reference(); + ~Reference(); + + inline operator zmq_msg_t*() { + return &msg; + } + }; + + Reference* ref = nullptr; + bool moved = false; +}; +} + +static_assert(!std::is_copy_constructible::value, "not copyable"); +static_assert(!std::is_move_constructible::value, "not movable"); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..305df4d8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,1820 @@ +export { + capability, + context, + curveKeyPair, + version, + Context, + Event, + EventOfType, + EventType, + Socket, + Observer, + Proxy, +} from "./native" + +import { + capability, + methods, + Context, + EventOfType, + EventType, + Observer, + Options, + ReadableKeys, + Socket, + SocketType, + WritableKeys, +} from "./native" + +import * as draft from "./draft" + + +const {send, receive} = methods + +/** + * A type representing the messages that are returned inside promises by + * {@link Readable.receive}(). + */ +export type Message = ( + Buffer +) + +/** + * Union type representing all message types that are accepted by + * {@link Writable.send}(). + */ +export type MessageLike = ( + ArrayBufferView | /* Includes Node.js Buffer and all TypedArray types. */ + ArrayBuffer | /* Backing buffer of TypedArrays. */ + SharedArrayBuffer | + string | + null +) + + +/** + * Describes sockets that can send messages. + * + * @typeparam M The type of the message or message parts that can be sent. + * @typeparam O Rest type for any options, if applicable to the socket type + * (DRAFT only). + */ +export interface Writable< + M extends MessageLike | MessageLike[] = MessageLike | MessageLike[], + O extends [...object[]] = [], +> { + /** + * ZMQ_MULTICAST_HOPS + * + * Sets the time-to-live field in every multicast packet sent from this + * socket. The default is 1 which means that the multicast packets don't leave + * the local network. + */ + multicastHops: number + + /** + * ZMQ_SNDBUF + * + * Underlying kernel transmit buffer size in bytes. A value of -1 means leave + * the OS default unchanged. + */ + sendBufferSize: number + + /** + * ZMQ_SNDHWM + * + * The high water mark is a hard limit on the maximum number of outgoing + * messages ØMQ shall queue in memory for any single peer that the specified + * socket is communicating with. A value of zero means no limit. + * + * If this limit has been reached the socket shall enter an exceptional state + * and depending on the socket type, ØMQ shall take appropriate action such as + * blocking or dropping sent messages. + */ + sendHighWaterMark: number + + /** + * ZMQ_SNDTIMEO + * + * Sets the timeout for sending messages on the socket. If the value is 0, + * {@link send}() will return a rejected promise immediately if the message + * cannot be sent. If the value is -1, it will wait asynchronously until the + * message is sent. For all other values, it will try to send the message for + * that amount of time before rejecting. + */ + sendTimeout: number + + /** + * Sends a single message or a multipart message on the socket. Queues the + * message immediately if possible, and returns a resolved promise. If the + * message cannot be queued because the high water mark has been reached, it + * will wait asynchronously. The promise will be resolved when the message was + * queued successfully. + * + * ```typescript + * await socket.send("hello world") + * await socket.send(["hello", "world"]) + * ``` + * + * Queueing may fail eventually if the socket has been configured with a + * {@link sendTimeout}. + * + * A call to {@link send}() is guaranteed to return with a resolved promise + * immediately if the message could be queued directly. + * + * Only **one** asynchronously blocking call to {@link send}() may be executed + * simultaneously. If you call {@link send}() again on a socket that is in the + * mute state it will return a rejected promise with an `EAGAIN` error. + * + * The reason for disallowing multiple {@link send}() calls simultaneously is + * that it could create an implicit queue of unsendable outgoing messages. + * This would circumvent the socket's {@link sendHighWaterMark}. Such an + * implementation could even exhaust all system memory and cause the Node.js + * process to abort. + * + * For most application you should not notice this implementation detail. Only + * in rare occasions will a call to {@link send}() that does not resolve + * immediately be undesired. Here are some common scenarios: + * + * * If you wish to **send a message**, use `await send(...)`. ZeroMQ socket + * types have been carefully designed to give you the correct blocking + * behaviour on the chosen socket type in almost all cases: + * + * * If sending is not possible, it is often better to wait than to continue + * as if nothing happened. For example, on a {@link Request} socket, you + * can only receive a reply once a message has been sent; so waiting until + * a message could be queued before continuing with the rest of the + * program (likely to read from the socket) is required. + * + * * Certain socket types (such as {@link Router}) will always allow + * queueing messages and `await send(...)` won't delay any code that comes + * after. This makes sense for routers, since typically you don't want a + * single send operation to stop the handling of other incoming or + * outgoing messages. + * + * * If you wish to send on an occasionally **blocking** socket (for example + * on a {@link Router} with the {@link Router.mandatory} option set, or on a + * {@link Dealer}) and you're 100% certain that **dropping a message is + * better than blocking**, then you can set the {@link sendTimeout} option + * to `0` to effectively force {@link send}() to always resolve immediately. + * Be prepared to catch exceptions if sending a message is not immediately + * possible. + * + * * If you wish to send on a socket and **messages should be queued before + * they are dropped**, you should implement a [simple + * queue](examples/queue/queue.ts) in JavaScript. Such a queue is not + * provided by this library because most real world applications need to + * deal with undeliverable messages in more complex ways – for example, they + * might need to reply with a status message; or first retry delivery a + * certain number of times before giving up. + * + * @param message Single message or multipart message to queue for sending. + * @param options Any options, if applicable to the socket type (DRAFT only). + * @returns Resolved when the message was successfully queued. + */ + send(message: M, ...options: O): Promise +} + +type ReceiveType = T extends {receive(): Promise} ? U : never + +/** + * Describes sockets that can receive messages. + * + * @typeparam M The type of the message or message parts that can be read. + */ +export interface Readable { + /** + * ZMQ_RCVBUF + * + * Underlying kernel receive buffer size in bytes. A value of -1 means leave + * the OS default unchanged. + */ + receiveBufferSize: number + + /** + * ZMQ_RCVHWM + * + * The high water mark is a hard limit on the maximum number of incoming + * messages ØMQ shall queue in memory for any single peer that the specified + * socket is communicating with. A value of zero means no limit. + * + * If this limit has been reached the socket shall enter an exceptional state + * and depending on the socket type, ØMQ shall take appropriate action such as + * blocking or dropping sent messages. + */ + receiveHighWaterMark: number + + /** + * ZMQ_RCVTIMEO + * + * Sets the timeout receiving messages on the socket. If the value is 0, + * {@link receive}() will return a rejected promise immediately if there is no + * message to receive. If the value is -1, it will wait asynchronously until a + * message is available. For all other values, it will wait for a message for + * that amount of time before rejecting. + */ + receiveTimeout: number + + /** + * Waits for the next single or multipart message to become availeble on the + * socket. Reads a message immediately if possible. If no messages can be + * read, it will wait asynchonously. The promise will be resolved with an + * array containing the parts of the next message when available. + * + * ```typescript + * const [msg] = await socket.receive() + * const [part1, part2] = await socket.receive() + * ``` + * + * Reading may fail (eventually) if the socket has been configured with a + * {@link receiveTimeout}. + * + * A call to {@link receive}() is guaranteed to return with a resolved promise + * immediately if a message could be read from the socket directly. + * + * Only **one** asynchronously blocking call to {@link receive}() can be in + * progress simultaneously. If you call {@link receive}() again on the same + * socket it will return a rejected promise with an `EAGAIN` error. For + * example, if no messages can be read and no `await` is used: + * + * ```typescript + * socket.receive() // -> pending promise until read is possible + * socket.receive() // -> promise rejection with `EAGAIN` error + * ``` + * + * **Note:** Due to the nature of Node.js and to avoid blocking the main + * thread, this method always attempts to read messages with the + * `ZMQ_DONTWAIT` flag. It polls asynchronously if reading is not currently + * possible. This means that all functionality related to timeouts and + * blocking behaviour is reimplemented in the Node.js bindings. Any + * differences in behaviour with the native ZMQ library is considered a bug. + * + * @returns Resolved with message parts that were successfully read. + */ + receive(): Promise + + /** + * Asynchronously iterate over messages becoming available on the socket. When + * the socket is closed with {@link Socket.close}(), the iterator will return. + * Returning early from the iterator will **not** close the socket unless it + * also goes out of scope. + * + * ```typescript + * for await (const [msg] of socket) { + * // handle messages + * } + * ``` + */ + [Symbol.asyncIterator](): AsyncIterator, undefined> +} + + +/** + * Represents the options that can be assigned in the constructor of a given + * socket type, for example `new Dealer({...})`. Readonly options + * for the particular socket will be omitted. + * + * @typeparam S The socket type to which the options should be applied. + */ +export type SocketOptions = Options + + +interface SocketLikeIterable { + closed: boolean + receive(): Promise +} + +/* Support async iteration over received messages. Implementing this in JS + is faster as long as there is no C++ native API to chain promises. */ +function asyncIterator, U>(this: T) { + return { + next: async (): Promise> => { + if (this.closed) { + /* Cast so we can omit 'value: undefined'. */ + return {done: true} as IteratorReturnResult + } + + try { + return {value: await this.receive(), done: false} + } catch (err) { + if (this.closed && err.code === "EAGAIN") { + /* Cast so we can omit 'value: undefined'. */ + return {done: true} as IteratorReturnResult + } else { + throw err + } + } + }, + } +} + +Object.assign(Socket.prototype, {[Symbol.asyncIterator]: asyncIterator}) +Object.assign(Observer.prototype, {[Symbol.asyncIterator]: asyncIterator}) + + +interface EventSubscriber { + /** + * Adds a listener function which will be invoked when the given event type is + * observed. Calling this method will convert the {@link Observer} to **event + * emitter mode**, which will make it impossible to call + * {@link Observer.receive}() at the same time. + * + * ```typescript + * socket.events.on("bind", event => { + * console.log(`Socket bound to ${event.address}`) + * // ... + * }) + * ``` + * + * @param type The type of event to listen for. + * @param listener The listener function that will be called with all event + * data when the event is observed. + */ + on( + type: E, + listener: (data: EventOfType) => void, + ): EventSubscriber + + /** + * Removes the specified listener function from the list of functions to call + * when the given event is observed. + * + * @param type The type of event that the listener was listening for. + * @param listener The previously registered listener function. + */ + off( + type: E, + listener: (data: EventOfType) => void, + ): EventSubscriber +} + +interface EventEmitter { + emit( + type: E, + data: EventOfType, + ): void +} + +Object.defineProperty(Observer.prototype, "emitter", { + get: function emitter(this: Observer) { + const events = require("events") + const value: EventEmitter = new events.EventEmitter() + + const boundReceive = this.receive.bind(this) + Object.defineProperty(this, "receive", { + get: () => { + throw new Error( + "Observer is in event emitter mode. " + + "After a call to events.on() it is not possible to read events " + + "with events.receive().", + ) + }, + }) + + const run = async () => { + while (!this.closed) { + const event = await boundReceive() + value.emit(event.type, event) + } + } + + run() + + Object.defineProperty(this, "emitter", {value}) + return value + }, +}) + +Observer.prototype.on = function on(this: {emitter: EventSubscriber}, ...args) { + return this.emitter.on(...args) +} + +Observer.prototype.off = function off(this: {emitter: EventSubscriber}, ...args) { + return this.emitter.off(...args) +} + + +/* Declare all additional TypeScript prototype methods that have been added + in this file here. They will augment the native module exports. */ +declare module "./native" { + export interface Context { + /** + * ZMQ_BLOCKY + * + * By default the context will block forever when closed at process exit. + * The assumption behind this behavior is that abrupt termination will cause + * message loss. Most real applications use some form of handshaking to + * ensure applications receive termination messages, and then terminate the + * context with {@link Socket.linger} set to zero on all sockets. This + * setting is an easier way to get the same result. When {@link blocky} is + * set to `false`, all new sockets are given a linger timeout of zero. You + * must still close all sockets before exiting. + */ + blocky: boolean + + /** + * ZMQ_IO_THREADS + * + * Size of the ØMQ thread pool to handle I/O operations. If your application + * is using only the `inproc` transport for messaging you may set this to + * zero, otherwise set it to at least one (default). + */ + ioThreads: number + + /** + * ZMQ_MAX_MSGSZ + * + * Maximum allowed size of a message sent in the context. + */ + maxMessageSize: number + + /** + * ZMQ_MAX_SOCKETS + * + * Maximum number of sockets allowed on the context. + */ + maxSockets: number + + /** + * ZMQ_IPV6 + * + * Enable or disable IPv6. When IPv6 is enabled, a socket will connect to, + * or accept connections from, both IPv4 and IPv6 hosts. + */ + ipv6: boolean + + /** + * ZMQ_THREAD_PRIORITY + * + * Scheduling priority for internal context's thread pool. This option is + * not available on Windows. Supported values for this option depend on + * chosen scheduling policy. Details can be found at + * http://man7.org/linux/man-pages/man2/sched_setscheduler.2.html. This + * option only applies before creating any sockets on the context. + * + * @writeonly + */ + threadPriority: number + + /** + * ZMQ_THREAD_SCHED_POLICY + * + * Scheduling policy for internal context's thread pool. This option is not + * available on Windows. Supported values for this option can be found at + * http://man7.org/linux/man-pages/man2/sched_setscheduler.2.html. This + * option only applies before creating any sockets on the context. + * + * @writeonly + */ + threadSchedulingPolicy: number + + /** + * ZMQ_SOCKET_LIMIT + * + * Largest number of sockets that can be set with {@link maxSockets}. + * + * @readonly + */ + readonly maxSocketsLimit: number + } + + /** + * Socket option names differ somewhat from the native libzmq option names. + * This is intentional to improve readability and be more idiomatic for + * JavaScript/TypeScript. + */ + export interface Socket { + /** + * ZMQ_AFFINITY + * + * I/O thread affinity, which determines which threads from the ØMQ I/O + * thread pool associated with the socket's context shall handle newly + * created connections. + * + * **Note:** This value is a bit mask, but values higher than + * `Number.MAX_SAFE_INTEGER` may not be represented accurately! This + * currently means that configurations beyond 52 threads are unreliable. + */ + affinity: number + + /** + * ZMQ_RATE + * + * Maximum send or receive data rate for multicast transports such as `pgm`. + */ + rate: number + + /** + * ZMQ_RECOVERY_IVL + * + * Maximum time in milliseconds that a receiver can be absent from a + * multicast group before unrecoverable data loss will occur. + */ + recoveryInterval: number + + /** + * ZMQ_LINGER + * + * Determines how long pending messages which have yet to be sent to a peer + * shall linger in memory after a socket is closed with {@link close}(). + */ + linger: number + + /** + * ZMQ_RECONNECT_IVL + * + * Period ØMQ shall wait between attempts to reconnect disconnected peers + * when using connection-oriented transports. The value -1 means no + * reconnection. + */ + reconnectInterval: number + + /** + * ZMQ_BACKLOG + * + * Maximum length of the queue of outstanding peer connections for the + * specified socket. This only applies to connection-oriented transports. + */ + backlog: number + + /** + * ZMQ_RECONNECT_IVL_MAX + * + * Maximum period ØMQ shall wait between attempts to reconnect. On each + * reconnect attempt, the previous interval shall be doubled until + * {@link reconnectMaxInterval} is reached. This allows for exponential + * backoff strategy. Zero (the default) means no exponential backoff is + * performed and reconnect interval calculations are only based on + * {@link reconnectInterval}. + */ + reconnectMaxInterval: number + + /** + * ZMQ_MAXMSGSIZE + * + * Limits the size of the inbound message. If a peer sends a message larger + * than the limit it is disconnected. Value of -1 means no limit. + */ + maxMessageSize: number + + /** + * ZMQ_TCP_KEEPALIVE + * + * Override SO_KEEPALIVE socket option (if supported by OS). The default + * value of -1 leaves it to the OS default. + */ + tcpKeepalive: number + + /** + * ZMQ_TCP_KEEPALIVE_CNT + * + * Overrides TCP_KEEPCNT socket option (if supported by OS). The default + * value of -1 leaves it to the OS default. + */ + tcpKeepaliveCount: number + + /** + * ZMQ_TCP_KEEPALIVE_IDLE + * + * Overrides TCP_KEEPIDLE / TCP_KEEPALIVE socket option (if supported by + * OS). The default value of -1 leaves it to the OS default. + */ + tcpKeepaliveIdle: number + + /** + * ZMQ_TCP_KEEPALIVE_INTVL + * + * Overrides TCP_KEEPINTVL socket option (if supported by the OS). The + * default value of -1 leaves it to the OS default. + */ + tcpKeepaliveInterval: number + + /** + * ZMQ_TCP_ACCEPT_FILTER + * + * Assign a filter that will be applied for each new TCP transport + * connection on a listening socket. If no filters are applied, then the TCP + * transport allows connections from any IP address. If at least one filter + * is applied then new connection source IP should be matched. To clear all + * filters set to `null`. Filter is a string with IPv6 or IPv4 CIDR. + */ + tcpAcceptFilter: string | null + + /** + * ZMQ_IMMEDIATE + * + * By default queues will fill on outgoing connections even if the + * connection has not completed. This can lead to "lost" messages on sockets + * with round-robin routing ({@link Request}, {@link Push}, {@link Dealer}). + * If this option is set to `true`, messages shall be queued only to + * completed connections. This will cause the socket to block if there are + * no other connections, but will prevent queues from filling on pipes + * awaiting connection. + */ + immediate: boolean + + /** + * ZMQ_IPV6 + * + * Enable or disable IPv6. When IPv6 is enabled, the socket will connect to, + * or accept connections from, both IPv4 and IPv6 hosts. + */ + ipv6: boolean + + /** + * ZMQ_PLAIN_SERVER + * + * Defines whether the socket will act as server for PLAIN security. A value + * of `true` means the socket will act as PLAIN server. A value of `false` + * means the socket will not act as PLAIN server, and its security role then + * depends on other option settings. + */ + plainServer: boolean + + /** + * ZMQ_PLAIN_USERNAME + * + * Sets the username for outgoing connections over TCP or IPC. If you set + * this to a non-null value, the security mechanism used for connections + * shall be PLAIN. + */ + plainUsername: string | null + + /** + * ZMQ_PLAIN_PASSWORD + * + * Sets the password for outgoing connections over TCP or IPC. If you set + * this to a non-null value, the security mechanism used for connections + * shall be PLAIN. + */ + plainPassword: string | null + + /** + * ZMQ_CURVE_SERVER + * + * Defines whether the socket will act as server for CURVE security. A value + * of `true` means the socket will act as CURVE server. A value of `false` + * means the socket will not act as CURVE server, and its security role then + * depends on other option settings. + */ + curveServer: boolean + + /** + * ZMQ_CURVE_PUBLICKEY + * + * Sets the socket's long term public key. You must set this on CURVE client + * sockets. A server socket does not need to know its own public key. You + * can create a new keypair with {@link curveKeyPair}(). + */ + curvePublicKey: string | null + + /** + * ZMQ_CURVE_SECRETKEY + * + * Sets the socket's long term secret key. You must set this on both CURVE + * client and server sockets. You can create a new keypair with + * {@link curveKeyPair}(). + */ + curveSecretKey: string | null + + /** + * ZMQ_CURVE_SERVERKEY + * + * Sets the socket's long term server key. This is the public key of the + * CURVE *server* socket. You must set this on CURVE *client* sockets. This + * key must have been generated together with the server's secret key. You + * can create a new keypair with {@link curveKeyPair}(). + */ + curveServerKey: string | null + + /** */ + gssapiServer: boolean + + /** */ + gssapiPrincipal: string | null + + /** */ + gssapiServicePrincipal: string | null + + /** */ + gssapiPlainText: boolean + + /** */ + gssapiPrincipalNameType: "hostBased" | "userName" | "krb5Principal" + + /** */ + gssapiServicePrincipalNameType: "hostBased" | "userName" | "krb5Principal" + + /** + * ZMQ_ZAP_DOMAIN + * + * Sets the domain for ZAP (ZMQ RFC 27) authentication. For NULL security + * (the default on all `tcp://` connections), ZAP authentication only + * happens if you set a non-empty domain. For PLAIN and CURVE security, ZAP + * requests are always made, if there is a ZAP handler present. See + * http://rfc.zeromq.org/spec:27 for more details. + */ + zapDomain: string | null + + /** + * ZMQ_TOS + * + * Sets the ToS fields (the *Differentiated Services* (DS) and *Explicit + * Congestion Notification* (ECN) field) of the IP header. The ToS field is + * typically used to specify a packet's priority. The availability of this + * option is dependent on intermediate network equipment that inspect the + * ToS field and provide a path for low-delay, high-throughput, + * highly-reliable service, etc. + */ + typeOfService: number + + /** + * ZMQ_HANDSHAKE_IVL + * + * Handshaking is the exchange of socket configuration information (socket + * type, identity, security) that occurs when a connection is first opened + * (only for connection-oriented transports). If handshaking does not + * complete within the configured time, the connection shall be closed. The + * value 0 means no handshake time limit. + */ + handshakeInterval: number + + /** + * ZMQ_SOCKS_PROXY + * + * The SOCKS5 proxy address that shall be used by the socket for the TCP + * connection(s). Does not support SOCKS5 authentication. If the endpoints + * are domain names instead of addresses they shall not be resolved and they + * shall be forwarded unchanged to the SOCKS proxy service in the client + * connection request message (address type 0x03 domain name). + */ + socksProxy: string | null + + /** + * ZMQ_HEARTBEAT_IVL + * + * Interval in milliseconds between sending ZMTP heartbeats for the + * specified socket. If this option is greater than 0, then a PING ZMTP + * command will be sent after every interval. + */ + heartbeatInterval: number + + /** + * ZMQ_HEARTBEAT_TTL + * + * The timeout in milliseconds on the remote peer for ZMTP heartbeats. If + * this option is greater than 0, the remote side shall time out the + * connection if it does not receive any more traffic within the TTL period. + * This option does not have any effect if {@link heartbeatInterval} is 0. + * Internally, this value is rounded down to the nearest decisecond, any + * value less than 100 will have no effect. + */ + heartbeatTimeToLive: number + + /** + * ZMQ_HEARTBEAT_TIMEOUT + * + * How long (in milliseconds) to wait before timing-out a connection after + * sending a PING ZMTP command and not receiving any traffic. This option is + * only valid if {@link heartbeatInterval} is greater than 0. The connection + * will time out if there is no traffic received after sending the PING + * command. The received traffic does not have to be a PONG command - any + * received traffic will cancel the timeout. + */ + heartbeatTimeout: number + + /** + * ZMQ_CONNECT_TIMEOUT + * + * Sets how long to wait before timing-out a connect() system call. The + * connect() system call normally takes a long time before it returns a time + * out error. Setting this option allows the library to time out the call at + * an earlier interval. + */ + connectTimeout: number + + /** + * ZMQ_TCP_MAXRT + * + * Sets how long before an unacknowledged TCP retransmit times out (if + * supported by the OS). The system normally attempts many TCP retransmits + * following an exponential backoff strategy. This means that after a + * network outage, it may take a long time before the session can be + * re-established. Setting this option allows the timeout to happen at a + * shorter interval. + */ + tcpMaxRetransmitTimeout: number + + /** + * ZMQ_MULTICAST_MAXTPDU + * + * Sets the maximum transport data unit size used for outbound multicast + * packets. This must be set at or below the minimum Maximum Transmission + * Unit (MTU) for all network paths over which multicast reception is + * required. + */ + multicastMaxTransportDataUnit: number + + /** + * ZMQ_VMCI_BUFFER_SIZE + * + * The size of the underlying buffer for the socket. Used during negotiation + * before the connection is established. + * For `vmci://` transports only. + */ + vmciBufferSize: number + + /** + * ZMQ_VMCI_BUFFER_MIN_SIZE + * + * Minimum size of the underlying buffer for the socket. Used during + * negotiation before the connection is established. + * For `vmci://` transports only. + */ + vmciBufferMinSize: number + + /** + * ZMQ_VMCI_BUFFER_MAX_SIZE + * + * Maximum size of the underlying buffer for the socket. Used during + * negotiation before the connection is established. + * For `vmci://` transports only. + */ + vmciBufferMaxSize: number + + /** + * ZMQ_VMCI_CONNECT_TIMEOUT + * + * Connection timeout for the socket. + * For `vmci://` transports only. + */ + vmciConnectTimeout: number + + /** + * ZMQ_BINDTODEVICE + * + * Binds the socket to the given network interface (Linux only). Allows to + * use Linux VRF, see: + * https://www.kernel.org/doc/Documentation/networking/vrf.txt. Requires the + * program to be ran as root **or** with `CAP_NET_RAW`. + */ + interface: string | null + + /** + * ZMQ_ZAP_ENFORCE_DOMAIN + * + * The ZAP (ZMQ RFC 27) authentication protocol specifies that a domain must + * always be set. Older versions of libzmq did not follow the spec and + * allowed an empty domain to be set. This option can be used to enabled or + * disable the stricter, backward incompatible behaviour. For now it is + * disabled by default, but in a future version it will be enabled by + * default. + */ + zapEnforceDomain: boolean + + /** + * ZMQ_LOOPBACK_FASTPATH + * + * Enable faster TCP connections on loopback devices. An application can + * enable this option to reduce the latency and improve the performance of + * loopback operations on a TCP socket on Windows. + * + * @windows + */ + loopbackFastPath: boolean + + /** + * ZMQ_TYPE + * + * Retrieve the socket type. This is fairly useless because you can test the + * socket class with e.g. `socket instanceof Dealer`. + * + * @readonly + */ + readonly type: SocketType + + /** + * ZMQ_LAST_ENDPOINT + * + * The last endpoint bound for TCP and IPC transports. + * + * @readonly + */ + readonly lastEndpoint: string | null + + /** + * ZMQ_MECHANISM + * + * Returns the current security mechanism for the socket, if any. The + * security mechanism is set implictly by using any of the relevant security + * options. The returned value is one of: + * * `null` – No security mechanism is used. + * * `"plain"` – The PLAIN mechanism defines a simple username/password + * mechanism that lets a server authenticate a client. PLAIN makes no + * attempt at security or confidentiality. + * * `"curve"` – The CURVE mechanism defines a mechanism for secure + * authentication and confidentiality for communications between a client + * and a server. CURVE is intended for use on public networks. + * * `"gssapi"` – The GSSAPI mechanism defines a mechanism for secure + * authentication and confidentiality for communications between a client + * and a server using the Generic Security Service Application Program + * Interface (GSSAPI). The GSSAPI mechanism can be used on both public and + * private networks. + * + * @readonly + */ + readonly securityMechanism: null | "plain" | "curve" | "gssapi" + + /** + * ZMQ_THREAD_SAFE + * + * Whether or not the socket is threadsafe. Currently only DRAFT sockets is + * thread-safe. + * + * @readonly + */ + readonly threadSafe: boolean + } + + export interface Observer extends EventSubscriber { + /** + * Asynchronously iterate over socket events. When the socket is closed or + * when the observer is closed manually with {@link Observer.close}(), the + * iterator will return. + * + * ```typescript + * for await (event of socket.events) { + * switch (event.type) { + * case "bind": + * console.log(`Socket bound to ${event.address}`) + * break + * // ... + * } + * } + * ``` + */ + [Symbol.asyncIterator](): AsyncIterator, undefined> + } +} + + +/* Concrete socket types. */ + + +/** + * A {@link Pair} socket can only be connected to one other {@link Pair} at any + * one time. No message routing or filtering is performed on any messages. + * + * When a {@link Pair} socket enters the mute state due to having reached the + * high water mark for the connected peer, or if no peer is connected, then any + * {@link Writable.send}() operations on the socket shall block until the peer + * becomes available for sending; messages are not discarded. + * + * While {@link Pair} sockets can be used over transports other than + * `inproc://`, their inability to auto-reconnect coupled with the fact new + * incoming connections will be terminated while any previous connections + * (including ones in a closing state) exist makes them unsuitable for `tcp://` + * in most cases. + */ +export class Pair extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Pair, options) + } +} + +export interface Pair extends Writable, Readable {} +Object.assign(Pair.prototype, {send, receive}) + + +/** + * A {@link Publisher} socket is used to distribute data to {@link Subscriber}s. + * Messages sent are distributed in a fan out fashion to all connected peers. + * This socket cannot receive messages. + * + * When a {@link Publisher} enters the mute state due to having reached the high + * water mark for a connected {@link Subscriber}, then any messages that would + * be sent to the subscriber in question shall instead be dropped until the mute + * state ends. The {@link Writable.send}() method will never block. + */ +export class Publisher extends Socket { + /** + * ZMQ_XPUB_NODROP + * + * Sets the socket behaviour to return an error if the high water mark is + * reached and the message could not be send. The default is to drop the + * message silently when the peer high water mark is reached. + */ + noDrop: boolean + + /** + * ZMQ_CONFLATE + * + * If set to `true`, a socket shall keep only one message in its + * inbound/outbound queue: the last message to be received/sent. Ignores any + * high water mark options. Does not support multi-part messages – in + * particular, only one part of it is kept in the socket internal queue. + */ + conflate: boolean + + /** + * ZMQ_INVERT_MATCHING + * + * Causes messages to be sent to all connected sockets except those subscribed + * to a prefix that matches the message. + * + * All {@link Subscriber} sockets connecting to the {@link Publisher} must + * also have the option set to `true`. Failure to do so will have the + * {@link Subscriber} sockets reject everything the {@link Publisher} socket + * sends them. + */ + invertMatching: boolean + + constructor(options?: SocketOptions) { + super(SocketType.Publisher, options) + } +} + +export interface Publisher extends Writable {} +Object.assign(Publisher.prototype, {send}) + + +/** + * A {@link Subscriber} socket is used to subscribe to data distributed by a + * {@link Publisher}. Initially a {@link Subscriber} is not subscribed to any + * messages. Use {@link Subscriber.subscribe}() to specify which messages to + * subscribe to. This socket cannot send messages. + */ +export class Subscriber extends Socket { + /** + * ZMQ_CONFLATE + * + * If set to `true`, a socket shall keep only one message in its + * inbound/outbound queue: the last message to be received/sent. Ignores any + * high water mark options. Does not support multi-part messages – in + * particular, only one part of it is kept in the socket internal queue. + */ + conflate: boolean + + /** + * ZMQ_INVERT_MATCHING + * + * Causes incoming messages that do not match any of the socket's + * subscriptions to be received by the user. + * + * All {@link Subscriber} sockets connecting to a {@link Publisher} must also + * have the option set to `true`. Failure to do so will have the + * {@link Subscriber} sockets reject everything the {@link Publisher} socket + * sends them. + */ + invertMatching: boolean + + constructor(options?: SocketOptions) { + super(SocketType.Subscriber, options) + } + + /** + * Establish a new message filter. Newly created {@link Subsriber} sockets + * will filtered out all incoming messages. Call this method to subscribe to + * messages beginning with the given prefix. + * + * Multiple filters may be attached to a single socket, in which case a + * message shall be accepted if it matches at least one filter. Subscribing + * without any filters shall subscribe to **all** incoming messages. + * + * ```typescript + * const sub = new Subscriber() + * + * // Listen to all messages beginning with 'foo'. + * sub.subscribe("foo") + * + * // Listen to all incoming messages. + * sub.subscribe() + * ``` + * + * @param prefixes The prefixes of messages to subscribe to. + */ + subscribe(...prefixes: Array) { + if (prefixes.length === 0) { + this.setStringOption(6, null) + } else { + for (const prefix of prefixes) { + this.setStringOption(6, prefix) + } + } + } + + /** + * Remove an existing message filter which was previously established with + * {@link subscribe}(). Stops receiving messages with the given prefix. + * + * Unsubscribing without any filters shall unsubscribe from the "subscribe + * all" filter that is added by calling {@link subscribe}() without arguments. + * + * ```typescript + * const sub = new Subscriber() + * + * // Listen to all messages beginning with 'foo'. + * sub.subscribe("foo") + * // ... + * + * // Stop listening to messages beginning with 'foo'. + * sub.unsubscribe("foo") + * ``` + * + * @param prefixes The prefixes of messages to subscribe to. + */ + unsubscribe(...prefixes: Array) { + if (prefixes.length === 0) { + this.setStringOption(7, null) + } else { + for (const prefix of prefixes) { + this.setStringOption(7, prefix) + } + } + } +} + +export interface Subscriber extends Readable {} +Object.assign(Subscriber.prototype, {receive}) + + +/** + * A {@link Request} socket acts as a client to send requests to and receive + * replies from a {@link Reply} socket. This socket allows only an alternating + * sequence of {@link Writable.send}() and subsequent {@link Readable.receive}() + * calls. Each request sent is round-robined among all services, and each reply + * received is matched with the last issued request. + * + * If no services are available, then any send operation on the socket shall + * block until at least one service becomes available. The REQ socket shall not + * discard messages. + */ +export class Request extends Socket { + /** + * ZMQ_ROUTING_ID + * + * The identity of the specified socket when connecting to a `Router` socket. + */ + routingId: string | null + + /** + * ZMQ_PROBE_ROUTER + * + * When set to `true`, the socket will automatically send an empty message + * when a new connection is made or accepted. You may set this on sockets + * connected to a {@link Router} socket. The application must filter such + * empty messages. This option provides the {@link Router} with an event + * signaling the arrival of a new peer. + * + * *Warning:** Do not set this option on a socket that talks to any other + * socket type except {@link Router}: the results are undefined. + * + * @writeonly + */ + probeRouter: boolean + + /** + * ZMQ_REQ_CORRELATE + * + * The default behaviour of {@link Request} sockets is to rely on the ordering + * of messages to match requests and responses and that is usually sufficient. + * When this option is set to `true` the socket will prefix outgoing messages + * with an extra frame containing a request id. That means the full message is + * `[, `null`, user frames…]`. The {@link Request} socket will + * discard all incoming messages that don't begin with these two frames. + */ + correlate: boolean + + /** + * ZMQ_REQ_RELAXED + * + * By default, a {@link Request} socket does not allow initiating a new + * request until the reply to the previous one has been received. When set to + * `true`, sending another message is allowed and previous replies will be + * discarded. The request-reply state machine is reset and a new request is + * sent to the next available peer. + * + * **Note:** If set to `true`, also enable {@link correlate} to ensure correct + * matching of requests and replies. Otherwise a late reply to an aborted + * request can be reported as the reply to the superseding request. + */ + relaxed: boolean + + constructor(options?: SocketOptions) { + super(SocketType.Request, options) + } +} + +export interface Request extends Readable, Writable {} +Object.assign(Request.prototype, {send, receive}) + + +/** + * A {@link Reply} socket can act as a server which receives requests from and + * sends replies to a {@link Request} socket. This socket type allows only an + * alternating sequence of {@link Readable.receive}() and subsequent + * {@link Writable.send}() calls. Each request received is fair-queued from + * among all clients, and each reply sent is routed to the client that issued + * the last request. If the original requester does not exist any more the reply + * is silently discarded. + */ +export class Reply extends Socket { + /** + * ZMQ_ROUTING_ID + * + * The identity of the specified socket when connecting to a `Router` socket. + */ + routingId: string | null + + constructor(options?: SocketOptions) { + super(SocketType.Reply, options) + } +} + +export interface Reply extends Readable, Writable {} +Object.assign(Reply.prototype, {send, receive}) + + +/** + * A {@link Dealer} socket can be used to extend request/reply sockets. Each + * message sent is round-robined among all connected peers, and each message + * received is fair-queued from all connected peers. + * + * When a {@link Dealer} socket enters the mute state due to having reached the + * high water mark for all peers, or if there are no peers at all, then any + * {@link Writable.send}() operations on the socket shall block until the mute + * state ends or at least one peer becomes available for sending; messages are + * not discarded. + * + * When a {@link Dealer} is connected to a {@link Reply} socket, each message + * sent must consist of an empty message part, the delimiter, followed by one or + * more body parts. + */ +export class Dealer extends Socket { + /** + * ZMQ_ROUTING_ID + * + * The identity of the specified socket when connecting to a `Router` socket. + */ + routingId: string | null + + /** + * ZMQ_PROBE_ROUTER + * + * When set to `true`, the socket will automatically send an empty message + * when a new connection is made or accepted. You may set this on sockets + * connected to a {@link Router} socket. The application must filter such + * empty messages. This option provides the {@link Router} with an event + * signaling the arrival of a new peer. + * + * *Warning:** Do not set this option on a socket that talks to any other + * socket type except {@link Router}: the results are undefined. + * + * @writeonly + */ + probeRouter: boolean + + /** + * ZMQ_CONFLATE + * + * If set to `true`, a socket shall keep only one message in its + * inbound/outbound queue: the last message to be received/sent. Ignores any + * high water mark options. Does not support multi-part messages – in + * particular, only one part of it is kept in the socket internal queue. + */ + conflate: boolean + + constructor(options?: SocketOptions) { + super(SocketType.Dealer, options) + } +} + +export interface Dealer extends Readable, Writable {} +Object.assign(Dealer.prototype, {send, receive}) + + +/** + * A {@link Router} can be used to extend request/reply sockets. When receiving + * messages a {@link Router} shall prepend a message part containing the routing + * id of the originating peer to the message. Messages received are fair-queued + * from among all connected peers. When sending messages, the first part of the + * message is removed and used to determine the routing id of the peer the + * message should be routed to. + * + * If the peer does not exist anymore, or has never existed, the message shall + * be silently discarded. However, if {@link Router.mandatory} is set to `true`, + * the socket shall fail with a `EHOSTUNREACH` error in both cases. + * + * When a {@link Router} enters the mute state due to having reached the high + * water mark for all peers, then any messages sent to the socket shall be + * dropped until the mute state ends. Likewise, any messages routed to a peer + * for which the individual high water mark has been reached shall also be + * dropped. If {@link Router.mandatory} is set to `true` the socket shall block + * or return an `EAGAIN` error in both cases. + * + * When a {@link Request} socket is connected to a {@link Router}, in addition + * to the routing id of the originating peer each message received shall contain + * an empty delimiter message part. Hence, the entire structure of each received + * message as seen by the application becomes: one or more routing id parts, + * delimiter part, one or more body parts. When sending replies to a + * {@link Request} the delimiter part must be included. + */ +export class Router extends Socket { + /** + * ZMQ_ROUTING_ID + * + * The identity of the specified socket when connecting to a `Router` socket. + */ + routingId: string | null + + /** + * ZMQ_ROUTER_MANDATORY + * + * A value of `false` is the default and discards the message silently when it + * cannot be routed or the peer's high water mark is reached. A value of + * `true` causes {@link send}() to fail if it cannot be routed, or wait + * asynchronously if the high water mark is reached. + */ + mandatory: boolean + + /** + * ZMQ_PROBE_ROUTER + * + * When set to `true`, the socket will automatically send an empty message + * when a new connection is made or accepted. You may set this on sockets + * connected to a {@link Router} socket. The application must filter such + * empty messages. This option provides the {@link Router} with an event + * signaling the arrival of a new peer. + * + * *Warning:** Do not set this option on a socket that talks to any other + * socket type except {@link Router}: the results are undefined. + * + * @writeonly + */ + probeRouter: boolean + + /** + * ZMQ_ROUTER_HANDOVER + * + * If two clients use the same identity when connecting to a {@link Router}, + * the results shall depend on the this option. If it set to `false` + * (default), the {@link Router} socket shall reject clients trying to connect + * with an already-used identity. If it is set to `true`, the {@link Router} + * socket shall hand-over the connection to the new client and disconnect the + * existing one. + */ + handover: boolean + + constructor(options?: SocketOptions) { + super(SocketType.Router, options) + } + + /** + * Connects to the given remote address. To specificy a specific routing id, + * provide a `routingId` option. The identity should be unique, from 1 to 255 + * bytes long and MAY NOT start with binary zero. + * + * @param address The `tcp://` address to connect to. + * @param options Any connection options. + */ + connect(address: string, options: RouterConnectOptions = {}) { + if (options.routingId) { + this.setStringOption(61, options.routingId) + } + + super.connect(address) + } +} + +interface RouterConnectOptions { + routingId?: string +} + +export interface Router extends Readable, Writable {} +Object.assign(Router.prototype, {send, receive}) + + +/** + * A {@link Pull} socket is used by a pipeline node to receive messages from + * upstream pipeline nodes. Messages are fair-queued from among all connected + * upstream nodes. This socket cannot send messages. + */ +export class Pull extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Pull, options) + } +} + +export interface Pull extends Readable { + /** + * ZMQ_CONFLATE + * + * If set to `true`, a socket shall keep only one message in its + * inbound/outbound queue: the last message to be received/sent. Ignores any + * high water mark options. Does not support multi-part messages – in + * particular, only one part of it is kept in the socket internal queue. + */ + conflate: boolean +} + +Object.assign(Pull.prototype, {receive}) + + +/** + * A {@link Push} socket is used by a pipeline node to send messages to + * downstream pipeline nodes. Messages are round-robined to all connected + * downstream nodes. This socket cannot receive messages. + * + * When a {@link Push} socket enters the mute state due to having reached the + * high water mark for all downstream nodes, or if there are no downstream nodes + * at all, then {@link Writable.send}() will block until the mute state ends or + * at least one downstream node becomes available for sending; messages are not + * discarded. + */ +export class Push extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.Push, options) + } +} + +export interface Push extends Writable { + /** + * ZMQ_CONFLATE + * + * If set to `true`, a socket shall keep only one message in its + * inbound/outbound queue: the last message to be received/sent. Ignores any + * high water mark options. Does not support multi-part messages – in + * particular, only one part of it is kept in the socket internal queue. + */ + conflate: boolean +} + +Object.assign(Push.prototype, {send}) + + +/** + * Same as {@link Publisher}, except that you can receive subscriptions from the + * peers in form of incoming messages. Subscription message is a byte 1 (for + * subscriptions) or byte 0 (for unsubscriptions) followed by the subscription + * body. Messages without a sub/unsub prefix are also received, but have no + * effect on subscription status. + */ +export class XPublisher extends Socket { + /** + * ZMQ_XPUB_NODROP + * + * Sets the socket behaviour to return an error if the high water mark is + * reached and the message could not be send. The default is to drop the + * message silently when the peer high water mark is reached. + */ + noDrop: boolean + + /** + * ZMQ_XPUB_MANUAL + * + * Sets the {@link XPublisher} socket subscription handling mode to + * manual/automatic. A value of `true` will change the subscription requests + * handling to manual. + */ + manual: boolean + + /** + * ZMQ_XPUB_WELCOME_MSG + * + * Sets a welcome message that will be recieved by subscriber when connecting. + * Subscriber must subscribe to the welcome message before connecting. For + * welcome messages to work well, poll on incoming subscription messages on + * the {@link XPublisher} socket and handle them. + */ + welcomeMessage: string | null + + /** + * ZMQ_INVERT_MATCHING + * + * Causes messages to be sent to all connected sockets except those subscribed + * to a prefix that matches the message. + */ + invertMatching: boolean + + /** + * ZMQ_XPUB_VERBOSE / ZMQ_XPUB_VERBOSER + * + * Whether to pass any duplicate subscription/unsuscription messages. + * * `null` (default) – Only unique subscribe and unsubscribe messages are + * visible to the caller. + * * `"allSubs"` – All subscribe messages (including duplicates) are visible + * to the caller, but only unique unsubscribe messages are visible. + * * `"allSubsUnsubs"` – All subscribe and unsubscribe messages (including + * duplicates) are visible to the caller. + */ + set verbosity(value: null | "allSubs" | "allSubsUnsubs") { + /* ZMQ_XPUB_VERBOSE and ZMQ_XPUB_VERBOSER interact, so we normalize the + situation by making it a single property. */ + switch (value) { + case null: + /* This disables ZMQ_XPUB_VERBOSE + ZMQ_XPUB_VERBOSER: */ + this.setBoolOption(40 /* ZMQ_XPUB_VERBOSE */, false); break + case "allSubs": + this.setBoolOption(40 /* ZMQ_XPUB_VERBOSE */, true); break + case "allSubsUnsubs": + this.setBoolOption(78 /* ZMQ_XPUB_VERBOSER */, true); break + } + } + + constructor(options?: SocketOptions) { + super(SocketType.XPublisher, options) + } +} + +export interface XPublisher extends Readable, Writable {} +Object.assign(XPublisher.prototype, {send, receive}) + + +/** + * Same as {@link Subscriber}, except that you subscribe by sending subscription + * messages to the socket. Subscription message is a byte 1 (for subscriptions) + * or byte 0 (for unsubscriptions) followed by the subscription body. Messages + * without a sub/unsub prefix may also be sent, but have no effect on + * subscription status. + */ +export class XSubscriber extends Socket { + constructor(options?: SocketOptions) { + super(SocketType.XSubscriber, options) + } +} + +export interface XSubscriber extends Readable, Writable {} +Object.assign(XSubscriber.prototype, {send, receive}) + + +/** + * A {@link Stream} is used to send and receive TCP data from a non-ØMQ peer + * with the `tcp://` transport. A {@link Stream} can act as client and/or + * server, sending and/or receiving TCP data asynchronously. + * + * When sending and receiving data with {@link Writable.send}() and + * {@link Readable.receive}(), the first message part shall be the routing id of + * the peer. Unroutable messages will cause an error. + * + * When a connection is made to a {@link Stream}, a zero-length message will be + * received. Similarly, when the peer disconnects (or the connection is lost), a + * zero-length message will be received. + * + * To close a specific connection, {@link Writable.send}() the routing id frame + * followed by a zero-length message. + * + * To open a connection to a server, use {@link Stream.connect}(). + */ +export class Stream extends Socket { + /** + * ZMQ_STREAM_NOTIFY + * + * Enables connect and disconnect notifications on a {@link Stream} when set + * to `true`. When notifications are enabled, the socket delivers a + * zero-length message when a peer connects or disconnects. + */ + notify: boolean + + constructor(options?: SocketOptions) { + super(SocketType.Stream, options) + } + + /** + * Connects to the given remote address. To specificy a specific routing id, + * provide a `routingId` option. The identity should be unique, from 1 to 255 + * bytes long and MAY NOT start with binary zero. + * + * @param address The `tcp://` address to connect to. + * @param options Any connection options. + */ + connect(address: string, options: StreamConnectOptions = {}) { + if (options.routingId) { + this.setStringOption(61, options.routingId) + } + + super.connect(address) + } +} + +interface StreamConnectOptions { + routingId?: string +} + +export interface Stream extends + Readable<[Message, Message]>, Writable<[MessageLike, MessageLike]> {} +Object.assign(Stream.prototype, {send, receive}) + + +/* Meta functionality to define new socket/context options. */ +const enum Type { + Bool = "Bool", + Int32 = "Int32", + Uint32 = "Uint32", + Int64 = "Int64", + Uint64 = "Uint64", + String = "String", +} + +/* Defines the accessibility of options. */ +const enum Acc { + Read = 1, + ReadOnly = 1, + + Write = 2, + WriteOnly = 2, + + ReadWrite = 3, +} + +/* tslint:disable-next-line: ban-types */ +type PrototypeOf = T extends Function & {prototype: infer U} ? U : never + +/* Readable properties may be set as readonly. */ +function defineOpt>>( + targets: T[], + name: K, + id: number, + type: Type, + acc: Acc.ReadOnly, + values?: Array, +): void + +/* Writable properties may be set as writeable or readable & writable. */ +function defineOpt>>( + targets: T[], + name: K, + id: number, + type: Type, + acc?: Acc.ReadWrite | Acc.WriteOnly, + values?: Array, +): void + +/* The default is to use R/w. The overloads above ensure the correct flag is + set if the property has been defined as readonly in the interface/class. */ +function defineOpt>>( + targets: T[], + name: K, + id: number, + type: Type, + acc: Acc = Acc.ReadWrite, + values?: Array, +): void { + const desc: PropertyDescriptor = {} + + if (acc & Acc.Read) { + if (values) { + desc.get = function get(this: any) { + return values[this[`get${type}Option`](id)] + } + } else { + desc.get = function get(this: any) { + return this[`get${type}Option`](id) + } + } + } + + if (acc & Acc.Write) { + if (values) { + desc.set = function set(this: any, val: any) { + this[`set${type}Option`](id, values.indexOf(val)) + } + } else { + desc.set = function set(this: any, val: any) { + this[`set${type}Option`](id, val) + } + } + } + + for (const target of targets) { + Object.defineProperty(target.prototype, name, desc) + } +} + +/* Context options. ALSO include any options in the Context interface above. */ +defineOpt([Context], "ioThreads", 1, Type.Int32) +defineOpt([Context], "maxSockets", 2, Type.Int32) +defineOpt([Context], "maxSocketsLimit", 3, Type.Int32, Acc.ReadOnly) +defineOpt([Context], "threadPriority", 3, Type.Int32, Acc.WriteOnly) +defineOpt([Context], "threadSchedulingPolicy", 4, Type.Int32, Acc.WriteOnly) +defineOpt([Context], "maxMessageSize", 5, Type.Int32) +defineOpt([Context], "ipv6", 42, Type.Bool) +defineOpt([Context], "blocky", 70, Type.Bool) +/* Option 'msgTSize' is fairly useless in Node.js. */ +/* These options should be methods. */ +/* defineOpt([Context], "threadAffinityCpuAdd", 7, Type.Int32) */ +/* defineOpt([Context], "threadAffinityCpuRemove", 8, Type.Int32) */ +/* To be released in a new ZeroMQ version. */ +/* if (Context.prototype.setStringOption) { + defineOpt([Context], "threadNamePrefix", 9, Type.String) +} */ +/* There should be no reason to change this in JS. */ +/* defineOpt([Context], "zeroCopyRecv", 10, Type.Bool) */ + +/* Socket options. ALSO include any options in the Socket interface above. */ +const writables = [ + Pair, Publisher, Request, Reply, Dealer, Router, Push, XPublisher, XSubscriber, Stream, + draft.Server, draft.Client, draft.Radio, draft.Scatter, draft.Datagram, +] + +defineOpt(writables, "sendBufferSize", 11, Type.Int32) +defineOpt(writables, "sendHighWaterMark", 23, Type.Int32) +defineOpt(writables, "sendTimeout", 28, Type.Int32) +defineOpt(writables, "multicastHops", 25, Type.Int32) + +const readables = [ + Pair, Subscriber, Request, Reply, Dealer, Router, Pull, XPublisher, XSubscriber, Stream, + draft.Server, draft.Client, draft.Dish, draft.Gather, draft.Datagram, +] + +defineOpt(readables, "receiveBufferSize", 12, Type.Int32) +defineOpt(readables, "receiveHighWaterMark", 24, Type.Int32) +defineOpt(readables, "receiveTimeout", 27, Type.Int32) + +defineOpt([Socket], "affinity", 4, Type.Uint64) +defineOpt([Request, Reply, Router, Dealer], "routingId", 5, Type.String) +defineOpt([Socket], "rate", 8, Type.Int32) +defineOpt([Socket], "recoveryInterval", 9, Type.Int32) +defineOpt([Socket], "type", 16, Type.Int32, Acc.ReadOnly) +defineOpt([Socket], "linger", 17, Type.Int32) +defineOpt([Socket], "reconnectInterval", 18, Type.Int32) +defineOpt([Socket], "backlog", 19, Type.Int32) +defineOpt([Socket], "reconnectMaxInterval", 21, Type.Int32) +defineOpt([Socket], "maxMessageSize", 22, Type.Int64) +defineOpt([Socket], "lastEndpoint", 32, Type.String, Acc.ReadOnly) +defineOpt([Router], "mandatory", 33, Type.Bool) +defineOpt([Socket], "tcpKeepalive", 34, Type.Int32) +defineOpt([Socket], "tcpKeepaliveCount", 35, Type.Int32) +defineOpt([Socket], "tcpKeepaliveIdle", 36, Type.Int32) +defineOpt([Socket], "tcpKeepaliveInterval", 37, Type.Int32) +defineOpt([Socket], "tcpAcceptFilter", 38, Type.String) +defineOpt([Socket], "immediate", 39, Type.Bool) +/* Option 'verbose' is implemented as verbosity on XPublisher. */ +defineOpt([Socket], "ipv6", 42, Type.Bool) +defineOpt([Socket], "securityMechanism", 43, Type.Int32, + Acc.ReadOnly, [null, "plain", "curve", "gssapi"]) +defineOpt([Socket], "plainServer", 44, Type.Bool) +defineOpt([Socket], "plainUsername", 45, Type.String) +defineOpt([Socket], "plainPassword", 46, Type.String) + +if (capability.curve) { + defineOpt([Socket], "curveServer", 47, Type.Bool) + defineOpt([Socket], "curvePublicKey", 48, Type.String) + defineOpt([Socket], "curveSecretKey", 49, Type.String) + defineOpt([Socket], "curveServerKey", 50, Type.String) +} + +defineOpt([Router, Dealer, Request], "probeRouter", 51, Type.Bool, Acc.WriteOnly) +defineOpt([Request], "correlate", 52, Type.Bool, Acc.WriteOnly) +defineOpt([Request], "relaxed", 53, Type.Bool, Acc.WriteOnly) + +defineOpt([Pull, Push, Subscriber, Publisher, Dealer, draft.Scatter, draft.Gather], + "conflate", 54, Type.Bool, Acc.WriteOnly) + +defineOpt([Socket], "zapDomain", 55, Type.String) +defineOpt([Router], "handover", 56, Type.Bool, Acc.WriteOnly) +defineOpt([Socket], "typeOfService", 57, Type.Uint32) + +if (capability.gssapi) { + defineOpt([Socket], "gssapiServer", 62, Type.Bool) + defineOpt([Socket], "gssapiPrincipal", 63, Type.String) + defineOpt([Socket], "gssapiServicePrincipal", 64, Type.String) + defineOpt([Socket], "gssapiPlainText", 65, Type.Bool) + defineOpt([Socket], "gssapiPrincipalNameType", 90, Type.Int32, + Acc.ReadWrite, ["hostBased", "userName", "krb5Principal"]) + defineOpt([Socket], "gssapiServicePrincipalNameType", 91, Type.Int32, + Acc.ReadWrite, ["hostBased", "userName", "krb5Principal"]) +} + +defineOpt([Socket], "handshakeInterval", 66, Type.Int32) +defineOpt([Socket], "socksProxy", 68, Type.String) +defineOpt([XPublisher, Publisher], "noDrop", 69, Type.Bool, Acc.WriteOnly) +defineOpt([XPublisher], "manual", 71, Type.Bool, Acc.WriteOnly) +defineOpt([XPublisher], "welcomeMessage", 72, Type.String, Acc.WriteOnly) +defineOpt([Stream], "notify", 73, Type.Bool, Acc.WriteOnly) +defineOpt([Publisher, Subscriber, XPublisher], "invertMatching", 74, Type.Bool) +defineOpt([Socket], "heartbeatInterval", 75, Type.Int32) +defineOpt([Socket], "heartbeatTimeToLive", 76, Type.Int32) +defineOpt([Socket], "heartbeatTimeout", 77, Type.Int32) +/* Option 'verboser' is implemented as verbosity on XPublisher. */ +defineOpt([Socket], "connectTimeout", 79, Type.Int32) +defineOpt([Socket], "tcpMaxRetransmitTimeout", 80, Type.Int32) +defineOpt([Socket], "threadSafe", 81, Type.Bool, Acc.ReadOnly) +defineOpt([Socket], "multicastMaxTransportDataUnit", 84, Type.Int32) +defineOpt([Socket], "vmciBufferSize", 85, Type.Uint64) +defineOpt([Socket], "vmciBufferMinSize", 86, Type.Uint64) +defineOpt([Socket], "vmciBufferMaxSize", 87, Type.Uint64) +defineOpt([Socket], "vmciConnectTimeout", 88, Type.Int32) +/* Option 'useFd' is fairly useless in Node.js. */ +defineOpt([Socket], "interface", 92, Type.String) +defineOpt([Socket], "zapEnforceDomain", 93, Type.Bool) +defineOpt([Socket], "loopbackFastPath", 94, Type.Bool) + +/* The following options are still in DRAFT. */ +/* defineOpt([Socket], "metadata", 95, Type.String) */ +/* defineOpt([Socket], "multicastLoop", 96, Type.String) */ +/* defineOpt([Router], "notify", 97, Type.String) */ +/* defineOpt([XPublisher], "manualLastValue", 98, Type.String) */ +/* defineOpt([Socket], "socksUsername", 99, Type.String) */ +/* defineOpt([Socket], "socksPassword", 100, Type.String) */ +/* defineOpt([Socket], "inBatchSize", 101, Type.String) */ +/* defineOpt([Socket], "outBatchSize", 102, Type.String) */ diff --git a/src/native.ts b/src/native.ts new file mode 100644 index 00000000..8d8e9ed9 --- /dev/null +++ b/src/native.ts @@ -0,0 +1,633 @@ +/* tslint:disable: no-var-requires ban-types */ + +/* Declare all native C++ classes and methods in this file. */ +const path = require("path") +module.exports = require("node-gyp-build")(path.join(__dirname, "..")) + + +/* We are removing public methods from the Socket prototype that do not apply + to all socket types. We will re-assign them to the prototypes of the + relevant sockets later. For send/receive it is important that they are not + wrapped in JS methods later, to ensure best performance. Any changes to + their signatures should be handled in C++ exclusively. */ +interface SpecializedMethods { + send: Function, + receive: Function, + join: Function, + leave: Function, +} + +const sack: Partial = {} +const target: SpecializedMethods = module.exports.Socket.prototype +for (const key of ["send", "receive", "join", "leave"] as const) { + sack[key] = target[key] + delete target[key] +} + +module.exports.methods = sack +export declare const methods: SpecializedMethods + + +/** + * The version of the ØMQ library the bindings were built with. Formatted as + * `(major).(minor).(patch)`. For example: `"4.3.2"`. + */ +export declare const version: string + + +/** + * Exposes some of the optionally available ØMQ capabilities, which may depend + * on the library version and platform. + * + * This is an object with keys corresponding to supported ØMQ features and + * transport protocols. Available capabilities will be set to `true`. + * Unavailable capabilities will be absent or set to `false`. + * + * Possible keys include: + * * `ipc` – Support for the `ipc://` protocol. + * * `pgm` – Support for the `pgm://` protocol. + * * `tipc` – Support for the `tipc://` protocol. + * * `norm` – Support for the `norm://` protocol. + * * `curve` – Support for the CURVE security mechanism. + * * `gssapi` – Support for the GSSAPI security mechanism. + * * `draft` – Wether the library is built with support for DRAFT sockets. + */ +export declare const capability: Partial<{ + ipc: boolean, + pgm: boolean, + tipc: boolean, + norm: boolean, + curve: boolean, + gssapi: boolean, + draft: boolean, +}> + + +/** + * Returns a new random key pair to be used with the CURVE security mechanism. + * + * To correctly connect two sockets with this mechanism: + * + * * Generate a **client** keypair with {@link curveKeyPair}(). + * * Assign the private and public key on the client socket with + * {@link Socket.curveSecretKey} and {@link Socket.curvePublicKey}. + * * Generate a **server** keypair with {@link curveKeyPair}(). + * * Assign the private key on the server socket with {@link Socket.curveSecretKey}. + * * Assign the public key **on the client socket** with + * {@link Socket.curveServerKey}. The server does *not* need to know its own + * public key. Key distribution is *not* handled by the CURVE security + * mechanism. + * + * + * @returns An object with a `publicKey` and a `secretKey` property, each being + * a 40 character Z85-encoded string. + */ +export declare function curveKeyPair(): { + publicKey: string, + secretKey: string, +} + + +/** + * A ØMQ context. Contexts manage the background I/O to send and receive + * messages of their associated sockets. + * + * It is usually not necessary to instantiate a new context – the global + * {@link context} is used for new sockets by default. + * + * ```typescript + * // Use default context (recommended). + * const socket = new Dealer() + * ``` + * + * ```typescript + * // Use custom context. + * const context = new Context() + * const socket = new Dealer({context}) + * ``` + */ +export declare class Context { + /** + * Creates a new ØMQ context and sets any provided context options. Sockets + * need to be explicitly associated with a new context during construction. + * + * @param options An optional object with options that will be set on the + * context during creation. + */ + constructor(options?: Options) + + protected getBoolOption(option: number): boolean + protected setBoolOption(option: number, value: boolean): void + + protected getInt32Option(option: number): number + protected setInt32Option(option: number, value: number): void +} + +/** + * Any socket that has no explicit context passed in during construction will + * be associated with this context. The default context is exposed in order to + * be able to change its behaviour with {@link Context} options. + */ +export declare const context: Context + + +interface ErrnoError extends Error { + code: string + errno: number +} + +interface EventAddress { + address: string +} + +interface EventInterval { + interval: number +} + +interface EventError { + error: ErrnoError +} + +type EventFor = Expand<{type: T} & D> + +/** + * A union type that represents all possible even types and the associated data. + * Events always have a `type` property with an {@link EventType} value. + * + * The following socket events can be generated. This list may be different + * depending on the ZeroMQ version that is used. + * + * Note that the **error** event is avoided by design, since this has a [special + * behaviour](https://nodejs.org/api/events.html#events_error_events) in Node.js + * causing an exception to be thrown if it is unhandled. + * + * Other error names are adjusted to be as close to possible as other + * [networking related](https://nodejs.org/api/net.html) event names in Node.js + * and/or to the corresponding ZeroMQ.js method call. Events (including any + * errors) that correspond to a specific operation are namespaced with a colon + * `:`, e.g. `bind:error` or `connect:retry`. + * + * * **accept** – ZMQ_EVENT_ACCEPTED The socket has accepted a connection from a + * remote peer. + * + * * **accept:error** – ZMQ_EVENT_ACCEPT_FAILED The socket has rejected a + * connection from a remote peer. + * + * The following additional details will be included with this event: + * + * * `error` – An error object that describes the specific error + * that occurred. + * + * * **bind** – ZMQ_EVENT_LISTENING The socket was successfully bound to a + * network interface. + * + * * **bind:error** – ZMQ_EVENT_BIND_FAILED The socket could not bind to a given + * interface. + * + * The following additional details will be included with this event: + * + * * `error` – An error object that describes the specific error + * that occurred. + * + * * **connect** – ZMQ_EVENT_CONNECTED The socket has successfully connected to + * a remote peer. + * + * * **connect:delay** – ZMQ_EVENT_CONNECT_DELAYED A connect request on the + * socket is pending. + * + * * **connect:retry** – ZMQ_EVENT_CONNECT_RETRIED A connection attempt is being + * handled by reconnect timer. Note that the reconnect interval is + * recalculated at each retry. + * + * The following additional details will be included with this event: + * + * * `interval` – The current reconnect interval. + * + * * **close** – ZMQ_EVENT_CLOSED The socket was closed. + * + * * **close:error** – ZMQ_EVENT_CLOSE_FAILED The socket close failed. Note that + * this event occurs **only on IPC** transports.. + * + * The following additional details will be included with this event: + * + * * `error` – An error object that describes the specific error + * that occurred. + * + * * **disconnect** – ZMQ_EVENT_DISCONNECTED The socket was disconnected + * unexpectedly. + * + * * **handshake** – ZMQ_EVENT_HANDSHAKE_SUCCEEDED The ZMTP security mechanism + * handshake succeeded. NOTE: This event may still be in DRAFT statea and not + * yet available in stable releases. + * + * * **handshake:error:protocol** – ZMQ_EVENT_HANDSHAKE_FAILED_PROTOCOL The ZMTP + * security mechanism handshake failed due to some mechanism protocol error, + * either between the ZMTP mechanism peers, or between the mechanism server + * and the ZAP handler. This indicates a configuration or implementation error + * in either peer resp. the ZAP handler. NOTE: This event may still be in + * DRAFT state and not yet available in stable releases. + * + * * **handshake:error:auth** – ZMQ_EVENT_HANDSHAKE_FAILED_AUTH The ZMTP + * security mechanism handshake failed due to an authentication failure. NOTE: + * This event may still be in DRAFT state and not yet available in stable + * releases. + * + * * **handshake:error:other** – ZMQ_EVENT_HANDSHAKE_FAILED_NO_DETAIL + * Unspecified error during handshake. NOTE: This event may still be in DRAFT + * state and not yet available in stable releases. + * + * * **end** – ZMQ_EVENT_MONITOR_STOPPED Monitoring on this socket ended. + * + * * **unknown** An event was generated by ZeroMQ that the Node.js library could + * not interpret. Please submit a pull request for new event types if they are + * not yet included. + */ +export type Event = ( + EventFor<"accept", EventAddress> | + EventFor<"accept:error", EventAddress & EventError> | + EventFor<"bind", EventAddress> | + EventFor<"bind:error", EventAddress & EventError> | + EventFor<"connect", EventAddress> | + EventFor<"connect:delay", EventAddress> | + EventFor<"connect:retry", EventAddress & EventInterval> | + EventFor<"close", EventAddress> | + EventFor<"close:error", EventAddress & EventError> | + EventFor<"disconnect", EventAddress> | + EventFor<"end"> | + EventFor<"handshake", EventAddress> | + EventFor<"handshake:error:protocol", EventAddress> | /* TODO add error data */ + EventFor<"handshake:error:auth", EventAddress> | /* TODO add error data */ + EventFor<"handshake:error:other", EventAddress & EventError> | + EventFor<"unknown"> +) + +/** + * A union type of all available event types. See {@link Event} for an overview + * of the events that can be observed. + */ +export type EventType = Event["type"] + +/** + * Represents the event data object given one particular event type, for example + * `EventOfType<"accept">`. + * + * @typeparam E The specific event type. + */ +export type EventOfType = + Expand>> + +/** + * An event observer for ØMQ sockets. This starts up a ZMQ monitoring socket + * internally that receives all socket events. The event observer can be used in + * one of two ways, which are **mutually exclusive**: with {@link receive}() or + * with event listeners attached with {@link on}(). + */ +export declare class Observer { + /** + * Whether the observer was closed, either manually or because the associated + * socket was closed. + * + * @readonly + */ + readonly closed: boolean + + /** + * Creates a new ØMQ observer. It should not be necessary to instantiate a new + * observer. Access an existing observer for a socket with + * {@link Socket.events}. + * + * ```typescript + * const socket = new Publisher() + * const events = socket.events + * ``` + * + * @param socket The socket to observe. + */ + constructor(socket: Socket) + + /** + * Closes the observer. Afterwards no new events will be received or emitted. + * Calling this method is optional. + */ + close(): void + + /** + * Waits for the next event to become availeble on the observer. Reads an + * event immediately if possible. If no events are queued, it will wait + * asynchonously. The promise will be resolved with the next event when + * available. + * + * When reading events with {@link receive}() the observer may **not** be in + * event emitter mode. Avoid mixing calls to {@link receive}() with event + * handlers via attached with {@link on}(). + * + * ```typescript + * for await (event of socket.events) { + * switch (event.type) { + * case "bind": + * console.log(`Socket bound to ${event.address}`) + * break + * // ... + * } + * } + * ``` + * + * @returns Resolved with the next event and its details. See {@link Event}. + */ + receive(): Promise +} + +/** + * Proxy messages between two ØMQ sockets. The proxy connects a front-end socket + * to a back-end socket. Conceptually, data flows from front-end to back-end. + * Depending on the socket types, replies may flow in the opposite direction. + * The direction is conceptual only; the proxy is fully symmetric and there is + * no technical difference between front-end and back-end. + * + * ```typescript + * // Proxy between a router/dealer socket for 5 seconds. + * const proxy = new Proxy(new Router, new Dealer) + * await proxy.frontEnd.bind("tcp://*:3001") + * await proxy.backEnd.bind("tcp://*:3002") + * setTimeout(() => proxy.terminate(), 5000) + * await proxy.run() + * ``` + * + * [Review the ØMQ documentation](http://api.zeromq.org/4-3:zmq-proxy#toc3) for + * an overview of some example applications of a proxy. + * + * @typeparam F The front-end socket type. + * @typeparam B The back-end socket type. + */ +export declare class Proxy< + F extends Socket = Socket, + B extends Socket = Socket, +> { + /** + * Returns the original front-end socket. + * + * @readonly + */ + readonly frontEnd: F + + /** + * Returns the original back-end socket. + * + * @readonly + */ + readonly backEnd: B + + /** + * Creates a new ØMQ proxy. Proxying will start between the front-end and + * back-end sockets when {@link run}() is called after both sockets have been + * bound or connected. + * + * @param frontEnd The front-end socket. + * @param backEnd The back-end socket. + */ + constructor(frontEnd: F, backEnd: B) + + /** + * Starts the proxy loop in a worker thread and waits for its termination. + * Before starting, you must set any socket options, and connect or bind both + * front-end and back-end sockets. + * + * On termination the front-end and back-end sockets will be closed + * automatically. + * + * @returns Resolved when the proxy has terminated. + */ + run(): Promise + + /** + * Temporarily suspends any proxy activity. Resume activity with + * {@link resume}(). + */ + pause(): void + + /** + * Resumes proxy activity after suspending it with {@link pause}(). + */ + resume(): void + + /** + * Gracefully shuts down the proxy. The front-end and back-end sockets will be + * closed automatically. There might be a slight delay between terminating and + * the {@link run}() method resolving. + */ + terminate(): void +} + +/** + * A ØMQ socket. This class should generally not be used directly. Instead, + * create one of its subclasses that corresponds to the socket type you want to + * use. + * + * ```typescript + * new zmq.Pair(...) + * new zmq.Publisher(...) + * new zmq.Subscriber(...) + * new zmq.Request(...) + * new zmq.Reply(...) + * new zmq.Dealer(...) + * new zmq.Router(...) + * new zmq.Pull(...) + * new zmq.Push(...) + * new zmq.XPublisher(...) + * new zmq.XSubscriber(...) + * new zmq.Stream(...) + * ``` + * + * Socket options can be set during construction or via a property after the + * socket was created. Most socket options do not take effect until the next + * {@link bind}() or {@link connect}() call. Setting such an option after the + * socket is already connected or bound will display a warning. + */ +export declare abstract class Socket { + /** + * Event {@link Observer} for this socket. This starts up a ØMQ monitoring + * socket internally that receives all socket events. + * + * @readonly + */ + readonly events: Observer + + /** + * {@link Context} that this socket belongs to. + * + * @readonly + */ + readonly context: Context + + /** + * Whether this socket was previously closed with {@link close}(). + * + * @readonly + */ + readonly closed: boolean + + /** + * Whether any messages are currently available. If `true`, the next call to + * {@link Readable.receive}() will immediately read a message from the socket. + * For sockets that cannot receive messsages this is always `false`. + * + * @readonly + */ + readonly readable: boolean + + /** + * Whether any messages can be queued for sending. If `true`, the next call to + * {@link Writable.send}() will immediately queue a message on the socket. + * For sockets that cannot send messsages this is always `false`. + * + * @readonly + */ + readonly writable: boolean + + /** + * Creates a new socket of the specified type. Subclasses are expected to + * provide the correct socket type. + * + * @param type The socket type. + * @param options Any options to set during construction. + */ + protected constructor(type: SocketType, options?: {}) + + /** + * Closes the socket and disposes of all resources. Any messages that are + * queued may be discarded or sent in the background depending on the + * {@link linger} setting. + * + * After this method is called, it is no longer possible to call any other + * methods on this socket. + * + * Sockets that go out of scope and have no {@link Readable.receive}() or + * {@link Writable.send}() operations in progress will automatically be + * closed. Therefore it is not necessary in most applications to call + * {@link close}() manually. + * + * Calling this method on a socket that is already closed is a no-op. + */ + close(): void + + /** + * Binds the socket to the given address. During {@link bind}() the socket + * cannot be used. Do not call any other methods until the returned promise + * resolves. Make sure to use `await`. + * + * ```typescript + * await socket.bind("tcp://*:3456") + * ``` + * + * @param address Address to bind this socket to. + * @returns Resolved when the socket was successfully bound. + */ + bind(address: string): Promise + + /** + * Unbinds the socket to the given address. During {@link unbind}() the socket + * cannot be used. Do not call any other methods until the returned promise + * resolves. Make sure to use `await`. + * + * @param address Address to unbind this socket from. + * @returns Resolved when the socket was successfully unbound. + */ + unbind(address: string): Promise + + /** + * Connects to the socket at the given remote address and returns immediately. + * The connection will be made asynchronously in the background. + * + * ```typescript + * socket.connect("tcp://127.0.0.1:3456") + * ``` + * + * @param address The address to connect to. + */ + connect(address: string): void + + /** + * Disconnects a previously connected socket from the given address and + * returns immediately. Disonnection will happen asynchronously in the + * background. + * + * ```typescript + * socket.disconnect("tcp://127.0.0.1:3456") + * ``` + * + * @param address The previously connected address to disconnect from. + */ + disconnect(address: string): void + + /* The following methods are meant to be called by generated JS code only + from specialized subclasses. */ + + protected getBoolOption(option: number): boolean + protected setBoolOption(option: number, value: boolean): void + + protected getInt32Option(option: number): number + protected setInt32Option(option: number, value: number): void + + protected getUint32Option(option: number): number + protected setUint32Option(option: number, value: number): void + + protected getInt64Option(option: number): number + protected setInt64Option(option: number, value: number): void + + protected getUint64Option(option: number): number + protected setUint64Option(option: number, value: number): void + + protected getStringOption(option: number): string | null + protected setStringOption(option: number, value: string | Buffer | null): void +} + + +export const enum SocketType { + Pair = 0, + Publisher = 1, + Subscriber = 2, + Request = 3, + Reply = 4, + Dealer = 5, + Router = 6, + Pull = 7, + Push = 8, + XPublisher = 9, + XSubscriber = 10, + Stream = 11, + + /* DRAFT socket types. */ + Server = 12, + Client = 13, + Radio = 14, + Dish = 15, + Gather = 16, + Scatter = 17, + Datagram = 18, +} + + +/* Utility types. */ + +/* https://stackoverflow.com/questions/49579094 */ +type IfEquals = + (() => T extends X ? 1 : 2) extends + (() => T extends Y ? 1 : 2) ? A : B + +/* https://stackoverflow.com/questions/57683303 */ +type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never + +/** @internal */ +export type ReadableKeys = { + /* tslint:disable-next-line: ban-types */ + [P in keyof T]-?: T[P] extends Function ? never : P +}[keyof T] + +/** @internal */ +export type WritableKeys = { + /* tslint:disable-next-line: ban-types */ + [P in keyof T]-?: T[P] extends Function ? + never : IfEquals<{[Q in P]: T[P]}, {-readonly [Q in P]: T[P]}, P> +}[keyof T] + +export type Options = Expand>>> diff --git a/src/observer.cc b/src/observer.cc new file mode 100644 index 00000000..cfe22d4b --- /dev/null +++ b/src/observer.cc @@ -0,0 +1,272 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#include "observer.h" +#include "context.h" +#include "socket.h" + +#include "incoming_msg.h" +#include "util/async_scope.h" + +#include + +namespace zmq { +Napi::FunctionReference Observer::Constructor; + +template +auto constexpr make_array(N&&... args) -> std::array { + return {{std::forward(args)...}}; +} + +/* Events must be in order corresponding to the value of the #define value. */ +static auto events = make_array("connect", // ZMQ_EVENT_CONNECTED + "connect:delay", // ZMQ_EVENT_CONNECT_DELAYED + "connect:retry", // ZMQ_EVENT_CONNECT_RETRIED + "bind", // ZMQ_EVENT_LISTENING + "bind:error", // ZMQ_EVENT_BIND_FAILED + "accept", // ZMQ_EVENT_ACCEPTED + "accept:error", // ZMQ_EVENT_ACCEPT_FAILED + "close", // ZMQ_EVENT_CLOSED + "close:error", // ZMQ_EVENT_CLOSE_FAILED + "disconnect", // ZMQ_EVENT_DISCONNECTED + "end", // ZMQ_EVENT_MONITOR_STOPPED + "handshake:error:other", // ZMQ_EVENT_HANDSHAKE_FAILED_NO_DETAIL + "handshake", // ZMQ_EVENT_HANDSHAKE_SUCCEEDED + "handshake:error:protocol", // ZMQ_EVENT_HANDSHAKE_FAILED_PROTOCOL + "handshake:error:auth", // ZMQ_EVENT_HANDSHAKE_FAILED_AUTH + /* ^-- Insert new events here. */ + "unknown"); + +/* https://stackoverflow.com/questions/757059/position-of-least-significant-bit-that-is-set + */ +static inline const char* EventName(uint32_t val) { + static const int multiply[32] = {0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, + 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; + + uint32_t ffs = multiply[((uint32_t)((val & -val) * 0x077CB531U)) >> 27]; + + if (ffs >= events.size()) return events.back(); + return events[ffs]; +} + +Observer::Observer(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info), async_context(Env(), "Observer"), poller(*this) { + auto args = { + Argument{"Socket must be a socket object", &Napi::Value::IsObject}, + }; + + if (!ValidateArguments(info, args)) return; + + auto target = Socket::Unwrap(info[0].As()); + if (Env().IsExceptionPending()) return; + + /* Use `this` pointer as unique identifier for monitoring socket. */ + auto address = std::string("inproc://zmq.monitor.") + + to_string(reinterpret_cast(this)); + + if (zmq_socket_monitor(target->socket, address.c_str(), ZMQ_EVENT_ALL) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + + auto context = Context::Unwrap(target->context_ref.Value()); + socket = zmq_socket(context->context, ZMQ_PAIR); + if (socket == nullptr) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + + uv_os_sock_t fd; + size_t length = sizeof(fd); + + if (zmq_connect(socket, address.c_str()) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + goto error; + } + + if (zmq_getsockopt(socket, ZMQ_FD, &fd, &length) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + goto error; + } + + if (poller.Initialize(Env(), fd) < 0) { + ErrnoException(Env(), errno).ThrowAsJavaScriptException(); + goto error; + } + + /* Initialization was successful, store the socket pointer in a list for + cleanup at process exit. */ + Socket::ActivePtrs.insert(socket); + + return; + +error: + auto err = zmq_close(socket); + assert(err == 0); + + socket = nullptr; + return; +} + +Observer::~Observer() { + Close(); +} + +bool Observer::ValidateOpen() const { + if (socket == nullptr) { + ErrnoException(Env(), EBADF).ThrowAsJavaScriptException(); + return false; + } + + return true; +} + +bool Observer::HasEvents() const { + int32_t events; + size_t events_size = sizeof(events); + + while (zmq_getsockopt(socket, ZMQ_EVENTS, &events, &events_size) < 0) { + /* Ignore errors. */ + if (zmq_errno() != EINTR) return 0; + } + + return events & ZMQ_POLLIN; +} + +void Observer::Close() { + if (socket != nullptr) { + Napi::HandleScope scope(Env()); + + /* Close succeeds unless socket is invalid. */ + Socket::ActivePtrs.erase(socket); + auto err = zmq_close(socket); + assert(err == 0); + + socket = nullptr; + + /* Stop all polling and release event handlers. Callling this after + setting socket to null causes a pending receive promise to be + resolved with undefined. */ + poller.Close(); + } +} + +void Observer::Receive(const Napi::Promise::Deferred& res) { + zmq_msg_t msg1; + zmq_msg_t msg2; + + zmq_msg_init(&msg1); + while (zmq_msg_recv(&msg1, socket, ZMQ_DONTWAIT) < 0) { + if (zmq_errno() != EINTR) { + res.Reject(ErrnoException(Env(), zmq_errno()).Value()); + zmq_msg_close(&msg1); + return; + } + } + + auto data1 = static_cast(zmq_msg_data(&msg1)); + auto event_id = *reinterpret_cast(data1); + auto event_value = *reinterpret_cast(data1 + 2); + zmq_msg_close(&msg1); + + zmq_msg_init(&msg2); + while (zmq_msg_recv(&msg2, socket, ZMQ_DONTWAIT) < 0) { + if (zmq_errno() != EINTR) { + res.Reject(ErrnoException(Env(), zmq_errno()).Value()); + zmq_msg_close(&msg2); + return; + } + } + + auto data2 = reinterpret_cast(zmq_msg_data(&msg2)); + auto length = zmq_msg_size(&msg2); + + auto event = Napi::Object::New(Env()); + event["type"] = Napi::String::New(Env(), EventName(event_id)); + + if (length > 0) { + event["address"] = Napi::String::New(Env(), data2, length); + } + + zmq_msg_close(&msg2); + + switch (event_id) { + case ZMQ_EVENT_CONNECT_RETRIED: + event["interval"] = Napi::Number::New(Env(), event_value); + break; + case ZMQ_EVENT_BIND_FAILED: + case ZMQ_EVENT_ACCEPT_FAILED: + case ZMQ_EVENT_CLOSE_FAILED: +#ifdef ZMQ_EVENT_HANDSHAKE_FAILED_NO_DETAIL + case ZMQ_EVENT_HANDSHAKE_FAILED_NO_DETAIL: +#endif + event["error"] = ErrnoException(Env(), event_value).Value(); + break; + case ZMQ_EVENT_MONITOR_STOPPED: + /* Also close the monitoring socket. */ + Close(); + break; + // case ZMQ_EVENT_HANDSHAKE_FAILED_PROTOCOL: + // case ZMQ_EVENT_HANDSHAKE_FAILED_AUTH: + } + + res.Resolve(event); +} + +void Observer::Close(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return; + + Close(); +} + +Napi::Value Observer::Receive(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return Env().Undefined(); + if (!ValidateOpen()) return Env().Undefined(); + + if (HasEvents()) { + /* We can read from the socket immediately. This is a separate code + path so we can avoid creating a lambda. */ + auto res = Napi::Promise::Deferred::New(Env()); + Receive(res); + return res.Promise(); + } else { + /* Check if we are already polling for reads. Only one promise may + receive the next message, so we must ensure that receive + operations are in sequence. */ + if (poller.PollingReadable()) { + ErrnoException(Env(), EAGAIN).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return poller.ReadPromise(); + } +} + +Napi::Value Observer::GetClosed(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(Env(), socket == nullptr); +} + +void Observer::Initialize(Napi::Env& env, Napi::Object& exports) { + auto proto = { + InstanceMethod("close", &Observer::Close), + InstanceMethod("receive", &Observer::Receive), + InstanceAccessor("closed", &Observer::GetClosed, nullptr), + }; + + auto constructor = DefineClass(env, "Observer", proto); + + Constructor = Napi::Persistent(constructor); + Constructor.SuppressDestruct(); + + exports.Set("Observer", constructor); +} + +void Observer::Poller::ReadableCallback() { + AsyncScope scope(read_deferred.Env(), socket.async_context); + socket.Receive(read_deferred); +} + +Napi::Value Observer::Poller::ReadPromise() { + read_deferred = Napi::Promise::Deferred(read_deferred.Env()); + zmq::Poller::PollReadable(0); + return read_deferred.Promise(); +} +} diff --git a/src/observer.h b/src/observer.h new file mode 100644 index 00000000..994045cb --- /dev/null +++ b/src/observer.h @@ -0,0 +1,60 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "binding.h" +#include "poller.h" + +namespace zmq { +class Observer : public Napi::ObjectWrap { +public: + static Napi::FunctionReference Constructor; + static void Initialize(Napi::Env& env, Napi::Object& exports); + + explicit Observer(const Napi::CallbackInfo& info); + ~Observer(); + +protected: + inline void Close(const Napi::CallbackInfo& info); + inline Napi::Value Receive(const Napi::CallbackInfo& info); + + inline Napi::Value GetClosed(const Napi::CallbackInfo& info); + +private: + inline bool ValidateOpen() const; + bool HasEvents() const; + void Close(); + + force_inline void Receive(const Napi::Promise::Deferred& res); + + class Poller : public zmq::Poller { + Observer& socket; + Napi::Promise::Deferred read_deferred; + + public: + explicit Poller(Observer& observer) + : socket(observer), read_deferred(socket.Env()) {} + + Napi::Value ReadPromise(); + + inline bool ValidateReadable() const { + return socket.HasEvents(); + } + + inline bool ValidateWritable() const { + return false; + } + + void ReadableCallback(); + inline void WritableCallback() {} + }; + + Napi::AsyncContext async_context; + Observer::Poller poller; + void* socket = nullptr; + + friend class Socket; +}; +} + +static_assert(!std::is_copy_constructible::value, "not copyable"); +static_assert(!std::is_move_constructible::value, "not movable"); diff --git a/src/outgoing_msg.cc b/src/outgoing_msg.cc new file mode 100644 index 00000000..cc3787e0 --- /dev/null +++ b/src/outgoing_msg.cc @@ -0,0 +1,163 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#include "outgoing_msg.h" + +namespace zmq { +/* Static collection of outgoing message references that can be recycled. */ +Trash OutgoingMsg::trash; + +OutgoingMsg::OutgoingMsg(Napi::Value value) { + static auto constexpr zero_copy_threshold = 32; + auto buffer_send = [&](uint8_t* data, size_t length) { + /* Zero-copy heuristic. There's an overhead in releasing the buffer with an + async call to the main thread (v8 is not threadsafe), so copying small + amounts of memory is faster than releasing the initial buffer + asynchronously. */ + if (length > zero_copy_threshold) { + /* Create a reference and a recycle lambda which is called when the + message is sent by ZeroMQ on an *arbitrary* thread. It will add + the reference to the global trash, which will schedule a callback + on the main v8 thread in order to safely dispose of the reference. */ + auto ref = new Reference(value); + auto recycle = [](void*, void* item) { + trash.Add(static_cast(item)); + }; + + if (zmq_msg_init_data(&msg, data, length, recycle, ref) < 0) { + /* Initialisation failed, so the recycle callback is not called and we + have to clean up the reference manually. */ + delete ref; + ErrnoException(value.Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + } else { + if (zmq_msg_init_size(&msg, length) < 0) { + ErrnoException(value.Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + + std::copy(data, data + length, static_cast(zmq_msg_data(&msg))); + } + }; + + /* String data should first be converted to UTF-8 before we can send it; + but once converted we do not have to copy a second time. */ + auto string_send = [&](std::string* str) { + auto length = str->size(); + auto data = const_cast(str->data()); + + auto release = [](void*, void* str) { + delete reinterpret_cast(str); + }; + + if (zmq_msg_init_data(&msg, data, length, release, str) < 0) { + delete str; + ErrnoException(value.Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + }; + + /* It is likely that the message is either a buffer or a string. Don't test + for other object types (such as array buffer), until we've established + it is neither! */ + if (value.IsBuffer()) { + auto buf = value.As>(); + buffer_send(buf.Data(), buf.Length()); + } else { + switch (value.Type()) { + case napi_null: + zmq_msg_init(&msg); + return; + + case napi_string: + string_send(new std::string(value.As())); + return; + + case napi_object: + if (value.IsArrayBuffer()) { + auto buf = value.As(); + buffer_send(static_cast(buf.Data()), buf.ByteLength()); + return; + } + /* Fall through */ + + default: + string_send(new std::string(value.ToString())); + } + } +} + +OutgoingMsg::~OutgoingMsg() { + auto err = zmq_msg_close(&msg); + assert(err == 0); +} + +void OutgoingMsg::Initialize(Napi::Env env) { + trash.Initialize(env); +} + +void OutgoingMsg::Terminate() { + trash.Terminate(); +} + +OutgoingMsg::Parts::Parts(Napi::Value value) { + if (value.IsArray()) { + /* Reverse insert parts into outgoing message list. */ + auto arr = value.As(); + for (auto i = arr.Length(); i--;) { + parts.emplace_front(arr[i]); + } + } else { + parts.emplace_front(value); + } +} + +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE +bool OutgoingMsg::Parts::SetGroup(Napi::Value value) { + if (value.IsUndefined()) { + ErrnoException(value.Env(), EINVAL).ThrowAsJavaScriptException(); + return false; + } + + auto group = [&]() { + if (value.IsString()) { + return std::string(value.As()); + } else if (value.IsBuffer()) { + Napi::Object buf = value.As(); + auto length = buf.As>().Length(); + auto value = buf.As>().Data(); + return std::string(value, length); + } else { + return std::string(); + } + }(); + + for (auto& part : parts) { + if (zmq_msg_set_group(part, group.c_str()) < 0) { + ErrnoException(value.Env(), zmq_errno()).ThrowAsJavaScriptException(); + return false; + } + } + + return true; +} + +bool OutgoingMsg::Parts::SetRoutingId(Napi::Value value) { + if (value.IsUndefined()) { + ErrnoException(value.Env(), EINVAL).ThrowAsJavaScriptException(); + return false; + } + + auto id = value.As().Uint32Value(); + + for (auto& part : parts) { + if (zmq_msg_set_routing_id(part, id) < 0) { + ErrnoException(value.Env(), zmq_errno()).ThrowAsJavaScriptException(); + return false; + } + } + + return true; +} +#endif + +} diff --git a/src/outgoing_msg.h b/src/outgoing_msg.h new file mode 100644 index 00000000..eea805b8 --- /dev/null +++ b/src/outgoing_msg.h @@ -0,0 +1,74 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "binding.h" +#include "util/trash.h" + +#include + +namespace zmq { +class OutgoingMsg { +public: + class Parts; + + static void Initialize(Napi::Env env); + static void Terminate(); + + /* Avoid copying outgoing messages, since the destructor is not copy safe, + nor should we have to copy messages with the right STL containers. */ + OutgoingMsg(const OutgoingMsg&) = delete; + OutgoingMsg& operator=(const OutgoingMsg&) = delete; + + /* Outgoing message. Takes a string or buffer argument and releases + the underlying V8 resources whenever the message is sent, or earlier + if the message was copied (small buffers & strings). */ + explicit OutgoingMsg(Napi::Value value); + ~OutgoingMsg(); + + inline operator zmq_msg_t*() { + return &msg; + } + +private: + class Reference { + Napi::Reference persistent; + + public: + inline explicit Reference(Napi::Value val) : persistent(Napi::Persistent(val)) {} + }; + + static Trash trash; + + zmq_msg_t msg; +}; + +/* Simple list over outgoing messages. Will take a single v8 value or an array + of values and keep references to these items as necessary. */ +class OutgoingMsg::Parts { + std::forward_list parts; + +public: + inline Parts() {} + explicit Parts(Napi::Value value); + + inline std::forward_list::iterator begin() { + return parts.begin(); + } + + inline std::forward_list::iterator end() { + return parts.end(); + } + +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE + bool SetGroup(Napi::Value value); + bool SetRoutingId(Napi::Value value); +#endif + + inline void Clear() { + parts.clear(); + } +}; +} + +static_assert(!std::is_copy_constructible::value, "not copyable"); +static_assert(!std::is_move_constructible::value, "not movable"); diff --git a/src/poller.h b/src/poller.h new file mode 100644 index 00000000..a122f65f --- /dev/null +++ b/src/poller.h @@ -0,0 +1,185 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "util/uvhandle.h" +#include "util/uvloop.h" + +namespace zmq { +/* Starts a UV poller with an attached timeout. The poller can be started + and stopped multiple times. */ +template +class Poller { + UvHandle poll; + + UvHandle readable_timer; + UvHandle writable_timer; + + int32_t events{0}; + std::function finalize = nullptr; + +public: + /* Initialize the poller with the given file descriptor. FD should be + ZMQ style edge-triggered, with READABLE state indicating that ANY + event may be present on the corresponding ZMQ socket. */ + inline int32_t Initialize( + Napi::Env env, uv_os_sock_t& fd, std::function finalizer = nullptr) { + int32_t err; + auto loop = UvLoop(env); + + /* Initialize uv pollers and timers, but unreference them immediately. + The poller will be referenced once the socket connects or binds. + The timers are weak references and will never prevent an exit. */ + + poll->data = this; + err = uv_poll_init_socket(loop, poll, fd); + if (err != 0) return err; + uv_unref(poll); + + readable_timer->data = this; + err = uv_timer_init(loop, readable_timer); + if (err != 0) return err; + uv_unref(readable_timer); + + writable_timer->data = this; + err = uv_timer_init(loop, writable_timer); + if (err != 0) return err; + uv_unref(writable_timer); + + finalize = finalizer; + return 0; + } + + inline void Ref() { + /* Avoid segfault if accidentally called after closing. */ + if (poll != nullptr) { + uv_ref(poll); + } + } + + inline void Unref() { + /* Avoid segfault if accidentally called after closing. */ + if (poll != nullptr) { + uv_unref(poll); + } + } + + /* Safely close and release all handles. This can be called before + destruction to release resources early. */ + inline void Close() { + /* Trigger all watched events manually, which causes any pending + operation to succeed or fail immediately. */ + if (events) Trigger(events); + + /* Pollers and timers are stopped automatically by uv_close() which is + wrapped in UvHandle. */ + + /* Release references to all UV handles. */ + poll.reset(nullptr); + readable_timer.reset(nullptr); + writable_timer.reset(nullptr); + + if (finalize) finalize(); + } + + inline bool PollingReadable() const { + return events & UV_READABLE; + } + + inline bool PollingWritable() const { + return events & UV_WRITABLE; + } + + /* Start polling for readable state, with the given timeout. */ + inline void PollReadable(int64_t timeout) { + assert((events & UV_READABLE) == 0); + + if (timeout > 0) { + auto result = uv_timer_start(readable_timer, + [](uv_timer_t* timer) { + auto& poller = *reinterpret_cast(timer->data); + poller.Trigger(UV_READABLE); + }, + timeout, 0); + + assert(result == 0); + } + + if (!events) { + /* Only start polling if we were not polling already. */ + auto result = uv_poll_start(poll, UV_READABLE, Callback); + assert(result == 0); + } + + events |= UV_READABLE; + } + + inline void PollWritable(int64_t timeout) { + assert((events & UV_WRITABLE) == 0); + + if (timeout > 0) { + auto result = uv_timer_start(writable_timer, + [](uv_timer_t* timer) { + auto& poller = *reinterpret_cast(timer->data); + poller.Trigger(UV_WRITABLE); + }, + timeout, 0); + + assert(result == 0); + } + + /* Note: We poll for READS only! "ZMQ shall signal ANY pending + events on the socket in an edge-triggered fashion by making the + file descriptor become ready for READING." */ + if (!events) { + auto result = uv_poll_start(poll, UV_READABLE, Callback); + assert(result == 0); + } + + events |= UV_WRITABLE; + } + + /* Trigger any events that are ready. Use validation callbacks to see + which events are actually available. */ + inline void Trigger() { + if (events & UV_READABLE) { + if (static_cast(this)->ValidateReadable()) Trigger(UV_READABLE); + } + + if (events & UV_WRITABLE) { + if (static_cast(this)->ValidateWritable()) Trigger(UV_WRITABLE); + } + } + +private: + /* Trigger one or more specific events manually. No validation is + performed, which means these will cause EAGAIN errors if no events + were actually available. */ + inline void Trigger(int32_t triggered) { + events &= ~triggered; + if (!events) { + auto result = uv_poll_stop(poll); + assert(result == 0); + } + + if (triggered & UV_READABLE) { + auto result = uv_timer_stop(readable_timer); + assert(result == 0); + static_cast(this)->ReadableCallback(); + } + + if (triggered & UV_WRITABLE) { + auto result = uv_timer_stop(writable_timer); + assert(result == 0); + static_cast(this)->WritableCallback(); + } + } + + /* Callback is called when FD is set to a readable state. This is an + edge trigger that should allow us to check for read AND write events. + There is no guarantee that any events are available. */ + static void Callback(uv_poll_t* poll, int32_t status, int32_t events) { + auto& poller = *reinterpret_cast(poll->data); + if (status == 0) poller.Trigger(); + }; +}; +} diff --git a/src/proxy.cc b/src/proxy.cc new file mode 100644 index 00000000..67bd9381 --- /dev/null +++ b/src/proxy.cc @@ -0,0 +1,198 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#include "proxy.h" +#include "context.h" +#include "socket.h" + +#include "util/async_scope.h" +#include "util/uvwork.h" + +#ifdef ZMQ_HAS_STEERABLE_PROXY + +namespace zmq { +Napi::FunctionReference Proxy::Constructor; + +struct ProxyContext { + std::string address; + uint32_t error = 0; + + ProxyContext(std::string&& address) : address(std::move(address)) {} +}; + +Proxy::Proxy(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info), async_context(Env(), "Proxy") { + auto args = { + Argument{"Front-end must be a socket object", &Napi::Value::IsObject}, + Argument{"Back-end must be a socket object", &Napi::Value::IsObject}, + }; + + if (!ValidateArguments(info, args)) return; + + front_ref.Reset(info[0].As(), 1); + Socket::Unwrap(front_ref.Value()); + if (Env().IsExceptionPending()) return; + + back_ref.Reset(info[1].As(), 1); + Socket::Unwrap(back_ref.Value()); + if (Env().IsExceptionPending()) return; +} + +Proxy::~Proxy() {} + +Napi::Value Proxy::Run(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return Env().Undefined(); + + auto front = Socket::Unwrap(front_ref.Value()); + if (Env().IsExceptionPending()) return Env().Undefined(); + + auto back = Socket::Unwrap(back_ref.Value()); + if (Env().IsExceptionPending()) return Env().Undefined(); + + if (front->endpoints == 0) { + Napi::Error::New(Env(), "Front-end socket must be bound or connected") + .ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + if (back->endpoints == 0) { + Napi::Error::New(Env(), "Back-end socket must be bound or connected") + .ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + auto context = Context::Unwrap(front->context_ref.Value()); + if (Env().IsExceptionPending()) return Env().Undefined(); + + control_sub = zmq_socket(context->context, ZMQ_DEALER); + if (control_sub != nullptr) { + Socket::ActivePtrs.insert(control_sub); + } else { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + control_pub = zmq_socket(context->context, ZMQ_DEALER); + if (control_pub != nullptr) { + Socket::ActivePtrs.insert(control_pub); + } else { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + /* Use `this` pointer as unique identifier for control socket. */ + auto address = std::string("inproc://zmq.proxycontrol.") + + to_string(reinterpret_cast(this)); + + /* Connect publisher so we can start queueing control messages. */ + if (zmq_connect(control_pub, address.c_str()) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + front->state = Socket::State::Blocked; + back->state = Socket::State::Blocked; + + auto res = Napi::Promise::Deferred::New(Env()); + auto run_ctx = std::make_shared(std::move(address)); + + auto front_ptr = front->socket; + auto back_ptr = back->socket; + + auto status = UvQueue(Env(), + [=]() { + /* Don't access V8 internals here! Executed in worker thread. */ + if (zmq_bind(control_sub, run_ctx->address.c_str()) < 0) { + run_ctx->error = zmq_errno(); + return; + } + + if (zmq_proxy_steerable(front_ptr, back_ptr, nullptr, control_sub) < 0) { + run_ctx->error = zmq_errno(); + return; + } + }, + [=]() { + AsyncScope scope(Env(), async_context); + + front->Close(); + back->Close(); + + Socket::ActivePtrs.erase(control_pub); + auto err1 = zmq_close(control_pub); + assert(err1 == 0); + + Socket::ActivePtrs.erase(control_sub); + auto err2 = zmq_close(control_sub); + assert(err2 == 0); + + control_pub = nullptr; + control_sub = nullptr; + + if (run_ctx->error != 0) { + res.Reject(ErrnoException(Env(), run_ctx->error).Value()); + return; + } + + res.Resolve(Env().Undefined()); + }); + + if (status < 0) { + ErrnoException(Env(), EBADF).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return res.Promise(); +} + +void Proxy::SendCommand(const char* command) { + while (zmq_send_const(control_pub, command, strlen(command), ZMQ_DONTWAIT) < 0) { + if (zmq_errno() != EINTR) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + } +} + +void Proxy::Pause(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return; + SendCommand("PAUSE"); +} + +void Proxy::Resume(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return; + SendCommand("RESUME"); +} + +void Proxy::Terminate(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return; + SendCommand("TERMINATE"); +} + +Napi::Value Proxy::GetFrontEnd(const Napi::CallbackInfo& info) { + return front_ref.Value(); +} + +Napi::Value Proxy::GetBackEnd(const Napi::CallbackInfo& info) { + return back_ref.Value(); +} + +void Proxy::Initialize(Napi::Env& env, Napi::Object& exports) { + auto proto = { + InstanceMethod("run", &Proxy::Run), + InstanceMethod("pause", &Proxy::Pause), + InstanceMethod("resume", &Proxy::Resume), + InstanceMethod("terminate", &Proxy::Terminate), + + InstanceAccessor("frontEnd", &Proxy::GetFrontEnd, nullptr), + InstanceAccessor("backEnd", &Proxy::GetBackEnd, nullptr), + }; + + auto constructor = DefineClass(env, "Proxy", proto); + + Constructor = Napi::Persistent(constructor); + Constructor.SuppressDestruct(); + + exports.Set("Proxy", constructor); +} +} + +#endif diff --git a/src/proxy.h b/src/proxy.h new file mode 100644 index 00000000..8166dd5e --- /dev/null +++ b/src/proxy.h @@ -0,0 +1,43 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "binding.h" + +#ifdef ZMQ_HAS_STEERABLE_PROXY + +namespace zmq { +class Proxy : public Napi::ObjectWrap { +public: + static Napi::FunctionReference Constructor; + static void Initialize(Napi::Env& env, Napi::Object& exports); + + explicit Proxy(const Napi::CallbackInfo& info); + ~Proxy(); + +protected: + inline Napi::Value Run(const Napi::CallbackInfo& info); + + inline void Pause(const Napi::CallbackInfo& info); + inline void Resume(const Napi::CallbackInfo& info); + inline void Terminate(const Napi::CallbackInfo& info); + + inline Napi::Value GetFrontEnd(const Napi::CallbackInfo& info); + inline Napi::Value GetBackEnd(const Napi::CallbackInfo& info); + +private: + inline void SendCommand(const char* command); + + Napi::AsyncContext async_context; + Napi::ObjectReference front_ref; + Napi::ObjectReference back_ref; + Napi::ObjectReference capture_ref; + + void* control_sub = nullptr; + void* control_pub = nullptr; +}; +} + +static_assert(!std::is_copy_constructible::value, "not copyable"); +static_assert(!std::is_move_constructible::value, "not movable"); + +#endif diff --git a/src/socket.cc b/src/socket.cc new file mode 100644 index 00000000..9a3eae1f --- /dev/null +++ b/src/socket.cc @@ -0,0 +1,886 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#include "socket.h" +#include "context.h" +#include "observer.h" + +#include "incoming_msg.h" +#include "util/async_scope.h" +#include "util/uvloop.h" +#include "util/uvwork.h" + +#include +#include + +namespace zmq { +/* Ordinary static cast for all available numeric types. */ +template +T NumberCast(const Napi::Number& num) { + return static_cast(num); +} + +/* Specialization for uint64_t; check for out of bounds and warn on values + that cannot be represented accurately. TODO: Use native JS BigInt. */ +template <> +uint64_t NumberCast(const Napi::Number& num) { + auto value = num.DoubleValue(); + + if (std::nextafter(value, -0.0) < 0) return 0; + + if (value > static_cast((1ull << 53) - 1)) { + Warn(num.Env(), + "Value is larger than Number.MAX_SAFE_INTEGER and may have been rounded " + "inaccurately"); + } + + /* If the next representable value of the double is beyond the maximum + integer, then assume the maximum integer. */ + if (std::nextafter(value, std::numeric_limits::infinity()) + > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(value); +} + +struct AddressContext { + std::string address; + uint32_t error = 0; + + explicit AddressContext(std::string&& address) : address(std::move(address)) {} +}; + +Napi::FunctionReference Socket::Constructor; + +std::unordered_set Socket::ActivePtrs; + +Socket::Socket(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info), async_context(Env(), "Socket"), poller(*this) { + auto args = { + Argument{"Socket type must be a number", &Napi::Value::IsNumber}, + Argument{"Options must be an object", &Napi::Value::IsObject, + &Napi::Value::IsUndefined}, + }; + + if (!ValidateArguments(info, args)) return; + + type = info[0].As().Uint32Value(); + + if (info[1].IsObject()) { + auto options = info[1].As(); + if (options.Has("context")) { + context_ref.Reset(options.Get("context").As(), 1); + options.Delete("context"); + } else { + context_ref.Reset(GlobalContext.Value(), 1); + } + } else { + context_ref.Reset(GlobalContext.Value(), 1); + } + + auto context = Context::Unwrap(context_ref.Value()); + if (Env().IsExceptionPending()) return; + + socket = zmq_socket(context->context, type); + if (socket == nullptr) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + + uv_os_sock_t fd; + std::function finalize = nullptr; + +#ifdef ZMQ_THREAD_SAFE + { + int value = 0; + size_t length = sizeof(value); + if (zmq_getsockopt(socket, ZMQ_THREAD_SAFE, &value, &length) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + goto error; + } + + thread_safe = value; + } +#endif + + /* Currently only some DRAFT sockets are threadsafe. */ + if (thread_safe) { +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE + /* Threadsafe sockets do not expose an FD we can integrate into the + event loop, so we have to construct one by creating a zmq_poller. */ + auto poll = zmq_poller_new(); + if (poll == nullptr) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + goto error; + } + + /* Callback to free the underlying poller. Move the poller to transfer + ownership after the constructor has completed. */ + finalize = [=]() mutable { + auto err = zmq_poller_destroy(&poll); + assert(err == 0); + }; + + if (zmq_poller_add(poll, socket, nullptr, ZMQ_POLLIN | ZMQ_POLLOUT) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + finalize(); + goto error; + } + + if (zmq_poller_fd(poll, &fd) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + finalize(); + goto error; + } +#else + /* A thread safe socket was requested, but there is no support for + retrieving a poller FD, so we cannot construct them. */ + ErrnoException(Env(), EINVAL).ThrowAsJavaScriptException(); + goto error; +#endif + } else { + size_t length = sizeof(fd); + if (zmq_getsockopt(socket, ZMQ_FD, &fd, &length) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + goto error; + } + } + + if (poller.Initialize(Env(), fd, finalize) < 0) { + ErrnoException(Env(), errno).ThrowAsJavaScriptException(); + goto error; + } + + /* Initialization was successful, store the socket pointer in a list for + cleanup at process exit. */ + ActivePtrs.insert(socket); + + /* Sealing causes setting/getting invalid options to throw an error. + Otherwise they would fail silently, which is very confusing. */ + Seal(info.This().As()); + + /* Set any options after the socket has been successfully created. */ + if (info[1].IsObject()) { + Assign(info.This().As(), info[1].As()); + } + + return; + +error: + auto err = zmq_close(socket); + assert(err == 0); + + socket = nullptr; + return; +} + +Socket::~Socket() { + Close(); +} + +/* Define all socket options that should not trigger a warning when set on + a socket that is already bound/connected. */ +void Socket::WarnUnlessImmediateOption(int32_t option) const { + static const std::unordered_set immediate = { + ZMQ_SUBSCRIBE, + ZMQ_UNSUBSCRIBE, + ZMQ_LINGER, + ZMQ_ROUTER_MANDATORY, + ZMQ_PROBE_ROUTER, + ZMQ_XPUB_VERBOSE, + ZMQ_REQ_CORRELATE, + ZMQ_REQ_RELAXED, + +#ifdef ZMQ_ROUTER_HANDOVER + ZMQ_ROUTER_HANDOVER, +#endif + +#ifdef ZMQ_XPUB_VERBOSER + ZMQ_XPUB_VERBOSER, +#endif + +#if ZMQ_VERSION >= ZMQ_MAKE_VERSION(4, 2, 0) + /* As of 4.2.0 these options can take effect after bind/connect. */ + ZMQ_SNDHWM, + ZMQ_RCVHWM, +#endif + + /* These take effect immediately due to our Node.js implementation. */ + ZMQ_SNDTIMEO, + ZMQ_RCVTIMEO, + }; + + if (immediate.count(option) != 0) return; + if (endpoints == 0 && state == State::Open) return; + Warn(Env(), "Socket option will not take effect until next connect/bind"); +} + +bool Socket::ValidateOpen() const { + if (state == State::Blocked) { + ErrnoException(Env(), EBUSY).ThrowAsJavaScriptException(); + return false; + } + + if (state == State::Closed) { + ErrnoException(Env(), EBADF).ThrowAsJavaScriptException(); + return false; + } + + return true; +} + +bool Socket::HasEvents(int32_t requested) const { + int32_t events; + size_t events_size = sizeof(events); + + while (zmq_getsockopt(socket, ZMQ_EVENTS, &events, &events_size) < 0) { + /* Ignore errors. */ + if (zmq_errno() != EINTR) return 0; + } + + return events & requested; +} + +void Socket::Close() { + if (socket != nullptr) { + Napi::HandleScope scope(Env()); + + /* Unreference this socket if necessary. */ + if (endpoints > 0) { + poller.Unref(); + endpoints = 0; + } + + /* Stop all polling and release event handlers. */ + poller.Close(); + + /* Close succeeds unless socket is invalid. */ + ActivePtrs.erase(socket); + auto err = zmq_close(socket); + assert(err == 0); + + /* Release reference to context and observer. */ + observer_ref.Reset(); + context_ref.Reset(); + + state = State::Closed; + socket = nullptr; + } +} + +void Socket::Send(const Napi::Promise::Deferred& res, OutgoingMsg::Parts& parts) { + auto iter = parts.begin(); + auto end = parts.end(); + + while (iter != end) { + auto& part = *iter; + iter++; + + uint32_t flags = iter == end ? ZMQ_DONTWAIT : ZMQ_DONTWAIT | ZMQ_SNDMORE; + while (zmq_msg_send(part, socket, flags) < 0) { + if (zmq_errno() != EINTR) { + res.Reject(ErrnoException(Env(), zmq_errno()).Value()); + return; + } + } + } + + res.Resolve(Env().Undefined()); +} + +void Socket::Receive(const Napi::Promise::Deferred& res) { + /* Return an array of message parts, or an array with a single message + followed by a metadata object. */ + auto list = Napi::Array::New(Env(), 1); + + uint32_t i = 0; + while (true) { + IncomingMsg part; + while (zmq_msg_recv(part, socket, ZMQ_DONTWAIT) < 0) { + if (zmq_errno() != EINTR) { + res.Reject(ErrnoException(Env(), zmq_errno()).Value()); + return; + } + } + + list[i++] = part.IntoBuffer(Env()); + +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE + switch (type) { + case ZMQ_SERVER: { + auto meta = Napi::Object::New(Env()); + meta.Set("routingId", zmq_msg_routing_id(part)); + list[i++] = meta; + break; + } + + case ZMQ_DISH: { + auto meta = Napi::Object::New(Env()); + auto data = zmq_msg_group(part); + auto length = strnlen(data, ZMQ_GROUP_MAX_LENGTH); + meta.Set("group", Napi::Buffer::Copy(Env(), data, length)); + list[i++] = meta; + break; + } + } +#endif + + if (!zmq_msg_more(part)) break; + } + + res.Resolve(list); +} + +Napi::Value Socket::Bind(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Address must be a string", &Napi::Value::IsString}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + if (!ValidateOpen()) return Env().Undefined(); + + state = Socket::State::Blocked; + auto res = Napi::Promise::Deferred::New(Env()); + auto run_ctx = + std::make_shared(info[0].As().Utf8Value()); + + auto status = UvQueue(Env(), + [=]() { + /* Don't access V8 internals here! Executed in worker thread. */ + while (zmq_bind(socket, run_ctx->address.c_str()) < 0) { + if (zmq_errno() != EINTR) { + run_ctx->error = zmq_errno(); + return; + } + } + }, + [=]() { + AsyncScope scope(Env(), async_context); + state = Socket::State::Open; + + if (request_close) { + Close(); + } + + if (run_ctx->error != 0) { + res.Reject( + ErrnoException(Env(), run_ctx->error, run_ctx->address).Value()); + return; + } + + if (endpoints++ == 0) { + poller.Ref(); + } + + res.Resolve(Env().Undefined()); + }); + + if (status < 0) { + ErrnoException(Env(), EBADF).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return res.Promise(); +} + +Napi::Value Socket::Unbind(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Address must be a string", &Napi::Value::IsString}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + if (!ValidateOpen()) return Env().Undefined(); + + state = Socket::State::Blocked; + auto res = Napi::Promise::Deferred::New(Env()); + auto run_ctx = + std::make_shared(info[0].As().Utf8Value()); + + auto status = UvQueue(Env(), + [=]() { + /* Don't access V8 internals here! Executed in worker thread. */ + while (zmq_unbind(socket, run_ctx->address.c_str()) < 0) { + if (zmq_errno() != EINTR) { + run_ctx->error = zmq_errno(); + return; + } + } + }, + [=]() { + AsyncScope scope(Env(), async_context); + state = Socket::State::Open; + + if (request_close) { + Close(); + } + + if (run_ctx->error != 0) { + res.Reject( + ErrnoException(Env(), run_ctx->error, run_ctx->address).Value()); + return; + } + + if (--endpoints == 0) { + poller.Unref(); + } + + res.Resolve(Env().Undefined()); + }); + + if (status < 0) { + ErrnoException(Env(), EBADF).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return res.Promise(); +} + +void Socket::Connect(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Address must be a string", &Napi::Value::IsString}, + }; + + if (!ValidateArguments(info, args)) return; + if (!ValidateOpen()) return; + + std::string address = info[0].As(); + if (zmq_connect(socket, address.c_str()) < 0) { + ErrnoException(Env(), zmq_errno(), address).ThrowAsJavaScriptException(); + return; + } + + if (endpoints++ == 0) { + poller.Ref(); + } +} + +void Socket::Disconnect(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Address must be a string", &Napi::Value::IsString}, + }; + + if (!ValidateArguments(info, args)) return; + if (!ValidateOpen()) return; + + std::string address = info[0].As(); + if (zmq_disconnect(socket, address.c_str()) < 0) { + ErrnoException(Env(), zmq_errno(), address).ThrowAsJavaScriptException(); + return; + } + + if (--endpoints == 0) { + poller.Unref(); + } +} + +void Socket::Close(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return; + + if (state == State::Blocked) { + request_close = true; + } else { + request_close = false; + Close(); + } +} + +inline bool IsNotUndefined(const Napi::Value& value) { + return !value.IsUndefined(); +} + +Napi::Value Socket::Send(const Napi::CallbackInfo& info) { + switch (type) { +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE + case ZMQ_SERVER: + case ZMQ_RADIO: { + auto args = { + Argument{"Message must be present", &IsNotUndefined}, + Argument{"Options must be an object", &Napi::Value::IsObject}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + break; + } + +#endif + default: { + auto args = { + Argument{"Message must be present", &IsNotUndefined}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + } + } + + if (!ValidateOpen()) return Env().Undefined(); + + OutgoingMsg::Parts parts(info[0]); + +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE + switch (type) { + case ZMQ_SERVER: { + if (!parts.SetRoutingId(info[1].As().Get("routingId"))) { + return Env().Undefined(); + } + break; + } + + case ZMQ_RADIO: { + if (!parts.SetGroup(info[1].As().Get("group"))) { + return Env().Undefined(); + } + break; + } + } +#endif + + if (send_timeout == 0 || HasEvents(ZMQ_POLLOUT)) { + /* We can send on the socket immediately. This is a separate code + path so we can avoid creating a lambda. */ + auto res = Napi::Promise::Deferred::New(Env()); + Send(res, parts); + + /* This operation may have caused a state change, so we must update + the poller state manually! */ + poller.Trigger(); + + return res.Promise(); + } else { + /* Check if we are already polling for writes. If so that means + two async read operations are started; which we do not allow. + This is not laziness; we should not introduce additional queueing + because it would break ZMQ semantics. */ + if (poller.PollingWritable()) { + ErrnoException(Env(), EAGAIN).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return poller.WritePromise(send_timeout, std::move(parts)); + } +} + +Napi::Value Socket::Receive(const Napi::CallbackInfo& info) { + if (!ValidateArguments(info, {})) return Env().Undefined(); + if (!ValidateOpen()) return Env().Undefined(); + + if (receive_timeout == 0 || HasEvents(ZMQ_POLLIN)) { + /* We can read from the socket immediately. This is a separate code + path so we can avoid creating a lambda. */ + auto res = Napi::Promise::Deferred::New(Env()); + Receive(res); + + /* This operation may have caused a state change, so we must update + the poller state manually! */ + poller.Trigger(); + + return res.Promise(); + } else { + /* Check if we are already polling for reads. Only one promise may + receive the next message, so we must ensure that receive + operations are in sequence. */ + if (poller.PollingReadable()) { + ErrnoException(Env(), EAGAIN).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return poller.ReadPromise(receive_timeout); + } +} + +void Socket::Join(const Napi::CallbackInfo& info) { +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE + auto args = { + Argument{"Group must be a string or buffer", &Napi::Value::IsString, + &Napi::Value::IsBuffer}, + }; + + if (!ValidateArguments(info, args)) return; + if (!ValidateOpen()) return; + + auto str = [&]() { + if (info[0].IsString()) { + return std::string(info[0].As()); + } else { + Napi::Object buf = info[0].As(); + auto length = buf.As>().Length(); + auto value = buf.As>().Data(); + return std::string(value, length); + } + }(); + + if (zmq_join(socket, str.c_str()) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } +#endif +} + +void Socket::Leave(const Napi::CallbackInfo& info) { +#ifdef ZMQ_HAS_POLLABLE_THREAD_SAFE + auto args = { + Argument{"Group must be a string or buffer", &Napi::Value::IsString, + &Napi::Value::IsBuffer}, + }; + + if (!ValidateArguments(info, args)) return; + if (!ValidateOpen()) return; + + auto str = [&]() { + if (info[0].IsString()) { + return std::string(info[0].As()); + } else { + Napi::Object buf = info[0].As(); + auto length = buf.As>().Length(); + auto value = buf.As>().Data(); + return std::string(value, length); + } + }(); + + if (zmq_leave(socket, str.c_str()) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } +#endif +} + +template <> +Napi::Value Socket::GetSockOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + + uint32_t option = info[0].As(); + + int32_t value = 0; + size_t length = sizeof(value); + if (zmq_getsockopt(socket, option, &value, &length) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return Napi::Boolean::New(Env(), value); +} + +template <> +void Socket::SetSockOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + Argument{"Option value must be a boolean", &Napi::Value::IsBoolean}, + }; + + if (!ValidateArguments(info, args)) return; + + int32_t option = info[0].As(); + WarnUnlessImmediateOption(option); + + int32_t value = info[1].As(); + if (zmq_setsockopt(socket, option, &value, sizeof(value)) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } +} + +template <> +Napi::Value Socket::GetSockOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + + uint32_t option = info[0].As(); + + char value[1024]; + size_t length = sizeof(value) - 1; + if (zmq_getsockopt(socket, option, value, &length) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + if (length == 0 || (length == 1 && value[0] == 0)) { + return Env().Null(); + } else { + value[length] = '\0'; + return Napi::String::New(Env(), value); + } +} + +template <> +void Socket::SetSockOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + Argument{"Option value must be a string or buffer", &Napi::Value::IsString, + &Napi::Value::IsBuffer, &Napi::Value::IsNull}, + }; + + if (!ValidateArguments(info, args)) return; + + int32_t option = info[0].As(); + WarnUnlessImmediateOption(option); + + int32_t err; + if (info[1].IsBuffer()) { + Napi::Object buf = info[1].As(); + auto length = buf.As>().Length(); + auto value = buf.As>().Data(); + err = zmq_setsockopt(socket, option, value, length); + } else if (info[1].IsString()) { + std::string str = info[1].As(); + auto length = str.length(); + auto value = str.data(); + err = zmq_setsockopt(socket, option, value, length); + } else { + auto length = 0u; + auto value = nullptr; + err = zmq_setsockopt(socket, option, value, length); + } + + if (err < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } +} + +template +Napi::Value Socket::GetSockOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + }; + + if (!ValidateArguments(info, args)) return Env().Undefined(); + + uint32_t option = info[0].As(); + + T value = 0; + size_t length = sizeof(value); + if (zmq_getsockopt(socket, option, &value, &length) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return Env().Undefined(); + } + + return Napi::Number::New(Env(), static_cast(value)); +} + +template +void Socket::SetSockOpt(const Napi::CallbackInfo& info) { + auto args = { + Argument{"Identifier must be a number", &Napi::Value::IsNumber}, + Argument{"Option value must be a number", &Napi::Value::IsNumber}, + }; + + if (!ValidateArguments(info, args)) return; + + int32_t option = info[0].As(); + WarnUnlessImmediateOption(option); + + T value = NumberCast(info[1].As()); + if (zmq_setsockopt(socket, option, &value, sizeof(value)) < 0) { + ErrnoException(Env(), zmq_errno()).ThrowAsJavaScriptException(); + return; + } + + /* Mirror a few options that are used internally. */ + switch (option) { + case ZMQ_SNDTIMEO: + send_timeout = value; + break; + case ZMQ_RCVTIMEO: + receive_timeout = value; + break; + } +} + +Napi::Value Socket::GetEvents(const Napi::CallbackInfo& info) { + /* Reuse the same observer object every time it is accessed. */ + if (observer_ref.IsEmpty()) { + observer_ref.Reset(Observer::Constructor.New({Value()}), 1); + } + + return observer_ref.Value(); +} + +Napi::Value Socket::GetContext(const Napi::CallbackInfo& info) { + return context_ref.Value(); +} + +Napi::Value Socket::GetClosed(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(Env(), state == State::Closed); +} + +Napi::Value Socket::GetReadable(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(Env(), HasEvents(ZMQ_POLLIN)); +} + +Napi::Value Socket::GetWritable(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(Env(), HasEvents(ZMQ_POLLOUT)); +} + +void Socket::Initialize(Napi::Env& env, Napi::Object& exports) { + auto proto = { + InstanceMethod("bind", &Socket::Bind), + InstanceMethod("unbind", &Socket::Unbind), + InstanceMethod("connect", &Socket::Connect), + InstanceMethod("disconnect", &Socket::Disconnect), + InstanceMethod("close", &Socket::Close), + + /* Marked 'configurable' so they can be removed from the base Socket + prototype and re-assigned to the sockets to which they apply. */ + InstanceMethod("send", &Socket::Send, napi_configurable), + InstanceMethod("receive", &Socket::Receive, napi_configurable), + InstanceMethod("join", &Socket::Join, napi_configurable), + InstanceMethod("leave", &Socket::Leave, napi_configurable), + + InstanceMethod("getBoolOption", &Socket::GetSockOpt), + InstanceMethod("setBoolOption", &Socket::SetSockOpt), + InstanceMethod("getInt32Option", &Socket::GetSockOpt), + InstanceMethod("setInt32Option", &Socket::SetSockOpt), + InstanceMethod("getUint32Option", &Socket::GetSockOpt), + InstanceMethod("setUint32Option", &Socket::SetSockOpt), + InstanceMethod("getInt64Option", &Socket::GetSockOpt), + InstanceMethod("setInt64Option", &Socket::SetSockOpt), + InstanceMethod("getUint64Option", &Socket::GetSockOpt), + InstanceMethod("setUint64Option", &Socket::SetSockOpt), + InstanceMethod("getStringOption", &Socket::GetSockOpt), + InstanceMethod("setStringOption", &Socket::SetSockOpt), + + InstanceAccessor("events", &Socket::GetEvents, nullptr), + InstanceAccessor("context", &Socket::GetContext, nullptr), + + InstanceAccessor("closed", &Socket::GetClosed, nullptr), + InstanceAccessor("readable", &Socket::GetReadable, nullptr), + InstanceAccessor("writable", &Socket::GetWritable, nullptr), + }; + + auto constructor = DefineClass(env, "Socket", proto); + + Constructor = Napi::Persistent(constructor); + Constructor.SuppressDestruct(); + + exports.Set("Socket", constructor); +} + +void Socket::Poller::ReadableCallback() { + AsyncScope scope(read_deferred.Env(), socket.async_context); + socket.Receive(read_deferred); +} + +void Socket::Poller::WritableCallback() { + AsyncScope scope(write_deferred.Env(), socket.async_context); + socket.Send(write_deferred, write_value); + write_value.Clear(); +} + +Napi::Value Socket::Poller::ReadPromise(int64_t timeout) { + read_deferred = Napi::Promise::Deferred(read_deferred.Env()); + zmq::Poller::PollReadable(timeout); + return read_deferred.Promise(); +} + +Napi::Value Socket::Poller::WritePromise(int64_t timeout, OutgoingMsg::Parts&& value) { + write_deferred = Napi::Promise::Deferred(read_deferred.Env()); + write_value = std::move(value); + zmq::Poller::PollWritable(timeout); + return write_deferred.Promise(); +} +} diff --git a/src/socket.h b/src/socket.h new file mode 100644 index 00000000..f6a6eb3a --- /dev/null +++ b/src/socket.h @@ -0,0 +1,115 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "binding.h" +#include "outgoing_msg.h" +#include "poller.h" + +#include + +namespace zmq { +class Socket : public Napi::ObjectWrap { +public: + static Napi::FunctionReference Constructor; + static std::unordered_set ActivePtrs; + static void Initialize(Napi::Env& env, Napi::Object& exports); + + explicit Socket(const Napi::CallbackInfo& info); + ~Socket(); + +protected: + enum class State : uint8_t { + Open, /* Socket is open. */ + Closed, /* Socket is closed. */ + Blocked, /* Async operation in progress that disallows socket access. */ + }; + + inline void Close(const Napi::CallbackInfo& info); + + inline Napi::Value Bind(const Napi::CallbackInfo& info); + inline Napi::Value Unbind(const Napi::CallbackInfo& info); + + inline void Connect(const Napi::CallbackInfo& info); + inline void Disconnect(const Napi::CallbackInfo& info); + + inline Napi::Value Send(const Napi::CallbackInfo& info); + inline Napi::Value Receive(const Napi::CallbackInfo& info); + + inline void Join(const Napi::CallbackInfo& info); + inline void Leave(const Napi::CallbackInfo& info); + + template + inline Napi::Value GetSockOpt(const Napi::CallbackInfo& info); + + template + inline void SetSockOpt(const Napi::CallbackInfo& info); + + inline Napi::Value GetEvents(const Napi::CallbackInfo& info); + inline Napi::Value GetContext(const Napi::CallbackInfo& info); + + inline Napi::Value GetClosed(const Napi::CallbackInfo& info); + inline Napi::Value GetReadable(const Napi::CallbackInfo& info); + inline Napi::Value GetWritable(const Napi::CallbackInfo& info); + +private: + inline void WarnUnlessImmediateOption(int32_t option) const; + inline bool ValidateOpen() const; + bool HasEvents(int32_t events) const; + + inline void Ref(); + inline void Unref(); + void Close(); + + /* Send/receive are usually in a hot path and will benefit slightly + from being inlined. They are used in more than one location and are + not necessarily automatically inlined by all compilers. */ + force_inline void Send(const Napi::Promise::Deferred& res, OutgoingMsg::Parts& parts); + force_inline void Receive(const Napi::Promise::Deferred& res); + + class Poller : public zmq::Poller { + Socket& socket; + Napi::Promise::Deferred read_deferred; + Napi::Promise::Deferred write_deferred; + OutgoingMsg::Parts write_value; + + public: + explicit Poller(Socket& socket) + : socket(socket), read_deferred(socket.Env()), write_deferred(socket.Env()) {} + + Napi::Value ReadPromise(int64_t timeout); + Napi::Value WritePromise(int64_t timeout, OutgoingMsg::Parts&& parts); + + inline bool ValidateReadable() const { + return socket.HasEvents(ZMQ_POLLIN); + } + + inline bool ValidateWritable() const { + return socket.HasEvents(ZMQ_POLLOUT); + } + + void ReadableCallback(); + void WritableCallback(); + }; + + Napi::AsyncContext async_context; + Napi::ObjectReference context_ref; + Napi::ObjectReference observer_ref; + Socket::Poller poller; + void* socket = nullptr; + + int64_t send_timeout = -1; + int64_t receive_timeout = -1; + uint32_t endpoints = 0; + + State state = State::Open; + bool request_close = false; + bool thread_safe = false; + uint8_t type = 0; + + friend class Observer; + friend class Proxy; +}; +} + +static_assert(!std::is_copy_constructible::value, "not copyable"); +static_assert(!std::is_move_constructible::value, "not movable"); diff --git a/src/util/arguments.h b/src/util/arguments.h new file mode 100644 index 00000000..aa7952bd --- /dev/null +++ b/src/util/arguments.h @@ -0,0 +1,59 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "to_string.h" + +namespace zmq { +class Argument { + typedef bool (Napi::Value::*ArgValCb)() const; + + std::function fn = nullptr; + const std::string msg; + +public: + inline Argument(const std::string& msg, std::function fn) + : fn(fn), msg(msg) {} + + inline Argument(const std::string& msg, ArgValCb f1) + : fn([=](const Napi::Value& value) { return (value.*f1)(); }), msg(msg) {} + + inline Argument(const std::string& msg, ArgValCb f1, ArgValCb f2) + : fn([=](const Napi::Value& value) { return (value.*f1)() || (value.*f2)(); }), + msg(msg) {} + + inline Argument(const std::string& msg, ArgValCb f1, ArgValCb f2, ArgValCb f3) + : fn([=](const Napi::Value& value) { + return (value.*f1)() || (value.*f2)() || (value.*f3)(); + }), + msg(msg) {} + + inline const std::string& Message() const { + return msg; + } + + inline bool Valid(const Napi::Value& arg) const { + return fn(arg); + } +}; + +inline bool ValidateArguments( + const Napi::CallbackInfo& info, const std::initializer_list& args) { + for (const auto& arg : args) { + auto i = &arg - args.begin(); + + if (!arg.Valid(info[i])) { + Napi::TypeError::New(info.Env(), arg.Message()).ThrowAsJavaScriptException(); + return false; + } + } + + if (info.Length() > args.size()) { + auto msg = "Expected " + to_string(args.size()) + " argument" + + (args.size() != 1 ? "s" : ""); + Napi::TypeError::New(info.Env(), msg).ThrowAsJavaScriptException(); + return false; + } + + return true; +} +} diff --git a/src/util/async_scope.h b/src/util/async_scope.h new file mode 100644 index 00000000..8b1479c7 --- /dev/null +++ b/src/util/async_scope.h @@ -0,0 +1,13 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +namespace zmq { +class AsyncScope { + Napi::HandleScope handle_scope; + Napi::CallbackScope callback_scope; + +public: + inline explicit AsyncScope(Napi::Env env, const Napi::AsyncContext& context) + : handle_scope(env), callback_scope(env, context) {} +}; +} diff --git a/src/util/error.h b/src/util/error.h new file mode 100644 index 00000000..80c33a65 --- /dev/null +++ b/src/util/error.h @@ -0,0 +1,406 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include + +namespace zmq { +static inline const char* ErrnoMessage(int32_t errorno); +static inline const char* ErrnoCode(int32_t errorno); + +/* Generates a process warning message. */ +static inline void Warn(const Napi::Env& env, const std::string& msg) { + auto global = env.Global(); + auto fn = + global.Get("process").As().Get("emitWarning").As(); + fn.Call({Napi::String::New(env, msg)}); +} + +/* This mostly duplicates node::ErrnoException, but it is not public. */ +static inline Napi::Error ErrnoException(const Napi::Env& env, int32_t error) { + Napi::HandleScope scope(env); + auto exception = Napi::Error::New(env, ErrnoMessage(error)); + exception.Set("errno", Napi::Number::New(env, error)); + exception.Set("code", Napi::String::New(env, ErrnoCode(error))); + return exception; +} + +static inline Napi::Error ErrnoException( + const Napi::Env& env, int32_t error, const std::string& address) { + auto exception = ErrnoException(env, error); + exception.Set("address", Napi::String::New(env, address)); + return exception; +} + +/* Convert errno to human readable error message. */ +static inline const char* ErrnoMessage(int32_t errorno) { + /* Clarify a few confusing default messages; otherwise rely on zmq. */ + switch (errorno) { + case EFAULT: + return "Context is closed"; + case EAGAIN: + return "Socket temporarily unavailable"; + case EMFILE: + return "Too many open file descriptors"; + case ENOENT: + return "No such endpoint"; + case EBUSY: + return "Socket is blocked by async operation (e.g. bind/unbind)"; + case EBADF: + return "Socket is closed"; + case EADDRINUSE: + /* Make sure this description is the same on all platforms. */ + return "Address already in use"; + default: + return zmq_strerror(errorno); + } +} + +/* This is copied from Node.js; the mapping is not in a public API. */ +/* Copyright Node.js contributors. All rights reserved. */ +static inline const char* ErrnoCode(int32_t errorno) { +#define ERRNO_CASE(e) \ + case e: \ + return #e; + + switch (errorno) { +/* ZMQ specific codes. */ +#ifdef EFSM + ERRNO_CASE(EFSM); +#endif + +#ifdef ENOCOMPATPROTO + ERRNO_CASE(ENOCOMPATPROTO); +#endif + +#ifdef ETERM + ERRNO_CASE(ETERM); +#endif + +#ifdef EMTHREAD + ERRNO_CASE(EMTHREAD); +#endif + +/* Generic codes. */ +#ifdef EACCES + ERRNO_CASE(EACCES); +#endif + +#ifdef EADDRINUSE + ERRNO_CASE(EADDRINUSE); +#endif + +#ifdef EADDRNOTAVAIL + ERRNO_CASE(EADDRNOTAVAIL); +#endif + +#ifdef EAFNOSUPPORT + ERRNO_CASE(EAFNOSUPPORT); +#endif + +#ifdef EAGAIN + ERRNO_CASE(EAGAIN); +#endif + +#ifdef EWOULDBLOCK +#if EAGAIN != EWOULDBLOCK + ERRNO_CASE(EWOULDBLOCK); +#endif +#endif + +#ifdef EALREADY + ERRNO_CASE(EALREADY); +#endif + +#ifdef EBADF + ERRNO_CASE(EBADF); +#endif + +#ifdef EBADMSG + ERRNO_CASE(EBADMSG); +#endif + +#ifdef EBUSY + ERRNO_CASE(EBUSY); +#endif + +#ifdef ECANCELED + ERRNO_CASE(ECANCELED); +#endif + +#ifdef ECHILD + ERRNO_CASE(ECHILD); +#endif + +#ifdef ECONNABORTED + ERRNO_CASE(ECONNABORTED); +#endif + +#ifdef ECONNREFUSED + ERRNO_CASE(ECONNREFUSED); +#endif + +#ifdef ECONNRESET + ERRNO_CASE(ECONNRESET); +#endif + +#ifdef EDEADLK + ERRNO_CASE(EDEADLK); +#endif + +#ifdef EDESTADDRREQ + ERRNO_CASE(EDESTADDRREQ); +#endif + +#ifdef EDOM + ERRNO_CASE(EDOM); +#endif + +#ifdef EDQUOT + ERRNO_CASE(EDQUOT); +#endif + +#ifdef EEXIST + ERRNO_CASE(EEXIST); +#endif + +#ifdef EFAULT + ERRNO_CASE(EFAULT); +#endif + +#ifdef EFBIG + ERRNO_CASE(EFBIG); +#endif + +#ifdef EHOSTUNREACH + ERRNO_CASE(EHOSTUNREACH); +#endif + +#ifdef EIDRM + ERRNO_CASE(EIDRM); +#endif + +#ifdef EILSEQ + ERRNO_CASE(EILSEQ); +#endif + +#ifdef EINPROGRESS + ERRNO_CASE(EINPROGRESS); +#endif + +#ifdef EINTR + ERRNO_CASE(EINTR); +#endif + +#ifdef EINVAL + ERRNO_CASE(EINVAL); +#endif + +#ifdef EIO + ERRNO_CASE(EIO); +#endif + +#ifdef EISCONN + ERRNO_CASE(EISCONN); +#endif + +#ifdef EISDIR + ERRNO_CASE(EISDIR); +#endif + +#ifdef ELOOP + ERRNO_CASE(ELOOP); +#endif + +#ifdef EMFILE + ERRNO_CASE(EMFILE); +#endif + +#ifdef EMLINK + ERRNO_CASE(EMLINK); +#endif + +#ifdef EMSGSIZE + ERRNO_CASE(EMSGSIZE); +#endif + +#ifdef EMULTIHOP + ERRNO_CASE(EMULTIHOP); +#endif + +#ifdef ENAMETOOLONG + ERRNO_CASE(ENAMETOOLONG); +#endif + +#ifdef ENETDOWN + ERRNO_CASE(ENETDOWN); +#endif + +#ifdef ENETRESET + ERRNO_CASE(ENETRESET); +#endif + +#ifdef ENETUNREACH + ERRNO_CASE(ENETUNREACH); +#endif + +#ifdef ENFILE + ERRNO_CASE(ENFILE); +#endif + +#ifdef ENOBUFS + ERRNO_CASE(ENOBUFS); +#endif + +#ifdef ENODATA + ERRNO_CASE(ENODATA); +#endif + +#ifdef ENODEV + ERRNO_CASE(ENODEV); +#endif + +#ifdef ENOENT + ERRNO_CASE(ENOENT); +#endif + +#ifdef ENOEXEC + ERRNO_CASE(ENOEXEC); +#endif + +#ifdef ENOLINK + ERRNO_CASE(ENOLINK); +#endif + +#ifdef ENOLCK +#if ENOLINK != ENOLCK + ERRNO_CASE(ENOLCK); +#endif +#endif + +#ifdef ENOMEM + ERRNO_CASE(ENOMEM); +#endif + +#ifdef ENOMSG + ERRNO_CASE(ENOMSG); +#endif + +#ifdef ENOPROTOOPT + ERRNO_CASE(ENOPROTOOPT); +#endif + +#ifdef ENOSPC + ERRNO_CASE(ENOSPC); +#endif + +#ifdef ENOSR + ERRNO_CASE(ENOSR); +#endif + +#ifdef ENOSTR + ERRNO_CASE(ENOSTR); +#endif + +#ifdef ENOSYS + ERRNO_CASE(ENOSYS); +#endif + +#ifdef ENOTCONN + ERRNO_CASE(ENOTCONN); +#endif + +#ifdef ENOTDIR + ERRNO_CASE(ENOTDIR); +#endif + +#ifdef ENOTEMPTY +#if ENOTEMPTY != EEXIST + ERRNO_CASE(ENOTEMPTY); +#endif +#endif + +#ifdef ENOTSOCK + ERRNO_CASE(ENOTSOCK); +#endif + +#ifdef ENOTSUP + ERRNO_CASE(ENOTSUP); +#else +#ifdef EOPNOTSUPP + ERRNO_CASE(EOPNOTSUPP); +#endif +#endif + +#ifdef ENOTTY + ERRNO_CASE(ENOTTY); +#endif + +#ifdef ENXIO + ERRNO_CASE(ENXIO); +#endif + +#ifdef EOVERFLOW + ERRNO_CASE(EOVERFLOW); +#endif + +#ifdef EPERM + ERRNO_CASE(EPERM); +#endif + +#ifdef EPIPE + ERRNO_CASE(EPIPE); +#endif + +#ifdef EPROTO + ERRNO_CASE(EPROTO); +#endif + +#ifdef EPROTONOSUPPORT + ERRNO_CASE(EPROTONOSUPPORT); +#endif + +#ifdef EPROTOTYPE + ERRNO_CASE(EPROTOTYPE); +#endif + +#ifdef ERANGE + ERRNO_CASE(ERANGE); +#endif + +#ifdef EROFS + ERRNO_CASE(EROFS); +#endif + +#ifdef ESPIPE + ERRNO_CASE(ESPIPE); +#endif + +#ifdef ESRCH + ERRNO_CASE(ESRCH); +#endif + +#ifdef ESTALE + ERRNO_CASE(ESTALE); +#endif + +#ifdef ETIME + ERRNO_CASE(ETIME); +#endif + +#ifdef ETIMEDOUT + ERRNO_CASE(ETIMEDOUT); +#endif + +#ifdef ETXTBSY + ERRNO_CASE(ETXTBSY); +#endif + +#ifdef EXDEV + ERRNO_CASE(EXDEV); +#endif + + default: + return ""; + } +} +} diff --git a/src/util/object.h b/src/util/object.h new file mode 100644 index 00000000..5b367389 --- /dev/null +++ b/src/util/object.h @@ -0,0 +1,19 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +namespace zmq { +/* Seals an object to prevent setting incorrect options. */ +static inline void Seal(Napi::Object object) { + auto global = object.Env().Global(); + auto seal = global.Get("Object").As().Get("seal").As(); + seal.Call({object}); +} + +/* Assign all properties in the given options object. */ +static inline void Assign(Napi::Object object, Napi::Object options) { + auto global = object.Env().Global(); + auto assign = + global.Get("Object").As().Get("assign").As(); + assign.Call({object, options}); +} +} diff --git a/src/util/to_string.h b/src/util/to_string.h new file mode 100644 index 00000000..9f3c6647 --- /dev/null +++ b/src/util/to_string.h @@ -0,0 +1,18 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +namespace zmq { +/* Provide an alternative, simplified std::to_string implementation for + integers to work around https://bugs.alpinelinux.org/issues/8626. */ +static inline std::string to_string(int64_t val) { + if (val == 0) return "0"; + std::string str; + + while (val > 0) { + str.insert(0, 1, val % 10 + 48); + val /= 10; + } + + return str; +} +} diff --git a/src/util/trash.h b/src/util/trash.h new file mode 100644 index 00000000..b1a3d722 --- /dev/null +++ b/src/util/trash.h @@ -0,0 +1,69 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "uvhandle.h" +#include "uvloop.h" + +#include +#include +#include + +namespace zmq { +/* Container for unused references to outgoing messages. Once an item is + added to the trash it will be cleared on the main thread once UV decides + to call the async callback. This is required because v8 objects cannot + be released on other threads. */ +template +class Trash { + std::deque> values; + std::mutex lock; + UvHandle async; + bool terminated; + +public: + /* Construct trash with an associated asynchronous callback. */ + inline void Initialize(Napi::Env env) { + auto loop = UvLoop(env); + + async->data = this; + auto err = uv_async_init(loop, async, + [](uv_async_t* async) { reinterpret_cast(async->data)->Clear(); }); + + assert(err == 0); + + /* Immediately unreference this handle in order to prevent the async + callback from preventing the Node.js process to exit. */ + uv_unref(async); + } + + /* Add given item to the trash, marking it for deletion next time the + async callback is called by UV. */ + inline void Add(T* item) { + { + std::lock_guard guard(lock); + if (terminated) return; + values.emplace_back(std::unique_ptr(item)); + } + + /* Call to uv_async_send() should never return nonzero. UV ensures + that calls are coalesced if they occur frequently. This is good + news for us, since that means frequent additions do not cause + unnecessary trash cycle operations. */ + auto err = uv_async_send(async); + assert(err == 0); + } + + /* Empty the trash. */ + inline void Clear() { + std::lock_guard guard(lock); + values.clear(); + } + + /* Stop the trash from touching any referenced values. This method is + called after event loop exit when V8 objects are no longer valid. */ + inline void Terminate() { + std::lock_guard guard(lock); + terminated = true; + } +}; +} diff --git a/src/util/uvhandle.h b/src/util/uvhandle.h new file mode 100644 index 00000000..c2ccd308 --- /dev/null +++ b/src/util/uvhandle.h @@ -0,0 +1,44 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include +#include + +namespace zmq { +template +struct UvDeleter { + inline void operator()(T* handle) { + /* If uninitialized, simply delete the memory. We + may not call uv_close() on uninitialized handles. */ + if (handle->type == 0) { + delete reinterpret_cast(handle); + return; + } + + /* Otherwise close the UV handle and delete in the callback. */ + uv_close(reinterpret_cast(handle), + [](uv_handle_t* handle) { delete reinterpret_cast(handle); }); + } +}; + +template +using handle_ptr = std::unique_ptr>; + +/* Smart UV handle that closes and releases itself on destruction. */ +template +struct UvHandle : public handle_ptr { + inline UvHandle() : handle_ptr{new T{}, UvDeleter()} {}; + + inline operator bool() { + return handle_ptr::operator bool() && handle_ptr::get()->type != 0; + } + + inline operator T*() { + return handle_ptr::get(); + } + + inline operator uv_handle_t*() { + return reinterpret_cast(handle_ptr::get()); + } +}; +} diff --git a/src/util/uvloop.h b/src/util/uvloop.h new file mode 100644 index 00000000..76134f46 --- /dev/null +++ b/src/util/uvloop.h @@ -0,0 +1,10 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +namespace zmq { +inline uv_loop_t* UvLoop(Napi::Env env) { + uv_loop_t* loop = nullptr; + napi_get_uv_event_loop(env, &loop); + return loop; +} +} diff --git a/src/util/uvwork.h b/src/util/uvwork.h new file mode 100644 index 00000000..ef5e9de8 --- /dev/null +++ b/src/util/uvwork.h @@ -0,0 +1,47 @@ +/* Copyright (c) 2017-2019 Rolf Timmermans */ +#pragma once + +#include "uvloop.h" + +namespace zmq { +/* Starts a UV worker. */ +template +class UvWork { + /* Simple unique pointer suffices, since uv_work_t does not require + calling uv_close() on completion. */ + std::unique_ptr work{new uv_work_t}; + + E execute_callback; + C complete_callback; + +public: + inline UvWork(E execute, C complete) + : execute_callback(std::move(execute)), complete_callback(std::move(complete)) { + work->data = this; + } + + inline int32_t Exec(uv_loop_t* loop) { + auto err = uv_queue_work(loop, work.get(), + [](uv_work_t* req) { + auto& work = *reinterpret_cast(req->data); + work.execute_callback(); + }, + [](uv_work_t* req, int status) { + auto& work = *reinterpret_cast(req->data); + work.complete_callback(); + delete &work; + }); + + if (err != 0) delete this; + + return err; + } +}; + +template +static inline int32_t UvQueue(Napi::Env env, E execute, C complete) { + auto loop = UvLoop(env); + auto work = new UvWork(std::move(execute), std::move(complete)); + return work->Exec(loop); +} +} diff --git a/test/bench/create-socket.js b/test/bench/create-socket.js new file mode 100644 index 00000000..cb51d146 --- /dev/null +++ b/test/bench/create-socket.js @@ -0,0 +1,27 @@ +if (zmq.cur) { + zmq.cur.Context.setMaxSockets(n) + suite.add(`create socket n=${n} zmq=cur`, Object.assign({ + fn: deferred => { + const sockets = [] + for (let i = 0; i < n; i++) { + sockets.push(zmq.cur.socket("dealer")) + } + deferred.resolve() + for (const socket of sockets) socket.close() + } + }, benchOptions)) +} + +if (zmq.ng) { + zmq.ng.global.maxSockets = n + suite.add(`create socket n=${n} zmq=ng`, Object.assign({ + fn: deferred => { + const sockets = [] + for (let i = 0; i < n; i++) { + sockets.push(new zmq.ng.Dealer) + } + deferred.resolve() + for (const socket of sockets) socket.close() + } + }, benchOptions)) +} diff --git a/test/bench/deliver-async-iterator.js b/test/bench/deliver-async-iterator.js new file mode 100644 index 00000000..281725ef --- /dev/null +++ b/test/bench/deliver-async-iterator.js @@ -0,0 +1,37 @@ +if (zmq.ng) { + suite.add(`deliver async iterator proto=${proto} msgsize=${msgsize} n=${n} zmq=ng`, Object.assign({ + fn: async deferred => { + const server = new zmq.ng.Dealer + const client = new zmq.ng.Dealer + + await server.bind(address) + client.connect(address) + + global.gc() + + const send = async () => { + for (let i = 0; i < n; i++) { + await client.send(Buffer.alloc(msgsize)) + } + } + + const receive = async () => { + let i = 0 + for await (const [msg] of server) { + if (++i == n) server.close() + } + } + + await Promise.all([send(), receive()]) + + global.gc() + + server.close() + client.close() + + global.gc() + + deferred.resolve() + } + }, benchOptions)) +} diff --git a/test/bench/deliver-multipart.js b/test/bench/deliver-multipart.js new file mode 100644 index 00000000..14087318 --- /dev/null +++ b/test/bench/deliver-multipart.js @@ -0,0 +1,71 @@ +if (zmq.cur) { + suite.add(`deliver multipart proto=${proto} msgsize=${msgsize} n=${n} zmq=cur`, Object.assign({ + fn: deferred => { + const server = zmq.cur.socket("dealer") + const client = zmq.cur.socket("dealer") + + let j = 0 + server.on("message", (msg1, msg2, mgs3) => { + j++ + if (j == n - 1) { + global.gc() + + server.close() + client.close() + + global.gc() + + deferred.resolve() + } + }) + + server.bind(address, () => { + client.connect(address) + + global.gc() + + for (let i = 0; i < n; i++) { + client.send([Buffer.alloc(msgsize), Buffer.alloc(msgsize), Buffer.alloc(msgsize)]) + } + }) + } + }, benchOptions)) +} + +if (zmq.ng) { + suite.add(`deliver multipart proto=${proto} msgsize=${msgsize} n=${n} zmq=ng`, Object.assign({ + fn: async deferred => { + const server = new zmq.ng.Dealer + const client = new zmq.ng.Dealer + + await server.bind(address) + client.connect(address) + + global.gc() + + const send = async () => { + for (let i = 0; i < n; i++) { + await client.send([Buffer.alloc(msgsize), Buffer.alloc(msgsize), Buffer.alloc(msgsize)]) + } + } + + const receive = async () => { + let j = 0 + for (j = 0; j < n - 1; j++) { + const [msg1, msg2, msg3] = await server.receive() + } + } + + await Promise.all([send(), receive()]) + + global.gc() + + server.close() + client.close() + + global.gc() + + deferred.resolve() + } + }, benchOptions)) +} diff --git a/test/bench/deliver.js b/test/bench/deliver.js new file mode 100644 index 00000000..1e240d88 --- /dev/null +++ b/test/bench/deliver.js @@ -0,0 +1,71 @@ +if (zmq.cur) { + suite.add(`deliver proto=${proto} msgsize=${msgsize} n=${n} zmq=cur`, Object.assign({ + fn: deferred => { + const server = zmq.cur.socket("dealer") + const client = zmq.cur.socket("dealer") + + let j = 0 + server.on("message", msg => { + j++ + if (j == n - 1) { + global.gc() + + server.close() + client.close() + + global.gc() + + deferred.resolve() + } + }) + + server.bind(address, () => { + client.connect(address) + + global.gc() + + for (let i = 0; i < n; i++) { + client.send(Buffer.alloc(msgsize)) + } + }) + } + }, benchOptions)) +} + +if (zmq.ng) { + suite.add(`deliver proto=${proto} msgsize=${msgsize} n=${n} zmq=ng`, Object.assign({ + fn: async deferred => { + const server = new zmq.ng.Dealer + const client = new zmq.ng.Dealer + + await server.bind(address) + client.connect(address) + + global.gc() + + const send = async () => { + for (let i = 0; i < n; i++) { + await client.send(Buffer.alloc(msgsize)) + } + } + + const receive = async () => { + let j = 0 + for (j = 0; j < n - 1; j++) { + const [msg] = await server.receive() + } + } + + await Promise.all([send(), receive()]) + + global.gc() + + server.close() + client.close() + + global.gc() + + deferred.resolve() + } + }, benchOptions)) +} diff --git a/test/bench/index.js b/test/bench/index.js new file mode 100644 index 00000000..b84d0ff7 --- /dev/null +++ b/test/bench/index.js @@ -0,0 +1,98 @@ +/* Number of messages per benchmark. */ +const n = parseInt(process.env.N, 10) || 5000 + +/* Transport protocols to benchmark. */ +const protos = [ + "tcp", + "inproc", + // "ipc", +] + +/* Which message part sizes to benchmark (exponentially increasing). */ +const msgsizes = [ + 0, // 16^0 = 1 + 1, // 16^1 = 16 + 2, // 16^2 = 256 + 3, // ... + 4, + 5, + // 6, +].map(n => 16 ** n) + +/* Which benchmarks to run. */ +const benchmarks = { + "create-socket": {n, options: {delay: 0.5}}, + "queue": {n, msgsizes}, + "deliver": {n, protos, msgsizes}, + "deliver-multipart": {n, protos, msgsizes}, + "deliver-async-iterator": {n, protos, msgsizes}, +} + +/* Set the exported libraries: current and next-gen. */ +const zmq = { + /* Assumes zeromq.js directory is checked out in a directory next to this. */ + // cur: require("../../../zeromq.js"), + ng: require("../.."), +} + +/* Windows cannot bind on a ports just above 1014; start higher to be safe. */ +let seq = 5000 + +function uniqAddress(proto) { + const id = seq++ + switch (proto) { + case "ipc": + return `${proto}://${__dirname}/../../tmp/${proto}-${id}` + case "tcp": + case "udp": + return `${proto}://127.0.0.1:${id}` + default: + return `${proto}://${proto}-${id}` + } +} + +/* Continue to load and execute benchmarks. */ +const fs = require("fs") +const bench = require("benchmark") +const suite = new bench.Suite + +const defaultOptions = { + defer: true, + delay: 0.1, + onError: console.error, +} + +for (const [benchmark, {n, protos, msgsizes, options}] of Object.entries(benchmarks)) { + let load = ({n, proto, msgsize, address}) => { + const benchOptions = Object.assign({}, defaultOptions, options) + eval(fs.readFileSync(`${__dirname}/${benchmark}.js`).toString()) + } + + if (protos && msgsizes) { + for (const proto of protos) { + const address = uniqAddress(proto) + for (const msgsize of msgsizes) { + load({n, proto, msgsize, address}) + } + } + } else if (msgsizes) { + const address = uniqAddress("tcp") + for (const msgsize of msgsizes) { + load({n, msgsize, address}) + } + } else { + load({n}) + } +} + +suite.on("cycle", ({target}) => { + console.log(target.toString()) +}) + +suite.on("complete", () => { + console.log("Completed.") + process.exit(0) +}) + +console.log("Running benchmarks...") +suite.run() diff --git a/test/bench/queue.js b/test/bench/queue.js new file mode 100644 index 00000000..1042c1e6 --- /dev/null +++ b/test/bench/queue.js @@ -0,0 +1,44 @@ +if (zmq.cur) { + suite.add(`queue msgsize=${msgsize} n=${n} zmq=cur`, Object.assign({ + fn: deferred => { + const client = zmq.cur.socket("dealer") + client.linger = 0 + client.connect(address) + + global.gc() + + for (let i = 0; i < n; i++) { + client.send(Buffer.alloc(msgsize)) + } + + global.gc() + + client.close() + + deferred.resolve() + } + }, benchOptions)) +} + +if (zmq.ng) { + suite.add(`queue msgsize=${msgsize} n=${n} zmq=ng`, Object.assign({ + fn: async deferred => { + const client = new zmq.ng.Dealer + client.linger = 0 + client.sendHighWaterMark = n * 2 + client.connect(address) + + global.gc() + + for (let i = 0; i < n; i++) { + await client.send(Buffer.alloc(msgsize)) + } + + global.gc() + + client.close() + + deferred.resolve() + } + }, benchOptions)) +} diff --git a/test/context.js b/test/context.js deleted file mode 100644 index 09eda651..00000000 --- a/test/context.js +++ /dev/null @@ -1,32 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver') - -describe('context', function() { - - it('should support setting max io threads', function(done) { - // 3.2 and above. - if (!semver.gte(zmq.version, '3.2.0')) { - done(); - return console.warn('Test requires libzmq >= 3.2.0'); - } - zmq.Context.setMaxThreads(3); - zmq.Context.getMaxThreads().should.equal(3); - zmq.Context.setMaxThreads(1); - done(); - }); - - it('should support setting max number of sockets', function(done) { - // 3.2 and above. - if (!semver.gte(zmq.version, '3.2.0')) { - done(); - return console.warn('Test requires libzmq >= 3.2.0'); - } - var currMaxSockets = zmq.Context.getMaxSockets(); - zmq.Context.setMaxSockets(256); - zmq.Context.getMaxSockets().should.equal(256); - zmq.Context.setMaxSockets(currMaxSockets); - done(); - }); - -}); diff --git a/test/exports.js b/test/exports.js deleted file mode 100644 index 6c75885a..00000000 --- a/test/exports.js +++ /dev/null @@ -1,137 +0,0 @@ - -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('exports', function(){ - it('should export a valid version', function(){ - semver.valid(zmq.version).should.be.ok; - }); - - it('should generate valid curve keypair', function(done) { - try { - var rep = zmq.socket('rep'); - rep.curve_server = 0; - } catch(e) { - console.log("libsodium seems to be missing (skipping curve test)"); - done(); - return; - } - - var curve = zmq.curveKeypair(); - should.exist(curve); - should.exist(curve.public); - should.exist(curve.secret); - curve.public.length.should.equal(40); - curve.secret.length.should.equal(40); - done(); - }); - - it('should export socket types and options', function(){ - // All versions. - var constants = [ - 'PUB', - 'SUB', - 'REQ', - 'XREQ', - 'REP', - 'XREP', - 'DEALER', - 'ROUTER', - 'PUSH', - 'PULL', - 'PAIR', - 'AFFINITY', - 'IDENTITY', - 'SUBSCRIBE', - 'UNSUBSCRIBE', - 'RCVTIMEO', - 'SNDTIMEO', - 'RATE', - 'RECOVERY_IVL', - 'SNDBUF', - 'RCVBUF', - 'RCVMORE', - 'FD', - 'EVENTS', - 'TYPE', - 'LINGER', - 'RECONNECT_IVL', - 'RECONNECT_IVL_MAX', - 'BACKLOG', - 'POLLIN', - 'POLLOUT', - 'POLLERR', - 'SNDMORE' - ]; - - // 2.x only. - if (semver.satisfies(zmq.version, '2.x')) { - constants.concat([ - 'HWM', - 'SWAP', - 'MCAST_LOOP', - 'ZMQ_RECOVERY_IVL_MSEC', - 'NOBLOCK' - ]); - } - - // 3.0 and above. - if (semver.gte(zmq.version, '3.0.0')) { - constants.concat([ - 'XPUB', - 'XSUB', - 'SNDHWM', - 'RCVHWM', - 'MAXMSGSIZE', - 'ZMQ_MULTICAST_HOPS', - 'TCP_KEEPALIVE', - 'TCP_KEEPALIVE_CNT', - 'TCP_KEEPALIVE_IDLE', - 'TCP_KEEPALIVE_INTVL' - ]); - } - - // 3.2 and above. - if (semver.gte(zmq.version, '3.2.0')) { - constants.concat([ - 'IPV4ONLY', - 'DELAY_ATTACH_ON_CONNECT', - 'ROUTER_MANDATORY', - 'XPUB_VERBOSE', - 'TCP_KEEPALIVE', - 'TCP_KEEPALIVE_IDLE', - 'TCP_KEEPALIVE_CNT', - 'TCP_KEEPALIVE_INTVL', - 'TCP_ACCEPT_FILTER', - 'LAST_ENDPOINT' - ]); - } - - // 3.3 and above. - if (semver.gte(zmq.version, '3.3.0')) { - constants.concat([ - 'ROUTER_RAW' - ]); - } - - constants.forEach(function(typeOrProp){ - zmq['ZMQ_' + typeOrProp].should.be.a.Number; - }); - }); - - it('should export states', function(){ - ['STATE_READY', 'STATE_BUSY', 'STATE_CLOSED'].forEach(function(state){ - zmq[state].should.be.a.Number; - }); - }); - - it('should export constructors', function(){ - zmq.Context.should.be.a.Function; - zmq.Socket.should.be.a.Function; - }); - - it('should export methods', function(){ - zmq.socket.should.be.a.Function; - }); -}); diff --git a/test/gc.js b/test/gc.js deleted file mode 100644 index 7eb6bf63..00000000 --- a/test/gc.js +++ /dev/null @@ -1,55 +0,0 @@ -if (process.versions['electron'] === undefined) { - var zmq = require('..') - , should = require('should'); - - it('should cooperate with gc', function(done){ - var a = zmq.socket('dealer') - , b = zmq.socket('dealer'); - - /** - * We create 2 dealer sockets. - * One of them (`a`) is not referenced explicitly after the main loop - * finishes so it's a pretender for garbage collection. - * This test performs gc() explicitly and then tries to send a message - * to a dealer socket that could be destroyed and collected. - * If a message is delivered, than everything is ok. Otherwise the guard - * timeout will make the test fail. - */ - a.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - this.close(); - b.close(); - clearTimeout(timeout); - done(); - }); - - var bound = false; - - a.bind('tcp://127.0.0.1:5555', function(e){ - if (e) { - clearInterval(interval); - done(e); - } else { - bound = true; - } - }); - - var interval = setInterval(function(){ - gc(); - if (bound) { - clearInterval(interval); - b.connect('tcp://127.0.0.1:5555'); - b.send('hello'); - } - }, 100); - - // guard against hanging - var timeout = setTimeout(function(){ - clearInterval(interval); - done(new Error('timeout of 5000ms exceeded (bound: ' + bound + ')')); - }, 15000); - }); -} else { - console.log('Running in electron: GC test skipped.'); -} diff --git a/test/hook.js b/test/hook.js new file mode 100644 index 00000000..bac0e887 --- /dev/null +++ b/test/hook.js @@ -0,0 +1,14 @@ +/* Display zmq async hooks during test run, throw if trigger ID is invalid. */ +require("async_hooks").createHook({ + init(id, type, triggerId, resource) { + if (type == "zmq") { + console.log("Created async context", id, type, triggerId, resource) + } + + if (triggerId < 0 || id < 0) { + process._rawDebug("init", {id, type, triggerId}) + Error.stackTraceLimit = Infinity + throw new Error("bad async trigger id") + } + } +}).enable() diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 00000000..4854b3bd --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,6 @@ +test/unit/*-test.ts test/unit/compat/*-test.{ts,js} +--require test/hook +--require ts-node/register +--require choma +--expose-gc +--exit diff --git a/test/socket.error-callback.js b/test/socket.error-callback.js deleted file mode 100644 index 8e095586..00000000 --- a/test/socket.error-callback.js +++ /dev/null @@ -1,26 +0,0 @@ - -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -var version = semver.gte(zmq.version, '3.2.0'); - -describe('socket.error-callback', function(){ - var sock; - - if (!version) { - return console.warn("ZMQ_ROUTER_MANDATORY requires libzmq 3.2.0, skipping test"); - } - - it('should create a socket and set ZMQ_ROUTER_MANDATORY', function () { - sock = zmq.socket('router'); - sock.setsockopt(zmq.ZMQ_ROUTER_MANDATORY, 1); - }); - - it('should callback with error when not connected', function (done) { - sock.send(['foo', 'bar'], null, function (error) { - error.should.be.an.instanceof(Error); - done(); - }); - }); -}); diff --git a/test/socket.events.js b/test/socket.events.js deleted file mode 100644 index bfe56b0c..00000000 --- a/test/socket.events.js +++ /dev/null @@ -1,32 +0,0 @@ -var zmq = require('..') - , should = require('should'); - -describe('socket.events', function(){ - - it('should support events', function(done){ - var rep = zmq.socket('rep') - , req = zmq.socket('req'); - - rep.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - rep.bind('inproc://stuffevents', function (error) { - if (error) throw error; - }); - - rep.on('bind', function(){ - req.connect('inproc://stuffevents'); - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - req.close(); - rep.close(); - done(); - }); - req.send('hello'); - }); - }); -}); diff --git a/test/socket.js b/test/socket.js deleted file mode 100644 index d6540041..00000000 --- a/test/socket.js +++ /dev/null @@ -1,62 +0,0 @@ - -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket', function(){ - var sock; - - it('should alias socket', function(){ - zmq.createSocket.should.equal(zmq.socket); - }); - - it('should include type and close', function(){ - sock = zmq.socket('req'); - sock.type.should.equal('req'); - sock.close.should.be.a.Function; - }); - - it('should use socketopt', function(){ - sock.getsockopt(zmq.ZMQ_BACKLOG).should.not.equal(75); - sock.setsockopt(zmq.ZMQ_BACKLOG, 75).should.equal(sock); - sock.getsockopt(zmq.ZMQ_BACKLOG).should.equal(75); - sock.setsockopt(zmq.ZMQ_BACKLOG, 100); - }); - - it('should use socketopt with sugar', function(){ - sock.getsockopt('backlog').should.not.equal(75); - sock.setsockopt('backlog', 75).should.equal(sock); - sock.getsockopt('backlog').should.equal(75); - - sock.backlog.should.be.a.Number; - sock.backlog.should.not.equal(50); - sock.backlog = 50; - sock.backlog.should.equal(50); - }); - - it('should close', function(){ - sock.closed.should.equal(false); - sock.close(); - sock.closed.should.equal(true); - }); - - it('should support options', function(){ - sock = zmq.socket('req', { backlog: 30 }); - sock.backlog.should.equal(30); - sock.close(); - }); - - it('should throw a javascript error if it hits the system file descriptor limit', function() { - var i, socks = [], numSocks = 10000; - function hitlimit() { - for (i = 0; i < numSocks; i++) { - socks.push(zmq.socket('router')); - } - } - hitlimit.should['throw']; - for (i = 0; i < socks.length; i++) { - socks[i].close(); - } - }); - -}); diff --git a/test/socket.messages.js b/test/socket.messages.js deleted file mode 100644 index db64792a..00000000 --- a/test/socket.messages.js +++ /dev/null @@ -1,146 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.messages', function(){ - var push, pull; - - beforeEach(function(){ - push = zmq.socket('push'); - pull = zmq.socket('pull'); - }); - - it('should support messages', function(done){ - var n = 0; - - pull.on('message', function(msg){ - msg = msg.toString(); - switch (n++) { - case 0: - msg.should.equal('string'); - break; - case 1: - msg.should.equal('15.99'); - break; - case 2: - msg.should.equal('buffer'); - push.close(); - pull.close(); - done(); - break; - } - }); - - pull.bind('inproc://stuff_ssm', function (error) { - if (error) throw error; - push.connect('inproc://stuff_ssm'); - push.send('string'); - push.send(15.99); - push.send(Buffer.from('buffer')); - }); - }); - - it('should support multipart messages', function(done){ - pull.on('message', function(msg1, msg2, msg3){ - msg1.toString().should.equal('string'); - msg2.toString().should.equal('15.99'); - msg3.toString().should.equal('buffer'); - push.close(); - pull.close(); - done(); - }); - - pull.bind('inproc://stuff_ssmm', function (error) { - if (error) throw error; - push.connect('inproc://stuff_ssmm'); - push.send(['string', 15.99, Buffer.from('buffer')]); - }); - }); - - it('should support sndmore', function(done){ - pull.on('message', function(a, b, c, d, e){ - a.toString().should.equal('tobi'); - b.toString().should.equal('loki'); - c.toString().should.equal('jane'); - d.toString().should.equal('luna'); - e.toString().should.equal('manny'); - push.close(); - pull.close(); - done(); - }); - - pull.bind('inproc://stuff_sss', function (error) { - if (error) throw error; - push.connect('inproc://stuff_sss'); - push.send(['tobi', 'loki'], zmq.ZMQ_SNDMORE); - push.send(['jane', 'luna'], zmq.ZMQ_SNDMORE); - push.send('manny'); - }); - }); - - it('should handle late connect', function(done){ - var n = 0; - - pull.on('message', function(msg){ - msg = msg.toString(); - switch (n++) { - case 0: - msg.should.equal('string'); - break; - case 1: - msg.should.equal('15.99'); - break; - case 2: - msg.should.equal('buffer'); - push.close(); - pull.close(); - done(); - break; - } - }); - - if (semver.satisfies(zmq.version, '>=3.x')) { - push.setsockopt(zmq.ZMQ_SNDHWM, 1); - pull.setsockopt(zmq.ZMQ_RCVHWM, 1); - } else if (semver.satisfies(zmq.version, '2.x')) { - push.setsockopt(zmq.ZMQ_HWM, 1); - pull.setsockopt(zmq.ZMQ_HWM, 1); - } - - push.bind('tcp://127.0.0.1:12345', function (error) { - if (error) throw error; - push.send('string'); - push.send(15.99); - push.send(Buffer.from('buffer')); - pull.connect('tcp://127.0.0.1:12345'); - }); - }); - - it('should call send() callbacks', function(done){ - var received = 0; - var callbacks = 0; - - function cb() { - callbacks += 1; - } - - pull.on('message', function () { - received += 1; - - if (received === 4) { - callbacks.should.equal(received); - done(); - } - }); - - pull.bind('inproc://stuff_ssmm', function (error) { - if (error) throw error; - push.connect('inproc://stuff_ssmm'); - - push.send('hello', null, cb); - push.send('hello', null, cb); - push.send('hello', null, cb); - push.send(['hello', 'world'], null, cb); - }); - }); -}); diff --git a/test/socket.monitor.js b/test/socket.monitor.js deleted file mode 100644 index 0c72ed51..00000000 --- a/test/socket.monitor.js +++ /dev/null @@ -1,112 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.monitor', function() { - if (!zmq.ZMQ_CAN_MONITOR) { - console.log("monitoring not enabled skipping test"); - return; - } - - it('should be able to monitor the socket', function(done) { - var rep = zmq.socket('rep') - , req = zmq.socket('req'); - - rep.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - var testedEvents = ['listen', 'accept', 'disconnect']; - testedEvents.forEach(function(e) { - rep.on(e, function(event_value, event_endpoint_addr) { - // Test the endpoint addr arg - event_endpoint_addr.toString().should.equal('tcp://127.0.0.1:5423'); - - testedEvents.pop(); - if (testedEvents.length === 0) { - rep.unmonitor(); - rep.close(); - done(); - } - }); - }); - - // enable monitoring for this socket - rep.monitor(); - - rep.bind('tcp://127.0.0.1:5423', function (error) { - if (error) throw error; - }); - - rep.on('bind', function(){ - req.connect('tcp://127.0.0.1:5423'); - req.send('hello'); - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - req.close(); - }); - - // Test that bind errors pass an Error both to the callback - // and to the monitor event - var doubleRep = zmq.socket('rep'); - doubleRep.monitor(); - doubleRep.on('bind_error', function (errno, bindAddr, ex) { - (ex instanceof Error).should.equal(true); - }); - doubleRep.bind('tcp://127.0.0.1:5423', function (error) { - (error instanceof Error).should.equal(true); - }); - }); - }); - - it('should use default interval and numOfEvents', function(done) { - var req = zmq.socket('req'); - req.setsockopt(zmq.ZMQ_RECONNECT_IVL, 5); // We want a quick connect retry from zmq - - // We will try to connect to a non-existing server, zmq will issue events: "connect_retry", "close", "connect_retry" - // The connect_retry will be issued immediately after the close event, so we will measure the time between the close - // event and connect_retry event, those should >= 9 (this will tell us that we are reading 1 event at a time from - // the monitor socket). - - var closeTime; - req.on('close', function() { - closeTime = Date.now(); - }); - - req.on('connect_retry', function() { - var diff = Date.now() - closeTime; - req.unmonitor(); - req.close(); - diff.should.be.within(9, 20); - done(); - }); - - req.monitor(); - req.connect('tcp://127.0.0.1:5423'); - }); - - it('should read multiple events on monitor interval', function(done) { - var req = zmq.socket('req'); - req.setsockopt(zmq.ZMQ_RECONNECT_IVL, 5); - var closeTime; - req.on('close', function() { - closeTime = Date.now(); - }); - - req.on('connect_retry', function() { - var diff = Date.now() - closeTime; - req.unmonitor(); - req.close(); - diff.should.be.within(0, 5); - done(); - }); - - // This should read all available messages from the queue, and we expect that "close" and "connect_retry" will be - // read on the same interval (for further details see the comment in the previous test) - req.monitor(10, 0); - req.connect('tcp://127.0.0.1:5423'); - }); -}); diff --git a/test/socket.pair.js b/test/socket.pair.js deleted file mode 100644 index 294b2fa8..00000000 --- a/test/socket.pair.js +++ /dev/null @@ -1,48 +0,0 @@ -var zmq = require('..') - , should = require('should'); - -describe('socket.pair', function(){ - - it('should support pair-pair', function (done){ - var pairB = zmq.socket('pair') - , pairC = zmq.socket('pair'); - - var n = 0; - pairB.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - - switch (n++) { - case 0: - msg.toString().should.equal('foo'); - break; - case 1: - msg.toString().should.equal('bar'); - break; - case 2: - msg.toString().should.equal('baz'); - pairB.close(); - pairC.close(); - done(); - break; - } - }); - - pairC.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('barnacle'); - }) - - var addr = "inproc://stuffpair"; - - pairB.bind(addr, function (error) { - if (error) throw error; - - pairC.connect(addr); - pairB.send('barnacle'); - pairC.send('foo'); - pairC.send('bar'); - pairC.send('baz'); - }); - }); - -}); diff --git a/test/socket.pub-sub.js b/test/socket.pub-sub.js deleted file mode 100644 index 3a51ca80..00000000 --- a/test/socket.pub-sub.js +++ /dev/null @@ -1,191 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.pub-sub', function(){ - var pub, sub; - - beforeEach(function() { - pub = zmq.socket('pub'); - sub = zmq.socket('sub'); - }); - - it('should support pub-sub', function(done){ - var n = 0; - - sub.subscribe(''); - sub.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - switch (n++) { - case 0: - msg.toString().should.equal('foo'); - break; - case 1: - msg.toString().should.equal('bar'); - break; - case 2: - msg.toString().should.equal('baz'); - sub.close(); - pub.close(); - done(); - break; - } - }); - - var addr = "inproc://stuff_ssps"; - - sub.bind(addr, function (error) { - if (error) throw error; - pub.connect(addr); - - // The connect is asynchronous, and messages published to a non- - // connected socket are silently dropped. That means that there is - // a race between connecting and sending the first message which - // causes this test to hang, especially when running on Linux. Even an - // inproc:// socket seems to be asynchronous. So instead of - // sending straight away, we wait 100ms for the connection to be - // established before we start the send. This fixes the observed - // hang. - - setTimeout(function() { - pub.send('foo'); - pub.send('bar'); - pub.send('baz'); - }, 100.0); - }); - }); - - it('should support pub-sub filter', function(done){ - var n = 0; - - sub.subscribe('js'); - sub.subscribe('luna'); - - sub.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - switch (n++) { - case 0: - msg.toString().should.equal('js is cool'); - break; - case 1: - msg.toString().should.equal('luna is cool too'); - sub.close(); - pub.close(); - done(); - break; - } - }); - - sub.bind('inproc://stuff_sspsf', function (error) { - if (error) throw error; - pub.connect('inproc://stuff_sspsf'); - - // See comments on pub-sub test. - - setTimeout(function() { - pub.send('js is cool'); - pub.send('ruby is meh'); - pub.send('py is pretty cool'); - pub.send('luna is cool too'); - }, 100.0); - }); - }); - - it('should continue to deliver messages even after error in message handler is thrown', function(done){ - var n = 0; - - sub.subscribe(''); - var errorHandlerCalled = 0; - - sub.on('error', function (error) { - errorHandlerCalled++; - }); - - sub.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - switch (n++) { - case 0: - msg.toString().should.equal('foo'); - throw Error('test error'); - break; - case 1: - msg.toString().should.equal('bar'); - sub.close(); - pub.close(); - errorHandlerCalled.should.eql(1) - done(); - break; - } - }); - - var addr = "inproc://stuff_ssps"; - - sub.bind(addr, function (error) { - if (error) throw error; - pub.connect(addr); - - setTimeout(function() { - pub.send('foo'); - pub.send('bar'); - }, 100.0); - }); - }); - - it('should subscribe to character arguments', function(done){ - - sub.subscribe('ABC012'); - - sub.on('message', function (msg) { - - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('ABC012'); - sub.close(); - return done(); - }); - - var addr = "inproc://stuff_sspsfa"; - - sub.bind(addr, function (error) { - if (error) throw error; - pub.connect(addr); - - setTimeout(function() { - pub.send('012'); - pub.send('ABC'); - pub.send('MSG'); - pub.send('ABC012'); - }, 100.0); - - }); - }) - - it('should subscribe to binary argument', function(done){ - - - sub.subscribe(Buffer.from(new Uint8Array([65, 66, 67, 48, 49, 50]))); - - sub.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('ABC0123'); - sub.close(); - return done(); - - }); - - var addr = "inproc://stuff_sspsfb"; - - sub.bind(addr, function (error) { - if (error) throw error; - pub.connect(addr); - - setTimeout(function() { - pub.send('ABC'); - pub.send('MSG'); - pub.send('MSG1'); - pub.send(Buffer.from(new Uint8Array([65, 66, 67, 48, 49, 50, 51]))); - }, 100.0); - - }); - - }) -}); diff --git a/test/socket.push-pull.js b/test/socket.push-pull.js deleted file mode 100644 index 8eeb66f4..00000000 --- a/test/socket.push-pull.js +++ /dev/null @@ -1,156 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.push-pull', function(){ - - it('should support push-pull', function(done){ - var push = zmq.socket('push') - , pull = zmq.socket('pull'); - - var n = 0; - pull.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - switch (n++) { - case 0: - msg.toString().should.equal('foo'); - break; - case 1: - msg.toString().should.equal('bar'); - break; - case 2: - msg.toString().should.equal('baz'); - pull.close(); - push.close(); - done(); - break; - } - }); - - var addr = "inproc://stuff"; - - pull.bind(addr, function (error) { - if (error) throw error; - push.connect(addr); - - push.send('foo'); - push.send('bar'); - push.send('baz'); - }); - }); - - - it('should not emit messages after pause()', function(done){ - var push = zmq.socket('push') - , pull = zmq.socket('pull'); - - var n = 0; - - pull.on('message', function(msg){ - if(n++ === 0) { - msg.toString().should.equal('foo'); - } - else{ - should.not.exist(msg); - } - }); - - var addr = "inproc://pause_stuff"; - - pull.bind(addr, function (error) { - if (error) throw error; - push.connect(addr); - - push.send('foo'); - pull.pause() - push.send('bar'); - push.send('baz'); - }); - - setTimeout(function (){ - pull.close(); - push.close(); - done(); - }, 100); - }); - - it('should be able to read messages after pause()', function(done){ - var push = zmq.socket('push') - , pull = zmq.socket('pull'); - - var addr = "inproc://pause_stuff"; - - var messages = ['bar', 'foo']; - pull.bind(addr, function (error) { - if (error) throw error; - push.connect(addr); - - pull.pause() - messages.forEach(function(message){ - push.send(message); - }); - - messages.forEach(function(message){ - pull.read().toString().should.eql(message); - }); - }); - - setTimeout(function (){ - pull.close(); - push.close(); - done(); - }, 100); - }); - - - it('should emit messages after resume()', function(done){ - var push = zmq.socket('push') - , pull = zmq.socket('pull'); - - var n = 0; - - function checkNoMessages(msg){ - should.not.exist(msg); - } - - function checkMessages(msg){ - msg.should.be.an.instanceof(Buffer); - switch (n++) { - case 0: - msg.toString().should.equal('foo'); - break; - case 1: - msg.toString().should.equal('bar'); - break; - case 2: - msg.toString().should.equal('baz'); - pull.close(); - push.close(); - done(); - break; - } - } - - pull.on('message', checkNoMessages) - - var addr = "inproc://resume_stuff"; - - pull.bind(addr, function (error) { - if (error) throw error; - push.connect(addr); - pull.pause() - - push.send('foo'); - push.send('bar'); - push.send('baz'); - - setTimeout(function (){ - pull.removeListener('message', checkNoMessages) - pull.on('message', checkMessages) - pull.resume() - }, 100) - - }); - - }); -}); diff --git a/test/socket.req-rep.js b/test/socket.req-rep.js deleted file mode 100644 index cbf28906..00000000 --- a/test/socket.req-rep.js +++ /dev/null @@ -1,95 +0,0 @@ -var zmq = require('..') - , should = require('should'); - -describe('socket.req-rep', function(){ - it('should support req-rep', function(done){ - var rep = zmq.socket('rep') - , req = zmq.socket('req'); - - rep.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - rep.bind('inproc://stuffreqrep', function (error) { - if (error) throw error; - req.connect('inproc://stuffreqrep'); - req.send('hello'); - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - rep.close(); - req.close(); - done(); - }); - }); - }); - - it('should support multiple', function(done){ - var n = 5; - - for (var i = 0; i < n; i++) { - (function(n){ - var rep = zmq.socket('rep') - , req = zmq.socket('req'); - - rep.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - rep.bind('inproc://' + n, function (error) { - if (error) throw error; - req.connect('inproc://' + n); - req.send('hello'); - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - req.close(); - rep.close(); - if (!--n) done(); - }); - }); - })(i); - } - }); - - it('should support a burst', function (done) { - var rep = zmq.socket('rep'); - var req = zmq.socket('req'); - var n = 10; - - rep.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - rep.bind('inproc://reqrepburst', function (error) { - if (error) throw error; - req.connect('inproc://reqrepburst'); - - var received = 0; - - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - - received += 1; - - if (received === n) { - rep.close(); - req.close(); - done(); - } - }); - - for (var i = 0; i < n; i += 1) { - req.send('hello'); - } - }); - }); - -}); diff --git a/test/socket.router.js b/test/socket.router.js deleted file mode 100644 index d5188477..00000000 --- a/test/socket.router.js +++ /dev/null @@ -1,129 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.router', function(){ - it('should handle the unroutable', function(done){ - var complete = 0; - - if (!semver.gte(zmq.version, '3.2.0')) { - done(); - return console.warn('Test requires libzmq >= 3.2.0'); - } - - if (semver.eq(zmq.version, '3.2.1')) { - done(); - return console.warn('ZMQ_ROUTER_MANDATORY is broken in libzmq = 3.2.1'); - } - - var envelope = '12384982398293'; - - var errMsgs = require('os').platform() === 'win32' ? ['Unknown error'] : []; - errMsgs.push('No route to host'); - errMsgs.push('Resource temporarily unavailable'); - errMsgs.push('Host unreachable'); - - function assertRouteError(err) { - if (errMsgs.indexOf(err.message) === -1) { - throw new Error(err.message); - } - } - - // should emit an error event on unroutable msgs if mandatory = 1 and error handler is set - - (function(){ - var sock = zmq.socket('router'); - sock.on('error', function (err) { - sock.close(); - assertRouteError(err); - if (++complete === 2) done(); - }); - - sock.setsockopt(zmq.ZMQ_ROUTER_MANDATORY, 1); - - sock.send([envelope, '']); - })(); - - // should throw an error on unroutable msgs if mandatory = 1 and no error handler is set - - (function(){ - var sock = zmq.socket('router'); - - sock.setsockopt(zmq.ZMQ_ROUTER_MANDATORY, 1); - - try { - sock.send([envelope, '']); - } catch (err) { - assertRouteError(err); - } - - try { - sock.send([envelope, '']); - } catch (err) { - assertRouteError(err); - } - - try { - sock.send([envelope, '']); - } catch (err) { - assertRouteError(err); - } - - sock.close(); - })(); - - // should silently ignore unroutable msgs if mandatory = 0 - - (function(){ - var sock = zmq.socket('router'); - - (function(){ - sock.send([envelope, '']); - sock.close(); - }).should.not.throw; - })(); - if (++complete === 2) done(); - }); - - it('should handle router <-> dealer message bursts', function (done) { - // tests https://github.com/JustinTulloss/zeromq.node/issues/523 - // based on https://gist.github.com/messa/862638ab44ca65f712fe4d6ef79aeb67 - - var router = zmq.socket('router'); - var dealer = zmq.socket('dealer'); - var addr = 'tcp://127.0.0.1:12345'; - var expected = 1000; - var counted = 0; - - router.bindSync(addr); - - router.on('message', function () { - var msg = []; - for (var i = 0; i < arguments.length; i += 1) { - msg.push(arguments[i]); - } - router.send(msg); - }); - - dealer.on('message', function (part1, part2, part3, part4, part5) { - String(part1).should.equal('Hello'); - String(part2).should.equal('world'); - String(part3).should.equal('part3'); - String(part4).should.equal('part4'); - String(part5).should.equal('undefined'); - - counted += 1; - if (counted === expected) { - router.close(); - dealer.close(); - done(); - } - }); - - dealer.connect(addr); - - for (var i = 0; i < expected; i += 1) { - dealer.send(['Hello', 'world', 'part3', 'part4']); - } - }); -}); diff --git a/test/socket.stream.js b/test/socket.stream.js deleted file mode 100644 index 4efe0b03..00000000 --- a/test/socket.stream.js +++ /dev/null @@ -1,75 +0,0 @@ -var zmq = require('..') - , http = require('http') - , should = require('should') - , semver = require('semver'); - -describe('socket.stream', function(){ - - it('should support a stream socket type', function (done){ - - // socket stream type API after libzmq4+, target > 4.0.0 - // v4.1.3's stream socket is a little buggy, let's exclude it for now - if (semver.gte(zmq.version, '4.0.0') && semver.lt(zmq.version, '4.1.3')) { - - var stream = zmq.socket('stream'); - stream.on('message', function (id,msg){ - - msg.should.be.an.instanceof(Buffer); - - var raw_header = String(msg).split('\r\n'); - var method = raw_header[0].split(' ')[0]; - method.should.equal('GET'); - - //finding an HTTP GET method, prepare HTTP response for TCP socket - var httpProtocolString = 'HTTP/1.0 200 OK\r\n' //status code - + 'Content-Type: text/html\r\n' //headers - + '\r\n' - + '' //response body - + '' //make it xml, json, html or something else - + '' - + '' - + '' - +'

derpin over protocols

' - + '' - +'' - - //zmq streaming prefixed by envelope's routing identifier - stream.send([id,httpProtocolString]); - }); - - var addr = '127.0.0.1:5513'; - stream.bind('tcp://'+addr, function (error) { - if (error) throw error; - //send non-peer request to zmq, like an http GET method with URI path - http.get('http://'+addr+'/aRandomRequestPath', function (httpMsg){ - - //msg should now be a node readable stream as the good lord intended - if (semver.gte(process.versions.node, '0.11.0')){ - httpMsg.socket._readableState.reading.should.be.false - } else { - if(semver.gte(process.versions.node, '0.10.0')){ - httpMsg.socket._readableState.reading.should.be.true - } - } - - //conventional node streams emit data events to process zmq stream response - httpMsg.on('data',function (msg){ - msg.should.be.an.instanceof(Buffer); - String(msg).should.equal('' - +'' - +'

derpin over protocols

' - +'' - +''); - done(); - }); - }); - }); - - } else { - - done(); - return console.warn('stream socket type in libzmq v4+'); - - } - }); -}); diff --git a/test/socket.unbind.js b/test/socket.unbind.js deleted file mode 100644 index 4d7b627f..00000000 --- a/test/socket.unbind.js +++ /dev/null @@ -1,55 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.unbind', function(){ - - it('should be able to unbind', function(done){ - if (!zmq.ZMQ_CAN_UNBIND) { - done(); - return; - } - var a = zmq.socket('dealer') - , b = zmq.socket('dealer') - , c = zmq.socket('dealer'); - - var message_count = 0; - a.bind('tcp://127.0.0.1:5420', function (error) { - if (error) throw error; - a.bind('tcp://127.0.0.1:5421', function (error) { - if (error) throw error; - b.connect('tcp://127.0.0.1:5420'); - b.send('Hello from b.'); - c.connect('tcp://127.0.0.1:5421'); - c.send('Hello from c.'); - }); - }); - - a.on('unbind', function(addr) { - if (addr === 'tcp://127.0.0.1:5420') { - b.send('Error from b.'); - c.send('Messsage from c.'); - setTimeout(function () { - c.send('Final message from c.'); - }, 100); - } - }); - - a.on('message', function(msg) { - message_count++; - if (msg.toString() === 'Hello from b.') { - a.unbind('tcp://127.0.0.1:5420', function (error) { - if (error) throw error; - }); - } else if (msg.toString() === 'Final message from c.') { - message_count.should.equal(4); - a.close(); - b.close(); - c.close(); - done(); - } else if (msg.toString() === 'Error from b.') { - throw Error('b should have been unbound'); - } - }); - }); -}); diff --git a/test/socket.xpub-xsub.js b/test/socket.xpub-xsub.js deleted file mode 100644 index ae2b5aa7..00000000 --- a/test/socket.xpub-xsub.js +++ /dev/null @@ -1,88 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.xpub-xsub', function () { - it('should support pub-sub tracing and filtering', function (done) { - if (!semver.gte(zmq.version, '3.1.0')) { - done(); - return console.warn('Test requires libzmq >= 3.1.0'); - } - - var n = 0; - var m = 0; - var pub = zmq.socket('pub'); - var sub = zmq.socket('sub'); - var xpub = zmq.socket('xpub'); - var xsub = zmq.socket('xsub'); - - pub.bindSync('tcp://*:5556'); - xsub.connect('tcp://127.0.0.1:5556'); - xpub.bindSync('tcp://*:5555'); - sub.connect('tcp://127.0.0.1:5555'); - - xsub.on('message', function (msg) { - xpub.send(msg); // Forward message using the xpub so subscribers can receive it - }); - - xpub.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - - var type = msg[0] === 0 ? 'unsubscribe' : 'subscribe'; - var channel = msg.slice(1).toString(); - - switch (type) { - case 'subscribe': - switch (m++) { - case 0: - channel.should.equal('js'); - break; - case 1: - channel.should.equal('luna'); - break; - } - break; - case 'unsubscribe': - switch (m++) { - case 2: - channel.should.equal('luna'); - sub.close(); - pub.close(); - xsub.close(); - xpub.close(); - done(); - break; - } - break; - } - - xsub.send(msg); // Forward message using the xsub so the publisher knows it has a subscriber - }); - - sub.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - switch (n++) { - case 0: - msg.toString().should.equal('js is cool'); - break; - case 1: - msg.toString().should.equal('luna is cool too'); - break; - } - }); - - sub.subscribe('js'); - sub.subscribe('luna'); - - setTimeout(function () { - pub.send('js is cool'); - pub.send('ruby is meh'); - pub.send('py is pretty cool'); - pub.send('luna is cool too'); - }, 100.0); - - setTimeout(function () { - sub.unsubscribe('luna'); - }, 300); - }); -}); diff --git a/test/socket.zap.js b/test/socket.zap.js deleted file mode 100644 index dd37df9a..00000000 --- a/test/socket.zap.js +++ /dev/null @@ -1,133 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -describe('socket.zap', function(){ - - var zap = require('./zap') - , zapSocket, rep, req, count = 0; - - beforeEach(function(){ - count++; - zapSocket = zap.start(count); - rep = zmq.socket('rep'); - req = zmq.socket('req'); - }); - - afterEach(function(){ - req.close(); - rep.close(); - zapSocket.close(); - }); - - it('should support curve', function(done){ - var port = 'tcp://127.0.0.1:12347'; - if (!semver.gte(zmq.version, '4.0.0')) { - done(); - return; - } - - try { - rep.curve_server = 0; - } catch(e) { - console.log("libsodium seems to be missing (skipping curve test)"); - done(); - return; - } - - var serverPublicKey = Buffer.from('7f188e5244b02bf497b86de417515cf4d4053ce4eb977aee91a55354655ec33a', 'hex') - , serverPrivateKey = Buffer.from('1f5d3873472f95e11f4723d858aaf0919ab1fb402cb3097742c606e61dd0d7d8', 'hex') - , clientPublicKey = Buffer.from('ea1cc8bd7c8af65497d43fc21dbec6560c5e7b61bcfdcbd2b0dfacf0b4c38d45', 'hex') - , clientPrivateKey = Buffer.from('83f99afacfab052406e5f421612568034e85f4c8182a1c92671e83dca669d31d', 'hex'); - - rep.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - rep.zap_domain = "test"; - rep.curve_server = 1; - rep.curve_secretkey = serverPrivateKey; - rep.mechanism.should.eql(2); - - rep.bind(port, function (error) { - if (error) throw error; - req.curve_serverkey = serverPublicKey; - req.curve_publickey = clientPublicKey; - req.curve_secretkey = clientPrivateKey; - req.mechanism.should.eql(2); - - req.connect(port); - req.send('hello'); - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - done(); - }); - }); - - }); - - it('should support null', function(done){ - var port = 'tcp://127.0.0.1:12345'; - if (!semver.gte(zmq.version, '4.0.0')) { - done(); - return; - } - - rep.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - rep.zap_domain = "test"; - rep.mechanism.should.eql(0); - - rep.bind(port, function (error) { - if (error) throw error; - req.mechanism.should.eql(0); - req.connect(port); - req.send('hello'); - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - done(); - }); - }); - }); - - it('should support plain', function(done){ - var port = 'tcp://127.0.0.1:12346'; - if (!semver.gte(zmq.version, '4.0.0')) { - done(); - return; - } - - rep.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('hello'); - rep.send('world'); - }); - - rep.zap_domain = "test"; - rep.plain_server = 1; - rep.mechanism.should.eql(1); - - rep.bind(port, function (error) { - if (error) throw error; - req.plain_username = "user"; - req.plain_password = "pass"; - req.mechanism.should.eql(1); - - req.connect(port); - req.send('hello'); - req.on('message', function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('world'); - done(); - }); - }); - }); -}); diff --git a/test/tslint.json b/test/tslint.json new file mode 100644 index 00000000..9f4c9b42 --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,9 @@ +{ + "defaultSeverity": "error", + "extends": ["../tslint.json"], + "rules": { + "new-parens": [false], + "no-console": [false], + "only-arrow-functions": [false] + } +} \ No newline at end of file diff --git a/test/unit/compat/README.md b/test/unit/compat/README.md new file mode 100644 index 00000000..744d5643 --- /dev/null +++ b/test/unit/compat/README.md @@ -0,0 +1,22 @@ +The tests in this directory have been copied from the original ZeroMQ.js version +(up to 5.x) for which the license and copyright notice is reproduced below. + +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2010, 2011 Justin Tulloss + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/test/unit/compat/context-test.js b/test/unit/compat/context-test.js new file mode 100644 index 00000000..de113ab3 --- /dev/null +++ b/test/unit/compat/context-test.js @@ -0,0 +1,19 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + + describe("compat context", function() { + it("should support setting max io threads", function() { + zmq.Context.setMaxThreads(3) + assert.equal(zmq.Context.getMaxThreads(), 3) + zmq.Context.setMaxThreads(1) + }) + + it("should support setting max number of sockets", function() { + const currMaxSockets = zmq.Context.getMaxSockets() + zmq.Context.setMaxSockets(256) + assert.equal(zmq.Context.getMaxSockets(), 256) + zmq.Context.setMaxSockets(currMaxSockets) + }) + }) +} diff --git a/test/unit/compat/exports-test.js b/test/unit/compat/exports-test.js new file mode 100644 index 00000000..410d968f --- /dev/null +++ b/test/unit/compat/exports-test.js @@ -0,0 +1,99 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const semver = require("semver") + const {assert} = require("chai") + + describe("compat exports", function() { + it("should export a valid version", function(){ + assert.ok(semver.valid(zmq.version)) + }) + + it("should generate valid curve keypair", function() { + if (!zmq.capability.curve) this.skip() + + const curve = zmq.curveKeypair() + assert.typeOf(curve.public, "string") + assert.typeOf(curve.secret, "string") + assert.equal(curve.public.length, 40) + assert.equal(curve.secret.length, 40) + }) + + it("should export socket types and options", function() { + const constants = [ + "PUB", + "SUB", + "REQ", + "XREQ", + "REP", + "XREP", + "DEALER", + "ROUTER", + "PUSH", + "PULL", + "PAIR", + "AFFINITY", + "IDENTITY", + "SUBSCRIBE", + "UNSUBSCRIBE", + "RCVTIMEO", + "SNDTIMEO", + "RATE", + "RECOVERY_IVL", + "SNDBUF", + "RCVBUF", + "RCVMORE", + "FD", + "EVENTS", + "TYPE", + "LINGER", + "RECONNECT_IVL", + "RECONNECT_IVL_MAX", + "BACKLOG", + "POLLIN", + "POLLOUT", + "POLLERR", + "SNDMORE", + "XPUB", + "XSUB", + "SNDHWM", + "RCVHWM", + "MAXMSGSIZE", + "MULTICAST_HOPS", + "TCP_KEEPALIVE", + "TCP_KEEPALIVE_CNT", + "TCP_KEEPALIVE_IDLE", + "TCP_KEEPALIVE_INTVL", + "IPV4ONLY", + "DELAY_ATTACH_ON_CONNECT", + "ROUTER_MANDATORY", + "XPUB_VERBOSE", + "TCP_KEEPALIVE", + "TCP_KEEPALIVE_IDLE", + "TCP_KEEPALIVE_CNT", + "TCP_KEEPALIVE_INTVL", + "TCP_ACCEPT_FILTER", + "LAST_ENDPOINT", + "ROUTER_RAW", + ] + + constants.forEach(function(typeOrProp) { + assert.typeOf(zmq["ZMQ_" + typeOrProp], "number") + }) + }) + + it("should export states", function(){ + ["STATE_READY", "STATE_BUSY", "STATE_CLOSED"].forEach(function(state) { + assert.typeOf(zmq[state], "number") + }) + }) + + it("should export constructors", function(){ + assert.typeOf(zmq.Context, "function") + assert.typeOf(zmq.Socket, "function") + }) + + it("should export methods", function(){ + assert.typeOf(zmq.socket, "function") + }) + }) +} diff --git a/test/unit/compat/gc-test.js b/test/unit/compat/gc-test.js new file mode 100644 index 00000000..a8f2c136 --- /dev/null +++ b/test/unit/compat/gc-test.js @@ -0,0 +1,52 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto}`, function() { + it("should cooperate with gc", function(done) { + const sockA = zmq.socket("dealer") + const sockB = zmq.socket("dealer") + + /** + * We create 2 dealer sockets. + * One of them (`a`) is not referenced explicitly after the main loop + * finishes so it"s a pretender for garbage collection. + * This test performs global.gc() explicitly and then tries to send a message + * to a dealer socket that could be destroyed and collected. + * If a message is delivered, than everything is ok. Otherwise the guard + * timeout will make the test fail. + */ + sockA.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + sockA.close() + sockB.close() + done() + }) + + let bound = false + + const address = uniqAddress(proto) + sockA.bind(address, err => { + if (err) { + clearInterval(interval) + done(err) + } else { + bound = true + } + }) + + let interval = setInterval(function() { + global.gc() + if (bound) { + clearInterval(interval) + sockB.connect(address) + sockB.send("hello") + } + }, 15) + }) + }) + } +} diff --git a/test/unit/compat/load.js b/test/unit/compat/load.js new file mode 100644 index 00000000..0aec5bb3 --- /dev/null +++ b/test/unit/compat/load.js @@ -0,0 +1,10 @@ +const path = require("path") + +module.exports = require( + process.env.ZMQ_COMPAT_PATH ? + path.resolve(process.cwd(), process.env.ZMQ_COMPAT_PATH) : + "../../../src/compat" +) + +/* Copy capabilities from regular module. */ +module.exports.capability = require("../../../src").capability diff --git a/test/unit/compat/socket-error-callback-test.js b/test/unit/compat/socket-error-callback-test.js new file mode 100644 index 00000000..0ad1ef3d --- /dev/null +++ b/test/unit/compat/socket-error-callback-test.js @@ -0,0 +1,29 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + + describe("compat socket error callback", function() { + let sock + + beforeEach(function() { + sock = zmq.socket("router") + }) + + afterEach(function() { + sock.close() + }) + + it("should create a socket with mandatory", function() { + sock.setsockopt(zmq.ZMQ_ROUTER_MANDATORY, 1) + sock.setsockopt(zmq.ZMQ_SNDTIMEO, 10) + }) + + it("should callback with error when not connected", function(done) { + sock.send(["foo", "bar"], null, err => { + assert.instanceOf(err, Error) + sock.close() + done() + }) + }) + }) +} diff --git a/test/unit/compat/socket-events-test.js b/test/unit/compat/socket-events-test.js new file mode 100644 index 00000000..78ca700d --- /dev/null +++ b/test/unit/compat/socket-events-test.js @@ -0,0 +1,39 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} events`, function() { + it("should support events", function(done) { + const rep = zmq.socket("rep") + const req = zmq.socket("req") + + const address = uniqAddress(proto) + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + rep.bind(address, err => { + if (err) throw err + }) + + rep.on("bind", function() { + req.connect(address) + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + req.close() + rep.close() + done() + }) + + req.send("hello") + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-messages-test.js b/test/unit/compat/socket-messages-test.js new file mode 100644 index 00000000..65be0a56 --- /dev/null +++ b/test/unit/compat/socket-messages-test.js @@ -0,0 +1,157 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} messages`, function() { + let push, pull + + beforeEach(function() { + push = zmq.socket("push") + pull = zmq.socket("pull") + }) + + it("should support messages", function(done) { + const address = uniqAddress(proto) + + let n = 0 + + pull.on("message", function(msg) { + msg = msg.toString() + switch (n++) { + case 0: + assert.equal(msg, "string") + break + case 1: + assert.equal(msg, "15.99") + break + case 2: + assert.equal(msg, "buffer") + push.close() + pull.close() + done() + break + } + }) + + pull.bind(address, err => { + if (err) throw err + push.connect(address) + push.send("string") + push.send(15.99) + push.send(Buffer.from("buffer")) + }) + }) + + it("should support multipart messages", function(done) { + const address = uniqAddress(proto) + + pull.on("message", function(msg1, msg2, msg3) { + assert.equal(msg1.toString(), "string") + assert.equal(msg2.toString(), "15.99") + assert.equal(msg3.toString(), "buffer") + push.close() + pull.close() + done() + }) + + pull.bind(address, err => { + if (err) throw err + push.connect(address) + push.send(["string", 15.99, Buffer.from("buffer")]) + }) + }) + + it("should support sndmore", function(done) { + const address = uniqAddress(proto) + + pull.on("message", function(a, b, c, d, e) { + assert.equal(a.toString(), "tobi") + assert.equal(b.toString(), "loki") + assert.equal(c.toString(), "jane") + assert.equal(d.toString(), "luna") + assert.equal(e.toString(), "manny") + push.close() + pull.close() + done() + }) + + pull.bind(address, err => { + if (err) throw err + push.connect(address) + push.send(["tobi", "loki"], zmq.ZMQ_SNDMORE) + push.send(["jane", "luna"], zmq.ZMQ_SNDMORE) + push.send("manny") + }) + }) + + if (proto != "inproc") { + it("should handle late connect", function(done) { + const address = uniqAddress(proto) + let n = 0 + + pull.on("message", function(msg) { + msg = msg.toString() + switch (n++) { + case 0: + assert.equal(msg, "string") + break + case 1: + assert.equal(msg, "15.99") + break + case 2: + assert.equal(msg, "buffer") + push.close() + pull.close() + done() + break + } + }) + + push.setsockopt(zmq.ZMQ_SNDHWM, 1) + pull.setsockopt(zmq.ZMQ_RCVHWM, 1) + + push.bind(address, err => { + if (err) throw err + push.send("string") + push.send(15.99) + push.send(Buffer.from("buffer")) + pull.connect(address) + }) + }) + } + + it("should call send callbacks", function(done) { + const address = uniqAddress(proto) + let received = 0 + let callbacks = 0 + + function cb() { + callbacks += 1 + } + + pull.on("message", function() { + received += 1 + + if (received === 4) { + assert.equal(callbacks, received) + pull.close() + push.close() + done() + } + }) + + pull.bind(address, err => { + if (err) throw err + push.connect(address) + + push.send("hello", null, cb) + push.send("hello", null, cb) + push.send("hello", null, cb) + push.send(["hello", "world"], null, cb) + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-monitor-test.js b/test/unit/compat/socket-monitor-test.js new file mode 100644 index 00000000..4a910533 --- /dev/null +++ b/test/unit/compat/socket-monitor-test.js @@ -0,0 +1,82 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const semver = require("semver") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + /* This test case only seems to work reliably with TCP. */ + for (const proto of testProtos("tcp")) { + describe(`compat socket with ${proto} monitor`, function() { + beforeEach(function() { + /* ZMQ < 4.2 occasionally fails with assertion errors. */ + if (semver.satisfies(zmq.version, "< 4.2")) this.skip() + + this.warningListeners = process.listeners("warning") + }) + + afterEach(function() { + process.removeAllListeners("warning") + for (const listener of this.warningListeners) { + process.on("warning", listener) + } + }) + + it("should be able to monitor the socket", function(done) { + const rep = zmq.socket("rep") + const req = zmq.socket("req") + + const address = uniqAddress(proto) + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + const testedEvents = ["listen", "accept", "disconnect"] + testedEvents.forEach(function(e) { + rep.on(e, function(event_value, event_endpoint_addr) { + assert.equal(event_endpoint_addr.toString(), address) + + testedEvents.pop() + if (testedEvents.length === 0) { + rep.unmonitor() + rep.close() + done() + } + }) + }) + + // enable monitoring for this socket + rep.monitor() + + rep.bind(address, err => { + if (err) throw err + }) + + rep.on("bind", function() { + req.connect(address) + req.send("hello") + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + req.close() + }) + + // Test that bind errors pass an Error both to the callback + // and to the monitor event + const doubleRep = zmq.socket("rep") + doubleRep.monitor() + doubleRep.on("bind_error", function(errno, bindAddr, ex) { + assert.instanceOf(ex, Error) + doubleRep.close() + }) + + doubleRep.bind(address, err => { + assert.instanceOf(err, Error) + }) + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-pair-test.js b/test/unit/compat/socket-pair-test.js new file mode 100644 index 00000000..59bcb81b --- /dev/null +++ b/test/unit/compat/socket-pair-test.js @@ -0,0 +1,55 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp")) { + describe(`compat socket with ${proto} pair`, function() { + it("should support pair-pair", function(done) { + const pairA = zmq.socket("pair") + const pairB = zmq.socket("pair") + + const address = uniqAddress(proto) + + let n = 0 + pairA.monitor() + pairB.monitor() + pairA.on("bindError", console.log) + pairB.on("bindError", console.log) + pairA.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + + switch (n++) { + case 0: + assert.equal(msg.toString(), "foo") + break + case 1: + assert.equal(msg.toString(), "bar") + break + case 2: + assert.equal(msg.toString(), "baz") + pairA.close() + pairB.close() + done() + break + } + }) + + pairB.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "barnacle") + }) + + pairA.bind(address, async err => { + if (err) throw err + + pairB.connect(address) + pairA.send("barnacle") + pairB.send("foo") + pairB.send("bar") + pairB.send("baz") + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-pub-sub-test.js b/test/unit/compat/socket-pub-sub-test.js new file mode 100644 index 00000000..da181b76 --- /dev/null +++ b/test/unit/compat/socket-pub-sub-test.js @@ -0,0 +1,147 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} pub-sub`, function() { + let pub, sub + + beforeEach(function() { + pub = zmq.socket("pub") + sub = zmq.socket("sub") + }) + + it("should support pub-sub", function(done) { + const address = uniqAddress(proto) + let n = 0 + + sub.subscribe("") + sub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + switch (n++) { + case 0: + assert.equal(msg.toString(), "foo") + break + case 1: + assert.equal(msg.toString(), "bar") + break + case 2: + assert.equal(msg.toString(), "baz") + sub.close() + pub.close() + done() + break + } + }) + + sub.bind(address, err => { + if (err) throw err + pub.connect(address) + + // The connect is asynchronous, and messages published to a non- + // connected socket are silently dropped. That means that there is + // a race between connecting and sending the first message which + // causes this test to hang, especially when running on Linux. Even an + // inproc:// socket seems to be asynchronous. So instead of + // sending straight away, we wait 100ms for the connection to be + // established before we start the send. This fixes the observed + // hang. + + setTimeout(() => { + pub.send("foo") + pub.send("bar") + pub.send("baz") + }, 15) + }) + }) + + it("should support pub-sub filter", function(done) { + const address = uniqAddress(proto) + let n = 0 + + sub.subscribe("js") + sub.subscribe("luna") + + sub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + switch (n++) { + case 0: + assert.equal(msg.toString(), "js is cool") + break + case 1: + assert.equal(msg.toString(), "luna is cool too") + sub.close() + pub.close() + done() + break + } + }) + + sub.bind(address, err => { + if (err) throw err + pub.connect(address) + + // See comments on pub-sub test. + + setTimeout(() => { + pub.send("js is cool") + pub.send("ruby is meh") + pub.send("py is pretty cool") + pub.send("luna is cool too") + }, 15) + }) + }) + + describe("with errors", function() { + before(function() { + this.uncaughtExceptionListeners = process.listeners("uncaughtException") + process.removeAllListeners("uncaughtException") + }) + + after(function() { + process.removeAllListeners("uncaughtException") + for (const listener of this.uncaughtExceptionListeners) { + process.on("uncaughtException", listener) + } + }) + + it("should continue to deliver messages in message handler", function(done) { + let error + process.once("uncaughtException", err => {error = err}) + + const address = uniqAddress(proto) + let n = 0 + + sub.subscribe("") + sub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + switch (n++) { + case 0: + assert.equal(msg.toString(), "foo") + throw Error("test error") + break + case 1: + assert.equal(msg.toString(), "bar") + sub.close() + pub.close() + assert.equal(error.message, "test error") + done() + break + } + }) + + sub.bind(address, err => { + if (err) throw err + pub.connect(address) + + setTimeout(() => { + pub.send("foo") + pub.send("bar") + }, 15) + }) + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-push-pull-test.js b/test/unit/compat/socket-push-pull-test.js new file mode 100644 index 00000000..263a553b --- /dev/null +++ b/test/unit/compat/socket-push-pull-test.js @@ -0,0 +1,157 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} push-pull`, function() { + it("should support push-pull", function(done) { + const push = zmq.socket("push") + const pull = zmq.socket("pull") + + const address = uniqAddress(proto) + + let n = 0 + pull.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + switch (n++) { + case 0: + assert.equal(msg.toString(), "foo") + break + case 1: + assert.equal(msg.toString(), "bar") + break + case 2: + assert.equal(msg.toString(), "baz") + pull.close() + push.close() + done() + break + } + }) + + pull.bind(address, err => { + if (err) throw err + push.connect(address) + + push.send("foo") + push.send("bar") + push.send("baz") + }) + }) + + + it("should not emit messages after pause", function(done) { + const push = zmq.socket("push") + const pull = zmq.socket("pull") + + const address = uniqAddress(proto) + + let n = 0 + + pull.on("message", function(msg) { + if (n++ === 0) { + assert.equal(msg.toString(), "foo") + } else { + assert.equal(msg, undefined) + } + }) + + pull.bind(address, err => { + if (err) throw err + push.connect(address) + + push.send("foo") + pull.pause() + push.send("bar") + push.send("baz") + }) + + setTimeout(() => { + pull.close() + push.close() + done() + }, 15) + }) + + it("should be able to read messages after pause", function(done) { + const push = zmq.socket("push") + const pull = zmq.socket("pull") + + const address = uniqAddress(proto) + + const messages = ["bar", "foo"] + pull.bind(address, err => { + if (err) throw err + push.connect(address) + + pull.pause() + messages.forEach(function(message) { + push.send(message) + }) + + let i = 0 + pull.on("message", message => { + assert.equal(message.toString(), messages[i++]) + }) + }) + + setTimeout(() => { + pull.close() + push.close() + done() + }, 15) + }) + + + it("should emit messages after resume", function(done) { + const push = zmq.socket("push") + const pull = zmq.socket("pull") + + const address = uniqAddress(proto) + + let n = 0 + + function checkNoMessages(msg) { + assert.equal(msg, undefined) + } + + function checkMessages(msg) { + assert.instanceOf(msg, Buffer) + switch (n++) { + case 0: + assert.equal(msg.toString(), "foo") + break + case 1: + assert.equal(msg.toString(), "bar") + break + case 2: + assert.equal(msg.toString(), "baz") + pull.close() + push.close() + done() + break + } + } + + pull.on("message", checkNoMessages) + + pull.bind(address, err => { + if (err) throw err + push.connect(address) + pull.pause() + + push.send("foo") + push.send("bar") + push.send("baz") + + setTimeout(() => { + pull.removeListener("message", checkNoMessages) + pull.on("message", checkMessages) + pull.resume() + }, 15) + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-req-rep-test.js b/test/unit/compat/socket-req-rep-test.js new file mode 100644 index 00000000..3702f3c1 --- /dev/null +++ b/test/unit/compat/socket-req-rep-test.js @@ -0,0 +1,106 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} req-rep`, function() { + it("should support req-rep", function(done) { + const rep = zmq.socket("rep") + const req = zmq.socket("req") + + const address = uniqAddress(proto) + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + rep.bind(address, err => { + if (err) throw err + req.connect(address) + req.send("hello") + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + rep.close() + req.close() + done() + }) + }) + }) + + it("should support multiple", function(done) { + let n = 5 + + for (let i = 0; i < n; i++) { + (function(n) { + const rep = zmq.socket("rep") + const req = zmq.socket("req") + + const address = uniqAddress(proto) + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + rep.bind(address, err => { + if (err) throw err + req.connect(address) + req.send("hello") + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + req.close() + rep.close() + if (!--n) done() + }) + }) + })(i) + } + }) + + it("should support a burst", function(done) { + const rep = zmq.socket("rep") + const req = zmq.socket("req") + + const address = uniqAddress(proto) + + let n = 10 + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + rep.bind(address, err => { + if (err) throw err + req.connect(address) + + let received = 0 + + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + + received += 1 + + if (received === n) { + rep.close() + req.close() + done() + } + }) + + for (let i = 0; i < n; i += 1) { + req.send("hello") + } + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-router-test.js b/test/unit/compat/socket-router-test.js new file mode 100644 index 00000000..d98cc197 --- /dev/null +++ b/test/unit/compat/socket-router-test.js @@ -0,0 +1,113 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} router`, function() { + it("should handle unroutable messages", function(done) { + let complete = 0 + + const envelope = "12384982398293" + + const errMsgs = require("os").platform() === "win32" ? ["Unknown error"] : [] + errMsgs.push("No route to host") + errMsgs.push("Resource temporarily unavailable") + errMsgs.push("Host unreachable") + + function assertRouteError(err) { + if (errMsgs.indexOf(err.message) === -1) { + throw new Error(err.message) + } + } + + // should emit an error event on unroutable msgs if mandatory = 1 and error handler is set + + const sockA = zmq.socket("router") + sockA.on("error", err => { + sockA.close() + assertRouteError(err) + if (++complete === 2) done() + }) + + sockA.setsockopt(zmq.ZMQ_ROUTER_MANDATORY, 1) + sockA.setsockopt(zmq.ZMQ_SNDTIMEO, 10) + + sockA.send([envelope, ""]) + + // should throw an error on unroutable msgs if mandatory = 1 and no error handler is set + + const sockB = zmq.socket("router") + + sockB.setsockopt(zmq.ZMQ_ROUTER_MANDATORY, 1) + sockA.setsockopt(zmq.ZMQ_SNDTIMEO, 10) + + sockB.send([envelope, ""], null, err => { + assertRouteError(err) + }) + + sockB.send([envelope, ""], null, err => { + assertRouteError(err) + }) + + sockB.send([envelope, ""], null, err => { + assertRouteError(err) + }) + + sockB.close() + + // should silently ignore unroutable msgs if mandatory = 0 + + const sockC = zmq.socket("router") + + sockC.send([envelope, ""]) + sockC.close() + + if (++complete === 2) done() + }) + + it("should handle router-dealer message bursts", function(done) { + this.slow(150) + // tests https://github.com/JustinTulloss/zeromq.node/issues/523 + // based on https://gist.github.com/messa/862638ab44ca65f712fe4d6ef79aeb67 + + const router = zmq.socket("router") + const dealer = zmq.socket("dealer") + + const address = uniqAddress(proto) + + const expected = 1000 + let counted = 0 + + router.bind(address, err => { + if (err) throw err + + router.on("message", function(...msg) { + router.send(msg) + }) + + dealer.on("message", function(part1, part2, part3, part4, part5) { + assert.equal(part1.toString(), "Hello") + assert.equal(part2.toString(), "world") + assert.equal(part3.toString(), "part3") + assert.equal(part4.toString(), "part4") + assert.equal(part5, undefined) + + counted += 1 + if (counted === expected) { + router.close() + dealer.close() + done() + } + }) + + dealer.connect(address) + + for (let i = 0; i < expected; i += 1) { + dealer.send(["Hello", "world", "part3", "part4"]) + } + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-stream-test.js b/test/unit/compat/socket-stream-test.js new file mode 100644 index 00000000..a67ef8f8 --- /dev/null +++ b/test/unit/compat/socket-stream-test.js @@ -0,0 +1,58 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + const http = require("http") + + describe("compat socket stream", function() { + it("should support a stream socket type", function(done) { + const stream = zmq.socket("stream") + const address = uniqAddress("tcp") + + stream.on("message", function(id, msg) { + assert.instanceOf(msg, Buffer) + if (msg.length == 0) return + + const raw_header = String(msg).split("\r\n") + const method = raw_header[0].split(" ")[0] + assert.equal(method, "GET") + + //finding an HTTP GET method, prepare HTTP response for TCP socket + const httpProtocolString = "HTTP/1.0 200 OK\r\n" //status code + + "Content-Type: text/html\r\n" //headers + + "\r\n" + + "" //response body + + "" //make it xml, json, html or something else + + "" + + "" + + "" + + "

derpin over protocols

" + + "" + + "" + + //zmq streaming prefixed by envelope"s routing identifier + stream.send([id, httpProtocolString]) + }) + + stream.bind(address, err => { + if (err) throw err + + //send non-peer request to zmq, like an http GET method with URI path + http.get(address.replace("tcp:", "http:") + "/aRandomRequestPath", function(httpMsg) { + assert.equal(httpMsg.socket._readableState.reading, false) + + httpMsg.on("data", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "" + + "" + + "

derpin over protocols

" + + "" + + "") + stream.close() + done() + }) + }) + }) + }) + }) +} diff --git a/test/unit/compat/socket-test.js b/test/unit/compat/socket-test.js new file mode 100644 index 00000000..24625b82 --- /dev/null +++ b/test/unit/compat/socket-test.js @@ -0,0 +1,54 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + + describe("compat socket", function() { + let sock + + beforeEach(function() { + sock = zmq.socket("req") + }) + + afterEach(function() { + sock.close() + }) + + it("should alias socket", function() { + assert.equal(zmq.createSocket, zmq.socket) + }) + + it("should include type and close", function() { + assert.equal(sock.type, "req") + assert.typeOf(sock.close, "function") + }) + + it("should use socketopt", function() { + assert.notEqual(sock.getsockopt(zmq.ZMQ_BACKLOG), 75) + assert.equal(sock.setsockopt(zmq.ZMQ_BACKLOG, 75), sock) + assert.equal(sock.getsockopt(zmq.ZMQ_BACKLOG), 75) + sock.setsockopt(zmq.ZMQ_BACKLOG, 100) + }) + + it("should use socketopt with sugar", function() { + assert.notEqual(sock.getsockopt("backlog"), 75) + assert.equal(sock.setsockopt("backlog", 75), sock) + assert.equal(sock.getsockopt("backlog"), 75) + + assert.typeOf(sock.backlog, "number") + assert.notEqual(sock.backlog, 50) + sock.backlog = 50 + assert.equal(sock.backlog, 50) + }) + + it("should close", function() { + sock.close() + assert.equal(sock.closed, true) + }) + + it("should support options", function() { + sock.close() + sock = zmq.socket("req", {backlog: 30}) + assert.equal(sock.getsockopt("backlog"), 30) + }) + }) +} diff --git a/test/unit/compat/socket-unbind-test.js b/test/unit/compat/socket-unbind-test.js new file mode 100644 index 00000000..c583a449 --- /dev/null +++ b/test/unit/compat/socket-unbind-test.js @@ -0,0 +1,69 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const semver = require("semver") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + /* TODO: This test regularly hangs. */ + for (const proto of testProtos("tcp")) { + describe(`compat socket with ${proto} unbind`, function() { + beforeEach(function() { + /* Seems < 4.2 is affected by https://github.com/zeromq/libzmq/issues/1583 */ + if (semver.satisfies(zmq.version, "< 4.2")) this.skip() + }) + + let sockA, sockB, sockC + + beforeEach(function() { + sockA = zmq.socket("dealer", {linger: 0}) + sockB = zmq.socket("dealer", {linger: 0}) + sockC = zmq.socket("dealer", {linger: 0}) + }) + + afterEach(function() { + sockA.close() + sockB.close() + sockC.close() + }) + + it("should be able to unbind", function(done) { + const address1 = uniqAddress(proto) + const address2 = uniqAddress(proto) + + let msgCount = 0 + sockA.bind(address1, async err => { + if (err) throw err + sockA.bind(address2, async err => { + if (err) throw err + sockB.connect(address1) + sockC.connect(address2) + sockB.send("Hello from sockB.") + sockC.send("Hello from sockC.") + }) + }) + + sockA.on("unbind", async function(addr) { + if (addr === address1) { + sockB.send("Error from sockB.") + sockC.send("Messsage from sockC.") + sockC.send("Final message from sockC.") + } + }) + + sockA.on("message", async function(msg) { + msgCount++ + if (msg.toString() === "Hello from sockB.") { + sockA.unbind(address1, err => { + if (err) throw err + }) + } else if (msg.toString() === "Final message from sockC.") { + assert.equal(msgCount, 4) + done() + } else if (msg.toString() === "Error from sockB.") { + throw Error("sockB should have been unbound") + } + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-xpub-xsub-test.js b/test/unit/compat/socket-xpub-xsub-test.js new file mode 100644 index 00000000..76d7afcd --- /dev/null +++ b/test/unit/compat/socket-xpub-xsub-test.js @@ -0,0 +1,95 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} xpub-xsub`, function() { + it("should support pub-sub tracing and filtering", function(done) { + let n = 0 + let m = 0 + const pub = zmq.socket("pub") + const sub = zmq.socket("sub") + const xpub = zmq.socket("xpub") + const xsub = zmq.socket("xsub") + + const address1 = uniqAddress(proto) + const address2 = uniqAddress(proto) + + pub.bind(address1, err => { + if (err) throw err + xsub.connect(address1) + + xpub.bind(address2, err => { + if (err) throw err + sub.connect(address2) + + xsub.on("message", function(msg) { + xpub.send(msg) // Forward message using the xpub so subscribers can receive it + }) + + xpub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + + const type = msg[0] === 0 ? "unsubscribe" : "subscribe" + const channel = msg.slice(1).toString() + + switch (type) { + case "subscribe": + switch (m++) { + case 0: + assert.equal(channel, "js") + break + case 1: + assert.equal(channel, "luna") + break + } + break + case "unsubscribe": + switch (m++) { + case 2: + assert.equal(channel, "luna") + sub.close() + pub.close() + xsub.close() + xpub.close() + done() + break + } + break + } + + xsub.send(msg) // Forward message using the xsub so the publisher knows it has a subscriber + }) + + sub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + switch (n++) { + case 0: + assert.equal(msg.toString(), "js is cool") + break + case 1: + assert.equal(msg.toString(), "luna is cool too") + break + } + }) + + sub.subscribe("js") + sub.subscribe("luna") + + setTimeout(() => { + pub.send("js is cool") + pub.send("ruby is meh") + pub.send("py is pretty cool") + pub.send("luna is cool too") + }, 15) + + setTimeout(() => { + sub.unsubscribe("luna") + }, 15) + }) + }) + }) + }) + } +} diff --git a/test/unit/compat/socket-zap-test.js b/test/unit/compat/socket-zap-test.js new file mode 100644 index 00000000..2c6c4551 --- /dev/null +++ b/test/unit/compat/socket-zap-test.js @@ -0,0 +1,173 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const semver = require("semver") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + function start() { + const zap = zmq.socket("router") + + zap.on("message", function() { + const data = Array.prototype.slice.call(arguments) + + if (!data || !data.length) throw new Error("Invalid ZAP request") + + const returnPath = [] + let frame = data.shift() + while (frame && (frame.length != 0)) { + returnPath.push(frame) + frame = data.shift() + } + + returnPath.push(frame) + + if (data.length < 6) throw new Error("Invalid ZAP request") + + const zapReq = { + version: data.shift(), + requestId: data.shift(), + domain: Buffer.from(data.shift()).toString("utf8"), + address: Buffer.from(data.shift()).toString("utf8"), + identity: Buffer.from(data.shift()).toString("utf8"), + mechanism: Buffer.from(data.shift()).toString("utf8"), + credentials: data.slice(0) + } + + zap.send(returnPath.concat([ + zapReq.version, + zapReq.requestId, + Buffer.from("200", "utf8"), + Buffer.from("OK", "utf8"), + Buffer.alloc(0), + Buffer.alloc(0) + ])) + }) + + return new Promise((resolve, reject) => { + zap.bind("inproc://zeromq.zap.01", err => { + if (err) return reject(err) + resolve(zap) + }) + }) + } + + for (const proto of testProtos("tcp", "inproc")) { + describe(`compat socket with ${proto} zap`, function() { + let zapSocket, rep, req + + before(async function() { + zapSocket = await start() + }) + + after(async function() { + zapSocket.close() + await new Promise(resolve => setTimeout(resolve, 15)) + }) + + beforeEach(function() { + /* Since ZAP uses inproc transport, it does not work reliably. */ + if (semver.satisfies(zmq.version, "< 4.2")) this.skip() + + rep = zmq.socket("rep") + req = zmq.socket("req") + }) + + afterEach(function() { + req.close() + rep.close() + }) + + it("should support curve", function(done) { + if (!zmq.capability.curve) this.skip() + + const address = uniqAddress(proto) + const serverPublicKey = Buffer.from("7f188e5244b02bf497b86de417515cf4d4053ce4eb977aee91a55354655ec33a", "hex") + const serverPrivateKey = Buffer.from("1f5d3873472f95e11f4723d858aaf0919ab1fb402cb3097742c606e61dd0d7d8", "hex") + const clientPublicKey = Buffer.from("ea1cc8bd7c8af65497d43fc21dbec6560c5e7b61bcfdcbd2b0dfacf0b4c38d45", "hex") + const clientPrivateKey = Buffer.from("83f99afacfab052406e5f421612568034e85f4c8182a1c92671e83dca669d31d", "hex") + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + rep.zap_domain = "test" + rep.curve_server = 1 + rep.curve_secretkey = serverPrivateKey + assert.equal(rep.mechanism, 2) + + rep.bind(address, err => { + if (err) throw err + req.curve_serverkey = serverPublicKey + req.curve_publickey = clientPublicKey + req.curve_secretkey = clientPrivateKey + assert.equal(req.mechanism, 2) + + req.connect(address) + req.send("hello") + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + done() + }) + }) + + }) + + it("should support null", function(done) { + const address = uniqAddress(proto) + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + rep.zap_domain = "test" + assert.equal(rep.mechanism, 0) + + rep.bind(address, err => { + if (err) throw err + assert.equal(req.mechanism, 0) + req.connect(address) + req.send("hello") + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + done() + }) + }) + }) + + it("should support plain", function(done) { + const address = uniqAddress(proto) + + rep.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "hello") + rep.send("world") + }) + + rep.zap_domain = "test" + rep.plain_server = 1 + assert.equal(rep.mechanism, 1) + + rep.bind(address, err => { + if (err) throw err + req.plain_username = "user" + req.plain_password = "pass" + assert.equal(req.mechanism, 1) + + req.connect(address) + req.send("hello") + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "world") + done() + }) + }) + }) + }) + } +} diff --git a/test/unit/compat/zmq-proxy-push-pull-test.js b/test/unit/compat/zmq-proxy-push-pull-test.js new file mode 100644 index 00000000..12003be9 --- /dev/null +++ b/test/unit/compat/zmq-proxy-push-pull-test.js @@ -0,0 +1,96 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp")) { + describe(`compat proxy with ${proto} push-pull`, function() { + const sockets = [] + + afterEach(function() { + while (sockets.length) { + sockets.pop().close() + } + }) + + it("should proxy push-pull connected to pull-push", function(done) { + const frontendAddr = uniqAddress(proto) + const backendAddr = uniqAddress(proto) + + const frontend = zmq.socket("pull") + const backend = zmq.socket("push") + + const pull = zmq.socket("pull") + const push = zmq.socket("push") + + frontend.bind(frontendAddr, err => { + if (err) throw err + backend.bind(backendAddr, err => { + if (err) throw err + push.connect(frontendAddr) + pull.connect(backendAddr) + sockets.push(frontend, backend, push, pull) + + pull.on("message", msg => { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo") + done() + }) + + setTimeout(() => push.send("foo"), 15) + zmq.proxy(frontend, backend) + }) + }) + }) + + it("should proxy pull-push connected to push-pull with capture", function(done) { + const frontendAddr = uniqAddress(proto) + const backendAddr = uniqAddress(proto) + const captureAddr = uniqAddress(proto) + + const frontend = zmq.socket("push") + const backend = zmq.socket("pull") + + const capture = zmq.socket("pub") + const capSub = zmq.socket("sub") + + const pull = zmq.socket("pull") + const push = zmq.socket("push") + sockets.push(frontend, backend, push, pull, capture, capSub) + + frontend.bind(frontendAddr, err => { + if (err) throw err + backend.bind(backendAddr, err => { + if (err) throw err + capture.bind(captureAddr, err => { + if (err) throw err + pull.connect(frontendAddr) + push.connect(backendAddr) + capSub.connect(captureAddr) + + let counter = 2 + + pull.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo") + + if (--counter == 0) done() + }) + + capSub.subscribe("") + capSub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo") + + if (--counter == 0) done() + }) + + setTimeout(() => push.send("foo"), 15) + zmq.proxy(frontend, backend, capture) + }) + }) + }) + }) + }) + } +} diff --git a/test/unit/compat/zmq-proxy-router-dealer-test.js b/test/unit/compat/zmq-proxy-router-dealer-test.js new file mode 100644 index 00000000..1084501a --- /dev/null +++ b/test/unit/compat/zmq-proxy-router-dealer-test.js @@ -0,0 +1,103 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp")) { + describe(`compat proxy with ${proto} router-dealer`, function() { + const sockets = [] + + afterEach(function() { + while (sockets.length) { + sockets.pop().close() + } + }) + + it("should proxy req-rep connected over router-dealer", function(done) { + const frontendAddr = uniqAddress(proto) + const backendAddr = uniqAddress(proto) + + const frontend = zmq.socket("router") + const backend = zmq.socket("dealer") + + const rep = zmq.socket("rep") + const req = zmq.socket("req") + + frontend.bind(frontendAddr, err => { + if (err) throw err + backend.bind(backendAddr, err => { + if (err) throw err + + req.connect(frontendAddr) + rep.connect(backendAddr) + sockets.push(frontend, backend, req, rep) + + req.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo bar") + done() + }) + + rep.on("message", function(msg) { + rep.send(msg + " bar") + }) + + setTimeout(() => req.send("foo"), 15) + zmq.proxy(frontend, backend) + }) + }) + }) + + it("should proxy rep-req connections with capture", function(done) { + const frontendAddr = uniqAddress(proto) + const backendAddr = uniqAddress(proto) + const captureAddr = uniqAddress(proto) + + const frontend = zmq.socket("router") + const backend = zmq.socket("dealer") + + const rep = zmq.socket("rep") + const req = zmq.socket("req") + + const capture = zmq.socket("pub") + const capSub = zmq.socket("sub") + + frontend.bind(frontendAddr, err => { + if (err) throw err + backend.bind(backendAddr, err => { + if (err) throw err + capture.bind(captureAddr, err => { + if (err) throw err + + req.connect(frontendAddr) + rep.connect(backendAddr) + capSub.connect(captureAddr) + capSub.subscribe("") + sockets.push(frontend, backend, req, rep, capture, capSub) + + let counter = 2 + + req.on("message", function(msg) { + if (--counter == 0) done() + }) + + rep.on("message", function(msg) { + rep.send(msg + " bar") + }) + + capSub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo bar") + + if (--counter == 0) done() + }) + + setTimeout(() => req.send("foo"), 15) + zmq.proxy(frontend, backend, capture) + }) + }) + }) + }) + }) + } +} diff --git a/test/unit/compat/zmq-proxy-test.js b/test/unit/compat/zmq-proxy-test.js new file mode 100644 index 00000000..7f893d71 --- /dev/null +++ b/test/unit/compat/zmq-proxy-test.js @@ -0,0 +1,10 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + + describe("compat proxy", function() { + it("should be a function off the module namespace", function() { + assert.typeOf(zmq.proxy, "function") + }) + }) +} diff --git a/test/unit/compat/zmq-proxy-xpub-xsub-test.js b/test/unit/compat/zmq-proxy-xpub-xsub-test.js new file mode 100644 index 00000000..c687f77d --- /dev/null +++ b/test/unit/compat/zmq-proxy-xpub-xsub-test.js @@ -0,0 +1,116 @@ +if (process.env.INCLUDE_COMPAT_TESTS) { + const zmq = require("./load") + const {assert} = require("chai") + const {testProtos, uniqAddress} = require("../helpers") + + for (const proto of testProtos("tcp")) { + describe(`compat proxy with ${proto} xpub-xsub`, function() { + const sockets = [] + + afterEach(function() { + while (sockets.length) { + sockets.pop().close() + } + }) + + it("should proxy pub-sub connected to xpub-xsub", function(done) { + const frontendAddr = uniqAddress(proto) + const backendAddr = uniqAddress(proto) + + const frontend = zmq.socket("xpub") + const backend = zmq.socket("xsub") + + const sub = zmq.socket("sub") + const pub = zmq.socket("pub") + sockets.push(frontend, backend, sub, pub) + + sub.subscribe("") + sub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo") + done() + }) + + frontend.bind(frontendAddr, err => { + if (err) throw err + backend.bind(backendAddr, err => { + if (err) throw err + + sub.connect(frontendAddr) + pub.connect(backendAddr) + + setTimeout(() => pub.send("foo"), 15) + zmq.proxy(frontend, backend) + }) + }) + }) + + it("should proxy connections with capture", function(done) { + const frontendAddr = uniqAddress(proto) + const backendAddr = uniqAddress(proto) + const captureAddr = uniqAddress(proto) + + const frontend = zmq.socket("xpub") + const backend = zmq.socket("xsub") + + const capture = zmq.socket("pub") + const capSub = zmq.socket("sub") + + const sub = zmq.socket("sub") + const pub = zmq.socket("pub") + sockets.push(frontend, backend, sub, pub, capture, capSub) + + let counter = 2 + + sub.subscribe("") + sub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo") + + if (--counter == 0) done() + }) + + capSub.subscribe("") + capSub.on("message", function(msg) { + assert.instanceOf(msg, Buffer) + assert.equal(msg.toString(), "foo") + + if (--counter == 0) done() + }) + + capture.bind(captureAddr, err => { + if (err) throw err + frontend.bind(frontendAddr, err => { + if (err) throw err + backend.bind(backendAddr, err => { + if (err) throw err + + pub.connect(backendAddr) + sub.connect(frontendAddr) + capSub.connect(captureAddr) + + setTimeout(() => pub.send("foo"), 15) + zmq.proxy(frontend, backend, capture) + }) + }) + }) + }) + + it("should throw an error if the order is wrong", function() { + const frontend = zmq.socket("xpub") + const backend = zmq.socket("xsub") + + sockets.push(frontend, backend) + + try { + zmq.proxy(backend, frontend) + } catch (err) { + assert.include([ + "wrong socket order to proxy", + "This socket type order is not supported in compatibility mode", + ], err.message) + } + }) + }) + } +} diff --git a/test/unit/context-construction-test.ts b/test/unit/context-construction-test.ts new file mode 100644 index 00000000..149fa52c --- /dev/null +++ b/test/unit/context-construction-test.ts @@ -0,0 +1,62 @@ +import * as zmq from "../../src" + +import {assert} from "chai" + +describe("context construction", function() { + afterEach(function() { + global.gc() + }) + + it("should throw if called as function", function() { + assert.throws( + () => (zmq.Context as any)(), + TypeError, + "Class constructors cannot be invoked without 'new'", + ) + }) + + it("should throw with wrong options argument", function() { + assert.throws( + () => new (zmq.Context as any)(1), + TypeError, + "Options must be an object", + ) + }) + + it("should throw with too many arguments", function() { + assert.throws( + () => new (zmq.Context as any)({}, 2), + TypeError, + "Expected 1 argument", + ) + }) + + it("should set option", function() { + const context = new zmq.Context({ioThreads: 5}) + assert.equal(context.ioThreads, 5) + }) + + it("should throw with invalid option value", function() { + assert.throws( + () => new (zmq.Context as any)({ioThreads: "hello"}), + TypeError, + "Option value must be a number", + ) + }) + + it("should throw with readonly option", function() { + assert.throws( + () => new (zmq.Context as any)({maxSocketsLimit: 1}), + TypeError, + "Cannot set property maxSocketsLimit of # which has only a getter", + ) + }) + + it("should throw with unknown option", function() { + assert.throws( + () => new (zmq.Context as any)({doesNotExist: 1}), + TypeError, + "Cannot add property doesNotExist, object is not extensible", + ) + }) +}) diff --git a/test/unit/context-options-test.ts b/test/unit/context-options-test.ts new file mode 100644 index 00000000..0cdfd762 --- /dev/null +++ b/test/unit/context-options-test.ts @@ -0,0 +1,41 @@ +import * as zmq from "../../src" + +import {assert} from "chai" + +describe("context options", function() { + afterEach(function() { + global.gc() + }) + + it("should set and get bool socket option", function() { + const context = new zmq.Context + assert.equal(context.ipv6, false) + context.ipv6 = true + assert.equal(context.ipv6, true) + }) + + it("should set and get int socket option", function() { + const context = new zmq.Context + assert.equal(context.ioThreads, 1) + context.ioThreads = 75 + assert.equal(context.ioThreads, 75) + }) + + it("should throw for readonly option", function() { + const context = new zmq.Context + assert.throws( + () => (context as any).maxSocketsLimit = 1, + TypeError, + "Cannot set property maxSocketsLimit of # which has only a getter", + ) + }) + + it("should throw for unknown option", function() { + const context = new zmq.Context + assert.throws( + () => (context as any).doesNotExist = 1, + TypeError, + "Cannot add property doesNotExist, object is not extensible", + ) + }) +}) diff --git a/test/unit/context-process-exit-test.ts b/test/unit/context-process-exit-test.ts new file mode 100644 index 00000000..52a99f74 --- /dev/null +++ b/test/unit/context-process-exit-test.ts @@ -0,0 +1,132 @@ +/* tslint:disable: no-unused-expression */ +import {assert} from "chai" +import {spawn} from "child_process" + +/* This file is in JavaScript instead of TypeScript because most code is + being evaluated with toString() and executed in a sub-process. */ +describe("context process exit", function() { + describe("with default context", function() { + it("should occur when sockets are closed", async function() { + this.slow(200) + await ensureExit(function() { + const zmq = require(".") + const socket1 = new zmq.Dealer + socket1.close() + const socket2 = new zmq.Router + socket2.close() + }) + }) + + it("should occur when sockets are not closed", async function() { + this.slow(200) + await ensureExit(function() { + const zmq = require(".") + const socket1 = new zmq.Dealer + const socket2 = new zmq.Router + }) + }) + + it("should not occur when sockets are open and polling", async function() { + this.slow(750) + await ensureNoExit(function() { + const zmq = require(".") + const socket1 = new zmq.Dealer + socket1.connect("inproc://foo") + socket1.receive() + }) + }) + }) + + describe("with custom context", function() { + it("should occur when sockets are closed", async function() { + this.slow(200) + await ensureExit(function() { + const zmq = require(".") + const context = new zmq.Context + const socket1 = new zmq.Dealer({context}) + socket1.close() + const socket2 = new zmq.Router({context}) + socket2.close() + }) + }) + + it("should occur when sockets are closed and context is gced", async function() { + this.slow(200) + await ensureExit(function() { + const zmq = require(".") + function run() { + const context = new zmq.Context + const socket1 = new zmq.Dealer({context}) + socket1.close() + const socket2 = new zmq.Router({context}) + socket2.close() + } + + run() + global.gc() + }) + }) + + it("should occur when sockets are not closed", async function() { + this.slow(200) + await ensureExit(function() { + const zmq = require(".") + const context = new zmq.Context + const socket1 = new zmq.Dealer({context}) + const socket2 = new zmq.Router({context}) + }) + }) + + it("should not occur when sockets are open and polling", async function() { + this.slow(750) + await ensureNoExit(function() { + const zmq = require(".") + const context = new zmq.Context + const socket1 = new zmq.Dealer({context}) + socket1.connect("inproc://foo") + socket1.receive() + }) + }) + }) +}) + +async function ensureExit(fn: () => void): Promise { + return new Promise((resolve) => { + const child = spawn(process.argv[0], ["--expose_gc"]) + child.stdin.write(`(${fn})()`) + child.stdin.end() + + child.stdout.on("data", (data: Buffer) => console.log(data.toString())) + child.stderr.on("data", (data: Buffer) => console.error(data.toString())) + + child.on("close", (code: number) => { + assert.equal(code, 0) + resolve() + }) + + setTimeout(() => { + resolve() + child.kill() + }, 2000) + }) +} + +async function ensureNoExit(fn: () => void): Promise { + return new Promise((resolve, reject) => { + const child = spawn(process.argv[0], ["--expose_gc"]) + child.stdin.write(`(${fn})()`) + child.stdin.end() + + child.stdout.on("data", (data: Buffer) => console.log(data.toString())) + child.stderr.on("data", (data: Buffer) => console.error(data.toString())) + + child.on("close", (code: number) => { + reject(new Error(`Exit with code ${code}`)) + }) + + setTimeout(() => { + resolve() + child.kill() + }, 500) + }) +} diff --git a/test/unit/helpers.ts b/test/unit/helpers.ts new file mode 100644 index 00000000..c503d492 --- /dev/null +++ b/test/unit/helpers.ts @@ -0,0 +1,33 @@ +import * as semver from "semver" + +import * as zmq from "../../src" + +/* Windows cannot bind on a ports just above 1014; start higher to be safe. */ +let seq = 5000 + +export function uniqAddress(proto: string) { + const id = seq++ + switch (proto) { + case "ipc": + return `${proto}://${__dirname}/../../tmp/${proto}-${id}` + case "tcp": + case "udp": + return `${proto}://127.0.0.1:${id}` + default: + return `${proto}://${proto}-${id}` + } +} + +export function testProtos(...requested: string[]) { + const set = new Set(requested) + + /* Do not test with ipc if unsupported. */ + if (!zmq.capability.ipc) set.delete("ipc") + + /* Only test inproc with version 4.2+, earlier versions are unreliable. */ + if (semver.satisfies(zmq.version, "< 4.2")) set.delete("inproc") + + if (!set.size) console.error("Warning: test protocol set is empty") + + return [...set] +} diff --git a/test/unit/proxy-construction-test.ts b/test/unit/proxy-construction-test.ts new file mode 100644 index 00000000..6744ad54 --- /dev/null +++ b/test/unit/proxy-construction-test.ts @@ -0,0 +1,55 @@ +import * as semver from "semver" +import * as zmq from "../../src" + +import {assert} from "chai" + +describe("proxy construction", function() { + beforeEach(function() { + /* ZMQ < 4.0.5 has no steerable proxy support. */ + if (semver.satisfies(zmq.version, "< 4.0.5")) this.skip() + }) + + afterEach(function() { + global.gc() + }) + + describe("with constructor", function() { + it("should throw if called as function", function() { + assert.throws( + () => (zmq.Proxy as any)(), + TypeError, + "Class constructors cannot be invoked without 'new'", + ) + }) + + it("should throw with too few arguments", function() { + assert.throws( + () => new (zmq.Proxy as any), + TypeError, + "Front-end must be a socket object", + ) + }) + + it("should throw with too many arguments", function() { + assert.throws( + () => new (zmq.Proxy as any)(new zmq.Dealer, new zmq.Dealer, new zmq.Dealer), + TypeError, + "Expected 2 arguments", + ) + }) + + it("should throw with invalid socket", function() { + try { + /* tslint:disable-next-line: no-unused-expression */ + new (zmq.Proxy as any)({}, {}) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.oneOf(err.message, [ + "Invalid pointer passed as argument", /* before 8.7 */ + "Invalid argument", /* as of 8.7 */ + ]) + } + }) + }) +}) diff --git a/test/unit/proxy-router-dealer-test.ts b/test/unit/proxy-router-dealer-test.ts new file mode 100644 index 00000000..c42bebd0 --- /dev/null +++ b/test/unit/proxy-router-dealer-test.ts @@ -0,0 +1,96 @@ +import * as semver from "semver" +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`proxy with ${proto} router/dealer`, function() { + let proxy: zmq.Proxy + + let frontAddress: string + let backAddress: string + + let req: zmq.Request + let rep: zmq.Reply + + beforeEach(async function() { + /* ZMQ < 4.0.5 has no steerable proxy support. */ + if (semver.satisfies(zmq.version, "< 4.0.5")) this.skip() + + proxy = new zmq.Proxy(new zmq.Router, new zmq.Dealer) + + frontAddress = uniqAddress(proto) + backAddress = uniqAddress(proto) + + req = new zmq.Request + rep = new zmq.Reply + }) + + afterEach(function() { + /* Closing proxy sockets is only necessary if run() fails. */ + proxy.frontEnd.close() + proxy.backEnd.close() + + req.close() + rep.close() + global.gc() + }) + + describe("run", function() { + it("should proxy messages", async function() { + /* REQ -> foo -> ROUTER <-> DEALER -> foo -> REP + <- foo <- <- foo <- + -> bar -> -> bar -> + <- bar <- <- bar <- + pause + resume + -> baz -> -> baz -> + <- baz <- <- baz <- + -> qux -> -> qux -> + <- qux <- <- qux <- + */ + + await proxy.frontEnd.bind(frontAddress) + await proxy.backEnd.bind(backAddress) + + const done = proxy.run() + + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + await req.connect(frontAddress) + await rep.connect(backAddress) + + const echo = async () => { + for await (const msg of rep) { + await rep.send(msg) + } + } + + const send = async () => { + for (const msg of messages) { + if (received.length === 2) { + proxy.pause() + proxy.resume() + } + + await req.send(Buffer.from(msg)) + + const [res] = await req.receive() + received.push(res.toString()) + if (received.length === messages.length) break + } + + rep.close() + } + + await Promise.all([echo(), send()]) + assert.deepEqual(received, messages) + + proxy.terminate() + await done + }) + }) + }) +} diff --git a/test/unit/proxy-run-test.ts b/test/unit/proxy-run-test.ts new file mode 100644 index 00000000..7f3b9d8e --- /dev/null +++ b/test/unit/proxy-run-test.ts @@ -0,0 +1,78 @@ +import * as semver from "semver" +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`proxy with ${proto} run`, function() { + let proxy: zmq.Proxy + + beforeEach(async function() { + /* ZMQ < 4.0.5 has no steerable proxy support. */ + if (semver.satisfies(zmq.version, "< 4.0.5")) this.skip() + + proxy = new zmq.Proxy(new zmq.Router, new zmq.Dealer) + }) + + afterEach(function() { + proxy.frontEnd.close() + proxy.backEnd.close() + global.gc() + }) + + describe("run", function() { + it("should fail if front end is not bound or connected", async function() { + await proxy.backEnd.bind(uniqAddress(proto)) + + try { + await proxy.run() + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Front-end socket must be bound or connected") + } + }) + + it("should fail if front end is not open", async function() { + await proxy.frontEnd.bind(uniqAddress(proto)) + await proxy.backEnd.bind(uniqAddress(proto)) + proxy.frontEnd.close() + + try { + await proxy.run() + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Front-end socket must be bound or connected") + } + }) + + it("should fail if back end is not bound or connected", async function() { + await proxy.frontEnd.bind(uniqAddress(proto)) + + try { + await proxy.run() + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Back-end socket must be bound or connected") + } + }) + + it("should fail if back end is not open", async function() { + await proxy.frontEnd.bind(uniqAddress(proto)) + await proxy.backEnd.bind(uniqAddress(proto)) + proxy.backEnd.close() + + try { + await proxy.run() + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Back-end socket must be bound or connected") + } + }) + }) + }) +} diff --git a/test/unit/socket-bind-unbind-test.ts b/test/unit/socket-bind-unbind-test.ts new file mode 100644 index 00000000..9aaaad1b --- /dev/null +++ b/test/unit/socket-bind-unbind-test.ts @@ -0,0 +1,137 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} bind/unbind`, function() { + let sock: zmq.Dealer + + beforeEach(function() { + sock = new zmq.Dealer + }) + + afterEach(function() { + sock.close() + global.gc() + }) + + describe("bind", function() { + it("should resolve", async function() { + await sock.bind(uniqAddress(proto)) + assert.ok(true) + }) + + it("should throw error if not bound to endpoint", async function() { + const address = uniqAddress(proto) + try { + await sock.unbind(address) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "No such endpoint") + assert.equal(err.code, "ENOENT") + assert.typeOf(err.errno, "number") + assert.equal(err.address, address) + } + }) + + it("should throw error for invalid uri", async function() { + try { + await sock.bind("foo-bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Invalid argument") + assert.equal(err.code, "EINVAL") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo-bar") + } + }) + + it("should throw error for invalid protocol", async function() { + try { + await sock.bind("foo://bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Protocol not supported") + assert.equal(err.code, "EPROTONOSUPPORT") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo://bar") + } + }) + + it("should fail during other bind", async function() { + let promise + try { + promise = sock.bind(uniqAddress(proto)) + await sock.bind(uniqAddress(proto)) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, + "Socket is blocked by async operation (e.g. bind/unbind)", + ) + assert.equal(err.code, "EBUSY") + assert.typeOf(err.errno, "number") + } + await promise + }) + }) + + describe("unbind", function() { + it("should unbind", async function() { + const address = uniqAddress(proto) + await sock.bind(address) + await sock.unbind(address) + assert.ok(true) + }) + + it("should throw error for invalid uri", async function() { + try { + await sock.unbind("foo-bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Invalid argument") + assert.equal(err.code, "EINVAL") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo-bar") + } + }) + + it("should throw error for invalid protocol", async function() { + try { + await sock.unbind("foo://bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Protocol not supported") + assert.equal(err.code, "EPROTONOSUPPORT") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo://bar") + } + }) + + it("should fail during other unbind", async function() { + let promise + const address = uniqAddress(proto) + await sock.bind(address) + try { + promise = sock.unbind(address) + await sock.unbind(address) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, + "Socket is blocked by async operation (e.g. bind/unbind)", + ) + assert.equal(err.code, "EBUSY") + assert.typeOf(err.errno, "number") + } + await promise + }) + }) + }) +} diff --git a/test/unit/socket-close-test.ts b/test/unit/socket-close-test.ts new file mode 100644 index 00000000..00af1492 --- /dev/null +++ b/test/unit/socket-close-test.ts @@ -0,0 +1,150 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} close`, function() { + let sock: zmq.Dealer + + beforeEach(function() { + sock = new zmq.Dealer + }) + + afterEach(function() { + sock.close() + global.gc() + }) + + describe("with explicit call", function() { + it("should close socket", function() { + assert.equal(sock.closed, false) + sock.close() + assert.equal(sock.closed, true) + }) + + it("should close socket and cancel send", async function() { + assert.equal(sock.closed, false) + const promise = sock.send(Buffer.from("foo")) + sock.close() + assert.equal(sock.closed, true) + try { + await promise + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Socket temporarily unavailable") + assert.equal(err.code, "EAGAIN") + assert.typeOf(err.errno, "number") + } + }) + + it("should close socket and cancel receive", async function() { + assert.equal(sock.closed, false) + const promise = sock.receive() + sock.close() + assert.equal(sock.closed, true) + try { + await promise + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Socket temporarily unavailable") + assert.equal(err.code, "EAGAIN") + assert.typeOf(err.errno, "number") + } + }) + + it("should close after successful bind", async function() { + const promise = sock.bind(uniqAddress(proto)) + sock.close() + assert.equal(sock.closed, false) + await promise + assert.equal(sock.closed, true) + }) + + it("should close after unsuccessful bind", async function() { + const address = uniqAddress(proto) + await sock.bind(address) + const promise = sock.bind(address) + sock.close() + assert.equal(sock.closed, false) + try { + await promise + assert.ok(false) + } catch (err) { /* Ignore */ } + assert.equal(sock.closed, true) + }) + + it("should close after successful unbind", async function() { + const address = uniqAddress(proto) + await sock.bind(address) + const promise = sock.unbind(address) + sock.close() + assert.equal(sock.closed, false) + await promise + assert.equal(sock.closed, true) + }) + + it("should close after unsuccessful unbind", async function() { + const address = uniqAddress(proto) + const promise = sock.unbind(address) + sock.close() + assert.equal(sock.closed, false) + try { + await promise + assert.ok(false) + } catch (err) { /* Ignore */ } + assert.equal(sock.closed, true) + }) + + it("should release reference to context", async function() { + this.slow(200) + + const weak = require("weak-napi") + + let released = false + const task = async () => { + let context: zmq.Context|undefined = new zmq.Context + const socket = new zmq.Dealer({context, linger: 0}) + + weak(context, () => {released = true}) + context = undefined + + global.gc() + socket.connect(uniqAddress(proto)) + await socket.send(Buffer.from("foo")) + socket.close() + } + + await task() + global.gc() + await new Promise((resolve) => setTimeout(resolve, 5)) + assert.equal(released, true) + }) + }) + + describe("in gc finalizer", function() { + it("should release reference to context", async function() { + this.slow(200) + + const weak = require("weak-napi") + + let released = false + const task = async () => { + let context: zmq.Context|undefined = new zmq.Context + + /* tslint:disable-next-line: no-unused-expression */ + new zmq.Dealer({context, linger: 0}) + + weak(context, () => {released = true}) + context = undefined + global.gc() + } + + await task() + global.gc() + await new Promise((resolve) => setTimeout(resolve, 5)) + assert.equal(released, true) + }) + }) + }) +} diff --git a/test/unit/socket-connect-disconnect-test.ts b/test/unit/socket-connect-disconnect-test.ts new file mode 100644 index 00000000..6ac2cace --- /dev/null +++ b/test/unit/socket-connect-disconnect-test.ts @@ -0,0 +1,98 @@ +import * as semver from "semver" +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} connect/disconnect`, function() { + let sock: zmq.Dealer | zmq.Router + + beforeEach(function() { + sock = new zmq.Dealer + }) + + afterEach(function() { + sock.close() + global.gc() + }) + + describe("connect", function() { + it("should throw error for invalid uri", async function() { + try { + await sock.connect("foo-bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Invalid argument") + assert.equal(err.code, "EINVAL") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo-bar") + } + }) + + it("should throw error for invalid protocol", async function() { + try { + await sock.connect("foo://bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Protocol not supported") + assert.equal(err.code, "EPROTONOSUPPORT") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo://bar") + } + }) + + if (semver.satisfies(zmq.version, ">= 4.1")) { + it("should allow setting routing id on router", async function() { + sock = new zmq.Router({mandatory: true, linger: 0}) + await sock.connect(uniqAddress(proto), {routingId: "remoteId"}) + await sock.send(["remoteId", "hi"]) + }) + } + }) + + describe("disconnect", function() { + it("should throw error if not connected to endpoint", async function() { + const address = uniqAddress(proto) + try { + await sock.disconnect(address) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "No such endpoint") + assert.equal(err.code, "ENOENT") + assert.typeOf(err.errno, "number") + assert.equal(err.address, address) + } + }) + + it("should throw error for invalid uri", async function() { + try { + await sock.disconnect("foo-bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Invalid argument") + assert.equal(err.code, "EINVAL") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo-bar") + } + }) + + it("should throw error for invalid protocol", async function() { + try { + await sock.disconnect("foo://bar") + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Protocol not supported") + assert.equal(err.code, "EPROTONOSUPPORT") + assert.typeOf(err.errno, "number") + assert.equal(err.address, "foo://bar") + } + }) + }) + }) +} diff --git a/test/unit/socket-construction-test.ts b/test/unit/socket-construction-test.ts new file mode 100644 index 00000000..ff4b1846 --- /dev/null +++ b/test/unit/socket-construction-test.ts @@ -0,0 +1,189 @@ +import * as zmq from "../../src" + +import {assert} from "chai" + +describe("socket construction", function() { + afterEach(function() { + global.gc() + }) + + describe("with constructor", function() { + it("should throw if called as function", function() { + assert.throws( + () => (zmq.Socket as any)(1, new zmq.Context), + TypeError, + "Class constructors cannot be invoked without 'new'", + ) + }) + + it("should throw with too few arguments", function() { + assert.throws( + () => new (zmq.Socket as any), + TypeError, + "Socket type must be a number", + ) + }) + + it("should throw with too many arguments", function() { + assert.throws( + () => new (zmq.Socket as any)(1, new zmq.Context, 2), + TypeError, + "Expected 2 arguments", + ) + }) + + it("should throw with wrong options argument", function() { + assert.throws( + () => new (zmq.Socket as any)(3, 1), + TypeError, + "Options must be an object", + ) + }) + + it("should throw with wrong type argument", function() { + assert.throws( + () => new (zmq.Socket as any)("foo", new zmq.Context), + TypeError, + "Socket type must be a number", + ) + }) + + it("should throw with wrong type id", function() { + try { + /* tslint:disable-next-line: no-unused-expression */ + new (zmq.Socket as any)(37, new zmq.Context) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Invalid argument") + assert.equal(err.code, "EINVAL") + assert.typeOf(err.errno, "number") + } + }) + + it("should throw with invalid context", function() { + try { + /* tslint:disable-next-line: no-unused-expression */ + new (zmq.Socket as any)(1, {context: {}}) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.oneOf(err.message, [ + "Invalid pointer passed as argument", /* before 8.7 */ + "Invalid argument", /* as of 8.7 */ + ]) + } + }) + + it("should create socket with default context", function() { + class MySocket extends zmq.Socket { + constructor() { super(1) } + } + const sock1 = new MySocket() + const sock2 = new MySocket() + assert.instanceOf(sock1, zmq.Socket) + assert.equal(sock1.context, sock2.context) + }) + + it("should create socket with given context", function() { + class MySocket extends zmq.Socket { + constructor(opts: zmq.SocketOptions) { super(1, opts) } + } + const context = new zmq.Context + const socket = new MySocket({context}) + assert.instanceOf(socket, zmq.Socket) + assert.equal(socket.context, context) + }) + }) + + describe("with child constructor", function() { + it("should throw if called as function", function() { + assert.throws( + () => (zmq.Dealer as any)(), + TypeError, + "Class constructor Dealer cannot be invoked without 'new'", + ) + }) + + it("should create socket with default context", function() { + const sock = new zmq.Dealer + assert.instanceOf(sock, zmq.Dealer) + assert.equal(sock.context, zmq.context) + }) + + it("should create socket with given context", function() { + const ctxt = new zmq.Context + const sock = new zmq.Dealer({context: ctxt}) + assert.instanceOf(sock, zmq.Socket) + assert.equal(sock.context, ctxt) + }) + + it("should set option", function() { + const sock = new zmq.Dealer({recoveryInterval: 5}) + assert.equal(sock.recoveryInterval, 5) + }) + + it("should throw with invalid option value", function() { + assert.throws( + () => new (zmq.Dealer as any)({recoveryInterval: "hello"}), + TypeError, + "Option value must be a number", + ) + }) + + it("should throw with readonly option", function() { + assert.throws( + () => new (zmq.Dealer as any)({securityMechanism: 1}), + TypeError, + "Cannot set property securityMechanism of # which has only a getter", + ) + }) + + it("should throw with unknown option", function() { + assert.throws( + () => new (zmq.Dealer as any)({doesNotExist: 1}), + TypeError, + "Cannot add property doesNotExist, object is not extensible", + ) + }) + + it("should throw with invalid type", function() { + assert.throws( + () => new (zmq.Socket as any)(4591), + Error, + "Invalid argument", + ) + }) + + if (!zmq.capability.draft) { + it("should throw with draft type", function() { + assert.throws( + () => new (zmq.Socket as any)(14), + Error, + "Invalid argument", + ) + }) + } + + it("should throw error on file descriptor limit", async function() { + const context = new zmq.Context({maxSockets: 10}) + const sockets = [] + const n = 10 + + try { + for (let i = 0; i < n; i++) { + sockets.push(new zmq.Dealer({context})) + } + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Too many open file descriptors") + assert.equal(err.code, "EMFILE") + assert.typeOf(err.errno, "number") + } finally { + for (const socket of sockets) { + socket.close() + } + } + }) + }) +}) diff --git a/test/unit/socket-curve-send-receive-test.ts b/test/unit/socket-curve-send-receive-test.ts new file mode 100644 index 00000000..e65b4d0d --- /dev/null +++ b/test/unit/socket-curve-send-receive-test.ts @@ -0,0 +1,56 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} curve send/receive`, function() { + let sockA: zmq.Pair + let sockB: zmq.Pair + + beforeEach(function() { + if (!zmq.capability.curve) this.skip() + + const serverKeypair = zmq.curveKeyPair() + const clientKeypair = zmq.curveKeyPair() + + sockA = new zmq.Pair({ + linger: 0, + curveServer: true, + curvePublicKey: serverKeypair.publicKey, + curveSecretKey: serverKeypair.secretKey, + }) + + sockB = new zmq.Pair({ + linger: 0, + curveServerKey: serverKeypair.publicKey, + curvePublicKey: clientKeypair.publicKey, + curveSecretKey: clientKeypair.secretKey, + }) + }) + + afterEach(function() { + sockA.close() + sockB.close() + global.gc() + }) + + describe("when connected", function() { + beforeEach(async function() { + if (!zmq.capability.curve) this.skip() + + const address = uniqAddress(proto) + await sockB.bind(address) + await sockA.connect(address) + }) + + it("should deliver single string message", async function() { + const sent = "foo" + await sockA.send(sent) + + const recv = await sockB.receive() + assert.deepEqual([sent], recv.map((buf: Buffer) => buf.toString())) + }) + }) + }) +} diff --git a/test/unit/socket-draft-dgram-test.ts b/test/unit/socket-draft-dgram-test.ts new file mode 100644 index 00000000..50671f3a --- /dev/null +++ b/test/unit/socket-draft-dgram-test.ts @@ -0,0 +1,64 @@ +import * as zmq from "../../src" +import * as draft from "../../src/draft" + +import {assert} from "chai" +import {createSocket} from "dgram" +import {testProtos, uniqAddress} from "./helpers" + +if (zmq.capability.draft) { + for (const proto of testProtos("udp")) { + describe(`draft socket with ${proto} dgram`, function() { + let dgram: draft.Datagram + + beforeEach(function() { + dgram = new draft.Datagram + }) + + afterEach(function() { + dgram.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + const messages = ["foo", "bar", "baz", "qux"] + const address = uniqAddress(proto) + const port = parseInt(address.split(":").pop()!, 10) + + await dgram.bind(address) + + const echo = async () => { + for await (const [id, msg] of dgram) { + await dgram.send([id, msg]) + } + } + + const received: string[] = [] + const send = async () => { + for (const msg of messages) { + const client = createSocket("udp4") + await new Promise((resolve) => { + client.on("message", (res) => { + received.push(res.toString()) + client.close() + resolve() + }) + + client.send(msg, port, "localhost") + }) + } + + dgram.close() + } + + await Promise.all([echo(), send()]) + assert.deepEqual(received, messages) + }) + }) + }) + } +} else { + if (process.env.ZMQ_DRAFT) { + throw new Error("Draft API requested but not available at runtime.") + } +} diff --git a/test/unit/socket-draft-radio-dish-test.ts b/test/unit/socket-draft-radio-dish-test.ts new file mode 100644 index 00000000..7b727e12 --- /dev/null +++ b/test/unit/socket-draft-radio-dish-test.ts @@ -0,0 +1,118 @@ +import * as zmq from "../../src" +import * as draft from "../../src/draft" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +if (zmq.capability.draft) { + for (const proto of testProtos("tcp", "ipc", "inproc", "udp")) { + describe(`draft socket with ${proto} radio/dish`, function() { + let radio: draft.Radio + let dish: draft.Dish + + beforeEach(function() { + radio = new draft.Radio + dish = new draft.Dish + }) + + afterEach(function() { + global.gc() + radio.close() + dish.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + /* RADIO -> foo -> DISH + -> bar -> joined all + -> baz -> + -> qux -> + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + + /* Max 15 non-null bytes. */ + const uuid = Buffer.from([ + 0xf6, 0x46, 0x1f, 0x03, 0xd2, 0x0d, 0xc8, 0x66, + 0xe5, 0x5f, 0xf5, 0xa1, 0x65, 0x62, 0xb2, + ]) + + const received: string[] = [] + + dish.join(uuid) + + await dish.bind(address) + await radio.connect(address) + + const send = async () => { + /* Wait briefly before publishing to avoid slow joiner syndrome. */ + await new Promise((resolve) => setTimeout(resolve, 25)) + for (const msg of messages) { + await radio.send(msg, {group: uuid}) + } + } + + const receive = async () => { + for await (const [msg, {group}] of dish) { + assert.instanceOf(msg, Buffer) + assert.instanceOf(group, Buffer) + assert.deepEqual(group, uuid) + received.push(msg.toString()) + if (received.length === messages.length) break + } + } + + await Promise.all([send(), receive()]) + assert.deepEqual(received, messages) + }) + }) + + describe("join/leave", function() { + it("should filter messages", async function() { + /* RADIO -> foo -X DISH + -> bar -> joined "ba" + -> baz -> + -> qux -X + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + /* Everything after null byte should be ignored. */ + dish.join(Buffer.from("fo\x00ba"), Buffer.from("ba\x00fo")) + dish.leave(Buffer.from("fo")) + + await dish.bind(address) + await radio.connect(address) + + const send = async () => { + /* Wait briefly before publishing to avoid slow joiner syndrome. */ + await new Promise((resolve) => setTimeout(resolve, 25)) + for (const msg of messages) { + await radio.send(msg, {group: msg.slice(0, 2)}) + } + } + + const receive = async () => { + for await (const [msg, {group}] of dish) { + assert.instanceOf(msg, Buffer) + assert.deepEqual(group, msg.slice(0, 2)) + received.push(msg.toString()) + if (received.length === 2) break + } + } + + await Promise.all([send(), receive()]) + assert.deepEqual(received, ["bar", "baz"]) + }) + }) + }) + } +} else { + if (process.env.ZMQ_DRAFT) { + throw new Error("Draft API requested but not available at runtime.") + } +} diff --git a/test/unit/socket-draft-scatter-gather-test.ts b/test/unit/socket-draft-scatter-gather-test.ts new file mode 100644 index 00000000..3ac6f6f5 --- /dev/null +++ b/test/unit/socket-draft-scatter-gather-test.ts @@ -0,0 +1,86 @@ +import * as zmq from "../../src" +import * as draft from "../../src/draft" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +if (zmq.capability.draft) { + for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} scatter/gather`, function() { + let scatter: draft.Scatter + let gather: draft.Gather + + beforeEach(function() { + scatter = new draft.Scatter + gather = new draft.Gather + }) + + afterEach(function() { + scatter.close() + gather.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + /* SCATTER -> foo -> GATHER + -> bar -> + -> baz -> + -> qux -> + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + await gather.bind(address) + await scatter.connect(address) + + for (const msg of messages) { + await scatter.send(msg) + } + + for await (const [msg] of gather) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === messages.length) break + } + + assert.deepEqual(received, messages) + }) + + if (proto !== "inproc") { + it("should deliver messages with immediate", async function() { + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + await gather.bind(address) + + scatter.immediate = true + await scatter.connect(address) + + /* Never connected, without immediate: true it would cause lost msgs. */ + await scatter.connect(uniqAddress(proto)) + + for (const msg of messages) { + await scatter.send(msg) + } + + for await (const [msg] of gather) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === messages.length) break + } + + assert.deepEqual(received, messages) + }) + } + }) + }) + } +} else { + if (process.env.ZMQ_DRAFT) { + throw new Error("Draft API requested but not available at runtime.") + } +} diff --git a/test/unit/socket-draft-server-client-test.ts b/test/unit/socket-draft-server-client-test.ts new file mode 100644 index 00000000..3030f52a --- /dev/null +++ b/test/unit/socket-draft-server-client-test.ts @@ -0,0 +1,88 @@ +import * as zmq from "../../src" +import * as draft from "../../src/draft" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +if (zmq.capability.draft) { + for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`draft socket with ${proto} server/client`, function() { + let server: draft.Server + let clientA: draft.Client + let clientB: draft.Client + + beforeEach(function() { + server = new draft.Server + clientA = new draft.Client + clientB = new draft.Client + }) + + afterEach(function() { + server.close() + clientA.close() + clientB.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const receivedA: string[] = [] + const receivedB: string[] = [] + + await server.bind(address) + clientA.connect(address) + clientB.connect(address) + + const echo = async () => { + for await (const [msg, {routingId}] of server) { + assert.typeOf(routingId, "number") + await server.send(msg, {routingId}) + } + } + + const send = async () => { + for (const msg of messages) { + await clientA.send(msg) + await clientB.send(msg) + } + + for await (const msg of clientA) { + receivedA.push(msg.toString()) + if (receivedA.length === messages.length) break + } + + for await (const msg of clientB) { + receivedB.push(msg.toString()) + if (receivedB.length === messages.length) break + } + + server.close() + } + + await Promise.all([echo(), send()]) + assert.deepEqual(receivedA, messages) + assert.deepEqual(receivedB, messages) + }) + + it("should fail with unroutable message", async function() { + try { + await server.send("foo", {routingId: 12345}) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + + assert.equal(err.message, "Host unreachable") + assert.equal(err.code, "EHOSTUNREACH") + assert.typeOf(err.errno, "number") + } + }) + }) + }) + } +} else { + if (process.env.ZMQ_DRAFT) { + throw new Error("Draft API requested but not available at runtime.") + } +} diff --git a/test/unit/socket-events-test.ts b/test/unit/socket-events-test.ts new file mode 100644 index 00000000..c8bf8da1 --- /dev/null +++ b/test/unit/socket-events-test.ts @@ -0,0 +1,222 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} events`, function() { + let sockA: zmq.Dealer + let sockB: zmq.Dealer + + beforeEach(function() { + sockA = new zmq.Dealer + sockB = new zmq.Dealer + }) + + afterEach(function() { + sockA.close() + sockB.close() + global.gc() + }) + + describe("when not connected", function() { + it("should receive events", async function() { + const events: zmq.Event[] = [] + + const read = async () => { + for await (const event of sockA.events) { + events.push(event) + } + } + + const done = read() + await sockA.close() + await done + + assert.deepEqual(events, [{type: "end"}]) + }) + }) + + describe("when connected", function() { + it("should return same object", function() { + assert.equal(sockA.events, sockA.events) + }) + + it("should receive bind events", async function() { + const address = uniqAddress(proto) + const events: zmq.Event[] = [] + + const read = async () => { + for await (const event of sockA.events) { + events.push(event) + } + } + + const done = read() + + await sockA.bind(address) + await sockB.connect(address) + await new Promise((resolve) => setTimeout(resolve, 15)) + sockA.close() + sockB.close() + await done + await new Promise((resolve) => setTimeout(resolve, 15)) + + if (proto === "inproc") { + assert.deepEqual(events, [{type: "end"}]) + } else { + assert.deepInclude(events, {type: "bind", address}) + assert.deepInclude(events, {type: "accept", address}) + assert.deepInclude(events, {type: "close", address}) + assert.deepInclude(events, {type: "end"}) + } + }) + + it("should receive connect events", async function() { + const address = uniqAddress(proto) + const events: zmq.Event[] = [] + + const read = async () => { + for await (const event of sockB.events) { + events.push(event) + } + } + + const done = read() + + await sockA.bind(address) + await sockB.connect(address) + await new Promise((resolve) => setTimeout(resolve, 15)) + sockA.close() + sockB.close() + await done + await new Promise((resolve) => setTimeout(resolve, 15)) + + if (proto === "inproc") { + assert.deepEqual(events, [{type: "end"}]) + } else { + if (proto === "tcp") { + assert.deepInclude(events, {type: "connect:delay", address}) + } + + assert.deepInclude(events, {type: "connect", address}) + assert.deepInclude(events, {type: "end"}) + } + }) + + it("should receive error events", async function() { + const address = uniqAddress(proto) + const events: zmq.Event[] = [] + + const read = async () => { + for await (const event of sockB.events) { + events.push(event) + } + } + + const done = read() + + await sockA.bind(address) + try { + await sockB.bind(address) + } catch (err) { + /* Ignore error here */ + } + + await new Promise((resolve) => setTimeout(resolve, 15)) + sockA.close() + sockB.close() + await done + + if (proto === "tcp") { + let bindError = false + for (const event of events) { + if (event.type === "bind:error") { + bindError = true + assert.equal("tcp://" + event.address, address) + assert.instanceOf(event.error, Error) + assert.equal(event.error.message, "Address already in use") + assert.equal(event.error.code, "EADDRINUSE") + assert.typeOf(event.error.errno, "number") + } + } + + assert.equal(true, bindError) + } + + assert.deepInclude(events, {type: "end"}) + }) + + it("should receive events with emitter", async function() { + const address = uniqAddress(proto) + const events: zmq.Event[] = [] + + sockA.events.on("bind", (event) => { + events.push(event) + }) + + sockA.events.on("accept", (event) => { + events.push(event) + }) + + sockA.events.on("close", (event) => { + events.push(event) + }) + + sockA.events.on("end", (event) => { + events.push(event) + }) + + assert.throws( + () => sockA.events.receive(), + Error, + "Observer is in event emitter mode. After a call to events.on() it " + + "is not possible to read events with events.receive().", + ) + + await sockA.bind(address) + await sockB.connect(address) + await new Promise((resolve) => setTimeout(resolve, 15)) + sockA.close() + sockB.close() + await new Promise((resolve) => setTimeout(resolve, 15)) + + if (proto === "inproc") { + assert.deepEqual(events, [{type: "end"}]) + } else { + assert.deepInclude(events, {type: "bind", address}) + assert.deepInclude(events, {type: "accept", address}) + assert.deepInclude(events, {type: "close", address}) + assert.deepInclude(events, {type: "end"}) + } + }) + }) + + describe("when closed automatically", function() { + it("should not be able to receive", async function() { + const events = sockA.events + sockA.close() + + const {type} = await events.receive() + assert.equal(type, "end") + + try { + await events.receive() + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Socket is closed") + assert.equal(err.code, "EBADF") + assert.typeOf(err.errno, "number") + } + }) + + it("should be closed", async function() { + const events = sockA.events + sockA.close() + await events.receive() + assert.equal(events.closed, true) + }) + }) + }) +} diff --git a/test/unit/socket-options-test.ts b/test/unit/socket-options-test.ts new file mode 100644 index 00000000..4412ce09 --- /dev/null +++ b/test/unit/socket-options-test.ts @@ -0,0 +1,258 @@ +/* tslint:disable: whitespace align */ +import * as semver from "semver" +import * as zmq from "../../src" + +import {assert} from "chai" +import {uniqAddress} from "./helpers" + +describe("socket options", function() { + let warningListeners: NodeJS.WarningListener[] + + beforeEach(function() { + warningListeners = process.listeners("warning") + }) + + afterEach(function() { + process.removeAllListeners("warning") + for (const listener of warningListeners) { + process.on("warning", listener as (warning: Error) => void) + } + + global.gc() + }) + + it("should set and get bool socket option", function() { + const sock = new zmq.Dealer + assert.equal(sock.immediate, false) + sock.immediate = true + assert.equal(sock.immediate, true) + }) + + it("should set and get int32 socket option", function() { + const sock = new zmq.Dealer + assert.equal(sock.backlog, 100) + sock.backlog = 75 + assert.equal(sock.backlog, 75) + }) + + it("should set and get int64 socket option", function() { + const sock = new zmq.Dealer + assert.equal(sock.maxMessageSize, -1) + sock.maxMessageSize = 0xffffffff + assert.equal(sock.maxMessageSize, 0xffffffff) + }) + + it("should set and get string socket option", function() { + const sock = new zmq.Dealer + assert.equal(sock.routingId, null) + sock.routingId = "åbçdéfghïjk" + assert.equal(sock.routingId, "åbçdéfghïjk") + }) + + it("should set and get string socket option as buffer", function() { + const sock = new zmq.Dealer + assert.equal(sock.routingId, null) + ;(sock as any).routingId = Buffer.from("åbçdéfghïjk") + assert.equal(sock.routingId, "åbçdéfghïjk") + }) + + it("should set and get string socket option to undefined", function() { + if (semver.satisfies(zmq.version, "> 4.2.3")) { + /* As of ZMQ 4.2.4, zap domain can no longer be reset to null. */ + const sock = new zmq.Dealer + assert.equal(sock.socksProxy, undefined) + ;(sock as any).socksProxy = Buffer.from("foo") + assert.equal(sock.socksProxy, "foo") + ;(sock as any).socksProxy = null + assert.equal(sock.socksProxy, undefined) + } else { + /* Older ZMQ versions did not allow socks proxy to be reset to null. */ + const sock = new zmq.Dealer + assert.equal(sock.zapDomain, undefined) + ;(sock as any).zapDomain = Buffer.from("foo") + assert.equal(sock.zapDomain, "foo") + ;(sock as any).zapDomain = null + assert.equal(sock.zapDomain, undefined) + } + }) + + it("should set and get bool socket option", function() { + const sock = new zmq.Dealer + assert.equal((sock as any).getBoolOption(39), false) + ;(sock as any).setBoolOption(39, true) + assert.equal((sock as any).getBoolOption(39), true) + }) + + it("should set and get int32 socket option", function() { + const sock = new zmq.Dealer + assert.equal((sock as any).getInt32Option(19), 100) + ;(sock as any).setInt32Option(19, 75) + assert.equal((sock as any).getInt32Option(19), 75) + }) + + it("should set and get int64 socket option", function() { + const sock = new zmq.Dealer + assert.equal((sock as any).getInt64Option(22), -1) + ;(sock as any).setInt64Option(22, 0xffffffffffff) + assert.equal((sock as any).getInt64Option(22), 0xffffffffffff) + }) + + it("should set and get uint64 socket option", function() { + process.removeAllListeners("warning") + + const sock = new zmq.Dealer + assert.equal((sock as any).getUint64Option(4), 0) + ;(sock as any).setUint64Option(4, 0xffffffffffffffff) + assert.equal((sock as any).getUint64Option(4), 0xffffffffffffffff) + }) + + it("should set and get string socket option", function() { + const sock = new zmq.Dealer + assert.equal((sock as any).getStringOption(5), null) + ;(sock as any).setStringOption(5, "åbçdéfghïjk") + assert.equal((sock as any).getStringOption(5), "åbçdéfghïjk") + }) + + it("should set and get string socket option as buffer", function() { + const sock = new zmq.Dealer + assert.equal((sock as any).getStringOption(5), null) + ;(sock as any).setStringOption(5, Buffer.from("åbçdéfghïjk")) + assert.equal((sock as any).getStringOption(5), "åbçdéfghïjk") + }) + + it("should set and get string socket option to null", function() { + if (semver.satisfies(zmq.version, "> 4.2.3")) { + /* As of ZMQ 4.2.4, zap domain can no longer be reset to null. */ + const sock = new zmq.Dealer + assert.equal((sock as any).getStringOption(68), null) + ;(sock as any).setStringOption(68, Buffer.from("åbçdéfghïjk")) + assert.equal((sock as any).getStringOption(68), Buffer.from("åbçdéfghïjk")) + ;(sock as any).setStringOption(68, null) + assert.equal((sock as any).getStringOption(68), null) + } else { + /* Older ZMQ versions did not allow socks proxy to be reset to null. */ + const sock = new zmq.Dealer + assert.equal((sock as any).getStringOption(55), null) + ;(sock as any).setStringOption(55, Buffer.from("åbçdéfghïjk")) + assert.equal((sock as any).getStringOption(55), Buffer.from("åbçdéfghïjk")) + ;(sock as any).setStringOption(55, null) + assert.equal((sock as any).getStringOption(55), null) + } + }) + + it("should throw for readonly option", function() { + const sock = new zmq.Dealer + assert.throws( + () => (sock as any).securityMechanism = 1, + TypeError, + "Cannot set property securityMechanism of # which has only a getter", + ) + }) + + it("should throw for unknown option", function() { + const sock = new zmq.Dealer + assert.throws( + () => (sock as any).doesNotExist = 1, + TypeError, + "Cannot add property doesNotExist, object is not extensible", + ) + }) + + it("should get mechanism", function() { + const sock = new zmq.Dealer + assert.equal(sock.securityMechanism, null) + sock.plainServer = true + assert.equal(sock.securityMechanism, "plain") + }) + + describe("warnings", function() { + beforeEach(function() { + /* ZMQ < 4.2 fails with assertion errors with inproc. + See: https://github.com/zeromq/libzmq/pull/2123/files */ + if (semver.satisfies(zmq.version, "< 4.2")) this.skip() + + warningListeners = process.listeners("warning") + }) + + afterEach(function() { + process.removeAllListeners("warning") + for (const listener of warningListeners) { + process.on("warning", listener as (warning: Error) => void) + } + }) + + it("should be emitted for set after connect", async function() { + const warnings: Error[] = [] + process.removeAllListeners("warning") + process.on("warning", (warning) => warnings.push(warning)) + + const sock = new zmq.Dealer + sock.connect(uniqAddress("inproc")) + sock.routingId = "asdf" + + await new Promise(process.nextTick) + assert.deepEqual( + warnings.map((w) => w.message), + ["Socket option will not take effect until next connect/bind"], + ) + + sock.close() + }) + + it("should be emitted for set during bind", async function() { + const warnings: Error[] = [] + process.removeAllListeners("warning") + process.on("warning", (warning) => warnings.push(warning)) + + const sock = new zmq.Dealer + const promise = sock.bind(uniqAddress("inproc")) + sock.routingId = "asdf" + + await new Promise(process.nextTick) + assert.deepEqual( + warnings.map((w) => w.message), + ["Socket option will not take effect until next connect/bind"], + ) + + await promise + sock.close() + }) + + it("should be emitted for set after bind", async function() { + const warnings: Error[] = [] + process.removeAllListeners("warning") + process.on("warning", (warning) => warnings.push(warning)) + + const sock = new zmq.Dealer + await sock.bind(uniqAddress("inproc")) + sock.routingId = "asdf" + + await new Promise(process.nextTick) + assert.deepEqual( + warnings.map((w) => w.message), + ["Socket option will not take effect until next connect/bind"], + ) + + sock.close() + }) + + it("should be emitted when setting large uint64 socket option", async function() { + const warnings: Error[] = [] + process.removeAllListeners("warning") + process.on("warning", (warning) => warnings.push(warning)) + + const sock = new zmq.Dealer + ;(sock as any).setUint64Option(4, 0xfffffff7fab7fb) + assert.equal((sock as any).getUint64Option(4), 0xfffffff7fab7fb) + + await new Promise(process.nextTick) + assert.deepEqual( + warnings.map((w) => w.message), + [ + "Value is larger than Number.MAX_SAFE_INTEGER and " + + "may have been rounded inaccurately", + ], + ) + }) + }) +}) diff --git a/test/unit/socket-pair-test.ts b/test/unit/socket-pair-test.ts new file mode 100644 index 00000000..6148a621 --- /dev/null +++ b/test/unit/socket-pair-test.ts @@ -0,0 +1,65 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} pair/pair`, function() { + let sockA: zmq.Dealer + let sockB: zmq.Dealer + + beforeEach(function() { + sockA = new zmq.Dealer + sockB = new zmq.Dealer + }) + + afterEach(function() { + sockA.close() + sockB.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + /* PAIR -> foo -> PAIR + [A] -> bar -> [B] + -> baz -> responds when received + -> qux -> + <- foo <- + <- bar <- + <- baz <- + <- qux <- + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + await sockA.bind(address) + await sockB.connect(address) + + const echo = async () => { + for await (const msg of sockB) { + await sockB.send(msg) + } + } + + const send = async () => { + for (const msg of messages) { + await sockA.send(msg) + } + + for await (const msg of sockA) { + received.push(msg.toString()) + if (received.length === messages.length) break + } + + sockB.close() + } + + await Promise.all([echo(), send()]) + assert.deepEqual(received, messages) + }) + }) + }) +} diff --git a/test/unit/socket-pub-sub-test.ts b/test/unit/socket-pub-sub-test.ts new file mode 100644 index 00000000..bd38c3c9 --- /dev/null +++ b/test/unit/socket-pub-sub-test.ts @@ -0,0 +1,100 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} pub/sub`, function() { + let pub: zmq.Publisher + let sub: zmq.Subscriber + + beforeEach(function() { + pub = new zmq.Publisher + sub = new zmq.Subscriber + }) + + afterEach(function() { + pub.close() + sub.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + /* PUB -> foo -> SUB + -> bar -> subscribed to all + -> baz -> + -> qux -> + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + /* Subscribe to all. */ + sub.subscribe() + + await sub.bind(address) + await pub.connect(address) + + const send = async () => { + /* Wait briefly before publishing to avoid slow joiner syndrome. */ + await new Promise((resolve) => setTimeout(resolve, 25)) + for (const msg of messages) { + await pub.send(msg) + } + } + + const receive = async () => { + for await (const [msg] of sub) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === messages.length) break + } + } + + await Promise.all([send(), receive()]) + assert.deepEqual(received, messages) + }) + }) + + describe("subscribe/unsubscribe", function() { + it("should filter messages", async function() { + /* PUB -> foo -X SUB + -> bar -> subscribed to "ba" + -> baz -> + -> qux -X + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + sub.subscribe("fo", "ba", "qu") + sub.unsubscribe("fo", "qu") + + await sub.bind(address) + await pub.connect(address) + + const send = async () => { + /* Wait briefly before publishing to avoid slow joiner syndrome. */ + await new Promise((resolve) => setTimeout(resolve, 25)) + for (const msg of messages) { + await pub.send(msg) + } + } + + const receive = async () => { + for await (const [msg] of sub) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === 2) break + } + } + + await Promise.all([send(), receive()]) + assert.deepEqual(received, ["bar", "baz"]) + }) + }) + }) +} diff --git a/test/unit/socket-push-pull-test.ts b/test/unit/socket-push-pull-test.ts new file mode 100644 index 00000000..6223e8af --- /dev/null +++ b/test/unit/socket-push-pull-test.ts @@ -0,0 +1,79 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} push/pull`, function() { + let push: zmq.Push + let pull: zmq.Pull + + beforeEach(function() { + push = new zmq.Push + pull = new zmq.Pull + }) + + afterEach(function() { + push.close() + pull.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + /* PUSH -> foo -> PULL + -> bar -> + -> baz -> + -> qux -> + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + await pull.bind(address) + await push.connect(address) + + for (const msg of messages) { + await push.send(msg) + } + + for await (const [msg] of pull) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === messages.length) break + } + + assert.deepEqual(received, messages) + }) + + if (proto !== "inproc") { + it("should deliver messages with immediate", async function() { + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + await pull.bind(address) + + push.immediate = true + await push.connect(address) + + /* Never connected, without immediate: true it would cause lost msgs. */ + await push.connect(uniqAddress(proto)) + + for (const msg of messages) { + await push.send(msg) + } + + for await (const [msg] of pull) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === messages.length) break + } + + assert.deepEqual(received, messages) + }) + } + }) + }) +} diff --git a/test/unit/socket-req-rep-test.ts b/test/unit/socket-req-rep-test.ts new file mode 100644 index 00000000..ac5cf4b2 --- /dev/null +++ b/test/unit/socket-req-rep-test.ts @@ -0,0 +1,105 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} req/rep`, function() { + let req: zmq.Request + let rep: zmq.Reply + + beforeEach(function() { + req = new zmq.Request + rep = new zmq.Reply + }) + + afterEach(function() { + req.close() + rep.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + /* REQ -> foo -> REP + <- foo <- + -> bar -> + <- bar <- + -> baz -> + <- baz <- + -> qux -> + <- qux <- + */ + + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + await rep.bind(address) + await req.connect(address) + + const echo = async () => { + for await (const msg of rep) { + await rep.send(msg) + } + } + + const send = async () => { + for (const msg of messages) { + await req.send(Buffer.from(msg)) + + const [res] = await req.receive() + received.push(res.toString()) + if (received.length === messages.length) break + } + + rep.close() + } + + await Promise.all([echo(), send()]) + assert.deepEqual(received, messages) + }) + + it("should throw when waiting for a response", async function() { + /* REQ -> foo -> REP + -X foo + <- foo <- + */ + + const address = uniqAddress(proto) + + /* FIXME: Also trigger EFSM without setting timeout. */ + req.sendTimeout = 2 + await rep.bind(address) + await req.connect(address) + + const echo = async () => { + const msg = await rep.receive() + await rep.send(msg) + } + + const send = async () => { + await req.send(Buffer.from("foo")) + assert.equal(req.writable, false) + + try { + await req.send(Buffer.from("bar")) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Operation cannot be accomplished in current state") + assert.equal(err.code, "EFSM") + assert.typeOf(err.errno, "number") + } + + const [msg] = await req.receive() + assert.deepEqual(msg, Buffer.from("foo")) + + rep.close() + } + + await Promise.all([echo(), send()]) + }) + }) + }) +} diff --git a/test/unit/socket-router-dealer-test.ts b/test/unit/socket-router-dealer-test.ts new file mode 100644 index 00000000..da09483e --- /dev/null +++ b/test/unit/socket-router-dealer-test.ts @@ -0,0 +1,86 @@ +import * as semver from "semver" +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} router/dealer`, function() { + let router: zmq.Router + let dealerA: zmq.Dealer + let dealerB: zmq.Dealer + + beforeEach(function() { + router = new zmq.Router + dealerA = new zmq.Dealer + dealerB = new zmq.Dealer + }) + + afterEach(function() { + router.close() + dealerA.close() + dealerB.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + const address = uniqAddress(proto) + const messages = ["foo", "bar", "baz", "qux"] + const receivedA: string[] = [] + const receivedB: string[] = [] + + await router.bind(address) + dealerA.connect(address) + dealerB.connect(address) + + const echo = async () => { + for await (const [sender, msg] of router) { + await router.send([sender, msg]) + } + } + + const send = async () => { + for (const msg of messages) { + await dealerA.send(msg) + await dealerB.send(msg) + } + + for await (const msg of dealerA) { + receivedA.push(msg.toString()) + if (receivedA.length === messages.length) break + } + + for await (const msg of dealerB) { + receivedB.push(msg.toString()) + if (receivedB.length === messages.length) break + } + + router.close() + } + + await Promise.all([echo(), send()]) + assert.deepEqual(receivedA, messages) + assert.deepEqual(receivedB, messages) + }) + + /* This only works reliably with ZMQ 4.2.3+ */ + if (semver.satisfies(zmq.version, ">= 4.2.3")) { + it("should fail with unroutable message if mandatory", async function() { + router.mandatory = true + router.sendTimeout = 0 + try { + await router.send(["fooId", "foo"]) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + + assert.equal(err.message, "Host unreachable") + assert.equal(err.code, "EHOSTUNREACH") + assert.typeOf(err.errno, "number") + } + }) + } + }) + }) +} diff --git a/test/unit/socket-send-receive-test.ts b/test/unit/socket-send-receive-test.ts new file mode 100644 index 00000000..fe69dfee --- /dev/null +++ b/test/unit/socket-send-receive-test.ts @@ -0,0 +1,400 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} send/receive`, function() { + let sockA: zmq.Pair + let sockB: zmq.Pair + + beforeEach(function() { + sockA = new zmq.Pair({linger: 0}) + sockB = new zmq.Pair({linger: 0}) + }) + + afterEach(function() { + sockA.close() + sockB.close() + global.gc() + }) + + describe("when not applicable", function() { + it("should fail sending", function() { + try { + (new zmq.Subscriber() as any).send() + } catch (err) { + assert.instanceOf(err, Error) + assert.include(err.message, "send is not a function") + } + }) + + it("should fail receiving", function() { + try { + (new zmq.Publisher() as any).receive() + } catch (err) { + assert.instanceOf(err, Error) + assert.include(err.message, "receive is not a function") + } + }) + + it("should fail iterating", async function() { + try { + /* tslint:disable-next-line: no-empty */ + for await (const msg of (new zmq.Publisher() as any)) {} + } catch (err) { + assert.instanceOf(err, Error) + assert.include(err.message, "receive is not a function") + } + }) + }) + + describe("when not connected", function() { + beforeEach(async function() { + sockA.sendHighWaterMark = 1 + await sockA.connect(uniqAddress(proto)) + }) + + it("should be writable", async function() { + assert.equal(sockA.writable, true) + }) + + it("should not be readable", async function() { + assert.equal(sockA.readable, false) + }) + + it("should honor send high water mark and timeout", async function() { + sockA.sendTimeout = 2 + await sockA.send(Buffer.alloc(8192)) + try { + await sockA.send(Buffer.alloc(8192)) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Socket temporarily unavailable") + assert.equal(err.code, "EAGAIN") + assert.typeOf(err.errno, "number") + } + }) + + it("should copy and release small buffers", async function() { + const weak = require("weak-napi") + + let released = false + sockA.connect(uniqAddress(proto)) + const send = async (size: number) => { + const msg = Buffer.alloc(size) + weak(msg, () => {released = true}) + await sockA.send(msg) + } + + await send(16) + global.gc() + await new Promise((resolve) => setTimeout(resolve, 5)) + assert.equal(released, true) + }) + + it("should retain large buffers", async function() { + const weak = require("weak-napi") + + let released = false + sockA.connect(uniqAddress(proto)) + const send = async (size: number) => { + const msg = Buffer.alloc(size) + weak(msg, () => {released = true}) + await sockA.send(msg) + } + + await send(1025) + global.gc() + await new Promise((resolve) => setTimeout(resolve, 5)) + assert.equal(released, false) + }) + }) + + describe("when connected", function() { + beforeEach(async function() { + const address = uniqAddress(proto) + await sockB.bind(address) + await sockA.connect(address) + }) + + it("should be writable", async function() { + assert.equal(sockA.writable, true) + }) + + it("should not be readable", async function() { + assert.equal(sockA.readable, false) + }) + + it("should be readable if message is available", async function() { + await sockB.send(Buffer.from("foo")) + await new Promise((resolve) => setTimeout(resolve, 15)) + assert.equal(sockA.readable, true) + }) + + it("should deliver single string message", async function() { + const sent = "foo" + await sockA.send(sent) + + const recv = await sockB.receive() + assert.deepEqual([sent], recv.map((buf: Buffer) => buf.toString())) + }) + + it("should deliver single buffer message", async function() { + const sent = Buffer.from("foo") + await sockA.send(sent) + + const recv = await sockB.receive() + assert.deepEqual([sent], recv) + }) + + it("should deliver single multipart string message", async function() { + const sent = ["foo", "bar"] + await sockA.send(sent) + + const recv = await sockB.receive() + assert.deepEqual(sent, recv.map((buf: Buffer) => buf.toString())) + }) + + it("should deliver single multipart buffer message", async function() { + const sent = [Buffer.from("foo"), Buffer.from("bar")] + await sockA.send(sent) + + const recv = await sockB.receive() + assert.deepEqual(sent, recv) + }) + + it("should deliver multiple messages", async function() { + const messages = ["foo", "bar", "baz", "qux"] + for (const msg of messages) { + await sockA.send(msg) + } + + const received: string[] = [] + for await (const msg of sockB) { + received.push(msg.toString()) + if (received.length === messages.length) break + } + + assert.deepEqual(received, messages) + }) + + it("should deliver typed array and array buffer messages", async function() { + const messages = [ + Uint8Array.from([0x66, 0x6f, 0x6f]), + Uint8Array.from([0x66, 0x6f, 0x6f]).buffer, + Int32Array.from([0x66, 0x6f, 0x6f]), + Int32Array.from([0x66, 0x6f, 0x6f]).buffer, + ] + + for (const msg of messages) { + await sockA.send(msg) + } + + const received: string[] = [] + for await (const msg of sockB) { + received.push(msg.toString()) + if (received.length === messages.length) break + } + + assert.deepEqual(received, [ + "foo", + "foo", + "f\x00\x00\x00o\x00\x00\x00o\x00\x00\x00", + "f\x00\x00\x00o\x00\x00\x00o\x00\x00\x00", + ]) + }) + + it("should deliver messages coercible to string", async function() { + /* tslint:disable-next-line: no-empty */ + const messages = [null, function() {}, 16.19, true, {}, Promise.resolve()] + for (const msg of messages) { + await sockA.send(msg as any) + } + + const received: string[] = [] + for await (const msg of sockB) { + received.push(msg.toString()) + if (received.length === messages.length) break + } + + /* Unify different output across Node/TypeScript versions. */ + received[1] = received[1].replace("function()", "function ()") + received[1] = received[1].replace("function () { }", "function () {}") + assert.deepEqual( + received, + ["", "function () {}", "16.19", "true", "[object Object]", "[object Promise]"], + ) + }) + + it("should poll simultaneously", async function() { + const sendReceiveA = async () => { + const [msg1] = await Promise.all([ + sockA.receive(), + sockA.send(Buffer.from("foo")), + ]) + return msg1.toString() + } + + const sendReceiveB = async () => { + const [msg2] = await Promise.all([ + sockB.receive(), + sockB.send(Buffer.from("bar")), + ]) + return msg2.toString() + } + + const msgs = await Promise.all([sendReceiveA(), sendReceiveB()]) + assert.deepEqual(msgs, ["bar", "foo"]) + }) + + it("should poll simultaneously after delay", async function() { + await new Promise((resolve) => setTimeout(resolve, 15)) + const sendReceiveA = async () => { + const [msg1] = await Promise.all([ + sockA.receive(), + sockA.send(Buffer.from("foo")), + ]) + return msg1.toString() + } + + const sendReceiveB = async () => { + const [msg2] = await Promise.all([ + sockB.receive(), + sockB.send(Buffer.from("bar")), + ]) + return msg2.toString() + } + + const msgs = await Promise.all([sendReceiveA(), sendReceiveB()]) + assert.deepEqual(msgs, ["bar", "foo"]) + }) + + it("should honor receive timeout", async function() { + sockA.receiveTimeout = 2 + try { + await sockA.receive() + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Socket temporarily unavailable") + assert.equal(err.code, "EAGAIN") + assert.typeOf(err.errno, "number") + } + }) + + it("should release buffers", async function() { + const weak = require("weak-napi") + + const address = uniqAddress(proto) + await sockB.bind(address) + sockA.connect(address) + + let released = 0 + + const send = async (size: number) => { + const msg = Buffer.alloc(size) + weak(msg, () => {released++}) + await sockA.send(msg) + } + + const receive = async () => { + const msg = await sockB.receive() + weak(msg, () => {released++}) + } + + await Promise.all([ + send(2048), + receive(), + ]) + + await sockB.unbind(address) + + global.gc() + await new Promise((resolve) => setTimeout(resolve, 5)) + assert.equal(released, proto === "inproc" ? 1 : 2) + }) + + if (proto === "inproc") { + it("should share memory of large buffers", async function() { + const orig = Buffer.alloc(2048) + await sockA.send(orig) + + const echo = async (sock: zmq.Pair) => { + const msg = await sock.receive() + sock.send(msg) + } + + echo(sockB) + + const [final] = await sockA.receive() + final.writeUInt8(0x40, 0) + assert.equal(orig.slice(0, 1).toString(), "@") + }) + } + }) + + describe("when closed", function() { + beforeEach(function() { + sockA.close() + sockB.close() + }) + + it("should not be writable", async function() { + assert.equal(sockA.writable, false) + }) + + it("should not be readable", async function() { + assert.equal(sockA.readable, false) + }) + + it("should not be able to send", async function() { + try { + await sockA.send(Buffer.alloc(8192)) + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Socket is closed") + assert.equal(err.code, "EBADF") + assert.typeOf(err.errno, "number") + } + }) + + it("should not be able to receive", async function() { + try { + await sockA.receive() + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Socket is closed") + assert.equal(err.code, "EBADF") + assert.typeOf(err.errno, "number") + } + }) + }) + + describe("during close", function() { + it("should gracefully stop async iterator", async function() { + process.nextTick(() => sockA.close()) + /* tslint:disable-next-line: no-empty */ + for await (const _ of sockA) {} + }) + + it("should not mask other error type in async iterator", async function() { + sockA = new zmq.Request + process.nextTick(() => sockA.close()) + try { + /* tslint:disable-next-line: no-empty */ + for await (const _ of sockA) {} + assert.ok(false) + } catch (err) { + assert.instanceOf(err, Error) + assert.equal(err.message, "Operation cannot be accomplished in current state") + assert.equal(err.code, "EFSM") + assert.typeOf(err.errno, "number") + } + }) + }) + }) +} diff --git a/test/unit/socket-stream-test.ts b/test/unit/socket-stream-test.ts new file mode 100644 index 00000000..4c91328f --- /dev/null +++ b/test/unit/socket-stream-test.ts @@ -0,0 +1,99 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {createServer, get, Server} from "http" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp")) { + describe(`socket with ${proto} stream`, function() { + let stream: zmq.Stream + + beforeEach(function() { + stream = new zmq.Stream + }) + + afterEach(function() { + stream.close() + global.gc() + }) + + describe("send/receive as server", function() { + it("should deliver messages", async function() { + const address = uniqAddress(proto) + + await stream.bind(address) + + const serve = async () => { + for await (const [id, msg] of stream) { + if (!msg.length) continue + assert.equal(msg.toString().split("\r\n")[0], "GET /foo HTTP/1.1") + + await stream.send([id, + "HTTP/1.0 200 OK\r\n" + + "Content-Type: text/plan\r\n" + + "\r\n" + + "Hello world!", + ]) + + stream.close() + } + } + + let body = "" + const request = async () => { + return new Promise((resolve) => { + get(address.replace("tcp:", "http:") + "/foo", (res) => { + res.on("data", (buffer) => {body += buffer.toString()}) + res.on("end", resolve) + }) + }) + } + + await Promise.all([request(), serve()]) + assert.equal(body, "Hello world!") + }) + }) + + describe("send/receive as client", function() { + it("should deliver messages", async function() { + const address = uniqAddress(proto) + const port = parseInt(address.split(":").pop()!, 10) + + const server = await new Promise((resolve) => { + const http = createServer((req, res) => { + res.writeHead(200, {"Content-Type": "text/plain", "Content-Length": 12}) + res.end("Hello world!") + }) + + http.listen(port, () => resolve(http)) + }) + + const routingId = "abcdef1234567890" + stream.connect(address, {routingId}) + + let body = "" + const request = async () => { + await stream.send([ + routingId, + "GET /foo HTTP/1.1\r\n" + + `Host: ${address.replace("tcp://", "")}\r\n\r\n`, + ]) + + for await (const [id, data] of stream) { + assert.equal(id.toString(), routingId) + if (data.length) { + body += data + break + } + } + + stream.close() + server.close() + } + + await Promise.all([request()]) + assert.equal(body.split("\r\n\r\n").pop(), "Hello world!") + }) + }) + }) +} diff --git a/test/unit/socket-xpub-xsub-test.ts b/test/unit/socket-xpub-xsub-test.ts new file mode 100644 index 00000000..d64456f5 --- /dev/null +++ b/test/unit/socket-xpub-xsub-test.ts @@ -0,0 +1,265 @@ +import * as zmq from "../../src" + +import {assert} from "chai" +import {testProtos, uniqAddress} from "./helpers" + +for (const proto of testProtos("tcp", "ipc", "inproc")) { + describe(`socket with ${proto} xpub/xsub`, function() { + let pub: zmq.Publisher + let sub: zmq.Subscriber + let xpub: zmq.XPublisher + let xsub: zmq.XSubscriber + + beforeEach(function() { + pub = new zmq.Publisher + sub = new zmq.Subscriber + xpub = new zmq.XPublisher + xsub = new zmq.XSubscriber + }) + + afterEach(function() { + pub.close() + sub.close() + xpub.close() + xsub.close() + global.gc() + }) + + describe("send/receive", function() { + it("should deliver messages", async function() { + /* PUB -> foo -> XSUB -> XPUB -> SUB + -> bar -> subscribed to all + -> baz -> + -> qux -> + */ + + const address1 = uniqAddress(proto) + const address2 = uniqAddress(proto) + + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + /* Subscribe to all. */ + sub.subscribe() + + await pub.bind(address1) + await xpub.bind(address2) + await xsub.connect(address1) + await sub.connect(address2) + + const send = async () => { + /* Wait briefly before publishing to avoid slow joiner syndrome. */ + await new Promise((resolve) => setTimeout(resolve, 25)) + for (const msg of messages) { + await pub.send(msg) + } + } + + let subbed = 0 + const forward = async () => { + for await (const [msg] of xpub) { + assert.instanceOf(msg, Buffer) + await xsub.send(msg) + if (++subbed === 1) break + } + } + + let pubbed = 0 + const publish = async () => { + for await (const [msg] of xsub) { + assert.instanceOf(msg, Buffer) + await xpub.send(msg) + if (++pubbed === messages.length) break + } + } + + const receive = async () => { + for await (const [msg] of sub) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === messages.length) break + } + } + + await Promise.all([send(), forward(), publish(), receive()]) + assert.deepEqual(received, messages) + }) + }) + + describe("subscribe/unsubscribe", function() { + it("should filter messages", async function() { + /* PUB -> foo -X XSUB -> XPUB -> SUB + -> bar -> subscribed to "ba" + -> baz -> + -> qux -X + */ + + const address1 = uniqAddress(proto) + const address2 = uniqAddress(proto) + + const messages = ["foo", "bar", "baz", "qux"] + const received: string[] = [] + + sub.subscribe("fo", "ba", "qu") + sub.unsubscribe("fo", "qu") + + await pub.bind(address1) + await xpub.bind(address2) + await xsub.connect(address1) + await sub.connect(address2) + + const send = async () => { + /* Wait briefly before publishing to avoid slow joiner syndrome. */ + await new Promise((resolve) => setTimeout(resolve, 25)) + + for (const msg of messages) { + await pub.send(msg) + } + } + + let subbed = 0 + const forward = async () => { + for await (const [msg] of xpub) { + assert.instanceOf(msg, Buffer) + await xsub.send(msg) + if (++subbed === 1) break + } + } + + let pubbed = 0 + const publish = async () => { + for await (const [msg] of xsub) { + assert.instanceOf(msg, Buffer) + await xpub.send(msg) + if (++pubbed === 2) break + } + } + + const receive = async () => { + for await (const [msg] of sub) { + assert.instanceOf(msg, Buffer) + received.push(msg.toString()) + if (received.length === 2) break + } + } + + await Promise.all([send(), forward(), publish(), receive()]) + assert.deepEqual(received, ["bar", "baz"]) + }) + }) + + describe("verbosity", function() { + it("should deduplicate subscriptions/unsubscriptions", async function() { + const address = uniqAddress(proto) + + const subs: Buffer[] = [] + + xpub.verbosity = null + + const sub1 = sub + const sub2 = new zmq.Subscriber + await xpub.bind(address) + await sub1.connect(address) + await sub2.connect(address) + + const subscribe = async () => { + await new Promise((resolve) => setTimeout(resolve, 25)) + sub1.subscribe("fo") + sub2.subscribe("fo") + sub2.unsubscribe("fo") + } + + const forward = async () => { + for await (const [msg] of xpub) { + assert.instanceOf(msg, Buffer) + await xsub.send(msg) + subs.push(msg) + if (subs.length === 1) break + } + } + + await Promise.all([subscribe(), forward()]) + assert.sameDeepMembers(subs, [Buffer.from("\x01fo")]) + + sub2.close() + }) + + it("should forward all subscriptions", async function() { + const address = uniqAddress(proto) + + const subs: Buffer[] = [] + + xpub.verbosity = "allSubs" + + const sub1 = sub + const sub2 = new zmq.Subscriber + await xpub.bind(address) + await sub1.connect(address) + await sub2.connect(address) + + const subscribe = async () => { + await new Promise((resolve) => setTimeout(resolve, 25)) + sub1.subscribe("fo") + sub2.subscribe("fo") + sub2.unsubscribe("fo") + } + + const forward = async () => { + for await (const [msg] of xpub) { + assert.instanceOf(msg, Buffer) + await xsub.send(msg) + subs.push(msg) + if (subs.length === 2) break + } + } + + await Promise.all([subscribe(), forward()]) + assert.sameDeepMembers(subs, [ + Buffer.from("\x01fo"), + Buffer.from("\x01fo"), + ]) + + sub2.close() + }) + + it("should forward all subscriptions/unsubscriptions", async function() { + const address = uniqAddress(proto) + + const subs: Buffer[] = [] + + xpub.verbosity = "allSubsUnsubs" + + const sub1 = sub + const sub2 = new zmq.Subscriber + await xpub.bind(address) + await sub1.connect(address) + await sub2.connect(address) + + const subscribe = async () => { + await new Promise((resolve) => setTimeout(resolve, 25)) + sub1.subscribe("fo") + sub2.subscribe("fo") + sub2.unsubscribe("fo") + } + + const forward = async () => { + for await (const [msg] of xpub) { + assert.instanceOf(msg, Buffer) + await xsub.send(msg) + subs.push(msg) + if (subs.length === 3) break + } + } + + await Promise.all([subscribe(), forward()]) + assert.sameDeepMembers(subs, [ + Buffer.from("\x01fo"), + Buffer.from("\x01fo"), + Buffer.from("\x00fo"), + ]) + + sub2.close() + }) + }) + }) +} diff --git a/test/unit/typings-test.ts b/test/unit/typings-test.ts new file mode 100644 index 00000000..03265b71 --- /dev/null +++ b/test/unit/typings-test.ts @@ -0,0 +1,133 @@ +import * as zmq from "../../src" + +describe("typings", function() { + it("should compile successfully", function() { + /* To test the TypeScript typings this file should compile successfully. + We don't actually execute the code in this function. */ + + /* @ts-ignore unused function */ + function test() { + const version: string = zmq.version + console.log(version) + + const capability = zmq.capability + if (capability.ipc) console.log("ipc") + if (capability.pgm) console.log("pgm") + if (capability.tipc) console.log("tipc") + if (capability.norm) console.log("norm") + if (capability.curve) console.log("curve") + if (capability.gssapi) console.log("gssapi") + if (capability.draft) console.log("draft") + + const keypair = zmq.curveKeyPair() + console.log(keypair.publicKey) + console.log(keypair.secretKey) + + const context = new zmq.Context({ + ioThreads: 1, + ipv6: true, + }) + + context.threadPriority = 4 + + console.log(context.ioThreads) + console.log(context.ipv6) + + zmq.context.ioThreads = 5 + zmq.context.ipv6 = true + + const socket = new zmq.Dealer({ + context: zmq.context, + sendTimeout: 200, + probeRouter: true, + routingId: "foobar", + }) + + const router = new zmq.Router + if (router.type !== 6) throw new Error() + + console.log(socket.context) + console.log(socket.sendTimeout) + console.log(socket.routingId) + + const exec = async () => { + await socket.bind("tcp://foobar") + await socket.unbind("tcp://foobar") + + socket.connect("tcp://foobar") + socket.disconnect("tcp://foobar") + router.connect("tcp://foobar", {routingId: "remote_id"}) + + for await (const [p1, p2] of socket) { + console.log(p1) + console.log(p2) + } + + const [part1, part2] = await socket.receive() + + await socket.send(part1) + await socket.send([part1, part2]) + + await socket.send([null, Buffer.alloc(1), "foo"]) + await socket.send(null) + await socket.send(Buffer.alloc(1)) + await socket.send("foo") + + socket.close() + + socket.events.on("bind", (details) => { + console.log(details.address) + }) + + socket.events.off("bind", (details) => { + console.log(details.address) + }) + + socket.events.on("connect:retry", (details) => { + console.log(details.interval) + console.log(details.address) + }) + + socket.events.on("accept:error", (details) => { + console.log(details.error.code) + console.log(details.error.errno) + console.log(details.address) + }) + + for await (const event of socket.events) { + switch (event.type) { + case "end": + case "unknown": + break + case "connect:retry": + console.log(event.interval) + console.log(event.address) + break + case "accept:error": + case "bind:error": + case "close:error": + case "handshake:error:other": + console.log(event.error.code) + console.log(event.error.errno) + console.log(event.address) + break + default: + console.log(event.address) + } + } + + const proxy = new zmq.Proxy(new zmq.Router, new zmq.Dealer) + await proxy.run() + + proxy.pause() + proxy.resume() + proxy.terminate() + + proxy.frontEnd.close() + proxy.backEnd.close() + } + + exec() + } + }) +}) diff --git a/test/unit/zmq-draft-test.ts b/test/unit/zmq-draft-test.ts new file mode 100644 index 00000000..d47bcbcc --- /dev/null +++ b/test/unit/zmq-draft-test.ts @@ -0,0 +1,23 @@ +import * as zmq from "../../src" +import * as draft from "../../src/draft" + +import {assert} from "chai" + +if (zmq.capability.draft) { + describe("zmq draft", function() { + describe("exports", function() { + it("should include functions and constructors", function() { + const expected = [ + /* Specific socket constructors. */ + "Server", "Client", "Radio", "Dish", "Gather", "Scatter", "Datagram", + ] + + assert.sameMembers(Object.keys(draft), expected) + }) + }) + }) +} else { + if (process.env.ZMQ_DRAFT) { + throw new Error("Draft API requested but not available at runtime.") + } +} diff --git a/test/unit/zmq-test.ts b/test/unit/zmq-test.ts new file mode 100644 index 00000000..cfda504d --- /dev/null +++ b/test/unit/zmq-test.ts @@ -0,0 +1,64 @@ +import * as semver from "semver" +import * as zmq from "../../src" + +import {assert} from "chai" + +describe("zmq", function() { + describe("exports", function() { + it("should include functions and constructors", function() { + const expected = [ + /* Utility functions. */ + "version", "capability", "curveKeyPair", + + /* The global/default context. */ + "context", + + /* Generic constructors. */ + "Context", "Socket", "Observer", "Proxy", + + /* Specific socket constructors. */ + "Pair", "Publisher", "Subscriber", "Request", "Reply", + "Dealer", "Router", "Pull", "Push", "XPublisher", "XSubscriber", + "Stream", + ] + + /* ZMQ < 4.0.5 has no steerable proxy support. */ + if (semver.satisfies(zmq.version, "< 4.0.5")) { + expected.splice(expected.indexOf("Proxy"), 1) + } + + assert.sameMembers(Object.keys(zmq), expected) + }) + }) + + describe("version", function() { + it("should return version string", function() { + if (process.env.ZMQ_VERSION) { + assert.equal(zmq.version, process.env.ZMQ_VERSION) + } else { + assert.match(zmq.version, /^\d+\.\d+\.\d+$/) + } + }) + }) + + describe("capability", function() { + it("should return library capability booleans", function() { + assert.equal( + Object.values(zmq.capability).every((c) => typeof c === "boolean"), + true, + ) + }) + }) + + describe("curve keypair", function() { + beforeEach(function() { + if (!zmq.capability.curve) this.skip() + }) + + it("should return keypair", function() { + const {publicKey, secretKey} = zmq.curveKeyPair() + assert.match(publicKey, /^[\x20-\x7F]{40}$/) + assert.match(secretKey, /^[\x20-\x7F]{40}$/) + }) + }) +}) diff --git a/test/util.js b/test/util.js deleted file mode 100644 index 54942153..00000000 --- a/test/util.js +++ /dev/null @@ -1,27 +0,0 @@ -// cleanup utils for sockets -var sockets = []; - -exports.cleanup = function (done) { - while (sockets.length) { - sockets.pop().close(); - } - // give underlying sockets some time to close - // this shouldn't be necessary on *ix, but it seems to be - setTimeout(function () { - done(); - }, 500); -}; - -exports.push_sockets = function () { - sockets.push.apply(sockets, arguments); -}; - -exports.done_countdown = function (done, counter) { - // add a done-countdown, so multiple async events can be awaited before triggering done - return function () { - counter -= 1; - if (counter === 0) { - done(); - } - } -}; \ No newline at end of file diff --git a/test/zap.js b/test/zap.js deleted file mode 100644 index fe268f88..00000000 --- a/test/zap.js +++ /dev/null @@ -1,46 +0,0 @@ -// This is mainly for testing that the security mechanisms themselves are working -// not the ZAP protocol itself. As long as the request is valid, this will -// authenticate it. - -var zmq = require('../'); - -module.exports.start = function(count) { - var zap = zmq.socket('router'); - zap.on('message', function() { - var data = Array.prototype.slice.call(arguments); - - if (!data || !data.length) throw new Error("Invalid ZAP request"); - - var returnPath = [], - frame = data.shift(); - while (frame && (frame.length != 0)) { - returnPath.push(frame); - frame = data.shift(); - } - returnPath.push(frame); - - if (data.length < 6) throw new Error("Invalid ZAP request"); - - var zapReq = { - version: data.shift(), - requestId: data.shift(), - domain: Buffer.from(data.shift()).toString('utf8'), - address: Buffer.from(data.shift()).toString('utf8'), - identity: Buffer.from(data.shift()).toString('utf8'), - mechanism: Buffer.from(data.shift()).toString('utf8'), - credentials: data.slice(0) - }; - - zap.send(returnPath.concat([ - zapReq.version, - zapReq.requestId, - Buffer.from("200", "utf8"), - Buffer.from("OK", "utf8"), - Buffer.alloc(0), - Buffer.alloc(0) - ])); - }); - - zap.bindSync("inproc://zeromq.zap.01."+count); - return zap; -} diff --git a/test/zmq_proxy.js b/test/zmq_proxy.js deleted file mode 100644 index 438814e6..00000000 --- a/test/zmq_proxy.js +++ /dev/null @@ -1,10 +0,0 @@ - -var zmq = require('..') - , should = require('should'); - -describe('proxy', function() { - it('should be a function off the module namespace', function (done) { - zmq.proxy.should.be.a.Function; - done(); - }); -}); \ No newline at end of file diff --git a/test/zmq_proxy.push-pull.js b/test/zmq_proxy.push-pull.js deleted file mode 100644 index 98d3aa15..00000000 --- a/test/zmq_proxy.push-pull.js +++ /dev/null @@ -1,86 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -var addr = 'tcp://127.0.0.1' - , frontendAddr = addr+':5501' - , backendAddr = addr+':5502' - , captureAddr = addr+':5503'; - -var testutil = require('./util'); - -describe('proxy.push-pull', function() { - afterEach(testutil.cleanup); - - it('should proxy push-pull connected to pull-push',function (done) { - - var frontend = zmq.socket('pull'); - var backend = zmq.socket('push'); - - var pull = zmq.socket('pull'); - var push = zmq.socket('push'); - - frontend.bindSync(frontendAddr); - backend.bindSync(backendAddr); - - push.connect(frontendAddr); - pull.connect(backendAddr); - testutil.push_sockets(frontend, backend, push, pull); - - pull.on('message',function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo'); - done(); - }); - - setTimeout(function() { - push.send('foo'); - }, 100.0); - - zmq.proxy(frontend,backend); - - }); - - it('should proxy pull-push connected to push-pull with capture',function (done) { - - var frontend = zmq.socket('push'); - var backend = zmq.socket('pull'); - - var capture = zmq.socket('pub'); - var capSub = zmq.socket('sub'); - - var pull = zmq.socket('pull'); - var push = zmq.socket('push'); - testutil.push_sockets(frontend, backend, push, pull, capture, capSub); - - frontend.bindSync(frontendAddr); - backend.bindSync(backendAddr); - capture.bindSync(captureAddr); - - pull.connect(frontendAddr); - push.connect(backendAddr); - capSub.connect(captureAddr); - - var countdown = testutil.done_countdown(done, 2); - - pull.on('message',function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo'); - countdown(); - }); - - capSub.subscribe(''); - capSub.on('message',function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo'); - countdown(); - }); - - setTimeout(function() { - push.send('foo'); - }, 100.0); - - zmq.proxy(frontend,backend,capture); - - }); -}); diff --git a/test/zmq_proxy.router-dealer.js b/test/zmq_proxy.router-dealer.js deleted file mode 100644 index d1030735..00000000 --- a/test/zmq_proxy.router-dealer.js +++ /dev/null @@ -1,93 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -var addr = 'tcp://127.0.0.1' - , frontendAddr = addr+':5504' - , backendAddr = addr+':5505' - , captureAddr = addr+':5506'; - -var testutil = require('./util'); - -describe('proxy.router-dealer', function() { - afterEach(testutil.cleanup); - - it('should proxy req-rep connected over router-dealer', function (done){ - - var frontend = zmq.socket('router'); - var backend = zmq.socket('dealer'); - - var rep = zmq.socket('rep'); - var req = zmq.socket('req'); - - frontend.bindSync(frontendAddr); - backend.bindSync(backendAddr); - - req.connect(frontendAddr); - rep.connect(backendAddr); - testutil.push_sockets(frontend, backend, req, rep); - - req.on('message',function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo bar'); - done(); - }); - - rep.on('message', function (msg) { - rep.send(msg+' bar'); - }); - - setTimeout(function() { - req.send('foo'); - }, 100.0); - - zmq.proxy(frontend,backend); - - }); - - it('should proxy rep-req connections with capture', function (done){ - - var frontend = zmq.socket('router'); - var backend = zmq.socket('dealer'); - - var rep = zmq.socket('rep'); - var req = zmq.socket('req'); - - var capture = zmq.socket('pub'); - var capSub = zmq.socket('sub'); - - frontend.bindSync(frontendAddr); - backend.bindSync(backendAddr); - capture.bindSync(captureAddr); - - req.connect(frontendAddr); - rep.connect(backendAddr); - capSub.connect(captureAddr); - capSub.subscribe(''); - testutil.push_sockets(frontend, backend, req, rep, capture, capSub); - var countdown = testutil.done_countdown(done, 2); - - req.on('message',function (msg) { - countdown(); - }); - - rep.on('message', function (msg) { - rep.send(msg+' bar'); - }); - - capSub.on('message',function (msg) { - setTimeout(function() { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo bar'); - countdown(); - },100.0) - }); - - setTimeout(function() { - req.send('foo'); - },200.0) - - zmq.proxy(frontend,backend,capture); - - }); -}); diff --git a/test/zmq_proxy.xpub-xsub.js b/test/zmq_proxy.xpub-xsub.js deleted file mode 100644 index b42c60d4..00000000 --- a/test/zmq_proxy.xpub-xsub.js +++ /dev/null @@ -1,149 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -var addr = 'tcp://127.0.0.1' - , frontendAddr = addr+':5507' - , backendAddr = addr+':5508' - , captureAddr = addr+':5509'; - -var version = semver.gte(zmq.version, '3.1.0'); -var testutil = require('./util'); - -describe('proxy.xpub-xsub', function() { - afterEach(testutil.cleanup); - - it('should proxy pub-sub connected to xpub-xsub', function (done) { - if (!version) { - done(); - return console.warn('Test requires libzmq >= 3.1.0'); - } - - var frontend = zmq.socket('xpub'); - var backend = zmq.socket('xsub'); - - var sub = zmq.socket('sub'); - var pub = zmq.socket('pub'); - testutil.push_sockets(frontend, backend, sub, pub); - - sub.subscribe(''); - sub.on('message',function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo'); - console.log(msg.toString()); - done(); - }); - - frontend.bind(frontendAddr, function (error) { - if (error) throw error; - backend.bind(backendAddr, function (error) { - if (error) throw error; - - sub.connect(frontendAddr); - pub.connect(backendAddr); - - setTimeout(function() { - pub.send('foo'); - }, 500); - - setTimeout(function () { - throw Error("Timeout"); - }, 10000); - - zmq.proxy(frontend, backend); - - }); - }); - }); - - it('should proxy connections with capture', function (done) { - if (!version) { - done(); - return console.warn('Test requires libzmq >= 3.1.0'); - } - - var frontend = zmq.socket('xpub'); - var backend = zmq.socket('xsub'); - - var capture = zmq.socket('pub'); - var capSub = zmq.socket('sub'); - - var sub = zmq.socket('sub'); - var pub = zmq.socket('pub'); - testutil.push_sockets(frontend, backend, sub, pub, capture, capSub); - - var countdown = testutil.done_countdown(done, 2); - - sub.subscribe(''); - sub.on('message', function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo'); - countdown(); - }); - - capSub.subscribe(''); - capSub.on('message',function (msg) { - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo'); - countdown(); - }); - - capture.bind(captureAddr, function (error) { - if (error) throw error; - frontend.bind(frontendAddr, function (error) { - if (error) throw error; - backend.bind(backendAddr, function (error) { - if (error) throw error; - - pub.connect(backendAddr); - sub.connect(frontendAddr); - capSub.connect(captureAddr); - - setTimeout(function () { - pub.send('foo'); - }, 500); - - setTimeout(function () { - throw Error("Timeout"); - }, 10000); - - zmq.proxy(frontend,backend,capture); - }); - }); - }); - }); - - it('should throw an error if the order is wrong', function (done) { - if (!version) { - done(); - return console.warn('Test requires libzmq >= 3.1.0'); - } - - var frontend = zmq.socket('xpub'); - var backend = zmq.socket('xsub'); - - var sub = zmq.socket('sub'); - var pub = zmq.socket('pub'); - - frontend.bindSync(frontendAddr); - backend.bindSync(backendAddr); - - sub.connect(frontendAddr); - pub.connect(backendAddr); - testutil.push_sockets(frontend, backend, sub, pub); - - try{ - - zmq.proxy(backend,frontend); - - } catch(e){ - - e.message.should.equal('wrong socket order to proxy'); - - } finally{ - - done(); - - } - }) -}); diff --git a/test/zmq_proxy.xrep-xreq.js b/test/zmq_proxy.xrep-xreq.js deleted file mode 100644 index 99ecf0fd..00000000 --- a/test/zmq_proxy.xrep-xreq.js +++ /dev/null @@ -1,52 +0,0 @@ -var zmq = require('..') - , should = require('should') - , semver = require('semver'); - -var addr = 'tcp://127.0.0.1' - , frontendAddr = addr+':5510' - , backendAddr = addr+':5511' - , captureAddr = addr+':5512'; - -//since its for libzmq2, we target versions < 3.0.0 -var version = semver.lte(zmq.version, '3.0.0'); -var testutil = require('./util'); - -describe('proxy.xrep-xreq', function() { - afterEach(testutil.cleanup); - - it('should proxy req-rep connected to xrep-xreq', function (done) { - if (!version) { - console.warn('Test requires libzmq v2 (skipping)'); - return done(); - } - - var frontend = zmq.socket('xrep'); - var backend = zmq.socket('xreq'); - - var req = zmq.socket('req'); - var rep = zmq.socket('rep'); - - frontend.bindSync(frontendAddr); - backend.bindSync(backendAddr); - - req.connect(frontendAddr); - rep.connect(backendAddr); - testutil.push_sockets(frontend, backend, req, rep); - - req.on('message',function(msg){ - msg.should.be.an.instanceof(Buffer); - msg.toString().should.equal('foo bar'); - done(); - }); - - rep.on('message', function (msg) { - rep.send(msg+' bar'); - }); - - setTimeout(function() { - req.send('foo'); - }, 100.0); - - zmq.proxy(frontend,backend); - }); -}); diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tsconfig-build.json b/tsconfig-build.json new file mode 100644 index 00000000..a9fead6c --- /dev/null +++ b/tsconfig-build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib", + "noUnusedLocals": true, + "noUnusedParameters": true + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..3cd298b3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "exclude": ["*.d.ts"], + "compilerOptions": { + "allowJs": false, + "target": "es2017", + "outDir": "lib", + "declaration": true, + "module": "commonjs", + "types": ["node", "mocha"], + "lib": ["es2017", "esnext.asynciterable"], + "downlevelIteration": true, + "strict": true, + "strictPropertyInitialization": false + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..171238e4 --- /dev/null +++ b/tslint.json @@ -0,0 +1,21 @@ +{ + "defaultSeverity": "error", + "extends": ["tslint:recommended"], + "rules": { + "arrow-parens": [true], + "curly": [true, "ignore-same-line"], + "interface-name": [true, "never-prefix"], + "linebreak-style": [true, "LF"], + "max-classes-per-file": [true, 30], + "max-line-length": [true, {"limit": 90, "check-strings": true}], + "member-access": [true, "no-public"], + "no-bitwise": [false], + "no-consecutive-blank-lines": [true, 2], + "no-empty-interface": [false], + "no-switch-case-fall-through": [true], + "ordered-imports": [true, {"named-imports-order": "lowercase-first"}], + "object-literal-sort-keys": [true, "match-declaration-order-only"], + "prefer-const": [true, {"destructuring": "all"}], + "semicolon": [true, "never"] + } +} \ No newline at end of file diff --git a/v5-compat.d.ts b/v5-compat.d.ts new file mode 100644 index 00000000..cb00807b --- /dev/null +++ b/v5-compat.d.ts @@ -0,0 +1 @@ +export * from "./lib/compat" diff --git a/v5-compat.js b/v5-compat.js new file mode 100644 index 00000000..e716eaa0 --- /dev/null +++ b/v5-compat.js @@ -0,0 +1 @@ +module.exports = require("./lib/compat") diff --git a/windows/include/zmq.h b/windows/include/zmq.h deleted file mode 100644 index 0f466450..00000000 --- a/windows/include/zmq.h +++ /dev/null @@ -1,631 +0,0 @@ -/* - Copyright (c) 2007-2016 Contributors as noted in the AUTHORS file - - This file is part of libzmq, the ZeroMQ core engine in C++. - - libzmq is free software; you can redistribute it and/or modify it under - the terms of the GNU Lesser General Public License (LGPL) as published - by the Free Software Foundation; either version 3 of the License, or - (at your option) any later version. - - As a special exception, the Contributors give you permission to link - this library with independent modules to produce an executable, - regardless of the license terms of these independent modules, and to - copy and distribute the resulting executable under terms of your choice, - provided that you also meet, for each linked independent module, the - terms and conditions of the license of that module. An independent - module is a module which is not derived from or based on this library. - If you modify this library, you must extend this exception to your - version of the library. - - libzmq is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public - License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with this program. If not, see . - - ************************************************************************* - NOTE to contributors. This file comprises the principal public contract - for ZeroMQ API users. Any change to this file supplied in a stable - release SHOULD not break existing applications. - In practice this means that the value of constants must not change, and - that old values may not be reused for new constants. - ************************************************************************* -*/ - -#ifndef __ZMQ_H_INCLUDED__ -#define __ZMQ_H_INCLUDED__ - -/* Version macros for compile-time API version detection */ -#define ZMQ_VERSION_MAJOR 4 -#define ZMQ_VERSION_MINOR 2 -#define ZMQ_VERSION_PATCH 0 - -#define ZMQ_MAKE_VERSION(major, minor, patch) \ - ((major) * 10000 + (minor) * 100 + (patch)) -#define ZMQ_VERSION \ - ZMQ_MAKE_VERSION(ZMQ_VERSION_MAJOR, ZMQ_VERSION_MINOR, ZMQ_VERSION_PATCH) - -#ifdef __cplusplus -extern "C" { -#endif - -#if !defined _WIN32_WCE -#include -#endif -#include -#include -#if defined _WIN32 -// Set target version to Windows Server 2008, Windows Vista or higher. -// Windows XP (0x0501) is supported but without client & server socket types. -#ifndef _WIN32_WINNT -#define _WIN32_WINNT 0x0600 -#endif - -#ifdef __MINGW32__ -// Require Windows XP or higher with MinGW for getaddrinfo(). -#if(_WIN32_WINNT >= 0x0600) -#else -#undef _WIN32_WINNT -#define _WIN32_WINNT 0x0600 -#endif -#endif -#include -#endif - -/* Handle DSO symbol visibility */ -#if defined _WIN32 -# if defined ZMQ_STATIC -# define ZMQ_EXPORT -# elif defined DLL_EXPORT -# define ZMQ_EXPORT __declspec(dllexport) -# else -# define ZMQ_EXPORT __declspec(dllimport) -# endif -#else -# if defined __SUNPRO_C || defined __SUNPRO_CC -# define ZMQ_EXPORT __global -# elif (defined __GNUC__ && __GNUC__ >= 4) || defined __INTEL_COMPILER -# define ZMQ_EXPORT __attribute__ ((visibility("default"))) -# else -# define ZMQ_EXPORT -# endif -#endif - -/* Define integer types needed for event interface */ -#define ZMQ_DEFINED_STDINT 1 -#if defined ZMQ_HAVE_SOLARIS || defined ZMQ_HAVE_OPENVMS -# include -#elif defined _MSC_VER && _MSC_VER < 1600 -# ifndef int32_t - typedef __int32 int32_t; -# endif -# ifndef uint16_t - typedef unsigned __int16 uint16_t; -# endif -# ifndef uint8_t - typedef unsigned __int8 uint8_t; -# endif -#else -# include -#endif - - -/******************************************************************************/ -/* 0MQ errors. */ -/******************************************************************************/ - -/* A number random enough not to collide with different errno ranges on */ -/* different OSes. The assumption is that error_t is at least 32-bit type. */ -#define ZMQ_HAUSNUMERO 156384712 - -/* On Windows platform some of the standard POSIX errnos are not defined. */ -#ifndef ENOTSUP -#define ENOTSUP (ZMQ_HAUSNUMERO + 1) -#endif -#ifndef EPROTONOSUPPORT -#define EPROTONOSUPPORT (ZMQ_HAUSNUMERO + 2) -#endif -#ifndef ENOBUFS -#define ENOBUFS (ZMQ_HAUSNUMERO + 3) -#endif -#ifndef ENETDOWN -#define ENETDOWN (ZMQ_HAUSNUMERO + 4) -#endif -#ifndef EADDRINUSE -#define EADDRINUSE (ZMQ_HAUSNUMERO + 5) -#endif -#ifndef EADDRNOTAVAIL -#define EADDRNOTAVAIL (ZMQ_HAUSNUMERO + 6) -#endif -#ifndef ECONNREFUSED -#define ECONNREFUSED (ZMQ_HAUSNUMERO + 7) -#endif -#ifndef EINPROGRESS -#define EINPROGRESS (ZMQ_HAUSNUMERO + 8) -#endif -#ifndef ENOTSOCK -#define ENOTSOCK (ZMQ_HAUSNUMERO + 9) -#endif -#ifndef EMSGSIZE -#define EMSGSIZE (ZMQ_HAUSNUMERO + 10) -#endif -#ifndef EAFNOSUPPORT -#define EAFNOSUPPORT (ZMQ_HAUSNUMERO + 11) -#endif -#ifndef ENETUNREACH -#define ENETUNREACH (ZMQ_HAUSNUMERO + 12) -#endif -#ifndef ECONNABORTED -#define ECONNABORTED (ZMQ_HAUSNUMERO + 13) -#endif -#ifndef ECONNRESET -#define ECONNRESET (ZMQ_HAUSNUMERO + 14) -#endif -#ifndef ENOTCONN -#define ENOTCONN (ZMQ_HAUSNUMERO + 15) -#endif -#ifndef ETIMEDOUT -#define ETIMEDOUT (ZMQ_HAUSNUMERO + 16) -#endif -#ifndef EHOSTUNREACH -#define EHOSTUNREACH (ZMQ_HAUSNUMERO + 17) -#endif -#ifndef ENETRESET -#define ENETRESET (ZMQ_HAUSNUMERO + 18) -#endif - -/* Native 0MQ error codes. */ -#define EFSM (ZMQ_HAUSNUMERO + 51) -#define ENOCOMPATPROTO (ZMQ_HAUSNUMERO + 52) -#define ETERM (ZMQ_HAUSNUMERO + 53) -#define EMTHREAD (ZMQ_HAUSNUMERO + 54) - -/* This function retrieves the errno as it is known to 0MQ library. The goal */ -/* of this function is to make the code 100% portable, including where 0MQ */ -/* compiled with certain CRT library (on Windows) is linked to an */ -/* application that uses different CRT library. */ -ZMQ_EXPORT int zmq_errno (void); - -/* Resolves system errors and 0MQ errors to human-readable string. */ -ZMQ_EXPORT const char *zmq_strerror (int errnum); - -/* Run-time API version detection */ -ZMQ_EXPORT void zmq_version (int *major, int *minor, int *patch); - -/******************************************************************************/ -/* 0MQ infrastructure (a.k.a. context) initialisation & termination. */ -/******************************************************************************/ - -/* Context options */ -#define ZMQ_IO_THREADS 1 -#define ZMQ_MAX_SOCKETS 2 -#define ZMQ_SOCKET_LIMIT 3 -#define ZMQ_THREAD_PRIORITY 3 -#define ZMQ_THREAD_SCHED_POLICY 4 -#define ZMQ_MAX_MSGSZ 5 - -/* Default for new contexts */ -#define ZMQ_IO_THREADS_DFLT 1 -#define ZMQ_MAX_SOCKETS_DFLT 1023 -#define ZMQ_THREAD_PRIORITY_DFLT -1 -#define ZMQ_THREAD_SCHED_POLICY_DFLT -1 - -ZMQ_EXPORT void *zmq_ctx_new (void); -ZMQ_EXPORT int zmq_ctx_term (void *context); -ZMQ_EXPORT int zmq_ctx_shutdown (void *context); -ZMQ_EXPORT int zmq_ctx_set (void *context, int option, int optval); -ZMQ_EXPORT int zmq_ctx_get (void *context, int option); - -/* Old (legacy) API */ -ZMQ_EXPORT void *zmq_init (int io_threads); -ZMQ_EXPORT int zmq_term (void *context); -ZMQ_EXPORT int zmq_ctx_destroy (void *context); - - -/******************************************************************************/ -/* 0MQ message definition. */ -/******************************************************************************/ - -/* Some architectures, like sparc64 and some variants of aarch64, enforce pointer - * alignment and raise sigbus on violations. Make sure applications allocate - * zmq_msg_t on addresses aligned on a pointer-size boundary to avoid this issue. - */ -typedef struct zmq_msg_t { -#if defined (__GNUC__) || defined ( __INTEL_COMPILER) || \ - (defined (__SUNPRO_C) && __SUNPRO_C >= 0x590) || \ - (defined (__SUNPRO_CC) && __SUNPRO_CC >= 0x590) - unsigned char _ [64] __attribute__ ((aligned (sizeof (void *)))); -#elif defined (_MSC_VER) && (defined (_M_X64) || defined (_M_ARM64)) - __declspec (align (8)) unsigned char _ [64]; -#elif defined (_MSC_VER) && (defined (_M_IX86) || defined (_M_ARM_ARMV7VE)) - __declspec (align (4)) unsigned char _ [64]; -#else - unsigned char _ [64]; -#endif -} zmq_msg_t; - -typedef void (zmq_free_fn) (void *data, void *hint); - -ZMQ_EXPORT int zmq_msg_init (zmq_msg_t *msg); -ZMQ_EXPORT int zmq_msg_init_size (zmq_msg_t *msg, size_t size); -ZMQ_EXPORT int zmq_msg_init_data (zmq_msg_t *msg, void *data, - size_t size, zmq_free_fn *ffn, void *hint); -ZMQ_EXPORT int zmq_msg_send (zmq_msg_t *msg, void *s, int flags); -ZMQ_EXPORT int zmq_msg_recv (zmq_msg_t *msg, void *s, int flags); -ZMQ_EXPORT int zmq_msg_close (zmq_msg_t *msg); -ZMQ_EXPORT int zmq_msg_move (zmq_msg_t *dest, zmq_msg_t *src); -ZMQ_EXPORT int zmq_msg_copy (zmq_msg_t *dest, zmq_msg_t *src); -ZMQ_EXPORT void *zmq_msg_data (zmq_msg_t *msg); -ZMQ_EXPORT size_t zmq_msg_size (zmq_msg_t *msg); -ZMQ_EXPORT int zmq_msg_more (zmq_msg_t *msg); -ZMQ_EXPORT int zmq_msg_get (zmq_msg_t *msg, int property); -ZMQ_EXPORT int zmq_msg_set (zmq_msg_t *msg, int property, int optval); -ZMQ_EXPORT const char *zmq_msg_gets (zmq_msg_t *msg, const char *property); - -/******************************************************************************/ -/* 0MQ socket definition. */ -/******************************************************************************/ - -/* Socket types. */ -#define ZMQ_PAIR 0 -#define ZMQ_PUB 1 -#define ZMQ_SUB 2 -#define ZMQ_REQ 3 -#define ZMQ_REP 4 -#define ZMQ_DEALER 5 -#define ZMQ_ROUTER 6 -#define ZMQ_PULL 7 -#define ZMQ_PUSH 8 -#define ZMQ_XPUB 9 -#define ZMQ_XSUB 10 -#define ZMQ_STREAM 11 - -/* Deprecated aliases */ -#define ZMQ_XREQ ZMQ_DEALER -#define ZMQ_XREP ZMQ_ROUTER - -/* Socket options. */ -#define ZMQ_AFFINITY 4 -#define ZMQ_IDENTITY 5 -#define ZMQ_SUBSCRIBE 6 -#define ZMQ_UNSUBSCRIBE 7 -#define ZMQ_RATE 8 -#define ZMQ_RECOVERY_IVL 9 -#define ZMQ_SNDBUF 11 -#define ZMQ_RCVBUF 12 -#define ZMQ_RCVMORE 13 -#define ZMQ_FD 14 -#define ZMQ_EVENTS 15 -#define ZMQ_TYPE 16 -#define ZMQ_LINGER 17 -#define ZMQ_RECONNECT_IVL 18 -#define ZMQ_BACKLOG 19 -#define ZMQ_RECONNECT_IVL_MAX 21 -#define ZMQ_MAXMSGSIZE 22 -#define ZMQ_SNDHWM 23 -#define ZMQ_RCVHWM 24 -#define ZMQ_MULTICAST_HOPS 25 -#define ZMQ_RCVTIMEO 27 -#define ZMQ_SNDTIMEO 28 -#define ZMQ_LAST_ENDPOINT 32 -#define ZMQ_ROUTER_MANDATORY 33 -#define ZMQ_TCP_KEEPALIVE 34 -#define ZMQ_TCP_KEEPALIVE_CNT 35 -#define ZMQ_TCP_KEEPALIVE_IDLE 36 -#define ZMQ_TCP_KEEPALIVE_INTVL 37 -#define ZMQ_IMMEDIATE 39 -#define ZMQ_XPUB_VERBOSE 40 -#define ZMQ_ROUTER_RAW 41 -#define ZMQ_IPV6 42 -#define ZMQ_MECHANISM 43 -#define ZMQ_PLAIN_SERVER 44 -#define ZMQ_PLAIN_USERNAME 45 -#define ZMQ_PLAIN_PASSWORD 46 -#define ZMQ_CURVE_SERVER 47 -#define ZMQ_CURVE_PUBLICKEY 48 -#define ZMQ_CURVE_SECRETKEY 49 -#define ZMQ_CURVE_SERVERKEY 50 -#define ZMQ_PROBE_ROUTER 51 -#define ZMQ_REQ_CORRELATE 52 -#define ZMQ_REQ_RELAXED 53 -#define ZMQ_CONFLATE 54 -#define ZMQ_ZAP_DOMAIN 55 -#define ZMQ_ROUTER_HANDOVER 56 -#define ZMQ_TOS 57 -#define ZMQ_CONNECT_RID 61 -#define ZMQ_GSSAPI_SERVER 62 -#define ZMQ_GSSAPI_PRINCIPAL 63 -#define ZMQ_GSSAPI_SERVICE_PRINCIPAL 64 -#define ZMQ_GSSAPI_PLAINTEXT 65 -#define ZMQ_HANDSHAKE_IVL 66 -#define ZMQ_SOCKS_PROXY 68 -#define ZMQ_XPUB_NODROP 69 -#define ZMQ_BLOCKY 70 -#define ZMQ_XPUB_MANUAL 71 -#define ZMQ_XPUB_WELCOME_MSG 72 -#define ZMQ_STREAM_NOTIFY 73 -#define ZMQ_INVERT_MATCHING 74 -#define ZMQ_HEARTBEAT_IVL 75 -#define ZMQ_HEARTBEAT_TTL 76 -#define ZMQ_HEARTBEAT_TIMEOUT 77 -#define ZMQ_XPUB_VERBOSER 78 -#define ZMQ_CONNECT_TIMEOUT 79 -#define ZMQ_TCP_MAXRT 80 -#define ZMQ_THREAD_SAFE 81 -#define ZMQ_MULTICAST_MAXTPDU 84 -#define ZMQ_VMCI_BUFFER_SIZE 85 -#define ZMQ_VMCI_BUFFER_MIN_SIZE 86 -#define ZMQ_VMCI_BUFFER_MAX_SIZE 87 -#define ZMQ_VMCI_CONNECT_TIMEOUT 88 -#define ZMQ_USE_FD 89 -// All options after this is for version 4.3 and still *draft* -// Subject to arbitrary change without notice - -/* Message options */ -#define ZMQ_MORE 1 -#define ZMQ_SHARED 3 - -/* Send/recv options. */ -#define ZMQ_DONTWAIT 1 -#define ZMQ_SNDMORE 2 - -/* Security mechanisms */ -#define ZMQ_NULL 0 -#define ZMQ_PLAIN 1 -#define ZMQ_CURVE 2 -#define ZMQ_GSSAPI 3 - -/* RADIO-DISH protocol */ -#define ZMQ_GROUP_MAX_LENGTH 15 - -/* Deprecated options and aliases */ -#define ZMQ_TCP_ACCEPT_FILTER 38 -#define ZMQ_IPC_FILTER_PID 58 -#define ZMQ_IPC_FILTER_UID 59 -#define ZMQ_IPC_FILTER_GID 60 -#define ZMQ_IPV4ONLY 31 -#define ZMQ_DELAY_ATTACH_ON_CONNECT ZMQ_IMMEDIATE -#define ZMQ_NOBLOCK ZMQ_DONTWAIT -#define ZMQ_FAIL_UNROUTABLE ZMQ_ROUTER_MANDATORY -#define ZMQ_ROUTER_BEHAVIOR ZMQ_ROUTER_MANDATORY - -/* Deprecated Message options */ -#define ZMQ_SRCFD 2 - -/******************************************************************************/ -/* 0MQ socket events and monitoring */ -/******************************************************************************/ - -/* Socket transport events (TCP, IPC and TIPC only) */ - -#define ZMQ_EVENT_CONNECTED 0x0001 -#define ZMQ_EVENT_CONNECT_DELAYED 0x0002 -#define ZMQ_EVENT_CONNECT_RETRIED 0x0004 -#define ZMQ_EVENT_LISTENING 0x0008 -#define ZMQ_EVENT_BIND_FAILED 0x0010 -#define ZMQ_EVENT_ACCEPTED 0x0020 -#define ZMQ_EVENT_ACCEPT_FAILED 0x0040 -#define ZMQ_EVENT_CLOSED 0x0080 -#define ZMQ_EVENT_CLOSE_FAILED 0x0100 -#define ZMQ_EVENT_DISCONNECTED 0x0200 -#define ZMQ_EVENT_MONITOR_STOPPED 0x0400 -#define ZMQ_EVENT_ALL 0xFFFF - -ZMQ_EXPORT void *zmq_socket (void *, int type); -ZMQ_EXPORT int zmq_close (void *s); -ZMQ_EXPORT int zmq_setsockopt (void *s, int option, const void *optval, - size_t optvallen); -ZMQ_EXPORT int zmq_getsockopt (void *s, int option, void *optval, - size_t *optvallen); -ZMQ_EXPORT int zmq_bind (void *s, const char *addr); -ZMQ_EXPORT int zmq_connect (void *s, const char *addr); -ZMQ_EXPORT int zmq_unbind (void *s, const char *addr); -ZMQ_EXPORT int zmq_disconnect (void *s, const char *addr); -ZMQ_EXPORT int zmq_send (void *s, const void *buf, size_t len, int flags); -ZMQ_EXPORT int zmq_send_const (void *s, const void *buf, size_t len, int flags); -ZMQ_EXPORT int zmq_recv (void *s, void *buf, size_t len, int flags); -ZMQ_EXPORT int zmq_socket_monitor (void *s, const char *addr, int events); - - -/******************************************************************************/ -/* I/O multiplexing. */ -/******************************************************************************/ - -#define ZMQ_POLLIN 1 -#define ZMQ_POLLOUT 2 -#define ZMQ_POLLERR 4 -#define ZMQ_POLLPRI 8 - -typedef struct zmq_pollitem_t -{ - void *socket; -#if defined _WIN32 - SOCKET fd; -#else - int fd; -#endif - short events; - short revents; -} zmq_pollitem_t; - -#define ZMQ_POLLITEMS_DFLT 16 - -ZMQ_EXPORT int zmq_poll (zmq_pollitem_t *items, int nitems, long timeout); - -/******************************************************************************/ -/* Message proxying */ -/******************************************************************************/ - -ZMQ_EXPORT int zmq_proxy (void *frontend, void *backend, void *capture); -ZMQ_EXPORT int zmq_proxy_steerable (void *frontend, void *backend, void *capture, void *control); - -/******************************************************************************/ -/* Probe library capabilities */ -/******************************************************************************/ - -#define ZMQ_HAS_CAPABILITIES 1 -ZMQ_EXPORT int zmq_has (const char *capability); - -/* Deprecated aliases */ -#define ZMQ_STREAMER 1 -#define ZMQ_FORWARDER 2 -#define ZMQ_QUEUE 3 - -/* Deprecated methods */ -ZMQ_EXPORT int zmq_device (int type, void *frontend, void *backend); -ZMQ_EXPORT int zmq_sendmsg (void *s, zmq_msg_t *msg, int flags); -ZMQ_EXPORT int zmq_recvmsg (void *s, zmq_msg_t *msg, int flags); -struct iovec; -ZMQ_EXPORT int zmq_sendiov (void *s, struct iovec *iov, size_t count, int flags); -ZMQ_EXPORT int zmq_recviov (void *s, struct iovec *iov, size_t *count, int flags); - -/******************************************************************************/ -/* Encryption functions */ -/******************************************************************************/ - -/* Encode data with Z85 encoding. Returns encoded data */ -ZMQ_EXPORT char *zmq_z85_encode (char *dest, const uint8_t *data, size_t size); - -/* Decode data with Z85 encoding. Returns decoded data */ -ZMQ_EXPORT uint8_t *zmq_z85_decode (uint8_t *dest, const char *string); - -/* Generate z85-encoded public and private keypair with tweetnacl/libsodium. */ -/* Returns 0 on success. */ -ZMQ_EXPORT int zmq_curve_keypair (char *z85_public_key, char *z85_secret_key); - -/* Derive the z85-encoded public key from the z85-encoded secret key. */ -/* Returns 0 on success. */ -ZMQ_EXPORT int zmq_curve_public (char *z85_public_key, const char *z85_secret_key); - -/******************************************************************************/ -/* Atomic utility methods */ -/******************************************************************************/ - -ZMQ_EXPORT void *zmq_atomic_counter_new (void); -ZMQ_EXPORT void zmq_atomic_counter_set (void *counter, int value); -ZMQ_EXPORT int zmq_atomic_counter_inc (void *counter); -ZMQ_EXPORT int zmq_atomic_counter_dec (void *counter); -ZMQ_EXPORT int zmq_atomic_counter_value (void *counter); -ZMQ_EXPORT void zmq_atomic_counter_destroy (void **counter_p); - - -/******************************************************************************/ -/* These functions are not documented by man pages -- use at your own risk. */ -/* If you need these to be part of the formal ZMQ API, then (a) write a man */ -/* page, and (b) write a test case in tests. */ -/******************************************************************************/ - -/* Helper functions are used by perf tests so that they don't have to care */ -/* about minutiae of time-related functions on different OS platforms. */ - -/* Starts the stopwatch. Returns the handle to the watch. */ -ZMQ_EXPORT void *zmq_stopwatch_start (void); - -/* Stops the stopwatch. Returns the number of microseconds elapsed since */ -/* the stopwatch was started. */ -ZMQ_EXPORT unsigned long zmq_stopwatch_stop (void *watch_); - -/* Sleeps for specified number of seconds. */ -ZMQ_EXPORT void zmq_sleep (int seconds_); - -typedef void (zmq_thread_fn) (void*); - -/* Start a thread. Returns a handle to the thread. */ -ZMQ_EXPORT void *zmq_threadstart (zmq_thread_fn* func, void* arg); - -/* Wait for thread to complete then free up resources. */ -ZMQ_EXPORT void zmq_threadclose (void* thread); - - -/******************************************************************************/ -/* These functions are DRAFT and disabled in stable releases, and subject to */ -/* change at ANY time until declared stable. */ -/******************************************************************************/ - -#ifdef ZMQ_BUILD_DRAFT_API - -/* DRAFT Socket types. */ -#define ZMQ_SERVER 12 -#define ZMQ_CLIENT 13 -#define ZMQ_RADIO 14 -#define ZMQ_DISH 15 -#define ZMQ_GATHER 16 -#define ZMQ_SCATTER 17 -#define ZMQ_DGRAM 18 - -/* DRAFT Socket methods. */ -ZMQ_EXPORT int zmq_join (void *s, const char *group); -ZMQ_EXPORT int zmq_leave (void *s, const char *group); - -/* DRAFT Msg methods. */ -ZMQ_EXPORT int zmq_msg_set_routing_id(zmq_msg_t *msg, uint32_t routing_id); -ZMQ_EXPORT uint32_t zmq_msg_routing_id(zmq_msg_t *msg); -ZMQ_EXPORT int zmq_msg_set_group(zmq_msg_t *msg, const char *group); -ZMQ_EXPORT const char *zmq_msg_group(zmq_msg_t *msg); - -/******************************************************************************/ -/* Poller polling on sockets,fd and thread-safe sockets */ -/******************************************************************************/ - -#define ZMQ_HAVE_POLLER - -typedef struct zmq_poller_event_t -{ - void *socket; -#if defined _WIN32 - SOCKET fd; -#else - int fd; -#endif - void *user_data; - short events; -} zmq_poller_event_t; - -ZMQ_EXPORT void *zmq_poller_new (void); -ZMQ_EXPORT int zmq_poller_destroy (void **poller_p); -ZMQ_EXPORT int zmq_poller_add (void *poller, void *socket, void *user_data, short events); -ZMQ_EXPORT int zmq_poller_modify (void *poller, void *socket, short events); -ZMQ_EXPORT int zmq_poller_remove (void *poller, void *socket); -ZMQ_EXPORT int zmq_poller_wait (void *poller, zmq_poller_event_t *event, long timeout); -ZMQ_EXPORT int zmq_poller_wait_all (void *poller, zmq_poller_event_t *events, int n_events, long timeout); - -#if defined _WIN32 -ZMQ_EXPORT int zmq_poller_add_fd (void *poller, SOCKET fd, void *user_data, short events); -ZMQ_EXPORT int zmq_poller_modify_fd (void *poller, SOCKET fd, short events); -ZMQ_EXPORT int zmq_poller_remove_fd (void *poller, SOCKET fd); -#else -ZMQ_EXPORT int zmq_poller_add_fd (void *poller, int fd, void *user_data, short events); -ZMQ_EXPORT int zmq_poller_modify_fd (void *poller, int fd, short events); -ZMQ_EXPORT int zmq_poller_remove_fd (void *poller, int fd); -#endif - -/******************************************************************************/ -/* Scheduling timers */ -/******************************************************************************/ - -#define ZMQ_HAVE_TIMERS - -typedef void (zmq_timer_fn)(int timer_id, void *arg); - -ZMQ_EXPORT void *zmq_timers_new (void); -ZMQ_EXPORT int zmq_timers_destroy (void **timers_p); -ZMQ_EXPORT int zmq_timers_add (void *timers, size_t interval, zmq_timer_fn handler, void *arg); -ZMQ_EXPORT int zmq_timers_cancel (void *timers, int timer_id); -ZMQ_EXPORT int zmq_timers_set_interval (void *timers, int timer_id, size_t interval); -ZMQ_EXPORT int zmq_timers_reset (void *timers, int timer_id); -ZMQ_EXPORT long zmq_timers_timeout (void *timers); -ZMQ_EXPORT int zmq_timers_execute (void *timers); - -#endif // ZMQ_BUILD_DRAFT_API - - -#undef ZMQ_EXPORT - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/windows/include/zmq_utils.h b/windows/include/zmq_utils.h deleted file mode 100644 index f29638d5..00000000 --- a/windows/include/zmq_utils.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright (c) 2007-2016 Contributors as noted in the AUTHORS file - - This file is part of libzmq, the ZeroMQ core engine in C++. - - libzmq is free software; you can redistribute it and/or modify it under - the terms of the GNU Lesser General Public License (LGPL) as published - by the Free Software Foundation; either version 3 of the License, or - (at your option) any later version. - - As a special exception, the Contributors give you permission to link - this library with independent modules to produce an executable, - regardless of the license terms of these independent modules, and to - copy and distribute the resulting executable under terms of your choice, - provided that you also meet, for each linked independent module, the - terms and conditions of the license of that module. An independent - module is a module which is not derived from or based on this library. - If you modify this library, you must extend this exception to your - version of the library. - - libzmq is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public - License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with this program. If not, see . -*/ - -/* This file is deprecated, and all its functionality provided by zmq.h */ -/* Note that -Wpedantic compilation requires GCC to avoid using its custom - extensions such as #warning, hence the trick below. Also, pragmas for - warnings or other messages are not standard, not portable, and not all - compilers even have an equivalent concept. - So in the worst case, this include file is treated as silently empty. */ - -#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) || defined(_MSC_VER) -#if defined(__GNUC__) || defined(__GNUG__) -#pragma GCC diagnostic push -#pragma GCC diagnostic warning "-Wcpp" -#pragma GCC diagnostic ignored "-Werror" -#pragma GCC diagnostic ignored "-Wall" -#endif -#pragma message("Warning: zmq_utils.h is deprecated. All its functionality is provided by zmq.h.") -#if defined(__GNUC__) || defined(__GNUG__) -#pragma GCC diagnostic pop -#endif -#endif