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

snapman

v1.0.10

Published

Snapshots manager for taking snapshots where the values are automatically deep cloned when taken. Track timeline. And can be used as events source. Navigate snapshots and there timeline. Can be used for testing and any other application.

Downloads

26

Readme

snapman

snapman.js banner

Snapshots manager for taking snapshots where the values are automatically deep cloned when taken. Track timeline. And can be used as events source. Navigate snapshots and there timeline. Can be used for testing and any other application.

See examples at the end for usage with jest. Here some articles and content showing that.

Installation

npm install snapman
pnpm add snapman
pnpm add snapman

For usage with browser you can use bundlers like webpack or vite or rollup.

Otherwise you can use the CDN link:

<!-- Latest version -->
<script src="https://unpkg.com/snapman"></script>

<!-- Specific: -->
<script src="https://unpkg.com/[email protected]"></script>

And also you can use the already bundled umd index.umd.js file in releases.

The library is exposed as SnapmanJs

const sm = new SnapmanJs.Snapman();

Usage

Construction

interface ITypeDef {
  [id: string]: {
    id: string;
  };
}

const s = new Snapman<ITypeDef>();

Taking snapshots

public snap<TId extends string>(id: string, val?: TMapDef[TId]): this
for (let i = 0; i < 5; i++) {
  s.snap(`category1:snap-${i}`, {
    id: `snap-${i}`,
  });
}

for (let i = 0; i < 3; i++) {
  s.snap(`category2:snap-${i}`, {
    id: `snap-${i}`,
  });
}

That is 8 snapshots taken

Timeline

For the example above, the timeline is

// type: Snap[]
[Snap-category1:snap-1, ...., Snap-category1:snap-4, Snap-category2:snap-0, ..., Snap-category2:snap-3]

to access a timeline

sm.getSnapTimeLine()

Accessing snapshots

// by id
sm.getSnap('category1:snap-4')

// by index in the timeline, start from zero
sm.getSnapAtIndex(index)

// Search and get the snapshots from the timeline that start with the part of the id. (start)
const snaps = sm.getSnaps('category1');
// ---> Will get all the snapshots of an id equal to `category1` or starting by `category1:` (the `:` delimiter is used)
const snaps = sm.getSnaps('tegory1');
// ---> will return an empty array. getSnaps() doesn't match substring but only t he one that start from the start.
// You can use searchSnaps() instead of you want to match against just any substring
const snaps = sm.getSnaps('category');
// ---> Will return an empty array as well. because no `category` id or an id that start with `category:` exist.


// Searching for snapshots matching against id
// ---- substring
sm.searchSnaps('category1')
// return all snapshots that category1 is a substring of there id
// ---- regex
sm.searchSnaps(/category1/)
// return all snapshots that the regex /category1/ match there id

// Snap.next() Snap.previous() and navigating timeline
const snap = sm.getSnapAtIndex(index)
const nextSnap = snap.next() // access the next snap in the timeline
const prevSnap = snap.previous() // access the previous snap in the timeline

Multiple snapshots of same id

For convenience and ease of use. If you are in a case where you need to take snapshots of the same event (id). Snapman help with that in the following way:

If we take multiple snaps with same id like in:

for (let i = 1; i < 5; i++) {
  sm2.snap('sameId', {
    id: `snap-${i}`,
  });
}

That would do the following:

  • register first snapshot as sameId
  • The next ones as sameId:{index} while index gonna start with 2.

Meaning sameId, sameId:2, sameId:3 ... you got it.

  • the first snapshot sameId. Is added to the timeline and added to the map.
  • The next ones sameId:2, sameId:3 ... will be added to the timeline in the order they were taken. So if we do:
sm.snap('some', {})
sm.snap('sameId', {})
sm.snap('someOther', {})
sm.snap('sameId', {})
sm.snap('sameId', {})
sm.snap('someOther', {})

timeline ==>

some, someId, someOther, sameId:2, sameId:3, someOther:2
  • To access the elements of same id, we use sm.getSnapsOfId()
for (let i = 1; i < 5; i++) {
  sm.snap('sameId', {
    id: `snap-${i}`,
  });
}

const snaps = sm.getSnapsOfId('sameId')
// Snap[] -> [sameId, sameId:2, sameId:3, sameId:4]
  • We can access a one exactly directly by sm.getSnap(id)
const snap3 = sm.getSnap('sameId:3')
  • If we take a snapshot like:
// sameId:3 already exists
sm.snap('sameId:3', {})
sm.snap('sameId:3', {})

=> This will create sameId:3:2, sameId:3:3. Making the totality of sameId:3, sameId:3:2, sameId:3:3.

const snaps = sm.getSnapsOfId('sameId:3')
// Snap[] -> [sameId:3, sameId:3:2, sameId:3:3]

Do that only when you need it.

Also given that the above is done.

const snaps = sm.getSnapsOfId('sameId')
// Snap[] -> [sameId, sameId:2, sameId:3, sameId:4]

Will still return the same as before. As getSnapsOfId() will return the snapshots that were taken by the same id when using snap().

And surely to access just all in case you ever need. use:

const snaps = sm.getSnaps('sameId');
// Snap[] -> [sameId, sameId:2, sameId:3, sameId:4, sameId:3:2, sameId:3:3]

And it follows the timeline, in matter of order.

Snap object and accessing values

  const snap = s.getSnapAtIndex(3);
  
  // accessing id of the snap
  snap.getId()
  snap.id() // alias

  // accessing the value of the snap, (safely cloned at the time the snapshot was created)
  snap.getVal()
  snap.val() // alias

  // accessing the index of the snapshot in the timeline
  snap.getTimelineIndex()
  snap.tIndex() // alias

API and examples

Funny enough one of the main usage intended for snapman is testing.

First get to know the api through the test file of snapman itself.

import { Snapman } from './index.js';
import { Snap } from '/Snap/index.js';
import { last } from '/Utils/helpers.js';

interface ITypeDef {
  [id: string]: {
    id: string;
  };
}

const sm = new Snapman<ITypeDef>();

for (let i = 0; i < 5; i++) {
  sm.snap(`category1:snap-${i}`, {
    id: `snap-${i}`,
  });
}

for (let i = 0; i < 3; i++) {
  sm.snap(`category2:snap-${i}`, {
    id: `snap-${i}`,
  });
}

const sm2 = new Snapman<ITypeDef>();

for (let i = 0; i < 3; i++) {
  sm2.snap(`before-sameId:${i}`, {
    id: `snap-${i}`,
  });
}

for (let i = 1; i < 5; i++) {
  sm2.snap('sameId', {
    id: `snap-${i}`,
  });
}

sm2.snap('after-sameId', { id: 'afterSameId' });

test('snap() and Timeline is working well', () => {
  expect(sm.getSnapsCount()).toBe(8);
  expect(sm.getSnapTimeLine().map((snap) => snap.id())).toEqual(
    Array(8)
      .fill(0)
      .map((_, i) => {
        if (i < 5) {
          return `category1:snap-${i}`;
        }
        return `category2:snap-${i - 5}`;
      }),
  );
  expect(sm.getSnapAtIndex(4).id()).toBe('category1:snap-4');
  expect(sm.getSnapAtIndex(2).next(2)?.id()).toBe('category1:snap-4');

  let snap = sm.getSnapAtIndex(4);
  for (let i = 5; i < 8; i++) {
    snap = snap.next() as Snap;
    expect(snap.id()).toBe(`category2:snap-${i - 5}`);
  }
  expect(snap.next()).toBe(undefined);
});

test('getSnap(), getVal()', () => {
  expect(sm.getSnap('category1:snap-4').val().id).toBe('snap-4');
  expect(sm.getSnap('category1:snap-3').getVal().id).toBe('snap-3');
});

test('getSnapAtIndex()', () => {
  for (let i = 0; i < 5; i++) {
    expect(sm.getSnapAtIndex(i).id()).toBe(`category1:snap-${i}`);
  }
});

test('searchSnaps() substring', () => {
  expect(sm.searchSnaps('category1').map((snap) => snap.id())).toEqual(
    Array(5)
      .fill(0)
      .map((_, i) => `category1:snap-${i}`),
  );
});

test('searchSnaps() regex', () => {
  expect(sm.searchSnaps(/category1/).map((snap) => snap.id())).toEqual(
    Array(5)
      .fill(0)
      .map((_, i) => `category1:snap-${i}`),
  );
});

test('Snap api works well for accessor', () => {
  const snap = sm.getSnapAtIndex(3);
  expect(snap.id()).toBe('category1:snap-3');
  expect(snap.getId()).toBe('category1:snap-3');
  expect(snap.getVal().id).toBe('snap-3');
  expect(snap.val().id).toBe('snap-3');
  expect(snap.getTimelineIndex()).toBe(3);
  expect(snap.tIndex()).toBe(3);
});

test('previous(), next() navigation', () => {
  /**
   * next()
   */
  let snap = sm.getSnapAtIndex(4);
  for (let i = 5; i < 8; i++) {
    snap = snap.next() as Snap;
    expect(snap.id()).toBe(`category2:snap-${i - 5}`);
  }
  expect(snap.next()).toBe(undefined);

  snap = sm.getSnapAtIndex(5);
  for (let i = 4; i >= 0; i--) {
    snap = snap.previous() as Snap;
    expect(snap.id()).toBe(`category1:snap-${i}`);
  }
  expect(snap.previous()).toBe(undefined);
});

test('Testing same id snap taking and getter (getSnapsOfId())', () => {
  const snaps = sm2.getSnapsOfId('sameId');
  snaps.forEach((snap, index) => {
    let id = 'sameId';
    if (index > 0) {
      id += `:${index + 1}`;
    }
    expect(snap.id()).toBe(id);
    expect(snap.val().id).toBe(`snap-${index + 1}`);
  });
  const lastSnap = last(snaps);
  const nextSnapInTimeLine = lastSnap.next();
  expect(nextSnapInTimeLine?.id()).toBe('after-sameId');
  expect(nextSnapInTimeLine?.val().id).toBe('afterSameId');

  let backSnap: Snap = snaps[0];
  for (let i = 2; i >= 0; i--) {
    backSnap = backSnap.previous()!;
    expect(backSnap.id()).toBe(`before-sameId:${i}`);
  }
  expect(backSnap.previous()).toBe(undefined);
});

test('getting snaps using getSnaps() and from start matching', () => {
  {
    const snaps = sm.getSnaps('category1');
    for (let i = 0; i < 5; i++) {
      expect(snaps[i].getId()).toBe(`category1:snap-${i}`);
    }
  }

  {
    // testing that it still work if : was included
    const snaps = sm.getSnaps('category1:');
    for (let i = 0; i < 5; i++) {
      expect(snaps[i].getId()).toBe(`category1:snap-${i}`);
    }
  }

  {
    const snaps = sm.getSnaps('category');
    expect(snaps.length).toBe(0);
  }

  {
    const snaps = sm.getSnaps('ategory');
    expect(snaps.length).toBe(0);
  }

  {
    const snaps = sm2.getSnaps('sameId');
    expect(snaps[0].getId()).toBe('sameId');

    for (let i = 1; i < 4; i++) {
      expect(snaps[i].getId()).toBe(`sameId:${i + 1}`);
    }
  }

  // testing sub category
  {
    const _sm = new Snapman<ITypeDef>();
    _sm.snap('experience1:target1', { id: 'target1:1' });
    _sm.snap('experience1:target1', { id: 'target1:2' });
    _sm.snap('experience1:target1', { id: 'target1:3' });
    _sm.snap('experience1:target2', { id: 'target2:1' });
    _sm.snap('experience1:target2', { id: 'target2:2' });
    _sm.snap('experience1:target2', { id: 'target2:3' });

    const experienceSnaps = _sm.getSnaps('experience1');
    for (let i = 0; i < 6; i++) {
      expect(experienceSnaps[i].getId()).toBe(
        `experience1:target${i < 3 ? 1 : 2}${
          i === 0 || i === 3 ? '' : `:${(i % 3) + 1}`
        }`,
      );
    }

    const target1Snaps = _sm.getSnaps('experience1:target1');
    for (let i = 0; i < 3; i++) {
      expect(target1Snaps[i].getId()).toBe(
        `experience1:target1${i === 0 ? '' : `:${i + 1}`}`,
      );
    }

    const target2Snaps = _sm.getSnaps('experience1:target2');
    for (let i = 0; i < 3; i++) {
      expect(target2Snaps[i].getId()).toBe(
        `experience1:target2${i === 0 ? '' : `:${i + 1}`}`,
      );
    }

    // testing when there is no such sub category and it's just a substring
    const noMatchSnaps = _sm.getSnaps('experience1:target');
    expect(noMatchSnaps.length).toBe(0);
  }
});
test('getting sameId snapshots using getSnap()', () => {
  for (let i = 2; i < 5; i++) {
    const snap = sm2.getSnap(`sameId:${i}`);
    expect(snap).toBeTruthy();
    expect(snap.id()).toBe(`sameId:${i}`);
  }
});

test('Taking extra snaps on the sameId auto incremented snaps', () => {
  const _sm = new Snapman<ITypeDef>();
  _sm.snap('sameId', { id: 'sameId-1' });
  _sm.snap('sameId', { id: 'sameId-2' });
  _sm.snap('sameId', { id: 'sameId-3' });

  _sm.snap('sameId:2', { id: 'sameId-1-2' });
  _sm.snap('sameId:2', { id: 'sameId-1-3' });
  _sm.snap('sameId:2', { id: 'sameId-1-4' });

  const sameIdSnaps = _sm.getSnapsOfId('sameId');
  expect(sameIdSnaps.length).toBe(3);
  for (let i = 0; i < 3; i++) {
    const secondPart = i === 0 ? '' : `:${i + 1}`;
    expect(sameIdSnaps[i].id()).toBe(`sameId${secondPart}`);
  }

  const sameId2Snaps = _sm.getSnapsOfId('sameId:2');
  expect(sameId2Snaps.length).toBe(4);
  for (let i = 0; i < 4; i++) {
    const secondPart = i === 0 ? '' : `:${i + 1}`;
    expect(sameId2Snaps[i].id()).toBe(`sameId:2${secondPart}`);
  }

  expect(_sm.getSnaps('sameId').length).toBe(6);
  expect(_sm.getSnaps('sameId:2').length).toBe(4);
});

Usage in real tests with "run experiences first, tests after" pattern

If you have something that works through time. Like for instance a client. ...

If you do e2e testing like testing something like laravel-mix or laravel-mix-glob or webpack ... Something cli based. You account for output ...

A great pattern is to create experiments and run them all at first. While at it, you collect all sort of relevant events and there data. And then we write the test by consuming and testing against the experiments collected data. Kind like with event sourcing.

Snapman was created to help with that process. Taking snapshot. Automatically the values are deeply cloned. And a timeline is created and managed. You can access any snapshot. And you can navigate the timeline and in different ways. And you can too search as well.

And by using the right ids structure. You can also categorize events that are alike. And group them.

id = `category1:sub2:someEvent1`

s.searchSnaps(/^category1/) // would give all the events of category1
s.searchSnaps(/^category1\:sub2/) // would give all the events of category1:sub2

Example of a real experiment based testing:

[to be added]