kuzzle-sdk
Version:
Official Javascript SDK for Kuzzle
387 lines • 14.2 kB
JavaScript
"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