UNPKG

ravendb

Version:
510 lines 24.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DatabaseChanges = void 0; const DatabaseConnectionState_js_1 = require("./DatabaseConnectionState.js"); const ChangesObservable_js_1 = require("./ChangesObservable.js"); const index_js_1 = require("../../Exceptions/index.js"); const ws_1 = require("ws"); const StringUtil_js_1 = require("../../Utility/StringUtil.js"); const node_events_1 = require("node:events"); const PromiseUtil_js_1 = require("../../Utility/PromiseUtil.js"); const SemaphoreUtil_js_1 = require("../../Utility/SemaphoreUtil.js"); const Certificate_js_1 = require("../../Auth/Certificate.js"); const ObjectUtil_js_1 = require("../../Utility/ObjectUtil.js"); const ServerNode_js_1 = require("../../Http/ServerNode.js"); const UpdateTopologyParameters_js_1 = require("../../Http/UpdateTopologyParameters.js"); const TypeUtil_js_1 = require("../../Utility/TypeUtil.js"); const AggressiveCacheChange_js_1 = require("./AggressiveCacheChange.js"); const Semaphore_js_1 = require("../../Utility/Semaphore.js"); const DateUtil_js_1 = require("../../Utility/DateUtil.js"); class DatabaseChanges { _emitter = new node_events_1.EventEmitter(); _commandId = 0; _onConnectionStatusChangedWrapped; _semaphore = new Semaphore_js_1.Semaphore(); _requestExecutor; _conventions; _database; _onDispose; _client; _task; _isCanceled = false; _tcs; _confirmations = new Map(); _counters = new Map(); //TODO: use DatabaseChangesOptions as key? _immediateConnection = 0; _serverNode; _nodeIndex; _url; constructor(requestExecutor, databaseName, onDispose, nodeTag) { this._requestExecutor = requestExecutor; this._conventions = requestExecutor.conventions; this._database = databaseName; this._tcs = (0, PromiseUtil_js_1.defer)(); this._onDispose = onDispose; this._onConnectionStatusChangedWrapped = () => this._onConnectionStatusChanged(); this._emitter.on("connectionStatus", this._onConnectionStatusChangedWrapped); this._task = this._doWork(nodeTag); } static async createClientWebSocket(requestExecutor, url) { const authOptions = requestExecutor.getAuthOptions(); let options = undefined; if (authOptions) { const certificate = Certificate_js_1.Certificate.createFromOptions(authOptions); options = certificate.toWebSocketOptions(); } const { WebSocket } = await import("ws"); return new WebSocket(url, options); } async _onConnectionStatusChanged() { const acquiredSemContext = (0, SemaphoreUtil_js_1.acquireSemaphore)(this._semaphore); try { await acquiredSemContext.promise; if (this.connected) { this._tcs.resolve(this); return; } if (this._tcs.isFulfilled) { this._tcs = (0, PromiseUtil_js_1.defer)(); } } finally { acquiredSemContext.dispose(); } } get connected() { return this._client && this._client.readyState === ws_1.WebSocket.OPEN; } on(eventName, handler) { this._emitter.addListener(eventName, handler); return this; } off(eventName, handler) { this._emitter.removeListener(eventName, handler); return this; } ensureConnectedNow() { return Promise.resolve(this._tcs.promise); } forIndex(indexName) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(indexName)) { (0, index_js_1.throwError)("InvalidArgumentException", "IndexName cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("indexes/" + indexName, "watch-index", "unwatch-index", indexName); return new ChangesObservable_js_1.ChangesObservable("Index", counter, notification => notification.name && notification.name.toLocaleLowerCase() === indexName.toLocaleLowerCase()); } get lastConnectionStateException() { for (const counter of Array.from(this._counters.values())) { if (counter.lastError) { return counter.lastError; } } return null; } forDocument(docId) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(docId)) { (0, index_js_1.throwError)("InvalidArgumentException", "DocumentId cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("docs/" + docId, "watch-doc", "unwatch-doc", docId); return new ChangesObservable_js_1.ChangesObservable("Document", counter, notification => notification.id && notification.id.toLocaleLowerCase() === docId.toLocaleLowerCase()); } forAllDocuments() { const counter = this._getOrAddConnectionState("all-docs", "watch-docs", "unwatch-docs", null); return new ChangesObservable_js_1.ChangesObservable("Document", counter, () => true); } forOperationId(operationId) { const counter = this._getOrAddConnectionState("operations/" + operationId, "watch-operation", "unwatch-operation", operationId.toString()); return new ChangesObservable_js_1.ChangesObservable("Operation", counter, notification => notification.operationId === operationId); } forAllOperations() { const counter = this._getOrAddConnectionState("all-operations", "watch-operations", "unwatch-operations", null); return new ChangesObservable_js_1.ChangesObservable("Operation", counter, () => true); } forAllIndexes() { const counter = this._getOrAddConnectionState("all-indexes", "watch-indexes", "unwatch-indexes", null); return new ChangesObservable_js_1.ChangesObservable("Index", counter, () => true); } forDocumentsStartingWith(docIdPrefix) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(docIdPrefix)) { (0, index_js_1.throwError)("InvalidArgumentException", "DocumentId cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("prefixes/" + docIdPrefix, "watch-prefix", "unwatch-prefix", docIdPrefix); return new ChangesObservable_js_1.ChangesObservable("Document", counter, notification => notification.id && notification.id.toLocaleLowerCase().startsWith(docIdPrefix.toLocaleLowerCase())); } forDocumentsInCollection(collectionNameOrDescriptor) { const collectionName = typeof collectionNameOrDescriptor !== "string" ? this._conventions.getCollectionNameForType(collectionNameOrDescriptor) : collectionNameOrDescriptor; if (!collectionName) { (0, index_js_1.throwError)("InvalidArgumentException", "CollectionName cannot be null"); } const counter = this._getOrAddConnectionState("collections/" + collectionName, "watch-collection", "unwatch-collection", collectionName); return new ChangesObservable_js_1.ChangesObservable("Document", counter, notification => notification.collectionName && collectionName.toLocaleLowerCase() === notification.collectionName.toLocaleLowerCase()); } dispose() { for (const confirmation of this._confirmations.values()) { confirmation.reject(); } this._isCanceled = true; if (this._client) { this._client.close(); } for (const value of this._counters.values()) { value.dispose(); } this._counters.clear(); this._emitter.emit("connectionStatus"); this._emitter.removeListener("connectionStatus", this._onConnectionStatusChangedWrapped); if (this._onDispose) { this._onDispose(); } } _getOrAddConnectionState(name, watchCommand, unwatchCommand, value, values = null) { let newValue = false; let counter; if (!this._counters.has(name)) { const connectionState = new DatabaseConnectionState_js_1.DatabaseConnectionState(() => this._send(watchCommand, value, values), async () => { try { if (this.connected) { await this._send(unwatchCommand, value, values); } } catch { // if we are not connected then we unsubscribed already // because connections drops with all subscriptions } const state = this._counters.get(name); this._counters.delete(name); state.dispose(); }); this._counters.set(name, connectionState); counter = connectionState; newValue = true; } else { counter = this._counters.get(name); } if (newValue && this._immediateConnection) { counter.set(counter.onConnect()); } return counter; } _send(command, value, values) { // eslint-disable-next-line no-async-promise-executor return new Promise((async (resolve, reject) => { let currentCommandId; const acquiredSemContext = (0, SemaphoreUtil_js_1.acquireSemaphore)(this._semaphore, { timeout: 15000, contextName: "DatabaseChanges._send()" }); try { await acquiredSemContext.promise; currentCommandId = ++this._commandId; const payload = { CommandId: currentCommandId, Command: command, Param: value }; if (values && values.length) { payload["Params"] = values; } this._confirmations.set(currentCommandId, { resolve, reject }); const payloadAsString = JSON.stringify(payload, null, 0); this._client.send(payloadAsString); } catch (err) { if (!this._isCanceled) { throw err; } } finally { if (acquiredSemContext) { acquiredSemContext.dispose(); } } })); } async _doWork(nodeTag) { let preferredNode; try { preferredNode = nodeTag || this._requestExecutor.conventions.disableTopologyUpdates ? await this._requestExecutor.getRequestedNode(nodeTag) : await this._requestExecutor.getPreferredNode(); this._nodeIndex = preferredNode.currentIndex; this._serverNode = preferredNode.currentNode; } catch (e) { this._emitter.emit("connectionStatus"); this._notifyAboutError(e); this._tcs.reject(e); return; } await this._doWorkInternal(); } async _doWorkInternal() { if (this._isCanceled) { return; } let wasConnected = false; if (!this.connected) { const urlString = this._serverNode.url + "/databases/" + this._database + "/changes"; const url = StringUtil_js_1.StringUtil.toWebSocketPath(urlString); this._client = await DatabaseChanges.createClientWebSocket(this._requestExecutor, url); this._client.on("open", async () => { wasConnected = true; this._immediateConnection = 1; for (const counter of this._counters.values()) { counter.set(counter.onConnect()); } this._emitter.emit("connectionStatus"); }); this._client.on("error", async (e) => { if (wasConnected) { this._emitter.emit("connectionStatus"); } wasConnected = false; try { this._serverNode = await this._requestExecutor.handleServerNotResponsive(this._url, this._serverNode, this._nodeIndex, e); } catch (ee) { if (ee.name === "DatabaseDoesNotExistException") { e = ee; throw ee; } else { //We don't want to stop observe for changes if server down. we will wait for one to be up } } this._notifyAboutError(e); }); this._client.on("close", () => { if (this._reconnectClient()) { setTimeout(() => this._doWorkInternal(), 1000); } for (const confirm of this._confirmations.values()) { confirm.reject(); } this._confirmations.clear(); }); this._client.on("message", async (data) => { await this._processChanges(data); }); } } _reconnectClient() { if (this._isCanceled) { return false; } this._client.close(); this._immediateConnection = 0; return true; } async _processChanges(data) { if (this._isCanceled) { return; } const payloadParsed = JSON.parse(data); try { const messages = Array.isArray(payloadParsed) ? payloadParsed : [payloadParsed]; for (const message of messages) { const type = message.Type; if (message.TopologyChange) { const state = this._getOrAddConnectionState("Topology", "watch-topology-change", "", ""); state.addOnError(TypeUtil_js_1.TypeUtil.NOOP); const updateParameters = new UpdateTopologyParameters_js_1.UpdateTopologyParameters(this._serverNode); updateParameters.timeoutInMs = 0; updateParameters.forceUpdate = true; updateParameters.debugTag = "watch-topology-change"; // noinspection ES6MissingAwait this._requestExecutor.updateTopology(updateParameters); continue; } if (!type) { continue; } switch (type) { case "Error": { const exceptionAsString = message.Exception; this._notifyAboutError(exceptionAsString); break; } case "Confirm": { const commandId = message.CommandId; const confirmationResolver = this._confirmations.get(commandId); if (confirmationResolver) { confirmationResolver.resolve(); this._confirmations.delete(commandId); } break; } default: { const value = message.Value; let transformedValue = ObjectUtil_js_1.ObjectUtil.transformObjectKeys(value, { defaultTransform: ObjectUtil_js_1.ObjectUtil.camel }); if (type === "TimeSeriesChange") { const timeSeriesValue = transformedValue; const overrides = { from: DateUtil_js_1.DateUtil.utc.parse(timeSeriesValue.from), to: DateUtil_js_1.DateUtil.utc.parse(timeSeriesValue.to) }; transformedValue = Object.assign(transformedValue, overrides); } this._notifySubscribers(type, transformedValue); break; } } } } catch (err) { this._notifyAboutError(err); (0, index_js_1.throwError)("ChangeProcessingException", "There was an error during notification processing.", err); } } _notifySubscribers(type, value) { switch (type) { case "AggressiveCacheChange": { for (const state of this._counters.values()) { state.send("AggressiveCache", AggressiveCacheChange_js_1.AggressiveCacheChange.INSTANCE); } break; } case "DocumentChange": { for (const state of this._counters.values()) { state.send("Document", value); } break; } case "CounterChange": { for (const state of this._counters.values()) { state.send("Counter", value); } break; } case "TimeSeriesChange": { for (const state of this._counters.values()) { state.send("TimeSeries", value); } break; } case "IndexChange": { for (const state of this._counters.values()) { state.send("Index", value); } break; } case "OperationStatusChange": { for (const state of this._counters.values()) { state.send("Operation", value); } break; } case "TopologyChange": { const topologyChange = value; const requestExecutor = this._requestExecutor; if (requestExecutor) { const node = new ServerNode_js_1.ServerNode({ url: topologyChange.url, database: topologyChange.database }); const updateParameters = new UpdateTopologyParameters_js_1.UpdateTopologyParameters(node); updateParameters.timeoutInMs = 0; updateParameters.forceUpdate = true; updateParameters.debugTag = "topology-change-notification"; // noinspection JSIgnoredPromiseFromCall requestExecutor.updateTopology(updateParameters); } break; } default: { (0, index_js_1.throwError)("NotSupportedException"); } } } _notifyAboutError(e) { if (this._isCanceled) { return; } this._emitter.emit("error", e); for (const state of this._counters.values()) { state.error(e); } } forAllCounters() { const counter = this._getOrAddConnectionState("all-counters", "watch-counters", "unwatch-counters", null); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("Counter", counter, notification => true); return taskedObservable; } forCounter(counterName) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(counterName)) { (0, index_js_1.throwError)("InvalidArgumentException", "CounterName cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("counter/" + counterName, "watch-counter", "unwatch-counter", counterName); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("Counter", counter, notification => StringUtil_js_1.StringUtil.equalsIgnoreCase(counterName, notification.name)); return taskedObservable; } forCounterOfDocument(documentId, counterName) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(documentId)) { (0, index_js_1.throwError)("InvalidArgumentException", "DocumentId cannot be null or whitespace."); } if (StringUtil_js_1.StringUtil.isNullOrWhitespace(counterName)) { (0, index_js_1.throwError)("InvalidArgumentException", "CounterName cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("document/" + documentId + "/counter/" + counterName, "watch-document-counter", "unwatch-document-counter", null, [documentId, counterName]); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("Counter", counter, notification => StringUtil_js_1.StringUtil.equalsIgnoreCase(documentId, notification.documentId) && StringUtil_js_1.StringUtil.equalsIgnoreCase(counterName, notification.name)); return taskedObservable; } forCountersOfDocument(documentId) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(documentId)) { (0, index_js_1.throwError)("InvalidArgumentException", "DocumentId cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("document/" + documentId + "/counter", "watch-document-counters", "unwatch-document-counters", documentId); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("Counter", counter, notification => StringUtil_js_1.StringUtil.equalsIgnoreCase(documentId, notification.documentId)); return taskedObservable; } forAllTimeSeries() { const counter = this._getOrAddConnectionState("all-timeseries", "watch-all-timeseries", "unwatch-all-timeseries", null); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("TimeSeries", counter, () => true); return taskedObservable; } forTimeSeries(timeSeriesName) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(timeSeriesName)) { (0, index_js_1.throwError)("InvalidArgumentException", "TimeSeriesName cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("timeseries/" + timeSeriesName, "watch-timeseries", "unwatch-timeseries", timeSeriesName); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("TimeSeries", counter, notification => StringUtil_js_1.StringUtil.equalsIgnoreCase(timeSeriesName, notification.name)); return taskedObservable; } forTimeSeriesOfDocument(documentId, timeSeriesName) { if (timeSeriesName) { return this._forTimeSeriesOfDocumentWithNameInternal(documentId, timeSeriesName); } else { return this._forTimeSeriesOfDocumentInternal(documentId); } } _forTimeSeriesOfDocumentInternal(documentId) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(documentId)) { (0, index_js_1.throwError)("InvalidArgumentException", "DocumentId cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("document/" + documentId + "/timeseries", "watch-all-document-timeseries", "unwatch-all-document-timeseries", documentId); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("TimeSeries", counter, notification => StringUtil_js_1.StringUtil.equalsIgnoreCase(documentId, notification.documentId)); return taskedObservable; } _forTimeSeriesOfDocumentWithNameInternal(documentId, timeSeriesName) { if (StringUtil_js_1.StringUtil.isNullOrWhitespace(documentId)) { (0, index_js_1.throwError)("InvalidArgumentException", "DocumentId cannot be null or whitespace."); } if (StringUtil_js_1.StringUtil.isNullOrWhitespace(timeSeriesName)) { (0, index_js_1.throwError)("InvalidArgumentException", "TimeSeriesName cannot be null or whitespace."); } const counter = this._getOrAddConnectionState("document/" + documentId + "/timeseries/" + timeSeriesName, "watch-document-timeseries", "unwatch-document-timeseries", null, [documentId, timeSeriesName]); const taskedObservable = new ChangesObservable_js_1.ChangesObservable("TimeSeries", counter, notification => StringUtil_js_1.StringUtil.equalsIgnoreCase(timeSeriesName, notification.name) && StringUtil_js_1.StringUtil.equalsIgnoreCase(documentId, notification.documentId)); return taskedObservable; } } exports.DatabaseChanges = DatabaseChanges; //# sourceMappingURL=DatabaseChanges.js.map