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
Maintainers
Readme
snapman
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 with2
.
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]