diff --git a/.gitignore b/.gitignore index 3b40bac..0f29139 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,6 @@ infinispan-server-* nohup.out /build -/server/infinispan-server/ +/server/ batch CLAUDE.md diff --git a/documentation/asciidoc/topics/code_examples/admin-operations.js b/documentation/asciidoc/topics/code_examples/admin-operations.js new file mode 100644 index 0000000..2d018b6 --- /dev/null +++ b/documentation/asciidoc/topics/code_examples/admin-operations.js @@ -0,0 +1,67 @@ +var infinispan = require('infinispan'); + +var connected = infinispan.client( + {port: 11222, host: '127.0.0.1'}, + { + authentication: { + enabled: true, + saslMechanism: 'SCRAM-SHA-256', + userName: 'admin', + password: 'changeme' + } + } +); + +connected.then(function (client) { + + // Create a cache with a JSON configuration. + var cacheConfig = JSON.stringify({ + 'local-cache': { + encoding: { 'media-type': 'text/plain' } + } + }); + var create = client.admin.createCache('myNewCache', cacheConfig); + + // Get or create a cache (creates if it does not exist). + var getOrCreate = create.then(function() { + return client.admin.getOrCreateCache('myNewCache', cacheConfig); + }); + + // List all cache names. + var listNames = getOrCreate.then(function() { + return client.admin.cacheNames(); + }); + listNames.then(function(names) { + console.log('Cache names: ' + JSON.stringify(names)); + }); + + // Remove a cache. + var remove = listNames.then(function() { + return client.admin.removeCache('myNewCache'); + }); + + // Register a Protobuf schema. + var registerSchema = remove.then(function() { + return client.admin.registerSchema('person.proto', + 'package example;\n' + + 'message Person {\n' + + ' required string name = 1;\n' + + '}\n'); + }); + + // Remove a Protobuf schema. + var removeSchema = registerSchema.then(function() { + return client.admin.removeSchema('person.proto'); + }); + + // Disconnect from {brandname} Server. + return removeSchema.then(function() { + return client.disconnect(); + }); + +}).catch(function(error) { + + // Log any errors. + console.log("Got error: " + error.message); + +}); diff --git a/documentation/asciidoc/topics/code_examples/distributed-counters.js b/documentation/asciidoc/topics/code_examples/distributed-counters.js new file mode 100644 index 0000000..3e760cd --- /dev/null +++ b/documentation/asciidoc/topics/code_examples/distributed-counters.js @@ -0,0 +1,92 @@ +var infinispan = require('infinispan'); + +var connected = infinispan.client( + {port: 11222, host: '127.0.0.1'}, + { + authentication: { + enabled: true, + saslMechanism: 'SCRAM-SHA-256', + userName: 'admin', + password: 'changeme' + } + } +); + +connected.then(function (client) { + + // Create a strong counter with an initial value of 0. + var create = client.counterCreate('my-counter', { + type: 'strong', + storage: 'PERSISTENT', + initialValue: 0 + }); + + // Get the current counter value. + var get = create.then(function() { + return client.counterGet('my-counter'); + }); + get.then(function(value) { + console.log('Counter value: ' + value); // 0 + }); + + // Add a value and return the new counter value. + var addAndGet = get.then(function() { + return client.counterAddAndGet('my-counter', 5); + }); + addAndGet.then(function(value) { + console.log('After add: ' + value); // 5 + }); + + // Set a value and return the previous counter value. + var getAndSet = addAndGet.then(function() { + return client.counterGetAndSet('my-counter', 10); + }); + getAndSet.then(function(previous) { + console.log('Previous value: ' + previous); // 5 + }); + + // Compare and swap: update only if the current value matches. + var cas = getAndSet.then(function() { + return client.counterCompareAndSwap('my-counter', 10, 20); + }); + cas.then(function(value) { + console.log('After CAS: ' + value); // 20 + }); + + // Reset the counter to its initial value. + var reset = cas.then(function() { + return client.counterReset('my-counter'); + }); + + // Check if a counter is defined. + var isDefined = reset.then(function() { + return client.counterIsDefined('my-counter'); + }); + isDefined.then(function(defined) { + console.log('Is defined: ' + defined); // true + }); + + // Retrieve the counter configuration. + var getConfig = isDefined.then(function() { + return client.counterGetConfiguration('my-counter'); + }); + getConfig.then(function(config) { + console.log('Counter type: ' + config.type); // strong + }); + + // Remove the counter. + var remove = getConfig.then(function() { + return client.counterRemove('my-counter'); + }); + + // Disconnect from {brandname} Server. + return remove.then(function() { + return client.disconnect(); + }); + +}).catch(function(error) { + + // Log any errors. + console.log("Got error: " + error.message); + +}); diff --git a/documentation/asciidoc/topics/proc_installing_clients.adoc b/documentation/asciidoc/topics/proc_installing_clients.adoc index 80956db..2bede5a 100644 --- a/documentation/asciidoc/topics/proc_installing_clients.adoc +++ b/documentation/asciidoc/topics/proc_installing_clients.adoc @@ -4,12 +4,10 @@ .Prerequisites -* Node.js version `12` or `14`. +* Node.js version `22` or `24`. //Community content ifdef::community[] -* {brandname} Server 9.4.x or later. -+ -Use js-client `0.7` for {brandname} Server `8.2.x` to `9.3.x`. +* {brandname} Server 14.x or later. endif::community[] //Downstream content ifdef::downstream[] diff --git a/documentation/asciidoc/topics/ref_client_usage.adoc b/documentation/asciidoc/topics/ref_client_usage.adoc index b15b377..a4ca91b 100644 --- a/documentation/asciidoc/topics/ref_client_usage.adoc +++ b/documentation/asciidoc/topics/ref_client_usage.adoc @@ -46,6 +46,55 @@ include::code_examples/await-single-entries.js[] include::code_examples/await-multiple-entries.js[] ---- +== Performing admin operations + +Use the `admin` property on the client to manage caches and Protobuf schemas on {brandname} Server. + +Admin operations let you create and remove caches, list cache names, update configuration attributes, manage index schemas, and register or remove Protobuf schemas. + +[NOTE] +==== +Admin operations require the authenticated user to have administrative permissions on {brandname} Server. +==== + +[source,javascript,options="nowrap",subs=attributes+] +---- +include::code_examples/admin-operations.js[] +---- + +.Available admin operations +[cols="1,3",options="header"] +|=== +|Method |Description + +|`createCache(name, config, opts)` +|Creates a cache with a JSON, XML, or YAML configuration. Use `opts.template` to create from a server template. Use `opts.flags` (e.g. `'VOLATILE'`) for non-persistent caches. + +|`getOrCreateCache(name, config, opts)` +|Creates a cache if it does not already exist, otherwise returns the existing cache. + +|`removeCache(name)` +|Removes a cache and all its data. + +|`cacheNames()` +|Returns a list of all cache names on the server. + +|`updateConfigurationAttribute(name, attribute, value)` +|Updates a mutable configuration attribute on a cache (e.g. `'memory.max-count'`). + +|`reindex(name)` +|Rebuilds indexes for an indexed cache. + +|`updateIndexSchema(name)` +|Updates the index schema for an indexed cache. + +|`registerSchema(name, schema)` +|Registers or updates a Protobuf schema (`.proto` file) on the server. + +|`removeSchema(name)` +|Removes a registered Protobuf schema from the server. +|=== + == Running server-side scripts You can add custom scripts to {brandname} Server and then run them from {hr_js} clients. @@ -115,6 +164,52 @@ Use the `getWithMetadata` and `size` methods expire cache entries. include::code_examples/ephemeral-data.js[] ---- +== Using distributed counters + +Distributed counters provide cluster-wide atomic counters that you can use to coordinate state across {brandname} Server nodes. +{brandname} supports two types of counters: + +* **Strong counters** provide linearizable semantics with optional upper and lower bounds. +* **Weak counters** are more performant but offer weaker consistency guarantees. + +[source,javascript,options="nowrap",subs=attributes+] +---- +include::code_examples/distributed-counters.js[] +---- + +.Available counter operations +[cols="1,3",options="header"] +|=== +|Method |Description + +|`counterCreate(name, config)` +|Creates a counter. Config requires `type` (`'strong'` or `'weak'`), and optionally `storage`, `initialValue`, `upperBound`, `lowerBound` (strong), or `concurrencyLevel` (weak). + +|`counterGet(name)` +|Returns the current counter value, or `undefined` if the counter does not exist. + +|`counterAddAndGet(name, value)` +|Adds a value to the counter and returns the new value. + +|`counterGetAndSet(name, value)` +|Sets a value and returns the previous value. + +|`counterCompareAndSwap(name, expect, update)` +|Updates the counter to `update` only if the current value equals `expect`. Returns the value before the operation. + +|`counterReset(name)` +|Resets the counter to its initial value. + +|`counterIsDefined(name)` +|Returns `true` if the counter exists. + +|`counterGetConfiguration(name)` +|Returns the counter configuration, or `undefined` if the counter does not exist. + +|`counterRemove(name)` +|Removes the counter from the cluster. +|=== + == Working with queries Use the `query` method to perform queries on your caches. diff --git a/gen-asciidoc.sh b/gen-asciidoc.sh new file mode 100755 index 0000000..0bd8240 --- /dev/null +++ b/gen-asciidoc.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# Generates HTML documentation from AsciiDoc sources. +# Mirrors the asciidoctor-maven-plugin configuration used by +# the main Infinispan documentation build (documentation/pom.xml). + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SRC_DIR="${SCRIPT_DIR}/documentation/asciidoc" +OUT_DIR="${SCRIPT_DIR}/out/docs" + +mkdir -p "${OUT_DIR}" + +asciidoctor \ + --doctype book \ + --backend html5 \ + --safe-mode unsafe \ + -a idprefix="" \ + -a idseparator="-" \ + -a sectanchors \ + -a toc=left \ + -a toclevels=3 \ + -a numbered \ + -a icons=font \ + -a experimental \ + -a source-highlighter=highlight.js \ + -a highlightjs-theme=github \ + -a imagesdir=../../topics/images \ + -a stories=../stories \ + -a topics=../topics \ + -a community \ + -a brandname=Infinispan \ + -a fullbrandname=Infinispan \ + -a brandshortname=infinispan \ + -a hr_js="Hot Rod JS" \ + -a doc_home=https://infinispan.org/documentation/ \ + -a download_url=https://infinispan.org/download/ \ + -a node_docs=https://docs.jboss.org/infinispan/hotrod-clients/javascript/1.0/apidocs/ \ + -a server_docs=https://infinispan.org/docs/stable/titles/server/server.html \ + -a code_tutorials=https://github.com/infinispan/infinispan-simple-tutorials/ \ + -a query_docs=https://infinispan.org/docs/stable/titles/query/query.html \ + -o "${OUT_DIR}/index.html" \ + "${SRC_DIR}/titles/js_client.asciidoc" + +echo "Documentation generated: ${OUT_DIR}/index.html" diff --git a/lib/infinispan.js b/lib/infinispan.js index 89c2c68..26c34bf 100644 --- a/lib/infinispan.js +++ b/lib/infinispan.js @@ -1058,6 +1058,156 @@ */ nearCacheSize: function() { return nc ? nc.size() : 0; + }, + + /** + * Admin operations for cache lifecycle, schema management, and indexing. + * These operations use Hot Rod server tasks (exec opcode 0x2B) with + * predefined @@-prefixed task names. + * @memberof Client# + * @since 0.15 + */ + admin: { + /** + * Create a cache with the given configuration. + * + * @param {String} name Cache name. + * @param {String} config Cache configuration (XML or JSON). + * @param {Object} [opts] Optional settings. + * @param {String} [opts.template] Template name to use instead of config. + * @param {String} [opts.flags] Admin flags (e.g. 'VOLATILE' for non-persistent caches). + * @returns {Promise} A promise that completes when the cache is created. + * @memberof Client.admin# + * @since 0.15 + */ + createCache: function(name, config, opts) { + var params = {name: name}; + if (f.existy(config)) params.configuration = config; + if (f.existy(opts) && f.existy(opts.template)) params.template = opts.template; + if (f.existy(opts) && f.existy(opts.flags)) params.flags = opts.flags; + var ctx = transport.context(MEDIUM); + logger.debugf('Invoke admin.createCache(msgId=%d,name=%s)', ctx.id, name); + return future(ctx, 0x2B, p.encodeNameParams('@@cache@create', params), p.decodeValue()); + }, + /** + * Get an existing cache or create it with the given configuration. + * + * @param {String} name Cache name. + * @param {String} config Cache configuration (XML or JSON). + * @param {Object} [opts] Optional settings. + * @param {String} [opts.template] Template name to use instead of config. + * @param {String} [opts.flags] Admin flags (e.g. 'VOLATILE'). + * @returns {Promise} A promise that completes when the cache is available. + * @memberof Client.admin# + * @since 0.15 + */ + getOrCreateCache: function(name, config, opts) { + var params = {name: name}; + if (f.existy(config)) params.configuration = config; + if (f.existy(opts) && f.existy(opts.template)) params.template = opts.template; + if (f.existy(opts) && f.existy(opts.flags)) params.flags = opts.flags; + var ctx = transport.context(MEDIUM); + logger.debugf('Invoke admin.getOrCreateCache(msgId=%d,name=%s)', ctx.id, name); + return future(ctx, 0x2B, p.encodeNameParams('@@cache@getorcreate', params), p.decodeValue()); + }, + /** + * Remove a cache. + * + * @param {String} name Cache name. + * @returns {Promise} A promise that completes when the cache is removed. + * @memberof Client.admin# + * @since 0.15 + */ + removeCache: function(name) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke admin.removeCache(msgId=%d,name=%s)', ctx.id, name); + return future(ctx, 0x2B, p.encodeNameParams('@@cache@remove', {name: name}), p.decodeValue()); + }, + /** + * List all cache names. + * + * @returns {Promise} A promise that completes with the list of cache names. + * @memberof Client.admin# + * @since 0.15 + */ + cacheNames: function() { + var ctx = transport.context(SMALL); + logger.debugf('Invoke admin.cacheNames(msgId=%d)', ctx.id); + return future(ctx, 0x2B, p.encodeNameParams('@@cache@names', {}), p.decodeValue()) + .then(function(result) { return JSON.parse(result); }); + }, + /** + * Update a mutable configuration attribute on a cache. + * + * @param {String} name Cache name. + * @param {String} attribute Attribute path (e.g. 'memory.max-count'). + * @param {String} value New attribute value. + * @returns {Promise} A promise that completes when the attribute is updated. + * @memberof Client.admin# + * @since 0.15 + */ + updateConfigurationAttribute: function(name, attribute, value) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke admin.updateConfigurationAttribute(msgId=%d,name=%s,attr=%s)', ctx.id, name, attribute); + return future(ctx, 0x2B, p.encodeNameParams('@@cache@updateConfigurationAttribute', { + name: name, attribute: attribute, value: value + }), p.decodeValue()); + }, + /** + * Rebuild indexes for a cache. + * + * @param {String} name Cache name. + * @returns {Promise} A promise that completes when reindexing starts. + * @memberof Client.admin# + * @since 0.15 + */ + reindex: function(name) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke admin.reindex(msgId=%d,name=%s)', ctx.id, name); + return future(ctx, 0x2B, p.encodeNameParams('@@cache@reindex', {name: name}), p.decodeValue()); + }, + /** + * Update the index schema for a cache. + * + * @param {String} name Cache name. + * @returns {Promise} A promise that completes when the index schema is updated. + * @memberof Client.admin# + * @since 0.15 + */ + updateIndexSchema: function(name) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke admin.updateIndexSchema(msgId=%d,name=%s)', ctx.id, name); + return future(ctx, 0x2B, p.encodeNameParams('@@cache@updateindexschema', {name: name}), p.decodeValue()); + }, + /** + * Register (create or update) a Protobuf schema on the server. + * + * @param {String} name Schema name (e.g. 'person.proto'). + * @param {String} schema Protobuf schema content. + * @returns {Promise} A promise that completes when the schema is registered. + * @memberof Client.admin# + * @since 0.15 + */ + registerSchema: function(name, schema) { + var ctx = transport.context(MEDIUM); + logger.debugf('Invoke admin.registerSchema(msgId=%d,name=%s)', ctx.id, name); + return future(ctx, 0x2B, p.encodeNameParams('@@schemas@createOrUpdate', { + name: name, content: schema, op: 's' + }), p.decodeValue()); + }, + /** + * Remove a registered Protobuf schema. + * + * @param {String} name Schema name. + * @returns {Promise} A promise that completes when the schema is removed. + * @memberof Client.admin# + * @since 0.15 + */ + removeSchema: function(name) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke admin.removeSchema(msgId=%d,name=%s)', ctx.id, name); + return future(ctx, 0x2B, p.encodeNameParams('@@schemas@delete', {name: name}), p.decodeValue()); + } } }; }; diff --git a/spec/infinispan_admin_spec.js b/spec/infinispan_admin_spec.js new file mode 100644 index 0000000..859df5c --- /dev/null +++ b/spec/infinispan_admin_spec.js @@ -0,0 +1,97 @@ +var t = require('./utils/testing'); // Testing dependency + +describe('Infinispan admin operations', function() { + var client = t.client(t.local, t.authOpts); + + describe('cache lifecycle', function() { + afterEach(function(done) { + client.then(function(c) { + return c.admin.removeCache('admin-test-cache').catch(function() {}); + }).then(function() { done(); }, t.failed(done)); + }); + + it('creates and removes a cache', function(done) { + client + .then(function(c) { + return c.admin.createCache('admin-test-cache', '') + .then(function() { return c.admin.cacheNames(); }) + .then(function(names) { + expect(names).toContain('admin-test-cache'); + return c.admin.removeCache('admin-test-cache'); + }) + .then(function() { return c.admin.cacheNames(); }) + .then(function(names) { + expect(names).not.toContain('admin-test-cache'); + }); + }) + .then(function() { done(); }, t.failed(done)); + }); + + it('creates a cache with configuration', function(done) { + client + .then(function(c) { + return c.admin.createCache('admin-test-cache', '') + .then(function() { return c.admin.cacheNames(); }) + .then(function(names) { + expect(names).toContain('admin-test-cache'); + }); + }) + .then(function() { done(); }, t.failed(done)); + }); + + it('getOrCreateCache creates if missing', function(done) { + client + .then(function(c) { + return c.admin.getOrCreateCache('admin-test-cache', '') + .then(function() { return c.admin.cacheNames(); }) + .then(function(names) { + expect(names).toContain('admin-test-cache'); + }) + // Calling again should not fail + .then(function() { + return c.admin.getOrCreateCache('admin-test-cache', ''); + }); + }) + .then(function() { done(); }, t.failed(done)); + }); + + it('lists cache names', function(done) { + client + .then(function(c) { + return c.admin.cacheNames().then(function(names) { + expect(Array.isArray(names)).toBe(true); + // Default cache should always exist + expect(names).toContain('default'); + }); + }) + .then(function() { done(); }, t.failed(done)); + }); + }); + + describe('schema management', function() { + var protoSchema = 'package test;\nmessage Person {\n required string name = 1;\n}\n'; + + afterEach(function(done) { + client.then(function(c) { + return c.admin.removeSchema('test-person.proto').catch(function() {}); + }).then(function() { done(); }, t.failed(done)); + }); + + it('registers a schema', function(done) { + client + .then(function(c) { + return c.admin.registerSchema('test-person.proto', protoSchema); + }) + .then(function() { done(); }, t.failed(done)); + }); + + it('removes a schema', function(done) { + client + .then(function(c) { + return c.admin.registerSchema('test-person.proto', protoSchema) + .then(function() { return c.admin.removeSchema('test-person.proto'); }); + }) + .then(function() { done(); }, t.failed(done)); + }); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 7189ae6..d73e844 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -909,6 +909,72 @@ export function client(args: { * @since 0.14 */ nearCacheSize: () => number; + /** + * Admin operations for cache lifecycle, schema management, and indexing. + * @since 0.15 + */ + admin: { + /** + * Create a cache with the given configuration. + * @param name Cache name. + * @param config Cache configuration (XML or JSON). + * @param opts Optional settings. + * @since 0.15 + */ + createCache: (name: string, config?: string, opts?: { template?: string; flags?: string }) => Promise; + /** + * Get an existing cache or create it with the given configuration. + * @param name Cache name. + * @param config Cache configuration (XML or JSON). + * @param opts Optional settings. + * @since 0.15 + */ + getOrCreateCache: (name: string, config?: string, opts?: { template?: string; flags?: string }) => Promise; + /** + * Remove a cache. + * @param name Cache name. + * @since 0.15 + */ + removeCache: (name: string) => Promise; + /** + * List all cache names. + * @since 0.15 + */ + cacheNames: () => Promise; + /** + * Update a mutable configuration attribute on a cache. + * @param name Cache name. + * @param attribute Attribute path (e.g. 'memory.max-count'). + * @param value New attribute value. + * @since 0.15 + */ + updateConfigurationAttribute: (name: string, attribute: string, value: string) => Promise; + /** + * Rebuild indexes for a cache. + * @param name Cache name. + * @since 0.15 + */ + reindex: (name: string) => Promise; + /** + * Update the index schema for a cache. + * @param name Cache name. + * @since 0.15 + */ + updateIndexSchema: (name: string) => Promise; + /** + * Register (create or update) a Protobuf schema on the server. + * @param name Schema name (e.g. 'person.proto'). + * @param schema Protobuf schema content. + * @since 0.15 + */ + registerSchema: (name: string, schema: string) => Promise; + /** + * Remove a registered Protobuf schema. + * @param name Schema name. + * @since 0.15 + */ + removeSchema: (name: string) => Promise; + }; }; /** * Cluster information.