From ec0b2d3d2d33c3e5879a2e47f746e33dcc79d889 Mon Sep 17 00:00:00 2001 From: Ryan Lubke Date: Tue, 28 Jan 2025 18:53:55 -0800 Subject: [PATCH] Changes to support resolving gRPC proxies via the Coherence name service. --- .github/workflows/{node.js.yml => build.yml} | 10 +- .github/workflows/discovery.yml | 68 +++ README.md | 38 +- etc/docker-compose-2-members.yaml | 1 + etc/jvm-args-clear.txt | 4 +- etc/jvm-args-tls.txt | 4 +- package-lock.json | 306 +++++++++++++- package.json | 8 +- src/session.ts | 420 +++++++++++++++++-- src/util.ts | 4 +- test/discovery/resolver-tests.js | 78 ++++ test/session-tests.js | 32 +- 12 files changed, 907 insertions(+), 66 deletions(-) rename .github/workflows/{node.js.yml => build.yml} (89%) create mode 100644 .github/workflows/discovery.yml create mode 100644 test/discovery/resolver-tests.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/build.yml similarity index 89% rename from .github/workflows/node.js.yml rename to .github/workflows/build.yml index af28ca0..52bc3d5 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -# Copyright 2020, 2023, Oracle Corporation and/or its affiliates. All rights reserved. +# Copyright 2020, 2025, Oracle Corporation and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl. @@ -6,7 +6,7 @@ # Coherence JavaScript Client GitHub Actions CI build. # --------------------------------------------------------------------------- -name: Node.js CI +name: JS Client Validation on: schedule: @@ -18,7 +18,7 @@ on: types: - opened branches: - - 'main' + - '*' jobs: build: @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: node-version: [18.x, 19.x, 20.x, 21.x, 22.x, 23.x] - coherence-version: [22.06.11, 24.09.2] + coherence-version: [22.06.11, 14.1.2-0-1, 24.09.2] steps: - uses: actions/checkout@v4 @@ -48,7 +48,7 @@ jobs: - run: COHERENCE_VERSION=${{ matrix.coherence-version }} npm run test-cycle-tls # clean up - name: Archive production artifacts - if: always() + if: failure() uses: actions/upload-artifact@v4 with: name: save-log-file-${{ matrix.node-version }}-${{ matrix.coherence-version }} diff --git a/.github/workflows/discovery.yml b/.github/workflows/discovery.yml new file mode 100644 index 0000000..89e1304 --- /dev/null +++ b/.github/workflows/discovery.yml @@ -0,0 +1,68 @@ +# Copyright 2025, Oracle Corporation and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at +# https://oss.oracle.com/licenses/upl. + +# --------------------------------------------------------------------------- +# Coherence JavaScript Client GitHub Actions CI build. +# --------------------------------------------------------------------------- + +name: JS Client Discovery Validation + +on: + schedule: + - cron: "0 5 * * *" + push: + branches-ignore: + - ghpages + pull_request: + types: + - opened + branches: + - '*' +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [20.x, 21.x, 22.x, 23.x] + coherence-version: [22.06.11, 14.1.2-0-1, 24.09.2] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: curl -LO "https://github.com/protocolbuffers/protobuf/releases/download/v22.2/protoc-22.2-linux-x86_64.zip" + - run: unzip protoc-22.2-linux-x86_64.zip -d /tmp/grpc + - run: echo "/tmp/grpc/bin" >> $GITHUB_PATH + - run: npm install + - run: npm run compile + + - name: Run Coherence Server + shell: bash + run: | + export COHERENCE_VERSION=${{ matrix.coherence-version }} + curl -sL https://raw.githubusercontent.com/oracle/coherence-cli/main/scripts/install.sh | bash + cohctl version + cohctl set profile grpc-cluster1 -v "-Dcoherence.grpc.server.port=10000" -y + cohctl create cluster grpc-cluster1 -P grpc-cluster1 -r 1 -v ${{ matrix.coherence-version }} -y -a coherence-grpc-proxy + cohctl set profile grpc-cluster2 -v "-Dcoherence.grpc.server.port=10001" -y + cohctl create cluster grpc-cluster2 -P grpc-cluster2 -r 1 -H 30001 -v ${{ matrix.coherence-version }} -y -a coherence-grpc-proxy + sleep 20 + cohctl monitor health -n localhost:7574 -T 40 -w + + - name: Run resolver tests + shell: bash + run: | + npm run test-resolver + + - name: Archive production artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: save-log-file-${{ matrix.node-version }}-${{ matrix.coherence-version }} + path: ~/.cohctl/logs/*.*.log diff --git a/README.md b/README.md index 3d59f40..5f7fbe0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ the network transport. * Registration of listeners to be notified of map mutations ### Requirements -* Coherence CE `22.06` or later (or equivalent non-open source editions) with a configured [gRPC Proxy](https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.2206/develop-remote-clients/using-coherence-grpc-server.html) +* Coherence CE versions `22.06`, `14.1.2-0-0`, `24.09` or later (or equivalent non-open source editions) with a configured [gRPC Proxy](https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.2206/develop-remote-clients/using-coherence-grpc-server.html) * Node `18.15.x` or later * NPM `9.x` or later @@ -40,7 +40,7 @@ For more details on the image, see the [documentation](https://github.com/oracle ### Declare Your Dependency -To use the Coherence gRPC JavaScript Client, simply declare it as a dependency in your +To use the JavaScript Client for Oracle Coherence, simply declare it as a dependency in your project's `package.json`: ``` ... @@ -52,8 +52,8 @@ project's `package.json`: ### Compatibility with Java Types The following table provides a listing of mappings between Java types and Javascript types when working with -Coherence `23.09` or later. If using Coherence `22.06.x`, these types will be returned as Number. It is recommended -using `23.09` if intentionally using `java.math.BigInteger` or `java.math.BigDecimal` as part of your application. +Coherence `24.09` or later. If using Coherence `22.06.x`, these types will be returned as Number. It is recommended +using `24.09` if intentionally using `java.math.BigInteger` or `java.math.BigDecimal` as part of your application. | Java Type | JavascriptType | |----------------------|------------------------| @@ -68,7 +68,7 @@ using `23.09` if intentionally using `java.math.BigInteger` or `java.math.BigDec #### Establishing a Session The Coherence uses the concept of a `Session` to manage a set of related Coherence resources, -such as maps and/or caches. When using the Coherence JavaScript Client, a `Session` connects to a specific +such as maps and/or caches. When using the JavaScript Client for Oracle Coherence, a `Session` connects to a specific gRPC endpoint and uses a specific serialization format to marshal requests and responses. This means that different sessions using different serializers may connect to the same server endpoint. Typically, for efficiency the client and server would be configured to use matching serialization formats to avoid @@ -76,7 +76,7 @@ deserialization of data on the server, but this does not have to be the case. If serializer for the server-side caches, it must be able to deserialize the client's requests, so there must be a serializer configured on the server to match that used by the client. -> NOTE: Currently, the Coherence JavaScript client only supports JSON serialization +> NOTE: Currently, the JavaScript Client for Oracle Coherence only supports JSON serialization A `Session` is constructed using an `Options` instance, or a generic object with the same keys and values. @@ -126,6 +126,32 @@ const opts = new Options({address: 'example.com:4444'}) let session = new Session(opts) ``` +As of v1.2.3 of the JavaScript Client for Oracle Coherence, it's now possible to use the Coherence +NameService to lookup gRPC Proxy endpoints. The format to enable this feature is +`coherence:///([:port]|[:cluster-name]|[:port:cluster-name])` + +For example: + * `coherence:///localhost` will connect to the name service bound to a local coherence cluster on port `7574` (the default Coherence cluster port). + * `coherence:///localhost:8000` will connect to the name service bound to a local coherence cluster on port `8000`. + * `coherence:///localhost:remote-cluster` will connect to the name service bound to a local coherence cluster on port `7574` (the default Coherence cluster port) and look up the name service for the given cluster name. Note: this typically means both clusters have a local member sharing a cluster port. + * `coherence:///localhost:8000:remote-cluster` will connect to the name service bound to a local coherence cluster on port `8000` and look up the name service for the given cluster name. Note: this typically means both clusters have a local member sharing a cluster port. + +While this is useful for local development, this may have limited uses in a production environment. For example, +Coherence running within a container with the cluster port (`7574`) exposed so external clients may connect. The +lookup will fail to work for the client as the Coherence name service return a private network address which +won't resolve. Lastly, if connecting to a cluster that has multiple proxies bound to different ports, gRPC, by default, +will use the first address returned by the resolver. It is possible to enable round-robin load balancing by including +a custom channel option when creating the session: + +```typescript +const { Session } = require('@oracle/coherence') + +const opts = new Options({address: 'example.com:4444', + channelOptions: {'grpc.service_config': JSON.stringify({ loadBalancingConfig: [{ round_robin: {} }], })}}) + +let session = new Session(opts) +``` + It's also possible to control the default address the session will bind to by providing an address via the `COHERENCE_SERVER_ADDRESS` environment variable. The format of the value would be the same as if you configured it programmatically as the above example shows. diff --git a/etc/docker-compose-2-members.yaml b/etc/docker-compose-2-members.yaml index fa3b2dc..860631b 100644 --- a/etc/docker-compose-2-members.yaml +++ b/etc/docker-compose-2-members.yaml @@ -19,6 +19,7 @@ services: - "9612:9612" - "8080:8080" - "6676:6676" + - "7574:7574" volumes: - .:/args - ./cert:/certs diff --git a/etc/jvm-args-clear.txt b/etc/jvm-args-clear.txt index 232a58c..2542bf4 100644 --- a/etc/jvm-args-clear.txt +++ b/etc/jvm-args-clear.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2023, Oracle and/or its affiliates. +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl. @@ -6,4 +6,4 @@ -Xms1g -Xmx1g -Dcoherence.log.level=9 --Dcoherence.io.json.debug=true +-Dcoherence.io.json.debug=false diff --git a/etc/jvm-args-tls.txt b/etc/jvm-args-tls.txt index 14ccb41..63bb284 100644 --- a/etc/jvm-args-tls.txt +++ b/etc/jvm-args-tls.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2023, Oracle and/or its affiliates. +# Copyright (c) 2023, 2025, Oracle and/or its affiliates. # # Licensed under the Universal Permissive License v 1.0 as shown at # https://oss.oracle.com/licenses/upl. @@ -6,7 +6,7 @@ -Xms1g -Xmx1g -Dcoherence.log.level=9 --Dcoherence.io.json.debug=true +-Dcoherence.io.json.debug=false -Dcoherence.grpc.server.socketprovider=tls-files -Dcoherence.security.key=/certs/star-lord.pem -Dcoherence.security.cert=/certs/star-lord.crt diff --git a/package-lock.json b/package-lock.json index d0073ac..0346e72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@grpc/grpc-js": "^1.11", "@grpc/proto-loader": "^0.7", "decimal.js": "^10.4", - "google-protobuf": "^3.21" + "google-protobuf": "^3.21", + "typedoc": "^0.27.6" }, "devDependencies": { "@types/google-protobuf": "^3.15", @@ -525,6 +526,17 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@gerrit0/mini-shiki": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", + "integrity": "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^1.27.2", + "@shikijs/types": "^1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.3.tgz", @@ -899,6 +911,32 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", + "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -929,11 +967,26 @@ "integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==", "dev": true }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1077,14 +1130,12 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -1099,7 +1150,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1457,6 +1507,18 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -2159,6 +2221,15 @@ "node": ">=6" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2218,6 +2289,12 @@ "node": ">=10" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -2248,6 +2325,29 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -2932,6 +3032,15 @@ "node": ">=12.0.0" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3405,11 +3514,47 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typedoc": { + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.6.tgz", + "integrity": "sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw==", + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^1.24.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.6.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3419,6 +3564,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.16.1", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.1.tgz", @@ -3606,6 +3757,18 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -4088,6 +4251,16 @@ } } }, + "@gerrit0/mini-shiki": { + "version": "1.27.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz", + "integrity": "sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og==", + "requires": { + "@shikijs/engine-oniguruma": "^1.27.2", + "@shikijs/types": "^1.27.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, "@grpc/grpc-js": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.3.tgz", @@ -4371,6 +4544,29 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "requires": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "requires": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "@shikijs/vscode-textmate": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", + "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==" + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -4401,11 +4597,24 @@ "integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==", "dev": true }, + "@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "requires": { + "@types/unist": "*" + } + }, "@types/node": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" }, + "@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4512,14 +4721,12 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "binary-extensions": { "version": "2.2.0", @@ -4531,7 +4738,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "requires": { "balanced-match": "^1.0.0" } @@ -4790,6 +4996,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -5294,6 +5505,14 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "requires": { + "uc.micro": "^2.0.0" + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5338,6 +5557,11 @@ "yallist": "^4.0.0" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5361,6 +5585,24 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "requires": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + } + }, + "mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, "minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -5868,6 +6110,11 @@ "long": "^5.0.0" } }, + "punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6214,11 +6461,37 @@ "is-typedarray": "^1.0.0" } }, + "typedoc": { + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.6.tgz", + "integrity": "sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw==", + "requires": { + "@gerrit0/mini-shiki": "^1.24.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.6.1" + }, + "dependencies": { + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" + }, + "uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" }, "uglify-js": { "version": "3.16.1", @@ -6357,6 +6630,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==" + }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 24810fd..379749c 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,17 @@ ], "repository": "https://github.com/oracle/coherence-js-client", "dependencies": { - "@grpc/proto-loader": "^0.7", "@grpc/grpc-js": "^1.11", + "@grpc/proto-loader": "^0.7", + "decimal.js": "^10.4", "google-protobuf": "^3.21", - "decimal.js": "^10.4" + "typedoc": "^0.27.6" }, "devDependencies": { - "grpc-tools": "^1.12", "@types/google-protobuf": "^3.15", "glob-parent": "^6.0", "grpc_tools_node_protoc_ts": "^5.3", + "grpc-tools": "^1.12", "mocha": "^11.1", "nyc": "^15.1", "source-map-support": "^0.5", @@ -35,6 +36,7 @@ "test": "npm run compile && npm exec mocha 'test/**.js' --recursive --exit", "test-cycle": "bin/test-cycle.sh -c", "test-cycle-tls": "bin/test-cycle.sh -s", + "test-resolver": "npm run compile && npm exec mocha test/discovery/resolver-tests.js --recursive --exit", "coh-up": "bin/docker-utils.sh -u", "coh-down": "bin/docker-utils.sh -d", "coverage": "nyc mocha 'test/**.js' --exit", diff --git a/src/session.ts b/src/session.ts index 9eb6fcc..efb4e53 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,18 +1,29 @@ /* - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. * * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. */ -import {CallOptions, Channel, ChannelCredentials, credentials} from '@grpc/grpc-js' +import { + CallOptions, + Channel, + ChannelCredentials, + ChannelOptions, + credentials, + experimental, Metadata, + status, +} from '@grpc/grpc-js' import {EventEmitter} from 'events' import {PathLike, readFileSync} from 'fs' import {event} from './events' import {NamedCache, NamedCacheClient, NamedMap} from './named-cache-client' import {util} from './util' -import {ConnectivityState} from "@grpc/grpc-js/build/src/connectivity-state"; +import {ConnectivityState} from "@grpc/grpc-js/build/src/connectivity-state" +import {registerResolver} from "@grpc/grpc-js/build/src/resolver" +import {Endpoint} from "@grpc/grpc-js/build/src/subchannel-address" +import * as net from "node:net" /** * Supported {@link Session} options. @@ -21,7 +32,7 @@ export class Options { /** * Regular expression for basic validation of IPv4 address. */ - private static readonly ADDRESS_REGEXP = RegExp('\\S+:\\d{1,5}$') + private static readonly ADDRESS_REGEXP = RegExp('^(coherence:\/\/\/\\S+|coherence:\\S+:\\d{1,5}|coherence:\/\/\/\\S+:\\d{1,5}:[a-zA-Z0-9]+|coherence:\/\/\/\\S+:[a-zA-Z0-9]+|\\S+:\\d{1,5})$') /** * Address of the target Coherence cluster. If not explicitly set, this defaults to {@link Session.DEFAULT_ADDRESS}. @@ -74,7 +85,7 @@ export class Options { /** * Flag indicating mutations are no longer allowed. */ - private locked: boolean = false; + private locked: boolean = false /** * Optional TLS configuration. @@ -91,17 +102,24 @@ export class Options { } /** - * Set the IPv4 host address and port in the format of `[host]:[port]` + * Set the IPv4 host address and port in the format of `[host]:[port]`. + *

+ * If the Coherence cluster port is available, it's possible to uss + * the Coherence name service in order to resolve and connect to the available + * gRPC proxies. If using this functionality, the format is + * `coherence:///([:port]|[:cluster-name]|[:port:cluster-name])`. * * @param address the IPv4 host address and port in the format of `[host]:[port]` + * or the Coherence name service format of + * `coherence:///([:port]|[:cluster-name]|[:port:cluster-name])` */ set address (address: string) { if (this.locked) { - return; + return } // ensure address is sane if (!Options.ADDRESS_REGEXP.test(address)) { - throw new Error('Expected address format is \':\'. Configured: ' + address) + throw new Error('Expected address format is \':\' or \'coherence:([:port]|[:cluster-name]|[:port:cluster-name])\'. Configured: ' + address) } this._address = address @@ -123,7 +141,7 @@ export class Options { */ set requestTimeoutInMillis (timeout: number) { if (this.locked) { - return; + return } if (timeout <= 0) { timeout = Number.POSITIVE_INFINITY @@ -137,7 +155,7 @@ export class Options { * @return the ready timeout in `milliseconds` */ get readyTimeoutInMillis(): number { - return this._readyTimeoutInMillis; + return this._readyTimeoutInMillis } /** @@ -147,12 +165,12 @@ export class Options { */ set readyTimeoutInMillis(timeout: number) { if (this.locked) { - return; + return } if (timeout <= 0) { timeout = Number.POSITIVE_INFINITY } - this._readyTimeoutInMillis = timeout; + this._readyTimeoutInMillis = timeout } /** @@ -181,7 +199,7 @@ export class Options { * Return the `gRPC` `ChannelOptions`. */ get channelOptions(): { [p: string]: any } { - return this._channelOptions; + return this._channelOptions } /** @@ -193,9 +211,9 @@ export class Options { */ set channelOptions(value: { [p: string]: any }) { if (this.locked) { - return; + return } - this._channelOptions = value; + this._channelOptions = value } /** @@ -233,7 +251,7 @@ export class Options { */ set callOptions (callOptions: () => CallOptions) { if (this.locked) { - return; + return } this._callOptions = callOptions } @@ -252,7 +270,7 @@ export class Options { * @hidden */ lock(): void { - this.locked = true; + this.locked = true this.tls.lock() } @@ -340,7 +358,7 @@ export class TlsOptions { */ set enabled (value: boolean) { if (this.locked) { - return; + return } this._enabled = value } @@ -361,7 +379,7 @@ export class TlsOptions { */ set caCertPath (value: PathLike | undefined) { if (this.locked) { - return; + return } this._caCertPath = value } @@ -382,7 +400,7 @@ export class TlsOptions { */ set clientCertPath (value: PathLike | undefined) { if (this.locked) { - return; + return } this._clientCertPath = value } @@ -403,7 +421,7 @@ export class TlsOptions { */ set clientKeyPath (value: PathLike | undefined) { if (this.locked) { - return; + return } this._clientKeyPath = value } @@ -507,6 +525,9 @@ export class Session */ constructor (sessionOptions?: Options | object) { super() + + registerResolver("coherence", CoherenceResolver) + if (sessionOptions) { this._sessionOptions = Object.assign(new Options(), sessionOptions) // @ts-ignore -- added for 'tls' index access @@ -545,15 +566,15 @@ export class Session // emit the `disconnect` event. // When transitioning from any other state, // other than SHUTDOWN, to READY, emit the 'reconnect' event. - let connected: boolean = false; - let firstConnect: boolean = true; - let lastState: number = 0; + let connected: boolean = false + let firstConnect: boolean = true + let lastState: number = 0 let callback = async () => { - let state = channel.getConnectivityState(false); - lastState = state; + let state = channel.getConnectivityState(false) + lastState = state if (state === ConnectivityState.SHUTDOWN) { // nothing to do - return; + return } else if (state === ConnectivityState.READY) { if (!firstConnect && !connected) { this.emit(event.SessionLifecycleEvent.RECONNECTED) @@ -566,7 +587,7 @@ export class Session } else { if (connected) { this.emit(event.SessionLifecycleEvent.DISCONNECTED) - connected = false; + connected = false } } let deadline = Number.POSITIVE_INFINITY @@ -836,3 +857,346 @@ export class Session }) } } + +enum ResolveState { + CHANNEL, + CONNECTION, + DONE, + INITIAL, + LOOKUP, +} + +class LookupState { + state: ResolveState = ResolveState.INITIAL + query: string + result?: string + channel?: Buffer + resolve?: (value: (PromiseLike | void)) => void; + reject?: (reason?: any) => void + waitResolve: Promise + + constructor(query: string) { + this.query = query + this.waitResolve = new Promise((resolve, reject) => { + this.resolve = resolve + this.reject = reject + }) + } +} + +/** + * INTERNAL: Implementation of the gRPC Resolver that will resolve gRPC proxies endpoints + * using the Coherence name service. + */ +export class CoherenceResolver implements experimental.Resolver { + + private static readonly MULTIPLEXED_SOCKET = Buffer.from([90, 193, 224, 0]) + private static readonly NAME_SERVICE_SUB_PORT = Buffer.from([0, 0, 0, 3]) + private static readonly CONNECTION_OPEN = Buffer.from([ + 0, 1, 2, 0, 66, 0, 1, 14, 0, 0, 66, 166, 182, 159, 222, 178, 81, + 1, 65, 227, 243, 228, 221, 15, 2, 65, 143, 246, 186, 153, 1, 3, + 65, 248, 180, 229, 242, 4, 4, 65, 196, 254, 220, 245, 5, 5, 65, 215, + 206, 195, 141, 7, 6, 65, 219, 137, 220, 213, 10, 64, 2, 110, 3, + 93, 78, 87, 2, 17, 77, 101, 115, 115, 97, 103, 105, 110, 103, 80, + 114, 111, 116, 111, 99, 111, 108, 2, 65, 2, 65, 2, 19, 78, 97, 109, + 101, 83, 101, 114, 118, 105, 99, 101, 80, 114, 111, 116, 111, 99, + 111, 108, 2, 65, 1, 65, 1, 5, 160, 2, 0, 0, 14, 0, 0, 66, 174, 137, + 158, 222, 178, 81, 1, 65, 129, 128, 128, 240, 15, 5, 65, 152, 159, + 129, 128, 8, 6, 65, 147, 158, 1, 64, 1, 106, 2, 110, 3, 106, 4, 113, + 5, 113, 6, 78, 8, 67, 108, 117, 115, 116, 101, 114, 66, 9, 78, 9, 108, + 111, 99, 97, 108, 104, 111, 115, 116, 10, 78, 5, 50, 48, 50, 51, 51, 12, + 78, 16, 67, 111, 104, 101, 114, 101, 110, 99, 101, 67, 111, 110, 115, + 111, 108, 101, 64, 64, + ]) + private static readonly CHANNEL_OPEN = Buffer.from([ + 0, 11, 2, 0, 66, 1, 1, 78, 19, 78, 97, 109, 101, 83, 101, 114, 118, + 105, 99, 101, 80, 114, 111, 116, 111, 99, 111, 108, 2, 78, 11, 78, + 97, 109, 101, 83, 101, 114, 118, 105, 99, 101, 64, + ]) + private static readonly NS_LOOKUP_REQ_ID = Buffer.from([1, 1, 0, 66, 0, 1, 78]) + private static readonly REQ_END_MARKER = Buffer.from([64]) + + private static readonly NAME_SERVICE: string = 'NameService/string' + private static readonly CLUSTER_NS_LOOKUP_PREFIX: string = `${CoherenceResolver.NAME_SERVICE}/Cluster/foreign/` + private static readonly CLUSTER_NS_LOOKUP_SUFFIX: string = '/NameService/localPort' + private static readonly GRPC_PROXY_LOOKUP: string = `${CoherenceResolver.NAME_SERVICE}/$GRPC:GrpcProxy` + + private readonly target: experimental.GrpcUri + private readonly listener: experimental.ResolverListener + + constructor ( + target: experimental.GrpcUri, + listener: experimental.ResolverListener, + channelOptions: ChannelOptions + ) { + this.target = target + this.listener = listener + } + + // ----- experimental.Resolver interface ---------------------------------- + + destroy(): void { + } + + updateResolution(): void { + let [host, port, clusterName] = CoherenceResolver.parseConn(this.target.path) + let socket + // check for foreign cluster reference + if (clusterName.length > 0) { + let clusterNameState: LookupState = this.createForeignLookupState(clusterName) + socket = this.createSocket(clusterNameState) + socket.connect(parseInt(port), host) + clusterNameState.waitResolve.then(() => { + if (clusterNameState.result) { + port = clusterNameState.result + this.runLookup(this.createGrpcLookupState(), host, parseInt(port)) + } else { + this.listener.onError({ + code: status.UNAVAILABLE, + details: `Unable to resolve an address from ${JSON.stringify(this.target)}`, + metadata: new Metadata(), + }) + } + }).catch(reason => { + console.log(`Unable to resolve an address from ${JSON.stringify(this.target)}: ${reason}`) + this.listener.onError({ + code: status.UNAVAILABLE, + details: reason, + metadata: new Metadata(), + }) + }) + } else { + this.runLookup(this.createGrpcLookupState(), host, parseInt(port)) + } + } + + static getDefaultAuthority(target: experimental.GrpcUri): string { + let [host] = this.parseConn(target.path) + return host + } + + // ----- internal --------------------------------------------------------- + + createForeignLookupState(clusterName: string): LookupState { + return new LookupState(`${CoherenceResolver.CLUSTER_NS_LOOKUP_PREFIX}${clusterName}${CoherenceResolver.CLUSTER_NS_LOOKUP_SUFFIX}`) + } + + createGrpcLookupState(): LookupState { + return new LookupState(CoherenceResolver.GRPC_PROXY_LOOKUP) + } + + static parseConn(path: string): [string, string, string] { + let parts: string[] = path.split(':') + const host = parts[0] + let port = '7574' + let clusterName = '' + if (parts.length === 2) { + let p = parseInt(parts[1]) + if (isNaN(p)) { + // this is a cluster reference using the default port + clusterName = parts[1] + } + } else if (parts.length === 3) { + port = parts[1] + clusterName = parts[2] + } + return [host, port, clusterName] + } + + private runLookup(state: LookupState, host: string, port: number): void { + let socket: net.Socket = this.createSocket(state) + socket.connect(port, host) + state.waitResolve.then(() => { + let ep: Endpoint[] = this.parseLookupResult(state) + if (ep.length > 0) { + this.listener.onSuccessfulResolution(ep, null, null, null, {}) + } else { + this.listener.onError({ + code: status.UNAVAILABLE, + details: `Unable to resolve an address from ${JSON.stringify(this.target)}`, + metadata: new Metadata(), + }) + } + }) + } + + private parseLookupResult(state: LookupState): Endpoint[] { + if (state.result) { + const parts: string[] = state.result.substring(1, state.result.length - 1).split(', ') + if (parts) { + const iterCnt: number = parts.length / 2 + return Array.from({length: iterCnt}, (_, i) => ({ + addresses: [{ + port: parseInt(parts[i * 2 + 1]), + host: parts[i * 2], + }], + })) + } else { + return [] + } + } + return [] + } + + private static readResponse(socket: net.Socket): Buffer { + const length: number = CoherenceResolver.readPackedInt(socket) + return socket.read(length) + } + + private static writePackedInt(n: number): Buffer { + let result: Buffer = Buffer.alloc(0) + let b = 0 + + if (n < 0) { + b = 0x40 + n = ~n // bitwise negation + } + + b |= n & 0x3F + n >>= 6 + + while (n !== 0) { + result = Buffer.concat([result, Buffer.from([b | 0x80])]) + b = n & 0x7F + n >>= 7 + } + + return Buffer.concat([result, Buffer.from([b])]) + } + + private static readPackedInt(socket: net.Socket): number { + let bits = 6 + let bytes: Buffer = socket.read(1) + let byte = bytes[0] + let negative: boolean = (byte & 0x40) !== 0 + let result = (byte & 0x3F) + + while ((byte & 0x80) !== 0) { + bytes = socket.read(1) + byte = bytes[0] + result |= (byte & 0x7F) << bits + bits += 7 + } + + return negative ? ~result : result + } + + private static readPackedIntFromBuffer(bytes: Buffer): [number, number] { + let bits = 6 + + if (bytes.length > 7) { + let byte = bytes ? bytes[6] : 0 + let negative: boolean = (byte & 0x40) !== 0 + let result = (byte & 0x3F) + + while ((byte & 0x80) !== 0) { + byte = bytes ? bytes[7] : 0 + result |= (byte & 0x7F) << bits + bits += 7 + } + + return [negative ? ~result : result, bits] + } + return [0, bits] + } + + private static sendConnectionOpen(socket: net.Socket) { + socket.write(CoherenceResolver.MULTIPLEXED_SOCKET) + socket.write(CoherenceResolver.NAME_SERVICE_SUB_PORT) + socket.write(CoherenceResolver.writePackedInt(CoherenceResolver.CONNECTION_OPEN.length)) + socket.write(CoherenceResolver.CONNECTION_OPEN) + } + + private static sendChannelOpen(socket: net.Socket) { + socket.write(CoherenceResolver.writePackedInt(CoherenceResolver.CHANNEL_OPEN.length)) + socket.write(CoherenceResolver.CHANNEL_OPEN) + } + + private static sendLookup(socket: net.Socket, channel: Buffer, query: string) { + const request = Buffer.concat([ + channel, + CoherenceResolver.NS_LOOKUP_REQ_ID, + CoherenceResolver.writePackedInt(query.length), + Buffer.from(query), + CoherenceResolver.REQ_END_MARKER, + ]) + + socket.write(CoherenceResolver.writePackedInt(request.length)) + socket.write(request) + } + + // Read a string from the response + private static readString(data: Buffer): string { + let [len, bits] = CoherenceResolver.readPackedIntFromBuffer(data) + return data.subarray(7 + (bits / 7), 7 + (bits / 7) + len).toString() + } + + private createSocket(state: LookupState): net.Socket { + let socket = new net.Socket() + + socket.setNoDelay(true) + socket.setTimeout(10 * 1000) // Set timeout + + socket.on("connect", () => { + CoherenceResolver.sendConnectionOpen(socket) + state.state = ResolveState.CONNECTION + } + ) + + socket.on('error', err => { + state.reject!(err) + }) + + socket.on('readable', () => { + switch (state.state) { + case ResolveState.DONE: + return + + case ResolveState.CONNECTION: { + while (socket.read() !== null) {} // consume the open connection response + + // open the remote channel + CoherenceResolver.sendChannelOpen(socket) + + // transition read state + state.state = ResolveState.CHANNEL + + break + } + + case ResolveState.CHANNEL: { + let msg: Buffer = CoherenceResolver.readResponse(socket) + + state.channel = msg.subarray(8, 8 + msg.length - 9) + + if (!state.channel) { + state.state = ResolveState.DONE + state.reject!("Unable to parse channel from response") + } + + CoherenceResolver.sendLookup(socket, state.channel, state.query) + + // transition read state + state.state = ResolveState.LOOKUP + + break + } + + case ResolveState.LOOKUP: { + let msg = CoherenceResolver.readResponse(socket) + msg = msg.subarray(state.channel!.length + 1) + state.result = CoherenceResolver.readString(msg) + socket.destroy() + if (state.result.length > 0) { + state.resolve!() // signal caller, resolution completed + } else { + state.reject!("Failure to parse lookup response") + } + } + } + }) + + return socket + } +} + diff --git a/src/util.ts b/src/util.ts index 7a23ef4..7b51985 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Oracle and/or its affiliates. + * Copyright (c) 2022, 2025, Oracle and/or its affiliates. * * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl. @@ -1585,7 +1585,7 @@ export namespace util { * When Coherence serializes a type, it populates the `@class` metadata with the type alias name, which * can be used to reconstruct the Java class running in another VM, or in our case reconstruct a similar * object in Javascript. Using 1BigInt` as an example; using a TypeHandler the BigInt can be deserialized - * to JSON in the required fashion in order to have a BigInteger created in Coherence and vice-versa. + * to JSON in the required fashion in order to have a BigInteger created in Coherence and vice versa. */ export abstract class TypeHandler { protected readonly _type: string diff --git a/test/discovery/resolver-tests.js b/test/discovery/resolver-tests.js new file mode 100644 index 0000000..2c4aa69 --- /dev/null +++ b/test/discovery/resolver-tests.js @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. + * + * Licensed under the Universal Permissive License v 1.0 as shown at + * https://oss.oracle.com/licenses/upl. + */ + +const { CoherenceResolver, Session } = require('../../lib') +const assert = require('assert').strict +const { describe, it } = require('mocha'); + +describe('CoherenceResolver Test Suite (unit/IT)', () => { + describe('A CoherenceResolver', () => { + + function createListener(done, expectedPort) { + return { + onSuccessfulResolution: ( + addressList, + serviceConfig, + serviceConfigError, + configSelector, + attributes + ) => { + try { + assert.equal(addressList.length, 1) + assert.equal(addressList[0].addresses[0].host, '127.0.0.1') + assert.equal(addressList[0].addresses[0].port, expectedPort) + done() + } catch (error) { + done(error) + } + }, + onError(error) { + done(new Error(`Unexpected error resolving: ${JSON.stringify(error)}`)) + } + } + } + + it('should parse and resolve the connection format \'coherence:///[host]\'', (done) => { + const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost'}, createListener(done, 10000), null) + resolver.updateResolution() + }) + + it('should parse and resolve the connection format \'coherence:///[host]:[port]\'', (done) => { + const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost:7574'}, createListener(done, 10000), null) + resolver.updateResolution() + }) + + it('should parse and resolve the connection format \'coherence:///[host]:[clusterName]\'', (done) => { + const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost:grpc-cluster2'}, createListener(done, 10001), null) + resolver.updateResolution() + }) + + it('should parse and resolve the connection format \'coherence:///[host]:[port]:[clusterName]\'', (done) => { + const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost:7574:grpc-cluster2'}, createListener(done, 10001), null) + resolver.updateResolution() + }) + }) + + describe('A Session', () => { + it('should be able to resolve the gRPC Proxy', async () => { + const session = new Session({address: 'coherence:///localhost'}) + const cache = session.getCache('test') + await cache.set('a', 'b') + assert.equal(await cache.get('a'), 'b') + await session.close() + }) + + it('should be able to resolve the gRPC Proxy of a foreign cluster', async () => { + const session = new Session({address: 'coherence:///localhost:grpc-cluster2'}) + const cache = session.getCache('test') + await cache.set('a', 'b') + assert.equal(await cache.get('a'), 'b') + console.log("HERE") + await session.close() + }) + }) +}) \ No newline at end of file diff --git a/test/session-tests.js b/test/session-tests.js index bd6c59c..2144202 100644 --- a/test/session-tests.js +++ b/test/session-tests.js @@ -1,14 +1,13 @@ /* - * Copyright (c) 2020 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025, Oracle and/or its affiliates. * * Licensed under the Universal Permissive License v 1.0 as shown at - * http://oss.oracle.com/licenses/upl. + * https://oss.oracle.com/licenses/upl. */ const { event, Session } = require('../lib') const assert = require('assert').strict const { describe, it } = require('mocha'); -const path = require('path') describe('Session Tests Suite (unit/IT)', () => { describe('Session Unit Test Suite', () => { @@ -29,7 +28,9 @@ describe('Session Tests Suite (unit/IT)', () => { assert.equal(session.options.tls.clientKeyPath, undefined) } - assert.equal(session.address, Session.DEFAULT_ADDRESS) + if (!process.env.COHERENCE_SERVER_ADDRESS) { + assert.equal(session.address, Session.DEFAULT_ADDRESS) + } assert.equal(session.options.requestTimeoutInMillis, 60000) assert.equal(session.options.format, Session.DEFAULT_FORMAT) }) @@ -65,6 +66,29 @@ describe('Session Tests Suite (unit/IT)', () => { describe('Session IT Test Suite', () => { describe('A Session', () => { + it('should not allow invalid addresses', () => { + assert.throws(() => new Session({address: 'localhost'})) + assert.throws(() => new Session({address: 'localhost:801a'})) + assert.throws(() => new Session({address: 'localhost:800000'})) + assert.throws(() => new Session({address: 'localhost:8000:'})) + assert.throws(() => new Session({address: 'localhost:8000:test'})) + assert.throws(() => new Session({address: 'coherence'})) + assert.throws(() => new Session({address: 'coherence:'})) + assert.throws(() => new Session({address: 'coherence:/'})) + assert.throws(() => new Session({address: 'coherence://'})) + assert.throws(() => new Session({address: 'coherence://localhost'})) + // assert.throws(() => new Session({address: 'coherence:localhost:8080:'})) + // assert.throws(() => new Session({address: 'coherence:localhost:'})) + }) + + it('should allow valid addresses', () => { + assert.doesNotThrow(() => new Session({address: 'localhost:8080'})) + assert.doesNotThrow(() => new Session({address: 'coherence:///localhost'})) + assert.doesNotThrow(() => new Session({address: 'coherence:///localhost:8080'})) + assert.doesNotThrow(() => new Session({address: 'coherence:///localhost:test'})) + assert.doesNotThrow(() => new Session({address: 'coherence:///localhost:8080:test'})) + }) + it('should not have active sessions upon creation', async () => { const sess = new Session()