UNPKG

kuzzle-sdk

Version:
387 lines 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Observer = void 0; const RealtimeDocument_1 = require("./RealtimeDocument"); const RealtimeDocument_2 = require("./searchResult/RealtimeDocument"); /** * Class based on a Set<string> that holds the observed documents IDs of * a specific collection. * * @internal */ class ObservedDocuments extends Set { /** * Gets documents IDs */ get ids() { return Array.from(this.values()); } /** * Gets Koncorde filters for observed documents */ get filters() { return { ids: { values: Array.from(this.values()) }, }; } constructor(index, collection) { super(); /** * Room ID for the realtime subscription on the collection of observed documents */ this.roomId = null; this.index = index; this.collection = collection; } } /** * Provide an Uniform Resource Locator given an index, a collection and a document */ function documentUrn(index, collection, id) { return `${index}:${collection}:${id}`; } /** * Provide an Uniform Resource Locator given an index and a collection */ function collectionUrn(index, collection) { return `${index}:${collection}`; } /** * The Observer class allows to manipulate realtime documents. * * A RealtimeDocument is like a normal document from Kuzzle except that it is * connected to the realtime engine and its content will change with changes * occuring on the database. * * They can be retrieved using methods with the same syntax as in the Document * Controller: * * ```js * const docs = await observer.get('montenegro', 'budva', 'foobar'); * * const result = await observer.search('montenegro', 'budva', { query: { exists: 'beaches' } }); * ``` * * Realtime documents are resources that should be disposed either with the * stop() or the dispose() method otherwise subscriptions will never be * terminated, documents will be keep into memory and you will end with a * memory leak. * * ```js * await observer.stop('nyc-open-data', 'yellow-taxi'); * ``` * * A good frontend practice is to instantiate one observer for the actual page * and/or component(s) displaying realtime documents and to dispose them when * they are not displayed anymore. * * If the SDK is using the HTTP protocol, then documents are retrieved through the * document.mGet method every specified interval (default is 5 sec). This interval * can be modified with the `pullingDelay` option of the constructor. */ class Observer { /** * Instantiate a new Observer * * @param sdk SDK instance */ constructor(sdk, options) { /** * Map used to keep track of the observed documents ids by collections. * * @internal */ this.documentsByCollection = new Map(); /** * Map containing the list of realtime documents managed by this observer. * * This map is used to update realtime documents content when notifications * are received. * * @internal */ this.documents = new Map(); /** * @internal */ this.options = { pullingDelay: 5000, }; Reflect.defineProperty(this, "sdk", { value: sdk, }); this.options = { ...this.options, ...options }; this.mode = this.sdk.protocol.name === "http" ? "pulling" : "realtime"; } /** * Stop observing documents and release associated ressources. * * Can be used either with: * - a list of documents from a collection: stop observing those documents * - an index and collection: stop observing all documents in the collection * * @param index Index name * @param collection Collection name * @param documents Array of documents * */ stop(index, collection, documents) { if (index && collection && documents) { return this.disposeDocuments(index, collection, documents); } if (index && collection) { return this.disposeCollection(index, collection); } if (index) { return Promise.reject(new Error('Missing "collection" argument"')); } return this.disposeAll(); } disposeDocuments(index, collection, documents) { const observedDocuments = this.documentsByCollection.get(collectionUrn(index, collection)); for (const document of documents) { this.documents.delete(documentUrn(index, collection, document._id)); observedDocuments.delete(document._id); } if (observedDocuments.size === 0) { this.documentsByCollection.delete(collectionUrn(index, collection)); } return this.watchCollection(index, collection); } disposeCollection(index, collection) { const observedDocuments = this.documentsByCollection.get(collectionUrn(index, collection)); for (const id of observedDocuments.ids) { this.documents.delete(documentUrn(index, collection, id)); } this.documentsByCollection.delete(collectionUrn(index, collection)); if (this.mode === "realtime") { return this.sdk.realtime.unsubscribe(observedDocuments.roomId); } this.clearPullingTimer(); return Promise.resolve(); } /** * Unsubscribe from every collections and clear all the realtime documents. * * @internal */ disposeAll() { const promises = []; for (const observedDocuments of this.documentsByCollection.values()) { if (observedDocuments.roomId) { promises.push(this.sdk.realtime.unsubscribe(observedDocuments.roomId)); } } if (this.mode === "pulling") { promises.push(this.clearPullingTimer()); } this.documentsByCollection.clear(); this.documents.clear(); // eslint-disable-next-line @typescript-eslint/no-empty-function return Promise.all(promises).then(() => { }); } /** * Gets a realtime document * * @param index Index name * @param collection Collection name * @param id Document ID * @param options Additional options * * @returns The realtime document */ get(index, collection, id, options = {}) { return this.sdk.document .get(index, collection, id, options) .then((document) => this.observe(index, collection, document)); } /** * * Gets multiple realtime documents. * * @param index Index name * @param collection Collection name * @param ids Document IDs * @param options Additional options * * @returns An object containing 2 arrays: "successes" and "errors" */ mGet(index, collection, ids, options = {}) { const rtDocuments = []; let _errors; return this.sdk.document .mGet(index, collection, ids, options) .then(({ successes, errors }) => { _errors = errors; for (const document of successes) { rtDocuments.push(this.addDocument(index, collection, document)); } return this.watchCollection(index, collection); }) .then(() => ({ errors: _errors, successes: rtDocuments })); } /** * Searches for documents and returns a SearchResult containing realtime * documents. * * @param index Index name * @param collection Collection name * @param searchBody Search query * @param options Additional options * * @returns A SearchResult containing realtime documents */ search(index, collection, searchBody = {}, options = {}) { // eslint-disable-next-line dot-notation return this.sdk.document["_search"](index, collection, searchBody, options).then(({ response, request, opts }) => { const result = new RealtimeDocument_2.RealtimeDocumentSearchResult(this.sdk, request, opts, response.result, this); return result.start(); }); } /** * Retrieve a realtime document from a document * * @param index Index name * @param collection Collection name * @param document Document to observe * * @returns A realtime document */ observe(index, collection, document) { const rtDocument = this.addDocument(index, collection, document); return this.watchCollection(index, collection).then(() => rtDocument); } /** * Adds a document and retrieve managed realtime document * * @internal * * Use observe() to retrieve a realtime document. */ addDocument(index, collection, document) { const rtDocument = new RealtimeDocument_1.RealtimeDocument(document); const urn = collectionUrn(index, collection); if (!this.documentsByCollection.has(urn)) { this.documentsByCollection.set(urn, new ObservedDocuments(index, collection)); } const observedDocuments = this.documentsByCollection.get(urn); observedDocuments.add(document._id); this.documents.set(documentUrn(index, collection, document._id), rtDocument); return rtDocument; } /** * Start subscription or pulling on the collection * * @internal */ watchCollection(index, collection) { if (this.mode === "realtime") { return this.resubscribe(index, collection); } this.restartPulling(); return Promise.resolve(); } restartPulling() { this.clearPullingTimer(); if (this.documentsByCollection.size !== 0) { this.pullingTimer = setInterval(this.pullingHandler.bind(this), this.options.pullingDelay); } } /** * Use the document.mGet method to pull documents from observed collections * and update internal realtime documents. * * This method never returns a rejected promise. */ pullingHandler() { const promises = []; for (const observedDocuments of this.documentsByCollection.values()) { const promise = this.sdk.document .mGet(observedDocuments.index, observedDocuments.collection, observedDocuments.ids) .then(({ successes, errors }) => { for (const document of successes) { const urn = documentUrn(observedDocuments.index, observedDocuments.collection, document._id); const rtDocument = this.documents.get(urn); Object.assign(rtDocument._source, document._source); } for (const deletedDocumentId of errors) { const urn = documentUrn(observedDocuments.index, observedDocuments.collection, deletedDocumentId); const rtDocument = this.documents.get(urn); rtDocument.deleted = true; this.documents.delete(urn); observedDocuments.delete(deletedDocumentId); if (observedDocuments.size === 0) { this.documentsByCollection.delete(collectionUrn(observedDocuments.index, observedDocuments.collection)); } } }) .catch(() => { // A `queryError` event is already emitted by the protocol // This handler ensure we don't have any unhandledRejection error }); promises.push(promise); } // eslint-disable-next-line @typescript-eslint/no-empty-function return Promise.all(promises).then(() => { }); } clearPullingTimer() { if (this.pullingTimer) { clearInterval(this.pullingTimer); } } /** * Renew a collection subscription with filters according to the list of * currently managed documents. * * @internal */ resubscribe(index, collection) { const observedDocuments = this.documentsByCollection.get(collectionUrn(index, collection)); if (!observedDocuments) { return Promise.resolve(); } // Do not resubscribe if there is no documents if (observedDocuments.size === 0) { return observedDocuments.roomId ? this.sdk.realtime.unsubscribe(observedDocuments.roomId) : Promise.resolve(); } return this.sdk.realtime .subscribe(index, collection, observedDocuments.filters, this.notificationHandler.bind(this)) .then((roomId) => { const oldRoomId = observedDocuments.roomId; observedDocuments.roomId = roomId; if (oldRoomId) { return this.sdk.realtime.unsubscribe(oldRoomId); } }); } /** * Handler method to process notifications and update realtime documents content. * * @internal */ notificationHandler(notification) { const { index, collection, result } = notification; const urn = documentUrn(index, collection, result._id); const rtDocument = this.documents.get(urn); // On "write", mutate document with changes // On "publish", nothing if (notification.event !== "delete") { if (notification.event === "write") { Object.assign(rtDocument._source, result._source); } return Promise.resolve(); } rtDocument.deleted = true; this.documents.delete(documentUrn(index, collection, rtDocument._id)); const observedDocuments = this.documentsByCollection.get(collectionUrn(index, collection)); observedDocuments.delete(result._id); if (observedDocuments.size === 0) { this.documentsByCollection.delete(collectionUrn(index, collection)); } return this.resubscribe(index, collection); } } exports.Observer = Observer; //# sourceMappingURL=Observer.js.map