npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

struct-vec

v0.1.2

Published

Javascript array-like data structures designed for multithreading

Downloads

2

Readme

بسم الله الرحمن الرحيم

CI Workflow Dependencies Bundle Size

Struct Vec

🧰 Javascript array-like containers for multithreading

Efficiently communicating between js workers is a pain because you are forced either to pass data by structured cloning or represent your data as raw buffers. Structured cloning isn't ideal for performance because it requires de-serialization/serialization every time you pass a message, and raw buffers aren't ideal for productivity because they are esoteric and hard to work with.

This package attempts to solve this problem by allowing you to define data structures called Vecs. Vecs provide an API is similar to javascript Arrays, but are completely backed by SharedArrayBuffers - thus can be passed between workers at zero-cost, while still being intuitive to work with with.

This package was inspired by Google's FlatBuffers, Rust's std::Vec, and @bnaya/objectbuffer package.

Table of Contents

Examples

Quick Start

npm i struct-vec
import {vec} from "struct-vec"

// define the typing of elements in
// vec. Returns a class
const PositionV = vec({x: "f32", y: "f32", z: "f32"})

// initialize a vec
const positions = new PositionV()

// add some elements
for (let i = 0; i < 200; i++) {
  positions.push({x: 1, y: 2, z: 3})
}

console.log(positions.length) // output: 200

// loop over vec
for (let i = 0; i < positions.length; i++) {
  // get element with ".index" method
  const element = positions.index(i)
  console.log(element.x) // output: 1
  console.log(element.y) // output: 2
  console.log(element.z) // output: 3
}

positions.forEach(pos => {
    // use the ".e" method to get
    // the object representation
    // of your element
    console.log(pos.e) // output: {x: 1, y: 1, z: 1}
})

// get a reference to an index
const firstElement = positions.index(0).ref

// remove elements
const allElements = positions.length
for (let i = 0; i < allElements; i++) {
  positions.pop()
}

console.log(positions.length) // output: 0

Initializing a Vec

import {vec} from "struct-vec"

// define what an element should look like 
// definitions are called "struct defs" 
const PositionV = vec({x: "f32", y: "f32", z: "f32"})

// you can initialize your vecs without any arguments
const noArg = new PositionV()

// Or you can specify how much capacity it initially has
// (check the api reference for more info on capacity)
const withCapacity = new PositionV(15_000)
console.log(withCapacity.capacity) // output: 15_000

// Or you can construct a vec from another vec's memory
const fromMemory = PositionV.fromMemory(withCapacity.memory)
console.log(fromMemory.capacity) // output: 15_000

Indexing

Whenever you wish to operate on an element in a vec (get the value or set it), reference a specific field of the element NOT the entire element.

Getting Values at an Index

If you want the value of an element, refer to one of it's fields (yourElement.x for example), the e field to get the entire element by value, or the ref field to get a reference (The e and ref fields are auto-generated for all struct defs).

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()
positions.push({x: 1, y: 2, z: 3})

// 🛑 "wrongValue" doesn't equal {x: 1, y: 2, z: 3}
const wrongValue = positions.index(0)

// ✅ refer to one the fields
const {x, y, z} = positions.index(0)
console.log(x, y, z) // output: 1 2 3

// ✅ get entire element by value
const correctValue = positions.index(0).e
console.log(correctValue) // output: {x: 1, y: 2, z: 3}

// ✅ get a reference to index
const first = positions.index(0).ref
console.log(first.x, first.y, first.z) // output: 1 2 3

// ✅ array destructuring is allowed as well
const [element] = positions
console.log(element) // output: {x: 1, y: 2, z: 3}

Setting Values at an Index

If you want to set the value of an element, refer to one of it's fields (yourElement.x = 2 for example) or reference the e field to set the entire element (The e field is auto-generated for all struct defs). Both these methods work for references as well.

import {vec} from "struct-vec"

const Cats = vec({
     cuteness: "i32",
     isDangerous: "bool", 
     emoji: "char"
})
const cats = new Cats()

cats.push({
     cuteness: 10_000, 
     isDangerous: false, 
     emoji: "😸"
})

// 🛑 does not work - throws error
cats.index(0) = {
     cuteness: 2_876, 
     isDangerous: true, 
     emoji: "🐆"
}

// ✅ refer to one the fields
cats.index(0).cuteness = 2_876
cats.index(0).isDangerous = true
cats.index(0).emoji = "🐆"
const {cuteness, emoji, isDangerous} = cats.index(0)
console.log(cuteness, emoji, isDangerous) // output: 2876 true 🐆

// ✅ set entire element at once
cats.index(0).e = {
     cuteness: 2_876, 
     isDangerous: true, 
     emoji: "🐆"
}
console.log(cats.index(0).e) // output: {cuteness: 2_876, isDangerous: true, emoji: "🐆"}

// ✅ works for references as well
const first = cats.index(0).ref

first.cuteness = 2_876
first.isDangerous = true
first.emoji = "🐆"
console.log(first.cuteness, first.emoji, first.isDangerous) // output: 2876 true 🐆

first.e = {
     cuteness: 2_876, 
     isDangerous: true, 
     emoji: "🐆"
}
console.log(first.e) // output: {cuteness: 2_876, isDangerous: true, emoji: "🐆"}

Adding Elements

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

for (let i = 0; i < 100_000; i++) {
  // add elements
  positions.push({x: 1, y: 1, z: 1})
}

console.log(positions.index(0).e) // output: {x: 1, y: 1, z: 1}
console.log(positions.index(2_500).e) // output: {x: 1, y: 1, z: 1}
console.log(positions.length) // output: 100_000

Iterating

Imperatively

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

const positions = new PositionV(5_560).fill({x: 4, y: 3, z: 2})

for (let i = 0; i < positions.length; i++) {
  const element = positions.index(i)
  element.x = 20
  element.y = 5
  element.z = element.x + element.y
}

Iterators

Vecs support the majority of iterators that are found on javascript arrays. Check the API Reference for a full list of available iterators.

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

const positions = new PositionV(5_560).fill({x: 4, y: 3, z: 2})

positions.forEach((element, i, v) => {
  element.x = 20
  element.y = 5
  element.z = element.x + element.y
})

const bigPositions = positions.filter((element) => element.x > 10)

// note: vec es6 iterators are slow!!! but work nonetheless
for (const element of positions) {
  element.x = 20
  element.y = 5
  element.z = element.x + element.y
}

Nested Loops

Due to some limitations, vecs usually can only point to one element at a time. To overcome a detachedCursor or ref can be used.

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV(5).fill({x: 1, y: 1, z: 1})

// create a cursor initially pointing at index 0
const innnerCursor = positions.detachedCursor(0)
for (let i = 0; i < positions.length; i++) {
     const outerEl = positions.index(i)
     for (let x = 0; x < vec.length; x++) {
          const innerEl = extraCursor.index(x)
          
          if (innerEl.x === outerEl.x) {
               console.log("same x")
          } else if (innerEl.y === outerEl.y) {
               console.log("same y")
          } else if (innerEl.z === outerEl.z) {
               console.log("same z")
          }
     }
}

Removing Elements

End of Vec

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

positions.push({x: 1, y: 1, z: 1})
const removed = positions.pop()
console.log(removed) // output: {x: 1, y: 1, z: 1}
console.log(positions.length) // output: 0

Start of Vec

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

positions.push({x: 1, y: 1, z: 1})
positions.push({x: 2, y: 2, z: 2})
const removed = positions.shift()
console.log(removed) // output: {x: 1, y: 1, z: 1}
console.log(positions.length) // output: 1

Middle of Vec

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

positions.push({x: 1, y: 1, z: 1})
positions.push({x: 3, y: 3, z: 3})
positions.push({x: 2, y: 2, z: 2})
const [removed] = positions.splice(1, 1)
console.log(removed) // output: {x: 3, y: 3, z: 3}
console.log(positions.length) // output: 2

Swapping Elements

Due to how vecs work internally, swapping can feel awkward. Luckily, there is a swap method that lets you forget about the details.

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

positions.push({x: 1, y: 1, z: 1})
positions.push({x: 3, y: 3, z: 3})

// 🛑 incorrect swap
const tmp = positions.index(0)
positions.index(0) = positions.index(1) // throws Error
positions.index(1) = tmp // throws Error

// ✅ Correct swap
positions.swap(0, 1)
// ✅ This also works, but looks a little
// awkward
const correctTmp = positions.index(0).e
positions.index(0).e = positions.index(1).e
positions.index(1).e = correctTmp

Casting

Array

*Note: Vecs use 32-bit floats, so there will be a loss of precision for decimal numbers when converting an array to a vec.

import {vec, Vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const p = new PositionV(15).fill({x: 1, y: 2, z: 3})

// cast to array
const pArray = [...p]
console.log(pArray.length) // output: 15
console.log(pArray[0]) // output: {x: 1, y: 2, z: 3}
console.log(Array.isArray(pArray)) // output: true

// create from array
// note: array elements and vec elements must be
// of same type
const pFromArray = PositionsV.fromArray(pArray)
console.log(pFromArray.length) // output: 15
console.log(pFromArray.index(0).e) // output: {x: 1, y: 2, z: 3}
console.log(Vec.isVec(pFromArray)) // output: true
console.log(pFromArray !== p) // output: true

String

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const p = new PositionV(20).fill({x: 1, y: 1, z: 1})

// cast to string
const vecString = p.toJSON()
// can be casted to string like so as well
const vecString1 = JSON.stringify(p)
console.log(typeof vecString) // output: "string"
console.log(vecString1 === vecString) // output: true
// create vec from string
const jsonVec = PositionV.fromString(vecString)

console.log(jsonVec.length) // output: 20
jsonVec.forEach(pos => {
     console.log(pos.e) // output: {x: 1, y: 1, z: 1}
})

Multithreading

Vecs are backed by SharedArrayBuffers and can therefore be sent between workers at zero-cost (check out the benchmarks), irrespective of how many elements are in the vec.

Multithreading with vecs is as easy as this:

index.mjs

import {vec} from "struct-vec"

const Position = vec({x: "f32", y: "f32", z: "f32"})
const positions = new Position(10_000).fill(
    {x: 1, y: 1, z: 1}
)

const worker = new Worker("worker.mjs", {type: "module"})
// pass by reference, no copying
worker.postMessage(positions.memory)

worker.mjs

import {vec} from "struct-vec" 

const Position = vec({x: "f32", y: "f32", z: "f32"})

self.onmessage = ({data}) => {
   Position.fromMemory(data).forEach(p => {
      p.x += 1
      p.y += 2
      p.z += 3
   })
   self.postMessage("finished")
}

SAFETY-NOTES:

  • do not attempt to use length-changing methods while multithreading
  • because vecs are shared across multiple contexts, you can run into real threading issues like data races. Vecs do not come with any type of protections against threading-related bugs, so you'll have to devise your own (mutexes, scheduling, etc.).

Requirements

This package requires javascript environments that support ES6 and SharedArrayBuffers (eg. Node, Deno, supported browsers, etc.).

In order to allow enable SharedArrayBuffers in supported browsers you probably need to fulfill these security requirements.

Also be aware that the runtime compiler (the vec function) uses the unsafe Function constructor behind the scenes to generate vec classes at runtime. This will render vec useless in javascript environments that disallow the use of unsafe constructors such as Function, eval, etc. If it is the case that your environment disallows unsafe constructors, then consider using the build-time compiler (the vecCompile function) instead.

Typescript

Typescript bindings requires version 3.3.3+.

Struct Definitions

All elements in a vec are structs, which can be thought of as strictly-typed objects, that only carry data (no methods). Once a vec is given a struct definition (struct def), structs within the vec can only be exactly the type specified in the definition. This is why it is highly recommended to use this package with Typescript, as setting struct fields with incorrect types can lead to odd behaviors.

Creating a Struct Definition

Creating a struct def is similar to defining a struct def in statically-typed languages (C, Rust, Go, etc.) or an interface in Typescript. Define a struct def by creating an object and mapping fields (with a valid name) to supported data types. Nesting of struct defs is NOT allowed.

// should have floats at "x", "y", and "z" fields
const positionDef = {x: "f32", y: "f32", z: "f32"}

// should have character type at "emoji" field, and
// integer type at "cuteness" field
const catDef = {emoji: "char", cuteness: "i32"}

// should have boolean type at "isScary" and
// integer type at "power" field
const monsterDef = {isScary: "bool", power: "i32"}

Default Struct Fields

Every struct, regardless of definition has some auto-generated fields. Auto-generated fields are:

e : allows you to get and set an entire element.

  • calling .e on an element returns the element by value not by reference. See ref to get element by reference.
import {vec} from "struct-vec"

const Cats = vec({
     cuteness: "i32",
     isDangerous: "bool", 
     emoji: "char"
})
const cats = new Cats()

cats.push({
     cuteness: 10_000, 
     isDangerous: false, 
     emoji: "😸"
})
// get entire element
console.log(cats.index(0).e) // output: {cuteness: 10_000, isDangerous: false, emoji: "😸"}

// set entire element
cats.index(0).e = {
     cuteness: 2_876, 
     isDangerous: true, 
     emoji: "🐈‍⬛"
}
console.log(cats.index(0).e) // output: {cuteness: 2_876, isDangerous: true, emoji: "🐈‍⬛"}

ref: returns a reference to an index in a vec.

  • these references refer to an index in a vec NOT the element at the index. Meaning that if the underlying element is moved, the reference will no longer point to it and potentially dangle.
import {vec} from "struct-vec"

const Enemy = vec({power: "i32", isDead: "bool"})
const orcs = new Enemy()
orcs.push(
     {power: 55, isDead: false},
     {power: 13, isDead: false},
     {power: 72, isDead: false},
)
// get a reference to index 0
const firstElement = orcs.index(0).ref
console.log(orcs.index(2).e) // output: {power: 72, isDead: false},
console.log(firstElement.e) // output: {power: 55, isDead: false}

// if underlying element of a ref moves 
// it does not move with it
orcs.swap(0, 1)
console.log(firstElement.e) // output: {power: 13, isDead: false}

// ✅ references can create other references
const firstElementRef = firstElement.ref

Data types

f32

Single-precision floating point number, takes 4 bytes (32 bits) of memory. Similar to javascript's Number type, but with less precision. Also similar to to C's float type.

To define a f32 field:

import {vec} from "struct-vec"

// "num" should be a float type
const NumVec = vec({num: "f32"})
const v = new NumVec()

v.push({num: 1.1})
console.log(v.index(0).num) // output: 1.100000023841858
v.index(0).num = 2.1
// notice the loss of precision
console.log(v.index(0).num) // output: 2.0999999046325684

access-speed-num

This data type very fast in terms of access speed as it maps exactly to a native javascript type.

type-safety-num

If one sets a f32 field with an incorrect type (String type for example), the field will be set to NaN. There a couple of exceptions to this rule, such as if the incorrect type is null, an Array, a BigInt, a Symbol, or a Boolean which will either throw a runtime error, set the field to 0 or 1, depending on the type and javascript engine.

i32

A 32-bit signed integer, takes 4 bytes (32 bits) of memory. Similar to javascript's Number type, but without the ability to carry decimals. Also similar to C's int type.

To define a i32 field:

import {vec} from "struct-vec"

// "num" should be a integer type
const NumVec = vec({num: "i32"})
const v = new NumVec()

v.push({num: 1})
console.log(v.index(0).num) // output: 1
v.index(0).num = 2
console.log(v.index(0).num) // output: 2
v.index(0).num = 2.2
// notice that i32s cannot hold decimals
console.log(v.index(0).num) // output: 2

access-speed-num

same as f32

type-safety-num

same as f32

bool

A boolean value that can be either true or false, takes 1/8 of a byte of memory (1 bit). Same as javascript's Boolean type.

To define a bool field:

import {vec} from "struct-vec"

// "bool" should be boolean type
const BoolVec = vec({bool: "bool"})
const v = new BoolVec()

v.push({bool: true})
console.log(v.index(0).bool) // output: true
v.index(0).bool = false
console.log(v.index(0).bool) // output: false

access-speed-bool

This data type requires a small conversion when getting and setting it's value.

type-safety-bool

When a bool field is set with an incorrect type (Number type for example), the field will be set to true except if the type is falsy, in which case the field will be set to false.

char

One valid unicode 14.0.0 character, takes 4 bytes (32 bits). Same as javascript's String type, except that it is restricted to exactly one character.

To define a char field:

import {vec} from "struct-vec"

// "char" should be character type
const CharVec = vec({char: "char"})
const v = new CharVec()
v.push({char: "😀"})
console.log(v.index(0).char) // output: "😀"
v.index(0).char = "a"
console.log(v.index(0).char) // output: "a"

access-speed-char

This data type requires a medium level conversion in order to access. Can be up to 100% slower (2x slower) than the f32 type.

type-safety-char

When a char field is set with an incorrect type an error will be thrown. If the inputted type is a string longer than one character, the field will be set to the first character of input. If the inputted type is an empty string, the field will be set to " " (space character).

Disallowed Field Names

Struct field names follow the same naming convention as javascript variables, excluding unicode characters.

Fields also cannot be named e, _viewingIndex, ref,index, self.

import {vec} from "struct-vec"

// 🛑 Not allowed
const v0 = vec({_viewingIndex: "char"})
const v1 = vec({self: "f32"})
const v5 = vec({e: "f32"})
const v2 = vec({["my-field"]: "bool"})
const v3 = vec({["👍"]: "char"})
const v4 = vec({0: "f32"})

// ✅ allowed
const v11 = vec({x: "f32", y: "f32", z: "f32"})
const v6 = vec({$field: "bool"})
const v7 = vec({_field: "char"})
const v8 = vec({e0: "f32"})
const v9 = vec({myCoolField: "f32"})
const v10 = vec({my$_crazy0_fieldName123: "bool"})

Compilers

This package provides two ways to define vecs, either through the exported vec (runtime compiler) or vecCompile (build-time compiler) functions. Both compilers emit the exact same vec classes.

The key differences between the compilers is that the build-time compiler returns a string containing your vec class which you can write to disk to use in another application, instead of creating a vec class which can be used right away.

Runtime Compiler

Generally, the runtime compiler is easier to work with and that's why all the documentation examples use it. Essentially, the runtime compiler takes your struct def and returns a vec class that can immediately be used.

In case your were wondering, the runtime compiler is quite fast. Defining a vec like so:

const v = vec({x: "f32", y: "f32", z: "f32", w: "f32"})

takes about 0.013 milliseconds in Node. Unless you are planning on defining tens of thousands of vecs, the runtime compiler won't really slow down your application.

The runtime compiler does however make use of the unsafe Function constructor internally. If you know that your javascript environment doesn't allow the use of unsafe constructors (like Function, eval, etc.), then use the build-time compiler.

Also, if you want a build tool like Webpack, ESBuild, Vite, etc. to apply transforms on your vec classes (such as convert them to ES5 syntax), the build-time compiler might be the right choice for you.

Build-time Compiler

The build-time compiler is almost exactly the same as the runtime one. The difference being, after the compiler takes your struct def, it returns a string version of your vec class, instead of a vec class that can be immediately used.

Here's an example:

build.mjs

import fs from "fs"
import {vecCompile} from "struct-vec"

// the path to the "struct-vec" library.
// For the web or Deno, you would
// put the full url to the library.
// for example: https://deno.land/....
// or https://unpkg.com/...
const LIB_PATH = "struct-vec"

// create a struct def, just like with the
// runtime compiler
const def = {x: "bool", y: "f32", z: "char"}

const MyClass = vecCompile(def, LIB_PATH, {
     // generate a javascript class, not typescript
     lang: "js",
     // create class with "export" statement
     exportSyntax: "named",
     className: "MyClass"
})
console.log(typeof MyClass) // output: string
// write the class to disk to use later
// or in another application
fs.writeFileSync("MyClass.mjs", MyClass, {encoding: "utf-8"})

now take a look at the file the class was written to:

MyClass.mjs

// imported dependencies from LIB_PATH
import {Vec} from "struct-vec"
/**
 * @extends {Vec<{"x":"bool","y":"f32","z":"char"}>}
 */
// class was named correctly
// and it was created as a "named" export
export class MyClass extends Vec {
     // some auto generated stuff
     // that looks like it was written by an ape
    static Cursor = class Cursor {
        _viewingIndex = 0
        constructor(self) { this.self = self }
        get y() { return this.self._memory[this._viewingIndex] }; set y(newValue) { this.self._memory[this._viewingIndex] = newValue };
        get z() { return String.fromCodePoint(this.self._memory[this._viewingIndex + 1] || 32) }; set z(newValue) { this.self._memory[this._viewingIndex + 1] = newValue.codePointAt(0) || 32 };
        get x() { return Boolean(this.self._memory[this._viewingIndex + 2] & 1) }; set x(newValue) { this.self._memory[this._viewingIndex + 2] &= -2;this.self._memory[this._viewingIndex + 2] |= Boolean(newValue)};
        set e({y, z, x}) { this.y = y;this.z = z;this.x = x; }
        get e() { return {y: this.y, z: this.z, x: this.x} }        
    }
    get elementSize() { return 3 }
    // here's the inputted struct def
    get def() { return {"x":"bool","y":"f32","z":"char"} }
    get cursorDef() { return MyClass.Cursor }
}

You can now import MyClass into a javascript or typescript file and it will work just like any other vec. Other build options are found in the API Reference.

Unfortunately the build-time compiler does not come with a command-line tool - so you'll need to figure out how you want to generate and store your vec classes.

Caveats

Indexing does NOT Return Element

Indexing into a vec (calling index) is similar to calling next on an iterator. Calling myVec.index(0) will take you to the first element, allowing you to access it's fields, but does not return the element.

To get an element by value use the e field. To get an entire element by reference use the ref field.

The implication of all this, is that a vec can only point to one element at a time. If you want to look at two or more elements at once, you will have to create additional cursors, which can created with the Vec.detachedCursor method.

An example to illustrate this point is a nested for loop:

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

positions.push({x: 1, y: 1, z: 1})
positions.push({x: 3, y: 3, z: 3})
positions.push({x: 2, y: 2, z: 2})

// 🛑 incorrect example
for (let i = 0; i < positions.length; i++) {
     // move vec to index 0
     const el = positions.index(i)
     // move vec from index 0 through 2
     for (let x = 0; x < vec.length; x++) {
          const innerEl = positions.index(x)
     }
     // ❌ vec was moved to index 2 (vec.length) at the 
     // end of the inner loop and is no longer 
     // pointing to index i
     console.log(el.e) // output: {x: 2, y: 2, z: 2}
}

// ✅ Use a detached cursor
// create a cursor initially pointing
// at index 0
const extraCursor = positions.detachedCursor(0)
for (let i = 0; i < positions.length; i++) {
     const el = positions.index(i)
     // move extra cursor from index 0 through 2
     for (let x = 0; x < vec.length; x++) {
          // detached cursors can be move via the "index"
          // method, just like vecs but don't
          // influence were the vec is pointing
          const innerEl = extraCursor.index(x)
     }
     console.log(el.e) // output: what ever is at index i
}

// ✅ Use a reference, also works
// but less efficent
for (let i = 0; i < positions.length; i++) {
     // refs are just a special
     // type of "detachedCursor" under the hood
     const el = positions.index(i).ref
     // move vec from index 0 through 2
     for (let x = 0; x < vec.length; x++) {
          const innerEl = positions.index(x)
     }
     console.log(el.e) // output: what ever is at index i
}

Indexing Out of Bounds

Indexing out of bounds negatively (i < 0) will return undefined just like an Array. Indexing out of bounds past the length (i > vec.length - 1) may or may not return undefined. Sometimes a vec will keep garbage memory at the end to avoid resizing and this is the expected behavior.

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV(5)

positions.push({x: 1, y: 1, z: 1})
positions.push({x: 2, y: 2, z: 2})
positions.pop()
// current length
console.log(positions.length) // output: 1

// ✅ indexing out of bounds negatively (i < 0) 
// always returns undefined
console.log(positions.index(-1).x) // output: undefined

// ⚠️ indexing out of bounds positively (i > vec.length - 1)
// may or may not return undefined
console.log(positions.index(1).x) // output: 2
console.log(positions.index(2).x) // output: 0
console.log(positions.index(10_000).x) // output: undefined

Do Not Mutate Vec Length or Capacity during Multithreading

Do not use any methods that may potentially change the length (or capacity) of a vec during multi-threading. Doing so will lead to unpredictable bugs.

Length-changing methods include: pop, truncate, splice, shift, push, fill, unshift

Capacity-changing methods include: shrinkTo, reserve

Performance Tips

Adding Many Elements

Generally speaking vecs manage their own memory, so you never have to think about resizing or shrinking a vec. However, vecs also provide the ability to expand their memory (or shrink it) on-demand, which is useful when you know ahead of time that you will be adding many elements at once.

Before pushing many elements at once you can use the reserve method to resize the vec's memory as to avoid resizing multiple times and increase performance.

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

// make sure there is enough memory for an additional
// 10,00 elements
positions.reserve(10_000)

for (let i = 0; i < 10_000; i++) {
  positions.push({x: 1, z: 1, y: 1})
}

Removing Many Elements

Similar to when adding many elements, vecs provide a couple of mass removal methods that are more performant.

If you want to remove the last n elements use the truncate method

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV(10_000).fill({x: 1, y: 1, z: 1})

console.log(positions.length) // output: 10_000

// remove last 5_000 elements at once
positions.truncate(5_000)

console.log(positions.length) // output: 5_000

Avoid ES6 Iterators and Indexing

ES6 array destructuring operator (const [element] = vec), spread operator (...vec), and for...of loops (for (const el of vec)) should be avoided except if you want to cast a vec to an array or something similar.

These operators force vecs to deserialize their internal binary representation of structs to objects - which is costly and can cause some unexpected side-effects due to fact that they return elements by value, NOT by reference.

NOTE: the values, entries, keys methods are also es6 iterators.

Avoid Using the e Field Except for Setting an Element

Using the e field to view the value of an element is costly as it forces the vec to deserialize it's internal binary representation into object format (similar to es6 methods). Getting the value of individual fields is far more performant than using the e field. Fortunately, this bottleneck doesn't seem to exist when setting with e.

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const positions = new PositionV()

positions.push({x: 1, z: 1, y: 1})

// ✅ performant
const {x, y, z} = positions.index(0)
// ✅ also performant
const xVal = positions.index(0).x
const yVal = positions.index(0).y
const zVal = positions.index(0).z

// ⚠️ less performant
const val = positions.index(0).e

// ✅ setting with "e" field is also performant 
// unlike viewing
positions.index(0).e = {x: 2, y: 5, z: 7}

Benchmarks

All test were done over 100 samples, with 4 warmup runs before recording. The multithreaded benchmarks are the only exception to this.

Test machine was a Windows 11/WSL-Ubuntu 20.04 (x64), with a Intel i7-9750H CPU (12 core), and 16 GB RAM.

Iteration

Imperative loop

Iterate over 10 million elements in an imperative manner, adding 10 to one of the element fields.

The raw buffer fields here are Float32Arrays.

Taken on March 31, 2022

Node 16.13.1 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Raw Buffer | 15.28 ms ±0.96 | | Array | 49.82 ms ±2.35 | | Vec | 21.69 ms ±0.74 |

Deno 1.20.2 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Raw Buffer | 14.79 ms ±0.89 | | Array | 32.01 ms ±1.15 | | Vec | 20.63 ms ±0.76 |

Chrome 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Raw Buffer | 18.80 ms ±2.42 | | Array | 38.19 ms ±2.64 | | Vec | 21.54 ms ±1.66 |

Firefox 98 (Windows 11) | Candidate | Result | | ---- | ------ | | Raw Buffer | 23.97 ms ±0.93 | | Array | 64.30 ms ±3.13 | | Vec | 54.68 ms ±1.54 |

Edge 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Raw Buffer | 17.19 ms ±1.87 | | Array | 37.82 ms ±2.10 | | Vec | 21.36 ms ±1.28 |

ForEach loop

Iterate over 10 million elements with ForEach iterator, adding 10 to one of the element fields.

Taken on March 24, 2022

Node 16.13.1 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Array | 116.84 ms ±3.53 | | Vec | 47.84 ms ±0.77 |

Deno 1.20.2 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Array | 103.82 ms ±2.98 | | Vec | 45.57 ms ±1.14 |

Chrome 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Array | 126.19 ms ±5.72 | | Vec | 48.67 ms ±4.08 |

Firefox 98 (Windows 11) | Candidate | Result | | ---- | ------ | | Array | 102.04 ms ±4.00 | | Vec | 149.01 ms ±10.09 |

Edge 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Array | 124.70 ms ±4.44 | | Vec | 48.71 ms ±2.59 |

ES6 Iterator loop

Iterate over 10 million elements with ES6's for...of loop and add 10 to one of the element fields.

Taken on March 24, 2022

Node 16.13.1 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Raw Buffer (imperative) | 30.59 ms ±1.56 | | Array | 53.12 ms ±1.96 | | Vec | 196.70 ms ±6.47 |

Deno 1.20.2 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Raw Buffer (imperative) | 30.45 ms ±1.54 | | Array | 34.95 ms ±1.19 | | Vec | 194.63 ms ±4.82 |

Chrome 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Raw Buffer (imperative) | 32.13 ms ±2.15 | | Array | 34.97 ms ±1.57 | | Vec | 200.56 ms ±7.61 |

Firefox 98 (Windows 11) | Candidate | Result | | ---- | ------ | | Raw Buffer (imperative) | 29.21 ms ±3.35 | | Array | 106.89 ms ±4.14 | | Vec | 346.72 ms ±13.57 |

Edge 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Raw Buffer (imperative) | 31.20 ms ±1.66 | | Array | 34.46 ms ±0.83 | | Vec | 200.35 ms ±6.35 |

Parallel Loop

Iterate over 8 million elements in a parallel (4 cores) and perform a significant computation. Average of 10 runs, with 4 warmups runs before recording.

Taken on March 31, 2022

Node 16.13.1 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Single-thread Array | 6,415.10 ms ±469.00 | | Multithreaded Array | 18,833.40 ms ±246.66 | | Single-thread Vec | 4,856.90 ms ±120.40 | | Multithreaded Vec | 1,411.40 ms ±98.34 |

Deno 1.20.2 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Single-thread Array | 6,541.40 ms ±167.11 | | Multithreaded Array | 18,204.20 ms ±172.01 | | Single-thread Vec | 5,487.70 ms ±43.90 | | Multithreaded Vec | 1,411.40 ms ±98.34 |

Chrome 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Single-thread Array | 5,746.00 ms ±76.65 | | Multithreaded Array | 17,989.40 ms ±751.12 | | Single-thread Vec | 5,350.60 ms ±162.57 | | Multithreaded Vec | 1,580.80 ms ±39.07 |

Firefox 98 (Windows 11) | Candidate | Result | | ---- | ------ | | Single-thread Array | 6387.00 ms ±26.23 | | Multithreaded Array | Crashed with no error code | | Single-thread Vec | 6293.40 ms ±179.05 | | Multithreaded Vec | 1847.10 ms ±74.04 |

Edge 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Single-thread Array | 6,388.00 ms ±233.65 | | Multithreaded Array | Crashed with code STATUS_BREAKPOINT | | Single-thread Vec | 5,338.30 ms ±127.40 | | Multithreaded Vec | 1,569.20 ms ±73.29 |

Pushing Elements

Pushing 10 million elements in a row.

"with reserve" label means that vec/array preallocated enough space for all 10 million elements before attempting to push elements.

Preallocation with arrays looks like this:

const arr = new Array(10_000_000)
// start pushing elements

For vecs:

const vec = new PositionV()
vec.reserve(10_000_000)
// start pushing elements 

Taken on March 31, 2022

Node 16.13.1 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Vec | 138.99 ms ±15.51 | | Array | 690.30 ms ±227.53 | | Vec with reserve | 76.92 ms ±4.69 | | Array with reserve | 2305.48 ms ±85.52 |

Deno 1.20.2 (Ubuntu 20.04) | Candidate | Result | | ---- | ------ | | Vec | 143.74 ms ±12.57 | | Array | 1459.62 ms ±170.93 | | Vec with reserve | 101.21 ms ±5.23 | | Array with reserve | 1602.00 ms ±27.78 |

Chrome 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Vec | 228.77 ms ±14.33 | | Array | 1373.45 ms ±262.41 | | Vec with reserve | 129.86 ms ±62.47 | | Array with reserve | 1459.22 ms ±35.14 |

Firefox 98 (Windows 11) | Candidate | Result | | ---- | ------ | | Vec | 630.28 ms ±9.57 | | Array | 612.50 ms ±10.76 | | Vec with reserve | 446.65 ms ±22.07 | | Array with reserve | 2348.31 ms ±198.37 |

Edge 99 (Windows 11) | Candidate | Result | | ---- | ------ | | Vec | 243.50 ms ±11.83 | | Array | 1370.32 ms ±259.06 | | Vec with reserve | 132.42 ms ±10.41 | | Array with reserve | 1457.89 ms ±58.15 |

API Reference

vec-struct~Vec

The base class that all generated vec classes inherit from.

This class isn't intended to be manually inherited from, as the vec and vecCompile functions will automatically inherit this class and generate the necessary override methods based on your struct definition. The class is still made available however as it has some useful static methods, such as:

isVec : can be used to check if a particular type is a vec at runtime, similar to the Array.isArray method.

The class is generic over T which extends the StructDef type. In other words, the Vec class is type Vec<T extends StructDef>

Kind: inner class of vec-struct

new Vec([initialCapacity])

| Param | Type | Default | Description | | --- | --- | --- | --- | | [initialCapacity] | number | 15 | the amount of capacity to initialize vec with. Defaults to 15. |

Example (Basic Usage)

import {vec} from "struct-vec"

const geoCoordinates = vec({latitude: "f32", longitude: "f32"})

// both are valid ways to initialize
const withCapacity = new geoCoordinates(100)
const without = new geoCoordinates()

vec.elementSize : number

The amount of raw memory an individual struct (element of a vec) requires for this vec type. An individual block of memory corresponds to 4 bytes (32-bits).

For example if elementSize is 2, each struct will take 8 bytes.

Kind: instance property of Vec

vec.def : StructDef

The definition of an individual struct (element) in a vec.

Kind: instance property of Vec

vec.length : number

The number of elements in vec. The value is between 0 and (2^32) - 1 (about 2 billion), always numerically greater than the highest index in the array.

Kind: instance property of Vec

vec.capacity : number

The number of elements a vec can hold before needing to resize. The value is between 0 and (2^32) - 1 (about 2 billion).

Kind: instance property of Vec
Example (Expanding Capacity)

import {vec} from "struct-vec"

const Cats = vec({isCool: "f32", isDangerous: "f32"})
// initialize with a capacity of 15
const cats = new Cats(15)
// currently the "cats" array can hold
// up to 15 elements without resizing
// but does not have any elements yet
console.log(cats.capacity) // output: 15
console.log(cats.length) // output: 0

// fill entire capacity with elements
cats.fill({isCool: 1, isDangerous: 1})
// now the cats array will need to resize
// if we attempt to add more elements
console.log(cats.capacity) // output: 15
console.log(cats.length) // output: 15

const capacity = cats.capacity
cats.push({isCool: 1, isDangerous: 1})
// vec resized capacity to accommodate
// for more elements
console.log(capacity < cats.capacity) // output: true
console.log(cats.length) // output: 16

Example (Shrinking Capacity)

import {vec} from "struct-vec"

const Cats = vec({isCool: "f32", isDangerous: "f32"})
// initialize with a capacity of 15
const cats = new Cats(15)
// currently the "cats" array can hold
// up to 15 elements without resizing
// but does not have any elements yet
console.log(cats.capacity) // output: 15
console.log(cats.length) // output: 0
for (let i = 0; i < 5; i++) {
     cats.push({isCool: 1, isDangerous: 1})
}

// vec can hold 3x more elements than we need
// lets shrink the capacity to be memory efficient
console.log(cats.capacity) // output: 15
console.log(cats.length) // output: 5

// shrink vec memory so that length
// and capacity are the same
cats.shrinkTo(0)
console.log(cats.capacity) // output: 5
console.log(cats.length) // output: 5

vec.memory : ReadonlyInt32Array

The binary representation of a vec.

WARNING: It is never recommended to manually edit the underlying memory, doing so may lead to memory corruption.

Kind: instance property of Vec

vec.index(index) ⇒ VecCursor.<StructDef>

Returns a cursor which allows the viewing of the element at the inputted index.

NOTE: this method does not return the actual element at the index. In order to get the entire element at a given index you must use the ".e" method on the cursor. If you want one of the fields of the element just reference the field (for example ".x")

Kind: instance method of Vec
Returns: VecCursor.<StructDef> - A cursor of the target index

| Param | Type | Description | | --- | --- | --- | | index | number | the index you want to view |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})

const pos = new PositionsV()

pos.push({x: 1, y: 1, z: 1})
pos.push({x: 1, y: 2, z: 1})
pos.push({x: 1, y: 3, z: 1})

// get entire element at index 0.
// The "e" property comes with all elements
// automatically
console.log(pos.index(0).e) // output: {x: 1, y: 1, z: 1}
console.log(pos.index(1).e) // output: {x: 1, y: 2, z: 1}
// get the "y" field of the element
// at index 2
console.log(pos.index(2).y) // output: 3

vec.at(index) ⇒ VecCursor.<StructDef>

Returns a cursor which allows the viewing of the element at the inputted index.

This method is identical to the index method except that it accepts negative indices. Negative indices are counted from the back of the vec (vec.length + index)

PERFORMANCE-TIP: this method is far less efficient than the index method.

NOTE: this method does not return the actual element at the index. In order to get the entire element at a given index you must use the ".e" method on the cursor. If you want one of the fields of the element just reference the field (for example ".x")

Kind: instance method of Vec
Returns: VecCursor.<StructDef> - A cursor of the target index

| Param | Type | Description | | --- | --- | --- | | index | number | the index you want to view. Supports negative indices. |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})

const pos = new PositionsV()

pos.push({x: 1, y: 1, z: 1})
pos.push({x: 1, y: 2, z: 1})
pos.push({x: 1, y: 3, z: 1})

// get entire element at index 0.
// The "e" property comes with all elements
// automatically
console.log(pos.index(-1).e) // output: {x: 1, y: 3, z: 1}
console.log(pos.index(-2).e) // output: {x: 1, y: 2, z: 1}
// get the "y" field of the element
// at index 2
console.log(pos.index(-3).y) // output: 1

vec.forEach(callback) ⇒ void

Executes a provided function once for each vec element.

Kind: instance method of Vec

| Param | Type | Description | | --- | --- | --- | | callback | ForEachCallback.<StructDef> | A function to execute for each element taking three arguments: - element The current element being processed in the - index (optional) The index of the current element being processed in the vec. - vec (optional) The vec which method was called upon. |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const pos = PositionsV(15).fill({x: 1, y: 1, z: 1})

pos.forEach((p, i, v) => {
     console.log(p.e) // output: {x: 1, y: 1, z: 1}
})

vec.map(callback) ⇒ Array.<YourCallbackReturnType>

Creates a new array populated with the results of calling a provided function on every element in the calling vec.

Kind: instance method of Vec
Returns: Array.<YourCallbackReturnType> - A new array with each element being the result of the callback function.

| Param | Type | Description | | --- | --- | --- | | callback | MapCallback.<StructDef, YourCallbackReturnValue> | Function that is called for every element of vec. Each time callbackFn executes, the returned value is added to new Array. Taking three arguments: - element The current element being processed - index (optional) The index of the current element being processed in the vec. - vec (optional) The vec which method was called upon. |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const pos = PositionsV(15).fill({x: 1, y: 1, z: 1})
const xVals = pos.map(p => p.x)

xVals.forEach((num) => {
     console.log(num) // output: 1
})

vec.mapv(callback) ⇒ Vec.<StructDef>

Creates a new vec populated with the results of calling a provided function on every element in the calling vec.

Essentially mapv is the same as chaining slice and forEach together.

Kind: instance method of Vec
Returns: Vec.<StructDef> - A new vec with each element being the result of the callback function.

| Param | Type | Description | | --- | --- | --- | | callback | MapvCallback.<StructDef> | Function that is called for every element of vec. Please note that each element is an exact copy of the vec mapv was called on. Taking three arguments: - element The current element being processed - index (optional) The index of the current element being processed in the vec. - vec (optional) The vec which method was called upon. |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const pos = PositionsV(15).fill({x: 1, y: 1, z: 1})
const yAdd = pos.mapv(p => p.y += 2)

yAdd.forEach((p) => {
     console.log(p.e) // output: {x: 1, y: 3, z: 1}
})
pos.forEach((p) => {
     console.log(p.e) // output: {x: 1, y: 1, z: 1}
})
console.log(pos !== yAdd) // output: true

vec.filter(callback) ⇒ Vec.<StructDef>

Creates a new vec with all elements that pass the test implemented by the provided function.

Kind: instance method of Vec
Returns: Vec.<StructDef> - A new vec with the elements that pass the test. If no elements pass the test, an empty vec will be returned.

| Param | Type | Description | | --- | --- | --- | | callback | TruthyIterCallback.<StructDef> | A function to test for each element, taking three arguments: - element The current element being processed - index (optional) The index of the current element being processed in the vec. - vec (optional) The vec which method was called upon. |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const pos = PositionsV()
for (let i = 0; i < 5; i++) {
     pos.push({x: 1, y: 2, z: 10})
}
for (let i = 0; i < 5; i++) {
     pos.push({x: 1, y: 2, z: 0})
}
const bigZs = pos.filter(p => p.z > 5)

console.log(bigZs.length) // output: 5
bigZs.forEach((p) => {
     console.log(p.e) // output: {x: 1, y: 2, z: 10}
})
console.log(pos.length) // output: 10
console.log(pos !== bigZs) // output: true

vec.find(callback) ⇒ VecCursor.<StructDef> | undefined

Returns a vec cursor to the first element in the provided vec that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned.

Kind: instance method of Vec

| Param | Type | Description | | --- | --- | --- | | callback | TruthyIterCallback.<StructDef> | A function to test for each element, taking three arguments: - element The current element being processed - index (optional) The index of the current element being processed in the vec. - vec (optional) The vec which method was called upon. |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const pos = PositionsV()
for (let i = 0; i < 5; i++) {
     pos.push({x: 1, y: 2, z: 10})
}
for (let i = 0; i < 5; i++) {
     pos.push({x: 1, y: 2, z: 0})
}

const nonExistent = pos.find(p => p.z === 100)
console.log(nonExistent) // output: undefined

const exists = pos.find(p => p.z === 10)
console.log(exists.e) // output: {x: 1, y: 2, z: 10}

vec.findIndex(callback) ⇒ number

Returns the index of the first element in the vec that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test

Kind: instance method of Vec
Returns: number - The index of the first element in the vec that passes the test. Otherwise, -1

| Param | Type | Description | | --- | --- | --- | | callback | TruthyIterCallback.<StructDef> | A function to test for each element, taking three arguments: - element The current element being processed - index (optional) The index of the current element being processed in the vec. - vec (optional) The vec which method was called upon. |

Example (Basic Usage)

import {vec} from "struct-vec"

const PositionV = vec({x: "f32", y: "f32", z: "f32"})
const pos = PositionsV()
for (let i = 0; i < 5; i++)