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

sinks

v3.1.1

Published

Tools for object sync (get it?!), validation, diffing, and immutable deep setting

Downloads

8,217

Readme

sinks

A set of simple utilities for defining, validating, updating, diffing, and ultimately syncing (get it?!) state stored as simple objects and arrays.

Small enough for use in client-side code < 2kb. In fact, it's very handy if you're looking for a way to enforce more structure in redux reducers.

Enables validation (optional), deep immutable setting of values, and efficient diffing.

What does it do?

It was designed to be part of a system where data stored as simple objects needs to be synchronized between servers and clients yet allow for disconnected changes and caching state locally then synchronizing it again when reconnected.

Basic use cases

  1. You have a large object and a modified version of that large object, you want them to be the same, but you don't want to send the entire new object.

  2. Defining/validating/updating objects stored in redux reducers. Updates are always immutable!

  3. You want to define, at a high-level, a simple object shape where all values are optional but any values have to match defined shape and types.

  4. You want to try merging two object not knowing if they have conflicting changes or not. Giving you the information about what properties are in conflict.

Main exports

getChanges(originalObject, finalState, {includeDeletions: true, ignoredKeys: []})

This will return an object containing changes that can be applied to another object using updateObject(). If there are no changes, returns null.

You it takes an optional options object where you can opt out of including deletions and you can specify a list of top-level object keys to ignore.

updateObject(currentObject, changes)

This returns an updated object with changes applied. These changes need to be structured as an object of paths to update.

Example of updating a nested item:

const obj1 = {
  name: 'Henrik',
}

const updatedObject = updateObject(obj1, {
  'favoriteColors.foo.name': 'yellow',
})

console.log(updatedObject)
// {
//   name: 'Henrik',
//   favoriteColors: {
//     foo: {
//       name: 'yellow',
//     },
//   },
// }

Example of deleting a value:

const obj1 = {
  name: 'Henrik',
  something: {
    foo: 'cool',
  },
}

// setting a value to `null` deletes it
const updatedObject = updateObject(obj1, {
  'something.foo': null,
})

// Note: empty objects are removed
console.log(updatedObject)
// {
//   name: 'Henrik',
// }

Example of updating item in an array

const obj1 = {
  name: 'Henrik',
  myStuff: [{ id: 'thing', description: 'pizza' }],
}

// if an array already exists in the first object
// you can just provide an update that uses the
// index as a number in your update path:
const updated = updateObject(obj1, {
  'myStuff.0.description': 'skis',
})

console.log(updated)
// {
//   name: 'Henrik',
//   myStuff: [{ id: 'thing', description: 'skis' }],
// }

Updating by array index doesn't work if the array doesn't exist in the object. But we can explicitly specify that an item in an array is an index by putting square brackets around it:

// note that missing things are created
// but we can't know whether to create an
// array or an object unless you tell it with `[]`
const obj1 = {
  name: 'Henrik',
}

// The square brackets tells the updater to create an array
// instead of an object with a key named '0'
const updated = updateObject(obj1, {
  'myStuff.[0].description': 'skis',
})

console.log(updated)
// {
//   name: 'Henrik',
//   myStuff: [{ id: 'thing', description: 'skis' }],
// }

// if you *DON'T* supply the brackets this would happen
console.log(
  updateObject(obj1, {
    'myStuff.0.description': 'skis',
  })
)
// {
//   name: 'Henrik',
//   myStuff: {
//     0: {
//       description: 'skis',
//     },
//   },
// }

You don't have to supply a whole path if you want to set an object:

const obj1 = {
  name: 'Henrik',
}

// can just supply an object
console.log(
  updateObject(obj1, {
    other: {
      nested: 'thing',
    },
  })
)
// {
//   name: 'Henrik',
//   other: {
//     nested: 'thing'
//   }
// }

Empty objects and arrays and null values are automatically removed.

const obj1 = {
  name: 'Henrik',
}

// even if you set a deeply nested set of
// objects and the very last value is empty
// the whole chain of empty stuff is removed
console.log(
  updateObject(obj1, {
    other: {
      nested: {
        // note final value is an empty array
        // once this is removed, the parent ones
        // will be empty. So the whole thing is
        // removed.
        hi: [],
      },
    },
  })
)
// {
//   name: 'Henrik',
// }

setValue(obj1, keyPath, updatedValue)

This is the single-key update version of updateObject in fact, this what updateObject calls for each key you provide in the update object.

mergeObjects(obj1, obj2)

This will get additive changes (not deletions) from each object compared to the other, and try to build a shared object of merged changes.

It returns an object with two properties:

  1. updated: the new merged object
  2. conflicts: this property only exists if there are conflicts. These conflicts are an object keyed by conflicting key name and containing an array of original and new values for that key.
const obj1 = {
  name: 'bob',
  favoriteColor: 'blue',
}

const obj2 = {
  name: 'sue',
  age: 28,
}

const { updated, conflicts } = mergeObjects(obj1, obj2)

console.log(updated)
// {
//   name: 'bob', original name (no update was made)
//   age: 28, no conflict here, so age was applied from obj2
//   favoriteColor: 'blue', (no conflict so favoriteColor was
//     kept from first, notice it was *NOT* deleted.
// }
console.log(conflicts)
// {
//   name: ['bob', 'sue'] // value from first listed first
// }

buildDefinition(definitionObject, fnsObject[optional]) Using validation and object definitions

You can optionally choose to create a definition that describes valid shape of the object. Doing this can give you some comfort at runtime that you're not getting unexpected values.

buildDefinition returns an object with the following methods:

  • definition.validate(object) takes object to validate,
  • definition.setValue(startingObject, keyPath, newValue, shouldValidate [defaults to true]): ,
  • definition.update(startingObject, updatesObject, shouldValidate [defaults to true])
  • definition.merge(startingObject, otherObject, shouldValidate [defaults to true])

You define an object as follows. Please note that the "types" are just strings. These get mapped to functions you can supply as a second argument to buildDefinition.

If you don't supply one, we have a simple default set of very basic type checks out of the box. Please see src/basic-types.js. These are also exported as import { basicTypes } from 'sinks' so they can easily be extended.

import { buildDefinition } from 'sinks'

// example definition
const bareDefinition = {
  // meta
  lastChanged: 'timestamp',
  lastSaved: 'timestamp',
  created: 'timestamp',

  // provider
  sedationProviderName: 'str',
  surgeonName: 'str',
  recorderName: 'str',
  office: 'str',

  // vitals types
  // the "{}" here allows for any keyed name.
  // This is very important when building state objects with unknown keys
  'trackedVitalTypes.{}.id': 'str',
  'trackedVitalTypes.{}.selected': 'bool',
  'trackedVitalTypes.{}.hasReceivedAutoValue': 'bool',

  // You can also define arrays
  medications: 'arr',
  'medications.[].id': 'str',
  'medications.[].name': 'str',
}

const definition = buildDefinition(bareDefinition)

// note you still have to supply the object each time
// it does not maintain state internally!
const startingObject = {}

// this will throw because it's not defined
try {
  const newObject = definition.setValue(
    startingObject,
    'somethingSilly',
    'blah'
  )
} catch (e) {
  // this will throw!!
}

// The same thing will not throw if we tell it not to validate
const finalObject = definition.setValue(
  startingObject,
  'somethingSilly',
  'blah',
  false // here we turn *off* validation
)

// NOTE: these are immutable sets!
// any object in the chain that has been edited
// has been copied and replaced.
console.log(startingObject === finalObject) // false

Running tests

npm test

install

npm install sinks

Change log

  • 3.1.1: Now exporting simpleObjectDeepEqual as part of main export.
  • 3.1.0: Added simpleObjectDeepEqual utility for lightweight object comparisons of simple objects.
  • 3.0.4: Added other test for getChanges to ensure it handle nested objects with integer keys correctly.
  • 3.0.3: Fixed bug where getChanges not handle objects with integer keys correctly.
  • 3.0.2: Fix bad prepublish script.
  • 3.0.1: Removing unnecessary conditional check.
  • 3.0.0: getChanges now returns individual key path updates, this is important for simultaneous changes of nested objects. Technically, this should not be a breaking change unless you manually modify or somehow deal with the changes object. But since it changes the shape of something returned by public API I decided to bump the major version.
  • 2.0.0: Now recursively removes all keys with values {}, [], or null at the end of all set/update operations.
  • 1.0.0: getChanges now takes an options object instead of just a boolean and that option option now can take a ignoredKeys: [] option to ignore changes to specified top-level keys.
  • 0.0.1: First public release.

credits

If you like this follow @HenrikJoreteg on twitter.

Props to Jason Miller for dlv (a dependency) and Frank Wilkerson for clean-set which I modified and included here (along with his MIT license).

license

MIT