UNPKG

19.2 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Kuzzle = void 0;
4const KuzzleEventEmitter_1 = require("./core/KuzzleEventEmitter");
5const Auth_1 = require("./controllers/Auth");
6const Bulk_1 = require("./controllers/Bulk");
7const Collection_1 = require("./controllers/Collection");
8const Document_1 = require("./controllers/Document");
9const Index_1 = require("./controllers/Index");
10const Realtime_1 = require("./controllers/Realtime");
11const Server_1 = require("./controllers/Server");
12const Security_1 = require("./controllers/Security");
13const MemoryStorage_1 = require("./controllers/MemoryStorage");
14const uuidv4_1 = require("./utils/uuidv4");
15const proxify_1 = require("./utils/proxify");
16const events = [
17 'connected',
18 'discarded',
19 'disconnected',
20 'loginAttempt',
21 'networkError',
22 'offlineQueuePush',
23 'offlineQueuePop',
24 'queryError',
25 'reconnected',
26 'tokenExpired'
27];
28class Kuzzle extends KuzzleEventEmitter_1.KuzzleEventEmitter {
29 /**
30 * Instantiate a new SDK
31 *
32 * @example
33 *
34 * import { Kuzzle, WebSocket } from 'kuzzle-sdk';
35 *
36 * const kuzzle = new Kuzzle(
37 * new WebSocket('localhost')
38 * );
39 */
40 constructor(
41 /**
42 * Network protocol to connect to Kuzzle. (e.g. `Http` or `WebSocket`)
43 */
44 protocol, options = {}) {
45 super();
46 if (protocol === undefined || protocol === null) {
47 throw new Error('"protocol" argument missing');
48 }
49 // check the existence of required methods
50 for (const method of ['addListener', 'isReady', 'query']) {
51 if (typeof protocol[method] !== 'function') {
52 throw new Error(`Protocol instance must implement a "${method}" method`);
53 }
54 }
55 this.protocol = protocol;
56 this._protectedEvents = {
57 connected: {},
58 error: {},
59 disconnected: {},
60 reconnected: {},
61 tokenExpired: {},
62 loginAttempt: {}
63 };
64 this.autoResubscribe = typeof options.autoResubscribe === 'boolean'
65 ? options.autoResubscribe
66 : true;
67 this.eventTimeout = typeof options.eventTimeout === 'number'
68 ? options.eventTimeout
69 : 200;
70 this.sdkVersion = typeof SDKVERSION === 'undefined'
71 // eslint-disable-next-line @typescript-eslint/no-var-requires
72 ? require('../package').version
73 : SDKVERSION;
74 this.sdkName = `js@${this.sdkVersion}`;
75 this.volatile = typeof options.volatile === 'object'
76 ? options.volatile
77 : {};
78 // controllers
79 this.useController(Auth_1.AuthController, 'auth');
80 this.useController(Bulk_1.BulkController, 'bulk');
81 this.useController(Collection_1.CollectionController, 'collection');
82 this.useController(Document_1.DocumentController, 'document');
83 this.useController(Index_1.IndexController, 'index');
84 this.useController(MemoryStorage_1.MemoryStorageController, 'ms');
85 this.useController(Realtime_1.RealtimeController, 'realtime');
86 this.useController(Security_1.SecurityController, 'security');
87 this.useController(Server_1.ServerController, 'server');
88 // offline queue
89 this._offlineQueue = [];
90 this._autoQueue = typeof options.autoQueue === 'boolean'
91 ? options.autoQueue
92 : false;
93 this._autoReplay = typeof options.autoReplay === 'boolean'
94 ? options.autoReplay
95 : false;
96 this._offlineQueueLoader = typeof options.offlineQueueLoader === 'function'
97 ? options.offlineQueueLoader
98 : null;
99 this._queueFilter = typeof options.queueFilter === 'function'
100 ? options.queueFilter
101 : null;
102 this._queueMaxSize = typeof options.queueMaxSize === 'number'
103 ? options.queueMaxSize
104 : 500;
105 this._queueTTL = typeof options.queueTTL === 'number'
106 ? options.queueTTL
107 : 120000;
108 this._replayInterval = typeof options.replayInterval === 'number'
109 ? options.replayInterval
110 : 10;
111 this._tokenExpiredInterval = typeof options.tokenExpiredInterval === 'number'
112 ? options.tokenExpiredInterval
113 : 1000;
114 if (options.offlineMode === 'auto') {
115 this._autoQueue = true;
116 this._autoReplay = true;
117 }
118 this._queuing = false;
119 this._lastTokenExpired = null;
120 return proxify_1.proxify(this, {
121 seal: true,
122 name: 'kuzzle',
123 exposeApi: true
124 });
125 }
126 get authenticated() {
127 return this.auth.authenticationToken && !this.auth.authenticationToken.expired;
128 }
129 get autoQueue() {
130 return this._autoQueue;
131 }
132 set autoQueue(value) {
133 this._checkPropertyType('_autoQueue', 'boolean', value);
134 this._autoQueue = value;
135 }
136 get autoReconnect() {
137 return this.protocol.autoReconnect;
138 }
139 set autoReconnect(value) {
140 this._checkPropertyType('autoReconnect', 'boolean', value);
141 this.protocol.autoReconnect = value;
142 }
143 get autoReplay() {
144 return this._autoReplay;
145 }
146 set autoReplay(value) {
147 this._checkPropertyType('_autoReplay', 'boolean', value);
148 this._autoReplay = value;
149 }
150 get connected() {
151 return this.protocol.connected;
152 }
153 get host() {
154 return this.protocol.host;
155 }
156 get jwt() {
157 if (!this.auth.authenticationToken) {
158 return null;
159 }
160 return this.auth.authenticationToken.encodedJwt;
161 }
162 set jwt(encodedJwt) {
163 this.auth.authenticationToken = encodedJwt;
164 }
165 get offlineQueue() {
166 return this._offlineQueue;
167 }
168 get offlineQueueLoader() {
169 return this._offlineQueueLoader;
170 }
171 set offlineQueueLoader(value) {
172 this._checkPropertyType('_offlineQueueLoader', 'function', value);
173 this._offlineQueueLoader = value;
174 }
175 get port() {
176 return this.protocol.port;
177 }
178 get queueFilter() {
179 return this._queueFilter;
180 }
181 set queueFilter(value) {
182 this._checkPropertyType('_queueFilter', 'function', value);
183 this._queueFilter = value;
184 }
185 get queueMaxSize() {
186 return this._queueMaxSize;
187 }
188 set queueMaxSize(value) {
189 this._checkPropertyType('_queueMaxSize', 'number', value);
190 this._queueMaxSize = value;
191 }
192 get queueTTL() {
193 return this._queueTTL;
194 }
195 set queueTTL(value) {
196 this._checkPropertyType('_queueTTL', 'number', value);
197 this._queueTTL = value;
198 }
199 get reconnectionDelay() {
200 return this.protocol.reconnectionDelay;
201 }
202 get replayInterval() {
203 return this._replayInterval;
204 }
205 set replayInterval(value) {
206 this._checkPropertyType('_replayInterval', 'number', value);
207 this._replayInterval = value;
208 }
209 get sslConnection() {
210 return this.protocol.sslConnection;
211 }
212 get tokenExpiredInterval() {
213 return this._tokenExpiredInterval;
214 }
215 set tokenExpiredInterval(value) {
216 this._checkPropertyType('_tokenExpiredInterval', 'number', value);
217 this._tokenExpiredInterval = value;
218 }
219 /**
220 * Emit an event to all registered listeners
221 * An event cannot be emitted multiple times before a timeout has been reached.
222 */
223 emit(eventName, ...payload) {
224 const now = Date.now(), protectedEvent = this._protectedEvents[eventName];
225 if (protectedEvent) {
226 if (protectedEvent.lastEmitted
227 && protectedEvent.lastEmitted > now - this.eventTimeout) {
228 return false;
229 }
230 protectedEvent.lastEmitted = now;
231 }
232 return this._superEmit(eventName, ...payload);
233 }
234 _superEmit(eventName, ...payload) {
235 return super.emit(eventName, ...payload);
236 }
237 /**
238 * Connects to a Kuzzle instance
239 */
240 connect() {
241 if (this.protocol.isReady()) {
242 return Promise.resolve();
243 }
244 if (this.autoQueue) {
245 this.startQueuing();
246 }
247 this.protocol.addListener('queryError', (err, query) => this.emit('queryError', err, query));
248 this.protocol.addListener('tokenExpired', () => this.tokenExpired());
249 this.protocol.addListener('connect', () => {
250 if (this.autoQueue) {
251 this.stopQueuing();
252 }
253 if (this.autoReplay) {
254 this.playQueue();
255 }
256 this.emit('connected');
257 });
258 this.protocol.addListener('networkError', error => {
259 if (this.autoQueue) {
260 this.startQueuing();
261 }
262 this.emit('networkError', error);
263 });
264 this.protocol.addListener('disconnect', () => {
265 this.emit('disconnected');
266 });
267 this.protocol.addListener('reconnect', () => {
268 if (this.autoQueue) {
269 this.stopQueuing();
270 }
271 if (this.autoReplay) {
272 this.playQueue();
273 }
274 if (this.auth.authenticationToken) {
275 return this.auth.checkToken()
276 .then(res => {
277 // shouldn't obtain an error but let's invalidate the token anyway
278 if (!res.valid) {
279 this.auth.authenticationToken = null;
280 }
281 })
282 .catch(() => {
283 this.auth.authenticationToken = null;
284 })
285 .then(() => this.emit('reconnected'));
286 }
287 this.emit('reconnected');
288 });
289 this.protocol.addListener('discarded', data => this.emit('discarded', data));
290 return this.protocol.connect();
291 }
292 /**
293 * Adds a listener to a Kuzzle global event. When an event is fired, listeners are called in the order of their
294 * insertion.
295 *
296 * @param {string} event - name of the global event to subscribe to
297 * @param {function} listener - callback to invoke each time an event is fired
298 */
299 addListener(event, listener) {
300 if (events.indexOf(event) === -1) {
301 throw new Error(`[${event}] is not a known event. Known events: ${events.toString()}`);
302 }
303 return this._superAddListener(event, listener);
304 }
305 _superAddListener(event, listener) {
306 return super.addListener(event, listener);
307 }
308 /**
309 * Empties the offline queue without replaying it.
310 *
311 * @returns {Kuzzle}
312 */
313 flushQueue() {
314 this._offlineQueue = [];
315 return this;
316 }
317 /**
318 * Disconnects from Kuzzle and invalidate this instance.
319 */
320 disconnect() {
321 this.protocol.close();
322 }
323 /**
324 * This is a low-level method, exposed to allow advanced SDK users to bypass
325 * high-level methods.
326 * Base method used to send read queries to Kuzzle
327 *
328 * Takes an optional argument object with the following properties:
329 * - volatile (object, default: null):
330 * Additional information passed to notifications to other users
331 *
332 * @param request
333 * @param options - Optional arguments
334 */
335 query(request = {}, options = {}) {
336 if (typeof request !== 'object' || Array.isArray(request)) {
337 throw new Error(`Kuzzle.query: Invalid request: ${JSON.stringify(request)}`);
338 }
339 if (typeof options !== 'object' || Array.isArray(options)) {
340 throw new Error(`Kuzzle.query: Invalid "options" argument: ${JSON.stringify(options)}`);
341 }
342 if (!request.requestId) {
343 request.requestId = uuidv4_1.uuidv4();
344 }
345 if (typeof request.refresh === 'undefined' && typeof options.refresh !== 'undefined') {
346 request.refresh = options.refresh;
347 }
348 if (!request.volatile) {
349 request.volatile = this.volatile;
350 }
351 else if (typeof request.volatile !== 'object'
352 || Array.isArray(request.volatile)) {
353 throw new Error(`Kuzzle.query: Invalid volatile argument received: ${JSON.stringify(request.volatile)}`);
354 }
355 for (const item of Object.keys(this.volatile)) {
356 if (request.volatile[item] === undefined) {
357 request.volatile[item] = this.volatile[item];
358 }
359 }
360 request.volatile.sdkInstanceId = request.volatile.sdkInstanceId || this.protocol.id;
361 request.volatile.sdkName = request.volatile.sdkName || this.sdkName;
362 this.auth.authenticateRequest(request);
363 let queuable = true;
364 if (options && options.queuable === false) {
365 queuable = false;
366 }
367 if (this.queueFilter) {
368 queuable = queuable && this.queueFilter(request);
369 }
370 if (this._queuing) {
371 if (queuable) {
372 this._cleanQueue();
373 this.emit('offlineQueuePush', { request });
374 return new Promise((resolve, reject) => {
375 this.offlineQueue.push({
376 resolve,
377 reject,
378 request,
379 ts: Date.now()
380 });
381 });
382 }
383 this.emit('discarded', { request });
384 return Promise.reject(new Error(`Unable to execute request: not connected to a Kuzzle server.
385Discarded request: ${JSON.stringify(request)}`));
386 }
387 return this.protocol.query(request, options);
388 }
389 /**
390 * Starts the requests queuing.
391 */
392 startQueuing() {
393 this._queuing = true;
394 return this;
395 }
396 /**
397 * Stops the requests queuing.
398 */
399 stopQueuing() {
400 this._queuing = false;
401 return this;
402 }
403 /**
404 * Plays the requests queued during offline mode.
405 */
406 playQueue() {
407 if (this.protocol.isReady()) {
408 this._cleanQueue();
409 this._dequeue();
410 }
411 return this;
412 }
413 /**
414 * On token expiration, reset jwt and unsubscribe all rooms.
415 * Throttles to avoid duplicate event triggers.
416 */
417 tokenExpired() {
418 const now = Date.now();
419 if ((now - this._lastTokenExpired) < this.tokenExpiredInterval) {
420 // event was recently already fired
421 return;
422 }
423 this._lastTokenExpired = now;
424 this.emit('tokenExpired');
425 }
426 /**
427 * Adds a new controller and make it available in the SDK.
428 *
429 * @param ControllerClass
430 * @param accessor
431 */
432 useController(ControllerClass, accessor) {
433 if (!(accessor && accessor.length > 0)) {
434 throw new Error('You must provide a valid accessor.');
435 }
436 if (this.__proxy__ ? this.__proxy__.hasProp(accessor) : this[accessor]) {
437 throw new Error(`There is already a controller with the accessor '${accessor}'. Please use another one.`);
438 }
439 const controller = new ControllerClass(this);
440 if (!(controller.name && controller.name.length > 0)) {
441 throw new Error('Controllers must have a name.');
442 }
443 if (controller.kuzzle !== this) {
444 throw new Error('You must pass the Kuzzle SDK instance to the parent constructor.');
445 }
446 if (this.__proxy__) {
447 this.__proxy__.registerProp(accessor);
448 }
449 this[accessor] = controller;
450 return this;
451 }
452 _checkPropertyType(prop, typestr, value) {
453 const wrongType = typestr === 'array' ? !Array.isArray(value) : typeof value !== typestr;
454 if (wrongType) {
455 throw new Error(`Expected ${prop} to be a ${typestr}, ${typeof value} received`);
456 }
457 }
458 /**
459 * Clean up the queue, ensuring the queryTTL and queryMaxSize properties are respected
460 */
461 _cleanQueue() {
462 const now = Date.now();
463 let lastDocumentIndex = -1;
464 if (this.queueTTL > 0) {
465 this.offlineQueue.forEach((query, index) => {
466 if (query.ts < now - this.queueTTL) {
467 lastDocumentIndex = index;
468 }
469 });
470 if (lastDocumentIndex !== -1) {
471 this.offlineQueue
472 .splice(0, lastDocumentIndex + 1)
473 .forEach(droppedRequest => {
474 this.emit('offlineQueuePop', droppedRequest.query);
475 });
476 }
477 }
478 if (this.queueMaxSize > 0 && this.offlineQueue.length > this.queueMaxSize) {
479 this.offlineQueue
480 .splice(0, this.offlineQueue.length - this.queueMaxSize)
481 .forEach(droppedRequest => {
482 this.emit('offlineQueuePop', droppedRequest.query);
483 });
484 }
485 }
486 /**
487 * Play all queued requests, in order.
488 */
489 _dequeue() {
490 const uniqueQueue = {}, dequeuingProcess = () => {
491 if (this.offlineQueue.length > 0) {
492 this.protocol.query(this.offlineQueue[0].request)
493 .then(this.offlineQueue[0].resolve)
494 .catch(this.offlineQueue[0].reject);
495 this.emit('offlineQueuePop', this.offlineQueue.shift());
496 setTimeout(() => {
497 dequeuingProcess();
498 }, Math.max(0, this.replayInterval));
499 }
500 };
501 if (this.offlineQueueLoader) {
502 if (typeof this.offlineQueueLoader !== 'function') {
503 throw new Error('Invalid value for offlineQueueLoader property. Expected: function. Got: ' + typeof this.offlineQueueLoader);
504 }
505 return Promise.resolve()
506 .then(() => this.offlineQueueLoader())
507 .then(additionalQueue => {
508 if (Array.isArray(additionalQueue)) {
509 this._offlineQueue = additionalQueue
510 .concat(this.offlineQueue)
511 .filter(query => {
512 // throws if the request does not contain required attributes
513 if (!query.request || query.request.requestId === undefined || !query.request.action || !query.request.controller) {
514 throw new Error('Invalid offline queue request. One or more missing properties: requestId, action, controller.');
515 }
516 return Object.prototype.hasOwnProperty.call(uniqueQueue, query.request.requestId)
517 ? false
518 : (uniqueQueue[query.request.requestId] = true);
519 });
520 dequeuingProcess();
521 }
522 else {
523 throw new Error('Invalid value returned by the offlineQueueLoader function. Expected: array. Got: ' + typeof additionalQueue);
524 }
525 });
526 }
527 dequeuingProcess();
528 }
529}
530exports.Kuzzle = Kuzzle;
531//# sourceMappingURL=Kuzzle.js.map
\No newline at end of file