Skip to content

Commit

Permalink
Merge pull request #3 from zicklag/time-travel-example
Browse files Browse the repository at this point in the history
feat: add basic, read-only time-travel example.
  • Loading branch information
zxch3n authored Jan 26, 2025
2 parents 8306af2 + 66f6765 commit 1152cf5
Showing 1 changed file with 109 additions and 2 deletions.
111 changes: 109 additions & 2 deletions pages/docs/tutorial/time_travel.mdx
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ description: "time travel in Loro"

In Loro, you can call `doc.checkout(frontiers)` to jump to the version specified
by the
frontiers([Learn more about frontiers](/docs/advanced/version_deep_dive)).
frontiers([Learn more about frontiers](/docs/advanced/version_deep_dive#frontiers)).

Note that using `doc.checkout(frontiers)` to jump to a specific version places
the document in a detached state, preventing further edits. To learn more, see
@@ -16,7 +16,114 @@ To continue editing, reattach the document to the latest version using
`doc.attach()`. This design is temporary and will be phased out once we have a
more refined version control API in place.

Below is an example demonstrating Time Travel functionality.
## Read-only Time Travel

Below we demonstrate how to implement simple, read-only time-travel. You could,
for example, combine this with a slider in a UI to allow users to view the document
over time.

### Enable Timestamps

Before this example will work, it is important that the edits made to the document
have had [timestamp storage](/docs/advanced/timestamp) enabled:

```ts
doc.setRecordTimestamp(true);
```

This makes sure that all changes to the document will have a timestamp added to it.
We will use this timestamp to sort changes so that the ordering will match user
intuition.

### Implementing Time Travel

The first step is to load our document. Here we assume that you have a snapshot from your database
or API.

```ts
// Get the snapshot for your doc from your database / API
let snapshot = fetchSnapshot();

// Import into a new document
const doc = new LoroDoc();
doc.import(snapshot);
```

Next we must collect and sort the timestamps for every change in the document. We want uesrs to be
able to drag a slider to select a timestamp out of this list.

```ts
// Collect all changes from the document
const changes = doc.getAllChanges();

// Get the timestamps for all changes
const timestamps = Array.from(
new Set(
[...changes.values()]
.flat() // Flatten changes from all peers into one list
.map((x) => x.timestamp) // Get the timestamp from each peer
.filter((x) => !!x)
)
);

// Sort the timestamps
timestamps.sort((a, b) => a - b);
```

Next we need to make a helper function that will return a list of
[Frontiers](/docs/advanced/version_deep_dive#frontiers) for any timestamp.

For each peer that has edited a document, there is a list of changes by that peer. Each change has a
`counter`, and a `length`. That `counter` is like an always incrementing version number for the
changes made by that peer.

A change's `counter` is the starting point of the change, and the `length` indicates how much the
change incremented the counter before the end of the change.

The frontiers are the list of counters that we want to checkout from each peer. Since we are going
for a timeline view, we want to get the highest counter that we know happned before our timestamp
for each peer.

Here we make a helper function to do that.

```ts
const getFrontiersForTimestamp = (
changes: Map<string, Change>,
ts: number
): { peer: string; counter: number }[] => {
const frontiers = [] as { peer: string; counter: number }[];

// Record the highest counter for each peer where it's change is not later than
// our target timestamp.
changes.forEach((changes, peer) => {
let counter = -1;
for (const change of changes) {
if (change.timestamp <= ts) {
counter = Math.max(counter, change.counter + change.length - 1);
}
}
if (counter > -1) {
frontiers.push({ counter, peer });
}
});
return frontiers;
};
```

Finally, all we can get the index from our slider, get the timestamp from our list, and then
checkout the calculated frontiers.

```ts
let sliderIdx = 3;
const timestamp = timestamps[sliderIdx - 1];
const frontiers = getFrontiersForTimestamp(changes, timestamp);

doc.checkout(frontiers);
```

## Time Travel With Editing

Below is a more complete example demonstrating Time Travel functionality with a node editor.

<iframe
src="https://loro-react-flow-example.vercel.app/"

0 comments on commit 1152cf5

Please sign in to comment.