ravendb
Version:
RavenDB client for Node.js
510 lines • 24.2 kB
JavaScript
"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