Skip to content

Commit

Permalink
refactor: improve internal subscription api
Browse files Browse the repository at this point in the history
  • Loading branch information
lwhiteley committed Jan 8, 2024
1 parent c1064c2 commit c4dab09
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 34 deletions.
47 changes: 30 additions & 17 deletions packages/history-utility/docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- [History](modules.md#history)
- [HistoryNode](modules.md#historynode)
- [SkipSubscribeOrCallback](modules.md#skipsubscribeorcallback)

### Functions

Expand All @@ -27,15 +28,16 @@

#### Type declaration

| Name | Type | Description |
| :------ | :----------------------------------------------- | :-------------------------------------------------------------- |
| `index` | `number` | the history index of the current snapshot |
| `nodes` | [`HistoryNode`](modules.md#historynode)\<`T`\>[] | the nodes of the history for each change |
| `wip?` | `Snapshot`\<`T`\> | field for holding sandbox changes; used to avoid infinite loops |
| Name | Type | Description |
| :------------ | :----------------------------------------------- | :-------------------------------------------------------------- |
| `index` | `number` | the history index of the current snapshot |
| `nodes` | [`HistoryNode`](modules.md#historynode)\<`T`\>[] | the nodes of the history for each change |
| `unsubscribe` | `ReturnType`\<typeof `subscribe`\> | a function to stop the internal subscription process |
| `wip?` | `Snapshot`\<`T`\> | field for holding sandbox changes; used to avoid infinite loops |

#### Defined in

[packages/history-utility/src/history-utility.ts:26](https://github.com/valtiojs/valtio-history/blob/86c1430/packages/history-utility/src/history-utility.ts#L26)
[packages/history-utility/src/history-utility.ts:26](https://github.com/valtiojs/valtio-history/blob/30951d0/packages/history-utility/src/history-utility.ts#L26)

---

Expand All @@ -59,22 +61,33 @@

#### Defined in

[packages/history-utility/src/history-utility.ts:10](https://github.com/valtiojs/valtio-history/blob/86c1430/packages/history-utility/src/history-utility.ts#L10)
[packages/history-utility/src/history-utility.ts:10](https://github.com/valtiojs/valtio-history/blob/30951d0/packages/history-utility/src/history-utility.ts#L10)

---

### SkipSubscribeOrCallback

Ƭ **SkipSubscribeOrCallback**: `boolean` \| `SubscribeCallback`

A field to either enable/disable the internal subscribe functionality.
Optionally a callback function can be provided to hook into the
internal subscribe handler.

#### Defined in

[packages/history-utility/src/history-utility.ts:52](https://github.com/valtiojs/valtio-history/blob/30951d0/packages/history-utility/src/history-utility.ts#L52)

## Functions

### proxyWithHistory

**proxyWithHistory**\<`V`\>(`initialValue`, `skipSubscribe?`): `Object`
**proxyWithHistory**\<`V`\>(`initialValue`, `skipSubscribeOrCallback?`): `Object`

This creates a new proxy with history support (ProxyHistoryObject).
It includes following main properties:<br>

- value: any value (does not have to be an object)<br>
- history: an object holding the history of snapshots and other metadata<br>
- history.index: the history index of the current snapshot<br>
- history.nodes: the nodes of the history for each change<br>
- history.wip: field for holding sandbox changes; used to avoid infinite loops<br>
- canUndo: a function to return true if undo is available <br>
- undo: a function to go back history <br>
- canRedo: a function to return true if redo is available <br>
Expand All @@ -97,10 +110,10 @@ Notes: <br>

#### Parameters

| Name | Type | Default value | Description |
| :-------------- | :-------- | :------------ | :---------------------------------------------------------------- |
| `initialValue` | `V` | `undefined` | any object to track |
| `skipSubscribe` | `boolean` | `false` | determines if the internal subscribe behaviour should be skipped. |
| Name | Type | Default value | Description |
| :------------------------ | :-------------------------------------------------------------- | :------------ | :----------------------------------------------------------------------------------------------------------------- |
| `initialValue` | `V` | `undefined` | any object to track |
| `skipSubscribeOrCallback` | [`SkipSubscribeOrCallback`](modules.md#skipsubscribeorcallback) | `false` | determines if the internal subscribe behaviour should be skipped. Optionally, a callback function can be provided. |

#### Returns

Expand All @@ -116,7 +129,7 @@ proxyObject
| `getCurrentChangeDate` | () => `undefined` \| `Date` | get the date when a node was entered into history. |
| `getNode` | (`index`: `number`) => `undefined` \| \{ `createdAt`: `Date` ; `snapshot`: `Snapshot`\<`V`\> ; `updatedAt?`: `Date` } | utility method to get a history node. The snapshot within this node is already cloned and will not affect the original value if updated. |
| `goTo` | (`index`: `number`) => `void` | a function to go to a specific index in history |
| `history` | [`History`](modules.md#history)\<`V`\> & `AsRef` | an object holding the history of snapshots and other metadata <br> - history.index: the history index to the current snapshot <br> - history.nodes: the nodes of the history for each change <br> - history.wip: field for holding sandbox changes; used to avoid infinite loops<br> |
| `history` | [`History`](modules.md#history)\<`V`\> & `AsRef` | an object holding the history of snapshots and other metadata <br> - history.index: the history index to the current snapshot <br> - history.nodes: the nodes of the history for each change <br> - history.wip: field for holding sandbox changes; used to avoid infinite loops <br> - history.unsubscribe: a function to stop the internal subscription process <br> |
| `redo` | () => `void` | a function to go forward in history |
| `remove` | (`index`: `number`) => `undefined` \| [`HistoryNode`](modules.md#historynode)\<`V`\> | The remove method is only invoked when there are more than one nodes and when a valid index is provided. If the current index is removed, An index greater than the current index will be preferred as the next value. |
| `replace` | (`index`: `number`, `value`: `INTERNAL_Snapshot`\<`V`\>) => `void` | utility to replace a value in history. The history changes will 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. <br> <br> Notes: <br> - No operations are done on the value provided to this utility. <br> - This is an advanced method, please ensure the value provided is a snapshot of the same type of the value being tracked. <br> |
Expand All @@ -136,4 +149,4 @@ const state = proxyWithHistory({

#### Defined in

[packages/history-utility/src/history-utility.ts:94](https://github.com/valtiojs/valtio-history/blob/86c1430/packages/history-utility/src/history-utility.ts#L94)
[packages/history-utility/src/history-utility.ts:105](https://github.com/valtiojs/valtio-history/blob/30951d0/packages/history-utility/src/history-utility.ts#L105)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';

import { HistoryNode, proxyWithHistory } from '../history-utility';

Expand All @@ -7,6 +7,10 @@ const mapNumbers = (node: HistoryNode<{ count: number }>) =>

describe('proxyWithHistory: vanilla', () => {
describe('basic', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('should provide basic history functionality', async () => {
const state = proxyWithHistory({ count: 0 });
await Promise.resolve();
Expand All @@ -25,6 +29,27 @@ describe('proxyWithHistory: vanilla', () => {
expect(state.value.count).toEqual(1);
});

it('should call subscribe callback when provided', async () => {
const callback = vi.fn();
const state = proxyWithHistory({ count: 0 }, callback);
await Promise.resolve();
expect(state.value.count).toEqual(0);

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

state.undo();
await Promise.resolve();
expect(state.value.count).toEqual(0);

state.redo();
await Promise.resolve();
expect(state.value.count).toEqual(1);

expect(callback).toHaveBeenCalledTimes(3);
});

it('should provide basic sequential undo functionality', async () => {
const state = proxyWithHistory({ count: 0 });
await Promise.resolve();
Expand Down
49 changes: 33 additions & 16 deletions packages/history-utility/src/history-utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,21 @@ export type History<T> = {
* the history index of the current snapshot
*/
index: number;
/**
* a function to stop the internal subscription process
*/
unsubscribe: ReturnType<typeof subscribe>;
};

type SubscribeOps = Parameters<Parameters<typeof subscribe>[1]>[0];
type SubscribeCallback = (ops: SubscribeOps, historySaved: boolean) => void;
/**
* A field to either enable/disable the internal subscribe functionality.
* Optionally a callback function can be provided to hook into the
* internal subscribe handler.
*/
export type SkipSubscribeOrCallback = boolean | SubscribeCallback;

const isObject = (value: unknown): value is object =>
!!value && typeof value === 'object';

Expand All @@ -64,9 +77,6 @@ const deepClone = <T>(value: T): T => {
* It includes following main properties:<br>
* - value: any value (does not have to be an object)<br>
* - history: an object holding the history of snapshots and other metadata<br>
* - history.index: the history index of the current snapshot<br>
* - history.nodes: the nodes of the history for each change<br>
* - history.wip: field for holding sandbox changes; used to avoid infinite loops<br>
* - canUndo: a function to return true if undo is available <br>
* - undo: a function to go back history <br>
* - canRedo: a function to return true if redo is available <br>
Expand All @@ -82,7 +92,8 @@ const deepClone = <T>(value: T): T => {
* - Suspense/promise is not supported. <br>
*
* @param initialValue - any object to track
* @param skipSubscribe - determines if the internal subscribe behaviour should be skipped.
* @param skipSubscribeOrCallback - determines if the internal subscribe behaviour should be skipped. Optionally,
* a callback function can be provided.
* @returns proxyObject
*
* @example
Expand All @@ -91,7 +102,10 @@ const deepClone = <T>(value: T): T => {
* count: 1,
* })
*/
export function proxyWithHistory<V>(initialValue: V, skipSubscribe = false) {
export function proxyWithHistory<V>(
initialValue: V,
skipSubscribeOrCallback: SkipSubscribeOrCallback = false
) {
const proxyObject = proxy({
/**
* any value to be tracked (does not have to be an object)
Expand All @@ -101,12 +115,14 @@ export function proxyWithHistory<V>(initialValue: V, skipSubscribe = false) {
* an object holding the history of snapshots and other metadata <br>
* - history.index: the history index to the current snapshot <br>
* - history.nodes: the nodes of the history for each change <br>
* - history.wip: field for holding sandbox changes; used to avoid infinite loops<br>
* - history.wip: field for holding sandbox changes; used to avoid infinite loops <br>
* - history.unsubscribe: a function to stop the internal subscription process <br>
*/
history: ref<History<V>>({
wip: undefined, // to avoid infinite loop
nodes: [],
index: -1,
unsubscribe: () => {},
}),
/**
* get the date when a node was entered into history.
Expand Down Expand Up @@ -196,14 +212,15 @@ export function proxyWithHistory<V>(initialValue: V, skipSubscribe = false) {
*/
subscribe: () =>
subscribe(proxyObject, (ops) => {
if (
ops.every(
(op) =>
op[1][0] === 'value' &&
(op[0] !== 'set' || op[2] !== proxyObject.history.wip)
)
) {
proxyObject.saveHistory();
const shouldSaveHistory = ops.every(
(op) =>
op[1][0] === 'value' &&
(op[0] !== 'set' || op[2] !== proxyObject.history.wip)
);

if (shouldSaveHistory) proxyObject.saveHistory();
if (typeof skipSubscribeOrCallback === 'function') {
skipSubscribeOrCallback(ops, shouldSaveHistory);
}
}),

Expand Down Expand Up @@ -283,8 +300,8 @@ export function proxyWithHistory<V>(initialValue: V, skipSubscribe = false) {

proxyObject.saveHistory();

if (!skipSubscribe) {
proxyObject.subscribe();
if (skipSubscribeOrCallback !== true) {
proxyObject.history.unsubscribe = proxyObject.subscribe();
}

return proxyObject;
Expand Down

0 comments on commit c4dab09

Please sign in to comment.