Getting Started with the Hazelcast Node.js Client

What You’ll Learn

This tutorial will get you started with the Hazelcast Node.js client and manipulate a map.

Before you Begin

Start a Hazelcast Viridian Cloud Cluster

  1. Sign up for a Hazelcast Viridian Cloud account (free trial is available).

  2. Log in to your Hazelcast Viridian Cloud account and start your trial by filling in the welcome questionnaire.

  3. A Viridian cluster will be created automatically when you start your trial.

  4. Press the Connect Cluster dialog and switch over to the Advanced setup tab for connection information needed below.

  5. From the Advanced setup tab, download the keystore files and take note of your Cluster ID, Discovery Token and Password as you will need them later.

Setup a Hazelcast Client

Create a new folder and navigate to it:

mkdir hazelcast-nodejs-example
cd hazelcast-nodejs-example

Initialize a new npm package and choose default values when asked:

npm init

Install Hazelcast Node.js client’s latest version:

npm install --save hazelcast-client

Extract the keystore files you downloaded from Viridian into this directory. The files you need for this tutorial are:

ca.pem
cert.pem
key.pem

Understanding the Node.js Client

The following section creates and starts a Hazelcast client with default configuration, connects to your Viridian cluster before shutting the client down at the end.

Create a JavaScript file named “index.js” and put the following code inside it:

'use strict';

const { Client } = require('hazelcast-client');
const fs = require('fs');
const path = require('path');
const process = require('process');
const sprintf= require('sprintf-js').sprintf;

(async () => {

    const client = await Client.newHazelcastClient({
        clusterName: '<YOUR_CLUSTER_ID>',

        // Connection details for cluster
        network: {
            hazelcastCloud: {
                discoveryToken: '<YOUR_DISCOVERY_TOKEN>',
            },

            ssl: {
                enabled: true,
                sslOptions: {
                    ca: [fs.readFileSync(path.resolve(path.join(__dirname, 'ca.pem')))],
                    cert: [fs.readFileSync(path.resolve(path.join(__dirname, 'cert.pem')))],
                    key: [fs.readFileSync(path.resolve(path.join(__dirname, 'key.pem')))],
                    passphrase: '<YOUR_CERTIFICATE_PASSWORD>',
                    checkServerIdentity: () => null
                },
            },
        },

        // Other environment propreties
        properties: {
            'hazelcast.logging.level': 'WARN' // this property value is case-insensitive
        },
    });

    process.stdout.write('Welcome to your Hazelcast Viridian Cluster!');

    await client.shutdown();

})().catch(err => {
    process.stderr.write(`An error occured: ${err}\n`);
});

To run this Node.js script, use the following command:

node index.js

The majority of the client methods return promises using the async/await syntax, but you can use the regular then / catch syntax, too.

Understanding the Hazelcast SQL API

Hazelcast SQL API is a Calcite SQL based interface to allow you to interact with Hazelcast much like any other datastore.

In the following example, we will create a map and insert into it, entries where the keys are ids and the values are defined as an object representing a city.

SSL certificate files are available from the Python client download available from Viridian.
'use strict';

const { Client } = require('hazelcast-client');
const fs = require('fs');
const path = require('path');
const process = require('process');
const sprintf= require('sprintf-js').sprintf;

class CityDTO {
    constructor(city, country, population) {
        this.city = city;
        this.country = country;
        this.population = population;
    }
}

class CitySerializer {

    getClass() {
        return CityDTO;
    }

    getTypeName() {
        return 'CityDTO'
    }

    write(writer, cityDTO) {
        writer.writeString('city', cityDTO.city);
        writer.writeString('country', cityDTO.country);
        writer.writeInt32('population', cityDTO.population);
    }

    read(reader) {
        const city = reader.readString('city');
        const country = reader.readString('country');
        const population = reader.readInt32('population');

        return new CityDTO(city, country, population);
    }
}

async function createMapping(client) {
    process.stdout.write('Creating the mapping...');

    // Mapping is required for your distributed map to be queried over SQL.
    // See: https://docs.hazelcast.com/hazelcast/latest/sql/mapping-to-maps
    const mappingQuery = `
        CREATE OR REPLACE MAPPING
        cities (
            __key INT,
            country VARCHAR,
            city VARCHAR,
            population INT) TYPE IMAP
        OPTIONS (
            'keyFormat' = 'int',
            'valueFormat' = 'compact',
            'valueCompactTypeName' = 'CityDTO')
    `;

    await client.getSql().execute(mappingQuery);
    process.stdout.write('OK.\n');
}

async function populateCities(client) {
    process.stdout.write('Inserting data...');

    // Mapping is required for your distributed map to be queried over SQL.
    // See: https://docs.hazelcast.com/hazelcast/latest/sql/mapping-to-maps
    const insertQuery = `
        INSERT INTO cities
        (__key, city, country, population) VALUES
        (1, 'London', 'United Kingdom', 9540576),
        (2, 'Manchester', 'United Kingdom', 2770434),
        (3, 'New York', 'United States', 19223191),
        (4, 'Los Angeles', 'United States', 3985520),
        (5, 'Istanbul', 'Türkiye', 15636243),
        (6, 'Ankara', 'Türkiye', 5309690),
        (7, 'Sao Paulo ', 'Brazil', 22429800)
    `;

    try {
        await client.getSql().execute('DELETE from cities');
        await client.getSql().execute(insertQuery);

        process.stdout.write('OK.\n');
    } catch (error) {
        process.stderr.write('FAILED.\n', error)
    }
}

async function fetchCities(client) {
    process.stdout.write('Fetching cities...');

    const sqlResultAll = await client.sqlService.execute('SELECT __key, this FROM cities', [], { returnRawResult: true });

    process.stdout.write('OK.\n');
    process.stdout.write('--Results of SELECT __key, this FROM cities\n');
    process.stdout.write(sprintf('| %4s | %20s | %20s | %15s |\n', 'id', 'country', 'city', 'population'));

    // NodeJS client does lazy deserialization. In order to update schema table on the client,
    // it's required to get a map value.
    const cities = await client.getMap('cities');
    await cities.get(1);

    for await (const row of sqlResultAll) {
        const id = row.getObject('__key');
        const cityDTO = row.getObject('this');
        process.stdout.write(sprintf('| %4d | %20s | %20s | %15d |\n', id, cityDTO.country, cityDTO.city, cityDTO.population));
    }

    process.stdout.write('\n!! Hint !! You can execute your SQL queries on your Viridian cluster over the management center. \n 1. Go to "Management Center" of your Hazelcast Viridian cluster. \n 2. Open the "SQL Browser". \n 3. Try to execute "SELECT * FROM cities".\n');
}

///////////////////////////////////////////////////////

(async () => {

    const client = await Client.newHazelcastClient({
        clusterName: '<YOUR_CLUSTER_ID>',

        // Connection details for cluster
        network: {
            hazelcastCloud: {
                discoveryToken: '<YOUR_DISCOVERY_TOKEN>',
            },

            ssl: {
                enabled: true,
                sslOptions: {
                    ca: [fs.readFileSync(path.resolve(path.join(__dirname, 'ca.pem')))],
                    cert: [fs.readFileSync(path.resolve(path.join(__dirname, 'cert.pem')))],
                    key: [fs.readFileSync(path.resolve(path.join(__dirname, 'key.pem')))],
                    passphrase: '<YOUR_CERTIFICATE_PASSWORD>',
                    checkServerIdentity: () => null
                },
            },
        },

        // Register Compact Serializers
        serialization: {
            compact: {
                serializers: [new CitySerializer()],
            },
            defaultNumberType:"integer",
        },

        // Other environment propreties
        properties: {
            'hazelcast.logging.level': 'WARN' // this property value is case-insensitive
        },
    });

    await createMapping(client);
    await populateCities(client);
    await fetchCities(client);

    await client.shutdown();

})().catch(err => {
    process.stderr.write(`An error occured: ${err}\n`);
});

The output of this code is given below:

Connection Successful!
Creating the mapping...OK.
Inserting data...OK.
Fetching cities...OK.
--Results of 'SELECT __key, this FROM cities'
|   id | country              | city                 | population      |
|    2 | United Kingdom       | Manchester           | 2770434         |
|    6 | Türkiye              | Ankara               | 5309690         |
|    1 | United Kingdom       | London               | 9540576         |
|    7 | Brazil               | Sao Paulo            | 22429800        |
|    4 | United States        | Los Angeles          | 3985520         |
|    5 | Türkiye              | Istanbul             | 15636243        |
|    3 | United States        | New York             | 19223191        |
Ordering of the keys is NOT enforced and results may NOT correspond to insertion order.

Understanding the Hazelcast Map API

A Hazelcast Map is a distributed key-value store, similar to Node map. You can store key-value pairs in a Hazelcast Map.

In the following example, we will work with map entries where the keys are ids and the values are defined as a string representing a city name.

'use strict';

const { Client } = require('hazelcast-client');
const fs = require('fs');
const path = require('path');
const process = require('process');
const sprintf= require('sprintf-js').sprintf;

####################################

(async () => {

    const client = await Client.newHazelcastClient({
        clusterName: '<YOUR_CLUSTER_ID>',

        // Connection details for cluster
        network: {
            hazelcastCloud: {
                discoveryToken: '<YOUR_DISCOVERY_TOKEN>',
            },

            ssl: {
                enabled: true,
                sslOptions: {
                    ca: [fs.readFileSync(path.resolve(path.join(__dirname, 'ca.pem')))],
                    cert: [fs.readFileSync(path.resolve(path.join(__dirname, 'cert.pem')))],
                    key: [fs.readFileSync(path.resolve(path.join(__dirname, 'key.pem')))],
                    passphrase: '<YOUR_CERTIFICATE_PASSWORD>',
                    checkServerIdentity: () => null
                },
            },
        },

        // Register Compact Serializers
        serialization: {
            compact: {
                serializers: [new CitySerializer()],
            },
            defaultNumberType:"integer",
        },

        // Other environment propreties
        properties: {
            'hazelcast.logging.level': 'WARN' // this property value is case-insensitive
        },
    });

    //
    var citiesMap = await client.getMap('cities');

    // Clear the map
    await citiesMap.clear();

    // Add some data
    await citiesMap.put(1, 'London');
    await citiesMap.put(2, 'New York');
    await citiesMap.put(3, 'Tokyo');

    // Output the data
    const entries = await citiesMap.entrySet();

    for (const [key, value] of entries) {
        process.stdout.write(`${key} -> ${value}\n`);
    }

    await client.shutdown();

})().catch(err => {
    process.stderr.write(`An error occured: ${err}\n`);
});

Following line returns a map proxy object for the cities map:

var citiesMap = await client.getMap('cities');

If cities doesn’t exist, it will be automatically created. All the clients connected to the same cluster will have access to the same map.

With these lines, client adds data to the cities map. The first parameter is the key of the entry, the second one is the value.

await citiesMap.put(1, 'London');
await citiesMap.put(2, 'New York');
await citiesMap.put(3, 'Tokyo');

Then, we get the data using the entrySet() method and iterate over the results.

const entries = await citiesMap.entrySet();

for (const [key, value] of entries) {
    process.stdout.write(`${key} -> ${value}\n`);
}

Finally, client.shutdown() terminates our client and release its resources.

The output of this code is given below:

2 -> New York
1 -> London
3 -> Tokyo
Ordering of the keys is NOT enforced and results may NOT correspond to entry order.

Adding a Listener to the Map

You can add an entry listener using the addEntryListener() method available on the map proxy object. This will allow you to listen to certain events that happen in the map across the cluster.

The first argument to the addEntryListener() method is an object that is used to define listeners. In this example, we register listeners for the added, removed and updated events.

The second argument to the addEntryListener() method is includeValue. This boolean parameter, if set to true, ensures the entry event contains the entry value.

This enables your code to listen to map events of that particular map.

'use strict';

const { Client } = require('hazelcast-client');
const fs = require('fs');
const path = require('path');
const process = require('process');
const sprintf= require('sprintf-js').sprintf;

####################################

(async () => {

    const client = await Client.newHazelcastClient({
        clusterName: '<YOUR_CLUSTER_ID>',

        // Connection details for cluster
        network: {
            hazelcastCloud: {
                discoveryToken: '<YOUR_DISCOVERY_TOKEN>',
            },

            ssl: {
                enabled: true,
                sslOptions: {
                    ca: [fs.readFileSync(path.resolve(path.join(__dirname, 'ca.pem')))],
                    cert: [fs.readFileSync(path.resolve(path.join(__dirname, 'cert.pem')))],
                    key: [fs.readFileSync(path.resolve(path.join(__dirname, 'key.pem')))],
                    passphrase: '<YOUR_CERTIFICATE_PASSWORD>',
                    checkServerIdentity: () => null
                },
            },
        },

        // Register Compact Serializers
        serialization: {
            compact: {
                serializers: [new CitySerializer()],
            },
            defaultNumberType:"integer",
        },

        // Other environment propreties
        properties: {
            'hazelcast.logging.level': 'WARN' // this property value is case-insensitive
        },
    });

    //
    var citiesMap = await client.getMap('cities');

    citiesMap.addEntryListener({
        added: (event) => {
            process.stdout.write(`Entry added with key: ${event.key}, value: ${event.value}\n`)
        },
        removed: (event) => {
            process.stdout.write(`Entry removed with key: ${event.key}\n`);
        },
        updated: (event) => {
            process.stdout.write(`Entry updated with key: ${event.key}, old value: ${event.oldValue}, new value: ${event.value}\n`)
        },
    }, undefined, true);

    // Clear the map
    await citiesMap.clear();

    // Add some data
    await citiesMap.put(1, 'London');
    await citiesMap.put(2, 'New York');
    await citiesMap.put(3, 'Tokyo');

    await citiesMap.remove(1);
    await citiesMap.replace(2, 'Paris');

    // Output the data
    const entries = await citiesMap.entrySet();

    for (const [key, value] of entries) {
        process.stdout.write(`${key} -> ${value}\n`);
    }

    await client.shutdown();

})().catch(err => {
    process.stderr.write(`An error occured: ${err}\n`);
});

First, the map is cleared, which will trigger removed events if there are some entries in the map. Then, entries are added, and they are logged. After that, we remove one of the entries and update the other one. Then, we log the entries again.

The output is as follows.

Entry added with key: 1, value: London
Entry added with key: 2, value: New York
Entry added with key: 3, value: Tokyo
Entry removed with key: 1
Entry updated with key: 2, old value: New York, new value: Paris
2 -> Paris
3 -> Tokyo

The value of the first entry becomes "null" since it is removed.

Summary

In this tutorial, you learned how to get started with the Hazelcast Node.js Client, connect to a Viridian instance and put data into a distributed map.

See Also

There are a lot of things that you can do with the Node.js Client. For more, such as how you can query a map with predicates and SQL, check out our Node.js Client repository and our Node.js API documentation to better understand what is possible.

If you have any questions, suggestions, or feedback please do not hesitate to reach out to us via Hazelcast Community Slack. Also, please take a look at the issue list if you would like to contribute to the client.