Coherence JavaScript Client
Coherence JavaScript Client allows Node applications to act as
cache clients to a Coherence Cluster using Google's gRPC framework for
the network transport.
Features
- Familiar
Map
-like interface for manipulating entries - Cluster-side querying and aggregation of map entries
- Cluster-side manipulation of map entries using
EntryProcessors
- Registration of listeners to be notified of map mutations
Requirements
- Coherence CE 22.06 or later (or equivalent non-open source editions) with a configured gRPC Proxy
- Node 14
- NPM 8.x
Usage
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.
docker run -d -p 1408:1408 oraclecoherence/coherence-ce:22.06
For more details on the image, see the documentation.
Declare Your Dependency
To use the Coherence gRPC JavaScript Client, simply declare it as a dependency in your
project's package.json
:
...
"dependencies": {
"@oracle/coherence": "^1.1",
},
...
Examples
NOTE: The following examples assume the Coherence container is running locally.
You can start a container by running npm run coh-up
.
Establishing a Session
The Coherence uses the concept of a Session
to manage a set of related Coherence resources,
such as maps and/or caches. When using the Coherence JavaScript Client, a Session
connects to a specific
gRPC endpoint and uses a specific serialization format to marshal requests and responses.
This means that different sessions using different serializers may connect to the same server endpoint. Typically,
for efficiency the client and server would be configured to use matching serialization formats to avoid
deserialization of data on the server, but this does not have to be the case. If the server is using a different
serializer for the server-side caches, it must be able to deserialize the client's requests, so there must be
a serializer configured on the server to match that used by the client.
NOTE: Currently, the Coherence JavaScript client only supports JSON serialization
A Session
is constructed using an Options
instance, or a generic object with the same keys and values.
The currently supported properties are:
address
- the address of the Coherence gRPC proxy. This defaults to localhost:1408
.requestTimeoutInMillis
- the gRPC request timeout in milliseconds. This defaults to 60000
.callOptions
- per-request gRPC call options.tls
- options related to the configuration of TLS.
enabled
- determines if TLS is enabled or not. This defaults to false
.caCertPath
- the path to the CA certificate.clientCertPath
- the path to the client certificate.clientKeyPath
- the path to the client certificate key.
const { Session } = require('@oracle/coherence')
let session = new Session()
This is the simplest invocation which assumes the following defaults:
address
is localhost:1408
requestTimeoutInMillis
is 60000
tls
is disabled
To use values other than the default, create a new Options
instance, configure as desired,
and pass it to the constructor of the Session
:
const { Session, Options } = require('@oracle/coherence')
const opts = new Options()
opts.address = 'example.com:4444'
let session = new Session(opts)
or instead of an Options
instance, using a generic JavaScript object:
const { Session } = require('@oracle/coherence')
const opts = new Options({address: 'example.com:4444'})
let session = new Session(opts)
It's also possible to control the default address the session will bind to by providing
an address via the grpc_proxy_address
environment variable. The format of the value would
be the same as if you configured it programmatically as the above example shows.
Once the session has been constructed, it will now be possible to create maps and caches.
Basic Map Operations
The map (NamedMap
) and cache (NamedCache
) implementations provide the same basic features as the Map provided
by JavaScript except for the following differences:
- key equality isn't restricted to reference equality
- insertion order is not maintained
set()
calls cannot be chained because of the asynchronous nature of the API
NOTE: The only difference between NamedCache
and NamedMap
is that the 'NamedCache' allows associating a
time-to-live
on the cache entry, while NamedMap
does not
For the following examples, let's assume that we have a Map defined in Coherence named Test
.
To get access to the map from the client:
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.
let map = session.getMap('Test')
Once we have a handle to our map, we can invoke the same basic operations as a standard JavaScript Map:
await map.size
await map.set('key1', 'value1')
await map.set('key2', 'value2')
await map.size
await map.get('key1')
await map.has('key2')
await map.has('key3')
await map.keys()
await map.values()
await map.entries()
await map.forEach((value, key) => console.log(key + ': ' + value))
Querying the Map
Coherence provides a rich set of primitives that allow developers to create advanced queries against
a set of entries returning only those keys and/or values matching the specified criteria.
See the documentation for details
on the Filters provided by this client.
Let's assume we have a NamedMap
in which we're storing string
keys and some objects with the structure of:
{
name: <string>
age: <number>
hobbies: [] // of string
}
First, let's insert a few objects:
await map.set('0001', {name: "Bill Smith", age: 38, hobbies: ["gardening", "painting"]})
await map.set('0002', {name: "Fred Jones", age: 56, hobbies: ["racing", "golf"]})
await map.set('0003', {name: "Jane Doe", age: 48, hobbies: ["gardening", "photography"]})
Using a filter, we can limit the result set returned by the map:
const { Filters } = require('@oracle/coherence')
await map.entries(Filters.greater('age', 40))
await map.keys(Filters.arrayContains('hobbies', 'gardening'))
await map.values(Filters.not(Filters.arrayContains('hobbies', 'gardening')))
Aggregation
Coherence provides developers with the ability to process some subset of the entries in a map,
resulting in an aggregated result. See the documentation for aggregators provided by this client.
Assume the same set of keys and values are present from the filtering example above:
const { Aggregators, Filters } = require('@oracle/coherence')
await map.aggregate(Aggregators.average('age'))
await map.aggregate(Aggregators.sum('age'))
await map.aggregate(Filters.greater('age', 40), Aggregators.count())
Entry Processing
An entry processor allows mutation of map entries in-place within the cluster instead of bringing the entire object
to the client, updating, and pushing the value back. See the documentation for the processors provided by this client.
Assume the same set of keys and values are present from the filtering and aggregation examples:
const { Filters, Processors } = require('@oracle/coherence')
await map.invoke('0001', Processors.extract('age'))
await map.invokeAll(Processors.extract('age'))
await map.invokeAll(Filters.greater('age', 40), Processors.extract('age'))
await map.invokeAll(Filters.greater('age', 40), Processors.increment('age', 1))
await map.invoke('0001', Processors.update('age', 100))
await map.get('0001')
Events
Coherence provides the ability to subscribe to notifications pertaining to a particular map/cache.
Registration works similarly to event registration with Node, with some key differences. In addition
to listening for specific events, it is possible to listen to events for changes made to a specific key, or using
a Filter, it's possible to limit the events raised to be for a subset of the map entries.
Now, let's register a listener:
import { event } from '@oracle/coherence'
const MapEventType = event.MapEventType
const MapListener = event.MapListener
const handler = (event: MapEvent) => {
console.log('Event: ' + event.description
+ ', Key: ' + JSON.stringify(event.key)
+ ', New Value: ' + JSON.stringify(event.newValue)
+ ', Old Value: ' + JSON.stringify(event.oldValue))
}
const listener = new MapListener()
.on(MapEventType.INSERT, handler)
.on(MapEventType.UPDATE, handler)
.on(MapEventType.DELETE, handler)
await map.addMapListener(listener)
await map.set('a', 'b')
await map.set('a', 'c')
await map.delete('a')
await map.removeMapListener(listener)
await map.addMapListener(listener, '0001')
await map.update('0002', '0002')
await map.invoke('0001', Processors.increment('age', 1))
await map.delete('0001')
await map.removeMapListener(listener, '0001')
const filter = Filters.event(Filters.greater('age', 40), filter.MapEventFilter.UPDATED)
await map.addMapListener(listener, filter)
await map.invokeAll(Processors.increment('age', 1));
await map.removeMapListener(listener, filter)
Cut/Paste Example
Here's an example that can be pasted into a new node project that is using this library:
const { Session } = require('@oracle/coherence')
let session = new Session()
let map = session.getMap('Test')
setImmediate(async () => {
console.log("Map size is " + (await map.size))
console.log("Inserting entry (key=1, value=One)")
await map.set(1, "One")
console.log("Inserting entry (key=2, value=Two)")
await map.set(2, "Two")
let entries = await map.entries();
console.log("All entries")
for await (const entry of entries) {
console.log(entry.key + '=' + entry.value)
}
console.log("Key 1 is " + (await map.get(1)))
console.log("Key 2 is " + (await map.get(2)))
console.log("Deleting entry (key=1)")
await map.delete(1)
console.log("Map size is " + (await map.size))
await session.close()
})
When run, it produces:
Map size is 0
Inserting entry (key=1, value=One)
Map entry is One
Deleting entry (key=1)
Map size is 0
References