UNPKG

ydb-sdk

Version:

Node.js bindings for working with YDB API over gRPC

241 lines (240 loc) 8.39 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Context = void 0; exports.setContextIdGenerator = setContextIdGenerator; const symbols_1 = require("./symbols"); let currentCtx; let count = 0; const defaultIdGenerator = () => { return (++count).toString().padStart(4, '0'); }; let idGenerator = defaultIdGenerator; /** * Sets the Context.id generation rule for new contexts. It is desirable to call this funtion at the very beginning of * the application before the contexts are used. */ function setContextIdGenerator(_idGenerator) { if (_idGenerator) idGenerator = _idGenerator; else { count = 0; idGenerator = defaultIdGenerator; } } /** * TypeScript Context implementation inspired by golang context (https://pkg.go.dev/context). * * Supports cancel, timeout, value, done, cancel cancel-chain behaviours. */ class Context { constructor(id) { this[symbols_1.idSymbol] = id; } /** * Unique id of Context Useful for tracing. */ get id() { return this[symbols_1.idSymbol]; } /** * That is the cause that was passed to cancel. * * If defined, the context is cancelled. */ get err() { return this[symbols_1.errSymbol]; } /** * Creates a new context. */ static createNew(opts = {}) { const ctx = new Context(typeof opts.id === 'string' ? opts.id : idGenerator()); const res = initContext.call(ctx, opts); res.ctx = ctx; return res; } /** * Creates a child context from the this one. * * Note: If there are no sufficient requirements for a new context the parent context * will be keep using. */ createChild(opts = {}) { if (opts.id) throw new Error('This method cannot change the context id'); if (!(opts.hasOwnProperty('cancel') || opts.timeout > 0 || opts.done) && !opts.force) return { ctx: this }; const ctx = Object.create(this); const originOpts = opts; if (this.onCancel) if (opts.cancel === false) ctx.onCancel = undefined; // block parent onCancel else opts = Object.assign(Object.assign({}, opts), { cancel: true }); const res = initContext.call(ctx, opts); if (this.onCancel && res.cancel) { const unsub = this.onCancel(res.cancel); if (res.dispose) { const parentDispose = res.dispose; res.dispose = () => { parentDispose(); unsub(); }; } else res.dispose = unsub; } if (originOpts.cancel !== true) delete res.cancel; res.ctx = ctx; return res; } /** * Makes a pr omise cancellable through context, if the context allows cancel or has a timeout. */ cancelRace(promise) { if (!this.onCancel) return promise; let cancelReject; const cancelPromise = new Promise((_, reject) => { cancelReject = reject; }); const unsub = this.onCancel((cause) => { cancelReject(cause); }); return Promise.race([promise, cancelPromise]).finally(() => { unsub(); }); } /** * Wraps a method with a context with specified properties. Just, syntactic sugar. */ async wrap(opts, fn) { const { ctx, dispose, cancel, done } = this.createChild(opts); try { return await ctx.cancelRace(fn(ctx, cancel, done)); } finally { if (dispose) dispose(); } } /** * True if the reason for canceling is timeout. */ static isTimeout(cause) { return typeof cause === 'object' && cause !== null && cause.cause === symbols_1.timeoutSymbol; } /** * True if the reason for canceling is call of ctx.Done() . */ static isDone(cause) { return typeof cause === 'object' && cause !== null && cause.cause === symbols_1.doneSymbol; } toString() { return this[symbols_1.idSymbol]; } /** * Passes the context through a global variable, as in _npm context_. * * Allows context to be passed through code that does not know about the context and cannot be changed. For example, * the code generated by protobufs js. * * It is important to pick up the context from the same synchronous code block that was called from ctx.do(). Otherwise, the context * will be removed or possibly a context from another branch of the logic will be set. Which would create confusion. * * In practice, if Context.get() is called in an async method. It should be called before the first await - the easiest way is * to get the context in the first line of the function. * * @param fn a function of the form "() => AFuncOrMethod(...args)", at the moment of calling which the current context will be set. */ do(fn) { if (currentCtx !== undefined) throw new Error(`There is a "ctx" that was passed through "ctx.do()" and was not processed till next "ctx.do()": ${currentCtx}`); currentCtx = this; try { return fn(); } finally { // TODO: Think of warning if context is not undefined, so there was no Context.get() call currentCtx = undefined; } } /** * Returns the current context from global variable. See the description of Context.do() for a detailed explanation. */ static get() { if (!(currentCtx instanceof Context)) throw new Error(`"ctx" was either not passed through Context.do() or was already taken through Context.get()`); const ctx = currentCtx; currentCtx = undefined; return ctx; } } exports.Context = Context; function makeContextCancellable(context) { context.onCancel = (listener) => { if (context[symbols_1.errSymbol]) setImmediate(listener.bind(undefined, context[symbols_1.errSymbol])); else if (context.hasOwnProperty(symbols_1.cancelListenersSymbol)) context[symbols_1.cancelListenersSymbol].push(listener); else context[symbols_1.cancelListenersSymbol] = [listener]; function dispose() { if (context[symbols_1.cancelListenersSymbol]) { const index = context[symbols_1.cancelListenersSymbol].indexOf(listener); if (index > -1) context[symbols_1.cancelListenersSymbol].splice(index, 1); } } return dispose; }; function cancel(cause) { var _a; if (context.hasOwnProperty(symbols_1.errSymbol)) return; // already cancelled if (!cause) cause = new Error('Unknown'); context[symbols_1.errSymbol] = cause; (_a = context[symbols_1.cancelListenersSymbol]) === null || _a === void 0 ? void 0 : _a.forEach((l) => l(cause)); delete context[symbols_1.cancelListenersSymbol]; } return cancel; } function setContextTimeout(timeout, cancel) { let timer = setTimeout(() => { // An error is always created rather than using a constant to have an actual callstack const err = new Error(`Timeout: ${timeout} ms`); err.cause = symbols_1.timeoutSymbol; cancel(err); }, timeout); function dispose() { if (timer) { clearTimeout(timer); timer = undefined; } } return dispose; } function createDone(cancel) { function done() { // The error is always created rather than using a constant to have an actual callstack const err = new Error('Done'); err.cause = symbols_1.doneSymbol; cancel(err); } return done; } function initContext(opts) { const res = {}; let cancel; if (opts.cancel === true) res.cancel = cancel = makeContextCancellable(this); if (opts.timeout > 0) res.dispose = setContextTimeout(opts.timeout, cancel || (cancel = makeContextCancellable(this))); if (opts.done) res.done = createDone(cancel || makeContextCancellable(this)); return res; }