Skip to content

Commit fa55047

Browse files
Merge pull request #76 from robertmoura/main
Fixed history extension
2 parents 77a33da + c2caad0 commit fa55047

File tree

6 files changed

+176
-87
lines changed

6 files changed

+176
-87
lines changed

extensions/history/src/constants.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type {
2-
CommandTasks,
3-
CommandType,
2+
ChangeCommands,
3+
ChangeType,
44
} from './types';
55

66
export const SENDER = 'extension:history';
77
export const MUTATION_FILTER = /^(plugin|extension)/;
88

9-
export const COMMAND_MAP = {
9+
export const CHANGE_MAP: Record<ChangeType, Partial<ChangeCommands>> = {
1010
exec: {
1111
set: (target, prop, newValue) => target[prop] = newValue,
1212
deleteProperty: (target, prop) => delete target[prop],
@@ -15,4 +15,4 @@ export const COMMAND_MAP = {
1515
set: (target, prop, newValue, oldValue) => target[prop] = oldValue,
1616
deleteProperty: (target, prop, newValue, oldValue) => target[prop] = oldValue,
1717
},
18-
} as Record<CommandType, Partial<CommandTasks>>;
18+
};

extensions/history/src/index.ts

Lines changed: 114 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
COMMAND_MAP,
2+
CHANGE_MAP,
33
MUTATION_FILTER,
44
SENDER,
55
} from './constants';
@@ -17,12 +17,16 @@ import traceExtension, {
1717
} from '@harlem/extension-trace';
1818

1919
import {
20+
matchGetFilter,
2021
objectFromPath,
22+
typeIsMatchable,
23+
typeIsObject,
2124
} from '@harlem/utilities';
2225

2326
import type {
24-
CommandType,
25-
HistoryCommand,
27+
ChangeType,
28+
HistoryGroup,
29+
MutationTrace,
2630
Options,
2731
} from './types';
2832

@@ -31,18 +35,23 @@ export * from './types';
3135
function getOptions(options?: Partial<Options>): Options {
3236
return {
3337
max: 50,
34-
mutations: [],
38+
mutations: '*',
3539
...options,
3640
};
3741
}
3842

3943
export default function historyExtension<TState extends BaseState>(options?: Partial<Options>) {
4044
const _options = getOptions(options);
41-
const mutationLookup = new Map(_options.mutations.map(({ name, description }) => [name, description]));
45+
const groups = getMutationGroups(_options.mutations);
46+
4247
const createTraceExtension = traceExtension<TState>({
4348
autoStart: true,
4449
});
4550

51+
function mutationFilter(mutation: string) {
52+
return groups.some(({ filter }) => filter(mutation));
53+
}
54+
4655
return (store: InternalStore<TState>) => {
4756
store.register('extensions', 'history', () => _options);
4857

@@ -52,21 +61,51 @@ export default function historyExtension<TState extends BaseState>(options?: Par
5261
onTraceResult,
5362
} = createTraceExtension(store);
5463

55-
let position = 0;
56-
let commands = [] as HistoryCommand[];
57-
let results = [] as TraceResult<any>[];
64+
const createHistoryState = () => {
65+
const results: TraceResult<any>[] = [];
66+
const historyGroups = groups.reduce((out, { key }) => {
67+
out[key] = {
68+
position: -1,
69+
history: [],
70+
};
71+
72+
return out;
73+
}, {} as Record<string, HistoryGroup>);
74+
75+
return {
76+
results,
77+
groups: historyGroups,
78+
};
79+
};
5880

59-
function executeCommand(type: CommandType, command: HistoryCommand) {
60-
store.write(`extension:history:${type}`, SENDER, state => {
61-
const tasks = COMMAND_MAP[type];
81+
let historyState = createHistoryState();
82+
83+
store.on(EVENTS.mutation.before, (event?: EventPayload<TriggerEventData>) => {
84+
if (!event || MUTATION_FILTER.test(event.data.name) || !mutationFilter(event.data.name)) {
85+
return;
86+
}
6287

63-
let {
64-
results,
65-
} = command;
88+
startTrace([
89+
'set',
90+
'deleteProperty',
91+
]);
6692

67-
if (type === 'undo') {
68-
results = results.slice().reverse();
69-
}
93+
const listener = onTraceResult(result => historyState.results.push(result));
94+
95+
store.once(EVENTS.mutation.after, () => {
96+
stopTrace();
97+
processResults(event.data.name);
98+
99+
listener.dispose();
100+
});
101+
});
102+
103+
function applyChange(type: ChangeType, change: MutationTrace) {
104+
store.write(`extension:history:${type}`, SENDER, state => {
105+
const tasks = CHANGE_MAP[type];
106+
const results = type === 'exec'
107+
? change.results
108+
: change.results.slice().reverse();
70109

71110
results.forEach(({ gate, nodes, prop, newValue, oldValue }) => {
72111
const target = objectFromPath(state, nodes);
@@ -78,68 +117,65 @@ export default function historyExtension<TState extends BaseState>(options?: Par
78117
});
79118
}
80119

81-
function processResults(name: string) {
82-
if (results.length === 0) {
120+
function processResults(mutation: string) {
121+
if (historyState.results.length === 0) {
83122
return;
84123
}
85124

86-
if (commands.length >= _options.max) {
87-
commands.shift();
88-
}
89-
90-
commands.push({
91-
name,
92-
results: Array.from(results),
93-
});
125+
for (const { key, filter } of groups) {
126+
if (!filter(mutation)) {
127+
continue;
128+
}
94129

95-
results = [];
96-
position = commands.length - 1;
97-
}
130+
const historyGroup = historyState.groups[key];
98131

99-
store.on(EVENTS.mutation.before, (event?: EventPayload<TriggerEventData>) => {
100-
if (!event || MUTATION_FILTER.test(event.data.name) || (mutationLookup.size > 0 && !mutationLookup.has(event.data.name))) {
101-
return;
102-
}
132+
if (historyGroup.history.length - 1 !== historyGroup.position) {
133+
historyGroup.history = historyGroup.history.slice(0, historyGroup.position + 1);
134+
}
103135

104-
startTrace([
105-
'set',
106-
'deleteProperty',
107-
]);
136+
if (historyGroup.history.length >= _options.max) {
137+
historyGroup.history.shift();
138+
}
108139

109-
const listener = onTraceResult(result => results.push(result));
140+
historyGroup.history.push({
141+
name: mutation,
142+
results: Array.from(historyState.results),
143+
});
110144

111-
store.once(EVENTS.mutation.after, () => {
112-
stopTrace();
113-
processResults(event.data.name);
145+
historyGroup.position = historyGroup.history.length - 1;
146+
}
114147

115-
listener.dispose();
116-
});
117-
});
148+
historyState.results = [];
149+
}
118150

119-
function run(type: CommandType, offset: number) {
120-
const command = commands[position];
151+
function run(groupKey: string, type: ChangeType, offset: number) {
152+
const historyGroup = historyState.groups[groupKey];
121153

122-
if (!command) {
154+
if (!historyGroup) {
123155
return;
124156
}
125157

126-
executeCommand(type, command);
158+
const changeIndex = historyGroup.position + (offset === -1 ? 0 : 1);
159+
const change = historyGroup.history[changeIndex];
160+
161+
if (!change) {
162+
return;
163+
}
127164

128-
position = Math.max(0, Math.min(commands.length - 1, position + offset));
165+
applyChange(type, change);
166+
historyGroup.position = Math.max(-1, Math.min(historyGroup.history.length - 1, historyGroup.position + offset));
129167
}
130168

131-
function undo() {
132-
run('undo', -1);
169+
function undo(group: string = '') {
170+
run(group, 'undo', -1);
133171
}
134172

135-
function redo() {
136-
run('exec', 1);
173+
function redo(group: string = '') {
174+
run(group, 'exec', 1);
137175
}
138176

139177
function clearHistory() {
140-
position = 0;
141-
commands = [];
142-
results = [];
178+
historyState = createHistoryState();
143179
}
144180

145181
return {
@@ -148,4 +184,25 @@ export default function historyExtension<TState extends BaseState>(options?: Par
148184
clearHistory,
149185
};
150186
};
187+
}
188+
189+
function getMutationGroups(mutations: Options['mutations']) {
190+
const hasGroups = typeIsObject(mutations) && 'groups' in mutations;
191+
const groups = hasGroups ? mutations.groups : {};
192+
193+
if (!hasGroups || typeIsMatchable(mutations)) {
194+
groups[''] = mutations;
195+
}
196+
197+
return Object.entries(groups)
198+
.map(([key, matcher]) => {
199+
const matchable = typeIsMatchable(matcher) ? matcher : {
200+
include: matcher,
201+
};
202+
203+
return {
204+
key,
205+
filter: matchGetFilter(matchable),
206+
};
207+
});
151208
}

extensions/history/src/types.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
1-
import {
1+
import type {
22
TraceGate,
33
TraceResult,
44
} from '@harlem/extension-trace';
55

6-
export type CommandType = 'exec' | 'undo';
7-
export type CommandTask = (target: any, prop: PropertyKey, newValue: unknown, oldValue: unknown) => void;
8-
export type CommandTasks = Record<TraceGate<any>, CommandTask>;
6+
import type {
7+
Matchable,
8+
Matcher,
9+
} from '@harlem/utilities';
910

10-
export interface MutationPayload {
11-
type: CommandType;
12-
command: HistoryCommand;
13-
}
11+
export type ChangeType = 'exec' | 'undo';
12+
export type ChangeCommand = (target: any, prop: PropertyKey, newValue: unknown, oldValue: unknown) => void;
13+
export type ChangeCommands = Record<TraceGate<any>, ChangeCommand>;
1414

15-
export interface HistoryCommand {
15+
export type MutationTrace = {
1616
name: string;
1717
results: TraceResult<any>[];
18-
}
18+
};
1919

20-
export interface HistoryMutation {
21-
name: string;
22-
description?: string;
20+
export type HistoryGroup = {
21+
position: number;
22+
history: MutationTrace[];
2323
}
2424

25-
export interface Options {
25+
export type MutationGroups = {
26+
groups: Record<string, Matcher | Matchable>;
27+
};
28+
29+
export type Options = {
2630
max: number;
27-
mutations: HistoryMutation[];
28-
}
31+
mutations: Matcher | Matchable | MutationGroups | Matchable & MutationGroups;
32+
};

extensions/history/test/history.test.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ describe('History Extension', () => {
1818

1919
const getInstance = () => getStore({
2020
extensions: [
21-
historyExtension(),
21+
historyExtension({
22+
mutations: {
23+
groups: {
24+
userDetails: ['set-user-details'],
25+
},
26+
},
27+
}),
2228
],
2329
});
2430

25-
let instance = getInstance();
31+
let instance: ReturnType<typeof getInstance>;
2632

2733
beforeAll(() => bootstrap());
2834
beforeEach(() => {
@@ -49,11 +55,31 @@ describe('History Extension', () => {
4955
firstName: 'John',
5056
});
5157

58+
setUserDetails({
59+
firstName: 'More things',
60+
});
61+
62+
expect(state.details.firstName).toBe('More things');
63+
undo('userDetails');
5264
expect(state.details.firstName).toBe('John');
53-
undo();
65+
undo('userDetails');
5466
expect(state.details.firstName).toBe('');
55-
// redo();
56-
// expect(state.details.firstName).toBe('John');
67+
redo('userDetails');
68+
expect(state.details.firstName).toBe('John');
69+
redo('userDetails');
70+
expect(state.details.firstName).toBe('More things');
71+
undo('userDetails');
72+
expect(state.details.firstName).toBe('John');
73+
74+
setUserDetails({
75+
firstName: 'After',
76+
});
77+
78+
expect(state.details.firstName).toBe('After');
79+
undo('userDetails');
80+
expect(state.details.firstName).toBe('John');
81+
redo('userDetails');
82+
expect(state.details.firstName).toBe('After');
5783
});
5884

5985
});

harlem/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Register the Harlem plugin with your Vue app instance:
8383
import App from './app.vue';
8484

8585
import {
86-
createVuePLugin
86+
createVuePlugin
8787
} from 'harlem';
8888

8989
createApp(App)

0 commit comments

Comments
 (0)