Skip to content

Commit

Permalink
feat(history): added basic history extension
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewcourtice committed Aug 5, 2021
1 parent bdfef62 commit 1ed86cd
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 0 deletions.
57 changes: 57 additions & 0 deletions extensions/history/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<p align="center">
<a href="https://harlemjs.com">
<img src="https://raw.githubusercontent.com/andrewcourtice/harlem/main/docs/src/.vuepress/public/assets/images/logo-192.svg" alt="Harlem"/>
</a>
</p>

# Harlem Reset Plugin

![npm](https://img.shields.io/npm/v/@harlem/plugin-reset)

This is the official Harlem plugin for resetting stores to their initial state.

<!-- TOC depthfrom:2 -->

- [Getting started](#getting-started)

<!-- /TOC -->

## Getting started

Before installing the reset plugin make sure you have installed `@harlem/core`.

1. Install `@harlem/plugin-reset`:
```
npm install @harlem/plugin-reset
```
Or if you're using Yarn:
```
yarn add @harlem/plugin-reset
```

2. Create an instance of the plugin and register it with Harlem:
```typescript
import App from './app.vue';

import harlem from '@harlem/core';
import createResetPlugin from '@harlem/plugin-reset';

createApp(App)
.use(harlem, {
plugins: [
createResetPlugin()
]
})
.mount('#app');
```

3. Call the reset method with the name of the store you wish to reset:
```typescript
import {
reset
} from '@harlem/plugin-reset';

export default function() {
reset('my-store');
}
```
47 changes: 47 additions & 0 deletions extensions/history/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@harlem/extension-history",
"amdName": "harlemHistory",
"version": "1.3.2",
"license": "MIT",
"author": "Andrew Courtice <[email protected]>",
"description": "The official history extension for Harlem",
"homepage": "https://harlemjs.com",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": "./dist/index.js",
"unpkg": "./dist/index.global.js",
"types": "./dist/index.d.ts",
"source": "./src/index.ts",
"keywords": [
"vue",
"state",
"harlem",
"extension",
"history"
],
"repository": {
"type": "git",
"url": "https://github.com/andrewcourtice/harlem.git",
"directory": "extensions/history"
},
"bugs": {
"url": "https://github.com/andrewcourtice/harlem/issues"
},
"scripts": {
"dev": "tsup --watch src",
"build": "tsup",
"prepublish": "yarn build"
},
"peerDependencies": {
"@harlem/core": "^1.1.2"
},
"dependencies": {
"@harlem/extension-trace": "^1.3.2",
"@harlem/utilities": "^1.3.2"
},
"devDependencies": {
"@harlem/core": "^1.3.2",
"@harlem/testing": "^1.3.2"
}
}
15 changes: 15 additions & 0 deletions extensions/history/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {
CommandType,
CommandTasks,
} from './types';

export const COMMAND_MAP = {
exec: {
set: (target, prop, newValue) => target[prop] = newValue,
deleteProperty: (target, prop) => delete target[prop],
},
undo: {
set: (target, prop, newValue, oldValue) => target[prop] = oldValue,
deleteProperty: (target, prop, newValue, oldValue) => target[prop] = oldValue,
},
} as Record<CommandType, Partial<CommandTasks>>;
145 changes: 145 additions & 0 deletions extensions/history/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
COMMAND_MAP,
} from './constants';

import {
EventPayload,
EVENTS,
MutationEventData,
InternalStore,
BaseState,
} from '@harlem/core';

import traceExtension, {
TraceResult,
} from '@harlem/extension-trace';

import {
fromPath,
} from '@harlem/utilities';

import type {
CommandType,
HistoryCommand,
MutationPayload,
Options,
} from './types';

export * from './types';

export default function historyExtension<TState extends BaseState>(options?: Partial<Options>) {
const {
max,
mutations,
} = {
max: 50,
mutations: [],
...options,
} as Options;

const mutationLookup = new Map(mutations.map(({ name, description }) => [name, description]));
const createTraceExtension = traceExtension<TState>({
autoStart: true,
});

return (store: InternalStore<TState>) => {
const {
startTrace,
stopTrace,
onTraceResult,
} = createTraceExtension(store);

let position = 0;
let commands = [] as HistoryCommand[];
let results = [] as TraceResult<any>[];

const mutation = store.mutation('$history', (state, { type, command }: MutationPayload) => {
const tasks = COMMAND_MAP[type];

const {
results,
} = command;

results.forEach(({ gate, nodes, prop, newValue, oldValue }) => {
const target = fromPath(state, nodes);

if (target && prop) {
tasks[gate]?.(target, prop, newValue, oldValue);
}
});
});

function processResults(name: string) {
if (results.length === 0) {
return;
}

if (commands.length >= max) {
commands.shift();
}

commands.push({
name,
results: Array.from(results),
});

results = [];
position = commands.length - 1;
}

store.on(EVENTS.mutation.before, (event?: EventPayload<MutationEventData>) => {
if (!event || event.data.mutation.startsWith('$') || (mutationLookup.size > 0 && !mutationLookup.has(event.data.mutation))) {
return;
}

startTrace([
'set',
'deleteProperty',
]);

const listener = onTraceResult(result => results.push(result));

store.once(EVENTS.mutation.after, () => {
stopTrace();
processResults(event.data.mutation);

listener.dispose();
});
});

function run(type: CommandType, offset: number) {
const command = commands[position];

if (!command) {
return;
}

mutation({
type,
command,
});

position = Math.max(0, Math.min(commands.length - 1, position + offset));
}

function undo() {
run('undo', -1);
}

function redo() {
run('exec', 1);
}

function clearHistory() {
position = 0;
commands = [];
results = [];
}

return {
undo,
redo,
clearHistory,
};
};
}
28 changes: 28 additions & 0 deletions extensions/history/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
TraceGate,
TraceResult,
} from '@harlem/extension-trace';

export type CommandType = 'exec' | 'undo';
export type CommandTask = (target: any, prop: PropertyKey, newValue: unknown, oldValue: unknown) => void;
export type CommandTasks = Record<TraceGate<any>, CommandTask>;

export interface MutationPayload {
type: CommandType;
command: HistoryCommand;
}

export interface HistoryCommand {
name: string;
results: TraceResult<any>[];
}

export interface HistoryMutation {
name: string;
description?: string;
}

export interface Options {
max: number;
mutations: HistoryMutation[];
}
47 changes: 47 additions & 0 deletions extensions/history/test/history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
getStore,
bootstrap,
} from '@harlem/testing';

import historyExtension from '../src';

describe('History Extension', () => {

const getInstance = () => getStore({
extensions: [
historyExtension(),
],
});

let instance = getInstance();

beforeAll(() => bootstrap());
beforeEach(() => instance = getInstance());
afterEach(() => instance.store.destroy());

test('Performs an undo/redo operation', () => {
const {
store,
setUserID,
setUserDetails,
} = instance;

const {
state,
undo,
redo,
} = store;

setUserID(5);
setUserDetails({
firstName: 'John',
});

expect(state.details.firstName).toBe('John');
undo();
expect(state.details.firstName).toBe('');
// redo();
// expect(state.details.firstName).toBe('John');
});

});
7 changes: 7 additions & 0 deletions extensions/history/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*.ts",
"../../global.d.ts"
]
}
10 changes: 10 additions & 0 deletions extensions/history/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import base from '../../tsup.config';

import type {
Options,
} from 'tsup';

export default {
...base,
globalName: 'HarlemHistoryExtension',
} as Options;

0 comments on commit 1ed86cd

Please sign in to comment.