├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── support-question--rfa-.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── discovery.yml │ ├── release.yml │ └── trivy-scan.yml ├── .gitignore ├── .npmignore ├── .nycrc ├── CONTRIBUTING.md ├── DEVELOPING.md ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── THIRD_PARTY_LICENSES.txt ├── bin ├── docker-utils.sh ├── keys.sh ├── npm-post-install.sh └── test-cycle.sh ├── etc ├── docker-compose-2-members.yaml ├── jvm-args-clear.txt └── jvm-args-tls.txt ├── package-lock.json ├── package.json ├── src ├── aggregators.ts ├── events.ts ├── extractors.ts ├── filters.ts ├── index.ts ├── named-cache-client.ts ├── processors.ts ├── session.ts ├── tsconfig.json └── util.ts ├── test ├── aggregator-tests.js ├── client-tests.js ├── discovery │ └── resolver-tests.js ├── extractor-tests.js ├── filter-tests.js ├── map-listener-tests.js ├── processor-tests.js ├── request-test.js ├── serialization-tests.js ├── session-tests.js └── util.js └── typedoc.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report helping us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 16 | **Expected behaviour** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Environment (please complete the following information):** 23 | - Coherence JS Client version (or Git SHA) 24 | - NodeJS version 25 | - Coherence CE version (or Git SHA) 26 | - Java version and Java vendor 27 | - OS: [e.g. iOS] 28 | - OS Version [e.g. 22] 29 | - Is this a container/cloud environment, e.g. Docker, CRI-O, Kubernetes, if so include additional information about the container environment, versions etc. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Enhancement Request 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support-question--rfa-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Support Question (RFA) 3 | about: Support questions and requests for advice 4 | title: '' 5 | labels: RFA 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | 18 | ## Type of question 19 | 20 | **Are you asking how to use a specific feature, or about general context and help around Coherence?** 21 | 22 | ## Question 23 | 24 | **What did you do?** 25 | A clear and concise description of the steps you took (or insert a code snippet). 26 | 27 | **What did you expect to see?** 28 | A clear and concise description of what you expected to happen (or insert a code snippet). 29 | 30 | **What did you see instead? Under which circumstances?** 31 | A clear and concise description of what you expected to happen (or insert a code snippet). 32 | 33 | 34 | **Environment** 35 | * Coherence version: 36 | 37 | insert release or Git SHA here 38 | 39 | **Additional context** 40 | Add any other context about the question here. 41 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020, 2025, Oracle Corporation and/or its affiliates. All rights reserved. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at 3 | # https://oss.oracle.com/licenses/upl. 4 | 5 | # --------------------------------------------------------------------------- 6 | # Coherence JavaScript Client GitHub Actions CI build. 7 | # --------------------------------------------------------------------------- 8 | 9 | name: JS Client Validation 10 | 11 | on: 12 | schedule: 13 | - cron: "0 5 * * *" 14 | push: 15 | branches-ignore: 16 | - ghpages 17 | pull_request: 18 | types: 19 | - opened 20 | branches: 21 | - '*' 22 | jobs: 23 | build: 24 | 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | node-version: [18.x, 19.x, 20.x, 21.x, 22.x, 23.x] 31 | coherence-version: [22.06.12, 14.1.2-0-2, 25.03.1] 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | # install protoc 40 | - run: curl -LO "https://github.com/protocolbuffers/protobuf/releases/download/v22.2/protoc-22.2-linux-x86_64.zip" 41 | - run: unzip protoc-22.2-linux-x86_64.zip -d /tmp/grpc 42 | - run: echo "/tmp/grpc/bin" >> $GITHUB_PATH 43 | # install project deps 44 | - run: npm install 45 | # run tests 46 | - run: COHERENCE_VERSION=${{ matrix.coherence-version }} npm run test-cycle 47 | # run tests using TLS 48 | - run: COHERENCE_VERSION=${{ matrix.coherence-version }} npm run test-cycle-tls 49 | # clean up 50 | - name: Archive production artifacts 51 | if: failure() 52 | uses: actions/upload-artifact@v4 53 | with: 54 | name: save-log-file-${{ matrix.node-version }}-${{ matrix.coherence-version }} 55 | path: logs-*.txt 56 | -------------------------------------------------------------------------------- /.github/workflows/discovery.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2025, Oracle Corporation and/or its affiliates. All rights reserved. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at 3 | # https://oss.oracle.com/licenses/upl. 4 | 5 | # --------------------------------------------------------------------------- 6 | # Coherence JavaScript Client GitHub Actions CI build. 7 | # --------------------------------------------------------------------------- 8 | 9 | name: JS Client Discovery Validation 10 | 11 | on: 12 | schedule: 13 | - cron: "0 5 * * *" 14 | push: 15 | branches-ignore: 16 | - ghpages 17 | pull_request: 18 | types: 19 | - opened 20 | branches: 21 | - '*' 22 | jobs: 23 | build: 24 | 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | node-version: [20.x, 21.x, 22.x, 23.x] 31 | coherence-version: [22.06.12, 14.1.2-0-2, 25.03.1] 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | - run: curl -LO "https://github.com/protocolbuffers/protobuf/releases/download/v22.2/protoc-22.2-linux-x86_64.zip" 40 | - run: unzip protoc-22.2-linux-x86_64.zip -d /tmp/grpc 41 | - run: echo "/tmp/grpc/bin" >> $GITHUB_PATH 42 | - run: npm install 43 | - run: npm run compile 44 | 45 | - name: Run Coherence Server 46 | shell: bash 47 | run: | 48 | export COHERENCE_VERSION=${{ matrix.coherence-version }} 49 | curl -sL https://raw.githubusercontent.com/oracle/coherence-cli/main/scripts/install.sh | bash 50 | cohctl version 51 | cohctl set profile grpc-cluster1 -v "-Dcoherence.grpc.server.port=10000" -y 52 | cohctl create cluster grpc-cluster1 -P grpc-cluster1 -r 1 -v ${{ matrix.coherence-version }} -y -a coherence-grpc-proxy 53 | cohctl set profile grpc-cluster2 -v "-Dcoherence.grpc.server.port=10001" -y 54 | cohctl create cluster grpc-cluster2 -P grpc-cluster2 -r 1 -H 30001 -v ${{ matrix.coherence-version }} -y -a coherence-grpc-proxy 55 | sleep 20 56 | cohctl monitor health -n localhost:7574 -T 40 -w 57 | 58 | - name: Run resolver tests 59 | shell: bash 60 | run: | 61 | npm run test-resolver 62 | 63 | - name: Archive production artifacts 64 | if: failure() 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: save-log-file-${{ matrix.node-version }}-${{ matrix.coherence-version }} 68 | path: ~/.cohctl/logs/*.*.log 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020, 2025, Oracle Corporation and/or its affiliates. All rights reserved. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at 3 | # https://oss.oracle.com/licenses/upl. 4 | 5 | # --------------------------------------------------------------------------- 6 | # Coherence JavaScript Client GitHub Release Actions build. 7 | # --------------------------------------------------------------------------- 8 | 9 | 10 | name: NPM publish CD workflow 11 | 12 | on: 13 | release: 14 | # This specifies that the build will be triggered when we publish a release 15 | types: [published] 16 | 17 | jobs: 18 | build: 19 | 20 | # Run on latest version of ubuntu 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | ref: ${{ github.event.release.target_commitish }} 27 | # install Node.js 28 | - name: Use Node.js 18.x 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 18.15.x 32 | # Specifies the registry, this field is required! 33 | registry-url: https://registry.npmjs.org/ 34 | # install protoc 35 | - run: curl -LO "https://github.com/protocolbuffers/protobuf/releases/download/v22.2/protoc-22.2-linux-x86_64.zip" 36 | - run: unzip protoc-22.2-linux-x86_64.zip -d /tmp/grpc 37 | - run: echo "/tmp/grpc/bin" >> $GITHUB_PATH 38 | - run: npm install 39 | # run unit tests 40 | - run: COHERENCE_VERSION=22.06.11 npm run test-cycle 41 | - run: COHERENCE_VERSION=25.03.1 npm run test-cycle 42 | - run: npm install --no-save typedoc 43 | # generate dist which runs other tasks 44 | - run: npm run dist 45 | # publish to NPM 46 | - run: npm publish 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_KEY }} 49 | - name: Archive Failure Logs 50 | if: failure() 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: npm.log 54 | path: /home/runner/.npm/_logs/* 55 | -------------------------------------------------------------------------------- /.github/workflows/trivy-scan.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Oracle Corporation and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at 3 | # https://oss.oracle.com/licenses/upl. 4 | 5 | # --------------------------------------------------------------------------- 6 | # Coherence JS Client GitHub Actions Scheduled Trivy Scan 7 | # --------------------------------------------------------------------------- 8 | name: Scheduled Trivy Scan 9 | 10 | on: 11 | workflow_dispatch: 12 | schedule: 13 | # Every day at midnight 14 | - cron: '0 0 * * *' 15 | 16 | jobs: 17 | trivy-scan: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Run Trivy vulnerability scanner to scan repo 25 | uses: aquasecurity/trivy-action@0.31.0 26 | with: 27 | scan-type: 'fs' 28 | exit-code: 1 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at 3 | # https://oss.oracle.com/licenses/upl. 4 | 5 | node_modules/** 6 | .idea 7 | lib 8 | src/grpc 9 | etc/proto 10 | etc/cert 11 | etc/jvm-args.txt 12 | .npmrc 13 | docs 14 | coverage 15 | /.nyc_output 16 | oracle-coherence-*tgz 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .idea 3 | .nyc_output 4 | coverage 5 | node_modules 6 | test 7 | etc 8 | bin 9 | src/tsconfig.json 10 | .gitignore 11 | .nycrc 12 | CONTRIBUTING.md 13 | DEVELOPING.md 14 | SECURITY.md 15 | typedoc.json 16 | build_spec.yaml 17 | logs*.txt 18 | *.zip 19 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "src/grpc/**", 4 | "lib/grpc/**", 5 | "test/**" 6 | ], 7 | "reporter": "html" 8 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 9 | # Contributing to this repository 10 | 11 | We welcome your contributions! There are multiple ways to contribute. 12 | 13 | ## Opening issues 14 | 15 | For bugs or enhancement requests, please file a GitHub issue unless it's 16 | security related. When filing a bug remember that the better written the bug is, 17 | the more likely it is to be fixed. If you think you've found a security 18 | vulnerability, do not raise a GitHub issue and follow the instructions in our 19 | [security policy](./SECURITY.md). 20 | 21 | ## Contributing code 22 | 23 | We welcome your code contributions. Before submitting code via a pull request, 24 | you will need to have signed the [Oracle Contributor Agreement][OCA] (OCA) and 25 | your commits need to include the following line using the name and e-mail 26 | address you used to sign the OCA: 27 | 28 | ```text 29 | Signed-off-by: Your Name 30 | ``` 31 | 32 | This can be automatically added to pull requests by committing with `--sign-off` 33 | or `-s`, e.g. 34 | 35 | ```text 36 | git commit --signoff 37 | ``` 38 | 39 | Only pull requests from committers that can be verified as having signed the OCA 40 | can be accepted. 41 | 42 | ## Pull request process 43 | 44 | 1. Ensure there is an issue created to track and discuss the fix or enhancement 45 | you intend to submit. 46 | 1. Fork this repository. 47 | 1. Create a branch in your fork to implement the changes. We recommend using 48 | the issue number as part of your branch name, e.g. `1234-fixes`. 49 | 1. Ensure that any documentation is updated with the changes that are required 50 | by your change. 51 | 1. Ensure that any samples are updated if the base image has been changed. 52 | 1. Submit the pull request. *Do not leave the pull request blank*. Explain exactly 53 | what your changes are meant to do and provide simple steps on how to validate. 54 | your changes. Ensure that you reference the issue you created as well. 55 | 1. We will assign the pull request to 2-3 people for review before it is merged. 56 | 57 | ## Code of conduct 58 | 59 | Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd 60 | like more specific guidelines, see the [Contributor Covenant Code of Conduct][COC]. 61 | 62 | [OCA]: https://oca.opensource.oracle.com 63 | [COC]: https://www.contributor-covenant.org/version/1/4/code-of-conduct/ 64 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Developing Coherence JavaScript Client 9 | 10 | ### Requirements 11 | * Node version 18.15.x or later 12 | * NPM 9.x or later 13 | 14 | ### Runnable NPM Scripts 15 | * `compile` - compiles the TypeScript sources to the `lib` directory 16 | * `clean` - removes all generated code, coverage, and documentation artifacts 17 | * `full-clean` - runs `clean` and removes the local `node_modules` directory 18 | * `test` - runs the unit tests 19 | * `test-cycle` - starts the cluster and if successful, runs the unit tests. After the cluster will be stopped. 20 | * `coverage` - runs the unit tests and gathers coverage metrics (results found in `coverage` directory) 21 | * `coh-up` - starts a two-member Coherence cluster for testing/developing against 22 | * `coh-down` - stops the previously started Coherence cluster 23 | * `dist` - creates a test distribution for inspection prior to publish 24 | 25 | ### Project Structure 26 | * `bin` - various shell scripts that will be called by npm 27 | * `etc` - contains the ProtoBuff .protoc files and other various files 28 | * `src` - TypeScript source files and related resources 29 | * `test` - contains the library test cases in plain JavaScript 30 | 31 | ### Building the Project 32 | * run `npm install` - this will install the necessary dependencies and compile the grpc artifacts 33 | * run `npm run compile` - this compiles the `Typescript` sources 34 | 35 | ### Running the Unit Tests 36 | * run `npm run coh-up` - this starts a Coherence test Docker container. This instance exposes the `grpc` port `1408` and exposes port `5005` for java debugging of the Coherence instance. To view the JSON payloads being sent to Coherence, check the docker container log for the instance this command started. 37 | * run `npm run test` - this will run all unit tests. You may optionally run the tests individually via an IDE as long as the Coherence container mentioned in the previous step was started. 38 | * run `npm run coh-down` when testing is complete and the Coherence test container is no longer needed. 39 | 40 | The above can also be shortened to: 41 | * `npm run test-cycle` - this will start the cluster, run test tests if the cluster start was successful, and then stop the cluster 42 | * `npm run test-cycle-tls` - The same as `test-cycle`, but will use TLS 43 | 44 | However, if developing new functionality or tests, the manual start of the cluster using `coh-up` may be preferred as 45 | it avoids restarting the cluster allowing for quicker development times. 46 | 47 | **Important!** When calling `coh-up`, `test`, `coh-down`, or `test-cycle` the LTS version of Coherence will be used (`22.06.11`). 48 | To use a later Coherence version, such as `22.03`, prefix the calls with, or export `COHERENCE_VERSION=`. 49 | For example: 50 | ```bash 51 | COHERENCE_VERSION=22.03 npm run test-cycle 52 | ``` 53 | 54 | ### Generating Documentation 55 | * Install `typedoc` globally: `npm install -g typescript && npm install -g typedoc` 56 | * Run `typedoc` from project root. The Generated documentation 57 | will be available in the `docs` directory. 58 | 59 | ### Code Style 60 | * Currently based on https://google.github.io/styleguide/jsguide#formatting 61 | 62 | ### FAQ 63 | * Question: How do I use the library locally in another project for testing purposes? 64 | > Answer: First, run `npm link` within the `coherence-js-client` project to create a global NPM reference. 65 | > then, from the project you want to use the library with, run `npm link @oracle/coherence` which 66 | > will install the library for use with that project 67 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2000, 2023 Oracle and/or its affiliates. 2 | 3 | The Universal Permissive License (UPL), Version 1.0 4 | 5 | Subject to the condition set forth below, permission is hereby granted to any 6 | person obtaining a copy of this software, associated documentation and/or data 7 | (collectively the "Software"), free of charge and under any and all copyright 8 | rights in the Software, and any and all patent rights owned or freely 9 | licensable by each licensor hereunder covering either (i) the unmodified 10 | Software as contributed to or provided by such licensor, or (ii) the Larger 11 | Works (as defined below), to deal in both 12 | 13 | (a) the Software, and 14 | (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if 15 | one is included with the Software (each a "Larger Work" to which the Software 16 | is contributed by such licensors), 17 | 18 | without restriction, including without limitation the rights to copy, create 19 | derivative works of, display, perform, and distribute the Software and make, 20 | use, sell, offer for sale, import, export, have made, and have sold the 21 | Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | either these or other terms. 23 | 24 | This license is subject to the following condition: 25 | The above copyright notice and either this complete permission notice or at 26 | a minimum a reference to the UPL must be included in all copies or 27 | substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Client for Oracle Coherence 2 | 3 | The JavaScript Client for Oracle Coherence allows Node applications to act as 4 | cache clients to a Coherence Cluster using Google's `gRPC` framework for 5 | the network transport. 6 | 7 | ### Features 8 | * Familiar `Map`-like interface for manipulating entries 9 | * Cluster-side querying and aggregation of map entries 10 | * Cluster-side manipulation of map entries using `EntryProcessors` 11 | * Registration of listeners to be notified of map mutations 12 | 13 | ### Requirements 14 | * Coherence CE versions `22.06`, `14.1.2-0-0`, `25.03` 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) 15 | * Node `18.15.x` or later 16 | * NPM `9.x` or later 17 | 18 | ### Usage 19 | 20 | Before testing the library, you must ensure a Coherence cluster is available. For local development, we recommend using the Coherence CE Docker image; it contains everything necessary for the client to operate correctly. 21 | 22 | ```bash 23 | docker run -d -p 1408:1408 ghcr.io/oracle/coherence-ce:25.03.1 24 | ``` 25 | 26 | or to save some keystrokes/time, use the included npm script, `coh-up` to start a two-member Cluster with the gRPC port at 1408" 27 | ```bash 28 | npm run coh-up 29 | ``` 30 | 31 | **Important!** When calling `coh-up` or `coh-down`, the LTS version of Coherence will be used (`22.06.11`). 32 | To use a later Coherence version, such as `25.03.1`, prefix the calls with, or export `COHERENCE_VERSION=`. 33 | For example: 34 | ```bash 35 | COHERENCE_VERSION=25.03.1 npm run coh-up 36 | ``` 37 | 38 | For more details on the image, see the [documentation](https://github.com/oracle/coherence/tree/master/prj/coherence-docker). 39 | 40 | ### Declare Your Dependency 41 | 42 | To use the JavaScript Client for Oracle Coherence, simply declare it as a dependency in your 43 | project's `package.json`: 44 | ``` 45 | ... 46 | "dependencies": { 47 | "@oracle/coherence": "^1.2", 48 | }, 49 | ... 50 | ``` 51 | 52 | ### Compatibility with Java Types 53 | The following table provides a listing of mappings between Java types and Javascript types when working with 54 | Coherence `25.03` or later. If using Coherence `22.06.x`, these types will be returned as Number. It is recommended 55 | using `25.03` if intentionally using `java.math.BigInteger` or `java.math.BigDecimal` as part of your application. 56 | 57 | | Java Type | JavascriptType | 58 | |----------------------|------------------------| 59 | | java.math.BigInteger | BigInt (ECMA standard) | 60 | | java.math.BigDecimal | Decimal ([decimal.js](https://www.npmjs.com/package/decimal.js)) | 61 | 62 | ### Examples 63 | 64 | > NOTE: The following examples assume the Coherence container is running locally. 65 | > You can start a container by running `npm run coh-up`. 66 | 67 | #### Establishing a Session 68 | 69 | The Coherence uses the concept of a `Session` to manage a set of related Coherence resources, 70 | such as maps and/or caches. When using the JavaScript Client for Oracle Coherence, a `Session` connects to a specific 71 | gRPC endpoint and uses a specific serialization format to marshal requests and responses. 72 | This means that different sessions using different serializers may connect to the same server endpoint. Typically, 73 | for efficiency the client and server would be configured to use matching serialization formats to avoid 74 | deserialization of data on the server, but this does not have to be the case. If the server is using a different 75 | serializer for the server-side caches, it must be able to deserialize the client's requests, so there must be 76 | a serializer configured on the server to match that used by the client. 77 | 78 | > NOTE: Currently, the JavaScript Client for Oracle Coherence only supports JSON serialization 79 | 80 | A `Session` is constructed using an `Options` instance, or a generic object with the same keys and values. 81 | 82 | The currently supported properties are: 83 | * `address` - the address of the Coherence gRPC proxy. This defaults to `localhost:1408`. 84 | * `requestTimeoutInMillis` - the gRPC request timeout in milliseconds. This defaults to `60000`. 85 | * `callOptions` - per-request gRPC call options. 86 | * `tls` - options related to the configuration of TLS. 87 | - `enabled` - determines if TLS is enabled or not. This defaults to `false` (NOTE: assumes `true` if all three `COHERENCE_TLS_*` (see subsequent bullets) environment variables are defined) 88 | - `caCertPath` - the path to the CA certificate. This may be configured using the environment variable `COHERENCE_TLS_CERTS_PATH` 89 | - `clientCertPath` - the path to the client certificate. This may be configured with the environment variable `COHERENCE_TLS_CLIENT_CERT` 90 | - `clientKeyPath` - the path to the client certificate key. This may be configured with the environment variable `COHERENCE_TLS_CLIENT_KEY` 91 | 92 | *NOTE*: If testing locally generated certificates, set `COHERENCE_IGNORE_INVALID_CERTS` to `true` to disable 93 | TLS validation of the certificates. 94 | 95 | ```typescript 96 | const { Session } = require('@oracle/coherence') 97 | 98 | let session = new Session() 99 | ``` 100 | 101 | This is the simplest invocation which assumes the following defaults: 102 | * `address` is `localhost:1408` 103 | * `requestTimeoutInMillis` is `60000` 104 | * `tls` is `disabled` 105 | 106 | To use values other than the default, create a new `Options` instance, configure as desired, 107 | and pass it to the constructor of the `Session`: 108 | 109 | ```javascript 110 | const { Session, Options } = require('@oracle/coherence') 111 | 112 | const opts = new Options() 113 | opts.address = 'example.com:4444' 114 | 115 | let session = new Session(opts) 116 | ``` 117 | 118 | or instead of an `Options` instance, using a generic JavaScript object: 119 | 120 | ```javascript 121 | const { Session } = require('@oracle/coherence') 122 | 123 | const opts = new Options({address: 'example.com:4444'}) 124 | 125 | let session = new Session(opts) 126 | ``` 127 | 128 | As of v1.2.3 of the JavaScript Client for Oracle Coherence, it's now possible to use the Coherence 129 | NameService to lookup gRPC Proxy endpoints. The format to enable this feature is 130 | `coherence:///([:port]|[/cluster-name]|[:port/cluster-name])` 131 | 132 | For example: 133 | * `coherence:///localhost` will connect to the name service bound to a local coherence cluster on port `7574` (the default Coherence cluster port). 134 | * `coherence:///localhost:8000` will connect to the name service bound to a local coherence cluster on port `8000`. 135 | * `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. 136 | * `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. 137 | 138 | While this is useful for local development, this may have limited uses in a production environment. For example, 139 | Coherence running within a container with the cluster port (`7574`) exposed so external clients may connect. The 140 | lookup will fail to work for the client as the Coherence name service return a private network address which 141 | won't resolve. Lastly, if connecting to a cluster that has multiple proxies bound to different ports, gRPC, by default, 142 | will use the first address returned by the resolver. It is possible to enable round-robin load balancing by including 143 | a custom channel option when creating the session: 144 | 145 | ```typescript 146 | const { Session } = require('@oracle/coherence') 147 | 148 | const opts = new Options({address: 'coherence:///localhost', 149 | channelOptions: {'grpc.service_config': JSON.stringify({ loadBalancingConfig: [{ round_robin: {} }], })}}) 150 | 151 | let session = new Session(opts) 152 | ``` 153 | 154 | *NOTE* The Coherence NameService feature requires Node `20.x` or later. 155 | 156 | It's also possible to control the default address the session will bind to by providing 157 | an address via the `COHERENCE_SERVER_ADDRESS` environment variable. The format of the value would 158 | be the same as if you configured it programmatically as the above example shows. 159 | 160 | Once the session has been constructed, it will now be possible to create maps and caches. 161 | 162 | #### Basic Map Operations 163 | 164 | The map (`NamedMap`) and cache (`NamedCache`) implementations provide the same basic features as the Map provided 165 | by JavaScript except for the following differences: 166 | 167 | * key equality isn't restricted to reference equality 168 | * insertion order is not maintained 169 | * `set()` calls cannot be chained because of the asynchronous nature of the API 170 | 171 | > NOTE: The only difference between `NamedCache` and `NamedMap` is that the 'NamedCache' allows associating a 172 | > `time-to-live` on the cache entry, while `NamedMap` does not 173 | 174 | For the following examples, let's assume that we have a Map defined in Coherence named `Test`. 175 | To get access to the map from the client: 176 | 177 | > NOTE: If using the Docker image previously mentioned for testing, you don't need to worry about the details of the map name. Any name will work. 178 | 179 | ```javascript 180 | let map = session.getMap('Test') 181 | ``` 182 | 183 | Once we have a handle to our map, we can invoke the same basic operations as a standard JavaScript Map: 184 | ```javascript 185 | await map.size 186 | // (zero) 187 | 188 | await map.set('key1', 'value1') 189 | await map.set('key2', 'value2') 190 | // returns a Promise vs the map itself, so these can't be chained 191 | 192 | await map.size 193 | // (two) 194 | 195 | await map.get('key1') 196 | // value1 197 | 198 | await map.has('key2') 199 | // true 200 | 201 | await map.has('key3') 202 | // false 203 | 204 | await map.keys() 205 | // ['key1', 'key2'] 206 | 207 | await map.values() 208 | // ['value1', 'value2'] 209 | 210 | await map.entries() 211 | // [{key: 'key1', value: 'value1'}, {key: 'key2', value: 'value2'}] 212 | 213 | await map.forEach((value, key) => console.log(key + ': ' + value)) 214 | // prints all of the entries 215 | ``` 216 | 217 | #### Querying the Map 218 | 219 | Coherence provides a rich set of primitives that allow developers to create advanced queries against 220 | a set of entries returning only those keys and/or values matching the specified criteria. 221 | See the [documentation](https://oracle.github.io/coherence/23.09/api/java/index.html) for details 222 | on the Filters provided by this client. 223 | 224 | Let's assume we have a `NamedMap` in which we're storing `string` keys and some objects with the structure of: 225 | 226 | ``` 227 | { 228 | name: 229 | age: 230 | hobbies: [] // of string 231 | } 232 | ``` 233 | 234 | First, let's insert a few objects: 235 | 236 | ```javascript 237 | await map.set('0001', {name: "Bill Smith", age: 38, hobbies: ["gardening", "painting"]}) 238 | await map.set('0002', {name: "Fred Jones", age: 56, hobbies: ["racing", "golf"]}) 239 | await map.set('0003', {name: "Jane Doe", age: 48, hobbies: ["gardening", "photography"]}) 240 | ``` 241 | 242 | Using a filter, we can limit the result set returned by the map: 243 | 244 | ```javascript 245 | const { Filters } = require('@oracle/coherence') 246 | 247 | // ... 248 | 249 | await map.entries(Filters.greater('age', 40)) 250 | // [{key: '0002', value: {name: "Fred Jones"...}}, {key: '0002', value: {name: "Jane Doe"...}}] 251 | 252 | await map.keys(Filters.arrayContains('hobbies', 'gardening')) 253 | // ['0001', '0003'] 254 | 255 | await map.values(Filters.not(Filters.arrayContains('hobbies', 'gardening'))) 256 | // [{name: "Fred Jones", age: 56, hobbies: ["racing", "golf"]}] 257 | ``` 258 | 259 | #### Aggregation 260 | 261 | Coherence provides developers with the ability to process some subset of the entries in a map, 262 | resulting in an aggregated result. See the [documentation](https://oracle.github.io/coherence/23.09/api/java/index.html) for aggregators provided by this client. 263 | 264 | Assume the same set of keys and values are present from the filtering example above: 265 | 266 | ```javascript 267 | const { Aggregators, Filters } = require('@oracle/coherence') 268 | 269 | // ... 270 | 271 | await map.aggregate(Aggregators.average('age')) 272 | // 47.3 273 | 274 | await map.aggregate(Aggregators.sum('age')) 275 | // 142 276 | 277 | await map.aggregate(Filters.greater('age', 40), Aggregators.count()) 278 | // 2 279 | ``` 280 | 281 | #### Entry Processing 282 | 283 | An entry processor allows mutation of map entries in-place within the cluster instead of bringing the entire object 284 | to the client, updating, and pushing the value back. See the [documentation](https://oracle.github.io/coherence/23.09/api/java/index.html) for the processors provided by this client. 285 | 286 | Assume the same set of keys and values are present from the filtering and aggregation examples: 287 | 288 | ```javascript 289 | const { Filters, Processors } = require('@oracle/coherence') 290 | 291 | // ... 292 | 293 | // targeting a specific entry 294 | await map.invoke('0001', Processors.extract('age')) 295 | // returns: 38 296 | 297 | // target all entries across the cluster 298 | await map.invokeAll(Processors.extract('age')) 299 | // returns: [['0001', 38], ['0002', 56], ['0003', 48]] 300 | 301 | // target all entries matching filtered critera 302 | await map.invokeAll(Filters.greater('age', 40), Processors.extract('age')) 303 | // returns: [['0002', 56], ['0003', 48]] 304 | 305 | // incrementing a number 'in-place' 306 | await map.invokeAll(Filters.greater('age', 40), Processors.increment('age', 1)) 307 | // returns [['0002', 57], ['0003', 49]] 308 | 309 | // update a value 'in-place' 310 | await map.invoke('0001', Processors.update('age', 100)) 311 | // returns true meaning the value was updated 312 | await map.get('0001') 313 | // the value will reflect the new age value 314 | ``` 315 | 316 | ### Events 317 | 318 | Coherence provides the ability to subscribe to notifications pertaining to a particular map/cache. 319 | Registration works similarly to event registration with Node, with some key differences. In addition 320 | to listening for specific events, it is possible to listen to events for changes made to a specific key, or using 321 | a Filter, it's possible to limit the events raised to be for a subset of the map entries. 322 | 323 | Now, let's register a listener: 324 | 325 | ```javascript 326 | import { event } from '@oracle/coherence' 327 | 328 | const MapEventType = event.MapEventType 329 | const MapListener = event.MapListener 330 | 331 | const handler = (event: MapEvent) => { 332 | console.log('Event: ' + event.description 333 | + ', Key: ' + JSON.stringify(event.key) 334 | + ', New Value: ' + JSON.stringify(event.newValue) 335 | + ', Old Value: ' + JSON.stringify(event.oldValue)) 336 | } 337 | 338 | const listener = new MapListener() 339 | .on(MapEventType.INSERT, handler) 340 | .on(MapEventType.UPDATE, handler) 341 | .on(MapEventType.DELETE, handler) 342 | 343 | // register to receive all event types for all entries within the map 344 | await map.addMapListener(listener) 345 | 346 | await map.set('a', 'b') 347 | // Event: insert, Key: a, New Value: b, Old Value: null 348 | 349 | await map.set('a', 'c') 350 | // Event: update, Key: a, New Value: c, Old Value: b 351 | 352 | await map.delete('a') 353 | // Event: delete, Key: a, New Value: null, Old Value: c 354 | 355 | // remove the listeners 356 | await map.removeMapListener(listener) 357 | 358 | // ======================================= 359 | 360 | // Assume the previous listener as well as the following key and values 361 | // ['0001', {name: "Bill Smith", age: 38, hobbies: ["gardening", "painting"]}] 362 | // ['0002', {name: "Fred Jones", age: 56, hobbies: ["racing", "golf"]}] 363 | // ['0003', {name: "Jane Doe", age: 48, hobbies: ["gardening", "photography"]}] 364 | 365 | // Add handlers for updates to '0001' 366 | await map.addMapListener(listener, '0001') 367 | 368 | await map.update('0002', '0002') 369 | // does not generate any events 370 | 371 | await map.invoke('0001', Processors.increment('age', 1)) 372 | // Event: update, Key: 0001, New Value: {name: "Bill Smith", age: 39, hobbies: ["gardening", "painting"]}, Old Value: {name: "Bill Smith", age: 38, hobbies: ["gardening", "painting"]} 373 | 374 | await map.delete('0001') 375 | // does not generate any events 376 | 377 | // remove the key listener 378 | await map.removeMapListener(listener, '0001') 379 | 380 | // ======================================= 381 | 382 | // Assume the same setup as the previous example, except instead of listening to events for a single key, 383 | // we'll instead listen for events raised for entries that match the filtered criteria. 384 | const filter = Filters.event(Filters.greater('age', 40), filter.MapEventFilter.UPDATED) 385 | 386 | // Listen to all updates to entries where the age property of the entry value is greater than 40 387 | await map.addMapListener(listener, filter) 388 | 389 | await map.invokeAll(Processors.increment('age', 1)); 390 | // Event: update, Key: 0002, New Value: {name: "Fred Jones", age: 57, hobbies: ["racing", "golf"]}, Old Value: {name: "Fred Jones", age: 56, hobbies: ["racing", "golf"]} 391 | // Event: update, Key: 0003, New Value: "Jane Doe", age: 49, hobbies: ["gardening", "photography"]}, Old Value: "Jane Doe", age: 48, hobbies: ["gardening", "photography"]} 392 | 393 | // remove the filter listener 394 | await map.removeMapListener(listener, filter) 395 | ``` 396 | 397 | ### Cut/Paste Example 398 | Here's an example that can be pasted into a new node project that is using this library: 399 | 400 | ```javascript 401 | const { Session } = require('@oracle/coherence') 402 | 403 | let session = new Session() 404 | let map = session.getMap('Test') 405 | 406 | setImmediate(async () => { 407 | console.log("Map size is " + (await map.size)) 408 | 409 | console.log("Inserting entry (key=1, value=One)") 410 | await map.set(1, "One") 411 | 412 | console.log("Inserting entry (key=2, value=Two)") 413 | await map.set(2, "Two") 414 | 415 | let entries = await map.entries(); 416 | 417 | console.log("All entries") 418 | for await (const entry of entries) { 419 | console.log(entry.key + '=' + entry.value) 420 | } 421 | 422 | console.log("Key 1 is " + (await map.get(1))) 423 | console.log("Key 2 is " + (await map.get(2))) 424 | 425 | console.log("Deleting entry (key=1)") 426 | await map.delete(1) 427 | 428 | console.log("Map size is " + (await map.size)) 429 | await session.close() 430 | }) 431 | ``` 432 | 433 | When run, it produces: 434 | 435 | ```bash 436 | Map size is 0 437 | Inserting entry (key=1, value=One) 438 | Map entry is One 439 | Deleting entry (key=1) 440 | Map size is 0 441 | ``` 442 | 443 | ### References 444 | * Oracle Coherence JavaScript Client - https://oracle.github.io/coherence-js-client/ 445 | * Oracle Coherence CE Documentation - https://coherence.community/23.09/docs/#/docs/about/01_overview 446 | 447 | ## Contributing 448 | 449 | This project welcomes contributions from the community. Before submitting a pull request, please [review our contribution guide](./CONTRIBUTING.md) 450 | 451 | ## Security 452 | 453 | Please consult the [security guide](./SECURITY.md) for our responsible security vulnerability disclosure process 454 | 455 | ## License 456 | 457 | Copyright (c) 2020, 2023 Oracle and/or its affiliates. 458 | 459 | Released under the Universal Permissive License v1.0 as shown at 460 | . 461 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security vulnerabilities 2 | 3 | Oracle values the independent security research community and believes that 4 | responsible disclosure of security vulnerabilities helps us ensure the security 5 | and privacy of all our users. 6 | 7 | Please do NOT raise a GitHub Issue to report a security vulnerability. If you 8 | believe you have found a security vulnerability, please submit a report to 9 | [secalert_us@oracle.com][1] preferably with a proof of concept. Please review 10 | some additional information on [how to report security vulnerabilities to Oracle][2]. 11 | We encourage people who contact Oracle Security to use email encryption using 12 | [our encryption key][3]. 13 | 14 | We ask that you do not use other channels or contact the project maintainers 15 | directly. 16 | 17 | Non-vulnerability related security issues including ideas for new or improved 18 | security features are welcome on GitHub Issues. 19 | 20 | ## Security updates, alerts and bulletins 21 | 22 | Security updates will be released on a regular cadence. Many of our projects 23 | will typically release security fixes in conjunction with the 24 | Oracle Critical Patch Update program. Additional 25 | information, including past advisories, is available on our [security alerts][4] 26 | page. 27 | 28 | ## Security-related information 29 | 30 | We will provide security related information such as a threat model, considerations 31 | for secure use, or any known security issues in our documentation. Please note 32 | that labs and sample code are intended to demonstrate a concept and may not be 33 | sufficiently hardened for production use. 34 | 35 | [1]: mailto:secalert_us@oracle.com 36 | [2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html 37 | [3]: https://www.oracle.com/security-alerts/encryptionkey.html 38 | [4]: https://www.oracle.com/security-alerts/ 39 | -------------------------------------------------------------------------------- /bin/docker-utils.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2020, 2023, Oracle and/or its affiliates. 4 | # 5 | # Licensed under the Universal Permissive License v 1.0 as shown at 6 | # https://oss.oracle.com/licenses/upl. 7 | 8 | set -e 9 | 10 | declare VERSION=${COHERENCE_VERSION:=22.06.11} 11 | declare TYPE=${COHERENCE_TYPE:=coherence-ce} 12 | declare REGISTRY=${DOCKER_REGISTRY:=ghcr.io/oracle} 13 | 14 | echo ${VERSION} 15 | echo ${TYPE} 16 | 17 | function coh_up() { 18 | echo "Starting test containers ..." 19 | DOCKER_REGISTRY="${REGISTRY}" COHERENCE_VERSION="${VERSION}" COHERENCE_TYPE="${TYPE}" docker compose --parallel 1 -f etc/docker-compose-2-members.yaml up --force-recreate --renew-anon-volumes -d 20 | SECONDS=0 21 | echo "Waiting for Coherence to be healthy (within 60s) ..." 22 | while [ ${SECONDS} -le 60 ]; do 23 | READY=$(curl -o /dev/null -s -w "%{http_code}" "http://127.0.0.1:6676/ready") || true 24 | if [ "${READY}" -eq "200" ]; then 25 | sleep 5 26 | echo "Coherence is ready!" 27 | return 28 | fi 29 | done 30 | node_version=$(node -v) 31 | filename="logs-startup-${VERSION}-${node_version}.txt" 32 | DOCKER_REGISTRY="${REGISTRY}" COHERENCE_VERSION="${VERSION}" COHERENCE_TYPE="${TYPE}" docker compose --parallel 1 -f etc/docker-compose-2-members.yaml logs --no-color > "${filename}" 33 | echo "Coherence failed to become healthy. See ${filename} for details." 34 | coh_down 35 | exit 1 36 | } 37 | 38 | function coh_down() { 39 | DOCKER_REGISTRY="${REGISTRY}" COHERENCE_VERSION="${VERSION}" COHERENCE_TYPE="${TYPE}" docker compose -f etc/docker-compose-2-members.yaml down -v 40 | } 41 | 42 | while getopts "ud" OPTION; do 43 | case "${OPTION}" in 44 | u) 45 | coh_up 46 | ;; 47 | d) 48 | coh_down 49 | ;; 50 | ?) 51 | echo "Usage: $(basename "$0") [-u] [-d]" 52 | exit 1 53 | ;; 54 | esac 55 | done 56 | -------------------------------------------------------------------------------- /bin/keys.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Copyright (c) 2023, Oracle and/or its affiliates. 5 | # Licensed under the Universal Permissive License v 1.0 as shown at 6 | # https://oss.oracle.com/licenses/upl. 7 | # 8 | 9 | set -e 10 | 11 | if [ "${COMPUTER_NAME}" == "" ] 12 | then 13 | COMPUTER_NAME="127.0.0.1" 14 | fi 15 | 16 | CERTS_DIR=$1 17 | if [ -z "${CERTS_DIR}" ] ; then 18 | echo "Please provide certs_dir" 19 | exit 1 20 | fi 21 | 22 | # check for location of openssl.cnf 23 | if [ -f "/etc/ssl/openssl.cnf" ]; then 24 | export SSL_CNF=/etc/ssl/openssl.cnf 25 | else 26 | export SSL_CNF=/etc/pki/tls/openssl.cnf 27 | fi 28 | 29 | mkdir -p "${CERTS_DIR}" 30 | 31 | # Generate random passwords for each run 32 | CAPASS=$(uuidgen | sha256sum | cut -f 1 -d " ") 33 | 34 | echo Generate Guardians CA key: 35 | echo "${CAPASS}" | openssl genrsa -passout stdin -aes256 \ 36 | -out "${CERTS_DIR}"/guardians-ca.key 4096 37 | 38 | echo Generate Guardians CA certificate: 39 | echo "${CAPASS}" | openssl req -passin stdin -new -x509 -days 3650 \ 40 | -reqexts SAN \ 41 | -config <(cat "${SSL_CNF}" \ 42 | <(printf "\n[SAN]\nsubjectAltName=DNS:localhost,DNS:127.0.0.1")) \ 43 | -key "${CERTS_DIR}"/guardians-ca.key \ 44 | -out "${CERTS_DIR}"/guardians-ca.crt \ 45 | -subj "/CN=${COMPUTER_NAME}" # guardians-ca.crt is a trustCertCollectionFile 46 | 47 | echo Generate client Star-Lord key 48 | echo "${CAPASS}" | openssl genrsa -passout stdin -aes256 \ 49 | -out "${CERTS_DIR}"/star-lord.key 4096 50 | 51 | echo Generate client Star-Lord signing request: 52 | echo "${CAPASS}" | openssl req -passin stdin -new \ 53 | -key "${CERTS_DIR}"/star-lord.key \ 54 | -out "${CERTS_DIR}"/star-lord.csr -subj "/CN=Star-Lord" 55 | 56 | echo Self-signed client Star-Lord certificate: 57 | echo "${CAPASS}" | openssl x509 -passin stdin -req -days 3650 \ 58 | -in "${CERTS_DIR}"/star-lord.csr \ 59 | -CA "${CERTS_DIR}"/guardians-ca.crt \ 60 | -CAkey "${CERTS_DIR}"/guardians-ca.key \ 61 | -set_serial 01 \ 62 | -out "${CERTS_DIR}"/star-lord.crt # star-lord.crt is the certChainFile for the client (Mutual TLS only) 63 | 64 | echo Remove passphrase from Star-Lord key: 65 | echo "${CAPASS}" | openssl rsa -passin stdin \ 66 | -in "${CERTS_DIR}"/star-lord.key \ 67 | -out "${CERTS_DIR}"/star-lord.key 68 | 69 | openssl pkcs8 -topk8 -nocrypt \ 70 | -in "${CERTS_DIR}"/star-lord.key \ 71 | -out "${CERTS_DIR}"/star-lord.pem # star-lord.pem is the privateKey for the Client (mutual TLS only) 72 | -------------------------------------------------------------------------------- /bin/npm-post-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2020, 2023, Oracle and/or its affiliates. 4 | # 5 | # Licensed under the Universal Permissive License v 1.0 as shown at 6 | # https://oss.oracle.com/licenses/upl. 7 | 8 | set -e 9 | 10 | declare -r ROOT="${PWD}" 11 | 12 | # Grabs the proto files from the Coherence project. 13 | function grab_proto_files() { 14 | declare -r BASE_URL="https://raw.githubusercontent.com/oracle/coherence/22.06.10/prj/coherence-grpc/src/main/proto/" 15 | declare -r PROTO_FILES=("messages.proto" "services.proto") 16 | declare -r PROTO_DIR="${ROOT}/etc/proto" 17 | 18 | if [[ ! -d "${PROTO_DIR}" ]]; then 19 | mkdir -p "${PROTO_DIR}" 20 | fi 21 | 22 | cd "${ROOT}" 23 | 24 | for i in "${PROTO_FILES[@]}"; do curl -s "${BASE_URL}${i}" -o "${PROTO_DIR}/${i}"; done 25 | } 26 | 27 | # Generates and compiles the stubs generated from the installed proto files. 28 | function gen_compile_proto_files() { 29 | declare -r PROTO_SRC_DIR="${ROOT}"/etc/proto 30 | declare -r PROTO_GEN_SRC_DIR="${ROOT}"/src/grpc 31 | declare -r PROTO_GEN_OUT_DIR="${ROOT}"/lib/grpc 32 | 33 | rm -rf "${PROTO_GEN_SRC_DIR}" "${PROTO_GEN_OUT_DIR}" 34 | mkdir -p "${PROTO_GEN_SRC_DIR}" "${PROTO_GEN_OUT_DIR}" 35 | 36 | npx grpc_tools_node_protoc \ 37 | --plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts \ 38 | --ts_out=grpc_js:"${PROTO_GEN_SRC_DIR}" \ 39 | --js_out=import_style=commonjs:"${PROTO_GEN_OUT_DIR}" \ 40 | --grpc_out=grpc_js:"${PROTO_GEN_OUT_DIR}" \ 41 | -I "${PROTO_SRC_DIR}" \ 42 | "${PROTO_SRC_DIR}"/*.proto 43 | } 44 | 45 | function main() { 46 | cp "${ROOT}"/etc/jvm-args-clear.txt "${ROOT}"/etc/jvm-args.txt 47 | grab_proto_files 48 | gen_compile_proto_files 49 | } 50 | 51 | main 52 | -------------------------------------------------------------------------------- /bin/test-cycle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Copyright (c) 2023, Oracle and/or its affiliates. 5 | # Licensed under the Universal Permissive License v 1.0 as shown at 6 | # https://oss.oracle.com/licenses/upl. 7 | # 8 | 9 | set -e 10 | mkdir -p "${PWD}"/etc/cert 11 | chmod 777 "${PWD}"/etc/cert 12 | 13 | declare VERSION=${COHERENCE_VERSION:=22.06.11} 14 | declare TYPE=${COHERENCE_TYPE:=coherence-ce} 15 | declare REGISTRY=${DOCKER_REGISTRY:=ghcr.io/oracle} 16 | declare LABEL=clear 17 | 18 | function run_secure() { 19 | LABEL=tls 20 | "${PWD}"/bin/keys.sh "${PWD}"/etc/cert 21 | cp "${PWD}"/etc/jvm-args-tls.txt "${PWD}"/etc/jvm-args.txt 22 | 23 | export COHERENCE_TLS_CERTS_PATH="${PWD}"/etc/cert/guardians-ca.crt 24 | export COHERENCE_TLS_CLIENT_CERT="${PWD}"/etc/cert/star-lord.crt 25 | export COHERENCE_TLS_CLIENT_KEY="${PWD}"/etc/cert/star-lord.pem 26 | export COHERENCE_IGNORE_INVALID_CERTS="true" 27 | 28 | run_tests 29 | } 30 | 31 | function run_clear() { 32 | cp "${PWD}"/etc/jvm-args-clear.txt "${PWD}"/etc/jvm-args.txt 33 | run_tests 34 | } 35 | 36 | function run_tests() { 37 | npm run compile 38 | npm run coh-up 39 | sleep 5 40 | timeout 3m npm exec mocha "${PWD}"/test/**.js --recursive --exit 41 | } 42 | 43 | function dump_logs() { 44 | node_version=$(node -v) 45 | DOCKER_REGISTRY="${REGISTRY}" COHERENCE_VERSION="${VERSION}" COHERENCE_TYPE="${TYPE}" docker compose -f etc/docker-compose-2-members.yaml logs --no-color > logs-"${1}"-test-"${VERSION}"-"${node_version}".txt 46 | } 47 | 48 | function cleanup() { 49 | dump_logs $LABEL 50 | npm run coh-down 51 | export -n COHERENCE_TLS_CERTS_PATH 52 | export -n COHERENCE_TLS_CLIENT_CERT 53 | export -n COHERENCE_TLS_CLIENT_KEY 54 | export -n COHERENCE_IGNORE_INVALID_CERTS 55 | cp "${PWD}"/etc/jvm-args-clear.txt "${PWD}"/etc/jvm-args.txt 56 | rm -rf "${PWD}"/etc/cert 57 | } 58 | 59 | trap cleanup EXIT 60 | 61 | while getopts "sc" OPTION; do 62 | case "${OPTION}" in 63 | s) 64 | run_secure 65 | ;; 66 | c) 67 | run_clear 68 | ;; 69 | ?) 70 | echo "Usage: $(basename "$0") [-s (run tests using TLS)] || [-c (run tests without TLS]]" 71 | exit 1 72 | ;; 73 | esac 74 | done -------------------------------------------------------------------------------- /etc/docker-compose-2-members.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023, Oracle Corporation and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at 3 | # https://oss.oracle.com/licenses/upl. 4 | 5 | version: "3.5" 6 | services: 7 | coherence1: 8 | hostname: server1 9 | networks: 10 | coherence: 11 | aliases: 12 | - server1 13 | image: ${DOCKER_REGISTRY}/${COHERENCE_TYPE}:${COHERENCE_VERSION} 14 | environment: 15 | - COHERENCE_WKA=server1 16 | ports: 17 | - "30000:30000" 18 | - "1408:1408" 19 | - "9612:9612" 20 | - "8080:8080" 21 | - "6676:6676" 22 | - "7574:7574" 23 | volumes: 24 | - .:/args 25 | - ./cert:/certs 26 | 27 | coherence2: 28 | hostname: server2 29 | networks: 30 | coherence: 31 | aliases: 32 | - server2 33 | image: ${DOCKER_REGISTRY}/${COHERENCE_TYPE}:${COHERENCE_VERSION} 34 | environment: 35 | - COHERENCE_WKA=server1 36 | ports: 37 | - "9613:9613" 38 | volumes: 39 | - .:/args 40 | - ./cert:/certs 41 | 42 | networks: 43 | coherence: -------------------------------------------------------------------------------- /etc/jvm-args-clear.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, 2025, Oracle and/or its affiliates. 2 | # 3 | # Licensed under the Universal Permissive License v 1.0 as shown at 4 | # https://oss.oracle.com/licenses/upl. 5 | 6 | -Xms1g 7 | -Xmx1g 8 | -Dcoherence.log.level=9 9 | -Dcoherence.io.json.debug=false 10 | -------------------------------------------------------------------------------- /etc/jvm-args-tls.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, 2025, Oracle and/or its affiliates. 2 | # 3 | # Licensed under the Universal Permissive License v 1.0 as shown at 4 | # https://oss.oracle.com/licenses/upl. 5 | 6 | -Xms1g 7 | -Xmx1g 8 | -Dcoherence.log.level=9 9 | -Dcoherence.io.json.debug=false 10 | -Dcoherence.grpc.server.socketprovider=tls-files 11 | -Dcoherence.security.key=/certs/star-lord.pem 12 | -Dcoherence.security.cert=/certs/star-lord.crt 13 | -Dcoherence.security.ca.cert=/certs/guardians-ca.crt 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oracle/coherence", 3 | "version": "1.2.4", 4 | "license": "UPL-1.0", 5 | "main": "lib/index.js", 6 | "keywords": [ 7 | "Oracle Coherence", 8 | "Coherence", 9 | "gRPC", 10 | "node" 11 | ], 12 | "repository": "https://github.com/oracle/coherence-js-client", 13 | "dependencies": { 14 | "@grpc/proto-loader": "^0.7", 15 | "@grpc/grpc-js": "^1.13", 16 | "google-protobuf": "^3.21", 17 | "decimal.js": "^10.5" 18 | }, 19 | "devDependencies": { 20 | "grpc-tools": "^1.13", 21 | "@types/google-protobuf": "^3.15", 22 | "glob-parent": "^6.0", 23 | "grpc_tools_node_protoc_ts": "^5.3", 24 | "mocha": "^11.5", 25 | "nyc": "^15.1", 26 | "source-map-support": "^0.5", 27 | "ts-node": "^10.9", 28 | "typescript": "^5.8" 29 | }, 30 | "scripts": { 31 | "grpc": "bin/npm-post-install.sh", 32 | "compile": "tsc -p src", 33 | "full-clean": "npm run clean; rm -rf node_modules", 34 | "clean": "rm -rf lib docs coverage .nyc_output oracle-*tgz", 35 | "test": "npm run compile && npm exec mocha 'test/**.js' --recursive --exit", 36 | "test-cycle": "bin/test-cycle.sh -c", 37 | "test-cycle-tls": "bin/test-cycle.sh -s", 38 | "test-resolver": "npm run compile && npm exec mocha test/discovery/resolver-tests.js --recursive --exit", 39 | "coh-up": "bin/docker-utils.sh -u", 40 | "coh-down": "bin/docker-utils.sh -d", 41 | "coverage": "nyc mocha 'test/**.js' --exit", 42 | "dist": "npm run compile; typedoc; npm pack", 43 | "prepare": "bin/npm-post-install.sh" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/extractors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2023, Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | import { util } from './util' 9 | 10 | export namespace extractor { 11 | 12 | /** 13 | * ValueExtractor is used to both extract values (for example, for sorting 14 | * or filtering) from an object, and to provide an identity for that extraction. 15 | */ 16 | export abstract class ValueExtractor { 17 | /** 18 | * Server-side ValueExtractor implementation type identifier. 19 | */ 20 | protected '@class': string 21 | 22 | /** 23 | * Construct a ValueExtractor. 24 | * 25 | * @param clz server-side ValueExtractor implementation type identifier 26 | */ 27 | protected constructor (clz: string) { 28 | this['@class'] = clz 29 | } 30 | 31 | /** 32 | * Returns a composed extractor that first applies the *before* 33 | * extractor to its input, and then applies this extractor to the result. 34 | * If evaluation of either extractor throws an exception, it is relayed 35 | * to the caller of the composed extractor. 36 | * 37 | * @param before the extractor to apply before this extractor is applied 38 | * 39 | * @return a composed extractor that first applies the *before* 40 | * extractor and then applies this extractor 41 | */ 42 | compose (before: ValueExtractor): ValueExtractor { 43 | util.ensureNotNull(before, 'before cannot be null') 44 | 45 | return (before instanceof ChainedExtractor) 46 | ? before.andThen(this) 47 | : new ChainedExtractor([before, this]) 48 | } 49 | 50 | /** 51 | * Returns a composed extractor that first applies this extractor to its 52 | * input, and then applies the *after* extractor to the result. If 53 | * evaluation of either extractor throws an exception, it is relayed to 54 | * the caller of the composed extractor. 55 | * 56 | * @param after the extractor to apply after this extractor is applied 57 | * 58 | * @return a composed extractor that first applies this extractor and then 59 | * applies the *after* extractor 60 | */ 61 | andThen (after: ValueExtractor): ValueExtractor { 62 | util.ensureNotNull(after, 'before cannot be null') 63 | 64 | return (!(after instanceof ChainedExtractor)) 65 | ? after.compose(this) 66 | : new ChainedExtractor([this, after]) 67 | } 68 | } 69 | 70 | /** 71 | * Abstract super class for {@link ValueExtractor} implementations that are based on 72 | * an underlying array of {@link ValueExtractor} objects. 73 | */ 74 | export class AbstractCompositeExtractor 75 | extends ValueExtractor { 76 | extractors: ValueExtractor[] 77 | 78 | /** 79 | * Constructs a new AbstractCompositeExtractor. 80 | * 81 | * @param typeName the server-side ValueExtractor implementation type identifier. 82 | * @param extractors an array of extractors 83 | */ 84 | protected constructor (typeName: string, extractors: ValueExtractor[]) { 85 | super(typeName) 86 | this.extractors = extractors 87 | } 88 | } 89 | 90 | /** 91 | * Universal ValueExtractor implementation. 92 | *

93 | * Either a property or method based extractor based on parameters passed to 94 | * constructor. 95 | * Generally, the name value passed to the `UniversalExtractor` constructor 96 | * represents a property unless the *name* value ends in `()`, 97 | * then this instance is a reflection based method extractor. 98 | * Special cases are described in the constructor documentation. 99 | */ 100 | export class UniversalExtractor 101 | extends ValueExtractor { 102 | 103 | /** 104 | * A method or property name. 105 | */ 106 | protected name: string 107 | 108 | /** 109 | * The parameter array. Must be `null` or `zero length` for a property based extractor. 110 | */ 111 | protected params?: any[] 112 | 113 | /** 114 | * Construct a UniversalExtractor based on a name and optional 115 | * parameters. 116 | * 117 | * If *name* does not end in `()`, this extractor is a property extractor. 118 | * If `name` is prefixed with one of `set` or `get` and ends in `()`, 119 | * this extractor is a property extractor. If the *name* 120 | * just ends in `()`, this extractor is considered a method extractor. 121 | * 122 | * @param name a method or property name 123 | * @param params the array of arguments to be used in the method 124 | * invocation; may be `null` 125 | */ 126 | constructor (name: string, params?: any[]) { 127 | super(extractorName('UniversalExtractor')) 128 | this.name = name 129 | if (params) { 130 | this.params = params 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Composite {@link ValueExtractor} implementation based on an array of extractors. 137 | * The extractors in the array are applied sequentially left-to-right, so a 138 | * result of a previous extractor serves as a target object for a next one. 139 | */ 140 | export class ChainedExtractor 141 | extends AbstractCompositeExtractor { 142 | 143 | /** 144 | * Create a new `ChainedExtractor`. 145 | * 146 | * @param extractorsOrMethod an array of {@link ValueExtractor}s, or a dot-delimited sequence of method 147 | * names which results in a ChainedExtractor that is based on an array of 148 | * corresponding {@link UniversalExtractor} objects 149 | */ 150 | constructor (extractorsOrMethod: ValueExtractor[] | string) { 151 | super(extractorName('ChainedExtractor'), 152 | ((typeof extractorsOrMethod === 'string') 153 | ? ChainedExtractor.createExtractors(extractorsOrMethod) 154 | : extractorsOrMethod)) 155 | } 156 | 157 | /** 158 | * Create a new `ChainedExtractor` based on the provided dot-delimited sequence of method names. 159 | * 160 | * @param fields a dot-delimited sequence of method names 161 | * 162 | * @return an array of {@link ValueExtractor}s based on the input string 163 | */ 164 | protected static createExtractors (fields: string): ValueExtractor[] { 165 | const names = fields.split('.').filter(f => f != null && f.length > 0) 166 | const arr = new Array() 167 | for (const name of names) { 168 | arr.push(new UniversalExtractor(name)) 169 | } 170 | 171 | return arr 172 | } 173 | } 174 | 175 | /** 176 | * A Trivial {@link ValueExtractor} implementation that does not actually extract 177 | * anything from the passed value, but returns the value itself. 178 | */ 179 | export class IdentityExtractor 180 | extends ValueExtractor { 181 | public static INSTANCE = new IdentityExtractor() 182 | 183 | /** 184 | * Constructs a new `IdentityExtractor` instance. 185 | */ 186 | protected constructor () { 187 | super(extractorName('IdentityExtractor')) 188 | } 189 | } 190 | 191 | 192 | /** 193 | * Composite ValueExtractor implementation based on an array of extractors. 194 | * All extractors in the array are applied to the same target object and the 195 | * result of the extraction is a array of extracted values. 196 | * 197 | * Common scenarios for using the MultiExtractor involve the 198 | * `DistinctValuesAggregator` or `GroupAggregator` aggregators that allow clients 199 | * to collect all distinct combinations of a given set of attributes or collect 200 | * and run additional aggregation against the corresponding groups of entries. 201 | */ 202 | export class MultiExtractor 203 | extends AbstractCompositeExtractor { 204 | /** 205 | * Constructs a new `MultiExtractor`. 206 | * 207 | * @param extractorsOrMethod an array of {@link ValueExtractor}s or a comma-delimited 208 | * of method names which results in a MultiExtractor that 209 | * is based on a corresponding array of {@link ValueExtractor} objects 210 | */ 211 | constructor (extractorsOrMethod: ValueExtractor[] | string) { 212 | super(extractorName('MultiExtractor'), 213 | ((typeof extractorsOrMethod === 'string') 214 | ? MultiExtractor.createExtractors(extractorsOrMethod) 215 | : extractorsOrMethod)) 216 | } 217 | 218 | /** 219 | * Parse a comma-delimited sequence of method names and instantiate 220 | * a corresponding array of {@link ValueExtractor} objects. 221 | * 222 | * @param methods a comma-delimited sequence of method names 223 | * 224 | * @return an array of {@link ValueExtractor} objects 225 | */ 226 | protected static createExtractors (methods: string): ValueExtractor[] { 227 | const names = methods.split(',').filter(f => f != null && f.length > 0) 228 | const arr = new Array() 229 | for (const name of names) { 230 | arr.push(name.indexOf('.') < 0 ? new UniversalExtractor(name) : new ChainedExtractor(name)) 231 | } 232 | 233 | return arr 234 | } 235 | } 236 | 237 | 238 | /** 239 | * ValueUpdater is used to update an object's state. 240 | */ 241 | export abstract class ValueUpdater { 242 | /** 243 | * The server-side `ValueUpdater` type identifier. 244 | */ 245 | protected readonly '@class': string 246 | 247 | /** 248 | * Constructs a new `ValueUpdater`. 249 | * 250 | * @param clz the server-side `ValueUpdater` type identifier 251 | */ 252 | protected constructor (clz: string) { 253 | this['@class'] = clz 254 | } 255 | } 256 | 257 | /** 258 | * ValueManipulator represents a composition of {@link ValueExtractor} and 259 | * {@link ValueUpdater} implementations. 260 | */ 261 | export interface ValueManipulator { 262 | 263 | /** 264 | * Retrieve the underlying ValueExtractor reference. 265 | * 266 | * @return the ValueExtractor 267 | */ 268 | getExtractor (): ValueExtractor; 269 | 270 | /** 271 | * Retrieve the underlying ValueUpdater reference. 272 | * 273 | * @return the ValueUpdater 274 | */ 275 | getUpdater (): ValueUpdater; 276 | } 277 | 278 | /** 279 | * A ValueUpdater implementation based on an extractor-updater pair that could 280 | * also be used as a ValueManipulator. 281 | */ 282 | export class CompositeUpdater 283 | extends ValueUpdater 284 | implements ValueManipulator { 285 | 286 | /** 287 | * The ValueExtractor part. 288 | */ 289 | protected readonly extractor: ValueExtractor 290 | 291 | /** 292 | * The ValueUpdater part. 293 | */ 294 | protected readonly updater: ValueUpdater 295 | 296 | /** 297 | * Constructs a new `CompositeUpdater`. 298 | * 299 | * @param methodOrExtractor the {@link ValueExtractor} or the name of the method to invoke via reflection 300 | * @param updater the {@link ValueUpdater} 301 | */ 302 | constructor (methodOrExtractor: string | ValueExtractor, updater?: ValueUpdater) { 303 | super(extractorName(('CompositeUpdater'))) 304 | if (updater) { 305 | // Two arg constructor 306 | this.extractor = methodOrExtractor as ValueExtractor 307 | this.updater = updater 308 | } else { 309 | // One arg with method name 310 | const methodName = methodOrExtractor as string 311 | util.ensureNonEmptyString(methodName, 'method name has to be non empty') 312 | 313 | const last = methodName.lastIndexOf('.') 314 | this.extractor = last == -1 315 | ? IdentityExtractor.INSTANCE 316 | : new ChainedExtractor(methodName.substring(0, last)) 317 | this.updater = new UniversalUpdater(methodName.substring(last + 1)) 318 | } 319 | } 320 | 321 | /** 322 | * @inheritDoc 323 | */ 324 | getExtractor (): ValueExtractor { 325 | return this.extractor 326 | } 327 | 328 | /** 329 | * @inheritDoc 330 | */ 331 | getUpdater (): ValueUpdater { 332 | return this.updater 333 | } 334 | } 335 | 336 | /** 337 | * Universal ValueUpdater implementation. 338 | * 339 | * Either a property-based and method-based {@link ValueUpdater} 340 | * based on whether constructor parameter *name* is evaluated to be a property or method. 341 | */ 342 | export class UniversalUpdater 343 | extends ValueUpdater { 344 | /** 345 | * The method or property name. 346 | */ 347 | protected readonly name: string 348 | 349 | /** 350 | * Construct a UniversalUpdater for the provided name. 351 | * If method ends in a '()', 352 | * then the name is a method name. This implementation assumes that a 353 | * target's class will have one and only one method with the 354 | * specified name and this method will have exactly one parameter; 355 | * if the method is a property name, there should be a corresponding 356 | * JavaBean property modifier method or it will be used as a 357 | * key in a Map. 358 | * 359 | * @param method a method or property name 360 | */ 361 | constructor (method: string) { 362 | super(extractorName('UniversalUpdater')) 363 | this.name = method 364 | } 365 | } 366 | 367 | function extractorName (name: string): string { 368 | return 'extractor.' + name 369 | } 370 | } 371 | 372 | /** 373 | * Simple Extractor DSL. 374 | * 375 | * @remarks 376 | * The methods in this class are for the most part simple factory methods for 377 | * various {@link extractor.ValueExtractor} classes, but in some cases provide additional type 378 | * safety. They also tend to make the code more readable, especially if imported 379 | * statically, so their use is strongly encouraged in lieu of direct construction 380 | * of {@link extractor.ValueExtractor} classes. 381 | */ 382 | export class Extractors { 383 | /** 384 | * Returns an extractor that extracts the specified fields or 385 | * extractors where extraction occurs in a chain where the result of each 386 | * field extraction is the input to the next extractor. The result 387 | * returned is the result of the final extractor in the chain. 388 | * 389 | * @param extractorsOrFields If extractorsOrFields is a string type, then the 390 | * field names to extract (if any field name contains a dot '.' 391 | * that field name is split into multiple field names delimiting on 392 | * the dots. If extractorsOrFields is of ValueExtractor[] type, 393 | * then the {@link extractor.ValueExtractor}s are used to extract the values 394 | * 395 | * @return an extractor that extracts the value(s) of the specified field(s) 396 | */ 397 | static chained (extractorsOrFields: extractor.ValueExtractor[] | string): extractor.ValueExtractor { 398 | let extractors = new Array() 399 | 400 | if (extractorsOrFields && (typeof extractorsOrFields === 'string')) { 401 | const s = extractorsOrFields as string 402 | if (s.length > 0) { 403 | for (const fieldName of s.split('.')) { 404 | extractors.push(Extractors.extract(fieldName)) 405 | } 406 | } 407 | } else { 408 | extractors = extractorsOrFields as extractor.ValueExtractor[] 409 | } 410 | 411 | if (extractors.length == 1) { 412 | return extractors[0] 413 | } 414 | return new extractor.ChainedExtractor(extractors) 415 | } 416 | 417 | /** 418 | * Returns an extractor that extracts the value of the specified field. 419 | * 420 | * @param from the name of the field or method to extract the value from. 421 | * @param params the parameters to pass to the method. 422 | * 423 | * @return an extractor that extracts the value of the specified field. 424 | */ 425 | static extract (from: string, params?: any[]): extractor.ValueExtractor { 426 | if (params) { 427 | if (!from.endsWith('()')) { 428 | from = from + '()' 429 | } 430 | } 431 | 432 | return new extractor.UniversalExtractor(from, params) 433 | } 434 | 435 | /** 436 | * Returns an extractor that always returns its input argument. 437 | * 438 | * @return an extractor that always returns its input argument 439 | */ 440 | static identity (): extractor.ValueExtractor { 441 | return extractor.IdentityExtractor.INSTANCE 442 | } 443 | 444 | /** 445 | * Returns an extractor that casts its input argument. 446 | * 447 | * @return an extractor that always returns its input argument 448 | */ 449 | static identityCast (): extractor.ValueExtractor { 450 | return extractor.IdentityExtractor.INSTANCE 451 | } 452 | 453 | /** 454 | * Returns an extractor that extracts the specified fields 455 | * and returns the extracted values in an array. 456 | * 457 | * @param extractorOrFields the field names to extract 458 | * 459 | * @return an extractor that extracts the value(s) of the specified field(s) 460 | */ 461 | static multi (extractorOrFields: extractor.ValueExtractor[] | string): extractor.MultiExtractor { 462 | return new extractor.MultiExtractor(extractorOrFields) 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2023, Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | export * from './extractors' 9 | export * from './session' 10 | export * from './events' 11 | export * from './aggregators' 12 | export * from './filters' 13 | export * from './processors' 14 | export * from './util' 15 | export { NamedMap, NamedCache, MapEntry } from './named-cache-client' 16 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es6", 5 | "module": "commonjs", 6 | "lib": [ 7 | "es6", 8 | "esnext.asynciterable", 9 | "es2020.bigint" 10 | ], 11 | "declaration": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "outDir": "../lib", 15 | "downlevelIteration": true, 16 | "strict": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "noImplicitReturns": true, 19 | "strictBindCallApply": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/aggregator-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2023, Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { Aggregators, Filters, Session, aggregator } = require('../lib') 9 | const test = require('./util') 10 | const assert = require('assert').strict 11 | const { describe, it, after, beforeEach } = require('mocha') 12 | 13 | describe('Aggregators IT Test Suite', function () { 14 | const val123 = { id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3], group: 1 } 15 | const val234 = { id: 234, str: '234', ival: 234, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' } 16 | const val345 = { id: 345, str: '345', ival: 345, fval: 34.5, iarr: [3, 4, 5], group: 2 } 17 | const val456 = { id: 456, str: '456', ival: 456, fval: 45.6, iarr: [4, 5, 6], group: 3, nullIfOdd: 'non-null' } 18 | 19 | const session = new Session() 20 | const cache = session.getCache('cache-client') 21 | this.timeout(30000) 22 | 23 | beforeEach(async () => { 24 | await cache.clear() 25 | await cache.set(val123, val123) 26 | await cache.set(val234, val234) 27 | await cache.set(val345, val345) 28 | await cache.set(val456, val456) 29 | 30 | assert.equal(await cache.empty, false) 31 | assert.equal(await cache.size, 4) 32 | }) 33 | 34 | after(() => { 35 | cache.release().finally(() => session.close()) 36 | }) 37 | 38 | describe('Average Aggregator', async () => { 39 | const agg = Aggregators.average('id') 40 | 41 | it('should aggregate all entries', async () => { 42 | const result = await cache.aggregate(agg) 43 | assert.equal(Number(result), 289.5) 44 | }) 45 | 46 | it('should aggregate filtered entries', async () => { 47 | const filter = Filters.between('id', 123, 456) 48 | const result = await cache.aggregate(filter, agg) 49 | assert.equal(Number(result), 289.5) 50 | }) 51 | 52 | it('should aggregate entries based on keys', async () => { 53 | const result = await cache.aggregate([val345, val456], agg) 54 | assert.equal(Number(result), 400.5) 55 | }) 56 | }) 57 | 58 | describe('Min Aggregator', () => { 59 | const agg = Aggregators.min('str') 60 | 61 | it('should aggregate all entries', async () => { 62 | const result = await cache.aggregate(agg) 63 | assert.equal(result, 123) 64 | }) 65 | 66 | it('should aggregate filtered entries', async () => { 67 | const filter = Filters.between('id', 345, 456, true, true) 68 | const result = await cache.aggregate(filter, agg) 69 | assert.equal(result, 345) 70 | }) 71 | 72 | it('should aggregate entries based on keys', async () => { 73 | const result = await cache.aggregate([val345, val456], agg) 74 | assert.equal(result, 345) 75 | }) 76 | }) 77 | 78 | describe('Max Aggregator', () => { 79 | const agg = Aggregators.max('fval') 80 | 81 | it('should aggregate all entries', async () => { 82 | const result = await cache.aggregate(agg) 83 | assert.equal(result, 45.6) 84 | }) 85 | 86 | it('should aggregate filtered entries', async () => { 87 | const filter = Filters.between('id', 123, 456, true, false) 88 | const result = await cache.aggregate(filter, agg) 89 | assert.equal(result, 34.5) 90 | }) 91 | 92 | it('should aggregate entries based on keys', async () => { 93 | const result = await cache.aggregate([val123, val345], agg) 94 | assert.equal(result, 34.5) 95 | }) 96 | }) 97 | 98 | describe('Count Aggregator', function () { 99 | const agg = Aggregators.count() 100 | 101 | it('should aggregate all entries', async () => { 102 | const result = await cache.aggregate(agg) 103 | assert.equal(result, 4) 104 | }) 105 | 106 | it('should aggregate filtered entries', async () => { 107 | const filter = Filters.between('id', 123, 456, true, false) 108 | const result = await cache.aggregate(filter, agg) 109 | assert.equal(result, 3) 110 | 111 | }) 112 | 113 | it('should aggregate entries based on keys', async () => { 114 | const result = await cache.aggregate([val123, val345], agg) 115 | assert.equal(result, 2) 116 | }) 117 | }) 118 | 119 | describe('Distinct Aggregator', () => { 120 | const agg = Aggregators.distinct('group') 121 | 122 | it('should aggregate all entries', async () => { 123 | const result = await cache.aggregate(agg) 124 | await test.compareElements([1, 2, 3], result) 125 | }) 126 | 127 | it('should aggregate filtered entries', async () => { 128 | const filter = Filters.between('id', 123, 456, true, false) 129 | const result = await cache.aggregate(filter, agg) 130 | await test.compareElements([1, 2], result) 131 | }) 132 | 133 | it('should aggregate entries based on keys', async () => { 134 | const result = await cache.aggregate([val123, val345], agg) 135 | await test.compareElements([1, 2], result) 136 | }) 137 | }) 138 | 139 | describe('GroupBy Aggregator', () => { 140 | const agg = Aggregators.groupBy('group', Aggregators.min('id'), Filters.always()) 141 | 142 | it('should aggregate all entries', async () => { 143 | const result = await cache.aggregate(agg) 144 | await test.compareElements([{ key: 2, value: 234 }, { key: 1, value: 123 }, { key: 3, value: 456 }], 145 | result) 146 | }) 147 | 148 | it('should aggregate filtered entries', async () => { 149 | const filter = Filters.between('id', 123, 456, true, false) 150 | const result = await cache.aggregate(filter, agg) 151 | await test.compareElements([{ key: 2, value: 234 }, { key: 1, value: 123 }], result) 152 | }) 153 | 154 | it('should aggregate entries based on keys', async () => { 155 | const result = await cache.aggregate([val123, val345], agg) 156 | await test.compareElements([{ key: 1, value: 123 }, { key: 2, value: 345 }], result) 157 | }) 158 | }) 159 | 160 | describe('Sum Aggregator', () => { 161 | const agg = Aggregators.sum('ival') 162 | 163 | it('should aggregate all entries', async () => { 164 | const result = await cache.aggregate(agg) 165 | test.checkNumericResult(result, 1158) 166 | }) 167 | 168 | it('should aggregate filtered entries', async () => { 169 | const filter = Filters.between('id', 123, 456, true, false) 170 | const result = await cache.aggregate(filter, agg) 171 | test.checkNumericResult(result, 702) 172 | }) 173 | 174 | it('should aggregate entries based on keys', async () => { 175 | const result = await cache.aggregate([val123, val456], agg) 176 | test.checkNumericResult(result, 579) 177 | }) 178 | }) 179 | 180 | describe('Priority Aggregator', () => { 181 | const agg = Aggregators.priority(Aggregators.sum('ival')) 182 | 183 | it('should have the expected structure', () => { 184 | assert.equal(agg['@class'], 'aggregator.PriorityAggregator') 185 | assert.equal(agg.requestTimeoutInMillis, aggregator.Timeout.DEFAULT) 186 | assert.equal(agg.executionTimeoutInMillis, aggregator.Timeout.DEFAULT) 187 | assert.equal(agg.schedulingPriority, aggregator.Schedule.STANDARD) 188 | assert.deepEqual(agg['aggregator'], Aggregators.sum('ival')) 189 | 190 | const agg2 = Aggregators.priority(Aggregators.sum('ival'), aggregator.Schedule.IMMEDIATE, 191 | aggregator.Timeout.NONE, aggregator.Timeout.NONE) 192 | 193 | assert.equal(agg2['@class'], 'aggregator.PriorityAggregator') 194 | assert.equal(agg2.requestTimeoutInMillis, aggregator.Timeout.NONE) 195 | assert.equal(agg2.executionTimeoutInMillis, aggregator.Timeout.NONE) 196 | assert.equal(agg2.schedulingPriority, aggregator.Schedule.IMMEDIATE) 197 | assert.deepEqual(agg2['aggregator'], Aggregators.sum('ival')) 198 | }) 199 | 200 | it('should aggregate all entries', async () => { 201 | const result = await cache.aggregate(agg) 202 | test.checkNumericResult(result, 1158) 203 | }) 204 | 205 | it('should aggregate filtered entries', async () => { 206 | const filter = Filters.between('id', 123, 456, true, false) 207 | const result = await cache.aggregate(filter, agg) 208 | test.checkNumericResult(result, 702) 209 | }) 210 | 211 | it('should aggregate entries based on keys', async () => { 212 | const result = await cache.aggregate([val123, val456], agg) 213 | test.checkNumericResult(result, 579) 214 | }) 215 | }) 216 | 217 | describe('Query Recorder', () => { 218 | const agg = Aggregators.record() 219 | 220 | it('should have the expected structure', () => { 221 | assert.equal(agg['@class'], 'aggregator.QueryRecorder') 222 | assert.deepEqual(agg['type'], { enum: 'EXPLAIN' }) 223 | 224 | const agg2 = Aggregators.record(aggregator.RecordType.TRACE) 225 | assert.equal(agg2['@class'], 'aggregator.QueryRecorder') 226 | assert.deepEqual(agg2['type'], { enum: 'TRACE' }) 227 | }) 228 | 229 | it('[EXPLAIN] should aggregate filtered entries', async () => { 230 | const filter = Filters.between('id', 123, 456, true, false) 231 | const result = await cache.aggregate(filter, agg) 232 | assert.notEqual(result.results, undefined) 233 | assert.equal(result.results.length, 1) 234 | assert.notEqual(result.results[0]['partitionSet'], undefined) 235 | assert.notEqual(result.results[0]['steps'], undefined) 236 | }) 237 | 238 | it('[TRACE] should aggregate filtered entries', async () => { 239 | const filter = Filters.between('id', 123, 456, true, false) 240 | const result = await cache.aggregate(filter, Aggregators.record(aggregator.RecordType.TRACE)) 241 | assert.notEqual(result.results, undefined) 242 | assert.equal(result.results.length, 1) 243 | assert.notEqual(result.results[0]['partitionSet'], undefined) 244 | assert.notEqual(result.results[0]['steps'], undefined) 245 | }) 246 | }) 247 | 248 | describe('Top Aggregator', function () { 249 | 250 | describe('in ascending mode', function () { 251 | 252 | const aggregator = Aggregators.top(3).orderBy('ival').ascending() 253 | 254 | it('should have the expected structure', function () { 255 | 256 | assert.equal(aggregator['@class'], 'aggregator.TopNAggregator') 257 | assert.equal(aggregator['comparator']['comparator']['extractor']['name'], 'ival') 258 | assert.equal(aggregator['results'], 3) 259 | assert.equal(aggregator['inverse'], true) 260 | }) 261 | 262 | it('should aggregate entries based on keys', function (done) { 263 | cache.aggregate([val123, val234, val345], aggregator) 264 | .then(function (data) { 265 | assert.deepEqual([val123, val234, val345], data) 266 | done() 267 | }) 268 | .catch(e => done(e)) 269 | }) 270 | 271 | it('should aggregate filtered entries', function (done) { 272 | cache.aggregate(Filters.between('id', 123, 456, true, false), aggregator) 273 | .then(function (data) { 274 | assert.deepEqual([val123, val234, val345], data) 275 | done() 276 | }) 277 | .catch(e => done(e)) 278 | }) 279 | }) 280 | 281 | describe('in descending mode', function () { 282 | 283 | const aggregator = Aggregators.top(3).orderBy('ival').descending() 284 | 285 | it('should have the expected structure', function () { 286 | assert.equal(aggregator['@class'], 'aggregator.TopNAggregator') 287 | assert.equal(aggregator['comparator']['comparator']['extractor']['name'], 'ival') 288 | assert.equal(aggregator['results'], 3) 289 | assert.equal(aggregator['inverse'], false) 290 | }) 291 | 292 | it('should aggregate entries based on keys', function (done) { 293 | cache.aggregate([val123, val234, val345], aggregator) 294 | .then(function (data) { 295 | assert.deepEqual([val345, val234, val123], data) 296 | done() 297 | }) 298 | .catch(e => done(e)) 299 | }) 300 | 301 | it('should aggregate filtered entries', function (done) { 302 | cache.aggregate(Filters.between('id', 123, 456, true, false), aggregator) 303 | .then(function (data) { 304 | assert.deepEqual([val345, val234, val123], data) 305 | done() 306 | }) 307 | .catch(e => done(e)) 308 | }) 309 | }) 310 | }) 311 | 312 | describe('Reducer Aggregator', function () { 313 | 314 | const aggregator = Aggregators.reduce('ival') 315 | 316 | it('should have the expected structure', function () { 317 | assert.equal(aggregator['@class'], 'aggregator.ReducerAggregator') 318 | assert.equal(aggregator['extractor']['name'], 'ival') 319 | }) 320 | 321 | it('should aggregate entries based on keys', function (done) { 322 | cache.aggregate([val123, val234, val345], aggregator) 323 | .then(function (data) { 324 | test.compareEntries([[val123, 123], [val234, 234], [val345, 345]], data) 325 | done() 326 | }) 327 | .catch(e => done(e)) 328 | }) 329 | 330 | it('should aggregate filtered entries', function (done) { 331 | cache.aggregate(Filters.between('id', 123, 456, true, false), aggregator) 332 | .then(function (data) { 333 | test.compareEntries([[val123, 123], [val234, 234], [val345, 345]], data) 334 | done() 335 | }) 336 | .catch(e => done(e)) 337 | }) 338 | }) 339 | 340 | describe('Script Aggregator', function () { 341 | 342 | const aggregator = Aggregators.script('js', 'someFilter', ['a', 123]) 343 | 344 | it('can be constructed', function () { 345 | assert.equal(aggregator['@class'], 'aggregator.ScriptAggregator') 346 | assert.equal(aggregator['language'], 'js') 347 | assert.equal(aggregator['name'], 'someFilter') 348 | assert.deepEqual(aggregator['args'], ['a', 123]) 349 | }) 350 | }) 351 | 352 | describe('An aggregator', function () { 353 | it('should be able to be run in a sequence by using andThen', async () => { 354 | const aggregator = Aggregators.max('ival').andThen(Aggregators.min('ival')) 355 | const result = await cache.aggregate(aggregator) 356 | assert.deepEqual(result, [456, 123]) 357 | }) 358 | }) 359 | }) 360 | -------------------------------------------------------------------------------- /test/client-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * http://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { event, Filters, Extractors, Session, Aggregators } = require('../lib') 9 | const test = require('./util') 10 | const assert = require('assert').strict 11 | const { describe, it, after, beforeEach } = require('mocha') 12 | const MapListener = event.MapListener 13 | 14 | describe('NamedCacheClient IT Test Suite', function () { 15 | const val123 = { id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3], group: 1 } 16 | const val234 = { id: 234, str: '234', ival: 234, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' } 17 | const val345 = { id: 345, str: '345', ival: 345, fval: 34.5, iarr: [3, 4, 5], group: 2 } 18 | const val456 = { id: 456, str: '456', ival: 456, fval: 45.6, iarr: [4, 5, 6], group: 3, nullIfOdd: 'non-null' } 19 | 20 | const session = new Session() 21 | const cache = session.getCache('cache-client') 22 | this.timeout(30000) 23 | 24 | beforeEach(async () => { 25 | await cache.clear() 26 | await cache.set(val123, val123) 27 | await cache.set(val234, val234) 28 | await cache.set(val345, val345) 29 | await cache.set(val456, val456) 30 | 31 | assert.equal(await cache.empty, false) 32 | assert.equal(await cache.size, 4) 33 | }) 34 | 35 | after(async () => { 36 | await cache.release().finally(() => session.close().catch()) 37 | }) 38 | 39 | describe('The cache lifecycle', () => { 40 | const CACHE_NAME = 'lifecycle-cache' 41 | 42 | it('should generate \'released\' event when the cache is released', async () => { 43 | const cache = session.getCache(CACHE_NAME) 44 | 45 | const prom = new Promise((resolve) => { 46 | cache.on(event.MapLifecycleEvent.RELEASED, cacheName => { 47 | if (cacheName === CACHE_NAME) { 48 | resolve() 49 | } 50 | }) 51 | 52 | setTimeout(() => { 53 | cache.release() 54 | }, 3000) 55 | }) 56 | 57 | await prom 58 | }) 59 | 60 | it('should generate \'destroyed\' event when the cache is destroyed', async () => { 61 | const cache = session.getCache(CACHE_NAME) 62 | 63 | const prom = new Promise((resolve) => { 64 | cache.on(event.MapLifecycleEvent.DESTROYED, cacheName => { 65 | if (cacheName === CACHE_NAME) { 66 | resolve() 67 | } 68 | }) 69 | 70 | setTimeout(() => { 71 | cache.destroy() 72 | }, 3000) 73 | }) 74 | 75 | await prom 76 | }) 77 | }) 78 | 79 | describe('Cache function', () => { 80 | 81 | describe('clear()', () => { 82 | it('should result in an empty cache', async () => { 83 | await cache.clear() 84 | assert.equal(await cache.empty, true) 85 | assert.equal(await cache.size, 0) 86 | }) 87 | }) 88 | 89 | describe('hasEntry', () => { 90 | it('should return true for an existing entry', async () => { 91 | assert.equal(await cache.hasEntry(val123, val123), true) 92 | }) 93 | 94 | it('should return false for a non-existing entry', async () => { 95 | assert.equal(await cache.hasEntry(val345, { id: 123, str: '123' }), false) 96 | }) 97 | 98 | it('should return false after clear using previously existing value', async () => { 99 | await cache.clear() 100 | assert.equal(await cache.hasEntry(val123, val123), false) 101 | }) 102 | }) 103 | 104 | describe('has()', () => { 105 | it('should return true for an existing entry', async () => { 106 | assert.equal(await cache.has(val123), true) 107 | }) 108 | 109 | it('should return false for a non-existing entry', async () => { 110 | assert.equal(await cache.has('val345'), false) 111 | }) 112 | 113 | it('should return false after clear using previously existing value', async () => { 114 | await cache.clear() 115 | assert.equal(await cache.has(val123), false) 116 | }) 117 | }) 118 | 119 | describe('hasValue()', () => { 120 | it('should return true for an existing entry', async () => { 121 | assert.equal(await cache.hasValue(val123), true) 122 | }) 123 | 124 | it('should return false for a non-existing entry', async () => { 125 | assert.equal(await cache.hasValue('val345'), false) 126 | }) 127 | 128 | it('should return false after clear using previously existing value', async () => { 129 | await cache.clear() 130 | assert.equal(await cache.hasValue(val123), false) 131 | }) 132 | }) 133 | 134 | describe('get()', () => { 135 | it('should return the correct value for the provided key', async () => { 136 | assert.deepEqual(await cache.get(val123), val123) 137 | }) 138 | 139 | it('should return null for a non-existing entry', async () => { 140 | assert.equal(await cache.get('val345'), null) 141 | }) 142 | }) 143 | 144 | describe('getAll()', () => { 145 | it('should return map of mapped keys that match the provided keys', async () => { 146 | const entries = await cache.getAll([val123, val234, val345, 'val789']) 147 | await test.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 148 | }) 149 | }) 150 | 151 | describe('getOrDefault()', () => { 152 | it('should return the mapped value when the key is mapped', async () => { 153 | assert.deepEqual(await cache.getOrDefault(val123, val234), val123) 154 | }) 155 | 156 | it('should return the provided default if the key is not mapped', async () => { 157 | assert.deepEqual(await cache.getOrDefault('val345', val234), val234) 158 | }) 159 | }) 160 | 161 | describe('put()', () => { 162 | it('should return the previously mapped value when key is value is updated', async () => { 163 | assert.deepEqual(await cache.set(val123, val345), val123) 164 | }) 165 | 166 | it('should return null when inserting a new mapping', async () => { 167 | await cache.clear() 168 | assert.deepEqual(await cache.set(val123, val123), null) 169 | }) 170 | }) 171 | 172 | describe('empty', () => { 173 | it('should return false when the cache is empty', async () => { 174 | assert.deepEqual(await cache.empty, false) 175 | }) 176 | 177 | it('should return true when the cache is empty', async () => { 178 | await cache.clear() 179 | assert.deepEqual(await cache.empty, true) 180 | }) 181 | }) 182 | 183 | describe('forEach()', () => { 184 | it('should iterate over the entries associated with the specified keys and invoke the callback', async () => { 185 | const entriesSeen = new Set() 186 | await cache.clear() 187 | await cache.set('123', val123).then(() => cache.set('234', val234)) 188 | await cache.set('345', val345) 189 | await cache.set('456', val456) 190 | await cache.forEach((value, key) => entriesSeen.add([value, key]), ['123', '456']) 191 | await test.compareElements([[val123, '123'], [val456, '456']], entriesSeen) 192 | }) 193 | 194 | it('should iterate over all entries invoke the callback for each', async () => { 195 | const entriesSeen = new Set() 196 | await cache.clear() 197 | await cache.set('123', val123).then(() => cache.set('234', val234)) 198 | await cache.set('345', val345) 199 | await cache.set('456', val456) 200 | await cache.forEach((value, key) => entriesSeen.add([value, key])) 201 | await test.compareElements([[val123, '123'], [val234, '234'], [val345, '345'], [val456, '456']], entriesSeen) 202 | }) 203 | 204 | it('should iterate over all filtered entries and invoke the callback for each', async () => { 205 | const entriesSeen = new Set() 206 | await cache.clear() 207 | await cache.set('123', val123).then(() => cache.set('234', val234)) 208 | await cache.set('345', val345) 209 | await cache.set('456', val456) 210 | await cache.forEach((value, key) => entriesSeen.add([value, key]), Filters.greater('ival', 300)) 211 | await test.compareElements([[val345, '345'], [val456, '456']], entriesSeen) 212 | }) 213 | }) 214 | 215 | describe('entries()', async () => { 216 | it('should return a set of all entries within the cache', async () => { 217 | const entries = await cache.entries() 218 | assert.equal(await entries.size, 4) 219 | assert.equal(entries instanceof Set, false) 220 | assert.equal(entries.hasOwnProperty('namedCache'), true) 221 | await test.compareEntries([[val123, val123], [val234, val234], [val345, val345], [val456, val456]], 222 | entries) 223 | }) 224 | 225 | it('should be filterable', async () => { 226 | const entries = await cache.entries(Filters.greater(Extractors.extract('ival'), 123)) 227 | assert.equal(await entries.size, 3) 228 | assert.equal(entries.hasOwnProperty('namedCache'), false) 229 | await test.compareEntries([[val234, val234], [val345, val345], [val456, val456]], 230 | entries) 231 | }) 232 | }) 233 | 234 | describe('keySet()', () => { 235 | it('should return a set of all keys within the cache', async () => { 236 | const keySet = await cache.keys() 237 | assert.equal(await keySet.size, 4) 238 | assert.equal(keySet instanceof Set, false) 239 | assert.equal(keySet.hasOwnProperty('namedCache'), true) 240 | await test.compareElements([val123, val234, val345, val456], keySet) 241 | }) 242 | 243 | it('should be filterable', async () => { 244 | const keySet = await cache.keys(Filters.greater(Extractors.extract('ival'), 123)) 245 | assert.equal(await keySet.size, 3) 246 | assert.equal(keySet.hasOwnProperty('namedCache'), false) 247 | await test.compareElements([val234, val345, val345], await keySet) 248 | }) 249 | }) 250 | 251 | describe('ready()', () => { 252 | it('should return true if 22.06.5 or later, otherwise, it should raise error', async () => { 253 | try { 254 | assert.equal(await cache.ready, true) 255 | } catch (err) { 256 | if (!err.message.startsWith("This operation is")) { 257 | assert.fail("Unexpected error", err) 258 | } 259 | } 260 | }) 261 | }) 262 | 263 | describe('removeMapping()', () => { 264 | it('should return true if a mapping was removed', async () => { 265 | assert.equal(await cache.removeMapping(val123, val123), true) 266 | assert.equal(await cache.size, 3) 267 | }) 268 | 269 | it('should return false if a mapping was not removed', async () => { 270 | assert.equal(await cache.removeMapping('val123', val123), false) 271 | assert.equal(await cache.size, 4) 272 | }) 273 | }) 274 | 275 | describe('replace()', () => { 276 | it('should return null if the mapping does not exist and does not result in an entry in the map', async () => { 277 | assert.equal(await cache.replace('val123', 'val123'), null) 278 | assert.equal(await cache.size, 4) 279 | }) 280 | 281 | it('should return the previously mapped value if the mapping is replaced', async () => { 282 | assert.deepEqual(await cache.replace(val123, 'val123'), val123) 283 | assert.equal(await cache.size, 4) 284 | assert.equal(await cache.get(val123), 'val123') 285 | }) 286 | }) 287 | 288 | describe('replaceMapping()', () => { 289 | it('should return false if the mapping does not exist', async () => { 290 | assert.equal(await cache.replaceMapping(val123, 'val123', 'NOPE'), false) 291 | assert.equal(await cache.size, 4) 292 | assert.deepEqual(await cache.get(val123), val123) 293 | }) 294 | 295 | it('should return true if the mapping was replaced', async () => { 296 | assert.equal(await cache.replaceMapping(val123, val123, val456), true) 297 | assert.equal(await cache.size, 4) 298 | assert.deepEqual(await cache.get(val123), val456) 299 | }) 300 | }) 301 | 302 | describe('setIfAbsent()', () => { 303 | it('should return null if the mapping is absent and result in a new entry', async () => { 304 | assert.equal(await cache.setIfAbsent('val123', val123), null) 305 | assert.equal(await cache.size, 5) 306 | assert.deepEqual(await cache.get('val123'), val123) 307 | }) 308 | 309 | it('should return the currently mapped value if the mapping exists and does not mutate the map', async () => { 310 | assert.deepEqual(await cache.setIfAbsent(val123, val345), val123) 311 | assert.equal(await cache.size, 4) 312 | assert.deepEqual(await cache.get(val123), val123) 313 | }) 314 | }) 315 | 316 | describe('set() with TTL', () => { 317 | it('should be possible to associate a ttl with a cache entry', async () => { 318 | await cache.set('val123', val123, 1000) 319 | assert.deepEqual(await cache.get('val123'), val123) 320 | assert.deepEqual(await cache.get('val123'), val123) 321 | assert.deepEqual(await cache.get('val123'), val123) 322 | await new Promise(resolve => setTimeout(resolve, 1500)) 323 | assert.equal(await cache.get('val123'), null) 324 | }) 325 | }) 326 | 327 | describe('setAll()', () => { 328 | it('should be to store a map of entries', async () => { 329 | await cache.clear(); 330 | await cache.setAll(new Map().set(val123, val123).set(val234, val234)) 331 | assert.equal(await cache.size, 2) 332 | assert.deepEqual(await cache.get(val123), val123) 333 | assert.deepEqual(await cache.get(val234), val234) 334 | }) 335 | }) 336 | 337 | describe('{add,remove}Index()', () => { 338 | it('should add and remove an index to the remote cache', async () => { 339 | await cache.addIndex(Extractors.extract('ival')) 340 | const result = await cache.aggregate(Filters.greater('ival', 200), Aggregators.record()) 341 | assert.notEqual(JSON.stringify(result).indexOf('"index":'), -1) 342 | 343 | await cache.removeIndex(Extractors.extract('ival')) 344 | const result2 = await cache.aggregate(Filters.greater('ival', 200), Aggregators.record()) 345 | assert.equal(JSON.stringify(result2).indexOf('"index":'), -1) 346 | }) 347 | }) 348 | 349 | describe('truncate()', () => { 350 | it('should clear the cache without raising listener events', async () => { 351 | let deleteCount = 0 352 | const listener = new MapListener().on(event.MapEventType.DELETE, () => deleteCount++) 353 | await cache.addMapListener(listener).then(() => cache.truncate()).then(() => cache.removeMapListener(listener)) 354 | 355 | assert.equal(await cache.size, 0) 356 | assert.equal(deleteCount, 0) 357 | }) 358 | }) 359 | 360 | describe('values()', () => { 361 | it('should return a set of all values within the cache', async () => { 362 | const values = await cache.values() 363 | assert.equal(await values.size, 4) 364 | assert.equal(values instanceof Set, false) 365 | assert.equal(values.hasOwnProperty('namedCache'), true) 366 | await test.compareElements([val123, val234, val345, val345], values) 367 | }) 368 | 369 | it('should be filterable', async () => { 370 | const values = await cache.values(Filters.greater(Extractors.extract('ival'), 123)) 371 | assert.equal(await values.size, 3) 372 | assert.equal(values.hasOwnProperty('namedCache'), false) 373 | await test.compareElements([val234, val345, val345], await values) 374 | }) 375 | }) 376 | describe('The cache lifecycle', () => { 377 | it('should generate \'released\' event when the cache is released', async () => { 378 | const sess = new Session() 379 | const cache = sess.getCache('test') 380 | 381 | const prom = new Promise((resolve) => { 382 | cache.on(event.MapLifecycleEvent.RELEASED, cacheName => { 383 | if (cacheName === 'test') { 384 | resolve() 385 | } 386 | }) 387 | 388 | setTimeout(() => { 389 | cache.release() 390 | }, 3000) 391 | }) 392 | 393 | await prom.finally(() => sess.close().finally(() => sess.waitUntilClosed())) 394 | }) 395 | 396 | it('should generate \'destroyed\' event when the cache is destroyed', async () => { 397 | const sess = new Session() 398 | const cache = sess.getCache('test') 399 | 400 | const prom = new Promise((resolve) => { 401 | cache.on(event.MapLifecycleEvent.DESTROYED, cacheName => { 402 | if (cacheName === 'test') { 403 | resolve() 404 | } 405 | }) 406 | 407 | setTimeout(() => { 408 | cache.destroy() 409 | }, 3000) 410 | }) 411 | 412 | await prom.finally(() => sess.close().finally(() => sess.waitUntilClosed())) 413 | }) 414 | }) 415 | }) 416 | }) 417 | -------------------------------------------------------------------------------- /test/discovery/resolver-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025, Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { CoherenceResolver, Session } = require('../../lib') 9 | const assert = require('assert').strict 10 | const { describe, it } = require('mocha'); 11 | 12 | describe('CoherenceResolver Test Suite (unit/IT)', () => { 13 | describe('A CoherenceResolver', () => { 14 | 15 | function createListener(done, expectedPort) { 16 | return { 17 | onSuccessfulResolution: ( 18 | addressList, 19 | serviceConfig, 20 | serviceConfigError, 21 | configSelector, 22 | attributes 23 | ) => { 24 | try { 25 | assert.equal(addressList.length, 1) 26 | assert.equal(addressList[0].addresses[0].host, '127.0.0.1') 27 | assert.equal(addressList[0].addresses[0].port, expectedPort) 28 | done() 29 | } catch (error) { 30 | done(error) 31 | } 32 | }, 33 | onError(error) { 34 | done(new Error(`Unexpected error resolving: ${JSON.stringify(error)}`)) 35 | } 36 | } 37 | } 38 | 39 | it('should parse and resolve the connection format \'coherence:///[host]\'', (done) => { 40 | const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost'}, createListener(done, 10000), null) 41 | resolver.updateResolution() 42 | }) 43 | 44 | it('should parse and resolve the connection format \'coherence:///[host]:[port]\'', (done) => { 45 | const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost:7574'}, createListener(done, 10000), null) 46 | resolver.updateResolution() 47 | }) 48 | 49 | it('should parse and resolve the connection format \'coherence:///[host]:[clusterName]\'', (done) => { 50 | const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost/grpc-cluster2'}, createListener(done, 10001), null) 51 | resolver.updateResolution() 52 | }) 53 | 54 | it('should parse and resolve the connection format \'coherence:///[host]:[port]:[clusterName]\'', (done) => { 55 | const resolver = new CoherenceResolver({scheme: 'coherence', path: 'localhost:7574/grpc-cluster2'}, createListener(done, 10001), null) 56 | resolver.updateResolution() 57 | }) 58 | }) 59 | 60 | describe('A Session', () => { 61 | it('should be able to resolve the gRPC Proxy', async () => { 62 | const session = new Session({address: 'coherence:///localhost'}) 63 | const cache = session.getCache('test') 64 | await cache.set('a', 'b') 65 | assert.equal(await cache.get('a'), 'b') 66 | await session.close() 67 | }) 68 | 69 | it('should be able to resolve the gRPC Proxy of a foreign cluster', async () => { 70 | const session = new Session({address: 'coherence:///localhost/grpc-cluster2'}) 71 | const cache = session.getCache('test') 72 | await cache.set('a', 'b') 73 | assert.equal(await cache.get('a'), 'b') 74 | await session.close() 75 | }) 76 | }) 77 | }) -------------------------------------------------------------------------------- /test/extractor-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * http://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { Filters, Extractors, Session } = require('../lib') 9 | const test = require('./util') 10 | const assert = require('assert').strict 11 | const { describe, it, after, beforeEach } = require('mocha'); 12 | 13 | describe('Extractor IT Test Suite', function () { 14 | const session = new Session() 15 | this.timeout(30000) 16 | 17 | describe('A ValueExtractor', () => { 18 | const toKey = { name: 'To' } 19 | const tscKey = { name: 'TypeScript' } 20 | const trieKey = { name: 'Trie' } 21 | const jadeKey = { name: 'Jade' } 22 | const javascriptKey = { name: 'JavaScript' } 23 | const toObj = { t: { o: { level: 3, word: 'To', tokens: ['t', 'o'] } } } 24 | const tscObj = { t: { y: { level: 3, word: 'TypeScript', tokens: ['t', 'y'] } } } 25 | const trieObj = { t: { r: { level: 3, word: 'Trie', tokens: ['t', 'r'] } } } 26 | const jadeObj = { j: { a: { d: { level: 4, word: 'Jade', tokens: ['j', 'a', 'd'] } } } } 27 | const javascriptObj = { j: { a: { level: 4, v: { word: 'JavaScript', tokens: ['j', 'a', 'v'] } } } } 28 | 29 | const cache = session.getCache('nested-cache') 30 | this.timeout(30000) 31 | 32 | beforeEach(async () => { 33 | await cache.clear() 34 | await cache.set(toKey, toObj) 35 | await cache.set(tscKey, tscObj) 36 | await cache.set(trieKey, trieObj) 37 | await cache.set(jadeKey, jadeObj) 38 | await cache.set(javascriptKey, javascriptObj) 39 | 40 | assert.equal(await cache.empty, false) 41 | assert.equal(await cache.size, 5) 42 | }) 43 | 44 | after(() => { 45 | cache.release().finally(() => session.close().catch()) 46 | }) 47 | 48 | it('should be composable by chaining extractors to narrow results', async () => { 49 | const f1 = Filters.equal( 50 | Extractors.chained( 51 | [Extractors.extract('t'), // t field 52 | Extractors.extract('r'), // r field of t 53 | Extractors.extract('word')] // word field of r 54 | ), 'Trie') 55 | await test.compareEntries([[trieKey, trieObj]], await cache.entries(f1)) 56 | }) 57 | 58 | it('should be composable using a compound string to chain extractors to narrow results', async () => { 59 | const ext = Extractors.chained('t.r.word') 60 | const f1 = Filters.equal(ext, 'Trie') 61 | await test.compareEntries([[trieKey, trieObj]], await cache.entries(f1)) 62 | }) 63 | 64 | it('should be composable by applying a multi extractor', async () => { 65 | const f1 = Filters.equal( 66 | Extractors.multi( 67 | [Extractors.chained('t.r.word'), 68 | Extractors.chained('t.r.word'), 69 | Extractors.chained('t.r.word')] 70 | ), ['Trie', 'Trie', 'Trie']) 71 | 72 | await test.compareEntries([[trieKey, trieObj]], await cache.entries(f1)) 73 | }) 74 | 75 | it('should be composable by applying a multi extractor as a string', async () => { 76 | const ext = Extractors.multi('t.r.word') 77 | const f1 = Filters.equal(ext, ['Trie']) 78 | await test.compareEntries([[trieKey, trieObj]], await cache.entries(f1)) 79 | }) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /test/filter-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * http://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { Filters, Session } = require('../lib') 9 | const t = require('./util') 10 | const assert = require('assert').strict 11 | const { describe, it, after, beforeEach } = require('mocha') 12 | 13 | describe('Filter IT Test Suite', function () { 14 | const val123 = { id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3], group: 1 } 15 | const val234 = { id: 234, str: '234', ival: 234, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' } 16 | const val345 = { id: 345, str: '345', ival: 345, fval: 34.5, iarr: [3, 4, 5], group: 2 } 17 | const val456 = { id: 456, str: '456', ival: 456, fval: 45.6, iarr: [4, 5, 6], group: 3, nullIfOdd: 'non-null' } 18 | 19 | const session = new Session() 20 | const cache = session.getCache('cache-client') 21 | this.timeout(30000) 22 | 23 | beforeEach(async () => { 24 | await cache.clear() 25 | await cache.set(val123, val123) 26 | await cache.set(val234, val234) 27 | await cache.set(val345, val345) 28 | await cache.set(val456, val456) 29 | 30 | assert.equal(await cache.empty, false) 31 | assert.equal(await cache.size, 4) 32 | }) 33 | 34 | after(() => { 35 | cache.release().finally(() => session.close().catch()) 36 | }) 37 | 38 | describe('A Filter', () => { 39 | it('should be composable with \'and\'', async () => { 40 | const f1 = Filters.equal('str', '123') 41 | const f2 = f1.and(Filters.equal('ival', 123)) 42 | const entries = await cache.entries(f2) 43 | await t.compareEntries([[val123, val123]], entries) 44 | }) 45 | 46 | it('should be composable with \'or\'', async () => { 47 | const f1 = Filters.equal('str', '123') 48 | const f2 = f1.or(Filters.equal('ival', 234)) 49 | 50 | await t.compareElements([val123, val234], await cache.values(f2)) 51 | }) 52 | 53 | it('should be composable with \'xor\'', async () => { 54 | const f1 = Filters.equal('str', '123') 55 | const f2 = f1.xor(Filters.equal('ival', 123)) 56 | const entries = await cache.entries(f2) 57 | 58 | assert.equal(await entries.size, 0) 59 | }) 60 | }) 61 | 62 | describe('Filters.all()', () => { 63 | it('should only return entries that match all applied filters', async () => { 64 | // fval discriminate matches three entries. group discriminate narrows to two entries 65 | const f1 = Filters.all(Filters.greater('fval', 23.1), Filters.equal('group', 2)) 66 | const entries = await cache.entries(f1) 67 | 68 | await t.compareEntries([[val234, val234], [val345, val345]], entries) 69 | }) 70 | }) 71 | 72 | describe('Filters.any()', () => { 73 | it('should return results that match any of the applied filters', async () => { 74 | const f1 = Filters.any(Filters.less('ival', 150), Filters.greater('ival', 400)) 75 | const entries = await cache.entries(f1) 76 | 77 | await t.compareEntries([[val123, val123], [val456, val456]], entries) 78 | }) 79 | }) 80 | 81 | describe('Filters.arrayContains()', () => { 82 | it('should return results based on the value of a single array element', async () => { 83 | const f1 = Filters.arrayContains('iarr', 3) 84 | const entries = await cache.entries(f1) 85 | 86 | await t.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 87 | }) 88 | }) 89 | 90 | describe('Filters.arrayContainsAll()', () => { 91 | it('should return results based on the value of a multiple array elements; entries must contain all elements', async () => { 92 | // noinspection JSCheckFunctionSignatures 93 | const f1 = Filters.arrayContainsAll('iarr', new Set([2, 3])) 94 | const entries = await cache.entries(f1) 95 | 96 | await t.compareEntries([[val123, val123], [val234, val234]], entries) 97 | }) 98 | }) 99 | 100 | describe('Filters.arrayContainsAny()', () => { 101 | it('should return results based on the value of a multiple array elements; entries must contain any of the elements', async () => { 102 | // noinspection JSCheckFunctionSignatures 103 | const f1 = Filters.arrayContainsAny('iarr', new Set([2, 3])) 104 | const entries = await cache.entries(f1) 105 | 106 | await t.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 107 | }) 108 | }) 109 | 110 | describe('Filters.between()', () => { 111 | it('should return results including upper and lower boundaries (default)', async () => { 112 | const f1 = Filters.between('ival', 123, 345) 113 | const entries = await cache.entries(f1) 114 | 115 | await t.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 116 | }) 117 | 118 | it('should return results excluding upper and lower boundaries (explicit)', async () => { 119 | const f1 = Filters.between('ival', 123, 345, false, false) 120 | const entries = await cache.entries(f1) 121 | 122 | await t.compareEntries([[val234, val234]], entries) 123 | }) 124 | 125 | it('should be possible to include results matching lower boundary', async () => { 126 | const f1 = Filters.between('ival', 123, 345, true) 127 | const entries = await cache.entries(f1) 128 | 129 | await t.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 130 | }) 131 | 132 | it('should be possible to include results matching upper boundary', async () => { 133 | const f1 = Filters.between('ival', 123, 345, false, true) 134 | const entries = await cache.entries(f1) 135 | 136 | await t.compareEntries([[val234, val234], [val345, val345]], entries) 137 | }) 138 | 139 | it('should be possible to include results matching both upper and lower boundaries', async () => { 140 | const f1 = Filters.between('ival', 123, 345, true, true) 141 | const entries = await cache.entries(f1) 142 | 143 | await t.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 144 | }) 145 | }) 146 | 147 | describe('Filters.contains()', () => { 148 | it('should return results based on the value of a single collection element', async () => { 149 | const f1 = Filters.contains('iarr', 3) 150 | const entries = await cache.entries(f1) 151 | 152 | await t.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 153 | }) 154 | }) 155 | 156 | describe('Filters.containsAll()', () => { 157 | it('should return results based on the value of a multiple collection elements; entries must contain all elements', async () => { 158 | // noinspection JSCheckFunctionSignatures 159 | const f1 = Filters.containsAll('iarr', new Set([2, 3])) 160 | const entries = await cache.entries(f1) 161 | 162 | await t.compareEntries([[val123, val123], [val234, val234]], entries) 163 | }) 164 | }) 165 | 166 | describe('Filters.containsAny()', () => { 167 | it('should return results based on the value of a multiple collection elements; entries must contain any of the elements', async () => { 168 | // noinspection JSCheckFunctionSignatures 169 | const f1 = Filters.containsAny('iarr', new Set([2, 3])) 170 | const entries = await cache.entries(f1) 171 | 172 | await t.compareEntries([[val123, val123], [val234, val234], [val345, val345]], entries) 173 | }) 174 | }) 175 | 176 | describe('Filters.equal()', () => { 177 | it('should return results based on field equality', async () => { 178 | const f1 = Filters.equal('ival', 345) 179 | const entries = await cache.entries(f1) 180 | 181 | await t.compareEntries([[val345, val345]], entries) 182 | }) 183 | }) 184 | 185 | describe('Filters.greater()', () => { 186 | it('should return results only if the provided field is greater than the provided value (excluding boundary)', async () => { 187 | const f1 = Filters.greater('ival', 345) 188 | const entries = await cache.entries(f1) 189 | 190 | await t.compareEntries([[val456, val456]], entries) 191 | }) 192 | }) 193 | 194 | describe('Filters.greaterEqual()', () => { 195 | it('should return results only if the provided field is greater than the provided value (including boundary)', async () => { 196 | const f1 = Filters.greaterEqual('ival', 345) 197 | const entries = await cache.entries(f1) 198 | 199 | await t.compareEntries([[val345, val345], [val456, val456]], entries) 200 | }) 201 | }) 202 | 203 | describe('Filters.in()', () => { 204 | it('should return results for those entries that have matching field values', async () => { 205 | // noinspection JSCheckFunctionSignatures 206 | const f1 = Filters.in('ival', new Set([234, 345])) 207 | const entries = await cache.entries(f1) 208 | 209 | await t.compareEntries([[val234, val234], [val345, val345]], entries) 210 | }) 211 | }) 212 | 213 | describe('Filters.not()', () => { 214 | it('should return entries that resolve to the logical \'not\' of the provided filter', async () => { 215 | const f1 = Filters.not(Filters.equal('ival', 234)) 216 | const entries = await cache.entries(f1) 217 | await t.compareEntries([[val123, val123], [val345, val345], [val456, val456]], entries) 218 | }) 219 | }) 220 | 221 | describe('Filters.isNull()', () => { 222 | it('should return entries whose extracted value is \'null\'', async () => { 223 | const f1 = Filters.isNull('nullIfOdd') 224 | const entries = await cache.entries(f1) 225 | await t.compareEntries([[val123, val123], [val345, val345]], entries) 226 | }) 227 | }) 228 | 229 | describe('Filters.isNotNull()', () => { 230 | it('should return entries whose extracted value is not \'null\'', async () => { 231 | const f1 = Filters.isNotNull('nullIfOdd') 232 | const entries = await cache.entries(f1) 233 | await t.compareEntries([[val234, val234], [val456, val456]], entries) 234 | }) 235 | }) 236 | }) 237 | -------------------------------------------------------------------------------- /test/map-listener-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2023, Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { MapEventResponse } = require('../lib/grpc/messages_pb') 9 | const { event, Filters, Session, filter } = require('../lib') 10 | const assert = require('assert').strict 11 | const { describe, it } = require('mocha') 12 | 13 | const MapListener = event.MapListener 14 | const MapLifecycleEvent = event.MapLifecycleEvent 15 | const MapEventType = event.MapEventType 16 | const MapEvent = event.MapEvent 17 | 18 | describe('Map Events IT Test Suite', function () { 19 | const session = new Session() 20 | const stringify = JSON.stringify 21 | const debug = process.env.DEBUG || false 22 | 23 | this.timeout(10000) 24 | 25 | after(async () => { 26 | await session.close() 27 | }) 28 | 29 | async function runBasicEventTest (expectedEvents /* object */, filterMask /* number */) { 30 | const cache = session.getCache('event-map' + Date.now()) 31 | const prom = new Promise((resolve) => { 32 | cache.on(MapLifecycleEvent.DESTROYED, () => { 33 | resolve() 34 | }) 35 | }) 36 | 37 | const listener = new CountingMapListener('listener-default') 38 | setImmediate(async () => { 39 | if (filterMask) { 40 | const eventFilter = Filters.event(Filters.always(), filterMask) 41 | await cache.addMapListener(listener, eventFilter); 42 | } else { 43 | await cache.addMapListener(listener) 44 | } 45 | 46 | await cache.set('123', { xyz: '123-xyz' }) 47 | await cache.set('123', { abc: '123-abc' }) 48 | await cache.delete('123') 49 | 50 | let numberOfEvents = expectedEvents.inserts.length + expectedEvents.updates.length + expectedEvents.deletes.length 51 | 52 | listener.waitFor(numberOfEvents).catch(error => console.log(error)).finally(() => cache.destroy()) 53 | }) 54 | 55 | await prom.then(() => validateEventsForListener(listener, expectedEvents)).catch(error => console.log(error)) 56 | } 57 | 58 | function validateEventsForListener (listener, expectedEvents) { 59 | // validate inserts 60 | const expectedInserts = expectedEvents.inserts 61 | assert.equal(listener.insertEvents.length, expectedInserts.length, 'Unexpected number of insert events received') 62 | validateEvent('INSERT(' + listener.name + ')', expectedInserts, listener.insertEvents) 63 | 64 | // validate updates 65 | const expectedUpdates = expectedEvents.updates 66 | assert.equal(listener.updateEvents.length, expectedUpdates.length, 'Unexpected number of update events received') 67 | validateEvent('UPDATE(' + listener.name + ')', expectedUpdates, listener.updateEvents) 68 | 69 | // validate deletes 70 | const expectedDeletes = expectedEvents.deletes 71 | assert.equal(listener.deleteEvents.length, expectedDeletes.length, 'Unexpected number of update events received') 72 | validateEvent('DELETE(' + listener.name + ')', expectedDeletes, listener.deleteEvents) 73 | 74 | // validate ordering if required 75 | if (expectedEvents.order) { 76 | const expectedOrder = expectedEvents.order 77 | const actualOrder = listener.eventOrder 78 | assert.equal(actualOrder.length, expectedOrder.length, 'Unexpected length for all events.') 79 | for (let i = 0, len = expectedOrder.length; i < len; i++) { 80 | const expectedEvent = expectedOrder[i] 81 | const actualEvent = actualOrder[i] 82 | const expectedKey = expectedEvent.key 83 | const actualKey = actualEvent.key 84 | assert.deepEqual(actualKey, expectedKey, 'Unexpected event order; expected key: ' 85 | + stringify(expectedKey) + ', received: ' + stringify(actualKey)) 86 | if (expectedEvent.new) { 87 | assert.deepEqual(actualEvent.newValue, expectedEvent.new, 'Unexpected event order; expected new value: ' 88 | + stringify(expectedEvent.new) + ', received: ' + stringify(actualEvent.newValue)) 89 | } else { 90 | assert.equal(actualEvent.newValue, null, 'Unexpected event order; event incorrectly has new value') 91 | } 92 | 93 | if (expectedEvent.old) { 94 | assert.deepEqual(actualEvent.oldValue, expectedEvent.old, 'Unexpected event order; expected old value: ' 95 | + stringify(expectedEvent.old) + ', received: ' + stringify(actualEvent.oldValue)) 96 | } else { 97 | assert.equal(actualEvent.oldValue, null, 'Unexpected event order; event incorrectly has old value') 98 | } 99 | } 100 | } 101 | } 102 | 103 | function validateEvent (mode, expectedEvents, eventsReceived) { 104 | for (const expectedEvent of expectedEvents) { 105 | let keyFound = false 106 | const key = expectedEvent.key 107 | for (const receivedEvent of eventsReceived) { 108 | try { 109 | assert.deepEqual(key, receivedEvent.key) 110 | keyFound = true 111 | } catch (error) { 112 | } 113 | 114 | if (expectedEvent.new) { 115 | if (!receivedEvent.newValue) { 116 | assert.fail('[' + mode + '] Expected event for key ' + stringify(key) + ' to have new value, but none was found') 117 | } else { 118 | assert.deepEqual(expectedEvent.new, receivedEvent.newValue, '[' + mode + '] Unexpected new value found for event keyed by ' + stringify(key)) 119 | } 120 | } else { 121 | if (receivedEvent.new) { 122 | assert.fail('[' + mode + '] Did not expect event for key ' + stringify(key) + ' to have new value, but found' + stringify(receivedEvent.newValue)) 123 | } 124 | } 125 | 126 | if (expectedEvent.old) { 127 | if (!receivedEvent.oldValue) { 128 | assert.fail('[' + mode + '] Expected event for key ' + stringify(key) + ' to have old value, but none was found') 129 | } else { 130 | assert.deepEqual(expectedEvent.old, receivedEvent.oldValue, '[' + mode + '] Unexpected old value found for event keyed by ' + stringify(key)) 131 | } 132 | } else { 133 | if (receivedEvent.old) { 134 | assert.fail('[' + mode + '] Did not expect event for key ' + stringify(key) + ' to have old value, but found' + stringify(receivedEvent.oldValue)) 135 | } 136 | } 137 | } 138 | 139 | if (!keyFound) { 140 | assert.fail('Unable to find insert event for key ' + stringify(key)) 141 | } 142 | } 143 | } 144 | 145 | describe('A MapEvent callback', () => { 146 | it('should be able to receive insert, update, and delete events in default registration case', async () => { 147 | const expected = { 148 | 'inserts': [{ key: '123', new: { xyz: '123-xyz' } }], 149 | 'updates': [{ key: '123', new: { abc: '123-abc' }, old: { xyz: '123-xyz' } }], 150 | 'deletes': [{ key: '123', old: { abc: '123-abc' } }], 151 | 'order': [{ key: '123', new: { xyz: '123-xyz' } }, 152 | { key: '123', new: { abc: '123-abc' }, old: { xyz: '123-xyz' } }, 153 | { key: '123', old: { abc: '123-abc' } }] 154 | } 155 | await runBasicEventTest(expected) 156 | }) 157 | 158 | it('should be able to receive insert, update, and delete events in explicit registration case', async () => { 159 | const expected = { 160 | 'inserts': [{ key: '123', new: { xyz: '123-xyz' } }], 161 | 'updates': [{ key: '123', new: { abc: '123-abc' }, old: { xyz: '123-xyz' } }], 162 | 'deletes': [{ key: '123', old: { abc: '123-abc' } }], 163 | 'order': [{ key: '123', new: { xyz: '123-xyz' } }, 164 | { key: '123', new: { abc: '123-abc' }, old: { xyz: '123-xyz' } }, 165 | { key: '123', old: { abc: '123-abc' } }] 166 | } 167 | await runBasicEventTest(expected, filter.MapEventFilter.ALL) 168 | }) 169 | 170 | it('should be able to receive insert events only', async () => { 171 | const expected = { 172 | 'inserts': [{ key: '123', new: { xyz: '123-xyz' } }], 173 | 'updates': [], 174 | 'deletes': [] 175 | } 176 | await runBasicEventTest(expected, filter.MapEventFilter.INSERTED) 177 | }) 178 | 179 | it('should be able to receive update events only', async () => { 180 | const expected = { 181 | 'inserts': [], 182 | 'updates': [{ key: '123', new: { abc: '123-abc' }, old: { xyz: '123-xyz' } }], 183 | 'deletes': [] 184 | } 185 | await runBasicEventTest(expected, filter.MapEventFilter.UPDATED) 186 | }) 187 | 188 | it('should be able to receive deleted events only', async () => { 189 | const expected = { 190 | 'inserts': [], 191 | 'updates': [], 192 | 'deletes': [{ key: '123', old: { abc: '123-abc' } }] 193 | } 194 | await runBasicEventTest(expected, filter.MapEventFilter.DELETED) 195 | }) 196 | 197 | it('should properly handle multiple listeners', (done) => { 198 | const cache = session.getCache('event-map' + Date.now()) 199 | const prom = new Promise((resolve) => { 200 | cache.on(MapLifecycleEvent.DESTROYED, () => { 201 | resolve() 202 | }) 203 | }) 204 | const listener = new CountingMapListener('listener-default') 205 | const listener2 = new CountingMapListener('listener-2') 206 | 207 | setImmediate(async () => { 208 | await cache.addMapListener(listener) 209 | 210 | await cache.set('123', { xyz: '123-xyz' }) 211 | await cache.set('123', { abc: '123-abc' }) 212 | await cache.delete('123') 213 | 214 | await listener.waitFor(3).catch(error => { 215 | cache.destroy().catch(error => console.log('cache destroy raised error: ' + error)) 216 | throw error 217 | }) 218 | 219 | await cache.addMapListener(listener2) 220 | 221 | await cache.set('123', { a: 2 }) 222 | await cache.set('123', { a: 1 }) 223 | 224 | await listener.waitFor(5).catch(error => { 225 | cache.destroy().catch(error => console.log('cache destroy raised error: ' + error)) 226 | throw error 227 | }) 228 | 229 | await cache.removeMapListener(listener) 230 | await cache.delete('123') 231 | 232 | await listener2.waitFor(3).catch(error => { 233 | cache.destroy().catch(error => console.log('cache destroy raised error: ' + error)) 234 | throw error 235 | }) 236 | 237 | await cache.destroy() 238 | }) 239 | 240 | prom.then(() => done()).catch(err => done(err)) 241 | }) 242 | 243 | it('should be registrable with a key', (done) => { 244 | const cache = session.getCache('event-map' + Date.now()) 245 | const prom = new Promise((resolve) => { 246 | cache.on(MapLifecycleEvent.DESTROYED, () => { 247 | resolve() 248 | }) 249 | }) 250 | 251 | const listener = new CountingMapListener('listener-default') 252 | setImmediate(async () => { 253 | await cache.addMapListener(listener, '123') 254 | 255 | await cache.set('123', { xyz: '123-xyz' }) 256 | await cache.set('234', { abc: '123-abc' }) 257 | await cache.delete('123') 258 | 259 | await listener.waitFor(2).catch(error => done(error)).finally(() => cache.destroy()) 260 | }) 261 | 262 | prom.then(() => { 263 | validateEventsForListener(listener, { 264 | 'inserts': [{ key: '123', new: { xyz: '123-xyz' } }], 265 | 'updates': [], 266 | 'deletes': [{ key: '123', old: { xyz: '123-xyz' } }], 267 | 'order': [{ key: '123', new: { xyz: '123-xyz' } }, { key: '123', old: { xyz: '123-xyz' } }] 268 | }) 269 | }).then(() => done()).catch(error => done(error)) 270 | }) 271 | 272 | it('should be registrable with a filter', (done) => { 273 | const cache = session.getCache('event-map' + Date.now()) 274 | const prom = new Promise((resolve) => { 275 | cache.on(MapLifecycleEvent.DESTROYED, () => { 276 | resolve() 277 | }) 278 | }) 279 | 280 | const listener = new CountingMapListener('listener-default') 281 | setImmediate(async () => { 282 | const mapEventFilter = Filters.event(Filters.isNotNull('xyz')) 283 | await cache.addMapListener(listener, mapEventFilter) 284 | 285 | await cache.set('123', { xyz: '123-xyz' }) 286 | await cache.set('234', { abc: '123-abc' }) 287 | await cache.delete('123') 288 | 289 | await listener.waitFor(2).catch(error => console.log(error)).finally(() => cache.destroy()) 290 | 291 | }) 292 | 293 | prom.then(() => { 294 | validateEventsForListener(listener, { 295 | 'inserts': [{ key: '123', new: { xyz: '123-xyz' } }], 296 | 'updates': [], 297 | 'deletes': [{ key: '123', old: { xyz: '123-xyz' } }], 298 | 'order': [{ key: '123', new: { xyz: '123-xyz' } }, { key: '123', old: { xyz: '123-xyz' } }] 299 | }) 300 | }).then(() => done()).catch(error => done(error)) 301 | }) 302 | }) 303 | 304 | describe('A MapEvent', () => { 305 | it('should have the correct source', async () => { 306 | const cache = session.getCache('event-map' + Date.now()) 307 | const prom = new Promise((resolve) => { 308 | cache.on(MapLifecycleEvent.DESTROYED, () => { 309 | resolve() 310 | }) 311 | }) 312 | 313 | const listener = new MapListener() 314 | listener.on(MapEventType.INSERT, async (event) => { 315 | assert.deepEqual(event.source, cache) 316 | await cache.destroy() 317 | }) 318 | 319 | await cache.addMapListener(listener, Filters.event(Filters.always(), filter.MapEventFilter.INSERTED)) 320 | 321 | await cache.set('a', 'b') 322 | await prom.catch((error) => assert.fail(error)) 323 | }) 324 | 325 | it('should have the same name as the source cache', async () => { 326 | const cache = session.getCache('event-map' + Date.now()) 327 | const prom = new Promise((resolve) => { 328 | cache.on(MapLifecycleEvent.DESTROYED, () => { 329 | resolve() 330 | }) 331 | }) 332 | 333 | const listener = new MapListener() 334 | listener.on(MapEventType.INSERT, async (event) => { 335 | assert.deepEqual(event.name, cache.name) 336 | await cache.destroy() 337 | }) 338 | 339 | await cache.addMapListener(listener) 340 | 341 | await cache.set('a', 'b') 342 | await prom.catch((error) => assert.fail(error)) 343 | }) 344 | 345 | it('should produce a readable description of the event type', async () => { 346 | const cache = session.getCache('event-map' + Date.now()) 347 | const prom = new Promise((resolve) => { 348 | cache.on(MapLifecycleEvent.DESTROYED, () => { 349 | resolve() 350 | }) 351 | }) 352 | 353 | let count = 0 354 | const listener = new MapListener().on(MapEventType.INSERT, async (event) => { 355 | assert.equal(event.description, 'insert') 356 | if (++count === 3) { 357 | await cache.destroy() 358 | } 359 | }).on(MapEventType.DELETE, async (event) => { 360 | assert.equal(event.description, 'delete') 361 | if (++count === 3) { 362 | await cache.destroy() 363 | } 364 | }).on(MapEventType.UPDATE, async (event) => { 365 | assert.equal(event.description, 'update') 366 | if (++count === 3) { 367 | await cache.destroy() 368 | } 369 | }) 370 | 371 | await cache.addMapListener(listener) 372 | 373 | await cache.set('a', 'b') 374 | await cache.set('a', 'c') 375 | await cache.delete('a') 376 | await prom.catch((error) => assert.fail(error)) 377 | }) 378 | 379 | it('should return unknown string for unknown event IDs', async () => { 380 | const cache = session.getCache('event-map' + Date.now()) 381 | const response = new MapEventResponse() 382 | response.setId(8) 383 | const e = new MapEvent(cache, response, null) 384 | assert.equal(e.description, '') 385 | }) 386 | 387 | it('should throw if key cannot be deserialized', async () => { 388 | const cache = session.getCache('event-map' + Date.now()) 389 | const response = new MapEventResponse() 390 | response.setId(8) 391 | const e = new MapEvent(cache, response, { 392 | deserialize () { 393 | return undefined 394 | }, serialize () { 395 | return undefined 396 | }, format: 'Lossy' 397 | }) 398 | assert.throws(() => e.getKey()) 399 | }) 400 | }) 401 | 402 | class CountingMapListener extends MapListener { 403 | constructor (name) { 404 | super() 405 | this.name = name 406 | this.counter = 0 407 | this.insertEvents = [] 408 | this.updateEvents = [] 409 | this.deleteEvents = [] 410 | this.eventOrder = [] 411 | 412 | this.on(MapEventType.DELETE, (event) => { 413 | this.deleteEvents.push(event) 414 | this.eventOrder.push(event) 415 | this.counter++ 416 | if (debug) { 417 | console.log('[' + this.name + '] Received \'delete\' event: {key: ' + 418 | stringify(event.key) + ', new-value: ' + stringify(event.newValue) + 419 | ', old-value: ' + stringify(event.oldValue) + '}') 420 | } 421 | super.emit('event', 'delete') 422 | }).on(MapEventType.INSERT, (event) => { 423 | this.insertEvents.push(event) 424 | this.eventOrder.push(event) 425 | this.counter++ 426 | if (debug) { 427 | console.log('[' + this.name + '] Received \'insert\' event: {key: ' + 428 | stringify(event.key) + ', new-value: ' + stringify(event.newValue) + 429 | ', old-value: ' + stringify(event.oldValue) + '}') 430 | } 431 | super.emit('event', 'insert') 432 | }).on(MapEventType.UPDATE, (event) => { 433 | this.updateEvents.push(event) 434 | this.eventOrder.push(event) 435 | this.counter++ 436 | if (debug) { 437 | console.log('[' + this.name + '] Received \'updated\' event: {key: ' + 438 | stringify(event.key) + ', new-value: ' + stringify(event.newValue) + 439 | ', old-value: ' + stringify(event.oldValue) + '}') 440 | } 441 | super.emit('event', 'update') 442 | }) 443 | } 444 | 445 | waitFor (numberOfEvents) { 446 | if (this.counter === numberOfEvents) { 447 | return Promise.resolve() 448 | } 449 | if (this.counter > numberOfEvents) { 450 | return Promise.reject(new Error('Received more events than expected. Expected: ' + numberOfEvents + ', actual: ' + this.counter)) 451 | } 452 | return this.promiseTimeout(10000, new Promise((resolve, reject) => { 453 | this.on('event', () => { 454 | if (this.counter === numberOfEvents) { 455 | resolve() 456 | } 457 | if (this.counter > numberOfEvents) { 458 | return reject(new Error('Received more events than expected. Expected: ' + numberOfEvents + ', actual: ' + this.counter)) 459 | } 460 | }) 461 | })) 462 | } 463 | 464 | promiseTimeout (ms, promise) { 465 | let id 466 | let timeout = new Promise((resolve, reject) => { 467 | id = setTimeout(() => { 468 | reject(new Error('Timed out waiting for events in ' + ms + 'ms.')) 469 | }, ms) 470 | }) 471 | 472 | return Promise.race([ 473 | promise, 474 | timeout 475 | ]).then((result) => { 476 | clearTimeout(id) 477 | return result 478 | }) 479 | } 480 | } 481 | }) 482 | -------------------------------------------------------------------------------- /test/processor-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2023 Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { Filters, Extractors, Processors, Session } = require('../lib') 9 | const t = require('./util') 10 | const assert = require('assert').strict 11 | const { describe, it, after, beforeEach } = require('mocha'); 12 | 13 | describe('processor.Processors IT Test Suite', function () { 14 | const val123 = { id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3], group: 1 } 15 | const val234 = { id: 234, str: '234', ival: 234, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' } 16 | const val345 = { id: 345, str: '345', ival: 345, fval: 34.5, iarr: [3, 4, 5], group: 2 } 17 | const val456 = { id: 456, str: '456', ival: 456, fval: 45.6, iarr: [4, 5, 6], group: 3, nullIfOdd: 'non-null' } 18 | 19 | const toKey = { name: 'To' } 20 | const tscKey = { name: 'TypeScript' } 21 | const trieKey = { name: 'Trie' } 22 | const jadeKey = { name: 'Jade' } 23 | const javascriptKey = { name: 'JavaScript' } 24 | 25 | const toObj = { t: { o: { level: 3, word: 'To', tokens: ['t', 'o'] } } } 26 | const tscObj = { t: { y: { level: 3, word: 'TypeScript', tokens: ['t', 'y'] } } } 27 | const trieObj = { t: { r: { level: 3, word: 'Trie', tokens: ['t', 'r'] } } } 28 | const jadeObj = { j: { a: { d: { level: 4, word: 'Jade', tokens: ['j', 'a', 'd'] } } } } 29 | const javascriptObj = { j: { a: { level: 4, v: { word: 'JavaScript', tokens: ['j', 'a', 'v'] } } } } 30 | 31 | const versioned123 = { '@version': 1, id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3] } 32 | const versioned234 = { 33 | '@version': 2, 34 | id: 234, 35 | str: '234', 36 | ival: 234, 37 | fval: 23.4, 38 | iarr: [2, 3, 4], 39 | nullIfOdd: 'non-null' 40 | } 41 | const versioned345 = { '@version': 3, id: 345, str: '345', ival: 345, fval: 34.5, iarr: [3, 4, 5] } 42 | const versioned456 = { 43 | '@version': 4, 44 | id: 456, 45 | str: '456', 46 | ival: 456, 47 | fval: 45.6, 48 | iarr: [4, 5, 6], 49 | nullIfOdd: 'non-null' 50 | } 51 | 52 | const session = new Session() 53 | 54 | const cache = session.getCache('client-cache') 55 | const nested = session.getCache('nest-cache') 56 | const versioned = session.getCache('versioned-cache') 57 | 58 | this.timeout(300000) 59 | 60 | beforeEach(async () => { 61 | await cache.clear() 62 | await nested.clear() 63 | await versioned.clear() 64 | 65 | await cache.set(val123, val123) 66 | await cache.set(val234, val234) 67 | await cache.set(val345, val345) 68 | await cache.set(val456, val456) 69 | 70 | await nested.set(toKey, toObj) 71 | await nested.set(tscKey, tscObj) 72 | await nested.set(trieKey, trieObj) 73 | await nested.set(jadeKey, jadeObj) 74 | await nested.set(javascriptKey, javascriptObj) 75 | 76 | await versioned.set('123', versioned123) 77 | await versioned.set('234', versioned234) 78 | await versioned.set('345', versioned345) 79 | await versioned.set('456', versioned456) 80 | 81 | assert.equal(await cache.empty, false) 82 | assert.equal(await cache.size, 4) 83 | 84 | assert.equal(await nested.empty, false) 85 | assert.equal(await nested.size, 5) 86 | 87 | assert.equal(await versioned.empty, false) 88 | assert.equal(await versioned.size, 4) 89 | }) 90 | 91 | after(async () => { 92 | await cache.release().finally(() => session.close().catch()) 93 | }) 94 | 95 | describe('An EntryProcessor', () => { 96 | it('should be composable', async () => { 97 | const composite = Processors.nop().andThen(Processors.nop()) 98 | 99 | assert.equal(composite['@class'], 'processor.CompositeProcessor') 100 | // noinspection JSAccessibilityCheck 101 | assert.deepEqual(composite.processors, [Processors.nop(), Processors.nop()]) 102 | }) 103 | 104 | it('should be conditional', async () => { 105 | const conditional = Processors.nop().when(Filters.isNotNull('id')) 106 | 107 | assert.equal(conditional['@class'], 'processor.ConditionalProcessor') 108 | assert.deepEqual(conditional.processor, Processors.nop()) 109 | assert.deepEqual(conditional.filter, Filters.isNotNull('id')) 110 | }) 111 | }) 112 | 113 | describe('An ExtractorProcessor', () => { 114 | it('should have the proper internal type', async () => { 115 | const processor = Processors.extract('str') 116 | assert.equal(processor['@class'], 'processor.ExtractorProcessor') 117 | }) 118 | 119 | it('should be able to be invoked against a value associated with a key', async () => { 120 | const processor = Processors.extract('id').andThen(Processors.extract('str')) 121 | const value = await cache.invoke(val123, processor) 122 | 123 | assert.equal(value.length, 2) 124 | assert.deepEqual(value, [123, '123']) 125 | }) 126 | 127 | it('should be able to be invoked against all entries using a filter', async () => { 128 | const processor = Processors.extract('id').andThen(Processors.extract('str')) 129 | const result = await cache.invokeAll(Filters.always(), processor) 130 | 131 | await t.compareEntries([[val123, [123, '123']], [val234, [234, '234']], 132 | [val345, [345, '345']], [val456, [456, '456']]], result) 133 | }) 134 | 135 | it('should be able to be invoked against all entries using a set of keys', async () => { 136 | const processor = Processors.extract('id').andThen(Processors.extract('str')) 137 | const result = await cache.invokeAll([val123, val234], processor) 138 | 139 | await t.compareEntries([[val123, [123, '123']], [val234, [234, '234']]], result) 140 | }) 141 | }) 142 | 143 | describe('A CompositeProcessor', () => { 144 | it('should be able to be invoked against a value associated with a key', async () => { 145 | const processor = Processors.extract('id').andThen(Processors.extract('str')) 146 | const value = await cache.invoke(val123, processor) 147 | 148 | assert.equal(value.length, 2) 149 | assert.deepEqual(value, [123, '123']) 150 | }) 151 | 152 | it('should be able to be invoked against all entries using a filter', async () => { 153 | const processor = Processors.extract('id').andThen(Processors.extract('str')) 154 | const result = await cache.invokeAll(Filters.always(), processor) 155 | 156 | await t.compareEntries([[val123, [123, '123']], [val234, [234, '234']], 157 | [val345, [345, '345']], [val456, [456, '456']]], result) 158 | }) 159 | 160 | it('should be able to be invoked against entries matching a set of keys', async () => { 161 | const processor = Processors.extract('id').andThen(Processors.extract('str')) 162 | const result = await cache.invokeAll([val123, val234], processor) 163 | 164 | await t.compareEntries([[val123, [123, '123']], [val234, [234, '234']]], result) 165 | }) 166 | }) 167 | 168 | describe('A ConditionalProcessor', () => { 169 | it('should be able to be invoked against a value associated with a key', async () => { 170 | const ep = Processors.extract('str') 171 | .when(Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([1, 2]))) 172 | const value = await cache.invoke(val123, ep) 173 | 174 | assert.equal(value, '123') 175 | }) 176 | 177 | it('should be able to be invoked against all entries using a filter', async () => { 178 | const ep = Processors.extract('str') 179 | .when(Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([2, 3]))) 180 | const result = await cache.invokeAll(Filters.always(), ep) 181 | 182 | await t.compareEntries([[val123, '123'], [val234, '234']], result) 183 | }) 184 | 185 | it('should be able to be invoked against entries matching a set of keys', async () => { 186 | const ep = Processors.extract('str') 187 | .when(Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([2, 3]))) 188 | const result = await cache.invokeAll([val123, val234, val345], ep) 189 | 190 | await t.compareEntries([[val123, '123'], [val234, '234']], result) 191 | }) 192 | }) 193 | 194 | describe('A ConditionalPut Processor', () => { 195 | it('should have the proper internal type', async () => { 196 | const processor = Processors.conditionalPut(Filters.always(), 'someValue') 197 | 198 | assert.equal(processor['@class'], 'processor.ConditionalPut') 199 | }) 200 | 201 | it('should be able to be invoked against a value associated with a key', async () => { 202 | const f1 = Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([1, 2])) 203 | const ep = Processors.conditionalPut(f1, val234).returnCurrent() 204 | 205 | await cache.invoke(val123, ep) 206 | 207 | await t.compareEntries([[val123, val234], [val234, val234], 208 | [val345, val345], [val456, val456]], await cache.entries()) 209 | }) 210 | 211 | it('should be able to be invoked against all entries using a filter', async () => { 212 | const f1 = Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([3, 4])) 213 | const ep = Processors.conditionalPut(Filters.always(), val123).returnCurrent() 214 | 215 | await cache.invokeAll(f1, ep) 216 | 217 | await t.compareEntries([[val123, val123], [val234, val123], 218 | [val345, val123], [val456, val456]], await cache.entries()) 219 | }) 220 | 221 | it('should be able to be invoked against all entries using a set of keys', async () => { 222 | const f1 = Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([3, 4])) 223 | const ep = Processors.conditionalPut(f1, val123).returnCurrent() 224 | 225 | await cache.invokeAll([val123, val234], ep) 226 | 227 | await t.compareEntries([[val123, val123], [val234, val123], 228 | [val345, val345], [val456, val456]], await cache.entries()) 229 | }) 230 | }) 231 | 232 | describe('A ConditionalPutAll Processor', () => { 233 | it('should have the proper internal type', async () => { 234 | const processor = Processors.conditionalPutAll(Filters.always(), new Map().set('a', 'b')) 235 | 236 | assert.equal(processor['@class'], 'processor.ConditionalPutAll') 237 | assert.deepEqual(processor.filter, Filters.always()) 238 | assert.notEqual(processor.entries, undefined) 239 | }) 240 | 241 | it('should be able to ve invoked against all entries', async () => { 242 | const values = new Map([[val123, val345], [val345, val456]]) 243 | const ep = Processors.conditionalPutAll(Filters.always(), values) 244 | 245 | await cache.invokeAll(ep) 246 | 247 | await t.compareEntries([[val123, val345], [val234, val234], 248 | [val345, val456], [val456, val456]], await cache.entries()) 249 | }) 250 | 251 | it('should be able to be invoked against all entries using a set of keys', async () => { 252 | const values = new Map([['a', 'b']]) 253 | const ep = Processors.conditionalPutAll(Filters.not(Filters.present()), values) 254 | 255 | await cache.invokeAll([val123, 'a'], ep) 256 | 257 | await t.compareEntries([[val123, val123], [val234, val234], 258 | [val345, val345], [val456, val456], ['a', 'b']], await cache.entries()) 259 | }) 260 | }) 261 | 262 | describe('A ConditionalRemove Processor', () => { 263 | it('should have the proper internal type', async () => { 264 | const processor = Processors.conditionalRemove(Filters.always()) 265 | 266 | assert.equal(processor['@class'], 'processor.ConditionalRemove') 267 | }) 268 | 269 | it('should be able to be invoked against a value associated with a key', async () => { 270 | const f1 = Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([1, 2])) 271 | const ep = Processors.conditionalRemove(f1).returnCurrent() 272 | 273 | const result = await cache.invoke(val123, ep) 274 | 275 | assert.equal(result, null) 276 | await t.compareEntries([[val234, val234], [val345, val345], [val456, val456]], await cache.entries()) 277 | }) 278 | 279 | it('should be able to be invoked against all entries using a filter', async () => { 280 | const f1 = Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([3, 4])) 281 | const ep = Processors.conditionalRemove(Filters.always()).returnCurrent() 282 | 283 | const result = await cache.invokeAll(f1, ep) 284 | 285 | assert.deepEqual(result.size, 0) 286 | await t.compareEntries([[val123, val123], [val456, val456]], await cache.entries()) 287 | }) 288 | 289 | it('should be able to be invoked against all entries using a set of keys', async () => { 290 | const f1 = Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([3, 4])) 291 | const ep = Processors.conditionalRemove(f1).returnCurrent() 292 | 293 | await cache.invokeAll([val123, val234], ep) 294 | 295 | assert.deepEqual(val123, val123) 296 | await t.compareEntries([[val123, val123], [val345, val345], [val456, val456]], await cache.entries()) 297 | }) 298 | }) 299 | 300 | describe('A VersionedPut Processor', () => { 301 | it('should have the proper internal type', async () => { 302 | const processor = Processors.versionedPut(Filters.always()) 303 | 304 | assert.equal(processor['@class'], 'processor.VersionedPut') 305 | }) 306 | 307 | it('should be able to be invoked against a value associated with a key', async () => { 308 | const ep = Processors.versionedPut(versioned123) 309 | const result = await versioned.invoke('123', ep) 310 | 311 | const expected = { '@version': 2, id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3] } 312 | assert.equal(result, null) 313 | assert.deepEqual(await versioned.get('123'), expected) 314 | }) 315 | 316 | it('should be able to be invoked against all entries using a filter', async () => { 317 | const f1 = Filters.arrayContains(Extractors.extract('iarr'), 1) 318 | const ep = Processors.versionedPut(versioned123) 319 | 320 | const result = await versioned.invokeAll(f1, ep) 321 | 322 | const expected = { '@version': 2, id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3] } 323 | assert.deepEqual(result.size, 0) 324 | await t.compareEntries([['123', expected], ['234', versioned234], ['345', versioned345], 325 | ['456', versioned456]], await versioned.entries()) 326 | }) 327 | 328 | it('should be able to be invoked against all entries using a set of keys', async () => { 329 | const ep = Processors.versionedPut(versioned123) 330 | 331 | const result = await versioned.invokeAll(['123', '456'], ep) 332 | 333 | const expected = { '@version': 2, id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3] } 334 | assert.deepEqual(result.size, 0) 335 | await t.compareEntries([['123', expected], ['234', versioned234], ['345', versioned345], 336 | ['456', versioned456]], await versioned.entries()) 337 | }) 338 | }) 339 | 340 | describe('A VersionedPutAll Processor', () => { 341 | it('should have the proper internal type', async () => { 342 | const processor = Processors.versionedPutAll(new Map()) 343 | 344 | assert.equal(processor['@class'], 'processor.VersionedPutAll') 345 | }) 346 | 347 | it('should be able to be invoked against all entries', async () => { 348 | const entries = new Map([['123', versioned123], ['456', versioned456]]) 349 | const ep = Processors.versionedPutAll(entries) 350 | 351 | const result = await versioned.invokeAll(ep) 352 | 353 | const expected123 = { '@version': 2, id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3] } 354 | const expected456 = { 355 | '@version': 5, 356 | id: 456, 357 | str: '456', 358 | ival: 456, 359 | fval: 45.6, 360 | iarr: [4, 5, 6], 361 | nullIfOdd: 'non-null' 362 | } 363 | 364 | assert.deepEqual(result.size, 0) 365 | await t.compareEntries([['123', expected123], ['234', versioned234], ['345', versioned345], 366 | ['456', expected456]], await versioned.entries()) 367 | }) 368 | 369 | it('should be able to be invoked against all entries using a filter', async () => { 370 | const entries = new Map([['123', versioned123], ['456', versioned456]]) 371 | const ep = Processors.versionedPutAll(entries) 372 | 373 | const result = await versioned.invokeAll(Filters.always(), ep) 374 | 375 | const expected123 = { '@version': 2, id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3] } 376 | const expected456 = { 377 | '@version': 5, 378 | id: 456, 379 | str: '456', 380 | ival: 456, 381 | fval: 45.6, 382 | iarr: [4, 5, 6], 383 | nullIfOdd: 'non-null' 384 | } 385 | 386 | assert.deepEqual(result.size, 0) 387 | await t.compareEntries([['123', expected123], ['234', versioned234], ['345', versioned345], 388 | ['456', expected456]], await versioned.entries()) 389 | }) 390 | 391 | it('should be able to be invoked against all entries using a set of keys', async () => { 392 | const entries = new Map([['123', versioned123], ['456', versioned456]]) 393 | const ep = Processors.versionedPutAll(entries) 394 | 395 | const result = await versioned.invokeAll(['123', '456'], ep) 396 | 397 | const expected123 = { '@version': 2, id: 123, str: '123', ival: 123, fval: 12.3, iarr: [1, 2, 3] } 398 | const expected456 = { 399 | '@version': 5, 400 | id: 456, 401 | str: '456', 402 | ival: 456, 403 | fval: 45.6, 404 | iarr: [4, 5, 6], 405 | nullIfOdd: 'non-null' 406 | } 407 | 408 | assert.deepEqual(result.size, 0) 409 | await t.compareEntries([['123', expected123], ['234', versioned234], ['345', versioned345], 410 | ['456', expected456]], await versioned.entries()) 411 | }) 412 | }) 413 | 414 | describe('An Updater Processor', () => { 415 | it('should have the proper internal type', async () => { 416 | const processor = Processors.update('a.b.ival', 12300) 417 | 418 | assert.equal(processor['@class'], 'processor.UpdaterProcessor') 419 | assert.notDeepEqual(processor.value, undefined) 420 | // noinspection JSAccessibilityCheck 421 | assert.notDeepEqual(processor.updater, undefined) 422 | }) 423 | 424 | it('should be able to be invoked against a value associated with a key', async () => { 425 | const ep = Processors.update('str', '123000') 426 | .andThen(Processors.update('ival', 123000)) 427 | 428 | const result = await cache.invoke(val123, ep) 429 | 430 | const processor = Processors.extract('ival').andThen(Processors.extract('str')) 431 | const value = await cache.invoke(val123, processor) 432 | 433 | const others = await cache.getAll([val234, val345, val456]) 434 | 435 | assert.deepEqual(result, [true, true]) 436 | assert.equal(value.length, 2) 437 | assert.deepEqual(value, [123000, '123000']) 438 | await t.compareEntries([[val234, val234], [val345, val345], [val456, val456]], others) 439 | }) 440 | 441 | it('should be able to be invoked against all entries using a filter', async () => { 442 | const f1 = Filters.arrayContainsAll(Extractors.extract('iarr'), new Set([3, 4])) 443 | const ep = Processors.update('str', '123000') 444 | .andThen(Processors.update('ival', 123000)) 445 | 446 | const result = await cache.invokeAll(f1, ep) 447 | 448 | const processor = Processors.extract('ival').andThen(Processors.extract('str')) 449 | const value = await cache.invokeAll([val234, val345], processor) 450 | 451 | const others = await cache.getAll([val123, val456]) 452 | 453 | await t.compareEntries([[val234, [123000, '123000']], [val345, [123000, '123000']]], value) 454 | await t.compareEntries([[val234, [true, true]], [val345, [true, true]]], result) 455 | await t.compareEntries([[val123, val123], [val456, val456]], others) 456 | }) 457 | 458 | it('should be able to be invoked against all entries using a set of keys', async () => { 459 | const ep = Processors.update('str', '123000') 460 | .andThen(Processors.update('ival', 123000)) 461 | 462 | const result = await cache.invokeAll([val234, val345], ep) 463 | 464 | const processor = Processors.extract('ival').andThen(Processors.extract('str')) 465 | const value = await cache.invokeAll([val234, val345], processor) 466 | 467 | const others = await cache.getAll([val123, val456]) 468 | 469 | await t.compareEntries([[val234, [123000, '123000']], [val345, [123000, '123000']]], value) 470 | await t.compareEntries([[val234, [true, true]], [val345, [true, true]]], result) 471 | await t.compareEntries([[val123, val123], [val456, val456]], others) 472 | }) 473 | }) 474 | 475 | describe('A MethodInvocation Processor (non-mutating)', () => { 476 | it('should have the proper internal type', async () => { 477 | const processor = Processors.invokeAccessor('a.b.ival', 12300, 12301) 478 | 479 | assert.equal(processor['@class'], 'processor.MethodInvocationProcessor') 480 | assert.equal(processor.methodName, 'a.b.ival') 481 | assert.equal(processor.mutator, false) 482 | assert.deepEqual(processor.args, [12300, 12301]) 483 | }) 484 | 485 | it('should be able to be invoked against a value associated with a key', async () => { 486 | const ep = Processors.invokeAccessor('get', 'ival') 487 | const value = await cache.invoke(val123, ep) 488 | 489 | assert.equal(value, 123) 490 | }) 491 | 492 | it('should be able to be invoked against all entries using a filter', async () => { 493 | const ep = Processors.invokeAccessor('get', 'ival') 494 | const value = await cache.invokeAll(Filters.greater('ival', 200), ep) 495 | 496 | await t.compareEntries([[val234, 234], [val345, 345], [val456, 456]], value) 497 | }) 498 | 499 | it('should be able to be invoked against all entries using a set of keys', async () => { 500 | const ep = Processors.invokeAccessor('get', 'ival') 501 | const value = await cache.invokeAll([val234, val345], ep) 502 | 503 | await t.compareEntries([[val234, 234], [val345, 345]], value) 504 | }) 505 | }) 506 | 507 | describe('A MethodInvocation Processor (mutating)', () => { 508 | it('should have the proper internal type', async () => { 509 | const processor = Processors.invokeMutator('a.b.ival', 12300, 12301) 510 | 511 | assert.equal(processor['@class'], 'processor.MethodInvocationProcessor') 512 | assert.equal(processor.methodName, 'a.b.ival') 513 | assert.equal(processor.mutator, true) 514 | assert.deepEqual(processor.args, [12300, 12301]) 515 | }) 516 | 517 | it('should be able to be invoked against a value associated with a key', async () => { 518 | const ep = Processors.invokeMutator('remove', 'ival') 519 | const value = await cache.invoke(val123, ep) 520 | 521 | assert.equal(value, 123) 522 | }) 523 | 524 | it('should be able to be invoked against all entries using a filter', async () => { 525 | const ep = Processors.invokeMutator('remove', 'ival') 526 | .andThen(Processors.invokeMutator('remove', 'iarr')) 527 | 528 | const result = await cache.invokeAll(Filters.greater('ival', 200), ep) 529 | 530 | // Check removed values 531 | await t.compareEntries([[val234, [234, [2, 3, 4]]], [val345, [345, [3, 4, 5]]], [val456, [456, [4, 5, 6]]]], result) 532 | 533 | // Ensure that remaining attributes are still intact. 534 | await t.compareEntries([[val123, val123], 535 | [val234, { id: 234, str: '234', fval: 23.4, group: 2, nullIfOdd: 'non-null' }], 536 | [val345, { id: 345, str: '345', fval: 34.5, group: 2 }], 537 | [val456, { id: 456, str: '456', fval: 45.6, group: 3, nullIfOdd: 'non-null' }]], await cache.entries()) 538 | }) 539 | 540 | it('should be able to be invoked against all entries using a set of keys', async () => { 541 | const ep = Processors.invokeMutator('remove', 'ival') 542 | .andThen(Processors.invokeMutator('remove', 'iarr')) 543 | 544 | const result = await cache.invokeAll([val234, val345], ep) 545 | 546 | // Check removed values 547 | await t.compareEntries([[val234, [234, [2, 3, 4]]], [val345, [345, [3, 4, 5]]]], result) 548 | 549 | // Ensure that remaining attributes are still intact. 550 | await t.compareEntries([[val123, val123], 551 | [val234, { id: 234, str: '234', fval: 23.4, group: 2, nullIfOdd: 'non-null' }], 552 | [val345, { id: 345, str: '345', fval: 34.5, group: 2 }], 553 | [val456, val456]], await cache.entries()) 554 | }) 555 | }) 556 | 557 | describe('A NumberMultiplier processor', () => { 558 | it('should have the proper internal type', async () => { 559 | const processor = Processors.multiply('ival', 2) 560 | 561 | assert.equal(processor['@class'], 'processor.NumberMultiplier') 562 | // noinspection JSAccessibilityCheck 563 | assert.equal(processor.multiplier, 2) 564 | // noinspection JSAccessibilityCheck 565 | assert.equal(processor.postMultiplication, false) 566 | 567 | const processor2 = Processors.multiply('ival', 2, true) 568 | // noinspection JSAccessibilityCheck 569 | assert.equal(processor2.postMultiplication, true) 570 | }) 571 | 572 | it('should be able to be invoked against a value associated with a key', async () => { 573 | const processor = Processors.multiply('ival', 2) 574 | 575 | const result = await cache.invoke(val123, processor) 576 | assert.equal(result, 246) 577 | }) 578 | 579 | it('should be able to be invoked against all entries using a filter', async () => { 580 | const processor = Processors.multiply('ival', 2) 581 | 582 | const result = await cache.invokeAll(Filters.greater('ival', 200), processor) 583 | 584 | // Check result 585 | await t.compareEntries([[val234, 468], [val345, 690], [val456, 912]], result) 586 | 587 | // Ensure that remaining attributes are still intact. 588 | await t.compareEntries([[val123, val123], 589 | [val234, { id: 234, str: '234', ival: 468, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' }], 590 | [val345, { id: 345, str: '345', ival: 690, fval: 34.5, iarr: [3, 4, 5], group: 2 }], 591 | [val456, { id: 456, str: '456', ival: 912, fval: 45.6, iarr: [4, 5, 6], group: 3, nullIfOdd: 'non-null' }]], await cache.entries()) 592 | }) 593 | 594 | it('should be able to be invoked against all entries using a set of keys', async () => { 595 | const processor = Processors.multiply('ival', 2) 596 | 597 | const result = await cache.invokeAll([val234, val345, val456], processor) 598 | 599 | // Check result 600 | await t.compareEntries([[val234, 468], [val345, 690], [val456, 912]], result) 601 | 602 | // Ensure that remaining attributes are still intact. 603 | await t.compareEntries([[val123, val123], 604 | [val234, { id: 234, str: '234', ival: 468, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' }], 605 | [val345, { id: 345, str: '345', ival: 690, fval: 34.5, iarr: [3, 4, 5], group: 2 }], 606 | [val456, { id: 456, str: '456', ival: 912, fval: 45.6, iarr: [4, 5, 6], group: 3, nullIfOdd: 'non-null' }]], await cache.entries()) 607 | }) 608 | }) 609 | 610 | describe('A NumberIncrementor processor', () => { 611 | it('should have the proper internal type', async () => { 612 | const processor = Processors.increment('ival', 2) 613 | 614 | assert.equal(processor['@class'], 'processor.NumberIncrementor') 615 | // noinspection JSAccessibilityCheck 616 | assert.equal(processor.increment, 2) 617 | // noinspection JSAccessibilityCheck 618 | assert.equal(processor.postIncrement, false) 619 | 620 | const processor2 = Processors.increment('ival', 2, true) 621 | // noinspection JSAccessibilityCheck 622 | assert.equal(processor2.postIncrement, true) 623 | }) 624 | 625 | it('should be able to be invoked against a value associated with a key', async () => { 626 | const processor = Processors.increment('ival', 2) 627 | 628 | const result = await cache.invoke(val123, processor) 629 | assert.equal(result, 125) 630 | }) 631 | 632 | it('should be able to be invoked against all entries using a filter', async () => { 633 | const processor = Processors.increment('ival', 2) 634 | 635 | const result = await cache.invokeAll(Filters.greater('ival', 200), processor) 636 | 637 | // Check result 638 | await t.compareEntries([[val234, 236], [val345, 347], [val456, 458]], result) 639 | 640 | // Ensure that remaining attributes are still intact. 641 | await t.compareEntries([[val123, val123], 642 | [val234, { id: 234, str: '234', ival: 236, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' }], 643 | [val345, { id: 345, str: '345', ival: 347, fval: 34.5, iarr: [3, 4, 5], group: 2 }], 644 | [val456, { id: 456, str: '456', ival: 458, fval: 45.6, iarr: [4, 5, 6], group: 3, nullIfOdd: 'non-null' }]], await cache.entries()) 645 | }) 646 | 647 | it('should be able to be invoked against all entries using a set of keys', async () => { 648 | const processor = Processors.increment('ival', 2) 649 | 650 | const result = await cache.invokeAll([val234, val345, val456], processor) 651 | 652 | // Check result 653 | await t.compareEntries([[val234, 236], [val345, 347], [val456, 458]], result) 654 | 655 | // Ensure that remaining attributes are still intact. 656 | await t.compareEntries([[val123, val123], 657 | [val234, { id: 234, str: '234', ival: 236, fval: 23.4, iarr: [2, 3, 4], group: 2, nullIfOdd: 'non-null' }], 658 | [val345, { id: 345, str: '345', ival: 347, fval: 34.5, iarr: [3, 4, 5], group: 2 }], 659 | [val456, { 660 | id: 456, 661 | str: '456', 662 | ival: 458, 663 | fval: 45.6, 664 | iarr: [4, 5, 6], 665 | group: 3, 666 | nullIfOdd: 'non-null' 667 | }]], await cache.entries()) 668 | }) 669 | }) 670 | 671 | describe('A PreloadRequest processor', () => { 672 | it('should have the proper internal type', async () => { 673 | const processor = Processors.preload() 674 | 675 | assert.equal(processor['@class'], 'processor.PreloadRequest') 676 | }) 677 | 678 | it('should be able to be invokable', async () => { 679 | const processor = Processors.preload() 680 | 681 | const result = await cache.invoke(val123, processor) 682 | assert.equal(result, null) 683 | }) 684 | }) 685 | 686 | describe('A Touch processor', () => { 687 | it('should have the proper internal type', async () => { 688 | const processor = Processors.touch() 689 | 690 | assert.equal(processor['@class'], 'processor.TouchProcessor') 691 | }) 692 | }) 693 | 694 | describe('A Script processor', () => { 695 | it('should have the proper internal type', async () => { 696 | const processor = Processors.script('js', 'jsprocessor', 'a', 'b') 697 | 698 | assert.equal(processor['@class'], 'processor.ScriptProcessor') 699 | assert.equal(processor['language'], 'js') 700 | assert.equal(processor['name'], 'jsprocessor') 701 | assert.deepEqual(processor['args'], ['a', 'b']) 702 | }) 703 | }) 704 | }) 705 | -------------------------------------------------------------------------------- /test/request-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * http://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { Filters, Extractors, util } = require('../lib') 9 | const assert = require('assert').strict 10 | const { 11 | ClearRequest, AddIndexRequest, ContainsEntryRequest, ContainsKeyRequest, ContainsValueRequest, 12 | GetRequest, EntrySetRequest, KeySetRequest, ValuesRequest 13 | } = require('../lib/grpc/messages_pb') 14 | const { describe, it } = require('mocha'); 15 | 16 | describe('The RequestFactory', function () { 17 | this.timeout(10000) 18 | const serializer = util.SerializerRegistry.instance().serializer('json') 19 | const reqFactory = new util.RequestFactory('States', 'test', serializer) 20 | 21 | const states = { 22 | ca: { 23 | name: 'California', 24 | abbreviation: 'CA', 25 | capital: 'Sacramento', 26 | tz: 'Pacific', 27 | population: 39, // 39.55 28 | neighbors: ['OR', 'NV', 'AZ'] 29 | }, 30 | ny: { 31 | name: 'New York', 32 | abbreviation: 'NY', 33 | capital: 'Albany', 34 | tz: 'Eastern', 35 | population: 19, // 19.54 36 | neighbors: ['NJ', 'PN', 'CT', 'MA', 'VA'] 37 | }, 38 | tx: { 39 | name: 'Texas', 40 | abbreviation: 'TX', 41 | capital: 'Austin', 42 | tz: 'Mountain', 43 | population: 29, // 19.54 44 | neighbors: ['NJ', 'PN', 'CT', 'MA', 'VA'] 45 | } 46 | } 47 | 48 | it('should be able to create a clear request', () => { 49 | const request = reqFactory.clear() 50 | assert.equal(request.getCache(), 'States') 51 | assert.equal(request.getScope(), 'test') 52 | assert.equal(request instanceof ClearRequest, true) 53 | }) 54 | 55 | it('should be able to create an unsorted IndexRequest', () => { 56 | const request = reqFactory.addIndex(Extractors.extract('abbreviation')) 57 | const ue = serializer.deserialize(serializer.serialize(Extractors.extract('abbreviation'))) 58 | 59 | assert.equal(request instanceof AddIndexRequest, true) 60 | assert.equal(request.getCache(), 'States') 61 | assert.equal(request.getScope(), 'test') 62 | assert.deepEqual(serializer.deserialize(request.getExtractor()), ue) 63 | assert.equal(request.getSorted(), false) 64 | assert.equal(request.getComparator_asU8().length, 0) 65 | }) 66 | 67 | it('should be able to create a sorted IndexRequest', () => { 68 | const request = reqFactory.addIndex(Extractors.extract('abbreviation'), true, {'@class': 'SimpleComparator'}) 69 | const ue = serializer.deserialize(serializer.serialize(Extractors.extract('abbreviation'))) 70 | 71 | assert.equal(request instanceof AddIndexRequest, true) 72 | assert.equal(request.getCache(), 'States') 73 | assert.equal(request.getScope(), 'test') 74 | assert.deepEqual(serializer.deserialize(request.getExtractor()), ue) 75 | assert.equal(request.getSorted(), true) 76 | assert.equal(request.getComparator_asU8().length, 30) 77 | assert.deepEqual(serializer.deserialize(request.getComparator()), {'@class': 'SimpleComparator'}) 78 | }) 79 | 80 | it('should be able to create a ContainsEntryRequest', () => { 81 | const ce = reqFactory.containsEntry('key1', states.ca) 82 | 83 | assert.equal(ce instanceof ContainsEntryRequest, true) 84 | assert.equal(ce.getCache(), 'States') 85 | assert.equal(ce.getScope(), 'test') 86 | assert.equal(serializer.deserialize(ce.getKey()), 'key1') 87 | assert.deepEqual(serializer.deserialize(ce.getValue()), states.ca) 88 | }) 89 | 90 | it('should be able to create a ContainsKeyRequest', () => { 91 | const ce = reqFactory.containsKey('key1') 92 | 93 | assert.equal(ce instanceof ContainsKeyRequest, true) 94 | assert.equal(ce.getCache(), 'States') 95 | assert.equal(ce.getScope(), 'test') 96 | assert.equal(serializer.deserialize(ce.getKey()), 'key1') 97 | }) 98 | 99 | it('should be able to create a ContainsValueRequest', () => { 100 | const ce = reqFactory.containsValue(states.ca) 101 | 102 | assert.equal(ce instanceof ContainsValueRequest, true) 103 | assert.equal(ce.getCache(), 'States') 104 | assert.equal(ce.getScope(), 'test') 105 | assert.deepEqual(serializer.deserialize(ce.getValue()), states.ca) 106 | }) 107 | 108 | it('should be able to create a GetRequest', () => { 109 | const ce = reqFactory.get('key1') 110 | 111 | assert.equal(ce instanceof GetRequest, true) 112 | assert.equal(ce.getCache(), 'States') 113 | assert.equal(ce.getScope(), 'test') 114 | assert.equal(serializer.deserialize(ce.getKey()), 'key1') 115 | }) 116 | 117 | it('should be able to creat an EntrySetRequest with Filter', () => { 118 | const filterSer = serializer.deserialize(serializer.serialize(Filters.always())) 119 | const ce = reqFactory.entrySet(Filters.always()) 120 | 121 | assert.equal(ce instanceof EntrySetRequest, true) 122 | assert.equal(ce.getCache(), 'States') 123 | assert.equal(ce.getScope(), 'test') 124 | assert.deepEqual(serializer.deserialize(ce.getFilter()), filterSer) 125 | assert.equal(ce.getComparator_asU8().length, 0) 126 | }) 127 | 128 | it('should be able to creat an EntrySetRequest with Filter and Comparator', () => { 129 | const filterSer = serializer.deserialize(serializer.serialize(Filters.always())) 130 | const ce = reqFactory.entrySet(Filters.always(), {'@class': 'SimpleComparator'}) 131 | 132 | assert.equal(ce instanceof EntrySetRequest, true) 133 | assert.equal(ce.getCache(), 'States') 134 | assert.equal(ce.getScope(), 'test') 135 | assert.deepEqual(serializer.deserialize(ce.getFilter()), filterSer) 136 | assert.equal(ce.getComparator_asU8().length, 30) 137 | assert.deepEqual(serializer.deserialize(ce.getComparator()), {'@class': 'SimpleComparator'}) 138 | }) 139 | 140 | it('should be able to creat a KeySetRequest with Filter', () => { 141 | const filterSer = serializer.deserialize(serializer.serialize(Filters.always())) 142 | const ce = reqFactory.keySet(Filters.always()) 143 | 144 | assert.equal(ce instanceof KeySetRequest, true) 145 | assert.equal(ce.getCache(), 'States') 146 | assert.equal(ce.getScope(), 'test') 147 | assert.deepEqual(serializer.deserialize(ce.getFilter()), filterSer) 148 | }) 149 | 150 | it('should be able to creat an ValuesRequest with Filter and Comparator', () => { 151 | const filterSer = serializer.deserialize(serializer.serialize(Filters.always())) 152 | const ce = reqFactory.values(Filters.always(), {'@class': 'SimpleComparator'}) 153 | 154 | assert.equal(ce instanceof ValuesRequest, true) 155 | assert.equal(ce.getCache(), 'States') 156 | assert.equal(ce.getScope(), 'test') 157 | assert.deepEqual(serializer.deserialize(ce.getFilter()), filterSer) 158 | assert.equal(ce.getComparator_asU8().length, 30) 159 | assert.deepEqual(serializer.deserialize(ce.getComparator()), {'@class': 'SimpleComparator'}) 160 | }) 161 | }) 162 | 163 | -------------------------------------------------------------------------------- /test/serialization-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const {util, Session} = require('../lib') 9 | const assert = require('assert').strict 10 | const {describe, it} = require('mocha'); 11 | const Decimal = require("decimal.js"); 12 | const test = require('./util') 13 | 14 | describe('Serialization Unit/Integration Test Suite', () => { 15 | function getSimpleJson(forType, value) { 16 | return "{\"@class\": \"" + forType + "\", \"value\": \"" + value + "\"}" 17 | } 18 | 19 | function getNestedJSON(forType, value) { 20 | let simple = getSimpleJson(forType, value) 21 | return "{\n" + 22 | " \"propA\": \"valueA\",\n" + 23 | " \"propB\": [\n" + 24 | simple + 25 | " ],\n" + 26 | " \"inner\": {\n\"inner\":" + 27 | simple + 28 | " }\n" + 29 | " }" 30 | } 31 | 32 | describe("Serialization Unit Tests", () => { 33 | describe('When deserializing', () => { 34 | it('should be possible to deserialize a large decimal number from simple payload', () => { 35 | let serializer = new util.JSONSerializer() 36 | let result = serializer.deserialize(getSimpleJson("math.BigDec", "11.009283736183")) 37 | let expected = result instanceof Decimal 38 | assert.equal(expected, true) 39 | }) 40 | 41 | it('should be possible to deserialize a large decimal number from nested payload', () => { 42 | let serializer = new util.JSONSerializer() 43 | let result = serializer.deserialize(getNestedJSON("math.BigDec", "11.009283736183")) 44 | let arrayResult = result["propB"][0] instanceof Decimal 45 | let innerResult = result["inner"]["inner"] instanceof Decimal 46 | console.log(typeof new Decimal("11.1111")) 47 | assert.equal(arrayResult, true) 48 | assert.equal(innerResult, true) 49 | }) 50 | 51 | it('should be possible to deserialize a large integer number from simple payload', () => { 52 | let serializer = new util.JSONSerializer() 53 | let result = serializer.deserialize(getSimpleJson("math.BigInt", "11009283736183")) 54 | let expected = typeof result === "bigint" 55 | assert.equal(expected, true) 56 | }) 57 | 58 | it('should be possible to deserialize a large integer number from nested payload', () => { 59 | let serializer = new util.JSONSerializer() 60 | let result = serializer.deserialize(getNestedJSON("math.BigInt", "11009283736183")) 61 | let arrayResult = typeof result["propB"][0] === "bigint" 62 | let innerResult = typeof result["inner"]["inner"] === "bigint" 63 | assert.equal(arrayResult, true) 64 | assert.equal(innerResult, true) 65 | }) 66 | }) 67 | 68 | describe('When serializing', () => { 69 | it('should be possible to serialize a large decimal number, standalone', () => { 70 | let serializer = new util.JSONSerializer() 71 | let result = serializer.serialize(new Decimal("11.02430570239475")) 72 | if (result.readInt8(0) === 21) { 73 | result = result.subarray(1) 74 | } 75 | assert.equal(result.toString(), "{\"@class\":\"math.BigDec\",\"value\":\"11.02430570239475\"}") 76 | }) 77 | 78 | it('should be possible to serialize a large decimal number, nested', () => { 79 | let serializer = new util.JSONSerializer() 80 | let testObj = { 81 | id: new Decimal("11.02430570239475"), 82 | arr: [new Decimal("11.02430570239474"), new Decimal("11.02430570239473")], 83 | inner: { 84 | inner: new Decimal("11.02430570239476") 85 | } 86 | } 87 | let result = serializer.serialize(testObj) 88 | if (result.readInt8(0) === 21) { 89 | result = result.subarray(1) 90 | } 91 | assert.equal(result.toString(), "{\"id\":{\"@class\":\"math.BigDec\",\"value\":\"11.02430570239475\"},\"arr\":[{\"@class\":\"math.BigDec\",\"value\":\"11.02430570239474\"},{\"@class\":\"math.BigDec\",\"value\":\"11.02430570239473\"}],\"inner\":{\"inner\":{\"@class\":\"math.BigDec\",\"value\":\"11.02430570239476\"}}}") 92 | }) 93 | 94 | it('should be possible to serialize a large integer number, standalone', () => { 95 | let serializer = new util.JSONSerializer() 96 | let result = serializer.serialize(BigInt(11)) 97 | if (result.readInt8(0) === 21) { 98 | result = result.subarray(1) 99 | } 100 | assert.equal(result.toString(), "{\"@class\":\"math.BigInt\",\"value\":\"11\"}") 101 | }) 102 | 103 | it('should be possible to serialize a large integer number, nested', () => { 104 | let serializer = new util.JSONSerializer() 105 | let testObj = { 106 | id: BigInt("11"), 107 | arr: [BigInt("12"), BigInt("13")], 108 | inner: { 109 | inner: BigInt("14") 110 | } 111 | } 112 | let result = serializer.serialize(testObj) 113 | if (result.readInt8(0) === 21) { 114 | result = result.subarray(1) 115 | } 116 | assert.equal(result.toString(), "{\"id\":{\"@class\":\"math.BigInt\",\"value\":\"11\"},\"arr\":[{\"@class\":\"math.BigInt\",\"value\":\"12\"},{\"@class\":\"math.BigInt\",\"value\":\"13\"}],\"inner\":{\"inner\":{\"@class\":\"math.BigInt\",\"value\":\"14\"}}}") 117 | }) 118 | }) 119 | }) 120 | 121 | describe("Serialization Integration Tests", () => { 122 | const session = new Session() 123 | const cache = session.getCache("test-ser") 124 | 125 | after(async () => { 126 | await cache.release().finally(() => session.close().catch()) 127 | }) 128 | 129 | async function validateRoundTrip(expected) { 130 | await cache.set("a", expected) 131 | let result = await cache.get("a") 132 | assert.deepStrictEqual(result, expected) 133 | } 134 | 135 | it('should be possible to round-trip a large decimal number with Coherence', async () => { 136 | await validateRoundTrip(new Decimal("11.59999838372726283940498483472672627384384949484747376362627")) 137 | }) 138 | 139 | it('should be possible to round-trip a large integer number with Coherence', async () => { 140 | await validateRoundTrip(BigInt("1159999838372726283940498483472672627384384949484747376362627")) 141 | }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /test/session-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2025, Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | 8 | const { event, Session } = require('../lib') 9 | const assert = require('assert').strict 10 | const { describe, it } = require('mocha'); 11 | 12 | describe('Session Tests Suite (unit/IT)', () => { 13 | describe('Session Unit Test Suite', () => { 14 | describe('A SessionBuilder', () => { 15 | it('should have the expected defaults', () => { 16 | const session = new Session() 17 | 18 | if (process.env.COHERENCE_TLS_CERTS_PATH) { 19 | let currentDir = process.cwd() 20 | assert.equal(session.options.tls.enabled, true) 21 | assert.equal(session.options.tls.caCertPath, currentDir + '/etc/cert/guardians-ca.crt') 22 | assert.equal(session.options.tls.clientCertPath, currentDir + '/etc/cert/star-lord.crt') 23 | assert.equal(session.options.tls.clientKeyPath, currentDir + '/etc/cert/star-lord.pem') 24 | } else { 25 | assert.equal(session.options.tls.enabled, false) 26 | assert.equal(session.options.tls.caCertPath, undefined) 27 | assert.equal(session.options.tls.clientCertPath, undefined) 28 | assert.equal(session.options.tls.clientKeyPath, undefined) 29 | } 30 | 31 | if (!process.env.COHERENCE_SERVER_ADDRESS) { 32 | assert.equal(session.address, Session.DEFAULT_ADDRESS) 33 | } 34 | assert.equal(session.options.requestTimeoutInMillis, 60000) 35 | assert.equal(session.options.format, Session.DEFAULT_FORMAT) 36 | }) 37 | 38 | it('should be able to specify a custom address', () => { 39 | const session = new Session({ address: 'localhost:14444' }) 40 | assert.equal(session.address, 'localhost:14444') 41 | }) 42 | 43 | it('should be able to specify a custom request timeout', () => { 44 | const session = new Session({ requestTimeoutInMillis: 1000 }) 45 | assert.equal(session.options.requestTimeoutInMillis, 1000) 46 | }) 47 | 48 | it ('should be possible to specify a custom scope', () => { 49 | const session = new Session({ scope: 'test' }) 50 | assert.equal(session.options.scope, 'test') 51 | assert.equal(session.scope, 'test') 52 | }) 53 | 54 | it ('should be possible to specify a custom channel options', () => { 55 | const session = new Session({ channelOptions: {'grpc.max_receive_message_length': 1024 * 1024 * 1024 }}) 56 | assert.deepEqual(session.options.channelOptions, {'grpc.max_receive_message_length': 1024 * 1024 * 1024 }) 57 | }) 58 | 59 | it('should be able to specify custom call options', () => { 60 | const fn = function () { Date.now() } 61 | const session = new Session({ callOptions: fn}) 62 | assert.equal(session.options.callOptions, fn) 63 | }) 64 | }) 65 | }) 66 | 67 | describe('Session IT Test Suite', () => { 68 | describe('A Session', () => { 69 | it('should not allow invalid addresses', () => { 70 | assert.throws(() => new Session({address: 'localhost'})) 71 | assert.throws(() => new Session({address: 'localhost:801a'})) 72 | assert.throws(() => new Session({address: 'localhost:800000'})) 73 | assert.throws(() => new Session({address: 'localhost:8000:'})) 74 | assert.throws(() => new Session({address: 'localhost:8000:test'})) 75 | assert.throws(() => new Session({address: 'coherence'})) 76 | assert.throws(() => new Session({address: 'coherence:'})) 77 | assert.throws(() => new Session({address: 'coherence:/'})) 78 | assert.throws(() => new Session({address: 'coherence://'})) 79 | assert.throws(() => new Session({address: 'coherence://localhost'})) 80 | // assert.throws(() => new Session({address: 'coherence:localhost:8080:'})) 81 | // assert.throws(() => new Session({address: 'coherence:localhost:'})) 82 | }) 83 | 84 | it('should allow valid addresses', () => { 85 | assert.doesNotThrow(() => new Session({address: 'localhost:8080'})) 86 | assert.doesNotThrow(() => new Session({address: 'coherence:///localhost'})) 87 | assert.doesNotThrow(() => new Session({address: 'coherence:///localhost:8080'})) 88 | assert.doesNotThrow(() => new Session({address: 'coherence:///localhost:test'})) 89 | assert.doesNotThrow(() => new Session({address: 'coherence:///localhost:8080:test'})) 90 | }) 91 | 92 | it('should not have active sessions upon creation', async () => { 93 | const sess = new Session() 94 | 95 | assert.equal(sess.activeCacheCount, 0) 96 | assert.equal(sess.activeCaches.length, 0) 97 | 98 | await sess.close() 99 | }) 100 | 101 | it('should have active sessions after getCache() is called', async () => { 102 | const sess = new Session() 103 | 104 | sess.getCache('sess-cache') 105 | assert.equal(sess.activeCacheCount, 1) 106 | assert.equal(sess.activeCaches[0].name, 'sess-cache') 107 | 108 | await sess.close() 109 | }) 110 | 111 | it('should return the same cache instance upon multiple getCache() invocations for the same cache', async () => { 112 | const sess = new Session() 113 | 114 | const cache1 = sess.getCache('sess-cache') 115 | const cache2 = sess.getCache('sess-cache') 116 | assert.equal(sess.activeCacheCount, 1) 117 | assert.equal(sess.activeCaches[0].name, 'sess-cache') 118 | assert.equal(cache1, cache2) 119 | assert.deepEqual(cache1, cache2) 120 | 121 | await sess.close() 122 | }) 123 | 124 | it('should return the same cache instance upon multiple getMap() invocations for the same map', async () => { 125 | const sess = new Session() 126 | 127 | const map1 = sess.getMap('sess-map') 128 | const map2 = sess.getMap('sess-map') 129 | assert.equal(sess.activeCacheCount, 1) 130 | assert.equal(sess.activeCaches[0].name, 'sess-map') 131 | assert.equal(map1, map2) 132 | assert.deepEqual(map1, map2) 133 | 134 | await sess.close() 135 | }) 136 | 137 | it('should return different cache instances for differing getCache() invocations', async () => { 138 | const sess = new Session() 139 | 140 | const cache1 = sess.getCache('sess-cache') 141 | const cache2 = sess.getCache('sess-cache2') 142 | assert.equal(sess.activeCacheCount, 2) 143 | assert.deepEqual(sess.activeCacheNames, new Set(['sess-cache', 'sess-cache2'])) 144 | assert.notEqual(cache1, cache2) 145 | assert.notDeepEqual(cache1, cache2) 146 | 147 | await sess.close() 148 | }) 149 | 150 | it('should return different cache instances for differing getMap() invocations', async () => { 151 | const sess = new Session() 152 | 153 | const map1 = sess.getCache('sess-map') 154 | const map2 = sess.getCache('sess-map2') 155 | assert.equal(sess.activeCacheCount, 2) 156 | assert.deepEqual(sess.activeCacheNames, new Set(['sess-map', 'sess-map2'])) 157 | assert.notEqual(map1, map2) 158 | assert.notDeepEqual(map1, map2) 159 | 160 | await sess.close() 161 | }) 162 | 163 | it('should getCache() should return the same instance as getMap() for the same name', async () => { 164 | const sess = new Session() 165 | 166 | const cache = sess.getCache('sess-test') 167 | const map = sess.getMap('sess-test') 168 | 169 | assert.equal(cache, map) 170 | 171 | await sess.close() 172 | }) 173 | 174 | it('should release active caches when closed', async () => { 175 | const sess = new Session() 176 | 177 | const cache1 = sess.getCache('sess-cache-1') 178 | const cache2 = sess.getCache('sess-cache-2') 179 | 180 | assert.equal(sess.activeCacheCount, 2) 181 | assert.deepEqual(sess.activeCacheNames, new Set(['sess-cache-1', 'sess-cache-2'])) 182 | 183 | assert.notDeepEqual(cache1, cache2) 184 | await sess.close() 185 | await sess.waitUntilClosed() 186 | 187 | assert.equal(sess.activeCacheCount, 0) 188 | assert.deepEqual(sess.activeCacheNames, new Set([])) 189 | 190 | assert.equal(cache1.active, false) 191 | assert.equal(cache2.active, false) 192 | }) 193 | 194 | it('should not maintain references to explicitly released caches', async () => { 195 | const sess = new Session() 196 | 197 | const cache1 = sess.getCache('sess-test-cache-1') 198 | const cache2 = sess.getCache('sess-test-cache-2') 199 | 200 | assert.equal(sess.activeCacheCount, 2) 201 | assert.deepEqual(sess.activeCacheNames, new Set(['sess-test-cache-1', 'sess-test-cache-2'])) 202 | 203 | assert.notDeepEqual(cache1, cache2) 204 | const prom = new Promise((resolve) => { 205 | cache1.on(event.MapLifecycleEvent.RELEASED, (cacheName) => { 206 | if (cacheName === 'sess-test-cache-1') { 207 | resolve() 208 | } 209 | }) 210 | }) 211 | await cache1.release() 212 | await prom 213 | 214 | assert.equal(sess.activeCacheCount, 1) 215 | assert.deepEqual(sess.activeCacheNames, new Set(['sess-test-cache-2'])) 216 | 217 | assert.equal(cache1.active, false) 218 | assert.equal(cache2.active, true) 219 | 220 | await sess.close() 221 | await sess.waitUntilClosed() 222 | 223 | assert.equal(sess.activeCacheCount, 0) 224 | assert.deepEqual(sess.activeCacheNames, new Set([])) 225 | 226 | assert.equal(cache1.active, false) 227 | assert.equal(cache2.active, false) 228 | }) 229 | }) 230 | 231 | describe('The session lifecycle', function () { 232 | const CACHE_NAME = 'lifecycle-cache' 233 | const CACHE2_NAME = 'lifecycle-cache2' 234 | this.timeout(10000) 235 | 236 | it('should trigger a \'released\' event for each active cache', async () => { 237 | const cache2Name = CACHE_NAME + '2' 238 | const sess = new Session() 239 | const cache = sess.getCache(CACHE_NAME) 240 | const cache2 = sess.getCache(cache2Name) 241 | 242 | let destroyed1 = false 243 | const prom1 = new Promise((resolve, reject) => { 244 | cache.on(event.MapLifecycleEvent.RELEASED, cacheName => { 245 | if (cacheName === CACHE_NAME) { 246 | resolve() 247 | } 248 | }) 249 | cache.on(event.MapLifecycleEvent.DESTROYED, cacheName => { 250 | destroyed1 = true 251 | reject('Session close incorrectly triggered cache \'destroyed\' event for cache ' + cacheName) 252 | }) 253 | }) 254 | 255 | let destroyed2 = false 256 | const prom2 = new Promise((resolve, reject) => { 257 | cache2.on(event.MapLifecycleEvent.RELEASED, cacheName => { 258 | if (cacheName === CACHE2_NAME) { 259 | resolve() 260 | } 261 | }) 262 | cache2.on(event.MapLifecycleEvent.DESTROYED, cacheName => { 263 | destroyed2 = true 264 | reject('Session close incorrectly triggered cache \'destroyed\' event for cache ' + cacheName) 265 | }) 266 | }) 267 | 268 | // trigger the events 269 | sess.close().then(() => sess.waitUntilClosed()) 270 | 271 | await prom1 272 | await new Promise(r => setTimeout(r, 1000)) // wait to make sure the destroyed event isn't triggered 273 | if (destroyed1) { 274 | assert.fail('Cache1 incorrectly emitted cache a \'destroyed\' event') 275 | } 276 | 277 | await prom2 278 | await new Promise(r => setTimeout(r, 1000)) // wait to make sure the destroyed event isn't triggered 279 | if (destroyed2) { 280 | assert.fail('Cache2 incorrectly emitted cache a \'destroyed\' event') 281 | } 282 | }) 283 | }) 284 | }) 285 | }) 286 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, 2023 Oracle and/or its affiliates. 3 | * 4 | * Licensed under the Universal Permissive License v 1.0 as shown at 5 | * https://oss.oracle.com/licenses/upl. 6 | */ 7 | const Decimal = require("decimal.js"); 8 | const assert = require('assert').strict 9 | 10 | module.exports = { 11 | checkNumericResult: function (result, expected) { 12 | if (result instanceof Decimal) { 13 | assert.deepStrictEqual(result, new Decimal(expected)) 14 | } else if (typeof(result) === 'bigint') { 15 | assert.deepStrictEqual(result, BigInt(expected)) 16 | } else { 17 | assert(false, "Unexpected result type [" + typeof(result) + "]") 18 | } 19 | }, 20 | 21 | compareEntries: async function (control /* array of array */, test /* Iterable */) { 22 | const testLen = Array.isArray(test) ? test.length : await test.size 23 | assert.equal(testLen, control.length, 'Incorrect number of entries returned') 24 | for (const entry of control) { 25 | let foundKey = false 26 | let foundVal = false 27 | const controlKey = entry[0] 28 | const controlVal = entry[1] 29 | for await (const testEntry of test) { 30 | let testKey 31 | let testVal 32 | 33 | if (testEntry.key) { 34 | testKey = testEntry.key 35 | testVal = testEntry.value 36 | } else if (Array.isArray(testEntry)) { 37 | testKey = testEntry[0] 38 | testVal = testEntry[1] 39 | } else { 40 | testKey = testEntry['key'] 41 | testVal = testEntry['value'] 42 | } 43 | 44 | try { 45 | assert.deepEqual(testKey, controlKey) 46 | foundKey = true 47 | } catch (error) { 48 | // ignored 49 | } 50 | 51 | try { 52 | assert.deepEqual(testVal, controlVal) 53 | foundVal = true 54 | } catch (error) { 55 | // ignored 56 | } 57 | 58 | if (foundKey) { 59 | break 60 | } 61 | } 62 | 63 | if (!foundKey) { 64 | console.log('Unable to find key \'' + JSON.stringify(controlKey) + '\' in entries sent from server: ') 65 | for await (const e of test) { 66 | let testKey 67 | 68 | if (e.key) { 69 | testKey = e.key 70 | } else if (Array.isArray(e)) { 71 | testKey = e[0] 72 | } else { 73 | testKey = e['key'] 74 | } 75 | console.log('\t ' + JSON.stringify(testKey)) 76 | } 77 | } 78 | if (!foundVal) { 79 | console.log('Unexpected value associated with key \'' + JSON.stringify(controlKey) + '\'. Expected value \'' + JSON.stringify(controlVal) + '\' in entry sent from server: ') 80 | for await (const e of test) { 81 | let testKey 82 | let testVal 83 | 84 | if (e.key) { 85 | testKey = e.key 86 | testVal = e.value 87 | } else if (Array.isArray(e)) { 88 | testKey = e[0] 89 | testVal = e[1] 90 | } else { 91 | testKey = e['key'] 92 | testVal = e['value'] 93 | } 94 | console.log('\t ' + JSON.stringify(testKey) + ', ' + JSON.stringify(testVal)) 95 | } 96 | } 97 | 98 | if (!foundKey || !foundVal) { 99 | assert.fail() 100 | } 101 | } 102 | }, 103 | 104 | compareElements: async function (control /* array */, test /* set */) { 105 | const testLen = Array.isArray(test) ? test.length : await test.size 106 | assert.equal(testLen, control.length, 'Incorrect number of elements returned') 107 | for (const element of control) { 108 | let found = false 109 | for await (const testElement of test) { 110 | try { 111 | assert.deepEqual(testElement, element) 112 | found = true 113 | } catch (error) { 114 | // ignored 115 | } 116 | } 117 | if (!found) { 118 | console.log('Unexpected or missing value in provided set: \'' + JSON.stringify(test) + '\', expected \'' + JSON.stringify(control) + '\'') 119 | assert.fail() 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tsconfig": "src/tsconfig.json", 3 | "entryPoints": [ 4 | "./src/index.ts" 5 | ], 6 | "entryPointStrategy": "resolve", 7 | "out": "docs", 8 | "excludePrivate": true, 9 | "name": "JavaScript Client API Reference for Oracle Coherence", 10 | "includeVersion": true, 11 | "readme": "README.md", 12 | "hideGenerator": true, 13 | "excludeExternals": false, 14 | "disableSources": true, 15 | "exclude": [ 16 | "**/grpc/**", 17 | "**/node_modules/**" 18 | ] 19 | } 20 | 21 | --------------------------------------------------------------------------------