UNPKG

kuzzle-sdk

Version:
784 lines 28.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Kuzzle = void 0; const KuzzleEventEmitter_1 = require("./core/KuzzleEventEmitter"); const Auth_1 = require("./controllers/Auth"); const Bulk_1 = require("./controllers/Bulk"); const Collection_1 = require("./controllers/Collection"); const Document_1 = require("./controllers/Document"); const Index_1 = require("./controllers/Index"); const Realtime_1 = require("./controllers/Realtime"); const Server_1 = require("./controllers/Server"); const Security_1 = require("./controllers/Security"); const MemoryStorage_1 = require("./controllers/MemoryStorage"); const Deprecation_1 = require("./utils/Deprecation"); const uuidv4_1 = require("./utils/uuidv4"); const proxify_1 = require("./utils/proxify"); const debug_1 = require("./utils/debug"); const RequestTimeoutError_1 = require("./RequestTimeoutError"); class Kuzzle extends KuzzleEventEmitter_1.KuzzleEventEmitter { /** * Instantiate a new SDK * * @example * * import { Kuzzle, WebSocket } from 'kuzzle-sdk'; * * const kuzzle = new Kuzzle( * new WebSocket('localhost') * ); */ constructor( /** * Network protocol to connect to Kuzzle. (e.g. `Http` or `WebSocket`) */ protocol, options = {}) { super(); /** * Authenticator function called after a reconnection if the SDK is no longer * authenticated. */ this.authenticator = null; /** * List of every events emitted by the SDK. */ this.events = [ "callbackError", "connected", "discarded", "disconnected", "loginAttempt", "beforeLogin", "afterLogin", "logoutAttempt", "beforeLogout", "afterLogout", "networkError", "offlineQueuePush", "offlineQueuePop", "queryError", "reAuthenticated", "reconnected", "reconnectionError", "tokenExpired", ]; if (protocol === undefined || protocol === null) { throw new Error('"protocol" argument missing'); } // check the existence of required methods for (const method of ["addListener", "isReady", "query"]) { if (typeof protocol[method] !== "function") { throw new Error(`Protocol instance must implement a "${method}" method`); } } this.protocol = protocol; this._protectedEvents = { afterLogin: {}, connected: {}, disconnected: {}, error: {}, loginAttempt: {}, reconnected: {}, tokenExpired: {}, }; this.autoResubscribe = typeof options.autoResubscribe === "boolean" ? options.autoResubscribe : true; this.eventTimeout = typeof options.eventTimeout === "number" ? options.eventTimeout : 200; this.sdkVersion = typeof SDKVERSION === "undefined" ? // eslint-disable-next-line @typescript-eslint/no-var-requires require("../package").version : SDKVERSION; this.sdkName = `js@${this.sdkVersion}`; this.volatile = typeof options.volatile === "object" ? options.volatile : {}; this._cookieAuthentication = typeof options.cookieAuth === "boolean" ? options.cookieAuth : false; if (this._cookieAuthentication) { this.protocol.enableCookieSupport(); let autoQueueState; let autoReplayState; let autoResbuscribeState; this.protocol.addListener("websocketRenewalStart", () => { autoQueueState = this.autoQueue; autoReplayState = this.autoReplay; autoResbuscribeState = this.autoResubscribe; this.autoQueue = true; this.autoReplay = true; this.autoResubscribe = true; }); this.protocol.addListener("websocketRenewalDone", () => { this.autoQueue = autoQueueState; this.autoReplay = autoReplayState; this.autoResubscribe = autoResbuscribeState; }); } this.deprecationHandler = new Deprecation_1.Deprecation(typeof options.deprecationWarning === "boolean" ? options.deprecationWarning : true); if (this._cookieAuthentication && typeof XMLHttpRequest === "undefined") { throw new Error("Support for cookie authentication with cookieAuth option is not supported outside a browser"); } // controllers this.useController(Auth_1.AuthController, "auth"); this.useController(Bulk_1.BulkController, "bulk"); this.useController(Collection_1.CollectionController, "collection"); this.useController(Document_1.DocumentController, "document"); this.useController(Index_1.IndexController, "index"); this.useController(MemoryStorage_1.MemoryStorageController, "ms"); this.useController(Realtime_1.RealtimeController, "realtime"); this.useController(Security_1.SecurityController, "security"); this.useController(Server_1.ServerController, "server"); // offline queue this._offlineQueue = []; this._autoQueue = typeof options.autoQueue === "boolean" ? options.autoQueue : false; this._autoReplay = typeof options.autoReplay === "boolean" ? options.autoReplay : false; this._offlineQueueLoader = typeof options.offlineQueueLoader === "function" ? options.offlineQueueLoader : null; this._queueFilter = typeof options.queueFilter === "function" ? options.queueFilter : null; this._queueMaxSize = typeof options.queueMaxSize === "number" ? options.queueMaxSize : 500; this._queueTTL = typeof options.queueTTL === "number" ? options.queueTTL : 120000; this._replayInterval = typeof options.replayInterval === "number" ? options.replayInterval : 10; this._requestTimeout = typeof options.requestTimeout === "number" ? options.requestTimeout : -1; this._tokenExpiredInterval = typeof options.tokenExpiredInterval === "number" ? options.tokenExpiredInterval : 1000; if (options.offlineMode === "auto") { this._autoQueue = true; this._autoReplay = true; } this._queuing = false; this._lastTokenExpired = null; this._reconnectInProgress = false; this._loggedIn = false; /** * When successfuly logged in */ this.on("afterLogin", async (status) => { if (status.success) { this._loggedIn = true; return; } /** * In case of login failure we need to be sure that the stored token is still valid */ try { this._loggedIn = await this.isAuthenticated(); } catch { this._loggedIn = false; } }); /** * When successfuly logged out */ this.on("afterLogout", (status) => { if (status.success) { this._loggedIn = false; } }); /** * On connection we need to verify if the token is still valid to know if we are still "logged in" */ this.on("connected", async () => { try { this._loggedIn = await this.isAuthenticated(); } catch { this._loggedIn = false; } }); return (0, proxify_1.proxify)(this, { exposeApi: true, name: "kuzzle", seal: true, }); } /** * Returns `true` if the SDK holds a valid token */ get authenticated() { return Boolean(this.auth.authenticationToken && !this.auth.authenticationToken.expired); } get autoQueue() { return this._autoQueue; } set autoQueue(value) { this._checkPropertyType("_autoQueue", "boolean", value); this._autoQueue = value; } get autoReconnect() { const protocol = this.protocol; return protocol.autoReconnect; } set autoReconnect(value) { this._checkPropertyType("autoReconnect", "boolean", value); const protocol = this.protocol; protocol.autoReconnect = value; } get autoReplay() { return this._autoReplay; } set autoReplay(value) { this._checkPropertyType("_autoReplay", "boolean", value); this._autoReplay = value; } /** * Returns `true` if the SDK is using the cookie authentication mode. * (Web only) */ get cookieAuthentication() { return this._cookieAuthentication; } /** * Returns `true` if the SDK is currently connected to a Kuzzle server. */ get connected() { return this.protocol.connected; } get host() { return this.protocol.host; } get jwt() { if (!this.auth.authenticationToken) { return null; } return this.auth.authenticationToken.encodedJwt; } set jwt(encodedJwt) { this.auth.authenticationToken = encodedJwt; this._loggedIn = encodedJwt ? true : false; } get offlineQueue() { return this._offlineQueue; } get offlineQueueLoader() { return this._offlineQueueLoader; } set offlineQueueLoader(value) { this._checkPropertyType("_offlineQueueLoader", "function", value); this._offlineQueueLoader = value; } get port() { return this.protocol.port; } get queueFilter() { return this._queueFilter; } set queueFilter(value) { this._checkPropertyType("_queueFilter", "function", value); this._queueFilter = value; } get queueMaxSize() { return this._queueMaxSize; } set queueMaxSize(value) { this._checkPropertyType("_queueMaxSize", "number", value); this._queueMaxSize = value; } get queueTTL() { return this._queueTTL; } set queueTTL(value) { this._checkPropertyType("_queueTTL", "number", value); this._queueTTL = value; } get reconnectionDelay() { const protocol = this.protocol; return protocol.reconnectionDelay; } get replayInterval() { return this._replayInterval; } set replayInterval(value) { this._checkPropertyType("_replayInterval", "number", value); this._replayInterval = value; } get requestTimeout() { return this._requestTimeout; } set requestTimeout(value) { this._checkPropertyType("_requestTimeout", "number", value); this._requestTimeout = value; } get sslConnection() { return this.protocol.sslConnection; } get tokenExpiredInterval() { return this._tokenExpiredInterval; } set tokenExpiredInterval(value) { this._checkPropertyType("_tokenExpiredInterval", "number", value); this._tokenExpiredInterval = value; } /** * Emit an event to all registered listeners * An event cannot be emitted multiple times before a timeout has been reached. */ emit(eventName, ...payload) { const now = Date.now(), protectedEvent = this._protectedEvents[eventName]; if (protectedEvent) { if (protectedEvent.lastEmitted && protectedEvent.lastEmitted > now - this.eventTimeout) { return false; } protectedEvent.lastEmitted = now; } return this._superEmit(eventName, ...payload); } _superEmit(eventName, ...payload) { return super.emit(eventName, ...payload); } on(eventName, listener) { return super.on(eventName, listener); } /** * Connects to a Kuzzle instance */ connect() { if (this.protocol.isReady()) { return Promise.resolve(); } if (this.autoQueue) { this.startQueuing(); } this.protocol.addListener("queryError", ({ error, request }) => { this.emit("queryError", { error, request }); }); this.protocol.addListener("tokenExpired", () => this.tokenExpired()); this.protocol.addListener("connect", () => { if (this.autoQueue) { this.stopQueuing(); } if (this.autoReplay) { this.playQueue(); } this.emit("connected"); }); this.protocol.addListener("networkError", (error) => { if (this.autoQueue) { this.startQueuing(); } this.emit("networkError", error); }); this.protocol.addListener("disconnect", (context) => { this.emit("disconnected", context); }); this.protocol.addListener("reconnect", this._reconnect.bind(this)); this.protocol.addListener("discarded", (data) => this.emit("discarded", data)); this.protocol.addListener("websocketRenewalStart", () => { this._reconnectInProgress = true; }); this.protocol.addListener("websocketRenewalDone", () => { this._reconnectInProgress = false; }); return this.protocol.connect(); } /** * Set this client to use a specific API key. * * After doing this you don't need to use login as it bypasses the authentication process. */ setAPIKey(apiKey) { if (apiKey.match(/^kapikey-/) === null) { throw new Error("Invalid API key. Missing the `kapikey-` prefix."); } this.jwt = apiKey; } async _reconnect() { if (this._reconnectInProgress) { return; } if (this.autoQueue) { this.stopQueuing(); } // If an authenticator was set, check if a user was logged in and if the token is still valid and try // to re-authenticate if needed. Otherwise the SDK is in disconnected state. if (this._loggedIn && !(await this.tryReAuthenticate())) { this._loggedIn = false; this.disconnect(); return; } if (this.autoReplay) { this.playQueue(); } this.emit("reconnected"); } /** * Try to re-authenticate the SDK if the current token is invalid. * * If the token is invalid, this method will return false and emit a * "reconnectionError" event when: * - the SDK cannot re-authenticate using the authenticator function * - the authenticator function is not set * * This method never returns a rejected promise. */ async tryReAuthenticate() { this._reconnectInProgress = true; try { const valid = await this.isAuthenticated(); if (valid) { return true; } /** * Check if there is an authenticator after verifying if the token is still valid, * like so API Keys can be used even if there is no authenticator since they will be still valid. */ if (!this.authenticator) { this.emit("reconnectionError", { error: new Error('Could not re-authenticate: "authenticator" property is not set.'), }); return false; } await this.authenticate(); return true; } catch (err) { this.emit("reconnectionError", { error: new Error(`Failed to authenticate the SDK after reconnection: ${err}`), }); return false; } finally { this._reconnectInProgress = false; } } /** * Use the "authenticator" function to authenticate the SDK. * * @returns The authentication token */ async authenticate() { if (typeof this.authenticator !== "function") { throw new Error('The "authenticator" property must be a function.'); } await this.authenticator(); const valid = await this.isAuthenticated(); this._loggedIn = valid; if (!valid) { throw new Error('The "authenticator" function failed to authenticate the SDK.'); } } /** * Check wether the user is authenticated or not * by verifiying if a token is present and still valid * and if the token doesn't belong to the anonymous user. */ async isAuthenticated() { const { valid, kuid } = await this.auth.checkToken(); return valid && kuid !== "-1"; } /** * Adds a listener to a Kuzzle global event. When an event is fired, listeners are called in the order of their * insertion. * * @param {string} event - name of the global event to subscribe to * @param {function} listener - callback to invoke each time an event is fired */ addListener(event, listener) { if (this.events.indexOf(event) === -1) { throw new Error(`[${event}] is not a known event. Known events: ${this.events.join(", ")}`); } return this._superAddListener(event, listener); } _superAddListener(event, listener) { return super.addListener(event, listener); } /** * Empties the offline queue without replaying it. * * @returns {Kuzzle} */ flushQueue() { this._offlineQueue = []; return this; } /** * Disconnects from Kuzzle and invalidate this instance. */ disconnect() { this._loggedIn = false; this.protocol.close(); } /** * This is a low-level method, exposed to allow advanced SDK users to bypass * high-level methods. * Base method used to send read queries to Kuzzle * * Takes an optional argument object with the following properties: * - volatile (object, default: null): * Additional information passed to notifications to other users * * @param req * @param opts - Optional arguments */ query(req, opts = {}) { if (typeof req !== "object" || Array.isArray(req)) { throw new Error(`Kuzzle.query: Invalid request: ${JSON.stringify(req)}`); } if (typeof opts !== "object" || Array.isArray(opts)) { throw new Error(`Kuzzle.query: Invalid "options" argument: ${JSON.stringify(opts)}`); } const request = JSON.parse(JSON.stringify(req)); const options = JSON.parse(JSON.stringify(opts)); if (!request.requestId) { request.requestId = (0, uuidv4_1.uuidv4)(); } let queuable = true; if (options && options.queuable === false) { queuable = false; } if (this.queueFilter) { queuable = queuable && this.queueFilter(request); } const requestTimeout = typeof options.timeout === "number" ? options.timeout : this._requestTimeout; for (const [key, value] of Object.entries(options)) { // Ignore common SDK option if (["queuable", "timeout"].includes(key)) { continue; } request[key] = value; } if (request.refresh === undefined && options.refresh !== undefined) { request.refresh = options.refresh; } if (request.retryOnConflict === undefined && options.retryOnConflict !== undefined) { request.retryOnConflict = options.retryOnConflict; } if (!request.volatile) { request.volatile = this.volatile; } else if (typeof request.volatile !== "object" || Array.isArray(request.volatile)) { throw new Error(`Kuzzle.query: Invalid volatile argument received: ${JSON.stringify(request.volatile)}`); } for (const item of Object.keys(this.volatile)) { if (request.volatile[item] === undefined) { request.volatile[item] = this.volatile[item]; } } request.volatile.sdkInstanceId = request.volatile.sdkInstanceId || this.protocol.id; request.volatile.sdkName = request.volatile.sdkName || this.sdkName; this.auth.authenticateRequest(request); if (this._queuing) { if (queuable) { this._cleanQueue(); this.emit("offlineQueuePush", { request }); return new Promise((resolve, reject) => { this.offlineQueue.push({ reject, request, resolve, timeout: requestTimeout, ts: Date.now(), }); }); } this.emit("discarded", { request }); return Promise.reject(new Error(`Unable to execute request: not connected to a Kuzzle server. Discarded request: ${JSON.stringify(request)}`)); } return this._timeoutRequest(requestTimeout, request, options).then((response) => { (0, debug_1.debug)("RESPONSE", response); return this.deprecationHandler.logDeprecation(response); }); } /** * Starts the requests queuing. */ startQueuing() { this._queuing = true; return this; } /** * Stops the requests queuing. */ stopQueuing() { this._queuing = false; return this; } /** * Plays the requests queued during offline mode. */ playQueue() { if (this.protocol.isReady()) { this._cleanQueue(); this._dequeue(); } return this; } /** * On token expiration, reset jwt and unsubscribe all rooms. * Throttles to avoid duplicate event triggers. */ async tokenExpired() { if (this._reconnectInProgress) { return; } if (this._loggedIn && (await this.tryReAuthenticate())) { this.emit("reAuthenticated"); return; } const now = Date.now(); if (now - this._lastTokenExpired < this.tokenExpiredInterval) { // event was recently already fired return; } this._lastTokenExpired = now; this.jwt = null; this.emit("tokenExpired"); } /** * Adds a new controller and make it available in the SDK. * * @param ControllerClass * @param accessor */ useController(ControllerClass, accessor) { if (!(accessor && accessor.length > 0)) { throw new Error("You must provide a valid accessor."); } if (this.__proxy__ ? this.__proxy__.hasProp(accessor) : this[accessor]) { throw new Error(`There is already a controller with the accessor '${accessor}'. Please use another one.`); } const controller = new ControllerClass(this); if (!(controller.name && controller.name.length > 0)) { throw new Error("Controllers must have a name."); } if (controller.kuzzle !== this) { throw new Error("You must pass the Kuzzle SDK instance to the parent constructor."); } if (this.__proxy__) { this.__proxy__.registerProp(accessor); } this[accessor] = controller; return this; } _checkPropertyType(prop, typestr, value) { const wrongType = typestr === "array" ? !Array.isArray(value) : typeof value !== typestr; if (wrongType) { throw new Error(`Expected ${prop} to be a ${typestr}, ${typeof value} received`); } } /** * Clean up the queue, ensuring the queryTTL and queryMaxSize properties are respected */ _cleanQueue() { const now = Date.now(); let lastDocumentIndex = -1; if (this.queueTTL > 0) { this.offlineQueue.forEach((query, index) => { if (query.ts < now - this.queueTTL) { lastDocumentIndex = index; } }); if (lastDocumentIndex !== -1) { this.offlineQueue .splice(0, lastDocumentIndex + 1) .forEach((droppedRequest) => { this.emit("offlineQueuePop", droppedRequest.request); droppedRequest.reject(new Error("Query aborted: queued time exceeded the queueTTL option value")); }); } } if (this.queueMaxSize > 0 && this.offlineQueue.length > this.queueMaxSize) { this.offlineQueue .splice(0, this.offlineQueue.length - this.queueMaxSize) .forEach((droppedRequest) => { this.emit("offlineQueuePop", droppedRequest.request); droppedRequest.reject(new Error("Query aborted: too many queued requests (see the queueMaxSize option)")); }); } } /** * Play all queued requests, in order. */ _dequeue() { const uniqueQueue = {}, dequeuingProcess = () => { if (this.offlineQueue.length > 0) { // Reapply the jwt to the request since it could have been expired this.offlineQueue[0].request.jwt = this.jwt; this._timeoutRequest(this.offlineQueue[0].timeout, this.offlineQueue[0].request) .then(this.offlineQueue[0].resolve) .catch(this.offlineQueue[0].reject); this.emit("offlineQueuePop", this.offlineQueue.shift().request); setTimeout(() => { dequeuingProcess(); }, Math.max(0, this.replayInterval)); } }; if (this.offlineQueueLoader) { if (typeof this.offlineQueueLoader !== "function") { throw new Error("Invalid value for offlineQueueLoader property. Expected: function. Got: " + typeof this.offlineQueueLoader); } return Promise.resolve() .then(() => this.offlineQueueLoader()) .then((additionalQueue) => { if (Array.isArray(additionalQueue)) { this._offlineQueue = additionalQueue .concat(this.offlineQueue) .filter((query) => { // throws if the request does not contain required attributes if (!query.request || query.request.requestId === undefined || !query.request.action || !query.request.controller) { throw new Error("Invalid offline queue request. One or more missing properties: requestId, action, controller."); } return Object.prototype.hasOwnProperty.call(uniqueQueue, query.request.requestId) ? false : (uniqueQueue[query.request.requestId] = true); }); dequeuingProcess(); } else { throw new Error("Invalid value returned by the offlineQueueLoader function. Expected: array. Got: " + typeof additionalQueue); } }); } dequeuingProcess(); } /** * Sends a request with a timeout * * @param delay Delay before the request is rejected if not resolved * @param request Request object * @param options Request options * @returns Resolved request or a TimedOutError */ _timeoutRequest(delay, request, options = {}) { (0, debug_1.debug)("REQUEST", request); // No timeout if (delay === -1) { return this.protocol.query(request, options); } const timeout = new Promise((resolve, reject) => { setTimeout(() => { reject(new RequestTimeoutError_1.RequestTimeoutError(request, delay)); }, delay); }); return Promise.race([timeout, this.protocol.query(request, options)]); } } exports.Kuzzle = Kuzzle; //# sourceMappingURL=Kuzzle.js.map