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

immerhin

v0.10.0

Published

Send patches around to keep the system in sync.

Downloads

1,595

Readme

The core idea is to use patches to keep the UI in sync between client and server, multiple clients, or multiple windows.

It uses Immer as an interface for state mutations and provides a convenient way to group mutations into a single transaction, and enables undo/redo out of the box.

Play with it on Codesandbox

Read the article

Features

  1. Sync application state using patches
  2. Get undo/redo for free
  3. Sync to the server
  4. Server agnostic
  5. State management libraries agnostic (a container interface)
  6. Small bundle size
  7. Sync between iframes (not implemented yet)
  8. Sync between tabs (not implemented yet)
  9. Resolve conflicts (not implemented yet)
  10. Provide server handler (not implemented yet)

Example

import store, { sync } from "immerhin";

// Create containers for each state. Sync engine only cares that the result has a "get()" and a "set(newValue)"
const container1 = atom(initialValue);
const container2 = atom(initialValue);

// - Explicitely enable containers for transactions
// - Define a namespace for each container, so that server knows which object it has to patch.
store.register("container1", container1);
store.register("container2", container2);

// Creating the actual transaction that will:
// - generate patches
// - update states
// - inform all subscribers
// - register a transaction for potential undo/redo and sync calls
store.createTransaction(
  [container1, container2, ...rest],
  (value1, value2, ...rest) => {
    mutateValue(value1);
    mutateValue(value2);
    // ...
  }
);

// Setup periodic sync with a fetch, or do this with Websocket
setInterval(async () => {
  const entries = sync();
  await fetch("/patch", { method: "POST", payload: JSON.stringify(entries) });
}, 1000);

// Undo/redo

store.undo();
store.redo();

How it works

Containers

A container is an interface that implements .get() and .set(value) methods so that a value can be updated and propagated to all consumers.

You can use anything to create containers, it could be a Redux store, could be an observable, a nano state or nanostores.

You can use the same container instance to subscribe to the changes across the entire application.

Example using nano state:

import { atom } from "nanostores";
import { useStore } from "@nanostores/react";
const myContainer = atom(initialValue);

// I can call a set from anywhere
myContainer.set(newValue);

// I can subscribe to updates in React
const Component = () => {
  const value = useStore(myContainer);
};

Container registration

We register containers for two reasons:

  1. To define a namespace for each container so that whoever consumes the changes knows which object to apply the patches to.
  2. Ensure that the container was intentionally registered to be synced to the server and be part of undo/redo transactions. You may not want this for every container since you can use them for ephemeral states.

Example

store.register("myName", myContainer);

Creating a transaction

A transaction is a set of changes applied to a set of states. When you apply changes to the states inside a transaction, you are essentially telling the engine which changes are associated with the same user action so that undo/redo can use that as a single step to work with.

A call into store.createTransaction()does all of this:

  • generate patches (using Immer)
  • update states and inform all subscribers (by calling container.set(newValue))
  • register a transaction for potential undo/redo and calls

Example

store.createTransaction(
  [container1, container2, ...rest],
  (value1, value2, ...rest) => {
    mutateValue(value1);
    mutateValue(value2);
    // ...
  }
);

Undo/redo

Calling undo() and redo() functions will essentially apply the right patch for the value and dispatch the update.

Sync

The sync() function returns you all changes queued up for a sync since the last call. With the return from sync(), you can do anything you want, for example, send it to your server.

Example

// Setup periodic sync with a fetch, or do this with Websocket
setInterval(async () => {
  const entries = sync();
  await fetch("/patch", { method: "POST", payload: JSON.stringify(entries) });
}, 1000);

Example entries:

[
  {
    "transactionId": "6243062b469f516835327f65",
    "changes": [
      {
        "namespace": "root",
        "patches": [
          {
            "op": "replace",
            "path": ["children", 1],
            "value": {
              "component": "Box",
              "id": "6241f55791596f2467df9c2a",
              "style": {},
              "children": []
            }
          },
          {
            "op": "replace",
            "path": ["children", 2],
            "value": {
              "component": "Box",
              "id": "6241f55a91596f2467df9c36",
              "style": {},
              "children": []
            }
          },
          {
            "op": "replace",
            "path": ["children", "length"],
            "value": 3
          }
        ]
      }
    ]
  }
]

Create a new store

If you want to have multiple separate undoable states, create a separate store for each. They add to the same sync queue in the end.

import { Store } from "immerhin";

const store = new Store();