diff --git a/packages/history-utility/docs/modules.md b/packages/history-utility/docs/modules.md index e521edc..632ebe1 100644 --- a/packages/history-utility/docs/modules.md +++ b/packages/history-utility/docs/modules.md @@ -8,6 +8,7 @@ - [History](modules.md#history) - [HistoryNode](modules.md#historynode) +- [SkipSubscribeOrCallback](modules.md#skipsubscribeorcallback) ### Functions @@ -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`\ | 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) --- @@ -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:
- value: any value (does not have to be an object)
- history: an object holding the history of snapshots and other metadata
- - history.index: the history index of the current snapshot
- - history.nodes: the nodes of the history for each change
- - history.wip: field for holding sandbox changes; used to avoid infinite loops
- canUndo: a function to return true if undo is available
- undo: a function to go back history
- canRedo: a function to return true if redo is available
@@ -97,10 +110,10 @@ Notes:
#### 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 @@ -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
- history.index: the history index to the current snapshot
- history.nodes: the nodes of the history for each change
- history.wip: field for holding sandbox changes; used to avoid infinite loops
| +| `history` | [`History`](modules.md#history)\<`V`\> & `AsRef` | an object holding the history of snapshots and other metadata
- history.index: the history index to the current snapshot
- history.nodes: the nodes of the history for each change
- history.wip: field for holding sandbox changes; used to avoid infinite loops
- history.unsubscribe: a function to stop the internal subscription process
| | `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.

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.
| @@ -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) diff --git a/packages/history-utility/src/__tests__/history-utility.vanilla.spec.ts b/packages/history-utility/src/__tests__/history-utility.vanilla.spec.ts index 909526c..693d4e9 100644 --- a/packages/history-utility/src/__tests__/history-utility.vanilla.spec.ts +++ b/packages/history-utility/src/__tests__/history-utility.vanilla.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { HistoryNode, proxyWithHistory } from '../history-utility'; @@ -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(); @@ -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(); diff --git a/packages/history-utility/src/history-utility.ts b/packages/history-utility/src/history-utility.ts index 9288ba5..df22636 100644 --- a/packages/history-utility/src/history-utility.ts +++ b/packages/history-utility/src/history-utility.ts @@ -36,8 +36,21 @@ export type History = { * the history index of the current snapshot */ index: number; + /** + * a function to stop the internal subscription process + */ + unsubscribe: ReturnType; }; +type SubscribeOps = Parameters[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'; @@ -64,9 +77,6 @@ const deepClone = (value: T): T => { * It includes following main properties:
* - value: any value (does not have to be an object)
* - history: an object holding the history of snapshots and other metadata
- * - history.index: the history index of the current snapshot
- * - history.nodes: the nodes of the history for each change
- * - history.wip: field for holding sandbox changes; used to avoid infinite loops
* - canUndo: a function to return true if undo is available
* - undo: a function to go back history
* - canRedo: a function to return true if redo is available
@@ -82,7 +92,8 @@ const deepClone = (value: T): T => { * - Suspense/promise is not supported.
* * @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 @@ -91,7 +102,10 @@ const deepClone = (value: T): T => { * count: 1, * }) */ -export function proxyWithHistory(initialValue: V, skipSubscribe = false) { +export function proxyWithHistory( + initialValue: V, + skipSubscribeOrCallback: SkipSubscribeOrCallback = false +) { const proxyObject = proxy({ /** * any value to be tracked (does not have to be an object) @@ -101,12 +115,14 @@ export function proxyWithHistory(initialValue: V, skipSubscribe = false) { * an object holding the history of snapshots and other metadata
* - history.index: the history index to the current snapshot
* - history.nodes: the nodes of the history for each change
- * - history.wip: field for holding sandbox changes; used to avoid infinite loops
+ * - history.wip: field for holding sandbox changes; used to avoid infinite loops
+ * - history.unsubscribe: a function to stop the internal subscription process
*/ history: ref>({ wip: undefined, // to avoid infinite loop nodes: [], index: -1, + unsubscribe: () => {}, }), /** * get the date when a node was entered into history. @@ -196,14 +212,15 @@ export function proxyWithHistory(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); } }), @@ -283,8 +300,8 @@ export function proxyWithHistory(initialValue: V, skipSubscribe = false) { proxyObject.saveHistory(); - if (!skipSubscribe) { - proxyObject.subscribe(); + if (skipSubscribeOrCallback !== true) { + proxyObject.history.unsubscribe = proxyObject.subscribe(); } return proxyObject;