multithreading
Version:
The missing standard library for multithreading in JavaScript (Works in the browser, Node.js, Deno, Bun).
1,178 lines (1,177 loc) • 54.3 kB
JavaScript
var _a;
import { register, Serializable, toDeserialized, toSerialized, } from "./shared.js";
const CONSOLE_VIEW = Symbol("SharedJsonBuffer.consoleView");
const OFFSET_FREE_PTR = 0;
const OFFSET_ROOT = 8; // 8-byte aligned
const HEADER_SIZE = 16;
const TYPE_NULL = 0;
const TYPE_TRUE = 1;
const TYPE_FALSE = 2;
const TYPE_NUMBER = 3;
const TYPE_STRING = 4;
const TYPE_OBJECT = 5;
const TYPE_ARRAY = 6;
const TYPE_MOVED = 0xffffffff;
function initConsoleHooks() {
if (typeof console === "undefined")
return;
const methods = [
"log",
"info",
"warn",
"error",
"dir",
"table",
"debug",
"trace",
];
for (const method of methods) {
const original = console[method];
console[method] = function (...args) {
for (let i = 0; i < args.length; i++) {
const arg = args[i];
// Check if it's our object and has the specific console view method
if (typeof arg === "object" &&
typeof arg[CONSOLE_VIEW] === "function") {
try {
// We access the hidden method via the Proxy trap
args[i] = arg[CONSOLE_VIEW]();
}
catch (e) {
// Fallback if something goes wrong
args[i] = arg;
}
}
}
return original.apply(this, args);
};
}
}
function countUtf8Bytes(str) {
let byteLen = 0;
const len = str.length;
for (let i = 0; i < len; i++) {
const code = str.charCodeAt(i);
if (code < 0x80) {
byteLen += 1;
}
else if (code < 0x800) {
byteLen += 2;
}
else if (code < 0xd800 || code >= 0xe000) {
byteLen += 3;
}
else {
i++; // Surrogate pair, skip next unit
byteLen += 4;
}
}
return byteLen;
}
class SharedJsonBufferImpl extends Serializable {
constructor(obj, optionsOrBuffer) {
super();
this.textDecoder = new TextDecoder();
this.textEncoder = new TextEncoder();
// Caches
this.stringCache = new Map();
this.proxyCache = new Map();
this.propertyHints = new Map();
// GC State
this.activeTargets = new Set();
this.registry = new FinalizationRegistry((target) => {
this.activeTargets.delete(target);
});
this.tempRoots = [];
// Instance Scratch Variables
this.scratchPtr = 0; // Public for ArrayCursor access
this.scratchCap = 0;
this.scratchLen = 0;
this.scratchStart = 0;
this.objectHandler = {
get: (target, prop, receiver) => {
if (typeof prop === "symbol") {
if (prop === CONSOLE_VIEW) {
return () => this.toConsoleView(target.__ptr);
}
if (prop === toSerialized)
return () => this[toSerialized]();
if (prop === Symbol.iterator)
return undefined;
return Reflect.get(target, prop, receiver);
}
if (prop === "__ptr")
return target.__ptr;
if (prop === "toJSON")
return () => this.toJSON(target.__ptr);
const ptr = target.__ptr;
if (ptr === 0)
return undefined;
// Optimization: Inline Pointer Resolve (Avoids function call)
const curr = ptr;
const type = this.u32[curr >> 2];
if (type !== TYPE_MOVED) {
this.scratchLen = this.u32[(curr + 8) >> 2];
this.scratchStart = curr + 12;
}
else {
this.resolvePtr(ptr);
}
const count = this.scratchLen;
const start = this.scratchStart;
const propStr = String(prop);
// Check Hints (O(1) Access)
const hint = this.propertyHints.get(propStr);
if (hint !== undefined && hint < count) {
const entryOffset = start + hint * 12;
const keyPtr = this.u32[entryOffset >> 2];
// Direct String Check (Optimized for "Happy Path")
// We assume the hint is correct, so we skip the byte-length math
// and just verify the string directly.
if (this.readString(keyPtr) === propStr) {
return this.readSlot(entryOffset + 4);
}
}
// Optimization: Pre-calculate UTF-8 Byte Length (O(K))
// This allows us to perform an integer check (O(1)) per entry
// instead of a string decode (O(K)) per entry.
const targetByteLen = countUtf8Bytes(propStr);
// Optimized Scan (O(N))
for (let i = 0; i < count; i++) {
const entryOffset = start + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
// Length check: Read the string byte-length header from memory
const storedLen = this.u32[keyPtr >> 2];
// Skip if lengths don't match.
// This avoids decoding/allocating strings for 99% of mismatches.
if (storedLen !== targetByteLen)
continue;
// Full string check (only performed on length candidates)
const key = this.readString(keyPtr);
if (key === propStr) {
this.propertyHints.set(propStr, i);
return this.readSlot(entryOffset + 4);
}
}
return undefined;
},
has: (target, prop) => {
if (typeof prop === "symbol") {
if (prop === CONSOLE_VIEW || prop === toSerialized)
return true;
return Reflect.has(target, prop);
}
if (prop === "__ptr" || prop === "toJSON")
return true;
const ptr = target.__ptr;
if (ptr === 0)
return false;
// Optimization: Inline Pointer Resolve
const curr = ptr;
const type = this.u32[curr >> 2];
if (type !== TYPE_MOVED) {
this.scratchLen = this.u32[(curr + 8) >> 2];
this.scratchStart = curr + 12;
}
else {
this.resolvePtr(ptr);
}
const propStr = String(prop);
const count = this.scratchLen;
const start = this.scratchStart;
// Check hint
const hint = this.propertyHints.get(propStr);
if (hint !== undefined && hint < count) {
const entryOffset = start + hint * 12;
const keyPtr = this.u32[entryOffset >> 2];
if (this.readString(keyPtr) === propStr)
return true;
}
// Optimization: Length Check
const targetByteLen = countUtf8Bytes(propStr);
// Scan
for (let i = 0; i < count; i++) {
const entryOffset = start + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
const storedLen = this.u32[keyPtr >> 2];
if (storedLen !== targetByteLen)
continue;
if (this.readString(keyPtr) === propStr) {
this.propertyHints.set(propStr, i);
return true;
}
}
return false;
},
set: (target, prop, value) => {
if (typeof prop === "symbol")
return false;
this.objectSet(target, String(prop), value);
return true;
},
defineProperty: (target, prop, descriptor) => {
if (typeof prop === "symbol")
return false;
if (descriptor.get || descriptor.set) {
throw new Error("SharedJsonBuffer cannot store accessors (get/set)");
}
if ("value" in descriptor) {
this.objectSet(target, String(prop), descriptor.value);
}
// We ignore enumerable/configurable/writable.
return true;
},
deleteProperty: (target, prop) => {
if (typeof prop === "symbol")
return false;
return this.objectDelete(target, String(prop));
},
ownKeys: (target) => {
this.resolvePtr(target.__ptr);
if (target.__ptr === 0)
return [];
const keys = [];
const start = this.scratchStart;
// Pre-fill hints during iteration
for (let i = 0; i < this.scratchLen; i++) {
const keyPtr = this.u32[(start + i * 12) >> 2];
const key = this.readString(keyPtr);
// When GOPD is called immediately after, it will hit this hint.
this.propertyHints.set(key, i);
keys.push(key);
}
return keys;
},
getOwnPropertyDescriptor: (target, prop) => {
// 1. Reset state to THIS object
this.resolvePtr(target.__ptr);
if (target.__ptr === 0)
return undefined;
const count = this.scratchLen;
const start = this.scratchStart;
const propStr = String(prop);
// 2. Check hint
const hint = this.propertyHints.get(propStr);
if (hint !== undefined && hint < count) {
const entryOffset = start + hint * 12;
const keyPtr = this.u32[entryOffset >> 2];
const keyStr = this.readString(keyPtr);
if (keyStr === propStr) {
// We just called resolvePtr, so we can read the value now
const val = this.readSlot(entryOffset + 4);
return {
enumerable: true,
configurable: true,
writable: true,
value: val,
};
}
}
// Optimization: Length Check
const targetByteLen = countUtf8Bytes(propStr);
// 3. Scan
for (let i = 0; i < count; i++) {
const entryOffset = start + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
const storedLen = this.u32[keyPtr >> 2];
if (storedLen !== targetByteLen)
continue;
const key = this.readString(keyPtr);
if (key === propStr) {
this.propertyHints.set(propStr, i);
// We just called resolvePtr, so we can read the value now
const val = this.readSlot(entryOffset + 4);
return {
enumerable: true,
configurable: true,
writable: true,
value: val,
};
}
}
return undefined;
},
};
this.arrayHandler = {
get: (target, prop, receiver) => {
if (prop === CONSOLE_VIEW)
return () => this.toConsoleView(target.__ptr);
if (prop === toSerialized)
return () => this[toSerialized]();
if (prop === "__ptr")
return target.__ptr;
if (prop === "toJSON")
return () => this.toJSON(target.__ptr);
if (prop === Symbol.iterator) {
return () => new ArrayCursor(this, target.__ptr);
}
this.resolvePtr(target.__ptr);
if (target.__ptr === 0)
return undefined;
if (prop === "length")
return this.scratchLen;
// 1. Map common mutators to splice (efficient, no array copy)
if (prop === "push") {
return (...args) => {
this.arraySpliceImpl(target, this.scratchLen, 0, args);
return this.scratchLen;
};
}
if (prop === "pop") {
return () => {
if (this.scratchLen === 0)
return undefined;
return this.arraySpliceImpl(target, this.scratchLen - 1, 1)[0];
};
}
if (prop === "shift") {
return () => {
if (this.scratchLen === 0)
return undefined;
return this.arraySpliceImpl(target, 0, 1)[0];
};
}
if (prop === "unshift") {
return (...args) => {
this.arraySpliceImpl(target, 0, 0, args);
return this.scratchLen;
};
}
if (prop === "splice") {
return (start, deleteCount, ...items) => {
const len = this.scratchLen;
const actualStart = start < 0 ? len + start : start;
const maxDel = len - (actualStart < 0 ? 0 : actualStart);
const actualDel = deleteCount === undefined
? maxDel
: Math.min(Math.max(deleteCount, 0), maxDel);
return this.arraySpliceImpl(target, actualStart, actualDel, items);
};
}
// 2. Explicit ES2019 Flattening Methods
if (prop === "flat") {
return (depth = 1) => {
const result = [];
const flatten = (ptr, currentDepth) => {
this.resolvePtr(ptr);
const len = this.scratchLen;
const start = this.scratchStart;
// Capture start offset so loop is safe even if recursing changes s_ptr
const captureStart = start;
for (let i = 0; i < len; i++) {
const offset = captureStart + i * 8;
const type = this.u32[offset >> 2];
const payload = this.u32[(offset + 4) >> 2];
if (type === TYPE_ARRAY && currentDepth > 0) {
flatten(payload, currentDepth - 1);
}
else {
result.push(this.readSlot(offset));
}
}
};
flatten(target.__ptr, Math.floor(depth));
return result;
};
}
if (prop === "flatMap") {
return (callback, thisArg) => {
const len = this.scratchLen;
const start = this.scratchStart;
const result = [];
for (let i = 0; i < len; i++) {
// Read value (resolves proxies if needed)
const val = this.readSlot(start + i * 8);
const mapped = callback.call(thisArg, val, i, receiver);
if (Array.isArray(mapped)) {
result.push(...mapped);
}
else {
result.push(mapped);
}
}
return result;
};
}
// 3. Map in-place mutators via temporary array (Sort, Reverse, Fill, CopyWithin)
if (typeof prop === "string" &&
["sort", "reverse", "fill", "copyWithin"].includes(prop)) {
return (...args) => {
const arr = this.toArrayShallow(target.__ptr);
arr[prop](...args);
// Write back changes
arr.forEach((v, i) => this.arraySet(target, i, v));
return receiver;
};
}
// 4. Fallback: Map all other read-only Array methods (map, filter, reduce, slice, join, etc.)
if (typeof prop === "string" && prop in Array.prototype) {
const nativeMethod = Array.prototype[prop];
if (typeof nativeMethod === "function") {
return (...args) => {
const arr = this.toArrayShallow(target.__ptr);
return nativeMethod.apply(arr, args);
};
}
}
// 5. Index Access
if (typeof prop === "string") {
const idx = Number(prop);
if (!isNaN(idx)) {
if (idx >= this.scratchLen)
return undefined;
return this.readSlot(this.scratchStart + idx * 8);
}
}
return Reflect.get(target, prop, receiver);
},
set: (target, prop, value) => {
if (prop === "length") {
const newLen = Number(value);
if (!isNaN(newLen) && newLen >= 0) {
this.resolvePtr(target.__ptr);
const currentLen = this.scratchLen;
if (newLen < currentLen) {
this.arraySpliceImpl(target, newLen, currentLen - newLen);
}
else if (newLen > currentLen) {
this.arrayEnsureCapacity(target, newLen);
this.u32[(this.scratchPtr + 8) >> 2] = newLen;
}
return true;
}
return false;
}
const idx = Number(prop);
if (!isNaN(idx)) {
this.arraySet(target, idx, value);
return true;
}
return false;
},
ownKeys: (target) => {
this.resolvePtr(target.__ptr);
const keys = [];
for (let i = 0; i < this.scratchLen; i++)
keys.push(String(i));
keys.push("length");
return keys;
},
getOwnPropertyDescriptor: (target, prop) => {
if (prop === "length") {
this.resolvePtr(target.__ptr);
return {
value: this.scratchLen,
writable: true,
enumerable: false,
configurable: false,
};
}
const idx = Number(prop);
if (!isNaN(idx)) {
this.resolvePtr(target.__ptr);
if (idx >= 0 && idx < this.scratchLen) {
const val = this.readSlot(this.scratchStart + idx * 8);
return {
value: val,
enumerable: true,
configurable: true,
writable: true,
};
}
}
return undefined;
},
};
if (optionsOrBuffer instanceof SharedArrayBuffer) {
this.buffer = optionsOrBuffer;
this.initViews();
if (Atomics.load(this.u32, OFFSET_FREE_PTR >> 2) === 0) {
this.initializeBuffer(obj);
}
}
else {
const size = optionsOrBuffer?.size || 1024 * 64;
this.buffer = new SharedArrayBuffer(size);
this.initViews();
this.initializeBuffer(obj);
}
return this.getRootProxy();
}
initViews() {
this.u32 = new Uint32Array(this.buffer);
this.f64 = new Float64Array(this.buffer);
this.u8 = new Uint8Array(this.buffer);
}
initializeBuffer(obj) {
Atomics.store(this.u32, OFFSET_FREE_PTR >> 2, HEADER_SIZE);
const isArr = Array.isArray(obj);
const initialKeys = isArr
? obj.length
: (obj ? Object.keys(obj).length : 0);
const rootPtr = isArr
? this.allocArray(initialKeys)
: this.allocObject(initialKeys);
Atomics.store(this.u32, OFFSET_ROOT >> 2, rootPtr);
if (obj) {
const rootTarget = { __ptr: rootPtr };
this.tempRoots.push({
handle: rootTarget,
type: isArr ? TYPE_ARRAY : TYPE_OBJECT,
});
try {
this.writeInitial(rootTarget, obj);
}
finally {
this.tempRoots.pop();
Atomics.store(this.u32, OFFSET_ROOT >> 2, rootTarget.__ptr);
}
}
}
getRootProxy() {
const rootPtr = Atomics.load(this.u32, OFFSET_ROOT >> 2);
return this.getProxyForPtr(rootPtr);
}
alloc(byteSize, retry = true) {
const idx = OFFSET_FREE_PTR >> 2;
const currentPtr = Atomics.load(this.u32, idx);
// Align to 8 bytes for Float64 performance
const nextPtr = currentPtr + byteSize;
const alignedNext = (nextPtr + 7) & ~7;
if (alignedNext > this.buffer.byteLength) {
if (retry) {
this.collectGarbage();
this.propertyHints.clear();
return this.alloc(byteSize, false);
}
throw new Error(`SharedJsonBuffer OOM: Used ${alignedNext} of ${this.buffer.byteLength}`);
}
Atomics.store(this.u32, idx, alignedNext);
return currentPtr;
}
collectGarbage() {
const tempBuffer = new ArrayBuffer(this.buffer.byteLength);
const tempU32 = new Uint32Array(tempBuffer);
const tempF64 = new Float64Array(tempBuffer);
const tempU8 = new Uint8Array(tempBuffer);
let freePtr = HEADER_SIZE;
const visited = new Map();
const allocTemp = (size) => {
const ptr = freePtr;
freePtr = (freePtr + size + 7) & ~7;
if (freePtr > tempBuffer.byteLength) {
throw new Error("GC Fatal: Fragmentation too high");
}
return ptr;
};
const relocate = (oldPtr, type) => {
if (oldPtr === 0)
return 0;
if (type === TYPE_STRING) {
if (visited.has(oldPtr))
return visited.get(oldPtr);
const len = this.u32[oldPtr >> 2];
const newPtr = allocTemp(4 + len);
tempU32[newPtr >> 2] = len;
tempU8.set(this.u8.subarray(oldPtr + 4, oldPtr + 4 + len), newPtr + 4);
visited.set(oldPtr, newPtr);
return newPtr;
}
if (type === TYPE_NUMBER) {
if (visited.has(oldPtr))
return visited.get(oldPtr);
const newPtr = allocTemp(8);
tempF64[newPtr >> 3] = this.f64[oldPtr >> 3];
visited.set(oldPtr, newPtr);
return newPtr;
}
this.resolvePtr(oldPtr);
const actualOldPtr = this.scratchPtr;
if (visited.has(actualOldPtr))
return visited.get(actualOldPtr);
const actualType = this.u32[actualOldPtr >> 2];
let newPtr = 0;
if (actualType === TYPE_OBJECT) {
const count = this.u32[(actualOldPtr + 8) >> 2];
const newCap = Math.max(4, count);
newPtr = allocTemp(12 + newCap * 12);
tempU32[newPtr >> 2] = TYPE_OBJECT;
tempU32[(newPtr + 4) >> 2] = newCap;
tempU32[(newPtr + 8) >> 2] = count;
const startOffset = actualOldPtr + 12;
const newStartOffset = newPtr + 12;
for (let i = 0; i < count; i++) {
const entryOff = startOffset + i * 12;
const kPtr = this.u32[entryOff >> 2];
const vType = this.u32[(entryOff + 4) >> 2];
const vPayload = this.u32[(entryOff + 8) >> 2];
const newKeyPtr = relocate(kPtr, TYPE_STRING);
let newPayload = vPayload;
if (vType === TYPE_OBJECT || vType === TYPE_ARRAY ||
vType === TYPE_STRING || vType === TYPE_NUMBER) {
newPayload = relocate(vPayload, vType);
}
const destOff = newStartOffset + i * 12;
tempU32[destOff >> 2] = newKeyPtr;
tempU32[(destOff + 4) >> 2] = vType;
tempU32[(destOff + 8) >> 2] = newPayload;
}
}
else if (actualType === TYPE_ARRAY) {
const len = this.u32[(actualOldPtr + 8) >> 2];
const newCap = Math.max(4, len);
newPtr = allocTemp(12 + newCap * 8);
tempU32[newPtr >> 2] = TYPE_ARRAY;
tempU32[(newPtr + 4) >> 2] = newCap;
tempU32[(newPtr + 8) >> 2] = len;
const startOffset = actualOldPtr + 12;
const newStartOffset = newPtr + 12;
for (let i = 0; i < len; i++) {
const entryOff = startOffset + i * 8;
const vType = this.u32[entryOff >> 2];
const vPayload = this.u32[(entryOff + 4) >> 2];
let newPayload = vPayload;
if (vType === TYPE_OBJECT || vType === TYPE_ARRAY ||
vType === TYPE_STRING || vType === TYPE_NUMBER) {
newPayload = relocate(vPayload, vType);
}
const destOff = newStartOffset + i * 8;
tempU32[destOff >> 2] = vType;
tempU32[(destOff + 4) >> 2] = newPayload;
}
}
visited.set(actualOldPtr, newPtr);
return newPtr;
};
const oldRoot = Atomics.load(this.u32, OFFSET_ROOT >> 2);
const newRoot = relocate(oldRoot, TYPE_OBJECT);
for (let i = 0; i < this.tempRoots.length; i++) {
const root = this.tempRoots[i];
relocate(root.handle.__ptr, root.type);
}
this.u8.set(new Uint8Array(tempBuffer).subarray(0, freePtr), 0);
Atomics.store(this.u32, OFFSET_FREE_PTR >> 2, freePtr);
Atomics.store(this.u32, OFFSET_ROOT >> 2, newRoot);
this.stringCache.clear();
this.proxyCache.clear();
this.propertyHints.clear();
const fixupPointer = (target) => {
this.resolvePtr(target.__ptr);
const oldP = this.scratchPtr;
if (visited.has(oldP)) {
target.__ptr = visited.get(oldP);
}
else {
target.__ptr = 0;
}
};
for (const target of this.activeTargets) {
fixupPointer(target);
}
for (const root of this.tempRoots) {
fixupPointer(root.handle);
}
}
resolvePtr(ptr) {
if (ptr === 0) {
this.scratchPtr = 0;
this.scratchLen = 0;
return;
}
let curr = ptr;
let type = this.u32[curr >> 2];
while (type === TYPE_MOVED) {
curr = this.u32[(curr + 4) >> 2];
type = this.u32[curr >> 2];
}
this.scratchPtr = curr;
this.scratchCap = this.u32[(curr + 4) >> 2];
this.scratchLen = this.u32[(curr + 8) >> 2];
this.scratchStart = curr + 12;
}
readString(ptr) {
if (this.stringCache.has(ptr)) {
return this.stringCache.get(ptr);
}
const len = this.u32[ptr >> 2];
const offset = ptr + 4;
// Optimization: SWAR (SIMD Within A Register) for short strings.
// TextDecoder has high overhead for short strings (< ~64 chars).
// Manual decoding is faster, provided we can process 4 bytes at a time.
if (len < 64) {
let res = "";
let i = 0;
// We can safely read u32 from 'offset' because 'ptr' is 8-byte aligned,
// making 'offset' (ptr + 4) always 4-byte aligned.
const u32Index = offset >> 2;
const loopLimit = len - 3; // Ensure we have a full 4-byte chunk
for (; i < loopLimit; i += 4) {
const chunk = this.u32[u32Index + (i >> 2)];
// Magic Mask: 0x80808080
// Checks bit 7 of all 4 bytes simultaneously.
// If ANY bit is set, it's UTF-8 (multibyte), so we bail to TextDecoder.
if ((chunk & 0x80808080) !== 0) {
i = -1; // Flag as failed
break;
}
// Fast Decode: We verified all 4 bytes are ASCII.
// Unpack Little-Endian u32 into characters.
res += String.fromCharCode(chunk & 0xff, (chunk >> 8) & 0xff, (chunk >> 16) & 0xff, chunk >>> 24);
}
// Handle trailing bytes (0 to 3 bytes remainder) or check failure
if (i !== -1) {
for (; i < len; i++) {
const code = this.u8[offset + i];
if (code & 0x80) {
i = -1;
break;
}
res += String.fromCharCode(code);
}
}
// If i != -1, we successfully decoded everything as ASCII
if (i !== -1) {
this.stringCache.set(ptr, res);
return res;
}
// If we flagged -1, we hit a UTF-8 char. Fall through to TextDecoder.
}
// Fallback: Long strings OR Strings containing Multi-byte chars
const str = this.textDecoder.decode(this.u8.subarray(offset, offset + len));
this.stringCache.set(ptr, str);
return str;
}
readSlot(offset) {
const type = this.u32[offset >> 2];
const payload = this.u32[(offset + 4) >> 2];
if (type === TYPE_NUMBER) {
return this.f64[payload >> 3];
}
switch (type) {
case TYPE_STRING:
return this.readString(payload);
case TYPE_OBJECT:
case TYPE_ARRAY:
return this.getProxyForPtr(payload);
case TYPE_TRUE:
return true;
case TYPE_FALSE:
return false;
case TYPE_NULL:
return null;
default:
return undefined;
}
}
writeValue(value) {
if (typeof value === "number") {
const ptr = this.alloc(8);
this.f64[ptr >> 3] = value;
return { type: TYPE_NUMBER, payload: ptr };
}
if (value === null || value === undefined) {
return { type: TYPE_NULL, payload: 0 };
}
if (value === true)
return { type: TYPE_TRUE, payload: 0 };
if (value === false)
return { type: TYPE_FALSE, payload: 0 };
if (typeof value === "string") {
// Get current free pointer directly (bypass alloc() overhead for now)
const freePtrIdx = OFFSET_FREE_PTR >> 2;
let currentPtr = Atomics.load(this.u32, freePtrIdx);
// Ensure alignment for the 4-byte length header
currentPtr = (currentPtr + 3) & ~3;
// Calculate worst-case size (3 bytes per char for UTF-8 + 4 bytes header)
const maxBytes = value.length * 3 + 4;
// Safety Check: If near end of buffer, fallback to standard alloc() (which handles GC and OOM errors properly)
if (currentPtr + maxBytes > this.buffer.byteLength) {
const encoded = this.textEncoder.encode(value); // Slow path allocation
const len = encoded.byteLength;
const ptr = this.alloc(4 + len);
this.u32[ptr >> 2] = len;
this.u8.set(encoded, ptr + 4);
return { type: TYPE_STRING, payload: ptr };
}
// This faster because it writes directly to shared memory but does not work in the browser because of security reasons:
// TextEncoder.encodeInto: Argument 2 can't be a SharedArrayBuffer or an ArrayBufferView backed by a SharedArrayBuffer
// const { written } = this.textEncoder.encodeInto(
// value,
// this.u8.subarray(currentPtr + 4, currentPtr + maxBytes),
// );
// So alternatively we use the (slower) approach below to stay compatible with browsers
const encoded = this.textEncoder.encode(value);
const written = Math.min(encoded.length, maxBytes);
this.u8.set(encoded.subarray(0, written), currentPtr + 4);
// Write actual length
this.u32[currentPtr >> 2] = written;
// Manually advance free pointer (Align to 8 bytes for future number writes)
const actualSize = 4 + written;
const nextPtr = (currentPtr + actualSize + 7) & ~7;
Atomics.store(this.u32, freePtrIdx, nextPtr);
return { type: TYPE_STRING, payload: currentPtr };
}
if (Array.isArray(value)) {
const ptr = this.allocArray(value.length);
const target = { __ptr: ptr };
this.tempRoots.push({ handle: target, type: TYPE_ARRAY });
try {
value.forEach((v, i) => this.arraySet(target, i, v));
}
finally {
this.tempRoots.pop();
}
return { type: TYPE_ARRAY, payload: target.__ptr };
}
if (typeof value === "object") {
const keys = Object.keys(value);
const ptr = this.allocObject(keys.length);
const target = { __ptr: ptr };
this.tempRoots.push({ handle: target, type: TYPE_OBJECT });
try {
Object.entries(value).forEach(([k, v]) => this.objectSet(target, k, v));
}
finally {
this.tempRoots.pop();
}
return { type: TYPE_OBJECT, payload: target.__ptr };
}
throw new Error(`Unsupported type: ${typeof value}`);
}
allocObject(initialCap) {
const capacity = Math.max(4, initialCap);
const byteSize = 12 + capacity * 12;
const ptr = this.alloc(byteSize);
const idx = ptr >> 2;
this.u32[idx] = TYPE_OBJECT;
this.u32[idx + 1] = capacity;
this.u32[idx + 2] = 0;
return ptr;
}
allocArray(initialCap) {
const capacity = Math.max(4, initialCap);
const byteSize = 12 + capacity * 8;
const ptr = this.alloc(byteSize);
const idx = ptr >> 2;
this.u32[idx] = TYPE_ARRAY;
this.u32[idx + 1] = capacity;
this.u32[idx + 2] = 0;
return ptr;
}
writeInitial(target, data) {
if (Array.isArray(data)) {
data.forEach((v, i) => this.arraySet(target, i, v));
}
else {
Object.entries(data).forEach(([k, v]) => this.objectSet(target, k, v));
}
}
getProxyForPtr(ptr) {
this.resolvePtr(ptr);
const resolvedPtr = this.scratchPtr;
// Check cache
if (this.proxyCache.has(resolvedPtr)) {
const ref = this.proxyCache.get(resolvedPtr);
const cached = ref?.deref();
if (cached)
return cached; // Return if still in memory
}
const type = this.u32[resolvedPtr >> 2];
// Initialize proper target for formatting
const target = type === TYPE_ARRAY ? [] : {};
Object.defineProperty(target, "__ptr", {
value: resolvedPtr,
writable: true,
configurable: true,
enumerable: false, // Ensure this is hidden
});
this.activeTargets.add(target);
const proxy = new Proxy(target, type === TYPE_ARRAY ? this.arrayHandler : this.objectHandler);
this.registry.register(proxy, target);
// Store as WeakRef
this.proxyCache.set(resolvedPtr, new WeakRef(proxy));
return proxy;
}
toConsoleView(ptr, depth = 0) {
this.resolvePtr(ptr);
const len = this.scratchLen;
const start = this.scratchStart;
const type = this.u32[this.scratchPtr >> 2];
const result = type === TYPE_ARRAY ? new Array(len) : {};
// Config: How much to show eagerly?
const EAGER_DEPTH = 5; // Show root +4 nested levels
const EAGER_BREADTH = 100; // Only show first 100 items of arrays (default in Node and Deno)
for (let i = 0; i < len; i++) {
let key;
let offset;
if (type === TYPE_ARRAY) {
key = i;
offset = start + i * 8;
}
else {
const entryOffset = start + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
key = this.readString(keyPtr);
offset = entryOffset + 4;
}
const itemType = this.u32[offset >> 2];
const itemPayload = this.u32[(offset + 4) >> 2];
if (itemType === TYPE_OBJECT || itemType === TYPE_ARRAY) {
// Eager vs lazy decision
// We eagerly decode if:
// 1. We haven't hit the depth limit and
// 2. We haven't hit the breadth limit
const isEager = depth < EAGER_DEPTH && i < EAGER_BREADTH;
if (isEager) {
result[key] = this.toConsoleView(itemPayload, depth + 1);
}
else {
// Lazy Getter: When clicked, restart with depth 0 so the user sees the content
Object.defineProperty(result, key, {
enumerable: true,
configurable: true,
get: () => {
return this.toConsoleView(itemPayload, 0);
},
});
}
}
else {
result[key] = this.readSlot(offset);
}
}
return result;
}
toJSON(ptr) {
this.resolvePtr(ptr);
const len = this.scratchLen;
const start = this.scratchStart;
const type = this.u32[this.scratchPtr >> 2];
if (type === TYPE_ARRAY) {
const arr = new Array(len);
for (let i = 0; i < len; i++) {
const offset = start + i * 8;
const itemType = this.u32[offset >> 2];
const itemPayload = this.u32[(offset + 4) >> 2];
if (itemType === TYPE_OBJECT || itemType === TYPE_ARRAY) {
arr[i] = this.toJSON(itemPayload);
}
else {
arr[i] = this.readSlot(offset);
}
}
return arr;
}
else {
const obj = {};
for (let i = 0; i < len; i++) {
const entryOffset = start + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
const key = this.readString(keyPtr);
const itemType = this.u32[(entryOffset + 4) >> 2];
const itemPayload = this.u32[(entryOffset + 8) >> 2];
if (itemType === TYPE_OBJECT || itemType === TYPE_ARRAY) {
obj[key] = this.toJSON(itemPayload);
}
else {
obj[key] = this.readSlot(entryOffset + 4);
}
}
return obj;
}
}
objectDelete(target, key) {
this.resolvePtr(target.__ptr);
const ptr = this.scratchPtr;
const count = this.scratchLen;
const entriesStart = this.scratchStart;
let foundIdx = -1;
for (let i = 0; i < count; i++) {
const entryOffset = entriesStart + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
if (this.readString(keyPtr) === key) {
foundIdx = i;
break;
}
}
if (foundIdx === -1)
return true;
const lastIdx = count - 1;
if (foundIdx !== lastIdx) {
const lastOffset = entriesStart + lastIdx * 12;
const foundOffset = entriesStart + foundIdx * 12;
this.u32[foundOffset >> 2] = this.u32[lastOffset >> 2];
this.u32[(foundOffset + 4) >> 2] = this.u32[(lastOffset + 4) >> 2];
this.u32[(foundOffset + 8) >> 2] = this.u32[(lastOffset + 8) >> 2];
}
this.u32[(ptr + 8) >> 2] = count - 1;
return true;
}
objectSet(target, key, value) {
this.resolvePtr(target.__ptr);
const preScanLen = this.scratchLen;
const preScanStart = this.scratchStart;
for (let i = 0; i < preScanLen; i++) {
const entryOffset = preScanStart + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
if (this.readString(keyPtr) === key) {
this.u32[(entryOffset + 4) >> 2] = TYPE_NULL;
this.u32[(entryOffset + 8) >> 2] = 0;
break;
}
}
const valResult = this.writeValue(value);
const valHandle = { __ptr: valResult.payload };
const isValPtr = valResult.type >= TYPE_NUMBER;
if (isValPtr) {
this.tempRoots.push({ handle: valHandle, type: valResult.type });
}
try {
this.resolvePtr(target.__ptr);
let ptr = this.scratchPtr;
const entriesStart = this.scratchStart;
const count = this.scratchLen;
const cap = this.scratchCap;
for (let i = 0; i < count; i++) {
const entryOffset = entriesStart + i * 12;
const keyPtr = this.u32[entryOffset >> 2];
if (this.readString(keyPtr) === key) {
this.u32[(entryOffset + 4) >> 2] = valResult.type;
this.u32[(entryOffset + 8) >> 2] = valHandle.__ptr;
return;
}
}
const keyResult = this.writeValue(key);
const keyHandle = { __ptr: keyResult.payload };
this.tempRoots.push({ handle: keyHandle, type: TYPE_STRING });
try {
this.resolvePtr(target.__ptr);
ptr = this.scratchPtr;
const currentCap = this.scratchCap;
const currentCount = this.scratchLen;
if (currentCount >= currentCap) {
const newCap = Math.max(currentCap * 2, 4);
const newByteSize = 12 + newCap * 12;
const newPtr = this.alloc(newByteSize);
this.resolvePtr(target.__ptr);
const oldDataStart = this.scratchStart;
const idx = newPtr >> 2;
this.u32[idx] = TYPE_OBJECT;
this.u32[idx + 1] = newCap;
this.u32[idx + 2] = currentCount + 1;
this.u8.set(this.u8.subarray(oldDataStart, oldDataStart + currentCount * 12), newPtr + 12);
const entryOffset = newPtr + 12 + currentCount * 12;
const eIdx = entryOffset >> 2;
this.u32[eIdx] = keyHandle.__ptr;
this.u32[eIdx + 1] = valResult.type;
this.u32[eIdx + 2] = valHandle.__ptr;
const pIdx = this.scratchPtr >> 2;
this.u32[pIdx] = TYPE_MOVED;
this.u32[pIdx + 1] = newPtr;
}
else {
const entryOffset = this.scratchStart + currentCount * 12;
const eIdx = entryOffset >> 2;
this.u32[eIdx] = keyHandle.__ptr;
this.u32[eIdx + 1] = valResult.type;
this.u32[eIdx + 2] = valHandle.__ptr;
this.u32[(ptr + 8) >> 2] = currentCount + 1;
}
}
finally {
this.tempRoots.pop();
}
}
finally {
if (isValPtr)
this.tempRoots.pop();
}
}
// --- Array Methods Support ---
arrayEnsureCapacity(target, minCap) {
this.resolvePtr(target.__ptr);
if (this.scratchCap >= minCap)
return;
const oldCap = this.scratchCap;
const oldLen = this.scratchLen;
const oldDataStart = this.scratchStart;
const newCap = Math.max(oldCap * 2, minCap);
const newByteSize = 12 + newCap * 8;
const newPtr = this.alloc(newByteSize);
// Re-resolve after alloc
this.resolvePtr(target.__ptr);
const idx = newPtr >> 2;
this.u32[idx] = TYPE_ARRAY;
this.u32[idx + 1] = newCap;
this.u32[idx + 2] = oldLen;
// Copy existing data
this.u8.set(this.u8.subarray(oldDataStart, oldDataStart + oldLen * 8), newPtr + 12);
// Mark old as moved
const pIdx = this.scratchPtr >> 2;
this.u32[pIdx] = TYPE_MOVED;
this.u32[pIdx + 1] = newPtr;
this.resolvePtr(target.__ptr);
}
arraySpliceImpl(target, start, deleteCount, items = []) {
this.resolvePtr(target.__ptr);
const len = this.scratchLen;
const actualStart = start < 0
? Math.max(len + start, 0)
: Math.min(start, len);
const actualDeleteCount = Math.min(Math.max(deleteCount, 0), len - actualStart);
// 1. Read deleted items to return
const deletedItems = [];
for (let i = 0; i < actualDeleteCount; i++) {
const offset = this.scratchStart + (actualStart + i) * 8;
deletedItems.push(this.readSlot(offset));
}
const insertCount = items.length;
const delta = insertCount - actualDeleteCount;
const newLen = len + delta;
// 2. Ensure Capacity (Allocates new buffer if needed)
this.arrayEnsureCapacity(target, newLen);
// After this, this.s_start, s_ptr, s_cap are updated to potentially new location
// 3. Move Memory (Shift tail)
if (delta !== 0) {
const tailCount = len - (actualStart + actualDeleteCount);
const srcIdx = actualStart + actualDeleteCount;
const destIdx = actualStart + insertCount;
const srcOffset = this.scratchStart + srcIdx * 8;
const destOffset = this.scratchStart + destIdx * 8;
const byteLen = tailCount * 8;
this.u8.copyWithin(destOffset, srcOffset, srcOffset + byteLen);
}
// 4. Insert Items
for (let i = 0; i < insertCount; i++) {
const val = items[i];
const valResult = this.writeValue(val);
const valHandle = { __ptr: valResult.payload };
const isValPtr = valResult.type >= TYPE_NUMBER;
if (isValPtr) {
this.tempRoots.push({ handle: valHandle, type: valResult.type });
}
this.resolvePtr(target.__ptr);
const offset = this.scratchStart + (actualStart + i) * 8;
const oIdx = offset >> 2;
this.u32[oIdx] = valResult.type;
this.u32[oIdx + 1] = valHandle.__ptr;
if (isValPtr)
this.tempRoots.pop();
}
// 5. Update Length
this.u32[(this.scratchPtr + 8) >> 2] = newLen;
this.scratchLen = newLen;
return deletedItems;
}
/**
* Helper to create a shallow JS Array containing Proxies or primitives
* derived from the underlying buffer.
*/
toArrayShallow(ptr) {
this.resolvePtr(ptr);
const len = this.scratchLen;
const start = this.scratchStart;
const result = new Array(len);
for (let i = 0; i < len; i++) {
result[i] = this.readSlot(start + i * 8);
}