diff --git a/documentation/asciidoc/topics/code_examples/multimap-basic.js b/documentation/asciidoc/topics/code_examples/multimap-basic.js new file mode 100644 index 0000000..7e7ffc4 --- /dev/null +++ b/documentation/asciidoc/topics/code_examples/multimap-basic.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) { + + // Store multiple values under the same key. + var put = client.multimapPut('colors', 'red'); + put = put.then(function() { + return client.multimapPut('colors', 'green'); + }); + put = put.then(function() { + return client.multimapPut('colors', 'blue'); + }); + + // Retrieve all values for a key. + var get = put.then(function() { + return client.multimapGet('colors'); + }); + get.then(function(values) { + console.log('Colors: ' + values); // ['red', 'green', 'blue'] + }); + + // Check if a key exists in the multimap. + var containsKey = get.then(function() { + return client.multimapContainsKey('colors'); + }); + containsKey.then(function(exists) { + console.log('Contains key: ' + exists); // true + }); + + // Check if a specific value exists anywhere in the multimap. + var containsValue = containsKey.then(function() { + return client.multimapContainsValue('red'); + }); + containsValue.then(function(exists) { + console.log('Contains value: ' + exists); // true + }); + + // Check if a specific key-value pair exists. + var containsEntry = containsValue.then(function() { + return client.multimapContainsEntry('colors', 'green'); + }); + containsEntry.then(function(exists) { + console.log('Contains entry: ' + exists); // true + }); + + // Get the total number of entries across all keys. + var size = containsEntry.then(function() { + return client.multimapSize(); + }); + size.then(function(count) { + console.log('Total entries: ' + count); // 3 + }); + + // Remove a single value from a key. + var removeEntry = size.then(function() { + return client.multimapRemoveEntry('colors', 'red'); + }); + removeEntry.then(function(removed) { + console.log('Removed entry: ' + removed); // true + }); + + // Remove all values for a key. + var removeKey = removeEntry.then(function() { + return client.multimapRemoveKey('colors'); + }); + removeKey.then(function(removed) { + console.log('Removed key: ' + removed); // true + }); + + // Disconnect from {brandname} Server. + return removeKey.then(function() { + return client.disconnect(); + }); + +}).catch(function(error) { + + // Log any errors. + console.log("Got error: " + error.message); + +}); diff --git a/documentation/asciidoc/topics/ref_client_usage.adoc b/documentation/asciidoc/topics/ref_client_usage.adoc index 1fb46a7..86e1841 100644 --- a/documentation/asciidoc/topics/ref_client_usage.adoc +++ b/documentation/asciidoc/topics/ref_client_usage.adoc @@ -210,6 +210,46 @@ include::code_examples/distributed-counters.js[] |Removes the counter from the cluster. |=== +== Working with multimaps + +Multimaps allow you to associate multiple values with a single key. +The {hr_js} client uses Set semantics by default, which means duplicate values for the same key are not stored. + +[source,javascript,options="nowrap",subs=attributes+] +---- +include::code_examples/multimap-basic.js[] +---- + +.Available multimap operations +[cols="1,3",options="header"] +|=== +|Method |Description + +|`multimapPut(key, value)` +|Adds a value to the collection for the given key. + +|`multimapGet(key)` +|Returns an array of all values for the given key, or an empty array if the key does not exist. + +|`multimapContainsKey(key)` +|Returns `true` if the key exists in the multimap. + +|`multimapContainsValue(value)` +|Returns `true` if the value exists for any key in the multimap. + +|`multimapContainsEntry(key, value)` +|Returns `true` if the key-value pair exists in the multimap. + +|`multimapSize()` +|Returns the total number of entries across all keys. + +|`multimapRemoveEntry(key, value)` +|Removes a single value from the given key. Returns `true` if the entry existed. + +|`multimapRemoveKey(key)` +|Removes all values for the given key. Returns `true` if the key existed. +|=== + == Working with queries Use the `query` method to perform queries on your caches. diff --git a/lib/infinispan.js b/lib/infinispan.js index 5a0f03a..c41523d 100644 --- a/lib/infinispan.js +++ b/lib/infinispan.js @@ -1096,6 +1096,122 @@ logger.debugf('Invoke counterGetAndSet(msgId=%d,name=%s,value=%d)', ctx.id, name, value); return future(ctx, 0x7F, p.encodeCounterNameValue(name, value), p.decodeCounterValue); }, + /** + * Get all values associated with a key in the multimap. + * + * @param {(String|Object)} k Key to retrieve. + * @returns {Promise.} + * A promise that will be completed with an array of values, + * or an empty array if the key does not exist. + * @memberof Client# + * @since 0.16 + */ + multimapGet: function(k) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke multimapGet(msgId=%d,key=%s)', ctx.id, u.str(k)); + return futureKey(ctx, 0x67, k, p.encodeMultimapKey(k), p.decodeMultimapCollection()); + }, + /** + * Add a value to the collection associated with a key in the multimap. + * + * @param {(String|Object)} k Key. + * @param {(String|Object)} v Value to add. + * @param {Object=} opts Optional store options (lifespan, maxIdle). + * @returns {Promise} + * A promise that will be completed when the value has been added. + * @memberof Client# + * @since 0.16 + */ + multimapPut: function(k, v, opts) { + var ctx = transport.context(MEDIUM); + logger.debugf('Invoke multimapPut(msgId=%d,key=%s,value=%s)', ctx.id, u.str(k), u.str(v)); + return futureKey(ctx, 0x6B, k, p.encodeMultimapPut(k, v), p.complete(_.constant(undefined)), opts); + }, + /** + * Remove all values associated with a key in the multimap. + * + * @param {(String|Object)} k Key to remove. + * @returns {Promise.} + * A promise that will be completed with true if the key existed. + * @memberof Client# + * @since 0.16 + */ + multimapRemoveKey: function(k) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke multimapRemoveKey(msgId=%d,key=%s)', ctx.id, u.str(k)); + return futureKey(ctx, 0x6D, k, p.encodeMultimapKey(k), p.decodeMultimapBoolean); + }, + /** + * Remove a specific value from the collection associated with a key. + * + * @param {(String|Object)} k Key. + * @param {(String|Object)} v Value to remove. + * @returns {Promise.} + * A promise that will be completed with true if the entry existed. + * @memberof Client# + * @since 0.16 + */ + multimapRemoveEntry: function(k, v) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke multimapRemoveEntry(msgId=%d,key=%s,value=%s)', ctx.id, u.str(k), u.str(v)); + return futureKey(ctx, 0x6F, k, p.encodeMultimapKeyValue(k, v), p.decodeMultimapBoolean); + }, + /** + * Get the total number of key-value pairs in the multimap. + * + * @returns {Promise.} + * A promise that will be completed with the total size. + * @memberof Client# + * @since 0.16 + */ + multimapSize: function() { + var ctx = transport.context(SMALL); + logger.debugf('Invoke multimapSize(msgId=%d)', ctx.id); + return future(ctx, 0x71, p.encodeMultimapSupportsDuplicates(), p.decodeMultimapSize); + }, + /** + * Check whether a specific key-value pair exists in the multimap. + * + * @param {(String|Object)} k Key. + * @param {(String|Object)} v Value. + * @returns {Promise.} + * A promise that will be completed with true if the entry exists. + * @memberof Client# + * @since 0.16 + */ + multimapContainsEntry: function(k, v) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke multimapContainsEntry(msgId=%d,key=%s,value=%s)', ctx.id, u.str(k), u.str(v)); + return futureKey(ctx, 0x73, k, p.encodeMultimapKeyValue(k, v), p.decodeMultimapBoolean); + }, + /** + * Check whether a key exists in the multimap. + * + * @param {(String|Object)} k Key to check. + * @returns {Promise.} + * A promise that will be completed with true if the key exists. + * @memberof Client# + * @since 0.16 + */ + multimapContainsKey: function(k) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke multimapContainsKey(msgId=%d,key=%s)', ctx.id, u.str(k)); + return futureKey(ctx, 0x75, k, p.encodeMultimapKey(k), p.decodeMultimapBoolean); + }, + /** + * Check whether any key contains the given value in the multimap. + * + * @param {(String|Object)} v Value to check. + * @returns {Promise.} + * A promise that will be completed with true if any key contains the value. + * @memberof Client# + * @since 0.16 + */ + multimapContainsValue: function(v) { + var ctx = transport.context(SMALL); + logger.debugf('Invoke multimapContainsValue(msgId=%d,value=%s)', ctx.id, u.str(v)); + return future(ctx, 0x77, p.encodeMultimapValue(v), p.decodeMultimapBoolean); + }, /** * Get server topology related information. * diff --git a/lib/protocols.js b/lib/protocols.js index 758cf90..bdc8fef 100644 --- a/lib/protocols.js +++ b/lib/protocols.js @@ -1323,6 +1323,81 @@ }; }()); + var MultimapMixin = (function() { + var SUPPORTS_DUPLICATES = codec.encodeUByte(0x00); + + return { + encodeMultimapKey: function(k) { + var outer = this; + return function() { + return [outer.encodeMediaKey(k), SUPPORTS_DUPLICATES]; + }; + }, + encodeMultimapPut: function(k, v) { + var outer = this; + return function(opts) { + return f.cat( + [outer.encodeMediaKey(k)], + outer.encodeExpiry(opts), + [outer.encodeMediaValue(v), SUPPORTS_DUPLICATES] + ); + }; + }, + encodeMultimapKeyValue: function(k, v) { + var outer = this; + return function() { + return f.cat( + [outer.encodeMediaKey(k)], + [codec.encodeUByte(0x77)], + [outer.encodeMediaValue(v), SUPPORTS_DUPLICATES] + ); + }; + }, + encodeMultimapValue: function(v) { + var outer = this; + return function() { + return [codec.encodeUByte(0x77), outer.encodeMediaValue(v), SUPPORTS_DUPLICATES]; + }; + }, + encodeMultimapSupportsDuplicates: function() { + return function() { + return [SUPPORTS_DUPLICATES]; + }; + }, + decodeMultimapCollection: function() { + var decoderValue = decoderMedia(this.valueMediaType); + var decodeSingleValue = decodeSingle(decoderValue); + return function(header, bytebuf) { + if (header.status !== 0x00 && header.status !== 0x03) + return {result: [], continue: true}; + var count = DECODE_VINT(bytebuf); + if (!f.existy(count)) + return {continue: false}; + var values = []; + for (var i = 0; i < count; i++) { + var v = decodeSingleValue(bytebuf); + if (!f.existy(v)) + return {continue: false}; + values.push(v); + } + return {result: values, continue: true}; + }; + }, + decodeMultimapBoolean: function(header, bytebuf) { + var b = DECODE_UBYTE(bytebuf); + if (!f.existy(b)) + return {continue: false}; + return {result: b !== 0, continue: true}; + }, + decodeMultimapSize: function(header, bytebuf) { + var size = f.actions([codec.decodeVLong()], codec.lastDecoded)(bytebuf); + if (!f.existy(size) && size !== 0) + return {continue: false}; + return {result: size, continue: true}; + } + }; + }()); + /** * Protocol 4.0+ requires an 'otherParams' map after media types in the header. * This mixin overrides stepsHeader to append the count (0 = no params). @@ -1595,6 +1670,7 @@ , SASLMixin , Ping30Mixin , CounterMixin + , MultimapMixin , ProtostreamType , ProtobufRoot , TransactionMixin @@ -1614,6 +1690,7 @@ , SASLMixin , Ping30Mixin , CounterMixin + , MultimapMixin , ProtostreamType , ProtobufRoot , TransactionMixin @@ -1633,6 +1710,7 @@ , SASLMixin , Ping30Mixin , CounterMixin + , MultimapMixin , ProtostreamType , ProtobufRoot , TransactionMixin diff --git a/spec/infinispan_multimap_spec.js b/spec/infinispan_multimap_spec.js new file mode 100644 index 0000000..ecf5bb3 --- /dev/null +++ b/spec/infinispan_multimap_spec.js @@ -0,0 +1,104 @@ +var _ = require('underscore'); +var t = require('./utils/testing'); + +describe('Infinispan multimap', function() { + var client = t.client(t.local, t.authOpts); + + afterEach(function(done) { + client.then(t.clear()).then(function() { done(); }, t.failed(done)); + }); + + it('can put and get multiple values for a key', function(done) { + client + .then(t.assert(t.multimapPut('colors', 'red'))) + .then(t.assert(t.multimapPut('colors', 'green'))) + .then(t.assert(t.multimapPut('colors', 'blue'))) + .then(t.assert(t.multimapGet('colors'), function(values) { + expect(_.sortBy(values)).toEqual(['blue', 'green', 'red']); + })) + .then(function() { done(); }, t.failed(done)); + }); + + it('returns empty array for non-existent key', function(done) { + client + .then(t.assert(t.multimapGet('missing'), function(values) { + expect(values).toEqual([]); + })) + .then(function() { done(); }, t.failed(done)); + }); + + it('deduplicates values in set mode', function(done) { + client + .then(t.assert(t.multimapPut('key', 'val'))) + .then(t.assert(t.multimapPut('key', 'val'))) + .then(t.assert(t.multimapGet('key'), function(values) { + expect(values).toEqual(['val']); + })) + .then(function() { done(); }, t.failed(done)); + }); + + it('can check if a key exists with containsKey', function(done) { + client + .then(t.assert(t.multimapContainsKey('k'), t.toBeFalsy)) + .then(t.assert(t.multimapPut('k', 'v'))) + .then(t.assert(t.multimapContainsKey('k'), t.toBeTruthy)) + .then(function() { done(); }, t.failed(done)); + }); + + it('can check if a value exists with containsValue', function(done) { + client + .then(t.assert(t.multimapContainsValue('v'), t.toBeFalsy)) + .then(t.assert(t.multimapPut('k', 'v'))) + .then(t.assert(t.multimapContainsValue('v'), t.toBeTruthy)) + .then(t.assert(t.multimapContainsValue('other'), t.toBeFalsy)) + .then(function() { done(); }, t.failed(done)); + }); + + it('can check if an entry exists with containsEntry', function(done) { + client + .then(t.assert(t.multimapPut('k', 'v1'))) + .then(t.assert(t.multimapPut('k', 'v2'))) + .then(t.assert(t.multimapContainsEntry('k', 'v1'), t.toBeTruthy)) + .then(t.assert(t.multimapContainsEntry('k', 'v2'), t.toBeTruthy)) + .then(t.assert(t.multimapContainsEntry('k', 'v3'), t.toBeFalsy)) + .then(function() { done(); }, t.failed(done)); + }); + + it('can remove a specific entry', function(done) { + client + .then(t.assert(t.multimapPut('k', 'v1'))) + .then(t.assert(t.multimapPut('k', 'v2'))) + .then(t.assert(t.multimapRemoveEntry('k', 'v1'), t.toBeTruthy)) + .then(t.assert(t.multimapGet('k'), function(values) { + expect(values).toEqual(['v2']); + })) + .then(function() { done(); }, t.failed(done)); + }); + + it('can remove all values for a key', function(done) { + client + .then(t.assert(t.multimapPut('k', 'v1'))) + .then(t.assert(t.multimapPut('k', 'v2'))) + .then(t.assert(t.multimapRemoveKey('k'), t.toBeTruthy)) + .then(t.assert(t.multimapGet('k'), function(values) { + expect(values).toEqual([]); + })) + .then(function() { done(); }, t.failed(done)); + }); + + it('returns false when removing a non-existent key', function(done) { + client + .then(t.assert(t.multimapRemoveKey('missing'), t.toBeFalsy)) + .then(function() { done(); }, t.failed(done)); + }); + + it('can get the total size of the multimap', function(done) { + client + .then(t.assert(t.multimapSize(), t.toBe(0))) + .then(t.assert(t.multimapPut('k1', 'v1'))) + .then(t.assert(t.multimapPut('k1', 'v2'))) + .then(t.assert(t.multimapPut('k2', 'v3'))) + .then(t.assert(t.multimapSize(), t.toBe(3))) + .then(function() { done(); }, t.failed(done)); + }); +}); diff --git a/spec/utils/testing.js b/spec/utils/testing.js index 5af5b0c..bd8c540 100644 --- a/spec/utils/testing.js +++ b/spec/utils/testing.js @@ -625,6 +625,38 @@ exports.counterGetAndSet = function(name, value) { return function(client) { return client.counterGetAndSet(name, value); }; }; +exports.multimapGet = function(k) { + return function(client) { return client.multimapGet(k); }; +}; + +exports.multimapPut = function(k, v, opts) { + return function(client) { return client.multimapPut(k, v, opts); }; +}; + +exports.multimapRemoveKey = function(k) { + return function(client) { return client.multimapRemoveKey(k); }; +}; + +exports.multimapRemoveEntry = function(k, v) { + return function(client) { return client.multimapRemoveEntry(k, v); }; +}; + +exports.multimapSize = function() { + return function(client) { return client.multimapSize(); }; +}; + +exports.multimapContainsKey = function(k) { + return function(client) { return client.multimapContainsKey(k); }; +}; + +exports.multimapContainsValue = function(v) { + return function(client) { return client.multimapContainsValue(v); }; +}; + +exports.multimapContainsEntry = function(k, v) { + return function(client) { return client.multimapContainsEntry(k, v); }; +}; + exports.expectToThrow = function(func, errorMessage, done) { expect(func).toThrowError(errorMessage); if (f.existy(done)) done();