Skip to content

Commit

Permalink
flesh out the history API a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
pvh committed Aug 16, 2024
1 parent 512f5e6 commit 73ce227
Show file tree
Hide file tree
Showing 3 changed files with 6,508 additions and 5,081 deletions.
57 changes: 57 additions & 0 deletions packages/automerge-repo/src/DocHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,63 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
return A.getHeads(this.#doc)
}

/**
* Creates a fixed "view" of an automerge document at the given point in time represented
* by the `heads` passed in. The return value is the same type as docSync() and will return
* undefined if the object hasn't finished loading.
*
* @remarks
* A point-in-time in an automerge document is an *array* of heads since there may be
* concurrent edits. This API just returns a topologically sorted history of all edits
* so every previous entry will be (in some sense) before later ones, but the set of all possible
* history views would be quite large under concurrency (every thing in each branch against each other).
* There might be a clever way to think about this, but we haven't found it yet, so for now at least
* we present a single traversable view which excludes concurrency.
* @returns The individual heads for every change in the document.
*/
history(): A.Heads[] | undefined {
if (!this.isReady()) {
return undefined
}
// This just returns all the heads as individual strings.

return A.topoHistoryTraversal(this.#doc).map(h => [h]) as A.Heads[]
}

/**
* Creates a fixed "view" of an automerge document at the given point in time represented
* by the `heads` passed in. The return value is the same type as docSync() and will return
* undefined if the object hasn't finished loading.
* @returns
*/
view(heads: A.Heads): A.Doc<T> | undefined {
if (!this.isReady()) {
return undefined
}
return A.view(this.#doc, heads)
}

/**
* Creates a fixed "view" of an automerge document at the given point in time represented
* by the `heads` passed in. The return value is the same type as docSync() and will return
* undefined if the object hasn't finished loading.
*
* @remarks
* We allow specifying both a from/to heads or just a single comparison point, in which case
* the base will be the current document heads.
*
* @returns Automerge patches that go from one document state to the other. Use view() to get the full state.
*/
diff(first: A.Heads, second?: A.Heads): A.Patch[] | undefined {
if (!this.isReady()) {
return undefined
}
// We allow only one set of heads to be specified, in which case we use the doc's heads
const from = second ? first : this.heads() || [] // because we guard above this should always have useful data
const to = second ? second : first
return A.diff(this.#doc, from, to)
}

/**
* `update` is called any time we have a new document state; could be
* from a local change, a remote change, or a new document from storage.
Expand Down
82 changes: 82 additions & 0 deletions packages/automerge-repo/test/DocHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ describe("DocHandle", () => {
assert.equal(doc?.foo, "bar")
})

/** HISTORY TRAVERSAL
* This API is relatively alpha-ish but we're already
* doing things in our own apps that are fairly ambitious
* by routing around to a lower-level API.
* This is an attempt to wrap up the existing practice
* in a slightly more supportable set of APIs but should be
* considered provisional: expect further improvements.
*/

it("should return the heads when requested", async () => {
const handle = setup()
handle.change(d => (d.foo = "bar"))
Expand All @@ -84,6 +93,79 @@ describe("DocHandle", () => {
assert.deepEqual(handle.heads(), undefined)
})

it("should return the history when requested", async () => {
const handle = setup()
handle.change(d => (d.foo = "bar"))
handle.change(d => (d.foo = "baz"))
assert.equal(handle.isReady(), true)

const history = handle.history()
assert.deepEqual(handle.history().length, 2)
})

it("should return a commit from the history", async () => {
const handle = setup()
handle.change(d => (d.foo = "zero"))
handle.change(d => (d.foo = "one"))
handle.change(d => (d.foo = "two"))
handle.change(d => (d.foo = "three"))
assert.equal(handle.isReady(), true)

const history = handle.history()
const view = handle.view(history[1])
assert.deepEqual(view, { foo: "one" })
})

it("should return a commit from the history", async () => {
const handle = setup()
handle.change(d => (d.foo = "zero"))
handle.change(d => (d.foo = "one"))
handle.change(d => (d.foo = "two"))
handle.change(d => (d.foo = "three"))
assert.equal(handle.isReady(), true)

const history = handle.history()
const view = handle.view(history[1])
assert.deepEqual(view, { foo: "one" })
})

it("should return diffs", async () => {
const handle = setup()
handle.change(d => (d.foo = "zero"))
handle.change(d => (d.foo = "one"))
handle.change(d => (d.foo = "two"))
handle.change(d => (d.foo = "three"))
assert.equal(handle.isReady(), true)

const history = handle.history()
const patches = handle.diff(history[1])
assert.deepEqual(patches, [
{ action: "put", path: ["foo"], value: "" },
{ action: "splice", path: ["foo", 0], value: "one" },
])
})

it("should support arbitrary diffs too", async () => {
const handle = setup()
handle.change(d => (d.foo = "zero"))
handle.change(d => (d.foo = "one"))
handle.change(d => (d.foo = "two"))
handle.change(d => (d.foo = "three"))
assert.equal(handle.isReady(), true)

const history = handle.history()
const patches = handle.diff(history[1], history[3])
assert.deepEqual(patches, [
{ action: "put", path: ["foo"], value: "" },
{ action: "splice", path: ["foo", 0], value: "three" },
])
const backPatches = handle.diff(history[3], history[1])
assert.deepEqual(backPatches, [
{ action: "put", path: ["foo"], value: "" },
{ action: "splice", path: ["foo", 0], value: "one" },
])
})

/**
* Once there's a Repo#stop API this case should be covered in accompanying
* tests and the following test removed.
Expand Down
Loading

0 comments on commit 73ce227

Please sign in to comment.