Skip to content

Commit

Permalink
feat: add replace utility function
Browse files Browse the repository at this point in the history
the replace method is a function to replace a snapshot at a specified history index
  • Loading branch information
lwhiteley committed Jan 7, 2024
1 parent f9bc8ec commit 65b63e3
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/history-utility/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export default function App() {
canUndo,
canRedo,
getCurrentChangeDate,
getNode,
remove,
replace,
} = useSnapshot(state);

...
Expand Down
130 changes: 130 additions & 0 deletions packages/history-utility/src/__tests__/history-utility.vanilla.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,134 @@ describe('proxyWithHistory: vanilla', () => {
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 2, 3, 4]);
});
});

describe('replace', () => {
it('should replace no items in history when invalid index is provided', async () => {
const state = proxyWithHistory({ count: 0 });

state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();

expect(state.value.count).toEqual(5);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(5);
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 2, 3, 4, 5]);

state.replace(100, { count: 100 });
await Promise.resolve();

expect(state.value.count).toEqual(5);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(5);
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 2, 3, 4, 5]);
});

it('should replace current index as last index in history without increasing history length', async () => {
const state = proxyWithHistory({ count: 0 });

state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();

expect(state.value.count).toEqual(5);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(5);
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 2, 3, 4, 5]);

state.replace(5, { count: 100 });
await Promise.resolve();

expect(state.value.count).toEqual(100);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(5);
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 2, 3, 4, 100]);
});

it('should replace current index when not last index in history without increasing history', async () => {
const state = proxyWithHistory({ count: 0 });

state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.undo();
await Promise.resolve();
state.undo();
await Promise.resolve();
state.undo();
await Promise.resolve();

expect(state.value.count).toEqual(2);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(2);
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 2, 3, 4, 5]);

state.replace(2, { count: 100 });
await Promise.resolve();

expect(state.value.count).toEqual(100);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(2);
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 100, 3, 4, 5]);
});

it('should replace item in history without increasing history', async () => {
const state = proxyWithHistory({ count: 0 });

state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.value.count += 1;
await Promise.resolve();
state.undo();
await Promise.resolve();
state.undo();
await Promise.resolve();
state.undo();
await Promise.resolve();

expect(state.value.count).toEqual(2);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(2);
expect(state.history.nodes.map(mapNumbers)).toEqual([0, 1, 2, 3, 4, 5]);

state.replace(3, { count: 100 });
await Promise.resolve();
state.replace(4, { count: 200 });
await Promise.resolve();

expect(state.value.count).toEqual(2);
expect(state.history.nodes.length).toEqual(6);
expect(state.history.index).toEqual(2);
expect(state.history.nodes.map(mapNumbers)).toEqual([
0, 1, 2, 100, 200, 5,
]);
});
});
});
63 changes: 62 additions & 1 deletion packages/history-utility/src/history-utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
import type { INTERNAL_Snapshot as Snapshot } from 'valtio/vanilla';

export type HistoryNode<T> = {
createdAt: Date;
snapshot: Snapshot<T>;
createdAt: Date;
updatedAt?: Date;
};

export type History<T> = {
Expand Down Expand Up @@ -56,6 +57,8 @@ const deepClone = <T>(value: T): T => {
* - saveHistory: a function to save history
* - getCurrentChangeDate: gets the date of the current change
* - remove: a function to remove a specified history index
* - replace: a function to replace a snapshot at a specified history index
* - getNode: a function to get the node at a specified history index
*
* [Notes]
* - Suspense/promise is not supported.
Expand All @@ -78,10 +81,33 @@ export function proxyWithHistory<V>(initialValue: V, skipSubscribe = false) {
nodes: [],
index: -1,
}),
/**
* getCurrentChangeDate
*
* get the date when a node was entered into history.
*
* @returns date
*/
getCurrentChangeDate: () => {
const node = proxyObject.history.nodes[proxyObject.history.index];
return node?.createdAt;
},
/**
* getNode
*
* utility method to get a history node.
* The snapshot within this node is already cloned and
* will not affect the original value if updated.
*
* @param index
* @returns historyNode
*/
getNode: (index: number) => {
const node = proxyObject.history.nodes[index];
return node
? { ...node, snapshot: proxyObject.clone(node.snapshot) }
: undefined;
},
clone: deepClone,
canUndo: () => proxyObject.history.index > 0,
undo: () => {
Expand Down Expand Up @@ -163,6 +189,41 @@ export function proxyWithHistory<V>(initialValue: V, skipSubscribe = false) {

return node;
},

/**
* replace
*
* utility to replace a value in history. The history
* changes with not be affected, only the value to be replaced.
* If a base value is needed to operate on,
* the `getNode` utility can be used to retrieve
* a cloned historyNode.
*
* Notes:
* - No operations are done on the value provided to this utility.
* - This is an advanced method, please ensure the value provided
* is a snapshot of the same type of the value being tracked.
*
* @param index - index to replace value for
* @param value - the updated snapshot to be stored at the index
*/
replace: (index: number, value: Snapshot<V>) => {
const node = proxyObject.history.nodes[index];
const isCurrentIndex = proxyObject.history.index === index;

if (!node) return;

proxyObject.history.nodes[index] = {
...node,
snapshot: value,
updatedAt: new Date(),
};

if (isCurrentIndex) {
proxyObject.history.wip = value;
proxyObject.value = proxyObject.history.wip as V;
}
},
});

proxyObject.saveHistory();
Expand Down

0 comments on commit 65b63e3

Please sign in to comment.