infinispan 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/listeners.js CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  var events = require('events');
12
12
 
13
+ var CQ_FACTORY = 'continuous-query-filter-converter-factory';
14
+
13
15
  module.exports = listeners;
14
16
 
15
17
  /**
@@ -132,8 +134,10 @@
132
134
  dispatchEvent: function(event, listenerId, bytebuf, emitFunc) {
133
135
  return function() {
134
136
  var l = listeners.get(listenerId);
135
- if (f.existy(l))
136
- return emitFunc(event, l.emitter, bytebuf, listenerId);
137
+ if (f.existy(l)) {
138
+ var emit = l.cqEmit || emitFunc;
139
+ return emit(event, l.emitter, bytebuf, listenerId);
140
+ }
137
141
 
138
142
  logger.error('No emitter exists for listener %s', listenerId);
139
143
  return true;
@@ -152,7 +156,117 @@
152
156
  setProtocol: function(newProtocol) {
153
157
  protocol = newProtocol;
154
158
  },
159
+ /**
160
+ * Register a continuous query listener.
161
+ * @param {Object} transport Transport instance.
162
+ * @param {Object} ctx Request context.
163
+ * @param {string} queryString Ickle query string.
164
+ * @param {Object} [opts] Options with optional params map.
165
+ * @returns {Promise<Object>} ContinuousQuery handle.
166
+ */
167
+ addContinuousQueryListener: function(transport, ctx, queryString, opts) {
168
+ opts = opts || {};
169
+ var listenerId = _.uniqueId('cq_');
170
+ logger.debugl(function() {
171
+ return ['Invoke addContinuousQueryListener(msgId=%d,query=%s,listenerId=%s)',
172
+ ctx.id, queryString, listenerId]; });
173
+
174
+ var cqParams = buildCQParams(queryString, opts.params);
175
+
176
+ var listenerOpts = {
177
+ includeState: true,
178
+ useRawData: true,
179
+ filterFactory: { name: CQ_FACTORY, params: cqParams },
180
+ converterFactory: { name: CQ_FACTORY, params: cqParams }
181
+ };
182
+
183
+ var encodeListenerAddCommon = protocol.encodeListenerAdd(listenerId, listenerOpts)();
184
+ var encodeListenerAddInterests = protocol.encodeListenerInterests(listenerOpts);
185
+ var encodeListenerAdd = function() {
186
+ return f.cat(encodeListenerAddCommon, encodeListenerAddInterests);
187
+ };
188
+
189
+ var bufferedEvents = [];
190
+ var buffering = true;
191
+
192
+ var cqEmitFn = function(event, emitter, bytebuf) {
193
+ var payloadLength = codec.decodeVariableBytes()(bytebuf);
194
+ if (!f.existy(payloadLength) || !f.existy(payloadLength.answer))
195
+ return false;
196
+ var payload = payloadLength.answer;
197
+ var wrapped = codec.decodeWrappedMessage(payload);
198
+ if (!f.existy(wrapped) || !f.existy(wrapped.wrappedMessage))
199
+ return true;
200
+ var cqResult = codec.decodeContinuousQueryResult(wrapped.wrappedMessage);
201
+ if (buffering) {
202
+ bufferedEvents.push(cqResult);
203
+ } else {
204
+ emitter.emit(cqResult.resultType, cqResult.key, cqResult.value, cqResult.projection);
205
+ }
206
+ return true;
207
+ };
208
+
209
+ var preWrite = function(conn) {
210
+ var emitter = new events.EventEmitter();
211
+ listeners.put(listenerId, {
212
+ id: listenerId,
213
+ emitter: emitter,
214
+ conn: conn,
215
+ cqEmit: cqEmitFn
216
+ });
217
+ };
218
+
219
+ var remote = futurePreWrite(transport, ctx, 0x25
220
+ , encodeListenerAdd, protocol.complete(protocol.hasSuccess)
221
+ , listenerOpts, preWrite);
222
+
223
+ return remote
224
+ .then(function(success) {
225
+ if (success) {
226
+ var l = listeners.get(listenerId);
227
+ var emitter = l.emitter;
228
+ return {
229
+ on: function(event, callback) {
230
+ emitter.on(event, callback);
231
+ if (buffering) {
232
+ buffering = false;
233
+ bufferedEvents.forEach(function(ev) {
234
+ emitter.emit(ev.resultType, ev.key, ev.value, ev.projection);
235
+ });
236
+ bufferedEvents = [];
237
+ }
238
+ return this;
239
+ },
240
+ getListenerId: function() { return listenerId; }
241
+ };
242
+ }
243
+ listen.removeListeners(listenerId);
244
+ return undefined;
245
+ })
246
+ .catch(function(err) {
247
+ listen.removeListeners(listenerId);
248
+ throw err;
249
+ });
250
+ },
155
251
  };
252
+
253
+ /**
254
+ * Build CQ factory params from query and named params.
255
+ * @param {string} queryString Ickle query.
256
+ * @param {Object} [params] Named parameter map.
257
+ * @returns {Buffer[]} Array of wrapped param byte arrays.
258
+ */
259
+ function buildCQParams(queryString, params) {
260
+ var result = [codec.wrapScalar(queryString)];
261
+ if (f.existy(params)) {
262
+ _.each(params, function(value, name) {
263
+ result.push(codec.wrapScalar(name));
264
+ result.push(codec.wrapScalar(value));
265
+ });
266
+ }
267
+ return result;
268
+ }
269
+
156
270
  return listen;
157
271
  }
158
272
 
package/lib/protocols.js CHANGED
@@ -699,18 +699,31 @@
699
699
 
700
700
  var steps = [
701
701
  codec.encodeString(listenerId), // listener id
702
- codec.encodeUByte(includeState), // include state
703
- codec.encodeUByte(0) // TODO filter factory name
702
+ codec.encodeUByte(includeState) // include state
704
703
  ];
705
704
 
705
+ // Filter factory
706
+ if (_.has(opts, 'filterFactory') && _.has(opts.filterFactory, 'name')) {
707
+ steps.push(codec.encodeString(opts.filterFactory.name));
708
+ var filterParams = opts.filterFactory.params || [];
709
+ steps.push(codec.encodeUByte(filterParams.length));
710
+ filterParams.forEach(function(p) { steps.push(codec.encodeBytesWithLength(p)); });
711
+ } else {
712
+ steps.push(codec.encodeUByte(0));
713
+ }
714
+
715
+ // Converter factory
706
716
  if (_.has(opts, 'converterFactory') && _.has(opts.converterFactory, 'name')) {
707
717
  steps.push(codec.encodeString(opts.converterFactory.name));
708
- steps.push(codec.encodeUByte(0)); // TODO add converter parameter support
718
+ var converterParams = opts.converterFactory.params || [];
719
+ steps.push(codec.encodeUByte(converterParams.length));
720
+ converterParams.forEach(function(p) { steps.push(codec.encodeBytesWithLength(p)); });
709
721
  } else {
710
- steps.push(codec.encodeUByte(0)); // no converter
722
+ steps.push(codec.encodeUByte(0));
711
723
  }
712
724
 
713
- steps.push(codec.encodeUByte(0)); // raw data disabled
725
+ // Raw data
726
+ steps.push(codec.encodeUByte(hasOpt(opts, 'useRawData') ? 1 : 0));
714
727
 
715
728
  return function() {
716
729
  return steps;
@@ -1105,9 +1118,9 @@
1105
1118
  var authDone = DECODE_UBYTE(bytebuf);
1106
1119
  if(authDone == 1) {
1107
1120
  logger.tracef('SASL authentication complete');
1108
- return {result: {response: DECODE_UBYTE(bytebuf)}, continue: true};
1121
+ return {result: {response: DECODE_UBYTE(bytebuf), complete: true}, continue: true};
1109
1122
  } else {
1110
- return {result: {response: DECODE_STRING(bytebuf)}, continue: true};
1123
+ return {result: {response: DECODE_STRING(bytebuf), complete: false}, continue: true};
1111
1124
  }
1112
1125
  }
1113
1126
  };
package/lib/uri.js ADDED
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ (function() {
4
+
5
+ var _ = require('underscore');
6
+
7
+ var DEFAULT_PORT = 11222;
8
+
9
+ var QUERY_PARAMS = {
10
+ 'sasl_mechanism': { path: ['authentication', 'saslMechanism'], type: 'string' },
11
+ 'trust_store_file_name': { path: ['ssl', 'trustCerts'], type: 'string_array' },
12
+ 'trust_ca': { path: ['ssl', 'trustCerts'], type: 'string_array' },
13
+ 'key_store_file_name': { path: ['ssl', 'clientAuth', 'cert'], type: 'string' },
14
+ 'client_cert': { path: ['ssl', 'clientAuth', 'cert'], type: 'string' },
15
+ 'key_store_password': { path: ['ssl', 'clientAuth', 'key'], type: 'string' },
16
+ 'client_key': { path: ['ssl', 'clientAuth', 'key'], type: 'string' },
17
+ 'sni_host_name': { path: ['ssl', 'sniHostName'], type: 'string' },
18
+ 'sni_host': { path: ['ssl', 'sniHostName'], type: 'string' },
19
+ 'max_retries': { path: ['maxRetries'], type: 'int' },
20
+ 'cache_name': { path: ['cacheName'], type: 'string' }
21
+ };
22
+
23
+ /**
24
+ * Checks whether the argument is a Hot Rod URI string.
25
+ * @param {*} arg Value to check.
26
+ * @returns {boolean} True if the argument is a hotrod:// or hotrods:// URI.
27
+ */
28
+ function isHotrodURI(arg) {
29
+ return _.isString(arg) &&
30
+ (arg.indexOf('hotrod://') === 0 || arg.indexOf('hotrods://') === 0);
31
+ }
32
+
33
+ /**
34
+ * Splits a host string into host and port, handling IPv6 bracket notation.
35
+ * @param {string} hostStr Host string like "host:port" or "[::1]:port".
36
+ * @returns {{host: string, port: number}} Parsed host and port.
37
+ */
38
+ function splitHostPort(hostStr) {
39
+ hostStr = hostStr.trim();
40
+ if (hostStr.length === 0) {
41
+ throw new Error('Empty host in URI');
42
+ }
43
+
44
+ // IPv6 bracket notation: [::1]:port
45
+ if (hostStr.charAt(0) === '[') {
46
+ var closeBracket = hostStr.indexOf(']');
47
+ if (closeBracket === -1) {
48
+ throw new Error(`Invalid IPv6 address, missing closing bracket: ${hostStr}`);
49
+ }
50
+ var ipv6Host = hostStr.substring(1, closeBracket);
51
+ var afterBracket = hostStr.substring(closeBracket + 1);
52
+ if (afterBracket.length === 0) {
53
+ return { host: ipv6Host, port: DEFAULT_PORT };
54
+ }
55
+ if (afterBracket.charAt(0) === ':') {
56
+ var ipv6Port = parseInt(afterBracket.substring(1), 10);
57
+ if (isNaN(ipv6Port)) {
58
+ throw new Error(`Invalid port in URI: ${afterBracket.substring(1)}`);
59
+ }
60
+ return { host: ipv6Host, port: ipv6Port };
61
+ }
62
+ throw new Error(`Invalid IPv6 host format: ${hostStr}`);
63
+ }
64
+
65
+ var lastColon = hostStr.lastIndexOf(':');
66
+ if (lastColon === -1) {
67
+ return { host: hostStr, port: DEFAULT_PORT };
68
+ }
69
+
70
+ var host = hostStr.substring(0, lastColon);
71
+ var portStr = hostStr.substring(lastColon + 1);
72
+ var port = parseInt(portStr, 10);
73
+ if (isNaN(port)) {
74
+ throw new Error(`Invalid port in URI: ${portStr}`);
75
+ }
76
+ return { host: host, port: port };
77
+ }
78
+
79
+ /**
80
+ * Sets a nested value in an object given a path array.
81
+ * @param {Object} obj Target object.
82
+ * @param {string[]} path Array of keys.
83
+ * @param {*} value Value to set.
84
+ * @returns {void}
85
+ */
86
+ function setNested(obj, path, value) {
87
+ var current = obj;
88
+ for (var i = 0; i < path.length - 1; i++) {
89
+ if (!current[path[i]] || !_.isObject(current[path[i]])) {
90
+ current[path[i]] = {};
91
+ }
92
+ current = current[path[i]];
93
+ }
94
+ current[path[path.length - 1]] = value;
95
+ }
96
+
97
+ /**
98
+ * Parses a Hot Rod URI string into servers and options.
99
+ * @param {string} uriString URI in the format hotrod://[user:pass@]host1[:port1][,host2[:port2]][?params].
100
+ * @returns {{servers: Array<{host: string, port: number}>, options: Object}} Parsed result.
101
+ */
102
+ function parseHotrodURI(uriString) {
103
+ var tls = false;
104
+ var rest;
105
+
106
+ if (uriString.indexOf('hotrods://') === 0) {
107
+ tls = true;
108
+ rest = uriString.substring('hotrods://'.length);
109
+ } else if (uriString.indexOf('hotrod://') === 0) {
110
+ rest = uriString.substring('hotrod://'.length);
111
+ } else {
112
+ throw new Error('Invalid URI scheme, expected hotrod:// or hotrods://');
113
+ }
114
+
115
+ // Split authority from query string
116
+ var queryString = '';
117
+ var qIndex = rest.indexOf('?');
118
+ var authority;
119
+ if (qIndex !== -1) {
120
+ authority = rest.substring(0, qIndex);
121
+ queryString = rest.substring(qIndex + 1);
122
+ } else {
123
+ authority = rest;
124
+ }
125
+
126
+ // Split userinfo from hosts (find last @ to handle passwords with @)
127
+ var userName, password;
128
+ var atIndex = authority.lastIndexOf('@');
129
+ var hostsStr;
130
+ if (atIndex !== -1) {
131
+ var userinfo = authority.substring(0, atIndex);
132
+ hostsStr = authority.substring(atIndex + 1);
133
+ var colonIndex = userinfo.indexOf(':');
134
+ if (colonIndex !== -1) {
135
+ userName = decodeURIComponent(userinfo.substring(0, colonIndex));
136
+ password = decodeURIComponent(userinfo.substring(colonIndex + 1));
137
+ } else {
138
+ userName = decodeURIComponent(userinfo);
139
+ }
140
+ } else {
141
+ hostsStr = authority;
142
+ }
143
+
144
+ if (hostsStr.length === 0) {
145
+ throw new Error('No host specified in URI');
146
+ }
147
+
148
+ // Parse comma-separated hosts
149
+ var hostParts = hostsStr.split(',');
150
+ var servers = [];
151
+ for (var i = 0; i < hostParts.length; i++) {
152
+ if (hostParts[i].length > 0) {
153
+ servers.push(splitHostPort(hostParts[i]));
154
+ }
155
+ }
156
+ if (servers.length === 0) {
157
+ throw new Error('No host specified in URI');
158
+ }
159
+
160
+ // Build options from URI components
161
+ var options = {};
162
+
163
+ if (tls) {
164
+ setNested(options, ['ssl', 'enabled'], true);
165
+ }
166
+
167
+ if (userName !== undefined) {
168
+ setNested(options, ['authentication', 'enabled'], true);
169
+ setNested(options, ['authentication', 'userName'], userName);
170
+ if (password !== undefined) {
171
+ setNested(options, ['authentication', 'password'], password);
172
+ }
173
+ setNested(options, ['authentication', 'saslMechanism'], 'PLAIN');
174
+ }
175
+
176
+ // Parse query parameters
177
+ if (queryString.length > 0) {
178
+ var params = new URLSearchParams(queryString);
179
+ params.forEach(function(value, key) {
180
+ var mapping = QUERY_PARAMS[key];
181
+ if (!mapping) {
182
+ throw new Error(`Unknown URI parameter: ${key}`);
183
+ }
184
+
185
+ var parsed;
186
+ if (mapping.type === 'int') {
187
+ parsed = parseInt(value, 10);
188
+ if (isNaN(parsed)) {
189
+ throw new Error(`Invalid integer value for ${key}: ${value}`);
190
+ }
191
+ } else if (mapping.type === 'string_array') {
192
+ parsed = [value];
193
+ } else {
194
+ parsed = value;
195
+ }
196
+ setNested(options, mapping.path, parsed);
197
+ });
198
+ }
199
+
200
+ return { servers: servers, options: options };
201
+ }
202
+
203
+ exports.isHotrodURI = isHotrodURI;
204
+ exports.parseHotrodURI = parseHotrodURI;
205
+
206
+ }.call(this));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinispan",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Infinispan Javascript client",
5
5
  "main": "index",
6
6
  "typings": "./types",
@@ -9,7 +9,13 @@
9
9
  },
10
10
  "scripts": {
11
11
  "lint": "eslint --ignore-path .gitignore lib spec index.js",
12
- "test": "jasmine"
12
+ "test": "jasmine",
13
+ "test:docker": "node scripts/docker-test.js",
14
+ "docker:up": "docker compose -p ispn-test up -d --wait && docker compose -p ispn-test --profile failover create server-failover-one server-failover-two server-failover-three",
15
+ "docker:down": "docker compose -p ispn-test --profile failover down --remove-orphans",
16
+ "ssl:generate": "node scripts/make-ssl.js",
17
+ "docs:api": "jsdoc lib/*.js",
18
+ "docs:user": "node scripts/gen-asciidoc.js"
13
19
  },
14
20
  "author": "Infinispan Community",
15
21
  "license": "Apache-2.0",
@@ -28,7 +34,6 @@
28
34
  },
29
35
  "dependencies": {
30
36
  "buffer-xor": "^2.0.2",
31
- "jsdoc": "^4.0.2",
32
37
  "log4js": "^6.4.6",
33
38
  "protobufjs": "^7.0.0",
34
39
  "underscore": "^1.13.3",
@@ -37,6 +42,7 @@
37
42
  "devDependencies": {
38
43
  "eslint": "^8.26.0",
39
44
  "jasmine": "^6.1.0",
45
+ "jsdoc": "^4.0.2",
40
46
  "long": "^5.2.3"
41
47
  }
42
48
  }
package/server/.keep ADDED
File without changes
package/types/index.d.ts CHANGED
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Connect to an Infinispan Server using a Hot Rod URI.
3
+ *
4
+ * URI format: hotrod://[user:pass@]host1[:port1][,host2[:port2]][?params]
5
+ * Use hotrods:// to enable TLS.
6
+ *
7
+ * Supported query parameters:
8
+ * sasl_mechanism, trust_store_file_name (alias trust_ca),
9
+ * key_store_file_name (alias client_cert), key_store_password (alias client_key),
10
+ * sni_host_name (alias sni_host), max_retries, cache_name.
11
+ *
12
+ * @param uri Hot Rod URI string.
13
+ * @param options Optional overrides (take precedence over URI values).
14
+ */
15
+ export function client(uri: string, options?: Record<string, any>): Promise<any>;
16
+
1
17
  export function client(args: {
2
18
  /**
3
19
  * - Server host name.
@@ -668,6 +684,34 @@ export function client(args: {
668
684
  * @since 0.3
669
685
  */
670
686
  removeListener: (listenerId: string) => Promise<any>;
687
+ /**
688
+ * Register a continuous query that watches for cache changes
689
+ * matching the given Ickle query.
690
+ *
691
+ * @param queryString Ickle query string.
692
+ * @param opts Optional CQ options.
693
+ * @returns A promise completed with a ContinuousQuery handle.
694
+ * @memberof Client#
695
+ * @since 0.16
696
+ */
697
+ addContinuousQuery: (queryString: string, opts?: {
698
+ /** Named parameter bindings for the Ickle query. */
699
+ params?: { [name: string]: string | number | boolean };
700
+ }) => Promise<{
701
+ /** Register a callback for continuous query events. */
702
+ on(event: 'joining' | 'leaving' | 'updated', callback: (key: Buffer, value: Buffer, projection?: any[]) => void): any;
703
+ /** Get the listener ID for this continuous query. */
704
+ getListenerId(): string;
705
+ }>;
706
+ /**
707
+ * Remove a continuous query.
708
+ *
709
+ * @param cq ContinuousQuery handle returned by addContinuousQuery.
710
+ * @returns A promise completed when the continuous query has been removed.
711
+ * @memberof Client#
712
+ * @since 0.16
713
+ */
714
+ removeContinuousQuery: (cq: { getListenerId(): string }) => Promise<any>;
671
715
  /**
672
716
  * Create a distributed counter.
673
717
  *
package/Dockerfile.server DELETED
@@ -1,8 +0,0 @@
1
- FROM quay.io/infinispan/server:16.1.3
2
-
3
- # Install Nashorn script engine for server-side JavaScript execution
4
- RUN /opt/infinispan/bin/cli.sh install org.openjdk.nashorn:nashorn-core:15.4 && \
5
- /opt/infinispan/bin/cli.sh install org.ow2.asm:asm:9.4 && \
6
- /opt/infinispan/bin/cli.sh install org.ow2.asm:asm-commons:9.4 && \
7
- /opt/infinispan/bin/cli.sh install org.ow2.asm:asm-tree:9.4 && \
8
- /opt/infinispan/bin/cli.sh install org.ow2.asm:asm-util:9.4