Skip to content

Commit

Permalink
update history
Browse files Browse the repository at this point in the history
  • Loading branch information
benStre committed Dec 6, 2023
1 parent 5f65e7a commit 6b75de5
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 31 deletions.
25 changes: 25 additions & 0 deletions docs/manual/14 History.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ history.back();
console.log(name.val) // "Max"
```

The `back()` and `forward()` method both return a boolean indicating if the state change could be
executed. If the end or start of the recorded history is reached, `false` is returned.

The `backSteps` and `forwardSteps` properties indicate how many steps can currently be performed in both directions.

## Save Points

For some use cases, it is required to not undo and redo any atomic state change, but instead jump between manually defined *save points*.
This can be achieved by enabling the `explicitSavePoints` option:
```ts
const history = new History({explicitSavePoints: true});
```

Now, you can set save points at any point in time by calling
```ts
history.setSavePoint()
```

The `back()` and `forward()` methods can be used as before.
They now jump between the defined save point states.

When the `back()` method is called before a new save point was set after pointer changes occured,
a new save point for the current state is automatically inserted.


## Enabling undo/redo shortcuts

The History API also provides a builtin way to go back or forward in history when the user
Expand Down
27 changes: 10 additions & 17 deletions runtime/pointers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { IterableWeakMap } from "../utils/iterable-weak-map.ts";
import { LazyPointer } from "./lazy-pointer.ts";
import { ReactiveArrayMethods } from "../types/reactive-methods/array.ts";

export type observe_handler<K=any, V extends RefLike = any> = (value:V extends RefLike<infer T> ? T : V, key?:K, type?:Ref.UPDATE_TYPE, transform?:boolean, is_child_update?:boolean, previous?: any)=>void|boolean
export type observe_handler<K=any, V extends RefLike = any> = (value:V extends RefLike<infer T> ? T : V, key?:K, type?:Ref.UPDATE_TYPE, transform?:boolean, is_child_update?:boolean, previous?: any, atomic_id?:symbol)=>void|boolean
export type observe_options = {types?:Ref.UPDATE_TYPE[], ignore_transforms?:boolean, recursive?:boolean}

const arrayProtoNames = Object.getOwnPropertyNames(Array.prototype);
Expand Down Expand Up @@ -1852,22 +1852,13 @@ export class Pointer<T = any> extends Ref<T> {
* @returns
*/
public assertEndpointCanRead(endpoint?: Endpoint) {
// logger.error(this.val)
// console.log("assert " + endpoint, Logical.matches(endpoint, this.allowed_access, Target), !(
// Runtime.OPTIONS.PROTECT_POINTERS
// && !(endpoint == Runtime.endpoint)
// && this.is_origin
// && (!endpoint || !Logical.matches(endpoint, this.allowed_access, Target))
// && (endpoint && !Runtime.trustedEndpoints.get(endpoint.main)?.includes("protected-pointer-access"))
// ), this.allowed_access)
if (
Runtime.OPTIONS.PROTECT_POINTERS
&& !(endpoint == Runtime.endpoint)
&& this.is_origin
&& (!endpoint || !Logical.matches(endpoint, this.allowed_access, Target))
&& (endpoint && !Runtime.trustedEndpoints.get(endpoint.main)?.includes("protected-pointer-access"))
) {
console.log("inv",new Error().stack)
throw new PermissionError("Endpoint has no read permissions for this pointer")
}
}
Expand Down Expand Up @@ -3602,15 +3593,17 @@ export class Pointer<T = any> extends Ref<T> {
}, 0)
}

const atomicId = Symbol("ATOMIC_SPLICE")

// inform observers after splice finished - value already in right position
for (let i = originalLength-1; i>=start_index; i--) {
// element moved here?
if (i < obj.length) {
this.callObservers(obj[i], i, Ref.UPDATE_TYPE.SET, undefined, undefined, previous[i])
this.callObservers(obj[i], i, Ref.UPDATE_TYPE.SET, undefined, undefined, previous[i], atomicId)
}
// end of array, trigger delete
else {
this.callObservers(VOID, i, Ref.UPDATE_TYPE.DELETE, undefined, undefined, previous[i])
this.callObservers(VOID, i, Ref.UPDATE_TYPE.DELETE, undefined, undefined, previous[i], atomicId)
}
}

Expand Down Expand Up @@ -3838,20 +3831,20 @@ export class Pointer<T = any> extends Ref<T> {
}


private callObservers(value:any, key:any, type:Ref.UPDATE_TYPE, is_transform = false, is_child_update = false, previous?: any) {
private callObservers(value:any, key:any, type:Ref.UPDATE_TYPE, is_transform = false, is_child_update = false, previous?: any, atomic_id?: symbol) {
const promises = [];
// key specific observers
if (key!=undefined) {
for (const [o, options] of this.change_observers.get(key)||[]) {
if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) promises.push(o(value, key, type, is_transform, is_child_update, previous));
if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) promises.push(o(value, key, type, is_transform, is_child_update, previous, atomic_id));
}
// bound observers
for (const [object, entries] of this.bound_change_observers.entries()) {
for (const [k, handlers] of entries) {
if (k === key) {
for (const [handler, options] of handlers) {
if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) {
const res = handler.call(object, value, key, type, is_transform, is_child_update, previous);
const res = handler.call(object, value, key, type, is_transform, is_child_update, previous, atomic_id);
promises.push(res)
if (res === false) this.unobserve(handler, object, key);
}
Expand All @@ -3862,13 +3855,13 @@ export class Pointer<T = any> extends Ref<T> {
}
// general observers
for (const [o, options] of this.general_change_observers||[]) {
if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) promises.push(o(value, key, type, is_transform, is_child_update, previous));
if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) promises.push(o(value, key, type, is_transform, is_child_update, previous, atomic_id));
}
// bound generalobservers
for (const [object, handlers] of this.bound_general_change_observers||[]) {
for (const [handler, options] of handlers) {
if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) {
const res = handler.call(object, value, key, type, is_transform, is_child_update, previous)
const res = handler.call(object, value, key, type, is_transform, is_child_update, previous, atomic_id)
promises.push(res)
if (res === false) this.unobserve(handler, object, key);
}
Expand Down
4 changes: 2 additions & 2 deletions types/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export class Type<T = any> extends ExtensibleFunction {
#proxify_children = false // proxify all (new) children of this type
children_timeouts?: Map<string, number> // individual timeouts for children

static #jsTypeDefModuleMapper?: (url:string|URL) => string|URL
static #jsTypeDefModuleMapper?: (url:string|URL) => string|URL|undefined

static setJSTypeDefModuleMapper(fn: (url:string|URL) => string|URL) {
static setJSTypeDefModuleMapper(fn: (url:string|URL) => string|URL|undefined) {
this.#jsTypeDefModuleMapper = fn;
// update existing typedef modules
for (const type of this.types.values()) {
Expand Down
77 changes: 65 additions & 12 deletions utils/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { logger } from "./global_values.ts";
type HistoryStateChange = {
pointer: Pointer,
type: Ref.UPDATE_TYPE,
value: any,
previous: any,
key?: any
value: unknown,
previous: unknown,
key?: unknown,
atomicId?: symbol
}

type HistoryOptions = {
Expand All @@ -20,9 +21,25 @@ export class History {
#options: HistoryOptions = {}
#changes = new Array<HistoryStateChange|HistoryStateChange[]>()
#index = -1;
#lastSavePoint = 0;
#currentSavePoint = 0;

#frozenPointers = new Set<Pointer>()

get forwardSteps() {
return -this.#index - 1
}

get backSteps() {
const range = this.#changes.length + this.#index + 1;
if (this.#options.explicitSavePoints) {
return this.#currentSavePoint + (
this.#changes.length !== this.#lastSavePoint ? 1 : 0 // additional step back that is not yet a save point
)
}
else return range
}

constructor(options?: HistoryOptions) {
if (options) this.#options = options
if (this.#options.enableKeyboardShortcuts) this.enableKeyboardShortcuts()
Expand All @@ -32,9 +49,7 @@ export class History {
const pointer = Pointer.pointerifyValue(val);
if (!(pointer instanceof Pointer)) throw new Error("Cannot include non-pointer value in history");

Ref.observe(pointer, (value, key, type, transform, is_child_update, previous) => {

// TODO: group atomic state changes (e.g. splice)
Ref.observe(pointer, (value, key, type, _transform, _isChildUpdate, previous, atomicId) => {

if (type == Pointer.UPDATE_TYPE.BEFORE_DELETE) return; // ignore
if (type == Pointer.UPDATE_TYPE.BEFORE_REMOVE) return; // ignore
Expand All @@ -55,13 +70,28 @@ export class History {
this.#index = -1;
}

this.#changes.push({
const change = {
pointer,
type,
value,
previous,
key
})
key,
atomicId
};

// group atomic changes
if (atomicId && this.#changes.at(-1) instanceof Array && (this.#changes.at(-1) as HistoryStateChange[])[0]?.atomicId === atomicId) {
(this.#changes.at(-1) as HistoryStateChange[]).push(change)
}
else if (atomicId && (this.#changes.at(-1) as HistoryStateChange)?.atomicId === atomicId) {
const lastChange = this.#changes.at(-1) as HistoryStateChange;
this.#changes[this.#changes.length-1] = [lastChange, change]
}

// single change
else {
this.#changes.push(change)
}
})
}

Expand All @@ -70,13 +100,18 @@ export class History {
* @returns false if already at the first state
*/
back() {
// create save point for current state
if (this.#options.explicitSavePoints && this.#changes.length !== this.#lastSavePoint) this.setSavePoint()

const lastChanges = this.#changes.at(this.#index);
if (!lastChanges) return false;

this.#index--;
if (this.#options.explicitSavePoints) this.#currentSavePoint--;

console.log("<- undo",lastChanges)

for (const lastChange of (lastChanges instanceof Array ? lastChanges : [lastChanges])) {
for (const lastChange of (lastChanges instanceof Array ? lastChanges.toReversed() : [lastChanges])) {
this.#silent(lastChange.pointer, () => {
if (lastChange.type == Pointer.UPDATE_TYPE.INIT) {
lastChange.pointer.val = lastChange.previous;
Expand All @@ -88,6 +123,12 @@ export class History {
if (lastChange.previous == NOT_EXISTING) lastChange.pointer.handleDelete(lastChange.key, true)
else lastChange.pointer.handleSet(lastChange.key, lastChange.previous, true, true)
}
else if (lastChange.type == Pointer.UPDATE_TYPE.ADD) {
lastChange.pointer.handleRemove(lastChange.value)
}
else if (lastChange.type == Pointer.UPDATE_TYPE.REMOVE) {
lastChange.pointer.handleAdd(lastChange.value)
}
})
}

Expand All @@ -99,8 +140,10 @@ export class History {
* @returns false if already at the last state
*/
forward() {
if (this.#index >= -1) return;
if (this.#index >= -1) return false;
this.#index++;
if (this.#options.explicitSavePoints) this.#currentSavePoint++;

const nextChanges = this.#changes.at(this.#index);
if (!nextChanges) return false;

Expand All @@ -118,16 +161,26 @@ export class History {
if (nextChange.value == NOT_EXISTING) nextChange.pointer.handleDelete(nextChange.key, true)
else nextChange.pointer.handleSet(nextChange.key, nextChange.value, true, true)
}
else if (nextChange.type == Pointer.UPDATE_TYPE.ADD) {
nextChange.pointer.handleAdd(nextChange.value)
}
else if (nextChange.type == Pointer.UPDATE_TYPE.REMOVE) {
nextChange.pointer.handleRemove(nextChange.value)
}
})
}


return true;
}

createSavePoint() {
setSavePoint() {
if (!this.#options.explicitSavePoints) throw new Error("Explicit save points are not enabled");

const historyChunk = this.#changes.splice(this.#lastSavePoint, this.#changes.length-this.#lastSavePoint).flat()
if (historyChunk.length) this.#changes.push(historyChunk);
this.#lastSavePoint = this.#changes.length;
this.#currentSavePoint = this.#lastSavePoint;
}

#silent(pointer: Pointer, handler: ()=>void) {
Expand Down

0 comments on commit 6b75de5

Please sign in to comment.