commit 6f14993bcda9c4e2baa7c167debf75052659e2e9
parent f8e3ae45c77839b9038cb448d7acb0acc8ae33fe
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date: Tue, 20 Aug 2024 15:59:26 -0700
Add ability to go back and forth through changes
This required fixing the BackendHistory methods and introducing a
little change in the backend's method to push updates to the server.
But yes, basically reconstructTo() has finally been implemented as
a method for the backend.
Diffstat:
2 files changed, 248 insertions(+), 80 deletions(-)
diff --git a/files/floorplans/floorplan/backend.js b/files/floorplans/floorplan/backend.js
@@ -3,52 +3,86 @@ import * as api from "/lib/api.js"
class BackendHistory {
constructor() {
// The current position in history (diffs)
- this.place = null,
+ // -1 for before everything
+ this.place = -1,
// Metadata for diff groups
this.groups = [],
// Actual changes
- this.diffs = []
+ this.diffs = [],
+
+ // Says the time at which the diffs were truncated
+ // It's purpose is to tell the backend it can't just
+ // update the server with the diffs.
+ this.truncated = null
+ }
+
+ set diff(diff) {
+ this.place = diff.id
+ return diff
}
get diff() {
return this.diffs[this.place]
}
+ get last() {
+ return this.diffs.length - 1
+ }
+
get group() {
if (!this.diff) {
- if (this.groups.length === 1) {
- return this.groups[0]
+ if (this.groups.length > 1) {
+ throw new Error("Expected at most one group")
}
- return undefined
+ return this.groups[0]
}
return this.groups[this.diff.group]
}
+ groupLength(group) {
+ if (group == undefined) {
+ return 0
+ }
+ if (typeof group === "number") {
+ group = this.groups[group]
+ }
+
+ if (group.first == undefined) {
+ return 0;
+ }
+ if (group.last == undefined) {
+ return this.diffs.length - 1 - group.first
+ }
+ return group.last - group.first
+ }
+
newGroup() {
- console.log(this.groups, this.diff, this.group)
- if (this.groups.length > 0) {
- if (this.group.length === 0) {
- console.warn("Backend.History.newGroup",
- "Not creating new group: In an empty group")
- return this.group.id
- }
- this.group.last = this.place
- if (this.group.id < this.groups.length) {
- // Truncate history to this point since we're altering it
- this.groups = this.groups.slice(0, this.group.id)
+ const pushGroup = function(history) {
+ let group = {
+ type: "group",
}
+ group.id = history.groups.push(group) - 1
+ console.debug("Backend.History.newGroup", group.id)
+ return group
}
- let group = {
- type: "group",
- length: 0
+ if (this.groups.length === 0) {
+ return pushGroup(this)
}
- group.id = this.groups.push(group) - 1
- console.debug("Backend.History.newGroup", group.id)
- // NOTE: New diff callback function
- return group.id
+
+ if (this.groupLength(this.group) === 0) {
+ console.warn("Backend.History.newGroup",
+ "Not creating new group: In an empty group")
+ return null
+ }
+
+ if (this.group.last && this.group.last != this.diffs.at(-1).id) {
+ throw new Error("I don't think this should happen")
+ }
+ this.group.last = this.diffs.at(-1).id
+ return pushGroup(this)
}
addDiff(op, path, value, oldvalue, options) {
@@ -63,13 +97,18 @@ class BackendHistory {
throw new Error("Only add and remove operations supported")
}
- if (!this.diff) {
- this.newGroup()
+ this.truncate()
+
+ let group
+ if (this.groups.length === 0) {
+ group = this.newGroup()
+ } else {
+ group = this.groups.at(-1)
}
let diff = {
type: "diff",
- group: this.group.id,
+ group: group.id,
op: op,
path: path,
value: value,
@@ -82,16 +121,41 @@ class BackendHistory {
}
diff.id = this.diffs.push(diff) - 1
- this.group.length += 1
+ if (group.first == undefined) {
+ group.first = diff.id
+ }
this.place = diff.id
- console.debug("History.Backend.addDiff", diff.id)
+ console.debug("History.Backend.addDiff", diff.id, diff)
return diff.id
}
+ truncate() {
+ if (!this.diff || this.diff.id === this.diffs.at(-1).id) {
+ return
+ }
+
+ this.truncated = Date.now()
+
+ console.debug("Backend.History.truncate", this.diff.id, "from", this.diffs.length - 1)
+ this.diffs = this.between(-1, this.diff.id)
+ this.groups = this.groups.slice(0, this.group.id + 1)
+ if (this.group.last != undefined) {
+ this.group.last = this.diff.id
+ }
+ this.newGroup()
+ }
+
// Get the required operations to go from a, a group or
// diff, to b, another group or diff
between(a, b) {
+ const backend = this
const getDiff = function(v) {
+ if (typeof v === "number") {
+ if (v < -1) {
+ return -1
+ }
+ return v
+ }
if (typeof(v) === "object") {
if (!v.id) {
throw new Error("Doesn't have an id")
@@ -99,42 +163,49 @@ class BackendHistory {
if (v.type === "diff") {
return v.id
} else if (v.type === "group") {
- return this.groups[v.id].first
+ return backend.groups[v.id].first
}
throw new Error("Not a valid type")
}
- // Diff id
- return Number(v)
+ throw new Error(v + ": Invalid diff")
}
+ const getParams = function(from, to, max) {
+ from = getDiff(from)
+ to = getDiff(to)
- a = a ? getDiff(a) : 0
- b = b ? getDiff(b) : (this.diffs.length - 1)
- if (a < 0 || a >= this.diffs.length ||
- b < 0 || b >= this.diffs.length) {
- throw new Error("Invalid diff range")
+ if (from > max || to > max) {
+ throw new Error(from + ":" + to + ": Maximum range of " + max)
+ }
+ from += 1
+ to += 1
+ if (from === to) {
+ return null
+ }
+ if (from > to) {
+ return { reverse: true, from: to, to: from }
+ }
+ return { from: from, to: to }
}
- if (a == b) {
+ /*
+ * So 'a' is already applied, and we want the state to look
+ * like what it did when 'b' was added, so if we're going
+ * forward, skip 'a', but if going backward include it.
+ */
+ let params = getParams(a, b, this.diffs.length)
+ if (!params) {
return []
}
-
- let reverse = false
- if (a < b) {
- b = this.groups[this.diffs[b].group].last
- if (!b) {
- b = this.diffs.length - 1
- }
- } else {
- reverse = true
- let t = a
- a = b
- b = t
+ let diffs = this.diffs.slice(params.from, params.to)
+ if (params.reverse) {
+ diffs = this.reverseDiffs(diffs)
}
+ console.debug("Backend.History.between",
+ params.reverse ? "reversed" : "forward", params.from, params.to, diffs)
+ return diffs
+ }
- let diffs = this.diffs.slice(a, b + 1)
- if (!reverse) {
- return diffs
- }
+ reverseDiffs(diffs) {
for (let i in diffs) {
diffs[i] = this.reverseDiff(diffs[i])
}
@@ -168,6 +239,52 @@ class BackendHistory {
dirty() {
return this.diffs.filter(item => item.dirty)
}
+
+ // Step to the end of the next group
+ forward(diff) {
+ console.debug("Backend.History.forward", diff)
+ if (diff < 0) {
+ if (this.groups[0].last == undefined) {
+ return this.diffs.at(-1).id
+ }
+ return this.groups[0].last
+ }
+
+ diff = this.diffs[diff]
+ if (!diff) {
+ throw new Error("Diff does not exist")
+ }
+
+ if (diff.group === this.groups.at(-1).id) {
+ return this.diffs.at(-1).id
+ }
+ let group = this.groups[diff.group + 1]
+ if (group.last == undefined) {
+ throw new Error("Last should be defined. Bug!")
+ }
+ return group.last
+ }
+
+ // Step to the beginning of the previous group
+ backward(diff) {
+ if (diff < 0) {
+ throw new Error("Cannot go backward")
+ }
+
+ diff = this.diffs[diff]
+ if (!diff) {
+ throw new Error("Cannot go backward from nowhere! Bug!")
+ }
+
+ if (diff.group === 0) {
+ return -1;
+ }
+ let group = this.groups[diff.group - 1]
+ if (!group || group.last == undefined) {
+ throw new Error("This should not happen")
+ }
+ return group.last
+ }
}
export class FloorplanBackend {
@@ -209,6 +326,12 @@ export class FloorplanBackend {
}
this.history = new BackendHistory()
+
+ // Server's position in history
+ this.serverPosition = -1
+
+ // Time of last server update
+ this.serverUpdated = null
}
get endpoint() {
@@ -216,7 +339,21 @@ export class FloorplanBackend {
}
// Apply's diffs in order to get to the state at the beginning of the given diff id
- // reconstructTo(diff) {}
+ reconstructTo(diff) {
+ let diffs = this.history.between(this.history.place, diff)
+ this.applyDiff(diffs, { nodiff: true })
+ this.history.place = diff
+ console.debug("Backend.reconstructTo", "Reconstructed state to", diff)
+ return diff
+ }
+
+ undo() {
+ this.reconstructTo(this.history.backward(this.history.place))
+ }
+
+ redo() {
+ this.reconstructTo(this.history.forward(this.history.place))
+ }
/*
* Add some type of data within the cache.
@@ -318,10 +455,23 @@ export class FloorplanBackend {
}
}
- // Push updates to the server
push() {
- // Need a method of making sure we're only sending these once...
- let dirty = this.history.dirty()
+ // WARNING: This needs a lock
+ let put = (this.history.truncated &&
+ (!this.lastUpdated || this.lastUpdated < this.history.truncated))
+
+ this.lastUpdated = Date.now()
+
+ if (put) {
+ return this.putServer()
+ }
+
+ let dirty = this.history.between(this.serverPosition, this.history.last)
+ if (dirty.length === 0) {
+ console.log("Not updating server: already up to date")
+ return Promise.resolve()
+ }
+
let patch = []
for (let i in dirty) {
@@ -349,6 +499,7 @@ export class FloorplanBackend {
let backend = this
return api.fetch("PATCH", this.endpoint, patch)
.then(function(data) {
+ backend.serverPosition = dirty.at(-1).id
updateIds(backend, data)
for (let i in dirty) {
delete dirty[i].dirty
@@ -358,24 +509,38 @@ export class FloorplanBackend {
})
}
+ putServer() {
+ // WARNING: This needs a lock
+ let backend = this
+
+ return api.fetch("PUT", this.endpoint, this.cache)
+ .then(function() {
+ backend.serverPositoin = backend.history.place
+ })
+ }
+
/*
* Pull updates from the server.
* (Set AddData diff option to false, and call newGroup()
* once at the end.)
*/
pull() {
+ // WARNING: This probably needs a lock
let backend = this
return api.fetch("GET", this.endpoint)
.then(function(data) {
let diff = gendiff("", backend.cache, data)
- console.log("Backend.Pull (diff)", diff)
+ console.debug("Backend.Pull (diff)", diff)
backend.applyDiff(diff, { clean: true })
backend.cb("pull")
})
}
applyDiff(diff, options) {
- this.history.newGroup()
+ options = options ?? {}
+ if (!options.nodiff) {
+ this.history.newGroup()
+ }
for (let i in diff) {
let ref = parsePath(diff[i].path)
if (diff[i].op === "remove") {
@@ -384,7 +549,9 @@ export class FloorplanBackend {
this.addData(ref.type, diff[i].value, ref.id, options)
}
}
- this.history.newGroup()
+ if (!options.nodiff) {
+ this.history.newGroup()
+ }
}
}
diff --git a/files/floorplans/floorplan/editor.js b/files/floorplans/floorplan/editor.js
@@ -207,7 +207,7 @@ export class FloorplanEditor {
this.backend = new backend.FloorplanBackend(floorplan, options.backend)
// The diff which reflects the state of the displayed objects
- this.diff = null
+ this.diff = -1
this.grids = {}
for (let system in this.units.systems) {
@@ -359,6 +359,16 @@ export class FloorplanEditor {
this.backend.history.newGroup()
}
+ undo() {
+ this.backend.undo()
+ this.updateDisplay()
+ }
+
+ redo() {
+ this.backend.redo()
+ this.updateDisplay()
+ }
+
addPoint(point) {
let already = this.pointAt(point)
if (already) {
@@ -411,23 +421,20 @@ export class FloorplanEditor {
}
updateDisplay() {
- let diffs = this.backend.history.between(this.diff ?? 0, this.backend.history.diff.id)
+ let diffs = this.backend.history.between(this.diff, this.backend.history.place)
if (diffs.length > 0) {
- this.applyDiff(diffs)
+ this.applyDiffs(diffs)
this.diff = diffs.at(-1).id
+ if (this.diff > this.backend.history.place) {
+ this.diff -= 1
+ }
console.debug("Editor.updateDisplay", "Updated display to diff id", this.diff)
}
}
- applyDiff(diff, reverse) {
- if (!reverse) {
- for (let op in diff) {
- this.applyOp(diff[op], reverse)
- }
- } else {
- for (let op = diff.length - 1; i >= 0; --i) {
- this.applyOp(diff[op], reverse)
- }
+ applyDiffs(diffs) {
+ for (let op in diffs) {
+ this.applyOp(diffs[op])
}
}
@@ -435,10 +442,6 @@ export class FloorplanEditor {
console.debug("Editor.applyOp", diff)
let editor = this
- const reverseOps = {
- add: "remove",
- remove: "add"
- }
const ops = {
add: {
points: function(name, value) {
@@ -494,12 +497,10 @@ export class FloorplanEditor {
}
let ref = backend.parsePath(diff.path)
- let op = reverse ? reverseOps[diff.op] : diff.op
-
- if (!ops[op][ref.type]) {
+ if (!ops[diff.op][ref.type]) {
throw new Error("Unhandled patch")
}
- ops[op][ref.type](refId(ref), diff.value)
+ ops[diff.op][ref.type](refId(ref), diff.value)
}
updateId(ids) {