🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@nxtedition/shared

Package Overview
Dependencies
Maintainers
10
Versions
81
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nxtedition/shared - npm Package Compare versions

Comparing version
5.0.2
to
5.0.3
binding.gyp

Sorry, the diff of this file is not supported yet

+1
export default (await import('node-gyp-build')).default(import.meta.dirname)
#include <nan.h>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
// ─── Platform-specific ring buffer implementation ────────────────────────────
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <memoryapi.h> // VirtualAlloc2, MapViewOfFile3, UnmapViewOfFileEx (Windows 10 1803+)
struct ring_buf_t
{
std::atomic<int> refcount;
size_t size;
void *addr;
HANDLE mapping;
};
static void ring_buf_free(ring_buf_t *rb)
{
// Restore both views to placeholders, then release each separately.
// VirtualFree(addr, 0, MEM_RELEASE) only releases the placeholder it was
// called on — the split created two distinct regions, so both must be freed.
UnmapViewOfFileEx(rb->addr, MEM_PRESERVE_PLACEHOLDER);
UnmapViewOfFileEx((char *)rb->addr + rb->size, MEM_PRESERVE_PLACEHOLDER);
VirtualFree(rb->addr, 0, MEM_RELEASE);
VirtualFree((char *)rb->addr + rb->size, 0, MEM_RELEASE);
CloseHandle(rb->mapping);
free(rb);
}
static ring_buf_t *ring_buf_alloc(size_t requested_size)
{
SYSTEM_INFO si;
GetSystemInfo(&si);
size_t align = si.dwAllocationGranularity; // typically 65536 on Windows
size_t size = (requested_size + align - 1) & ~(align - 1);
// Reserve a contiguous 2*size placeholder, then split it into two halves.
void *addr = VirtualAlloc2(nullptr, nullptr, size * 2, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER,
PAGE_NOACCESS, nullptr, 0);
if (!addr)
return nullptr;
if (!VirtualFree(addr, size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER))
{
VirtualFree(addr, 0, MEM_RELEASE);
return nullptr;
}
HANDLE mapping = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
(DWORD)(size >> 32), (DWORD)(size & 0xFFFFFFFF), nullptr);
if (!mapping)
{
// After the split both halves are separate placeholders; free each individually.
VirtualFree(addr, 0, MEM_RELEASE);
VirtualFree((char *)addr + size, 0, MEM_RELEASE);
return nullptr;
}
// Map the same section into both placeholders.
void *v1 = MapViewOfFile3(mapping, nullptr, addr, 0, size, MEM_REPLACE_PLACEHOLDER,
PAGE_READWRITE, nullptr, 0);
if (!v1)
{
CloseHandle(mapping);
VirtualFree(addr, 0, MEM_RELEASE);
VirtualFree((char *)addr + size, 0, MEM_RELEASE);
return nullptr;
}
void *v2 = MapViewOfFile3(mapping, nullptr, (char *)addr + size, 0, size, MEM_REPLACE_PLACEHOLDER,
PAGE_READWRITE, nullptr, 0);
if (!v2)
{
UnmapViewOfFileEx(v1, MEM_PRESERVE_PLACEHOLDER);
VirtualFree(addr, 0, MEM_RELEASE);
VirtualFree((char *)addr + size, 0, MEM_RELEASE);
CloseHandle(mapping);
return nullptr;
}
ring_buf_t *rb = (ring_buf_t *)malloc(sizeof(ring_buf_t));
if (!rb)
{
UnmapViewOfFileEx(v1, MEM_PRESERVE_PLACEHOLDER);
UnmapViewOfFileEx(v2, MEM_PRESERVE_PLACEHOLDER);
VirtualFree(addr, 0, MEM_RELEASE);
VirtualFree((char *)addr + size, 0, MEM_RELEASE);
CloseHandle(mapping);
return nullptr;
}
rb->refcount.store(1, std::memory_order_relaxed);
rb->size = size;
rb->addr = addr;
rb->mapping = mapping;
return rb;
}
static const char *ring_buf_errmsg(char *buf, size_t n)
{
snprintf(buf, n, "ring_alloc failed: Windows error %lu", (unsigned long)GetLastError());
return buf;
}
#else // POSIX (Linux / macOS)
#define _GNU_SOURCE 1
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#ifndef MAP_ANONYMOUS
#define MAP_ANONYMOUS MAP_ANON
#endif
#ifdef __linux__
#include <sys/syscall.h>
// Huge-page constants — defined here as fallback for older glibc headers.
#ifndef MFD_HUGETLB
#define MFD_HUGETLB 0x0004U
#endif
#ifndef MFD_HUGE_2MB
#define MFD_HUGE_2MB (21U << 26)
#endif
#ifndef MAP_HUGETLB
#define MAP_HUGETLB 0x040000
#endif
#ifndef MAP_HUGE_2MB
#define MAP_HUGE_2MB (21 << 26)
#endif
#define RING_HUGE_2MB (2UL * 1024 * 1024)
static int open_memfd(size_t size)
{
// MFD_CLOEXEC: prevent the fd from leaking into execve'd child processes.
int fd = (int)syscall(SYS_memfd_create, "ring", (unsigned)MFD_CLOEXEC);
if (fd < 0)
return -1;
if (ftruncate(fd, (off_t)size) < 0)
{
close(fd);
return -1;
}
return fd;
}
static int open_memfd_huge(size_t size)
{
// MFD_CLOEXEC: prevent the fd from leaking into execve'd child processes.
int fd =
(int)syscall(SYS_memfd_create, "ring", (unsigned)(MFD_CLOEXEC | MFD_HUGETLB | MFD_HUGE_2MB));
if (fd < 0)
return -1;
if (ftruncate(fd, (off_t)size) < 0)
{
close(fd);
return -1;
}
return fd;
}
#elif defined(__APPLE__)
#include <sys/stat.h>
static std::atomic<int> shm_seq{0};
static int open_memfd(size_t size)
{
char name[64];
snprintf(name, sizeof(name), "/ring_%d_%d", (int)getpid(),
shm_seq.fetch_add(1, std::memory_order_relaxed));
int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd < 0)
return -1;
shm_unlink(name);
if (ftruncate(fd, (off_t)size) < 0)
{
close(fd);
return -1;
}
return fd;
}
#else
#error "Unsupported platform"
#endif
// MAP_POPULATE is Linux-specific; define as 0 on platforms that lack it.
#ifndef MAP_POPULATE
#define MAP_POPULATE 0
#endif
struct ring_buf_t
{
std::atomic<int> refcount;
size_t size;
void *addr;
};
static void ring_buf_free(ring_buf_t *rb)
{
munmap(rb->addr, rb->size * 2);
free(rb);
}
// Maps fd (already ftruncated to size) into a contiguous 2*size virtual region.
// extra_flags is ORed into the data mmap calls only (e.g. MAP_HUGETLB|MAP_HUGE_2MB).
// Always consumes (closes) fd.
static ring_buf_t *ring_buf_alloc_from_fd(int fd, size_t size, int extra_flags)
{
// Reserve address space without huge-page flags — the reservation only needs
// virtual address range; applying MAP_HUGETLB here can cause spurious failures
// when the huge-page pool is exhausted even though no pages are faulted in yet.
void *addr = mmap(nullptr, size * 2, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
{
close(fd);
return nullptr;
}
// MAP_POPULATE: fault in all pages at mmap time so the hot path never takes a page-fault.
if (mmap(addr, size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED | MAP_POPULATE | extra_flags,
fd, 0) == MAP_FAILED ||
mmap((char *)addr + size, size, PROT_READ | PROT_WRITE,
MAP_FIXED | MAP_SHARED | MAP_POPULATE | extra_flags, fd, 0) == MAP_FAILED)
{
munmap(addr, size * 2);
close(fd);
return nullptr;
}
close(fd);
ring_buf_t *rb = (ring_buf_t *)malloc(sizeof(ring_buf_t));
if (!rb)
{
munmap(addr, size * 2);
return nullptr;
}
rb->refcount.store(1, std::memory_order_relaxed);
rb->size = size;
rb->addr = addr;
return rb;
}
static ring_buf_t *ring_buf_alloc(size_t requested_size)
{
#ifdef __linux__
// Try 2 MiB explicit huge pages first (requires pre-allocated huge page pool).
// Falls back silently if the kernel/system doesn't support it.
// Skip if the allocation is much smaller than a huge page to avoid wasting memory.
size_t huge_size = (requested_size + RING_HUGE_2MB - 1) & ~(RING_HUGE_2MB - 1);
int huge_fd = (requested_size >= RING_HUGE_2MB / 2) ? open_memfd_huge(huge_size) : -1;
if (huge_fd >= 0)
{
ring_buf_t *rb = ring_buf_alloc_from_fd(huge_fd, huge_size, MAP_HUGETLB | MAP_HUGE_2MB);
if (rb)
{
// MADV_DONTFORK: exclude from forked children (see normal path below for rationale).
madvise(rb->addr, rb->size * 2, MADV_DONTFORK);
return rb;
}
}
#endif
long page = sysconf(_SC_PAGESIZE);
if (page <= 0)
page = 4096;
size_t size = (requested_size + (size_t)page - 1) & ~((size_t)page - 1);
int fd = open_memfd(size);
if (fd < 0)
return nullptr;
ring_buf_t *rb = ring_buf_alloc_from_fd(fd, size, 0);
#ifdef __linux__
// Hint the kernel to back this region with transparent huge pages opportunistically.
if (rb)
madvise(rb->addr, rb->size * 2, MADV_HUGEPAGE);
#endif
// MADV_DONTFORK: exclude the ring buffer from forked child processes.
// Without this, fork() marks all pages copy-on-write; a single write in the
// child triggers physical page duplication even though the child has no use
// for the ring buffer. Guard with #ifdef because macOS SDK versions vary in
// whether MADV_DONTFORK is defined.
#ifdef MADV_DONTFORK
if (rb)
madvise(rb->addr, rb->size * 2, MADV_DONTFORK);
#endif
return rb;
}
static const char *ring_buf_errmsg(char *buf, size_t n)
{
snprintf(buf, n, "ring_alloc failed: %s", strerror(errno));
return buf;
}
#endif // _WIN32
// ─── Reference counting ──────────────────────────────────────────────────────
static void ring_buf_acquire(ring_buf_t *rb)
{
rb->refcount.fetch_add(1, std::memory_order_relaxed);
}
static void ring_buf_release(ring_buf_t *rb)
{
if (rb->refcount.fetch_sub(1, std::memory_order_acq_rel) == 1)
{
ring_buf_free(rb);
}
}
// ─── State buffer layout ─────────────────────────────────────────────────────
//
// The SharedArrayBuffer returned by ring_alloc uses a 128-byte state block
// allocated here. Layout (each slot = one uint32):
//
// Cache line 0 (bytes 0–63):
// [0] WRITE_INDEX (written by JS Writer, zero-initialised here)
// [1] MAGIC1 = 0x52494E47 ('RING')
// [2] MAGIC2 = 0x42554646 ('BUFF')
// [3] PTR_LO lower 32 bits of ring_buf_t*
// [4] PTR_HI upper 32 bits of ring_buf_t*
// [5..15] padding
//
// Cache line 1 (bytes 64–127):
// [16] READ_INDEX (written by JS Reader, zero-initialised here)
// [17..31] padding
//
// Total: 32 × 4 = 128 bytes — matches STATE_BYTES in index.ts.
static const size_t STATE_BYTES = 128;
static const uint32_t MAGIC1 = 0x52494E47;
static const uint32_t MAGIC2 = 0x42554646;
static const size_t MAGIC1_INDEX = 1;
static const size_t MAGIC2_INDEX = 2;
static const size_t PTR_LO_INDEX = 3;
static const size_t PTR_HI_INDEX = 4;
// BackingStore deleter for the state SharedArrayBuffer. Called by V8 when the
// last isolate that holds the SAB drops its reference (cross-thread ref-count
// reaches zero). Frees the state block and releases the native ring buffer.
// Safe to call from any thread — only C stdlib and atomic ops are used.
static void state_deleter(void *data, size_t /*length*/, void *hint)
{
free(data);
ring_buf_release(static_cast<ring_buf_t *>(hint));
}
// BackingStore deleter for per-instance ArrayBuffers returned by
// ring_get_array_buffer. Called by the isolate's GC when the ArrayBuffer
// (and all its views) are collected.
static void data_deleter(void * /*data*/, size_t /*length*/, void *hint)
{
ring_buf_release(static_cast<ring_buf_t *>(hint));
}
// ─── Helper: extract ring_buf_t* from a SharedHandle ─────────────────────────
// Reads and validates ring_buf_t* from a SharedArrayBuffer created by
// ring_alloc. Returns nullptr and throws a TypeError on failure.
static ring_buf_t *rb_from_handle(v8::Local<v8::Value> val, const char *ctx)
{
if (!val->IsSharedArrayBuffer())
{
char msg[128];
snprintf(msg, sizeof(msg), "%s: expected SharedArrayBuffer handle", ctx);
Nan::ThrowTypeError(msg);
return nullptr;
}
auto sab = val.As<v8::SharedArrayBuffer>();
auto bs = sab->GetBackingStore();
if (!bs || bs->ByteLength() < STATE_BYTES)
{
char msg[128];
snprintf(msg, sizeof(msg), "%s: invalid handle (too small)", ctx);
Nan::ThrowTypeError(msg);
return nullptr;
}
const uint32_t *s = static_cast<const uint32_t *>(bs->Data());
if (s[MAGIC1_INDEX] != MAGIC1 || s[MAGIC2_INDEX] != MAGIC2)
{
char msg[128];
snprintf(msg, sizeof(msg), "%s: invalid handle (bad magic)", ctx);
Nan::ThrowTypeError(msg);
return nullptr;
}
uint64_t lo = s[PTR_LO_INDEX];
uint64_t hi = s[PTR_HI_INDEX];
return reinterpret_cast<ring_buf_t *>(static_cast<uintptr_t>((hi << 32) | lo));
}
// ─── NAN methods ─────────────────────────────────────────────────────────────
// ring_alloc(size: number): SharedArrayBuffer
//
// Allocates a double-mapped ring buffer and a 192-byte state block.
// Returns a SharedArrayBuffer wrapping the state block. The SAB's
// BackingStore uses V8's cross-isolate ref-count: when the last thread that
// holds the SAB drops it, state_deleter fires — freeing the state block and
// releasing the native ring_buf_t. No explicit ring_free needed from JS.
NAN_METHOD(ring_alloc)
{
if (info.Length() < 1 || !info[0]->IsUint32())
{
Nan::ThrowTypeError("ring_alloc: size must be a positive integer");
return;
}
uint32_t req_size = Nan::To<uint32_t>(info[0]).FromMaybe(0u);
if (req_size == 0)
{
Nan::ThrowRangeError("ring_alloc: size must be positive");
return;
}
ring_buf_t *rb = ring_buf_alloc((size_t)req_size);
if (!rb)
{
char msg[128];
Nan::ThrowError(ring_buf_errmsg(msg, sizeof(msg)));
return;
}
// Allocate the state block. calloc zero-initialises it so WRITE_INDEX and
// READ_INDEX start at 0 without any extra memset.
void *state = calloc(1, STATE_BYTES);
if (!state)
{
ring_buf_release(rb);
Nan::ThrowError("ring_alloc: out of memory for state buffer");
return;
}
uint32_t *s = static_cast<uint32_t *>(state);
s[MAGIC1_INDEX] = MAGIC1;
s[MAGIC2_INDEX] = MAGIC2;
uintptr_t p = reinterpret_cast<uintptr_t>(rb);
s[PTR_LO_INDEX] = static_cast<uint32_t>(p & 0xFFFFFFFFu);
s[PTR_HI_INDEX] = static_cast<uint32_t>(static_cast<uint64_t>(p) >> 32);
// V8 ref-counts the BackingStore across isolates. When the last isolate
// that holds this SAB GC's it, state_deleter is called — regardless of
// which thread runs that GC. state_deleter only performs C stdlib calls
// and atomic operations, so it is safe to invoke from any thread.
auto backing = v8::SharedArrayBuffer::NewBackingStore(state, STATE_BYTES, state_deleter, rb);
auto sab = v8::SharedArrayBuffer::New(info.GetIsolate(), std::move(backing));
info.GetReturnValue().Set(sab);
}
// ring_get_array_buffer(handle: SharedArrayBuffer): ArrayBuffer
//
// Returns an ArrayBuffer backed by the double-mapped data region (2 * size
// bytes). Bumps the native refcount; data_deleter releases it when the
// ArrayBuffer (and all views derived from it) is GC'd in this isolate.
NAN_METHOD(ring_get_array_buffer)
{
if (info.Length() < 1)
{
Nan::ThrowTypeError("ring_get_array_buffer: expected 1 argument");
return;
}
ring_buf_t *rb = rb_from_handle(info[0], "ring_get_array_buffer");
if (!rb)
return;
ring_buf_acquire(rb);
auto backing = v8::ArrayBuffer::NewBackingStore(rb->addr, rb->size * 2, data_deleter, rb);
auto ab = v8::ArrayBuffer::New(info.GetIsolate(), std::move(backing));
info.GetReturnValue().Set(ab);
}
// ring_is_int32_lock_free(): boolean
//
// Returns true if std::atomic<uint32_t> is always lock-free on this
// architecture, meaning aligned 32-bit loads/stores are single hardware
// instructions and cannot tear. Safe to skip Atomics.load/store when this
// returns true and the access is aligned.
NAN_METHOD(ring_is_int32_lock_free)
{
info.GetReturnValue().Set(std::atomic<uint32_t>::is_always_lock_free);
}
// NODE_MODULE_INIT() registers a context-aware addon that can be loaded in
// both the main thread and worker threads. NODE_MODULE() (non-context-aware)
// would fail with "Module did not self-register" in worker threads.
NODE_MODULE_INIT()
{
NAN_EXPORT(exports, ring_alloc);
NAN_EXPORT(exports, ring_get_array_buffer);
NAN_EXPORT(exports, ring_is_int32_lock_free);
}
+6
-9

@@ -13,9 +13,9 @@ export interface BufferRegion {

}
declare const __sharedHandle: unique symbol;
declare const _handle: unique symbol;
/**
* Opaque handle to a ring buffer's SharedArrayBuffer. Use `alloc`
* to create one; pass it directly to `Reader` and `Writer`.
* Opaque handle to a ring buffer. A branded SharedArrayBuffer that embeds the
* native pointer and state indices. Pass between threads via `workerData`.
*/
export type SharedHandle = SharedArrayBuffer & {
readonly [__sharedHandle]: void;
readonly [_handle]: void;
};

@@ -30,7 +30,5 @@ /**

readBytes: number;
wrapCount: number;
};
get handle(): SharedHandle;
constructor(handleOrSize: SharedHandle | number);
[Symbol.dispose](): void;
readSome(next: (data: BufferRegion) => void | boolean): number;

@@ -49,8 +47,6 @@ readSome<U>(next: (data: BufferRegion, opaque: U) => void | boolean, opaque: U): number;

writeBytes: number;
wrapCount: number;
wrapBytes: number;
};
get handle(): SharedHandle;
get maxMessageSize(): number;
constructor(handleOrSize: SharedHandle | number, { yield: onYield, logger }?: WriterOptions);
[Symbol.dispose](): void;
/**

@@ -74,2 +70,3 @@ * Synchronously writes a message. Blocks (via `Atomics.wait`) until buffer space is available.

cork<T>(callback: () => T): T;
cork<T, O>(callback: (opaque: O) => T, opaque: O): T;
/**

@@ -76,0 +73,0 @@ * Publishes the pending write position to the reader.

+188
-193

@@ -7,2 +7,19 @@ // By placing the read and write indices far apart (multiples of a common

// SharedArrayBuffer layout (each unit = one Int32 = 4 bytes).
// The native ring_alloc writes slots [1..4]; JS owns [0] and [16].
//
// Cache line 0 (bytes 0–63):
// [0] WRITE_INDEX ─ cache-line aligned (written by Writer)
// [1] MAGIC1 = 0x52494E47 ('RING') (written by native, read by native)
// [2] MAGIC2 = 0x42554646 ('BUFF')
// [3] PTR_LO lower 32 bits of native ptr
// [4] PTR_HI upper 32 bits of native ptr
// [5..15] padding
//
// Cache line 1 (bytes 64–127):
// [16] READ_INDEX ─ cache-line aligned (written by Reader)
// [17..31] padding
//
// Total: 32 × 4 = 128 bytes
// Int32 is 4 bytes, so an index of 16 means 16 * 4 = 64 bytes offset.

@@ -12,13 +29,52 @@ const WRITE_INDEX = 0

// The first 128 bytes of the buffer are reserved for state (read/write pointers).
// Data starts at byte offset 128.
const STATE_BYTES = 128
// High-Water Mark for batching operations to reduce the frequency
// of expensive atomic writes.
const HWM_BYTES = 256 * 1024 // 256 KiB
const HWM_COUNT = 1024 // 1024 items
const isProduction = process.env.NODE_ENV === 'production'
// Dynamic import via Function to prevent TypeScript from following the module
// path for rootDir computation. binding.js uses node-gyp-build to load the .node addon.
// oxlint-disable-next-line no-implied-eval -- intentional: prevents TS static module resolution
const _dynamicImport = new Function('url', 'return import(url)')
const native = (
(await _dynamicImport(new URL('../binding.js', import.meta.url).href))
).default
// This module intentionally uses plain (non-atomic) reads and writes to
// SharedArrayBuffer indices in several hot paths to avoid the memory-barrier
// overhead of Atomics.load / Atomics.store. Two architectural properties must
// hold for this to be correct:
//
// 1. No tearing: a plain 32-bit aligned store/load must be a single
// indivisible hardware instruction, so the other side never observes a
// half-written value. Verified at startup by ring_is_int32_lock_free()
// below (true on x86-64 and ARM64).
//
// 2. Visibility: x86 / x86-64 implements Total Store Order (TSO), which
// guarantees that a plain store is observed by a subsequent Atomics.load
// on another core without any explicit fence. ARM64 has a weaker memory
// model (plain stores emit bare `str`, not `stlr`), so a plain store is
// not formally guaranteed to be seen by a remote `ldar` (Atomics.load).
// In practice V8 on ARM64 relies on cache-coherence hardware to make
// plain SAB stores visible quickly, and the protocol is designed so that
// a missed update only causes an extra yield/wait loop, never corruption.
// This is therefore safe but is not spec-guaranteed on ARM64.
if (!native.ring_is_int32_lock_free()) {
throw new Error(
'nxtedition/shared: std::atomic<uint32_t> is not always lock-free on this architecture — ' +
'plain (non-atomic) 32-bit reads/writes may tear. Unsupported platform.',
)
}

@@ -36,16 +92,14 @@

/**
* Opaque handle to a ring buffer's SharedArrayBuffer. Use `alloc`
* to create one; pass it directly to `Reader` and `Writer`.
* Opaque handle to a ring buffer. A branded SharedArrayBuffer that embeds the
* native pointer and state indices. Pass between threads via `workerData`.
*/
/**
* Allocates a ring buffer handle. The first 128 bytes are reserved for
* read/write pointers; the rest is the data region.
*
* The `size` parameter is the guaranteed max payload for a single write.
* Overhead (length header + alignment + next-header slot) is added automatically.
* Allocates a ring buffer handle. The `size` parameter is the guaranteed max
* payload for a single write. Overhead (length header + alignment) is added
* automatically, and the data region is rounded up to a page boundary.
*/

@@ -59,9 +113,16 @@ function alloc(size ) {

}
// Total allocation: STATE_BYTES (128) + aligned payload + 4-byte header + 4-byte next-header slot.
// SharedArrayBuffer max is 2GB (2**31), so the payload limit is 2**31 - STATE_BYTES - 8.
if (size > 2 ** 31 - STATE_BYTES - 8) {
// dataSize is a lower bound passed to the native allocator, which rounds it
// up to a page boundary. The extra 8 bytes are: 4-byte length header +
// 4-byte minimum gap ensuring writePos can never equal readPos (full vs. empty).
// maxMessageSize = physicalSize - 8 >= size.
const dataSize = ((size + 3) & ~3) + 8
// Max size: data region must fit in a signed 32-bit index.
if (dataSize > 2 ** 31) {
throw new RangeError('size exceeds maximum of 2GB')
}
// Data region: aligned payload + 4-byte length header + 4-byte next-header slot.
return new SharedArrayBuffer(STATE_BYTES + ((size + 3) & ~3) + 8)
// ring_alloc returns a SharedArrayBuffer whose BackingStore holds the state
// block and a custom V8 deleter. V8 ref-counts the BackingStore across all
// isolates that hold the SAB; when the last one drops it, the deleter fires
// and releases the native ring_buf_t — no FinalizationRegistry needed.
return native.ring_alloc(dataSize)
}

@@ -78,2 +139,4 @@

#handle
#readPos
#writePos

@@ -83,6 +146,5 @@ #stats = {

readBytes: 0,
wrapCount: 0,
}
get stats() {
get stats() {
return this.#stats

@@ -96,14 +158,15 @@ }

constructor(handleOrSize ) {
if (typeof handleOrSize === 'number') {
this.#handle = alloc(handleOrSize)
} else {
this.#handle = handleOrSize
}
this.#handle = typeof handleOrSize === 'number' ? alloc(handleOrSize) : handleOrSize
const sharedBuffer = this.#handle
const size = sharedBuffer.byteLength - STATE_BYTES
// ring_get_array_buffer bumps the native refcount and registers a V8
// ArrayBuffer finalizer that releases it, so the native ring_buf_t stays
// alive for at least as long as this Reader's dataBuffer is reachable.
const dataBuffer = native.ring_get_array_buffer(this.#handle)
this.#state = new Int32Array(sharedBuffer, 0, STATE_BYTES >> 2)
// The native buffer is 2 * physicalSize bytes (double-mapped).
const size = dataBuffer.byteLength >> 1
this.#state = new Int32Array(this.#handle)
this.#size = size
this.#int32 = new Int32Array(sharedBuffer, STATE_BYTES)
this.#int32 = new Int32Array(dataBuffer)

@@ -114,13 +177,10 @@ // This object is reused to avoid creating new objects in a hot path.

this.#data = {
buffer: Buffer.from(sharedBuffer, STATE_BYTES, size),
view: new DataView(sharedBuffer, STATE_BYTES, size),
buffer: Buffer.from(dataBuffer),
view: new DataView(dataBuffer),
byteOffset: 0,
byteLength: 0,
}
}
[Symbol.dispose]() {
// No resources to clean up in this implementation, but this method is defined
// to allow for future enhancements (e.g. if we add event listeners or other
// resources that need explicit cleanup).
this.#readPos = this.#state[READ_INDEX] | 0
this.#writePos = this.#state[WRITE_INDEX] | 0
}

@@ -139,50 +199,54 @@

let readPos = state[READ_INDEX] | 0
let writePos = state[WRITE_INDEX] | 0
if (this.#readPos === this.#writePos) {
// Intentional non-atomic plain read (see TSO note at top of file).
// A stale WRITE_INDEX at worst means returning 0 messages; the reader
// will be called again on the next tick. The writer always publishes via
// Atomics.store (release), so property 1 (no tearing) suffices here —
// the reader simply re-checks on the next call if the store hasn't
// propagated yet (property 2).
this.#writePos = state[WRITE_INDEX] | 0
}
// Process messages in a batch to minimize loop and atomic operation overhead.
while (count < HWM_COUNT && bytes < HWM_BYTES && readPos !== writePos) {
const dataLen = int32[readPos >> 2] | 0
const dataPos = readPos + 4
while (bytes < HWM_BYTES && this.#readPos !== this.#writePos) {
const dataLen = int32[this.#readPos >> 2] | 0
if (!isProduction && (dataLen < 0 || dataLen > size - 8)) {
throw new Error(
`Corrupt ring buffer: invalid message length ${dataLen} at position ${this.#readPos}`,
)
}
const alignedLen = (dataLen + 3) & ~3
const dataPos = this.#readPos + 4
bytes += 4
// Advance read position with modulo wrap — the double-mapped virtual
// buffer makes the data access at dataPos physically contiguous even
// when it spans the end of the logical ring.
this.#readPos = (this.#readPos + 4 + alignedLen) % size
// A length of -1 is a special marker indicating the writer has
// wrapped around to the beginning of the buffer.
if (dataLen === -1) {
this.#stats.wrapCount += 1
readPos = 0
// After wrapping, we must re-check against the writer's position.
// It's possible the writer is now at a position > 0.
writePos = state[WRITE_INDEX] | 0
} else {
if (dataLen < 0) {
throw new Error('Invalid data length')
}
if (dataPos + dataLen > size) {
throw new Error('Data exceeds buffer size')
}
bytes += 4 + alignedLen
count += 1
// Advance by aligned length so next header is on a 4-byte boundary.
const alignedLen = (dataLen + 3) & ~3
readPos += 4 + alignedLen
// This is a "zero-copy" operation. We don't copy the data out.
// Instead, we pass a "view" into the shared buffer.
// NOTE: byteOffset can be >= physicalSize (i.e. >= dataBuffer.byteLength / 2) for
// messages that span the wrap boundary — the double-mapped virtual buffer makes
// those bytes physically contiguous. Do not compare byteOffset against the physical
// ring size; the full double-mapped range is always valid.
data.byteOffset = dataPos
data.byteLength = dataLen
bytes += alignedLen
count += 1
// This is a "zero-copy" operation. We don't copy the data out.
// Instead, we pass a "view" into the shared buffer.
data.byteOffset = dataPos
data.byteLength = dataLen
if (next(data, opaque) === false) {
break
}
if (next(data, opaque) === false) {
break
}
}
// IMPORTANT: The reader only updates its shared `readPos` after a batch
// is processed. This significantly reduces shared memory overhead.
// Intentional non-atomic plain write (see TSO note at top of file).
// The writer reads READ_INDEX either via Atomics.wait (which wakes on
// notify, not used here) with a timeout, or via a plain read in the fast
// path and Atomics.load in the slow path. A delayed store propagation
// (property 2, ARM64) only causes the writer to loop one extra time —
// correctness is never affected. Atomics.store is deliberately avoided
// to keep the reader hot path free of memory-barrier overhead.
if (bytes > 0) {
state[READ_INDEX] = readPos | 0
state[READ_INDEX] = this.#readPos | 0
}

@@ -220,7 +284,5 @@

writeBytes: 0,
wrapCount: 0,
wrapBytes: 0,
}
get stats() {
get stats() {
return this.#stats

@@ -233,9 +295,7 @@ }

get maxMessageSize() {
return this.#size - 8
}
constructor(handleOrSize , { yield: onYield, logger } = {}) {
if (typeof handleOrSize === 'number') {
this.#handle = alloc(handleOrSize)
} else {
this.#handle = handleOrSize
}
if (onYield != null && typeof onYield !== 'function') {

@@ -245,8 +305,14 @@ throw new TypeError('onYield must be a function')

const sharedBuffer = this.#handle
const size = sharedBuffer.byteLength - STATE_BYTES
this.#handle = typeof handleOrSize === 'number' ? alloc(handleOrSize) : handleOrSize
this.#state = new Int32Array(sharedBuffer, 0, STATE_BYTES >> 2)
// ring_get_array_buffer bumps the native refcount and registers a V8
// ArrayBuffer finalizer that releases it, so the native ring_buf_t stays
// alive for at least as long as this Writer's dataBuffer is reachable.
const dataBuffer = native.ring_get_array_buffer(this.#handle)
const size = dataBuffer.byteLength >> 1
this.#state = new Int32Array(this.#handle)
this.#size = size
this.#int32 = new Int32Array(sharedBuffer, STATE_BYTES)
this.#int32 = new Int32Array(dataBuffer)

@@ -257,4 +323,4 @@ // This object is reused to avoid creating new objects in a hot path.

this.#data = {
buffer: Buffer.from(sharedBuffer, STATE_BYTES, size),
view: new DataView(sharedBuffer, STATE_BYTES, size),
buffer: Buffer.from(dataBuffer),
view: new DataView(dataBuffer),
byteOffset: 0,

@@ -277,9 +343,2 @@ byteLength: 0,

[Symbol.dispose]() {
this.flushSync()
// No resources to clean up in this implementation, but this method is defined
// to allow for future enhancements (e.g. if we add event listeners or other
// resources that need explicit cleanup).
}
/**

@@ -302,5 +361,4 @@ * Pauses the writer thread to wait for the reader to catch up.

try {
// Call the user-provided yield function, if any. This can be important
// if the writer is waiting for the reader to process data which would
// otherwise deadlock.
// Call the user-provided yield function to allow the reader to consume
// data and advance the read position, freeing space for the writer.
this.#onYield()

@@ -321,3 +379,3 @@ } finally {

// After waking up, refresh the local view of the reader's position.
this.#readPos = this.#state[READ_INDEX] | 0
this.#readPos = Atomics.load(this.#state, READ_INDEX) | 0
}

@@ -327,66 +385,28 @@

* Tries to acquire enough space in the buffer for a new message.
*
* With the virtual memory double-mapping, writes can span the physical end
* of the buffer — no sentinel value is needed. The free-space check is a
* single modular formula valid for all writer/reader position combinations.
*/
#acquire(len ) {
// Total space required: aligned payload + its 4-byte length header + a potential
// 4-byte header for the *next* message (for wrap-around check).
const required = ((len + 3) & ~3) + 4 + 4
// Total space required: 4-byte length header + aligned payload.
// No extra sentinel slot needed — the double-mapped region handles wrap.
const required = ((len + 3) & ~3) + 4
const size = this.#size
const state = this.#state
const int32 = this.#int32
if (this.#writePos >= this.#readPos) {
// Case 1: The writer is ahead of the reader. [ 0 - R ... W - size ]
// There is free space from W to the end (s) and from 0 to R.
if (size - this.#writePos >= required) {
// Enough space at the end of the buffer.
return true
}
this.#readPos = state[READ_INDEX] | 0
if (this.#readPos < 4) {
this.#yield(0)
}
// Not enough space at the end. Check if there's space at the beginning.
if (this.#readPos < 4) {
// Reader is at the beginning, so no space to wrap around into.
return false
}
// Mark the current position with a wrap-around signal (-1).
int32[this.#writePos >> 2] = -1
this.#stats.wrapCount += 1
this.#stats.wrapBytes += size - this.#writePos
// Reset writer position to the beginning.
this.#writePos = 0
if (!isProduction && this.#writePos + 4 > size) {
// assertion
throw new Error(
`Write position ${this.#writePos} with next header exceeds buffer size ${size}`,
)
}
if (!isProduction && this.#writePos === this.#readPos) {
// assertion
throw new Error(
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
)
}
Atomics.store(state, WRITE_INDEX, this.#writePos)
this.#pending = 0
// Fast path: check with the locally cached readPos first.
const usedCached = (this.#writePos - this.#readPos + size) % size
if (size - usedCached > required) {
return true
}
// Case 2: The writer has wrapped around. [ 0 ... W - R ... s ]
// The only free space is between W and R.
// Slow path: refresh readPos from shared state and re-check.
// Intentional non-atomic plain read (see TSO note at top of file).
// A stale value (property 2, ARM64) only causes one extra yield; the
// definitive re-read after Atomics.wait uses Atomics.load (line below in
// #yield), which has acquire semantics and will see the latest value.
this.#readPos = state[READ_INDEX] | 0
if (this.#readPos - this.#writePos < required) {
this.#yield(0)
}
return this.#readPos - this.#writePos >= required
const used = (this.#writePos - this.#readPos + size) % size
return size - used > required
}

@@ -419,15 +439,8 @@

const size = this.#size
if (!isProduction && dataPos + dataLen > size) {
// assertion
throw new Error(`Data position ${dataPos} with length ${dataLen} exceeds buffer size ${size}`)
}
const alignedLen = (dataLen + 3) & ~3
const nextPos = this.#writePos + 4 + alignedLen
// Advance write position with modulo wrap — the double-mapped virtual
// buffer ensures the data bytes are physically contiguous even when they
// span the end of the logical ring.
const nextPos = (this.#writePos + 4 + alignedLen) % this.#size
if (!isProduction && nextPos + 4 > size) {
// assertion
throw new Error(`Write position ${nextPos} with next header exceeds buffer size ${size}`)
}
if (!isProduction && nextPos === this.#readPos) {

@@ -440,3 +453,3 @@ // assertion

this.#int32[this.#writePos >> 2] = dataLen
this.#writePos += 4 + alignedLen
this.#writePos = nextPos
this.#pending += 4 + alignedLen

@@ -473,6 +486,3 @@

}
const size = this.#size
// Each message occupies: 4-byte length header + aligned payload + 4-byte slot for the
// next message's header (needed for wrap-around sentinel). So the maximum payload is size - 8.
const maxLen = size - 8
const maxLen = this.#size - 8
if (len > maxLen) {

@@ -487,2 +497,3 @@ throw new Error(`"len" ${len} exceeds maximum allowed size ${maxLen}`)

const startTime = performance.now()
this.#yield(0)
let yieldCount = 0

@@ -513,9 +524,2 @@ let yieldTime = 0

this.#write(len, fn, opaque)
if (!isProduction && this.#writePos === this.#readPos) {
// assertion
throw new Error(
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
)
}
}

@@ -536,6 +540,3 @@

}
const size = this.#size
// Each message occupies: 4-byte length header + aligned payload + 4-byte slot for the
// next message's header (needed for wrap-around sentinel). So the maximum payload is size - 8.
const maxLen = size - 8
const maxLen = this.#size - 8
if (len > maxLen) {

@@ -554,9 +555,2 @@ throw new Error(`"len" ${len} exceeds maximum allowed size ${maxLen}`)

if (!isProduction && this.#writePos === this.#readPos) {
// assertion
throw new Error(
`Write position ${this.#writePos} cannot equal read position ${this.#readPos}`,
)
}
return true

@@ -571,7 +565,8 @@ }

cork (callback ) {
cork (callback , opaque ) {
this.#corked += 1
if (callback != null) {
try {
return callback()
return callback(opaque)
} finally {

@@ -578,0 +573,0 @@ this.uncork()

{
"name": "@nxtedition/shared",
"version": "5.0.2",
"version": "5.0.3",
"type": "module",

@@ -9,2 +9,6 @@ "main": "lib/index.js",

"lib",
"binding.js",
"binding.gyp",
"src/ring.cc",
"prebuilds",
"README.md",

@@ -17,14 +21,28 @@ "LICENSE"

},
"engines": {
"node": ">=21.2.0"
},
"gypfile": true,
"scripts": {
"install": "node-gyp-build",
"build": "rimraf lib && tsc && amaroc ./src/index.ts && mv src/index.js lib/",
"rebuild": "JOBS=max npm run install --build-from-source",
"prepublishOnly": "yarn build",
"typecheck": "tsc --noEmit",
"test": "node --test",
"test": "JOBS=max npm run install --build-from-source && node --test",
"test:ci": "node --test",
"test:types": "tsd"
"test:types": "tsd",
"format:native": "clang-format -i src/*.cc",
"format:native:check": "clang-format -n --Werror src/*.cc"
},
"dependencies": {
"nan": "^2.25.0",
"node-gyp-build": "^4.8.2"
},
"devDependencies": {
"@types/node": "^25.2.3",
"@types/node": "^25.5.0",
"amaroc": "^1.0.1",
"oxlint-tsgolint": "^0.13.0",
"clang-format-node": "^3.0.0",
"node-gyp": "^12.1.0",
"oxlint-tsgolint": "^0.17.0",
"rimraf": "^6.1.3",

@@ -34,3 +52,3 @@ "tsd": "^0.33.0",

},
"gitHead": "9547032a3fa363bb353777a1b19b0852013459c5"
"gitHead": "7c9c7457c885c644c7a1e70ef894d4727ce240d6"
}
+47
-45

@@ -13,2 +13,10 @@ # @nxtedition/shared

## Native binding
The ring buffer relies on a native C++ addon for two capabilities that are not available from pure JavaScript.
**Double-mapped virtual memory.** The key trick that eliminates wrap-around copies is mapping the same physical memory region into two consecutive virtual address ranges. From the CPU's perspective the buffer appears twice as large: a message that starts near the end of the physical ring and would normally wrap around is simply read or written as a contiguous span across the boundary. On POSIX systems this uses `mmap` twice with `MAP_FIXED | MAP_SHARED` onto the same `memfd`; on Windows it uses `VirtualAlloc2` / `MapViewOfFile3`. Neither is expressible in JavaScript, which has no way to create an `ArrayBuffer` backed by a custom virtual memory layout.
**Huge pages (Linux).** When the requested buffer size is at least 1 MiB, the native allocator first attempts to back the memory with 2 MiB explicit huge pages (via `memfd_create` with `MFD_HUGETLB | MFD_HUGE_2MB`). Huge pages reduce TLB pressure substantially for large ring buffers — the CPU needs far fewer TLB entries to cover the same address range, which means fewer TLB misses on hot read/write paths. If explicit huge pages are unavailable (e.g. the huge-page pool is empty or the kernel does not support it), the allocator falls back to regular 4 KiB pages and advises the kernel to back the region with transparent huge pages (`madvise(MADV_HUGEPAGE)`).
## Platform Assumptions

@@ -30,3 +38,3 @@

// Create writer — allocates the ring buffer internally
using w = new Writer(1024 * 1024) // 1 MB ring buffer
const w = new Writer(1024 * 1024) // 1 MB ring buffer

@@ -40,3 +48,3 @@ const payload = Buffer.from('hello world')

// Create reader from the same handle (pass w.handle to the other thread)
using r = new Reader(w.handle)
const r = new Reader(w.handle)

@@ -93,3 +101,3 @@ r.readSome((data) => {

using w = new Writer(1024 * 1024)
const w = new Writer(1024 * 1024)
const worker = new Worker('./reader-worker.js', {

@@ -106,3 +114,3 @@ workerData: w.handle,

using r = new Reader(workerData)
const r = new Reader(workerData)

@@ -128,3 +136,3 @@ function poll() {

The underlying `SharedHandle` (`SharedArrayBuffer`). Pass this to another thread via `workerData` or to `new Writer(handle)` / `new Reader(handle)` to share the buffer.
The underlying `SharedHandle` (a branded `SharedArrayBuffer`). Pass this to another thread via `workerData` or to `new Writer(handle)` / `new Reader(handle)` to share the buffer.

@@ -137,3 +145,3 @@ #### `reader.readSome(next, opaque?)`

- `view: DataView` — A DataView over the shared buffer
- `byteOffset: number` — Start offset of the message payload
- `byteOffset: number` — Start offset of the message payload. Due to the double-mapped virtual memory layout, this value can be greater than or equal to the physical ring size for messages that span the wrap boundary — the bytes are always contiguous and valid within `buffer`. Do not compare `byteOffset` against the physical ring size.
- `byteLength: number` — Length of the message payload in bytes

@@ -145,8 +153,4 @@

Messages are batched: up to 1024 items or 256 KiB per call.
Messages are batched: up to 256 KiB of data per call.
#### `reader[Symbol.dispose]()`
Implements the [explicit resource management](https://github.com/tc39/proposal-explicit-resource-management) protocol. Currently a no-op; enables use with `using` declarations for forward compatibility.
### `new Writer(handleOrSize, options?)`

@@ -165,3 +169,3 @@

The underlying `SharedHandle` (`SharedArrayBuffer`). Pass this to another thread via `workerData` or to `new Reader(handle)` to share the buffer.
The underlying `SharedHandle` (a branded `SharedArrayBuffer`). Pass this to another thread via `workerData` or to `new Reader(handle)` to share the buffer.

@@ -196,51 +200,49 @@ #### `writer.writeSync(len, fn, opaque?)`

#### `writer[Symbol.dispose]()`
Implements the [explicit resource management](https://github.com/tc39/proposal-explicit-resource-management) protocol. Calls `flushSync()` to publish any pending writes, then releases held resources. Enables use with `using` declarations.
## Benchmarks
Measured on AMD EPYC 9355P (4.29 GHz), Node.js 25.6.1, 8 MiB ring buffer, Docker (x64-linux).
Measured on AMD EPYC 9355P 32-Core Processor, Node.js 25.8.1, 8 MiB ring buffer, Docker (x64-linux).
Each benchmark writes batches of fixed-size messages from the main thread and
reads them in a worker thread. The shared ring buffer is compared against
Node.js `postMessage` (structured clone).
Node.js `postMessage` (structured clone). "shared (string)" uses the latin1
fast path; UTF-8 strings are slower.
### Throughput
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
| -----: | --------------: | --------------: | -------------------: | -------------------: |
| 64 B | **838 MiB/s** | 388 MiB/s | 24 MiB/s | 42 MiB/s |
| 256 B | **2.65 GiB/s** | 1.46 GiB/s | 89 MiB/s | 168 MiB/s |
| 1 KiB | **4.95 GiB/s** | 4.86 GiB/s | 339 MiB/s | 525 MiB/s |
| 4 KiB | 8.42 GiB/s | **15.11 GiB/s** | 1.12 GiB/s | 1.86 GiB/s |
| 16 KiB | 12.02 GiB/s | **33.27 GiB/s** | 4.12 GiB/s | 6.02 GiB/s |
| 64 KiB | 12.96 GiB/s | **43.66 GiB/s** | 9.33 GiB/s | 14.73 GiB/s |
| Size | shared (buffer) | shared (latin1 str) | postMessage (buffer) | postMessage (string) |
| -----: | --------------: | ------------------: | -------------------: | -------------------: |
| 64 B | **2.00 GiB/s** | 549 MiB/s | 22.78 MiB/s | 39.58 MiB/s |
| 256 B | **3.62 GiB/s** | 1.73 GiB/s | 89.90 MiB/s | 174.62 MiB/s |
| 1 KiB | **11.23 GiB/s** | 6.00 GiB/s | 341.78 MiB/s | 521.39 MiB/s |
| 4 KiB | **24.36 GiB/s** | 20.04 GiB/s | 1.13 GiB/s | 1.59 GiB/s |
| 16 KiB | 44.16 GiB/s | **50.76 GiB/s** | 3.70 GiB/s | 7.45 GiB/s |
| 64 KiB | **90.02 GiB/s** | 78.88 GiB/s | 9.49 GiB/s | 15.09 GiB/s |
### Message rate
| Size | shared (buffer) | shared (string) | postMessage (buffer) | postMessage (string) |
| -----: | --------------: | --------------: | -------------------: | -------------------: |
| 64 B | **13.73 M/s** | 6.35 M/s | 391 K/s | 693 K/s |
| 256 B | **11.14 M/s** | 6.14 M/s | 366 K/s | 689 K/s |
| 1 KiB | **5.19 M/s** | 5.09 M/s | 348 K/s | 538 K/s |
| 4 KiB | 2.21 M/s | **3.96 M/s** | 295 K/s | 488 K/s |
| 16 KiB | 788 K/s | **2.18 M/s** | 270 K/s | 395 K/s |
| 64 KiB | 212 K/s | **715 K/s** | 153 K/s | 241 K/s |
| Size | shared (buffer) | shared (latin1 str) | postMessage (buffer) | postMessage (string) |
| -----: | --------------: | ------------------: | -------------------: | -------------------: |
| 64 B | **33.61 M/s** | 8.99 M/s | 373 K/s | 648 K/s |
| 256 B | **15.16 M/s** | 7.27 M/s | 368 K/s | 715 K/s |
| 1 KiB | **11.78 M/s** | 6.29 M/s | 350 K/s | 534 K/s |
| 4 KiB | **6.39 M/s** | 5.25 M/s | 297 K/s | 417 K/s |
| 16 KiB | 2.89 M/s | **3.33 M/s** | 242 K/s | 488 K/s |
| 64 KiB | **1.47 M/s** | 1.29 M/s | 155 K/s | 247 K/s |
### Key findings
- **Small messages (64–256 B):** The shared ring buffer with `Buffer.set` delivers
**13.7–11.1 M msg/s** — up to **35x faster** than `postMessage` (buffer) and
**20x faster** than `postMessage` (string). Per-message overhead dominates at
these sizes, and avoiding structured cloning makes the biggest difference.
- **Small messages (64–256 B):** `Buffer.set` delivers **33.6–15.2 M msg/s** —
up to **90x faster** than `postMessage` (buffer) and **52x faster** than
`postMessage` (string). Per-message overhead dominates at these sizes, and
avoiding structured cloning makes the biggest difference.
- **Medium messages (1 KiB):** `Buffer.set` and string are nearly identical
(**4.95 vs 4.86 GiB/s**), both **~9x faster** than the best `postMessage`
variant.
- **Medium messages (1 KiB):** `Buffer.set` pulls ahead at **11.23 GiB/s** —
roughly **1.9x faster** than the latin1 string path and **~22x faster** than
the best `postMessage` variant.
- **Large messages (4–64 KiB):** Shared string overtakes `Buffer.set` and
scales to **43.7 GiB/s** at 64 KiB — **3.4x faster** than `Buffer.set` and
**3.0x faster** than `postMessage` (string). At every size, the shared ring
buffer outperforms `postMessage`.
- **Large messages (4–64 KiB):** Both shared paths dominate. At 16 KiB the
latin1 string path (**50.76 GiB/s**) overtakes `Buffer.set` (44.16 GiB/s) —
V8's string-to-buffer fast path becomes more cache-efficient at this size. At
64 KiB `Buffer.set` reclaims the lead at **90.02 GiB/s** — **6x faster**
than `postMessage` (string).

@@ -247,0 +249,0 @@ - **Caveat:** The string benchmark uses ASCII-only content. Multi-byte UTF-8