UNPKG

@lightningjs/threadx

Version:

A web browser-based JavaScript library that helps manage the communcation of data between one or more web worker threads.

394 lines 15.4 kB
/* * Copyright 2023 Comcast Cable Communications Management, LLC * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * SPDX-License-Identifier: Apache-2.0 */ import { ThreadX } from './ThreadX.js'; import { stringifyTypeId } from './buffer-struct-utils.js'; const TYPEID_INT32_INDEX = 0; const NOTIFY_INT32_INDEX = 1; const LOCK_INT32_INDEX = 2; const DIRTY_INT32_INDEX = 6; const UNDEFINED_INT32_INDEX = 8; const ID_FLOAT64_INDEX = 2; const MAX_STRING_SIZE = 255; // eslint-disable-next-line @typescript-eslint/no-unused-vars function valueIsType(expectedType, type, value) { return expectedType === type; } function valuesAreEqual(a, b) { return a === b; } export function structProp(type, options) { return function (target, key, descriptor) { const constructor = target.constructor; // Make sure the static initializer has been called. We must check that the // constructor directly has its "own property" because it may be inherited // from a parent class. if (!Object.prototype.hasOwnProperty.call(constructor, 'staticInitialized') || !constructor.staticInitialized) { constructor.initStatic(); } let byteOffset = constructor.size; let offset = 0; let byteSize = 0; if (type === 'string') { byteOffset += byteOffset % 2; offset = byteOffset / 2; byteSize = (MAX_STRING_SIZE + 1) * 2; // 16-bits for size then 255 16-bit characters } else if (type === 'int32' || type === 'boolean') { byteOffset += byteOffset % 4; offset = byteOffset / 4; byteSize = 4; } else if (type === 'number') { byteOffset += byteOffset % 8; offset = byteOffset / 8; byteSize = 8; } const propDefs = constructor.propDefs; const propNum = propDefs.length; const allowUndefined = !!options?.allowUndefined; const propDef = { propNum, name: key, type, byteOffset, offset, byteSize, allowUndefined, }; propDefs.push(propDef); // console.log(constructor.size, byteOffset, byteSize, propDef); constructor.size = byteOffset + byteSize; // TODO: Move the descriptors to the prototype to avoid code duplication/closures descriptor.get = function () { let value; if (allowUndefined && this.isUndefined(propNum)) { value = undefined; } else if (type === 'string') { const length = this.uint16array[offset]; if (!length) return ''; if (length > MAX_STRING_SIZE) { // This should never happen because we truncate the string when setting it throw new Error(`get SharedObject.${key}: Text length is too long. Length: ${length}`); } value = String.fromCharCode(...this.uint16array.slice(offset + 1, offset + 1 + length)); } else if (type === 'int32') { value = this.int32array[offset]; } else if (type === 'boolean') { value = !!this.int32array[offset]; } else if (type === 'number') { value = this.float64array[offset]; } if (options?.bufferToProp) { value = options.bufferToProp(value); } return value; }; descriptor.set = function (value) { if (options?.propToBuffer) { value = options.propToBuffer(value); } if (allowUndefined) { const isUndefined = this.isUndefined(propNum); if (value === undefined) { if (isUndefined) return; this.setDirty(propNum); this.setUndefined(propNum, true); return; } else if (isUndefined) { this.setDirty(propNum); this.setUndefined(propNum, false); } } if (valueIsType('string', type, value)) { if (!valuesAreEqual(value, this[key])) { this.setDirty(propNum); // Copy string into shared memory in the most efficient way possible let length = value.length; if (length > MAX_STRING_SIZE) { console.error(`set SharedObject.${key}: Text length is too long. Truncating...`, length); length = MAX_STRING_SIZE; } this.uint16array[offset] = length; const startOffset = offset + 1; const endOffset = startOffset + length; let charIndex = 0; for (let i = startOffset; i < endOffset; i++) { this.uint16array[i] = value.charCodeAt(charIndex++); } } } else if (valueIsType('int32', type, value)) { if (!valuesAreEqual(value, this[key])) { this.setDirty(propNum); this.int32array[offset] = value; } } else if (valueIsType('boolean', type, value)) { if (!valuesAreEqual(value, this[key])) { this.setDirty(propNum); this.int32array[offset] = value ? 1 : 0; } } else if (valueIsType('number', type, value)) { if (!valuesAreEqual(value, this[key])) { this.setDirty(propNum); this.float64array[offset] = value; } } }; }; } /** * BufferStruct Header Structure: * Int32[0] * Type ID: Type of object (32-bit identifier) * Int32[1] * Notify / Last Mutator Worker ID * Int32[2] * Lock * Int32[3] * RESERVED (64-bit align) * Int32[4 - 5] / Float64[ID_FLOAT64_INDEX = 2] * Shared Unique ID of the object * Int32[DIRTY_INT32_INDEX = 6] * Dirty Bit Mask 1 (Property Indices 0-31) * Int32[DIRTY_INT32_INDEX + 1 = 7] * Dirty Bit Mask 2 (Property Indices 32-63) * Int32[UNDEFINED_INT32_INDEX = 8] * Undefined Bit Mask 1 (Property Indices 0-31) * Int32[UNDEFINED_INT32_INDEX + 1 = 9] * Undefined Bit Mask 2 (Property Indices 32-63) * * HEADER SIZE MUST BE A MULTIPLE OF 8 BYTES (64-BIT ALIGNMENT) */ class BufferStruct { buffer; // Lock ID that is a valid 32-bit random integer lockId = Math.floor(Math.random() * 0xffffffff); uint16array; int32array; float64array; static staticInitialized = false; static typeId = 0; static typeIdStr = ''; static size = 10 * 4; // Header size static propDefs = []; constructor(buffer) { const constructor = this.constructor; // Make sure the static initializer has been called. We must check that the // constructor directly has its "own property" because it may be inherited // from a parent class. if (!Object.prototype.hasOwnProperty.call(constructor, 'staticInitialized') || !constructor.staticInitialized) { constructor.initStatic(); } const isNew = !buffer; if (!buffer) { // Round constructor.size to the nearest multiple of 8 bytes (64-bit alignment) buffer = new SharedArrayBuffer(Math.ceil(constructor.size / 8) * 8); } this.buffer = buffer; this.uint16array = new Uint16Array(buffer); this.int32array = new Int32Array(buffer); this.float64array = new Float64Array(buffer); const typeId = constructor.typeId; // If this is a new buffer, initialize the TypeID and ID if (isNew) { this.int32array[TYPEID_INT32_INDEX] = typeId; this.float64array[ID_FLOAT64_INDEX] = ThreadX.instance.generateUniqueId(); // Iterate the propDefs and set undefined for all properties marked `allowUndefined` for (const propDef of constructor.propDefs) { if (propDef.allowUndefined) { this.setUndefined(propDef.propNum, true); } } } else if (this.int32array[TYPEID_INT32_INDEX] !== typeId) { // If this is an existing buffer, verify the TypeID is the same as expected // by this class throw new Error(`BufferStruct: TypeId mismatch. Expected '${constructor.typeIdStr}', got '${stringifyTypeId(this.int32array[TYPEID_INT32_INDEX])}'`); } } /** * Safely extract the TypeID from any SharedArrayBuffer (as if it is a BufferStruct) * * @remarks * Does not check if the TypeID is valid however it does a basic sanity check to * ensure the buffer is large enough to contain the TypeID at Int32[TYPEID_INT32_INDEX]. * * If the buffer is found to be invalid, 0 is returned. * * @param buffer * @returns */ static extractTypeId(buffer) { if (buffer.byteLength < BufferStruct.size || buffer.byteLength % 8 !== 0) { return 0; } return new Int32Array(buffer)[TYPEID_INT32_INDEX] || 0; } /** * Checks if typeId is valid and sets up static properties when the first * structProp() decorator is set-up on the class. * * @remarks * WARNING: This should not ever be called directly. * * @internal */ static initStatic() { const typeIdStr = stringifyTypeId(this.typeId); if (typeIdStr === '????') { throw new Error('BufferStruct.typeId must be set to a valid 32-bit integer'); } this.typeIdStr = typeIdStr; this.propDefs = [...this.propDefs]; this.staticInitialized = true; } setDirty(propIndex) { const dirtyWordOffset = Math.floor(propIndex / 32); const dirtyBitOffset = propIndex - dirtyWordOffset * 32; this.int32array[DIRTY_INT32_INDEX + dirtyWordOffset] = this.int32array[DIRTY_INT32_INDEX + dirtyWordOffset] | (1 << dirtyBitOffset); } resetDirty() { // TODO: Do we need to use atomics here? this.int32array[NOTIFY_INT32_INDEX] = 0; this.int32array[DIRTY_INT32_INDEX] = 0; this.int32array[DIRTY_INT32_INDEX + 1] = 0; } isDirty(propIndex) { if (propIndex !== undefined) { const dirtyWordOffset = Math.floor(propIndex / 32); const dirtyBitOffset = propIndex - dirtyWordOffset * 32; return !!(this.int32array[DIRTY_INT32_INDEX + dirtyWordOffset] & (1 << dirtyBitOffset)); } return !!(this.int32array[DIRTY_INT32_INDEX] || this.int32array[DIRTY_INT32_INDEX + 1]); } setUndefined(propIndex, value) { const undefWordOffset = Math.floor(propIndex / 32); const undefBitOffset = propIndex - undefWordOffset * 32; if (value) { this.int32array[UNDEFINED_INT32_INDEX + undefWordOffset] = this.int32array[UNDEFINED_INT32_INDEX + undefWordOffset] | (1 << undefBitOffset); } else { this.int32array[UNDEFINED_INT32_INDEX + undefWordOffset] = this.int32array[UNDEFINED_INT32_INDEX + undefWordOffset] & ~(1 << undefBitOffset); } } isUndefined(propIndex) { const undefWordOffset = Math.floor(propIndex / 32); const undefBitOffset = propIndex - undefWordOffset * 32; return !!(this.int32array[UNDEFINED_INT32_INDEX + undefWordOffset] & (1 << undefBitOffset)); } get typeId() { // Atomic load not required here because typeId is constant return this.int32array[TYPEID_INT32_INDEX]; } get id() { // Atomic load not required here because id is constant return this.float64array[ID_FLOAT64_INDEX]; } /** * Returns the current notify value */ get notifyValue() { return Atomics.load(this.int32array, NOTIFY_INT32_INDEX); } /** * Returns true if the BufferStruct is currently locked */ get isLocked() { return Atomics.load(this.int32array, LOCK_INT32_INDEX) !== 0; } lock(callback) { let origLock = Atomics.compareExchange(this.int32array, LOCK_INT32_INDEX, 0, this.lockId); while (origLock !== 0) { try { Atomics.wait(this.int32array, LOCK_INT32_INDEX, origLock); } catch (e) { if (e instanceof TypeError && e.message === 'Atomics.wait cannot be called in this context') { // Atomics.wait() not supported in this context (main worker), so just spin // TODO: Maybe we detect this earlier and avoid this exception? This works for now. } else { throw e; } } origLock = Atomics.compareExchange(this.int32array, LOCK_INT32_INDEX, 0, this.lockId); } let result; try { result = callback(); } finally { Atomics.store(this.int32array, LOCK_INT32_INDEX, 0); Atomics.notify(this.int32array, LOCK_INT32_INDEX); } return result; } async lockAsync(callback) { let origLock = Atomics.compareExchange(this.int32array, LOCK_INT32_INDEX, 0, this.lockId); while (origLock !== 0) { const result = Atomics.waitAsync(this.int32array, LOCK_INT32_INDEX, origLock); await result.value; origLock = Atomics.compareExchange(this.int32array, LOCK_INT32_INDEX, 0, this.lockId); } let result; try { result = await callback(); } finally { Atomics.store(this.int32array, LOCK_INT32_INDEX, 0); Atomics.notify(this.int32array, LOCK_INT32_INDEX); } return result; } notify(value) { if (value !== undefined) { Atomics.store(this.int32array, NOTIFY_INT32_INDEX, value); } return Atomics.notify(this.int32array, NOTIFY_INT32_INDEX); } wait(expectedValue, timeout = Infinity) { const result = Atomics.wait(this.int32array, NOTIFY_INT32_INDEX, expectedValue, timeout); return result; } async waitAsync(expectedValue, timeout = Infinity) { const result = Atomics.waitAsync(this.int32array, NOTIFY_INT32_INDEX, expectedValue, timeout); return result.value; } } export { BufferStruct }; //# sourceMappingURL=BufferStruct.js.map