UNPKG

ydb-sdk

Version:

Node.js bindings for working with YDB API over gRPC

237 lines (236 loc) 10.1 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TopicWriter = void 0; const context_1 = require("../context"); const long_1 = __importDefault(require("long")); const symbols_1 = require("./symbols"); class TopicWriter { constructor(ctx, writeStreamArgs, retrier, discovery, logger) { this.writeStreamArgs = writeStreamArgs; this.retrier = retrier; this.discovery = discovery; this.logger = logger; this.messageQueue = []; this.firstInnerStreamInitResp = true; this.getLastSeqNo = !!writeStreamArgs.getLastSeqNo; logger.trace('%s: new TopicWriter', ctx); let onCancelUnsub; if (ctx.onCancel) onCancelUnsub = ctx.onCancel((cause) => { if (this.reasonForClose) return; this.reasonForClose = cause; this.close(ctx, true); }); // background process of sending and retrying this.retrier.retry(ctx, async (ctx, logger, attemptsCount) => { logger.trace('%s: retry %d', ctx, attemptsCount); const attemptPromise = new Promise((_, reject) => { this.attemptPromiseReject = reject; }); await this.initInnerStream(ctx); return attemptPromise .catch((err) => { logger.trace('%s: retrier error: %o', ctx, err); if (this.messageQueue.length > 0) { return { err: err, idempotent: true }; } return this.reasonForClose && this.reasonForClose.cause === symbols_1.closeSymbol ? {} // stream is correctly closed : { err: err, idempotent: true }; }) .finally(() => { this.closeInnerStream(ctx); }); }) .then(() => { logger.debug('%s: closed successfully', ctx); }) .catch((err) => { logger.debug('%s: failed: %o', ctx, err); this.reasonForClose = err; this.spreadError(ctx, err); this.close(); }) .finally(() => { if (onCancelUnsub) onCancelUnsub(); }); } async initInnerStream(ctx) { this.logger.trace('%s: initInnerStream()', ctx); // fill lastSeqNo only when the first internal stream is opened if (!this.firstInnerStreamInitResp && this.writeStreamArgs.getLastSeqNo) { this.writeStreamArgs = Object.assign(this.writeStreamArgs); delete this.writeStreamArgs.getLastSeqNo; } delete this.firstInnerStreamInitResp; const stream = await (await this.discovery.getTopicNodeClient()).openWriteStreamWithEvents(ctx, this.writeStreamArgs); stream.events.on('initResponse', (resp) => { this.logger.trace('%s: TopicWriter.on "initResponse"', ctx); try { // if received lastSeqNo in mode this.getLastSeqNo === true if (resp.lastSeqNo || resp.lastSeqNo === 0) { this.lastSeqNo = long_1.default.fromValue(resp.lastSeqNo); // if there are messages that were queued before lastSeqNo was received this.messageQueue.forEach((queueItem) => { queueItem.args.messages.forEach((message) => { message.seqNo = this.lastSeqNo = this.lastSeqNo.add(1); }); }); } // TODO: Send messages as one batch. Add new messages to the batch if there are some this.messageQueue.forEach((queueItem) => { stream.writeRequest(ctx, queueItem.args); }); // this.innerWriteStream variable is defined only after the stream is initialized this.innerWriteStream = stream; } catch (err) { if (!this.attemptPromiseReject) throw err; this.attemptPromiseReject(err); } }); stream.events.on('writeResponse', (resp) => { this.logger.trace('%s: TopicWriter.on "writeResponse"', ctx); try { const { acks } = resp, shortResp = __rest(resp, ["acks"]); resp.acks.forEach((ack) => { const queueItem = this.messageQueue.shift(); // TODO: Check seqNo is expected and queueItem is not an undefined queueItem === null || queueItem === void 0 ? void 0 : queueItem.resolve(Object.assign(Object.assign({}, shortResp), ack)); }); } catch (err) { if (!this.attemptPromiseReject) throw err; this.attemptPromiseReject(err); } finally { if (this.closeResolve) this.closeResolve(); } }); stream.events.on('error', (err) => { this.logger.trace('%s: TopicWriter.on "error": %o', ctx, err); this.reasonForClose = err; this.spreadError(ctx, err); try { delete this.innerWriteStream; if (this.closeResolve) this.closeResolve(); } catch (err) { if (!this.attemptPromiseReject) throw err; this.attemptPromiseReject(err); } }); stream.events.on('end', (cause) => { this.logger.trace('%s: TopicWriter.on "end": %o', ctx, cause); try { delete this.innerWriteStream; if (this.closeResolve) this.closeResolve(); } catch (err) { if (!this.attemptPromiseReject) throw err; this.attemptPromiseReject(err); } }); } closeInnerStream(ctx) { var _a; this.logger.trace('%s: TopicWriter.closeInnerStream()', ctx); (_a = this.innerWriteStream) === null || _a === void 0 ? void 0 : _a.close(ctx); delete this.innerWriteStream; } async close(ctx, force) { var _a; this.logger.trace('%s: TopicWriter.close(force: %o)', ctx, !!force); if (this.reasonForClose) return; this.reasonForClose = new Error('close invoked'); this.reasonForClose.cause = symbols_1.closeSymbol; if (force || this.messageQueue.length === 0) { (_a = this.innerWriteStream) === null || _a === void 0 ? void 0 : _a.close(ctx); this.spreadError(ctx, this.reasonForClose); this.messageQueue.length = 0; // drop queue return; } else { return new Promise((resolve) => { this.closeResolve = resolve; }); } } send(ctx, sendMessagesArgs) { var _a; this.logger.trace('%s: TopicWriter.sendMessages()', ctx); if (this.reasonForClose) return Promise.reject(this.reasonForClose); (_a = sendMessagesArgs.messages) === null || _a === void 0 ? void 0 : _a.forEach((msg) => { if (this.getLastSeqNo) { if (!(msg.seqNo === undefined || msg.seqNo === null)) throw new Error('Writer was created with getLastSeqNo = true, explicit seqNo not supported'); if (this.lastSeqNo) { // else wait till initResponse will be received msg.seqNo = this.lastSeqNo = this.lastSeqNo.add(1); } } else { if (msg.seqNo === undefined || msg.seqNo === null) throw new Error('Writer was created without getLastSeqNo = true, explicit seqNo must be provided'); } }); return new Promise((resolve, reject) => { var _a; this.messageQueue.push({ args: sendMessagesArgs, resolve, reject }); (_a = this.innerWriteStream) === null || _a === void 0 ? void 0 : _a.writeRequest(ctx, sendMessagesArgs); }); } /** * Notify all incomplete Promise that an error has occurred. */ spreadError(ctx, err) { this.logger.trace('%s: TopicWriter.spreadError()', ctx); this.messageQueue.forEach((item) => { item.reject(err); }); this.messageQueue.length = 0; } } exports.TopicWriter = TopicWriter; __decorate([ (0, context_1.ensureContext)(true) ], TopicWriter.prototype, "close", null); __decorate([ (0, context_1.ensureContext)(true) ], TopicWriter.prototype, "send", null);