Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: useBranchedStateHistory #44

Open
petermakeswebsites opened this issue May 2, 2024 · 3 comments
Open

Feature Request: useBranchedStateHistory #44

petermakeswebsites opened this issue May 2, 2024 · 3 comments
Labels
enhancement New feature or request

Comments

@petermakeswebsites
Copy link

petermakeswebsites commented May 2, 2024

Describe the feature in detail (code, mocks, or screenshots encouraged)

For a Svelte 5 app I made, I created a branched state history, not dissimilar to Git branches. I'd be happy to create an abstraction of this and add it to this lib. The idea is essentially as follows:

  • During state change, a new "node" is created. This new node has a "parent" property that links to the previous state node.
  • Each node has one parent referring to its past, but if you go back (undo), and make a change to previous (parent) state, you create new branch linking from that parent node. So each node can have multiple children but only one parent.
  • If you "redo" to a parent that has multiple children, the child you "undid" from is remembered, and the redo applies to that one by default.
  • There's some API to see the children and parent so people can build their state explorers on top of it.

Is this something you would all find useful?

What type of pull request would this be?

New Feature

Provide relevant links or additional information.

No response

@petermakeswebsites petermakeswebsites added the enhancement New feature or request label May 2, 2024
@petermakeswebsites
Copy link
Author

I haven't actually tested this, so I'm not 100% it will work. But this is my alpha.

I will say, I noticed it doesn't really fit the pattern of the other history though, and I'm curious why the team chose to make some design decisions.

I like to stay as close to the primitives (runes) as possible. Using abstractions like box() and watch(), although useful, I think add an unnecessary extra layer of complexity on top. But maybe I don't fully understand their value. I wonder why you are using these in higher level functions?

I also use classes instead of functions for two reasons. First, it's more readable. Second, the methods are in the prototype so they are not re-instantiated every time a new one is created. Reduce reuse recycle ♻️!

Usage

You can create a BranchedHistory as follows:

// Create state
const history = useBranchedStateHistory("original state")

// Change state
history.state = "some new state"

// Undo
history.undo() // history.state == "original state"

// Create new state
history.state = "a new branch!"

// Undo again
history.undo() // history.state == "original state"

// Get children
children = [...history.children] // [BranchNode {value: "some new state"}, BranchNode {value: "a new branch!"} ]

// Redo
history.redo() // Remembers the last "undid" branch, ergo history.state == "a new branch!"

// Go to specific node
history.goto(children[0]) // history.state == "original state"

Proposed source

import { Set } from "svelte/reactivity";

/**
 * Represents a specific state in the timeline
 */
class BranchNode<T> {
	/**
	 * This is set to the node that we called undo from, so we know where to
	 * re-do. This should be reactive because it may change.
	 */
	public redoTarget = $state<BranchNode<T>>();

	/**
	 * Children may change, so we're using Svelte's built-in reactivity set
	 */
	public readonly children = new Set<BranchNode<T>>();

	constructor(
		public readonly value: T,
		/**
		 * parent will never change, and therefor does not need to be stateful
		 */
		public readonly parent: BranchNode<T> | undefined = undefined
	) {}
}

class BranchedHistory<T> {
	public readonly root: BranchNode<T>;
	public node = $state<BranchNode<T>>() as BranchNode<T>;
	constructor(def: T) {
		this.root = new BranchNode(def);
		this.node = this.root;
	}

	public get state() {
		return this.node!.value;
	}

	public set state(v: T) {
		const parentNode = this.node!;
		this.node = new BranchNode(v, parentNode);
		parentNode.children.add(this.node);
	}

	canUndo = $derived(!!this.node.parent);
	canRedo = $derived(!!this.node.redoTarget);

	/**
	 * Branches coming off this node
	 */
	children = $derived(this.node.children);
	
	/**
	 * Undefined if root note
	 */
	parent = $derived(this.node.parent);

	/**
	 * Go to the selected node
	 * @param node
	 */
	public goto(node: BranchNode<T>) {
		this.node = node;
	}

	public undo() {
		if (!this.node.parent) return;
		const redoNode = this.node;
		this.node = this.node.parent;
		this.node.redoTarget = redoNode;
	}

	public redo() {
		if (!this.node.redoTarget) return;
		this.node = this.node.redoTarget;
	}
}

export function useBranchedStateHistory<T>(def: T) {
	return new BranchedHistory(def);
}

@abdel-17
Copy link
Collaborator

abdel-17 commented May 4, 2024

The box abstraction is useful when you want to return boxed values directly, which allows destructuring.

const { x, y } = useMouse();

For returning values directly, I agree that $state and $derived are good enough.

@TGlide
Copy link
Member

TGlide commented May 17, 2024

Hey @petermakeswebsites , this could really be interesting! Do you want to submit a PR?

@huntabyte huntabyte changed the title useBranchedStateHistory Feature Request: useBranchedStateHistory Jun 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants