www.spaceplanner.app

Web client to the spaceplanner API
git clone git://jacobedwards.org/www.spaceplanner.app
Log | Files | Refs

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:
Mfiles/floorplans/floorplan/backend.js | 287++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mfiles/floorplans/floorplan/editor.js | 41+++++++++++++++++++++--------------------
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) {