struct-vec
v0.1.2
Published
Javascript array-like data structures designed for multithreading
Downloads
2
Readme
بسم الله الرحمن الرحيم
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. Seeref
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
This data type very fast in terms of access speed as it maps exactly to a native javascript type.
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
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
This data type requires a small conversion when getting and setting it's value.
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"
This data type requires a medium level conversion in order to access. Can be up to 100% slower (2x slower) than the f32
type.
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 Float32Array
s.
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
- new Vec([initialCapacity])
- instance
- .elementSize : number
- .def : StructDef
- .length : number
- .capacity : number
- .memory : ReadonlyInt32Array
- .index(index) ⇒ VecCursor.<StructDef>
- .at(index) ⇒ VecCursor.<StructDef>
- .forEach(callback) ⇒ void
- .map(callback) ⇒ Array.<YourCallbackReturnType>
- .mapv(callback) ⇒ Vec.<StructDef>
- .filter(callback) ⇒ Vec.<StructDef>
- .find(callback) ⇒ VecCursor.<StructDef> | undefined
- .findIndex(callback) ⇒ number
- .lastIndexOf(callback) ⇒ number
- .reduce(callback, initialValue) ⇒ YourCallbackReturnValue
- .reduceRight(callback, initialValue) ⇒ YourCallbackReturnValue
- .every(callback) ⇒ boolean
- .some(callback) ⇒ boolean
- .entries() ⇒ Iterator.<Array.<number, Struct.<StructDef>>>
- .keys() ⇒ Iterator.<number>
- .values() ⇒ Iterator.<Struct.<StructDef>>
- .slice([start], [end]) ⇒ Vec.<StructDef>
- .copyWithin(target, [start], [end]) ⇒ Vec.<StructDef>
- .reserve(additional) ⇒ Vec.<StructDef>
- .reverse() ⇒ Vec.<StructDef>
- .concat(...vecs) ⇒ Vec.<StructDef>
- .pop() ⇒ Struct.<StructDef> | undefined
- .truncate(count) ⇒ number
- .fill(value, [start], [end]) ⇒ Vec.<StructDef>
- .push(...structs) ⇒ number
- .splice(start, [deleteCount], ...items) ⇒ Vec.<StructDef>
- .shift() ⇒ Struct.<StructDef>
- .unshift(...structs) ⇒ number
- .shrinkTo([minCapacity]) ⇒ Vec.<StructDef>
- .sort(compareFn) ⇒ Vec.<StructDef>
- .swap(aIndex, bIndex) ⇒ Vec.<StructDef>
- .toJSON() ⇒ string
- .detachedCursor(index) ⇒ DetachedVecCursor
- static
- .def : Readonly.<StructDef>
- .elementSize : Readonly.<number>
- .isVec(candidate) ⇒ boolean
- .fromMemory(memory) ⇒ Vec.<StructDef>
- .fromArray(structArray) ⇒ Vec.<StructDef>
- .fromString(vecString) ⇒ Vec.<StructDef>
- ~validateStructDef(def) ⇒ boolean
- ~vec(structDef, [options]) ⇒ VecClass.<StructDef>
- ~vecCompile(structDef, pathToLib, [options]) ⇒ string
- ~Vec
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
- ~Vec
- new Vec([initialCapacity])
- instance
- .elementSize : number
- .def : StructDef
- .length : number
- .capacity : number
- .memory : ReadonlyInt32Array
- .index(index) ⇒ VecCursor.<StructDef>
- .at(index) ⇒ VecCursor.<StructDef>
- .forEach(callback) ⇒ void
- .map(callback) ⇒ Array.<YourCallbackReturnType>
- .mapv(callback) ⇒ Vec.<StructDef>
- .filter(callback) ⇒ Vec.<StructDef>
- .find(callback) ⇒ VecCursor.<StructDef> | undefined
- .findIndex(callback) ⇒ number
- .lastIndexOf(callback) ⇒ number
- .reduce(callback, initialValue) ⇒ YourCallbackReturnValue
- .reduceRight(callback, initialValue) ⇒ YourCallbackReturnValue
- .every(callback) ⇒ boolean
- .some(callback) ⇒ boolean
- .entries() ⇒ Iterator.<Array.<number, Struct.<StructDef>>>
- .keys() ⇒ Iterator.<number>
- .values() ⇒ Iterator.<Struct.<StructDef>>
- .slice([start], [end]) ⇒ Vec.<StructDef>
- .copyWithin(target, [start], [end]) ⇒ Vec.<StructDef>
- .reserve(additional) ⇒ Vec.<StructDef>
- .reverse() ⇒ Vec.<StructDef>
- .concat(...vecs) ⇒ Vec.<StructDef>
- .pop() ⇒ Struct.<StructDef> | undefined
- .truncate(count) ⇒ number
- .fill(value, [start], [end]) ⇒ Vec.<StructDef>
- .push(...structs) ⇒ number
- .splice(start, [deleteCount], ...items) ⇒ Vec.<StructDef>
- .shift() ⇒ Struct.<StructDef>
- .unshift(...structs) ⇒ number
- .shrinkTo([minCapacity]) ⇒ Vec.<StructDef>
- .sort(compareFn) ⇒ Vec.<StructDef>
- .swap(aIndex, bIndex) ⇒ Vec.<StructDef>
- .toJSON() ⇒ string
- .detachedCursor(index) ⇒ DetachedVecCursor
- static
- .def : Readonly.<StructDef>
- .elementSize : Readonly.<number>
- .isVec(candidate) ⇒ boolean
- .fromMemory(memory) ⇒ Vec.<StructDef>
- .fromArray(structArray) ⇒ Vec.<StructDef>
- .fromString(vecString) ⇒ Vec.<StructDef>
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++)