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

proxy-indexer

v0.3.2

Published

Create self-updating, easy to use in-memory indexes.

Downloads

2,456

Readme

proxy-indexer

Proxy-indexer allows you to easily index collections of mutable objects based on their own mutable properties.

While this is relatively easy to implement for immutable properties, it is challenging to keep an index up to date when it is based on a mutable property. Consider, you want to look up a set of orders based on their order status. However, their order status frequently changes. Proxy-indexer makes this process transparent via the following API:

import { createIndexes } from 'proxy-indexer';

const [
  { 
    hash: { statusIndex: orderStatusIndex } 
  }, 
  captureOrder
] = createIndexes({
  hash: {targetProperties: ['status']}, 
});

const exampleOrder = captureOrder({
  orderId: '123abc',
  status: 'PLACED',
});

const placedOrders = orderStatusIndex.get('PLACED');
console.log(exampleOrder === placedOrders.values().next().value); // true

exampleOrder.status = 'SHIPPED';

console.log(placedOrders.size); // 0

const shippedOrders = orderStatusIndex.get('SHIPPED');
console.log(exampleOrder === shippedOrders.values().next().value); // true

In the above example you will notice that the size of the placedOrders set reduced from 1 to 0 as soon as we updated the target property (status) of our example order.

When you have thousands of objects and need to look them up by a particular property, this indexing method is more than 1000x times faster than the alternative -- iterating and filtering.

const [
  {
    hash: { statusIndex: orderStatusIndex }
  },
  captureOrder
] = createIndexes({
  hash: {targetProperties: ['status']},
});

const statusMap = { 0: 'PLACED', 1: 'SHIPPED', 2: 'DELIVERED', 3: 'CANCELLED'};
const allObjects = Array.from(
  { length: 125000 }, 
  (_, i) => captureOrder({id: i, status: statusMap[i % 4] })
);

// Let's find all shipped orders the old fashioned way
const shippedOld = Object.values(allObjects).filter(({status}) => status === 'SHIPPED');
// ~4ms

// And the new way
const shipped = orderStatusIndex.get('SHIPPED');
// ~0.002ms

Not only is the new way much faster, but shipped will always be up-to-date, while shippedOld will contain incorrect records once a shipped order is updated to 'DELIVERED' -- the filtered view has to be recalculated each time it is used.

The following test run was performed on a Macbook Air with an M2 CPU and 16GB RAM:

  README.md examples
    ✔ returns indexed objects
    ✔ updates indexes when values change
    Performance tests
Vanilla lookup: 4.186499834060669ms
Indexed lookup: 0.0023751258850097656ms
Index lookups are 1763 times faster
      ✔ looks up objects more than 1000x faster
Vanilla update: 0.0009169578552246094ms
Indexed update: 0.001291036605834961ms
Updates are 1.41 times slower
      ✔ updates are not more than twice as slow


  4 passing (9ms)

Basic usage

Using an index has two steps:

  • Instantiating the index with a list of indexed properties
  • Giving it control of an object you want to index via the captureObject command returned in the tuple following instantiation

Let's examine the first step now:

import { createIndexes } from 'proxy-indexer';

type Customer = {
  name: string;
  country: 'US' | 'AU' | 'NZ' | 'UK' | 'FR';
  status: 'ACTIVE' | 'PENDING' | 'BANNED' | 'DEACTIVATED';
}

const [
  {
    hash: { statusIndex, countryIndex },
  }, 
  captureCustomer
] = createIndexes<Customer>({
  hash: { targetProperties: ['status', 'country'] },
}); 

In the above example we instantiate an index for our customer entity, with indexes on country and status. The createIndexes function returns a tuple containing:

  • An object containing the indexes, each as a property keyed by '${targetProperty}Index'
  • A capture function

Using the capture function correctly is critical to proxy-indexer usage. The capture function takes an object and returns a proxied version of the same object. This allows the index to trap or "spy on" property setting operations. This means that for proxy-indexer to work, all subsequent calls to the indexed object must be via the proxy, not the original object.

The best way to ensure there is no access to the original object is to capture it at instantiation:

const createCustomer = (name: string, country: 'US' | 'AU' | 'NZ' | 'UK' | 'FR'): Customer => {
  return captureCustomer({
    name,
    country,
    status: 'PENDING'
  });
}

Unique indexes that enforce one object per property

What if we wanted to enforce that no two objects have the same property, and look them up by that property? We can do that with a unique index.

const [{ 
    hash: {statusIndex}, 
    unique: {orderIdIndex} 
  }, 
  captureOrder
] = createIndexes({
  hash: {targetProperties: ['status']},
  unique: {targetProperties: ['orderId']}
});

const exampleOrder = captureOrder({
  orderId: '123abc',
  status: 'PLACED',
  cost: 400
});

const order = orderIdUniqueIndex.get('123abc')

const duplicateOrder = captureOrder({
  orderId: '123abc',
  status: 'SHIPPED',
  cost: 200
}); // throws UniqueConstraintViolation!

const updatedDuplicate = captureOrder({
  orderId: '456zyx',
  status: 'SHIPPED',
  cost: 200
});

updatedDuplicate.orderId = '123abc'; // throws UniqueConstraintViolation!

This is useful when an object has more than one identifier, such as an object that has an ID from a third party system.

Deleting objects

Before dereferencing an object in your application, you must call delete on the object. This deletes it from all indexes. Proxy indexer adds the delete method to every captured object:


const exampleOrder = captureOrder({
  orderId: '123abc',
  status: 'PLACED',
  cost: 400
});

exampleOrder.deleteFromIndexes()