@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
JavaScript
/*
* 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