From 66f6765796b76136c69f0d996e0cf99e4242cf11 Mon Sep 17 00:00:00 2001 From: Zicklag Date: Sat, 25 Jan 2025 11:11:42 -0600 Subject: [PATCH] feat: add basic, read-only time-travel example. --- pages/docs/tutorial/time_travel.mdx | 111 +++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/pages/docs/tutorial/time_travel.mdx b/pages/docs/tutorial/time_travel.mdx index 029385c..7a18505 100644 --- a/pages/docs/tutorial/time_travel.mdx +++ b/pages/docs/tutorial/time_travel.mdx @@ -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, + 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.