0.0.8 • Published 6 months ago

@alshdavid/worker-shared-memory v0.0.8

Weekly downloads
-
License
MIT
Repository
-
Last release
6 months ago

Node.js Shared Memory Across Workers

This npm package exposes a centralized storage for state shared between workers.

Installation

npm install @alshdavid/worker-shared-memory

Usage

// main.js
import { StringRef, MapRef } from '@alshdavid/worker-shared-memory'
import { Worker } from 'node:worker_threads'

// Spawn worker
const worker = new Worker('./worker.js')

// Create Map in store
const map = new MapRef()
map.set('foo', StringRef.from("Hello World"))

// Give the worker the id of the Map
// I might add "named" values in future so you don't need to transfer IDs
worker.postMessage(map.id)
// worker.js
import { MapRef } from '@alshdavid/worker-shared-memory'
import { parentPort } from 'node:worker_threads'

// Get the ID of the Map
parentPort.addEventListener('message', ({ data: map_id }) => {
    // Connect to the same map
    const map = new MapRef(map_id)

    console.log(map.get('foo').obtainValue()) // "Hello World"
})

Types

import { StringRef, NumberRef, MapRef, VectorRef } from '@alshdavid/worker-shared-memory'

const str = new StringRef() // Empty string (utf8 string)
const num = new NumberRef() // Empty number (64 bit float)
const map = new MapRef()    // Key/Value store that holds refs
const vec = new VectorRef() // Dynamic array that holds refs

Structs

You can define and instantiate structs

// Define struct
const Foo = new StructFactory({
    key_1: "string",
    key_2: "number",
})

const foo = Foo.new({
    key_1: "foo",
    key_2: 42,
})

console.log(foo.getKey('key_1')) // 'foo'
foo.setKey('key_1', 'something else')

You can access structs through a JavaScript proxy object for more ergonomic interactions. be mindful of accessing values as they are not references, the underlying value may change after it is obtained.

const Foo = new StructDefinition({
    key_1: "string",
    key_2: "number",
})

const foo = Foo.new_proxy({
    key_1: "foo",
    key_2: 42,
})

console.log(foo.key_1) // 'foo'
foo.key_1 = 'bar' 
console.log(foo.key_1) // 'bar'

Safety and Memory usage

All values are stored in a shared heap managed by the native module. Interacting with values occurs behind synchronization controls like mutexes and atomics.

The shared heap is not subject to the maximum heap size assigned to Node.js that you can modify via —-max-old-space-size - however to ensure minimal memory usage within the Node.js process + threads, values are passed by reference identifiers.

import { StringRef } from '@alshdavid/worker-shared-memory'

const str = StringRef.from('foo')

console.log(str)               // { id: 342 }
console.log(str.obtainValue()) // 'foo'

Only after a value is obtained is it copied into the Node.js heap and will, at that point, contribute to memory usage within the process/thread memory pool.

The limitation is how many references you can hold in the process - which appears to be several billion under Node's default memory limit.

Manual Memory Management

Unfortunately, usage of this library requires the manual cleanup of values stored in the shared value store and does expose the consumer to use after free errors.

Caution is advised, particularly with Vectors and Maps as dropping them does not drop their held resources. Structs drop their assigned values.

import { StringRef } from '@alshdavid/worker-shared-memory'

const str = StringRef.from('foo')

console.log(str.obtainValue()) // 'foo'

str.drop() // will deallocate the value

console.log(str.obtainValue()) // Thrown Error ('use after free')