All files / semaphore CriticalSection.ts

100% Statements 15/15
100% Branches 8/8
100% Functions 3/3
100% Lines 15/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 1791x                                                                                                                                             1x 45x   1x     85x 84x   84x     69x                                                                                                                                                     52x 2x     50x 1x     49x 31x               39x      
import { ISemaphore, Semaphore } from './Semaphore';
 
/**
 * A non-reentrant critical section offering execution of (async) functions therein.
 *
 * Effectively, it is a wrapper around {@link ISemaphore}, also accommodating
 * easy-to-miss edge cases when errors are thrown or Promises rejected.
 *
 * @example Simple usage:
 * ```
 * await criticalSection.do(() => {
 *   // We acquired the (internal) lock
 *   // Do something exclusively
 * });
 * ```
 *
 * @example Exceptions in a synchronous exclusive function:
 * ```
 * try {
 *   await criticalSection.do(() => {
 *     // Errors thrown within the critical section *do* release it again
 *     throw new Error('oops');
 *   });
 * }
 * catch (err) {
 *   // Here we will receive the error
 *   console.log(err);
 * }
 * ```
 *
 * @example Rejections in an asynchronous exclusive function:
 * ```
 * try {
 *   await criticalSection.do(async () => {
 *     // A returned rejected promise returned within the critical section
 *     // *does* release it again
 *     return Promise.reject('oops');
 *
 *     // `await Promise.reject('oops');` would have the same effect
 *   });
 * }
 * catch (err) {
 *   // Here we will receive the error
 *   console.log(err);
 * }
 * ```
 */
export interface ICriticalSection {
 
	/**
	 * Wait for the critical section to become available and execute a function.
	 *
	 * @param func A function which will be executed exclusively in the critical
	 *             section.
	 *
	 * @returns A promise which is resolved with the (eventually) returned
	 *          value by `func`. Effectively, once the critical section has
	 *          been entered, `Promise.resolve(func())` is returned.
	 *          Especially if `func` returns a Thenable, by the semantics of
	 *          `Promise.resolve`, this Thenable is adopted as the new Promise.
	 *          <br>
	 *          In other words, it is impossible to make
	 *          `criticalSection.do(...)` resolve to a Promise, i.e.
	 *          `await criticalSection.do(...)` be a Promise.
	 *
	 * @throws The returned promise will be rejected iff. the executed function
	 *         has thrown an error or has returned a rejecting promise itself.
	 */
	do<T>(func: () => T | Promise<T>): Promise<T>;
}
 
export class CriticalSection implements ICriticalSection {
	private lock: ISemaphore = new Semaphore(1);
 
	private static objectAccessor = Symbol('CriticalSection associated with the object');
 
	public async do<T>(func: () => (T | Promise<T>)): Promise<T> {
		await this.lock.take();
		try {
			// The await is actually redundant, but serves clarity
			return await func();
		}
		finally {
			this.lock.free();
		}
	}
 
	/**
	 * Create or get the object-bound critical section.
	 *
	 * If no critical section is yet bound to the object, a new one is created
	 * and bound to that specific object. Critical sections bound to objects
	 * higher up in the prototype chain do *not* get inherited.
	 *
	 * @example Multiple calls on the same object return the same
	 *          {@link CriticalSection}:
	 * ```
	 * // The object can also be a function
	 * const myObj = function () {
	 *   // ...
	 * };
	 * const firstSection = CriticalSection.for(myObj);
	 * const secondSection = CriticalSection.for(myObj);
	 *
	 * // true
	 * console.log(firstSection === secondSection);
	 * ```
	 *
	 * @example No inheritance with respect to the prototype chain:
	 * ```
	 * const myPrototype = {};
	 * const firstSection = CriticalSection.for(myPrototype);
	 *
	 * const object = Object.create(myPrototype);
	 *
	 * // true
	 * console.log(CriticalSection.for(object) !== firstSection);
	 * ```
	 * @example Beware of iframes and inter-website communicating code, e.g.
	 *          the `Array` constructors differ and are therefore considered
	 *          distinct objects by this method.
	 * ```
	 * const iframe = document.createElement('iframe');
	 * document.body.appendChild(iframe);
	 * const xArray = window.frames[window.frames.length-1].Array;
	 *
	 * const iframeArr = new xArray(1,2,3);
	 * const thisSiteArr = [4, 5, 6];
	 *
	 * // true in both cases
	 * console.log(iframeArr.constructor !== thisSiteArr.constructor);
	 * console.log(CriticalSection.for(iframeArr.constructor) !==
	 *             CriticalSection.for(thisSiteArr.constructor));
	 * ```
	 *
	 * @param object The object with which a new critical section bond should
	 *               be created if it does not exists yet.
	 *               `object` must be a real object and not a primitive as per
	 *               the ECMAScript standard specification. Especially, the
	 *               following values are primitives (see also [MDN][1]):
	 * - numbers (e.g. 1, 2, 3)
	 * - strings (e.g. 'Hello World')
	 * - booleans (true, false)
	 * - null
	 * - undefined
	 * - Symbols
	 *
	 * A {@link TypeError} will be thrown if such a value is passed.
	 * (Note that there is no method to check for primitive values in
	 * ECMAScript, therefore, this method might *not* throw a
	 * {@link TypeError} or other errors for new primitive values
	 * introduced in yet-to-come ECMAScript versions.)
	 *
	 * @throws A {@link TypeError} is raised when `object` is a [primitive value][1].
	 *
	 * [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures
	 */
	public static for(object: Object): ICriticalSection {
		if (object === null || object === undefined) {
			throw new TypeError('Cannot call CriticalSection.for on null or undefined.');
		}
 
		if (typeof object === 'symbol') {
			throw new TypeError('Cannot call CriticalSection.for on a symbol.');
		}
 
		if (!object.hasOwnProperty(this.objectAccessor)) {
			Object.defineProperty(object, this.objectAccessor, {
				configurable: false,
				enumerable: false,
				writable: false,
				value: new CriticalSection()
			});
		}
 
		return (object as any)[this.objectAccessor];
	}
}