ydb-sdk
Version:
Node.js bindings for working with YDB API over gRPC
249 lines (248 loc) • 8.59 kB
JavaScript
;
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 {
[symbols_1.idSymbol];
[symbols_1.cancelListenersSymbol];
[symbols_1.errSymbol];
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];
}
/**
* If defined, then the context supports cancel. And it is possible to subscribe to it.
*
* @return Function that removes just signed listener from listeners.
*/
onCancel;
/**
* 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 = { ...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) {
if (context.hasOwnProperty(symbols_1.errSymbol))
return; // already cancelled
if (!cause)
cause = new Error('Unknown');
context[symbols_1.errSymbol] = cause;
context[symbols_1.cancelListenersSymbol]?.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;
}