@hashgraph/sdk
Version:
449 lines (412 loc) • 13.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _TransactionId = _interopRequireDefault(require("../transaction/TransactionId.cjs"));
var _SubscriptionHandle = _interopRequireDefault(require("./SubscriptionHandle.cjs"));
var _TopicMessage = _interopRequireDefault(require("./TopicMessage.cjs"));
var HieroProto = _interopRequireWildcard(require("@hashgraph/proto"));
var _TopicId = _interopRequireDefault(require("./TopicId.cjs"));
var _long = _interopRequireDefault(require("long"));
var _Timestamp = _interopRequireDefault(require("../Timestamp.cjs"));
var _Executable = require("../Executable.cjs");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
// SPDX-License-Identifier: Apache-2.0
/**
* @typedef {import("../channel/Channel.js").default} Channel
* @typedef {import("../channel/MirrorChannel.js").default} MirrorChannel
* @typedef {import("../channel/MirrorChannel.js").MirrorError} MirrorError
*/
/**
* @template {Channel} ChannelT
* @typedef {import("../client/Client.js").default<ChannelT, MirrorChannel>} Client<ChannelT, MirrorChannel>
*/
/**
* Represents a class that you can use to subscribe to
* different topics on Hedera network.
* @augments {Query<TopicMessageQuery>}
*/
class TopicMessageQuery {
/**
* @param {object} props
* @param {TopicId | string} [props.topicId]
* @param {Timestamp} [props.startTime]
* @param {Timestamp} [props.endTime]
* @param {(message: TopicMessage | null, error: Error)=> void} [props.errorHandler]
* @param {() => void} [props.completionHandler]
* @param {(error: MirrorError | Error | null) => boolean} [props.retryHandler]
* @param {Long | number} [props.limit]
*/
constructor(props = {}) {
/**
* @private
* @type {?TopicId}
*/
this._topicId = null;
if (props.topicId != null) {
this.setTopicId(props.topicId);
}
/**
* @private
* @type {?Timestamp}
*/
this._startTime = null;
if (props.startTime != null) {
this.setStartTime(props.startTime);
}
/**
* @private
* @type {?Timestamp}
*/
this._endTime = null;
if (props.endTime != null) {
this.setEndTime(props.endTime);
}
/**
* @private
* @type {?Long}
*/
this._limit = null;
if (props.limit != null) {
this.setLimit(props.limit);
}
/**
* @private
* @type {(message: TopicMessage | null, error: Error) => void}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this._errorHandler = (message, error) => {
console.error(`Error attempting to subscribe to topic: ${this._topicId != null ? this._topicId.toString() : ""}`);
};
if (props.errorHandler != null) {
this._errorHandler = props.errorHandler;
}
/*
* @private
* @type {((message: TopicMessage) => void) | null}
*/
this._listener = null;
/**
* @private
* @type {() => void}
*/
this._completionHandler = () => {
console.log(`Subscription to topic ${this._topicId != null ? this._topicId.toString() : ""} complete`);
};
if (props.completionHandler != null) {
this._completionHandler = props.completionHandler;
}
/* The number of times we can retry the grpc call
*
* @internal
* @type {number}
*/
this._maxAttempts = 20;
/**
* This is the request's max backoff
*
* @internal
* @type {number}
*/
this._maxBackoff = 8000;
/**
* @private
* @type {(error: MirrorError | Error | null) => boolean}
*/
this._retryHandler = error => {
if (error != null) {
if (error instanceof Error) {
// Retry on all errors which are not `MirrorError` because they're
// likely lower level HTTP/2 errors
return true;
} else {
// Retry on `NOT_FOUND`, `RESOURCE_EXHAUSTED`, `UNAVAILABLE`, and conditionally on `INTERNAL`
// if the message matches the right regex.
switch (error.code) {
// INTERNAL
// eslint-disable-next-line no-fallthrough
case 13:
return _Executable.RST_STREAM.test(error.details.toString());
// NOT_FOUND
// eslint-disable-next-line no-fallthrough
case 5:
// RESOURCE_EXHAUSTED
// eslint-disable-next-line no-fallthrough
case 8:
// UNAVAILABLE
// eslint-disable-next-line no-fallthrough
case 14:
case 17:
return true;
default:
return false;
}
}
}
return false;
};
if (props.retryHandler != null) {
this._retryHandler = props.retryHandler;
}
/**
* @private
* @type {number}
*/
this._attempt = 0;
/**
* @private
* @type {SubscriptionHandle | null}
*/
this._handle = null;
this.setMaxBackoff(8000);
}
/**
* @returns {?TopicId}
*/
get topicId() {
return this._topicId;
}
/**
* @param {TopicId | string} topicId
* @returns {TopicMessageQuery}
*/
setTopicId(topicId) {
this.requireNotSubscribed();
this._topicId = typeof topicId === "string" ? _TopicId.default.fromString(topicId) : topicId.clone();
return this;
}
/**
* @returns {?Timestamp}
*/
get startTime() {
return this._startTime;
}
/**
* @param {Timestamp | Date | number} startTime
* @returns {TopicMessageQuery}
*/
setStartTime(startTime) {
this.requireNotSubscribed();
this._startTime = startTime instanceof _Timestamp.default ? startTime : startTime instanceof Date ? _Timestamp.default.fromDate(startTime) : new _Timestamp.default(startTime, 0);
return this;
}
/**
* @returns {?Timestamp}
*/
get endTime() {
return this._endTime;
}
/**
* @param {Timestamp | Date | number} endTime
* @returns {TopicMessageQuery}
*/
setEndTime(endTime) {
this.requireNotSubscribed();
this._endTime = endTime instanceof _Timestamp.default ? endTime : endTime instanceof Date ? _Timestamp.default.fromDate(endTime) : new _Timestamp.default(endTime, 0);
return this;
}
/**
* @returns {?Long}
*/
get limit() {
return this._limit;
}
/**
* @param {Long | number} limit
* @returns {TopicMessageQuery}
*/
setLimit(limit) {
this.requireNotSubscribed();
this._limit = limit instanceof _long.default ? limit : _long.default.fromValue(limit);
return this;
}
/**
* @param {(message: TopicMessage | null, error: Error)=> void} errorHandler
* @returns {TopicMessageQuery}
*/
setErrorHandler(errorHandler) {
this._errorHandler = errorHandler;
return this;
}
/**
* @param {() => void} completionHandler
* @returns {TopicMessageQuery}
*/
setCompletionHandler(completionHandler) {
this.requireNotSubscribed();
this._completionHandler = completionHandler;
return this;
}
/**
* @param {number} attempts
* @returns {this}
*/
setMaxAttempts(attempts) {
this.requireNotSubscribed();
this._maxAttempts = attempts;
return this;
}
/**
* @param {number} backoff
* @returns {this}
*/
setMaxBackoff(backoff) {
this.requireNotSubscribed();
this._maxBackoff = backoff;
return this;
}
/**
* @param {Client<Channel>} client
* @param {((message: TopicMessage | null, error: Error) => void) | null} errorHandler
* @param {(message: TopicMessage) => void} listener
* @returns {SubscriptionHandle}
*/
subscribe(client, errorHandler, listener) {
this._handle = new _SubscriptionHandle.default();
this._listener = listener;
if (errorHandler != null) {
this._errorHandler = errorHandler;
}
this._makeServerStreamRequest(client);
return this._handle;
}
/**
* Makes a server stream request to subscribe to topic messages
* @private
* @param {Client<Channel>} client
* @returns {void}
*/
_makeServerStreamRequest(client) {
const request = this._buildConsensusRequest();
/** @type {Map<string, HieroProto.com.hedera.mirror.api.proto.ConsensusTopicResponse[]>} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const list = new Map();
const streamHandler = client._mirrorNetwork.getNextMirrorNode().getChannel().makeServerStreamRequest("ConsensusService", "subscribeTopic", request, data => this._handleMessage(data, list), error => this._handleError(error, client), this._completionHandler);
if (this._handle != null) {
this._handle._setCall(() => streamHandler());
}
}
requireNotSubscribed() {
if (this._handle != null) {
throw new Error("Cannot change fields on an already subscribed query");
}
}
/**
* @private
* @param {TopicMessage} topicMessage
*/
_passTopicMessage(topicMessage) {
try {
if (this._listener != null) {
this._listener(topicMessage);
} else {
throw new Error("(BUG) listener is unexpectedly not set");
}
} catch (error) {
this._errorHandler(topicMessage, /** @type {Error} */error);
}
}
/**
* Builds the consensus topic query request
* @private
* @returns {Uint8Array} Encoded consensus topic query
*/
_buildConsensusRequest() {
return HieroProto.com.hedera.mirror.api.proto.ConsensusTopicQuery.encode({
topicID: this._topicId?._toProtobuf() ?? null,
consensusStartTime: this._startTime?._toProtobuf() ?? null,
consensusEndTime: this._endTime?._toProtobuf() ?? null,
limit: this._limit
}).finish();
}
/**
* Handles an incoming message from the topic subscription
* @private
* @param {Uint8Array} data - Raw message data
* @param {Map<string, HieroProto.com.hedera.mirror.api.proto.ConsensusTopicResponse[]>} list
*/
_handleMessage(data, list) {
const message = HieroProto.com.hedera.mirror.api.proto.ConsensusTopicResponse.decode(data);
if (this._limit?.gt(0)) {
this._limit = this._limit.sub(1);
}
this._startTime = _Timestamp.default._fromProtobuf(/** @type {HieroProto.proto.ITimestamp} */
message.consensusTimestamp).plusNanos(1);
if (message.chunkInfo == null || message.chunkInfo != null && message.chunkInfo.total === 1) {
this._passTopicMessage(_TopicMessage.default._ofSingle(message));
} else {
this._handleChunkedMessage(message, list);
}
}
/**
* Handles a chunked message from the topic subscription
* @private
* @param {HieroProto.com.hedera.mirror.api.proto.ConsensusTopicResponse} message - The message response
* @param {Map<string, HieroProto.com.hedera.mirror.api.proto.ConsensusTopicResponse[]>} list
*/
_handleChunkedMessage(message, list) {
const chunkInfo = /** @type {HieroProto.proto.IConsensusMessageChunkInfo} */
message.chunkInfo;
const initialTransactionID = /** @type {HieroProto.proto.ITransactionID} */
chunkInfo.initialTransactionID;
const total = /** @type {number} */chunkInfo.total;
const transactionId = _TransactionId.default._fromProtobuf(initialTransactionID).toString();
/** @type {HieroProto.com.hedera.mirror.api.proto.ConsensusTopicResponse[]} */
let responses = [];
const temp = list.get(transactionId);
if (temp == null) {
list.set(transactionId, responses);
} else {
responses = temp;
}
responses.push(message);
if (responses.length === total) {
const topicMessage = _TopicMessage.default._ofMany(responses);
list.delete(transactionId);
this._passTopicMessage(topicMessage);
}
}
/**
* Handles errors from the topic subscription
* @private
* @param {MirrorError | Error} error - The error that occurred
* @param {Client<Channel>} client - The client to use for retries
* @returns {void}
*/
_handleError(error, client) {
const message = error instanceof Error ? error.message : error.details;
if (this._handle?._unsubscribed) {
return;
}
if (this.shouldRetry(error)) {
this._scheduleRetry(client, message);
} else {
this._errorHandler(null, new Error(message));
}
}
/**
* Determines if a retry should be attempted
* @private
* @param {MirrorError | Error} error - The error to check
* @returns {boolean} - Whether to retry
*/
shouldRetry(error) {
return this._attempt < this._maxAttempts && this._retryHandler(error);
}
/**
* Schedules a retry of the server stream request
* @private
* @param {Client<Channel>} client - The client to use for the retry
* @param {string} errorMessage - The error message for logging
* @returns {void}
*/
_scheduleRetry(client, errorMessage) {
const delay = Math.min(250 * 2 ** this._attempt, this._maxBackoff);
console.warn(`Error subscribing to topic ${this._topicId?.toString() ?? "UNKNOWN"} ` + `during attempt ${this._attempt}. Waiting ${delay} ms before next attempt: ${errorMessage}`);
this._attempt += 1;
setTimeout(() => this._makeServerStreamRequest(client), delay);
}
}
exports.default = TopicMessageQuery;