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

forkable-list

v0.24.0

Published

Build an array, fork it, and modify both branches, sharing memory between them

Downloads

74

Readme

forkable-list lets you add stuff to an array, and then fork it so that you've got two versions you can add to and splice stuff into, without duplicating the whole array.

var forkableList = require("forkable-list")

var alphabet = forkableList(["a", "b", "c", "d"])

var spell = alphabet.fork()

spell.splice(2, 0, "r", "a")
spell.splice(3, 1, "a", "d", "a", "b", "r", "a")

alphabet.set(4, "e")
alphabet.splice(5, 0, "f", "g")

alphabet.values()
// returns ["a", "b", "c", "d", "e", "f", "g"]

spell.join("")
// returns "abracadabra"

At this point the original list is stored using a single array. This is what is created in memory:

alphabet = {
  "segments": [
    {
      "mutableAfter": 3,
      "extendable": true,
      "store": [   // <-- the original store
        "a",
        "b",
        "c",
        "d",
        "e",
        "f",
        "g"
      ],
      "start": 0,
      "length": 7
    }
  ],
  "length": 7
}
spell = {
  "segments": [
    {
      "mutableAfter": null,
      "extendable": false,
      "store": <<< reference to original store >>>,
      "start": 0,
      "length": 2
    },
    {
      "mutableAfter": -1,
      "extendable": true,
      "store": [
        "r",
        "a"
      ],
      "start": 0,
      "length": 2
    },
    {
      "mutableAfter": null,
      "extendable": false,
      "store": <<< reference to original store >>>,
      "start": 2,
      "length": 2
    }
  ],
  "length": 6
}

So, it's useful if you want to create a giant array and then make a bunch of forks of it without storing a giant array for every fork. You just store a bunch of little arrays with the changed segments of the fork.

There are bunch of methods for iterating through the list:

spell.forEach(function(letter) {
  console.log(letter)
})

spell.length
// returns 11, the length of abracadabra

spell.get(2)
// returns "r", the third letter of abracadabra

spell.find("r")
// returns 2

var codes = spell.map(function(letter) {
  return letter.charCodeAt(0)
})

You can also search for an item and splice relative to it:

alphabet.spliceRelativeTo("g", "after", 0, "h")
// alphabet is now ["a", "b", "c", "d", "e", "f", "g", "h"]

Why?

You can use it to implement undo without keeping a full copy of your data at every save point.

You can also have multiple users making small changes to a document without creating a full copy for each user.

Isn't this just an immutable array?

Well... sorta.

Why not use immutable.js

Immutable.js makes a new data structure on every write. So if you call set 3 times, you get 3 references to unique data structures.

ForkableList only forks the data structure when you explicitly fork. In between forks you can set and splice as many times as you want. It will mutate the underlying segments as long as doing so doesn't modify any of the forks. So pushing 20 items to a list just gives you a 20 item array with a little packaging around it.

Immutable.js is also built of like 100 files and written in ES6, so you have to transpile it to use it in the browser. ForkableList is less than 400 lines of plain old ES5 in a single file. If you want to understand what it's doing and how it performs, you just read that one file.

ForkableList also doesn't let you do too much crazy stuff like deep merging, so you can be reasonably sure of its performance characteristics.

Won't that still take up a lot of memory if you have lots of discontinuous segments?

Yes, if you make a a lot of discontinuous modifications, like changing every other item, and then fork that X times, it will have to make X arrays that have one reference to each segment. You may want to snapshot arrays if that is a common case for your data:

var mixedCase = alphabet.fork()
mixedCase.set(1, "B")
mixedCase.set(3, "D")
mixedCase.set(5, "F")

mixedCase.join("")
// returns "aBcDeFg"

This list has 4 segments: a reference to the original abcdefg plus three new segments for the capital letters. If we forked it three times, we'd have three lists with four segments each. If we make a snapshot we have three lists with just one reference to the same segment, using just a little bookkeeping memory:

var clean = forkableList(abracadabra.values())
var fork1 = clean.fork()
var fork2 = fork1.fork()
var fork3 = fork2.fork()

SWEET! IMMUTABLE ALL THE THINGS!! AMIRITE?

No, you are wrong. No pattern should be used everywhere, and people who treat immutability like a religion are destined to write bad code. ForkableList is intentionally a blend of mutable and immutable ideas, because it allows you to get fast write performance when changing multiple items in a row while also allowing you to keep multiple references to state.

ForkableList is written this way because I adhere to a different religion: Data Driven Programming. We start by asking, what is the minimum number of operations we must do. We write code that makes it easy to do just those operations, not code which arbitrarily snapshots every action in order to present a "pure" abstraction.