www.spaceplanner.app

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

commit 7a31e377a612fe61e9c86d64de73e389d57627a1
parent f3b9f2644697f6e9865843126ed1bc30c0033567
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Mon, 19 Aug 2024 17:08:47 -0700

Revise diff system

The previous diff system's complexity was causing issues, this new
one has a few properties which should make it much easier to work
with:

1. Just a flat diff array (groups are now in a separate array, and
   the diff references it's group)
2. Each individual diff has a unique id.
3. Naming scheme doesn't require you to use diffs.at(-1).diff[3]
   or somesuch

It's also it's own class which I thought might make things a bit
easier.

Diffstat:
Mfiles/floorplans/floorplan/backend.js | 309++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mfiles/floorplans/floorplan/editor.js | 16+++++++++-------
Mfiles/floorplans/floorplan/main.js | 4----
3 files changed, 188 insertions(+), 141 deletions(-)

diff --git a/files/floorplans/floorplan/backend.js b/files/floorplans/floorplan/backend.js @@ -1,5 +1,175 @@ import * as api from "/lib/api.js" +class BackendHistory { + constructor() { + // The current position in history (diffs) + this.place = null, + + // Metadata for diff groups + this.groups = [], + + // Actual changes + this.diffs = [] + } + + get diff() { + return this.diffs[this.place] + } + + get group() { + if (!this.diff) { + if (this.groups.length === 1) { + return this.groups[0] + } + return undefined + } + return this.groups[this.diff.group] + } + + 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) + } + } + + let group = { + type: "group", + length: 0 + } + group.id = this.groups.push(group) - 1 + console.debug("Backend.History.newGroup", group.id) + // NOTE: New diff callback function + return group.id + } + + addDiff(op, path, value, oldvalue, options) { + if (!op || !path) { + throw new Error("Requires op and path") + } + if (op === "add") { + if (!value) { + throw new Error("add: Requires value") + } + } else if (op !== "remove") { + throw new Error("Only add and remove operations supported") + } + + if (!this.diff) { + this.newGroup() + } + + let diff = { + type: "diff", + group: this.group.id, + op: op, + path: path, + value: value, + oldValue: oldvalue, // Should probably do some checks on oldvalue + time: Date.now() + } + + if (!options.clean) { + diff.dirty = true + } + + diff.id = this.diffs.push(diff) - 1 + this.group.length += 1 + this.place = diff.id + console.debug("History.Backend.addDiff", diff.id) + return diff.id + } + + // Get the required operations to go from a, a group or + // diff, to b, another group or diff + between(a, b) { + const getDiff = function(v) { + if (typeof(v) === "object") { + if (!v.id) { + throw new Error("Doesn't have an id") + } + if (v.type === "diff") { + return v.id + } else if (v.type === "group") { + return this.groups[v.id].first + } + throw new Error("Not a valid type") + } + // Diff id + return Number(v) + } + + 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 (a == b) { + 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(a, b + 1) + if (!reverse) { + return diffs + } + for (let i in updates) { + diffs[i] = reverseDiff(diffs[i]) + } + return diffs.reverse() + } + + reverseDiff(diff) { + diff = structuredClone(diff) + + if (diff.op === "add") { + if (diff.oldValue) { + diff.op = "replace" + diff.value = diff.oldValue + } else { + diff.op = "remove" + diff.value = null + } + } else if (diff.op === "remove") { + if (!diff.oldValue) { + throw new Error("There should be an old value") + } + diff.op = "add" + diff.value = diff.oldValue + } else { + throw new Error("Unsupported operation") + } + + return diff + } + + dirty() { + return this.diffs.filter(item => item.dirty) + } +} + export class FloorplanBackend { constructor(floorplan, options) { if (!options) { @@ -18,7 +188,6 @@ export class FloorplanBackend { this.server = options.server } - if (options.callbacks) { this.callbacks = options.callbacks } @@ -39,82 +208,12 @@ export class FloorplanBackend { // There will be here more later, such as furnature } - /* - * I considered making a diff tree, but decided against it - * because I won't know how much value there would be - * in it, especially considering the difficulty in providing - * users access to it. - * Nonetheless it can be added later if I like. - * - * [ - * // Array of diffs, a set for each user action - * { - * [dirty: true] - * diff: <JSON Patch> - * } - * [...] - * ] - */ - this.diffs = [] - - // The cache's state in relation to the diffs - this.diff = null + this.history = new BackendHistory() } get endpoint() { return "floorplans/" + this.floorplan.user + "/" + this.floorplan.name + "/data" } - - // Start writing new differences to a new diff set - newDiff() { - if (this.diffs.length > 0 && this.diffs[this.diff].diff.length === 0) { - console.warn("Current diff empty, not creating new one") - return this.diff - } - - for (let i = this.diffs.length - 1; i > this.diff; --i) { - delete this.diffs[i] - } - this.diff = this.diffs.push({ - diff: [] - }) - 1 - console.debug("newDiff", this.diff) - this.cb("newdiff") - return this.diff - } - - // Add to current diff - addToDiff(op, path, value, options) { - if (!op || !path) { - throw new Error("Requires op and path") - } - if (op === "add") { - if (!value) { - throw new Error("Add requires a value") - } - } else if (op !== "remove") { - throw new Error("Only add and remove operations supported") - } - - if (!this.diff) { - this.newDiff() - } - let diff = { - op: op, - path: path, - value: value, - time: Date.now() - } - if (!options.clean) { - diff.dirty = true - } - if (options.new) { - diff.new = true - } - this.diffs[this.diff].diff.push(diff) - console.debug("Backend.addToDiff", diff) - } - // Apply's diffs in order to get to the state at the beginning of the given diff id // reconstructTo(diff) {} @@ -123,37 +222,6 @@ export class FloorplanBackend { return this.updatesBetween(time) } - // Inclusive updates between time1 and time2, returned in the order required - // to get from time1 to time2 - updatesBetween(time1, time2) { - let updates = [] - let reverse = false - - if (this.diffs.length === 0 || this.diffs[0].length === 0) { - return [] - } - - if (time1 && time2 && time1 > time2) { - reverse = !reverse - let t = time1 - time1 = time2 - time2 = t - } - - for (let i in this.diffs) { - for (let j in this.diffs[i].diff) { - let diff = this.diffs[i].diff[j] - if (!time1 || diff.time >= time1) { - updates.push(diff) - } else if (time2 && diff.time > time2) { - return reverse ? updates.reverse() : updates - } - } - } - - return reverse ? updates.reverse() : updates - } - /* * Add some type of data within the cache. * If key is not given, a random one will be generated. @@ -176,16 +244,10 @@ export class FloorplanBackend { } console.debug("Backend.addData", type, key, value) - if (!this.cache[type][key]) { - options.new = true - } - this.cache[type][key] = value - - // May want to use replace op if it's appropriate. - // Doing this first so it can set new appropriately. if (!options.nodiff) { - this.addToDiff("add", diffPath(type, key), this.cache[type][key], options) + this.history.addDiff("add", diffPath(type, key), value, this.cache[type][key], options) } + this.cache[type][key] = value return key } @@ -200,7 +262,7 @@ export class FloorplanBackend { throw new Error("Expected " + key + " to exist") } if (!options.nodiff) { - this.addToDiff("remove", diffPath(type, key), null, options) + this.addDiff("remove", diffPath(type, key), null, this.cache[type][key], options) } delete this.cache[type][key] } @@ -254,19 +316,6 @@ export class FloorplanBackend { return this.cache[type][id] } - dirty() { - let a = [] - for (let i = 0; i <= this.diff; ++i) { - for (let diff in this.diffs[i].diff) { - if (this.diffs[i].diff[diff].dirty) { - a.push(this.diffs[i].diff[diff]) - } - } - } - console.debug("Backend.dirty", a) - return a - } - cb(name, arg) { if (this.callbacks[name]) { this.callbacks[name](arg) @@ -276,7 +325,7 @@ export class FloorplanBackend { // Push updates to the server push() { // Need a method of making sure we're only sending these once... - let dirty = this.dirty() + let dirty = this.history.dirty() let patch = [] for (let i in dirty) { @@ -284,7 +333,7 @@ export class FloorplanBackend { if (dirty[i].op != "add") { op = dirty[i].op } else { - if (dirty[i].new) { + if (!dirty[i].oldValue) { op = "new" } else { op = "replace" @@ -315,7 +364,7 @@ export class FloorplanBackend { /* * Pull updates from the server. - * (Set AddData diff option to false, and call newDiff() + * (Set AddData diff option to false, and call newGroup() * once at the end.) */ pull() { @@ -330,7 +379,7 @@ export class FloorplanBackend { } applyDiff(diff, options) { - this.newDiff() + this.history.newGroup() for (let i in diff) { let ref = parsePath(diff[i].path) if (diff[i].op === "remove") { @@ -339,7 +388,7 @@ export class FloorplanBackend { this.addData(ref.type, diff[i].value, ref.id, options) } } - this.newDiff() + this.history.newGroup() } } diff --git a/files/floorplans/floorplan/editor.js b/files/floorplans/floorplan/editor.js @@ -205,7 +205,9 @@ export class FloorplanEditor { options.backend.callbacks.updateId = function(ids) { editor.updateId(ids) } this.backend = new backend.FloorplanBackend(floorplan, options.backend) - this.updated = null // last time updated from backend + + // The diff which reflects the state of the displayed objects + this.diff = null this.grids = {} for (let system in this.units.systems) { @@ -354,7 +356,7 @@ export class FloorplanEditor { // Should be called after each user "action" finishAction() { - this.backend.newDiff() + this.backend.history.newGroup() } addPoint(point) { @@ -409,12 +411,12 @@ export class FloorplanEditor { } updateDisplay() { - let diffs = this.backend.updatesSince(this.updated ? this.updated + 1 : null) - if (diffs.length === 0) { - return + let diffs = this.backend.history.between(this.diff ?? 0, this.backend.history.diff.id) + if (diffs.length > 0) { + this.applyDiff(diffs) + this.diff = diffs.at(-1).id + console.debug("Editor.updateDisplay", "Updated display to diff id", this.diff) } - this.updated = diffs.at(-1).time - this.applyDiff(diffs) } applyDiff(diff, reverse) { diff --git a/files/floorplans/floorplan/main.js b/files/floorplans/floorplan/main.js @@ -69,10 +69,6 @@ function init() { }, push: function() { suffix.data = "" - }, - newdiff: function() { - suffix.data = "*" - editor.updateDisplay() } } }